|
|
<script setup lang="ts">
|
|
|
import { computed, ref, watch } from 'vue'
|
|
|
import { fetchDictByCode } from '@/api/admin/dict'
|
|
|
import TeacherPaperDialog from '@/components/TeacherPaperDialog.vue'
|
|
|
import {
|
|
|
approveTeacher,
|
|
|
createUniversity,
|
|
|
deleteTeacherPaper,
|
|
|
fetchTeacher,
|
|
|
fetchTeacherFilterOptions,
|
|
|
fetchTeacherPapers,
|
|
|
fetchUniversities,
|
|
|
rejectTeacher,
|
|
|
updateTeacher,
|
|
|
type DictItemBrief,
|
|
|
type PaperRow,
|
|
|
type TeacherRow,
|
|
|
} from '@/api/admin/teachers'
|
|
|
import {
|
|
|
demandStatusTagType,
|
|
|
fetchDemandHandleLogs,
|
|
|
fetchDemandsList,
|
|
|
type DemandHandleLogRow,
|
|
|
type DemandRow,
|
|
|
} from '@/api/admin/demands'
|
|
|
import { followRuleHint, previewNextFollowDate } from '@/utils/teacherFollowRule'
|
|
|
import { starDisplay } from '@/utils/teacherStar'
|
|
|
import { useAuthStore } from '@/stores/auth'
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
|
const auth = useAuthStore()
|
|
|
const isGridMember = computed(() => auth.isGridMember)
|
|
|
|
|
|
const props = defineProps<{
|
|
|
modelValue: boolean
|
|
|
teacherId: number | null
|
|
|
/** 仅查看,不可编辑(如地图页老师详情) */
|
|
|
readonly?: boolean
|
|
|
}>()
|
|
|
|
|
|
const isReadonly = computed(() => props.readonly === true)
|
|
|
|
|
|
const dialogTitle = computed(() => {
|
|
|
const name = teacher.value?.name
|
|
|
if (isReadonly.value) {
|
|
|
return name ? `查看 · ${name}` : '老师详情'
|
|
|
}
|
|
|
return name ? `编辑 · ${name}` : '编辑老师'
|
|
|
})
|
|
|
|
|
|
function displayUniversity(row: TeacherRow) {
|
|
|
if (row.university_name) return row.university_name
|
|
|
if (row.university_text) return `${row.university_text}(未关联高校库)`
|
|
|
return '—'
|
|
|
}
|
|
|
|
|
|
function displayDirections(row: TeacherRow) {
|
|
|
if (row.research_direction) return row.research_direction
|
|
|
return row.research_directions?.map((d) => d.name).join('、') || '—'
|
|
|
}
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
'update:modelValue': [boolean]
|
|
|
saved: []
|
|
|
}>()
|
|
|
|
|
|
async function approveTeacherAction() {
|
|
|
if (!teacher.value) return
|
|
|
await approveTeacher(teacher.value.id)
|
|
|
ElMessage.success('已确认入库')
|
|
|
emit('saved')
|
|
|
emit('update:modelValue', false)
|
|
|
}
|
|
|
|
|
|
async function rejectTeacherAction() {
|
|
|
if (!teacher.value) return
|
|
|
await ElMessageBox.confirm(`确定驳回「${teacher.value.name}」?驳回后记录会保留。`, '确认驳回', {
|
|
|
type: 'warning',
|
|
|
})
|
|
|
await rejectTeacher(teacher.value.id)
|
|
|
ElMessage.success('已驳回')
|
|
|
emit('saved')
|
|
|
emit('update:modelValue', false)
|
|
|
}
|
|
|
|
|
|
const loading = ref(false)
|
|
|
const teacher = ref<TeacherRow | null>(null)
|
|
|
const loadedStarId = ref<number | null>(null)
|
|
|
|
|
|
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[] = []
|
|
|
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 }
|
|
|
}
|
|
|
|
|
|
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: '',
|
|
|
})
|
|
|
|
|
|
const universityDialog = ref(false)
|
|
|
const universitySaving = ref(false)
|
|
|
const universityForm = ref({
|
|
|
name: '',
|
|
|
city: '',
|
|
|
province: '',
|
|
|
longitude: '',
|
|
|
latitude: '',
|
|
|
})
|
|
|
|
|
|
const paperDialog = ref(false)
|
|
|
const summaryVisible = ref(false)
|
|
|
const summaryText = ref('')
|
|
|
|
|
|
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 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 (!known.has(d.id)) {
|
|
|
directionOptions.value.push({ id: d.id, name: d.name })
|
|
|
known.add(d.id)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function openUniversityCreate() {
|
|
|
universityForm.value = {
|
|
|
name: '',
|
|
|
city: form.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)
|
|
|
}
|
|
|
form.value.university_id = u.id
|
|
|
if (u.city) {
|
|
|
form.value.city = u.city
|
|
|
}
|
|
|
universityDialog.value = false
|
|
|
ElMessage.success('高校已创建')
|
|
|
} finally {
|
|
|
universitySaving.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
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
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function validateForm(): boolean {
|
|
|
const f = form.value
|
|
|
if (!f.name.trim()) {
|
|
|
ElMessage.warning('请填写姓名')
|
|
|
return false
|
|
|
}
|
|
|
if (!f.university_id) {
|
|
|
ElMessage.warning('请选择高校,或点击「新建高校」')
|
|
|
return false
|
|
|
}
|
|
|
if (!f.city.trim()) {
|
|
|
ElMessage.warning('请填写城市')
|
|
|
return false
|
|
|
}
|
|
|
if (!f.title.trim()) {
|
|
|
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
|
|
|
}
|
|
|
|
|
|
async function save() {
|
|
|
if (!props.teacherId || !teacher.value) return
|
|
|
if (!validateForm()) return
|
|
|
|
|
|
const starChanged = (form.value.star_level_dict_item_id ?? null) !== loadedStarId.value
|
|
|
|
|
|
const directions = splitDirectionValues(form.value.research_direction_values)
|
|
|
|
|
|
await updateTeacher(props.teacherId, {
|
|
|
name: form.value.name.trim(),
|
|
|
university_id: form.value.university_id,
|
|
|
city: form.value.city.trim(),
|
|
|
title: form.value.title.trim(),
|
|
|
department: form.value.department.trim() || null,
|
|
|
bio: form.value.bio.trim() || null,
|
|
|
research_direction_ids: directions.ids,
|
|
|
new_research_directions: directions.names,
|
|
|
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,
|
|
|
...(starChanged
|
|
|
? { recalc_next_follow_date: true }
|
|
|
: { next_follow_date: form.value.next_follow_date || null, recalc_next_follow_date: false }),
|
|
|
})
|
|
|
ElMessage.success('已保存')
|
|
|
emit('saved')
|
|
|
emit('update:modelValue', false)
|
|
|
}
|
|
|
|
|
|
function onStarChange() {
|
|
|
const item = starOptions.value.find((s) => s.id === form.value.star_level_dict_item_id)
|
|
|
form.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 && !form.value.city) {
|
|
|
form.value.city = u.city
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function openAddPaper() {
|
|
|
paperDialog.value = true
|
|
|
}
|
|
|
|
|
|
async function reloadPapers() {
|
|
|
if (!props.teacherId) return
|
|
|
papers.value = await fetchTeacherPapers(props.teacherId)
|
|
|
}
|
|
|
|
|
|
async function removePaper(row: PaperRow) {
|
|
|
await ElMessageBox.confirm(`确定删除论文「${row.title}」?`, '确认删除', { type: 'warning' })
|
|
|
await deleteTeacherPaper(props.teacherId!, row.id)
|
|
|
papers.value = await fetchTeacherPapers(props.teacherId!)
|
|
|
}
|
|
|
|
|
|
function showSummary(row: PaperRow) {
|
|
|
summaryText.value = row.summary || '暂无摘要'
|
|
|
summaryVisible.value = true
|
|
|
}
|
|
|
|
|
|
async function openDemandRecords(row: DemandRow) {
|
|
|
demandRecordsTitle.value = row.title
|
|
|
demandRecords.value = await fetchDemandHandleLogs(row.id)
|
|
|
demandRecordsVisible.value = true
|
|
|
}
|
|
|
|
|
|
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()
|
|
|
}
|
|
|
await refreshDirectionOptions()
|
|
|
await load()
|
|
|
}
|
|
|
},
|
|
|
)
|
|
|
|
|
|
watch(
|
|
|
() => form.value.university_id,
|
|
|
(id) => onUniversityPick(id),
|
|
|
)
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
<el-dialog
|
|
|
:model-value="modelValue"
|
|
|
:title="dialogTitle"
|
|
|
width="960px"
|
|
|
top="4vh"
|
|
|
destroy-on-close
|
|
|
@update:model-value="emit('update:modelValue', $event)"
|
|
|
>
|
|
|
<div v-loading="loading" class="detail-body">
|
|
|
<div v-if="isReadonly && teacher" class="basic-info-block">
|
|
|
<h3 class="basic-info-title">基本信息</h3>
|
|
|
<el-descriptions :column="3" border size="small" class="view-desc">
|
|
|
<el-descriptions-item label="姓名">{{ teacher.name }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="高校">{{ displayUniversity(teacher) }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="城市">{{ teacher.city || '—' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="职称">{{ teacher.title || '—' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="研究方向" :span="2">
|
|
|
{{ displayDirections(teacher) }}
|
|
|
</el-descriptions-item>
|
|
|
<el-descriptions-item label="电话">{{ teacher.phone || '—' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="邮箱">{{ teacher.email || '—' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="星级">
|
|
|
{{
|
|
|
starDisplay(teacher.star_level_item?.value, teacher.star_level_item?.label)
|
|
|
}}
|
|
|
</el-descriptions-item>
|
|
|
<el-descriptions-item label="状态">
|
|
|
{{ teacher.status_item?.label || '—' }}
|
|
|
</el-descriptions-item>
|
|
|
<el-descriptions-item label="下次跟进日期">
|
|
|
{{ teacher.next_follow_date || '—' }}
|
|
|
</el-descriptions-item>
|
|
|
<el-descriptions-item v-if="teacher.source_item?.label" label="来源">
|
|
|
{{ teacher.source_item.label }}
|
|
|
</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="basic-info-block">
|
|
|
<h3 class="basic-info-title">基本信息</h3>
|
|
|
<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="form.name" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="8">
|
|
|
<el-form-item label="高校" required>
|
|
|
<p
|
|
|
v-if="teacher?.university_text && !form.university_id"
|
|
|
class="crawl-uni-hint"
|
|
|
>
|
|
|
论文抓取高校:{{ teacher.university_text }}(请在下拉中选择或新建以关联)
|
|
|
</p>
|
|
|
<div class="uni-row">
|
|
|
<el-select
|
|
|
v-model="form.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="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="form.city" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="8">
|
|
|
<el-form-item label="职称" required>
|
|
|
<el-input v-model="form.title" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="8">
|
|
|
<el-form-item label="所属学院">
|
|
|
<el-input v-model="form.department" placeholder="如:计算机科学与技术学院" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="16">
|
|
|
<el-form-item label="个人简介">
|
|
|
<el-input v-model="form.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="form.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="form.phone" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="6">
|
|
|
<el-form-item label="邮箱">
|
|
|
<el-input v-model="form.email" type="email" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :md="4">
|
|
|
<el-form-item label="星级">
|
|
|
<el-select v-model="form.star_level_dict_item_id" style="width: 100%" @change="onStarChange">
|
|
|
<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 === form.star_level_dict_item_id)?.value,
|
|
|
starOptions.find((s) => s.id === form.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="form.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="form.next_follow_date"
|
|
|
type="date"
|
|
|
value-format="YYYY-MM-DD"
|
|
|
placeholder="选择日期"
|
|
|
style="width: 100%"
|
|
|
/>
|
|
|
<p v-if="starFollowRule" class="star-follow-rule">{{ starFollowRule }}</p>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
</el-form>
|
|
|
</div>
|
|
|
|
|
|
<div class="section">
|
|
|
<div class="section-head">
|
|
|
<h3>论文列表</h3>
|
|
|
<span class="muted">共 {{ papers.length }} 篇</span>
|
|
|
<el-button v-if="!isReadonly" size="small" type="primary" plain @click="openAddPaper">
|
|
|
添加论文
|
|
|
</el-button>
|
|
|
</div>
|
|
|
<el-table :data="papers" size="small" row-key="id">
|
|
|
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
|
|
|
<el-table-column prop="authors" label="作者" width="140" />
|
|
|
<el-table-column prop="school_name" label="学校" width="120" />
|
|
|
<el-table-column prop="published_at" label="发表时间" width="110" />
|
|
|
<el-table-column label="链接" width="70">
|
|
|
<template #default="{ row }">
|
|
|
<el-link v-if="row.url" :href="row.url" target="_blank">查看</el-link>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="操作" :width="isReadonly ? 70 : 120">
|
|
|
<template #default="{ row }">
|
|
|
<el-button link type="primary" @click="showSummary(row)">摘要</el-button>
|
|
|
<el-button v-if="!isReadonly" link type="danger" @click="removePaper(row)">删除</el-button>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</div>
|
|
|
|
|
|
<div class="section">
|
|
|
<div class="section-head">
|
|
|
<h3>需求列表</h3>
|
|
|
<span class="muted">共 {{ demands.length }} 条</span>
|
|
|
</div>
|
|
|
<el-table :data="demands" size="small" row-key="id">
|
|
|
<el-table-column label="类型" width="100">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag size="small" effect="dark">{{ row.type_item?.label }}</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="title" label="标题" min-width="180" show-overflow-tooltip />
|
|
|
<el-table-column prop="submitted_at" label="时间" width="110" />
|
|
|
<el-table-column label="处理状态" width="100">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="demandStatusTagType(row.status_item?.value)" size="small" effect="dark">
|
|
|
{{ row.status_item?.label }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="跟进记录" width="100">
|
|
|
<template #default="{ row }">
|
|
|
<el-button size="small" @click="openDemandRecords(row)">
|
|
|
{{ row.handle_logs_count || 0 }}条记录
|
|
|
</el-button>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
<el-empty v-if="!demands.length" description="暂无关联需求" :image-size="64" />
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<template #footer>
|
|
|
<el-button @click="emit('update:modelValue', false)">关闭</el-button>
|
|
|
<el-button
|
|
|
v-if="teacher?.library_status === 'pending' && !isReadonly"
|
|
|
type="success"
|
|
|
@click="approveTeacherAction"
|
|
|
>
|
|
|
确认入库
|
|
|
</el-button>
|
|
|
<el-button
|
|
|
v-if="teacher?.library_status === 'pending' && !isReadonly"
|
|
|
type="danger"
|
|
|
@click="rejectTeacherAction"
|
|
|
>
|
|
|
驳回
|
|
|
</el-button>
|
|
|
<el-button v-if="!isReadonly" type="primary" class="teachers-primary-btn" @click="save">
|
|
|
保存
|
|
|
</el-button>
|
|
|
</template>
|
|
|
|
|
|
<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" class="teachers-primary-btn" :loading="universitySaving" @click="saveUniversityCreate">
|
|
|
保存
|
|
|
</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
<el-dialog v-model="summaryVisible" title="论文摘要" width="520px" append-to-body>
|
|
|
<p class="summary-body">{{ summaryText }}</p>
|
|
|
</el-dialog>
|
|
|
|
|
|
<TeacherPaperDialog
|
|
|
v-model="paperDialog"
|
|
|
:teacher-id="teacherId"
|
|
|
:default-authors="teacher?.name || form.name"
|
|
|
:default-school-name="universityOptions.find((u) => u.id === form.university_id)?.name || teacher?.university_name || ''"
|
|
|
@saved="reloadPapers"
|
|
|
/>
|
|
|
|
|
|
<el-dialog v-model="demandRecordsVisible" title="需求跟进记录" width="640px" append-to-body>
|
|
|
<p class="follow-summary">{{ demandRecordsTitle }}</p>
|
|
|
<div v-for="r in demandRecords" :key="r.id" class="log-card">
|
|
|
<div class="log-head">
|
|
|
<strong>{{ r.operator_name }}</strong>
|
|
|
<span>{{ r.handled_at }}</span>
|
|
|
<el-tag v-if="r.status_item" size="small" :type="demandStatusTagType(r.status_item.value)">
|
|
|
{{ r.status_item.label }}
|
|
|
</el-tag>
|
|
|
</div>
|
|
|
<p>{{ r.content }}</p>
|
|
|
</div>
|
|
|
<el-empty v-if="!demandRecords.length" description="暂无记录" />
|
|
|
</el-dialog>
|
|
|
</el-dialog>
|
|
|
</template>
|
|
|
|
|
|
<style scoped>
|
|
|
.detail-body {
|
|
|
width: 100%;
|
|
|
}
|
|
|
.basic-info-block {
|
|
|
margin-bottom: 16px;
|
|
|
}
|
|
|
.view-desc {
|
|
|
width: 100%;
|
|
|
}
|
|
|
.basic-info-title {
|
|
|
margin: 0 0 12px;
|
|
|
padding-left: 10px;
|
|
|
border-left: 3px solid #244e98;
|
|
|
font-size: 15px;
|
|
|
font-weight: 600;
|
|
|
line-height: 1.4;
|
|
|
color: var(--el-text-color-primary);
|
|
|
}
|
|
|
.crawl-uni-hint {
|
|
|
margin: 0 0 8px;
|
|
|
font-size: 12px;
|
|
|
line-height: 1.5;
|
|
|
color: var(--el-color-warning);
|
|
|
}
|
|
|
.uni-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
width: 100%;
|
|
|
}
|
|
|
.form-small :deep(.el-form-item__label) {
|
|
|
font-size: 13px;
|
|
|
padding-bottom: 4px;
|
|
|
}
|
|
|
.star-preview {
|
|
|
display: block;
|
|
|
margin-top: 4px;
|
|
|
color: #e6a23c;
|
|
|
font-size: 13px;
|
|
|
}
|
|
|
.star-follow-rule {
|
|
|
margin: 6px 0 0;
|
|
|
font-size: 12px;
|
|
|
line-height: 1.45;
|
|
|
color: #6b7280;
|
|
|
}
|
|
|
.section {
|
|
|
margin-top: 16px;
|
|
|
border-top: 1px solid var(--el-border-color-lighter);
|
|
|
padding-top: 12px;
|
|
|
}
|
|
|
.section-head {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
margin-bottom: 8px;
|
|
|
}
|
|
|
.section-head h3 {
|
|
|
margin: 0;
|
|
|
font-size: 15px;
|
|
|
}
|
|
|
.muted {
|
|
|
font-size: 13px;
|
|
|
color: var(--el-text-color-secondary);
|
|
|
}
|
|
|
.section-head .el-button {
|
|
|
margin-left: auto;
|
|
|
}
|
|
|
.summary-body {
|
|
|
white-space: pre-wrap;
|
|
|
margin: 0;
|
|
|
}
|
|
|
.follow-summary {
|
|
|
margin: 0 0 12px;
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
.log-card {
|
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
|
border-radius: 6px;
|
|
|
padding: 10px;
|
|
|
margin-bottom: 8px;
|
|
|
}
|
|
|
.log-head {
|
|
|
display: flex;
|
|
|
gap: 10px;
|
|
|
align-items: center;
|
|
|
margin-bottom: 6px;
|
|
|
font-size: 13px;
|
|
|
}
|
|
|
: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;
|
|
|
}
|
|
|
</style>
|