|
@@ -0,0 +1,254 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div class="page-actions" style="margin-bottom: 12px;">
|
|
|
|
|
+ <el-button type="primary" @click="triggerFileInput">导入</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Hidden native file input fallback to ensure upload works reliably -->
|
|
|
|
|
+ <input ref="fileInputRef" type="file" accept=".xlsx,.xls" style="display:none" @change="handleLocalFileChange" />
|
|
|
|
|
+
|
|
|
|
|
+ <import-dialog
|
|
|
|
|
+ v-if="false"
|
|
|
|
|
+ :visible="importDialogVisible"
|
|
|
|
|
+ :uploadUrl="importUrl"
|
|
|
|
|
+ :templateUrl="templateUrl"
|
|
|
|
|
+ @update:visible="importDialogVisible = $event"
|
|
|
|
|
+ @success="onImportSuccess"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <el-card>
|
|
|
|
|
+ <div style="margin-bottom: 8px;">
|
|
|
|
|
+ <span>已选择批次:</span>
|
|
|
|
|
+ <el-select v-model="selectedBatchId" placeholder="请选择批次" @change="onBatchChange" style="min-width: 220px;">
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="b in batches"
|
|
|
|
|
+ :key="b.id"
|
|
|
|
|
+ :label="b.batchNo + ' (' + b.createTime + ') - 合计:' + (b.totalAmount || 0)"
|
|
|
|
|
+ :value="String(b.id)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- debug info removed -->
|
|
|
|
|
+
|
|
|
|
|
+ <div style="margin-bottom: 8px; font-weight: 600;">
|
|
|
|
|
+ 总合计金额:{{ totalAmountDisplay }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="table-wrapper">
|
|
|
|
|
+ <el-table :data="items" border style="width: 100%; min-width: 1400px;">
|
|
|
|
|
+ <el-table-column prop="rowNo" label="序号" width="80" />
|
|
|
|
|
+ <el-table-column prop="serialNo" label="序号(原表)" width="120" />
|
|
|
|
|
+ <el-table-column prop="purpose" label="用途" min-width="180" />
|
|
|
|
|
+ <el-table-column prop="amount" label="金额(元)" width="140" />
|
|
|
|
|
+ <el-table-column prop="deptPaymentCategory" label="部门支出款项分类" min-width="160" />
|
|
|
|
|
+ <el-table-column prop="funcSubject" label="支出功能分类科目" min-width="160" />
|
|
|
|
|
+ <el-table-column prop="fundNature" label="资金性质" width="120" />
|
|
|
|
|
+ <el-table-column prop="indicatorSource" label="指标来源" min-width="140" />
|
|
|
|
|
+ <el-table-column prop="paymentType" label="支付类型" width="120" />
|
|
|
|
|
+ <el-table-column prop="payeeBank" label="收款人开户银行" min-width="180" />
|
|
|
|
|
+ <el-table-column prop="payee" label="收款人" width="160" />
|
|
|
|
|
+ <el-table-column prop="payeeAccount" label="收款账号" width="180" />
|
|
|
|
|
+ <el-table-column prop="projectType" label="项目类型" min-width="160" />
|
|
|
|
|
+ <el-table-column prop="project" label="项目" min-width="200" />
|
|
|
|
|
+ <el-table-column prop="receiptDate" label="回单时间" width="140" />
|
|
|
|
|
+ <el-table-column prop="localIndicatorDocNo" label="本级指标文号" min-width="160" />
|
|
|
|
|
+ <el-table-column prop="budgetSource" label="预算来源" min-width="140" />
|
|
|
|
|
+ <el-table-column prop="indicatorId" label="指标ID" width="160" />
|
|
|
|
|
+ <el-table-column prop="extField1" label="冗余字段1" width="140" />
|
|
|
|
|
+ <el-table-column prop="extField2" label="冗余字段2" width="140" />
|
|
|
|
|
+ <el-table-column prop="extField3" label="冗余字段3" width="140" />
|
|
|
|
|
+ <el-table-column prop="extField4" label="冗余字段4" width="140" />
|
|
|
|
|
+ <el-table-column prop="extField5" label="冗余字段5" width="140" />
|
|
|
|
|
+ <el-table-column prop="otherFields" label="扩展字段" min-width="200" />
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div style="margin-top: 12px; text-align: right;">
|
|
|
|
|
+ <el-pagination
|
|
|
|
|
+ background
|
|
|
|
|
+ layout="prev, pager, next, jumper"
|
|
|
|
|
+ :total="itemsTotal"
|
|
|
|
|
+ :page-size="pageSize"
|
|
|
|
|
+ :current-page.sync="pageNum"
|
|
|
|
|
+ @current-change="fetchItems"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { ref, reactive, onMounted, computed, watch, nextTick } from 'vue';
|
|
|
|
|
+import ImportDialog from '@/components/ImportDialog/index.vue';
|
|
|
|
|
+import { listImportBatches, listImportItems, downloadBudgetExecutionTemplate } from '@/api/cw/budgetExecutionImport';
|
|
|
|
|
+import { ElMessage } from 'element-plus';
|
|
|
|
|
+import { importBudgetExecution } from '@/api/cw/budgetExecutionImport';
|
|
|
|
|
+
|
|
|
|
|
+const importDialogVisible = ref(false);
|
|
|
|
|
+const importUrl = import.meta.env.VITE_APP_BASE_API + '/cw/budgetExecutionImport/importData';
|
|
|
|
|
+const templateUrl = import.meta.env.VITE_APP_BASE_API + '/cw/budgetExecutionImport/importTemplate';
|
|
|
|
|
+
|
|
|
|
|
+const batches = ref<any[]>([]);
|
|
|
|
|
+const selectedBatchId = ref<string | null>(null);
|
|
|
|
|
+
|
|
|
|
|
+const items = ref<any[]>([]);
|
|
|
|
|
+const itemsTotal = ref(0);
|
|
|
|
|
+const pageNum = ref(1);
|
|
|
|
|
+const pageSize = ref(20);
|
|
|
|
|
+
|
|
|
|
|
+const totalAmountDisplay = computed(() => {
|
|
|
|
|
+ const b = batches.value.find((x) => String(x.id) === selectedBatchId.value);
|
|
|
|
|
+ return b ? (b.totalAmount || 0) : 0;
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const importUrlLocal = importUrl;
|
|
|
|
|
+
|
|
|
|
|
+const openImport = () => {
|
|
|
|
|
+ importDialogVisible.value = true;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const downloadTemplate = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await downloadBudgetExecutionTemplate();
|
|
|
|
|
+ // browser will handle download
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ ElMessage.error('模板下载失败');
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const fileInputRef = ref<HTMLInputElement | null>(null);
|
|
|
|
|
+
|
|
|
|
|
+const triggerFileInput = () => {
|
|
|
|
|
+ fileInputRef.value?.click();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleLocalFileChange = async (e: Event) => {
|
|
|
|
|
+ const input = e.target as HTMLInputElement;
|
|
|
|
|
+ const file = input.files?.[0];
|
|
|
|
|
+ if (!file) {
|
|
|
|
|
+ ElMessage.warning('请先选择文件');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // simple client-side validation
|
|
|
|
|
+ const allowedTypes = ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel'];
|
|
|
|
|
+ if (file.size / 1024 / 1024 > 10) {
|
|
|
|
|
+ ElMessage.error('文件大小不能超过 10MB');
|
|
|
|
|
+ input.value = '';
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const formData = new FormData();
|
|
|
|
|
+ formData.append('file', file);
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 使用对象形式调用以兼容不同环境下的实现细节,避免 createMessage 内部错误
|
|
|
|
|
+ ElMessage({ message: '正在导入,请稍候...', type: 'info', duration: 0 });
|
|
|
|
|
+ const res: any = await importBudgetExecution(file);
|
|
|
|
|
+ ElMessage.closeAll();
|
|
|
|
|
+ if (res && res.code === 200) {
|
|
|
|
|
+ ElMessage.success('导入成功');
|
|
|
|
|
+ fetchBatches();
|
|
|
|
|
+ if (selectedBatchId.value) {
|
|
|
|
|
+ fetchItems();
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage.error(res?.msg || '导入失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ ElMessage.closeAll();
|
|
|
|
|
+ console.error(err);
|
|
|
|
|
+ ElMessage.error('导入失败,请检查文件格式或网络');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ input.value = '';
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const fetchBatches = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res: any = await listImportBatches({ pageNum: 1, pageSize: 100 });
|
|
|
|
|
+ console.debug('budgetExecutionImport - batch list raw res:', res);
|
|
|
|
|
+ let rows = [];
|
|
|
|
|
+ if (res) {
|
|
|
|
|
+ // response can be { code, msg, data: { rows } } or { total, rows, code }
|
|
|
|
|
+ rows = res.data?.rows ?? res.rows ?? res.data ?? [];
|
|
|
|
|
+ }
|
|
|
|
|
+ batches.value = Array.isArray(rows) ? rows : [];
|
|
|
|
|
+ console.debug('budgetExecutionImport - fetched batches:', batches.value);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('fetchBatches error', e);
|
|
|
|
|
+ batches.value = [];
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+// 监听 batches 变化:如果未选择批次则自动选中第一个并加载明细
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => batches.value,
|
|
|
|
|
+ async (val) => {
|
|
|
|
|
+ if (val && val.length > 0 && !selectedBatchId.value) {
|
|
|
|
|
+ // 等待 DOM 更新再触发
|
|
|
|
|
+ selectedBatchId.value = String(val[0].id);
|
|
|
|
|
+ await nextTick();
|
|
|
|
|
+ console.debug('budgetExecutionImport - auto selected batchId:', selectedBatchId.value);
|
|
|
|
|
+ fetchItems(1);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ { immediate: true }
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+// no debugInfo in production
|
|
|
|
|
+
|
|
|
|
|
+const fetchItems = async (page = pageNum.value) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!selectedBatchId.value) {
|
|
|
|
|
+ items.value = [];
|
|
|
|
|
+ itemsTotal.value = 0;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // Important: pass batchId as string to avoid JS Number precision loss for long IDs
|
|
|
|
|
+ const res: any = await listImportItems(selectedBatchId.value as string, { pageNum: page, pageSize: pageSize.value });
|
|
|
|
|
+ console.debug('budgetExecutionImport - item list raw res:', res);
|
|
|
|
|
+ let rows: any[] = [];
|
|
|
|
|
+ let totalCount = 0;
|
|
|
|
|
+ if (res) {
|
|
|
|
|
+ // support multiple formats: { code, data:{ rows, total } } or { total, rows, code }
|
|
|
|
|
+ rows = res.data?.rows ?? res.rows ?? res.data ?? [];
|
|
|
|
|
+ totalCount = res.data?.total ?? res.total ?? (Array.isArray(rows) ? rows.length : 0);
|
|
|
|
|
+ }
|
|
|
|
|
+ items.value = Array.isArray(rows) ? rows : [];
|
|
|
|
|
+ itemsTotal.value = totalCount || 0;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('fetchItems error', e);
|
|
|
|
|
+ items.value = [];
|
|
|
|
|
+ itemsTotal.value = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const onBatchChange = () => {
|
|
|
|
|
+ pageNum.value = 1;
|
|
|
|
|
+ fetchItems(1);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const onImportSuccess = () => {
|
|
|
|
|
+ importDialogVisible.value = false;
|
|
|
|
|
+ fetchBatches();
|
|
|
|
|
+ if (selectedBatchId.value) {
|
|
|
|
|
+ fetchItems();
|
|
|
|
|
+ }
|
|
|
|
|
+ ElMessage.success('导入成功');
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ fetchBatches();
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+defineExpose({});
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.page-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+.table-wrapper {
|
|
|
|
|
+ overflow-x: auto;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|
|
|
|
|
+
|
|
|
|
|
+
|