周报小程序

master
lion 3 days ago
parent bdccbd2a96
commit 9989006f3d

@ -11,6 +11,7 @@ export interface UniversityRow {
longitude?: number | null
address?: string | null
remark?: string | null
sort_order?: number
}
export interface PaperListRow {

@ -43,6 +43,9 @@ export interface TeacherRow {
phone?: string | null
email?: string | null
remark?: string | null
library_status?: 'pending' | 'active' | 'rejected'
library_status_label?: string | null
approved_at?: string | null
}
export interface TeacherStats {
@ -50,6 +53,7 @@ export interface TeacherStats {
month_followed: number
overdue: number
partners: number
library_pending?: number
}
export interface FollowRecordRow {
@ -127,6 +131,21 @@ export async function batchUpdateTeacherStar(payload: {
return data.data
}
export async function approveTeacher(id: number) {
const { data } = await http.post<ApiBody<TeacherRow>>(`/admin/v1/teachers/${id}/approve`)
return data.data
}
export async function rejectTeacher(id: number) {
const { data } = await http.post<ApiBody<TeacherRow>>(`/admin/v1/teachers/${id}/reject`)
return data.data
}
export async function batchApproveTeachers(ids: number[]) {
const { data } = await http.post<ApiBody<{ updated: number }>>('/admin/v1/teachers/batch-approve', { ids })
return data.data
}
export async function fetchTeacherFollowRecords(teacherId: number) {
const { data } = await http.get<ApiBody<{ items: FollowRecordRow[] }>>(
`/admin/v1/teachers/${teacherId}/follow-records`,

@ -55,6 +55,7 @@ async function loadTeachers(page = teacherPage.value) {
page,
page_size: teacherMeta.value.per_page,
keyword: teacherKeyword.value.trim() || undefined,
library_status: 'linkable',
})
teacherItems.value = res.items
teacherMeta.value = res.meta

@ -32,6 +32,7 @@ const form = ref({
latitude: '',
city: '',
province: '',
sort_order: 0,
})
const regionOptions = [
@ -71,7 +72,7 @@ function searchUniversities() {
function openCreate() {
editing.value = null
form.value = { name: '', longitude: '', latitude: '', city: '', province: '' }
form.value = { name: '', longitude: '', latitude: '', city: '', province: '', sort_order: 0 }
dialog.value = true
}
@ -83,6 +84,7 @@ function openEdit(row: UniversityRow) {
latitude: row.latitude != null ? String(row.latitude) : '',
city: row.city || '',
province: row.province || '',
sort_order: row.sort_order ?? 0,
}
dialog.value = true
}
@ -130,6 +132,7 @@ async function save() {
latitude: Number(form.value.latitude),
city: form.value.city || null,
province: form.value.province || null,
sort_order: Number(form.value.sort_order) || 0,
}
if (editing.value) {
await updateUniversity(editing.value.id, payload)
@ -177,6 +180,7 @@ usePageLoad(load)
</div>
<el-table v-loading="loading" :data="items" row-key="id">
<el-table-column prop="sort_order" label="排序" width="80" align="center" />
<el-table-column prop="name" label="高校名称" min-width="180" />
<el-table-column label="经度" width="120">
<template #default="{ row }">{{ row.longitude ?? '—' }}</template>
@ -239,6 +243,10 @@ usePageLoad(load)
</el-form-item>
</el-col>
</el-row>
<el-form-item label="排序">
<el-input-number v-model="form.sort_order" :min="0" :max="99999" />
<p class="list-page-hint" style="margin-top: 8px">数值越小越靠前雷达地图与列表均按此排序</p>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialog = false">取消</el-button>

@ -3,12 +3,14 @@ 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,
@ -62,6 +64,25 @@ const emit = defineEmits<{
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)
@ -584,6 +605,20 @@ watch(
<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>

@ -9,7 +9,9 @@ import TeacherPaperDialog from '@/components/TeacherPaperDialog.vue'
import { fetchAdminUsers } from '@/api/admin/users'
import type { DictItemBrief } from '@/api/admin/teachers'
import {
approveTeacher,
batchUpdateTeacherStar,
batchApproveTeachers,
createTeacher,
createTeacherFollowRecord,
createUniversity,
@ -18,6 +20,7 @@ import {
fetchTeachersList,
fetchTeacherStats,
fetchUniversities,
rejectTeacher,
type FollowRecordRow,
type TeacherRow,
type TeacherStats,
@ -25,7 +28,7 @@ import {
import { followRuleHint, previewNextFollowDate } from '@/utils/teacherFollowRule'
import { starDisplay, sourceTagType, statusTagType, urgencyTagType } from '@/utils/teacherStar'
import { useAuthStore } from '@/stores/auth'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
const auth = useAuthStore()
const isGridMember = computed(() => auth.isGridMember)
@ -44,7 +47,8 @@ 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 stats = ref<TeacherStats>({ month_pending: 0, month_followed: 0, overdue: 0, partners: 0, library_pending: 0 })
const libraryTab = ref<'active' | 'pending' | 'rejected'>('active')
const keyword = ref('')
const filterSource = ref<number | ''>('')
@ -196,6 +200,7 @@ async function load() {
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
params.library_status = libraryTab.value
const res = await fetchTeachersList(params)
items.value = res.items
meta.value = res.meta
@ -225,16 +230,56 @@ function searchTeachers() {
}
function pickStat(bucket: string) {
if (libraryTab.value !== 'active') {
libraryTab.value = 'active'
}
statBucket.value = statBucket.value === bucket ? '' : bucket
page.value = 1
load()
}
function switchLibraryTab(tab: 'active' | 'pending' | 'rejected') {
libraryTab.value = tab
statBucket.value = ''
page.value = 1
selectedRows.value = []
load()
}
async function approveRow(row: TeacherRow) {
await approveTeacher(row.id)
ElMessage.success('已确认入库')
await Promise.all([load(), loadStats()])
}
async function rejectRow(row: TeacherRow) {
await ElMessageBox.confirm(`确定驳回「${row.name}」?驳回后记录会保留。`, '确认驳回', {
type: 'warning',
})
await rejectTeacher(row.id)
ElMessage.success('已驳回')
await Promise.all([load(), loadStats()])
}
async function batchApproveSelected() {
if (!selectedIds.value.length) {
ElMessage.warning('请先选择老师')
return
}
const res = await batchApproveTeachers(selectedIds.value)
ElMessage.success(`已确认入库 ${res.updated} 位老师`)
await Promise.all([load(), loadStats()])
}
function goDetail(row: TeacherRow) {
detailTeacherId.value = row.id
detailVisible.value = true
}
async function onDetailSaved() {
await Promise.all([load(), loadStats()])
}
async function refreshDirectionOptions() {
const filters = await fetchTeacherFilterOptions()
directionOptions.value = filters.research_directions
@ -473,6 +518,14 @@ usePageLoad(async () => {
<div class="page-header">
<PageTitle />
<div class="page-header-actions">
<el-button
v-if="libraryTab === 'pending' && selectedIds.length"
type="success"
size="small"
@click="batchApproveSelected"
>
批量确认入库
</el-button>
<el-button class="btn-action-secondary" size="small" @click="openBatch"></el-button>
<el-button
v-if="!isGridMember"
@ -487,6 +540,15 @@ usePageLoad(async () => {
</div>
<div class="talent-stat-grid">
<button
type="button"
class="talent-stat-card"
:class="{ 'is-active': libraryTab === 'pending' }"
@click="switchLibraryTab('pending')"
>
<div class="talent-stat-label">待确认入库</div>
<div class="talent-stat-value is-warning">{{ stats.library_pending ?? 0 }}</div>
</button>
<button
type="button"
class="talent-stat-card"
@ -526,6 +588,13 @@ usePageLoad(async () => {
</div>
<el-card shadow="never" class="admin-list-card teachers-list-card">
<div class="library-tabs">
<el-radio-group v-model="libraryTab" @change="switchLibraryTab(libraryTab)">
<el-radio-button value="active">正式老师</el-radio-button>
<el-radio-button value="pending">待确认</el-radio-button>
<el-radio-button value="rejected">已驳回</el-radio-button>
</el-radio-group>
</div>
<div class="list-filter-bar">
<el-input
v-model="keyword"
@ -631,24 +700,37 @@ usePageLoad(async () => {
</el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="176" fixed="right">
<el-table-column label="操作" width="220" 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>
<template v-if="row.library_status === 'pending'">
<el-button size="small" type="success" class="teachers-table-btn" @click="approveRow(row)">
确认入库
</el-button>
<el-button size="small" type="danger" class="teachers-table-btn" @click="rejectRow(row)">
驳回
</el-button>
<el-button size="small" class="btn-action-primary teachers-table-btn" @click="openPaper(row)">
论文
</el-button>
</template>
<template v-else-if="libraryTab === 'active'">
<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>
</template>
<el-button size="small" class="btn-action-primary teachers-table-btn" @click="goDetail(row)">
编辑
</el-button>
@ -1021,7 +1103,11 @@ usePageLoad(async () => {
</template>
</el-dialog>
<TeacherDetailDialog v-model="detailVisible" :teacher-id="detailTeacherId" @saved="load" />
<TeacherDetailDialog
v-model="detailVisible"
:teacher-id="detailTeacherId"
@saved="onDetailSaved"
/>
</div>
</template>
@ -1094,6 +1180,14 @@ usePageLoad(async () => {
.talent-stat-value.is-success {
color: var(--el-color-success);
}
.talent-stat-value.is-warning {
color: #d97706;
}
.library-tabs {
margin-bottom: 16px;
}
.filter-source {
width: 132px;
}

Loading…
Cancel
Save