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