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",
"version": "3.8.9",
"description": "客户GeoTag管理系统",
"description": "2222客户GeoTag管理系统",
"author": "GeoTag",
"license": "MIT",
"scripts": {

16
src/api/system/user.js

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

269
src/components/device/TrajectoryDialog.vue

@ -54,6 +54,9 @@
<div class="trajectory-toolbar">
<span class="page-inline-filter__label">{{ $t("device.trajectory.provider.label") }}</span>
<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">
{{ $t("device.trajectory.provider.amap") }}
</el-radio-button>
@ -111,9 +114,17 @@
import { getDeviceTrajectory, getDeviceTrajectoryMapConfig } from "@/api/device/device";
import { loadAMap } from "@/utils/loadAMap";
import { loadGoogleMaps } from "@/utils/loadGoogleMaps";
import { loadLeaflet } from "@/utils/loadLeaflet";
const AMAP_DEFAULT_CENTER = [121.4737, 31.2304];
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 MAX_TRAJECTORY_POINTS = 100;
@ -143,6 +154,7 @@ export default {
polyline: null,
markers: [],
markerInfoWindows: [],
tileLayer: null,
mapsApi: null,
mapConfig: null,
locationTimeRange: [],
@ -169,6 +181,9 @@ export default {
hasGoogleKey() {
return !!(this.mapConfig && this.mapConfig.googleKey);
},
hasMaptilerKey() {
return !!(this.mapConfig && this.mapConfig.maptilerKey);
},
},
methods: {
async handleOpen() {
@ -213,12 +228,16 @@ export default {
await this.renderCurrentProviderMap();
},
resolveDefaultProvider() {
if (this.hasMaptilerKey) {
return "maptiler";
}
if (this.hasAmapKey) {
return "amap";
}
if (this.hasGoogleKey) {
return "google";
}
return "amap";
},
initDefaultLocationTimeRange() {
@ -267,7 +286,7 @@ export default {
try {
const response = await getDeviceTrajectoryMapConfig();
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");
return;
}
@ -294,8 +313,11 @@ export default {
};
})
.filter((item) => item.latNum !== null && item.lngNum !== null);
this.totalTrajectoryCount = validPoints.length;
this.trajectoryPoints = validPoints.slice(-MAX_TRAJECTORY_POINTS);
const sortedPoints = validPoints.slice().sort((left, right) => {
return this.getTrackTimestamp(left) - this.getTrackTimestamp(right);
});
this.totalTrajectoryCount = sortedPoints.length;
this.trajectoryPoints = sortedPoints.slice(-MAX_TRAJECTORY_POINTS);
} catch (error) {
this.trajectoryPoints = [];
this.totalTrajectoryCount = 0;
@ -310,6 +332,13 @@ export default {
}
this.loadError = "";
await this.$nextTick();
if (this.mapProvider === "maptiler") {
await this.ensureLeafletMap();
if (!this.loadError) {
this.renderLeafletTrajectory();
}
return;
}
if (this.mapProvider === "google") {
await this.ensureGoogleMap();
if (!this.loadError) {
@ -323,6 +352,211 @@ export default {
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() {
if (!this.hasAmapKey) {
this.loadError = this.$t("device.trajectory.message.missingAmapKey");
@ -515,11 +749,8 @@ export default {
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]);
}
const detailZoom = this.getAmapMaxZoom();
this.map.setZoomAndCenter(detailZoom, endPoint);
},
renderGoogleTrajectory() {
if (!this.map || !this.mapsApi || !this.trajectoryPoints.length) {
@ -531,8 +762,6 @@ export default {
}
this.clearOverlays();
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 = {
path,
@ -603,12 +832,8 @@ export default {
}
}
if (path.length === 1) {
this.map.setCenter(startPoint);
this.map.setZoom(15);
} else {
this.map.fitBounds(bounds);
}
this.map.setCenter(endPoint);
this.map.setZoom(GOOGLE_DETAIL_ZOOM);
},
openGoogleMarkerInfoWindow(marker, title, timeText) {
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 {
if (this.polyline) {
this.map.remove(this.polyline);
@ -690,10 +922,13 @@ export default {
}
if (this.mapVendor === "amap" && typeof this.map.destroy === "function") {
this.map.destroy();
} else if (this.mapVendor === "leaflet" && typeof this.map.remove === "function") {
this.map.remove();
} else if (this.$refs.map) {
this.$refs.map.innerHTML = "";
}
this.map = null;
this.tileLayer = null;
if (resetVendor) {
this.mapVendor = "";
this.mapsApi = null;
@ -817,4 +1052,4 @@ export default {
width: 100%;
height: 420px;
}
</style>
</style>

6
src/main.js

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

17
src/utils/dynamicTitle.js

@ -1,13 +1,24 @@
import store from '@/store'
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() {
const appTitle = resolveAppTitle()
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 {
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-col>
<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 :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>

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

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

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

@ -129,10 +129,20 @@
<el-table-column
:label="$t('systemUser.table.actions')"
align="center"
width="160"
width="260"
class-name="small-padding fixed-width"
>
<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
size="mini"
type="text"
@ -151,6 +161,15 @@
>
{{ $t("systemUser.button.resetPassword") }}
</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>
</el-table-column>
</el-table>
@ -251,6 +270,123 @@
</div>
</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-upload
ref="upload"
@ -284,7 +420,7 @@
</template>
<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 { getToken } from "@/utils/auth";
import Treeselect from "@riophae/vue-treeselect";
@ -302,6 +438,18 @@ export default {
showSearch: true,
total: 0,
userList: null,
userDeviceLoading: false,
userDeviceOpen: false,
userDeviceTotal: 0,
userDeviceList: [],
userDeviceIds: [],
userDeviceSingle: true,
userDeviceMultiple: true,
currentUserDeviceOwner: {
id: undefined,
account: undefined,
nickName: undefined,
},
title: "",
deptOptions: undefined,
open: false,
@ -343,6 +491,14 @@ export default {
status: undefined,
deptId: undefined,
},
userDeviceQueryParams: {
pageNum: 1,
pageSize: 10,
sn: undefined,
model: undefined,
orderCode: undefined,
alias: undefined,
},
columns: [],
rules: {},
employeeRules: {},
@ -427,6 +583,120 @@ export default {
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) {
if (!value) {
return true;

Loading…
Cancel
Save