This commit is contained in:
biss committed 2026-09-07 19:34:03 +08:00
1 parent cec56564b5
commit ae075bda6d
32 files changed
+10052 -90

No files matched your search

+118
View File
@@ -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>