截止‘运力管理’模块

This commit is contained in:
2026-07-20 11:26:51 +08:00
parent e66b4a3680
commit c6dffb5d97
98 changed files with 19429 additions and 176 deletions

View File

@@ -0,0 +1,815 @@
<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>
</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 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();
} 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();
} 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) return;
this.form.addressName = this.form.addressName || source.addressName;
this.form.siteCode = source.siteCode;
this.form.siteCodeDisplay = source.siteCode;
this.form.detailAddress = source.detailAddress;
this.form.regionName = source.regionName;
this.form.longitude = source.longitude;
this.form.latitude = source.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, 'longitude').disabled = !sourceDisabled;
this.findColumn(this.option.column, 'latitude').disabled = !sourceDisabled;
if (value === '常规地址') {
if (reset) {
this.form.sourceId = undefined;
this.form.siteCode = '/';
this.form.siteCodeDisplay = '/';
this.sourceOptions = [];
}
return;
}
if (reset) {
this.form.sourceId = undefined;
this.form.siteCode = '';
this.form.siteCodeDisplay = '';
}
this.loadSourceOptions();
},
normalizeRow(row) {
const submitRow = {
...row,
addressName: String(row.addressName || '').trim(),
addressType: String(row.addressType || '').trim(),
detailAddress: String(row.detailAddress || '').trim(),
regionName: String(row.regionName || '').trim(),
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.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 (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 (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,
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,
});
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>