feat(topone): 实现任务下发功能并优化界面布局

- 添加任务下发API接口并在任务列表组件中引入
- 修改任务创建逻辑,对接后端API
- 获取下属人员列表用于任务分配
- 优化表格布局,移除总业绩列
- 删除不必要的指导建议模块
This commit is contained in:
2025-08-21 11:43:59 +08:00
parent 544a66b8fa
commit 8780a94f82
9 changed files with 112 additions and 278 deletions

View File

@@ -65,4 +65,9 @@ export const getDetailedDataTable = (params) => {
return https.post('/api/v1/level_five/overview/detailed_data_table', params)
}
// 下发任务 /api/v1/level_five/overview/assign_tasks
export const assignTasks = (params) => {
return https.post('/api/v1/level_five/overview/assign_tasks', params)
}

View File

@@ -26,10 +26,6 @@
<div class="detail-label">成交单数</div>
<div class="detail-value">{{ selectedMember.deals || 0 }} </div>
</div>
<div class="detail-card">
<div class="detail-label">总业绩</div>
<div class="detail-value">¥{{ formatAmount(selectedMember.week_amount || selectedMember.performance) }}</div>
</div>
<div class="detail-card">
<div class="detail-label">转化率</div>
<div class="detail-value">{{ selectedMember.conversion_rate || selectedMember.conversion || '0%' }}</div>
@@ -38,7 +34,7 @@
</div>
<!-- 指导建议 -->
<div class="guidance-section">
<!-- <div class="guidance-section">
<div class="guidance-header" @click="toggleGuidanceCollapse">
<h3>💡 指导建议</h3>
<div class="collapse-toggle" :class="{ 'collapsed': isGuidanceCollapsed }">
@@ -69,7 +65,7 @@
<p>{{ selectedMember.user_name || selectedMember.name }} 的各项指标都很不错继续保持这种状态</p>
</div>
</div>
</div>
</div> -->
<!-- 录音列表 -->
<div class="recordings-section">

View File

