企迹(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:
@@ -0,0 +1,172 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import api from '@/api/index'
|
||||
|
||||
const managers = ref<any[]>([])
|
||||
const selectedUserIds = ref<string[]>([])
|
||||
const remindMessage = ref('')
|
||||
const announcementContent = ref('')
|
||||
const remindLoading = ref(false)
|
||||
const announceLoading = ref(false)
|
||||
const dailyCheckLoading = ref(false)
|
||||
|
||||
// Import
|
||||
const importDialogVisible = ref(false)
|
||||
const importFile = ref<File | null>(null)
|
||||
const importPreview = ref<any>(null)
|
||||
const importLoading = ref(false)
|
||||
const importResult = ref<any>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await api.get('/users/', { params: { role: 'manager' } })
|
||||
managers.value = res.data
|
||||
} catch (_) {}
|
||||
})
|
||||
|
||||
async function handleRemind() {
|
||||
if (!selectedUserIds.value.length) { ElMessage.warning('请选择要提醒的人员'); return }
|
||||
remindLoading.value = true
|
||||
try {
|
||||
const res = await api.post('/wecom/remind', {
|
||||
user_ids: selectedUserIds.value,
|
||||
message: remindMessage.value || undefined,
|
||||
})
|
||||
ElMessage.success(`已发送提醒给 ${res.data.sent_to} 人`)
|
||||
} catch (e: any) {
|
||||
ElMessage.error('发送失败')
|
||||
} finally {
|
||||
remindLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAnnouncement() {
|
||||
if (!announcementContent.value) { ElMessage.warning('请输入公告内容'); return }
|
||||
announceLoading.value = true
|
||||
try {
|
||||
const res = await api.post('/wecom/announcement', {
|
||||
content: announcementContent.value,
|
||||
})
|
||||
ElMessage.success(`公告已推送给 ${res.data.sent_to} 人`)
|
||||
announcementContent.value = ''
|
||||
} catch (e: any) {
|
||||
ElMessage.error('推送失败')
|
||||
} finally {
|
||||
announceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDailyCheck() {
|
||||
dailyCheckLoading.value = true
|
||||
try {
|
||||
const res = await api.post('/wecom/trigger-daily-check')
|
||||
ElMessage.success(`已执行:${res.data.reported}/${res.data.total_managers} 人已填报`)
|
||||
} catch (e: any) {
|
||||
ElMessage.error('执行失败')
|
||||
} finally {
|
||||
dailyCheckLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleImportFile(e: Event) {
|
||||
const target = e.target as HTMLInputElement
|
||||
if (target.files?.[0]) {
|
||||
importFile.value = target.files[0]
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImportPreview() {
|
||||
if (!importFile.value) return
|
||||
importLoading.value = true
|
||||
const formData = new FormData()
|
||||
formData.append('file', importFile.value)
|
||||
try {
|
||||
const res = await api.post('/import/weekly-report', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
importPreview.value = res.data.preview
|
||||
importResult.value = res.data
|
||||
} catch (e: any) {
|
||||
ElMessage.error('解析失败')
|
||||
} finally {
|
||||
importLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<h3>系统设置</h3>
|
||||
|
||||
<!-- Manual Remind -->
|
||||
<el-card class="setting-card">
|
||||
<template #header>📢 手动催办</template>
|
||||
<el-form>
|
||||
<el-form-item label="选择人员">
|
||||
<el-select v-model="selectedUserIds" multiple placeholder="选择客户经理" style="width:100%">
|
||||
<el-option v-for="m in managers" :key="m.id" :label="m.name" :value="m.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="提醒内容">
|
||||
<el-input v-model="remindMessage" type="textarea" :rows="2" placeholder="可选,留空使用默认提醒语" />
|
||||
</el-form-item>
|
||||
<el-button type="primary" :loading="remindLoading" @click="handleRemind">发送催办</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- Announcement -->
|
||||
<el-card class="setting-card">
|
||||
<template #header>📣 全员公告</template>
|
||||
<el-form>
|
||||
<el-form-item label="公告内容">
|
||||
<el-input v-model="announcementContent" type="textarea" :rows="3" placeholder="输入公告内容..." />
|
||||
</el-form-item>
|
||||
<el-button type="warning" :loading="announceLoading" @click="handleAnnouncement">推送公告</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- Daily Check -->
|
||||
<el-card class="setting-card">
|
||||
<template #header>⏰ 填报检查</template>
|
||||
<p>手动触发今日填报检查(通常每日 18:00 自动执行)</p>
|
||||
<el-button :loading="dailyCheckLoading" @click="handleDailyCheck">立即检查</el-button>
|
||||
</el-card>
|
||||
|
||||
<!-- Import -->
|
||||
<el-card class="setting-card">
|
||||
<template #header>📥 Excel 数据导入</template>
|
||||
<el-form>
|
||||
<el-form-item label="上传旧周报 Excel">
|
||||
<input type="file" accept=".xlsx,.xls" @change="handleImportFile" />
|
||||
</el-form-item>
|
||||
<el-button type="success" :loading="importLoading" @click="handleImportPreview" :disabled="!importFile">
|
||||
预览并导入
|
||||
</el-button>
|
||||
</el-form>
|
||||
<!-- Preview Results -->
|
||||
<div v-if="importPreview" style="margin-top:16px">
|
||||
<h4>文件预览</h4>
|
||||
<el-table :data="Object.entries(importPreview)" stripe>
|
||||
<el-table-column prop="0" label="Sheet" />
|
||||
<el-table-column prop="1.row_count" label="数据行数" />
|
||||
<el-table-column label="表头">
|
||||
<template #default="{ row }">
|
||||
{{ (row[1] as any).headers.join(', ') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-if="importResult" style="margin-top:12px">
|
||||
<el-alert type="success" :closable="false">
|
||||
导入完成:拜访 {{ importResult.visits }} 条,跳过 {{ importResult.skipped }} 条
|
||||
</el-alert>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings-page h3 { margin: 0 0 20px; }
|
||||
.setting-card { margin-bottom: 16px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user