generated from Leo_Ding/web-template
Merge branch 'master' of https://gitlab.guxuan.icu/Leo_Ding/hahaPension_admin
This commit is contained in:
commit
def6dbb546
11
package-lock.json
generated
11
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
296
src/components/ExportRecord/index.vue
Normal file
296
src/components/ExportRecord/index.vue
Normal 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>
|
||||
310
src/components/ImportRecord/index.vue
Normal file
310
src/components/ImportRecord/index.vue
Normal 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>
|
||||
|
||||
|
||||
|
||||
926
src/components/Map/index.vue
Normal file
926
src/components/Map/index.vue
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
// 清理地图资源
|
||||
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>
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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">
|
||||
@ -43,12 +34,12 @@
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="'负责人姓名'" name="manager" :required="true">
|
||||
<a-input v-model:value="formData.manager"></a-input>
|
||||
<a-input v-model:value="formData.manager"></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="'联系电话'" name="phone" :required="true">
|
||||
<a-input v-model:value="formData.phone" placeholder="请输入联系电话"></a-input>
|
||||
<a-input v-model:value="formData.phone" placeholder="请输入联系电话"></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
@ -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,19 +120,13 @@
|
||||
<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>
|
||||
|
||||
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="'营业状态'" name="businessStatus">
|
||||
<a-select v-model:value="formData.businessStatus">
|
||||
@ -168,7 +136,7 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
|
||||
<a-col :span="24">
|
||||
<a-form-item name="services">
|
||||
<template #label>
|
||||
@ -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="{ 'ant-tag-checkable-checked': formData.services?.includes(service.value) }"
|
||||
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>
|
||||
@ -272,8 +232,19 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
</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()
|
||||
@ -344,14 +316,14 @@ formRules.value = {
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入联系电话' },
|
||||
{
|
||||
{
|
||||
pattern: /^1[3-9]\d{9}$/,
|
||||
message: '请输入正确的联系电话'
|
||||
message: '请输入正确的联系电话'
|
||||
}
|
||||
],
|
||||
bedCount: [
|
||||
{ required: true, message: '请输入床位数' },
|
||||
{
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
if (typeof value === 'number' && value >= 0) {
|
||||
@ -366,8 +338,8 @@ formRules.value = {
|
||||
address: { required: true, message: '请选择省市区' },
|
||||
detailAddress: { required: true, message: '请输入详细地址' },
|
||||
buildTime: { required: true, message: '请选择建成时间' },
|
||||
area: {
|
||||
required: true,
|
||||
area: {
|
||||
required: true,
|
||||
validator: (_, value) => {
|
||||
if (value && value > 0) {
|
||||
return Promise.resolve();
|
||||
@ -378,8 +350,8 @@ formRules.value = {
|
||||
openTime: { required: true, message: '请选择开始营业时间' },
|
||||
closeTime: { required: true, message: '请选择结束营业时间' },
|
||||
businessStatus: { required: true, message: '请选择营业状态' },
|
||||
services: {
|
||||
required: true,
|
||||
services: {
|
||||
required: true,
|
||||
validator: (_, value) => {
|
||||
if (value && value.length > 0) {
|
||||
return Promise.resolve();
|
||||
@ -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([
|
||||
@ -514,12 +500,12 @@ function handleOk() {
|
||||
}
|
||||
params.sequence = Number.parseInt(params.sequence) || 0
|
||||
params.properties = JSON.stringify(params.properties)
|
||||
|
||||
|
||||
// 处理地址数据
|
||||
params.province = params.address[0]
|
||||
params.city = params.address[1]
|
||||
params.district = params.address[2]
|
||||
|
||||
|
||||
let result = null
|
||||
switch (modal.value.type) {
|
||||
case 'create':
|
||||
@ -600,7 +586,7 @@ function handleServiceClick(value) {
|
||||
if (!formData.value.services) {
|
||||
formData.value.services = []
|
||||
}
|
||||
|
||||
|
||||
const index = formData.value.services.indexOf(value)
|
||||
if (index > -1) {
|
||||
// 如果已选中,则取消选中
|
||||
@ -615,9 +601,7 @@ function handleServiceClick(value) {
|
||||
* 打开地图选择器
|
||||
*/
|
||||
function openMapSelector() {
|
||||
// 实际项目中可以调用地图组件或打开地图选择弹窗
|
||||
// 这里使用模拟数据
|
||||
formData.value.location = '北京市海淀区中关村大街1号 (经纬度: 39.9847, 116.3055)'
|
||||
showMapPicker.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
@ -655,18 +639,18 @@ defineExpose({
|
||||
// 服务标签选中样式
|
||||
.service-tag {
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
&.ant-tag-checkable-checked {
|
||||
background-color: #1890ff; // 蓝色背景
|
||||
color: #fff; // 白色文字
|
||||
border-color: #1890ff;
|
||||
|
||||
|
||||
&:hover {
|
||||
background-color: #40a9ff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&:not(.ant-tag-checkable-checked) {
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
|
||||
@ -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,8 +147,29 @@
|
||||
<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>
|
||||
|
||||
<script setup>
|
||||
@ -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>
|
||||
@ -337,4 +600,4 @@ function handleChange() {
|
||||
background-color: #e8e8e8; // 灰色横线
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user