|
|
<script setup lang="ts">
|
|
|
import PageTitle from '@/components/PageTitle.vue'
|
|
|
import { computed, ref, watch } from 'vue'
|
|
|
import { usePageLoad } from '@/composables/usePageLoad'
|
|
|
import { fetchDictByCode } from '@/api/admin/dict'
|
|
|
import {
|
|
|
convertMiniappUserToTeacher,
|
|
|
fetchMiniappUser,
|
|
|
fetchMiniappUsersList,
|
|
|
type MiniappUserDetail,
|
|
|
type MiniappUserRow,
|
|
|
} from '@/api/admin/miniapp-users'
|
|
|
import {
|
|
|
fetchTeacherFilterOptions,
|
|
|
fetchUniversities,
|
|
|
type DictItemBrief,
|
|
|
type ResearchDirectionBrief,
|
|
|
type UniversityOption,
|
|
|
} from '@/api/admin/teachers'
|
|
|
import { previewNextFollowDate } from '@/utils/teacherFollowRule'
|
|
|
import { starDisplay } from '@/utils/teacherStar'
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
|
const loading = ref(false)
|
|
|
const items = ref<MiniappUserRow[]>([])
|
|
|
const meta = ref({ current_page: 1, per_page: 20, total: 0 })
|
|
|
const page = ref(1)
|
|
|
const keyword = ref('')
|
|
|
const filterConverted = ref<'' | '0' | '1'>('')
|
|
|
|
|
|
const starOptions = ref<DictItemBrief[]>([])
|
|
|
const statusOptions = ref<DictItemBrief[]>([])
|
|
|
const directionOptions = ref<ResearchDirectionBrief[]>([])
|
|
|
const universityOptions = ref<UniversityOption[]>([])
|
|
|
|
|
|
const detailVisible = ref(false)
|
|
|
const detail = ref<MiniappUserDetail | null>(null)
|
|
|
|
|
|
const convertVisible = ref(false)
|
|
|
const convertRow = ref<MiniappUserRow | null>(null)
|
|
|
const convertSaving = ref(false)
|
|
|
const convertForm = ref({
|
|
|
name: '',
|
|
|
university_id: undefined as number | undefined,
|
|
|
city: '',
|
|
|
title: '',
|
|
|
research_direction_ids: [] as number[],
|
|
|
phone: '',
|
|
|
email: '',
|
|
|
star_level_dict_item_id: undefined as number | undefined,
|
|
|
status_dict_item_id: undefined as number | undefined,
|
|
|
next_follow_date: '',
|
|
|
})
|
|
|
|
|
|
const pendingStarId = computed(() => starOptions.value.find((s) => s.value === 'pending')?.id)
|
|
|
|
|
|
function formatEnrollments(titles: string[]): string {
|
|
|
if (!titles?.length) return '—'
|
|
|
return titles.join('、')
|
|
|
}
|
|
|
|
|
|
async function loadDicts() {
|
|
|
const [star, status, dirs, unis] = await Promise.all([
|
|
|
fetchDictByCode('teacher_level'),
|
|
|
fetchDictByCode('teacher_status'),
|
|
|
fetchTeacherFilterOptions(),
|
|
|
fetchUniversities({ page: 1, page_size: 500 }),
|
|
|
])
|
|
|
starOptions.value = star.items
|
|
|
statusOptions.value = status.items
|
|
|
directionOptions.value = dirs.research_directions
|
|
|
universityOptions.value = unis.items
|
|
|
}
|
|
|
|
|
|
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 (filterConverted.value !== '') params.converted = filterConverted.value
|
|
|
const res = await fetchMiniappUsersList(params)
|
|
|
items.value = res.items
|
|
|
meta.value = res.meta
|
|
|
} finally {
|
|
|
loading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function resetFilters() {
|
|
|
keyword.value = ''
|
|
|
filterConverted.value = ''
|
|
|
page.value = 1
|
|
|
load()
|
|
|
}
|
|
|
|
|
|
function searchList() {
|
|
|
page.value = 1
|
|
|
load()
|
|
|
}
|
|
|
|
|
|
async function openDetail(row: MiniappUserRow) {
|
|
|
detail.value = await fetchMiniappUser(row.id)
|
|
|
detailVisible.value = true
|
|
|
}
|
|
|
|
|
|
function openConvert(row: MiniappUserRow) {
|
|
|
if (row.teacher_id) {
|
|
|
ElMessage.warning('该学员已转入老师库')
|
|
|
return
|
|
|
}
|
|
|
convertRow.value = row
|
|
|
convertForm.value = {
|
|
|
name: row.name || '',
|
|
|
university_id: undefined,
|
|
|
city: '',
|
|
|
title: row.job_title || '',
|
|
|
research_direction_ids: [...(row.research_direction_ids || [])],
|
|
|
phone: row.mobile || '',
|
|
|
email: '',
|
|
|
star_level_dict_item_id: pendingStarId.value,
|
|
|
status_dict_item_id: statusOptions.value.find((s) => s.value === 'active')?.id,
|
|
|
next_follow_date: '',
|
|
|
}
|
|
|
convertVisible.value = true
|
|
|
}
|
|
|
|
|
|
function onConvertStarChange() {
|
|
|
const item = starOptions.value.find((s) => s.id === convertForm.value.star_level_dict_item_id)
|
|
|
convertForm.value.next_follow_date = previewNextFollowDate(item?.value) || ''
|
|
|
}
|
|
|
|
|
|
function onUniversityPick(id: number | undefined) {
|
|
|
if (!id) return
|
|
|
const u = universityOptions.value.find((x) => x.id === id)
|
|
|
if (u?.city && !convertForm.value.city) {
|
|
|
convertForm.value.city = u.city
|
|
|
}
|
|
|
}
|
|
|
|
|
|
watch(
|
|
|
() => convertForm.value.university_id,
|
|
|
(id) => onUniversityPick(id),
|
|
|
)
|
|
|
|
|
|
async function saveConvert() {
|
|
|
if (!convertRow.value) return
|
|
|
const f = convertForm.value
|
|
|
if (!f.name.trim()) {
|
|
|
ElMessage.warning('请填写姓名')
|
|
|
return
|
|
|
}
|
|
|
if (!f.university_id) {
|
|
|
ElMessage.warning('请选择高校')
|
|
|
return
|
|
|
}
|
|
|
if (!f.city.trim() || !f.title.trim()) {
|
|
|
ElMessage.warning('请填写城市与职称')
|
|
|
return
|
|
|
}
|
|
|
if (!f.research_direction_ids.length) {
|
|
|
ElMessage.warning('请选择研究方向')
|
|
|
return
|
|
|
}
|
|
|
if (!f.status_dict_item_id) {
|
|
|
ElMessage.warning('请选择状态')
|
|
|
return
|
|
|
}
|
|
|
|
|
|
convertSaving.value = true
|
|
|
try {
|
|
|
const res = await convertMiniappUserToTeacher(convertRow.value.id, {
|
|
|
name: f.name.trim(),
|
|
|
university_id: f.university_id,
|
|
|
city: f.city.trim(),
|
|
|
title: f.title.trim(),
|
|
|
research_direction_ids: f.research_direction_ids,
|
|
|
phone: f.phone || null,
|
|
|
email: f.email || null,
|
|
|
star_level_dict_item_id: f.star_level_dict_item_id ?? null,
|
|
|
status_dict_item_id: f.status_dict_item_id,
|
|
|
next_follow_date: f.next_follow_date || null,
|
|
|
recalc_next_follow_date: true,
|
|
|
})
|
|
|
ElMessage.success(`已转入老师库(老师 ID:${res.teacher_id})`)
|
|
|
convertVisible.value = false
|
|
|
await load()
|
|
|
} finally {
|
|
|
convertSaving.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
usePageLoad(async () => {
|
|
|
await loadDicts()
|
|
|
await load()
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
<div class="list-page students-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="searchList"
|
|
|
/>
|
|
|
<el-select v-model="filterConverted" placeholder="转入状态" clearable class="filter-select-wide">
|
|
|
<el-option label="未转入老师库" value="0" />
|
|
|
<el-option label="已转入老师库" value="1" />
|
|
|
</el-select>
|
|
|
<el-button type="primary" class="btn-create" @click="searchList">搜索</el-button>
|
|
|
<el-button @click="resetFilters">重置</el-button>
|
|
|
</div>
|
|
|
|
|
|
<el-table v-loading="loading" :data="items" row-key="id">
|
|
|
<el-table-column prop="name" label="姓名" width="100" />
|
|
|
<el-table-column prop="mobile" label="手机号" width="120" />
|
|
|
<el-table-column prop="company" label="公司" min-width="140" show-overflow-tooltip />
|
|
|
<el-table-column label="研究方向" min-width="160" show-overflow-tooltip>
|
|
|
<template #default="{ row }">
|
|
|
{{ row.research_direction || '—' }}
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="报名课程" min-width="160" show-overflow-tooltip>
|
|
|
<template #default="{ row }">
|
|
|
{{ formatEnrollments(row.course_titles) }}
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="报名活动" min-width="160" show-overflow-tooltip>
|
|
|
<template #default="{ row }">
|
|
|
{{ formatEnrollments(row.activity_titles) }}
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="转入状态" width="110" align="center">
|
|
|
<template #default="{ row }">
|
|
|
<span v-if="row.teacher_id" class="status-badge status-published">已转入</span>
|
|
|
<span v-else class="status-badge status-unpublished">未转入</span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="操作" width="200" fixed="right">
|
|
|
<template #default="{ row }">
|
|
|
<div class="table-row-actions">
|
|
|
<el-button size="small" class="btn-action-secondary table-inline-btn" @click="openDetail(row)">
|
|
|
详情
|
|
|
</el-button>
|
|
|
<el-button
|
|
|
v-if="!row.teacher_id"
|
|
|
size="small"
|
|
|
class="btn-action-brand table-inline-btn"
|
|
|
@click="openConvert(row)"
|
|
|
>
|
|
|
转入老师库
|
|
|
</el-button>
|
|
|
<el-button
|
|
|
v-else
|
|
|
size="small"
|
|
|
class="btn-action-secondary table-inline-btn"
|
|
|
disabled
|
|
|
>
|
|
|
已转入
|
|
|
</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>
|
|
|
|
|
|
<el-dialog v-model="detailVisible" title="学员详情" width="720px" destroy-on-close>
|
|
|
<template v-if="detail">
|
|
|
<el-descriptions :column="2" border size="small">
|
|
|
<el-descriptions-item label="姓名">{{ detail.name }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="手机号">{{ detail.mobile || '—' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="公司" :span="2">{{ detail.company || '—' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="职务">{{ detail.job_title || '—' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="研究方向">{{ detail.research_direction || '—' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="微信昵称">{{ detail.nickname || '—' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="转入老师">
|
|
|
{{ detail.teacher_name ? `${detail.teacher_name}(ID ${detail.teacher_id})` : '未转入' }}
|
|
|
</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
|
|
|
<h3 class="sub-title">报名课程</h3>
|
|
|
<el-table :data="detail.courses" size="small" empty-text="暂无报名">
|
|
|
<el-table-column prop="title" label="课程" min-width="180" />
|
|
|
<el-table-column prop="signed_up_at" label="报名时间" width="110" />
|
|
|
<el-table-column prop="company" label="报名时公司" width="140" show-overflow-tooltip />
|
|
|
</el-table>
|
|
|
|
|
|
<h3 class="sub-title">报名活动</h3>
|
|
|
<el-table :data="detail.activities" size="small" empty-text="暂无报名">
|
|
|
<el-table-column prop="title" label="活动" min-width="180" />
|
|
|
<el-table-column prop="signed_up_at" label="报名时间" width="110" />
|
|
|
<el-table-column prop="company" label="报名时公司" width="140" show-overflow-tooltip />
|
|
|
</el-table>
|
|
|
</template>
|
|
|
<template #footer>
|
|
|
<el-button @click="detailVisible = false">关闭</el-button>
|
|
|
<el-button
|
|
|
v-if="detail && !detail.teacher_id"
|
|
|
type="primary"
|
|
|
class="btn-create"
|
|
|
@click="detailVisible = false; openConvert(detail)"
|
|
|
>
|
|
|
转入老师库
|
|
|
</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
<el-dialog
|
|
|
v-model="convertVisible"
|
|
|
title="转入老师库"
|
|
|
width="960px"
|
|
|
destroy-on-close
|
|
|
@closed="convertRow = null"
|
|
|
>
|
|
|
<div v-if="convertRow" class="follow-teacher-summary">
|
|
|
学员:{{ convertRow.name }}
|
|
|
<span v-if="convertRow.mobile"> · {{ convertRow.mobile }}</span>
|
|
|
<span v-if="convertRow.company"> · {{ convertRow.company }}</span>
|
|
|
<span v-if="convertRow.research_direction"> · 研究方向:{{ convertRow.research_direction }}</span>
|
|
|
</div>
|
|
|
<el-form label-position="top" class="form-small" style="margin-top: 12px">
|
|
|
<el-row :gutter="12">
|
|
|
<el-col :xs="24" :md="4">
|
|
|
<el-form-item label="姓名" required>
|
|
|
<el-input v-model="convertForm.name" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="8">
|
|
|
<el-form-item label="高校" required>
|
|
|
<el-select
|
|
|
v-model="convertForm.university_id"
|
|
|
filterable
|
|
|
placeholder="选择高校"
|
|
|
style="width: 100%"
|
|
|
>
|
|
|
<el-option v-for="u in universityOptions" :key="u.id" :label="u.name" :value="u.id" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="4">
|
|
|
<el-form-item label="城市" required>
|
|
|
<el-input v-model="convertForm.city" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="8">
|
|
|
<el-form-item label="职称" required>
|
|
|
<el-input v-model="convertForm.title" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="8">
|
|
|
<el-form-item label="研究方向" required>
|
|
|
<el-select
|
|
|
v-model="convertForm.research_direction_ids"
|
|
|
multiple
|
|
|
filterable
|
|
|
collapse-tags
|
|
|
collapse-tags-tooltip
|
|
|
placeholder="选择研究方向(可多选)"
|
|
|
style="width: 100%"
|
|
|
>
|
|
|
<el-option v-for="d in directionOptions" :key="d.id" :label="d.name" :value="d.id" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="6">
|
|
|
<el-form-item label="电话">
|
|
|
<el-input v-model="convertForm.phone" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="6">
|
|
|
<el-form-item label="邮箱">
|
|
|
<el-input v-model="convertForm.email" type="email" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="4">
|
|
|
<el-form-item label="星级">
|
|
|
<el-select
|
|
|
v-model="convertForm.star_level_dict_item_id"
|
|
|
style="width: 100%"
|
|
|
@change="onConvertStarChange"
|
|
|
>
|
|
|
<el-option v-for="o in starOptions" :key="o.id" :label="o.label" :value="o.id" />
|
|
|
</el-select>
|
|
|
<span class="star-preview">
|
|
|
{{
|
|
|
starDisplay(
|
|
|
starOptions.find((s) => s.id === convertForm.star_level_dict_item_id)?.value,
|
|
|
starOptions.find((s) => s.id === convertForm.star_level_dict_item_id)?.label,
|
|
|
)
|
|
|
}}
|
|
|
</span>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="6">
|
|
|
<el-form-item label="状态" required>
|
|
|
<el-select v-model="convertForm.status_dict_item_id" placeholder="请选择状态" 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 :xs="24" :md="6">
|
|
|
<el-form-item label="下次跟进日期">
|
|
|
<el-date-picker
|
|
|
v-model="convertForm.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="convertVisible = false">取消</el-button>
|
|
|
<el-button type="primary" class="btn-create" :loading="convertSaving" @click="saveConvert">
|
|
|
确认转入老师库
|
|
|
</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<style scoped>
|
|
|
.follow-teacher-summary {
|
|
|
padding: 10px 12px;
|
|
|
border-left: 4px solid var(--brand-primary, #b11e23);
|
|
|
background: #fdf3f3;
|
|
|
border-radius: 6px;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
.sub-title {
|
|
|
margin: 16px 0 8px;
|
|
|
font-size: 15px;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
.star-preview {
|
|
|
display: block;
|
|
|
margin-top: 4px;
|
|
|
color: #e6a23c;
|
|
|
font-size: 13px;
|
|
|
}
|
|
|
.form-small :deep(.el-form-item__label) {
|
|
|
font-size: 13px;
|
|
|
padding-bottom: 4px;
|
|
|
}
|
|
|
</style>
|