|
|
<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 { createDemand } from '@/api/admin/demands'
|
|
|
import TeacherDetailDialog from '@/views/teachers/components/TeacherDetailDialog.vue'
|
|
|
import TeacherPaperDialog from '@/components/TeacherPaperDialog.vue'
|
|
|
import { fetchAdminUsers } from '@/api/admin/users'
|
|
|
import type { DictItemBrief } from '@/api/admin/teachers'
|
|
|
import {
|
|
|
batchUpdateTeacherStar,
|
|
|
createTeacher,
|
|
|
createTeacherFollowRecord,
|
|
|
createUniversity,
|
|
|
fetchTeacherFilterOptions,
|
|
|
fetchTeacherFollowRecords,
|
|
|
fetchTeachersList,
|
|
|
fetchTeacherStats,
|
|
|
fetchUniversities,
|
|
|
type FollowRecordRow,
|
|
|
type TeacherRow,
|
|
|
type TeacherStats,
|
|
|
} from '@/api/admin/teachers'
|
|
|
import { followRuleHint, previewNextFollowDate } from '@/utils/teacherFollowRule'
|
|
|
import { starDisplay, sourceTagType, statusTagType, urgencyTagType } from '@/utils/teacherStar'
|
|
|
import { useAuthStore } from '@/stores/auth'
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
|
const auth = useAuthStore()
|
|
|
const isGridMember = computed(() => auth.isGridMember)
|
|
|
|
|
|
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 detailVisible = ref(false)
|
|
|
const detailTeacherId = ref<number | null>(null)
|
|
|
const items = ref<TeacherRow[]>([])
|
|
|
const meta = ref({ current_page: 1, per_page: 20, total: 0 })
|
|
|
const page = ref(1)
|
|
|
const stats = ref<TeacherStats>({ month_pending: 0, month_followed: 0, overdue: 0, partners: 0 })
|
|
|
|
|
|
const keyword = ref('')
|
|
|
const filterSource = ref<number | ''>('')
|
|
|
const filterStar = ref<number | ''>('')
|
|
|
const filterStatus = ref<number | ''>('')
|
|
|
const filterUniversity = ref<number | ''>('')
|
|
|
const filterDirection = ref<number | ''>('')
|
|
|
const statBucket = ref('')
|
|
|
|
|
|
const sourceOptions = ref<DictItemBrief[]>([])
|
|
|
const starOptions = ref<DictItemBrief[]>([])
|
|
|
const statusOptions = ref<DictItemBrief[]>([])
|
|
|
const methodOptions = ref<DictItemBrief[]>([])
|
|
|
const urgencyOptions = ref<DictItemBrief[]>([])
|
|
|
const universityOptions = ref<{ id: number; name: string; city?: string | null }[]>([])
|
|
|
const directionOptions = ref<{ id: number; name: string }[]>([])
|
|
|
const adminOptions = ref<{ id: number; label: string }[]>([])
|
|
|
const demandTypeOptions = ref<DictItemBrief[]>([])
|
|
|
|
|
|
const selectedRows = ref<TeacherRow[]>([])
|
|
|
const selectedIds = computed(() => selectedRows.value.map((r) => r.id))
|
|
|
const selectedNamesText = computed(() => selectedRows.value.map((r) => r.name).join('、'))
|
|
|
|
|
|
const createDialog = ref(false)
|
|
|
const createForm = ref({
|
|
|
name: '',
|
|
|
university_id: undefined as number | undefined,
|
|
|
city: '',
|
|
|
title: '',
|
|
|
department: '',
|
|
|
bio: '',
|
|
|
research_direction_values: [] as Array<number | string>,
|
|
|
phone: '',
|
|
|
email: '',
|
|
|
source_dict_item_id: undefined as number | undefined,
|
|
|
star_level_dict_item_id: undefined as number | undefined,
|
|
|
status_dict_item_id: undefined as number | undefined,
|
|
|
})
|
|
|
|
|
|
const universityDialog = ref(false)
|
|
|
const universitySaving = ref(false)
|
|
|
const universityForm = ref({
|
|
|
name: '',
|
|
|
city: '',
|
|
|
province: '',
|
|
|
longitude: '',
|
|
|
latitude: '',
|
|
|
})
|
|
|
|
|
|
const batchDialog = ref(false)
|
|
|
const batchStarId = ref<number | undefined>()
|
|
|
const batchStarRule = computed(() => {
|
|
|
const item = starOptions.value.find((s) => s.id === batchStarId.value)
|
|
|
return followRuleHint(item?.value)
|
|
|
})
|
|
|
|
|
|
const followDialog = ref(false)
|
|
|
const followTeacher = ref<TeacherRow | null>(null)
|
|
|
const followNextDateRule = computed(() => followRuleHint(followTeacher.value?.star_level_item?.value))
|
|
|
const followForm = ref({
|
|
|
subject: '',
|
|
|
content: '',
|
|
|
followed_at: new Date().toISOString().slice(0, 10),
|
|
|
follow_method_dict_item_id: undefined as number | undefined,
|
|
|
urgency_dict_item_id: undefined as number | undefined,
|
|
|
admin_user_id: undefined as number | undefined,
|
|
|
next_follow_subject: '',
|
|
|
next_follow_date: '',
|
|
|
})
|
|
|
|
|
|
const recordsDialog = ref(false)
|
|
|
const recordsTeacher = ref<TeacherRow | null>(null)
|
|
|
const records = ref<FollowRecordRow[]>([])
|
|
|
|
|
|
const paperDialog = ref(false)
|
|
|
const paperTeacher = ref<TeacherRow | null>(null)
|
|
|
|
|
|
const demandDialog = ref(false)
|
|
|
const demandTeacher = ref<TeacherRow | null>(null)
|
|
|
const demandForm = ref({
|
|
|
type_dict_item_id: undefined as number | undefined,
|
|
|
title: '',
|
|
|
content: '',
|
|
|
})
|
|
|
|
|
|
const manualSourceId = computed(() => sourceOptions.value.find((o) => o.value === 'manual')?.id)
|
|
|
const pendingStarId = computed(() => starOptions.value.find((s) => s.value === 'pending')?.id)
|
|
|
|
|
|
function onUniversityPick(id: number | undefined) {
|
|
|
if (!id) return
|
|
|
const u = universityOptions.value.find((x) => x.id === id)
|
|
|
if (u?.city && !createForm.value.city) {
|
|
|
createForm.value.city = u.city
|
|
|
}
|
|
|
}
|
|
|
|
|
|
watch(
|
|
|
() => createForm.value.university_id,
|
|
|
(id) => onUniversityPick(id),
|
|
|
)
|
|
|
|
|
|
async function loadDicts() {
|
|
|
const [source, star, status, method, urgency, demandType, uni, admins, filters] = await Promise.all([
|
|
|
fetchDictByCode('teacher_source'),
|
|
|
fetchDictByCode('teacher_level'),
|
|
|
fetchDictByCode('teacher_status'),
|
|
|
fetchDictByCode('follow_method'),
|
|
|
fetchDictByCode('follow_urgency'),
|
|
|
fetchDictByCode('demand_type'),
|
|
|
fetchUniversities({ page: 1, page_size: 200 }),
|
|
|
fetchAdminUsers({ page: 1, page_size: 200, status: 1 }),
|
|
|
fetchTeacherFilterOptions(),
|
|
|
])
|
|
|
sourceOptions.value = source.items
|
|
|
starOptions.value = star.items
|
|
|
statusOptions.value = status.items
|
|
|
methodOptions.value = method.items
|
|
|
urgencyOptions.value = urgency.items
|
|
|
demandTypeOptions.value = demandType.items
|
|
|
const scopeIds = auth.user?.grid_scope?.university_ids
|
|
|
universityOptions.value = scopeIds?.length
|
|
|
? uni.items.filter((u) => scopeIds.includes(u.id))
|
|
|
: uni.items
|
|
|
const scopeDirIds = auth.user?.grid_scope?.research_direction_ids
|
|
|
directionOptions.value = scopeDirIds?.length
|
|
|
? filters.research_directions.filter((d) => scopeDirIds.includes(d.id))
|
|
|
: filters.research_directions
|
|
|
adminOptions.value = admins.items.map((u) => ({
|
|
|
id: u.id,
|
|
|
label: u.real_name || u.username,
|
|
|
}))
|
|
|
}
|
|
|
|
|
|
async function loadStats() {
|
|
|
stats.value = await fetchTeacherStats()
|
|
|
}
|
|
|
|
|
|
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 (filterSource.value !== '') params.source_dict_item_id = filterSource.value
|
|
|
if (filterStar.value !== '') params.star_level_dict_item_id = filterStar.value
|
|
|
if (filterStatus.value !== '') params.status_dict_item_id = filterStatus.value
|
|
|
if (filterUniversity.value !== '') params.university_id = filterUniversity.value
|
|
|
if (filterDirection.value !== '') params.research_direction_id = filterDirection.value
|
|
|
if (statBucket.value) params.stat_bucket = statBucket.value
|
|
|
const res = await fetchTeachersList(params)
|
|
|
items.value = res.items
|
|
|
meta.value = res.meta
|
|
|
selectedRows.value = []
|
|
|
const dirs = await fetchTeacherFilterOptions()
|
|
|
directionOptions.value = dirs.research_directions
|
|
|
} finally {
|
|
|
loading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function resetFilters() {
|
|
|
keyword.value = ''
|
|
|
filterSource.value = ''
|
|
|
filterStar.value = ''
|
|
|
filterStatus.value = ''
|
|
|
filterUniversity.value = ''
|
|
|
filterDirection.value = ''
|
|
|
statBucket.value = ''
|
|
|
page.value = 1
|
|
|
load()
|
|
|
}
|
|
|
|
|
|
function searchTeachers() {
|
|
|
page.value = 1
|
|
|
load()
|
|
|
}
|
|
|
|
|
|
function pickStat(bucket: string) {
|
|
|
statBucket.value = statBucket.value === bucket ? '' : bucket
|
|
|
page.value = 1
|
|
|
load()
|
|
|
}
|
|
|
|
|
|
function goDetail(row: TeacherRow) {
|
|
|
detailTeacherId.value = row.id
|
|
|
detailVisible.value = true
|
|
|
}
|
|
|
|
|
|
async function refreshDirectionOptions() {
|
|
|
const filters = await fetchTeacherFilterOptions()
|
|
|
directionOptions.value = filters.research_directions
|
|
|
}
|
|
|
|
|
|
function openUniversityCreate() {
|
|
|
universityForm.value = {
|
|
|
name: '',
|
|
|
city: createForm.value.city || '',
|
|
|
province: '',
|
|
|
longitude: '',
|
|
|
latitude: '',
|
|
|
}
|
|
|
universityDialog.value = true
|
|
|
}
|
|
|
|
|
|
async function saveUniversityCreate() {
|
|
|
const f = universityForm.value
|
|
|
if (!f.name.trim()) {
|
|
|
ElMessage.warning('请填写高校名称')
|
|
|
return
|
|
|
}
|
|
|
if (!f.longitude.trim() || !f.latitude.trim()) {
|
|
|
ElMessage.warning('请填写经度与纬度')
|
|
|
return
|
|
|
}
|
|
|
const longitude = Number(f.longitude)
|
|
|
const latitude = Number(f.latitude)
|
|
|
if (Number.isNaN(longitude) || Number.isNaN(latitude)) {
|
|
|
ElMessage.warning('经纬度须为有效数字')
|
|
|
return
|
|
|
}
|
|
|
universitySaving.value = true
|
|
|
try {
|
|
|
const u = await createUniversity({
|
|
|
name: f.name.trim(),
|
|
|
city: f.city.trim() || null,
|
|
|
province: f.province.trim() || null,
|
|
|
longitude,
|
|
|
latitude,
|
|
|
})
|
|
|
if (!universityOptions.value.some((x) => x.id === u.id)) {
|
|
|
universityOptions.value.push(u)
|
|
|
}
|
|
|
createForm.value.university_id = u.id
|
|
|
if (u.city) {
|
|
|
createForm.value.city = u.city
|
|
|
}
|
|
|
universityDialog.value = false
|
|
|
ElMessage.success('高校已创建')
|
|
|
} finally {
|
|
|
universitySaving.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function splitDirectionValues(values: Array<number | string>) {
|
|
|
const ids: number[] = []
|
|
|
const names: string[] = []
|
|
|
for (const value of values) {
|
|
|
if (typeof value === 'number') {
|
|
|
ids.push(value)
|
|
|
continue
|
|
|
}
|
|
|
const name = String(value).trim()
|
|
|
if (name) names.push(name)
|
|
|
}
|
|
|
return { ids, names }
|
|
|
}
|
|
|
|
|
|
async function openCreate() {
|
|
|
createForm.value = {
|
|
|
name: '',
|
|
|
university_id: undefined,
|
|
|
city: '',
|
|
|
title: '',
|
|
|
department: '',
|
|
|
bio: '',
|
|
|
research_direction_values: [],
|
|
|
phone: '',
|
|
|
email: '',
|
|
|
source_dict_item_id: manualSourceId.value,
|
|
|
star_level_dict_item_id: pendingStarId.value,
|
|
|
status_dict_item_id: statusOptions.value.find((s) => s.value === 'active')?.id,
|
|
|
}
|
|
|
await refreshDirectionOptions()
|
|
|
createDialog.value = true
|
|
|
}
|
|
|
|
|
|
async function saveCreate() {
|
|
|
const f = createForm.value
|
|
|
if (!f.name.trim()) {
|
|
|
ElMessage.warning('请填写姓名')
|
|
|
return
|
|
|
}
|
|
|
const directions = splitDirectionValues(f.research_direction_values)
|
|
|
if (!directions.ids.length && !directions.names.length) {
|
|
|
ElMessage.warning('请至少选择一个或新增研究方向')
|
|
|
return
|
|
|
}
|
|
|
if (!f.city.trim()) {
|
|
|
ElMessage.warning('请填写城市')
|
|
|
return
|
|
|
}
|
|
|
if (!f.title.trim()) {
|
|
|
ElMessage.warning('请填写职称')
|
|
|
return
|
|
|
}
|
|
|
if (!f.status_dict_item_id) {
|
|
|
ElMessage.warning('请选择状态')
|
|
|
return
|
|
|
}
|
|
|
|
|
|
if (!f.university_id) {
|
|
|
ElMessage.warning('请选择高校,或点击「新建高校」')
|
|
|
return
|
|
|
}
|
|
|
|
|
|
await createTeacher({
|
|
|
name: f.name.trim(),
|
|
|
university_id: f.university_id,
|
|
|
city: f.city.trim(),
|
|
|
title: f.title.trim(),
|
|
|
department: f.department.trim() || null,
|
|
|
bio: f.bio.trim() || null,
|
|
|
research_direction_ids: directions.ids,
|
|
|
new_research_directions: directions.names,
|
|
|
phone: f.phone || null,
|
|
|
email: f.email || null,
|
|
|
source_dict_item_id: f.source_dict_item_id ?? manualSourceId.value,
|
|
|
star_level_dict_item_id: f.star_level_dict_item_id ?? pendingStarId.value,
|
|
|
status_dict_item_id: f.status_dict_item_id,
|
|
|
recalc_next_follow_date: true,
|
|
|
})
|
|
|
ElMessage.success('已录入')
|
|
|
createDialog.value = false
|
|
|
await Promise.all([load(), loadStats()])
|
|
|
}
|
|
|
|
|
|
function openBatch() {
|
|
|
if (!selectedRows.value.length) {
|
|
|
ElMessage.warning('请先在列表中勾选需要改星的老师')
|
|
|
return
|
|
|
}
|
|
|
batchStarId.value = undefined
|
|
|
batchDialog.value = true
|
|
|
}
|
|
|
|
|
|
async function saveBatch() {
|
|
|
if (!batchStarId.value) {
|
|
|
ElMessage.warning('请选择目标星级')
|
|
|
return
|
|
|
}
|
|
|
await batchUpdateTeacherStar({
|
|
|
ids: selectedIds.value,
|
|
|
star_level_dict_item_id: batchStarId.value,
|
|
|
recalc_next_follow_date: true,
|
|
|
})
|
|
|
ElMessage.success('已批量更新')
|
|
|
batchDialog.value = false
|
|
|
await Promise.all([load(), loadStats()])
|
|
|
}
|
|
|
|
|
|
function openFollow(row: TeacherRow) {
|
|
|
followTeacher.value = row
|
|
|
followForm.value = {
|
|
|
subject: '',
|
|
|
content: '',
|
|
|
followed_at: new Date().toISOString().slice(0, 10),
|
|
|
follow_method_dict_item_id: methodOptions.value[0]?.id,
|
|
|
urgency_dict_item_id: urgencyOptions.value.find((u) => u.value === 'normal')?.id,
|
|
|
admin_user_id: defaultFollowAdminUserId(),
|
|
|
next_follow_subject: '',
|
|
|
next_follow_date: previewNextFollowDate(row.star_level_item?.value) || '',
|
|
|
}
|
|
|
followDialog.value = true
|
|
|
}
|
|
|
|
|
|
async function saveFollow() {
|
|
|
const f = followForm.value
|
|
|
if (!followTeacher.value) return
|
|
|
if (!f.subject.trim() || !f.next_follow_subject.trim() || !f.next_follow_date) {
|
|
|
ElMessage.warning('请填写必填项')
|
|
|
return
|
|
|
}
|
|
|
if (!f.follow_method_dict_item_id || !f.urgency_dict_item_id || !f.admin_user_id) {
|
|
|
ElMessage.warning('请填写跟进方式、紧急程度与跟进人员')
|
|
|
return
|
|
|
}
|
|
|
await createTeacherFollowRecord(followTeacher.value.id, { ...f })
|
|
|
ElMessage.success('已保存跟进')
|
|
|
followDialog.value = false
|
|
|
await Promise.all([load(), loadStats()])
|
|
|
}
|
|
|
|
|
|
async function openRecords(row: TeacherRow) {
|
|
|
recordsTeacher.value = row
|
|
|
records.value = await fetchTeacherFollowRecords(row.id)
|
|
|
recordsDialog.value = true
|
|
|
}
|
|
|
|
|
|
function openPaper(row: TeacherRow) {
|
|
|
paperTeacher.value = row
|
|
|
paperDialog.value = true
|
|
|
}
|
|
|
|
|
|
function openDemand(row: TeacherRow) {
|
|
|
demandTeacher.value = row
|
|
|
demandForm.value = { type_dict_item_id: undefined, title: '', content: '' }
|
|
|
demandDialog.value = true
|
|
|
}
|
|
|
|
|
|
async function saveDemand() {
|
|
|
if (!demandTeacher.value) return
|
|
|
if (!demandForm.value.type_dict_item_id || !demandForm.value.title.trim() || !demandForm.value.content.trim()) {
|
|
|
ElMessage.warning('请填写需求类型、标题与详细描述')
|
|
|
return
|
|
|
}
|
|
|
await createDemand({
|
|
|
teacher_id: demandTeacher.value.id,
|
|
|
type_dict_item_id: demandForm.value.type_dict_item_id,
|
|
|
title: demandForm.value.title.trim(),
|
|
|
content: demandForm.value.content.trim(),
|
|
|
})
|
|
|
ElMessage.success('已发布需求')
|
|
|
demandDialog.value = false
|
|
|
}
|
|
|
|
|
|
usePageLoad(async () => {
|
|
|
await loadDicts()
|
|
|
await Promise.all([loadStats(), load()])
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
<div class="list-page teachers-page">
|
|
|
<div class="page-header">
|
|
|
<PageTitle />
|
|
|
<div class="page-header-actions">
|
|
|
<el-button class="btn-action-secondary" size="small" @click="openBatch">批量改星</el-button>
|
|
|
<el-button
|
|
|
v-if="!isGridMember"
|
|
|
type="primary"
|
|
|
size="small"
|
|
|
class="teachers-primary-btn"
|
|
|
@click="openCreate"
|
|
|
>
|
|
|
手动录入
|
|
|
</el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="talent-stat-grid">
|
|
|
<button
|
|
|
type="button"
|
|
|
class="talent-stat-card"
|
|
|
:class="{ 'is-active': statBucket === 'month_pending' }"
|
|
|
@click="pickStat('month_pending')"
|
|
|
>
|
|
|
<div class="talent-stat-label">本月待跟进数量</div>
|
|
|
<div class="talent-stat-value">{{ stats.month_pending }}</div>
|
|
|
</button>
|
|
|
<button
|
|
|
type="button"
|
|
|
class="talent-stat-card"
|
|
|
:class="{ 'is-active': statBucket === 'month_followed' }"
|
|
|
@click="pickStat('month_followed')"
|
|
|
>
|
|
|
<div class="talent-stat-label">本月已跟进</div>
|
|
|
<div class="talent-stat-value is-dark">{{ stats.month_followed }}</div>
|
|
|
</button>
|
|
|
<button
|
|
|
type="button"
|
|
|
class="talent-stat-card"
|
|
|
:class="{ 'is-active': statBucket === 'overdue' }"
|
|
|
@click="pickStat('overdue')"
|
|
|
>
|
|
|
<div class="talent-stat-label">已逾期</div>
|
|
|
<div class="talent-stat-value is-danger">{{ stats.overdue }}</div>
|
|
|
</button>
|
|
|
<button
|
|
|
type="button"
|
|
|
class="talent-stat-card"
|
|
|
:class="{ 'is-active': statBucket === 'partner' }"
|
|
|
@click="pickStat('partner')"
|
|
|
>
|
|
|
<div class="talent-stat-label">入孵用户数量</div>
|
|
|
<div class="talent-stat-value is-success">{{ stats.partners }}</div>
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
<el-card shadow="never" class="admin-list-card teachers-list-card">
|
|
|
<div class="list-filter-bar">
|
|
|
<el-input
|
|
|
v-model="keyword"
|
|
|
class="filter-search teachers-filter-search"
|
|
|
placeholder="搜索姓名、高校、学院、简介、研究方向…"
|
|
|
clearable
|
|
|
@keyup.enter="searchTeachers"
|
|
|
/>
|
|
|
<el-select v-model="filterSource" class="filter-source" placeholder="来源" clearable>
|
|
|
<el-option v-for="o in sourceOptions" :key="o.id" :label="o.label" :value="o.id" />
|
|
|
</el-select>
|
|
|
<el-select v-model="filterStar" class="filter-stars" placeholder="星级" clearable>
|
|
|
<el-option v-for="o in starOptions" :key="o.id" :label="o.label" :value="o.id" />
|
|
|
</el-select>
|
|
|
<el-select v-model="filterStatus" class="filter-status" placeholder="状态" clearable>
|
|
|
<el-option v-for="o in statusOptions" :key="o.id" :label="o.label" :value="o.id" />
|
|
|
</el-select>
|
|
|
<el-select
|
|
|
v-model="filterUniversity"
|
|
|
class="filter-school"
|
|
|
placeholder="学校"
|
|
|
clearable
|
|
|
filterable
|
|
|
>
|
|
|
<el-option v-for="u in universityOptions" :key="u.id" :label="u.name" :value="u.id" />
|
|
|
</el-select>
|
|
|
<el-select
|
|
|
v-model="filterDirection"
|
|
|
class="filter-direction"
|
|
|
placeholder="研究方向"
|
|
|
clearable
|
|
|
filterable
|
|
|
>
|
|
|
<el-option v-for="d in directionOptions" :key="d.id" :label="d.name" :value="d.id" />
|
|
|
</el-select>
|
|
|
<el-button type="primary" @click="searchTeachers">搜索</el-button>
|
|
|
<el-button @click="resetFilters">重置</el-button>
|
|
|
</div>
|
|
|
|
|
|
<el-table
|
|
|
v-loading="loading"
|
|
|
:data="items"
|
|
|
row-key="id"
|
|
|
@selection-change="(rows: TeacherRow[]) => (selectedRows = rows)"
|
|
|
>
|
|
|
<el-table-column type="selection" width="44" />
|
|
|
<el-table-column prop="name" label="姓名" width="108">
|
|
|
<template #default="{ row }">
|
|
|
<a class="name-link" href="#" @click.prevent="goDetail(row)">{{ row.name }}</a>
|
|
|
<el-tag v-if="row.is_overdue" type="danger" size="small" class="tag-overdue">逾期</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="university_name" label="高校" min-width="130" />
|
|
|
<el-table-column prop="department" label="所属学院" min-width="120" show-overflow-tooltip />
|
|
|
<el-table-column prop="title" label="职称" width="96" />
|
|
|
<el-table-column label="研究方向" min-width="160">
|
|
|
<template #default="{ row }">
|
|
|
<template v-if="row.research_directions?.length">
|
|
|
<el-tag
|
|
|
v-for="d in row.research_directions"
|
|
|
:key="d.id"
|
|
|
size="small"
|
|
|
type="info"
|
|
|
style="margin: 2px 4px 2px 0"
|
|
|
>
|
|
|
{{ d.name }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
<span v-else class="text-muted">—</span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="来源" width="108" align="center">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag
|
|
|
:type="sourceTagType(row.source_item?.value)"
|
|
|
size="small"
|
|
|
effect="dark"
|
|
|
class="source-tag"
|
|
|
:class="{ 'source-tag-manual': row.source_item?.value === 'manual' }"
|
|
|
>
|
|
|
{{ row.source_item?.label || '—' }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="星级" width="108">
|
|
|
<template #default="{ row }">
|
|
|
<span class="star-text" :title="row.star_level_item?.label || ''">
|
|
|
{{ starDisplay(row.star_level_item?.value, row.star_level_item?.label) }}
|
|
|
</span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="状态" width="100">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="statusTagType(row.status_item?.value)" size="small" effect="dark">
|
|
|
{{ row.status_item?.label || '—' }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="跟进记录" width="96" align="center">
|
|
|
<template #default="{ row }">
|
|
|
<el-button size="small" class="btn-action-secondary teachers-table-btn" @click="openRecords(row)">
|
|
|
{{ row.follow_records_count || 0 }}条记录
|
|
|
</el-button>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="操作" width="176" fixed="right">
|
|
|
<template #default="{ row }">
|
|
|
<div class="table-row-actions teachers-table-actions">
|
|
|
<el-button size="small" class="btn-action-info teachers-table-btn" @click="openFollow(row)">
|
|
|
跟进
|
|
|
</el-button>
|
|
|
<el-button size="small" class="btn-action-primary teachers-table-btn" @click="openPaper(row)">
|
|
|
论文
|
|
|
</el-button>
|
|
|
<el-button
|
|
|
v-if="row.is_partner || row.status_item?.value === 'partner'"
|
|
|
type="primary"
|
|
|
size="small"
|
|
|
class="teachers-primary-btn teachers-table-btn"
|
|
|
@click="openDemand(row)"
|
|
|
>
|
|
|
需求
|
|
|
</el-button>
|
|
|
<el-button size="small" class="btn-action-primary teachers-table-btn" @click="goDetail(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>
|
|
|
|
|
|
<!-- 手动录入:对齐 createTeacherModal -->
|
|
|
<el-dialog v-model="createDialog" title="手动录入老师" width="960px" destroy-on-close>
|
|
|
<el-form label-position="top" class="form-small">
|
|
|
<el-row :gutter="12">
|
|
|
<el-col :xs="24" :md="4">
|
|
|
<el-form-item label="姓名" required>
|
|
|
<el-input v-model="createForm.name" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="8">
|
|
|
<el-form-item label="高校" required>
|
|
|
<div class="uni-row">
|
|
|
<el-select
|
|
|
v-model="createForm.university_id"
|
|
|
filterable
|
|
|
placeholder="选择高校"
|
|
|
style="flex: 1"
|
|
|
>
|
|
|
<el-option v-for="u in universityOptions" :key="u.id" :label="u.name" :value="u.id" />
|
|
|
</el-select>
|
|
|
<el-button
|
|
|
v-if="!isGridMember"
|
|
|
type="primary"
|
|
|
size="small"
|
|
|
class="btn-create teachers-primary-btn"
|
|
|
@click="openUniversityCreate"
|
|
|
>
|
|
|
新建高校
|
|
|
</el-button>
|
|
|
</div>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="4">
|
|
|
<el-form-item label="城市" required>
|
|
|
<el-input v-model="createForm.city" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="8">
|
|
|
<el-form-item label="职称" required>
|
|
|
<el-input v-model="createForm.title" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="8">
|
|
|
<el-form-item label="所属学院">
|
|
|
<el-input v-model="createForm.department" placeholder="如:计算机科学与技术学院" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="16">
|
|
|
<el-form-item label="个人简介">
|
|
|
<el-input v-model="createForm.bio" type="textarea" :rows="3" placeholder="老师个人简介" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="8">
|
|
|
<el-form-item label="研究方向" required>
|
|
|
<el-select
|
|
|
v-model="createForm.research_direction_values"
|
|
|
multiple
|
|
|
filterable
|
|
|
allow-create
|
|
|
default-first-option
|
|
|
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="createForm.phone" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="6">
|
|
|
<el-form-item label="邮箱">
|
|
|
<el-input v-model="createForm.email" type="email" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="4">
|
|
|
<el-form-item label="星级">
|
|
|
<el-select v-model="createForm.star_level_dict_item_id" style="width: 100%">
|
|
|
<el-option v-for="o in starOptions" :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="状态" required>
|
|
|
<el-select v-model="createForm.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-row>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
<el-button @click="createDialog = false">取消</el-button>
|
|
|
<el-button type="primary" @click="saveCreate">保存</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
<el-dialog
|
|
|
v-model="universityDialog"
|
|
|
title="新建高校"
|
|
|
width="480px"
|
|
|
append-to-body
|
|
|
destroy-on-close
|
|
|
>
|
|
|
<el-form label-position="top" class="form-small">
|
|
|
<el-form-item label="高校名称" required>
|
|
|
<el-input v-model="universityForm.name" placeholder="如:复旦大学" />
|
|
|
</el-form-item>
|
|
|
<el-row :gutter="12">
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="经度" required>
|
|
|
<el-input v-model="universityForm.longitude" placeholder="如:121.5031" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="纬度" required>
|
|
|
<el-input v-model="universityForm.latitude" placeholder="如:31.2970" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<el-row :gutter="12">
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="省份">
|
|
|
<el-input v-model="universityForm.province" placeholder="如:上海市" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="城市">
|
|
|
<el-input v-model="universityForm.city" placeholder="如:上海" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
<el-button @click="universityDialog = false">取消</el-button>
|
|
|
<el-button type="primary" :loading="universitySaving" @click="saveUniversityCreate">保存</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
<!-- 批量改星 -->
|
|
|
<el-dialog v-model="batchDialog" title="批量修改星级" width="480px">
|
|
|
<el-alert
|
|
|
v-if="!selectedRows.length"
|
|
|
type="warning"
|
|
|
:closable="false"
|
|
|
title="请先在列表中勾选需要改星的老师。"
|
|
|
show-icon
|
|
|
/>
|
|
|
<template v-else>
|
|
|
<div class="follow-teacher-summary">
|
|
|
已选择 <span class="text-brand">{{ selectedRows.length }}</span> 位老师
|
|
|
<span v-if="selectedNamesText" class="names-muted">{{ selectedNamesText }}</span>
|
|
|
</div>
|
|
|
<el-form label-position="top" style="margin-top: 12px">
|
|
|
<el-form-item label="目标星级" required>
|
|
|
<el-select v-model="batchStarId" placeholder="请选择目标星级" style="width: 100%">
|
|
|
<el-option v-for="o in starOptions" :key="o.id" :label="o.label" :value="o.id" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
<p class="batch-rule">{{ batchStarRule }}</p>
|
|
|
</el-form>
|
|
|
</template>
|
|
|
<template #footer>
|
|
|
<el-button @click="batchDialog = false">取消</el-button>
|
|
|
<el-button type="primary" :disabled="!selectedRows.length" @click="saveBatch">
|
|
|
保存并重算跟进日
|
|
|
</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
<!-- 新增跟进 -->
|
|
|
<el-dialog v-model="followDialog" title="新增跟进" width="720px" destroy-on-close>
|
|
|
<div v-if="followTeacher" class="follow-teacher-summary">
|
|
|
{{ followTeacher.name }} · {{ followTeacher.university_name }} · {{ followTeacher.title }}
|
|
|
</div>
|
|
|
<el-form label-position="top" class="form-small" style="margin-top: 12px">
|
|
|
<el-row :gutter="12">
|
|
|
<el-col :span="10">
|
|
|
<el-form-item label="跟进主题" required>
|
|
|
<el-input v-model="followForm.subject" placeholder="请输入跟进主题" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="6">
|
|
|
<el-form-item label="跟进日期" required>
|
|
|
<el-date-picker
|
|
|
v-model="followForm.followed_at"
|
|
|
type="date"
|
|
|
value-format="YYYY-MM-DD"
|
|
|
style="width: 100%"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="4">
|
|
|
<el-form-item label="跟进方式" required>
|
|
|
<el-select v-model="followForm.follow_method_dict_item_id" style="width: 100%">
|
|
|
<el-option v-for="o in methodOptions" :key="o.id" :label="o.label" :value="o.id" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="4">
|
|
|
<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="24">
|
|
|
<el-form-item label="跟进记录">
|
|
|
<el-input
|
|
|
v-model="followForm.content"
|
|
|
type="textarea"
|
|
|
:rows="4"
|
|
|
placeholder="请输入跟进内容"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="下次跟进主题" required>
|
|
|
<el-input v-model="followForm.next_follow_subject" placeholder="请输入下次跟进主题" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="6">
|
|
|
<el-form-item label="下次跟进日期" required>
|
|
|
<el-date-picker
|
|
|
v-model="followForm.next_follow_date"
|
|
|
type="date"
|
|
|
value-format="YYYY-MM-DD"
|
|
|
style="width: 100%"
|
|
|
/>
|
|
|
<p v-if="followNextDateRule" class="follow-date-rule">{{ followNextDateRule }}</p>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="6">
|
|
|
<el-form-item label="紧急程度" required>
|
|
|
<el-select v-model="followForm.urgency_dict_item_id" placeholder="请选择紧急程度" style="width: 100%">
|
|
|
<el-option v-for="o in urgencyOptions" :key="o.id" :label="o.label" :value="o.id" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
<el-button @click="followDialog = false">取消</el-button>
|
|
|
<el-button type="primary" @click="saveFollow">保存</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
<!-- 跟进记录时间线 -->
|
|
|
<el-dialog v-model="recordsDialog" title="跟进记录" width="960px">
|
|
|
<div v-if="recordsTeacher" class="follow-teacher-summary">
|
|
|
<span>{{ recordsTeacher.name }}</span>
|
|
|
<span class="dot">·</span>
|
|
|
<span>{{ recordsTeacher.university_name }}</span>
|
|
|
<span class="dot">·</span>
|
|
|
<span>{{ recordsTeacher.title }}</span>
|
|
|
<span class="dot">·</span>
|
|
|
<span class="star-text">
|
|
|
{{ starDisplay(recordsTeacher.star_level_item?.value, recordsTeacher.star_level_item?.label) }}
|
|
|
</span>
|
|
|
</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.subject }}</span>
|
|
|
<span class="follow-history-date">{{ r.followed_at }}</span>
|
|
|
</div>
|
|
|
<el-tag
|
|
|
v-if="r.urgency_item"
|
|
|
:type="urgencyTagType((r.urgency_item as { value?: string }).value)"
|
|
|
size="small"
|
|
|
>
|
|
|
{{ r.urgency_item.label }}
|
|
|
</el-tag>
|
|
|
</div>
|
|
|
<div class="follow-history-body">
|
|
|
<div class="follow-history-row">
|
|
|
<span>跟进主题:</span>
|
|
|
<div>{{ r.subject }}</div>
|
|
|
</div>
|
|
|
<div class="follow-history-row">
|
|
|
<span>跟进方式:</span>
|
|
|
<div>{{ r.follow_method_item?.label || '—' }}</div>
|
|
|
</div>
|
|
|
<div class="follow-history-row">
|
|
|
<span>跟进记录:</span>
|
|
|
<div>{{ r.content || '—' }}</div>
|
|
|
</div>
|
|
|
<div class="follow-history-row">
|
|
|
<span>下次跟进:</span>
|
|
|
<div>主题:{{ r.next_follow_subject }}<br />日期:{{ r.next_follow_date }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="follow-history-footer">跟进人:{{ r.operator_name || '—' }}</div>
|
|
|
</div>
|
|
|
<el-empty v-if="!records.length" description="暂无跟进记录" />
|
|
|
</div>
|
|
|
<template #footer>
|
|
|
<el-button @click="recordsDialog = false">关闭</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
<TeacherPaperDialog
|
|
|
v-model="paperDialog"
|
|
|
:teacher-id="paperTeacher?.id ?? null"
|
|
|
:default-authors="paperTeacher?.name"
|
|
|
:default-school-name="paperTeacher?.university_name || ''"
|
|
|
@saved="load"
|
|
|
/>
|
|
|
|
|
|
<!-- 发布需求(伙伴老师,需求模块待接) -->
|
|
|
<el-dialog v-model="demandDialog" title="发布需求" width="640px" destroy-on-close>
|
|
|
<el-form label-position="top" class="form-small">
|
|
|
<el-row :gutter="12">
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="关联老师">
|
|
|
<el-input :model-value="demandTeacher?.name || ''" readonly />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="需求类型" required>
|
|
|
<el-select v-model="demandForm.type_dict_item_id" placeholder="请选择需求类型" style="width: 100%">
|
|
|
<el-option v-for="o in demandTypeOptions" :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="demandForm.title" placeholder="请输入需求标题" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="24">
|
|
|
<el-form-item label="详细描述" required>
|
|
|
<el-input
|
|
|
v-model="demandForm.content"
|
|
|
type="textarea"
|
|
|
:rows="5"
|
|
|
placeholder="请描述需求背景、目标、期望资源或合作方式"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
<el-button @click="demandDialog = false">取消</el-button>
|
|
|
<el-button type="primary" @click="saveDemand">发布</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
<TeacherDetailDialog v-model="detailVisible" :teacher-id="detailTeacherId" @saved="load" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<style scoped>
|
|
|
.teachers-page {
|
|
|
--brand-primary: #244e98;
|
|
|
--brand-primary-hover: #8b1519;
|
|
|
--brand-bg-soft: #fdf3f3;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 12px;
|
|
|
}
|
|
|
.talent-stat-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
gap: 14px;
|
|
|
}
|
|
|
.teachers-list-card {
|
|
|
margin-top: 0;
|
|
|
}
|
|
|
.teachers-filter-search {
|
|
|
width: 180px;
|
|
|
}
|
|
|
.talent-stat-card {
|
|
|
min-height: 68px;
|
|
|
padding: 10px 16px;
|
|
|
border: 1px solid #d8dde4;
|
|
|
border-radius: 8px;
|
|
|
background: #fff;
|
|
|
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.08);
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: flex-start;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
text-align: left;
|
|
|
position: relative;
|
|
|
cursor: pointer;
|
|
|
font: inherit;
|
|
|
}
|
|
|
.talent-stat-card:hover,
|
|
|
.talent-stat-card.is-active {
|
|
|
border-color: var(--brand-primary);
|
|
|
}
|
|
|
.talent-stat-card.is-active::before {
|
|
|
content: '';
|
|
|
position: absolute;
|
|
|
left: 0;
|
|
|
top: 0;
|
|
|
bottom: 0;
|
|
|
width: 4px;
|
|
|
background: var(--brand-primary);
|
|
|
}
|
|
|
.talent-stat-label {
|
|
|
color: #6b7280;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
.talent-stat-value {
|
|
|
color: var(--brand-primary);
|
|
|
font-size: 24px;
|
|
|
line-height: 1.1;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
.talent-stat-value.is-dark {
|
|
|
color: #111827;
|
|
|
}
|
|
|
.talent-stat-value.is-danger {
|
|
|
color: var(--el-color-danger);
|
|
|
}
|
|
|
.talent-stat-value.is-success {
|
|
|
color: var(--el-color-success);
|
|
|
}
|
|
|
.filter-source {
|
|
|
width: 132px;
|
|
|
}
|
|
|
.filter-stars {
|
|
|
width: 100px;
|
|
|
}
|
|
|
.filter-status {
|
|
|
width: 112px;
|
|
|
}
|
|
|
.filter-school {
|
|
|
width: 184px;
|
|
|
}
|
|
|
.filter-direction {
|
|
|
width: 146px;
|
|
|
}
|
|
|
.name-link {
|
|
|
color: inherit;
|
|
|
text-decoration: none;
|
|
|
}
|
|
|
.name-link:hover {
|
|
|
color: var(--brand-primary);
|
|
|
}
|
|
|
.tag-overdue {
|
|
|
margin-left: 4px;
|
|
|
vertical-align: middle;
|
|
|
}
|
|
|
.star-text {
|
|
|
color: #e6a23c;
|
|
|
letter-spacing: 1px;
|
|
|
}
|
|
|
.source-tag {
|
|
|
border: none;
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
.source-tag-manual {
|
|
|
--el-tag-bg-color: #6c757d;
|
|
|
--el-tag-border-color: #6c757d;
|
|
|
--el-tag-text-color: #fff;
|
|
|
}
|
|
|
.teachers-table-actions {
|
|
|
flex-wrap: nowrap;
|
|
|
gap: 4px;
|
|
|
}
|
|
|
.teachers-table-btn {
|
|
|
padding: 2px 6px !important;
|
|
|
min-height: 22px;
|
|
|
font-size: 12px;
|
|
|
}
|
|
|
.follow-teacher-summary {
|
|
|
padding: 10px 12px;
|
|
|
border-left: 4px solid var(--brand-primary);
|
|
|
background: var(--brand-bg-soft);
|
|
|
border-radius: 6px;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
.text-brand {
|
|
|
color: var(--brand-primary);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
.names-muted {
|
|
|
margin-left: 8px;
|
|
|
color: #6b7280;
|
|
|
font-size: 13px;
|
|
|
}
|
|
|
.batch-rule {
|
|
|
font-size: 12px;
|
|
|
color: #6b7280;
|
|
|
margin: 0;
|
|
|
}
|
|
|
.follow-date-rule {
|
|
|
margin: 6px 0 0;
|
|
|
font-size: 12px;
|
|
|
line-height: 1.45;
|
|
|
color: #6b7280;
|
|
|
}
|
|
|
.follow-history-list {
|
|
|
display: grid;
|
|
|
gap: 14px;
|
|
|
margin-top: 12px;
|
|
|
max-height: 60vh;
|
|
|
overflow: auto;
|
|
|
}
|
|
|
.follow-history-card {
|
|
|
border: 1px solid #dfe4eb;
|
|
|
border-left: 4px solid var(--brand-primary);
|
|
|
border-radius: 6px;
|
|
|
background: #fff;
|
|
|
}
|
|
|
.follow-history-head {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 12px;
|
|
|
padding: 12px 14px;
|
|
|
border-bottom: 1px solid #e6eaf1;
|
|
|
}
|
|
|
.follow-history-title {
|
|
|
color: var(--brand-primary);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
.follow-history-date {
|
|
|
margin-left: 14px;
|
|
|
color: #6b7280;
|
|
|
font-size: 13px;
|
|
|
}
|
|
|
.follow-history-body {
|
|
|
padding: 14px;
|
|
|
font-size: 13px;
|
|
|
}
|
|
|
.follow-history-row {
|
|
|
display: grid;
|
|
|
grid-template-columns: 96px 1fr;
|
|
|
gap: 14px;
|
|
|
margin-bottom: 10px;
|
|
|
}
|
|
|
.follow-history-row > span {
|
|
|
color: #6b7280;
|
|
|
}
|
|
|
.follow-history-footer {
|
|
|
padding: 0 14px 14px;
|
|
|
color: #6b7280;
|
|
|
font-size: 13px;
|
|
|
}
|
|
|
.dot {
|
|
|
margin: 0 6px;
|
|
|
color: #9ca3af;
|
|
|
}
|
|
|
.uni-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
width: 100%;
|
|
|
}
|
|
|
:global(.teachers-primary-btn.el-button--primary) {
|
|
|
--el-button-text-color: #fff;
|
|
|
--el-button-bg-color: #244e98;
|
|
|
--el-button-border-color: #244e98;
|
|
|
--el-button-hover-text-color: #fff;
|
|
|
--el-button-hover-bg-color: #8b1519;
|
|
|
--el-button-hover-border-color: #8b1519;
|
|
|
--el-button-active-text-color: #fff;
|
|
|
--el-button-active-bg-color: #8b1519;
|
|
|
--el-button-active-border-color: #8b1519;
|
|
|
}
|
|
|
.form-small :deep(.el-form-item__label) {
|
|
|
font-size: 13px;
|
|
|
padding-bottom: 4px;
|
|
|
}
|
|
|
@media (max-width: 992px) {
|
|
|
.talent-stat-grid {
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
}
|
|
|
}
|
|
|
</style>
|