|
@@ -5,7 +5,7 @@
|
|
<el-col :span="18">
|
|
<el-col :span="18">
|
|
<el-row :gutter="12" class="home-card-one mb15">
|
|
<el-row :gutter="12" class="home-card-one mb15">
|
|
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
|
|
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
|
|
- <div class="home-card-one-title">上午好,{{}}。这是今日的人才情况动态。</div>
|
|
|
|
|
|
+ <div class="home-card-one-title">{{greeting}},{{name}}老师。这是今日的人才情况动态。</div>
|
|
</el-col>
|
|
</el-col>
|
|
</el-row>
|
|
</el-row>
|
|
<el-row :gutter="10" class="home-card-two mb15">
|
|
<el-row :gutter="10" class="home-card-two mb15">
|
|
@@ -13,10 +13,16 @@
|
|
<div class="home-card-item" style="margin-bottom: 10px;">
|
|
<div class="home-card-item" style="margin-bottom: 10px;">
|
|
<div class="home-card-item-title">待办事项</div>
|
|
<div class="home-card-item-title">待办事项</div>
|
|
<div class="todo-list">
|
|
<div class="todo-list">
|
|
- <div class="todo-item">
|
|
|
|
|
|
+ <div class="todo-item clickable" @click="handleTodoItemClick('interviewed-pending')">
|
|
|
|
+
|
|
<div class="todo-item-left">
|
|
<div class="todo-item-left">
|
|
- <span class="todo-tag">4位候选人待处理</span>
|
|
|
|
- <span class="todo-text">2025年6月12日 15:00最新投递</span>
|
|
|
|
|
|
+ <div class="item_icon">
|
|
|
|
+ <img src="../../../assets/首页图标1.png" alt="">
|
|
|
|
+ </div>
|
|
|
|
+ <div class="todo-item-text">
|
|
|
|
+ <span class="todo-tag">4位候选人待处理</span>
|
|
|
|
+ <span class="todo-text">2025年6月12日 15:00最新投递</span>
|
|
|
|
+ </div>
|
|
</div>
|
|
</div>
|
|
<div class="todo-item-right">
|
|
<div class="todo-item-right">
|
|
<el-icon size="20"><ArrowRightBold /></el-icon>
|
|
<el-icon size="20"><ArrowRightBold /></el-icon>
|
|
@@ -45,47 +51,108 @@
|
|
<div class="home-card-item">
|
|
<div class="home-card-item">
|
|
<!-- <div class="home-card-item-title">快捷操作</div> -->
|
|
<!-- <div class="home-card-item-title">快捷操作</div> -->
|
|
<div class="home-card-item-content">
|
|
<div class="home-card-item-content">
|
|
- <el-select placeholder="Select" style="margin-right: 5px;">
|
|
|
|
- <el-option label="Option 1" value="1" />
|
|
|
|
- <el-option label="Option 2" value="2" />
|
|
|
|
- <el-option label="Option 3" value="3" />
|
|
|
|
|
|
+ <el-select
|
|
|
|
+ v-model="selectedStatus"
|
|
|
|
+ placeholder="请选择面试状态"
|
|
|
|
+ style="margin-right: 5px; width: 120px;"
|
|
|
|
+ @change="handleFilterChange"
|
|
|
|
+ >
|
|
|
|
+ <el-option label="全部状态" value="" />
|
|
|
|
+ <el-option label="待面试" value="0" />
|
|
|
|
+ <el-option label="已面试" value="2" />
|
|
|
|
+ <el-option label="已录用" value="3" />
|
|
|
|
+ <el-option label="已拒绝" value="4" />
|
|
</el-select>
|
|
</el-select>
|
|
- <el-select placeholder="Select" style="margin-right: 5px;">
|
|
|
|
- <el-option label="Option 1" value="1" />
|
|
|
|
- <el-option label="Option 2" value="2" />
|
|
|
|
- <el-option label="Option 3" value="3" />
|
|
|
|
|
|
+ <el-select
|
|
|
|
+ v-model="selectedPosition"
|
|
|
|
+ placeholder="请选择职位"
|
|
|
|
+ style="margin-right: 5px; width: 120px;"
|
|
|
|
+ @change="handleFilterChange"
|
|
|
|
+ >
|
|
|
|
+ <el-option label="全部职位" value="" />
|
|
|
|
+ <el-option
|
|
|
|
+ v-for="position in positions"
|
|
|
|
+ :key="position.id"
|
|
|
|
+ :label="position.title"
|
|
|
|
+ :value="position.id"
|
|
|
|
+ />
|
|
</el-select>
|
|
</el-select>
|
|
- <el-select placeholder="Select" style="margin-right: 5px;">
|
|
|
|
- <el-option label="Option 1" value="1" />
|
|
|
|
- <el-option label="Option 2" value="2" />
|
|
|
|
- <el-option label="Option 3" value="3" />
|
|
|
|
|
|
+ <el-select
|
|
|
|
+ v-model="selectedTimeRange"
|
|
|
|
+ placeholder="请选择时间范围"
|
|
|
|
+ style="margin-right: 5px; width: 120px;"
|
|
|
|
+ @change="handleFilterChange"
|
|
|
|
+ >
|
|
|
|
+ <el-option label="全部时间" value="" />
|
|
|
|
+ <el-option label="今天" value="today" />
|
|
|
|
+ <el-option label="最近3天" value="3days" />
|
|
|
|
+ <el-option label="最近7天" value="7days" />
|
|
|
|
+ <el-option label="最近30天" value="30days" />
|
|
</el-select>
|
|
</el-select>
|
|
</div>
|
|
</div>
|
|
|
|
+ <!-- <p style="margin-top: 20px; font-size: 12px; color:#808080;">筛选结果</p>
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ <div class="candidate-list" v-loading="candidateLoading">
|
|
|
|
+ <div v-if="filteredCandidates.length === 0" class="no-data">
|
|
|
|
+ <el-empty description="暂无符合条件的候选人" />
|
|
|
|
+ </div>
|
|
|
|
+ <div v-else>
|
|
|
|
+ <div
|
|
|
|
+ v-for="candidate in filteredCandidates.slice(0, 5)"
|
|
|
|
+ :key="candidate.id"
|
|
|
|
+ class="candidate-item"
|
|
|
|
+ @click="handleCandidateClick(candidate)"
|
|
|
|
+ >
|
|
|
|
+ <div class="candidate-info">
|
|
|
|
+ <div class="candidate-name">{{ candidate.applicant_name || candidate.name }}</div>
|
|
|
|
+ <div class="candidate-details">
|
|
|
|
+ <span class="candidate-position">{{ candidate.position_title || '未知职位' }}</span>
|
|
|
|
+ <span class="candidate-status" :class="getStatusClass(candidate.status)">
|
|
|
|
+ {{ getStatusText(candidate.status) }}
|
|
|
|
+ </span>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="candidate-time">
|
|
|
|
+ {{ formatTime(candidate.created_at || candidate.application_time) }}
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ <div v-if="filteredCandidates.length > 5" class="more-candidates">
|
|
|
|
+ <el-button
|
|
|
|
+ type="text"
|
|
|
|
+ @click="viewAllCandidates"
|
|
|
|
+ style="color: #409EFF; font-size: 12px;"
|
|
|
|
+ >
|
|
|
|
+ 查看全部 {{ filteredCandidates.length }} 位候选人 →
|
|
|
|
+ </el-button>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div> -->
|
|
<p style="margin-top: 20px; font-size: 12px; color:#808080;">通过情况概览</p>
|
|
<p style="margin-top: 20px; font-size: 12px; color:#808080;">通过情况概览</p>
|
|
<div class="demo-collapse">
|
|
<div class="demo-collapse">
|
|
- <el-collapse >
|
|
|
|
- <el-collapse-item title="在此期间共计 2 位候选人通过面试" name="1">
|
|
|
|
|
|
+ <el-collapse v-model="activeCollapse">
|
|
|
|
+ <el-collapse-item :title="`在此期间共计${Math.min(filteredCandidates.length, 5)} 位候选人通过面试`" name="1">
|
|
<div class="score-list">
|
|
<div class="score-list">
|
|
- <div class="score-item">
|
|
|
|
- <div class="score-name">张老三</div>
|
|
|
|
- <div class="score-name">男</div>
|
|
|
|
- <div class="score-name">25岁</div>
|
|
|
|
- <div class="score-name">2年经验</div>
|
|
|
|
- <div class="score-value">82%</div>
|
|
|
|
|
|
+ <div class="score-item clickable-item" v-for="item in filteredCandidates.slice(0, 5)" :key="item.id" @click="handleCandidateClick(item)">
|
|
|
|
+ <div class="score-name">{{ item.applicant_name || item.name }}</div>
|
|
|
|
+ <div class="score-name">{{ item.applicant_gender_display || '男' }}</div>
|
|
|
|
+ <!-- <div class="score-name">25岁</div> -->
|
|
|
|
+ <div class="score-name">{{item.position_title}}</div>
|
|
|
|
+ <div class="score-value">{{item.comprehensive_score}}</div>
|
|
</div>
|
|
</div>
|
|
- <div class="score-item">
|
|
|
|
|
|
+ <!-- <div class="score-item">
|
|
<div class="score-name">李老四</div>
|
|
<div class="score-name">李老四</div>
|
|
<div class="score-name">男</div>
|
|
<div class="score-name">男</div>
|
|
<div class="score-name">42岁</div>
|
|
<div class="score-name">42岁</div>
|
|
<div class="score-name">5年经验</div>
|
|
<div class="score-name">5年经验</div>
|
|
<div class="score-value">65%</div>
|
|
<div class="score-value">65%</div>
|
|
- </div>
|
|
|
|
|
|
+ </div> -->
|
|
</div>
|
|
</div>
|
|
</el-collapse-item>
|
|
</el-collapse-item>
|
|
</el-collapse>
|
|
</el-collapse>
|
|
</div>
|
|
</div>
|
|
<p style="margin-top: 20px; font-size: 12px; color:#808080;">招聘情况总览</p>
|
|
<p style="margin-top: 20px; font-size: 12px; color:#808080;">招聘情况总览</p>
|
|
- <p style="font-size: 14px; color:#808080;">共计 10 位候选人参与面试,通过率 <span style="color: #409EFF;">20%</span></p>
|
|
|
|
|
|
+ <p style="font-size: 14px; color:#808080;">共计 {{ totalCandidates }} 位候选人参与面试,通过率 <span style="color: #409EFF;">{{ passRate }}%</span></p>
|
|
<!-- 这里可以添加右侧内容 -->
|
|
<!-- 这里可以添加右侧内容 -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@@ -118,7 +185,7 @@
|
|
</template>
|
|
</template>
|
|
|
|
|
|
<script lang="ts">
|
|
<script lang="ts">
|
|
-import { toRefs, reactive, defineComponent, onMounted, ref, watch, nextTick, onActivated } from 'vue';
|
|
|
|
|
|
+import { toRefs, reactive, defineComponent, onMounted, ref, watch, nextTick, onActivated, computed } from 'vue';
|
|
import * as echarts from 'echarts';
|
|
import * as echarts from 'echarts';
|
|
import { storeToRefs } from 'pinia';
|
|
import { storeToRefs } from 'pinia';
|
|
import { useThemeConfig } from '/@/stores/themeConfig';
|
|
import { useThemeConfig } from '/@/stores/themeConfig';
|
|
@@ -139,6 +206,25 @@ interface ApiData {
|
|
}>;
|
|
}>;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+// 定义职位接口
|
|
|
|
+interface Position {
|
|
|
|
+ id: number | string;
|
|
|
|
+ title: string;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 定义候选人接口
|
|
|
|
+interface Candidate {
|
|
|
|
+ id: number | string;
|
|
|
|
+ applicant_name?: string;
|
|
|
|
+ name?: string;
|
|
|
|
+ position_title?: string;
|
|
|
|
+ status: number | string;
|
|
|
|
+ created_at?: string;
|
|
|
|
+ application_time?: string;
|
|
|
|
+ applicant_gender_display?: string;
|
|
|
|
+ comprehensive_score?: number | string;
|
|
|
|
+}
|
|
|
|
+
|
|
let global: any = {
|
|
let global: any = {
|
|
homeChartOne: null,
|
|
homeChartOne: null,
|
|
homeChartTwo: null,
|
|
homeChartTwo: null,
|
|
@@ -162,7 +248,7 @@ export default defineComponent({
|
|
num1: '0',
|
|
num1: '0',
|
|
num2: '0',
|
|
num2: '0',
|
|
num3: '申请总数',
|
|
num3: '申请总数',
|
|
- num4: 'fa fa-meetup',
|
|
|
|
|
|
+ num4: 'fa fa-file-text-o',
|
|
color1: '#6690F9',
|
|
color1: '#6690F9',
|
|
color2: '--next-color-primary-lighter',
|
|
color2: '--next-color-primary-lighter',
|
|
color3: '--el-color-primary',
|
|
color3: '--el-color-primary',
|
|
@@ -170,8 +256,8 @@ export default defineComponent({
|
|
{
|
|
{
|
|
num1: '0',
|
|
num1: '0',
|
|
num2: '0',
|
|
num2: '0',
|
|
- num3: '职位总数',
|
|
|
|
- num4: 'iconfont icon-ditu',
|
|
|
|
|
|
+ num3: '面试岗位数',
|
|
|
|
+ num4: 'fa fa-briefcase',
|
|
color1: '#6690F9',
|
|
color1: '#6690F9',
|
|
color2: '--next-color-success-lighter',
|
|
color2: '--next-color-success-lighter',
|
|
color3: '--el-color-success',
|
|
color3: '--el-color-success',
|
|
@@ -180,7 +266,7 @@ export default defineComponent({
|
|
num1: '0',
|
|
num1: '0',
|
|
num2: '0',
|
|
num2: '0',
|
|
num3: '问题总数',
|
|
num3: '问题总数',
|
|
- num4: 'iconfont icon-zaosheng',
|
|
|
|
|
|
+ num4: 'fa fa-question-circle',
|
|
color1: '#6690F9',
|
|
color1: '#6690F9',
|
|
color2: '--next-color-warning-lighter',
|
|
color2: '--next-color-warning-lighter',
|
|
color3: '--el-color-warning',
|
|
color3: '--el-color-warning',
|
|
@@ -188,13 +274,15 @@ export default defineComponent({
|
|
{
|
|
{
|
|
num1: '0',
|
|
num1: '0',
|
|
num2: '0',
|
|
num2: '0',
|
|
- num3: '报告总数',
|
|
|
|
- num4: 'fa fa-github-alt',
|
|
|
|
|
|
+ num3: '在职奇才',
|
|
|
|
+ num4: 'fa fa-user',
|
|
color1: '#6690F9',
|
|
color1: '#6690F9',
|
|
color2: '--next-color-danger-lighter',
|
|
color2: '--next-color-danger-lighter',
|
|
color3: '--el-color-danger',
|
|
color3: '--el-color-danger',
|
|
},
|
|
},
|
|
],
|
|
],
|
|
|
|
+ // 控制折叠面板展开状态
|
|
|
|
+ activeCollapse: ['1'], // 初始化展开name为"1"的面板
|
|
homeThree: [
|
|
homeThree: [
|
|
{
|
|
{
|
|
icon: 'iconfont icon-yangan',
|
|
icon: 'iconfont icon-yangan',
|
|
@@ -258,8 +346,31 @@ export default defineComponent({
|
|
color: '#303133',
|
|
color: '#303133',
|
|
},
|
|
},
|
|
apiData: null as ApiData | null,
|
|
apiData: null as ApiData | null,
|
|
|
|
+ name:'',
|
|
|
|
+ greeting: '',
|
|
|
|
+ selectedStatus: '',
|
|
|
|
+ selectedPosition: '',
|
|
|
|
+ selectedTimeRange: '',
|
|
|
|
+ candidateLoading: false,
|
|
|
|
+ filteredCandidates: [] as Candidate[],
|
|
|
|
+ positions: [] as Position[],
|
|
|
|
+ totalCandidates: 0,
|
|
|
|
+ passedCandidates: 0,
|
|
});
|
|
});
|
|
const router = useRouter();
|
|
const router = useRouter();
|
|
|
|
+
|
|
|
|
+ // 获取问候语
|
|
|
|
+ const getGreeting = () => {
|
|
|
|
+ const hour = new Date().getHours();
|
|
|
|
+ if (hour >= 0 && hour < 12) {
|
|
|
|
+ return '上午好';
|
|
|
|
+ } else if (hour >= 12 && hour < 18) {
|
|
|
|
+ return '下午好';
|
|
|
|
+ } else {
|
|
|
|
+ return '晚上好';
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
// 折线图
|
|
// 折线图
|
|
const initLineChart = () => {
|
|
const initLineChart = () => {
|
|
if (!global.dispose.some((b: any) => b === global.homeChartOne)) global.homeChartOne.dispose();
|
|
if (!global.dispose.some((b: any) => b === global.homeChartOne)) global.homeChartOne.dispose();
|
|
@@ -553,8 +664,12 @@ export default defineComponent({
|
|
// 页面加载时
|
|
// 页面加载时
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
fetchApiData();
|
|
fetchApiData();
|
|
|
|
+ fetchPositions();
|
|
|
|
+ fetchCandidates();
|
|
|
|
+ fetchAllCandidatesForStats(); // 获取统计数据
|
|
initEchartsResize();
|
|
initEchartsResize();
|
|
-
|
|
|
|
|
|
+ state.name = Session.get('userInfo').name?.charAt(0) || '';
|
|
|
|
+ state.greeting = getGreeting();
|
|
console.log(Session.get('userInfo'));
|
|
console.log(Session.get('userInfo'));
|
|
});
|
|
});
|
|
// 由于页面缓存原因,keep-alive
|
|
// 由于页面缓存原因,keep-alive
|
|
@@ -638,7 +753,7 @@ export default defineComponent({
|
|
const routes = [
|
|
const routes = [
|
|
'/JobApplication/list/index', // 申请总数
|
|
'/JobApplication/list/index', // 申请总数
|
|
'/position/list', // 职位总数
|
|
'/position/list', // 职位总数
|
|
- '/questionBank/list', // 问题总数
|
|
|
|
|
|
+ '/talent/list/index', // 问题总数
|
|
'/JobApplication/list/index' // 报告总数
|
|
'/JobApplication/list/index' // 报告总数
|
|
];
|
|
];
|
|
|
|
|
|
@@ -646,11 +761,271 @@ export default defineComponent({
|
|
router.push(routes[index]);
|
|
router.push(routes[index]);
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
+
|
|
|
|
+ // 处理待办事项点击跳转功能
|
|
|
|
+ const handleTodoItemClick = (status: string) => {
|
|
|
|
+ // 跳转到求职申请列表页面,并传递状态参数
|
|
|
|
+ router.push({
|
|
|
|
+ path: '/JobApplication/list/index',
|
|
|
|
+ query: {
|
|
|
|
+ status: 3 // 传递状态参数,用于页面筛选已面试待处理的候选人
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // 获取职位列表
|
|
|
|
+ const fetchPositions = async () => {
|
|
|
|
+ try {
|
|
|
|
+ const res = await fetch(`${import.meta.env.VITE_API_URL}/api/system/job/list?page=1&limit=100&tenant_id=1`, {
|
|
|
|
+ method: 'GET',
|
|
|
|
+ headers: {
|
|
|
|
+ 'Authorization': 'JWT ' + Session.get('token'),
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ const data = await res.json();
|
|
|
|
+
|
|
|
|
+ if (data.code === 2000 && data.data) {
|
|
|
|
+ state.positions = data.data.map((item: any) => ({
|
|
|
|
+ id: item.id,
|
|
|
|
+ title: item.title || item.name || item.job_name,
|
|
|
|
+ }));
|
|
|
|
+ } else {
|
|
|
|
+ console.error('获取职位列表失败:', data.msg || '未知错误');
|
|
|
|
+ }
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('获取职位列表异常:', error);
|
|
|
|
+ // 设置默认职位
|
|
|
|
+ state.positions = [{ id: 1, title: '流水线操作工' }];
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // 获取候选人列表
|
|
|
|
+ const fetchCandidates = async (params: any = {}) => {
|
|
|
|
+ state.candidateLoading = true;
|
|
|
|
+ try {
|
|
|
|
+ const queryParams = new URLSearchParams({
|
|
|
|
+ page: '1',
|
|
|
|
+ limit: '50',
|
|
|
|
+ tenant_id: '1',
|
|
|
|
+ ...params
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ const res = await fetch(`${import.meta.env.VITE_API_URL}/api/system/job_applications/?${queryParams}`, {
|
|
|
|
+ method: 'GET',
|
|
|
|
+ headers: {
|
|
|
|
+ 'Authorization': 'JWT ' + Session.get('token'),
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ const data = await res.json();
|
|
|
|
+
|
|
|
|
+ if (data.code === 2000 && data.data) {
|
|
|
|
+ state.filteredCandidates = data.data;
|
|
|
|
+ // 更新统计信息
|
|
|
|
+ updateCandidateStats(data.data);
|
|
|
|
+ } else {
|
|
|
|
+ state.filteredCandidates = [];
|
|
|
|
+ console.error('获取候选人列表失败:', data.msg || '未知错误');
|
|
|
|
+ }
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('获取候选人列表异常:', error);
|
|
|
|
+ state.filteredCandidates = [];
|
|
|
|
+ } finally {
|
|
|
|
+ state.candidateLoading = false;
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // 处理筛选变化
|
|
|
|
+ const handleFilterChange = () => {
|
|
|
|
+ const params: any = {};
|
|
|
|
+
|
|
|
|
+ // 状态筛选
|
|
|
|
+ if (state.selectedStatus !== '') {
|
|
|
|
+ params.status = state.selectedStatus;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 职位筛选
|
|
|
|
+ if (state.selectedPosition !== '') {
|
|
|
|
+ params.position_id = state.selectedPosition;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 时间筛选
|
|
|
|
+ if (state.selectedTimeRange !== '') {
|
|
|
|
+ const timeFilter = getTimeFilter(state.selectedTimeRange);
|
|
|
|
+ if (timeFilter) {
|
|
|
|
+ params.date_from = timeFilter.from;
|
|
|
|
+ params.date_to = timeFilter.to;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ fetchCandidates(params);
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // 获取时间筛选范围
|
|
|
|
+ const getTimeFilter = (range: string) => {
|
|
|
|
+ const now = new Date();
|
|
|
|
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
|
|
+
|
|
|
|
+ switch (range) {
|
|
|
|
+ case 'today':
|
|
|
|
+ return {
|
|
|
|
+ from: today.toISOString().split('T')[0],
|
|
|
|
+ to: today.toISOString().split('T')[0]
|
|
|
|
+ };
|
|
|
|
+ case '3days':
|
|
|
|
+ const threeDaysAgo = new Date(today);
|
|
|
|
+ threeDaysAgo.setDate(today.getDate() - 3);
|
|
|
|
+ return {
|
|
|
|
+ from: threeDaysAgo.toISOString().split('T')[0],
|
|
|
|
+ to: today.toISOString().split('T')[0]
|
|
|
|
+ };
|
|
|
|
+ case '7days':
|
|
|
|
+ const sevenDaysAgo = new Date(today);
|
|
|
|
+ sevenDaysAgo.setDate(today.getDate() - 7);
|
|
|
|
+ return {
|
|
|
|
+ from: sevenDaysAgo.toISOString().split('T')[0],
|
|
|
|
+ to: today.toISOString().split('T')[0]
|
|
|
|
+ };
|
|
|
|
+ case '30days':
|
|
|
|
+ const thirtyDaysAgo = new Date(today);
|
|
|
|
+ thirtyDaysAgo.setDate(today.getDate() - 30);
|
|
|
|
+ return {
|
|
|
|
+ from: thirtyDaysAgo.toISOString().split('T')[0],
|
|
|
|
+ to: today.toISOString().split('T')[0]
|
|
|
|
+ };
|
|
|
|
+ default:
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // 获取状态文本
|
|
|
|
+ const getStatusText = (status: number | string) => {
|
|
|
|
+ const statusMap: Record<string, string> = {
|
|
|
|
+ '0': '待面试',
|
|
|
|
+ '1': '待面试',
|
|
|
|
+ '2': '已面试',
|
|
|
|
+ '3': '已录用',
|
|
|
|
+ '4': '已拒绝'
|
|
|
|
+ };
|
|
|
|
+ return statusMap[String(status)] || '未知状态';
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // 获取状态样式类
|
|
|
|
+ const getStatusClass = (status: number | string) => {
|
|
|
|
+ const classMap: Record<string, string> = {
|
|
|
|
+ '0': 'status-pending',
|
|
|
|
+ '1': 'status-pending',
|
|
|
|
+ '2': 'status-interviewed',
|
|
|
|
+ '3': 'status-hired',
|
|
|
|
+ '4': 'status-rejected'
|
|
|
|
+ };
|
|
|
|
+ return classMap[String(status)] || 'status-unknown';
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // 格式化时间
|
|
|
|
+ const formatTime = (timeStr: string) => {
|
|
|
|
+ if (!timeStr) return '';
|
|
|
|
+ const date = new Date(timeStr);
|
|
|
|
+ const now = new Date();
|
|
|
|
+ const diff = now.getTime() - date.getTime();
|
|
|
|
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
|
|
+
|
|
|
|
+ if (days === 0) {
|
|
|
|
+ return '今天';
|
|
|
|
+ } else if (days === 1) {
|
|
|
|
+ return '昨天';
|
|
|
|
+ } else if (days < 7) {
|
|
|
|
+ return `${days}天前`;
|
|
|
|
+ } else {
|
|
|
|
+ return date.toLocaleDateString('zh-CN', {
|
|
|
|
+ month: 'short',
|
|
|
|
+ day: 'numeric'
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // 处理候选人点击
|
|
|
|
+ const handleCandidateClick = (candidate: any) => {
|
|
|
|
+ // 跳转到候选人详情页面
|
|
|
|
+ const baseUrl = window.location.origin;
|
|
|
|
+ const url = `${baseUrl}/#/JobApplication/report/report?id=${candidate.id}&tenant_id=${1}&application_id=${candidate.applicant}`;
|
|
|
|
+ window.open(url, '_blank');
|
|
|
|
+
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // 查看全部候选人
|
|
|
|
+ const viewAllCandidates = () => {
|
|
|
|
+ const query: any = {};
|
|
|
|
+
|
|
|
|
+ // 传递当前筛选条件
|
|
|
|
+ if (state.selectedStatus !== '') {
|
|
|
|
+ query.status = state.selectedStatus;
|
|
|
|
+ }
|
|
|
|
+ if (state.selectedPosition !== '') {
|
|
|
|
+ query.position_id = state.selectedPosition;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ router.push({
|
|
|
|
+ path: '/JobApplication/list/index',
|
|
|
|
+ query
|
|
|
|
+ });
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // 更新候选人统计信息
|
|
|
|
+ const updateCandidateStats = (candidates: Candidate[]) => {
|
|
|
|
+ state.totalCandidates = candidates.length;
|
|
|
|
+ // 计算通过人数(状态为3表示已录用)
|
|
|
|
+ state.passedCandidates = candidates.filter(candidate =>
|
|
|
|
+ candidate.status === 3 || candidate.status === '3'
|
|
|
|
+ ).length;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // 计算通过率
|
|
|
|
+ const passRate = computed(() => {
|
|
|
|
+ if (state.totalCandidates === 0) return '0';
|
|
|
|
+ return ((state.passedCandidates / state.totalCandidates) * 100).toFixed(1);
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ // 页面加载时获取所有候选人数据用于统计
|
|
|
|
+ const fetchAllCandidatesForStats = async () => {
|
|
|
|
+ try {
|
|
|
|
+ const queryParams = new URLSearchParams({
|
|
|
|
+ page: '1',
|
|
|
|
+ limit: '1000', // 获取更多数据用于统计
|
|
|
|
+ tenant_id: '1'
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ const res = await fetch(`${import.meta.env.VITE_API_URL}/api/system/job_applications/?${queryParams}`, {
|
|
|
|
+ method: 'GET',
|
|
|
|
+ headers: {
|
|
|
|
+ 'Authorization': 'JWT ' + Session.get('token'),
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ const data = await res.json();
|
|
|
|
+
|
|
|
|
+ if (data.code === 2000 && data.data) {
|
|
|
|
+ updateCandidateStats(data.data);
|
|
|
|
+ }
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('获取候选人统计数据异常:', error);
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
return {
|
|
return {
|
|
homeLineRef,
|
|
homeLineRef,
|
|
homePieRef,
|
|
homePieRef,
|
|
homeBarRef,
|
|
homeBarRef,
|
|
handleCardClick,
|
|
handleCardClick,
|
|
|
|
+ handleTodoItemClick,
|
|
|
|
+ handleFilterChange,
|
|
|
|
+ handleCandidateClick,
|
|
|
|
+ viewAllCandidates,
|
|
|
|
+ getStatusText,
|
|
|
|
+ getStatusClass,
|
|
|
|
+ formatTime,
|
|
|
|
+ passRate,
|
|
...toRefs(state),
|
|
...toRefs(state),
|
|
};
|
|
};
|
|
},
|
|
},
|
|
@@ -753,11 +1128,11 @@ $homeNavLengh: 8;
|
|
}
|
|
}
|
|
.home-right {
|
|
.home-right {
|
|
.home-card-item {
|
|
.home-card-item {
|
|
- height: 500px;
|
|
|
|
|
|
+ height: 600px;
|
|
border-radius: 16px;
|
|
border-radius: 16px;
|
|
transition: all ease 0.3s;
|
|
transition: all ease 0.3s;
|
|
padding: 20px;
|
|
padding: 20px;
|
|
- overflow: hidden;
|
|
|
|
|
|
+ overflow: auto;
|
|
background: var(--el-color-white);
|
|
background: var(--el-color-white);
|
|
color: var(--el-text-color-primary);
|
|
color: var(--el-text-color-primary);
|
|
border: 1px solid var(--next-border-color-light);
|
|
border: 1px solid var(--next-border-color-light);
|
|
@@ -807,22 +1182,75 @@ $homeNavLengh: 8;
|
|
.todo-list {
|
|
.todo-list {
|
|
padding: 10px 20px;
|
|
padding: 10px 20px;
|
|
margin-top: 15px;
|
|
margin-top: 15px;
|
|
- margin-left: 50px;
|
|
|
|
|
|
+ margin-left: 10px;
|
|
.todo-item {
|
|
.todo-item {
|
|
display: flex;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
align-items: center;
|
|
padding: 8px 0;
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid var(--next-border-color-light);
|
|
border-bottom: 1px solid var(--next-border-color-light);
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
+ background: #fff;
|
|
|
|
+
|
|
|
|
+ // 可点击样式
|
|
|
|
+ &.clickable {
|
|
|
|
+ cursor: pointer;
|
|
|
|
+ padding: 12px 16px;
|
|
|
|
+ border-radius: 8px;
|
|
|
|
+ margin: 8px 0;
|
|
|
|
+ // background: var(--el-fill-color-lighter);
|
|
|
|
+ border-bottom: none;
|
|
|
|
+
|
|
|
|
+ &:hover {
|
|
|
|
+ background: #f2f2f2;
|
|
|
|
+ transform: translateY(-2px);
|
|
|
|
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ &:active {
|
|
|
|
+ transform: translateY(0);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
.todo-item-left {
|
|
.todo-item-left {
|
|
display: flex;
|
|
display: flex;
|
|
|
|
+ align-items: flex-start;
|
|
|
|
+ .item_icon{
|
|
|
|
+ width: 40px;
|
|
|
|
+ height: 40px;
|
|
|
|
+ img{
|
|
|
|
+ width: 100%;
|
|
|
|
+ height: 100%;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }.todo-item-text{
|
|
|
|
+ display: flex;
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
align-items: flex-start;
|
|
|
|
+ }
|
|
.todo-tag {
|
|
.todo-tag {
|
|
margin-right: 10px;
|
|
margin-right: 10px;
|
|
|
|
+ font-weight: 500;
|
|
|
|
+ // color: var(--el-color-primary);
|
|
|
|
+ }
|
|
|
|
+ .todo-text {
|
|
|
|
+ font-size: 12px;
|
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
|
+ margin-top: 4px;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .todo-item-right {
|
|
|
|
+ .el-icon {
|
|
|
|
+ // color: var(--el-color-primary);
|
|
|
|
+ transition: transform 0.3s ease;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ &.clickable:hover .todo-item-right .el-icon {
|
|
|
|
+ transform: translateX(4px);
|
|
|
|
+ }
|
|
|
|
+
|
|
&:last-child {
|
|
&:last-child {
|
|
border-bottom: none;
|
|
border-bottom: none;
|
|
}
|
|
}
|
|
@@ -848,16 +1276,151 @@ $homeNavLengh: 8;
|
|
background: var(--el-fill-color-light);
|
|
background: var(--el-fill-color-light);
|
|
border-radius: 4px;
|
|
border-radius: 4px;
|
|
|
|
|
|
|
|
+ // 可点击样式
|
|
|
|
+ &.clickable-item {
|
|
|
|
+ cursor: pointer;
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
+
|
|
|
|
+ &:hover {
|
|
|
|
+ background: var(--el-color-primary-light-9);
|
|
|
|
+ transform: translateY(-1px);
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ &:active {
|
|
|
|
+ transform: translateY(0);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
.score-name {
|
|
.score-name {
|
|
font-size: 14px;
|
|
font-size: 14px;
|
|
color: var(--el-text-color-primary);
|
|
color: var(--el-text-color-primary);
|
|
|
|
+ flex: 0 0 auto; // 不伸缩,固定宽度
|
|
|
|
+
|
|
|
|
+ &:nth-child(1) {
|
|
|
|
+ width: 60px; // 姓名宽度
|
|
|
|
+ text-align: left;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ &:nth-child(2) {
|
|
|
|
+ width: 30px; // 性别宽度
|
|
|
|
+ text-align: center;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ &:nth-child(3) {
|
|
|
|
+ width: 80px; // 职位宽度
|
|
|
|
+ text-align: left;
|
|
|
|
+ overflow: hidden;
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
+ white-space: nowrap;
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
.score-value {
|
|
.score-value {
|
|
font-size: 14px;
|
|
font-size: 14px;
|
|
color: var(--el-color-primary);
|
|
color: var(--el-color-primary);
|
|
font-weight: 500;
|
|
font-weight: 500;
|
|
|
|
+ width: 50px; // 分数宽度
|
|
|
|
+ text-align: right;
|
|
|
|
+ flex: 0 0 auto;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 候选人列表样式
|
|
|
|
+ .candidate-list {
|
|
|
|
+ max-height: 300px;
|
|
|
|
+ overflow-y: auto;
|
|
|
|
+
|
|
|
|
+ .no-data {
|
|
|
|
+ text-align: center;
|
|
|
|
+ padding: 20px 0;
|
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .candidate-item {
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: space-between;
|
|
|
|
+ align-items: center;
|
|
|
|
+ padding: 10px 5px;
|
|
|
|
+ border-bottom: 1px solid var(--el-border-color-light);
|
|
|
|
+ cursor: pointer;
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
+
|
|
|
|
+ &:hover {
|
|
|
|
+ background: var(--el-fill-color-light);
|
|
|
|
+ transform: translateX(2px);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ &:last-child {
|
|
|
|
+ border-bottom: none;
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ .candidate-info {
|
|
|
|
+ flex: 1;
|
|
|
|
+
|
|
|
|
+ .candidate-name {
|
|
|
|
+ font-size: 14px;
|
|
|
|
+ font-weight: 500;
|
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .candidate-details {
|
|
|
|
+ display: flex;
|
|
|
|
+ gap: 8px;
|
|
|
|
+ align-items: center;
|
|
|
|
+
|
|
|
|
+ .candidate-position {
|
|
|
|
+ font-size: 12px;
|
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .candidate-status {
|
|
|
|
+ font-size: 12px;
|
|
|
|
+ padding: 2px 6px;
|
|
|
|
+ border-radius: 12px;
|
|
|
|
+
|
|
|
|
+ &.status-pending {
|
|
|
|
+ background: #e6f4ff;
|
|
|
|
+ color: #1890ff;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ &.status-interviewed {
|
|
|
|
+ background: #fff7e6;
|
|
|
|
+ color: #fa8c16;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ &.status-hired {
|
|
|
|
+ background: #f6ffed;
|
|
|
|
+ color: #52c41a;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ &.status-rejected {
|
|
|
|
+ background: #fff2f0;
|
|
|
|
+ color: #ff4d4f;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ &.status-unknown {
|
|
|
|
+ background: #f5f5f5;
|
|
|
|
+ color: #999;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .candidate-time {
|
|
|
|
+ font-size: 12px;
|
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
|
+ white-space: nowrap;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .more-candidates {
|
|
|
|
+ text-align: center;
|
|
|
|
+ padding: 10px 0;
|
|
|
|
+ border-top: 1px solid var(--el-border-color-lighter);
|
|
|
|
+ margin-top: 5px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|