You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

402 lines
14 KiB

<script setup lang="ts">
import PageTitle from '@/components/PageTitle.vue'
import { ref } from 'vue'
import { usePageLoad } from '@/composables/usePageLoad'
import { fetchDictByCode } from '@/api/admin/dict'
import { fetchAdminUsers } from '@/api/admin/users'
import type { DictItemBrief } from '@/api/admin/teachers'
import {
createDemandHandleLog,
deleteDemand,
fetchDemand,
fetchDemandHandleLogs,
fetchDemandsList,
type DemandHandleLogRow,
type DemandRow,
} from '@/api/admin/demands'
import { demandStatusClass, demandTypeClass } from '@/utils/admin-list'
import { useAuthStore } from '@/stores/auth'
import { ElMessage, ElMessageBox } from 'element-plus'
const auth = useAuthStore()
function defaultFollowAdminUserId() {
const currentId = auth.user?.id
if (currentId != null && adminOptions.value.some((a) => a.id === currentId)) {
return currentId
}
return adminOptions.value[0]?.id
}
const loading = ref(false)
const items = ref<DemandRow[]>([])
const meta = ref({ current_page: 1, per_page: 20, total: 0 })
const page = ref(1)
const keyword = ref('')
const filterType = ref<number | ''>('')
const filterStatus = ref<number | ''>('')
const typeOptions = ref<DictItemBrief[]>([])
const statusOptions = ref<DictItemBrief[]>([])
const adminOptions = ref<{ id: number; label: string }[]>([])
const detailVisible = ref(false)
const detail = ref<DemandRow | null>(null)
const handleLogs = ref<DemandHandleLogRow[]>([])
const recordsVisible = ref(false)
const recordsTitle = ref('')
const records = ref<DemandHandleLogRow[]>([])
const followVisible = ref(false)
const followRow = ref<DemandRow | null>(null)
const followForm = ref({
handled_at: new Date().toISOString().slice(0, 10),
admin_user_id: undefined as number | undefined,
status_dict_item_id: undefined as number | undefined,
content: '',
next_plan: '',
next_follow_date: '',
})
async function loadDicts() {
const [type, status, admins] = await Promise.all([
fetchDictByCode('demand_type'),
fetchDictByCode('demand_status'),
fetchAdminUsers({ page: 1, page_size: 200, status: 1 }),
])
typeOptions.value = type.items
statusOptions.value = status.items
adminOptions.value = admins.items.map((u) => ({
id: u.id,
label: u.real_name || u.username,
}))
}
async function load() {
loading.value = true
try {
const params: Record<string, unknown> = { page: page.value, page_size: meta.value.per_page }
if (keyword.value) params.keyword = keyword.value
if (filterType.value !== '') params.type_dict_item_id = filterType.value
if (filterStatus.value !== '') params.status_dict_item_id = filterStatus.value
const res = await fetchDemandsList(params)
items.value = res.items
meta.value = res.meta
} finally {
loading.value = false
}
}
function resetFilters() {
keyword.value = ''
filterType.value = ''
filterStatus.value = ''
page.value = 1
load()
}
function searchDemands() {
page.value = 1
load()
}
async function openDetail(row: DemandRow) {
detail.value = await fetchDemand(row.id)
handleLogs.value = await fetchDemandHandleLogs(row.id)
detailVisible.value = true
}
async function openRecords(row: DemandRow) {
recordsTitle.value = row.title
records.value = await fetchDemandHandleLogs(row.id)
recordsVisible.value = true
}
function openFollow(row: DemandRow) {
followRow.value = row
followForm.value = {
handled_at: new Date().toISOString().slice(0, 10),
admin_user_id: defaultFollowAdminUserId(),
status_dict_item_id: row.status_dict_item_id,
content: '',
next_plan: '',
next_follow_date: '',
}
followVisible.value = true
}
async function saveFollow() {
if (!followRow.value) return
const f = followForm.value
if (!f.content.trim() || !f.status_dict_item_id || !f.admin_user_id) {
ElMessage.warning('请填写跟进内容、处理状态与跟进人员')
return
}
await createDemandHandleLog(followRow.value.id, { ...f })
ElMessage.success('已保存跟进')
followVisible.value = false
await load()
}
async function remove(row: DemandRow) {
await ElMessageBox.confirm(`确定删除需求「${row.title}」?`, '确认删除', { type: 'warning' })
await deleteDemand(row.id)
ElMessage.success('已删除')
await load()
}
function openFollowFromDetail() {
if (!detail.value) return
detailVisible.value = false
openFollow(detail.value)
}
usePageLoad(async () => {
await loadDicts()
await load()
})
</script>
<template>
<div class="list-page demands-page">
<div class="page-header">
<PageTitle />
</div>
<el-card shadow="never" class="admin-list-card">
<div class="list-filter-bar">
<el-input
v-model="keyword"
class="filter-search"
placeholder="搜索标题、姓名、公司、类型…"
clearable
@keyup.enter="searchDemands"
/>
<el-select v-model="filterType" placeholder="类型" clearable class="filter-select-wide">
<el-option v-for="o in typeOptions" :key="o.id" :label="o.label" :value="o.id" />
</el-select>
<el-select v-model="filterStatus" placeholder="处理状态" clearable class="filter-select">
<el-option v-for="o in statusOptions" :key="o.id" :label="o.label" :value="o.id" />
</el-select>
<el-button type="primary" class="btn-create" @click="searchDemands">搜索</el-button>
<el-button @click="resetFilters">重置</el-button>
</div>
<el-table v-loading="loading" :data="items" row-key="id">
<el-table-column label="类型" width="110">
<template #default="{ row }">
<span class="type-badge" :class="demandTypeClass(row.type_item?.value)">
{{ row.type_item?.label }}
</span>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="220" show-overflow-tooltip />
<el-table-column prop="contact_name" label="姓名" width="100" />
<el-table-column prop="company" label="公司" width="140" show-overflow-tooltip />
<el-table-column prop="submitted_at" label="时间" width="110" />
<el-table-column label="处理状态" width="100">
<template #default="{ row }">
<span class="status-badge" :class="demandStatusClass(row.status_item?.value)">
{{ row.status_item?.label }}
</span>
</template>
</el-table-column>
<el-table-column label="跟进记录" width="96" align="center">
<template #default="{ row }">
<el-button size="small" class="btn-action-secondary table-inline-btn" @click="openRecords(row)">
{{ row.handle_logs_count || 0 }}条记录
</el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<div class="table-row-actions">
<el-button class="btn-action-secondary" @click="openDetail(row)">详情</el-button>
<el-button class="btn-action-danger" @click="remove(row)">删除</el-button>
<el-button class="btn-action-info" @click="openFollow(row)">跟进</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="list-pager">
<el-pagination
v-model:current-page="page"
:page-size="meta.per_page"
:total="meta.total"
layout="total, prev, pager, next"
@current-change="load"
/>
</div>
</el-card>
<!-- 详情弹窗:对齐 demand-detail.html -->
<el-dialog v-model="detailVisible" :title="detail?.title || '需求详情'" width="800px" destroy-on-close>
<template v-if="detail">
<el-row :gutter="12" class="detail-meta">
<el-col :span="6">
<label class="field-label">需求类型</label>
<el-input :model-value="detail.type_item?.label || ''" readonly />
</el-col>
<el-col :span="6">
<label class="field-label">提交人</label>
<el-input :model-value="detail.contact_name || ''" readonly />
</el-col>
<el-col :span="6">
<label class="field-label">提交时间</label>
<el-input :model-value="detail.submitted_at || ''" readonly />
</el-col>
<el-col :span="6">
<label class="field-label">处理状态</label>
<el-input :model-value="detail.status_item?.label || ''" readonly />
</el-col>
<el-col :span="24">
<label class="field-label">需求描述</label>
<el-input :model-value="detail.content" type="textarea" :rows="3" readonly />
</el-col>
</el-row>
<h3 class="sub-title">处理记录</h3>
<el-table :data="handleLogs" size="small">
<el-table-column prop="handled_at" label="跟进日期" width="110" />
<el-table-column prop="operator_name" label="跟进人员" width="100" />
<el-table-column label="处理状态" width="100">
<template #default="{ row }">
<span class="status-badge" :class="demandStatusClass(row.status_item?.value)">
{{ row.status_item?.label }}
</span>
</template>
</el-table-column>
<el-table-column prop="content" label="跟进内容" min-width="160" show-overflow-tooltip />
<el-table-column prop="next_plan" label="下次跟进计划" width="140" show-overflow-tooltip />
<el-table-column prop="next_follow_date" label="下次跟进日期" width="120" />
</el-table>
</template>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
<el-button type="primary" class="btn-create" @click="openFollowFromDetail">添加跟进</el-button>
</template>
</el-dialog>
<el-dialog v-model="recordsVisible" title="跟进记录" width="640px">
<div class="follow-teacher-summary">{{ recordsTitle }}</div>
<div class="follow-history-list">
<div v-for="r in records" :key="r.id" class="follow-history-card">
<div class="follow-history-head">
<div>
<span class="follow-history-title">{{ r.operator_name }}</span>
<span class="follow-history-date">{{ r.handled_at }}</span>
</div>
</div>
<div class="follow-history-body">{{ r.content }}</div>
</div>
<el-empty v-if="!records.length" description="暂无记录" />
</div>
<template #footer>
<el-button @click="recordsVisible = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog v-model="followVisible" title="需求跟进" width="720px" destroy-on-close>
<div class="follow-teacher-summary">{{ followRow?.title }}</div>
<el-form label-position="top" class="form-small" style="margin-top: 12px">
<el-row :gutter="12">
<el-col :span="8">
<el-form-item label="跟进日期" required>
<el-date-picker v-model="followForm.handled_at" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="跟进人员" required>
<el-select v-model="followForm.admin_user_id" style="width: 100%">
<el-option v-for="a in adminOptions" :key="a.id" :label="a.label" :value="a.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="处理状态" required>
<el-select v-model="followForm.status_dict_item_id" style="width: 100%">
<el-option v-for="o in statusOptions" :key="o.id" :label="o.label" :value="o.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="跟进内容" required>
<el-input v-model="followForm.content" type="textarea" :rows="4" placeholder="请输入本次跟进内容" />
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="下次跟进计划">
<el-input v-model="followForm.next_plan" placeholder="请输入下一步计划" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="下次跟进日期">
<el-date-picker v-model="followForm.next_follow_date" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="followVisible = false">取消</el-button>
<el-button type="primary" class="btn-create" @click="saveFollow"></el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.demands-page {
--brand-bg-soft: #fdf3f3;
}
.field-label {
display: block;
font-size: 13px;
margin-bottom: 4px;
color: var(--el-text-color-regular);
}
.detail-meta {
margin-bottom: 16px;
}
.sub-title {
margin: 0 0 8px;
font-size: 15px;
}
.follow-teacher-summary {
padding: 10px 12px;
border-left: 4px solid var(--brand-primary);
background: var(--brand-bg-soft);
border-radius: 6px;
margin-bottom: 12px;
}
.follow-history-list {
display: grid;
gap: 10px;
max-height: 50vh;
overflow: auto;
}
.follow-history-card {
border: 1px solid #dfe4eb;
border-left: 4px solid var(--brand-primary, #b11e23);
border-radius: 6px;
padding: 10px 12px;
}
.follow-history-head {
margin-bottom: 6px;
}
.follow-history-title {
font-weight: 600;
color: var(--brand-primary, #b11e23);
}
.follow-history-date {
margin-left: 12px;
color: #6b7280;
font-size: 13px;
}
.follow-history-body {
font-size: 13px;
color: #374151;
}
</style>