Merge branch 'main' into develop
# Conflicts: # backend/app/api/key_visits.py # backend/app/api/mini_business.py # backend/app/api/visits.py # backend/app/api/work_plans.py # backend/app/main.py # backend/app/models/__init__.py # backend/app/schemas/key_visit.py # backend/app/schemas/mini_business.py # backend/app/schemas/work_plan.py # backend/app/services/light_board.py # frontend/src/components/DesktopLayout.vue # frontend/src/stores/theme.ts # frontend/src/views/desktop/ManagerWorkspace.vue # frontend/src/views/desktop/WorkPlans.vue # frontend/src/views/mobile/KeyVisitForm.vue # frontend/src/views/mobile/LeaveForm.vue # frontend/src/views/mobile/PlansList.vue # frontend/src/views/mobile/VisitForm.vue # frontend/src/views/mobile/WorkPlanForm.vue
This commit is contained in:
@@ -14,6 +14,7 @@ const themeStore = useThemeStore()
|
||||
const loading = ref(false)
|
||||
const workPlans = ref<any[]>([])
|
||||
const customers = ref<any[]>([])
|
||||
const allManagers = ref<any[]>([])
|
||||
const shotRef = ref<HTMLElement | null>(null)
|
||||
const { capturing, captureEl } = useScreenshot()
|
||||
|
||||
@@ -49,8 +50,11 @@ const filterStatus = ref('')
|
||||
const expandedSections = ref<Record<string, boolean>>({
|
||||
thisWeek: true, nextWeek: true, thisMonth: false, later: false, archived: false,
|
||||
})
|
||||
const searchText = ref('')
|
||||
|
||||
const isLight = computed(() => themeStore.currentTheme === 'light')
|
||||
function onSearch() { loadPlans() }
|
||||
|
||||
const isLight = computed(() => themeStore.mode === 'light')
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
'计划中': '#4A6741',
|
||||
@@ -128,17 +132,69 @@ const managerSummary = computed(() => {
|
||||
|
||||
function resetFilters() { filterManager.value = ''; filterStatus.value = '' }
|
||||
|
||||
onMounted(async () => { await Promise.all([loadPlans(), loadCustomers(), loadManagerColors()]) })
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadPlans(), loadCustomers(), loadManagerColors()])
|
||||
if (auth.isDirector || auth.isLeader) {
|
||||
api.get('/users/', { params: { role: 'manager' } }).then(r => { allManagers.value = r.data }).catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
const customerSearch = ref('')
|
||||
|
||||
async function loadCustomers(q?: string) {
|
||||
try { const params: any = { page_size: 500 }; if (q) params.search = q; const res = await api.get('/customers/', { params }); customers.value = res.data.items || [] } catch (_) {}
|
||||
}
|
||||
async function loadPlans() { loading.value = true; try { const res = await api.get('/work-plans/'); workPlans.value = res.data } catch (_) {} finally { loading.value = false } }
|
||||
|
||||
function openCreate() { dialogMode.value = 'create'; form.value = { customer_id: '', plan_content: '', plan_date: todayStr(), status: '计划中' }; dialogVisible.value = true }
|
||||
function openEdit(item: any) { dialogMode.value = 'edit'; form.value = { ...item }; if (item.customer_id && item.customer_name && !customers.value.find((c: any) => c.id === item.customer_id)) { customers.value.unshift({ id: item.customer_id, name: item.customer_name }) }; dialogVisible.value = true }
|
||||
async function handleCustomerSearch(query: string) {
|
||||
customerSearch.value = query
|
||||
if (query) { await loadCustomers(query) }
|
||||
else { await loadCustomers() }
|
||||
}
|
||||
|
||||
async function handleQuickCreate() {
|
||||
const name = customerSearch.value.trim()
|
||||
if (!name) { ElMessage.warning('请输入单位名称'); return }
|
||||
try {
|
||||
const res = await api.post('/customers/quick-create', null, { params: { name } })
|
||||
const newCust = res.data
|
||||
customers.value.unshift(newCust)
|
||||
form.value.customer_id = newCust.id
|
||||
customerSearch.value = ''
|
||||
ElMessage.success(`已创建客户:${name}`)
|
||||
await loadCustomers()
|
||||
} catch (e: any) {
|
||||
ElMessage.error('创建失败: ' + (e.response?.data?.detail || e.message))
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPlans() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = {}
|
||||
if (searchText.value) params.search = searchText.value
|
||||
const res = await api.get('/work-plans/', { params })
|
||||
workPlans.value = res.data
|
||||
} catch (e: any) { ElMessage.error('加载失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
dialogMode.value = 'create'
|
||||
form.value = { customer_id: '', plan_content: '', plan_date: todayStr(), status: '计划中', manager_id: (auth.isDirector || auth.isLeader) ? '' : (auth.userId || '') }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(item: any) {
|
||||
dialogMode.value = 'edit'
|
||||
form.value = { ...item }
|
||||
if (item.customer_id && item.customer_name && !customers.value.find((c: any) => c.id === item.customer_id)) {
|
||||
customers.value.unshift({ id: item.customer_id, name: item.customer_name })
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.value.customer_id) { ElMessage.warning('请选择客户单位'); return }
|
||||
try {
|
||||
if (dialogMode.value === 'create') await api.post('/work-plans/', form.value)
|
||||
else await api.put(`/work-plans/${form.value.id}`, form.value)
|
||||
@@ -190,8 +246,21 @@ async function handleScreenshot() { const d = new Date().toISOString().slice(0,
|
||||
<!-- Filter Bar -->
|
||||
<div class="filter-bar">
|
||||
<div class="filter-row">
|
||||
<div class="filter-item"><label class="filter-label">客户经理</label><el-select v-model="filterManager" clearable placeholder="全部" size="small" style="width:150px"><el-option v-for="m in managerOptions" :key="m" :label="m" :value="m" /></el-select></div>
|
||||
<div class="filter-item"><label class="filter-label">状态</label><el-select v-model="filterStatus" clearable placeholder="全部" size="small" style="width:120px"><el-option v-for="s in planStatuses" :key="s" :label="s" :value="s" /></el-select></div>
|
||||
<div class="filter-item">
|
||||
<el-input v-model="searchText" placeholder="搜索..." clearable size="small" style="width:180px" @keyup.enter="onSearch" @clear="onSearch" />
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label class="filter-label">客户经理</label>
|
||||
<el-select v-model="filterManager" clearable placeholder="全部" size="small" style="width:150px">
|
||||
<el-option v-for="m in managerOptions" :key="m" :label="m" :value="m" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label class="filter-label">状态</label>
|
||||
<el-select v-model="filterStatus" clearable placeholder="全部" size="small" style="width:120px">
|
||||
<el-option v-for="s in planStatuses" :key="s" :label="s" :value="s" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-button v-if="filterManager || filterStatus" size="small" plain @click="resetFilters">重置</el-button>
|
||||
<span class="filter-count">{{ workPlans.length }} 条</span>
|
||||
</div>
|
||||
@@ -246,10 +315,35 @@ async function handleScreenshot() { const d = new Date().toISOString().slice(0,
|
||||
<!-- Dialog -->
|
||||
<el-dialog v-model="dialogVisible" :title="(dialogMode === 'create' ? '新建' : '编辑') + ' 工作计划'" width="500px">
|
||||
<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="计划拜访时间"><el-date-picker v-model="form.plan_date" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="工作计划"><el-input v-model="form.plan_content" type="textarea" :rows="4" placeholder="请输入计划内容" /></el-form-item>
|
||||
<el-form-item label="状态"><el-select v-model="form.status"><el-option label="计划中" value="计划中" /><el-option label="已完成" value="已完成" /><el-option label="已取消" value="已取消" /></el-select></el-form-item>
|
||||
<el-form-item label="客户单位 *">
|
||||
<el-select v-model="form.customer_id" filterable remote :remote-method="handleCustomerSearch" placeholder="搜索选择客户" style="width:100%">
|
||||
<el-option v-for="c in customers" :key="c.id" :label="c.name" :value="c.id" />
|
||||
<template #empty>
|
||||
<div v-if="customerSearch" class="select-empty-create">
|
||||
<p style="color:var(--warm-gray);font-size:13px;margin:0 0 8px">未找到「{{ customerSearch }}」</p>
|
||||
<button type="button" class="quick-create-btn" @click.stop="handleQuickCreate">+ 新建客户「{{ customerSearch }}」</button>
|
||||
</div>
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="auth.isDirector || auth.isLeader" label="客户经理">
|
||||
<el-select v-model="form.manager_id" filterable placeholder="选择客户经理" style="width:100%">
|
||||
<el-option v-for="m in allManagers" :key="m.id" :label="m.name" :value="m.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="计划拜访时间">
|
||||
<el-date-picker v-model="form.plan_date" type="date" value-format="YYYY-MM-DD" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="工作计划">
|
||||
<el-input v-model="form.plan_content" type="textarea" :rows="4" placeholder="请输入计划内容" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status">
|
||||
<el-option label="计划中" value="计划中" />
|
||||
<el-option label="已完成" value="已完成" />
|
||||
<el-option label="已取消" value="已取消" />
|
||||
</el-select>
|
||||
</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>
|
||||
@@ -306,4 +400,9 @@ async function handleScreenshot() { const d = new Date().toISOString().slice(0,
|
||||
|
||||
/* ── Overdue Badge ── */
|
||||
.overdue-badge { display: inline-block; padding: 1px 7px; border-radius: 8px; background: var(--vermilion); color: #fff; font-family: var(--font-mono); font-size: 10px; white-space: nowrap; }
|
||||
|
||||
.quick-create-btn { display: inline-flex; align-items: center; gap: 4px; background: none; border: 1px dashed var(--gold); padding: 6px 12px; color: var(--gold-dark); font-family: var(--font-body); font-size: 12px; cursor: pointer; transition: all 0.2s; }
|
||||
.quick-create-btn:hover { border-color: var(--vermilion); color: var(--vermilion); background: rgba(184,71,46,0.03); }
|
||||
.select-empty-create { padding: 8px 12px; text-align: center; }
|
||||
.required-star { color: var(--vermilion); font-weight: 700; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user