Files
tms-erp-web/src/views/base/common-address.vue
2026-07-27 17:19:36 +08:00

880 lines
28 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<basic-container class="common-address-page">
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
v-model="form"
ref="crud"
:permission="permissionList"
:before-open="beforeOpen"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button
type="primary"
icon="el-icon-plus"
plain
v-if="hasPermission('common_address_add')"
@click="$refs.crud.rowAdd()"
>新增
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="hasPermission('common_address_export')"
@click="handleExport"
>批量导出
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="hasPermission('common_address_delete')"
@click="handleDelete"
>批量删除
</el-button>
<el-switch
v-if="canViewAllDept"
v-model="allDept"
class="common-address-page__scope"
active-text="全部组织"
inactive-text="当前组织"
:active-value="1"
:inactive-value="0"
@change="handleScopeChange"
/>
</template>
<template #siteCodeDisplay="{ row }">
<span>{{ row.addressType === '常规地址' ? '/' : row.siteCodeDisplay || row.siteCode }}</span>
</template>
<template #menu="{ row, index }">
<el-button
type="primary"
text
icon="el-icon-view"
v-if="hasPermission('common_address_view')"
@click="$refs.crud.rowView(row, index)"
>
查看
</el-button>
<el-button
type="primary"
text
icon="el-icon-edit"
v-if="hasPermission('common_address_edit') && !row.readonly"
@click="$refs.crud.rowEdit(row, index)"
>
编辑
</el-button>
<el-button
type="primary"
text
icon="el-icon-delete"
v-if="hasPermission('common_address_delete') && !row.readonly"
@click="rowDel(row)"
>
删除
</el-button>
</template>
<template #sourceId-form>
<el-select
v-model="form.sourceId"
filterable
remote
clearable
:remote-method="loadSourceOptions"
:loading="sourceLoading"
:disabled="dialogReadonly || form.addressType === '常规地址'"
placeholder="请选择来源主数据"
style="width: 100%"
@change="handleSourceChange"
>
<el-option
v-for="item in sourceOptions"
:key="item.sourceId"
:label="`${item.addressName}${item.siteCode}`"
:value="item.sourceId"
/>
</el-select>
</template>
<template #detailAddress-form>
<div class="common-address-page__address">
<el-input
v-model="form.detailAddress"
type="textarea"
:rows="3"
maxlength="255"
show-word-limit
:disabled="dialogReadonly || form.addressType !== '常规地址'"
/>
<el-button
icon="el-icon-location"
plain
:disabled="dialogReadonly || form.addressType !== '常规地址'"
@click="handleMapPick"
>
地图选点
</el-button>
</div>
</template>
<template #longitude-form>
<el-input
v-model="form.longitude"
clearable
placeholder="请输入经度"
:disabled="dialogReadonly || form.addressType !== '常规地址'"
@input="handleCoordinateInput('longitude', $event)"
/>
</template>
<template #latitude-form>
<el-input
v-model="form.latitude"
clearable
placeholder="请输入纬度"
:disabled="dialogReadonly || form.addressType !== '常规地址'"
@input="handleCoordinateInput('latitude', $event)"
/>
</template>
</avue-crud>
<el-dialog
title="地图选点"
append-to-body
v-model="mapBox"
width="920px"
class="common-address-page__map-dialog"
@opened="initTiandituMap"
>
<div class="common-address-page__map-toolbar">
<el-input
v-model="mapKeyword"
clearable
placeholder="输入地址关键词"
@keyup.enter="searchMapKeyword"
/>
<el-button type="primary" icon="el-icon-search" :loading="mapLoading" @click="searchMapKeyword">
搜索
</el-button>
</div>
<div ref="tiandituMap" class="common-address-page__map"></div>
<div class="common-address-page__map-info">
<span>{{ mapStatus }}</span>
<span v-if="mapSelected.longitude">
经度{{ mapSelected.longitude }}纬度{{ mapSelected.latitude }}
</span>
</div>
<template #footer>
<el-button @click="mapBox = false">取消</el-button>
<el-button type="primary" :disabled="!mapSelected.longitude" @click="confirmMapPick">
确定
</el-button>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { getDetail, getList, getSourceOptions, remove, submit } from '@/api/base/common-address';
import { getDeptTree } from '@/api/system/dept';
import { exportBlob } from '@/api/common';
import { createOption } from '@/option/base/common-address';
import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
import { getToken } from '@/utils/auth';
import { isCoordinateInput, normalizeCoordinateInput } from '@/utils/coordinate';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const TIANDITU_KEY = '71f234f251d4cb1feeb3c293a0f21709';
let tiandituLoader;
export default {
data() {
const validateLongitude = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback(new Error('经度不能为空'));
} else if (!isCoordinateInput(value)) {
callback(new Error('经度最多保留6位小数'));
} else if (Number(value) < -180 || Number(value) > 180) {
callback(new Error('经度范围为 -180 到 180'));
} else {
callback();
}
};
const validateLatitude = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback(new Error('纬度不能为空'));
} else if (!isCoordinateInput(value)) {
callback(new Error('纬度最多保留6位小数'));
} else if (Number(value) < -90 || Number(value) > 90) {
callback(new Error('纬度范围为 -90 到 90'));
} else {
callback();
}
};
return {
form: {},
query: {},
loading: true,
data: [],
allDept: 0,
dialogReadonly: false,
sourceLoading: false,
sourceOptions: [],
deptOptions: [],
mapBox: false,
mapLoading: false,
mapKeyword: '',
mapStatus: '可搜索地址或点击地图选点',
mapSelected: {},
tiandituMap: null,
tiandituMarker: null,
tiandituGeocoder: null,
tiandituClickHandler: null,
option: createOption({ validateLongitude, validateLatitude }),
page: {
pageSize: 10,
pageSizes: [10, 20, 50, 100],
currentPage: 1,
total: 0,
},
selectionList: [],
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
permissionList() {
return {
addBtn: this.hasPermission('common_address_add'),
};
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority) ? authority.includes('admin') : String(authority || '').includes('admin');
},
canViewAllDept() {
return this.isAdmin;
},
ids() {
return this.selectionList.map(item => item.id).join(',');
},
},
created() {
this.initDeptTree();
},
methods: {
hasPermission(code) {
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId).then(res => {
this.deptOptions = this.flattenDept(res.data.data || []);
const deptColumn = this.findColumn(this.option.column, 'deptId');
deptColumn.dicData = this.deptOptions;
});
},
flattenDept(tree, level = 0) {
const result = [];
tree.forEach(item => {
result.push({
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
value: item.id,
rawLabel: item.title || item.deptName || item.name,
});
if (item.children && item.children.length) {
result.push(...this.flattenDept(item.children, level + 1));
}
});
return result;
},
handleScopeChange() {
if (!this.canViewAllDept) {
this.allDept = 0;
}
if (!this.allDept) {
this.query.deptId = undefined;
}
this.page.currentPage = 1;
this.onLoad(this.page);
},
loadSourceOptions(keyword = '') {
if (!this.form.addressType || this.form.addressType === '常规地址') {
this.sourceOptions = [];
return;
}
this.sourceLoading = true;
getSourceOptions(this.form.addressType, keyword)
.then(res => {
this.sourceOptions = res.data.data || [];
})
.finally(() => {
this.sourceLoading = false;
});
},
handleSourceChange(value) {
const source = this.sourceOptions.find(item => item.sourceId === value);
if (!source) {
this.clearTypeRelatedFields('');
return;
}
this.form.addressName = source.addressName;
this.form.siteCode = source.siteCode;
this.form.siteCodeDisplay = source.siteCode;
this.form.detailAddress = source.detailAddress;
this.form.regionCode = source.regionCode;
this.form.regionName = source.regionName;
this.form.longitude = source.longitude;
this.form.latitude = source.latitude;
},
handleCoordinateInput(prop, value) {
this.form[prop] = normalizeCoordinateInput(value);
},
clearTypeRelatedFields(siteCode = '') {
this.form.sourceId = undefined;
this.form.siteCode = siteCode;
this.form.siteCodeDisplay = siteCode;
this.form.detailAddress = '';
this.form.regionCode = '';
this.form.regionName = '';
this.form.longitude = '';
this.form.latitude = '';
},
handleAddressTypeChange(value, reset = true) {
const sourceColumn = this.findColumn(this.option.column, 'sourceId');
sourceColumn.rules = value === '常规地址' ? [] : [{ required: true, message: '请选择来源主数据', trigger: 'change' }];
const sourceDisabled = value === '常规地址';
this.findColumn(this.option.column, 'regionName').disabled = !sourceDisabled;
this.findColumn(this.option.column, 'longitude').disabled = !sourceDisabled;
this.findColumn(this.option.column, 'latitude').disabled = !sourceDisabled;
if (value === '常规地址') {
if (reset) {
this.clearTypeRelatedFields('/');
this.sourceOptions = [];
}
return;
}
if (reset) {
this.clearTypeRelatedFields('');
}
this.loadSourceOptions();
},
normalizeRow(row) {
const submitRow = {
...row,
addressName: String(row.addressName || '').trim(),
addressType: String(row.addressType || '').trim(),
detailAddress: String(row.detailAddress || '').trim(),
regionCode: String(row.regionCode || '').trim(),
regionName: String(row.regionName || '').trim(),
longitude: normalizeCoordinateInput(row.longitude),
latitude: normalizeCoordinateInput(row.latitude),
contactName: String(row.contactName || '').trim(),
contactPhone: String(row.contactPhone || '').trim(),
remark: String(row.remark || '').trim(),
};
if (submitRow.addressType === '常规地址') {
submitRow.siteCode = '/';
submitRow.sourceId = undefined;
}
return submitRow;
},
validateRequiredFields(row) {
if (!row.addressType) {
this.$message.warning('请选择类型');
return false;
}
if (!row.addressName) {
this.$message.warning('请输入地址名称');
return false;
}
if (!row.detailAddress) {
this.$message.warning('请输入详细地址');
return false;
}
if (!row.regionName) {
this.$message.warning('请输入行政区划');
return false;
}
if (row.longitude === undefined || row.longitude === null || row.longitude === '') {
this.$message.warning('经度不能为空');
return false;
}
if (row.latitude === undefined || row.latitude === null || row.latitude === '') {
this.$message.warning('纬度不能为空');
return false;
}
if (row.addressType !== '常规地址' && !row.sourceId) {
this.$message.warning('请选择来源主数据');
return false;
}
if (row.remark && row.remark.length > 200) {
this.$message.warning('备注不能超过200个字');
return false;
}
if (row.longitude !== undefined && row.longitude !== null && row.longitude !== '') {
if (!isCoordinateInput(row.longitude)) {
this.$message.warning('经度最多保留6位小数');
return false;
}
if (Number(row.longitude) < -180 || Number(row.longitude) > 180) {
this.$message.warning('经度范围为 -180 到 180');
return false;
}
row.longitude = Number(row.longitude).toFixed(6);
}
if (row.latitude !== undefined && row.latitude !== null && row.latitude !== '') {
if (!isCoordinateInput(row.latitude)) {
this.$message.warning('纬度最多保留6位小数');
return false;
}
if (Number(row.latitude) < -90 || Number(row.latitude) > 90) {
this.$message.warning('纬度范围为 -90 到 90');
return false;
}
row.latitude = Number(row.latitude).toFixed(6);
}
return true;
},
stopSubmitLoading(loading) {
if (typeof loading === 'function') {
loading();
}
},
rowSave(row, done, loading) {
const submitRow = this.normalizeRow(row);
if (!this.validateRequiredFields(submitRow)) {
this.stopSubmitLoading(loading);
return;
}
submit(submitRow).then(
() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdate(row, index, done, loading) {
const submitRow = this.normalizeRow(row);
if (!this.validateRequiredFields(submitRow)) {
this.stopSubmitLoading(loading);
return;
}
submit(submitRow).then(
() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => remove(row.id))
.then(res => {
this.afterRemove(res.data.data);
});
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
const editableSelection = this.selectionList.filter(item => !item.readonly);
if (!editableSelection.length) {
this.$message.warning('所选数据均为只读组织数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => remove(editableSelection.map(item => item.id).join(',')))
.then(res => {
this.afterRemove(res.data.data);
});
},
afterRemove(result = {}) {
this.onLoad(this.page);
const successCount = result.successCount || 0;
const skippedCount = result.skippedCount || 0;
if (skippedCount) {
this.$message.warning(`成功删除${successCount}条,${skippedCount}条因业务引用已跳过`);
} else {
this.$message({ type: 'success', message: '删除成功!' });
}
this.$refs.crud.toggleSelection();
},
beforeOpen(done, type) {
this.dialogReadonly = type === 'view';
const addressTypeColumn = this.findColumn(this.option.column, 'addressType');
addressTypeColumn.change = ({ value }) => this.handleAddressTypeChange(value);
if (type === 'add') {
this.form.addressType = this.form.addressType || '常规地址';
this.form.siteCode = '/';
this.form.siteCodeDisplay = '/';
this.handleAddressTypeChange(this.form.addressType);
done();
return;
}
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
this.handleAddressTypeChange(this.form.addressType, false);
if (this.form.addressType !== '常规地址' && this.form.sourceId) {
this.sourceOptions = [
{
sourceId: this.form.sourceId,
addressName: this.form.addressName,
siteCode: this.form.siteCode,
detailAddress: this.form.detailAddress,
regionCode: this.form.regionCode,
regionName: this.form.regionName,
longitude: this.form.longitude,
latitude: this.form.latitude,
},
];
}
done();
});
return;
}
done();
},
searchReset() {
this.query = {};
this.page.currentPage = 1;
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud.toggleSelection();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
this.loading = true;
getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: this.allDept })
.then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.selectionClear();
})
.finally(() => {
this.loading = false;
});
},
handleExport() {
this.$confirm('是否导出常用地址?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob('/blade-transport/common-address/export-common-address', this.buildExportParams())
.then(res => {
downloadXls(res.data, `常用地址${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
})
.finally(() => {
NProgress.done();
});
});
},
buildExportParams() {
return {
...this.query,
allDept: this.allDept,
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
},
handleMapPick() {
if (this.dialogReadonly || this.form.addressType !== '常规地址') {
return;
}
this.mapKeyword = this.form.detailAddress || this.form.regionName || '';
this.mapSelected = this.buildMapSelection({
lng: this.form.longitude,
lat: this.form.latitude,
address: this.form.detailAddress,
regionName: this.form.regionName,
regionCode: this.form.regionCode,
});
this.mapStatus = this.mapSelected.longitude ? '已加载当前经纬度,可重新选点' : '可搜索地址或点击地图选点';
this.mapBox = true;
},
loadTianditu() {
if (window.T && window.T.Map) {
return Promise.resolve();
}
if (!tiandituLoader) {
tiandituLoader = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = `https://api.tianditu.gov.cn/api?v=4.0&tk=${TIANDITU_KEY}`;
script.async = true;
script.onload = resolve;
script.onerror = () => reject(new Error('天地图组件加载失败'));
document.body.appendChild(script);
});
}
return tiandituLoader;
},
initTiandituMap() {
this.loadTianditu()
.then(() => {
this.$nextTick(() => {
if (!this.tiandituMap) {
const center = this.toLngLat(
this.mapSelected.longitude || 116.40769,
this.mapSelected.latitude || 39.89945
);
this.tiandituMap = new window.T.Map(this.$refs.tiandituMap);
this.tiandituMap.centerAndZoom(center, this.mapSelected.longitude ? 14 : 11);
this.tiandituMap.addControl(new window.T.Control.Zoom());
this.tiandituClickHandler = event => this.pickMapPoint(event.lnglat);
this.tiandituMap.addEventListener('click', this.tiandituClickHandler);
this.tiandituGeocoder = new window.T.Geocoder();
} else {
if (typeof this.tiandituMap.checkResize === 'function') {
this.tiandituMap.checkResize();
}
}
if (this.mapSelected.longitude) {
this.renderMapMarker(this.toLngLat(this.mapSelected.longitude, this.mapSelected.latitude));
}
});
})
.catch(() => {
this.$message.error('天地图组件加载失败,请稍后重试或重新打开弹窗');
this.mapBox = false;
});
},
searchMapKeyword() {
const keyword = String(this.mapKeyword || '').trim();
if (!keyword) {
this.$message.warning('请输入地址关键词');
return;
}
this.loadTianditu()
.then(() => {
this.mapLoading = true;
if (!this.tiandituGeocoder) {
this.tiandituGeocoder = new window.T.Geocoder();
}
this.tiandituGeocoder.getPoint(keyword, result => {
this.mapLoading = false;
const point = this.resolveMapPoint(result);
if (!point) {
this.mapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
return;
}
this.pickMapPoint(point, keyword);
});
})
.catch(() => {
this.$message.error('天地图组件加载失败,请稍后重试或重新打开弹窗');
});
},
pickMapPoint(lnglat, keyword) {
const longitude = this.getPointLng(lnglat);
const latitude = this.getPointLat(lnglat);
if (longitude === undefined || latitude === undefined) {
this.$message.warning('选点坐标无效');
return;
}
const point = this.toLngLat(longitude, latitude);
this.renderMapMarker(point);
this.mapSelected = this.buildMapSelection({ lng: longitude, lat: latitude, address: keyword });
this.mapStatus = '正在反查地址...';
if (!this.tiandituGeocoder) {
this.tiandituGeocoder = new window.T.Geocoder();
}
this.tiandituGeocoder.getLocation(point, result => {
const address = this.resolveMapAddress(result);
this.mapSelected = {
...this.mapSelected,
detailAddress: address.detailAddress || this.mapSelected.detailAddress,
regionName: address.regionName || this.mapSelected.regionName,
regionCode: address.regionCode || this.mapSelected.regionCode,
};
this.mapKeyword = this.mapSelected.detailAddress || this.mapKeyword;
this.mapStatus = this.mapSelected.detailAddress || '已选点,可确认回填';
});
},
renderMapMarker(point) {
if (!this.tiandituMap || !window.T) return;
if (this.tiandituMarker) {
this.tiandituMap.removeOverLay(this.tiandituMarker);
}
this.tiandituMarker = new window.T.Marker(point);
this.tiandituMap.addOverLay(this.tiandituMarker);
this.tiandituMap.panTo(point);
},
confirmMapPick() {
if (!this.mapSelected.longitude) {
this.$message.warning('请先搜索或点击地图完成选点');
return;
}
this.form.detailAddress = this.mapSelected.detailAddress || this.form.detailAddress;
this.form.regionName = this.mapSelected.regionName || this.form.regionName;
this.form.regionCode = this.mapSelected.regionCode || this.form.regionCode;
this.form.longitude = this.mapSelected.longitude;
this.form.latitude = this.mapSelected.latitude;
this.mapBox = false;
},
buildMapSelection({ lng, lat, address, regionName, regionCode }) {
const longitude = lng === undefined || lng === null || lng === '' ? '' : Number(lng).toFixed(6);
const latitude = lat === undefined || lat === null || lat === '' ? '' : Number(lat).toFixed(6);
return {
longitude,
latitude,
detailAddress: address || '',
regionName: regionName || '',
regionCode: regionCode || '',
};
},
toLngLat(lng, lat) {
return new window.T.LngLat(Number(lng), Number(lat));
},
getPointLng(point) {
if (!point) return undefined;
if (typeof point.getLng === 'function') return point.getLng();
return point.lng ?? point.lon;
},
getPointLat(point) {
if (!point) return undefined;
if (typeof point.getLat === 'function') return point.getLat();
return point.lat;
},
resolveMapPoint(result) {
if (!result) return null;
if (typeof result.getLocationPoint === 'function') return result.getLocationPoint();
if (Array.isArray(result)) return result[0] || null;
if (result.location) return result.location;
if (result.lnglat) return result.lnglat;
if (result.lonlat) return result.lonlat;
if (result.getLng || result.lng || result.lon) return result;
return null;
},
resolveMapAddress(result = {}) {
const component =
(typeof result.getAddressComponent === 'function' && result.getAddressComponent()) ||
result.addressComponent ||
{};
const detailAddress =
(typeof result.getAddress === 'function' && result.getAddress()) ||
result.formatted_address ||
result.address ||
result.formattedAddress ||
'';
const regionName = [component.province, component.city, component.county || component.district]
.filter(Boolean)
.join('');
return {
detailAddress,
regionName,
regionCode: component.countyCode || component.adcode || component.cityCode || '',
};
},
},
};
</script>
<style lang="scss" scoped>
.common-address-page {
&__scope {
margin-left: 12px;
vertical-align: middle;
}
&__address {
display: flex;
gap: 10px;
align-items: flex-start;
.el-input {
flex: 1;
}
.el-button {
flex: 0 0 auto;
}
}
&__map-toolbar {
display: flex;
gap: 10px;
margin-bottom: 12px;
.el-input {
flex: 1;
}
}
&__map {
width: 100%;
height: 460px;
border: 1px solid #dcdfe6;
}
&__map-info {
display: flex;
justify-content: space-between;
gap: 16px;
min-height: 22px;
margin-top: 10px;
color: #606266;
font-size: 13px;
}
}
</style>