diff --git a/src/views/study-tours/StudyTourList.vue b/src/views/study-tours/StudyTourList.vue index 12d584d..1956f6c 100644 --- a/src/views/study-tours/StudyTourList.vue +++ b/src/views/study-tours/StudyTourList.vue @@ -8,7 +8,8 @@ import { listTableRowIndex } from '../../utils/listTableRowIndex' import { resolvePublicMediaUrl } from '../../utils/mediaUrl' import { adminUploadImageTooLargeMessage, ADMIN_IMAGE_RECOMMEND_LABEL } from '../../utils/adminMediaLimits' -const STUDY_TOUR_LIST_SCROLL_X = 1180 +const STUDY_TOUR_LIST_SCROLL_X = 1280 +const STUDY_TOUR_EXPORT_MAX = 50 type DictOption = { id: number; item_value: string; item_label: string } @@ -78,6 +79,8 @@ const formBaseline = ref('') const importDocInput = ref(null) const importingDoc = ref(false) const importedFromDoc = ref(false) +const selectedKeys = ref([]) +const exporting = ref(false) const form = reactive({ name: '', @@ -576,6 +579,8 @@ async function loadData(keepCurrentPage = false) { ]) rows.value = tourRes.data venues.value = venueRes.data + const idSet = new Set(rows.value.map((r) => r.id)) + selectedKeys.value = selectedKeys.value.filter((id) => idSet.has(id)) if (!keepCurrentPage) listPagination.current = 1 } catch (error: any) { Message.error(error?.response?.data?.message ?? '加载研学线路失败') @@ -588,6 +593,99 @@ function onListPageChange(page: number) { listPagination.current = page } +function onSelectionChange(keys: (string | number)[]) { + selectedKeys.value = keys + .map((k) => Number(k)) + .filter((id) => Number.isFinite(id) && id > 0) +} + +function filenameFromContentDisposition(header: string | undefined, fallback: string) { + if (!header) return fallback + const star = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(header) + if (star?.[1]) { + try { + const raw = decodeURIComponent(star[1].trim().replace(/^"+|"+$/g, '')) + if (raw) return raw + } catch { + // ignore + } + } + const plain = /filename="?([^";]+)/i.exec(header) + const raw = (plain?.[1] || '').trim().replace(/^"+|"+$/g, '') + return raw || fallback +} + +async function messageFromBlobError(error: any): Promise { + const data = error?.response?.data + if (data instanceof Blob) { + try { + const text = await data.text() + const j = JSON.parse(text) as { message?: string } + if (j?.message) return j.message + } catch { + // ignore + } + } + return error?.response?.data?.message ?? error?.message ?? '导出失败' +} + +async function exportStudyTours(ids: number[]) { + const uniqueIds = [...new Set(ids.filter((id) => Number.isFinite(id) && id > 0))] + if (!uniqueIds.length) { + Message.warning('请先勾选要导出的线路') + return + } + if (uniqueIds.length > STUDY_TOUR_EXPORT_MAX) { + Message.warning(`单次最多导出 ${STUDY_TOUR_EXPORT_MAX} 条线路`) + return + } + if (exporting.value) return + exporting.value = true + try { + const res = await http.post( + '/study-tours/export', + { ids: uniqueIds }, + { responseType: 'blob', timeout: 120000 }, + ) + const blob = res.data as Blob + if (blob.type && (blob.type.includes('json') || blob.type.includes('text/html'))) { + const text = await blob.text() + let msg = '导出失败' + try { + const j = JSON.parse(text) as { message?: string } + if (j?.message) msg = j.message + } catch { + // ignore + } + Message.error(msg) + return + } + const fallback = uniqueIds.length === 1 + ? '研学线路申报表.docx' + : `研学线路申报表-${new Date().toISOString().slice(0, 10)}.zip` + const filename = filenameFromContentDisposition(res.headers['content-disposition'] as string | undefined, fallback) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + a.click() + URL.revokeObjectURL(url) + Message.success(uniqueIds.length === 1 ? '已导出 Word' : `已导出 ${uniqueIds.length} 条线路`) + } catch (error: any) { + Message.error(await messageFromBlobError(error)) + } finally { + exporting.value = false + } +} + +function exportSelected() { + void exportStudyTours(selectedKeys.value) +} + +function exportOne(row: StudyTour) { + void exportStudyTours([row.id]) +} + async function toggleShelf(row: StudyTour) { try { await http.put(`/study-tours/${row.id}`, { is_on_shelf: !row.is_on_shelf }) @@ -893,6 +991,11 @@ onMounted(async () => { @change="onImportDocSelected" /> 从申报表导入 + 导出选中{{ selectedKeys.length ? `(${selectedKeys.length})` : '' }} 新建线路 @@ -902,7 +1005,9 @@ onMounted(async () => { :data="rows" :loading="loading" row-key="id" + :row-selection="{ type: 'checkbox', selectedRowKeys: selectedKeys, showCheckedAll: true, onlyCurrent: false }" :pagination="{ current: listPagination.current, pageSize: listPagination.pageSize, total: rows.length, showTotal: true }" + @selection-change="onSelectionChange" @page-change="onListPageChange" > - +