This commit is contained in:
Leo_Ding 2025-10-12 10:23:04 +08:00
commit def6dbb546
8 changed files with 1903 additions and 105 deletions

11
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "HaHa-Admin",
"version": "1.0.0",
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@ant-design/colors": "^7.0.0",
"@ant-design/icons-vue": "^6.1.0",
"@icon-park/vue-next": "^1.4.2",
@ -240,6 +241,11 @@
"@algolia/requester-common": "4.19.1"
}
},
"node_modules/@amap/amap-jsapi-loader": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/@amap/amap-jsapi-loader/-/amap-jsapi-loader-1.0.1.tgz",
"integrity": "sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw=="
},
"node_modules/@ant-design/colors": {
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-7.0.0.tgz",
@ -5179,6 +5185,11 @@
"@algolia/requester-common": "4.19.1"
}
},
"@amap/amap-jsapi-loader": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/@amap/amap-jsapi-loader/-/amap-jsapi-loader-1.0.1.tgz",
"integrity": "sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw=="
},
"@ant-design/colors": {
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-7.0.0.tgz",

View File

@ -17,6 +17,7 @@
"src/**/*.{js,vue}": "eslint --ext .js,.vue .eslintignore --no-cache --fix"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@ant-design/colors": "^7.0.0",
"@ant-design/icons-vue": "^6.1.0",
"@icon-park/vue-next": "^1.4.2",

View File

@ -0,0 +1,296 @@
<template>
<a-modal
v-model:open="visible"
:title="title || '导出记录'"
width="1200px"
:footer="null"
:maskClosable="false"
@cancel="handleCancel"
:destroyOnClose="true"
>
<div class="import-records-modal">
<!-- 表格 -->
<a-table
:dataSource="tableData"
:columns="columns"
:pagination="pagination"
:loading="loading"
:scroll="{ x: 1200 }"
rowKey="taskId"
>
<!-- 状态列渲染 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 进度列渲染 -->
<template #progress="{ record }">
<a-progress
:percent="record.progress"
size="small"
:status="getProgressStatus(record.status)"
/>
</template>
<!-- 操作列渲染 -->
<template #action="{ record }">
<a-space>
<a-button type="link" size="small" @click="viewDetails(record)" :disabled="record.status !== 'completed'">
查看详情
</a-button>
<a-button type="primary" size="small" @click="downloadResult(record)" :disabled="record.status !== 'completed'">
下载文件
</a-button>
<a-button type="link" size="small" @click="retryExport(record)" :disabled="record.status !== 'failed'">
重新导出
</a-button>
</a-space>
</template>
</a-table>
</div>
</a-modal>
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue';
import { message } from 'ant-design-vue';
// props
const props = defineProps({
//
title: {
type: String,
default: '导出记录'
},
//
modelValue: {
type: Boolean,
default: false
},
//
fetchData: {
type: Function,
required: true
}
});
// emit
const emit = defineEmits(['update:modelValue', 'close', 'view-details', 'download-result', 'retry-export']);
//
const visible = ref(false);
//
const tableData = ref([]);
//
const loading = ref(false);
//
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100'],
onChange: (page, pageSize) => {
pagination.current = page;
pagination.pageSize = pageSize;
fetchTableData();
},
onShowSizeChange: (current, size) => {
pagination.current = 1;
pagination.pageSize = size;
fetchTableData();
}
});
//
const columns = [
{
title: '任务ID',
dataIndex: 'taskId',
key: 'taskId',
align: 'center',
fixed: 'left',
width: 150,
},
{
title: '任务类型',
dataIndex: 'alias',
key: 'alias',
align: 'center',
width: 150,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
width: 100,
slots: { customRender: 'status' },
},
{
title: '进度',
dataIndex: 'progress',
key: 'progress',
align: 'center',
width: 150,
slots: { customRender: 'progress' },
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
width: 180,
},
{
title: '操作人',
dataIndex: 'createByName',
key: 'createByName',
align: 'center',
width: 120,
},
{
title: '操作',
key: 'action',
align: 'center',
fixed: 'right',
width: 280,
slots: { customRender: 'action' },
},
];
// modelValue
watch(
() => props.modelValue,
(val) => {
visible.value = val;
if (val) {
//
fetchTableData();
}
},
{ immediate: true } //
);
//
const fetchTableData = async () => {
loading.value = true;
try {
//
const result = await props.fetchData({
page: pagination.current,
pageSize: pagination.pageSize
});
// { list: [...], total: 100 }
tableData.value = Array.isArray(result.list) ? result.list : result;
pagination.total = result.total || tableData.value.length;
} catch (error) {
console.error('获取数据失败:', error);
message.error(error.message || '获取导出记录失败');
tableData.value = [];
pagination.total = 0;
} finally {
loading.value = false;
}
};
//
const getStatusText = (status) => {
switch (status) {
case 'completed': return '已完成';
case 'failed': return '失败';
case 'processing': return '处理中';
case 'pending': return '待处理';
default: return status;
}
};
//
const getStatusColor = (status) => {
switch (status) {
case 'completed': return 'green';
case 'failed': return 'red';
case 'processing': return 'blue';
case 'pending': return '#ffcc00';
default: return 'default';
}
};
//
const getProgressStatus = (status) => {
if (status === 'completed') return 'success';
if (status === 'failed') return 'exception';
if (status === 'processing') return 'active';
return 'normal';
};
//
const handleCancel = () => {
visible.value = false;
emit('update:modelValue', false);
emit('close');
};
//
const viewDetails = (record) => {
emit('view-details', record);
};
//
const downloadResult = (record) => {
emit('download-result', record);
};
//
const retryExport = (record) => {
emit('retry-export', record);
};
</script>
<style scoped>
.import-records-modal {
padding: 10px 0;
}
/* 针对表格的样式优化 */
:deep(.ant-table) {
background: #fff;
border-radius: 6px;
}
:deep(.ant-table-thead > tr > th) {
text-align: center;
background-color: #f9f9f9;
font-weight: 600;
}
:deep(.ant-table-tbody > tr > td) {
text-align: center;
}
/* 操作按钮样式 */
:deep(.ant-btn-link) {
height: auto;
padding: 2px 4px;
color: #1890ff;
}
:deep(.ant-btn-link:hover) {
color: #40a9ff;
}
:deep(.ant-btn-primary) {
height: 24px;
padding: 0 8px;
font-size: 12px;
}
</style>

