|
|
|
|
@ -1,15 +1,13 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, ref, watch } from 'vue'
|
|
|
|
|
import { fetchDictByCode } from '@/api/admin/dict'
|
|
|
|
|
import { computed, nextTick, ref, watch } from 'vue'
|
|
|
|
|
import TeacherPaperDialog from '@/components/TeacherPaperDialog.vue'
|
|
|
|
|
import {
|
|
|
|
|
approveTeacher,
|
|
|
|
|
createUniversity,
|
|
|
|
|
deleteTeacherPaper,
|
|
|
|
|
fetchTeacher,
|
|
|
|
|
fetchTeacherFilterOptions,
|
|
|
|
|
fetchTeacherBootstrap,
|
|
|
|
|
fetchTeacherPapers,
|
|
|
|
|
fetchUniversities,
|
|
|
|
|
rejectTeacher,
|
|
|
|
|
updateTeacher,
|
|
|
|
|
type DictItemBrief,
|
|
|
|
|
@ -34,14 +32,21 @@ const isGridMember = computed(() => auth.isGridMember)
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
modelValue: boolean
|
|
|
|
|
teacherId: number | null
|
|
|
|
|
/** 列表行预填,用于弹窗立刻展示,不等接口 */
|
|
|
|
|
previewTeacher?: TeacherRow | null
|
|
|
|
|
/** 仅查看,不可编辑(如地图页老师详情) */
|
|
|
|
|
readonly?: boolean
|
|
|
|
|
/** 父页已加载的下拉,传入后不再重复请求 */
|
|
|
|
|
starOptions?: DictItemBrief[]
|
|
|
|
|
statusOptions?: DictItemBrief[]
|
|
|
|
|
universityOptions?: { id: number; name: string; city?: string | null }[]
|
|
|
|
|
directionOptions?: { id: number; name: string }[]
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
const isReadonly = computed(() => props.readonly === true)
|
|
|
|
|
|
|
|
|
|
const dialogTitle = computed(() => {
|
|
|
|
|
const name = teacher.value?.name
|
|
|
|
|
const name = teacher.value?.name || props.previewTeacher?.name || form.value.name
|
|
|
|
|
if (isReadonly.value) {
|
|
|
|
|
return name ? `查看 · ${name}` : '老师详情'
|
|
|
|
|
}
|
|
|
|
|
@ -62,6 +67,8 @@ function displayDirections(row: TeacherRow) {
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
'update:modelValue': [boolean]
|
|
|
|
|
saved: []
|
|
|
|
|
'university-created': [{ id: number; name: string; city?: string | null }]
|
|
|
|
|
'directions-changed': [{ id: number; name: string }[]]
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
async function approveTeacherAction() {
|
|
|
|
|
@ -84,19 +91,38 @@ async function rejectTeacherAction() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
const selectsReady = ref(false)
|
|
|
|
|
const teacher = ref<TeacherRow | null>(null)
|
|
|
|
|
const loadedStarId = ref<number | null>(null)
|
|
|
|
|
|
|
|
|
|
const localStarOptions = ref<DictItemBrief[]>([])
|
|
|
|
|
const localStatusOptions = ref<DictItemBrief[]>([])
|
|
|
|
|
const localUniversityOptions = ref<{ id: number; name: string; city?: string | null }[]>([])
|
|
|
|
|
const localDirectionOptions = ref<{ id: number; name: string }[]>([])
|
|
|
|
|
|
|
|
|
|
const starOptions = computed(() =>
|
|
|
|
|
props.starOptions?.length ? props.starOptions : localStarOptions.value,
|
|
|
|
|
)
|
|
|
|
|
const statusOptions = computed(() =>
|
|
|
|
|
props.statusOptions?.length ? props.statusOptions : localStatusOptions.value,
|
|
|
|
|
)
|
|
|
|
|
const universityOptions = computed(() =>
|
|
|
|
|
props.universityOptions?.length ? props.universityOptions : localUniversityOptions.value,
|
|
|
|
|
)
|
|
|
|
|
const directionOptions = computed(() =>
|
|
|
|
|
props.directionOptions?.length ? props.directionOptions : localDirectionOptions.value,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const universitySelectOptions = computed(() =>
|
|
|
|
|
universityOptions.value.map((u) => ({ value: u.id, label: u.name })),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const starFollowRule = computed(() => {
|
|
|
|
|
const item = starOptions.value.find((s) => s.id === form.value.star_level_dict_item_id)
|
|
|
|
|
return followRuleHint(item?.value)
|
|
|
|
|
})
|
|
|
|
|
const papers = ref<PaperRow[]>([])
|
|
|
|
|
const demands = ref<DemandRow[]>([])
|
|
|
|
|
const starOptions = ref<DictItemBrief[]>([])
|
|
|
|
|
const statusOptions = ref<DictItemBrief[]>([])
|
|
|
|
|
const universityOptions = ref<{ id: number; name: string; city?: string | null }[]>([])
|
|
|
|
|
const directionOptions = ref<{ id: number; name: string }[]>([])
|
|
|
|
|
|
|
|
|
|
function splitDirectionValues(values: Array<number | string>) {
|
|
|
|
|
const ids: number[] = []
|
|
|
|
|
@ -112,20 +138,24 @@ function splitDirectionValues(values: Array<number | string>) {
|
|
|
|
|
return { ids, names }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const form = ref({
|
|
|
|
|
name: '',
|
|
|
|
|
university_id: undefined as number | undefined,
|
|
|
|
|
city: '',
|
|
|
|
|
title: '',
|
|
|
|
|
department: '',
|
|
|
|
|
bio: '',
|
|
|
|
|
research_direction_values: [] as Array<number | string>,
|
|
|
|
|
phone: '',
|
|
|
|
|
email: '',
|
|
|
|
|
star_level_dict_item_id: undefined as number | undefined,
|
|
|
|
|
status_dict_item_id: undefined as number | undefined,
|
|
|
|
|
next_follow_date: '',
|
|
|
|
|
})
|
|
|
|
|
function emptyForm() {
|
|
|
|
|
return {
|
|
|
|
|
name: '',
|
|
|
|
|
university_id: undefined as number | undefined,
|
|
|
|
|
city: '',
|
|
|
|
|
title: '',
|
|
|
|
|
department: '',
|
|
|
|
|
bio: '',
|
|
|
|
|
research_direction_values: [] as Array<number | string>,
|
|
|
|
|
phone: '',
|
|
|
|
|
email: '',
|
|
|
|
|
star_level_dict_item_id: undefined as number | undefined,
|
|
|
|
|
status_dict_item_id: undefined as number | undefined,
|
|
|
|
|
next_follow_date: '',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const form = ref(emptyForm())
|
|
|
|
|
|
|
|
|
|
const universityDialog = ref(false)
|
|
|
|
|
const universitySaving = ref(false)
|
|
|
|
|
@ -145,29 +175,40 @@ const demandRecordsVisible = ref(false)
|
|
|
|
|
const demandRecordsTitle = ref('')
|
|
|
|
|
const demandRecords = ref<DemandHandleLogRow[]>([])
|
|
|
|
|
|
|
|
|
|
async function loadUniversityOptions() {
|
|
|
|
|
const uni = await fetchUniversities({ page: 1, page_size: 200 })
|
|
|
|
|
const scopeIds = auth.user?.grid_scope?.university_ids
|
|
|
|
|
universityOptions.value = scopeIds?.length
|
|
|
|
|
? uni.items.filter((u) => scopeIds.includes(u.id))
|
|
|
|
|
: uni.items
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function refreshDirectionOptions() {
|
|
|
|
|
const filters = await fetchTeacherFilterOptions()
|
|
|
|
|
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
|
|
|
|
|
function applyTeacherToForm(detail: TeacherRow) {
|
|
|
|
|
teacher.value = detail
|
|
|
|
|
loadedStarId.value = detail.star_level_dict_item_id ?? null
|
|
|
|
|
form.value = {
|
|
|
|
|
name: detail.name || '',
|
|
|
|
|
university_id: detail.university_id ?? undefined,
|
|
|
|
|
research_direction_values: detail.research_direction_ids?.length
|
|
|
|
|
? [...detail.research_direction_ids]
|
|
|
|
|
: detail.research_directions?.map((d) => d.id) || [],
|
|
|
|
|
city: detail.city || '',
|
|
|
|
|
title: detail.title || '',
|
|
|
|
|
department: detail.department || '',
|
|
|
|
|
bio: detail.bio || '',
|
|
|
|
|
phone: detail.phone || '',
|
|
|
|
|
email: detail.email || '',
|
|
|
|
|
star_level_dict_item_id: detail.star_level_dict_item_id ?? undefined,
|
|
|
|
|
status_dict_item_id: detail.status_dict_item_id ?? undefined,
|
|
|
|
|
next_follow_date: detail.next_follow_date || '',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mergeTeacherDirectionOptions(detail: {
|
|
|
|
|
research_directions?: { id: number; name: string }[]
|
|
|
|
|
}) {
|
|
|
|
|
const known = new Set(directionOptions.value.map((d) => d.id))
|
|
|
|
|
for (const d of detail.research_directions || []) {
|
|
|
|
|
if (!detail.research_directions?.length) return
|
|
|
|
|
if (props.directionOptions?.length) {
|
|
|
|
|
// 父页托管列表时,仅通知新增项,避免本地大数组拷贝
|
|
|
|
|
emit('directions-changed', detail.research_directions)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const known = new Set(localDirectionOptions.value.map((d) => d.id))
|
|
|
|
|
for (const d of detail.research_directions) {
|
|
|
|
|
if (!known.has(d.id)) {
|
|
|
|
|
directionOptions.value.push({ id: d.id, name: d.name })
|
|
|
|
|
localDirectionOptions.value.push({ id: d.id, name: d.name })
|
|
|
|
|
known.add(d.id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -209,13 +250,14 @@ async function saveUniversityCreate() {
|
|
|
|
|
longitude,
|
|
|
|
|
latitude,
|
|
|
|
|
})
|
|
|
|
|
if (!universityOptions.value.some((x) => x.id === u.id)) {
|
|
|
|
|
universityOptions.value.push(u)
|
|
|
|
|
if (!props.universityOptions?.length && !localUniversityOptions.value.some((x) => x.id === u.id)) {
|
|
|
|
|
localUniversityOptions.value.push(u)
|
|
|
|
|
}
|
|
|
|
|
form.value.university_id = u.id
|
|
|
|
|
if (u.city) {
|
|
|
|
|
form.value.city = u.city
|
|
|
|
|
}
|
|
|
|
|
emit('university-created', u)
|
|
|
|
|
universityDialog.value = false
|
|
|
|
|
ElMessage.success('高校已创建')
|
|
|
|
|
} finally {
|
|
|
|
|
@ -223,39 +265,16 @@ async function saveUniversityCreate() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function load() {
|
|
|
|
|
if (!props.teacherId) return
|
|
|
|
|
loading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const detail = await fetchTeacher(props.teacherId)
|
|
|
|
|
teacher.value = detail
|
|
|
|
|
loadedStarId.value = detail.star_level_dict_item_id ?? null
|
|
|
|
|
mergeTeacherDirectionOptions(detail)
|
|
|
|
|
form.value = {
|
|
|
|
|
name: detail.name || '',
|
|
|
|
|
university_id: detail.university_id ?? undefined,
|
|
|
|
|
research_direction_values: detail.research_direction_ids?.length
|
|
|
|
|
? [...detail.research_direction_ids]
|
|
|
|
|
: detail.research_directions?.map((d) => d.id) || [],
|
|
|
|
|
city: detail.city || '',
|
|
|
|
|
title: detail.title || '',
|
|
|
|
|
department: detail.department || '',
|
|
|
|
|
bio: detail.bio || '',
|
|
|
|
|
phone: detail.phone || '',
|
|
|
|
|
email: detail.email || '',
|
|
|
|
|
star_level_dict_item_id: detail.star_level_dict_item_id ?? undefined,
|
|
|
|
|
status_dict_item_id: detail.status_dict_item_id ?? undefined,
|
|
|
|
|
next_follow_date: detail.next_follow_date || '',
|
|
|
|
|
}
|
|
|
|
|
const [paperList, demandRes] = await Promise.all([
|
|
|
|
|
fetchTeacherPapers(props.teacherId),
|
|
|
|
|
fetchDemandsList({ teacher_id: props.teacherId, page: 1, page_size: 50 }),
|
|
|
|
|
])
|
|
|
|
|
papers.value = paperList
|
|
|
|
|
demands.value = demandRes.items
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false
|
|
|
|
|
async function ensureOptions() {
|
|
|
|
|
if (isReadonly.value) return
|
|
|
|
|
if (starOptions.value.length && statusOptions.value.length && universityOptions.value.length) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const data = await fetchTeacherBootstrap()
|
|
|
|
|
if (!starOptions.value.length) localStarOptions.value = data.dicts.teacher_level || []
|
|
|
|
|
if (!statusOptions.value.length) localStatusOptions.value = data.dicts.teacher_status || []
|
|
|
|
|
if (!universityOptions.value.length) localUniversityOptions.value = data.universities || []
|
|
|
|
|
if (!directionOptions.value.length) localDirectionOptions.value = data.research_directions || []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateForm(): boolean {
|
|
|
|
|
@ -276,15 +295,6 @@ function validateForm(): boolean {
|
|
|
|
|
ElMessage.warning('请填写职称')
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
const directions = splitDirectionValues(f.research_direction_values)
|
|
|
|
|
if (!directions.ids.length && !directions.names.length) {
|
|
|
|
|
ElMessage.warning('请至少选择一个或新增研究方向')
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if (!f.status_dict_item_id) {
|
|
|
|
|
ElMessage.warning('请选择状态')
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -308,7 +318,7 @@ async function save() {
|
|
|
|
|
phone: form.value.phone || null,
|
|
|
|
|
email: form.value.email || null,
|
|
|
|
|
star_level_dict_item_id: form.value.star_level_dict_item_id ?? null,
|
|
|
|
|
status_dict_item_id: form.value.status_dict_item_id,
|
|
|
|
|
status_dict_item_id: form.value.status_dict_item_id ?? null,
|
|
|
|
|
...(starChanged
|
|
|
|
|
? { recalc_next_follow_date: true }
|
|
|
|
|
: { next_follow_date: form.value.next_follow_date || null, recalc_next_follow_date: false }),
|
|
|
|
|
@ -360,18 +370,64 @@ async function openDemandRecords(row: DemandRow) {
|
|
|
|
|
watch(
|
|
|
|
|
() => [props.modelValue, props.teacherId] as const,
|
|
|
|
|
async ([open, id]) => {
|
|
|
|
|
if (open && id) {
|
|
|
|
|
if (!starOptions.value.length) {
|
|
|
|
|
const [star, status] = await Promise.all([
|
|
|
|
|
fetchDictByCode('teacher_level'),
|
|
|
|
|
fetchDictByCode('teacher_status'),
|
|
|
|
|
])
|
|
|
|
|
starOptions.value = star.items
|
|
|
|
|
statusOptions.value = status.items
|
|
|
|
|
await loadUniversityOptions()
|
|
|
|
|
if (!open || !id) {
|
|
|
|
|
if (!open) {
|
|
|
|
|
teacher.value = null
|
|
|
|
|
form.value = emptyForm()
|
|
|
|
|
papers.value = []
|
|
|
|
|
demands.value = []
|
|
|
|
|
loadedStarId.value = null
|
|
|
|
|
selectsReady.value = false
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const requestId = id
|
|
|
|
|
papers.value = []
|
|
|
|
|
demands.value = []
|
|
|
|
|
selectsReady.value = false
|
|
|
|
|
|
|
|
|
|
// 用列表预览立刻填表,弹窗可马上可见
|
|
|
|
|
if (props.previewTeacher && props.previewTeacher.id === id) {
|
|
|
|
|
applyTeacherToForm(props.previewTeacher)
|
|
|
|
|
loading.value = false
|
|
|
|
|
} else {
|
|
|
|
|
teacher.value = null
|
|
|
|
|
form.value = emptyForm()
|
|
|
|
|
loadedStarId.value = null
|
|
|
|
|
loading.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 等弹窗先渲染出来,再挂载大批量下拉,避免点击后卡住
|
|
|
|
|
await nextTick()
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
if (props.modelValue && props.teacherId === requestId) {
|
|
|
|
|
selectsReady.value = true
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const detailPromise = fetchTeacher(requestId)
|
|
|
|
|
void ensureOptions()
|
|
|
|
|
|
|
|
|
|
const detail = await detailPromise
|
|
|
|
|
if (props.teacherId !== requestId || !props.modelValue) return
|
|
|
|
|
|
|
|
|
|
applyTeacherToForm(detail)
|
|
|
|
|
mergeTeacherDirectionOptions(detail)
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
|
|
|
|
const [paperList, demandRes] = await Promise.all([
|
|
|
|
|
fetchTeacherPapers(requestId),
|
|
|
|
|
fetchDemandsList({ teacher_id: requestId, page: 1, page_size: 50 }),
|
|
|
|
|
])
|
|
|
|
|
if (props.teacherId !== requestId || !props.modelValue) return
|
|
|
|
|
papers.value = paperList
|
|
|
|
|
demands.value = demandRes.items
|
|
|
|
|
} finally {
|
|
|
|
|
if (props.teacherId === requestId) {
|
|
|
|
|
loading.value = false
|
|
|
|
|
}
|
|
|
|
|
await refreshDirectionOptions()
|
|
|
|
|
await load()
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
@ -388,7 +444,7 @@ watch(
|
|
|
|
|
:title="dialogTitle"
|
|
|
|
|
width="960px"
|
|
|
|
|
top="4vh"
|
|
|
|
|
destroy-on-close
|
|
|
|
|
append-to-body
|
|
|
|
|
@update:model-value="emit('update:modelValue', $event)"
|
|
|
|
|
>
|
|
|
|
|
<div v-loading="loading" class="detail-body">
|
|
|
|
|
@ -439,14 +495,16 @@ watch(
|
|
|
|
|
论文抓取高校:{{ teacher.university_text }}(请在下拉中选择或新建以关联)
|
|
|
|
|
</p>
|
|
|
|
|
<div class="uni-row">
|
|
|
|
|
<el-select
|
|
|
|
|
<el-select-v2
|
|
|
|
|
v-if="selectsReady"
|
|
|
|
|
v-model="form.university_id"
|
|
|
|
|
filterable
|
|
|
|
|
clearable
|
|
|
|
|
placeholder="选择高校"
|
|
|
|
|
style="flex: 1"
|
|
|
|
|
>
|
|
|
|
|
<el-option v-for="u in universityOptions" :key="u.id" :label="u.name" :value="u.id" />
|
|
|
|
|
</el-select>
|
|
|
|
|
:options="universitySelectOptions"
|
|
|
|
|
/>
|
|
|
|
|
<el-input v-else disabled model-value="加载高校列表…" style="flex: 1" />
|
|
|
|
|
<el-button
|
|
|
|
|
v-if="!isGridMember"
|
|
|
|
|
type="primary"
|
|
|
|
|
@ -480,8 +538,9 @@ watch(
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :xs="24" :md="8">
|
|
|
|
|
<el-form-item label="研究方向" required>
|
|
|
|
|
<el-form-item label="研究方向">
|
|
|
|
|
<el-select
|
|
|
|
|
v-if="selectsReady"
|
|
|
|
|
v-model="form.research_direction_values"
|
|
|
|
|
multiple
|
|
|
|
|
filterable
|
|
|
|
|
@ -489,11 +548,13 @@ watch(
|
|
|
|
|
default-first-option
|
|
|
|
|
collapse-tags
|
|
|
|
|
collapse-tags-tooltip
|
|
|
|
|
placeholder="选择或输入研究方向"
|
|
|
|
|
clearable
|
|
|
|
|
placeholder="可选,选择或输入研究方向"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
>
|
|
|
|
|
<el-option v-for="d in directionOptions" :key="d.id" :label="d.name" :value="d.id" />
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-input v-else disabled model-value="加载研究方向…" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :xs="24" :md="6">
|
|
|
|
|
@ -522,8 +583,8 @@ watch(
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :xs="24" :md="6">
|
|
|
|
|
<el-form-item label="状态" required>
|
|
|
|
|
<el-select v-model="form.status_dict_item_id" placeholder="请选择状态" style="width: 100%">
|
|
|
|
|
<el-form-item label="状态">
|
|
|
|
|
<el-select v-model="form.status_dict_item_id" clearable 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>
|
|
|
|
|
|