@@ -6,7 +6,6 @@
<div class="table-header">
<span>排名</span>
<span>姓名</span>
<span>总业绩</span>
<span>转化率</span>
<span>加微率</span>
<span>入群率</span>
@@ -21,7 +20,6 @@
>
<span class="rank">{{ index + 1 }}</span>
<span class="name">{{ member.user_name || member.name }}</span>
<span class="performance">¥{{ formatAmount(member.week_amount || member.performance) }}</span>
<span class="conversion">{{ member.conversion_rate || member.conversion || '0%' }}</span>
<span class="wechat-rate">{{ member.plus_v_rate || member.wechatRate || '0%' }}</span>
<span class="group-rate">{{ member.group_rate || member.groupRate || '0%' }}</span>
@@ -164,7 +162,7 @@ const handleDoubleClick = (member) => {
.table-header {
display: grid;
grid-template-columns: 60px 1fr 120px 80px 90px 90px;
grid-template-columns: 60px 1fr 80px 90px 90px;
gap: 0.8rem;
padding: 0.75rem 0;
border-bottom: 1px solid #e2e8f0;
@@ -176,7 +174,7 @@ const handleDoubleClick = (member) => {
.table-row {
display: grid;
grid-template-columns: 60px 1fr 120px 80px 90px 90px;
grid-template-columns: 60px 1fr 80px 90px 90px;
gap: 0.8rem;
padding: 0.75rem 0;
border-bottom: 1px solid #f1f5f9;
@@ -243,7 +241,7 @@ const handleDoubleClick = (member) => {
.ranking-table {
.table-header {
grid-template-columns: 40px 1fr 70px 55px 55px 55px;
grid-template-columns: 40px 1fr 70px 55px 55px;
gap: 0.3rem;
font-size: 0.75rem;
padding: 0.5rem 0;
@@ -251,7 +249,7 @@ const handleDoubleClick = (member) => {
}
.table-row {
grid-template-columns: 40px 1fr 70px 55px 55px 55px;
grid-template-columns: 40px 1fr 70px 55px 55px;
gap: 0.3rem;
font-size: 0.8rem;
padding: 0.5rem 0;
@@ -292,14 +290,14 @@ const handleDoubleClick = (member) => {
.ranking-table {
.table-header {
grid-template-columns: 30px 1fr 55px 45px 45px 45px;
grid-template-columns: 30px 1fr 55px 45px 45px;
gap: 0.2rem;
font-size: 0.7rem;
white-space: nowrap;
}
.table-row {
grid-template-columns: 30px 1fr 55px 45px 45px 45px;
grid-template-columns: 30px 1fr 55px 45px 45px;
gap: 0.2rem;
font-size: 0.75rem;
white-space: nowrap;

View File

@@ -1288,7 +1288,7 @@ onMounted(async () => {
// PC端保持一致布局
@media (min-width: 1024px) {
grid-template-columns: 50px 1fr 100px 80px 90px 90px;
grid-template-columns: 50px 1fr 80px 90px 90px;
gap: 1rem;
font-size: 0.875rem;
padding: 1rem 0;
@@ -1296,7 +1296,7 @@ onMounted(async () => {
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
grid-template-columns: 45px 1fr 90px 70px 90px 90px;
grid-template-columns: 45px 1fr 70px 90px 90px;
gap: 0.75rem;
font-size: 0.8125rem;
@@ -1305,7 +1305,7 @@ onMounted(async () => {
// 移动端适配
@media (max-width: 768px) {
grid-template-columns: 40px 1fr 80px 60px 90px 90px;
grid-template-columns: 40px 1fr 60px 90px 90px;
gap: 0.5rem;
font-size: 0.75rem;
@@ -1343,7 +1343,7 @@ onMounted(async () => {
// PC端保持一致布局
@media (min-width: 1024px) {
grid-template-columns: 50px 1fr 100px 80px 90px 90px;
grid-template-columns: 50px 1fr 80px 90px 90px;
gap: 1rem;
font-size: 0.875rem;

View File

@@ -5,154 +5,10 @@
<div class="header-controls">
<select v-model="filterPriority" class="priority-filter">
<option value="all">全部优先级</option>
<option value="urgent">紧急</option>
<option value="high"></option>
<option value="medium"></option>
<option value="low"></option>
<option value="urgent">待处理</option>
<option value="high">正在处理</option>
<option value="medium">已完成</option>
</select>
<button class="add-btn" @click="showAddForm = true">+ 新增</button>
</div>
</div>
<!-- 统计概览 -->
<div class="actions-summary">
<div class="summary-item urgent">
<div class="summary-count">{{ getCountByPriority('urgent') }}</div>
<div class="summary-label">紧急事项</div>
</div>
<div class="summary-item high">
<div class="summary-count">{{ getCountByPriority('high') }}</div>
<div class="summary-label">高优先级</div>
</div>
<div class="summary-item medium">
<div class="summary-count">{{ getCountByPriority('medium') }}</div>
<div class="summary-label">中优先级</div>
</div>
<div class="summary-item completed">
<div class="summary-count">{{ completedCount }}</div>
<div class="summary-label">已完成</div>
</div>
</div>
<!-- 事项列表 -->
<div class="actions-list">
<div
v-for="action in filteredActions"
:key="action.id"
class="action-item"
:class="[action.priority, { completed: action.completed, overdue: isOverdue(action.dueDate) }]"
>
<div class="action-checkbox">
<input
type="checkbox"
:checked="action.completed"
@change="toggleComplete(action.id)"
class="checkbox"
>
</div>
<div class="action-content">
<div class="action-header">
<h4 class="action-title" :class="{ completed: action.completed }">{{ action.title }}</h4>
<div class="action-meta">
<span class="priority-badge" :class="action.priority">{{ getPriorityText(action.priority) }}</span>
<span class="due-date" :class="{ overdue: isOverdue(action.dueDate) }">
{{ formatDueDate(action.dueDate) }}
</span>
</div>
</div>
<p class="action-description">{{ action.description }}</p>
<div class="action-details">
<div class="detail-item">
<span class="detail-label">关联组别:</span>
<span class="detail-value">{{ action.relatedGroup || '全部' }}</span>
</div>
<div class="detail-item" v-if="action.assignee">
<span class="detail-label">负责人:</span>
<span class="detail-value">{{ action.assignee }}</span>
</div>
<div class="detail-item" v-if="action.progress !== undefined">
<span class="detail-label">进度:</span>
<div class="progress-mini">
<div class="progress-bar-mini">
<div class="progress-fill-mini" :style="{ width: action.progress + '%' }"></div>
</div>
<span class="progress-text-mini">{{ action.progress }}%</span>
</div>
</div>
</div>
<div class="action-footer">
<div class="action-tags">
<span v-for="tag in action.tags" :key="tag" class="tag">{{ tag }}</span>
</div>
<div class="action-buttons">
<button class="btn-edit" @click="editAction(action)">编辑</button>
<button class="btn-delete" @click="deleteAction(action.id)">删除</button>
</div>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="filteredActions.length === 0" class="empty-state">
<div class="empty-icon"></div>
<div class="empty-text">
<h3>暂无待处理事项</h3>
<p>{{ filterPriority === 'all' ? '所有事项都已处理完成' : '该优先级下暂无事项' }}</p>
</div>
</div>
<!-- 新增表单模态框 -->
<div v-if="showAddForm" class="modal-overlay" @click="showAddForm = false">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>新增待处理事项</h3>
<button class="close-btn" @click="showAddForm = false">×</button>
</div>
<form @submit.prevent="addAction" class="add-form">
<div class="form-group">
<label>标题</label>
<input v-model="newAction.title" type="text" required class="form-input">
</div>
<div class="form-group">
<label>描述</label>
<textarea v-model="newAction.description" class="form-textarea"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label>优先级</label>
<select v-model="newAction.priority" class="form-select">
<option value="low"></option>
<option value="medium"></option>
<option value="high"></option>
<option value="urgent">紧急</option>
</select>
</div>
<div class="form-group">
<label>截止日期</label>
<input v-model="newAction.dueDate" type="date" class="form-input">
</div>
</div>
<div class="form-group">
<label>关联组别</label>
<select v-model="newAction.relatedGroup" class="form-select">
<option value="">全部</option>
<option value="精英组">精英组</option>
<option value="冲锋组">冲锋组</option>
<option value="突破组">突破组</option>
<option value="新星组">新星组</option>
<option value="潜力组">潜力组</option>
</select>
</div>
<div class="form-actions">
<button type="button" @click="showAddForm = false" class="btn-cancel">取消</button>
<button type="submit" class="btn-submit">添加</button>
</div>
</form>
</div>
</div>
</div>
@@ -292,34 +148,7 @@ const completedCount = computed(() => {
return actions.value.filter(action => action.completed).length
})
// 按优先级获取数量
const getCountByPriority = (priority) => {
return actions.value.filter(action => action.priority === priority && !action.completed).length
}
// 切换完成状态
const toggleComplete = (id) => {
const action = actions.value.find(a => a.id === id)
if (action) {
action.completed = !action.completed
}
}
// 判断是否过期
const isOverdue = (dueDate) => {
return new Date(dueDate) < new Date()
}
// 获取优先级文本
const getPriorityText = (priority) => {
const priorityMap = {
urgent: '紧急',
high: '高',
medium: '中',
low: '低'
}
return priorityMap[priority] || priority
}
// 格式化截止日期
const formatDueDate = (dueDate) => {

View File

@@ -1122,7 +1122,7 @@ const selectMember = (member) => {
// PC端保持一致布局
@media (min-width: 1024px) {
grid-template-columns: 50px 1fr 100px 80px 90px 90px;
grid-template-columns: 50px 1fr 80px 90px 90px;
gap: 1rem;
font-size: 0.875rem;
@@ -1131,7 +1131,7 @@ const selectMember = (member) => {
// 平板端适配
@media (max-width: 1023px) and (min-width: 769px) {
grid-template-columns: 45px 1fr 90px 70px 90px 90px;
grid-template-columns: 45px 1fr 70px 90px 90px;
gap: 0.75rem;
font-size: 0.8125rem;
@@ -1140,7 +1140,7 @@ const selectMember = (member) => {
// 移动端适配
@media (max-width: 768px) {
grid-template-columns: 40px 1fr 80px 60px 90px 90px;
grid-template-columns: 40px 1fr 60px 90px 90px;
gap: 0.5rem;
font-size: 0.8rem;

View File

@@ -346,8 +346,6 @@ async function fetchAbnormalResponseRate() {
console.error('获取异常预警失败:', error)
}
}
// 统计指标--活跃客户沟通率
async function fetchCustomerCommunicationRate() {
const params = getRequestParams()
@@ -401,57 +399,6 @@ async function fetchUrgentNeedToAddress() {
try {
const response = await getUrgentNeedToAddress(hasParams ? params : undefined)
problemRanking.value = response.data
/**
* "data": {
"user_name": "陈盼良",
"user_level": 3,
"calculate_urgent_issue_ratio": {
"成绩提升": "0.00%",
"少玩手机": "0.00%",
"回归学校": "0.00%",
"心理健康": "0.00%"
}
}
}
}
}
// 团队详情加载样式
.team-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
.loading-text {
color: #666;
font-size: 0.9rem;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); },
"urgent_issue_consultations": {
"成绩提升": 0,
"少玩手机": 0,
"回归学校": 0,
"心理健康": 0
}
}
*/
// console.log('客户迫切解决的问题:', response.data)
} catch (error) {
console.error('获取客户迫切解决的问题失败:', error)
}

View File

@@ -25,6 +25,7 @@
<script setup>
import { defineProps, defineEmits } from 'vue';
import { assignTasks } from '@/api/top.js';
defineProps({
tasks: Array,

View File

@@ -94,9 +94,9 @@
<select v-model="newTask.assignee">
<option value="">请选择员工</option>
<option
v-for="employee in employees"
:key="employee.id"
:value="employee.name"
v-for="employee in assigneeOptions"
:key="employee.wechat_id"
:value="employee.wechat_id"
>
{{ employee.name }}
</option>
@@ -140,6 +140,7 @@
<script setup>
import { ref, reactive, computed, onMounted, nextTick } from "vue";
import axios from "axios";
import UserDropdown from "@/components/UserDropdown.vue";
import KpiMetrics from "./components/KpiMetrics.vue";
import SalesProgress from "./components/SalesProgress.vue";
@@ -157,8 +158,9 @@ import DataDetail from "./components/DataDetail.vue";
import CampManagement from "./components/CampManagement.vue";
import DetailedDataTable from "./components/DetailedDataTable.vue";
import { getOverallCompanyPerformance,getCompanyDepositConversionRate,getCompanyTotalCallCount,getCompanyNewCustomer,getCompanyConversionRate,getCompanyRealTimeProgress
,getCompanyConversionRateVsLast,getSalesMonthlyPerformance,getCustomerTypeDistribution,getUrgentNeedToAddress,getLevelTree,getDetailedDataTable
,getCompanyConversionRateVsLast,getSalesMonthlyPerformance,getCustomerTypeDistribution,getUrgentNeedToAddress,getLevelTree,getDetailedDataTable,assignTasks
} from "@/api/top";
const rankingPeriod = ref("month");
const rankingData = ref([
{ id: 1, name: "张三", department: "销售一部", performance: 125000 },
@@ -172,15 +174,7 @@ const sortField = ref("dealRate");
const sortOrder = ref("desc");
const selectedPerson = ref(null);
const tasks = ref([
{
id: 1,
title: "完成Q4销售目标制定",
assignee: "张三",
deadline: "2024-01-15",
status: "pending",
}
]);
const tasks = ref([]);
const employees = ref([
{ id: 1, name: "张三" }
@@ -193,7 +187,51 @@ const newTask = reactive({
deadline: "",
description: "",
});
// 下拉框人员
const assigneeOptions = ref([]);
async function name() {
try {
console.log('开始获取下属人员列表...');
const res = await axios.get('http://192.168.15.56:8890/api/v1/level_five/overview/get_subordinates',{
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
});
assigneeOptions.value = res.data.data;
console.log('assigneeOptions设置后:', assigneeOptions.value);
} catch (error) {
console.error('获取下属人员列表失败:', error);
}
/**
* "data": [
{
"name": "程琦",
"wechat_id": "1688856301330784"
},
{
"name": "潘加俊",
"wechat_id": "1688855836721980"
},
{
"name": "伍晶晶",
"wechat_id": "1688854476805987"
},
{
"name": "张三丰",
"wechat_id": "1212345648513"
},
{
"name": "朱一航",
"wechat_id": "1212345648513"
},
{
"name": "王卓琳",
"wechat_id": "1212345648513"
}
]
*/
}
// 计算属性
const filteredTableData = computed(() => {
let filtered = tableData.value;
@@ -321,31 +359,50 @@ const downloadCall = (callId) => {
console.log("下载通话录音:", callId);
};
const createTask = () => {
const createTask = async () => {
if (!newTask.title || !newTask.assignee || !newTask.deadline) {
alert("请填写完整信息");
return;
}
const task = {
id: Date.now(),
title: newTask.title,
assignee: newTask.assignee,
deadline: newTask.deadline,
status: "pending",
};
try {
// 构造API请求参数
const params = {
task_title: newTask.title,
task_assignee: [newTask.assignee], // 转换为数组格式
expiration_date: newTask.deadline.replace(/-/g, ''), // 移除日期中的横线
task_content: newTask.description || newTask.title
};
tasks.value.unshift(task);
// 调用API
const response = await assignTasks(params);
console.log('任务创建成功:', response);
// 重置表单
Object.assign(newTask, {
title: "",
assignee: "",
deadline: "",
description: "",
});
// 创建本地任务对象用于显示
const task = {
id: Date.now(),
title: newTask.title,
assignee: newTask.assignee,
deadline: newTask.deadline,
status: "pending",
};
showTaskModal.value = false;
tasks.value.unshift(task);
// 重置表单
Object.assign(newTask, {
title: "",
assignee: "",
deadline: "",
description: "",
});
showTaskModal.value = false;
alert('任务创建成功!');
} catch (error) {
console.error('创建任务失败:', error);
alert('创建任务失败,请重试');
}
};
// 核心数据
@@ -592,6 +649,7 @@ onMounted(async() => {
await getCustomerUrgency()
await CusotomGetLevelTree()
await getDetailData()
await name() // 获取下属人员列表
});
</script>