Files
Academic-Affairs-System/web/src/components/ArchiveDialog.vue
T
2026-09-07 19:34:03 +08:00

119 lines
5.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>