feat: 初始化Vue3项目并添加核心功能模块

新增项目基础结构,包括Vue3、Pinia、Element Plus等核心依赖
添加路由配置和用户认证状态管理
实现销售数据看板、客户画像、团队管理等核心功能模块
集成图表库和API请求工具,完成基础样式配置
This commit is contained in:
2025-08-12 14:34:44 +08:00
commit f93236ab36
71 changed files with 32821 additions and 0 deletions

View File

@@ -0,0 +1,349 @@
<template>
<div class="overview-container">
<!-- 加载状态 -->
<div v-if="isLoading" class="state-container">
<div class="spinner"></div>
<p>正在加载数据...</p>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="state-container">
<p class="error-text">数据加载失败{{ error }}</p>
<button @click="fetchData" class="retry-button">重试</button>
</div>
<!-- 成功状态显示数据网格 -->
<div v-else class="kpi-grid">
<!-- 1. 主卡片中心总业绩 -->
<div class="kpi-card primary">
<div class="card-header">
<span class="card-label">总成交单数</span>
<span class="card-trend" :class="getTrendClass(kpiData.totalSales.trend)">
{{ formatTrend(kpiData.totalSales.trend) }} vs 上期
</span>
</div>
<div class="card-body">
<span class="card-value">{{ formatNumber(kpiData.totalSales.value) }}</span>
<span class="card-unit">单数</span>
</div>
<div class="card-footer">
月目标完成率{{ kpiData.totalSales.targetCompletion }}%
</div>
</div>
<!-- 2. 活跃组数 -->
<div class="kpi-card">
<div class="card-header">
<span class="card-label">定金转化率</span>
<span class="card-trend" :class="getTrendClass(kpiData.activeTeams.trend)">
{{ formatTrend(kpiData.activeTeams.trend, true) }} vs 上期
</span>
</div>
<div class="card-body">
<span class="card-value">{{ kpiData.activeTeams.value }}</span>
<span class="card-unit"></span>
</div>
<div class="card-footer">
总人数{{ kpiData.activeTeams.totalMembers }}
</div>
</div>
<!-- 3. 总通话次数 -->
<div class="kpi-card">
<div class="card-header">
<span class="card-label">总通话次数</span>
<span class="card-trend" :class="getTrendClass(kpiData.totalCalls.trend)">
{{ formatTrend(kpiData.totalCalls.trend) }} vs 上期
</span>
</div>
<div class="card-body">
<span class="card-value">{{ formatNumber(kpiData.totalCalls.value) }}</span>
<span class="card-unit"></span>
</div>
<div class="card-footer">
有效通话{{ formatNumber(kpiData.totalCalls.effectiveCalls) }}
</div>
</div>
<!-- 4. 新增客户 -->
<div class="kpi-card">
<div class="card-header">
<span class="card-label">新增客户</span>
<span class="card-trend" :class="getTrendClass(kpiData.newCustomers.trend)">
{{ formatTrend(kpiData.newCustomers.trend) }} vs 上期
</span>
</div>
<div class="card-body">
<span class="card-value">{{ formatNumber(kpiData.newCustomers.value) }}</span>
<span class="card-unit"></span>
</div>
<div class="card-footer">
意向客户{{ formatNumber(kpiData.newCustomers.interestedCustomers) }}
</div>
</div>
<!-- 5. 中心转化率 -->
<div class="kpi-card">
<div class="card-header">
<span class="card-label">平均转化率</span>
<span class="card-trend" :class="getTrendClass(kpiData.conversionRate.trend)">
{{ formatTrend(kpiData.conversionRate.trend, true) }} vs 上期
</span>
</div>
<div class="card-body">
<span class="card-value">{{ kpiData.conversionRate.value }}</span>
<span class="card-unit">%</span>
</div>
<div class="card-footer">
行业平均{{ kpiData.conversionRate.industryAvg }}%
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// 1. 定义内部响应式状态
const kpiData = ref({
// 定义一个骨架结构,防止模板在初始渲染时出错
totalSales: {},
activeTeams: {},
conversionRate: {},
totalCalls: {},
newCustomers: {},
});
const isLoading = ref(true); // 加载状态默认为true
const error = ref(null); // 错误状态默认为null
// 2. 模拟从API获取数据的函数
async function fetchData() {
isLoading.value = true;
error.value = null;
try {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1500));
// 模拟成功获取的数据。在实际应用中,这里会是 fetch() 或 axios.get()
const responseData = {
totalSales: { value: 552000, trend: 12, targetCompletion: 56 },
activeTeams: { value: 5, total: 5, totalMembers: 40 },
conversionRate: { value: 5.2, trend: 0.3, industryAvg: 4.8 },
totalCalls: { value: 1247, trend: -8, effectiveCalls: 892 }, // 示例:负向趋势
newCustomers: { value: 117, trend: 15, interestedCustomers: 89 },
};
// 更新组件的内部数据
kpiData.value = responseData;
} catch (e) {
// 如果发生错误,更新错误状态
error.value = e.message || '未知错误';
} finally {
// 无论成功或失败,最后都设置加载完成
isLoading.value = false;
}
}
// 3. 在组件挂载后调用数据获取函数
onMounted(() => {
fetchData();
});
// --- 以下是辅助函数,保持不变 ---
// 格式化数字,添加千位分隔符
function formatNumber(num) {
if (num == null) return '0';
return num.toLocaleString();
}
// 根据趋势值返回 'positive' 或 'negative' 类
function getTrendClass(trend) {
if (trend > 0) return 'positive';
if (trend < 0) return 'negative';
return 'neutral';
}
// 格式化趋势百分比,自动添加 '+'
function formatTrend(trend, isPercentagePoint = false) {
if (trend == null) return '';
const sign = trend > 0 ? '+' : '';
const unit = isPercentagePoint ? '' : '%';
return `${sign}${trend}${unit}`;
}
</script>
<style scoped>
/* 可将此背景色应用到页面body或父容器 */
.overview-container {
background-color: #ffffff;
padding: 0.5rem;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
max-height: 350px; /* 给容器一个最小高度以容纳加载/错误状态 */
border-radius: 8px;
}
/* --- 新增:加载和错误状态的样式 --- */
.state-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 200px;
color: #86909c;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #3a7afe;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.error-text {
color: #f53f3f;
margin-bottom: 16px;
}
.retry-button {
background-color: #3a7afe;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.retry-button:hover {
background-color: #2f68ee;
}
/* --- 以下是卡片网格样式,保持不变 --- */
.kpi-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.kpi-card {
background-color: #ffffff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
gap: 8px;
transition: all 0.2s ease-in-out;
}
.kpi-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08);
}
.kpi-card.primary {
grid-column: 1 / 3;
background: linear-gradient(135deg, #3a7afe, #2f68ee);
color: #ffffff;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-label {
font-size: 14px;
color: #86909c;
}
.primary .card-label {
color: rgba(255, 255, 255, 0.8);
}
.card-trend {
font-size: 14px;
font-weight: 500;
}
.card-trend.positive { color: #00b42a; }
.card-trend.negative { color: #f53f3f; }
.primary .card-trend { color: #ffffff; }
.card-secondary-info {
font-size: 14px;
color: #4e5969;
font-weight: 500;
}
.card-body {
display: flex;
align-items: baseline;
margin-top: 4px;
margin-bottom: 4px;
}
.card-value {
font-size: 36px;
font-weight: 700;
color: #1d2129;
line-height: 1.2;
}
.primary .card-value {
font-size: 48px;
color: #ffffff;
}
.card-unit {
font-size: 16px;
font-weight: 500;
color: #1d2129;
margin-left: 8px;
}
.primary .card-unit {
font-size: 20px;
color: #ffffff;
}
.card-footer {
font-size: 12px;
color: #86909c;
}
.primary .card-footer {
color: rgba(255, 255, 255, 0.8);
}
@media (max-width: 1200px) {
.kpi-grid {
grid-template-columns: repeat(2, 1fr);
}
.kpi-card.primary {
grid-column: 1 / 3;
}
}
@media (max-width: 768px) {
.kpi-grid {
grid-template-columns: 1fr;
}
.kpi-card.primary {
grid-column: auto;
}
.primary .card-value {
font-size: 40px;
}
}
</style>