企迹(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
+124
View File
@@ -0,0 +1,124 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { ElMessage } from 'element-plus'
const router = useRouter()
const route = useRoute()
const auth = useAuthStore()
const loading = ref(false)
// Check for Casdoor callback
onMounted(async () => {
const code = route.query.code as string
const state = route.query.state as string
if (code) {
loading.value = true
try {
await auth.casdoorLogin(code, state)
ElMessage.success('登录成功')
router.push(auth.isManager ? '/m' : '/')
} catch (e: any) {
ElMessage.error('登录失败: ' + (e.response?.data?.detail || e.message))
} finally {
loading.value = false
}
return
}
// Check for wecom code
const wecomCode = route.query.wecom_code as string
if (wecomCode) {
loading.value = true
try {
const res = await auth.wecomLogin(wecomCode)
if (res.data.need_bind) {
// Redirect to Casdoor for binding
window.location.href = res.data.casdoor_url
return
}
auth.saveLogin({
access_token: res.data.access_token,
user_id: res.data.user_id,
name: res.data.name,
role: res.data.role,
})
ElMessage.success('登录成功')
router.push(auth.isManager ? '/m' : '/')
} catch (e: any) {
ElMessage.error('企微登录失败')
} finally {
loading.value = false
}
}
})
function goCasdoorLogin() {
// Redirect to Casdoor authorization page
const endpoint = import.meta.env.VITE_CASDOOR_ENDPOINT
const clientId = import.meta.env.VITE_CASDOOR_CLIENT_ID
const redirectUri = encodeURIComponent(window.location.origin + '/login')
const casdoorUrl = `${endpoint}/login/oauth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirectUri}&scope=openid+profile&state=login`
window.location.href = casdoorUrl
}
</script>
<template>
<div class="login-page">
<div class="login-card">
<div class="brand">
<h1>企迹</h1>
<p>政企周报管理系统</p>
</div>
<el-button
type="primary"
size="large"
:loading="loading"
@click="goCasdoorLogin"
style="width: 100%"
>
登录 / 注册
</el-button>
<p class="hint">使用 Casdoor 账号登录</p>
</div>
</div>
</template>
<style scoped>
.login-page {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, #1a73e8 0%, #4080ff 50%, #69a0ff 100%);
}
.login-card {
background: white;
border-radius: 16px;
padding: 48px 40px;
width: 360px;
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
}
.brand {
text-align: center;
margin-bottom: 36px;
}
.brand h1 {
margin: 0;
font-size: 32px;
color: #1a73e8;
}
.brand p {
margin: 8px 0 0;
color: #909399;
font-size: 14px;
}
.hint {
text-align: center;
color: #c0c4cc;
font-size: 12px;
margin-top: 16px;
}
</style>