View File

@ -0,0 +1,310 @@
<!-- ImportRecordsModal.vue -->
<template>
<a-modal
v-model:open="visible"
:title="title || '导入记录'"
width="1200px"
:footer="null"
:maskClosable="false"
@cancel="handleCancel"
:destroyOnClose="true"
>
<div class="import-records-modal">
<!-- 表格 -->
<a-table
:dataSource="tableData"
:columns="columns"
:pagination="pagination"
:loading="loading"
:scroll="{ x: 1200 }"
rowKey="taskId"
>
<!-- 状态列渲染 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 进度列渲染 -->
<template #progress="{ record }">
<a-progress
:percent="record.progress"
size="small"
:status="getProgressStatus(record.status)"
/>
</template>
<!-- 操作列渲染 -->
<template #action="{ record }">
<a-space>
<a-button type="link" size="small" @click="viewDetails(record)">
查看异常数据
</a-button>
</a-space>
</template>
</a-table>
</div>
</a-modal>
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue';
import { message } from 'ant-design-vue';
// props
const props = defineProps({
//
title: {
type: String,
default: '导入记录'
},
//
modelValue: {
type: Boolean,
default: false
},
//
fetchData: {
type: Function,
required: true
}
});
// emit
const emit = defineEmits(['update:modelValue', 'close', 'view-details', 'download-result', 'retry-import']);
//
const visible = ref(false);
//
const tableData = ref([]);
//
const loading = ref(false);
//
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100'],
onChange: (page, pageSize) => {
pagination.current = page;
pagination.pageSize = pageSize;
fetchTableData();
},
onShowSizeChange: (current, size) => {
pagination.current = 1;
pagination.pageSize = size;
fetchTableData();
}
});
//
const columns = [
{
title: '批次号',
dataIndex: 'taskId',
key: 'taskId',
align: 'center',
fixed: 'left',
width: 150,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
width: 100,
slots: { customRender: 'status' },
},
{
title: '导入任务类型',
dataIndex: 'alias',
key: 'alias',
align: 'center',
width: 150,
},
{
title: '总数量',
dataIndex: 'totalCount',
key: 'totalCount',
align: 'center',
width: 100,
},
{
title: '成功数量',
dataIndex: 'successCount',
key: 'successCount',
align: 'center',
width: 100,
},
{
title: '异常数量',
dataIndex: 'failCount',
key: 'failCount',
align: 'center',
width: 100,
},
{
title: '上传时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
width: 180,
},
{
title: '操作人',
dataIndex: 'createByName',
key: 'createByName',
align: 'center',
width: 120,
},
{
title: '进度',
dataIndex: 'progress',
key: 'progress',
align: 'center',
width: 150,
slots: { customRender: 'progress' },
},
{
title: '操作',
key: 'action',
align: 'center',
fixed: 'right',
width: 200,
slots: { customRender: 'action' },
},
];
// modelValue
watch(() => props.modelValue, (val) => {
visible.value = val;
if (val) {
//
fetchTableData();
}
});
//
const fetchTableData = async () => {
loading.value = true;
try {
//
const result = await props.fetchData({
page: pagination.current,
pageSize: pagination.pageSize
});
// { list: [...], total: 100 }
tableData.value = Array.isArray(result.list) ? result.list : result;
pagination.total = result.total || tableData.value.length;
} catch (error) {
console.error('获取数据失败:', error);
message.error(error.message || '获取导入记录失败');
tableData.value = [];
pagination.total = 0;
} finally {
loading.value = false;
}
};
//
const getStatusText = (status) => {
switch (status) {
case 'completed': return '已完成';
case 'failed': return '失败';
case 'processing': return '处理中';
case 'pending': return '待处理';
default: return status;
}
};
//
const getStatusColor = (status) => {
switch (status) {
case 'completed': return 'green';
case 'failed': return 'red';
case 'processing': return 'blue';
case 'pending': return '#ffcc00';
default: return 'default';
}
};
//
const getProgressStatus = (status) => {
if (status === 'completed') return 'success';
if (status === 'failed') return 'exception';
if (status === 'processing') return 'active';
return 'normal';
};
//
const handleCancel = () => {
visible.value = false;
emit('update:modelValue', false);
emit('close');
};
//
const viewDetails = (record) => {
emit('view-details', record);
};
//
const downloadResult = (record) => {
emit('download-result', record);
};
//
const retryImport = (record) => {
emit('retry-import', record);
};
//
onMounted(() => {
//
});
</script>
<style scoped>
.import-records-modal {
padding: 10px 0;
}
/* 针对表格的样式优化 */
:deep(.ant-table) {
background: #fff;
border-radius: 6px;
}
:deep(.ant-table-thead > tr > th) {
text-align: center;
background-color: #f9f9f9;
font-weight: 600;
}
:deep(.ant-table-tbody > tr > td) {
text-align: center;
}
/* 操作按钮样式 */
:deep(.ant-btn-link) {
height: auto;
padding: 2px 4px;
color: #1890ff;
}
:deep(.ant-btn-link:hover) {
color: #40a9ff;
}
</style>

View File

