Browse Source

b端

master
hx 1 week ago
parent
commit
6d388efde0
  1. BIN
      dist/static/img/profile.5528ca1b.png
  2. 758
      src/components/device/TrajectoryDialog.vue
  3. 314
      src/components/device/index.vue
  4. 735
      src/components/user/index.vue
  5. 226
      src/views/login2.vue
  6. 261
      src/views/system/user/components/ProfileSettingsCard.vue

BIN
dist/static/img/profile.5528ca1b.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

758
src/components/device/TrajectoryDialog.vue

@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\"/g, "&quot;")
.replace(/'/g, "&#39;");
},
},
};
</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>

314
src/components/device/index.vue

@ -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>

735
src/components/user/index.vue

@ -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">提示仅允许导入xlsxlsx格式文件</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
// roleIdsroles 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>

226
src/views/login2.vue

@ -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>

261
src/views/system/user/components/ProfileSettingsCard.vue

@ -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…
Cancel
Save