Merge remote-tracking branch 'origin/master' into feature/bfi-api-rate
# Conflicts: # src/views/vehicle/customer-archive.vue
This commit is contained in:
@@ -10,12 +10,8 @@
|
||||
<el-button type="primary" :loading="searching" @click="searchKeyword">搜索</el-button>
|
||||
</div>
|
||||
<div class="address-map-picker__content">
|
||||
<map-search-results
|
||||
v-if="searchResults.length"
|
||||
:results="searchResults"
|
||||
@select="selectSearchResult"
|
||||
/>
|
||||
<div ref="map" class="address-map-picker__map"></div>
|
||||
<map-search-results :results="searchResults" @select="selectSearchResult" />
|
||||
</div>
|
||||
<div class="address-map-picker__info">{{ status }}</div>
|
||||
<template #footer>
|
||||
@@ -28,6 +24,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { normalizeMapSearchResults } from '@/utils/map-search';
|
||||
|
||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||
let amapLoader;
|
||||
@@ -42,6 +40,14 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
longitude: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
latitude: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'confirm'],
|
||||
data() {
|
||||
@@ -66,9 +72,11 @@ export default {
|
||||
this.visible = value;
|
||||
if (value) {
|
||||
this.keyword = this.address || '';
|
||||
this.selected = {};
|
||||
this.status = '可搜索地址或点击地图选点';
|
||||
this.searchResults = [];
|
||||
this.selected = this.buildInitialSelection();
|
||||
this.status = this.selected.longitude
|
||||
? this.selected.address || '已选点,可确认回填'
|
||||
: '可搜索地址或点击地图选点';
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -82,6 +90,18 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buildInitialSelection() {
|
||||
const longitude = Number(this.longitude);
|
||||
const latitude = Number(this.latitude);
|
||||
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
longitude,
|
||||
latitude,
|
||||
address: this.address || '',
|
||||
};
|
||||
},
|
||||
loadAmap() {
|
||||
if (window.AMap && window.AMap.Map) {
|
||||
return Promise.resolve();
|
||||
@@ -110,15 +130,27 @@ export default {
|
||||
return new Promise(resolve => {
|
||||
this.$nextTick(() => {
|
||||
if (!this.$refs.map) return resolve(null);
|
||||
const initial = this.buildInitialSelection();
|
||||
const center =
|
||||
Number.isFinite(initial.longitude) && Number.isFinite(initial.latitude)
|
||||
? [initial.longitude, initial.latitude]
|
||||
: [116.40769, 39.89945];
|
||||
if (!this.amap) {
|
||||
this.amap = new window.AMap.Map(this.$refs.map, {
|
||||
center: [116.40769, 39.89945],
|
||||
zoom: 11,
|
||||
center,
|
||||
zoom: initial.longitude ? 14 : 11,
|
||||
});
|
||||
this.amap.on('click', event => this.pickPoint(event.lnglat));
|
||||
} else {
|
||||
this.amap.resize();
|
||||
this.clearMarker();
|
||||
this.amap.setZoomAndCenter(initial.longitude ? 14 : 11, center);
|
||||
}
|
||||
if (initial.longitude) {
|
||||
const point = new window.AMap.LngLat(initial.longitude, initial.latitude);
|
||||
this.renderMarker(point);
|
||||
this.selected = { ...initial };
|
||||
this.status = initial.address || '已选点,可确认回填';
|
||||
}
|
||||
resolve(this.amap);
|
||||
});
|
||||
@@ -180,19 +212,14 @@ export default {
|
||||
.then(result => {
|
||||
if (!result) return;
|
||||
const geocodes = result.geocodes || (result.location ? [result] : []);
|
||||
this.searchResults = geocodes.map((item, index) => ({
|
||||
id: item.id || index,
|
||||
name: item.formattedAddress || item.address || keyword,
|
||||
address: item.formattedAddress || item.address || keyword,
|
||||
location: item.location,
|
||||
}));
|
||||
const point = geocodes[0]?.location || result.location;
|
||||
this.searchResults = normalizeMapSearchResults(geocodes, keyword);
|
||||
const point = this.searchResults[0]?.location || result.location;
|
||||
if (!point) {
|
||||
throw new Error('未找到匹配地址');
|
||||
}
|
||||
return this.$nextTick().then(() => {
|
||||
this.amap?.resize();
|
||||
this.pickPoint(point, geocodes[0]?.formattedAddress || keyword);
|
||||
this.pickPoint(point, this.searchResults[0]?.address || keyword);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -254,11 +281,19 @@ export default {
|
||||
}
|
||||
},
|
||||
confirm() {
|
||||
if (!this.selected.longitude || !this.selected.latitude) {
|
||||
this.$message.warning('请先搜索或点击地图完成选点');
|
||||
return;
|
||||
}
|
||||
if (!this.selected.address) {
|
||||
this.$message.warning('请等待地址解析完成后再确认');
|
||||
return;
|
||||
}
|
||||
this.$emit('confirm', this.selected.address);
|
||||
this.$emit('confirm', {
|
||||
address: this.selected.address,
|
||||
longitude: this.selected.longitude,
|
||||
latitude: this.selected.latitude,
|
||||
});
|
||||
this.visible = false;
|
||||
},
|
||||
},
|
||||
@@ -278,7 +313,6 @@ export default {
|
||||
}
|
||||
|
||||
&__map {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 420px;
|
||||
@@ -286,9 +320,19 @@ export default {
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
|
||||
.address-map-picker__map {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:deep(.map-search-results) {
|
||||
z-index: 2000;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
title="变更记录详情"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
align-center
|
||||
width="1100px"
|
||||
class="change-record-detail-dialog"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
@open="resetPage"
|
||||
>
|
||||
<el-table
|
||||
:data="pageRows"
|
||||
border
|
||||
max-height="60vh"
|
||||
:show-overflow-tooltip="false"
|
||||
>
|
||||
<el-table-column prop="field" label="变更字段" min-width="180" />
|
||||
<el-table-column
|
||||
prop="before"
|
||||
label="变更前"
|
||||
min-width="360"
|
||||
class-name="change-record-detail-value"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="after"
|
||||
label="变更后"
|
||||
min-width="500"
|
||||
class-name="change-record-detail-value"
|
||||
/>
|
||||
</el-table>
|
||||
<el-empty v-if="!total" description="暂无变更内容" :image-size="60" />
|
||||
<div class="change-record-detail-dialog__pager">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
background
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="$emit('update:modelValue', false)">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ChangeRecordDetailDialog',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rows: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
total() {
|
||||
return this.rows.length;
|
||||
},
|
||||
pageRows() {
|
||||
const start = (this.currentPage - 1) * this.pageSize;
|
||||
return this.rows.slice(start, start + this.pageSize);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
rows() {
|
||||
const maxPage = Math.max(1, Math.ceil(this.total / this.pageSize) || 1);
|
||||
if (this.currentPage > maxPage) this.currentPage = 1;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resetPage() {
|
||||
this.currentPage = 1;
|
||||
this.pageSize = 10;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.change-record-detail-dialog__pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
:deep(.change-record-detail-value .cell) {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -1,20 +1,55 @@
|
||||
<template>
|
||||
<div v-if="results.length" class="map-search-results">
|
||||
<div class="map-search-results__title">搜索结果</div>
|
||||
<el-empty v-if="!results.length" description="暂无搜索结果" :image-size="60" />
|
||||
<div
|
||||
v-for="(item, index) in results"
|
||||
:key="item.id || index"
|
||||
class="map-search-results__item"
|
||||
@click="$emit('select', item)"
|
||||
>
|
||||
<div class="map-search-results__name">{{ item.name || item.address || '未命名地址' }}</div>
|
||||
<div class="map-search-results__address">{{ item.address || item.name || '-' }}</div>
|
||||
<div class="map-search-results__list">
|
||||
<div
|
||||
v-for="(item, index) in pageResults"
|
||||
:key="item.id || `${currentPage}-${index}`"
|
||||
class="map-search-results__item"
|
||||
:class="{ 'is-active': isActive(item, index) }"
|
||||
@click="handleSelect(item, index)"
|
||||
>
|
||||
<img
|
||||
v-if="item.photo || item.image"
|
||||
class="map-search-results__thumb"
|
||||
:src="item.photo || item.image"
|
||||
alt=""
|
||||
/>
|
||||
<div class="map-search-results__body">
|
||||
<div class="map-search-results__name">
|
||||
{{ item.name || item.address || '未命名地址' }}
|
||||
</div>
|
||||
<div class="map-search-results__address">
|
||||
地址:{{ item.address || item.name || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="totalPages > 1" class="map-search-results__pager">
|
||||
<button
|
||||
v-for="page in visiblePages"
|
||||
:key="page"
|
||||
type="button"
|
||||
class="map-search-results__page"
|
||||
:class="{ 'is-active': page === currentPage }"
|
||||
@click="currentPage = page"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="map-search-results__page map-search-results__page--next"
|
||||
:disabled="currentPage >= totalPages"
|
||||
@click="goNext"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const PAGE_SIZE = 5;
|
||||
|
||||
export default {
|
||||
props: {
|
||||
results: {
|
||||
@@ -23,43 +58,167 @@ export default {
|
||||
},
|
||||
},
|
||||
emits: ['select'],
|
||||
data() {
|
||||
return {
|
||||
currentPage: 1,
|
||||
activeKey: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
totalPages() {
|
||||
return Math.max(1, Math.ceil((this.results || []).length / PAGE_SIZE));
|
||||
},
|
||||
pageResults() {
|
||||
const start = (this.currentPage - 1) * PAGE_SIZE;
|
||||
return (this.results || []).slice(start, start + PAGE_SIZE);
|
||||
},
|
||||
visiblePages() {
|
||||
const maxButtons = 3;
|
||||
let start = Math.max(1, this.currentPage - 1);
|
||||
let end = Math.min(this.totalPages, start + maxButtons - 1);
|
||||
start = Math.max(1, end - maxButtons + 1);
|
||||
const pages = [];
|
||||
for (let page = start; page <= end; page += 1) {
|
||||
pages.push(page);
|
||||
}
|
||||
return pages;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
results() {
|
||||
this.currentPage = 1;
|
||||
this.activeKey = '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resultKey(item, index) {
|
||||
return item?.id ?? `${this.currentPage}-${index}`;
|
||||
},
|
||||
isActive(item, index) {
|
||||
return this.activeKey === this.resultKey(item, index);
|
||||
},
|
||||
handleSelect(item, index) {
|
||||
this.activeKey = this.resultKey(item, index);
|
||||
this.$emit('select', item);
|
||||
},
|
||||
goNext() {
|
||||
if (this.currentPage < this.totalPages) {
|
||||
this.currentPage += 1;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.map-search-results {
|
||||
width: 260px;
|
||||
height: 420px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #eff1f7;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 360px;
|
||||
max-height: 360px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
flex: 0 0 260px;
|
||||
border: 1px solid #dcdfe6;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
||||
pointer-events: auto;
|
||||
|
||||
&__title {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #eff1f7;
|
||||
font-weight: 600;
|
||||
&__list {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
&__item:hover {
|
||||
background: #f5f9ff;
|
||||
&__item:hover,
|
||||
&__item.is-active {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&__thumb {
|
||||
flex: 0 0 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: cover;
|
||||
border: 1px solid #ebeef5;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
&__body {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
overflow: hidden;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__address {
|
||||
margin-top: 4px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&__pager {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&__page {
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
line-height: 26px;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&--next {
|
||||
min-width: 56px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -212,7 +212,14 @@ export default {
|
||||
.filter(Boolean);
|
||||
},
|
||||
acceptText() {
|
||||
return this.acceptedList.join(',');
|
||||
return this.acceptedList
|
||||
.map(item => {
|
||||
const value = String(item).trim();
|
||||
if (!value || value.includes('/') || value.startsWith('.')) return value;
|
||||
return `.${value}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
},
|
||||
tipText() {
|
||||
return this.tip || `${UNIFIED_ATTACHMENT_TIP_PREFIX}${this.maxSize}M`;
|
||||
@@ -304,12 +311,22 @@ export default {
|
||||
});
|
||||
},
|
||||
handleSuccess(response, file, files) {
|
||||
if (!response || response.success === false || (response.code && response.code !== 200)) {
|
||||
const code = response?.code;
|
||||
const codeInvalid = code !== undefined && code !== null && code !== '' && Number(code) !== 200;
|
||||
if (!response || response.success === false || codeInvalid) {
|
||||
this.$message.error((response && response.msg) || '上传失败');
|
||||
return;
|
||||
}
|
||||
this.emitUploadFiles(this.multiple ? files : [file]);
|
||||
this.$emit('success', this.normalizeUploadFile(file));
|
||||
if (file && !file.response) {
|
||||
file.response = response;
|
||||
}
|
||||
const normalized = this.normalizeUploadFile(file, response);
|
||||
if (!normalized.url) {
|
||||
this.$message.error('上传成功但未返回文件地址');
|
||||
return;
|
||||
}
|
||||
this.$emit('success', normalized);
|
||||
this.emitUploadFiles(this.multiple ? files : [file], normalized);
|
||||
this.$message.success('上传成功');
|
||||
},
|
||||
handleChange(file, files) {
|
||||
@@ -323,35 +340,89 @@ export default {
|
||||
this.emitUploadFiles(files);
|
||||
return true;
|
||||
},
|
||||
emitUploadFiles(files) {
|
||||
const list = files
|
||||
.filter(file => file.status === 'success' || this.getFileUrl(file))
|
||||
.map(this.normalizeUploadFile)
|
||||
emitUploadFiles(files, preferredFile) {
|
||||
const list = (files || [])
|
||||
.filter(file => {
|
||||
if (file?.status === 'fail') return false;
|
||||
return (
|
||||
file?.status === 'success' ||
|
||||
this.getFileUrl(file) ||
|
||||
file?.response?.data ||
|
||||
file?.response?.link ||
|
||||
file?.response
|
||||
);
|
||||
})
|
||||
.map(file => this.normalizeUploadFile(file))
|
||||
.filter(item => item.url);
|
||||
this.$emit('update:modelValue', list);
|
||||
this.$emit('change', list);
|
||||
|
||||
if (preferredFile?.url) {
|
||||
const exists = list.some(
|
||||
item =>
|
||||
(preferredFile.uid && item.uid && String(item.uid) === String(preferredFile.uid)) ||
|
||||
item.url === preferredFile.url
|
||||
);
|
||||
if (!exists) list.push(preferredFile);
|
||||
}
|
||||
|
||||
const current = this.parseValue(this.modelValue).map(item => ({
|
||||
...item,
|
||||
url: this.getFileUrl(item),
|
||||
}));
|
||||
const merged = [...current];
|
||||
list.forEach(file => {
|
||||
const index = merged.findIndex(
|
||||
item =>
|
||||
(file.uid && item.uid && String(item.uid) === String(file.uid)) ||
|
||||
(file.url && this.getFileUrl(item) === file.url)
|
||||
);
|
||||
if (index >= 0) merged.splice(index, 1, { ...merged[index], ...file });
|
||||
else merged.push(file);
|
||||
});
|
||||
|
||||
const result = merged.filter(item => this.getFileUrl(item));
|
||||
this.$emit('update:modelValue', result);
|
||||
this.$emit('change', result);
|
||||
},
|
||||
normalizeUploadFile(file) {
|
||||
const data = (file.response && file.response.data) || file.data || file;
|
||||
const url = data.link || data.url || data.domain || file.url || '';
|
||||
normalizeUploadFile(file, responseOverride) {
|
||||
const response = responseOverride || file?.response;
|
||||
let data = {};
|
||||
if (response && typeof response === 'object') {
|
||||
if (response.data && typeof response.data === 'object') {
|
||||
data = response.data;
|
||||
} else if (response.link || response.url || response.domain) {
|
||||
data = response;
|
||||
}
|
||||
}
|
||||
if (!data.link && !data.url && !data.domain) {
|
||||
data = file?.data && typeof file.data === 'object' ? file.data : file || {};
|
||||
}
|
||||
const url =
|
||||
data.link ||
|
||||
data.url ||
|
||||
data.domain ||
|
||||
data.fileLink ||
|
||||
data.fileUrl ||
|
||||
file?.url ||
|
||||
file?.link ||
|
||||
'';
|
||||
const originalName =
|
||||
data.originalName ||
|
||||
file.originalName ||
|
||||
file.name ||
|
||||
file?.originalName ||
|
||||
file?.name ||
|
||||
data.name ||
|
||||
this.getFileName(url) ||
|
||||
'附件';
|
||||
const extension = this.getExtension({ name: originalName, url });
|
||||
return {
|
||||
...data,
|
||||
uid: file.uid || data.uid,
|
||||
uid: file?.uid || data.uid,
|
||||
originalName,
|
||||
name: originalName,
|
||||
url,
|
||||
link: data.link || url,
|
||||
size: data.size || data.attachSize || file.size || '',
|
||||
size: data.size || data.attachSize || file?.size || '',
|
||||
extension,
|
||||
mimeType: data.mimeType || data.contentType || file.raw?.type || MIME_MAP[extension] || '',
|
||||
mimeType: data.mimeType || data.contentType || file?.raw?.type || MIME_MAP[extension] || '',
|
||||
};
|
||||
},
|
||||
handlePreview(file) {
|
||||
|
||||
Reference in New Issue
Block a user