企迹(qiji) 政企周报管理系统 — v0.1

后端: FastAPI + SQLAlchemy 2.0 (async) + Alembic + MinIO + Casdoor + 企微
前端: Vue 3 + Vite + TypeScript + Element Plus + Pinia

功能清单:
- 8 张数据表自动建表 / Casdoor OIDC 登录 / 企微静默登录
- 双布局: 移动端(填报) + PC端(汇总管理)
- 拜访记录 CRUD + MinIO 照片直传 + 缩略图预览 + 同访人草稿
- 今日纪要 (6 分类) / 工作计划 / 小微商机 / 要客拜访 CRUD
- 客户档案: 备注/收支费用/联系人/归属分配/批量转移
- 客户导入导出 + 模板下载 + 搜索/分页/筛选
- 仪表盘: 四卡统计 + 填报进度 (拜访+纪要双维度)
- 周报详情: 五 Tab + 按人/客户筛选 + 时间轴
- 用户管理 / 客户经理 PC 端工作台
- 企微: 催办/公告/定时提醒 / 时区修正
- Docker 部署配置

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-23 01:35:46 +08:00
parent 2a8225c31f
commit a1886074dd
97 changed files with 8830 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { dashboardApi } from '@/api/dashboard'
import { ElMessage } from 'element-plus'
const router = useRouter()
const auth = useAuthStore()
const loading = ref(false)
const stats = ref({ week_visits: 0, work_plans: 0, mini_business: 0, key_visits: 0, week_start: '', week_end: '' })
const progress = ref<any[]>([])
onMounted(async () => {
loading.value = true
try {
const [sRes, pRes] = await Promise.all([
dashboardApi.getStats(),
dashboardApi.getProgress(),
])
stats.value = sRes.data
progress.value = pRes.data
} catch (e: any) {
ElMessage.error('加载仪表盘失败')
} finally {
loading.value = false
}
})
function goWeeklyReport() {
router.push('/weekly-report')
}
</script>
<template>
<div class="dashboard" v-loading="loading">
<h3>仪表盘</h3>
<p class="week-range">{{ stats.week_start }} ~ {{ stats.week_end }}</p>
<!-- Stats Cards -->
<el-row :gutter="16" style="margin-bottom: 24px">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value">{{ stats.week_visits }}</div>
<div class="stat-label">本周拜访次数</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value">{{ stats.work_plans }}</div>
<div class="stat-label">工作计划条数</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value">{{ stats.mini_business }}</div>
<div class="stat-label">商机跟单条数</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value">{{ stats.key_visits }}</div>
<div class="stat-label">要客拜访条数</div>
</el-card>
</el-col>
</el-row>
<!-- Quick Actions -->
<div class="actions">
<el-button type="primary" @click="goWeeklyReport">📋 本周周报详情</el-button>
<el-button type="success" v-if="auth.isDirector" @click="router.push('/weekly-report')">📥 导出 Excel</el-button>
<el-button @click="router.push('/customers')">👥 客户管理</el-button>
</div>
<!-- Reporting Progress -->
<el-card class="progress-card">
<template #header>填报进度</template>
<div v-if="progress.length === 0" class="empty">暂无数据</div>
<div v-for="p in progress" :key="p.manager_id" class="progress-row">
<div class="progress-info">
<span class="progress-name">
{{ p.manager_name }}
<el-tag v-if="!p.has_reported_today" type="danger" size="small"> 今日未填</el-tag>
<el-tag v-else type="success" size="small"> 已填</el-tag>
</span>
<span class="progress-count">{{ p.visit_count }} / {{ p.expected }} </span>
</div>
<el-progress
:percentage="Math.min(100, Math.round((p.visit_count / Math.max(p.expected, 1)) * 100))"
:color="p.completed ? '#67c23a' : '#f56c6c'"
:stroke-width="18"
/>
</div>
</el-card>
</div>
</template>
<style scoped>
.dashboard h3 { margin: 0 0 4px; }
.week-range { color: #909399; font-size: 13px; margin-bottom: 20px; }
.stat-card { text-align: center; }
.stat-value { font-size: 36px; font-weight: 700; color: #1a73e8; }
.stat-label { font-size: 13px; color: #909399; margin-top: 4px; }
.actions { display: flex; gap: 12px; margin-bottom: 24px; }
.progress-card { margin-top: 20px; }
.progress-row { margin-bottom: 16px; }
.progress-info { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.progress-name { display: flex; align-items: center; gap: 8px; font-weight: 500; }
.progress-count { font-size: 13px; color: #606266; }
.empty { text-align: center; color: #c0c4cc; padding: 24px 0; }
</style>