sohelp-eleplus 1.1.18 → 1.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,6 +25,7 @@
25
25
  <!-- advanced -->
26
26
  <div class="advanced-box" ref="advancedBoxRef">
27
27
  <filter-condition-item
28
+ @search="filter"
28
29
  v-for="(item, index) in getFilterList"
29
30
  :key="item.name"
30
31
  v-model="filterValue.filter[index]"
@@ -37,8 +38,8 @@
37
38
  v-if="filterList.length > 0 && config.filter?.config.filterPosition !== 'MODAL'"
38
39
  ref="searchButtonRef"
39
40
  >
40
- <el-button size="small" plain @click="reset">{{ t('grid.toolbar.reset') }}</el-button>
41
- <el-button type="primary" size="small" @click="filter">{{ t('grid.toolbar.filter') }}</el-button>
41
+ <el-button size="small" plain @click="reset" :icon="Refresh">{{ t('grid.toolbar.reset') }}</el-button>
42
+ <el-button type="primary" size="small" @click="filter" :icon="Search">{{ t('grid.toolbar.filter') }}</el-button>
42
43
  <el-link
43
44
  plain
44
45
  @click="toggleFilter"
@@ -58,11 +59,11 @@
58
59
  </template>
59
60
 
60
61
  <script setup>
61
- import { computed, nextTick, onMounted, reactive, ref, onBeforeUnmount } from 'vue';
62
+ import { computed, nextTick, onMounted, reactive, ref, onBeforeUnmount, watch } from 'vue';
62
63
  import FilterConditionItem from '../../sohelp-grid/components/filter-condition-item.vue';
63
64
  import { useMobile } from '@/utils/use-mobile';
64
65
  import { useI18n } from 'vue-i18n';
65
- import { Plus, Minus } from '@element-plus/icons-vue';
66
+ import { Plus, Minus, Search, Refresh } from '@element-plus/icons-vue';
66
67
 
67
68
  const { mobile } = useMobile();
68
69
  const { t } = useI18n();
@@ -159,16 +160,10 @@
159
160
  });
160
161
 
