diff --git a/my-vue-app/src/api/api.js b/my-vue-app/src/api/api.js index d3877fa..37b4ed3 100644 --- a/my-vue-app/src/api/api.js +++ b/my-vue-app/src/api/api.js @@ -16,7 +16,7 @@ export const getTableFillingRate = (params) => { } // 平均应答时间 /api/v1/more_level_screening/average_response_time -export const getAverageResponseTime = (params) => { +export const getAverageResponseTime = (params) => { return https.post('/api/v1/sales/average_response_time', params) } @@ -28,7 +28,6 @@ export const getWeeklyActiveCommunicationRate = (params) => { // 超时应答率 /api/v1/sales/timeout_response_rate export const getTimeoutResponseRate = (params) => { return https.post('/api/v1/sales/timeout_response_rate', params) - } // 客户通话录音 /api/v1/sales/get_customer_call_info @@ -46,10 +45,6 @@ export const getCustomerFormInfo = (params) => { return https.post('/api/v1/customer_list/get_customer_form_info', params) } -// 时间线 /api/v1/customer_list/sales_customers_list -export const getCustomerAttendance = (params) => { - return https.post('/api/v1/customer_list/sales_customers_list', params) -} // 转化率、分配数据量、加微率 /api/v1/sales/conversion_rate_and_allocated_data export const getConversionRateAndAllocatedData = (params) => { return https.post('/api/v1/sales/conversion_rate_and_allocated_data', params) @@ -57,5 +52,21 @@ export const getConversionRateAndAllocatedData = (params) => { +// 时间线 /api/v1/customer_list/sales_customers_list +export const getCustomerAttendance = (params ) => { + return https.post('/api/v1/sales_timeline/get_all_customers', params) +} + +// 课1-4之后的4个阶段 /api/v1/sales_timeline/get_class_customers +export const getCustomerAttendanceAfterClass4 = (params) => { + return https.post('/api/v1/sales_timeline/get_class_customers', params) +} + +// 成交 /api/v1/sales_timeline/get_pay_money_customers +export const getPayMoneyCustomers = (params) => { + return https.post('/api/v1/sales_timeline/get_pay_money_customers', params) +} + + diff --git a/my-vue-app/src/utils/https.js b/my-vue-app/src/utils/https.js index 85e7267..a674690 100644 --- a/my-vue-app/src/utils/https.js +++ b/my-vue-app/src/utils/https.js @@ -6,7 +6,7 @@ import { useUserStore } from '@/stores/user' // 创建axios实例 const service = axios.create({ baseURL: 'http://192.168.15.51:8890' || '', // API基础路径,支持完整URL - timeout: 30000, // 请求超时时间 + timeout: 100000, // 请求超时时间 headers: { 'Content-Type': 'application/json;charset=UTF-8' } @@ -157,7 +157,7 @@ const http = { }, // POST请求 - post(url, data = {}, config = {}) { + post(url, data, config) { return service({ method: 'post', url, diff --git a/my-vue-app/src/views/person/components/SalesTimelineWithTaskList.vue b/my-vue-app/src/views/person/components/SalesTimelineWithTaskList.vue index 2d569b9..5ff9aeb 100644 --- a/my-vue-app/src/views/person/components/SalesTimelineWithTaskList.vue +++ b/my-vue-app/src/views/person/components/SalesTimelineWithTaskList.vue @@ -19,10 +19,10 @@

{{ stage.displayName || stage.name }}

