提交
This commit is contained in:
1 parent
8e70047f4c
commit
b4a49183db
33 files changed
+11558
-5889
No files matched your search
@@ -0,0 +1,497 @@
|
||||
<!-- 运输地址库 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">地址库</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="名称/地址">
|
||||
<el-input
|
||||
v-model="searchForm.q"
|
||||
placeholder="搜索名称或详细地址"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="全部" clearable>
|
||||
<el-option label="启用" value="1" />
|
||||
<el-option label="停用" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增地址
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
@click="handleToolbarEdit"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length !== 1 || !canDeleteByCreator(selectedRows[0])"
|
||||
:title="selectedRows.length === 1 && !canDeleteByCreator(selectedRows[0]) ? CREATOR_DELETE_TIP : ''"
|
||||
@click="handleToolbarDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="addressRows"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="index => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="addressName" label="地址名称" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="handleView(row)">{{ row.addressName }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fullAddress" label="详细地址" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column label="坐标" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.lng != null && row.lat != null">{{ row.lng }}, {{ row.lat }}</span>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="contactName" label="联系人" min-width="100" align="center" />
|
||||
<el-table-column prop="contactPhone" label="联系电话" min-width="120" align="center" />
|
||||
<el-table-column prop="status" label="状态" min-width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
|
||||
{{ row.status === '1' ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="640px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
class="dialog-form-layout pl-transport-address-form-dialog"
|
||||
modal-class="order-module-dialog-mask"
|
||||
:close-on-click-modal="false"
|
||||
style="backdrop-filter: none"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="地址名称" prop="addressName">
|
||||
<el-input v-model="formData.addressName" placeholder="如:安庆仓库、某某化工厂" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="formData.status" placeholder="请选择状态">
|
||||
<el-option label="启用" value="1" />
|
||||
<el-option label="停用" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="详细地址" prop="fullAddress" class="form-col-span-2">
|
||||
<AmapAddressPicker
|
||||
v-model="formData.fullAddress"
|
||||
v-model:lng="formData.lng"
|
||||
v-model:lat="formData.lat"
|
||||
placeholder="请地图选点或搜索 POI"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人">
|
||||
<el-input v-model="formData.contactName" placeholder="联系人(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="formData.contactPhone" placeholder="联系电话(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="备注(可选)" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model="viewVisible"
|
||||
title="地址详情"
|
||||
:loading="viewLoading"
|
||||
:hero-title="viewDetail?.addressName || '—'"
|
||||
:hero-meta="viewHeroMeta"
|
||||
:sections="viewSections"
|
||||
>
|
||||
<template #hero-actions>
|
||||
<el-button @click="handleEditFromView">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
:disabled="!viewDetail || !canDeleteByCreator(viewDetail)"
|
||||
@click="handleDeleteFromView"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
transportAddressApi,
|
||||
type TransportAddress,
|
||||
type TransportAddressPayload
|
||||
} from '@/api/pl/index'
|
||||
import AmapAddressPicker from '@/components/business/amap-address-picker/index.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { pickBxPageResult } from '@/utils/http/bx'
|
||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const addressRows = ref<TransportAddress[]>([])
|
||||
const selectedRows = ref<TransportAddress[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formRef = ref()
|
||||
const isEdit = ref(false)
|
||||
const viewVisible = ref(false)
|
||||
const viewLoading = ref(false)
|
||||
const viewDetail = ref<TransportAddress | null>(null)
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: DEFAULT_LIST_PAGE_SIZE,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
addressName: '',
|
||||
fullAddress: '',
|
||||
lng: null as number | null,
|
||||
lat: null as number | null,
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
status: '1',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
addressName: [{ required: true, message: '请输入地址名称', trigger: 'blur' }],
|
||||
fullAddress: [{ required: true, message: '请填写详细地址', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
||||
lng: [{ required: true, message: '请通过地图选点获取经度', trigger: 'change' }],
|
||||
lat: [{ required: true, message: '请通过地图选点获取纬度', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const viewHeroMeta = computed(() => {
|
||||
if (!viewDetail.value) return ''
|
||||
return viewDetail.value.fullAddress || ''
|
||||
})
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
const d = viewDetail.value
|
||||
if (!d) return []
|
||||
return [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
{ label: '地址名称', value: d.addressName },
|
||||
{ label: '详细地址', value: d.fullAddress, span: 'full' },
|
||||
{
|
||||
label: '坐标',
|
||||
value: d.lng != null && d.lat != null ? `${d.lng}, ${d.lat}` : '—'
|
||||
},
|
||||
{ label: '联系人', value: d.contactName || '—' },
|
||||
{ label: '联系电话', value: d.contactPhone || '—' },
|
||||
{
|
||||
label: '状态',
|
||||
value: d.status === '1' ? '启用' : '停用',
|
||||
tagType: d.status === '1' ? 'success' : 'info'
|
||||
},
|
||||
{ label: '创建时间', value: formatDateTime(d.createTime) },
|
||||
{ label: '更新时间', value: formatDateTime(d.updateTime) },
|
||||
{ label: '备注', value: d.remark || '—', span: 'full' }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await transportAddressApi.list({
|
||||
q: searchForm.q || undefined,
|
||||
status: searchForm.status || undefined,
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const { list, total } = pickBxPageResult<TransportAddress>(res)
|
||||
addressRows.value = list
|
||||
pagination.total = total
|
||||
const idSet = new Set(addressRows.value.map(item => item.id))
|
||||
selectedRows.value = selectedRows.value.filter(item => idSet.has(item.id))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.status = ''
|
||||
selectedRows.value = []
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handlePageChange() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: TransportAddress[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
function handleToolbarEdit() {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
ElMessage.warning('请选择一条地址记录')
|
||||
return
|
||||
}
|
||||
handleEdit(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function handleToolbarDelete() {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
ElMessage.warning('请选择一条地址记录')
|
||||
return
|
||||
}
|
||||
handleDelete(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
dialogTitle.value = '新增地址'
|
||||
isEdit.value = false
|
||||
Object.assign(formData, {
|
||||
id: '',
|
||||
addressName: '',
|
||||
fullAddress: '',
|
||||
lng: null,
|
||||
lat: null,
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
status: '1',
|
||||
remark: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleView(row: TransportAddress) {
|
||||
viewVisible.value = true
|
||||
viewLoading.value = true
|
||||
viewDetail.value = null
|
||||
try {
|
||||
const res = await transportAddressApi.get(row.id)
|
||||
viewDetail.value = (res.data ?? res) as TransportAddress
|
||||
} catch {
|
||||
viewVisible.value = false
|
||||
ElMessage.error('加载地址详情失败')
|
||||
} finally {
|
||||
viewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit(row: TransportAddress) {
|
||||
dialogTitle.value = '编辑地址'
|
||||
isEdit.value = true
|
||||
dialogVisible.value = true
|
||||
try {
|
||||
const res = await transportAddressApi.get(row.id)
|
||||
const detail = (res.data ?? res) as TransportAddress
|
||||
Object.assign(formData, {
|
||||
id: detail.id,
|
||||
addressName: detail.addressName,
|
||||
fullAddress: detail.fullAddress,
|
||||
lng: detail.lng != null ? Number(detail.lng) : null,
|
||||
lat: detail.lat != null ? Number(detail.lat) : null,
|
||||
contactName: detail.contactName || '',
|
||||
contactPhone: detail.contactPhone || '',
|
||||
status: detail.status,
|
||||
remark: detail.remark || ''
|
||||
})
|
||||
} catch {
|
||||
ElMessage.error('加载地址详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditFromView() {
|
||||
if (!viewDetail.value) return
|
||||
viewVisible.value = false
|
||||
handleEdit(viewDetail.value)
|
||||
}
|
||||
|
||||
async function handleDeleteFromView() {
|
||||
if (!viewDetail.value) return
|
||||
await handleDelete(viewDetail.value)
|
||||
viewVisible.value = false
|
||||
viewDetail.value = null
|
||||
}
|
||||
|
||||
function validateCoordinates(): boolean {
|
||||
if (formData.lng == null || formData.lat == null) {
|
||||
ElMessage.warning('请通过地图选点填写经纬度')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid || !validateCoordinates()) return
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload: TransportAddressPayload = {
|
||||
addressName: formData.addressName.trim(),
|
||||
fullAddress: formData.fullAddress.trim(),
|
||||
lng: formData.lng as number,
|
||||
lat: formData.lat as number,
|
||||
contactName: formData.contactName.trim() || undefined,
|
||||
contactPhone: formData.contactPhone.trim() || undefined,
|
||||
status: formData.status,
|
||||
remark: formData.remark.trim() || undefined
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await transportAddressApi.update(formData.id, payload)
|
||||
} else {
|
||||
await transportAddressApi.create(payload)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '编辑成功' : '新增成功')
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDialogClose() {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
async function handleDelete(row: TransportAddress) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('确定删除该地址吗?', '提示', { type: 'warning' })
|
||||
await transportAddressApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
if (viewDetail.value?.id === row.id) {
|
||||
viewVisible.value = false
|
||||
viewDetail.value = null
|
||||
}
|
||||
fetchData()
|
||||
}
|
||||
|
||||
useDbInstantSearch(
|
||||
() => ({
|
||||
q: searchForm.q,
|
||||
status: searchForm.status
|
||||
}),
|
||||
handleSearch
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
@@ -15,6 +15,35 @@
|
||||
<van-cell title="路线" :value="routeDisplay" class="route-cell" />
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group
|
||||
v-if="pickupNavUrl || destinationNavUrl"
|
||||
inset
|
||||
class="section-card nav-card"
|
||||
title="导航"
|
||||
>
|
||||
<div class="nav-actions">
|
||||
<van-button
|
||||
v-if="pickupNavUrl"
|
||||
type="primary"
|
||||
block
|
||||
round
|
||||
@click="openNavigation(pickupNavUrl)"
|
||||
>
|
||||
导航到取货点
|
||||
</van-button>
|
||||
<van-button
|
||||
v-if="destinationNavUrl"
|
||||
type="primary"
|
||||
plain
|
||||
block
|
||||
round
|
||||
@click="openNavigation(destinationNavUrl)"
|
||||
>
|
||||
导航到送货点
|
||||
</van-button>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group v-if="page.products.length" inset class="section-card" title="产品明细">
|
||||
<van-cell
|
||||
v-for="(item, idx) in page.products"
|
||||
@@ -72,6 +101,7 @@
|
||||
type PublicTransportProduct
|
||||
} from '@/api/pl/public-transport'
|
||||
import PublicMobileShell from '@/layouts/PublicMobileShell.vue'
|
||||
import { buildAmapNavigationUrl } from '@/utils/amap/navigation'
|
||||
|
||||
import 'vant/es/toast/style'
|
||||
import '@/styles/public-mobile-vant.scss'
|
||||
@@ -99,6 +129,28 @@
|
||||
return `${page.value.pickupAddress || '—'} → ${page.value.destinationAddress || '—'}`
|
||||
})
|
||||
|
||||
const pickupNavUrl = computed(() => {
|
||||
if (!page.value) return null
|
||||
return buildAmapNavigationUrl(
|
||||
page.value.pickupLng,
|
||||
page.value.pickupLat,
|
||||
page.value.pickupAddress || '取货点'
|
||||
)
|
||||
})
|
||||
|
||||
const destinationNavUrl = computed(() => {
|
||||
if (!page.value) return null
|
||||
return buildAmapNavigationUrl(
|
||||
page.value.destinationLng,
|
||||
page.value.destinationLat,
|
||||
page.value.destinationAddress || '送货点'
|
||||
)
|
||||
})
|
||||
|
||||
function openNavigation(url: string) {
|
||||
window.location.href = url
|
||||
}
|
||||
|
||||
function formatProductQuantity(value?: number) {
|
||||
if (value == null || Number.isNaN(Number(value))) return '—'
|
||||
const num = Number(value)
|
||||
@@ -207,4 +259,11 @@
|
||||
.uploader-wrap {
|
||||
padding: 12px 16px 16px;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px 16px 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user