119 lines
5.8 KiB
Vue
119 lines
5.8 KiB
Vue
<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>
|