From 5de287e777cf0fd278aedb3357d8c23ef3086c14 Mon Sep 17 00:00:00 2001
From: lbw_9527443 <780139497@qq.com>
Date: Wed, 13 Aug 2025 20:33:13 +0800
Subject: [PATCH] =?UTF-8?q?feat(=E9=94=80=E5=94=AE=E6=97=B6=E9=97=B4?=
=?UTF-8?q?=E7=BA=BF):=20=E9=87=8D=E6=9E=84=E9=94=80=E5=94=AE=E6=97=B6?=
=?UTF-8?q?=E9=97=B4=E7=BA=BF=E7=BB=84=E4=BB=B6=E5=B9=B6=E4=BC=98=E5=8C=96?=
=?UTF-8?q?API=E8=B0=83=E7=94=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
重构销售时间线组件,增加对客户阶段数据的动态计算和展示
优化API端点配置,新增课1-4后阶段和成交客户的数据接口
调整样式和布局,增加客户类型标签显示
延长HTTP请求超时时间至100秒
---
my-vue-app/src/api/api.js | 23 +-
my-vue-app/src/utils/https.js | 4 +-
.../components/SalesTimelineWithTaskList.vue | 368 ++---
my-vue-app/src/views/person/sale.vue | 1365 ++++++-----------
.../src/views/senorManger/seniorManager.vue | 1 -
5 files changed, 619 insertions(+), 1142 deletions(-)
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 @@
+
+
+
@@ -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 @@
正在加载团队详情...
-