- {{ stage.count }} + {{ stage.name === '全部' ? customersCount : stage.count }} 位客户
-
{{ getPercentage(stage.count) }}%
+
{{ getPercentage(stage.count) }}%
@@ -58,12 +58,10 @@
{{ item.profession || '公务员' }} {{ item.education || '高中' }} - - - - {{ getAttendedLessons(item.class_situation) }} - + + {{ getAttendedLessons(item.class_situation) }} + {{ item.type }}
@@ -117,7 +115,6 @@ import { computed, ref } from 'vue'; // 定义props const props = defineProps({ - // SalesTimeline props data: { type: Object, default: () => ({}) @@ -126,7 +123,6 @@ const props = defineProps({ type: String, default: 'all' }, - // TaskList props contacts: { type: Array, required: true @@ -135,28 +131,34 @@ const props = defineProps({ type: Number, default: null }, - // 课程客户数据(课1-4) courseCustomers: { type: Object, default: () => ({}) }, - // KPI数据(转化率、分配数据量、加微率等) - kpiData: { - type: Object, - default: () => ({}) + customersList: { + type: Array, + default: () => [] + }, + customersCount: { + type: Number, + default: 0 + }, + payMoneyCustomersList: { + type: Array, + default: () => [] + }, + payMoneyCustomersCount: { + type: Number, + default: 0 } }); -// 定义emits const emit = defineEmits(['stage-select', 'select-contact']); -// SalesTimeline methods -// 计算总客户数 const totalCustomers = computed(() => { - // 使用 "全部" 字段作为总客户数,如果没有则使用所有阶段的最大值 - if (props.data['全部']) { - return props.data['全部']; - } + if (props.customersCount > 0) return props.customersCount; + if (props.customersList?.length > 0) return props.customersList.length; + if (props.data['全部']) return props.data['全部']; const baseStages = [ props.data['未加微'] || 0, @@ -169,53 +171,76 @@ const totalCustomers = computed(() => { props.data['定价转化'] || 0, props.data['成交'] || 0 ]; - return Math.max(...baseStages, 1); // 至少返回1避免除零错误 + return Math.max(...baseStages, 1); +}); + + +const getStageCount = (stageType) => { + if (!props.customersList?.length) { + return props.data[stageType] || 0; + } + + if (stageType === '课1-4' && props.courseCustomers?.['课1-4']) { + return props.courseCustomers['课1-4'].length; + } + + return props.customersList.filter(customer => customer.type === stageType).length; +}; + + +const stages = computed(() => { + const stageList = [ + { id: 0, name: '全部', displayName: '全部', count: props.customersCount || props.customersList.length, color: '#f3f4f6' }, + { id: 1, name: '待加微', displayName: '待加微', count: getStageCount('待加微'), color: '#e3f2fd' }, + { id: 2, name: '待填表单', displayName: '待填表单', count: getStageCount('待填表单'), color: '#90caf9' }, + { id: 3, name: '待入群', displayName: '待入群', count: getStageCount('待入群'), color: '#bbdefb' }, + { id: 4, name: '待联系', displayName: '待联系', count: getStageCount('待联系'), color: '#bbdefb' }, + { id: 5, name: '待到课', displayName: '待到课', count: getStageCount('待到课'), color: '#bbdefb' }, + { id: 6, name: '课1-4', displayName: '课1-4', count: getStageCount('课1-4'), color: '#64b5f6' }, + { id: 7, name: '点击未支付', displayName: '点击未支付', count: getStageCount('点击未支付'), color: '#42a5f5' }, + { id: 8, name: '付定金', displayName: '付定金', count: getStageCount('付定金'), color: '#2196f3' }, + { id: 9, name: '定金转化', displayName: '定金转化', count: getStageCount('定金转化'), color: '#1e88e5' }, + { id: 10, name: '成交', displayName: '成交', count: getStageCount('成交'), color: '#1976d2' } + ]; + + return stageList; }); -// 销售阶段数据 -const stages = computed(() => [ - { id: 0, name: '全部', displayName: '全部', count: props.data['全部'] || 0, color: '#f3f4f6' }, - { id: 1, name: '待加微', displayName: '待加微', count: props.data['未加微'] || 0, color: '#e3f2fd' }, - { id: 2, name: '待填表单', displayName: '待填表单', count: props.data['待填表单'] || 0, color: '#90caf9' }, - { id: 3, name: '待入群', displayName: '待入群', count: props.data['待入群'] || 0, color: '#bbdefb' }, - { id: 4, name: '待联系', displayName: '待联系', count: props.data['待联系'] || 0, color: '#bbdefb' }, - { id: 5, name: '待到课', displayName: '待到课', count: props.data['待到课'] || 0, color: '#bbdefb' }, - { id: 6, name: '课1-4', displayName: '课1-4', count: props.data['课1-4'] || 0, color: '#64b5f6' }, - { id: 7, name: '点击未支付', displayName: '点击未支付', count: props.data['点击未支付'] || 0, color: '#42a5f5' }, - { id: 8, name: '付定金', displayName: '付定金', count: props.data['付定金'] || 0, color: '#2196f3' }, - { id: 9, name: '定价转化', displayName: '定价转化', count: props.data['定价转化'] || 0, color: '#1e88e5' }, - { id: 10, name: '成交', displayName: '成交', count: props.data['成交'] || 0, color: '#1976d2' } -]); -// 计算百分比 const getPercentage = (count) => { if (totalCustomers.value === 0) return 0; return Math.round((count / totalCustomers.value) * 100); }; -// 选择阶段 + const selectStage = (stageName) => { - // 如果选择的是课1-4阶段,使用独立的课程数据处理逻辑 - if (stageName === '课1-4') { - console.log('选择课1-4阶段,课程数据:', props.courseCustomers); - // 发送课程数据选择事件,包含课程客户数据 + let filteredCustomers = []; + + if (stageName === '全部') { + filteredCustomers = props.customersList || []; + } else if (stageName === '课1-4' && props.courseCustomers?.['课1-4']) { emit('stage-select', stageName, { isCourseStage: true, - courseData: props.courseCustomers['课1-4'] || [] + courseData: props.courseCustomers['课1-4'], + filteredCustomers: props.courseCustomers['课1-4'] }); + return; } else { - // 普通阶段选择 - emit('stage-select', stageName); + filteredCustomers = props.customersList.filter(customer => customer.type === stageName); } + + emit('stage-select', stageName, { + filteredCustomers, + stageType: stageName, + customerCount: filteredCustomers.length + }); }; -// 计算选中客户的详细信息 const selectedContactDetails = computed(() => { if (!props.selectedContactId) return null; return props.contacts.find(contact => contact.id === props.selectedContactId); }); -// TaskList methods const selectContact = (id) => { emit('select-contact', id); }; @@ -226,39 +251,29 @@ const getHealthIndicator = (score) => { return { class: 'health-risk', text: '高风险', textColor: 'text-red' }; }; -// 获取客户参加的课程情况 + const getAttendedLessons = (classSituation) => { - if (!classSituation) { - return '暂无到课记录'; - } + if (!classSituation) return '暂无到课记录'; - // 如果是数组,直接展示 if (Array.isArray(classSituation)) { return classSituation.join(' '); } - // 如果是对象,使用现有的处理方式 if (typeof classSituation === 'object') { - // 提取课程编号并排序 const lessonNumbers = Object.keys(classSituation) .map(key => parseInt(key)) .filter(num => !isNaN(num)) .sort((a, b) => a - b); - if (lessonNumbers.length === 0) { - return '暂无到课记录'; - } - - return `${lessonNumbers.join(' ')}`; + return lessonNumbers.length > 0 ? lessonNumbers.join(' ') : '暂无到课记录'; } - // 其他情况 return '暂无到课记录'; }; \ No newline at end of file diff --git a/my-vue-app/src/views/person/sale.vue b/my-vue-app/src/views/person/sale.vue index da1763b..e8e06cb 100644 --- a/my-vue-app/src/views/person/sale.vue +++ b/my-vue-app/src/views/person/sale.vue @@ -2,6 +2,9 @@
+ + +
@@ -24,7 +27,14 @@
+ +
+
+
正在加载数据分析...
+
+ 客户转化全流程跟踪

