|
@@ -1,25 +1,291 @@
|
|
|
<template>
|
|
|
<fs-page>
|
|
|
- <fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
|
|
|
+ <div class="job-application-container">
|
|
|
+ <div class="sidebar">
|
|
|
+ <div class="tree-container">
|
|
|
+ <!-- 全部 -->
|
|
|
+ <div class="tree-item" :class="{ active: activeNode === 'all' }" @click="handleNodeClick({ type: 'all' })">
|
|
|
+ <div class="item-content">
|
|
|
+ <el-icon><Grid /></el-icon>
|
|
|
+ <span>全部</span>
|
|
|
+ </div>
|
|
|
+ <div class="item-count">{{ totalCount }}人</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 待安排 -->
|
|
|
+ <div class="tree-item" :class="{ active: activeNode === 'status-1' }" @click="handleNodeClick({ type: 'status', value: 1, id: 'status-1' })">
|
|
|
+ <div class="item-content">
|
|
|
+ <el-icon><Clock /></el-icon>
|
|
|
+ <span>待面试</span>
|
|
|
+ </div>
|
|
|
+ <div class="item-count" v-if="statusCounts[1]">{{ statusCounts[1] }}</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 推进中 -->
|
|
|
+ <div class="tree-item" :class="{ active: activeNode === 'status-2' }" @click="handleNodeClick({ type: 'status', value: 2, id: 'status-2' })">
|
|
|
+ <div class="item-content">
|
|
|
+ <el-icon><ArrowRight /></el-icon>
|
|
|
+ <span>已面试</span>
|
|
|
+ </div>
|
|
|
+ <div class="item-count" v-if="statusCounts[2]">{{ statusCounts[2] }}</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 已入职 -->
|
|
|
+ <div class="tree-item" :class="{ active: activeNode === 'status-3' }" @click="handleNodeClick({ type: 'status', value: 3, id: 'status-3' })">
|
|
|
+ <div class="item-content">
|
|
|
+ <el-icon><Check /></el-icon>
|
|
|
+ <span>已录用</span>
|
|
|
+ </div>
|
|
|
+ <div class="item-count" v-if="statusCounts[3]">{{ statusCounts[3] }}</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 待召回 -->
|
|
|
+ <div class="tree-item" :class="{ active: activeNode === 'status-4' }" @click="handleNodeClick({ type: 'status', value: 4, id: 'status-4' })">
|
|
|
+ <div class="item-content">
|
|
|
+ <el-icon><RefreshRight /></el-icon>
|
|
|
+ <span>已拒绝</span>
|
|
|
+ </div>
|
|
|
+ <div class="item-count" v-if="statusCounts[4]">{{ statusCounts[4] }}</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 职位分类 -->
|
|
|
+ <div class="category-title" @click="togglePositionList">
|
|
|
+ <el-icon><Briefcase /></el-icon>
|
|
|
+ <span>职位</span>
|
|
|
+ <el-icon class="expand-icon" :class="{ 'is-expanded': showPositionList }">
|
|
|
+ <ArrowRight />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 职位列表 -->
|
|
|
+ <div v-if="showPositionList" class="position-list">
|
|
|
+ <!-- 全部职位 -->
|
|
|
+ <div class="tree-item" :class="{ active: activeNode === 'position-all' }" @click="handleNodeClick({ type: 'all', id: 'position-all' })">
|
|
|
+ <div class="item-content">
|
|
|
+ <span>全部</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 职位列表项 -->
|
|
|
+ <div
|
|
|
+ v-for="position in positions"
|
|
|
+ :key="'position-' + position.id"
|
|
|
+ class="tree-item"
|
|
|
+ :class="{ active: activeNode === 'position-' + position.id }"
|
|
|
+ @click="handleNodeClick({ type: 'position', value: position.title, id: 'position-' + position.id })"
|
|
|
+ >
|
|
|
+ <div class="item-content">
|
|
|
+ <el-icon><ArrowRight /></el-icon>
|
|
|
+ <span>{{ position.title }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="item-count" v-if="position.count">{{ position.count }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="content">
|
|
|
+ <fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</fs-page>
|
|
|
</template>
|
|
|
|
|
|
<script lang="ts" setup name="areas">
|
|
|
-import { onMounted } from 'vue';
|
|
|
+import { onMounted, ref, reactive } from 'vue';
|
|
|
import { useFs } from '@fast-crud/fast-crud';
|
|
|
import { createCrudOptions } from './crud';
|
|
|
import { GetPermission } from './api';
|
|
|
import { handleColumnPermission } from '/@/utils/columnPermission';
|
|
|
+import { Grid, Clock, ArrowRight, Check, RefreshRight, Briefcase } from '@element-plus/icons-vue';
|
|
|
|
|
|
const { crudBinding, crudRef, crudExpose, crudOptions, resetCrudOptions } = useFs({ createCrudOptions });
|
|
|
|
|
|
+// 状态计数
|
|
|
+const totalCount = ref(0);
|
|
|
+const statusCounts = reactive<Record<number, number>>({
|
|
|
+ 1: 0, // 待面试
|
|
|
+ 2: 0, // 已面试
|
|
|
+ 3: 0, // 已录用
|
|
|
+ 4: 0 // 已拒绝
|
|
|
+});
|
|
|
+
|
|
|
+// 职位列表
|
|
|
+const showPositionList = ref(true);
|
|
|
+const positions = ref<Array<{id: number|string, title: string, count?: number}>>([
|
|
|
+ { id: 1, title: '流水线操作工', count: 0 },
|
|
|
+ { id: 2, title: '咖啡师', count: 0 },
|
|
|
+ { id: 3, title: '餐厅服务员', count: 0 }
|
|
|
+]);
|
|
|
+
|
|
|
+// 切换职位列表显示
|
|
|
+const togglePositionList = () => {
|
|
|
+ showPositionList.value = !showPositionList.value;
|
|
|
+};
|
|
|
+
|
|
|
+// 当前激活的节点
|
|
|
+const activeNode = ref('all');
|
|
|
+
|
|
|
+// 处理树节点点击
|
|
|
+const handleNodeClick = (data: any) => {
|
|
|
+ activeNode.value = data.id || 'all';
|
|
|
+
|
|
|
+ if (data.type === 'all') {
|
|
|
+ // 清除所有筛选条件
|
|
|
+ crudExpose.getSearchRef().resetFields();
|
|
|
+ crudExpose.doRefresh();
|
|
|
+ } else if (data.type === 'status') {
|
|
|
+ // 按状态筛选
|
|
|
+ crudExpose.doSearch({
|
|
|
+ form: {
|
|
|
+ status: data.value
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else if (data.type === 'position') {
|
|
|
+ // 按职位筛选
|
|
|
+ crudExpose.doSearch({
|
|
|
+ form: {
|
|
|
+ position_title: data.value
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 更新计数
|
|
|
+const updateCounts = (data: any) => {
|
|
|
+ if (!data || !data.records) return;
|
|
|
+
|
|
|
+ totalCount.value = data.total || 0;
|
|
|
+
|
|
|
+ // 重置计数
|
|
|
+ Object.keys(statusCounts).forEach(key => {
|
|
|
+ statusCounts[Number(key)] = 0;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 统计各状态数量
|
|
|
+ data.records.forEach((record: any) => {
|
|
|
+ const status = record.status;
|
|
|
+ if (status && typeof status === 'number' && status in statusCounts) {
|
|
|
+ statusCounts[status]++;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 统计各职位数量
|
|
|
+ // 重置职位计数
|
|
|
+ positions.value.forEach(position => {
|
|
|
+ position.count = 0;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 统计职位数量
|
|
|
+ data.records.forEach((record: any) => {
|
|
|
+ if (record.position_title) {
|
|
|
+ const position = positions.value.find(p => p.title === record.position_title);
|
|
|
+ if (position) {
|
|
|
+ position.count = (position.count || 0) + 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
// 页面打开后获取列表数据
|
|
|
onMounted(async () => {
|
|
|
// 设置列权限
|
|
|
const newOptions = await handleColumnPermission(GetPermission, crudOptions);
|
|
|
- //重置crudBinding
|
|
|
+
|
|
|
+ // 添加数据加载后的回调,用于更新计数
|
|
|
+ if (newOptions && newOptions.crudOptions && newOptions.crudOptions.request) {
|
|
|
+ const originalPageRequest = newOptions.crudOptions.request.pageRequest;
|
|
|
+ newOptions.crudOptions.request.pageRequest = async (query: any) => {
|
|
|
+ const res = await originalPageRequest(query);
|
|
|
+ updateCounts(res.data);
|
|
|
+ return res;
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重置crudBinding
|
|
|
resetCrudOptions(newOptions);
|
|
|
+
|
|
|
// 刷新
|
|
|
crudExpose.doRefresh();
|
|
|
});
|
|
|
</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.job-application-container {
|
|
|
+ display: flex;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.sidebar {
|
|
|
+ margin-top: 10px;
|
|
|
+ width: 200px;
|
|
|
+ margin-right: 16px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ background-color: #fff;
|
|
|
+ border-right: 1px solid #ebeef5;
|
|
|
+ border-radius: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-container {
|
|
|
+ padding: 8px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 10px 16px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background-color 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-item:hover {
|
|
|
+ background-color: #f5f7fa;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-item.active {
|
|
|
+ background-color: #f0f7ff;
|
|
|
+ color: #409eff;
|
|
|
+ border-right: 2px solid #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.item-content {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.item-count {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+.category-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 12px 16px;
|
|
|
+ margin-top: 8px;
|
|
|
+ font-weight: 500;
|
|
|
+ border-top: 1px solid #ebeef5;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+ gap: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.expand-icon {
|
|
|
+ margin-left: auto;
|
|
|
+ transition: transform 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.expand-icon.is-expanded {
|
|
|
+ transform: rotate(90deg);
|
|
|
+}
|
|
|
+
|
|
|
+.position-list {
|
|
|
+ padding-left: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.content {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+</style>
|