升级
This commit is contained in:
1 parent
cec56564b5
commit
ae075bda6d
32 files changed
+10052
-90
No files matched your search
Vendored
+1
@@ -13,6 +13,7 @@ declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AcademicCalendarReminder: typeof import('./components/AcademicCalendarReminder.vue')['default']
|
||||
AppUpdateManagementPanel: typeof import('./components/AppUpdateManagementPanel.vue')['default']
|
||||
ArchiveDialog: typeof import('./components/ArchiveDialog.vue')['default']
|
||||
ChinesePostalCodeInput: typeof import('./components/ChinesePostalCodeInput.vue')['default']
|
||||
CourseDiscussionPanel: typeof import('./components/CourseDiscussionPanel.vue')['default']
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
|
||||
const emit = defineEmits<{ changed: [] }>()
|
||||
const visible = ref(false)
|
||||
const busy = ref(false)
|
||||
const kind = ref('')
|
||||
const archived = ref(true)
|
||||
const reason = ref('')
|
||||
const previews = ref<any[]>([])
|
||||
const results = ref<any[]>([])
|
||||
const history = ref<any[]>([])
|
||||
const historyId = ref('')
|
||||
const historyPage = ref(1)
|
||||
const historyMode = ref(false)
|
||||
const eligible = computed(() => previews.value.filter(x => !x.error && !x.blockers?.length && x.isArchived !== archived.value))
|
||||
const title = computed(() => historyMode.value ? '归档操作历史' : archived.value ? '归档预检查' : '恢复预检查')
|
||||
async function open(entityKind: string, rows: { id: string }[], target: boolean) {
|
||||
if (!rows.length) { ElMessage.info('请先选择记录'); return }
|
||||
if (rows.length > 100) { ElMessage.warning('每次最多操作 100 条记录'); return }
|
||||
kind.value = entityKind
|
||||
archived.value = target
|
||||
historyMode.value = false
|
||||
reason.value = ''
|
||||
previews.value = []
|
||||
results.value = []
|
||||
visible.value = true
|
||||
busy.value = true
|
||||
try {
|
||||
previews.value = (await http.post(`/archives/${entityKind}/preview`, { ids: rows.map(x => x.id), archived: target })).data
|
||||
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||||
finally { busy.value = false }
|
||||
}
|
||||
async function apply() {
|
||||
if (!reason.value.trim() || !eligible.value.length || busy.value) return
|
||||
busy.value = true
|
||||
try {
|
||||
results.value = (await http.post(`/archives/${kind.value}/apply`, {
|
||||
ids: eligible.value.map(x => x.id), archived: archived.value, reason: reason.value.trim(),
|
||||
})).data
|
||||
emit('changed')
|
||||
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||||
finally { busy.value = false }
|
||||
}
|
||||
async function loadHistory() {
|
||||
busy.value = true
|
||||
history.value = []
|
||||
try { history.value = (await http.get(`/archives/${kind.value}/${historyId.value}/history`, { params: { page: historyPage.value } })).data }
|
||||
catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||||
finally { busy.value = false }
|
||||
}
|
||||
async function showHistory(entityKind: string, id: string) {
|
||||
kind.value = entityKind; historyId.value = id; historyPage.value = 1
|
||||
historyMode.value = true; visible.value = true
|
||||
await loadHistory()
|
||||
}
|
||||
defineExpose({ open, showHistory })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="title" width="min(720px, 94vw)" :close-on-click-modal="!busy" :close-on-press-escape="!busy" :show-close="!busy">
|
||||
<div v-loading="busy" class="archive-content">
|
||||
<template v-if="historyMode">
|
||||
<article v-for="entry in history" :key="entry.id">
|
||||
<strong>{{ entry.isArchived ? '归档' : '恢复' }} · {{ entry.actorName || entry.actorUserId }}</strong>
|
||||
<p>{{ new Date(entry.createdAt).toLocaleString() }}</p><p>{{ entry.reason }}</p>
|
||||
</article>
|
||||
<el-empty v-if="!busy && !history.length" description="暂无操作历史(旧归档记录可能没有操作日志)" />
|
||||
<div class="archive-pagination">
|
||||
<el-button :disabled="busy || historyPage === 1" @click="historyPage--; loadHistory()">上一页</el-button>
|
||||
<span>第 {{ historyPage }} 页</span>
|
||||
<el-button :disabled="busy || history.length < 20" @click="historyPage++; loadHistory()">下一页</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>符合条件 {{ eligible.length }} / {{ previews.length }} 条。提交时会再次检查,受阻记录不执行。</p>
|
||||
<article v-for="item in previews" :key="item.id">
|
||||
<strong>{{ item.name || '不可操作记录' }}</strong>
|
||||
<p v-if="item.error" class="archive-error">{{ item.error }}</p>
|
||||
<template v-else>
|
||||
<p>{{ item.isArchived ? '已归档' : '未归档' }}<span v-if="item.archivedAt"> · {{ new Date(item.archivedAt).toLocaleString() }}</span></p>
|
||||
<p v-if="item.archiveReason">归档原因:{{ item.archiveReason }}</p>
|
||||
<p v-if="item.isArchived === archived">已处于目标状态,无需重复操作。</p>
|
||||
<p v-for="(blocker, index) in item.blockers" :key="`b${index}`" class="archive-error">
|
||||
{{ blocker.message }} <router-link v-if="blocker.link" :to="blocker.link" @click="visible = false">前往处理</router-link>
|
||||
</p>
|
||||
<p v-for="(notice, index) in item.notices" :key="`n${index}`">{{ notice.message }}</p>
|
||||
</template>
|
||||
</article>
|
||||
<el-form v-if="!results.length" label-position="top">
|
||||
<el-form-item :label="archived ? '归档原因(必填)' : '恢复原因(必填)'">
|
||||
<el-input v-model="reason" type="textarea" :rows="3" maxlength="500" show-word-limit :disabled="busy" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<article v-for="result in results" :key="result.id">
|
||||
<strong>{{ previews.find(x => x.id === result.id)?.name }}</strong>
|
||||
<p :class="{ 'archive-error': !result.success }">{{ result.message }}</p>
|
||||
</article>
|
||||
</template>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button :disabled="busy" @click="visible = false">关闭</el-button>
|
||||
<el-button v-if="!historyMode && !results.length" type="primary" :loading="busy" :disabled="!reason.trim() || !eligible.length" @click="apply">
|
||||
{{ archived ? '确认归档' : '确认恢复' }}({{ eligible.length }})
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.archive-content { max-height: 65vh; overflow: auto; min-height: 140px; }
|
||||
article { border-bottom: 1px solid var(--el-border-color-light); padding: 12px 0; overflow-wrap: anywhere; }
|
||||
p { margin: 6px 0; line-height: 1.6; }
|
||||
.archive-error { color: var(--el-color-danger); }
|
||||
.archive-pagination { display: flex; align-items: center; gap: 12px; margin-top: 16px; }
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import ArchiveDialog from '../components/ArchiveDialog.vue'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { Document, Download, Plus, Refresh, Search, Upload } from '@element-plus/icons-vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
@@ -73,6 +74,9 @@ const fileInput = ref<HTMLInputElement>()
|
||||
const dialogVisible = ref(false)
|
||||
const editingId = ref('')
|
||||
const keyword = ref('')
|
||||
const archiveDialog = ref<InstanceType<typeof ArchiveDialog>>()
|
||||
const archiveSelection = ref<string[]>([])
|
||||
const archiveFilter = ref<boolean | undefined>()
|
||||
const references = reactive<Record<string, any[]>>({
|
||||
campuses: [], colleges: [], majors: [], buildings: [], counselors: [],
|
||||
})
|
||||
@@ -94,8 +98,10 @@ const canManage = computed(() =>
|
||||
)
|
||||
const filteredRows = computed(() => {
|
||||
const q = keyword.value.trim().toLowerCase()
|
||||
if (!q) return rows.value
|
||||
return rows.value.filter((row) =>
|
||||
const source = active.value === 'terms' && archiveFilter.value !== undefined
|
||||
? rows.value.filter(row => row.isArchived === archiveFilter.value) : rows.value
|
||||
if (!q) return source
|
||||
return source.filter((row) =>
|
||||
[row.code, row.name, row.collegeName, row.campusName, row.majorName, row.buildingName]
|
||||
.filter(Boolean).some((value) => String(value).toLowerCase().includes(q)),
|
||||
)
|
||||
@@ -105,6 +111,7 @@ const filteredMajors = computed(() => references.majors.filter((item: any) =>
|
||||
const filteredBuildings = computed(() => references.buildings.filter((item: any) =>
|
||||
!listFilters.campusId || item.campusId === listFilters.campusId))
|
||||
const currentPage = ref(1)
|
||||
watch(archiveFilter, () => { currentPage.value = 1; archiveSelection.value = [] })
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const serverPaged = computed(() =>
|
||||
@@ -159,6 +166,7 @@ function resetForm(row?: Row) {
|
||||
}
|
||||
|
||||
async function load(resetPage = true) {
|
||||
archiveSelection.value = []
|
||||
loading.value = true
|
||||
if (resetPage) currentPage.value = 1
|
||||
try {
|
||||
@@ -237,6 +245,7 @@ async function exportRows() {
|
||||
await downloadApiFile(
|
||||
`/base-data/${active.value}/export`,
|
||||
`${title.value}.xlsx`,
|
||||
{ params: active.value === 'terms' ? { isArchived: archiveFilter.value, keyword: keyword.value.trim() || undefined } : undefined },
|
||||
)
|
||||
ElMessage.success(`${title.value}已导出`)
|
||||
} catch (error) {
|
||||
@@ -336,26 +345,8 @@ async function setCurrentTerm(row: any) {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleTermArchive(row: any) {
|
||||
const restoring = row.isArchived
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
restoring
|
||||
? `撤销“${row.name}”的归档状态?撤销后仍作为历史学期显示,可再次设为当前。`
|
||||
: `归档“${row.name}”?归档只降低历史数据的显示优先级,不会冻结成绩更正或补考成绩录入。`,
|
||||
restoring ? '撤销学期归档' : '归档学期',
|
||||
{
|
||||
type: restoring ? 'info' : 'warning',
|
||||
confirmButtonText: restoring ? '撤销归档' : '确认归档',
|
||||
cancelButtonText: '取消',
|
||||
},
|
||||
)
|
||||
await http.post(`/base-data/terms/${row.id}/${restoring ? 'unarchive' : 'archive'}`)
|
||||
ElMessage.success(restoring ? '已撤销归档' : '学期已归档')
|
||||
await load()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
function toggleTermArchive(row: any) {
|
||||
archiveDialog.value?.open('terms', [row], !row.isArchived)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -418,6 +409,7 @@ watch(
|
||||
</nav>
|
||||
|
||||
<div class="table-toolbar base-data-toolbar">
|
||||
<el-select v-if="active === 'terms'" v-model="archiveFilter" clearable placeholder="全部归档状态"><el-option label="未归档" :value="false" /><el-option label="已归档" :value="true" /></el-select>
|
||||
<el-input v-model="keyword" :prefix-icon="Search" clearable placeholder="搜索编码、名称或所属单位" />
|
||||
<template v-if="active === 'classes'">
|
||||
<el-select v-model="listFilters.collegeId" clearable placeholder="全部学院" @change="() => { listFilters.majorId = undefined; load() }">
|
||||
@@ -470,6 +462,13 @@ watch(
|
||||
title="切换当前学期只改变默认工作学期;归档需手动执行,归档后仍可处理补考与成绩更正。"
|
||||
/>
|
||||
|
||||
<div v-if="active === 'terms' && canManage" style="display:flex; gap:8px; flex-wrap:wrap; margin:12px 0">
|
||||
<el-select v-model="archiveSelection" multiple collapse-tags placeholder="选择学期" style="width:260px">
|
||||
<el-option v-for="term in pagedRows" :key="term.id" :value="term.id" :label="term.name" />
|
||||
</el-select>
|
||||
<el-button :disabled="!archiveSelection.length" @click="archiveDialog?.open('terms', archiveSelection.map(id => ({ id })), true)">批量归档</el-button>
|
||||
<el-button :disabled="!archiveSelection.length" @click="archiveDialog?.open('terms', archiveSelection.map(id => ({ id })), false)">批量恢复</el-button>
|
||||
</div>
|
||||
<div v-if="active === 'terms'" class="term-mobile-list">
|
||||
<article
|
||||
v-for="row in pagedRows"
|
||||
@@ -501,13 +500,14 @@ watch(
|
||||
plain
|
||||
@click="toggleTermArchive(row)"
|
||||
>{{ row.isArchived ? '撤销归档' : '归档' }}</el-button>
|
||||
<el-button size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" @click="archiveDialog?.showHistory('terms', row.id)">归档历史</el-button>
|
||||
<el-button size="small" :disabled="row.isArchived" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button
|
||||
v-if="!row.isCurrent"
|
||||
size="small"
|
||||
type="danger"
|
||||
plain
|
||||
@click="remove(row)"
|
||||
:disabled="active === 'terms' && row.isArchived" @click="remove(row)"
|
||||
>删除</el-button>
|
||||
</footer>
|
||||
</article>
|
||||
@@ -520,6 +520,7 @@ watch(
|
||||
class="data-table"
|
||||
:class="{ 'term-desktop-table': active === 'terms' }"
|
||||
>
|
||||
<el-table-column v-if="active === 'terms' && canManage" label="归档历史" width="110"><template #default="{ row }"><el-button link @click="archiveDialog?.showHistory('terms', row.id)">查看历史</el-button></template></el-table-column>
|
||||
<el-table-column prop="code" label="编码" min-width="130" />
|
||||
<el-table-column prop="name" label="名称" min-width="180" />
|
||||
<el-table-column v-if="active === 'colleges'" prop="campusName" label="所属校区" min-width="140" />
|
||||
@@ -571,12 +572,12 @@ watch(
|
||||
@click="toggleTermArchive(row)"
|
||||
>{{ row.isArchived ? '撤销归档' : '归档' }}</el-button>
|
||||
</template>
|
||||
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="primary" :disabled="active === 'terms' && row.isArchived" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button
|
||||
v-if="active !== 'terms' || !row.isCurrent"
|
||||
link
|
||||
type="danger"
|
||||
@click="remove(row)"
|
||||
:disabled="active === 'terms' && row.isArchived" @click="remove(row)"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -686,4 +687,5 @@ watch(
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
<ArchiveDialog ref="archiveDialog" @changed="archiveSelection = []; load()" />
|
||||
</template>
|
||||
Loaded 3 of 32 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user