feat(topOne): 添加各中心营期阶段组件并优化API调用

- 新增PeriodStage组件展示各中心营期阶段信息
- 移除任务管理相关代码,替换为营期阶段展示
- 修改API端点路径,优化优秀录音文件接口调用
- 调整TeamAlerts组件样式,减小最大高度
This commit is contained in:
2025-08-26 13:55:55 +08:00
parent b7d46c3dde
commit 11e686d4d9
4 changed files with 319 additions and 248 deletions

View File

@@ -65,12 +65,12 @@ export const getDetailedDataTable = (params) => {
return https.post('/api/v1/level_five/overview/detailed_data_table', params) return https.post('/api/v1/level_five/overview/detailed_data_table', params)
} }
// 下发任务 /api/v1/level_five/overview/assign_tasks // 获取各中心营期阶段 /api/v1/level_five/overview/get_period_stage
export const assignTasks = (params) => { export const getPeriodStage = (params) => {
return https.post('http://192.168.15.60:8890/api/v1/level_five/overview/assign_tasks', params) return https.get('/api/v1/level_five/overview/get_period_stage', params)
} }
// 获取优秀录音文件 /api/v1/level_four/overview/get_excellent_record_file // 获取优秀录音文件 /api/v1/level_four/overview/get_excellent_record_file
export const getExcellentRecordFile = (params) => { export const getExcellentRecordFile = (params) => {
return https.post('/api/v1/level_four/overview/get_excellent_record_file', params) return https.post('/api/v1/common/get_excellent_record_file', params)
} }

View File

