6 changed files with 2294 additions and 0 deletions
|
After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,758 @@ |
|||
<template> |
|||
<el-dialog |
|||
:title="dialogTitle" |
|||
:visible.sync="dialogVisible" |
|||
width="1100px" |
|||
append-to-body |
|||
@open="handleOpen" |
|||
@close="handleClose" |
|||
> |
|||
<div class="trajectory-summary"> |
|||
<span>设备ID:{{ device && device.id ? device.id : "-" }}</span> |
|||
<span>序列号:{{ device && device.sn ? device.sn : "-" }}</span> |
|||
<span>名称:{{ device && device.alias ? device.alias : "-" }}</span> |
|||
<span>设备备注:{{ device && device.remark ? device.remark : "-" }}</span> |
|||
<span>显示轨迹点:{{ trajectoryPoints.length }}</span> |
|||
<span v-if="totalTrajectoryCount > trajectoryPoints.length"> |
|||
共 {{ totalTrajectoryCount }} 条,当前仅展示最近 {{ trajectoryPoints.length }} 条 |
|||
</span> |
|||
</div> |
|||
|
|||
<el-alert |
|||
v-if="loadError" |
|||
:title="loadError" |
|||
type="warning" |
|||
:closable="false" |
|||
show-icon |
|||
class="trajectory-alert" |
|||
/> |
|||
|
|||
<el-tabs |
|||
v-model="activePanel" |
|||
class="trajectory-tabs" |
|||
@tab-click="handlePanelChange" |
|||
> |
|||
<el-tab-pane label="地图轨迹" name="map"> |
|||
<div class="trajectory-toolbar"> |
|||
<span class="trajectory-toolbar__label">地图服务</span> |
|||
<el-radio-group |
|||
v-model="mapProvider" |
|||
size="small" |
|||
@change="handleProviderChange" |
|||
> |
|||
<el-radio-button label="amap" :disabled="!hasAmapKey"> |
|||
高德地图 |
|||
</el-radio-button> |
|||
<el-radio-button label="google" :disabled="!hasGoogleKey"> |
|||
谷歌地图 |
|||
</el-radio-button> |
|||
</el-radio-group> |
|||
</div> |
|||
|
|||
<div class="trajectory-map-shell" v-loading="loading || mapLoading"> |
|||
<el-empty |
|||
v-if="!loading && !mapLoading && !loadError && !trajectoryPoints.length" |
|||
description="暂无轨迹数据" |
|||
:image-size="88" |
|||
/> |
|||
<div |
|||
v-show="!loadError && trajectoryPoints.length" |
|||
ref="map" |
|||
class="trajectory-map" |
|||
></div> |
|||
</div> |
|||
</el-tab-pane> |
|||
|
|||
<el-tab-pane label="轨迹明细" name="table"> |
|||
<el-empty |
|||
v-if="!loading && !trajectoryPoints.length" |
|||
description="暂无轨迹数据" |
|||
:image-size="88" |
|||
/> |
|||
<el-table |
|||
v-else |
|||
:data="trajectoryPoints" |
|||
border |
|||
size="mini" |
|||
max-height="420" |
|||
class="trajectory-table" |
|||
> |
|||
<el-table-column label="#" type="index" width="60" align="center" /> |
|||
<el-table-column label="位置时间" min-width="180" align="center"> |
|||
<template slot-scope="scope"> |
|||
{{ |
|||
formatTrackTime( |
|||
scope.row.locationTime || |
|||
scope.row.reportedTime || |
|||
scope.row.createTime |
|||
) |
|||
}} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="经纬度" min-width="220" align="center"> |
|||
<template slot-scope="scope"> |
|||
{{ formatTrackCoordinates(scope.row.lat, scope.row.lng) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="地址" min-width="240" align="center"> |
|||
<template slot-scope="scope"> |
|||
{{ scope.row.address || "-" }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column |
|||
label="电量" |
|||
prop="battery" |
|||
width="100" |
|||
align="center" |
|||
/> |
|||
</el-table> |
|||
</el-tab-pane> |
|||
</el-tabs> |
|||
|
|||
<div slot="footer" class="dialog-footer"> |
|||
<el-button type="primary" @click="dialogVisible = false">关 闭</el-button> |
|||
</div> |
|||
</el-dialog> |
|||
</template> |
|||
|
|||
<script> |
|||
import { |
|||
getDeviceTrajectory, |
|||
getDeviceTrajectoryMapConfig, |
|||
} from "@/api/device/device"; |
|||
import { loadAMap } from "@/utils/loadAMap"; |
|||
import { loadGoogleMaps } from "@/utils/loadGoogleMaps"; |
|||
|
|||
const AMAP_DEFAULT_CENTER = [121.4737, 31.2304]; |
|||
const GOOGLE_DEFAULT_CENTER = { lat: 31.2304, lng: 121.4737 }; |
|||
const CONVERT_BATCH_SIZE = 40; |
|||
const MAX_TRAJECTORY_POINTS = 100; |
|||
|
|||
export default { |
|||
name: "DeviceTrajectoryDialog", |
|||
props: { |
|||
visible: { |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
device: { |
|||
type: Object, |
|||
default: () => ({}), |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
activePanel: "map", |
|||
mapProvider: "amap", |
|||
loading: false, |
|||
mapLoading: false, |
|||
loadError: "", |
|||
trajectoryPoints: [], |
|||
totalTrajectoryCount: 0, |
|||
map: null, |
|||
mapVendor: "", |
|||
polyline: null, |
|||
markers: [], |
|||
markerInfoWindows: [], |
|||
mapsApi: null, |
|||
mapConfig: null, |
|||
}; |
|||
}, |
|||
computed: { |
|||
dialogVisible: { |
|||
get() { |
|||
return this.visible; |
|||
}, |
|||
set(value) { |
|||
this.$emit("update:visible", value); |
|||
}, |
|||
}, |
|||
dialogTitle() { |
|||
const sn = this.device && this.device.sn ? this.device.sn : ""; |
|||
return sn ? `设备轨迹 - ${sn}` : "设备轨迹"; |
|||
}, |
|||
hasAmapKey() { |
|||
return !!(this.mapConfig && this.mapConfig.gaodeKey); |
|||
}, |
|||
hasGoogleKey() { |
|||
return !!(this.mapConfig && this.mapConfig.googleKey); |
|||
}, |
|||
}, |
|||
methods: { |
|||
async handleOpen() { |
|||
this.activePanel = "map"; |
|||
if (!this.device || !this.device.id) { |
|||
this.loadError = "未获取到设备信息,无法查看轨迹"; |
|||
return; |
|||
} |
|||
await this.fetchMapConfig(); |
|||
if (this.loadError) { |
|||
return; |
|||
} |
|||
this.mapProvider = this.resolveDefaultProvider(); |
|||
await this.fetchTrajectory(); |
|||
if (!this.trajectoryPoints.length || this.loadError) { |
|||
return; |
|||
} |
|||
await this.renderCurrentProviderMap(); |
|||
}, |
|||
handleClose() { |
|||
this.destroyMap(true); |
|||
this.activePanel = "map"; |
|||
this.mapProvider = "amap"; |
|||
this.loadError = ""; |
|||
this.trajectoryPoints = []; |
|||
this.totalTrajectoryCount = 0; |
|||
this.mapConfig = null; |
|||
}, |
|||
async handlePanelChange() { |
|||
if (this.activePanel !== "map" || !this.trajectoryPoints.length) { |
|||
return; |
|||
} |
|||
await this.renderCurrentProviderMap(); |
|||
}, |
|||
async handleProviderChange() { |
|||
if (this.activePanel !== "map" || !this.trajectoryPoints.length) { |
|||
return; |
|||
} |
|||
this.destroyMap(true); |
|||
await this.renderCurrentProviderMap(); |
|||
}, |
|||
resolveDefaultProvider() { |
|||
if (this.hasAmapKey) { |
|||
return "amap"; |
|||
} |
|||
if (this.hasGoogleKey) { |
|||
return "google"; |
|||
} |
|||
return "amap"; |
|||
}, |
|||
async fetchMapConfig() { |
|||
this.loadError = ""; |
|||
try { |
|||
const response = await getDeviceTrajectoryMapConfig(); |
|||
const data = response && response.data ? response.data : {}; |
|||
if (!data.gaodeKey && !data.googleKey) { |
|||
this.loadError = "当前企业未配置地图 Key"; |
|||
return; |
|||
} |
|||
this.mapConfig = data; |
|||
} catch (error) { |
|||
this.mapConfig = null; |
|||
this.loadError = |
|||
error && error.message ? error.message : "地图配置加载失败"; |
|||
} |
|||
}, |
|||
async fetchTrajectory() { |
|||
this.loading = true; |
|||
this.loadError = ""; |
|||
try { |
|||
const response = await getDeviceTrajectory(this.device.id); |
|||
const data = Array.isArray(response.data) ? response.data : []; |
|||
const validPoints = data |
|||
.map((item) => { |
|||
const latNum = this.normalizeCoordinate(item.lat, "lat"); |
|||
const lngNum = this.normalizeCoordinate(item.lng, "lng"); |
|||
return { |
|||
...item, |
|||
latNum, |
|||
lngNum, |
|||
}; |
|||
}) |
|||
.filter((item) => item.latNum !== null && item.lngNum !== null); |
|||
this.totalTrajectoryCount = validPoints.length; |
|||
this.trajectoryPoints = validPoints.slice(-MAX_TRAJECTORY_POINTS); |
|||
} catch (error) { |
|||
this.trajectoryPoints = []; |
|||
this.totalTrajectoryCount = 0; |
|||
this.loadError = error && error.message ? error.message : "轨迹加载失败"; |
|||
} finally { |
|||
this.loading = false; |
|||
} |
|||
}, |
|||
async renderCurrentProviderMap() { |
|||
if (!this.trajectoryPoints.length) { |
|||
return; |
|||
} |
|||
this.loadError = ""; |
|||
await this.$nextTick(); |
|||
if (this.mapProvider === "google") { |
|||
await this.ensureGoogleMap(); |
|||
if (!this.loadError) { |
|||
this.renderGoogleTrajectory(); |
|||
} |
|||
return; |
|||
} |
|||
await this.ensureAmap(); |
|||
if (!this.loadError) { |
|||
await this.prepareTrajectoryForAmap(); |
|||
this.renderAmapTrajectory(); |
|||
} |
|||
}, |
|||
async ensureAmap() { |
|||
if (!this.hasAmapKey) { |
|||
this.loadError = "当前企业未配置高德地图 Key"; |
|||
return; |
|||
} |
|||
this.mapLoading = true; |
|||
try { |
|||
const mapsApi = await loadAMap({ |
|||
key: this.mapConfig.gaodeKey, |
|||
securityJsCode: this.mapConfig.gaodeSecurityKey || "", |
|||
plugins: ["AMap.Scale", "AMap.ToolBar"], |
|||
}); |
|||
if (!this.$refs.map) { |
|||
return; |
|||
} |
|||
if (!this.map || this.mapVendor !== "amap") { |
|||
this.destroyMap(); |
|||
this.mapsApi = mapsApi; |
|||
this.map = new mapsApi.Map(this.$refs.map, { |
|||
center: AMAP_DEFAULT_CENTER, |
|||
zoom: 4, |
|||
resizeEnable: true, |
|||
viewMode: "2D", |
|||
}); |
|||
this.mapVendor = "amap"; |
|||
if (mapsApi.Scale) { |
|||
this.map.addControl(new mapsApi.Scale()); |
|||
} |
|||
if (mapsApi.ToolBar) { |
|||
this.map.addControl(new mapsApi.ToolBar()); |
|||
} |
|||
} else if (typeof this.map.resize === "function") { |
|||
this.mapsApi = mapsApi; |
|||
this.map.resize(); |
|||
} |
|||
} catch (error) { |
|||
this.loadError = error && error.message ? error.message : "高德地图加载失败"; |
|||
} finally { |
|||
this.mapLoading = false; |
|||
} |
|||
}, |
|||
async ensureGoogleMap() { |
|||
if (!this.hasGoogleKey) { |
|||
this.loadError = "当前企业未配置谷歌地图 Key"; |
|||
return; |
|||
} |
|||
this.mapLoading = true; |
|||
try { |
|||
const mapsApi = await loadGoogleMaps(this.mapConfig.googleKey); |
|||
if (!this.$refs.map) { |
|||
return; |
|||
} |
|||
if (!this.map || this.mapVendor !== "google") { |
|||
this.destroyMap(); |
|||
this.mapsApi = mapsApi; |
|||
this.map = new mapsApi.Map(this.$refs.map, { |
|||
center: GOOGLE_DEFAULT_CENTER, |
|||
zoom: 4, |
|||
mapTypeControl: false, |
|||
streetViewControl: false, |
|||
fullscreenControl: true, |
|||
}); |
|||
this.mapVendor = "google"; |
|||
} else if (mapsApi.event) { |
|||
this.mapsApi = mapsApi; |
|||
mapsApi.event.trigger(this.map, "resize"); |
|||
} |
|||
} catch (error) { |
|||
this.loadError = error && error.message ? error.message : "谷歌地图加载失败"; |
|||
} finally { |
|||
this.mapLoading = false; |
|||
} |
|||
}, |
|||
async prepareTrajectoryForAmap() { |
|||
if ( |
|||
this.trajectoryPoints.every( |
|||
(item) => item.amapLng !== undefined && item.amapLat !== undefined |
|||
) |
|||
) { |
|||
return; |
|||
} |
|||
const convertedPoints = await this.convertTrajectoryCoordinates( |
|||
this.trajectoryPoints |
|||
); |
|||
this.trajectoryPoints = this.trajectoryPoints.map((item, index) => { |
|||
const convertedPoint = convertedPoints[index]; |
|||
return { |
|||
...item, |
|||
amapLng: convertedPoint ? convertedPoint.lng : item.lngNum, |
|||
amapLat: convertedPoint ? convertedPoint.lat : item.latNum, |
|||
}; |
|||
}); |
|||
}, |
|||
async convertTrajectoryCoordinates(points) { |
|||
if (!this.mapsApi || !Array.isArray(points) || !points.length) { |
|||
return []; |
|||
} |
|||
const sourcePoints = points.map((item) => [item.lngNum, item.latNum]); |
|||
if (typeof this.mapsApi.convertFrom !== "function") { |
|||
return sourcePoints.map((item) => ({ lng: item[0], lat: item[1] })); |
|||
} |
|||
try { |
|||
const convertedPoints = []; |
|||
for ( |
|||
let index = 0; |
|||
index < sourcePoints.length; |
|||
index += CONVERT_BATCH_SIZE |
|||
) { |
|||
const batch = sourcePoints.slice(index, index + CONVERT_BATCH_SIZE); |
|||
const convertedBatch = await this.convertBatch(batch); |
|||
convertedPoints.push(...convertedBatch); |
|||
} |
|||
if (convertedPoints.length === sourcePoints.length) { |
|||
return convertedPoints; |
|||
} |
|||
} catch (error) { |
|||
return sourcePoints.map((item) => ({ lng: item[0], lat: item[1] })); |
|||
} |
|||
return sourcePoints.map((item) => ({ lng: item[0], lat: item[1] })); |
|||
}, |
|||
convertBatch(points) { |
|||
return new Promise((resolve, reject) => { |
|||
if (!points.length) { |
|||
resolve([]); |
|||
return; |
|||
} |
|||
this.mapsApi.convertFrom(points, "gps", (status, result) => { |
|||
if ( |
|||
status === "complete" && |
|||
result && |
|||
Array.isArray(result.locations) && |
|||
result.locations.length === points.length |
|||
) { |
|||
resolve( |
|||
result.locations.map((location) => ({ |
|||
lng: Number(location.lng.toFixed(7)), |
|||
lat: Number(location.lat.toFixed(7)), |
|||
})) |
|||
); |
|||
return; |
|||
} |
|||
reject(new Error("高德坐标转换失败")); |
|||
}); |
|||
}); |
|||
}, |
|||
renderAmapTrajectory() { |
|||
if (!this.map || !this.mapsApi || !this.trajectoryPoints.length) { |
|||
return; |
|||
} |
|||
this.clearOverlays(); |
|||
const path = this.trajectoryPoints.map((item) => [ |
|||
item.amapLng !== undefined ? item.amapLng : item.lngNum, |
|||
item.amapLat !== undefined ? item.amapLat : item.latNum, |
|||
]); |
|||
|
|||
this.polyline = new this.mapsApi.Polyline({ |
|||
path, |
|||
strokeColor: "#1a73e8", |
|||
strokeOpacity: 0.9, |
|||
strokeWeight: 5, |
|||
lineJoin: "round", |
|||
}); |
|||
|
|||
const startPoint = path[0]; |
|||
const endPoint = path[path.length - 1]; |
|||
const startItem = this.trajectoryPoints[0]; |
|||
const endItem = this.trajectoryPoints[this.trajectoryPoints.length - 1]; |
|||
|
|||
this.markers = [ |
|||
new this.mapsApi.Marker({ |
|||
position: startPoint, |
|||
content: this.buildAmapMarkerContent( |
|||
"起", |
|||
"#67c23a", |
|||
this.getPointTrackTime(startItem) |
|||
), |
|||
offset: new this.mapsApi.Pixel(-44, -52), |
|||
}), |
|||
]; |
|||
|
|||
if (path.length > 1) { |
|||
this.markers.push( |
|||
new this.mapsApi.Marker({ |
|||
position: endPoint, |
|||
content: this.buildAmapMarkerContent( |
|||
"终", |
|||
"#f56c6c", |
|||
this.getPointTrackTime(endItem) |
|||
), |
|||
offset: new this.mapsApi.Pixel(-44, -52), |
|||
}) |
|||
); |
|||
} |
|||
|
|||
this.map.add(this.polyline); |
|||
this.map.add(this.markers); |
|||
|
|||
if (path.length === 1) { |
|||
this.map.setZoomAndCenter(15, startPoint); |
|||
} else { |
|||
this.map.setFitView( |
|||
[this.polyline].concat(this.markers), |
|||
false, |
|||
[60, 60, 60, 60] |
|||
); |
|||
} |
|||
}, |
|||
renderGoogleTrajectory() { |
|||
if (!this.map || !this.mapsApi || !this.trajectoryPoints.length) { |
|||
return; |
|||
} |
|||
this.clearOverlays(); |
|||
const path = this.trajectoryPoints.map((item) => ({ |
|||
lat: item.latNum, |
|||
lng: item.lngNum, |
|||
})); |
|||
const bounds = new this.mapsApi.LatLngBounds(); |
|||
path.forEach((point) => bounds.extend(point)); |
|||
|
|||
this.polyline = new this.mapsApi.Polyline({ |
|||
path, |
|||
geodesic: true, |
|||
strokeColor: "#1a73e8", |
|||
strokeOpacity: 0.9, |
|||
strokeWeight: 4, |
|||
}); |
|||
this.polyline.setMap(this.map); |
|||
|
|||
const startPoint = path[0]; |
|||
const endPoint = path[path.length - 1]; |
|||
const startItem = this.trajectoryPoints[0]; |
|||
const endItem = this.trajectoryPoints[this.trajectoryPoints.length - 1]; |
|||
|
|||
this.markers = [ |
|||
new this.mapsApi.Marker({ |
|||
position: startPoint, |
|||
map: this.map, |
|||
label: "起", |
|||
}), |
|||
]; |
|||
|
|||
if (path.length > 1) { |
|||
this.markers.push( |
|||
new this.mapsApi.Marker({ |
|||
position: endPoint, |
|||
map: this.map, |
|||
label: "终", |
|||
}) |
|||
); |
|||
} |
|||
|
|||
const startInfoWindow = this.openGoogleMarkerInfoWindow( |
|||
this.markers[0], |
|||
"开始时间", |
|||
this.getPointTrackTime(startItem) |
|||
); |
|||
if (startInfoWindow) { |
|||
this.markerInfoWindows.push(startInfoWindow); |
|||
} |
|||
|
|||
if (this.markers.length > 1) { |
|||
const endInfoWindow = this.openGoogleMarkerInfoWindow( |
|||
this.markers[this.markers.length - 1], |
|||
"结束时间", |
|||
this.getPointTrackTime(endItem) |
|||
); |
|||
if (endInfoWindow) { |
|||
this.markerInfoWindows.push(endInfoWindow); |
|||
} |
|||
} |
|||
|
|||
if (path.length === 1) { |
|||
this.map.setCenter(startPoint); |
|||
this.map.setZoom(15); |
|||
} else { |
|||
this.map.fitBounds(bounds); |
|||
} |
|||
}, |
|||
openGoogleMarkerInfoWindow(marker, title, timeText) { |
|||
if (!this.mapsApi || !this.map || !marker || !this.mapsApi.InfoWindow) { |
|||
return null; |
|||
} |
|||
const infoWindow = new this.mapsApi.InfoWindow({ |
|||
content: |
|||
'<div style="min-width:160px;padding:6px 8px;line-height:1.6;">' + |
|||
'<div style="font-weight:600;color:#303133;">' + |
|||
this.escapeHtml(title) + |
|||
"</div>" + |
|||
'<div style="color:#606266;">' + |
|||
this.escapeHtml(timeText || "-") + |
|||
"</div>" + |
|||
"</div>", |
|||
}); |
|||
infoWindow.open({ |
|||
anchor: marker, |
|||
map: this.map, |
|||
shouldFocus: false, |
|||
}); |
|||
return infoWindow; |
|||
}, |
|||
buildAmapMarkerContent(label, color, timeText) { |
|||
return ( |
|||
'<div style="display:flex;flex-direction:column;align-items:center;gap:6px;">' + |
|||
'<div style="' + |
|||
"min-width:28px;height:28px;border-radius:14px;" + |
|||
"background:" + |
|||
color + |
|||
";color:#fff;display:flex;align-items:center;justify-content:center;" + |
|||
'font-size:12px;font-weight:600;box-shadow:0 4px 10px rgba(0,0,0,0.18);">' + |
|||
this.escapeHtml(label) + |
|||
"</div>" + |
|||
'<div style="padding:4px 8px;border-radius:6px;background:rgba(255,255,255,0.96);color:#303133;font-size:12px;line-height:1.4;box-shadow:0 4px 10px rgba(0,0,0,0.12);white-space:nowrap;">' + |
|||
this.escapeHtml(timeText || "-") + |
|||
"</div>" + |
|||
"</div>" |
|||
); |
|||
}, |
|||
clearOverlays() { |
|||
if (!this.map) { |
|||
this.polyline = null; |
|||
this.markers = []; |
|||
this.markerInfoWindows = []; |
|||
return; |
|||
} |
|||
if (this.mapVendor === "google") { |
|||
if (this.polyline) { |
|||
this.polyline.setMap(null); |
|||
} |
|||
if (this.markers.length) { |
|||
this.markers.forEach((marker) => marker.setMap(null)); |
|||
} |
|||
if (this.markerInfoWindows.length) { |
|||
this.markerInfoWindows.forEach((infoWindow) => { |
|||
if (infoWindow && typeof infoWindow.close === "function") { |
|||
infoWindow.close(); |
|||
} |
|||
}); |
|||
} |
|||
} else { |
|||
if (this.polyline) { |
|||
this.map.remove(this.polyline); |
|||
} |
|||
if (this.markers.length) { |
|||
this.map.remove(this.markers); |
|||
} |
|||
} |
|||
this.polyline = null; |
|||
this.markers = []; |
|||
this.markerInfoWindows = []; |
|||
}, |
|||
destroyMap(resetVendor = false) { |
|||
this.clearOverlays(); |
|||
if (!this.map) { |
|||
if (resetVendor) { |
|||
this.mapVendor = ""; |
|||
this.mapsApi = null; |
|||
} |
|||
return; |
|||
} |
|||
if (this.mapVendor === "amap" && typeof this.map.destroy === "function") { |
|||
this.map.destroy(); |
|||
} else if (this.$refs.map) { |
|||
this.$refs.map.innerHTML = ""; |
|||
} |
|||
this.map = null; |
|||
if (resetVendor) { |
|||
this.mapVendor = ""; |
|||
this.mapsApi = null; |
|||
} |
|||
}, |
|||
normalizeCoordinate(value, type) { |
|||
if (value === undefined || value === null || value === "") { |
|||
return null; |
|||
} |
|||
const numberValue = Number(value); |
|||
if (!Number.isFinite(numberValue)) { |
|||
return null; |
|||
} |
|||
const directLimit = type === "lat" ? 90 : 180; |
|||
const normalizedValue = |
|||
Math.abs(numberValue) > directLimit |
|||
? numberValue / 10000000 |
|||
: numberValue; |
|||
if (Math.abs(normalizedValue) > directLimit) { |
|||
return null; |
|||
} |
|||
return Number(normalizedValue.toFixed(7)); |
|||
}, |
|||
formatTrackTime(value) { |
|||
if (!value) { |
|||
return "-"; |
|||
} |
|||
return this.parseTime ? this.parseTime(value) : value; |
|||
}, |
|||
formatTrackCoordinates(lat, lng) { |
|||
const latText = |
|||
lat === null || lat === undefined || lat === "" ? "-" : lat; |
|||
const lngText = |
|||
lng === null || lng === undefined || lng === "" ? "-" : lng; |
|||
return `${latText} / ${lngText}`; |
|||
}, |
|||
getPointTrackTime(point) { |
|||
if (!point) { |
|||
return "-"; |
|||
} |
|||
return this.formatTrackTime( |
|||
point.locationTime || point.reportedTime || point.createTime |
|||
); |
|||
}, |
|||
escapeHtml(value) { |
|||
return String(value) |
|||
.replace(/&/g, "&") |
|||
.replace(/</g, "<") |
|||
.replace(/>/g, ">") |
|||
.replace(/\"/g, """) |
|||
.replace(/'/g, "'"); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.trajectory-summary { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 24px; |
|||
margin-bottom: 12px; |
|||
color: #606266; |
|||
} |
|||
|
|||
.trajectory-alert { |
|||
margin-bottom: 12px; |
|||
} |
|||
|
|||
.trajectory-tabs { |
|||
margin-top: 12px; |
|||
} |
|||
|
|||
.trajectory-toolbar { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 12px; |
|||
margin-bottom: 12px; |
|||
} |
|||
|
|||
.trajectory-toolbar__label { |
|||
color: #606266; |
|||
font-size: 13px; |
|||
} |
|||
|
|||
.trajectory-map-shell { |
|||
min-height: 420px; |
|||
border: 1px solid #ebeef5; |
|||
border-radius: 8px; |
|||
overflow: hidden; |
|||
background: #f5f7fa; |
|||
} |
|||
|
|||
.trajectory-map { |
|||
width: 100%; |
|||
height: 420px; |
|||
} |
|||
|
|||
.trajectory-table { |
|||
margin-top: 8px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,314 @@ |
|||
<template> |
|||
<el-dialog |
|||
title="认领设备" |
|||
:visible.sync="dialogVisible" |
|||
width="980px" |
|||
append-to-body |
|||
@close="handleClose" |
|||
> |
|||
<el-form |
|||
ref="queryForm" |
|||
:model="queryParams" |
|||
:inline="true" |
|||
label-width="80px" |
|||
class="claim-query-form" |
|||
> |
|||
<el-form-item label="订单号" prop="orderCode"> |
|||
<el-input |
|||
v-model.trim="queryParams.orderCode" |
|||
placeholder="请输入订单号" |
|||
clearable |
|||
size="small" |
|||
@keyup.enter.native="handleQuery" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery"> |
|||
搜索 |
|||
</el-button> |
|||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
|
|||
<el-alert |
|||
title="请先输入订单号查询设备,再勾选需要认领的设备进行确认。" |
|||
type="info" |
|||
:closable="false" |
|||
show-icon |
|||
class="claim-tip" |
|||
/> |
|||
|
|||
<el-table |
|||
ref="claimTable" |
|||
v-loading="loading" |
|||
:data="deviceList" |
|||
border |
|||
height="420" |
|||
row-key="id" |
|||
@selection-change="handleSelectionChange" |
|||
> |
|||
<el-table-column type="selection" width="55" align="center" :reserve-selection="true" /> |
|||
<el-table-column label="设备ID" prop="id" width="90" align="center" /> |
|||
<el-table-column label="序列号" prop="sn" min-width="180" align="center" /> |
|||
<!-- <el-table-column label="操作" width="100" align="center" fixed="right"> |
|||
<template slot-scope="scope"> |
|||
<el-button type="text" size="mini" @click="handleDetail(scope.row)">详情</el-button> |
|||
</template> |
|||
</el-table-column> --> |
|||
</el-table> |
|||
|
|||
<el-empty |
|||
v-if="searched && !loading && !deviceList.length" |
|||
description="未查询到可认领的设备" |
|||
:image-size="80" |
|||
class="claim-empty" |
|||
/> |
|||
|
|||
<pagination |
|||
v-show="searched && total > 0" |
|||
:total="total" |
|||
:page.sync="queryParams.pageNum" |
|||
:limit.sync="queryParams.pageSize" |
|||
@pagination="handlePageChange" |
|||
/> |
|||
|
|||
<div slot="footer" class="dialog-footer"> |
|||
<span class="claim-selected">已选 {{ selectedIds.length }} 台设备</span> |
|||
<el-button @click="handleClose">取 消</el-button> |
|||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit"> |
|||
确认认领 |
|||
</el-button> |
|||
</div> |
|||
|
|||
<el-dialog |
|||
title="设备详情" |
|||
:visible.sync="detailOpen" |
|||
width="720px" |
|||
append-to-body |
|||
> |
|||
<div v-loading="detailLoading"> |
|||
<el-descriptions v-if="deviceDetail" :column="2" border> |
|||
<el-descriptions-item label="设备ID"> |
|||
{{ deviceDetail.id || "-" }} |
|||
</el-descriptions-item> |
|||
<el-descriptions-item label="订单号"> |
|||
{{ deviceDetail.orderCode || "-" }} |
|||
</el-descriptions-item> |
|||
<el-descriptions-item label="序列号"> |
|||
{{ deviceDetail.sn || "-" }} |
|||
</el-descriptions-item> |
|||
<el-descriptions-item label="MAC 地址"> |
|||
{{ deviceDetail.mac || "-" }} |
|||
</el-descriptions-item> |
|||
<el-descriptions-item label="所属批次"> |
|||
{{ deviceDetail.batchNo || "-" }} |
|||
</el-descriptions-item> |
|||
<el-descriptions-item label="型号"> |
|||
{{ deviceDetail.model || "-" }} |
|||
</el-descriptions-item> |
|||
<el-descriptions-item label="企业"> |
|||
{{ deviceDetail.merchantName || "-" }} |
|||
</el-descriptions-item> |
|||
</el-descriptions> |
|||
</div> |
|||
<div slot="footer" class="dialog-footer"> |
|||
<el-button type="primary" @click="detailOpen = false">关 闭</el-button> |
|||
</div> |
|||
</el-dialog> |
|||
</el-dialog> |
|||
</template> |
|||
|
|||
<script> |
|||
import { claimDeviceBatch, getDevice, listAllDevice } from "@/api/device/device"; |
|||
|
|||
export default { |
|||
name: "DeviceClaimDialog", |
|||
props: { |
|||
visible: { |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
loading: false, |
|||
submitLoading: false, |
|||
detailLoading: false, |
|||
searched: false, |
|||
total: 0, |
|||
deviceList: [], |
|||
deviceDetail: null, |
|||
selectedIds: [], |
|||
selectedDeviceMap: {}, |
|||
detailOpen: false, |
|||
queryParams: { |
|||
orderCode: "", |
|||
pageNum: 1, |
|||
pageSize: 10, |
|||
}, |
|||
}; |
|||
}, |
|||
computed: { |
|||
dialogVisible: { |
|||
get() { |
|||
return this.visible; |
|||
}, |
|||
set(value) { |
|||
this.$emit("update:visible", value); |
|||
}, |
|||
}, |
|||
}, |
|||
methods: { |
|||
handleQuery() { |
|||
if (!this.queryParams.orderCode) { |
|||
this.$message.warning("请输入订单号后再搜索"); |
|||
return; |
|||
} |
|||
this.searched = true; |
|||
this.queryParams.pageNum = 1; |
|||
this.clearSelectedDevices(); |
|||
this.fetchDeviceList(); |
|||
}, |
|||
handlePageChange() { |
|||
if (!this.queryParams.orderCode) { |
|||
return; |
|||
} |
|||
this.fetchDeviceList(); |
|||
}, |
|||
fetchDeviceList() { |
|||
this.loading = true; |
|||
listAllDevice(this.queryParams) |
|||
.then((response) => { |
|||
const data = response.data || {}; |
|||
this.deviceList = Array.isArray(data.list) ? data.list : []; |
|||
this.total = Number(data.total) || 0; |
|||
this.$nextTick(() => { |
|||
this.syncSelection(); |
|||
}); |
|||
}) |
|||
.finally(() => { |
|||
this.loading = false; |
|||
}); |
|||
}, |
|||
resetQuery() { |
|||
this.queryParams = { |
|||
orderCode: "", |
|||
pageNum: 1, |
|||
pageSize: 10, |
|||
}; |
|||
this.total = 0; |
|||
this.deviceList = []; |
|||
this.searched = false; |
|||
this.clearSelectedDevices(); |
|||
if (this.$refs.queryForm) { |
|||
this.$refs.queryForm.resetFields(); |
|||
} |
|||
}, |
|||
handleSelectionChange(selection) { |
|||
const currentPageIds = this.deviceList.map((item) => item.id); |
|||
currentPageIds.forEach((id) => { |
|||
if (this.selectedDeviceMap[id]) { |
|||
this.$delete(this.selectedDeviceMap, id); |
|||
} |
|||
}); |
|||
selection.forEach((item) => { |
|||
this.$set(this.selectedDeviceMap, item.id, item); |
|||
}); |
|||
this.selectedIds = Object.keys(this.selectedDeviceMap).map((id) => Number(id)); |
|||
}, |
|||
handleSubmit() { |
|||
if (!this.queryParams.orderCode) { |
|||
this.$message.warning("请先输入订单号并查询设备"); |
|||
return; |
|||
} |
|||
if (!this.selectedIds.length) { |
|||
this.$message.warning("请至少选择一台设备"); |
|||
return; |
|||
} |
|||
this.$confirm( |
|||
`确认认领当前选中的 ${this.selectedIds.length} 台设备吗?`, |
|||
"提示", |
|||
{ |
|||
confirmButtonText: "确定", |
|||
cancelButtonText: "取消", |
|||
type: "warning", |
|||
} |
|||
) |
|||
.then(() => { |
|||
this.submitLoading = true; |
|||
return claimDeviceBatch({ |
|||
orderCode: this.queryParams.orderCode, |
|||
deviceIds: this.selectedIds, |
|||
}); |
|||
}) |
|||
.then(() => { |
|||
this.$message.success("认领成功"); |
|||
this.$emit("success"); |
|||
this.handleClose(); |
|||
}) |
|||
.finally(() => { |
|||
this.submitLoading = false; |
|||
}); |
|||
}, |
|||
handleDetail(row) { |
|||
if (!row || !row.id) { |
|||
return; |
|||
} |
|||
this.detailOpen = true; |
|||
this.detailLoading = true; |
|||
this.deviceDetail = { ...row }; |
|||
getDevice(row.id) |
|||
.then((response) => { |
|||
this.deviceDetail = response.data || {}; |
|||
}) |
|||
.finally(() => { |
|||
this.detailLoading = false; |
|||
}); |
|||
}, |
|||
handleClose() { |
|||
this.resetQuery(); |
|||
this.detailOpen = false; |
|||
this.deviceDetail = null; |
|||
this.dialogVisible = false; |
|||
}, |
|||
clearSelectedDevices() { |
|||
this.selectedDeviceMap = {}; |
|||
this.selectedIds = []; |
|||
if (this.$refs.claimTable) { |
|||
this.$refs.claimTable.clearSelection(); |
|||
} |
|||
}, |
|||
syncSelection() { |
|||
if (!this.$refs.claimTable) { |
|||
return; |
|||
} |
|||
this.$refs.claimTable.clearSelection(); |
|||
this.deviceList.forEach((row) => { |
|||
if (this.selectedDeviceMap[row.id]) { |
|||
this.$refs.claimTable.toggleRowSelection(row, true); |
|||
} |
|||
}); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.claim-query-form { |
|||
margin-bottom: 12px; |
|||
} |
|||
|
|||
.claim-tip { |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
.claim-empty { |
|||
margin-top: 12px; |
|||
} |
|||
|
|||
.claim-selected { |
|||
float: left; |
|||
line-height: 32px; |
|||
color: #606266; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,735 @@ |
|||
<template> |
|||
<div class="app-container"> |
|||
<el-row :gutter="20"> |
|||
|
|||
<!--用户数据--> |
|||
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px"> |
|||
<el-form-item label="用户名称" prop="account"> |
|||
<el-input |
|||
v-model="queryParams.account" |
|||
placeholder="请输入用户名称" |
|||
clearable |
|||
size="small" |
|||
style="width: 240px" |
|||
@keyup.enter.native="handleQuery" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="昵称" prop="nickName"> |
|||
<el-input |
|||
v-model="queryParams.nickName" |
|||
placeholder="请输入昵称" |
|||
clearable |
|||
size="small" |
|||
style="width: 240px" |
|||
@keyup.enter.native="handleQuery" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="状态" prop="status"> |
|||
<el-select |
|||
v-model="queryParams.status" |
|||
placeholder="用户状态" |
|||
clearable |
|||
size="small" |
|||
style="width: 240px" |
|||
> |
|||
<el-option label="启用" :value="0" /> |
|||
<el-option label="禁用" :value="1" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> |
|||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
|
|||
<el-row :gutter="10" class="mb8"> |
|||
<el-col v-if="!selectionMode" :span="1.5"> |
|||
<el-button |
|||
type="primary" |
|||
plain |
|||
icon="el-icon-plus" |
|||
size="mini" |
|||
@click="handleAdd" |
|||
v-hasPermi="['system:user:add']" |
|||
>新增</el-button> |
|||
</el-col> |
|||
<el-col v-if="!selectionMode" :span="1.5"> |
|||
<el-button |
|||
type="success" |
|||
plain |
|||
icon="el-icon-edit" |
|||
size="mini" |
|||
:disabled="single" |
|||
@click="handleUpdate" |
|||
v-hasPermi="['system:user:edit']" |
|||
>修改</el-button> |
|||
</el-col> |
|||
<el-col v-if="!selectionMode" :span="1.5"> |
|||
<el-button |
|||
type="danger" |
|||
plain |
|||
icon="el-icon-delete" |
|||
size="mini" |
|||
:disabled="multiple" |
|||
@click="handleDelete" |
|||
v-hasPermi="['system:user:remove']" |
|||
>删除</el-button> |
|||
</el-col> |
|||
<!-- <el-col :span="1.5"> |
|||
<el-button |
|||
type="info" |
|||
plain |
|||
icon="el-icon-upload2" |
|||
size="mini" |
|||
@click="handleImport" |
|||
v-hasPermi="['system:user:import']" |
|||
>导入</el-button> |
|||
</el-col> --> |
|||
<el-col v-if="!selectionMode" :span="1.5"> |
|||
<el-button |
|||
type="warning" |
|||
plain |
|||
icon="el-icon-download" |
|||
size="mini" |
|||
@click="handleExport" |
|||
v-hasPermi="['system:user:export']" |
|||
>导出</el-button> |
|||
</el-col> |
|||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" :columns="columns"></right-toolbar> |
|||
</el-row> |
|||
|
|||
<el-table |
|||
ref="userTable" |
|||
v-loading="loading" |
|||
:data="userList" |
|||
row-key="id" |
|||
@selection-change="handleSelectionChange" |
|||
@row-click="handleRowClick" |
|||
> |
|||
<el-table-column |
|||
type="selection" |
|||
width="50" |
|||
align="center" |
|||
:reserve-selection="selectionMode" |
|||
/> |
|||
<el-table-column label="用户编号" align="center" key="id" prop="id" v-if="columns[0].visible" /> |
|||
<el-table-column label="用户名称" align="center" key="account" prop="account" v-if="columns[1].visible" :show-overflow-tooltip="true" /> |
|||
<el-table-column label="用户昵称" align="center" key="nickName" prop="nickName" v-if="columns[2].visible" :show-overflow-tooltip="true" /> |
|||
<!-- <el-table-column label="部门" align="center" key="deptName" prop="dept.deptName" v-if="columns[3].visible" :show-overflow-tooltip="true" />--> |
|||
<!-- <el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber" v-if="columns[4].visible" width="120" />--> |
|||
<el-table-column label="状态" align="center" key="status" v-if="columns[5].visible"> |
|||
<template slot-scope="scope"> |
|||
<el-switch |
|||
v-if="!selectionMode" |
|||
v-model="scope.row.status" |
|||
active-value="0" |
|||
inactive-value="1" |
|||
@change="handleStatusChange(scope.row)" |
|||
></el-switch> |
|||
<span v-else>{{ scope.row.status === "0" ? "启用" : "禁用" }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="谷歌令牌" align="center" key="googleAuthSecret" prop="googleAuthSecret" v-if="columns[6].visible"> |
|||
<template slot-scope="scope"> |
|||
<!-- <img :src="columns[6].url+'?content=otpauth://totp/GeoTag_'+scope.row.account+'?secret='+scope.row.googleAuthSecret" style="width: 100px; height: 100px;"> --> |
|||
<img :src="columns[6].url + '?content=' + encodeURIComponent(`otpauth://totp/GeoTag_${scope.row.account}?secret=${scope.row.googleAuthSecret}`)" style="width: 100px; height: 100px;"> |
|||
|
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="创建时间" align="center" prop="createTime" v-if="columns[7].visible" width="160"> |
|||
<template slot-scope="scope"> |
|||
<span>{{ parseTime(scope.row.createTime) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column |
|||
v-if="!selectionMode" |
|||
label="操作" |
|||
align="center" |
|||
width="160" |
|||
class-name="small-padding fixed-width" |
|||
> |
|||
<template slot-scope="scope"> |
|||
<el-button |
|||
size="mini" |
|||
type="text" |
|||
icon="el-icon-edit" |
|||
@click="handleUpdate(scope.row)" |
|||
v-hasPermi="['system:user:edit']" |
|||
>修改</el-button> |
|||
<el-button |
|||
v-if="scope.row.id !== 1" |
|||
size="mini" |
|||
type="text" |
|||
icon="el-icon-delete" |
|||
@click="handleDelete(scope.row)" |
|||
v-hasPermi="['system:user:remove']" |
|||
>删除</el-button> |
|||
<el-button |
|||
size="mini" |
|||
type="text" |
|||
icon="el-icon-key" |
|||
@click="handleResetPwd(scope.row)" |
|||
v-hasPermi="['system:user:resetPwd']" |
|||
>重置</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
|
|||
<pagination |
|||
v-show="total>0" |
|||
:total="total" |
|||
:page.sync="queryParams.pageNum" |
|||
:limit.sync="queryParams.pageSize" |
|||
@pagination="getList" |
|||
/> |
|||
</el-row> |
|||
|
|||
<!-- 添加或修改参数配置对话框 --> |
|||
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body> |
|||
<el-form ref="form" :model="form" :rules="rules" label-width="80px"> |
|||
<el-row> |
|||
<el-col :span="12"> |
|||
<el-form-item v-if="form.id == undefined" label="用户名称" prop="account"> |
|||
<el-input v-model="form.account" placeholder="请输入用户名称" /> |
|||
</el-form-item> |
|||
</el-col> |
|||
<el-col :span="12"> |
|||
<el-form-item v-if="form.id == undefined" label="用户密码" prop="passwordHash"> |
|||
<el-input v-model="form.passwordHash" placeholder="请输入用户密码" type="passwordHash" /> |
|||
</el-form-item> |
|||
</el-col> |
|||
</el-row> |
|||
<el-row> |
|||
<el-col :span="12"> |
|||
<el-form-item label="用户昵称" prop="nickName"> |
|||
<el-input v-model="form.nickName" placeholder="请输入用户昵称" /> |
|||
</el-form-item> |
|||
</el-col> |
|||
|
|||
<el-col :span="12"> |
|||
<el-form-item label="角色"> |
|||
<el-select |
|||
v-model="form.roleIds" |
|||
multiple |
|||
filterable |
|||
placeholder="请选择角色(全部角色)" |
|||
style="width: 100%" |
|||
> |
|||
<el-option |
|||
v-for="item in roleOptions" |
|||
:key="item.roleId" |
|||
:label="(item.roleName || item.name) || ('角色' + item.roleId)" |
|||
:value="String(item.roleId)" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
</el-col> |
|||
</el-row> |
|||
|
|||
|
|||
|
|||
<el-row> |
|||
<el-col :span="12"> |
|||
<el-form-item label="状态" prop="status"> |
|||
<el-radio-group v-model="form.status"> |
|||
<el-radio :label="0">启用</el-radio> |
|||
<el-radio :label="1">禁用</el-radio> |
|||
</el-radio-group> |
|||
</el-form-item> |
|||
</el-col> |
|||
|
|||
</el-row> |
|||
<el-row> |
|||
<el-col :span="24"> |
|||
<el-form-item label="备注"> |
|||
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input> |
|||
</el-form-item> |
|||
</el-col> |
|||
</el-row> |
|||
</el-form> |
|||
<div slot="footer" class="dialog-footer"> |
|||
<el-button type="primary" @click="submitForm">确 定</el-button> |
|||
<el-button @click="cancel">取 消</el-button> |
|||
</div> |
|||
</el-dialog> |
|||
|
|||
<!-- 用户导入对话框 --> |
|||
<el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body> |
|||
<el-upload |
|||
ref="upload" |
|||
:limit="1" |
|||
accept=".xlsx, .xls" |
|||
:headers="upload.headers" |
|||
:action="upload.url + '?updateSupport=' + upload.updateSupport" |
|||
:disabled="upload.isUploading" |
|||
:on-progress="handleFileUploadProgress" |
|||
:on-success="handleFileSuccess" |
|||
:auto-upload="false" |
|||
drag |
|||
> |
|||
<i class="el-icon-upload"></i> |
|||
<div class="el-upload__text"> |
|||
将文件拖到此处,或 |
|||
<em>点击上传</em> |
|||
</div> |
|||
<div class="el-upload__tip" slot="tip"> |
|||
<el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据 |
|||
<el-link type="info" style="font-size:12px" @click="importTemplate">下载模板</el-link> |
|||
</div> |
|||
<div class="el-upload__tip" style="color:red" slot="tip">提示:仅允许导入“xls”或“xlsx”格式文件!</div> |
|||
</el-upload> |
|||
<div slot="footer" class="dialog-footer"> |
|||
<el-button type="primary" @click="submitFileForm">确 定</el-button> |
|||
<el-button @click="upload.open = false">取 消</el-button> |
|||
</div> |
|||
</el-dialog> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { listEmployeeUser,listUser, getUser, delUser, addUser, updateUser, exportUser, resetUserPwd, changeUserStatus, importTemplate } from "@/api/system/user"; |
|||
import { listRole } from "@/api/system/role"; |
|||
import { getToken } from "@/utils/auth"; |
|||
// import { treeselect } from "@/api/system/dept"; |
|||
import Treeselect from "@riophae/vue-treeselect"; |
|||
import "@riophae/vue-treeselect/dist/vue-treeselect.css"; |
|||
|
|||
export default { |
|||
name: "User", |
|||
props: { |
|||
selectionMode: { |
|||
type: Boolean, |
|||
default: false |
|||
} |
|||
}, |
|||
components: { Treeselect }, |
|||
data() { |
|||
return { |
|||
// 遮罩层 |
|||
loading: true, |
|||
// 选中数组 |
|||
ids: [], |
|||
// 非单个禁用 |
|||
single: true, |
|||
// 非多个禁用 |
|||
multiple: true, |
|||
// 显示搜索条件 |
|||
showSearch: true, |
|||
// 总条数 |
|||
total: 0, |
|||
// 用户表格数据 |
|||
userList: null, |
|||
// 选择模式下当前选中的员工 |
|||
selectedRows: [], |
|||
selectedUserMap: {}, |
|||
// 弹出层标题 |
|||
title: "", |
|||
// 部门树选项 |
|||
deptOptions: undefined, |
|||
// 是否显示弹出层 |
|||
open: false, |
|||
// 部门名称 |
|||
deptName: undefined, |
|||
// 默认密码 |
|||
passwordHash: undefined, |
|||
// 日期范围 |
|||
dateRange: [], |
|||
// 状态数据字典 |
|||
statusOptions: [], |
|||
// 性别状态字典 |
|||
sexOptions: [], |
|||
// 岗位选项 |
|||
postOptions: [], |
|||
// 角色选项 |
|||
roleOptions: [], |
|||
// 表单参数(保证 roleIds 为响应式数组) |
|||
form: { |
|||
roleIds: [] |
|||
}, |
|||
defaultProps: { |
|||
children: "children", |
|||
label: "label" |
|||
}, |
|||
// 用户导入参数 |
|||
upload: { |
|||
// 是否显示弹出层(用户导入) |
|||
open: false, |
|||
// 弹出层标题(用户导入) |
|||
title: "", |
|||
// 是否禁用上传 |
|||
isUploading: false, |
|||
// 是否更新已经存在的用户数据 |
|||
updateSupport: 0, |
|||
// 设置上传的请求头部 |
|||
headers: { Authorization: "Bearer " + getToken() }, |
|||
// 上传的地址 |
|||
url: process.env.VUE_APP_BASE_API + "/system/user/importData" |
|||
}, |
|||
// 查询参数 |
|||
queryParams: { |
|||
pageNum: 1, |
|||
pageSize: 10, |
|||
account: undefined, |
|||
phonenumber: undefined, |
|||
status: undefined, |
|||
deptId: undefined |
|||
}, |
|||
// 列信息 |
|||
columns: [ |
|||
{ key: 0, label: `用户编号`, visible: true }, |
|||
{ key: 1, label: `用户名称`, visible: true }, |
|||
{ key: 2, label: `用户昵称`, visible: true }, |
|||
{ key: 3, label: `部门`, visible: true }, |
|||
{ key: 4, label: `手机号码`, visible: true }, |
|||
{ key: 5, label: `状态`, visible: true }, |
|||
{ key: 6, label: `谷歌令牌`, visible: true, url: process.env.VUE_APP_BASE_API + "/business/businessUser/activityCode" }, |
|||
{ key: 7, label: `创建时间`, visible: true } |
|||
], |
|||
// 表单校验 |
|||
rules: { |
|||
account: [ |
|||
{ required: true, message: "用户名称不能为空", trigger: "blur" } |
|||
], |
|||
nickName: [ |
|||
{ required: true, message: "用户昵称不能为空", trigger: "blur" } |
|||
], |
|||
passwordHash: [ |
|||
{ required: true, message: "用户密码不能为空", trigger: "blur" } |
|||
], |
|||
email: [ |
|||
{ |
|||
type: "email", |
|||
message: "'请输入正确的邮箱地址", |
|||
trigger: ["blur", "change"] |
|||
} |
|||
], |
|||
phonenumber: [ |
|||
{ |
|||
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, |
|||
message: "请输入正确的手机号码", |
|||
trigger: "blur" |
|||
} |
|||
] |
|||
} |
|||
}; |
|||
}, |
|||
watch: { |
|||
// 根据名称筛选部门树 |
|||
deptName(val) { |
|||
this.$refs.tree.filter(val); |
|||
} |
|||
}, |
|||
created() { |
|||
this.getList(); |
|||
this.getRoleList(); |
|||
// this.getTreeselect(); |
|||
// this.getDicts("sys_normal_disable").then(response => { |
|||
// this.statusOptions = response.data; |
|||
// }); |
|||
// this.getDicts("sys_user_sex").then(response => { |
|||
// this.sexOptions = response.data; |
|||
// }); |
|||
// this.getConfigKey("sys.user.passwordHash").then(response => { |
|||
// this.passwordHash = response.msg; |
|||
// }); |
|||
}, |
|||
methods: { |
|||
/** 查询角色列表(拉取全部角色,统一为 roleId/roleName 便于下拉展示) */ |
|||
getRoleList() { |
|||
listRole({ pageNum: 1, pageSize: 1000 }).then(response => { |
|||
const list = response.data.list || response.data || []; |
|||
this.roleOptions = list.map(item => ({ |
|||
roleId: item.roleId != null ? item.roleId : item.id, |
|||
roleName: item.roleName != null ? item.roleName : item.name |
|||
})); |
|||
}); |
|||
}, |
|||
/** 查询用户列表 */ |
|||
getList() { |
|||
this.loading = true; |
|||
listEmployeeUser(this.addDateRange(this.queryParams, this.dateRange)) |
|||
.then(response => { |
|||
const data = response.data; |
|||
const list = Array.isArray(data) ? data : (data && Array.isArray(data.list) ? data.list : []); |
|||
this.userList = list.map(user => ({ |
|||
...user, |
|||
status: String(user.status) |
|||
})); |
|||
this.total = Array.isArray(data) |
|||
? list.length |
|||
: Number((data && data.total) || list.length); |
|||
if (this.selectionMode) { |
|||
this.$nextTick(() => { |
|||
this.syncSelection(); |
|||
}); |
|||
} |
|||
}) |
|||
.catch(() => { |
|||
this.userList = []; |
|||
this.total = 0; |
|||
}) |
|||
.finally(() => { |
|||
this.loading = false; |
|||
}); |
|||
}, |
|||
/** 查询部门下拉树结构 */ |
|||
getTreeselect() { |
|||
treeselect().then(response => { |
|||
this.deptOptions = response.data; |
|||
}); |
|||
}, |
|||
// 筛选节点 |
|||
filterNode(value, data) { |
|||
if (!value) return true; |
|||
return data.label.indexOf(value) !== -1; |
|||
}, |
|||
// 节点单击事件 |
|||
handleNodeClick(data) { |
|||
this.queryParams.deptId = data.id; |
|||
this.getList(); |
|||
}, |
|||
// 用户状态修改 |
|||
handleStatusChange(row) { |
|||
let text = row.status === "0" ? "启用" : "停用"; |
|||
this.$confirm('确认要"' + text + '""' + row.account + '"用户吗?', "警告", { |
|||
confirmButtonText: "确定", |
|||
cancelButtonText: "取消", |
|||
type: "warning" |
|||
}).then(function() { |
|||
return changeUserStatus(row.id, row.status); |
|||
}).then(() => { |
|||
this.msgSuccess(text + "成功"); |
|||
}).catch(function() { |
|||
row.status = row.status === "0" ? "1" : "0"; |
|||
}); |
|||
}, |
|||
// 取消按钮 |
|||
cancel() { |
|||
this.open = false; |
|||
this.reset(); |
|||
}, |
|||
// 表单重置 |
|||
reset() { |
|||
this.form = { |
|||
id: undefined, |
|||
deptId: undefined, |
|||
account: undefined, |
|||
nickName: undefined, |
|||
passwordHash: undefined, |
|||
phonenumber: undefined, |
|||
email: undefined, |
|||
sex: undefined, |
|||
status: 0, |
|||
remark: undefined, |
|||
postIds: [], |
|||
roleIds: [] |
|||
}; |
|||
this.resetForm("form"); |
|||
}, |
|||
/** 搜索按钮操作 */ |
|||
handleQuery() { |
|||
this.queryParams.pageNum = 1; |
|||
this.getList(); |
|||
}, |
|||
/** 重置按钮操作 */ |
|||
resetQuery() { |
|||
this.dateRange = []; |
|||
this.resetForm("queryForm"); |
|||
if (this.selectionMode) { |
|||
this.clearSelectionState(); |
|||
} |
|||
this.handleQuery(); |
|||
}, |
|||
// 多选框选中数据 |
|||
handleSelectionChange(selection) { |
|||
if (this.selectionMode) { |
|||
const currentPageIds = (this.userList || []).map(item => item.id); |
|||
currentPageIds.forEach((id) => { |
|||
if (this.selectedUserMap[id]) { |
|||
this.$delete(this.selectedUserMap, id); |
|||
} |
|||
}); |
|||
selection.forEach((item) => { |
|||
this.$set(this.selectedUserMap, item.id, item); |
|||
}); |
|||
this.selectedRows = Object.values(this.selectedUserMap); |
|||
this.ids = this.selectedRows.map(item => item.id); |
|||
this.single = this.selectedRows.length !== 1; |
|||
this.multiple = !this.selectedRows.length; |
|||
this.$emit("select", this.selectedRows); |
|||
return; |
|||
} |
|||
this.ids = selection.map(item => item.id); |
|||
this.single = selection.length != 1; |
|||
this.multiple = !selection.length; |
|||
}, |
|||
handleRowClick(row) { |
|||
if (!this.selectionMode) { |
|||
return; |
|||
} |
|||
this.$refs.userTable.toggleRowSelection(row); |
|||
}, |
|||
clearSelectionState() { |
|||
this.selectedRows = []; |
|||
this.selectedUserMap = {}; |
|||
this.ids = []; |
|||
this.single = true; |
|||
this.multiple = true; |
|||
if (this.$refs.userTable) { |
|||
this.$refs.userTable.clearSelection(); |
|||
} |
|||
}, |
|||
syncSelection() { |
|||
if (!this.selectionMode || !this.$refs.userTable) { |
|||
return; |
|||
} |
|||
this.$refs.userTable.clearSelection(); |
|||
(this.userList || []).forEach((row) => { |
|||
if (this.selectedUserMap[row.id]) { |
|||
this.$refs.userTable.toggleRowSelection(row, true); |
|||
} |
|||
}); |
|||
}, |
|||
/** 新增按钮操作 */ |
|||
handleAdd() { |
|||
this.reset(); |
|||
this.getRoleList(); |
|||
this.open = true; |
|||
this.title = "添加用户"; |
|||
this.form.passwordHash = this.passwordHash; |
|||
}, |
|||
/** 修改按钮操作 */ |
|||
handleUpdate(row) { |
|||
this.reset(); |
|||
const id = row.id || this.ids; |
|||
Promise.all([getUser(id), listRole({ pageNum: 1, pageSize: 1000 })]).then(([userRes, roleRes]) => { |
|||
const data = userRes.data; // 用户基本信息在 data 里 |
|||
// roleIds、roles 在响应根级别,不在 data 里 |
|||
const resRoleIds = userRes.roleIds || []; |
|||
const resRoles = userRes.roles || []; |
|||
|
|||
const list = roleRes.data?.list || roleRes.data || []; |
|||
// 角色选项:先取 listRole 列表,再用 getUser 返回的 roles 里的 roleName 覆盖/补充 |
|||
const optionsMap = new Map(); |
|||
list.forEach(item => { |
|||
optionsMap.set(String(item.roleId != null ? item.roleId : item.id), { |
|||
roleId: item.roleId != null ? item.roleId : item.id, |
|||
roleName: item.roleName != null ? item.roleName : item.name |
|||
}); |
|||
}); |
|||
resRoles.forEach(r => { |
|||
const rid = r.roleId != null ? r.roleId : r.id; |
|||
const name = r.roleName != null ? r.roleName : r.name; |
|||
if (rid != null) optionsMap.set(String(rid), { roleId: rid, roleName: name }); |
|||
}); |
|||
this.roleOptions = Array.from(optionsMap.values()); |
|||
|
|||
// 当前用户所属角色:取响应根级别的 roleIds |
|||
const roleIds = resRoleIds.map(rid => String(rid)); |
|||
|
|||
this.form = Object.assign({}, data, { |
|||
postIds: userRes.postIds || [], |
|||
roleIds: roleIds, |
|||
passwordHash: "" |
|||
}); |
|||
this.$set(this.form, "roleIds", roleIds); |
|||
this.title = "修改用户"; |
|||
this.$nextTick(() => { |
|||
this.open = true; |
|||
}); |
|||
}); |
|||
}, |
|||
/** 重置密码按钮操作 */ |
|||
handleResetPwd(row) { |
|||
this.$prompt('请输入"' + row.account + '"的新密码', "提示", { |
|||
confirmButtonText: "确定", |
|||
cancelButtonText: "取消" |
|||
}).then(({ value }) => { |
|||
resetUserPwd(row.id, value).then(response => { |
|||
this.msgSuccess("修改成功,新密码是:" + value); |
|||
}); |
|||
}).catch(() => {}); |
|||
}, |
|||
/** 提交按钮 */ |
|||
submitForm: function() { |
|||
this.$refs["form"].validate(valid => { |
|||
if (valid) { |
|||
// 将角色ID从字符串数组转换为数字数组 |
|||
const submitData = { |
|||
...this.form, |
|||
roleIds: this.form.roleIds.map(id => Number(id)) |
|||
}; |
|||
|
|||
if (this.form.id != undefined) { |
|||
updateUser(submitData).then(response => { |
|||
this.msgSuccess("修改成功"); |
|||
this.open = false; |
|||
this.getList(); |
|||
}); |
|||
} else { |
|||
addUser(submitData).then(response => { |
|||
this.msgSuccess("新增成功"); |
|||
this.open = false; |
|||
this.getList(); |
|||
}); |
|||
} |
|||
} |
|||
}); |
|||
}, |
|||
/** 删除按钮操作 */ |
|||
handleDelete(row) { |
|||
const ids = row.id || this.ids; |
|||
this.$confirm('是否确认删除用户编号为"' + ids + '"的数据项?', "警告", { |
|||
confirmButtonText: "确定", |
|||
cancelButtonText: "取消", |
|||
type: "warning" |
|||
}).then(function() { |
|||
return delUser(ids); |
|||
}).then(() => { |
|||
this.getList(); |
|||
this.msgSuccess("删除成功"); |
|||
}) |
|||
}, |
|||
/** 导出按钮操作 */ |
|||
handleExport() { |
|||
const queryParams = this.queryParams; |
|||
this.$confirm('是否确认导出所有用户数据项?', "警告", { |
|||
confirmButtonText: "确定", |
|||
cancelButtonText: "取消", |
|||
type: "warning" |
|||
}).then(function() { |
|||
return exportUser(queryParams); |
|||
}).then(response => { |
|||
this.download(response.msg); |
|||
}) |
|||
}, |
|||
/** 导入按钮操作 */ |
|||
handleImport() { |
|||
this.upload.title = "用户导入"; |
|||
this.upload.open = true; |
|||
}, |
|||
/** 下载模板操作 */ |
|||
importTemplate() { |
|||
importTemplate().then(response => { |
|||
this.download(response.msg); |
|||
}); |
|||
}, |
|||
// 文件上传中处理 |
|||
handleFileUploadProgress(event, file, fileList) { |
|||
this.upload.isUploading = true; |
|||
}, |
|||
// 文件上传成功处理 |
|||
handleFileSuccess(response, file, fileList) { |
|||
this.upload.open = false; |
|||
this.upload.isUploading = false; |
|||
this.$refs.upload.clearFiles(); |
|||
this.$alert(response.msg, "导入结果", { dangerouslyUseHTMLString: true }); |
|||
this.getList(); |
|||
}, |
|||
// 提交上传文件 |
|||
submitFileForm() { |
|||
this.$refs.upload.submit(); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
@ -0,0 +1,226 @@ |
|||
<template> |
|||
<div class="login"> |
|||
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form"> |
|||
<h3 class="title">客户 GeoTag管理后台</h3> |
|||
<el-form-item prop="username"> |
|||
<el-input v-model="loginForm.username" type="text" auto-complete="off" placeholder="账号"> |
|||
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" /> |
|||
</el-input> |
|||
</el-form-item> |
|||
<el-form-item prop="password"> |
|||
<el-input v-model="loginForm.password" type="password" auto-complete="off" placeholder="密码" |
|||
@keyup.enter.native="handleLogin"> |
|||
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" /> |
|||
</el-input> |
|||
</el-form-item> |
|||
<el-form-item prop="code"> |
|||
<el-input v-model="loginForm.code" auto-complete="off" placeholder="谷歌验证码" style="width: 100%" |
|||
@keyup.enter.native="handleLogin"> |
|||
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" /> |
|||
</el-input> |
|||
<!-- <div class="login-code"> |
|||
<img :src="codeUrl" @click="getCode" class="login-code-img"/> |
|||
</div> --> |
|||
</el-form-item> |
|||
<!-- 人机验证 --> |
|||
<!-- <el-form-item prop='validateCode'> |
|||
<el-row :span="24"> |
|||
<el-col :span="24"> |
|||
<reCaptcha :sitekey="key" @getValidateCode='getValidateCode' v-model="loginForm.validateCode"></reCaptcha> |
|||
</el-col> |
|||
</el-row> |
|||
</el-form-item> --> |
|||
|
|||
<!-- <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox> --> |
|||
<el-form-item style="width:100%;"> |
|||
<el-button :loading="loading" size="medium" type="waming" style="width:100%;" |
|||
@click.native.prevent="handleLogin"> |
|||
<span v-if="!loading">登 录</span> |
|||
<span v-else>登 录 中...</span> |
|||
</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
|
|||
<!-- 底部 --> |
|||
<div class="el-login-footer"> |
|||
<span></span> |
|||
|
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import Cookies from "js-cookie"; |
|||
import { encrypt, decrypt } from '@/utils/jsencrypt' |
|||
|
|||
export default { |
|||
name: "Login", |
|||
data() { |
|||
var checkCode = (rule, value, callback) => { |
|||
if (value == false) { |
|||
callback(new Error('请进行人机验证')); |
|||
} else { |
|||
callback(); |
|||
} |
|||
}; |
|||
return { |
|||
codeUrl: "", |
|||
cookiePassword: "", |
|||
key: '6LcBoGUaAAAAABUnZINfh4j6FgqpQR-yHakZepIR', |
|||
loginForm: { |
|||
username: "", |
|||
password: "", |
|||
rememberMe: false, |
|||
code: "", |
|||
uuid: "", |
|||
getValidateCode: false |
|||
}, |
|||
loginRules: { |
|||
username: [ |
|||
{ required: true, trigger: "blur", message: "用户名不能为空" } |
|||
], |
|||
password: [ |
|||
{ required: true, trigger: "blur", message: "密码不能为空" } |
|||
], |
|||
code: [ |
|||
{ required: true, trigger: "change", message: "验证码不能为空" } |
|||
], |
|||
validateCode: [ |
|||
{ validator: checkCode, trigger: 'change' } |
|||
] |
|||
}, |
|||
loading: false, |
|||
redirect: undefined |
|||
}; |
|||
|
|||
}, |
|||
watch: { |
|||
$route: { |
|||
handler: function (route) { |
|||
this.redirect = route.query && route.query.redirect; |
|||
}, |
|||
immediate: true |
|||
} |
|||
}, |
|||
created() { |
|||
// this.getCookie(); |
|||
}, |
|||
methods: { |
|||
icoCreate(icoUrl) { |
|||
var link = document.querySelector("link[rel*='icon']") || document.createElement('link'); |
|||
link.type = 'image/x-icon'; |
|||
link.rel = 'shortcut icon'; |
|||
link.href = icoUrl |
|||
document.getElementsByTagName('head')[0].appendChild(link); |
|||
}, |
|||
getValidateCode(value) { |
|||
this.loginForm.validateCode = value |
|||
}, |
|||
getCookie() { |
|||
const username = Cookies.get("username"); |
|||
const password = Cookies.get("password"); |
|||
const rememberMe = Cookies.get('rememberMe') |
|||
this.loginForm = { |
|||
username: username === undefined ? this.loginForm.username : username, |
|||
password: password === undefined ? this.loginForm.password : decrypt(password), |
|||
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe) |
|||
}; |
|||
}, |
|||
|
|||
handleLogin() { |
|||
this.$refs.loginForm.validate(valid => { |
|||
if (valid) { |
|||
this.loading = true; |
|||
if (this.loginForm.rememberMe) { |
|||
Cookies.set("username", this.loginForm.username, { expires: 30 }); |
|||
Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 }); |
|||
Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 }); |
|||
} else { |
|||
Cookies.remove("username"); |
|||
Cookies.remove("password"); |
|||
Cookies.remove('rememberMe'); |
|||
} |
|||
this.$store.dispatch("Login", this.loginForm).then(() => { |
|||
this.$router.push({ path: this.redirect || "/" }).catch(() => { }); |
|||
}).catch(() => { |
|||
this.loading = false; |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style rel="stylesheet/scss" lang="scss"> |
|||
.login { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
width: 100%; |
|||
height: 100%; |
|||
background-image: url("../assets/images/bg.png"); |
|||
background-size: 100%; |
|||
} |
|||
|
|||
.title { |
|||
margin: 0px auto 30px auto; |
|||
text-align: center; |
|||
color: #0A1146; |
|||
} |
|||
|
|||
.login-form { |
|||
border-radius: 6px; |
|||
background: #ffffff; |
|||
width: 400px; |
|||
padding: 25px 25px 5px 25px; |
|||
|
|||
.el-input { |
|||
height: 38px; |
|||
|
|||
input { |
|||
height: 38px; |
|||
} |
|||
} |
|||
|
|||
.input-icon { |
|||
height: 39px; |
|||
width: 14px; |
|||
margin-left: 2px; |
|||
} |
|||
} |
|||
|
|||
.login-tip { |
|||
font-size: 13px; |
|||
text-align: center; |
|||
color: #bfbfbf; |
|||
} |
|||
|
|||
.login-code { |
|||
width: 33%; |
|||
height: 38px; |
|||
float: right; |
|||
|
|||
img { |
|||
cursor: pointer; |
|||
vertical-align: middle; |
|||
} |
|||
} |
|||
|
|||
.el-login-footer { |
|||
height: 40px; |
|||
line-height: 40px; |
|||
position: fixed; |
|||
bottom: 0; |
|||
width: 100%; |
|||
text-align: center; |
|||
color: #fff; |
|||
font-family: Arial; |
|||
font-size: 12px; |
|||
letter-spacing: 1px; |
|||
} |
|||
|
|||
.login-code-img { |
|||
height: 38px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,261 @@ |
|||
<template> |
|||
<el-card class="profile-settings-card" shadow="never"> |
|||
<div slot="header" class="clearfix"> |
|||
<span>基本资料</span> |
|||
</div> |
|||
|
|||
<el-tabs v-model="activeTab"> |
|||
<el-tab-pane label="基本资料" name="basic"> |
|||
<el-form |
|||
ref="profileForm" |
|||
:model="profileForm" |
|||
:rules="profileRules" |
|||
label-width="120px" |
|||
size="small" |
|||
> |
|||
<el-row :gutter="20"> |
|||
<el-col :span="12"> |
|||
<el-form-item label="登录账户"> |
|||
<el-input v-model="profileForm.account" disabled /> |
|||
</el-form-item> |
|||
</el-col> |
|||
<el-col :span="12"> |
|||
<el-form-item label="所属企业"> |
|||
<el-input v-model="profileForm.businessName" disabled /> |
|||
</el-form-item> |
|||
</el-col> |
|||
</el-row> |
|||
|
|||
<el-row :gutter="20"> |
|||
<el-col :span="12"> |
|||
<el-form-item label="昵称" prop="nickName"> |
|||
<el-input v-model="profileForm.nickName" maxlength="30" /> |
|||
</el-form-item> |
|||
</el-col> |
|||
<el-col :span="12"> |
|||
<el-form-item label="谷歌地图 Key"> |
|||
<el-input |
|||
v-model="profileForm.googleKey" |
|||
:disabled="!profileForm.canEditBusinessConfig" |
|||
placeholder="请输入谷歌地图 Key" |
|||
/> |
|||
</el-form-item> |
|||
</el-col> |
|||
</el-row> |
|||
|
|||
<el-row :gutter="20"> |
|||
<el-col :span="12"> |
|||
<el-form-item label="高德地图 Key"> |
|||
<el-input |
|||
v-model="profileForm.gaodeKey" |
|||
:disabled="!profileForm.canEditBusinessConfig" |
|||
placeholder="请输入高德地图 Key" |
|||
/> |
|||
</el-form-item> |
|||
</el-col> |
|||
<el-col :span="12"> |
|||
<el-form-item label="高德安全密钥"> |
|||
<el-input |
|||
v-model="profileForm.gaodeSecurityKey" |
|||
:disabled="!profileForm.canEditBusinessConfig" |
|||
placeholder="请输入高德安全密钥" |
|||
/> |
|||
</el-form-item> |
|||
</el-col> |
|||
</el-row> |
|||
|
|||
<el-alert |
|||
v-if="!profileForm.canEditBusinessConfig" |
|||
type="info" |
|||
:closable="false" |
|||
show-icon |
|||
title="当前账号只能修改自己的昵称,企业地图 Key 仅企业管理员可修改。" |
|||
class="profile-tip" |
|||
/> |
|||
|
|||
<el-form-item> |
|||
<el-button type="primary" :loading="profileSaving" @click="submitProfile"> |
|||
保存资料 |
|||
</el-button> |
|||
<el-button @click="loadProfile">重置</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</el-tab-pane> |
|||
|
|||
<el-tab-pane label="修改密码" name="password"> |
|||
<el-form |
|||
ref="passwordForm" |
|||
:model="passwordForm" |
|||
:rules="passwordRules" |
|||
label-width="120px" |
|||
size="small" |
|||
> |
|||
<el-form-item label="旧密码" prop="oldPassword"> |
|||
<el-input |
|||
v-model="passwordForm.oldPassword" |
|||
type="password" |
|||
show-password |
|||
placeholder="请输入旧密码" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="新密码" prop="newPassword"> |
|||
<el-input |
|||
v-model="passwordForm.newPassword" |
|||
type="password" |
|||
show-password |
|||
placeholder="请输入新密码" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="确认密码" prop="confirmPassword"> |
|||
<el-input |
|||
v-model="passwordForm.confirmPassword" |
|||
type="password" |
|||
show-password |
|||
placeholder="请再次输入新密码" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button type="primary" :loading="passwordSaving" @click="submitPassword"> |
|||
修改密码 |
|||
</el-button> |
|||
<el-button @click="resetPasswordForm">重置</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</el-tab-pane> |
|||
</el-tabs> |
|||
</el-card> |
|||
</template> |
|||
|
|||
<script> |
|||
import { |
|||
getUserProfile, |
|||
updateUserProfile, |
|||
updateUserPwd, |
|||
} from "@/api/system/user"; |
|||
|
|||
function createDefaultProfileForm() { |
|||
return { |
|||
id: undefined, |
|||
businessId: undefined, |
|||
account: "", |
|||
nickName: "", |
|||
businessName: "", |
|||
gaodeKey: "", |
|||
gaodeSecurityKey: "", |
|||
googleKey: "", |
|||
canEditBusinessConfig: false, |
|||
}; |
|||
} |
|||
|
|||
function createDefaultPasswordForm() { |
|||
return { |
|||
oldPassword: "", |
|||
newPassword: "", |
|||
confirmPassword: "", |
|||
}; |
|||
} |
|||
|
|||
export default { |
|||
name: "ProfileSettingsCard", |
|||
data() { |
|||
const confirmPasswordValidator = (rule, value, callback) => { |
|||
if (value !== this.passwordForm.newPassword) { |
|||
callback(new Error("两次输入的新密码不一致")); |
|||
return; |
|||
} |
|||
callback(); |
|||
}; |
|||
|
|||
return { |
|||
activeTab: "basic", |
|||
profileSaving: false, |
|||
passwordSaving: false, |
|||
profileForm: createDefaultProfileForm(), |
|||
passwordForm: createDefaultPasswordForm(), |
|||
profileRules: { |
|||
nickName: [ |
|||
{ required: true, message: "昵称不能为空", trigger: "blur" }, |
|||
], |
|||
}, |
|||
passwordRules: { |
|||
oldPassword: [ |
|||
{ required: true, message: "旧密码不能为空", trigger: "blur" }, |
|||
], |
|||
newPassword: [ |
|||
{ required: true, message: "新密码不能为空", trigger: "blur" }, |
|||
{ min: 6, max: 20, message: "长度在 6 到 20 个字符", trigger: "blur" }, |
|||
], |
|||
confirmPassword: [ |
|||
{ required: true, message: "确认密码不能为空", trigger: "blur" }, |
|||
{ validator: confirmPasswordValidator, trigger: "blur" }, |
|||
], |
|||
}, |
|||
}; |
|||
}, |
|||
created() { |
|||
this.loadProfile(); |
|||
}, |
|||
methods: { |
|||
loadProfile() { |
|||
getUserProfile().then((response) => { |
|||
const data = response && response.data ? response.data : {}; |
|||
this.profileForm = Object.assign(createDefaultProfileForm(), data); |
|||
}); |
|||
}, |
|||
submitProfile() { |
|||
this.$refs.profileForm.validate((valid) => { |
|||
if (!valid) { |
|||
return; |
|||
} |
|||
const payload = { |
|||
nickName: this.profileForm.nickName, |
|||
gaodeKey: this.profileForm.gaodeKey, |
|||
gaodeSecurityKey: this.profileForm.gaodeSecurityKey, |
|||
googleKey: this.profileForm.googleKey, |
|||
}; |
|||
this.profileSaving = true; |
|||
updateUserProfile(payload) |
|||
.then(() => { |
|||
this.$modal.msgSuccess("基本资料已更新"); |
|||
this.loadProfile(); |
|||
}) |
|||
.finally(() => { |
|||
this.profileSaving = false; |
|||
}); |
|||
}); |
|||
}, |
|||
submitPassword() { |
|||
this.$refs.passwordForm.validate((valid) => { |
|||
if (!valid) { |
|||
return; |
|||
} |
|||
this.passwordSaving = true; |
|||
updateUserPwd(this.passwordForm.oldPassword, this.passwordForm.newPassword) |
|||
.then(() => { |
|||
this.$modal.msgSuccess("密码修改成功"); |
|||
this.resetPasswordForm(); |
|||
}) |
|||
.finally(() => { |
|||
this.passwordSaving = false; |
|||
}); |
|||
}); |
|||
}, |
|||
resetPasswordForm() { |
|||
this.passwordForm = createDefaultPasswordForm(); |
|||
this.$nextTick(() => { |
|||
this.$refs.passwordForm && this.$refs.passwordForm.clearValidate(); |
|||
}); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.profile-settings-card { |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
.profile-tip { |
|||
margin-bottom: 18px; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue