Browse Source

b端国际化 地图

master
hx 2 days ago
parent
commit
83f4ce317f
  1. 2
      package.json
  2. 16
      src/api/system/user.js
  3. 185
      src/components/HeaderSearch/index.vue
  4. 269
      src/components/device/TrajectoryDialog.vue
  5. 6
      src/main.js
  6. 17
      src/utils/dynamicTitle.js
  7. 4
      src/views/device/device/index.vue
  8. 41
      src/views/system/user/components/ProfileSettingsCard.vue
  9. 274
      src/views/system/user/index.vue

2
package.json

@ -1,7 +1,7 @@
{ {
"name": "ruoyi", "name": "ruoyi",
"version": "3.8.9", "version": "3.8.9",
"description": "客户GeoTag管理系统", "description": "2222客户GeoTag管理系统",
"author": "GeoTag", "author": "GeoTag",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

16
src/api/system/user.js

@ -153,3 +153,19 @@ export function listEmployeeUser(query) {
params: query params: query
}) })
} }
export function listUserDevices(userId, query) {
return request({
url: '/business/businessUser/employee/' + userId + '/devices',
method: 'get',
params: query
})
}
export function unbindUserDevices(userId, data) {
return request({
url: '/business/businessUser/employee/' + userId + '/devices/unbind',
method: 'post',
data: data
})
}

185
src/components/HeaderSearch/index.vue

