Files
qiji/frontend/src/views/desktop/KeyVisits.vue
T

247 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { ElMessage, ElMessageBox } from 'element-plus'
import { todayStr } from '@/utils'
import api from '@/api/index'
import EditLogPanel from '@/components/EditLogPanel.vue'
const auth = useAuthStore()
const loading = ref(false)
const keyVisits = ref<any[]>([])
const customers = ref<any[]>([])
const allUsers = ref<any[]>([])
const plannedVisitors = ref<string[]>([])
const dialogVisible = ref(false)
const dialogMode = ref<'create' | 'edit'>('create')
const form = ref<any>({})
const statusPick = ref<Record<string, string>>({})
const keyStatuses = ['未开始', '进行中', '已完成']
const mgrColors = ['#1C3738','#4A6741','#5B7FA5','#7B7568','#8B6F47','#6B5B4F','#3D5A5C','#5C4A3D']
function mgrColor(name: string) {
if (!name) return '#909399'
let h = 0; for (let i=0;i<name.length;i++) h = name.charCodeAt(i) + ((h<<5)-h)
return mgrColors[Math.abs(h) % mgrColors.length]
}
const managerSummary = computed(() => {
const map: Record<string, number> = {}
keyVisits.value.forEach((k: any) => {
const n = k.manager_name || '未知'
map[n] = (map[n] || 0) + 1
})
return Object.entries(map).sort((a, b) => b[1] - a[1])
})
onMounted(async () => {
await Promise.all([loadItems(), loadCustomers(), loadUsers()])
})
async function loadUsers() {
try {
const res = await api.get('/users/')
allUsers.value = res.data || []
} catch (_) {}
}
async function loadCustomers(q?: string) {
try {
const params: any = { page_size: 100 }
if (q) params.search = q
const res = await api.get('/customers/', { params })
customers.value = res.data.items || []
} catch (_) {}
}
async function loadItems() {
loading.value = true
try {
const res = await api.get('/key-visits/')
keyVisits.value = res.data
} catch (e: any) { ElMessage.error('加载失败') }
finally { loading.value = false }
}
function openCreate() {
dialogMode.value = 'create'
form.value = { customer_id: '', urgency_level: '一般', description: '', progress_status: '未开始', planned_date: '', planned_visitor: '', visit_target: '' }
plannedVisitors.value = []
dialogVisible.value = true
}
function openEdit(item: any) {
dialogMode.value = 'edit'
form.value = { ...item }
plannedVisitors.value = item.planned_visitor ? item.planned_visitor.split('、') : []
dialogVisible.value = true
}
function onVisitorsChange(val: string[]) {
form.value.planned_visitor = val.join('、')
}
async function handleSave() {
try {
if (dialogMode.value === 'create') {
await api.post('/key-visits/', form.value)
} else {
await api.put(`/key-visits/${form.value.id}`, form.value)
}
ElMessage.success(dialogMode.value === 'create' ? '已创建' : '已更新')
dialogVisible.value = false
await loadItems()
} catch (e: any) { ElMessage.error('保存失败: ' + (e.response?.data?.detail || e.message)) }
}
async function handleDelete(id: string) {
try {
await ElMessageBox.confirm('确定删除?', '确认', { type: 'warning' })
await api.delete(`/key-visits/${id}`)
ElMessage.success('已删除')
await loadItems()
} catch (e: any) { if (e !== 'cancel') ElMessage.error('删除失败') }
}
async function quickStatusChange(row: any, newStatus: string) {
try {
await ElMessageBox.confirm(`确定将进展改为「${newStatus}」?`, '确认', { type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' })
await api.put(`/key-visits/${row.id}`, { progress_status: newStatus })
ElMessage.success('状态已更新')
await loadItems()
} catch (e: any) {
if (e !== 'cancel') ElMessage.error('更新失败: ' + (e.response?.data?.detail || e.message))
delete statusPick.value[row.id]
}
}
</script>
<template>
<div class="key-visits-page" v-loading="loading">
<div class="page-head">
<div class="page-head-row">
<div>
<h2 class="page-title">要客拜访</h2>
<p class="page-desc">重要客户拜访计划按紧急程度排序跟踪</p>
</div>
<el-button type="primary" @click="openCreate">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:4px">
<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
新建要客
</el-button>
</div>
<div class="page-rule"></div>
<div v-if="managerSummary.length" class="summary-bar">
<span class="summary-label">客户经理汇总</span>
<span v-for="[name, count] in managerSummary" :key="name" class="summary-chip" :style="{ background: mgrColor(name) }">{{ name }} · {{ count }}</span>
</div>
</div>
<el-card>
<el-table :data="keyVisits" stripe size="small" v-if="keyVisits.length">
<el-table-column type="index" label="序号" width="50" />
<el-table-column prop="customer_name" label="客户" width="160">
<template #default="{ row }">
<el-link type="primary" :underline="false" @click="openEdit(row)">{{ row.customer_name }}</el-link>
<el-tooltip v-if="row.edit_log?.length > 1" placement="top">
<template #content>最后编辑{{ row.edit_log[row.edit_log.length-1].editor }} · {{ row.edit_log.length-1 }}次修改</template>
<span class="edit-indicator" title="有过修改">🕐</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="客户经理" width="100">
<template #default="{ row }">
<span class="mgr-tag" :style="{ background: mgrColor(row.manager_name) }">{{ row.manager_name || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="urgency_level" label="重要度" width="90">
<template #default="{ row }">
<el-tag size="small" :type="row.urgency_level==='紧急'?'danger':row.urgency_level==='重要'?'warning':''">{{ row.urgency_level }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="250" show-overflow-tooltip />
<el-table-column label="进展" width="130">
<template #default="{ row }">
<el-select
v-model="statusPick[row.id]"
size="small"
style="width:100%"
:placeholder="row.progress_status"
@change="(v: string) => quickStatusChange(row, v)"
@visible-change="(v: boolean) => { if (v) statusPick[row.id] = row.progress_status }"
>
<el-option v-for="s in keyStatuses" :key="s" :label="s" :value="s" />
</el-select>
</template>
</el-table-column>
<el-table-column prop="planned_date" label="计划时间" width="110" />
<el-table-column prop="planned_visitor" label="拜访人" width="80" />
<el-table-column prop="visit_target" label="拜访对象" width="110" />
<el-table-column label="操作" width="80" fixed="right">
<template #default="{ row }">
<el-button text size="small" type="danger" @click="handleDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div v-else class="empty">暂无需重点关注的客户</div>
</el-card>
<!-- Dialog -->
<el-dialog v-model="dialogVisible" :title="(dialogMode === 'create' ? '新建' : '编辑') + ' 要客拜访'" width="520px">
<el-form label-position="top">
<el-form-item label="客户单位">
<el-select v-model="form.customer_id" filterable remote :remote-method="(q: string) => loadCustomers(q)" placeholder="搜索选择客户" style="width:100%">
<el-option v-for="c in customers" :key="c.id" :label="c.name" :value="c.id" />
</el-select>
</el-form-item>
<el-form-item label="紧急重要度">
<div class="method-grid">
<el-button v-for="u in ['一般','重要','紧急']" :key="u" size="small"
:type="form.urgency_level===u ? (u==='紧急'?'danger':u==='重要'?'warning':'') : ''"
@click="form.urgency_level=u">{{ u }}</el-button>
</div>
</el-form-item>
<el-form-item label="内容描述"><el-input v-model="form.description" type="textarea" :rows="4" placeholder="请输入要客拜访描述" /></el-form-item>
<el-form-item label="进展状态">
<el-select v-model="form.progress_status">
<el-option label="未开始" value="未开始" />
<el-option label="进行中" value="进行中" />
<el-option label="已完成" value="已完成" />
</el-select>
</el-form-item>
<el-form-item label="计划拜访时间">
<el-date-picker v-model="form.planned_date" type="date" placeholder="选择日期" style="width:100%" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="计划拜访人可多选">
<el-select v-model="plannedVisitors" multiple filterable allow-create default-first-option placeholder="选择或输入姓名可多选" style="width:100%" @change="onVisitorsChange">
<el-option v-for="u in allUsers" :key="u.id" :label="u.name + (u.role==='director'?' (支局长)':u.role==='leader'?' (分管领导)':'')" :value="u.name" />
</el-select>
</el-form-item>
<el-form-item label="拜访对象"><el-input v-model="form.visit_target" placeholder="对方联系人/部门" /></el-form-item>
</el-form>
<EditLogPanel v-if="dialogMode === 'edit'" :edit-log="form.edit_log || []" />
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.edit-indicator { font-size: 12px; margin-left: 3px; opacity: 0.5; cursor: help; }
.page-head { margin-bottom: 20px; }
.page-head-row { display: flex; justify-content: space-between; align-items: flex-start; }
.page-title { margin: 0; font-family: 'ZCOOL XiaoWei', STSong, serif; font-size: 22px; font-weight: 400; color: var(--ink); letter-spacing: 0.06em; }
.page-desc { margin: 4px 0 0; font-size: 13px; color: var(--c-text-muted); font-family: 'Noto Serif SC', STSong, serif; }
.page-rule { width: 32px; height: 3px; background: var(--gold); margin-top: 12px; }
.summary-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 14px; padding: 10px 14px; background: var(--c-bg-light, #faf9f6); border-radius: 6px; border: 1px solid var(--c-border, #e8e5df); }
.summary-label { font-size: 12px; color: var(--c-text-muted); font-family: 'Noto Serif SC', STSong, serif; margin-right: 4px; }
.summary-chip { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; color: #fff; white-space: nowrap; }
.mgr-tag { display: inline-block; padding: 1px 9px; border-radius: 10px; font-size: 12px; color: #fff; white-space: nowrap; }
.method-grid { display: flex; gap: 8px; }
.empty { text-align: center; color: var(--c-text-muted); padding: 48px 0; font-family: 'Noto Serif SC', STSong, serif; }
</style>