Add common address page

This commit is contained in:
2026-07-20 13:39:11 +08:00
commit 0a8952f492
3 changed files with 640 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
import request from '@/utils/request'
export function fetchCommonAddressPage(params) {
return request({
url: '/api/basic/common-address/page',
method: 'get',
params,
})
}
export function fetchCommonAddressDetail(id) {
return request({
url: `/api/basic/common-address/${id}`,
method: 'get',
})
}
export function createCommonAddress(data) {
return request({
url: '/api/basic/common-address/save',
method: 'post',
data,
})
}
export function updateCommonAddress(data) {
return request({
url: '/api/basic/common-address/update',
method: 'post',
data,
})
}
export function deleteCommonAddress(ids) {
return request({
url: '/api/basic/common-address/delete',
method: 'post',
data: ids,
})
}
export function exportCommonAddress(params) {
return request({
url: '/api/basic/common-address/export',
method: 'get',
params,
responseType: 'blob',
})
}

View File

@@ -0,0 +1,9 @@
export default {
path: '/basic/common-address',
name: 'CommonAddress',
component: () => import('@/views/basic/address/index.vue'),
meta: {
title: '常用地址',
icon: 'location',
},
}

View File

@@ -0,0 +1,582 @@
<template>
<div class="common-address-page">
<el-card class="page-card" shadow="never">
<template #header>
<div class="page-header">
<span>常用地址</span>
<div class="actions">
<el-button type="primary" @click="handleAdd">新增常用地址</el-button>
<el-button @click="handleExport">批量导出</el-button>
<el-button type="danger" plain @click="handleBatchDelete">批量删除</el-button>
</div>
</div>
</template>
<el-form :inline="true" :model="query" class="search-form">
<el-form-item label="地址名称">
<el-input v-model="query.addressName" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="类型">
<el-select v-model="query.addressType" placeholder="全部" clearable style="width: 150px">
<el-option label="常规地址" value="NORMAL" />
<el-option label="港口/码头" value="PORT" />
<el-option label="铁路车站" value="RAIL" />
<el-option label="空港机场" value="AIRPORT" />
</el-select>
</el-form-item>
<el-form-item label="详细地址">
<el-input v-model="query.detailAddress" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="行政区划">
<el-input v-model="query.regionName" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="站点编码">
<el-input v-model="query.stationCode" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="组织">
<el-select v-model="query.orgId" filterable clearable placeholder="默认当前组织" style="width: 180px">
<el-option label="桂物物流" value="1" />
<el-option label="运输事业部" value="2" />
</el-select>
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="query.contactName" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="联系方式">
<el-input v-model="query.contactPhone" placeholder="请输入" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchList">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table
:data="tableData"
border
stripe
v-loading="loading"
@selection-change="onSelectionChange"
style="width: 100%"
>
<el-table-column type="selection" width="44" />
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="addressName" label="地址名称" min-width="140" />
<el-table-column prop="addressNo" label="地址编号" min-width="120" />
<el-table-column prop="addressType" label="类型" min-width="120">
<template #default="{ row }">
{{ formatType(row.addressType) }}
</template>
</el-table-column>
<el-table-column prop="stationCode" label="站点编码" min-width="110" />
<el-table-column prop="detailAddress" label="详细地址" min-width="220" />
<el-table-column prop="longitude" label="经度" min-width="110" />
<el-table-column prop="latitude" label="纬度" min-width="110" />
<el-table-column prop="regionName" label="行政区划" min-width="180" />
<el-table-column prop="contactName" label="联系人" min-width="110" />
<el-table-column prop="contactPhone" label="联系方式" min-width="140" />
<el-table-column prop="orgName" label="组织" min-width="120" />
<el-table-column prop="remark" label="备注" min-width="140" />
<el-table-column prop="updateTime" label="更新时间" min-width="170" />
<el-table-column prop="createTime" label="创建时间" min-width="170" />
<el-table-column fixed="right" label="操作" width="160">
<template #default="{ row }">
<el-button type="text" size="small" @click="openDetail(row)">查看</el-button>
<el-button type="text" size="small" @click="openEdit(row)">编辑</el-button>
<el-button type="text" size="small" class="danger-text" @click="removeRow(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
:current-page="page.current"
:page-size="page.size"
:total="page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</el-card>
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="920px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px" :disabled="dialogMode === 'detail'">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="类型" prop="addressType">
<el-radio-group v-model="form.addressType" @change="handleTypeChange">
<el-radio label="NORMAL">常规</el-radio>
<el-radio label="PORT">港口/码头</el-radio>
<el-radio label="RAIL">铁路车站</el-radio>
<el-radio label="AIRPORT">空港机场</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="地址名称" prop="addressName">
<el-input v-model="form.addressName" placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系人" prop="contactName">
<el-input v-model="form.contactName" placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系方式" prop="contactPhone">
<el-input v-model="form.contactPhone" placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="详细地址" prop="detailAddress">
<el-input v-model="form.detailAddress" placeholder="常规地址通过地图选择带出,其他类型从主数据带出" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="站点编码" prop="stationCode">
<el-input v-model="form.stationCode" :disabled="form.addressType === 'NORMAL'" placeholder="常规地址显示 /" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="组织" prop="orgName">
<el-select v-model="form.orgId" filterable placeholder="请选择">
<el-option label="桂物物流" value="1" />
<el-option label="运输事业部" value="2" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit />
</el-form-item>
</el-col>
</el-row>
<div v-if="form.addressType === 'NORMAL'" class="map-panel">
<div class="map-panel__header">
<span>地图选点</span>
<el-input
v-model="mapKeyword"
placeholder="请输入地址关键字"
style="width: 260px"
clearable
@keyup.enter="searchLocation"
/>
<el-button type="primary" @click="searchLocation">搜索</el-button>
</div>
<div class="map-panel__body">
<div class="result-list">
<div
v-for="item in locationOptions"
:key="item.key"
:class="['result-item', { active: selectedLocationKey === item.key }]"
@click="selectLocation(item)"
>
<div class="result-item__title">{{ item.title }}</div>
<div class="result-item__sub">{{ item.address }}</div>
</div>
</div>
<div class="map-placeholder">
<div>地图区域</div>
<div>这里对接真实地图 SDK 后可实现点击选点拖拽定位逆地址解析</div>
<div>当前示例同时回填经度纬度行政区划详细地址</div>
</div>
</div>
<el-row :gutter="16" class="coords-row">
<el-col :span="12">
<el-form-item label="经度" prop="longitude">
<el-input v-model="form.longitude" placeholder="自动回填" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="纬度" prop="latitude">
<el-input v-model="form.latitude" placeholder="自动回填" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="行政区划" prop="regionName">
<el-input v-model="form.regionName" placeholder="自动回填" />
</el-form-item>
</el-col>
</el-row>
</div>
<div v-else class="library-panel">
<el-alert type="info" :closable="false" title="其他类型地址请从主数据选择后回填站点编码、详细地址、行政区划及坐标。" />
<div class="library-panel__actions">
<el-button @click="chooseLibraryAddress('PORT')">选择港口/码头</el-button>
<el-button @click="chooseLibraryAddress('RAIL')">选择铁路车站</el-button>
<el-button @click="chooseLibraryAddress('AIRPORT')">选择空港机场</el-button>
</div>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">关闭</el-button>
<el-button v-if="dialogMode !== 'detail'" type="primary" @click="submitForm">确认</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import {
createCommonAddress,
deleteCommonAddress,
exportCommonAddress,
fetchCommonAddressDetail,
fetchCommonAddressPage,
updateCommonAddress,
} from '@/api/basic/common-address'
const EMPTY_FORM = {
id: null,
addressType: 'NORMAL',
addressName: '',
contactName: '',
contactPhone: '',
detailAddress: '',
stationCode: '/',
regionName: '',
longitude: '',
latitude: '',
orgId: '1',
orgName: '桂物物流',
remark: '',
}
export default {
name: 'CommonAddressPage',
data() {
return {
query: {
addressName: '',
addressType: '',
detailAddress: '',
regionName: '',
stationCode: '',
orgId: '',
contactName: '',
contactPhone: '',
current: 1,
size: 10,
},
page: {
current: 1,
size: 10,
total: 0,
},
loading: false,
tableData: [],
selectionRows: [],
dialogVisible: false,
dialogMode: 'create',
dialogTitle: '新建常用地址',
form: { ...EMPTY_FORM },
mapKeyword: '',
locationOptions: [],
selectedLocationKey: '',
rules: {
addressType: [{ required: true, message: '请选择类型', trigger: 'change' }],
addressName: [{ required: true, message: '请输入地址名称', trigger: 'blur' }],
detailAddress: [{ required: true, message: '请输入详细地址', trigger: 'blur' }],
},
}
},
created() {
this.fetchList()
},
methods: {
formatType(type) {
const map = {
NORMAL: '常规地址',
PORT: '港口/码头',
RAIL: '铁路车站',
AIRPORT: '空港机场',
}
return map[type] || type || '-'
},
fetchList() {
this.loading = true
const params = {
...this.query,
current: this.page.current,
size: this.page.size,
}
fetchCommonAddressPage(params)
.then((res) => {
const data = res?.data || res || {}
this.tableData = data.records || []
this.page.total = data.total || 0
})
.finally(() => {
this.loading = false
})
},
resetQuery() {
this.query = {
addressName: '',
addressType: '',
detailAddress: '',
regionName: '',
stationCode: '',
orgId: '',
contactName: '',
contactPhone: '',
current: 1,
size: 10,
}
this.page.current = 1
this.fetchList()
},
handleCurrentChange(val) {
this.page.current = val
this.fetchList()
},
handleSizeChange(val) {
this.page.size = val
this.page.current = 1
this.fetchList()
},
onSelectionChange(rows) {
this.selectionRows = rows
},
handleAdd() {
this.dialogMode = 'create'
this.dialogTitle = '新建常用地址'
this.form = { ...EMPTY_FORM }
this.locationOptions = []
this.mapKeyword = ''
this.selectedLocationKey = ''
this.dialogVisible = true
},
openEdit(row) {
this.dialogMode = 'edit'
this.dialogTitle = '编辑常用地址'
fetchCommonAddressDetail(row.id).then((res) => {
this.form = { ...EMPTY_FORM, ...(res?.data || res || {}) }
this.dialogVisible = true
})
},
openDetail(row) {
this.dialogMode = 'detail'
this.dialogTitle = '常用地址详情'
fetchCommonAddressDetail(row.id).then((res) => {
this.form = { ...EMPTY_FORM, ...(res?.data || res || {}) }
this.dialogVisible = true
})
},
handleTypeChange(val) {
if (val === 'NORMAL') {
this.form.stationCode = '/'
} else if (!this.form.stationCode || this.form.stationCode === '/') {
this.form.stationCode = ''
}
},
searchLocation() {
const base = [
{ key: 'a', title: `${this.mapKeyword || 'XX园区'}`, address: '广西南宁市江南区龙岗大道21号', lng: '108.348610', lat: '22.721864', regionName: '广西壮族自治区/南宁市/江南区' },
{ key: 'b', title: `${this.mapKeyword || 'XX园区'}A门`, address: '广西南宁市江南区龙岗大道21号', lng: '108.349020', lat: '22.722012', regionName: '广西壮族自治区/南宁市/江南区' },
{ key: 'c', title: `${this.mapKeyword || 'XX园区'}C门`, address: '广西南宁市江南区龙岗大道21号', lng: '108.347800', lat: '22.721400', regionName: '广西壮族自治区/南宁市/江南区' },
]
this.locationOptions = base
this.selectLocation(base[0])
},
selectLocation(item) {
this.selectedLocationKey = item.key
this.form.addressName = this.form.addressType === 'NORMAL' ? item.title : this.form.addressName
this.form.detailAddress = item.address
this.form.longitude = item.lng
this.form.latitude = item.lat
this.form.regionName = item.regionName
},
chooseLibraryAddress(type) {
const map = {
PORT: {
addressName: '舟山港',
stationCode: 'CNNGB',
detailAddress: '浙江省舟山市普陀区沈家门街道港口大道18号',
regionName: '浙江省/舟山市/普陀区',
longitude: '122.320000',
latitude: '29.988000',
},
RAIL: {
addressName: '柳州站',
stationCode: 'LZZ',
detailAddress: '广西柳州市柳北区站前路',
regionName: '广西壮族自治区/柳州市/柳北区',
longitude: '109.410000',
latitude: '24.327000',
},
AIRPORT: {
addressName: '成都双流机场',
stationCode: 'CTU',
detailAddress: '四川省成都市双流区机场路',
regionName: '四川省/成都市/双流区',
longitude: '103.956000',
latitude: '30.578000',
},
}
const picked = map[type]
if (picked) {
this.form.addressType = type
this.form.addressName = picked.addressName
this.form.stationCode = picked.stationCode
this.form.detailAddress = picked.detailAddress
this.form.regionName = picked.regionName
this.form.longitude = picked.longitude
this.form.latitude = picked.latitude
}
},
submitForm() {
this.$refs.formRef.validate((valid) => {
if (!valid) return
const action = this.form.id ? updateCommonAddress : createCommonAddress
action(this.form).then(() => {
this.dialogVisible = false
this.fetchList()
this.$message.success('保存成功')
})
})
},
removeRow(row) {
this.$confirm('确认删除该常用地址吗?删除后不可恢复。', '提示', { type: 'warning' })
.then(() => deleteCommonAddress([row.id]))
.then(() => {
this.$message.success('删除成功')
this.fetchList()
})
.catch(() => {})
},
handleBatchDelete() {
const ids = this.selectionRows.map((item) => item.id)
if (!ids.length) {
this.$message.warning('请选择要删除的数据')
return
}
this.$confirm('确认批量删除选中的常用地址吗?', '提示', { type: 'warning' })
.then(() => deleteCommonAddress(ids))
.then(() => {
this.$message.success('删除成功')
this.fetchList()
})
.catch(() => {})
},
handleExport() {
exportCommonAddress(this.query).then(() => {
this.$message.success('已发起导出')
})
},
},
}
</script>
<style scoped>
.page-card {
min-height: calc(100vh - 32px);
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.actions {
display: flex;
gap: 8px;
}
.search-form {
margin-bottom: 12px;
}
.pager {
display: flex;
justify-content: flex-end;
padding: 16px 0 0;
}
.danger-text {
color: #f56c6c;
}
.map-panel {
margin-top: 8px;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 12px;
}
.map-panel__header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.map-panel__body {
display: grid;
grid-template-columns: 260px 1fr;
gap: 12px;
min-height: 260px;
}
.result-list {
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
}
.result-item {
padding: 10px 12px;
border-bottom: 1px solid #f0f2f5;
cursor: pointer;
}
.result-item.active {
background: #ecf5ff;
}
.result-item__title {
font-weight: 600;
}
.result-item__sub {
margin-top: 4px;
color: #909399;
font-size: 12px;
}
.map-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px dashed #dcdfe6;
border-radius: 4px;
color: #606266;
gap: 8px;
background: #fafcff;
}
.coords-row {
margin-top: 12px;
}
.library-panel {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 12px;
}
.library-panel__actions {
display: flex;
gap: 8px;
}
</style>