@ -0,0 +1,926 @@
<template>
<a-modal
:open="visible"
:title="title"
:width="width"
:maskClosable="false"
@ok="handleOk"
@cancel="handleCancel"
>
<!-- 错误提示区域 -->
<a-alert
v-if="errorMessage"
:message="errorMessage"
:description="errorDetails"
type="error"
show-icon
style="margin-bottom: 12px"
closable
@close="clearError"
/>
<!-- 搜索区域 -->
<div class="search-section">
<a-form layout="vertical" :model="searchForm">
<a-form-item label="输入地址">
<a-input-search
v-model:value="searchForm.keyword"
placeholder="输入需要查找的地址"
enter-button
@search="handleSearch"
:disabled="!mapLoaded || isSearching"
allow-clear
/>
</a-form-item>
<a-form-item label="搜索结果" v-if="searchList.length > 0">
<a-select
v-model:value="selectedPlaceId"
style="width: 100%"
placeholder="请选择搜索结果"
@change="handlePlaceSelect"
>
<a-select-option
v-for="item in searchList"
:key="item.id"
:value="item.id"
>
{{ item.name }} - {{ item.address }}
</a-select-option>
</a-select>
</a-form-item>
</a-form>
</div>
<!-- 地图容器带加载状态 -->
<div class="map-container" :style="{ height: mapHeight }">
<div v-if="!mapLoaded" class="map-loading">
<a-spin size="large" />
<div class="loading-text">{{ loadingText }}</div>
</div>
<!-- 调试信息 (仅开发环境显示) -->
<!-- <div v-if="debugMode && mapLoaded" class="debug-info">
<div class="debug-header">调试信息</div>
<pre>{{ debugInfo }}</pre>
</div> -->
<div id="amap-container" class="amap-instance" ref="mapContainer"></div>
</div>
<a-form :model="formState" layout="vertical" style="margin-top: 16px">
<a-form-item label="经纬度">
<a-input
v-model:value="formState.lnglat"
readonly
placeholder="点击地图或搜索地点选择位置"
>
<template #suffix>
<a-spin v-if="isGeocoding" size="small" />
</template>
</a-input>
</a-form-item>
<a-form-item label="详细地址">
<a-input
v-model:value="formState.address"
readonly
placeholder="地址信息将自动显示"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick, reactive, computed } from 'vue'
import { message } from 'ant-design-vue'
import AMapLoader from '@amap/amap-jsapi-loader'
import { debounce } from 'lodash-es'
//
interface FormState {
lnglat: string
address: string
location: { lng: number; lat: number } | null
}
interface SearchPlace {
id: string
name: string
address: string
location: { lng: number; lat: number }
}
interface LocationData {
lng: number
lat: number
address: string
name?: string
}
//
const props = defineProps({
open: {
type: Boolean,
default: false
},
title: {
type: String,
default: '选择位置'
},
width: {
type: String,
default: '800px'
},
searchPlaceholder: {
type: String,
default: '请输入地点名称,如:北京西站、上海外滩'
},
defaultCenter: {
type: Array as () => [number, number],
default: () => [120.894522, 31.981269] //
},
defaultZoom: {
type: Number,
default: 12
},
searchCity: {
type: String,
default: '全国'
},
mapHeight: {
type: String,
default: '400px'
},
debugMode: {
type: Boolean,
default: process.env.NODE_ENV === 'development'
}
})
//
const emit = defineEmits(['update:open', 'select', 'error', 'handleGetLng'])
//
const visible = ref(false)
const mapLoaded = ref(false)
const isGeocoding = ref(false)
const isSearching = ref(false)
const loadingText = ref('地图加载中...')
const errorMessage = ref('')
const errorDetails = ref('')
const debugInfo = ref('')
const selectedPlaceId = ref('')
const formState: FormState = reactive({
lnglat: '',
address: '',
location: null
})
const searchForm = reactive({
keyword: ''
})
const searchList = ref<SearchPlace[]>([])
//
const mapContainer = ref<HTMLElement>()
let map: AMap.Map | null = null
let marker: AMap.Marker | null = null
let placeSearch: AMap.PlaceSearch | null = null
let geocoder: AMap.Geocoder | null = null
let infoWindow: AMap.InfoWindow | null = null
let markers: AMap.Marker[] = []
// (300ms)
const debouncedSearch = debounce((keyword: string) => {
performSearch(keyword)
}, 300)
//
const logDebug = (message: string) => {
if (props.debugMode) {
const timestamp = new Date().toLocaleTimeString()
debugInfo.value = `[${timestamp}] ${message}\n${debugInfo.value}`
if (debugInfo.value.length > 5000) {
debugInfo.value = debugInfo.value.substring(0, 5000)
}
}
}
//
const clearError = () => {
errorMessage.value = ''
errorDetails.value = ''
emit('error', null)
}
//
const setError = (message: string, details?: string) => {
errorMessage.value = message
errorDetails.value = details || ''
emit('error', { message, details })
logDebug(`错误: ${message} - ${details}`)
}
// XSS
const escapeHtml = (str: string): string => {
if (!str) return ''
return str.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
//
const cleanupMap = () => {
logDebug('开始清理地图资源')
debouncedSearch.cancel()
isSearching.value = false
clearMarkers()
if (placeSearch) {
try {
placeSearch.clear()
placeSearch.destroy()
placeSearch = null
logDebug('搜索服务已销毁')
} catch (e) {
logDebug(`清理搜索服务时出错: ${e instanceof Error ? e.message : String(e)}`)
}
}
if (infoWindow) {
infoWindow.close()
infoWindow = null
logDebug('信息窗口已关闭')
}
clearMarker()
if (map) {
try {
map.destroy()
map = null
logDebug('地图实例已销毁')
} catch (e) {
logDebug(`清理地图时出错: ${e instanceof Error ? e.message : String(e)}`)
}
}
mapLoaded.value = false
isGeocoding.value = false
clearError()
Object.assign(formState, {
lnglat: '',
address: '',
location: null
})
searchForm.keyword = ''
searchList.value = []
selectedPlaceId.value = ''
}
//
const clearMarker = () => {
if (marker) {
try {
map?.remove(marker)
marker = null
logDebug('主标记已移除')
} catch (e) {
logDebug(`清理主标记时出错: ${e instanceof Error ? e.message : String(e)}`)
}
}
}
//
const clearMarkers = () => {
markers.forEach(marker => {
map?.remove(marker)
})
markers = []
logDebug(`已清除 ${markers.length} 个搜索结果标记`)
}
//
const createMarkerContent = (name: string, index: number) => {
const escapedName = escapeHtml(name)
return `
<div style="
background: #1890ff;
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
white-space: nowrap;
border: 2px solid white;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
">
${index}. ${escapedName}
</div>
`
}
//
const showLocationInfo = (place: SearchPlace, isSearchResult: boolean = false) => {
if (infoWindow) {
infoWindow.close()
}
const escapedName = escapeHtml(place.name)
const escapedAddress = escapeHtml(place.address || '地址不详')
const content = `
<div style="padding: 8px; max-width: 200px;">
<div style="font-weight: bold; margin-bottom: 4px;">${escapedName}</div>
<div style="color: #666; font-size: 12px;">${escapedAddress}</div>
<div style="color: #999; font-size: 12px; margin-top: 4px;">
${isSearchResult ? '搜索结果' : '选中位置'}
</div>
</div>
`
infoWindow = new AMap.InfoWindow({
content: content,
offset: new AMap.Pixel(0, -30),
position: [place.location.lng, place.location.lat]
})
infoWindow.open(map, [place.location.lng, place.location.lat])
logDebug(`显示位置信息: ${place.name}`)
}
//
const handleSearchComplete = (result: AMap.PlaceSearchResult) => {
isSearching.value = false
logDebug(`搜索完成: ${result.info}, 结果数: ${result.poiList?.pois?.length || 0}`)
if (result.info === 'OK' && result.poiList?.pois?.length > 0) {
const pois = result.poiList.pois
//
searchList.value = pois.map((poi: any) => ({
id: poi.id,
name: poi.name,
address: poi.address,
location: {
lng: poi.location.lng,
lat: poi.location.lat
}
}))
//
clearMarkers()
//
pois.forEach((poi: any, index: number) => {
const position = [poi.location.lng, poi.location.lat]
const poiMarker = new AMap.Marker({
position: position,
map: map,
title: poi.name,
content: createMarkerContent(poi.name, index + 1),
offset: new AMap.Pixel(-15, -42)
})
//
poiMarker.on('click', () => {
const place: SearchPlace = {
id: poi.id,
name: poi.name,
address: poi.address,
location: { lng: poi.location.lng, lat: poi.location.lat }
}
updateSelectedLocation([poi.location.lng, poi.location.lat], place)
showLocationInfo(place, true)
selectedPlaceId.value = poi.id
})
markers.push(poiMarker)
})
//
if (pois.length > 0) {
setTimeout(() => {
const firstPoi = pois[0]
const place: SearchPlace = {
id: firstPoi.id,
name: firstPoi.name,
address: firstPoi.address,
location: { lng: firstPoi.location.lng, lat: firstPoi.location.lat }
}
selectedPlaceId.value = firstPoi.id
updateSelectedLocation([firstPoi.location.lng, firstPoi.location.lat], place)
map?.setCenter([firstPoi.location.lng, firstPoi.location.lat])
map?.setZoom(16)
}, 100)
}
map?.setFitView()
} else {
message.info('未找到相关地点')
searchList.value = []
logDebug('未找到相关地点')
}
}
//
const performSearch = (keyword: string) => {
if (!placeSearch) {
setError('搜索服务未初始化', '请等待地图加载完成后再试')
return
}
if (!mapLoaded.value) {
setError('地图未加载完成', '请等待地图加载完成后再试')
return
}
isSearching.value = true
searchList.value = []
selectedPlaceId.value = ''
logDebug(`执行搜索: ${keyword}`)
try {
placeSearch.clear()
clearMarkers()
placeSearch.search(keyword)
} catch (e) {
isSearching.value = false
const errorMsg = e instanceof Error ? e.message : '搜索执行失败'
setError('搜索执行失败', errorMsg)
message.error('搜索过程中发生错误')
}
}
//
const createPlaceSearch = () => {
if (!map) return
logDebug(`创建PlaceSearch实例城市限制: ${props.searchCity}`)
try {
placeSearch = new AMap.PlaceSearch({
map: map,
pageSize: 10,
city: props.searchCity,
citylimit: props.searchCity !== '全国',
autoFitView: true
})
placeSearch.on('complete', handleSearchComplete)
placeSearch.on('error', (error: any) => {
isSearching.value = false
logDebug(`搜索错误: 代码=${error.errorCode}, 信息=${error.errorMsg}, 状态=${error.status}`)
let userMsg = '搜索服务出错,请重试'
let details = `错误代码: ${error.errorCode}, 信息: ${error.errorMsg}`
switch(error.errorCode) {
case 10001:
userMsg = 'API Key错误或已过期'
details += '\n解决方案: 检查并更新高德地图API Key'
break
case 10002:
userMsg = '没有使用该服务的权限'
details += '\n解决方案: 在高德开放平台为当前Key开启POI搜索权限'
break
case 10003:
userMsg = '请求来源未被授权'
details += '\n解决方案: 将当前域名添加到Key的Referer白名单中'
break
case 10004:
userMsg = '服务调用配额不足'
details += '\n解决方案: 检查服务调用配额或升级服务'
break
case 10005:
userMsg = '服务暂不可用'
details += '\n解决方案: 稍后重试或联系高德技术支持'
break
}
setError(userMsg, details)
message.error(userMsg)
})
} catch (e) {
const errorMsg = e instanceof Error ? e.message : '创建搜索服务失败'
setError('搜索服务初始化失败', errorMsg)
logDebug(`创建搜索服务失败: ${errorMsg}`)
}
}
//
const createGeocoder = () => {
try {
geocoder = new AMap.Geocoder({
radius: 1000,
extensions: 'all',
city: '全国'
})
logDebug('地理编码服务已创建')
} catch (e) {
const errorMsg = e instanceof Error ? e.message : '创建地理编码服务失败'
setError('地理编码服务初始化失败', errorMsg)
logDebug(`创建地理编码服务失败: ${errorMsg}`)
}
}
//
const initPlugins = async (): Promise<void> => {
logDebug('初始化地图插件')
return new Promise((resolve, reject) => {
AMap.plugin(['AMap.PlaceSearch', 'AMap.Geocoder', 'AMap.ToolBar', 'AMap.Scale'], () => {
logDebug('插件加载回调触发')
try {
createPlaceSearch()
createGeocoder()
//
map?.addControl(new AMap.ToolBar())
map?.addControl(new AMap.Scale())
logDebug('插件加载成功')
resolve()
} catch (e) {
const errorMsg = e instanceof Error ? e.message : '插件初始化失败'
logDebug(`插件初始化失败: ${errorMsg}`)
reject(new Error(errorMsg))
}
})
})
}
//
const updateSelectedLocation = async (position: [number, number], place?: SearchPlace) => {
const [lng, lat] = position
formState.lnglat = `${lng.toFixed(6)}, ${lat.toFixed(6)}`
formState.location = { lng, lat }
logDebug(`位置更新: ${formState.lnglat}`)
//
updateMarker(position)
//
if (geocoder) {
isGeocoding.value = true
geocoder.getAddress(position, (status, result) => {
isGeocoding.value = false
if (status === 'complete' && result.regeocode) {
formState.address = result.regeocode.formattedAddress
logDebug(`地址反查成功: ${formState.address}`)
// handleGetLng
const locationData: LocationData = {
lng,
lat,
address: formState.address,
name: place?.name
}
emit('handleGetLng', locationData)
} else if (place?.address) {
formState.address = place.address
logDebug(`使用POI地址: ${formState.address}`)
const locationData: LocationData = {
lng,
lat,
address: place.address,
name: place.name
}
emit('handleGetLng', locationData)
} else {
formState.address = '地址不详'
logDebug('地址反查失败')
const locationData: LocationData = {
lng,
lat,
address: '地址不详'
}
emit('handleGetLng', locationData)
}
})
} else if (place?.address) {
formState.address = place.address
logDebug(`使用POI地址: ${formState.address}`)
const locationData: LocationData = {
lng,
lat,
address: place.address,
name: place.name
}
emit('handleGetLng', locationData)
}
}
//
const updateMarker = (position: [number, number]) => {
if (!map) return
clearMarker()
try {
marker = new AMap.Marker({
position: position,
map: map,
draggable: true,
icon: new AMap.Icon({
size: new AMap.Size(36, 36),
image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png',
imageSize: new AMap.Size(36, 36)
})
})
marker.on('dragend', (event) => {
const newPosition = [event.lnglat.lng, event.lnglat.lat] as [number, number]
updateSelectedLocation(newPosition)
})
logDebug(`标记已更新到: ${position.join(', ')}`)
} catch (e) {
const errorMsg = e instanceof Error ? e.message : '创建标记失败'
logDebug(`更新标记失败: ${errorMsg}`)
message.error('更新位置标记失败')
}
}
//
const handleMapClick = (e: any) => {
const lng = e.lnglat.getLng()
const lat = e.lnglat.getLat()
//
clearMarkers()
searchList.value = []
selectedPlaceId.value = ''
updateSelectedLocation([lng, lat])
// 使
if (geocoder) {
geocoder.getAddress([lng, lat], (status, result) => {
if (status === 'complete' && result.regeocode) {
const address = result.regeocode.formattedAddress
//
if (infoWindow) {
infoWindow.close()
}
infoWindow = new AMap.InfoWindow({
content: `
<div style="padding:10px;min-width:200px;">
<div style="font-weight:bold;margin-bottom:5px;">点击位置</div>
<div>${address}</div>
<div style="margin-top:8px;color:#666;">
<div>经度: ${lng.toFixed(6)}</div>
<div>纬度: ${lat.toFixed(6)}</div>
</div>
</div>
`,
offset: new AMap.Pixel(0, -35)
})
infoWindow.open(map, [lng, lat])
}
})
}
}
//
const initMap = async () => {
logDebug('开始初始化地图')
clearError()
if (!mapContainer.value) {
const errorMsg = '地图容器元素未找到'
setError('初始化失败', errorMsg)
console.error(errorMsg)
return
}
try {
loadingText.value = '正在初始化地图...'
//
window._AMapSecurityConfig = {
securityJsCode: 'df197447a4adc77f0cb376a44462272c'
}
// SDK
const AMap = await AMapLoader.load({
key: "38b334d84b1f89aa39d4efae76f0b341",
version: "2.0",
plugins: ['AMap.ToolBar', 'AMap.Scale', 'AMap.Geocoder', 'AMap.PlaceSearch']
})
//
map = new AMap.Map('amap-container', {
zoom: props.defaultZoom,
center: props.defaultCenter,
viewMode: "3D",
resizeEnable: true
})
map.on('complete', () => {
logDebug('地图加载完成')
mapLoaded.value = true
loadingText.value = '地图加载中...'
initPlugins().catch(error => {
console.error('插件初始化失败:', error)
setError('功能初始化失败', error.message)
message.error('地图功能初始化失败: ' + error.message)
})
})
map.on('click', handleMapClick)
//
map.on('error', (error) => {
const errorMsg = error.error || '地图加载错误'
logDebug(`地图错误: ${errorMsg}`)
setError('地图加载错误', errorMsg)
})
} catch (error) {
const errorMsg = error instanceof Error ? error.message : '地图初始化失败'
logDebug(`地图初始化失败: ${errorMsg}`)
setError('地图初始化失败', errorMsg)
message.error('地图初始化失败,请重试')
}
}
//
const handleSearch = (keyword: string) => {
clearError()
if (!keyword?.trim()) {
message.warning('请输入搜索关键词')
debouncedSearch.cancel()
searchList.value = []
selectedPlaceId.value = ''
clearMarkers()
return
}
if (!placeSearch) {
message.warning('搜索服务初始化中,请稍后重试')
logDebug('搜索服务尚未初始化,等待初始化后重试')
initPlugins().then(() => {
handleSearch(keyword)
}).catch(() => {
message.error('搜索功能初始化失败,无法执行搜索')
})
return
}
if (!mapLoaded.value) {
message.warning('地图尚未加载完成,请稍后重试')
return
}
debouncedSearch(keyword.trim())
}
//
const handlePlaceSelect = (placeId: string) => {
const selectedPlace = searchList.value.find(item => item.id === placeId)
if (selectedPlace) {
updateSelectedLocation([selectedPlace.location.lng, selectedPlace.location.lat], selectedPlace)
map?.setCenter([selectedPlace.location.lng, selectedPlace.location.lat])
map?.setZoom(16)
showLocationInfo(selectedPlace, true)
}
}
//
const handleOk = () => {
if (!formState.location) {
message.warning('请先在地图上选择一个位置')
return
}
emit('select', {
lnglat: formState.lnglat,
address: formState.address,
location: formState.location
})
handleCancel()
}
//
const handleCancel = () => {
visible.value = false
emit('update:open', false)
cleanupMap()
}
//
watch(
() => props.open,
(val) => {
logDebug(`弹窗状态变化: ${val}`)
visible.value = val
if (val) {
nextTick(() => {
initMap()
})
} else {
cleanupMap()
}
},
{ immediate: true }
)
watch(visible, (val) => {
if (!val) {
emit('update:open', false)
}
})
//
onUnmounted(() => {
logDebug('组件卸载,清理资源')
cleanupMap()
})
</script>
<style scoped>
:deep(.ant-input-search) {
width: 100%;
}
.search-section {
margin-bottom: 16px;
}
.map-container {
position: relative;
width: 100%;
border: 1px solid #d9d9d9;
border-radius: 6px;
overflow: hidden;
}
.amap-instance {
width: 100%;
height: 100%;
}
.map-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
}
.loading-text {
margin-top: 16px;
color: #666;
font-size: 14px;
}
.debug-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 12px;
padding: 8px;
max-height: 150px;
overflow-y: auto;
z-index: 100;
}
.debug-header {
font-weight: bold;
margin-bottom: 4px;
padding-bottom: 4px;
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
:deep(.ant-alert) {
margin-bottom: 12px;
}
</style>

View File

@ -5,7 +5,7 @@
<a-row :gutter="gutter">
<a-col v-bind="colSpan">
<a-form-item :label="'所在区域'" name="area">
<a-input :placeholder="'请选择区域'" v-model:value="searchFormData.area"></a-input>
<AreaCascader v-model:value="searchFormData.currentNode" @change="onAreaChange" />
</a-form-item>
</a-col>
@ -24,26 +24,30 @@
<a-col v-bind="colSpan">
<a-form-item :label="'等级'" name="level">
<a-select v-model:value="searchFormData.level" @change="handleChange">
<a-select-option value="jack">已结单</a-select-option>
<a-select-option value="lucy">已作废</a-select-option>
<a-select-option v-for="item in dicsStore.dictOptions.level" :key="item.dval"
:value="item.dval">
{{ item.introduction }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<!-- <a-col v-bind="colSpan">
<a-form-item :label="'可用钱包'" name="wallet">
<a-select v-model:value="searchFormData.wallet" @change="handleChange">
<a-select-option value="jack">已结单</a-select-option>
<a-select-option value="lucy">已作废</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-col> -->
<a-col v-bind="colSpan">
<a-form-item :label="'否中标'" name="bidStatus">
<a-form-item :label="'否中标'" name="bidStatus">
<a-select v-model:value="searchFormData.bidStatus" @change="handleChange">
<a-select-option value="jack">已结单</a-select-option>
<a-select-option value="lucy">已作废</a-select-option>
<a-select-option v-for="item in dicsStore.dictOptions.WinTheBidding" :key="item.dval"
:value="item.dval">
{{ item.introduction }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
@ -135,6 +139,8 @@ import EditDialog from './components/EditDialog.vue'
import DeviceManagementModal from './components/AddEquipments.vue'
import { useI18n } from 'vue-i18n'
import storage from '@/utils/storage'
import AreaCascader from '@/components/AreaCascader/index.vue'
import { useDicsStore } from '@/store'
defineOptions({
name: 'menu',
@ -169,6 +175,7 @@ const columns = ref([
const { listData, loading, showLoading, hideLoading, searchFormData, paginationState, resetPagination } =
usePagination()
const { resetForm } = useForm()
const dicsStore = useDicsStore()
getMenuList()

View File

@ -1,15 +1,6 @@
<template>
<a-modal
:width="800"
:open="modal.open"
:title="modal.title"
:confirm-loading="modal.confirmLoading"
:after-close="onAfterClose"
:cancel-text="cancelText"
:ok-text="okText"
@ok="handleOk"
@cancel="handleCancel"
>
<a-modal :width="800" :open="modal.open" :title="modal.title" :confirm-loading="modal.confirmLoading"
:after-close="onAfterClose" :cancel-text="cancelText" :ok-text="okText" @ok="handleOk" @cancel="handleCancel">
<a-form layout="vertical" ref="formRef" :model="formData" :rules="formRules">
<a-card class="mb-4">
<a-row :gutter="16">
@ -64,7 +55,8 @@
</a-col>
<a-col :span="12">
<a-form-item :label="'人员编制(个)'" name="staffCount">
<a-input-number v-model:value="formData.staffCount" min="0" style="width: 100%"></a-input-number>
<a-input-number v-model:value="formData.staffCount" min="0"
style="width: 100%"></a-input-number>
</a-form-item>
</a-col>
</a-row>
@ -75,12 +67,8 @@
<a-row :gutter="16">
<a-col :span="24">
<a-form-item :label="'服务中心地址'" name="address">
<a-cascader
v-model:value="formData.address"
:options="addressOptions"
placeholder="请选择省/市/区"
style="width: 100%"
></a-cascader>
<a-cascader v-model:value="formData.address" :options="addressOptions"
placeholder="请选择省/市/区" style="width: 100%"></a-cascader>
</a-form-item>
</a-col>
<a-col :span="24">
@ -92,11 +80,7 @@
<a-form-item :label="'地图定位地址'" name="location">
<a-row :gutter="8">
<a-col :span="18">
<a-input
v-model:value="formData.location"
placeholder="请选择地图位置"
readonly
></a-input>
<a-input v-model:value="formData.location" placeholder="请选择地图位置" readonly></a-input>
</a-col>
<a-col :span="6">
<a-button type="primary" block @click="openMapSelector">选择地图位置</a-button>
@ -112,30 +96,20 @@
<a-row :gutter="16">
<a-col :span="12">
<a-form-item :label="'建成时间'" name="buildTime">
<a-date-picker
v-model:value="formData.buildTime"
format="YYYY-MM-DD"
placeholder="请选择建成时间"
></a-date-picker>
<a-date-picker v-model:value="formData.buildTime" format="YYYY-MM-DD"
placeholder="请选择建成时间"></a-date-picker>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="'建筑面积(平方米)'" name="area">
<a-input-number
v-model:value="formData.area"
min="0"
style="width: 100%"
placeholder="请输入建筑面积"
></a-input-number>
<a-input-number v-model:value="formData.area" min="0" style="width: 100%"
placeholder="请输入建筑面积"></a-input-number>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item :label="'服务介绍'" name="description">
<a-textarea
v-model:value="formData.description"
rows="4"
placeholder="请输入服务介绍"
></a-textarea>
<a-textarea v-model:value="formData.description" rows="4"
placeholder="请输入服务介绍"></a-textarea>
</a-form-item>
</a-col>
</a-row>
@ -146,16 +120,10 @@
<a-row :gutter="16">
<a-col :span="12">
<a-form-item :label="'开始营业时间'" name="openTime">
<a-time-picker
v-model:value="formData.openTime"
format="HH:mm"
placeholder="请选择开始营业时间"
></a-time-picker>
<a-time-picker
v-model:value="formData.closeTime"
format="HH:mm"
placeholder="请选择结束营业时间"
></a-time-picker>
<a-time-picker v-model:value="formData.openTime" format="HH:mm"
placeholder="请选择开始营业时间"></a-time-picker>
<a-time-picker v-model:value="formData.closeTime" format="HH:mm"
placeholder="请选择结束营业时间"></a-time-picker>
</a-form-item>
</a-col>
@ -183,14 +151,11 @@
</span>
</template>
<div class="service-tags">
<a-tag
v-for="service in allServices"
:key="service.value"
<a-tag v-for="service in allServices" :key="service.value"
:checked="formData.services?.includes(service.value)"
@click="handleServiceClick(service.value)"
:class="{ 'ant-tag-checkable-checked': formData.services?.includes(service.value) }"
class="service-tag"
>
class="service-tag">
{{ service.label }}
</a-tag>
</div>
@ -228,7 +193,8 @@
</a-col>
<a-col :span="12">
<a-form-item :label="'床位数'" name="bedCount" :required="true">
<a-input-number v-model:value="formData.bedCount" placeholder="请输入床位数" min="0" style="width: 100%"></a-input-number>
<a-input-number v-model:value="formData.bedCount" placeholder="请输入床位数" min="0"
style="width: 100%"></a-input-number>
</a-form-item>
</a-col>
<a-col :span="12">
@ -244,11 +210,8 @@
<a-row :gutter="16">
<a-col :span="12">
<a-form-item :label="'资质附件'" name="qualificationFiles">
<a-upload
list-type="picture-card"
v-model:file-list="formData.qualificationFiles"
:before-upload="beforeUpload"
>
<a-upload list-type="picture-card" v-model:file-list="formData.qualificationFiles"
:before-upload="beforeUpload">
<div v-if="formData.qualificationFiles.length < 5">
<plus-outlined />
<div class="ant-upload-text">上传</div>
@ -258,11 +221,8 @@
</a-col>
<a-col :span="12">
<a-form-item :label="'站点图片'" name="siteImages">
<a-upload
list-type="picture-card"
v-model:file-list="formData.siteImages"
:before-upload="beforeUpload"
>
<a-upload list-type="picture-card" v-model:file-list="formData.siteImages"
:before-upload="beforeUpload">
<div v-if="formData.siteImages.length < 5">
<plus-outlined />
<div class="ant-upload-text">上传</div>
@ -274,6 +234,17 @@
</a-card>
</a-form>
<MapPickerModal
:open="showMapPicker"
@update:open="mapVisible = $event"
@handleGetLng="handleLocationChange"
@select="handleLocationSelect" />
<div v-if="selectedLocation">
<p>经纬度: {{ selectedLocation.lnglat }}</p>
<p>地址: {{ selectedLocation.address }}</p>
<p>名称: {{ selectedLocation.placeName }}</p>
</div>
</a-modal>
</template>
@ -288,6 +259,7 @@ import { config } from '@/config'
import { useI18n } from 'vue-i18n'
import storage from '@/utils/storage'
import { message } from 'ant-design-vue'
import MapPickerModal from '@/components/Map/index.vue'
const emit = defineEmits(['ok'])
const { t } = useI18n() // t
const { modal, showModal, hideModal, showLoading, hideLoading } = useModal()
@ -404,6 +376,20 @@ const reqType = ['GET', 'POST', 'PUT', 'PATCH', 'HEAD', 'DELETE']
const cancelText = ref(t('button.cancel'))
const okText = ref(t('button.confirm'))
const platform = ref('')
const showMapPicker = ref(false)
const location = ref('')
const handleLocationSelect = (lngLat) => {
console.log('确认选择:', lngLat)
formData.value.location = lngLat.address
showMapPicker.value = false
}
//
const handleLocationChange = (location) => {
console.log('位置变化:', location)
// UI
}
// -
const addressOptions = ref([
@ -615,9 +601,7 @@ function handleServiceClick(value) {
* 打开地图选择器
*/
function openMapSelector() {
//
// 使
formData.value.location = '北京市海淀区中关村大街1号 (经纬度: 39.9847, 116.3055)'
showMapPicker.value = true
}
/**

View File

@ -11,16 +11,18 @@
<a-col v-bind="colSpan">
<a-form-item :label="'站点类型'" name="type">
<a-select v-model:value="searchFormData.type" @change="handleChange">
<a-select-option value="jack">已结单</a-select-option>
<a-select-option value="lucy">已作废</a-select-option>
<a-select v-model:value="searchFormData.type" placeholder="请选择站点类型" allow-clear>
<a-select-option v-for="item in dicsStore.dictOptions.Station_Type" :key="item.dval"
:value="item.dval">
{{ item.introduction }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item :label="'所在区域'" name="area">
<a-input :placeholder="'请选择区域'" v-model:value="searchFormData.area"></a-input>
<AreaCascader v-model:value="searchFormData.area" @change="onAreaChange" />
</a-form-item>
</a-col>
@ -66,18 +68,18 @@
导入
</a-button>
<a-button v-action="'add'" type="primary" @click="$refs.editDialogRef.handleCreate()">
<a-button v-action="'add'" type="primary" @click="showImportRecordModal = true">
<template #icon>
<UnorderedListOutlined />
</template>
导入记录
</a-button>
<a-button v-action="'add'" type="primary" @click="$refs.editDialogRef.handleCreate()">
<a-button v-action="'add'" type="primary" @click="handleExport">
导出
</a-button>
<a-button v-action="'add'" type="primary" @click="$refs.editDialogRef.handleCreate()">
<a-button v-action="'add'" type="primary" @click="handleShowExportRecords">
<template #icon>
<UnorderedListOutlined />
</template>
@ -145,7 +147,28 @@
<import-modal
v-model:visible="showImportModal"
@download="handleDownloadTemplate"
@upload="handleFileUpload"
@upload="handleFileUpload"/>
<!-- 导入记录弹框组件 -->
<ImportRecordsModal
v-model:visible="showImportRecordModal"
title="导入记录"
:fetchData="fetchImportRecords"
@close="handleImportRecordClose"
@view-details="handleViewImportDetails"
@download-result="handleDownloadImportResult"
@retry-import="handleRetryImport"
/>
<!-- 导出记录弹框组件 -->
<ExportRecordsModal
v-model="showExportRecordModal"
title="导出记录"
:fetchData="fetchExportRecords"
@close="handleExportRecordClose"
@view-details="handleViewExportDetails"
@download-result="handleDownloadExportResult"
@retry-export="handleRetryExport"
/>
</template>
@ -160,9 +183,14 @@ import { usePagination, useForm } from '@/hooks'
import { formatUtcDateTime } from '@/utils/util'
import EditDialog from './components/EditDialog.vue'
//
import ImportModal from '@/components/Import/index.vue' //
import ImportModal from '@/components/Import/index.vue'
//
import ImportRecordsModal from '@/components/ImportRecord/index.vue'
import ExportRecordsModal from '@/components/ExportRecord/index.vue'
import { useI18n } from 'vue-i18n'
import storage from '@/utils/storage'
import AreaCascader from '@/components/AreaCascader/index.vue'
defineOptions({
name: 'menu',
@ -172,6 +200,10 @@ const { t } = useI18n() // 解构出t方法
// /
const showImportModal = ref(false)
// /
const showImportRecordModal = ref(false)
const showExportRecordModal = ref(false)
const columns = ref([
{ title: '工单号', dataIndex: 'name', key: 'name', fixed: true, width: 280 },
{ title: '服务对象', dataIndex: 'code', key: 'code', width: 240 },
@ -192,6 +224,8 @@ const { listData, loading, showLoading, hideLoading, searchFormData, paginationS
usePagination()
const { resetForm } = useForm()
const editDialogRef = ref()
import { useDicsStore } from '@/store'
const dicsStore = useDicsStore()
getMenuList()
@ -275,7 +309,31 @@ async function onOk() {
await getMenuList()
}
/**
* 导出数据
*/
function handleExport() {
Modal.confirm({
title: '确认导出',
content: '是否导出当前查询条件下的所有服务站点数据?',
onOk: () => {
// API
apis.exportServiceSite(searchFormData.value).then(() => {
message.success('导出任务已提交,请在导出记录中查看进度');
showExportRecordModal.value = true;
}).catch(() => {
message.error('导出任务提交失败');
});
}
});
}
/**
* 显示导出记录
*/
function handleShowExportRecords() {
showExportRecordModal.value = true;
}
/**
* 处理下载模板
@ -323,6 +381,211 @@ function handleChange() {
//
}
//
const fetchImportRecords = async (params) => {
// API - API
await new Promise(resolve => setTimeout(resolve, 800));
//
const mockData = [
{
taskId: 'BATCH_20231201_001',
status: 'completed',
alias: '服务站点数据导入',
totalCount: 1500,
successCount: 1480,
failCount: 20,
createTime: '2023-12-01 14:30:25',
createByName: '张三',
progress: 100
},
{
taskId: 'BATCH_20231201_002',
status: 'failed',
alias: '服务站点信息导入',
totalCount: 800,
successCount: 750,
failCount: 50,
createTime: '2023-12-01 15:45:10',
createByName: '李四',
progress: 93.75
},
{
taskId: 'BATCH_20231201_003',
status: 'processing',
alias: '服务站点数据导入',
totalCount: 3200,
successCount: 1200,
failCount: 0,
createTime: '2023-12-01 16:20:30',
createByName: '王五',
progress: 37.5
},
{
taskId: 'BATCH_20231201_004',
status: 'pending',
alias: '服务站点批量导入',
totalCount: 500,
successCount: 0,
failCount: 0,
createTime: '2023-12-01 17:10:15',
createByName: '赵六',
progress: 0
},
{
taskId: 'BATCH_20231201_005',
status: 'completed',
alias: '服务站点信息导入',
totalCount: 2100,
successCount: 2095,
failCount: 5,
createTime: '2023-12-01 18:05:40',
createByName: '钱七',
progress: 100
}
];
//
const start = (params.page - 1) * params.pageSize;
const end = start + params.pageSize;
const list = mockData.slice(start, end);
return {
list,
total: mockData.length
};
};
//
const fetchExportRecords = async (params) => {
// API - API
await new Promise(resolve => setTimeout(resolve, 800));
//
const mockData = [
{
taskId: 'EXPORT_20231201_001',
status: 'completed',
alias: '服务站点数据导出',
totalCount: 1500,
successCount: 1500,
failCount: 0,
createTime: '2023-12-01 14:30:25',
createByName: '张三',
progress: 100
},
{
taskId: 'EXPORT_20231201_002',
status: 'completed',
alias: '服务站点信息导出',
totalCount: 800,
successCount: 800,
failCount: 0,
createTime: '2023-12-01 15:45:10',
createByName: '李四',
progress: 100
},
{
taskId: 'EXPORT_20231201_003',
status: 'processing',
alias: '服务站点数据导出',
totalCount: 3200,
successCount: 2500,
failCount: 0,
createTime: '2023-12-01 16:20:30',
createByName: '王五',
progress: 78.125
},
{
taskId: 'EXPORT_20231201_004',
status: 'pending',
alias: '服务站点批量导出',
totalCount: 500,
successCount: 0,
failCount: 0,
createTime: '2023-12-01 17:10:15',
createByName: '赵六',
progress: 0
},
{
taskId: 'EXPORT_20231201_005',
status: 'completed',
alias: '服务站点信息导出',
totalCount: 2100,
successCount: 2100,
failCount: 0,
createTime: '2023-12-01 18:05:40',
createByName: '钱七',
progress: 100
}
];
//
const start = (params.page - 1) * params.pageSize;
const end = start + params.pageSize;
const list = mockData.slice(start, end);
return {
list,
total: mockData.length
};
};
//
const handleImportRecordClose = () => {
console.log('导入记录弹框已关闭');
};
//
const handleViewImportDetails = (record) => {
message.info(`查看批次 ${record.taskId} 的详情`);
console.log('查看详情:', record);
};
//
const handleDownloadImportResult = (record) => {
message.success(`正在下载批次 ${record.taskId} 的结果文件`);
console.log('下载结果:', record);
};
//
const handleRetryImport = (record) => {
message.info(`正在重新导入批次 ${record.taskId}`);
console.log('重新导入:', record);
};
//
const handleExportRecordClose = () => {
console.log('导出记录弹框已关闭');
};
//
const handleViewExportDetails = (record) => {
message.info(`查看导出批次 ${record.taskId} 的详情`);
console.log('查看导出详情:', record);
};
//
const handleDownloadExportResult = (record) => {
message.success(`正在下载导出批次 ${record.taskId} 的结果文件`);
// API
apis.downloadExportResult(record.taskId).then(() => {
message.success('文件下载成功');
}).catch(() => {
message.error('文件下载失败');
});
};
//
const handleRetryExport = (record) => {
message.info(`正在重新导出批次 ${record.taskId}`);
// API
apis.retryExport(record.taskId).then(() => {
message.success('重新导出任务已提交');
}).catch(() => {
message.error('重新导出失败');
});
};
</script>
<style lang="less" scoped>