@ -14,7 +14,7 @@
size="large" size="large"
@input="querySearch" @input="querySearch"
prefix-icon="el-icon-search" prefix-icon="el-icon-search"
placeholder="菜单搜索,支持标题、URL模糊查询" :placeholder="$t('app.headerSearch.placeholder')"
clearable clearable
@keyup.enter.native="selectActiveResult" @keyup.enter.native="selectActiveResult"
@keydown.up.native="navigateResult('up')" @keydown.up.native="navigateResult('up')"
@ -23,7 +23,14 @@
</el-input> </el-input>
<el-scrollbar wrap-class="right-scrollbar-wrapper"> <el-scrollbar wrap-class="right-scrollbar-wrapper">
<div class="result-wrap"> <div class="result-wrap">
<div class="search-item" v-for="(item, index) in options" :key="item.path" :style="activeStyle(index)" @mouseenter="activeIndex = index" @mouseleave="activeIndex = -1"> <div
class="search-item"
v-for="(item, index) in options"
:key="item.path"
:style="activeStyle(index)"
@mouseenter="activeIndex = index"
@mouseleave="activeIndex = -1"
>
<div class="left"> <div class="left">
<svg-icon class="menu-icon" :icon-class="item.icon" /> <svg-icon class="menu-icon" :icon-class="item.icon" />
</div> </div>
@ -35,84 +42,84 @@
{{ item.path }} {{ item.path }}
</div> </div>
</div> </div>
<svg-icon icon-class="enter" v-show="index === activeIndex"/> <svg-icon icon-class="enter" v-show="index === activeIndex" />
</div> </div>
</div> </div>
</el-scrollbar> </el-scrollbar>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script> <script>
import Fuse from 'fuse.js/dist/fuse.min.js' import Fuse from "fuse.js/dist/fuse.min.js";
import path from 'path' import path from "path";
import { isHttp } from '@/utils/validate' import { isHttp } from "@/utils/validate";
export default { export default {
name: 'HeaderSearch', name: "HeaderSearch",
data() { data() {
return { return {
search: '', search: "",
options: [], options: [],
searchPool: [], searchPool: [],
activeIndex: -1, activeIndex: -1,
show: false, show: false,
fuse: undefined fuse: undefined,
} };
}, },
computed: { computed: {
theme() { theme() {
return this.$store.state.settings.theme return this.$store.state.settings.theme;
}, },
routes() { routes() {
return this.$store.getters.defaultRoutes return this.$store.getters.defaultRoutes;
} },
}, },
watch: { watch: {
routes() { routes() {
this.searchPool = this.generateRoutes(this.routes) this.searchPool = this.generateRoutes(this.routes);
}, },
searchPool(list) { searchPool(list) {
this.initFuse(list) this.initFuse(list);
} },
}, },
mounted() { mounted() {
this.searchPool = this.generateRoutes(this.routes) this.searchPool = this.generateRoutes(this.routes);
}, },
methods: { methods: {
click() { click() {
this.show = !this.show this.show = !this.show;
if (this.show) { if (this.show) {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus() this.$refs.headerSearchSelectRef && this.$refs.headerSearchSelectRef.focus();
this.options = this.searchPool this.options = this.searchPool;
} }
}, },
close() { close() {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur() this.$refs.headerSearchSelectRef && this.$refs.headerSearchSelectRef.blur();
this.search = '' this.search = "";
this.options = [] this.options = [];
this.show = false this.show = false;
this.activeIndex = -1 this.activeIndex = -1;
}, },
change(val) { change(val) {
const path = val.path const routePath = val.path;
const query = val.query const query = val.query;
if(isHttp(val.path)) { if (isHttp(val.path)) {
// http(s):// // http(s):// path opens in a new window.
const pindex = path.indexOf("http") const pindex = routePath.indexOf("http");
window.open(path.substr(pindex, path.length), "_blank") window.open(routePath.substr(pindex, routePath.length), "_blank");
} else { } else {
if (query) { if (query) {
this.$router.push({ path: path, query: JSON.parse(query) }) this.$router.push({ path: routePath, query: JSON.parse(query) });
} else { } else {
this.$router.push(path) this.$router.push(routePath);
} }
} }
this.search = '' this.search = "";
this.options = [] this.options = [];
this.$nextTick(() => { this.$nextTick(() => {
this.show = false this.show = false;
}) });
}, },
initFuse(list) { initFuse(list) {
this.fuse = new Fuse(list, { this.fuse = new Fuse(list, {
@ -121,87 +128,107 @@ export default {
location: 0, location: 0,
distance: 100, distance: 100,
minMatchCharLength: 1, minMatchCharLength: 1,
keys: [{ keys: [
name: 'title', {
weight: 0.7 name: "title",
}, { weight: 0.7,
name: 'path', },
weight: 0.3 {
}] name: "path",
}) weight: 0.3,
},
],
});
}, },
// Filter out the routes that can be displayed in the sidebar // Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title // And generate the internationalized title
generateRoutes(routes, basePath = '/', prefixTitle = []) { generateRoutes(routes, basePath = "/", prefixTitle = []) {
let res = [] let res = [];
for (const router of routes) { for (const router of routes) {
// skip hidden router // Skip hidden routes.
if (router.hidden) { continue } if (router.hidden) {
continue;
}
const data = { const data = {
path: !isHttp(router.path) ? path.resolve(basePath, router.path) : router.path, path: !isHttp(router.path) ? path.resolve(basePath, router.path) : router.path,
title: [...prefixTitle], title: [...prefixTitle],
icon: '' icon: "",
} };
if (router.meta && router.meta.title) { const routeTitle = this.resolveRouteTitle(router);
data.title = [...data.title, router.meta.title] if (routeTitle) {
data.icon = router.meta.icon data.title = [...data.title, routeTitle];
data.icon = router.meta && router.meta.icon;
if (router.redirect !== 'noRedirect') { if (router.redirect !== "noRedirect") {
// only push the routes with title // Only push routes with title.
// special case: need to exclude parent router without redirect // Special case: exclude parent route without redirect.
res.push(data) res.push(data);
} }
} }
if (router.query) { if (router.query) {
data.query = router.query data.query = router.query;
} }
// recursive child routes // Recursive child routes.
if (router.children) { if (router.children) {
const tempRoutes = this.generateRoutes(router.children, data.path, data.title) const tempRoutes = this.generateRoutes(router.children, data.path, data.title);
if (tempRoutes.length >= 1) { if (tempRoutes.length >= 1) {
res = [...res, ...tempRoutes] res = [...res, ...tempRoutes];
} }
} }
} }
return res
return res;
},
resolveRouteTitle(router) {
const meta = router && router.meta;
if (!meta) {
return "";
}
if (meta.i18nKey) {
const translated = this.$t(meta.i18nKey);
if (translated && translated !== meta.i18nKey) {
return translated;
}
}
return meta.title || "";
}, },
querySearch(query) { querySearch(query) {
this.activeIndex = -1 this.activeIndex = -1;
if (query !== '') { if (query !== "") {
this.options = this.fuse.search(query).map((item) => item.item) ?? this.searchPool this.options = this.fuse.search(query).map((item) => item.item) ?? this.searchPool;
} else { } else {
this.options = this.searchPool this.options = this.searchPool;
} }
}, },
activeStyle(index) { activeStyle(index) {
if (index !== this.activeIndex) return {} if (index !== this.activeIndex) return {};
return { return {
"background-color": this.theme, "background-color": this.theme,
"color": "#fff" color: "#fff",
} };
}, },
navigateResult(direction) { navigateResult(direction) {
if (direction === "up") { if (direction === "up") {
this.activeIndex = this.activeIndex <= 0 ? this.options.length - 1 : this.activeIndex - 1 this.activeIndex = this.activeIndex <= 0 ? this.options.length - 1 : this.activeIndex - 1;
} else if (direction === "down") { } else if (direction === "down") {
this.activeIndex = this.activeIndex >= this.options.length - 1 ? 0 : this.activeIndex + 1 this.activeIndex = this.activeIndex >= this.options.length - 1 ? 0 : this.activeIndex + 1;
} }
}, },
selectActiveResult() { selectActiveResult() {
if (this.options.length > 0 && this.activeIndex >= 0) { if (this.options.length > 0 && this.activeIndex >= 0) {
this.change(this.options[this.activeIndex]) this.change(this.options[this.activeIndex]);
} }
} },
} },
} };
</script> </script>
<style lang='scss' scoped> <style lang="scss" scoped>
::v-deep { ::v-deep {
.el-dialog__header { .el-dialog__header {
padding: 0 !important; padding: 0 !important;
@ -249,6 +276,7 @@ export default {
.menu-path { .menu-path {
height: 20px; height: 20px;
} }
.menu-path { .menu-path {
color: #ccc; color: #ccc;
font-size: 10px; font-size: 10px;
@ -261,4 +289,3 @@ export default {
} }
} }
</style> </style>

269
src/components/device/TrajectoryDialog.vue

@ -54,6 +54,9 @@
<div class="trajectory-toolbar"> <div class="trajectory-toolbar">
<span class="page-inline-filter__label">{{ $t("device.trajectory.provider.label") }}</span> <span class="page-inline-filter__label">{{ $t("device.trajectory.provider.label") }}</span>
<el-radio-group v-model="mapProvider" size="small" @change="handleProviderChange"> <el-radio-group v-model="mapProvider" size="small" @change="handleProviderChange">
<el-radio-button label="maptiler" :disabled="!hasMaptilerKey">
{{ $t("device.trajectory.provider.maptiler") }}
</el-radio-button>
<el-radio-button label="amap" :disabled="!hasAmapKey"> <el-radio-button label="amap" :disabled="!hasAmapKey">
{{ $t("device.trajectory.provider.amap") }} {{ $t("device.trajectory.provider.amap") }}
</el-radio-button> </el-radio-button>
@ -111,9 +114,17 @@
import { getDeviceTrajectory, getDeviceTrajectoryMapConfig } from "@/api/device/device"; import { getDeviceTrajectory, getDeviceTrajectoryMapConfig } from "@/api/device/device";
import { loadAMap } from "@/utils/loadAMap"; import { loadAMap } from "@/utils/loadAMap";
import { loadGoogleMaps } from "@/utils/loadGoogleMaps"; import { loadGoogleMaps } from "@/utils/loadGoogleMaps";
import { loadLeaflet } from "@/utils/loadLeaflet";
const AMAP_DEFAULT_CENTER = [121.4737, 31.2304]; const AMAP_DEFAULT_CENTER = [121.4737, 31.2304];
const GOOGLE_DEFAULT_CENTER = { lat: 31.2304, lng: 121.4737 }; const GOOGLE_DEFAULT_CENTER = { lat: 31.2304, lng: 121.4737 };
const LEAFLET_DEFAULT_CENTER = [31.2304, 121.4737];
const AMAP_FALLBACK_MAX_ZOOM = 20;
const GOOGLE_DETAIL_ZOOM = 21;
const LEAFLET_MAX_ZOOM = 22;
const LEAFLET_LINE_ARROW_MAX = 36;
const LEAFLET_SHOW_LINE_ARROWS = false;
const LEAFLET_TRAJECTORY_LINE_COLOR = "#1a73e8";
const CONVERT_BATCH_SIZE = 40; const CONVERT_BATCH_SIZE = 40;
const MAX_TRAJECTORY_POINTS = 100; const MAX_TRAJECTORY_POINTS = 100;
@ -143,6 +154,7 @@ export default {
polyline: null, polyline: null,
markers: [], markers: [],
markerInfoWindows: [], markerInfoWindows: [],
tileLayer: null,
mapsApi: null, mapsApi: null,
mapConfig: null, mapConfig: null,
locationTimeRange: [], locationTimeRange: [],
@ -169,6 +181,9 @@ export default {
hasGoogleKey() { hasGoogleKey() {
return !!(this.mapConfig && this.mapConfig.googleKey); return !!(this.mapConfig && this.mapConfig.googleKey);
}, },
hasMaptilerKey() {
return !!(this.mapConfig && this.mapConfig.maptilerKey);
},
}, },
methods: { methods: {
async handleOpen() { async handleOpen() {
@ -213,12 +228,16 @@ export default {
await this.renderCurrentProviderMap(); await this.renderCurrentProviderMap();
}, },
resolveDefaultProvider() { resolveDefaultProvider() {
if (this.hasMaptilerKey) {
return "maptiler";
}
if (this.hasAmapKey) { if (this.hasAmapKey) {
return "amap"; return "amap";
} }
if (this.hasGoogleKey) { if (this.hasGoogleKey) {
return "google"; return "google";
} }
return "amap"; return "amap";
}, },
initDefaultLocationTimeRange() { initDefaultLocationTimeRange() {
@ -267,7 +286,7 @@ export default {
try { try {
const response = await getDeviceTrajectoryMapConfig(); const response = await getDeviceTrajectoryMapConfig();
const data = response && response.data ? response.data : {}; const data = response && response.data ? response.data : {};
if (!data.gaodeKey && !data.googleKey) { if (!data.gaodeKey && !data.googleKey && !data.maptilerKey) {
this.loadError = this.$t("device.trajectory.message.missingMapKey"); this.loadError = this.$t("device.trajectory.message.missingMapKey");
return; return;
} }
@ -294,8 +313,11 @@ export default {
}; };
}) })
.filter((item) => item.latNum !== null && item.lngNum !== null); .filter((item) => item.latNum !== null && item.lngNum !== null);
this.totalTrajectoryCount = validPoints.length; const sortedPoints = validPoints.slice().sort((left, right) => {
this.trajectoryPoints = validPoints.slice(-MAX_TRAJECTORY_POINTS); return this.getTrackTimestamp(left) - this.getTrackTimestamp(right);
});
this.totalTrajectoryCount = sortedPoints.length;
this.trajectoryPoints = sortedPoints.slice(-MAX_TRAJECTORY_POINTS);
} catch (error) { } catch (error) {
this.trajectoryPoints = []; this.trajectoryPoints = [];
this.totalTrajectoryCount = 0; this.totalTrajectoryCount = 0;
@ -310,6 +332,13 @@ export default {
} }
this.loadError = ""; this.loadError = "";
await this.$nextTick(); await this.$nextTick();
if (this.mapProvider === "maptiler") {
await this.ensureLeafletMap();
if (!this.loadError) {
this.renderLeafletTrajectory();
}
return;
}
if (this.mapProvider === "google") { if (this.mapProvider === "google") {
await this.ensureGoogleMap(); await this.ensureGoogleMap();
if (!this.loadError) { if (!this.loadError) {
@ -323,6 +352,211 @@ export default {
this.renderAmapTrajectory(); this.renderAmapTrajectory();
} }
}, },
async ensureLeafletMap() {
if (!this.hasMaptilerKey) {
this.loadError = this.$t("device.trajectory.message.missingMaptilerKey");
return;
}
this.mapLoading = true;
try {
const mapsApi = await loadLeaflet();
if (!this.$refs.map) {
return;
}
if (!this.map || this.mapVendor !== "leaflet") {
this.destroyMap();
this.mapsApi = mapsApi;
this.map = mapsApi.map(this.$refs.map, {
center: LEAFLET_DEFAULT_CENTER,
zoom: LEAFLET_MAX_ZOOM,
zoomControl: true,
});
this.tileLayer = mapsApi.tileLayer(
"https://api.maptiler.com/maps/streets/{z}/{x}/{y}{r}.png?key=" +
encodeURIComponent(this.mapConfig.maptilerKey),
{
attribution:
'&copy; <a href="https://www.maptiler.com/copyright/" target="_blank">MapTiler</a> &copy; <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>',
maxZoom: LEAFLET_MAX_ZOOM,
maxNativeZoom: LEAFLET_MAX_ZOOM,
detectRetina: true,
}
);
this.tileLayer.addTo(this.map);
this.mapVendor = "leaflet";
} else {
this.mapsApi = mapsApi;
if (typeof this.map.invalidateSize === "function") {
this.map.invalidateSize();
}
}
} catch (error) {
this.loadError = error && error.message ? error.message : this.$t("device.trajectory.message.maptilerLoadFailed");
} finally {
this.mapLoading = false;
}
},
renderLeafletTrajectory() {
if (!this.map || !this.mapsApi || !this.trajectoryPoints.length) {
return;
}
const mapPoints = this.getMapOrderedTrajectoryPoints();
if (!mapPoints.length) {
return;
}
this.clearOverlays();
const path = mapPoints.map((item) => [item.latNum, item.lngNum]);
this.polyline = this.createLeafletTrajectoryLayer(path);
if (this.polyline) {
this.polyline.addTo(this.map);
}
const startItem = mapPoints[0];
const endItem = mapPoints[mapPoints.length - 1];
this.markers = [];
if (path.length > 1) {
const startAngle = this.getLeafletDirectionAngle(path[0], path[1]);
const endAngle = this.getLeafletDirectionAngle(path[path.length - 2], path[path.length - 1]);
const startMarker = this.createLeafletDirectionMarker(
path[0],
startAngle,
this.$t("device.trajectory.marker.startShort"),
"#67c23a",
this.$t("device.trajectory.message.startTime"),
this.getPointTrackTime(startItem)
);
const endMarker = this.createLeafletDirectionMarker(
path[path.length - 1],
endAngle,
this.$t("device.trajectory.marker.endShort"),
"#f56c6c",
this.$t("device.trajectory.message.endTime"),
this.getPointTrackTime(endItem)
);
this.markers.push(startMarker, endMarker);
if (LEAFLET_SHOW_LINE_ARROWS) {
this.addLeafletLineArrows(path);
}
} else {
const startMarker = this.mapsApi.marker(path[0], {
title: this.$t("device.trajectory.marker.startShort"),
});
startMarker
.addTo(this.map)
.bindPopup(
`<b>${this.escapeHtml(this.$t("device.trajectory.message.startTime"))}</b><br/>${this.escapeHtml(
this.getPointTrackTime(startItem)
)}`
);
this.markers.push(startMarker);
}
const latestPoint = path[path.length - 1];
this.map.setView(latestPoint, LEAFLET_MAX_ZOOM);
},
createLeafletDirectionMarker(point, angle, shortLabel, color, timeLabel, timeText) {
const icon = this.mapsApi.divIcon({
className: "leaflet-trajectory-arrow",
iconSize: [30, 30],
iconAnchor: [15, 15],
html: this.buildLeafletDirectionHtml(angle, color),
});
const marker = this.mapsApi.marker(point, {
icon,
title: shortLabel,
});
marker
.addTo(this.map)
.bindPopup(`<b>${this.escapeHtml(timeLabel)}</b><br/>${this.escapeHtml(timeText || "-")}`);
return marker;
},
addLeafletLineArrows(path) {
if (!Array.isArray(path) || path.length < 2) {
return;
}
const segmentCount = path.length - 1;
const step = Math.max(1, Math.ceil(segmentCount / LEAFLET_LINE_ARROW_MAX));
for (let i = step; i < path.length; i += step) {
const fromPoint = path[i - 1];
const toPoint = path[i];
const marker = this.createLeafletLineArrowMarker(fromPoint, toPoint);
if (marker) {
this.markers.push(marker);
}
}
},
createLeafletLineArrowMarker(fromPoint, toPoint) {
if (!Array.isArray(fromPoint) || !Array.isArray(toPoint)) {
return null;
}
const midPoint = [
(Number(fromPoint[0]) + Number(toPoint[0])) / 2,
(Number(fromPoint[1]) + Number(toPoint[1])) / 2,
];
if (!Number.isFinite(midPoint[0]) || !Number.isFinite(midPoint[1])) {
return null;
}
const angle = this.getLeafletDirectionAngle(fromPoint, toPoint);
const icon = this.mapsApi.divIcon({
className: "leaflet-trajectory-line-arrow",
iconSize: [20, 20],
iconAnchor: [10, 10],
html:
`<div style="width:20px;height:20px;display:flex;align-items:center;justify-content:center;transform:rotate(${angle}deg);">` +
'<span style="display:inline-block;font-size:14px;line-height:14px;color:#1a73e8;text-shadow:0 1px 2px rgba(255,255,255,0.9);">&#10148;</span>' +
"</div>",
});
const marker = this.mapsApi.marker(midPoint, {
icon,
interactive: false,
keyboard: false,
});
marker.addTo(this.map);
return marker;
},
getLeafletDirectionAngle(fromPoint, toPoint) {
if (!Array.isArray(fromPoint) || !Array.isArray(toPoint)) {
return 0;
}
const deltaLat = Number(toPoint[0]) - Number(fromPoint[0]);
const deltaLng = Number(toPoint[1]) - Number(fromPoint[1]);
if (!Number.isFinite(deltaLat) || !Number.isFinite(deltaLng) || (deltaLat === 0 && deltaLng === 0)) {
return 0;
}
return (Math.atan2(deltaLat, deltaLng) * 180) / Math.PI;
},
buildLeafletDirectionHtml(angle, color) {
return (
`<div style="width:30px;height:30px;display:flex;align-items:center;justify-content:center;transform:rotate(${angle}deg);">` +
`<span style="display:inline-block;font-size:20px;line-height:20px;color:${this.escapeHtml(color)};text-shadow:0 1px 4px rgba(0,0,0,0.35);">&#10148;</span>` +
"</div>"
);
},
createLeafletTrajectoryLayer(path) {
if (!Array.isArray(path) || !path.length) {
return null;
}
return this.mapsApi.polyline(path, {
color: LEAFLET_TRAJECTORY_LINE_COLOR,
opacity: 0.95,
weight: 4,
lineJoin: "round",
});
},
getAmapMaxZoom() {
if (!this.map || typeof this.map.getZooms !== "function") {
return AMAP_FALLBACK_MAX_ZOOM;
}
const zoomRange = this.map.getZooms();
if (!Array.isArray(zoomRange) || zoomRange.length < 2) {
return AMAP_FALLBACK_MAX_ZOOM;
}
const maxZoom = Number(zoomRange[1]);
return Number.isFinite(maxZoom) ? maxZoom : AMAP_FALLBACK_MAX_ZOOM;
},
async ensureAmap() { async ensureAmap() {
if (!this.hasAmapKey) { if (!this.hasAmapKey) {
this.loadError = this.$t("device.trajectory.message.missingAmapKey"); this.loadError = this.$t("device.trajectory.message.missingAmapKey");
@ -515,11 +749,8 @@ export default {
this.map.add(this.polyline); this.map.add(this.polyline);
this.map.add(this.markers); this.map.add(this.markers);
if (path.length === 1) { const detailZoom = this.getAmapMaxZoom();
this.map.setZoomAndCenter(15, startPoint); this.map.setZoomAndCenter(detailZoom, endPoint);
} else {
this.map.setFitView([this.polyline].concat(this.markers), false, [60, 60, 60, 60]);
}
}, },
renderGoogleTrajectory() { renderGoogleTrajectory() {
if (!this.map || !this.mapsApi || !this.trajectoryPoints.length) { if (!this.map || !this.mapsApi || !this.trajectoryPoints.length) {
@ -531,8 +762,6 @@ export default {
} }
this.clearOverlays(); this.clearOverlays();
const path = mapPoints.map((item) => ({ lat: item.latNum, lng: item.lngNum })); const path = mapPoints.map((item) => ({ lat: item.latNum, lng: item.lngNum }));
const bounds = new this.mapsApi.LatLngBounds();
path.forEach((point) => bounds.extend(point));
const polylineOptions = { const polylineOptions = {
path, path,
@ -603,12 +832,8 @@ export default {
} }
} }
if (path.length === 1) { this.map.setCenter(endPoint);
this.map.setCenter(startPoint); this.map.setZoom(GOOGLE_DETAIL_ZOOM);
this.map.setZoom(15);
} else {
this.map.fitBounds(bounds);
}
}, },
openGoogleMarkerInfoWindow(marker, title, timeText) { openGoogleMarkerInfoWindow(marker, title, timeText) {
if (!this.mapsApi || !this.map || !marker || !this.mapsApi.InfoWindow) { if (!this.mapsApi || !this.map || !marker || !this.mapsApi.InfoWindow) {
@ -667,6 +892,13 @@ export default {
} }
}); });
} }
} else if (this.mapVendor === "leaflet") {
if (this.polyline && typeof this.map.removeLayer === "function") {
this.map.removeLayer(this.polyline);
}
if (this.markers.length && typeof this.map.removeLayer === "function") {
this.markers.forEach((marker) => this.map.removeLayer(marker));
}
} else { } else {
if (this.polyline) { if (this.polyline) {
this.map.remove(this.polyline); this.map.remove(this.polyline);
@ -690,10 +922,13 @@ export default {
} }
if (this.mapVendor === "amap" && typeof this.map.destroy === "function") { if (this.mapVendor === "amap" && typeof this.map.destroy === "function") {
this.map.destroy(); this.map.destroy();
} else if (this.mapVendor === "leaflet" && typeof this.map.remove === "function") {
this.map.remove();
} else if (this.$refs.map) { } else if (this.$refs.map) {
this.$refs.map.innerHTML = ""; this.$refs.map.innerHTML = "";
} }
this.map = null; this.map = null;
this.tileLayer = null;
if (resetVendor) { if (resetVendor) {
this.mapVendor = ""; this.mapVendor = "";
this.mapsApi = null; this.mapsApi = null;
@ -817,4 +1052,4 @@ export default {
width: 100%; width: 100%;
height: 420px; height: 420px;
} }
</style> </style>

6
src/main.js

@ -20,7 +20,7 @@ import directive from "./directive"; // directive
import plugins from "./plugins"; // plugins import plugins from "./plugins"; // plugins
import { download } from "@/utils/ruoyi"; import { download } from "@/utils/ruoyi";
import { math } from "@/utils/math.js"; import { math } from "@/utils/math.js";
import I18nPlugin from "@/lang"; import I18nPlugin, { t as i18nT } from "@/lang";
import { getLanguage } from "@/utils/language"; import { getLanguage } from "@/utils/language";
const ELEMENT_LOCALE_MAP = { const ELEMENT_LOCALE_MAP = {
@ -125,6 +125,10 @@ Vue.config.productionTip = false;
if (typeof document !== "undefined") { if (typeof document !== "undefined") {
document.documentElement.setAttribute("lang", currentLanguage); document.documentElement.setAttribute("lang", currentLanguage);
const appTitle = i18nT("app.sidebarTitle");
document.title = appTitle && appTitle !== "app.sidebarTitle"
? appTitle
: process.env.VUE_APP_TITLE;
} }
new Vue({ new Vue({

17
src/utils/dynamicTitle.js

@ -1,13 +1,24 @@
import store from '@/store' import store from '@/store'
import defaultSettings from '@/settings' import defaultSettings from '@/settings'
import { t as i18nT } from '@/lang'
function resolveAppTitle() {
const translated = i18nT('app.sidebarTitle')
return translated && translated !== 'app.sidebarTitle'
? translated
: defaultSettings.title
}
/** /**
* 动态修改标题 * 动态修改标题
*/ */
export function useDynamicTitle() { export function useDynamicTitle() {
const appTitle = resolveAppTitle()
if (store.state.settings.dynamicTitle) { if (store.state.settings.dynamicTitle) {
document.title = store.state.settings.title + ' - ' + defaultSettings.title document.title = store.state.settings.title
? (store.state.settings.title + ' - ' + appTitle)
: appTitle
} else { } else {
document.title = defaultSettings.title document.title = appTitle
} }
} }

4
src/views/device/device/index.vue

@ -69,10 +69,10 @@
<el-button type="success" plain icon="el-icon-circle-check" size="mini" :disabled="multiple" v-hasPermi="['device:device:activate:batch']" @click="handleBatchActivate">{{ $t("device.button.batchEnable") }}</el-button> <el-button type="success" plain icon="el-icon-circle-check" size="mini" :disabled="multiple" v-hasPermi="['device:device:activate:batch']" @click="handleBatchActivate">{{ $t("device.button.batchEnable") }}</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-circle-close" size="mini" :disabled="multiple" @click="handleBatchDisable">{{ $t("device.button.batchDisable") }}</el-button> <el-button type="danger" plain icon="el-icon-circle-close" size="mini" :disabled="multiple" @click="handleBatchDisable" v-hasPermi="['device:device:activate:batch']">{{ $t("device.button.batchDisable") }}</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="info" plain icon="el-icon-user" size="mini" :disabled="multiple" v-hasPermi="['device:device:activate:batch']" @click="handleAssignDevice">{{ $t("device.button.assign") }}</el-button> <el-button type="info" plain icon="el-icon-user" size="mini" :disabled="multiple" v-hasPermi="['business:businessUser:assign:devices']" @click="handleAssignDevice">{{ $t("device.button.assign") }}</el-button>
</el-col> </el-col>

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

@ -32,23 +32,21 @@
<el-input v-model="profileForm.nickName" maxlength="30" /> <el-input v-model="profileForm.nickName" maxlength="30" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col v-if="canEditMapConfig" :span="12">
<el-form-item :label="$t('profile.form.googleKey')"> <el-form-item :label="$t('profile.form.googleKey')">
<el-input <el-input
v-model="profileForm.googleKey" v-model="profileForm.googleKey"
:disabled="!profileForm.canEditBusinessConfig"
:placeholder="$t('profile.placeholder.googleKey')" :placeholder="$t('profile.placeholder.googleKey')"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20"> <el-row v-if="canEditMapConfig" :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item :label="$t('profile.form.gaodeKey')"> <el-form-item :label="$t('profile.form.gaodeKey')">
<el-input <el-input
v-model="profileForm.gaodeKey" v-model="profileForm.gaodeKey"
:disabled="!profileForm.canEditBusinessConfig"
:placeholder="$t('profile.placeholder.gaodeKey')" :placeholder="$t('profile.placeholder.gaodeKey')"
/> />
</el-form-item> </el-form-item>
@ -57,22 +55,12 @@
<el-form-item :label="$t('profile.form.gaodeSecurityKey')"> <el-form-item :label="$t('profile.form.gaodeSecurityKey')">
<el-input <el-input
v-model="profileForm.gaodeSecurityKey" v-model="profileForm.gaodeSecurityKey"
:disabled="!profileForm.canEditBusinessConfig"
:placeholder="$t('profile.placeholder.gaodeSecurityKey')" :placeholder="$t('profile.placeholder.gaodeSecurityKey')"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-alert
v-if="!profileForm.canEditBusinessConfig"
type="info"
:closable="false"
show-icon
:title="$t('profile.tip.businessConfigReadonly')"
class="profile-tip"
/>
<el-form-item> <el-form-item>
<el-button type="primary" :loading="profileSaving" @click="submitProfile"> <el-button type="primary" :loading="profileSaving" @click="submitProfile">
{{ $t("profile.button.saveProfile") }} {{ $t("profile.button.saveProfile") }}
@ -137,6 +125,7 @@ function createDefaultProfileForm() {
return { return {
id: undefined, id: undefined,
businessId: undefined, businessId: undefined,
parentId: undefined,
account: "", account: "",
nickName: "", nickName: "",
businessName: "", businessName: "",
@ -168,6 +157,18 @@ export default {
passwordRules: {}, passwordRules: {},
}; };
}, },
computed: {
isEmployeeByParentId() {
const parentId = this.profileForm && this.profileForm.parentId;
if (parentId === undefined || parentId === null || parentId === "") {
return false;
}
return String(parentId) !== "0";
},
canEditMapConfig() {
return !this.isEmployeeByParentId;
},
},
created() { created() {
this.initI18nState(); this.initI18nState();
this.loadProfile(); this.loadProfile();
@ -213,10 +214,12 @@ export default {
} }
const payload = { const payload = {
nickName: this.profileForm.nickName, nickName: this.profileForm.nickName,
gaodeKey: this.profileForm.gaodeKey,
gaodeSecurityKey: this.profileForm.gaodeSecurityKey,
googleKey: this.profileForm.googleKey,
}; };
if (this.canEditMapConfig) {
payload.gaodeKey = this.profileForm.gaodeKey;
payload.gaodeSecurityKey = this.profileForm.gaodeSecurityKey;
payload.googleKey = this.profileForm.googleKey;
}
this.profileSaving = true; this.profileSaving = true;
updateUserProfile(payload) updateUserProfile(payload)
.then(() => { .then(() => {
@ -258,8 +261,4 @@ export default {
.profile-settings-card { .profile-settings-card {
margin-bottom: 16px; margin-bottom: 16px;
} }
.profile-tip {
margin-bottom: 18px;
}
</style> </style>

274
src/views/system/user/index.vue

@ -129,10 +129,20 @@
<el-table-column <el-table-column
:label="$t('systemUser.table.actions')" :label="$t('systemUser.table.actions')"
align="center" align="center"
width="160" width="260"
class-name="small-padding fixed-width" class-name="small-padding fixed-width"
> >
<template slot-scope="scope"> <template slot-scope="scope">
<!-- <el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['system:user:remove']"
>
{{ $t("common.remove") }}
</el-button> -->
<el-button <el-button
size="mini" size="mini"
type="text" type="text"
@ -151,6 +161,15 @@
> >
{{ $t("systemUser.button.resetPassword") }} {{ $t("systemUser.button.resetPassword") }}
</el-button> </el-button>
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleViewDevices(scope.row)"
v-hasPermi="['business:businessUser:list']"
>
{{ $t("systemUser.button.viewDevices") }}
</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -251,6 +270,123 @@
</div> </div>
</el-dialog> </el-dialog>
<el-dialog
:title="$t('systemUser.deviceDialog.title')"
:visible.sync="userDeviceOpen"
width="980px"
append-to-body
@close="resetUserDeviceDialog"
>
<div class="page-footer-meta">
<strong>{{ $t("systemUser.deviceDialog.selectedUser") }}:</strong>
<span>{{ getUserDeviceOwnerLabel() }}</span>
</div>
<el-form
ref="userDeviceQueryForm"
:model="userDeviceQueryParams"
:inline="true"
class="page-query-form"
label-position="top"
@submit.native.prevent="handleUserDeviceQuery"
>
<el-form-item :label="$t('systemUser.deviceDialog.query.sn')" prop="sn">
<el-input
v-model="userDeviceQueryParams.sn"
:placeholder="$t('systemUser.deviceDialog.placeholder.sn')"
clearable
size="small"
@keyup.enter.native="handleUserDeviceQuery"
/>
</el-form-item>
<el-form-item :label="$t('systemUser.deviceDialog.query.model')" prop="model">
<el-input
v-model="userDeviceQueryParams.model"
:placeholder="$t('systemUser.deviceDialog.placeholder.model')"
clearable
size="small"
@keyup.enter.native="handleUserDeviceQuery"
/>
</el-form-item>
<el-form-item :label="$t('systemUser.deviceDialog.query.orderCode')" prop="orderCode">
<el-input
v-model="userDeviceQueryParams.orderCode"
:placeholder="$t('systemUser.deviceDialog.placeholder.orderCode')"
clearable
size="small"
@keyup.enter.native="handleUserDeviceQuery"
/>
</el-form-item>
<el-form-item :label="$t('systemUser.deviceDialog.query.alias')" prop="alias">
<el-input
v-model="userDeviceQueryParams.alias"
:placeholder="$t('systemUser.deviceDialog.placeholder.alias')"
clearable
size="small"
@keyup.enter.native="handleUserDeviceQuery"
/>
</el-form-item>
<el-form-item class="page-query-actions">
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleUserDeviceQuery">{{ $t("common.search") }}</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetUserDeviceQuery">{{ $t("common.reset") }}</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8 page-toolbar">
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-link"
size="mini"
:disabled="userDeviceMultiple"
@click="handleUnbindUserDevices"
v-hasPermi="['business:businessUser:assign:devices']"
>
{{ $t("systemUser.button.unbindDevices") }}
</el-button>
</el-col>
</el-row>
<el-table
ref="userDeviceTable"
v-loading="userDeviceLoading"
:data="userDeviceList"
@selection-change="handleUserDeviceSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column :label="$t('systemUser.deviceDialog.table.id')" align="center" prop="id" width="90" />
<el-table-column :label="$t('systemUser.deviceDialog.table.sn')" align="center" prop="sn" :show-overflow-tooltip="true" />
<el-table-column :label="$t('systemUser.deviceDialog.table.model')" align="center" prop="model" :show-overflow-tooltip="true" />
<el-table-column :label="$t('systemUser.deviceDialog.table.orderCode')" align="center" prop="orderCode" :show-overflow-tooltip="true" />
<el-table-column :label="$t('systemUser.deviceDialog.table.alias')" align="center" prop="alias" :show-overflow-tooltip="true" />
<el-table-column :label="$t('systemUser.deviceDialog.table.activationStatus')" align="center" width="130">
<template slot-scope="scope">
<el-tag :type="scope.row.activationStatus ? 'success' : 'info'" size="small">
{{ scope.row.activationStatus ? $t("systemUser.deviceDialog.status.activated") : $t("systemUser.deviceDialog.status.notActivated") }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('systemUser.deviceDialog.table.lastLocationTime')" align="center" prop="lastLocationTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.lastLocationTime) }}</span>
</template>
</el-table-column>
</el-table>
<pagination
v-show="userDeviceTotal > 0"
:total="userDeviceTotal"
:page.sync="userDeviceQueryParams.pageNum"
:limit.sync="userDeviceQueryParams.pageSize"
@pagination="getUserDeviceList"
/>
<div slot="footer" class="dialog-footer">
<el-button @click="userDeviceOpen = false">{{ $t("common.cancel") }}</el-button>
</div>
</el-dialog>
<el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body> <el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
<el-upload <el-upload
ref="upload" ref="upload"
@ -284,7 +420,7 @@
</template> </template>
<script> <script>
import { listUser, getUser, delUser, addUser, addEmployeeUser, updateEmployeeUser, exportUser, resetUserPwd, changeUserStatus, importTemplate } from "@/api/system/user"; import { listUser, getUser, delUser, addUser, addEmployeeUser, updateEmployeeUser, exportUser, resetUserPwd, changeUserStatus, importTemplate, listUserDevices, unbindUserDevices } from "@/api/system/user";
import { currentRoleOptions } from "@/api/system/role"; import { currentRoleOptions } from "@/api/system/role";
import { getToken } from "@/utils/auth"; import { getToken } from "@/utils/auth";
import Treeselect from "@riophae/vue-treeselect"; import Treeselect from "@riophae/vue-treeselect";
@ -302,6 +438,18 @@ export default {
showSearch: true, showSearch: true,
total: 0, total: 0,
userList: null, userList: null,
userDeviceLoading: false,
userDeviceOpen: false,
userDeviceTotal: 0,
userDeviceList: [],
userDeviceIds: [],
userDeviceSingle: true,
userDeviceMultiple: true,
currentUserDeviceOwner: {
id: undefined,
account: undefined,
nickName: undefined,
},
title: "", title: "",
deptOptions: undefined, deptOptions: undefined,
open: false, open: false,
@ -343,6 +491,14 @@ export default {
status: undefined, status: undefined,
deptId: undefined, deptId: undefined,
}, },
userDeviceQueryParams: {
pageNum: 1,
pageSize: 10,
sn: undefined,
model: undefined,
orderCode: undefined,
alias: undefined,
},
columns: [], columns: [],
rules: {}, rules: {},
employeeRules: {}, employeeRules: {},
@ -427,6 +583,120 @@ export default {
this.loading = false; this.loading = false;
}); });
}, },
getUserDeviceOwnerLabel() {
const nickName = this.currentUserDeviceOwner.nickName;
const account = this.currentUserDeviceOwner.account;
if (nickName && account) {
return `${nickName} (${account})`;
}
return nickName || account || "-";
},
getUserDeviceList() {
if (!this.currentUserDeviceOwner.id) {
this.userDeviceList = [];
this.userDeviceTotal = 0;
return;
}
this.userDeviceLoading = true;
listUserDevices(this.currentUserDeviceOwner.id, this.userDeviceQueryParams)
.then((response) => {
this.userDeviceList = (response.data.list || []).map((item) => ({
...item,
activationStatus: this.normalizeBooleanStatus(item.activationStatus),
}));
this.userDeviceTotal = Number(response.data.total) || 0;
})
.finally(() => {
this.userDeviceLoading = false;
});
},
handleViewDevices(row) {
this.currentUserDeviceOwner = {
id: row.id,
account: row.account,
nickName: row.nickName,
};
this.userDeviceQueryParams.pageNum = 1;
this.userDeviceOpen = true;
this.getUserDeviceList();
},
handleUserDeviceQuery() {
this.userDeviceQueryParams.pageNum = 1;
this.getUserDeviceList();
},
resetUserDeviceQuery() {
this.userDeviceQueryParams = {
pageNum: 1,
pageSize: 10,
sn: undefined,
model: undefined,
orderCode: undefined,
alias: undefined,
};
this.resetForm("userDeviceQueryForm");
this.getUserDeviceList();
},
handleUserDeviceSelectionChange(selection) {
this.userDeviceIds = selection.map((item) => item.id);
this.userDeviceSingle = selection.length !== 1;
this.userDeviceMultiple = !selection.length;
},
handleUnbindUserDevices() {
if (!this.userDeviceIds.length) {
this.$modal.msgError(this.$t("systemUser.deviceDialog.message.selectDeviceFirst"));
return;
}
const ownerLabel = this.currentUserDeviceOwner.account || this.currentUserDeviceOwner.nickName || "-";
this.$confirm(
this.$t("systemUser.deviceDialog.message.confirmUnbind", {
account: ownerLabel,
count: this.userDeviceIds.length,
}),
this.$t("systemUser.message.warningTitle"),
{
confirmButtonText: this.$t("common.confirm"),
cancelButtonText: this.$t("common.cancel"),
type: "warning",
}
)
.then(() => unbindUserDevices(this.currentUserDeviceOwner.id, { deviceIds: this.userDeviceIds }))
.then(() => {
this.msgSuccess(this.$t("systemUser.deviceDialog.message.unbindSuccess"));
this.userDeviceIds = [];
this.userDeviceSingle = true;
this.userDeviceMultiple = true;
if (this.$refs.userDeviceTable) {
this.$refs.userDeviceTable.clearSelection();
}
this.getUserDeviceList();
})
.catch(() => {});
},
resetUserDeviceDialog() {
this.userDeviceLoading = false;
this.userDeviceList = [];
this.userDeviceTotal = 0;
this.userDeviceIds = [];
this.userDeviceSingle = true;
this.userDeviceMultiple = true;
this.currentUserDeviceOwner = {
id: undefined,
account: undefined,
nickName: undefined,
};
this.userDeviceQueryParams = {
pageNum: 1,
pageSize: 10,
sn: undefined,
model: undefined,
orderCode: undefined,
alias: undefined,
};
if (this.$refs.userDeviceTable) {
this.$refs.userDeviceTable.clearSelection();
}
this.resetForm("userDeviceQueryForm");
},
filterNode(value, data) { filterNode(value, data) {
if (!value) { if (!value) {
return true; return true;

Loading…
Cancel
Save