@@ -63,7 +63,7 @@ const aggregatedAlerts = computed(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: 0.75rem;
max-height: 300px; max-height: 270px;
overflow-y: auto; overflow-y: auto;
// 自定义滚动条样式 // 自定义滚动条样式

View File

@@ -0,0 +1,287 @@
<template>
<div class="period-stage-container">
<div class="period-stage-header">
<h3>各中心营期阶段</h3>
<div class="stage-legend">
<span class="legend-item">
<span class="legend-dot active"></span>
进行中
</span>
<span class="legend-item">
<span class="legend-dot completed"></span>
已完成
</span>
<span class="legend-item">
<span class="legend-dot pending"></span>
未开始
</span>
</div>
</div>
<div class="period-stage-content">
<div class="center-list">
<div
v-for="(center, index) in periodStageData"
:key="index"
class="center-item"
>
<div class="center-info">
<div class="center-name">{{ center.center_name }}</div>
<div class="center-leader">负责人{{ center.center_leader_name }}</div>
</div>
<div class="stage-badge" :class="getStageClass(center.period_stage)">
{{ center.period_stage }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getPeriodStage } from '@/api/top'
// 营期阶段数据
const periodStageData = ref([])
// 获取营期阶段数据
const fetchPeriodStageData = async () => {
try {
console.log('开始获取营期阶段数据...')
const res = await getPeriodStage()
console.log('API响应:', res)
if (res && res.data) {
periodStageData.value = res.data.period_stage
console.log('数据设置成功:', periodStageData.value)
} else {
console.log('API响应无数据使用默认数据')
setDefaultData()
}
} catch (error) {
console.error('获取营期阶段数据失败:', error)
setDefaultData()
}
}
// 设置默认数据
const setDefaultData = () => {
periodStageData.value = [
{
center_name: '一中心',
center_leader_name: '张三丰',
period_stage: '接数据'
},
{
center_name: '二中心',
center_leader_name: '朱一航',
period_stage: '未知阶段'
},
{
center_name: '三中心',
center_leader_name: '程琦',
period_stage: '课1'
}
]
console.log('已设置默认数据:', periodStageData.value)
}
// 获取阶段状态样式类
const getStageClass = (stage) => {
const stageMap = {
'接数据': 'stage-data',
'课1': 'stage-course1',
'课2': 'stage-course2',
'课3': 'stage-course3',
'课4': 'stage-course4',
'休息': 'stage-rest',
'未知阶段': 'stage-unknown'
}
return stageMap[stage] || 'stage-default'
}
// 组件挂载时获取数据
onMounted(() => {
fetchPeriodStageData()
})
</script>
<style scoped>
.period-stage-container {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.period-stage-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.period-stage-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.stage-legend {
display: flex;
gap: 8px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #666;
}
.legend-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.legend-dot.active {
background-color: #1890ff;
}
.legend-dot.completed {
background-color: #52c41a;
}
.legend-dot.pending {
background-color: #d9d9d9;
}
.period-stage-content {
margin-top: 16px;
max-height: 250px;
overflow-y: auto;
}
.center-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.center-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
transition: all 0.2s ease;
}
.center-item:hover {
background: #f1f5f9;
border-color: #cbd5e1;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.center-info {
flex: 1;
}
.center-name {
font-size: 16px;
font-weight: 600;
color: #1e293b;
margin-bottom: 4px;
}
.center-leader {
font-size: 14px;
color: #64748b;
}
.stage-badge {
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
text-align: center;
min-width: 80px;
}
.stage-data {
background: #dbeafe;
color: #1e40af;
}
.stage-course1 {
background: #dcfce7;
color: #166534;
}
.stage-course2 {
background: #fef3c7;
color: #92400e;
}
.stage-course3 {
background: #fed7d7;
color: #c53030;
}
.stage-course4 {
background: #e9d5ff;
color: #7c3aed;
}
.stage-rest {
background: #f3f4f6;
color: #6b7280;
}
.stage-unknown {
background: #fecaca;
color: #dc2626;
}
.stage-default {
background: #e5e7eb;
color: #374151;
}
/* 响应式设计 */
@media (max-width: 768px) {
.period-stage-container {
padding: 16px;
}
.period-stage-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.stage-legend {
gap: 12px;
}
.stage-item {
padding-left: 32px;
margin-bottom: 24px;
}
.stage-timeline::before {
left: 10px;
}
.stage-dot {
left: 6px;
}
}
</style>

View File

@@ -7,19 +7,14 @@
<UserDropdown /> <UserDropdown />
</div> </div>
<!-- 第一行核心业绩指标销售实时进度下发任务 --> <!-- 第一行核心业绩指标销售实时进度 -->
<div class="dashboard-row row-1"> <div class="dashboard-row row-1">
<!-- 核心业绩指标 --> <!-- 核心业绩指标 -->
<kpi-metrics :kpi-data="totalDeals" :format-number="formatNumber" /> <kpi-metrics :kpi-data="totalDeals" :format-number="formatNumber" />
<!-- 销售实时进度 --> <!-- 销售实时进度 -->
<sales-progress :sales-data="realTimeProgress" /> <sales-progress :sales-data="realTimeProgress" />
<!-- 下发任务 --> <!-- 各中心营期阶段 -->
<task-list <period-stage />
:tasks="tasks"
:format-date="formatDate"
:get-task-status-text="getTaskStatusText"
@show-task-modal="showTaskModal = true"
/>
</div> </div>
<!-- 第二行 --> <!-- 第二行 -->
<div class="dashboard-row row-3"> <div class="dashboard-row row-3">
@@ -69,59 +64,7 @@
@filter-change="handleFilterChange" @filter-change="handleFilterChange"
/> />
</div> </div>
<!-- 新建任务模态框 -->
<div
v-if="showTaskModal"
class="modal-overlay"
@click="showTaskModal = false"
>
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>新建任务</h3>
<button class="close-btn" @click="showTaskModal = false">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>任务标题</label>
<input
v-model="newTask.title"
type="text"
placeholder="请输入任务标题"
/>
</div>
<div class="form-group">
<label>分配给</label>
<select v-model="newTask.assignee">
<option value="">请选择员工</option>
<option
v-for="employee in assigneeOptions"
:key="employee.wechat_id"
:value="employee.wechat_id"
>
{{ employee.name }}
</option>
</select>
</div>
<div class="form-group">
<label>截止日期</label>
<input v-model="newTask.deadline" type="date" />
</div>
<div class="form-group">
<label>任务描述</label>
<textarea
v-model="newTask.description"
placeholder="请输入任务描述"
></textarea>
</div>
</div>
<div class="modal-footer">
<button class="cancel-btn" @click="showTaskModal = false">
取消
</button>
<button class="confirm-btn" @click="createTask">创建任务</button>
</div>
</div>
</div>
</div> </div>
</template> </template>
@@ -144,7 +87,6 @@ import axios from "axios";
import UserDropdown from "@/components/UserDropdown.vue"; import UserDropdown from "@/components/UserDropdown.vue";
import KpiMetrics from "./components/KpiMetrics.vue"; import KpiMetrics from "./components/KpiMetrics.vue";
import SalesProgress from "./components/SalesProgress.vue"; import SalesProgress from "./components/SalesProgress.vue";
import TaskList from "./components/TaskList.vue";
import FunnelChart from "./components/FunnelChart.vue"; import FunnelChart from "./components/FunnelChart.vue";
import CustomerProfile from "./components/CustomerProfile.vue"; import CustomerProfile from "./components/CustomerProfile.vue";
import CustomerType from "./components/CustomerType.vue"; import CustomerType from "./components/CustomerType.vue";
@@ -157,8 +99,10 @@ import QualityCalls from "./components/QualityCalls.vue";
import DataDetail from "./components/DataDetail.vue"; import DataDetail from "./components/DataDetail.vue";
import CampManagement from "./components/CampManagement.vue"; import CampManagement from "./components/CampManagement.vue";
import DetailedDataTable from "./components/DetailedDataTable.vue"; import DetailedDataTable from "./components/DetailedDataTable.vue";
import PeriodStage from "./components/PeriodStage.vue";
import { getOverallCompanyPerformance,getCompanyDepositConversionRate,getCompanyTotalCallCount,getCompanyNewCustomer,getCompanyConversionRate,getCompanyRealTimeProgress import { getOverallCompanyPerformance,getCompanyDepositConversionRate,getCompanyTotalCallCount,getCompanyNewCustomer,getCompanyConversionRate,getCompanyRealTimeProgress
,getCompanyConversionRateVsLast,getSalesMonthlyPerformance,getCustomerTypeDistribution,getUrgentNeedToAddress,getLevelTree,getDetailedDataTable,assignTasks } from "@/api/top"; ,getCompanyConversionRateVsLast,getSalesMonthlyPerformance,getCustomerTypeDistribution,getUrgentNeedToAddress,getLevelTree,getDetailedDataTable,getExcellentRecordFile } from "@/api/top";
import { useUserStore } from "@/stores/user.js";
const rankingPeriod = ref("month"); const rankingPeriod = ref("month");
const rankingData = ref([ const rankingData = ref([
@@ -173,131 +117,8 @@ const sortField = ref("dealRate");
const sortOrder = ref("desc"); const sortOrder = ref("desc");
const selectedPerson = ref(null); const selectedPerson = ref(null);
const tasks = ref([]); const userStore = useUserStore();
const employees = ref([
{ id: 1, name: "张三" }
]);
const showTaskModal = ref(false);
const newTask = reactive({
title: "",
assignee: "",
deadline: "",
description: "",
});
// 获取任务列表
const getTaskList = async () => {
try {
const res = await axios.post('http://192.168.15.60:8890/api/v1/level_five/overview/view_tasks', {}, {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
})
console.log(888888,res)
if (res.data.code === 200) {
const apiTasks = res.data.data.tasks || res.data.data
// 将API数据格式转换为TaskList组件期望的格式
tasks.value = apiTasks.map(task => ({
id: task.task_id,
title: task.task_title,
assignee: task.assignee || '未分配',
deadline: task.expiration_date,
status: task.state === '待处理' ? 'pending' : task.state === '正在处理' ? 'in-progress' : 'completed',
description: task.task_content,
created_at: task.created_at
}))
console.log(777777,tasks.value)
/**
* tasks
:
[,…]
0
:
{task_id: "1755748690560728_22d55cc618784537973481228a15956a", task_title: "55", task_content: "222",…}
created_at
:
"2025-08-21 11:58:10"
expiration_date
:
"20250808"
state
:
"待处理"
task_content
:
"222"
task_id
:
"1755748690560728_22d55cc618784537973481228a15956a"
task_title
:
"55"
1
:
{task_id: "1755745331126891_650206e5b6d345699de3e3e406a2600e", task_title: "测试任务",…}
created_at
:
"2025-08-21 11:02:11"
expiration_date
:
"121221"
state
:
"待处理"
task_content
:
"测试任务"
task_id
:
"1755745331126891_650206e5b6d345699de3e3e406a2600e"
task_title
:
"测试任务"
2
:
{task_id: "1755745330094989_528dd87dc13a4a5bb33c9c272fb1a482", task_title: "测试任务",…}
created_at
:
"2025-08-21 11:02:10"
expiration_date
:
"121221"
state
:
"已完成"
task_content
:
"测试任务"
task_id
:
"1755745330094989_528dd87dc13a4a5bb33c9c272fb1a482"
task_title
:
"测试任务"
*/
}
} catch (error) {
console.error('获取任务列表失败:', error)
}
}
// 下拉框人员
const assigneeOptions = ref([]);
async function name() {
try {
console.log('开始获取下属人员列表...');
const res = await axios.get('http://192.168.15.60: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);
}
}
// 计算属性 // 计算属性
const filteredTableData = computed(() => { const filteredTableData = computed(() => {
let filtered = tableData.value; let filtered = tableData.value;
@@ -418,14 +239,7 @@ const selectPerson = (person) => {
selectedPerson.value = person; selectedPerson.value = person;
}; };
const getTaskStatusText = (status) => {
const statusMap = {
pending: "待处理",
"in-progress": "进行中",
completed: "已完成",
};
return statusMap[status] || status;
};
const playCall = (callId) => { const playCall = (callId) => {
console.log("播放通话录音:", callId); console.log("播放通话录音:", callId);
@@ -435,51 +249,7 @@ const downloadCall = (callId) => {
console.log("下载通话录音:", callId); console.log("下载通话录音:", callId);
}; };
const createTask = async () => {
if (!newTask.title || !newTask.assignee || !newTask.deadline) {
alert("请填写完整信息");
return;
}
try {
// 构造API请求参数
const params = {
task_title: newTask.title,
task_assignee: [newTask.assignee], // 转换为数组格式
expiration_date: newTask.deadline.replace(/-/g, ''), // 移除日期中的横线
task_content: newTask.description || newTask.title
};
// 调用API
const response = await assignTasks(params);
console.log('任务创建成功:', response);
// 创建本地任务对象用于显示
const task = {
id: Date.now(),
title: newTask.title,
assignee: newTask.assignee,
deadline: newTask.deadline,
status: "pending",
};
tasks.value.unshift(task);
// 重置表单
Object.assign(newTask, {
title: "",
assignee: "",
deadline: "",
description: "",
});
showTaskModal.value = false;
alert('任务创建成功!');
} catch (error) {
console.error('创建任务失败:', error);
alert('创建任务失败,请重试');
}
};
// 核心数据 // 核心数据
const totalDeals = ref({}); const totalDeals = ref({});
@@ -505,6 +275,9 @@ async function getTotalDeals() {
} }
// 实时进度 // 实时进度
const realTimeProgress = ref({}); const realTimeProgress = ref({});
async function getRealTimeProgress() { async function getRealTimeProgress() {
try { try {
const res = await getCompanyRealTimeProgress() const res = await getCompanyRealTimeProgress()
@@ -706,7 +479,6 @@ async function getDetailData(params) {
console.error("获取详细数据表格失败:", error); console.error("获取详细数据表格失败:", error);
} }
} }
} }
// 处理筛选器变化 // 处理筛选器变化
@@ -714,19 +486,31 @@ const handleFilterChange = (filterParams) => {
console.log('筛选器变化:', filterParams) console.log('筛选器变化:', filterParams)
getDetailData(filterParams) getDetailData(filterParams)
} }
// 优秀录音
const excellentRecord = ref({});
async function CenterExcellentRecord() {
const params={
user_level:userStore.userInfo.user_level.toString(),
user_name:userStore.userInfo.username
}
try {
const res = await getExcellentRecordFile(params)
excellentRecord.value = res.data
} catch (error) {
console.error("获取优秀录音失败:", error);
}
}
onMounted(async() => { onMounted(async() => {
// 页面初始化逻辑 // 页面初始化逻辑
await getRealTimeProgress() await getRealTimeProgress()
await getTotalDeals() await getTotalDeals()
await getTaskList()
await getConversionComparison('month') await getConversionComparison('month')
await getCompanySalesRank('red') await getCompanySalesRank('red')
await getCustomerTypeRatio('child_education') await getCustomerTypeRatio('child_education')
await getCustomerUrgency() await getCustomerUrgency()
await CusotomGetLevelTree() await CusotomGetLevelTree()
await getDetailData() await getDetailData()
await name() // 获取下属人员列表 await CenterExcellentRecord()
}); });
</script> </script>