1602 lines
62 KiB
Vue
1602 lines
62 KiB
Vue
<script setup lang="ts">
|
||
import type { FilterState } from '~/components/FilterBar.vue'
|
||
import { onMounted, ref, watch, computed } from 'vue'
|
||
import { onBeforeRouteLeave } from 'vue-router'
|
||
import { getUserMajorList, type MajorItem } from '~/service/api/major'
|
||
import { saveVolunteer, getVolunteerDetail, type VolunteerItem, type VolunteerInfo } from '~/service/api/volunteer'
|
||
|
||
// --- 类型定义 ---
|
||
type TabKey = 'all' | 'hard' | 'risky' | 'safe' | 'stable' | '本科' | '专科' | '985/211/双一流' | '公办本科' | '民办本科'
|
||
type PanelType = 'market' | 'my-volunteers'
|
||
|
||
interface VolunteerTab {
|
||
key: TabKey
|
||
label: string
|
||
count: number
|
||
max: number
|
||
}
|
||
|
||
interface MajorDetail {
|
||
code: string
|
||
name: string
|
||
prob: number
|
||
batch: string
|
||
score: number // 最低分
|
||
diff: number // 线差
|
||
plan: number // 计划数
|
||
req: string // 选科
|
||
tuition: string
|
||
rulesEnrollProbability: string //录取方式
|
||
}
|
||
|
||
// --- 状态数据 ---
|
||
const showSwitchModal = ref(false) // 控制切换方案弹窗显示
|
||
const activePlanId = ref('2025121426') // 当前选中的方案ID
|
||
// 模拟方案列表数据 (对应截图)
|
||
const volunteerPlans = ref([
|
||
{
|
||
id: '2025121426',
|
||
name: '志愿2025121426',
|
||
tag: '手动', // 截图中的橙色标签
|
||
province: '北京',
|
||
artType: '美术与设计类',
|
||
cultureScore: 450,
|
||
cultureSubjects: '物化生',
|
||
artScore: 250,
|
||
updateTime: '2025-12-14 10:33:46',
|
||
status: '有效',
|
||
},
|
||
{
|
||
id: '2025121427',
|
||
name: '志愿2025121427',
|
||
tag: '智能',
|
||
province: '湖北',
|
||
artType: '音乐类',
|
||
cultureScore: 480,
|
||
cultureSubjects: '历史',
|
||
artScore: 240,
|
||
updateTime: '2025-12-13 14:20:00',
|
||
status: '有效',
|
||
},
|
||
])
|
||
|
||
const activePanel = ref<PanelType>('market') // 当前激活的面板
|
||
const myVolunteers = ref<VolunteerItem[]>([])
|
||
const originalVolunteers = ref<VolunteerItem[]>([]) // 用于对比变动
|
||
const currentVolunteerInfo = ref<VolunteerInfo | null>(null)
|
||
|
||
|
||
// 志愿Tab
|
||
const volunteerCurrentTab = ref<string>('本科批')
|
||
const volunteerTabs = ref([
|
||
{ key: '提前批', label: '提前批', count: 0, max: 64 },
|
||
{ key: '本科批', label: '本科批', count: 0, max: 64 },
|
||
{ key: '专科批', label: '专科批', count: 0, max: 64 },
|
||
]) as any
|
||
|
||
|
||
//============= Panel A 数据
|
||
|
||
// 批次Tab
|
||
const currentBatchTab = ref<TabKey>('本科')
|
||
const batchTabs = [
|
||
{ key: '本科提前', label: '本科提前批', count: 0 },
|
||
{ key: '本科', label: '本科批', count: 0 },
|
||
{ key: '高职高专', label: '高职批', count: 0 },
|
||
] as VolunteerTab[]
|
||
|
||
// 二级批次Tab
|
||
const currentBatch2Tab = ref<TabKey>('公办本科')
|
||
const batch2Tabs = [
|
||
{ key: '双一流', label: '985/211/双一流', count: 0 },
|
||
{ key: '公办本科', label: '公办本科', count: 0 },
|
||
{ key: '民办本科', label: '民办本科', count: 0 },
|
||
] as VolunteerTab[]
|
||
|
||
|
||
|
||
|
||
// 概率Tab
|
||
const currentProbTab = ref<TabKey>('all')
|
||
const probTabs = [
|
||
{ key: 'all', label: '全部', count: 0 },
|
||
{ key: 'safe', label: '可保底', count: 1 },
|
||
{ key: 'stable', label: '较稳妥', count: 11 },
|
||
{ key: 'risky', label: '可冲击', count: 11 },
|
||
{ key: 'hard', label: '难录取', count: 145 },
|
||
] as VolunteerTab[]
|
||
|
||
const oldYears = ref(['2025','2024','2023'])
|
||
|
||
const schools = ref<MajorItem[]>([])
|
||
const page = ref(1)
|
||
const size = ref(10)
|
||
const total = ref(0)
|
||
const isLoading = ref(false)
|
||
const isFinished = ref(false)
|
||
const scrollContainer = ref<HTMLElement | null>(null)
|
||
|
||
// 弹窗相关状态
|
||
const showModal = ref(false)
|
||
const currentSchool = ref<MajorItem | null>(null)
|
||
const modalLoading = ref(false)
|
||
const modalMajors = ref<MajorDetail[]>([])
|
||
|
||
// 选课/志愿相关状态
|
||
const selectedMajorCodes = ref<string[]>([]) // 存储已选的专业Code,数组顺序即为志愿顺序
|
||
const showSaveConfirm = ref(false) // 控制气泡确认框显示
|
||
const isSaving = ref(false) // 保存接口Loading状态
|
||
|
||
|
||
//============= Panel A 数据
|
||
// --- 辅助函数 ---
|
||
function getProbabilityLabel(prob: number): string {
|
||
if (prob >= 93) return '保'
|
||
if (prob >= 73) return '稳'
|
||
if (prob >= 60) return '冲'
|
||
return '难'
|
||
}
|
||
|
||
function getStatusColor(status: string) {
|
||
switch (status) {
|
||
case '保':
|
||
return 'border-green-500 text-green-600 bg-green-50'
|
||
case '稳':
|
||
return 'border-blue-500 text-blue-600 bg-blue-50'
|
||
case '冲':
|
||
return 'border-orange-500 text-orange-500 bg-orange-50'
|
||
default:
|
||
return 'border-gray-400 text-gray-400 bg-gray-50'
|
||
}
|
||
}
|
||
|
||
// --- 数据加载逻辑 ---
|
||
async function loadMore(reset = false) {
|
||
if (isLoading.value) return
|
||
if (!reset && isFinished.value) return
|
||
|
||
isLoading.value = true
|
||
if (reset) {
|
||
page.value = 1
|
||
schools.value = []
|
||
isFinished.value = false
|
||
}
|
||
|
||
try {
|
||
// 映射 Tab 到 API 参数
|
||
let probability: string | undefined
|
||
if (currentProbTab.value === 'hard') probability = '难录取'
|
||
if (currentProbTab.value === 'risky') probability = '可冲击'
|
||
if (currentProbTab.value === 'stable') probability = '较稳妥'
|
||
if (currentProbTab.value === 'safe') probability = '可保底'
|
||
|
||
const res = await getUserMajorList({
|
||
page: page.value,
|
||
size: size.value,
|
||
probability,
|
||
batch: currentBatchTab.value,
|
||
batch2: currentBatch2Tab.value,
|
||
})
|
||
console.warn(res)
|
||
if (res && res.list && res.list.items) {
|
||
schools.value.push(...res.list.items)
|
||
total.value = res.total
|
||
|
||
// 更新 Tabs 计数
|
||
if (res.list.probCount) {
|
||
probTabs.forEach(tab => {
|
||
if (tab.key && tab.key == 'all'){
|
||
tab.count = total.value
|
||
}else if (tab.key && res.list.probCount[tab.key] !== undefined) {
|
||
tab.count = res.list.probCount[tab.key]
|
||
}
|
||
})
|
||
}
|
||
|
||
if (schools.value.length >= res.total || res.list.items.length < size.value) {
|
||
isFinished.value = true
|
||
} else {
|
||
page.value++
|
||
}
|
||
} else {
|
||
isFinished.value = true
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load major list:', error)
|
||
isFinished.value = true
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 监听 Tab 切换,重新加载
|
||
watch(currentBatchTab, () => {
|
||
loadMore(true)
|
||
})
|
||
|
||
watch(currentBatch2Tab, () => {
|
||
loadMore(true)
|
||
})
|
||
|
||
watch(currentProbTab, () => {
|
||
loadMore(true)
|
||
})
|
||
|
||
watch(activePanel, (newVal, oldVal) => {
|
||
if (oldVal === 'my-volunteers' && newVal === 'market' && isModified.value) {
|
||
const confirmRevert = window.confirm('您有未保存的志愿变动,切换回模拟填报将恢复初始状态,确认吗?')
|
||
if (confirmRevert) {
|
||
// 恢复数据
|
||
myVolunteers.value = JSON.parse(JSON.stringify(originalVolunteers.value))
|
||
} else {
|
||
// 阻止切换 (由于 watch 是在变化后触发的,这里需要 nextTick 或者特殊处理)
|
||
// 注意:直接修改 activePanel.value 会再次触发 watch,但因为满足 newVal === 'my-volunteers' 不会再次弹窗
|
||
activePanel.value = 'my-volunteers'
|
||
}
|
||
}
|
||
})
|
||
|
||
|
||
|
||
function handleScroll() {
|
||
if (!scrollContainer.value)
|
||
return
|
||
const { scrollTop, clientHeight, scrollHeight } = scrollContainer.value
|
||
// 距离底部 50px 时触发加载
|
||
if (scrollTop + clientHeight >= scrollHeight - 50) {
|
||
loadMore()
|
||
}
|
||
}
|
||
|
||
// --- 弹窗逻辑 ---
|
||
async function openMajorModal(school: MajorItem) {
|
||
currentSchool.value = school
|
||
showModal.value = true
|
||
modalLoading.value = true
|
||
modalMajors.value = []
|
||
// 重置弹窗内的状态
|
||
selectedMajorCodes.value = []
|
||
showSaveConfirm.value = false
|
||
|
||
try {
|
||
const res = await getUserMajorList({
|
||
schoolCode: school.schoolCode,
|
||
batch: currentBatchTab.value,
|
||
page: 1,
|
||
size: 100 // 获取该校所有推荐专业
|
||
})
|
||
|
||
if (res && res.list && res.list.items) {
|
||
modalMajors.value = res.list.items.map(m => ({
|
||
code: m.majorCode,
|
||
name: m.majorName,
|
||
prob: m.enrollProbability,
|
||
score: m.studentScore,
|
||
batch: m.batch != "高职高专" ? "本科": "高职高专",
|
||
rulesEnrollProbability: m.rulesEnrollProbability,
|
||
diff: 0,
|
||
plan: m.planNum,
|
||
req: m.mainSubjects,
|
||
tuition: m.tuition,
|
||
enrollmentCode: m.enrollmentCode // 保持后端原始字段以便拼接
|
||
}))
|
||
}
|
||
} catch (error) {
|
||
console.error('获取学校专业列表失败:', error)
|
||
} finally {
|
||
modalLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 2. 选择/取消专业 (点击行内按钮)
|
||
function toggleMajor(major: any) {
|
||
if (!currentSchool.value) return
|
||
|
||
// 拼接格式: schoolCode_majorCode_enrollmentCode
|
||
const fullCode = `${currentSchool.value.schoolCode}_${major.code}_${major.enrollmentCode || currentSchool.value.enrollmentCode}`
|
||
const index = selectedMajorCodes.value.indexOf(fullCode)
|
||
|
||
if (index > -1) {
|
||
// 已存在,移除
|
||
selectedMajorCodes.value.splice(index, 1)
|
||
}
|
||
else {
|
||
// 不存在,加入
|
||
selectedMajorCodes.value.push(fullCode)
|
||
}
|
||
}
|
||
|
||
// 获取按钮状态显示文本
|
||
function getVolunteerBtnText(major: any) {
|
||
if (!currentSchool.value) return '加入志愿单'
|
||
const fullCode = `${currentSchool.value.schoolCode}_${major.code}_${major.enrollmentCode || currentSchool.value.enrollmentCode}`
|
||
const index = selectedMajorCodes.value.indexOf(fullCode)
|
||
if (index > -1) {
|
||
return `志愿 ${index + 1}`
|
||
}
|
||
return '加入志愿单'
|
||
}
|
||
|
||
/**
|
||
* 获取志愿详情
|
||
*/
|
||
async function fetchVolunteerDetail() {
|
||
try {
|
||
const res = await getVolunteerDetail()
|
||
if (res && res.volunteer) {
|
||
currentVolunteerInfo.value = res.volunteer
|
||
// 合并所有批次数据用于 Panel B 显示(根据当前 Tab 过滤)
|
||
const allItems: VolunteerItem[] = []
|
||
Object.keys(res.items).forEach(batch => {
|
||
const items = res.items[batch].map(item => ({...item, batchName: batch}))
|
||
allItems.push(...items)
|
||
|
||
// 更新 Tab 计数
|
||
const tab = volunteerTabs.value.find((t: any) => t.key === batch)
|
||
if (tab) {
|
||
tab.count = res.items[batch].length
|
||
}
|
||
})
|
||
|
||
myVolunteers.value = allItems
|
||
// 备份原始数据用于变动对比
|
||
originalVolunteers.value = JSON.parse(JSON.stringify(allItems))
|
||
}
|
||
} catch (error) {
|
||
console.error('获取志愿详情失败:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 过滤后的当前显示志愿(Panel B)
|
||
*/
|
||
const filteredVolunteers = computed(() => {
|
||
// 根据 volunteerCurrentTab 过滤
|
||
// 直接从 myVolunteers 中过滤出属于该批次的
|
||
// 注意:fetchVolunteerDetail 时我们需要给 item 加上 batch 标识,或者重新设计存储
|
||
return myVolunteers.value.filter(v => (v as any).batchName === volunteerCurrentTab.value)
|
||
})
|
||
|
||
/**
|
||
* 是否修改了志愿列表(拖拽排序或删除)
|
||
*/
|
||
const isModified = computed(() => {
|
||
if (myVolunteers.value.length !== originalVolunteers.value.length) return true
|
||
// 检查 ID 顺序是否一致
|
||
return myVolunteers.value.some((v, i) => v.id !== originalVolunteers.value[i]?.id)
|
||
})
|
||
|
||
// 3. 保存逻辑
|
||
async function saveVolunteers() {
|
||
if (isSaving.value)
|
||
return
|
||
|
||
const codesToSave = activePanel.value === 'market'
|
||
? selectedMajorCodes.value
|
||
: myVolunteers.value.map(v => `${v.schoolCode}_${v.majorCode}_${v.enrollmentCode}`)
|
||
|
||
if (codesToSave.length === 0 && activePanel.value === 'market') {
|
||
// 弹窗中没选专业
|
||
showSaveConfirm.value = false
|
||
return
|
||
}
|
||
|
||
isSaving.value = true
|
||
|
||
try {
|
||
await saveVolunteer(codesToSave)
|
||
console.warn('保存成功')
|
||
|
||
// 成功后刷新详情
|
||
await fetchVolunteerDetail()
|
||
|
||
// 成功后逻辑
|
||
isSaving.value = false
|
||
showSaveConfirm.value = false
|
||
if (showModal.value) closeModal()
|
||
|
||
// Toast 提示
|
||
// @ts-ignore
|
||
window.$message?.success?.('保存志愿成功!')
|
||
} catch (error) {
|
||
console.error('保存失败:', error)
|
||
isSaving.value = false
|
||
}
|
||
}
|
||
|
||
// 页面离开保护
|
||
onBeforeRouteLeave((to, from, next) => {
|
||
if (isModified.value) {
|
||
const confirmLeave = window.confirm('您有未保存的志愿变动,确定要离开吗?')
|
||
if (confirmLeave) {
|
||
next()
|
||
} else {
|
||
next(false)
|
||
}
|
||
} else {
|
||
next()
|
||
}
|
||
})
|
||
|
||
function closeModal() {
|
||
showModal.value = false
|
||
showSaveConfirm.value = false
|
||
}
|
||
|
||
// 为了节省流量,我们可以加一个简单的判断:只有是 PC 端时才初始加载数据
|
||
onMounted(() => {
|
||
// 检查窗口宽度是否大于 1024px
|
||
if (window.innerWidth >= 1024) {
|
||
loadMore()
|
||
fetchVolunteerDetail()
|
||
}
|
||
|
||
// 生成一些模拟的“我的志愿”数据
|
||
// const initialVols = generateMockData(5, 1000)
|
||
// // 为了模拟真实志愿,给它们加上自定义的序号或者直接利用数组索引
|
||
// myVolunteers.value = initialVols
|
||
// 监听窗口大小变化(可选:如果用户旋转屏幕或拖拽窗口,动态决定是否加载)
|
||
window.addEventListener('resize', handleResize)
|
||
})
|
||
|
||
function handleResize() {
|
||
// 如果之前没加载过,且现在变宽了,则加载数据
|
||
if (
|
||
window.innerWidth >= 1024
|
||
&& schools.value.length === 0
|
||
&& !isLoading.value
|
||
) {
|
||
loadMore()
|
||
}
|
||
}
|
||
|
||
const currentFilters = ref<FilterState | null>(null)
|
||
const currentKeyword = ref('')
|
||
|
||
function handleDataChange(data: { keyword: string, filters: FilterState }) {
|
||
console.warn('发起请求:', data.keyword, data.filters)
|
||
currentKeyword.value = data.keyword
|
||
currentFilters.value = data.filters
|
||
}
|
||
|
||
// 用于记录当前鼠标是否悬停在手柄上,悬停时该行的索引存入这里
|
||
const dragEnabledIndex = ref<number | null>(null)
|
||
// --- 拖拽排序逻辑 (Panel B) ---
|
||
const dragStartIndex = ref<number | null>(null)
|
||
|
||
function switchVolunteerTab(tabKey: TabKey) {
|
||
volunteerCurrentTab.value = tabKey
|
||
}
|
||
|
||
function handleDragStart(e: DragEvent, index: number) {
|
||
dragStartIndex.value = index
|
||
if (e.dataTransfer) {
|
||
e.dataTransfer.effectAllowed = 'move'
|
||
// 某些浏览器需要设置数据才能拖拽
|
||
e.dataTransfer.setData('text/plain', index.toString())
|
||
// 可选:设置拖拽时的重影图像(使其看起来更好看,不包含多余背景)
|
||
// e.dataTransfer.setDragImage(target.closest('tr') as Element, 0, 0)
|
||
}
|
||
}
|
||
|
||
function handleDragOver(e: DragEvent) {
|
||
// 必须阻止默认行为才能触发 drop
|
||
e.preventDefault()
|
||
if (e.dataTransfer) {
|
||
e.dataTransfer.dropEffect = 'move'
|
||
}
|
||
}
|
||
|
||
function handleDrop(index: number) {
|
||
if (dragStartIndex.value === null || dragStartIndex.value === index)
|
||
return
|
||
|
||
const draggedItem = filteredVolunteers.value[dragStartIndex.value]
|
||
const targetItem = filteredVolunteers.value[index]
|
||
|
||
const realStartIdx = myVolunteers.value.findIndex(v => v.id === draggedItem.id)
|
||
const realTargetIdx = myVolunteers.value.findIndex(v => v.id === targetItem.id)
|
||
|
||
if (realStartIdx === -1 || realTargetIdx === -1) return
|
||
|
||
// 移动数组元素
|
||
const [removed] = myVolunteers.value.splice(realStartIdx, 1)
|
||
myVolunteers.value.splice(realTargetIdx, 0, removed)
|
||
|
||
// 触发结束事件
|
||
handleDragEnd()
|
||
|
||
dragStartIndex.value = null
|
||
}
|
||
|
||
function handleDragEnd() {
|
||
console.warn('拖拽排序结束,当前志愿顺序ID:', myVolunteers.value.map(v => v.id))
|
||
// 这里可以调用保存接口 API
|
||
}
|
||
|
||
function removeVolunteer(index: number) {
|
||
// index 是在 myVolunteers 中的索引
|
||
myVolunteers.value.splice(index, 1)
|
||
}
|
||
|
||
function handleCreatePlan() {
|
||
console.warn('点击新建方案')
|
||
// 逻辑:弹出新建表单...
|
||
}
|
||
|
||
function handleEditPlan() {
|
||
console.warn('点击修改方案信息')
|
||
// 逻辑:修改当前方案的分数/选科等...
|
||
}
|
||
|
||
function handleSwitchPlan() {
|
||
showSwitchModal.value = true
|
||
}
|
||
|
||
function handleExportPlan() {
|
||
console.warn('点击导出当前方案')
|
||
}
|
||
|
||
// 切换到指定方案
|
||
function switchActivePlan(planId: string) {
|
||
activePlanId.value = planId
|
||
showSwitchModal.value = false
|
||
console.warn('切换到了方案:', planId)
|
||
// 逻辑:重新加载 myVolunteers 数据...
|
||
// isLoading.value = true ...
|
||
}
|
||
|
||
function deletePlan(planId: string) {
|
||
console.warn('删除方案:', planId)
|
||
// 逻辑:删除API调用...
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<!-- =========================================================
|
||
1. 移动端提示层 (Phone View)
|
||
逻辑:默认显示 (flex),在 lg (1024px) 及以上屏幕隐藏
|
||
========================================================== -->
|
||
<div class="w-full flex flex-col items-center justify-center bg-gray-50 p-8 text-center lg:hidden">
|
||
<div class="max-w-sm w-full rounded-2xl bg-white p-8 shadow-lg">
|
||
<!-- 图标 (手机) -->
|
||
<div class="mx-auto mb-4 h-16 w-16 flex items-center justify-center rounded-full bg-blue-50 text-blue-500">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path
|
||
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
|
||
<h3 class="mb-2 text-xl text-slate-800 font-bold">
|
||
请访问小程序
|
||
</h3>
|
||
<p class="mb-6 text-sm text-slate-500 leading-relaxed">
|
||
当前页面包含大量数据表格,<br>为保证最佳体验,请在 PC 端浏览<br>或前往微信小程序查看。
|
||
</p>
|
||
|
||
<!-- 模拟一个小程序跳转按钮 -->
|
||
<button
|
||
class="w-full rounded-full bg-blue-600 py-2.5 text-sm text-white font-medium shadow-blue-200 shadow-md transition-colors hover:bg-blue-700"
|
||
>
|
||
打开微信小程序
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- =========================================================
|
||
新增:左侧固定侧边栏
|
||
========================================================== -->
|
||
<div
|
||
class="fixed left-0 top-24 z-50 hidden w-15 flex-col select-none items-center border-r border-slate-200 rounded-lg bg-white py-8 shadow-lg lg:flex"
|
||
>
|
||
<!-- Logo 占位 -->
|
||
<!-- <div class="mb-8">
|
||
<div class="h-10 w-10 rounded-lg bg-blue-600"></div>
|
||
</div> -->
|
||
|
||
<div class="w-full flex flex-col gap-6 px-2">
|
||
<button
|
||
class="group flex flex-col items-center gap-1 rounded-lg p-2 transition-colors"
|
||
:class="activePanel === 'market' ? 'bg-blue-50 text-blue-600' : 'text-slate-500 hover:bg-slate-50'"
|
||
@click="activePanel = 'market'"
|
||
>
|
||
<div
|
||
class="rounded-full p-2"
|
||
:class="activePanel === 'market' ? 'bg-blue-100' : 'bg-slate-100 group-hover:bg-slate-200'"
|
||
>
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
>
|
||
<path
|
||
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<span class="text-xs font-medium">模拟填报</span>
|
||
</button>
|
||
|
||
<button
|
||
class="group flex flex-col items-center gap-1 rounded-lg p-2 transition-colors"
|
||
:class="activePanel === 'my-volunteers' ? 'bg-blue-50 text-blue-600' : 'text-slate-500 hover:bg-slate-50'"
|
||
@click="activePanel = 'my-volunteers'"
|
||
>
|
||
<div
|
||
class="relative rounded-full p-2"
|
||
:class="activePanel === 'my-volunteers' ? 'bg-blue-100' : 'bg-slate-100 group-hover:bg-slate-200'"
|
||
>
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
>
|
||
<path
|
||
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
|
||
/>
|
||
</svg>
|
||
<!-- 数量角标 -->
|
||
<span
|
||
v-if="myVolunteers.length > 0"
|
||
class="absolute h-5 w-5 flex items-center justify-center rounded-full bg-red-500 text-[10px] text-white -right-1 -top-1"
|
||
>
|
||
{{ myVolunteers.length }}
|
||
</span>
|
||
</div>
|
||
<span class="text-xs font-medium">我的志愿</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
id="bPanel"
|
||
class="mx-auto hidden h-screen max-w-7xl flex-col select-none px-4 text-slate-700 font-sans lg:flex lg:px-8 sm:px-6"
|
||
>
|
||
<!-- =========================================================
|
||
Panel A: 模拟填志愿 (原有的市场列表)
|
||
使用 v-show 控制显示
|
||
========================================================== -->
|
||
<div v-show="activePanel === 'market'" class="h-full flex flex-col">
|
||
<!-- 顶部筛选栏 (保持不变) -->
|
||
<div class="mt-8 rounded-t-lg bg-white p-6 border-b border-slate-200">
|
||
<div class="grid grid-cols-1 select-none gap-6 lg:grid-cols-6">
|
||
<!-- Top Toolbar -->
|
||
<div class="lg:col-span-4">
|
||
<FilterBar :sort-enabled="false" @change="handleDataChange" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="flex flex-shrink-0 space-x-2 justify-between mb-8">
|
||
<div class="flex flex-shrink-0 space-x-2">
|
||
<button
|
||
v-for="tab in batchTabs" :key="tab.key"
|
||
class="border-x border-t border-transparent rounded-b-md px-6 py-2 text-sm font-medium transition-colors duration-200"
|
||
:class="[
|
||
currentBatchTab === tab.key
|
||
? 'bg-blue-500 text-white border-blue-500'
|
||
: 'bg-white text-slate-600 hover:text-blue-500 border-slate-200',
|
||
]" @click="currentBatchTab = tab.key"
|
||
>
|
||
{{ tab.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="flex flex-shrink-0 space-x-2" v-show="currentBatchTab == '本科'">
|
||
<button
|
||
v-for="tab in batch2Tabs" :key="tab.key"
|
||
class="border-x border-t border-transparent rounded-b-md px-6 py-2 text-sm font-medium transition-colors duration-200"
|
||
:class="[
|
||
currentBatch2Tab === tab.key
|
||
? 'bg-blue-500 text-white border-blue-500'
|
||
: 'bg-white text-slate-600 hover:text-blue-500 border-slate-200',
|
||
]" @click="currentBatch2Tab = tab.key"
|
||
>
|
||
{{ tab.label }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<div class="flex flex-shrink-0 space-x-2">
|
||
<button
|
||
v-for="tab in probTabs" :key="tab.key"
|
||
class="border-x border-t border-transparent rounded-t-md px-6 py-2 text-sm font-medium transition-colors duration-200"
|
||
:class="[
|
||
currentProbTab === tab.key
|
||
? 'bg-blue-500 text-white border-blue-500'
|
||
: 'bg-white text-slate-600 hover:text-blue-500 border-slate-200',
|
||
]" @click="currentProbTab = tab.key"
|
||
>
|
||
{{ tab.label }} <span class="ml-1 opacity-90">{{ tab.count }}</span>
|
||
</button>
|
||
</div>
|
||
|
||
|
||
<!-- 表格容器:限制高度 + 滚动监听 -->
|
||
<!-- 关键点:h-[calc(100vh-150px)] 用于限制高度,overflow-auto 用于滚动 -->
|
||
<div
|
||
ref="scrollContainer"
|
||
class="relative flex-1 h-[calc(100vh-320px)] overflow-auto scroll-smooth border border-slate-200 rounded-b-md bg-white shadow-sm"
|
||
@scroll="handleScroll"
|
||
>
|
||
<table class="relative min-w-[1500px] w-full border-collapse text-sm">
|
||
<!-- 表头 -->
|
||
<thead class="sticky top-0 z-30 bg-slate-50 text-center text-slate-500 font-medium shadow-sm">
|
||
<tr>
|
||
<!-- 左侧冻结列:院校 -->
|
||
<th
|
||
class="sticky left-0 z-40 w-52 border-r border-slate-200 bg-slate-50 p-4 text-left shadow-[4px_0_8px_-4px_rgba(0,0,0,0.1)]"
|
||
>
|
||
招生院校
|
||
</th>
|
||
<th class="w-auto border-r border-slate-200 p-4">
|
||
招生专业
|
||
</th>
|
||
<th class="w-40 border-r border-slate-200 p-2">
|
||
录取概率
|
||
</th>
|
||
<th class="w-32 border-r border-slate-200 p-2">
|
||
26省内招生
|
||
</th>
|
||
<!-- 假设中间有很多历年数据列,撑开宽度 -->
|
||
<th class="w-20 border-r border-slate-200 p-2">
|
||
历年
|
||
</th>
|
||
<th class="w-32 border-r border-slate-200 p-2" v-for="(item) in oldYears" :key="item">
|
||
{{item}}
|
||
</th>
|
||
<!-- 右侧冻结列:操作 -->
|
||
<th
|
||
class="sticky right-0 z-40 w-40 border-l border-slate-200 bg-slate-50 p-4 shadow-[-4px_0_8px_-4px_rgba(0,0,0,0.1)]"
|
||
>
|
||
操作
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
|
||
<!-- 表格内容 -->
|
||
<tbody class="divide-y divide-slate-200">
|
||
<template v-for="school in schools" :key="school.schoolCode + school.majorCode">
|
||
<!-- Row 1 -->
|
||
<tr class="group transition-colors hover:bg-slate-50">
|
||
<!-- Sticky Left: 院校信息 -->
|
||
<!-- 注意:bg-white 是为了遮挡滚动的文字,group-hover:bg-slate-50 是为了保持 hover 效果 -->
|
||
<td
|
||
rowspan="4"
|
||
class="sticky left-0 z-20 border-r border-slate-200 bg-white p-4 align-top shadow-[4px_0_8px_-4px_rgba(0,0,0,0.1)] group-hover:bg-slate-50"
|
||
>
|
||
<div class="mb-1 w-52 truncate text-left text-base text-slate-900 font-bold" :title="school.schoolName">
|
||
{{ school.schoolName }}
|
||
</div>
|
||
<div class="mb-2 flex flex-wrap gap-1 text-xs text-slate-500">
|
||
<span v-for="tag in [school.province, school.schoolNature, school.institutionType].filter(Boolean)" :key="tag" class="rounded bg-slate-100 px-1 py-0.5">{{ tag
|
||
}}</span>
|
||
</div>
|
||
<div class="text-left text-xs text-slate-400">
|
||
代码 {{ school.schoolCode }}
|
||
</div>
|
||
</td>
|
||
|
||
<!-- 中间普通滚动列 -->
|
||
|
||
<td rowspan="4" class="border-r border-slate-100 bg-white p-4 align-top group-hover:bg-slate-50">
|
||
<div class="mb-1 text-slate-900 font-bold">
|
||
{{ school.majorName }}
|
||
</div>
|
||
<div class="mb-1 text-xs text-slate-500">
|
||
{{ school.limitation }}
|
||
</div>
|
||
<div class="mt-2 text-xs text-slate-400">
|
||
{{ `[${school.enrollmentCode}]` }} {{ school.tuition }}
|
||
</div>
|
||
</td>
|
||
|
||
<td
|
||
rowspan="4"
|
||
class="border-r border-slate-100 bg-white p-2 text-center align-top group-hover:bg-slate-50"
|
||
>
|
||
<div class="mb-2 text-lg font-bold">
|
||
{{ school.enrollProbability }}%
|
||
</div>
|
||
<div class="mb-2 flex justify-center">
|
||
<div
|
||
class="h-8 w-8 flex items-center justify-center border-2 rounded-full text-xs font-bold"
|
||
:class="getStatusColor(getProbabilityLabel(school.enrollProbability))"
|
||
>
|
||
{{ getProbabilityLabel(school.enrollProbability) }}
|
||
</div>
|
||
</div>
|
||
<div class="mb-2 flex justify-center">
|
||
折合分:{{school.studentScore}}
|
||
</div>
|
||
</td>
|
||
|
||
<td
|
||
rowspan="4"
|
||
class="border-r border-slate-100 bg-white p-2 text-center align-top group-hover:bg-slate-50"
|
||
>
|
||
<div class="text-lg font-medium">
|
||
{{ school.planNum }}人
|
||
</div>
|
||
</td>
|
||
|
||
<!-- 历年数据区域 (横向较宽) -->
|
||
<td
|
||
class="h-10 border-r border-slate-100 bg-slate-50/50 px-2 py-2 text-center text-xs text-slate-500"
|
||
>
|
||
招生人数
|
||
</td>
|
||
<td class="border-r border-slate-100 px-2 py-2 text-center text-sm" v-for="year in oldYears" :key="year">
|
||
{{ school.historyMajorEnrollMap?.[year]?.enrollmentCount || "-" }}
|
||
</td>
|
||
<!-- Sticky Right: 操作 -->
|
||
<td
|
||
rowspan="4"
|
||
class="sticky right-0 z-20 border-l border-slate-200 bg-white p-4 text-center align-middle shadow-[-4px_0_8px_-4px_rgba(0,0,0,0.1)] group-hover:bg-slate-50"
|
||
>
|
||
<button
|
||
class="whitespace-nowrap border border-blue-500 rounded-full bg-white px-3 py-1.5 text-xs text-blue-500 transition-colors hover:bg-blue-50"
|
||
@click="openMajorModal(school)"
|
||
>
|
||
查看可选专业
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Row 2: 最低分数 -->
|
||
<tr class="group hover:bg-slate-50">
|
||
<td
|
||
class="h-10 border-r border-slate-100 bg-slate-50/50 px-2 py-2 text-center text-xs text-slate-500"
|
||
>
|
||
最低分数
|
||
</td>
|
||
<td class="border-r border-slate-100 px-2 py-2 text-center text-sm font-medium" v-for="year in oldYears" :key="year">
|
||
{{ school.historyMajorEnrollMap?.[year]?.admissionLine || "-" }}
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Row 3: 历年线差 -->
|
||
<tr class="group hover:bg-slate-50">
|
||
<td
|
||
class="h-10 border-r border-slate-100 bg-slate-50/50 px-2 py-2 text-center text-xs text-slate-500"
|
||
>
|
||
历年线差
|
||
</td>
|
||
<td class="border-r border-slate-100 px-2 py-2 text-center text-sm" v-for="year in oldYears" :key="year">
|
||
{{ (school.historyMajorEnrollMap?.[year]?.admissionLine && school.historyMajorEnrollMap?.[year]?.controlLine)
|
||
? (school.historyMajorEnrollMap[year].admissionLine - school.historyMajorEnrollMap[year].controlLine).toFixed(1)
|
||
: "-" }}
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Row 4: 录取方式 -->
|
||
<tr class="group border-b border-slate-200 hover:bg-slate-50">
|
||
<td
|
||
class="h-10 border-r border-slate-100 bg-slate-50/50 px-2 py-2 text-center text-xs text-slate-500"
|
||
>
|
||
录取方式
|
||
</td>
|
||
<td class="border-r border-slate-100 px-2 py-2 text-center text-xs text-slate-600" v-for="year in oldYears" :key="year">
|
||
{{ school.historyMajorEnrollMap?.[year]?.rulesEnrollProbability || "-" }}
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
|
||
<!-- 加载状态条 -->
|
||
<tr v-if="isLoading || isFinished">
|
||
<td colspan="100%" class="sticky left-0 bg-slate-50 p-4 text-center text-sm text-slate-500">
|
||
<span v-if="isLoading" class="flex items-center justify-center gap-2">
|
||
<svg
|
||
class="h-5 w-5 animate-spin text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||
<path
|
||
class="opacity-75" fill="currentColor"
|
||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||
/>
|
||
</svg>
|
||
正在加载更多数据...
|
||
</span>
|
||
<span v-else-if="isFinished">没有更多数据了</span>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- =========================================================
|
||
Panel B: 我的志愿表 (新增)
|
||
v-show="activePanel === 'my-volunteers'"
|
||
========================================================== -->
|
||
<div v-show="activePanel === 'my-volunteers'" class="h-full flex flex-col animate-fade-in-up">
|
||
<div
|
||
class="mb-6 mt-8 flex flex-col gap-4 rounded-lg bg-white p-6 shadow lg:flex-row lg:items-center lg:justify-between"
|
||
>
|
||
<!-- 左侧:标题与统计 -->
|
||
<div>
|
||
<div class="flex items-center gap-3">
|
||
<h2 class="text-2xl text-slate-800 font-bold">
|
||
{{ volunteerPlans.find(p => p.id === activePlanId)?.name || '未命名方案' }}
|
||
</h2>
|
||
<span class="border border-orange-100 rounded bg-orange-50 px-2 py-0.5 text-xs text-orange-500">
|
||
{{ volunteerPlans.find(p => p.id === activePlanId)?.tag || '手动' }}
|
||
</span>
|
||
</div>
|
||
<p class="mt-2 text-sm text-slate-500">
|
||
拖拽左侧手柄可调整顺序,共 <span class="text-blue-600 font-bold">{{ myVolunteers.length }}</span> 个志愿
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 右侧:功能按钮组 -->
|
||
<div class="flex flex-wrap items-center gap-3">
|
||
<!-- 新建 -->
|
||
<button
|
||
class="flex items-center gap-1.5 border border-transparent rounded-md px-3 py-2 text-sm text-slate-600 transition-colors hover:border-slate-200 hover:bg-slate-50 hover:text-blue-600"
|
||
@click="handleCreatePlan"
|
||
>
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
>
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
新建
|
||
</button>
|
||
|
||
<!-- 修改 -->
|
||
<button
|
||
class="flex items-center gap-1.5 border border-transparent rounded-md px-3 py-2 text-sm text-slate-600 transition-colors hover:border-slate-200 hover:bg-slate-50 hover:text-blue-600"
|
||
@click="handleEditPlan"
|
||
>
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
>
|
||
<path
|
||
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||
/>
|
||
</svg>
|
||
修改
|
||
</button>
|
||
|
||
<!-- 切换 (点击触发弹窗) -->
|
||
<button
|
||
class="flex items-center gap-1.5 border border-transparent rounded-md px-3 py-2 text-sm text-slate-600 transition-colors hover:border-slate-200 hover:bg-slate-50 hover:text-blue-600"
|
||
@click="handleSwitchPlan"
|
||
>
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
>
|
||
<path
|
||
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"
|
||
/>
|
||
</svg>
|
||
切换
|
||
</button>
|
||
|
||
<!-- 导出 -->
|
||
<button
|
||
class="flex items-center gap-1.5 border border-transparent rounded-md px-3 py-2 text-sm text-slate-600 transition-colors hover:border-slate-200 hover:bg-slate-50 hover:text-blue-600"
|
||
@click="handleExportPlan"
|
||
>
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
>
|
||
<path
|
||
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||
/>
|
||
</svg>
|
||
导出
|
||
</button>
|
||
|
||
<!-- 提交按钮 (突出显示) -->
|
||
<div class="mx-1 h-6 w-px bg-slate-200" />
|
||
<button
|
||
:disabled="!isModified || isSaving"
|
||
class="ml-1 rounded bg-blue-600 px-5 py-2 text-sm text-white font-medium shadow-md transition-all active:scale-95 hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
@click="saveVolunteers"
|
||
>
|
||
<span v-if="isSaving" class="mr-1 inline-block h-3 w-3 animate-spin border-2 border-white/30 border-t-white rounded-full"></span>
|
||
提交志愿表
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex flex-shrink-0 space-x-2">
|
||
<button
|
||
v-for="tab in volunteerTabs" :key="tab.key"
|
||
class="border-x border-t border-transparent rounded-t-md px-6 py-2 text-sm font-medium transition-colors duration-200"
|
||
:class="[
|
||
volunteerCurrentTab === tab.key
|
||
? 'bg-blue-500 text-white border-blue-500'
|
||
: 'bg-white text-slate-600 hover:text-blue-500 border-slate-200',
|
||
]" @click="switchVolunteerTab(tab.key)"
|
||
>
|
||
{{ tab.label }} <span class="ml-1 opacity-90">({{ `${tab.count}/${tab.max}` }})</span>
|
||
</button>
|
||
</div>
|
||
<div
|
||
class="relative flex-1 overflow-auto scroll-smooth border border-slate-200 rounded-b-md bg-white shadow-sm"
|
||
>
|
||
<table class="w-full border-collapse text-sm">
|
||
<thead class="sticky top-0 z-30 bg-slate-50 text-slate-500 font-medium shadow-sm">
|
||
<tr>
|
||
<th class="w-16 border-r border-slate-200 p-4 text-center">
|
||
排序
|
||
</th>
|
||
<th class="w-20 border-r border-slate-200 p-4 text-center">
|
||
序号
|
||
</th>
|
||
<th class="border-r border-slate-200 p-4 text-left">
|
||
院校信息
|
||
</th>
|
||
<th class="border-r border-slate-200 p-4 text-left">
|
||
专业信息
|
||
</th>
|
||
<th class="border-r border-slate-200 p-4 text-center">
|
||
概率/分数
|
||
</th>
|
||
<th class="border-r border-slate-200 p-4 text-center">
|
||
25省内招生
|
||
</th>
|
||
<th class="w-32 p-4 text-center">
|
||
操作
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-slate-200">
|
||
<tr
|
||
v-for="(vol, index) in filteredVolunteers" :key="vol.id"
|
||
class="group bg-white transition-colors hover:bg-slate-50" :draggable="dragEnabledIndex === index"
|
||
@dragstart="handleDragStart($event, index)" @dragover="handleDragOver($event)"
|
||
@drop="handleDrop(index)"
|
||
>
|
||
<!-- 拖拽手柄列 -->
|
||
<td
|
||
class="cursor-move cursor-move border-r border-slate-200 p-4 text-center text-slate-400 active:text-blue-700 hover:text-blue-500" @mouseenter="dragEnabledIndex = index"
|
||
@mouseleave="dragEnabledIndex = null"
|
||
>
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg" class="pointer-events-none mx-auto h-6 w-6" fill="none"
|
||
viewBox="0 0 24 24" stroke="currentColor"
|
||
>
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16" />
|
||
</svg>
|
||
</td>
|
||
|
||
<!-- 志愿序号 -->
|
||
<td class="border-r border-slate-200 p-4 text-center text-lg text-slate-500 font-bold">
|
||
{{ index + 1 }}
|
||
</td>
|
||
|
||
<!-- 院校信息 -->
|
||
<td class="border-r border-slate-200 p-4 text-left">
|
||
<div class="text-base text-slate-900 font-bold">
|
||
{{ vol.schoolName }}
|
||
</div>
|
||
<div class="mt-1 flex gap-2">
|
||
<span
|
||
v-for="tag in (vol.tags || [vol.province, vol.schoolNature].filter(Boolean))" :key="tag"
|
||
class="rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-500"
|
||
>{{ tag }}</span>
|
||
</div>
|
||
<div class="mt-1 text-xs text-slate-400">
|
||
代码: {{ vol.schoolCode }}
|
||
</div>
|
||
</td>
|
||
|
||
<!-- 专业信息 -->
|
||
<td class="border-r border-slate-200 p-4">
|
||
<div class="text-slate-800 font-bold">
|
||
{{ vol.majorName }}
|
||
</div>
|
||
<div class="mt-1 text-xs text-slate-500">
|
||
{{ vol.tuition || '-' }}
|
||
</div>
|
||
<div class="text-xs text-slate-400">
|
||
代码: {{ vol.majorCode }}
|
||
</div>
|
||
</td>
|
||
|
||
<!-- 分数/概率 -->
|
||
<td class="border-r border-slate-200 p-4 text-center">
|
||
<div class="text-lg text-blue-600 font-bold">
|
||
{{ vol.enrollProbability }}%
|
||
</div>
|
||
<div class="mt-1 text-xs text-slate-500">
|
||
排名 {{ vol.indexs }}
|
||
</div>
|
||
<div
|
||
class="mt-1 inline-block border rounded px-2 py-0.5 text-xs"
|
||
:class="getStatusColor(getProbabilityLabel(vol.enrollProbability))"
|
||
>
|
||
{{ getProbabilityLabel(vol.enrollProbability) }}
|
||
</div>
|
||
</td>
|
||
|
||
<!-- 25省内招生 -->
|
||
<td class="border-r border-slate-200 p-4 text-center">
|
||
<div class="text-lg font-bold">
|
||
{{ vol.planNum }}人
|
||
</div>
|
||
</td>
|
||
|
||
<!-- 操作:移除 -->
|
||
<td class="p-4 text-center">
|
||
<button
|
||
class="rounded-full p-2 text-red-500 transition-colors hover:bg-red-50 hover:text-red-700"
|
||
title="移除此志愿"
|
||
@click="removeVolunteer(index)"
|
||
>
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
>
|
||
<path
|
||
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- 空状态 -->
|
||
<tr v-if="myVolunteers.length === 0">
|
||
<td colspan="7" class="items-center justify-center py-20 text-center text-slate-400">
|
||
<div class="flex flex-col items-center justify-center">
|
||
<div class="mb-3 rounded-full bg-slate-100 p-4">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-slate-300" fill="none"
|
||
viewBox="0 0 24 24" stroke="currentColor"
|
||
>
|
||
<path
|
||
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<p>暂无志愿,请前往“模拟填报”添加</p>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<!-- Panel B End -->
|
||
</div>
|
||
<!-- =========================================================
|
||
弹窗层 (Modal) - 重点更新区域
|
||
========================================================== -->
|
||
<Teleport to="body">
|
||
<div
|
||
v-if="showModal" class="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
||
@click.self="closeModal"
|
||
>
|
||
<!-- 宽度加宽至 1100px 以容纳更多列 -->
|
||
<div
|
||
class="h-[85vh] w-[1100px] flex flex-col animate-fade-in-up overflow-hidden rounded-lg bg-white shadow-2xl"
|
||
>
|
||
<!-- Header -->
|
||
<div class="flex items-center justify-between border-b border-slate-200 bg-slate-50 px-6 py-4">
|
||
<div>
|
||
<div class="flex items-end gap-3">
|
||
<h3 class="text-xl text-slate-800 font-bold">
|
||
{{ currentSchool?.schoolName }}
|
||
</h3>
|
||
<span class="text-sm text-slate-500">院校代码: {{ currentSchool?.schoolCode }}</span>
|
||
</div>
|
||
<p class="mt-1 text-xs text-slate-500">
|
||
该院校下符合您选科要求的其他专业列表
|
||
</p>
|
||
</div>
|
||
<button class="rounded-full p-1 text-slate-400 hover:bg-slate-200 hover:text-slate-600" @click="closeModal">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
>
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Content: Table -->
|
||
<div class="flex-1 overflow-auto bg-slate-50 p-0">
|
||
<table class="w-full border-collapse text-left text-sm">
|
||
<thead
|
||
class="sticky top-0 z-10 bg-slate-100 text-xs text-slate-500 font-semibold tracking-wider uppercase shadow-sm"
|
||
>
|
||
<tr>
|
||
<th class="w-16 px-6 py-3">
|
||
代码
|
||
</th>
|
||
<th class="px-6 py-3">
|
||
专业名称 / 选科 / 学费
|
||
</th>
|
||
<th class="px-6 py-3 text-center">
|
||
录取概率
|
||
</th>
|
||
<th class="px-6 py-3 text-center">
|
||
上次最低分
|
||
</th>
|
||
<th class="px-6 py-3 text-center">
|
||
录取方式
|
||
</th>
|
||
<th class="px-6 py-3 text-center">
|
||
招生计划
|
||
</th>
|
||
<th
|
||
class="sticky right-0 bg-slate-100 px-6 py-3 text-center shadow-[-4px_0_8px_-4px_rgba(0,0,0,0.1)]"
|
||
>
|
||
加入志愿
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="bg-white divide-y divide-slate-100">
|
||
<tr v-if="modalLoading">
|
||
<td colspan="7" class="p-12 text-center text-slate-400">
|
||
<svg class="mx-auto mb-2 h-8 w-8 animate-spin text-blue-400" viewBox="0 0 24 24">
|
||
<path fill="currentColor" d="M12 2A10 10 0 1 0 22 12A10 10 0 0 0 12 2Z" opacity="0.2" />
|
||
<path fill="currentColor" d="M12 2A10 10 0 0 1 22 12h-2a8 8 0 0 0-8-8V2Z" />
|
||
</svg>
|
||
正在获取专业数据...
|
||
</td>
|
||
</tr>
|
||
<tr
|
||
v-for="major in modalMajors" v-else :key="major.code"
|
||
class="group transition-colors hover:bg-blue-50/30"
|
||
>
|
||
<td class="px-6 py-4 text-xs text-slate-400 font-mono">
|
||
{{ major.code }}
|
||
</td>
|
||
<td class="px-6 py-4">
|
||
<div class="text-base text-slate-800 font-bold">
|
||
{{ major.name }}
|
||
</div>
|
||
<div class="mt-1 flex gap-2 text-xs text-slate-500">
|
||
<span class="rounded bg-slate-100 px-1.5 py-0.5">{{
|
||
major.batch
|
||
}}</span>
|
||
<span class="rounded bg-slate-100 px-1.5 py-0.5">{{
|
||
major.tuition
|
||
}}</span>
|
||
</div>
|
||
</td>
|
||
<td class="px-6 py-4 text-center">
|
||
<span
|
||
class="font-bold" :class="major.prob >= 80 ? 'text-green-600' : 'text-orange-500'
|
||
"
|
||
>{{ major.prob }}%</span>
|
||
</td>
|
||
<td class="px-6 py-4 text-center font-medium">
|
||
{{ major.score }}
|
||
</td>
|
||
<td class="px-6 py-4 text-center text-slate-500">
|
||
{{ major.rulesEnrollProbability }}
|
||
</td>
|
||
<td class="px-6 py-4 text-center text-slate-500">
|
||
{{ major.plan }}人
|
||
</td>
|
||
|
||
<!-- 操作列:加入/移除 -->
|
||
<td
|
||
class="sticky right-0 bg-white px-6 py-4 text-center shadow-[-4px_0_8px_-4px_rgba(0,0,0,0.1)] group-hover:bg-blue-50/30"
|
||
>
|
||
<button
|
||
class="min-w-[90px] rounded-full px-3 py-1.5 text-xs font-medium transition-all duration-200"
|
||
:class="[
|
||
selectedMajorCodes.includes(`${currentSchool?.schoolCode}_${major.code}_${major.enrollmentCode || currentSchool?.enrollmentCode}`)
|
||
? 'bg-red-50 text-red-500 border border-red-200 hover:bg-red-100' // 选中状态:红色,显示志愿序号
|
||
: 'bg-white text-blue-600 border border-blue-500 hover:bg-blue-50', // 未选状态:蓝色
|
||
]" @click="toggleMajor(major)"
|
||
>
|
||
{{ getVolunteerBtnText(major) }}
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Footer: Confirm with Popover -->
|
||
<div class="relative flex items-center justify-between border-t border-slate-200 bg-white p-4">
|
||
<div class="text-sm text-slate-500">
|
||
已选
|
||
<span class="text-lg text-blue-600 font-bold">{{
|
||
selectedMajorCodes.length
|
||
}}</span>
|
||
个志愿
|
||
</div>
|
||
|
||
<div class="relative flex gap-3">
|
||
<button class="px-5 py-2 text-sm text-slate-600 hover:text-slate-900" @click="closeModal">
|
||
取消
|
||
</button>
|
||
|
||
<!--
|
||
气泡确认框 (Popconfirm) 实现逻辑
|
||
如果不想封装组件,这种 inline 方式最简单
|
||
-->
|
||
<div class="relative">
|
||
<!-- 主按钮 -->
|
||
<w-popconfirm
|
||
title="底部右对齐" placement="top-right" :ok-button-props="{ loading: isSaving }"
|
||
@confirm="saveVolunteers"
|
||
>
|
||
<button
|
||
:disabled="isSaving"
|
||
class="rounded bg-blue-600 px-6 py-2 text-sm text-white font-medium shadow-md transition-colors disabled:cursor-not-allowed hover:bg-blue-700 disabled:opacity-50"
|
||
>
|
||
确认保存
|
||
</button>
|
||
<!-- Slots -->
|
||
<template #icon>
|
||
<span class="text-xl">🎉</span>
|
||
</template>
|
||
<template #title>
|
||
<span class="text-purple-600">确认要保存吗?</span>
|
||
</template>
|
||
<template #description>
|
||
这里可以放很长很长的<br>HTML内容哦。
|
||
</template>
|
||
</w-popconfirm>
|
||
|
||
<!-- 气泡层 -->
|
||
<div
|
||
v-if="showSaveConfirm"
|
||
class="absolute bottom-full right-0 z-50 mb-3 w-64 animate-fade-in-up border border-slate-200 rounded-lg bg-white p-4 shadow-xl"
|
||
>
|
||
<!-- 小三角 -->
|
||
<div
|
||
class="absolute right-6 h-3 w-3 rotate-45 border-b border-r border-slate-200 bg-white -bottom-1.5"
|
||
/>
|
||
|
||
<div class="mb-3 flex items-start gap-3">
|
||
<div class="mt-0.5 text-orange-500">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||
<path
|
||
fill-rule="evenodd"
|
||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||
clip-rule="evenodd"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<p class="text-sm text-slate-800 font-bold">
|
||
确认提交志愿?
|
||
</p>
|
||
<p class="mt-1 text-xs text-slate-500">
|
||
提交后将更新您的志愿表,已选
|
||
{{ selectedMajorCodes.length }} 个专业。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex justify-end gap-2">
|
||
<button
|
||
class="rounded px-2 py-1 text-xs text-slate-500 hover:bg-slate-100"
|
||
@click="showSaveConfirm = false"
|
||
>
|
||
再想想
|
||
</button>
|
||
<button
|
||
class="flex items-center gap-1 rounded bg-blue-600 px-3 py-1 text-xs text-white hover:bg-blue-700"
|
||
:disabled="isSaving" @click="saveVolunteers"
|
||
>
|
||
<span
|
||
v-if="isSaving"
|
||
class="h-3 w-3 animate-spin border-2 border-white/30 border-t-white rounded-full"
|
||
/>
|
||
{{ isSaving ? "保存中" : "确定" }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- End of Popconfirm -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- =========================================================
|
||
方案切换模态框 (新增)
|
||
========================================================== -->
|
||
<div
|
||
v-if="showSwitchModal"
|
||
class="fixed inset-0 z-[110] flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
||
@click.self="showSwitchModal = false"
|
||
>
|
||
<div
|
||
class="max-h-[80vh] w-[1000px] flex flex-col animate-fade-in-up overflow-hidden rounded-lg bg-white shadow-2xl"
|
||
>
|
||
<!-- Header -->
|
||
<div class="flex items-center justify-between border-b border-slate-200 px-6 py-4">
|
||
<h3 class="text-lg text-slate-800 font-bold">
|
||
我的志愿方案
|
||
</h3>
|
||
|
||
<!-- 简单的关闭按钮 -->
|
||
<button class="text-slate-400 hover:text-slate-600" @click="showSwitchModal = false">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
>
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Toolbar / Filter -->
|
||
<div class="flex justify-end border-b border-slate-100 bg-slate-50 px-6 py-3">
|
||
<div class="relative">
|
||
<select
|
||
class="appearance-none border border-slate-300 rounded bg-white py-1.5 pl-3 pr-8 text-sm text-slate-700 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||
>
|
||
<option>请选择省份</option>
|
||
<option>北京</option>
|
||
<option>湖北</option>
|
||
</select>
|
||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-slate-500">
|
||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Table -->
|
||
<div class="flex-1 overflow-auto p-6">
|
||
<table class="w-full border border-slate-200 text-left text-sm">
|
||
<thead class="bg-slate-50 text-slate-500">
|
||
<tr>
|
||
<th class="border-b border-r border-slate-200 px-4 py-3 font-medium">
|
||
名称
|
||
</th>
|
||
<th class="border-b border-r border-slate-200 px-4 py-3 text-center font-medium">
|
||
省份
|
||
</th>
|
||
<th class="border-b border-r border-slate-200 px-4 py-3 text-center font-medium">
|
||
艺术类别
|
||
</th>
|
||
<th class="border-b border-r border-slate-200 px-4 py-3 text-center font-medium">
|
||
文化分
|
||
</th>
|
||
<th class="border-b border-r border-slate-200 px-4 py-3 text-center font-medium">
|
||
统考分
|
||
</th>
|
||
<th class="border-b border-r border-slate-200 px-4 py-3 text-center font-medium">
|
||
最后更新时间
|
||
</th>
|
||
<th class="border-b border-r border-slate-200 px-4 py-3 text-center font-medium">
|
||
状态
|
||
</th>
|
||
<th class="border-b border-slate-200 px-4 py-3 text-center font-medium">
|
||
操作
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-slate-100">
|
||
<tr v-for="plan in volunteerPlans" :key="plan.id" class="transition-colors hover:bg-slate-50">
|
||
<td class="border-r border-slate-100 px-4 py-3">
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-slate-700 font-medium">{{ plan.name }}</span>
|
||
<span
|
||
class="rounded px-1.5 py-0.5 text-[10px]"
|
||
:class="plan.tag === '手动' ? 'bg-orange-50 text-orange-500' : 'bg-blue-50 text-blue-500'"
|
||
>
|
||
{{ plan.tag }}
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td class="border-r border-slate-100 px-4 py-3 text-center text-slate-600">
|
||
{{ plan.province }}
|
||
</td>
|
||
<td class="border-r border-slate-100 px-4 py-3 text-center text-slate-600">
|
||
{{ plan.artType }}
|
||
</td>
|
||
<td class="border-r border-slate-100 px-4 py-3 text-center">
|
||
<span class="text-slate-800 font-medium">{{ plan.cultureScore }}</span>
|
||
<span class="ml-1 text-xs text-slate-400">{{ plan.cultureSubjects }}</span>
|
||
</td>
|
||
<td class="border-r border-slate-100 px-4 py-3 text-center text-slate-800 font-medium">
|
||
{{
|
||
plan.artScore }}
|
||
</td>
|
||
<td class="border-r border-slate-100 px-4 py-3 text-center text-xs text-slate-500 font-mono">
|
||
{{
|
||
plan.updateTime }}
|
||
</td>
|
||
<td class="border-r border-slate-100 px-4 py-3 text-center text-slate-600">
|
||
{{ plan.status }}
|
||
</td>
|
||
<td class="px-4 py-3 text-center">
|
||
<div class="flex items-center justify-center gap-2">
|
||
<!-- 当前选中状态 -->
|
||
<span v-if="activePlanId === plan.id" class="text-xs text-orange-500 font-bold">当前</span>
|
||
<!-- 切换按钮 -->
|
||
<button
|
||
v-else class="text-xs text-blue-600 hover:text-blue-800 hover:underline"
|
||
@click="switchActivePlan(plan.id)"
|
||
>
|
||
切换
|
||
</button>
|
||
<span class="text-slate-300">|</span>
|
||
<button class="text-xs text-slate-600 hover:text-slate-900 hover:underline">
|
||
导出
|
||
</button>
|
||
<span class="text-slate-300">|</span>
|
||
<button
|
||
class="text-xs text-slate-600 hover:text-red-600 hover:underline"
|
||
@click="deletePlan(plan.id)"
|
||
>
|
||
删除
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* 定义淡入动画 */
|
||
@keyframes fadeInUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.animate-fade-in-up {
|
||
animation: fadeInUp 0.3s ease-out;
|
||
}
|
||
|
||
/* 滚动条美化 */
|
||
::-webkit-scrollbar {
|
||
width: 8px;
|
||
height: 8px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: #cbd5e1; /* slate-300 */
|
||
border-radius: 4px;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: #94a3b8; /* slate-400 */
|
||
}
|
||
|
||
/* 兼容 Firefox */
|
||
* {
|
||
scrollbar-width: thin;
|
||
scrollbar-color: #cbd5e1 transparent;
|
||
}
|
||
</style>
|