+ +
+
+
正在加载销售时间线...
+
+
@@ -91,27 +112,49 @@ import { ref, reactive, computed, onMounted, nextTick } from "vue"; import { useRouter } from "vue-router"; import { useUserStore } from "@/stores/user"; -import http from "@/utils/https"; import CustomerDetail from "./components/CustomerDetail.vue"; import PersonalDashboard from "./components/PersonalDashboard.vue"; import SalesTimelineWithTaskList from "./components/SalesTimelineWithTaskList.vue"; import RawDataCards from "./components/RawDataCards.vue"; import FloatingTodo from "./components/FloatingTodo.vue"; -import Header from "@/components/header.vue"; import UserDropdown from "@/components/UserDropdown.vue"; -import {getCustomerAttendance,getTodayCall,getProblemDistribution,getTableFillingRate,getAverageResponseTime,getWeeklyActiveCommunicationRate,getTimeoutResponseRate,getCustomerCallInfo,getCustomerChatInfo,getCustomerFormInfo,getConversionRateAndAllocatedData} from "@/api/api.js" +import Loading from "@/components/Loading.vue"; +import {getCustomerAttendance,getTodayCall,getProblemDistribution,getTableFillingRate,getAverageResponseTime, + getWeeklyActiveCommunicationRate,getTimeoutResponseRate,getCustomerCallInfo,getCustomerChatInfo,getCustomerFormInfo, + getConversionRateAndAllocatedData,getCustomerAttendanceAfterClass4,getPayMoneyCustomers} from "@/api/api.js" // 路由实例 const router = useRouter(); // 用户store const userStore = useUserStore(); + +// 获取通用请求参数的函数 +const getRequestParams = () => { + const params = {} + // 只从路由参数获取 + const routeUserLevel = router.currentRoute.value.query.user_level || router.currentRoute.value.params.user_level + const routeUserName = router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name + // 如果路由有参数,使用路由参数 + if (routeUserLevel) { + params.user_level = routeUserLevel.toString() + } + if (routeUserName) { + params.user_name = routeUserName + } + + return params +} // STATE const selectedContactId = ref(null); const contextPanelRef = ref(null); const selectedStage = ref('全部'); // 选中的销售阶段 -const problemDistributionData = ref(null); // 问题分布数据 -const isLoadingProblemData = ref(false); // 加载状态 + +const isPageLoading = ref(true); // 页面整体加载状态 +const isKpiLoading = ref(false); // KPI数据加载状态 +const isStatisticsLoading = ref(false); // 统计数据加载状态 +const isUrgentProblemLoading = ref(false); // 紧急问题数据加载状态 +const isTimelineLoading = ref(false); // 时间线数据加载状态 // KPI数据 const kpiDataState = reactive({ @@ -139,11 +182,18 @@ const urgentProblemData = ref([]); const timelineData = ref({}); // 客户列表数据 -const customersList = ref({}); +const customersList = ref([]); + +// 客户总数 +const customersCount = ref(0); // 课程客户数据(课1-4) const courseCustomers = ref({}); +// 成交阶段客户数据 +const payMoneyCustomersList = ref([]); +const payMoneyCustomersCount = ref(0); + // MOCK DATA (Should ideally come from a store or API) const MOCK_DATA = reactive({ contacts: [ @@ -174,154 +224,237 @@ const MOCK_DATA = reactive({ }); // 核心Kpi async function getCoreKpi() { - console.log('userStore.userInfo.user_level', userStore.userInfo) - const params = { - user_level: userStore.userInfo.user_level.toString(), - user_name: userStore.userInfo.username - } - // 今日通话 - const res = await getTodayCall(params) - if (res.code === 200) { - kpiDataState.totalCalls = res.data.today_call - } - // 转化率、分配数据量、加微率 - const conversionRes = await getConversionRateAndAllocatedData(params) - if (conversionRes.code === 200) { - kpiDataState.conversionRate = conversionRes.data.conversion_rate || 0 - kpiDataState.assignedData = conversionRes.data.all_count || 0 - kpiDataState.wechatAddRate = conversionRes.data.plus_v_conversion_rate || 0 + isKpiLoading.value = true + try { + const params = getRequestParams() + const hasParams = params.user_name + + // 今日通话数据 + const res = await getTodayCall(hasParams ? params : undefined) + if (res.code === 200) { + kpiDataState.totalCalls = res.data.today_call + } + + // 转化率、分配数据量、加微率 + const conversionRes = await getConversionRateAndAllocatedData(hasParams ? params : undefined) + if (conversionRes.code === 200) { + kpiDataState.conversionRate = conversionRes.data.conversion_rate || 0 + kpiDataState.assignedData = conversionRes.data.all_count || 0 + kpiDataState.wechatAddRate = conversionRes.data.plus_v_conversion_rate || 0 + } + + } catch (error) { + console.error('获取核心KPI数据失败:', error) + } finally { + isKpiLoading.value = false } } - // 获取统计数据 async function getStatisticsData() { - const params = { - user_level: userStore.userInfo.user_level.toString(), - user_name: userStore.userInfo.username - } - + isStatisticsLoading.value = true try { + const params = getRequestParams() + const hasParams = params.user_name + // 获取表单填写率 - const fillingRateRes = await getTableFillingRate(params) + const fillingRateRes = await getTableFillingRate(hasParams ? params : undefined) if (fillingRateRes.code === 200) { statisticsData.formCompletionRate = fillingRateRes.data.filling_rate } // 获取平均响应时间 - const avgResponseRes = await getAverageResponseTime(params) + const avgResponseRes = await getAverageResponseTime(hasParams ? params : undefined) if (avgResponseRes.code === 200) { statisticsData.averageResponseTime = avgResponseRes.data.average_minutes } // 获取客户沟通率 - const communicationRes = await getWeeklyActiveCommunicationRate(params) + const communicationRes = await getWeeklyActiveCommunicationRate(hasParams ? params : undefined) if (communicationRes.code === 200) { statisticsData.customerCommunicationRate = communicationRes.data.communication_rate } // 获取超时响应率 - const timeoutRes = await getTimeoutResponseRate(params) + const timeoutRes = await getTimeoutResponseRate(hasParams ? params : undefined) if (timeoutRes.code === 200) { statisticsData.timeoutResponseRate = timeoutRes.data.overtime_rate_600 statisticsData.severeTimeoutRate = timeoutRes.data.overtime_rate_800 } } catch (error) { console.error('获取统计数据失败:', error) + } finally { + isStatisticsLoading.value = false } } // 客户迫切解决的问题 async function getUrgentProblem() { - const params = { - user_name: userStore.userInfo.username, - user_level: userStore.userInfo.user_level.toString(), - } - const res = await getProblemDistribution(params) - if(res.code === 200) { - // 将API返回的对象格式转换为数组格式 - const problemDistribution = res.data.problem_distribution - urgentProblemData.value = Object.entries(problemDistribution).map(([name, percentage]) => ({ - name: name, - value: parseInt(percentage.replace('%', '')) || 0 - })) + isUrgentProblemLoading.value = true + try { + const params = getRequestParams() + const hasParams = params.user_name + + const res = await getProblemDistribution(hasParams ? params : undefined) + if(res.code === 200) { + // 将API返回的对象格式转换为数组格式 + const problemDistribution = res.data.problem_distribution + urgentProblemData.value = Object.entries(problemDistribution).map(([name, percentage]) => ({ + name: name, + value: parseInt(percentage.replace('%', '')) || 0 + })) + } + } catch (error) { + console.error('获取紧急问题数据失败:', error) + } finally { + isUrgentProblemLoading.value = false } } - // 时间线 async function getTimeline() { - const params = { - user_name: userStore.userInfo.username, - user_level: userStore.userInfo.user_level.toString(), - } - const res = await getCustomerAttendance(params) - if(res.code === 200) { - timelineData.value = res.data.customers_count - - // 处理客户列表数据 - if (res.data.customers_list) { - customersList.value = res.data.customers_list + isTimelineLoading.value = true + try { + const params = getRequestParams() + const hasParams = params.user_name + // 前6个阶段 + const res = await getCustomerAttendance(hasParams ? params : undefined) + if(res.code === 200) { + // 处理时间线数据 + if (res.data.timeline) { + timelineData.value = Object.entries(res.data.timeline).map(([name, count]) => ({ + name: name, + value: parseInt(count) || 0 + })) + } - // 将客户数据转换为统一格式的数组 - const processedCustomers = [] + // 处理客户列表数据 + if (res.data.all_customers_list) { + customersList.value = res.data.all_customers_list + } - Object.keys(res.data.customers_list).forEach(stage => { - const customers = res.data.customers_list[stage] - // 检查是否是课程数据(课1-4) - if (stage === '课1-4') { - // 单独处理课程客户数据 - if (Array.isArray(customers)) { - courseCustomers.value[stage] = customers.map((customer, index) => { - const stableId = `${stage}_${customer.scrm_user_main_code || customer.weChat_nickname || 'unknown'}_${index}`.replace(/[^a-zA-Z0-9_\u4e00-\u9fa5]/g, '_') - - return { - id: Math.abs(stableId.split('').reduce((a,b) => (((a << 5) - a) + b.charCodeAt(0))|0, 0)), - name: customer.weChat_nickname || customer.customer_name || '未知用户', - time: customer.records && customer.records.length > 0 ? customer.records[0].first_visit_time : '未知时间', - profession: customer.customer_occupation || '学员', - education: customer.customer_child_education || '未知', - avatar: customer.weChat_avatar ? customer.weChat_avatar.trim() : null, - salesStage: stage, - health: Math.floor(Math.random() * 100) + 1, - // 原始课程相关数据 - customer_name: customer.customer_name, - customer_occupation: customer.customer_occupation, - customer_child_education: customer.customer_child_education, - scrm_user_main_code: customer.scrm_user_main_code, - weChat_avatar: customer.weChat_avatar, - class_situation: customer.class_situation, - records: customer.records - } - }) - // 课程客户数据独立存储,不添加到总客户列表中 - } - } else { - // 处理普通销售阶段数据 - if (Array.isArray(customers)) { - customers.forEach((customer, index) => { - // 生成稳定的ID - const stableId = `${stage}_${customer.customer_name || 'unknown'}_${index}`.replace(/[^a-zA-Z0-9_\u4e00-\u9fa5]/g, '_') - - processedCustomers.push({ - id: Math.abs(stableId.split('').reduce((a,b) => (((a << 5) - a) + b.charCodeAt(0))|0, 0)), - name: customer.customer_name || '未知', - time: customer.format_update_time || '未知时间', - profession: customer.customer_occupation || '未知职业', - education: customer.customer_child_education || '未知学历', - avatar: customer.customer_avatar_url ? customer.customer_avatar_url.trim() : null, - salesStage: stage, - health: Math.floor(Math.random() * 100) + 1 - }) - }) - } + // 处理客户总数 + if (res.data.all_customers_count) { + customersCount.value = res.data.all_customers_count + } + + + } + // 后4个阶段 + const classRes = await getCustomerAttendanceAfterClass4(hasParams ? params : undefined) + if(classRes.code === 200) { + // 处理课1-4阶段的客户数据 + if (classRes.data.class_customers_list) { + // 存储课1-4阶段的原始数据,根据pay_status设置正确的type + courseCustomers.value['课1-4'] = classRes.data.class_customers_list.map(customer => { + let customerType = '课1-4'; // 默认类型 + + // 根据pay_status设置具体的type + if (customer.pay_status === '未支付' || customer.pay_status === '点击未支付') { + customerType = '点击未支付'; + } else if (customer.pay_status === '付定金') { + customerType = '付定金'; + } else if (customer.pay_status === '定金转化' || customer.pay_status === '已转化') { + customerType = '定金转化'; } + + return { + id: customer.customer_name, + name: customer.customer_name, + phone: customer.phone, + profession: customer.customer_occupation, + education: customer.customer_child_education, + avatar: customer.weChat_avatar || '/default-avatar.svg', + time: customer.messages_last_time, + type: customerType, // 根据pay_status设置的type + health: 75, // 默认健康度 + customer_name: customer.customer_name, + customer_occupation: customer.customer_occupation, + customer_child_education: customer.customer_child_education, + scrm_user_main_code: customer.scrm_user_main_code, + weChat_avatar: customer.weChat_avatar, + class_situation: customer.class_situation, + class_num: Object.keys(customer.class_situation || {}), // 添加class_num字段 + pay_status: customer.pay_status, + records: [] + }; }) - console.log('课程客户数据 (courseCustomers):', courseCustomers.value) - console.log('所有处理后的客户 (processedCustomers):', processedCustomers) - console.log('原始客户列表数据 (customersList):', customersList.value) - - // 更新 MOCK_DATA.contacts 为实际数据 - MOCK_DATA.contacts = processedCustomers + // 根据pay_status筛选后三个阶段的客户 + const allClassCustomers = classRes.data.class_customers_list + + // 点击未支付阶段 - 可以根据具体业务逻辑调整筛选条件 + const unpaidCustomers = allClassCustomers.filter(customer => + customer.pay_status === '未支付' || customer.pay_status === '点击未支付' + ) + // 付定金阶段 + const depositCustomers = allClassCustomers.filter(customer => + customer.pay_status === '付定金' + ) + // 定价转化阶段 - 可以根据具体业务逻辑调整筛选条件 + const conversionCustomers = allClassCustomers.filter(customer => + customer.pay_status === '定金转化' || customer.pay_status === '已转化' + ) + + // 将筛选后的客户添加到对应的customersList中 + const formatCustomerForList = (customer, type) => ({ + customer_name: customer.customer_name, + phone: customer.phone, + customer_occupation: customer.customer_occupation, + customer_child_education: customer.customer_child_education, + latest_message_time: customer.messages_last_time, + customer_avatar_url: customer.weChat_avatar, + type: type, + class_num: Object.keys(customer.class_situation || {}), + class_situation: customer.class_situation, + scrm_user_main_code: customer.scrm_user_main_code, + weChat_avatar: customer.weChat_avatar, + pay_status: customer.pay_status + }) + + // 将后三个阶段的客户添加到customersList中 + const additionalCustomers = [ + ...unpaidCustomers.map(customer => formatCustomerForList(customer, '点击未支付')), + ...depositCustomers.map(customer => formatCustomerForList(customer, '付定金')), + ...conversionCustomers.map(customer => formatCustomerForList(customer, '定金转化')) + ] + + // 合并到现有的customersList中 + customersList.value = [...customersList.value, ...additionalCustomers] + + + } } + // 成交阶段 + const payRes = await getPayMoneyCustomers(hasParams ? params : undefined) + if(payRes.code === 200) { + // 处理成交阶段的客户数据 + if (payRes.data.pay_money_customers_list) { + payMoneyCustomersList.value = payRes.data.pay_money_customers_list + } + + // 处理成交阶段客户总数 + if (payRes.data.pay_money_customers_count) { + payMoneyCustomersCount.value = payRes.data.pay_money_customers_count + } + + // 将成交阶段客户添加到总的客户列表中 + if (payRes.data.pay_money_customers_list) { + const payMoneyCustomers = payRes.data.pay_money_customers_list.map(customer => ({ + customer_name: customer.customer_name, + phone: customer.phone, + customer_occupation: customer.customer_occupation, + customer_child_education: customer.customer_child_education, + latest_message_time: customer.latest_message_time, + customer_avatar_url: customer.customer_avatar_url, + type: '成交' + })) + + // 合并到现有的customersList中 + customersList.value = [...customersList.value, ...payMoneyCustomers] + } + } + } catch (error) { + console.error('获取时间线数据失败:', error) + } finally { + isTimelineLoading.value = false } } // 获取客户表单 @@ -331,38 +464,36 @@ async function getCustomerForm() { return; } + const routeParams = getRequestParams() const params = { - user_name: userStore.userInfo.username, + user_name: routeParams.user_name || userStore.userInfo.username, customer_name: selectedContact.value.name, } - console.log('获取客户表单参数:', params); - console.log('选中的客户信息:', selectedContact.value); + try { const res = await getCustomerFormInfo(params) - console.log('客户表单API响应:', res) if(res.code === 200) { MOCK_DATA.formFields = res.data - console.log('客户表单数据已更新:', MOCK_DATA.formFields); - } else { - console.error('获取客户表单失败:', res.message || '未知错误'); + } } catch (error) { - console.error('获取客户表单请求失败:', error); + // 静默处理错误 } } // 为组件准备数据 const kpiData = computed(() => kpiDataState); // COMPUTED PROPERTIES const selectedContact = computed(() => { - return ( - MOCK_DATA.contacts.find((c) => c.id === selectedContactId.value) || null - ); + // 优先从API数据中查找 + if (formattedCustomersList.value.length > 0) { + return formattedCustomersList.value.find((c) => c.id === selectedContactId.value) || null; + } + // 否则从MOCK数据中查找 + return MOCK_DATA.contacts.find((c) => c.id === selectedContactId.value) || null; }); - - const funnelData = computed(() => ({ labels: ["线索", "沟通", "意向", "预约", "成交"], data: MOCK_DATA.personalFunnel, @@ -373,9 +504,52 @@ const contactTimeData = computed(() => ({ data: MOCK_DATA.contactTimeAnalysis.data, })); +// 格式化客户列表数据 +const formattedCustomersList = computed(() => { + if (!customersList.value || customersList.value.length === 0) { + return []; + } + + return customersList.value.map(customer => ({ + id: customer.customer_name, // 使用客户姓名作为唯一标识 + name: customer.customer_name, + phone: customer.phone, + profession: customer.customer_occupation, + education: customer.customer_child_education, + lastMessageTime: customer.latest_message_time, + avatarUrl: customer.customer_avatar_url, + type: customer.type, + classNum: customer.class_num, + class_num: customer.class_num, // 确保字段名一致 + // 添加一些默认值以兼容现有组件 + salesStage: customer.type || '待联系', + priority: customer.type === '待联系' ? 'high' : 'normal', + time: customer.latest_message_time || '未知' + })); +}); + // 根据选中阶段筛选联系人 const filteredContacts = computed(() => { - // 默认显示"全部"阶段的客户 + // 优先使用当前筛选的客户数据(来自阶段选择) + if (currentFilteredCustomers.value.length > 0) { + return currentFilteredCustomers.value; + } + + // 对于课1-4阶段,如果没有currentFilteredCustomers数据,返回空数组 + // 避免使用formattedCustomersList中type可能为undefined的数据 + if (selectedStage.value === '课1-4') { + return []; + } + + // 如果有API数据,使用API数据 + if (formattedCustomersList.value.length > 0) { + if (selectedStage.value === 'all' || selectedStage.value === '全部') { + return formattedCustomersList.value; + } + return formattedCustomersList.value.filter(contact => contact.salesStage === selectedStage.value); + } + + // 否则使用MOCK数据 if (selectedStage.value === 'all' || selectedStage.value === '全部') { return MOCK_DATA.contacts.filter(contact => contact.salesStage === '全部'); } @@ -399,25 +573,35 @@ const selectContact = (id) => { }); }; +// 存储当前筛选的客户数据 +const currentFilteredCustomers = ref([]); + // 处理时间线阶段选择 const handleStageSelect = (stage, extraData = null) => { selectedStage.value = stage; - // 如果是课1-4阶段,处理课程数据 - if (extraData && extraData.isCourseStage) { - console.log('处理课1-4阶段选择,课程数据:', extraData.courseData); + // 如果传递了筛选后的客户数据,使用这些数据 + if (extraData && extraData.filteredCustomers) { + - // 将课程数据转换为contacts格式并更新显示 - const courseContacts = extraData.courseData.map(customer => ({ - id: customer.id, - name: customer.name, - time: customer.time, - profession: customer.profession, - education: customer.education, - avatar: customer.avatar, - salesStage: '课1-4', - health: customer.health, - // 保留课程相关的原始数据 + // 将筛选后的客户数据转换为contacts格式 + const filteredContacts = extraData.filteredCustomers.map(customer => ({ + id: customer.customer_name || customer.id, + name: customer.customer_name || customer.name, + phone: customer.phone, + profession: customer.customer_occupation || customer.profession, + education: customer.customer_child_education || customer.education, + lastMessageTime: customer.latest_message_time || customer.time, + avatarUrl: customer.customer_avatar_url || customer.avatar, + avatar: customer.customer_avatar_url || customer.avatar || '/default-avatar.svg', + type: customer.type, + classNum: customer.class_num, + class_num: customer.class_num, // 确保字段名一致 + salesStage: customer.type || stage, + priority: customer.type === '待联系' ? 'high' : 'normal', + time: customer.latest_message_time || customer.time || '未知', + health: customer.health || 75, + // 保留原始数据 customer_name: customer.customer_name, customer_occupation: customer.customer_occupation, customer_child_education: customer.customer_child_education, @@ -427,43 +611,80 @@ const handleStageSelect = (stage, extraData = null) => { records: customer.records })); - // 临时更新MOCK_DATA.contacts以显示课程客户 - MOCK_DATA.contacts = [...MOCK_DATA.contacts.filter(c => c.salesStage !== '课1-4'), ...courseContacts]; + // 更新当前筛选的客户数据 + currentFilteredCustomers.value = filteredContacts; - console.log('课1-4客户数据已更新到contacts:', courseContacts); + + } else if (extraData && extraData.isCourseStage) { + // 处理课1-4阶段的课程数据(保持原有逻辑) + + + const courseContacts = extraData.courseData.map(customer => ({ + id: customer.id, + name: customer.name, + time: customer.time, + profession: customer.profession, + education: customer.education, + avatar: customer.avatar, + type: customer.type || '课1-4', // 保持原有type字段,如果没有则默认为课1-4 + salesStage: customer.type || '课1-4', // 使用customer.type作为salesStage + health: customer.health, + customer_name: customer.customer_name, + customer_occupation: customer.customer_occupation, + customer_child_education: customer.customer_child_education, + scrm_user_main_code: customer.scrm_user_main_code, + weChat_avatar: customer.weChat_avatar, + class_situation: customer.class_situation, + class_num: customer.class_num, // 添加class_num字段 + pay_status: customer.pay_status, // 添加pay_status字段 + records: customer.records + })); + + currentFilteredCustomers.value = courseContacts; + + } else { + // 如果没有额外数据,清空筛选结果,使用默认逻辑 + currentFilteredCustomers.value = []; } }; -// 处理查看表单数据 const handleViewFormData = (contact) => { - console.log('查看表单数据:', contact); - // 这里可以添加打开表单数据弹窗的逻辑 + // TODO: 实现表单数据查看逻辑 }; -// 处理查看聊天记录 const handleViewChatData = (contact) => { - console.log('查看聊天记录:', contact); - // 这里可以添加打开聊天记录弹窗的逻辑 + // TODO: 实现聊天记录查看逻辑 }; -// 处理查看通话录音 const handleViewCallData = (contact) => { - console.log('查看通话录音:', contact); - // 这里可以添加打开通话录音弹窗的逻辑 + // TODO: 实现通话录音查看逻辑 }; // LIFECYCLE HOOKS onMounted(async () => { - await getCoreKpi() - await getStatisticsData() - await getUrgentProblem() + try { + isPageLoading.value = true await getTimeline() - // 默认选择第一个今日必须联系人 - const highPriorityContacts = MOCK_DATA.contacts.filter( - (contact) => contact.priority === "high" - ); - if (highPriorityContacts.length > 0) { - selectedContactId.value = highPriorityContacts[0].id; + + // 等待数据加载完成后选择默认客户 + await nextTick(); + + // 优先从API数据中选择第一个客户 + if (formattedCustomersList.value.length > 0) { + selectedContactId.value = formattedCustomersList.value[0].id; + } else { + // 否则选择MOCK数据中的第一个高优先级联系人 + const highPriorityContacts = MOCK_DATA.contacts.filter( + (contact) => contact.priority === "high" + ); + if (highPriorityContacts.length > 0) { + selectedContactId.value = highPriorityContacts[0].id; + } + } + } catch (error) { + // 静默处理错误 + } finally { + isPageLoading.value = false } }); @@ -473,7 +694,7 @@ body { margin: 0; padding: 0; } -// Color Palette +// Color Variables $slate-50: #f8fafc; $slate-100: #f1f5f9; $slate-200: #e2e8f0; @@ -484,247 +705,32 @@ $slate-600: #475569; $slate-700: #334155; $slate-800: #1e293b; $white: #ffffff; +$primary: #3b82f6; -$blue: #3b82f6; -$green: #22c55e; -$amber: #f59e0b; -$red: #ef4444; -$indigo: #4f46e5; -$purple: #a855f7; - -// 移动端全局优化 -@media (max-width: 768px) { - // 优化触摸目标大小 - * { - -webkit-tap-highlight-color: transparent; - } - - // 优化滚动性能 - .sidebar, - .detail-section, - .analytics-section-full { - -webkit-overflow-scrolling: touch; - scroll-behavior: smooth; - } - - // 防止横向滚动 - body { - overflow-x: hidden; +// 响应式混合器 +@mixin mobile { + @media (max-width: 768px) { + @content; } } -// 超小屏幕优化 -@media (max-width: 480px) { - // 减少动画以提升性能 - * { - transition-duration: 0.1s !important; - } - - // 优化字体渲染 - body { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - - .sales-dashboard { - padding: 0.25rem; - gap: 0.5rem; - } - - .analytics-section-full { - .section-header { - padding: 0.5rem; - - .app-title { - font-size: 1.125rem; - } - - .quick-stats { - flex-direction: column; - gap: 0.25rem !important; - - .quick-stat-item { - .stat-label { - font-size: 0.6875rem; - } - } - } - - .header-ringht { - .avatar { - width: 25px !important; - height: 25px !important; - } - - span { - font-size: 0.75rem; - } - } - } - } - - .timeline-section { - .section-header { - padding: 0.5rem; - - h2 { - font-size: 0.875rem; - } - - .section-subtitle { - font-size: 0.6875rem; - } - } - } - - .sidebar { - .sidebar-header { - padding: 0.5rem; - - h2 { - font-size: 0.875rem; - } - - .task-summary { - .task-count { - font-size: 1.25rem; - } - - .task-label { - font-size: 0.75rem; - } - } - } - - .sidebar-content { - padding: 0.5rem; - } - } - - .detail-section { - .section-header { - padding: 0.5rem; - - h2 { - font-size: 0.875rem; - } - - .section-actions { - .action-btn { - padding: 0.25rem 0.5rem; - font-size: 0.6875rem; - } - } - } +@mixin tablet { + @media (max-width: 1023px) and (min-width: 769px) { + @content; } } -/* 移动端优化 */ -@media (max-width: 768px) { - .sales-dashboard { - padding: 0.5rem; - gap: 0.75rem; - } - - .main-layout { - flex-direction: column; - gap: 1rem; - } - - .sidebar { - width: 100%; - min-height: auto; - order: 2; /* 在移动端将侧边栏放到主内容下方 */ - - .sidebar-header { - padding: 0.75rem 1rem; - - h2 { - font-size: 1rem; - } - } - - .sidebar-content { - padding: 0.75rem; - } - } - - .main-content { - width: 100%; - order: 1; /* 在移动端将主内容放到侧边栏上方 */ - } - - .detail-section { - .section-header { - padding: 0.75rem 1rem; - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; - - h2 { - font-size: 1rem; - } - - .section-actions { - width: 100%; - justify-content: flex-end; - - .action-btn { - padding: 0.375rem 0.75rem; - font-size: 0.75rem; - } - } - } - } - - .analytics-section-full { - .section-header { - padding: 0.75rem 1rem; - flex-direction: column; - align-items: flex-start; - gap: 0.75rem; - - .app-title { - font-size: 1.25rem; - } - - .quick-stats { - width: 100%; - flex-wrap: wrap; - gap: 0.5rem !important; - - .quick-stat-item { - .stat-label { - font-size: 0.75rem; - } - } - } - - .header-ringht { - width: 100%; - justify-content: flex-end; - - .avatar { - width: 30px !important; - height: 30px !important; - } - - span { - font-size: 0.875rem; - } - } - } +@mixin desktop { + @media (min-width: 1024px) { + @content; } } -// Base Styles .sales-dashboard { background: #ffffff; min-height: 100vh; color: $slate-800; - font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", - Arial, "Noto Sans", sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; display: flex; flex-direction: column; align-items: center; @@ -732,375 +738,20 @@ $purple: #a855f7; padding: 0; margin: 0; box-sizing: border-box; + font-size: 16px; + line-height: 1.6; - // PC端保持一致布局 - @media (min-width: 1024px) { - font-size: 16px; - line-height: 1.6; - padding: 0; - } - - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - font-size: 15px; - line-height: 1.55; - padding: 0; - } - - // 移动端适配 - @media (max-width: 768px) { + @include mobile { font-size: 14px; line-height: 1.5; - padding: 0; } - // 小屏移动端适配 - @media (max-width: 480px) { - font-size: 13px; - line-height: 1.4; - padding: 0; - } - - // 确保所有子元素都使用border-box *, *::before, *::after { box-sizing: border-box; } } -// 顶部导航栏 -.top-navbar { - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(20px); - border-bottom: 1px solid rgba(255, 255, 255, 0.2); - position: sticky; - top: 0; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); - width: 100%; - z-index: 1000; - // PC端保持一致布局 - @media (min-width: 1024px) { - width: 100%; - margin: 0; - } - - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - width: 100%; - margin: 0; - } - - // 移动端适配 - @media (max-width: 768px) { - width: 100%; - margin: 0; - } - - // 小屏移动端适配 - @media (max-width: 480px) { - width: 100%; - margin: 0; - } - .navbar-content { - max-width: 1400px; - margin: 0 auto; - padding: 0 2rem; - display: flex; - justify-content: space-between; - align-items: center; - height: 70px; - - // PC端保持一致布局 - @media (min-width: 1024px) { - padding: 0 2.5rem; - height: 75px; - } - - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - padding: 0 2rem; - height: 70px; - } - - // 移动端适配 - @media (max-width: 768px) { - padding: 0 1rem; - height: 60px; - } - - // 小屏移动端适配 - @media (max-width: 480px) { - padding: 0 0.5rem; - height: 55px; - } - } - - .navbar-left { - display: flex; - align-items: center; - gap: 2rem; - - @media (max-width: 768px) { - gap: 1rem; - } - - @media (max-width: 480px) { - gap: 0.75rem; - } - - .app-title { - font-size: 1.5rem; - font-weight: 700; - background: linear-gradient(135deg, #667eea, #764ba2); - -webkit-text-fill-color: transparent; - margin: 0; - - // PC端保持一致布局 - @media (min-width: 1024px) { - font-size: 1.75rem; - } - - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - font-size: 1.5rem; - } - - // 移动端适配 - @media (max-width: 768px) { - font-size: 1.25rem; - } - - // 小屏移动端适配 - @media (max-width: 480px) { - font-size: 1.125rem; - } - } - - .breadcrumb { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.875rem; - color: $slate-500; - - @media (max-width: 480px) { - font-size: 0.75rem; - gap: 0.25rem; - } - - .separator { - color: $slate-400; - } - - .current { - color: $slate-700; - font-weight: 500; - } - } - } - - .navbar-right { - display: flex; - align-items: center; - gap: 2rem; - - @media (max-width: 768px) { - gap: 1rem; - } - - @media (max-width: 480px) { - gap: 0.75rem; - } - - .quick-stats { - display: flex; - gap: 1.5rem; - - // PC端保持一致布局 - @media (min-width: 1024px) { - gap: 2rem; - flex-wrap: nowrap; - } - - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - gap: 1.5rem; - flex-wrap: nowrap; - } - - // 移动端适配 - @media (max-width: 768px) { - gap: 1rem; - flex-wrap: wrap; - } - - // 小屏移动端适配 - @media (max-width: 480px) { - gap: 0.5rem; - flex-wrap: wrap; - } - - .quick-stat-item { - text-align: center; - - @media (max-width: 480px) { - min-width: 50px; - } - - .stat-value { - font-size: 1.25rem; - font-weight: 700; - color: $slate-800; - line-height: 1; - - // PC端保持一致布局 - @media (min-width: 1024px) { - font-size: 1.375rem; - } - - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - font-size: 1.25rem; - } - - // 移动端适配 - @media (max-width: 768px) { - font-size: 1.125rem; - } - - // 小屏移动端适配 - @media (max-width: 480px) { - font-size: 1rem; - } - } - - .stat-label { - font-size: 0.75rem; - color: $slate-500; - margin-top: 2px; - - // PC端保持一致布局 - @media (min-width: 1024px) { - font-size: 0.8125rem; - } - - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - font-size: 0.75rem; - } - - // 移动端适配 - @media (max-width: 768px) { - font-size: 0.6875rem; - } - - // 小屏移动端适配 - @media (max-width: 480px) { - font-size: 0.625rem; - } - } - } - } - - .user-actions { - display: flex; - align-items: center; - gap: 1rem; - - @media (max-width: 768px) { - gap: 0.75rem; - } - - @media (max-width: 480px) { - gap: 0.5rem; - } - - .action-btn { - padding: 0.5rem 1rem; - border: 1px solid $slate-300; - border-radius: 0.5rem; - background: $white; - color: $slate-700; - font-size: 0.875rem; - cursor: pointer; - transition: all 0.2s; - touch-action: manipulation; - white-space: nowrap; - - // PC端保持一致布局 - @media (min-width: 1024px) { - padding: 0.625rem 1.25rem; - font-size: 0.875rem; - } - - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - padding: 0.5rem 1rem; - font-size: 0.875rem; - } - - // 移动端适配 - @media (max-width: 768px) { - padding: 0.375rem 0.75rem; - font-size: 0.8125rem; - } - - // 小屏移动端适配 - @media (max-width: 480px) { - padding: 0.25rem 0.5rem; - font-size: 0.75rem; - } - - &:hover { - background: $slate-50; - border-color: $slate-400; - } - } - - .user-avatar { - width: 36px; - height: 36px; - border-radius: 50%; - background: linear-gradient(135deg, #667eea, #764ba2); - color: white; - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - cursor: pointer; - touch-action: manipulation; - - // PC端保持一致布局 - @media (min-width: 1024px) { - width: 40px; - height: 40px; - font-size: 1rem; - } - - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - width: 36px; - height: 36px; - font-size: 0.9375rem; - } - - // 移动端适配 - @media (max-width: 768px) { - width: 32px; - height: 32px; - font-size: 0.875rem; - } - - // 小屏移动端适配 - @media (max-width: 480px) { - width: 28px; - height: 28px; - font-size: 0.75rem; - } - } - } - } -} // 主要布局 .main-layout { @@ -1110,31 +761,21 @@ $purple: #a855f7; display: flex; flex-direction: column; gap: 1rem; - // min-height: calc(100vh - 70px); - // PC端保持一致布局 - @media (min-width: 1024px) { - width: 100vw; - // max-width: 1400px; - // padding: 1.5rem; + @include desktop { gap: 1.5rem; } - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { + @include tablet { width: 90vw; - padding: 1rem; - gap: 1rem; } - // 移动端适配 - @media (max-width: 768px) { + @include mobile { width: 95vw; padding: 0.75rem; gap: 0.75rem; } - // 小屏移动端适配 @media (max-width: 480px) { width: 98vw; padding: 0.5rem; @@ -1155,28 +796,22 @@ $purple: #a855f7; display: flex; flex-direction: column; - // PC端保持一致布局 - @media (min-width: 1024px) { - border-radius: 1rem; + @include desktop { max-height: 450px; - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); } - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { + @include tablet { border-radius: 0.875rem; max-height: 420px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); } - // 移动端适配 - @media (max-width: 768px) { + @include mobile { border-radius: 0.75rem; max-height: calc(100vh - 120px); box-shadow: 0 6px 24px rgba(0, 0, 0, 0.06); } - // 小屏移动端适配 @media (max-width: 480px) { border-radius: 0.5rem; max-height: calc(100vh - 100px); @@ -1195,22 +830,14 @@ $purple: #a855f7; align-items: center; justify-content: space-between; - // PC端保持一致布局 - @media (min-width: 1024px) { + @include desktop { padding: 1.75rem; } - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - padding: 1.5rem; - } - - // 移动端适配 - @media (max-width: 768px) { + @include mobile { padding: 1.25rem; } - // 小屏移动端适配 @media (max-width: 480px) { padding: 1rem; } @@ -1221,22 +848,14 @@ $purple: #a855f7; font-weight: 600; color: $slate-800; - // PC端保持一致布局 - @media (min-width: 1024px) { + @include desktop { font-size: 1.25rem; } - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - font-size: 1.125rem; - } - - // 移动端适配 - @media (max-width: 768px) { + @include mobile { font-size: 1rem; } - // 小屏移动端适配 @media (max-width: 480px) { font-size: 0.9375rem; } @@ -1252,22 +871,14 @@ $purple: #a855f7; font-weight: 700; color: #667eea; - // PC端保持一致布局 - @media (min-width: 1024px) { + @include desktop { font-size: 1.75rem; } - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - font-size: 1.5rem; - } - - // 移动端适配 - @media (max-width: 768px) { + @include mobile { font-size: 1.375rem; } - // 小屏移动端适配 @media (max-width: 480px) { font-size: 1.25rem; } @@ -1277,23 +888,11 @@ $purple: #a855f7; font-size: 0.875rem; color: $slate-500; - // PC端保持一致布局 - @media (min-width: 1024px) { + @include desktop { font-size: 0.9375rem; } - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - font-size: 0.875rem; - } - - // 移动端适配 - @media (max-width: 768px) { - font-size: 0.8125rem; - } - - // 小屏移动端适配 - @media (max-width: 480px) { + @include mobile { font-size: 0.8125rem; } } @@ -1312,22 +911,14 @@ $purple: #a855f7; flex-direction: column; gap: 2rem; - // PC端保持一致布局 - @media (min-width: 1024px) { + @include desktop { gap: 2.5rem; } - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - gap: 2rem; - } - - // 移动端适配 - @media (max-width: 768px) { + @include mobile { gap: 1.5rem; } - // 小屏移动端适配 @media (max-width: 480px) { gap: 1rem; } @@ -1340,25 +931,16 @@ $purple: #a855f7; border: 1px solid rgba(255, 255, 255, 0.2); overflow: hidden; - // PC端保持一致布局 - @media (min-width: 1024px) { - border-radius: 1rem; - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); - } - - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { + @include tablet { border-radius: 0.875rem; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); } - // 移动端适配 - @media (max-width: 768px) { + @include mobile { border-radius: 0.75rem; box-shadow: 0 6px 24px rgba(0, 0, 0, 0.06); } - // 小屏移动端适配 @media (max-width: 480px) { border-radius: 0.5rem; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04); @@ -1376,31 +958,26 @@ $purple: #a855f7; rgba(118, 75, 162, 0.05) ); - // PC端保持一致布局 - @media (min-width: 1024px) { + @include desktop { padding: 1.75rem; flex-direction: row; align-items: center; gap: 2rem; } - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - padding: 1.5rem; + @include tablet { flex-direction: row; align-items: center; gap: 1.5rem; } - // 移动端适配 - @media (max-width: 768px) { + @include mobile { padding: 1.25rem; flex-direction: column; gap: 1rem; align-items: flex-start; } - // 小屏移动端适配 @media (max-width: 480px) { padding: 1rem; gap: 0.75rem; @@ -1412,22 +989,14 @@ $purple: #a855f7; font-weight: 600; color: $slate-800; - // PC端保持一致布局 - @media (min-width: 1024px) { + @include desktop { font-size: 1.25rem; } - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - font-size: 1.125rem; - } - - // 移动端适配 - @media (max-width: 768px) { + @include mobile { font-size: 1rem; } - // 小屏移动端适配 @media (max-width: 480px) { font-size: 0.9375rem; } @@ -1583,28 +1152,18 @@ $purple: #a855f7; margin-bottom: 1.5rem; width: 100%; - // PC端保持一致布局 - @media (min-width: 1024px) { - border-radius: 1rem; - margin-bottom: 1.5rem; - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); - } - - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { + @include tablet { border-radius: 0.875rem; margin-bottom: 1.25rem; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); } - // 移动端适配 - @media (max-width: 768px) { + @include mobile { border-radius: 0.75rem; margin-bottom: 1rem; box-shadow: 0 6px 24px rgba(0, 0, 0, 0.06); } - // 小屏移动端适配 @media (max-width: 480px) { border-radius: 0.5rem; margin-bottom: 0.75rem; @@ -1684,28 +1243,22 @@ $purple: #a855f7; width: 100%; min-height: 400px; - // PC端保持一致布局 - @media (min-width: 1024px) { + @include desktop { min-height: 450px; - border-radius: 1rem; - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); } - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { + @include tablet { min-height: 380px; border-radius: 0.875rem; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); } - // 移动端适配 - @media (max-width: 768px) { + @include mobile { border-radius: 0.75rem; min-height: 320px; box-shadow: 0 6px 24px rgba(0, 0, 0, 0.06); } - // 小屏移动端适配 @media (max-width: 480px) { border-radius: 0.5rem; min-height: 280px; @@ -1776,28 +1329,24 @@ $purple: #a855f7; border-radius: 0.5rem; padding: 0.25rem; - // PC端保持一致布局 - @media (min-width: 1024px) { + @include desktop { width: auto; flex-wrap: nowrap; justify-content: flex-start; } - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { + @include tablet { width: auto; flex-wrap: nowrap; justify-content: flex-start; } - // 移动端适配 - @media (max-width: 768px) { + @include mobile { width: 100%; justify-content: space-between; flex-wrap: nowrap; } - // 小屏移动端适配 @media (max-width: 480px) { flex-wrap: wrap; gap: 0.25rem; @@ -1816,28 +1365,22 @@ $purple: #a855f7; touch-action: manipulation; white-space: nowrap; - // PC端保持一致布局 - @media (min-width: 1024px) { + @include desktop { padding: 0.5rem 1.25rem; - font-size: 0.875rem; flex: none; } - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { + @include tablet { padding: 0.5rem 1rem; - font-size: 0.875rem; flex: none; } - // 移动端适配 - @media (max-width: 768px) { + @include mobile { padding: 0.375rem 0.75rem; font-size: 0.8125rem; flex: 1; } - // 小屏移动端适配 @media (max-width: 480px) { padding: 0.25rem 0.5rem; font-size: 0.75rem; @@ -1861,26 +1404,48 @@ $purple: #a855f7; .section-content { padding: 1.5rem; - // PC端保持一致布局 - @media (min-width: 1024px) { + @include desktop { padding: 1.75rem; } - // 平板端适配 - @media (max-width: 1023px) and (min-width: 769px) { - padding: 1.5rem; - } - - // 移动端适配 - @media (max-width: 768px) { + @include mobile { padding: 1.25rem; } - // 小屏移动端适配 @media (max-width: 480px) { padding: 1rem; } } + + // 局部加载状态样式 + .section-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 3rem; + min-height: 200px; + + .loading-spinner { + width: 40px; + height: 40px; + border: 3px solid #e2e8f0; + border-top: 3px solid #3b82f6; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 1rem; + } + + .loading-text { + color: #64748b; + font-size: 0.9rem; + } + } + + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } } diff --git a/my-vue-app/src/views/senorManger/seniorManager.vue b/my-vue-app/src/views/senorManger/seniorManager.vue index a90cace..84d0ed9 100644 --- a/my-vue-app/src/views/senorManger/seniorManager.vue +++ b/my-vue-app/src/views/senorManger/seniorManager.vue @@ -60,7 +60,6 @@
正在加载团队详情...
-