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.

464 lines
16 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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>