161
162
  const load = () => {
162
- maxRowsConfig.width = advancedBoxRef.value?.offsetWidth || 160;
163
- maxRowsConfig.current = findAccumulateIndex(maxRowsConfig.itemWidth, maxRowsConfig.width * maxRowsConfig.maxRows);
164
- };
165
-
166
- onMounted(() => {
167
- loading.value = true;
168
- nextTick(() => {
169
- loading.value = false;
170
- // 高级筛选高度
171
- maxRowsConfig.height = advancedBoxRef.value.offsetHeight + 'px';
163
+ const { filterPosition } = config.value.filter?.config;
164
+ if (filterPosition?.toUpperCase() === 'TOP') {
165
+ maxRowsConfig.current.index = filterList.value.length;
166
+ maxRowsConfig.height = advancedBoxRef.value?.offsetHeight + 'px';
172
167
  const advancedRow = advancedBoxRef.value.querySelector('.sohelp-filter-condition');
173
168
  if (advancedRow) {
174
169
  maxRowsConfig.rowHeight = advancedRow.offsetHeight;
@@ -176,8 +171,22 @@
176
171
  nextTick(() => {
177
172
  maxRowsConfig.searchButtonWidth = searchButtonRef.value?.offsetWidth + 35;
178
173
  maxRowsConfig.itemWidth = getAllHeights();
179
- load();
174
+
175
+ maxRowsConfig.width = advancedBoxRef.value?.offsetWidth || 160;
176
+ maxRowsConfig.current = findAccumulateIndex(
177
+ maxRowsConfig.itemWidth,
178
+ maxRowsConfig.width * maxRowsConfig.maxRows
179
+ );
180
180
  });
181
+ }
182
+ };
183
+
184
+ onMounted(() => {
185
+ loading.value = true;
186
+ nextTick(() => {
187
+ loading.value = false;
188
+ // 高级筛选高度
189
+ load();
181
190
  });
182
191
  });
183
192
 
@@ -229,6 +238,17 @@
229
238
  onBeforeUnmount(() => {
230
239
  removeEventListener('resize', () => {});
231
240
  });
241
+
242
+ watch(
243
+ () => config.value.filter.config.filterPosition,
244
+ (val) => {
245
+ if (val.toUpperCase() === 'TOP') {
246
+ nextTick(() => {
247
+ load();
248
+ });
249
+ }
250
+ }
251
+ );
232
252
  </script>
233
253
  <script>
234
254
  export default {
@@ -279,11 +299,12 @@
279
299
  .scheme-box {
280
300
  padding-top: 10px;
281
301
  display: flex;
282
- align-items: center;
302
+ align-items: flex-start;
283
303
  gap: 10px;
284
304
 
285
305
  .scheme-list {
286
306
  display: flex;
307
+ gap: 5px;
287
308
  }
288
309
  }
289
310
 
@@ -377,6 +398,10 @@
377
398
  bottom: 0px;
378
399
  position: absolute;
379
400
  }
401
+
402
+ .scheme-box {
403
+ flex-direction: column;
404
+ }
380
405
  }
381
406
 
382
407
  .sohelp-filter.right .sohelp-filter-condition {
@@ -4,6 +4,7 @@
4
4
  import { computed, ref } from 'vue';
5
5
  import FilterConfig from './config/index.vue';
6
6
  import { useI18n } from 'vue-i18n';
7
+ import { Search, Refresh } from '@element-plus/icons-vue';
7
8
 
8
9
  const emit = defineEmits('change', 'filter', 'update:modelValue', 'close', 'reset', 'save', 'changeFilterScheme');
9
10
 
@@ -73,7 +74,7 @@
73
74
  }"
74
75
  :class="['sohelp-filter-card', config.filter.config.filterPosition]"
75
76
  :header="t('grid.toolbar.adv_filter')"
76
- :headerStyle="{ padding: '0 10px 10px 0' }"
77
+ :headerStyle="{ padding: '0 0 5px 0', fontWeight: 'normal', fontSize: '14px' }"
77
78
  :bodyStyle="{ padding: '0', height: 'calc(100% - 40px)', background: '#fff' }"
78
79
  >
79
80
  <template #extra>
@@ -143,8 +144,10 @@
143
144
  ></filter-form>
144
145
  <template #footer>
145
146
  <div class="footer-right" v-if="config.filter.config.filterPosition !== 'TOP'">
146
- <el-button size="small" plain @click="reset">{{ t('grid.toolbar.reset') }}</el-button>
147
- <el-button type="primary" size="small" @click="filter(false)">{{ t('grid.toolbar.filter') }}</el-button>
147
+ <el-button size="small" plain @click="reset" :icon="Refresh">{{ t('grid.toolbar.reset') }}</el-button>
148
+ <el-button type="primary" size="small" @click="filter(false)" :icon="Search">{{
149
+ t('grid.toolbar.filter')
150
+ }}</el-button>
148
151
  </div>
149
152
  </template>
150
153
  </ele-modal>
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed, reactive, ref, watch } from 'vue';
2
+ import { computed, nextTick, reactive, ref, watch } from "vue";
3
3
  import { useMobile } from '@/utils/use-mobile';
4
4
  import SohelpFilter from './filter/index.vue';
5
5
  import SohelpGrid from '../sohelp-grid/index.vue';
@@ -128,19 +128,28 @@
128
128
 
129
129
  /**
130
130
  * 查询数据
131
- * @param param 区分是高级筛选,还是列筛选
131
+ * @param params 区分是高级筛选,还是列筛选
132
132
  */
133
- const reload = (param = {}, callback) => {
134
- sohelpGridRef.value.reload(param, callback);
133
+ const reload = (params = {}, callback) => {
134
+ sohelpGridRef.value.reload(params, callback);
135
135
  };
136
- const load = (param = {}, callback) => {
137
- sohelpGridRef.value.load(param, callback);
136
+ /**
137
+ * 加载数据
138
+ * @param params
139
+ * @param callback
140
+ */
141
+ const load = (params = {}, callback) => {
142
+ sohelpGridRef.value.load(params, callback);
138
143
  };
144
+ /**
145
+ * 刷新数据
146
+ */
139
147
  const refresh = () => {
140
148
  sohelpGridRef.value.refresh();
141
149
  };
142
150
 
143
151
  const changeFilterScheme = (schemeValue, filterValue) => {
152
+ sohelpGridRef.value.reload({});
144
153
  emit('changeFilterScheme', schemeValue, filterValue);
145
154
  };
146
155
  /**
@@ -4,9 +4,9 @@
4
4
  placeholder="请选择图标"
5
5
  filterable
6
6
  class="sohelp-icon-select"
7
- width="100%"
8
- style="min-width: 60px"
9
7
  clearable
8
+ fit-input-width
9
+ style="min-width: 60px; width: 100%"
10
10
  >
11
11
  <template #prefix>
12
12
  <el-icon :size="14" color="#666" style="margin-right: 5px">
@@ -0,0 +1,470 @@
1
+ <template>
2
+ <ele-modal form :model-value="modelValue" title="数据导入" :width="860" @update:modelValue="updateModelValue">
3
+ <div class="import-wrap" v-loading="loading">
4
+ <div class="import-header">
5
+ <div class="tips">
6
+ <ele-text>支持文件格式:xls、xlsx;大小 ≤ 50MB;最多 10,000 行</ele-text>
7
+ </div>
8
+ <div class="actions">
9
+ <el-button type="primary" plain :loading="downloadingTemplate" @click="downloadTemplate">下载导入模板</el-button>
10
+ <el-button plain @click="openConfig">配置导入列表</el-button>
11
+ <el-link :underline="false" @click="viewHistory">查看导入历史</el-link>
12
+ </div>
13
+ </div>
14
+ <div class="upload-area" @drop.prevent="onDrop" @dragover.prevent>
15
+ <el-upload
16
+ drag
17
+ :auto-upload="false"
18
+ :show-file-list="false"
19
+ :on-change="onFileChange"
20
+ :on-remove="onRemove"
21
+ :limit="1"
22
+ accept=".xls,.xlsx"
23
+ class="uploader"
24
+ >
25
+ <i class="el-icon"><UploadFilled /></i>
26
+ <div class="el-upload__text">拖拽文件到此处或点击上传</div>
27
+ <div class="el-upload__tip">仅限 .xls/.xlsx,大小不超过 50MB</div>
28
+ </el-upload>
29
+ <div class="status" v-if="fileName">
30
+ <ele-text>已选择:{{ fileName }}({{ formatSize(fileSize) }})</ele-text>
31
+ <el-progress :percentage="progress" :status="progressStatus" />
32
+ <ele-text v-if="rowCount">总共发现 {{ rowCount }} 条数据</ele-text>
33
+ </div>
34
+ </div>
35
+ <div class="result-actions" v-if="rowCount">
36
+ <el-button type="primary" plain @click="togglePreview">{{ previewVisible ? '隐藏预览数据' : '查看预览数据' }}</el-button>
37
+ <el-button plain @click="downloadProcessed">下载导入文件</el-button>
38
+ </div>
39
+ <ele-card v-if="previewVisible" style="margin-top: 12px" header="预览前 50 条">
40
+ <vxe-grid :columns="previewColumns" :data="previewData" size="mini" border />
41
+ </ele-card>
42
+ </div>
43
+ <template #footer>
44
+ <el-button @click="onClose">取消</el-button>
45
+ <el-button type="primary" :disabled="!canImport" @click="commitImport">确认导入</el-button>
46
+ </template>
47
+ </ele-modal>
48
+ <ele-modal :model-value="configVisible" title="配置导入列表" :width="640" @update:modelValue="updateConfigVisible">
49
+ <div class="config-wrap">
50
+ <ele-card header="可导入字段白名单">
51
+ <el-checkbox-group v-model="whitelist">
52
+ <el-checkbox v-for="f in fields" :key="f.name" :label="f.name">{{ f.label || f.name }}</el-checkbox>
53
+ </el-checkbox-group>
54
+ </ele-card>
55
+ <ele-card header="字段映射关系" style="margin-top: 12px">
56
+ <div class="mapping">
57
+ <div class="row" v-for="f in whitelist" :key="f">
58
+ <span class="label">{{ getFieldLabel(f) }}</span>
59
+ <el-input v-model="mapping[f]" placeholder="Excel列名" style="width: 240px" />
60
+ </div>
61
+ </div>
62
+ </ele-card>
63
+ </div>
64
+ <template #footer>
65
+ <el-button @click="updateConfigVisible(false)">取消</el-button>
66
+ <el-button type="primary" @click="saveConfig">保存</el-button>
67
+ </template>
68
+ </ele-modal>
69
+ <ele-modal
70
+ :model-value="historyVisible"
71
+ title="导入历史"
72
+ :width="800"
73
+ @update:modelValue="(v) => (historyVisible = v)"
74
+ >
75
+ <div class="history-wrap">
76
+ <vxe-grid :columns="historyColumns" :data="historyList" size="mini" border>
77
+ <template #op="{ row }">
78
+ <el-space :size="6">
79
+ <el-button size="small" plain @click="downloadHistoryFile(row)">下载文件</el-button>
80
+ <el-button size="small" plain @click="restoreHistoryFile(row)">恢复导入</el-button>
81
+ <el-button size="small" plain type="danger" @click="deleteHistoryFile(row)">删除文件</el-button>
82
+ </el-space>
83
+ </template>
84
+ </vxe-grid>
85
+ </div>
86
+ <template #footer>
87
+ <el-button @click="historyVisible = false">关闭</el-button>
88
+ </template>
89
+ </ele-modal>
90
+ </template>
91
+ <script setup>
92
+ import { ref, computed } from 'vue';
93
+ import { ElMessage } from 'element-plus/es';
94
+ import { EleMessage } from '@/components/ele-admin-plus/components';
95
+ import { UploadFilled } from '@element-plus/icons-vue';
96
+ import SohelpHttp from '../http/SohelpHttp.js';
97
+
98
+ const MAX_ROWS = 10000;
99
+
100
+ const props = defineProps({
101
+ modelValue: Boolean,
102
+ refid: {
103
+ type: String,
104
+ required: true
105
+ },
106
+ fields: {
107
+ type: Array,
108
+ default: () => []
109
+ }
110
+ });
111
+ const emit = defineEmits(['update:modelValue', 'close']);
112
+
113
+ const loading = ref(false);
114
+ const fileName = ref('');
115
+ const fileSize = ref(0);
116
+ const progress = ref(0);
117
+ const progressStatus = ref('success');
118
+ const rowCount = ref(0);
119
+ const fileId = ref('');
120
+ const previewVisible = ref(false);
121
+ const previewData = ref([]);
122
+ const previewColumns = ref([]);
123
+
124
+ const configVisible = ref(false);
125
+ const whitelist = ref([]);
126
+ const mapping = ref({});
127
+ const historyVisible = ref(false);
128
+ const historyList = ref([]);
129
+ const downloadingTemplate = ref(false);
130
+ const historyColumns = ref([
131
+ { field: 'file', title: '文件名', align: 'center', minWidth: 160 },
132
+ { field: 'size', title: '大小', align: 'center', minWidth: 100 },
133
+ { field: 'rows', title: '记录数', align: 'center', minWidth: 90 },
134
+ { field: 'upload_user', title: '导入人员', align: 'center', minWidth: 120 },
135
+ { field: 'update_time', title: '导入时间', align: 'center', minWidth: 180 },
136
+ { field: 'op', title: '操作', align: 'center', minWidth: 220, slots: { default: 'op' } }
137
+ ]);
138
+
139
+ const canImport = computed(() => !!fileId.value && rowCount.value > 0 && rowCount.value <= MAX_ROWS);
140
+
141
+ const updateModelValue = (val) => {
142
+ emit('update:modelValue', val);
143
+ if (!val) {
144
+ onRemove();
145
+ }
146
+ };
147
+
148
+ const onClose = () => {
149
+ emit('update:modelValue', false);
150
+ emit('close');
151
+ onRemove();
152
+ };
153
+
154
+ const formatSize = (n) => {
155
+ if (n < 1024) return n + 'B';
156
+ if (n < 1024 * 1024) return (n / 1024).toFixed(1) + 'KB';
157
+ return (n / 1024 / 1024).toFixed(1) + 'MB';
158
+ };
159
+
160
+ const openConfig = () => {
161
+ if (!whitelist.value.length) {
162
+ whitelist.value = props.fields.map((f) => f.name);
163
+ }
164
+ if (Object.keys(mapping.value).length === 0) {
165
+ props.fields.forEach((f) => (mapping.value[f.name] = f.label || f.name));
166
+ }
167
+ configVisible.value = true;
168
+ };
169
+ const updateConfigVisible = (v) => (configVisible.value = v);
170
+ const saveConfig = () => {
171
+ configVisible.value = false;
172
+ };
173
+ const getFieldLabel = (name) => props.fields.find((f) => f.name === name)?.label || name;
174
+
175
+ /**
176
+ * 下载导入模板
177
+ * - 绑定click事件监听器到 button 元素
178
+ * - 在事件处理函数中动态获取当前列表的refid值
179
+ * - 构造完整下载URL:/engine/web/import/downloadTemplate?refid=${动态获取的refid}
180
+ * - 使用SohelpHttp.download方法发起请求,参数格式为:{refid: '动态refid值'}
181
+ * - 错误处理:捕获refid无效、网络错误、服务器错误,并实现重试
182
+ * - 用户体验:显示loading,下载进度
183
+ */
184
+ const downloadTemplate = async () => {
185
+ // 验证 refid 参数
186
+ if (!props.refid) {
187
+ EleMessage.error('请选择有效的模板');
188
+ return;
189
+ }
190
+
191
+ downloadingTemplate.value = true;
192
+ const maxRetries = 3;
193
+ let retryCount = 0;
194
+ let lastError = null;
195
+
196
+ // 错误重试机制(最多3次)
197
+ while (retryCount <= maxRetries) {
198
+ try {
199
+ await SohelpHttp.download(
200
+ '/engine/web/import/downloadTemplate',
201
+ { refid: props.refid },
202
+ '导入模板.xlsx',
203
+ (loaded, total) => {
204
+ // 大文件下载时显示进度条
205
+ // 这里可以对接UI的进度条,目前简单处理,如果需要更复杂的UI交互可以扩展
206
+ if (total > 0) {
207
+ const percent = Math.floor((loaded / total) * 100);
208
+ // console.log(`下载进度: ${percent}%`);
209
+ }
210
+ },
211
+ { timeout: 30000 } // 实现请求超时机制,默认设置为30秒
212
+ );
213
+ EleMessage.success('模板下载成功');
214
+ downloadingTemplate.value = false;
215
+ return;
216
+ } catch (error) {
217
+ lastError = error;
218
+ retryCount++;
219
+ if (retryCount <= maxRetries) {
220
+ // 简单的指数退避或固定等待
221
+ await new Promise((resolve) => setTimeout(resolve, 1000));
222
+ }
223
+ }
224
+ }
225
+
226
+ downloadingTemplate.value = false;
227
+ // 错误处理机制
228
+ const errorMsg = String(lastError);
229
+ if (errorMsg.includes('timeout') || errorMsg.includes('Network Error')) {
230
+ EleMessage.error('网络连接异常,请稍后重试');
231
+ } else {
232
+ EleMessage.error(errorMsg || '下载失败');
233
+ }
234
+ };
235
+
236
+ const onDrop = async (e) => {
237
+ const file = e.dataTransfer.files?.[0];
238
+ if (file) await handleFile(file);
239
+ };
240
+ const onFileChange = async (file) => {
241
+ const f = file?.raw || file;
242
+ if (f) await handleFile(f);
243
+ };
244
+ const onRemove = () => {
245
+ fileName.value = '';
246
+ fileSize.value = 0;
247
+ progress.value = 0;
248
+ rowCount.value = 0;
249
+ previewVisible.value = false;
250
+ fileId.value = '';
251
+ previewData.value = [];
252
+ previewColumns.value = [];
253
+ progressStatus.value = 'success';
254
+ loading.value = false;
255
+ };
256
+
257
+ const handleFile = async (file) => {
258
+ const ext = file.name.toLowerCase().split('.').pop();
259
+ if (!['xls', 'xlsx'].includes(ext)) {
260
+ ElMessage.error('仅支持 .xls 或 .xlsx 文件');
261
+ return;
262
+ }
263
+ if (file.size > 50 * 1024 * 1024) {
264
+ ElMessage.error('文件大小超过 50MB 限制');
265
+ return;
266
+ }
267
+ fileName.value = file.name;
268
+ fileSize.value = file.size;
269
+ progress.value = 10;
270
+ const form = new FormData();
271
+ form.append('refid', props.refid);
272
+ form.append('file', file);
273
+ try {
274
+ const res = await SohelpHttp.post('/engine/web/import/upload', form);
275
+ if (!res.meta.success) {
276
+ progressStatus.value = 'exception';
277
+ ElMessage.error(res.meta.message || '上传失败');
278
+ return;
279
+ }
280
+ fileId.value = res.data?.fileId || '';
281
+ rowCount.value = res.data?.rows || 0;
282
+ if (rowCount.value > MAX_ROWS) {
283
+ progressStatus.value = 'exception';
284
+ ElMessage.error('超过最大行数限制(10,000)');
285
+ return;
286
+ }
287
+ progress.value = 60;
288
+ await loadPreview();
289
+ progress.value = 100;
290
+ progressStatus.value = 'success';
291
+ // 默认不显示预览数据
292
+ previewVisible.value = false;
293
+ } catch (e) {
294
+ progressStatus.value = 'exception';
295
+ ElMessage.error(String(e));
296
+ }
297
+ };
298
+
299
+ const togglePreview = () => {
300
+ previewVisible.value = !previewVisible.value;
301
+ };
302
+ const downloadProcessed = () => {
303
+ if (!fileId.value) {
304
+ ElMessage.error('请先上传导入文件');
305
+ return;
306
+ }
307
+ SohelpHttp.download(
308
+ '/engine/web/import/download',
309
+ { fileId: fileId.value },
310
+ fileName.value || '导入数据.xlsx'
311
+ ).catch((e) => ElMessage.error(String(e)));
312
+ };
313
+ const viewHistory = async () => {
314
+ try {
315
+ const res = await SohelpHttp.get('/engine/web/import/history', { refid: props.refid });
316
+ if (res.meta.success) {
317
+ const list = res.data?.results || [];
318
+ historyList.value = list;
319
+ historyVisible.value = true;
320
+ } else {
321
+ ElMessage.error(res.meta.message || '获取历史失败');
322
+ }
323
+ } catch (e) {
324
+ ElMessage.error(String(e));
325
+ }
326
+ };
327
+ const commitImport = async () => {
328
+ if (!canImport.value) {
329
+ ElMessage.error('请上传有效的导入文件');
330
+ return;
331
+ }
332
+ const loadingTip = EleMessage.loading('导入中..');
333
+ try {
334
+ loading.value = true;
335
+ const res = await SohelpHttp.post('/engine/web/import/confirm', { fileId: fileId.value, refid: props.refid });
336
+ if (res.meta.success) {
337
+ ElMessage.success(res.meta.message || '导入完成');
338
+ emit('update:modelValue', false);
339
+ } else {
340
+ ElMessage.error(res.meta.message || '导入失败');
341
+ }
342
+ } catch (e) {
343
+ ElMessage.error(String(e));
344
+ } finally {
345
+ loadingTip.close();
346
+ loading.value = false;
347
+ }
348
+ };
349
+
350
+ const loadPreview = async () => {
351
+ try {
352
+ const res = await SohelpHttp.get('/engine/web/import/preview', { refid: props.refid, fileId: fileId.value });
353
+ if (res.meta.success) {
354
+ const rows = Array.isArray(res.data.results) ? res.data.results : [];
355
+ previewData.value = rows.slice(0, 50);
356
+ const keys = rows.length
357
+ ? Object.keys(rows[0])
358
+ : whitelist.value.length
359
+ ? whitelist.value
360
+ : props.fields.map((f) => f.name);
361
+ previewColumns.value = keys.map((k) => ({
362
+ field: k,
363
+ title: getFieldLabel(k),
364
+ align: 'center',
365
+ minWidth: 120
366
+ }));
367
+ // previewVisible.value = true; // 加载数据后不自动显示
368
+ } else {
369
+ ElMessage.error(res.meta.message || '预览失败');
370
+ }
371
+ } catch (e) {
372
+ ElMessage.error(String(e));
373
+ }
374
+ };
375
+
376
+ const downloadHistoryFile = (row) => {
377
+ SohelpHttp.download('/engine/web/import/download', { fileId: row.file_uuid }, row.file).catch((e) =>
378
+ ElMessage.error(String(e))
379
+ );
380
+ };
381
+ const restoreHistoryFile = async (row) => {
382
+ try {
383
+ const res = await SohelpHttp.post('/engine/web/import/restore', { fileId: row.file_uuid, refid: props.refid });
384
+ if (res.meta.success) {
385
+ ElMessage.success(res.meta.message || '恢复成功');
386
+ } else {
387
+ ElMessage.error(res.meta.message || '恢复失败');
388
+ }
389
+ } catch (e) {
390
+ ElMessage.error(String(e));
391
+ }
392
+ };
393
+ const deleteHistoryFile = async (row) => {
394
+ try {
395
+ const res = await SohelpHttp.post('/engine/web/import/delete', { fileId: row.file_uuid, refid: props.refid });
396
+ if (res.meta.success) {
397
+ ElMessage.success(res.meta.message || '删除成功');
398
+ await viewHistory();
399
+ } else {
400
+ ElMessage.error(res.meta.message || '删除失败');
401
+ }
402
+ } catch (e) {
403
+ ElMessage.error(String(e));
404
+ }
405
+ };
406
+ </script>
407
+ <style scoped lang="scss">
408
+ .import-wrap {
409
+ display: flex;
410
+ flex-direction: column;
411
+ gap: 12px;
412
+ }
413
+ .import-header {
414
+ display: flex;
415
+ align-items: center;
416
+ justify-content: space-between;
417
+ }
418
+ .actions {
419
+ display: flex;
420
+ gap: 10px;
421
+ }
422
+ .actions :deep(.el-button:hover) {
423
+ opacity: 0.9;
424
+ }
425
+ .upload-area {
426
+ min-width: 300px;
427
+ min-height: 200px;
428
+ border: 1px dashed #bbb;
429
+ display: flex;
430
+ flex-direction: column;
431
+ align-items: center;
432
+ justify-content: center;
433
+ padding: 20px;
434
+ border-radius: 6px;
435
+ }
436
+ .uploader {
437
+ width: 100%;
438
+ }
439
+ .status {
440
+ width: 100%;
441
+ margin-top: 10px;
442
+ }
443
+ .result-actions {
444
+ display: flex;
445
+ gap: 10px;
446
+ }
447
+ .config-wrap {
448
+ display: flex;
449
+ flex-direction: column;
450
+ gap: 12px;
451
+ }
452
+ .mapping {
453
+ display: flex;
454
+ flex-direction: column;
455
+ gap: 8px;
456
+ }
457
+ .mapping .row {
458
+ display: flex;
459
+ align-items: center;
460
+ gap: 10px;
461
+ }
462
+ .mapping .label {
463
+ width: 160px;
464
+ }
465
+ .history-wrap {
466
+ display: flex;
467
+ flex-direction: column;
468
+ gap: 10px;
469
+ }
470
+ </style>