Browse Source

b端国际化

master
hx 3 days ago
parent
commit
5acddc9d1e
  1. 2
      src/components/Breadcrumb/index.vue
  2. 21
      src/components/device/TrajectoryDialog.vue
  3. 57
      src/layout/components/Navbar.vue
  4. 107
      src/layout/components/Sidebar/Logo.vue
  5. 8
      src/layout/components/Sidebar/index.vue
  6. 7
      src/main.js
  7. 14
      src/plugins/download.js
  8. 2
      src/store/modules/settings.js
  9. 109
      src/utils/language.js
  10. 122
      src/utils/request.js
  11. 367
      src/views/device/device/index.vue
  12. 22
      src/views/login.vue
  13. 493
      src/views/system/menu/index.vue

2
src/components/Breadcrumb/index.vue

@ -47,7 +47,7 @@ export default {
}
//
if (!this.isDashboard(matched[0])) {
matched = [{ path: "/index", meta: { title: "首页" } }].concat(matched)
matched = [{ path: "/index", meta: { title: this.$t("common.home") } }].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},

21
src/components/device/TrajectoryDialog.vue

@ -754,22 +754,25 @@ export default {
return this.parseTime ? this.parseTime(value) : value;
},
formatCoordinateValue(value) {
if (value === null || value === undefined || value === "") {
if (value === null || value === undefined) {
return "-";
}
let strValue = String(value);
const [integerPart, decimalPart] = strValue.split(".");
const strValue = String(value).trim();
if (!strValue || strValue === "-") {
return "-";
}
const match = strValue.match(/^([+-]?\d+)(?:\.(\d+))?$/);
if (!match) {
return "-";
}
const integerPart = match[1];
const decimalPart = match[2] || "";
const fixedDecimal = decimalPart.slice(0, 2).padEnd(2, "0");
return `${integerPart}.${fixedDecimal}`;
},
formatTrackCoordinates(lat, lng) {
const latText =
lat === null || lat === undefined || lat === "" ? "-" : lat;
const lngText =
lng === null || lng === undefined || lng === "" ? "-" : lng;
return `${this.formatCoordinateValue(latText)} / ${this.formatCoordinateValue(lngText)}`;
return `${this.formatCoordinateValue(lat)} / ${this.formatCoordinateValue(lng)}`;
},
getPointTrackTime(point) {
if (!point) {

57
src/layout/components/Navbar.vue

@ -9,15 +9,29 @@
<template v-if="device!=='mobile'">
<search id="header-search" class="right-menu-item" />
<screenfull id="screenfull" class="right-menu-item hover-effect" />
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<el-tooltip :content="$t('navbar.layoutSize')" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>
<el-dropdown class="right-menu-item hover-effect lang-menu" trigger="click" @command="changeLanguage">
<span class="lang-trigger">
<i class="el-icon-connection"></i>
<span>{{ $t(`lang.${currentLanguage}`) }}</span>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="item in languageOptions"
:key="item"
:command="item"
:disabled="item === currentLanguage"
>
{{ $t(`lang.${item}`) }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="hover">
@ -27,10 +41,10 @@
</div>
<el-dropdown-menu slot="dropdown">
<router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item>{{ $t("navbar.profile") }}</el-dropdown-item>
</router-link>
<el-dropdown-item divided @click.native="logout">
<span>退出登录</span>
<span>{{ $t("navbar.logout") }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
@ -52,6 +66,7 @@ import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc'
import { getLanguage, setLanguage, languageOptions } from '@/utils/language'
export default {
emits: ['setLayout'],
@ -65,6 +80,11 @@ export default {
RuoYiGit,
RuoYiDoc
},
data() {
return {
languageOptions
}
},
computed: {
...mapGetters([
'sidebar',
@ -81,6 +101,9 @@ export default {
get() {
return this.$store.state.settings.topNav
}
},
currentLanguage() {
return this.$store.state.settings.language || getLanguage()
}
},
methods: {
@ -91,15 +114,23 @@ export default {
this.$emit('setLayout')
},
logout() {
this.$confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
this.$confirm(this.$t('navbar.logoutConfirm'), this.$t('common.tips'), {
confirmButtonText: this.$t('common.confirm'),
cancelButtonText: this.$t('common.cancel'),
type: 'warning'
}).then(() => {
this.$store.dispatch('LogOut').then(() => {
location.href = '/index'
})
}).catch(() => {})
},
changeLanguage(lang) {
const normalized = setLanguage(lang)
this.$store.dispatch('settings/changeSetting', { key: 'language', value: normalized })
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('lang', normalized)
}
window.location.reload()
}
}
}
@ -167,6 +198,14 @@ export default {
}
}
.lang-menu {
.lang-trigger {
display: inline-flex;
align-items: center;
gap: 4px;
}
}
.avatar-container {
margin-right: 0px;
padding-right: 0px;

107
src/layout/components/Sidebar/Logo.vue

@ -1,13 +1,22 @@
<template>
<template>
<div class="sidebar-logo-container" :class="{'collapse':collapse}" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
<transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<router-link
:key="collapse ? 'collapse' : 'expand'"
class="sidebar-logo-link"
:class="{ 'is-collapse': collapse }"
:style="logoLinkStyle"
to="/"
>
<img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
<h1
v-if="!collapse || !logo"
class="sidebar-title"
:title="logoTitle"
:style="logoTitleStyle"
>
{{ logoTitle }}
</h1>
</router-link>
</transition>
</div>
@ -17,6 +26,14 @@
import logoImg from '@/assets/logo/logo.png'
import variables from '@/assets/styles/variables.scss'
const SIDEBAR_TITLE_MAP = {
'zh-CN': '\u5ba2\u6237 GeoTag\u7ba1\u7406\u7cfb\u7edf',
'en-US': 'GeoTag Customer Management System',
'fr-FR': 'Systeme de gestion client GeoTag',
'es-ES': 'Sistema de gestion de clientes GeoTag',
'pt-BR': 'Sistema de gestao de clientes GeoTag'
}
export default {
name: 'SidebarLogo',
props: {
@ -31,6 +48,57 @@ export default {
},
sideTheme() {
return this.$store.state.settings.sideTheme
},
currentLanguage() {
return this.$store.state.settings.language || 'zh-CN'
},
logoLinkStyle() {
if (this.collapse) {
return {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '0',
padding: '0',
width: '100%',
height: '100%',
overflow: 'hidden'
}
}
return {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
gap: '10px',
padding: '0 14px',
width: '100%',
height: '100%',
overflow: 'hidden',
boxSizing: 'border-box'
}
},
logoTitleStyle() {
return {
color: this.sideTheme === 'theme-dark' ? this.variables.logoTitleColor : this.variables.logoLightTitleColor,
flex: '1 1 auto',
minWidth: '0',
display: 'block',
margin: '0',
lineHeight: '50px',
fontSize: '13px',
textAlign: 'left',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}
},
logoTitle() {
const directTitle = SIDEBAR_TITLE_MAP[this.currentLanguage]
if (directTitle) {
return directTitle
}
const translated = this.$t('app.sidebarTitle')
return translated && translated !== 'app.sidebarTitle' ? translated : this.title
}
},
data() {
@ -62,25 +130,42 @@ export default {
overflow: hidden;
& .sidebar-logo-link {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 10px;
box-sizing: border-box;
padding: 0 14px;
height: 100%;
width: 100%;
& .sidebar-logo {
flex: 0 0 auto;
width: 32px;
height: 32px;
vertical-align: middle;
margin-right: 12px;
}
& .sidebar-title {
display: inline-block;
flex: 1 1 auto;
min-width: 0;
display: block;
margin: 0;
color: #fff;
font-weight: 600;
line-height: 50px;
font-size: 14px;
font-size: 13px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
vertical-align: middle;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&.is-collapse {
justify-content: center;
padding: 0;
gap: 0;
}
}

8
src/layout/components/Sidebar/index.vue

@ -1,7 +1,7 @@
<template>
<div :class="{ 'has-logo': showLogo }"
:style="{ backgroundColor: settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
<logo v-if="showLogo" :collapse="isCollapse" />
<logo v-if="showLogo" :key="logoKey" :collapse="isCollapse" />
<el-scrollbar :class="settings.sideTheme" wrap-class="scrollbar-wrapper">
<el-menu :default-active="activeMenu" :collapse="isCollapse"
:background-color="settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
@ -42,6 +42,12 @@ export default {
},
isCollapse() {
return !this.sidebar.opened
},
currentLanguage() {
return this.$store.state.settings.language || "zh-CN"
},
logoKey() {
return `logo-${this.currentLanguage}-${this.isCollapse ? "collapse" : "expand"}`
}
}
}

7
src/main.js

@ -14,6 +14,8 @@ 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 { getLanguage } from "@/utils/language";
Vue.prototype.$math = math;
Vue.prototype.msgSuccess = function (msg) {
this.$message({ showClose: true, message: msg, type: "success" });
@ -85,6 +87,7 @@ Vue.component("ImagePreview", ImagePreview);
Vue.use(directive);
Vue.use(plugins);
Vue.use(I18nPlugin);
DictData.install();
/**
@ -102,6 +105,10 @@ Vue.use(Element, {
Vue.config.productionTip = false;
if (typeof document !== "undefined") {
document.documentElement.setAttribute("lang", getLanguage());
}
new Vue({
el: "#app",
router,

14
src/plugins/download.js

@ -3,7 +3,9 @@ import {Loading, Message} from 'element-ui'
import { saveAs } from 'file-saver'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { getAcceptLanguage } from '@/utils/language'
import { blobValidate } from "@/utils/ruoyi"
import { t } from "@/lang"
const baseURL = process.env.VUE_APP_BASE_API
let downloadLoadingInstance
@ -15,7 +17,7 @@ export default {
method: 'get',
url: url,
responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() }
headers: { 'Authorization': 'Bearer ' + getToken(), 'Accept-Language': getAcceptLanguage() }
}).then((res) => {
const isBlob = blobValidate(res.data)
if (isBlob) {
@ -32,7 +34,7 @@ export default {
method: 'get',
url: url,
responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() }
headers: { 'Authorization': 'Bearer ' + getToken(), 'Accept-Language': getAcceptLanguage() }
}).then((res) => {
const isBlob = blobValidate(res.data)
if (isBlob) {
@ -45,12 +47,12 @@ export default {
},
zip(url, name) {
var url = baseURL + url
downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", })
downloadLoadingInstance = Loading.service({ text: t("request.downloading"), spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", })
axios({
method: 'get',
url: url,
responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() }
headers: { 'Authorization': 'Bearer ' + getToken(), 'Accept-Language': getAcceptLanguage() }
}).then((res) => {
const isBlob = blobValidate(res.data)
if (isBlob) {
@ -62,7 +64,7 @@ export default {
downloadLoadingInstance.close()
}).catch((r) => {
console.error(r)
Message.error('下载文件出现错误,请联系管理员!')
Message.error(t("request.downloadError"))
downloadLoadingInstance.close()
})
},
@ -72,7 +74,7 @@ export default {
async printErrMsg(data) {
const resText = await data.text()
const rspObj = JSON.parse(resText)
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
const errMsg = rspObj.msg || errorCode[rspObj.code] || errorCode['default']
Message.error(errMsg)
}
}

2
src/store/modules/settings.js

@ -1,11 +1,13 @@
import defaultSettings from '@/settings'
import { useDynamicTitle } from '@/utils/dynamicTitle'
import { getLanguage } from '@/utils/language'
const { sideTheme, showSettings, topNav, tagsView, tagsIcon, fixedHeader, sidebarLogo, dynamicTitle, footerVisible, footerContent } = defaultSettings
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
const state = {
title: '',
language: getLanguage(),
theme: storageSetting.theme || '#409EFF',
sideTheme: storageSetting.sideTheme || sideTheme,
showSettings: showSettings,

109
src/utils/language.js

@ -0,0 +1,109 @@
const LANGUAGE_STORAGE_KEY = "language";
const DEFAULT_LANGUAGE = "zh-CN";
const SUPPORTED_LANGUAGES = [
"zh-CN",
"en-US",
"fr-FR",
"es-ES",
"pt-BR",
];
const LANGUAGE_ALIAS_MAP = {
zh: "zh-CN",
"zh-cn": "zh-CN",
"zh-hans": "zh-CN",
"zh-sg": "zh-CN",
"zh-my": "zh-CN",
"zh-hk": "zh-CN",
"zh-mo": "zh-CN",
"zh-tw": "zh-CN",
"zh-hant": "zh-CN",
en: "en-US",
"en-us": "en-US",
"en-gb": "en-US",
"en-au": "en-US",
"en-ca": "en-US",
fr: "fr-FR",
"fr-fr": "fr-FR",
"fr-ca": "fr-FR",
es: "es-ES",
"es-es": "es-ES",
"es-mx": "es-ES",
"es-ar": "es-ES",
pt: "pt-BR",
"pt-br": "pt-BR",
"pt-pt": "pt-BR",
};
function normalizeTag(tag) {
return String(tag || "")
.trim()
.replace(/_/g, "-")
.toLowerCase();
}
function mapToSupportedLanguage(lang) {
const normalizedTag = normalizeTag(lang);
if (!normalizedTag) {
return null;
}
if (LANGUAGE_ALIAS_MAP[normalizedTag]) {
return LANGUAGE_ALIAS_MAP[normalizedTag];
}
const baseLang = normalizedTag.split("-")[0];
if (LANGUAGE_ALIAS_MAP[baseLang]) {
return LANGUAGE_ALIAS_MAP[baseLang];
}
return null;
}
function detectBrowserLanguage() {
if (typeof navigator === "undefined") {
return DEFAULT_LANGUAGE;
}
const candidates = Array.isArray(navigator.languages) && navigator.languages.length > 0
? navigator.languages
: [navigator.language, navigator.userLanguage];
for (const candidate of candidates) {
const mapped = mapToSupportedLanguage(candidate);
if (mapped && SUPPORTED_LANGUAGES.includes(mapped)) {
return mapped;
}
}
return DEFAULT_LANGUAGE;
}
function normalizeLanguage(lang) {
const mapped = mapToSupportedLanguage(lang);
if (mapped && SUPPORTED_LANGUAGES.includes(mapped)) {
return mapped;
}
return DEFAULT_LANGUAGE;
}
export function getLanguage() {
const stored = localStorage.getItem(LANGUAGE_STORAGE_KEY);
if (!stored) {
return DEFAULT_LANGUAGE;
}
return normalizeLanguage(stored);
}
export function setLanguage(lang) {
const normalized = normalizeLanguage(lang);
localStorage.setItem(LANGUAGE_STORAGE_KEY, normalized);
return normalized;
}
export function getAcceptLanguage() {
return getLanguage();
}
export const languageOptions = [...SUPPORTED_LANGUAGES];

122
src/utils/request.js

@ -1,27 +1,28 @@
import axios from "axios";
import { Notification, MessageBox, Message, Loading } from "element-ui";
import { Notification, MessageBox, Message } from "element-ui";
import store from "@/store";
import { getToken } from "@/utils/auth";
import errorCode from "@/utils/errorCode";
import { Encrypt } from "@/utils/encryp";
import { tansParams, blobValidate } from "@/utils/ruoyi";
import { tansParams } from "@/utils/ruoyi";
import { getAcceptLanguage } from "@/utils/language";
import cache from "@/plugins/cache";
import { saveAs } from "file-saver";
import { t } from "@/lang";
let downloadLoadingInstance;
// 是否显示重新登录
export let isRelogin = { show: false };
axios.defaults.headers["Content-Type"] = "application/json;charset=utf-8";
// 创建axios实例
// 创建 axios 实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
// axios 中请求配置有 baseURL 选项,表示请求 URL 公共部分
baseURL: process.env.VUE_APP_BASE_API,
// 超时
timeout: 10000,
});
// request拦截器
// request 拦截器
service.interceptors.request.use(
(config) => {
// 接口密码加密
@ -34,7 +35,12 @@ service.interceptors.request.use(
"/business/businessUser/profile/updatePwd",
];
if (encryptUrls.includes(config.url)) {
const encryptMap = ["passwordHash", "newPasswordHash", "oldPassword", "newPassword"];
const encryptMap = [
"passwordHash",
"newPasswordHash",
"oldPassword",
"newPassword",
];
encryptMap.forEach((key) => {
if (config.data && config.data[key]) {
config.data[key] = Encrypt(config.data[key]);
@ -44,27 +50,36 @@ service.interceptors.request.use(
}
});
}
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false;
// 是否需要防止数据重复提交
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false;
if (getToken() && !isToken) {
config.headers["Authorization"] = "Bearer " + getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改
config.headers["Authorization"] = "Bearer " + getToken();
}
console.log(`语言:${getAcceptLanguage()}`)
console.log("navigator.language =", navigator.language);
console.log("navigator.languages =", navigator.languages);
console.log("localStorage.language =", localStorage.getItem("language"));
config.headers["Accept-Language"] = getAcceptLanguage();
const clientTimeZone =
(Intl.DateTimeFormat &&
Intl.DateTimeFormat().resolvedOptions &&
Intl.DateTimeFormat().resolvedOptions().timeZone) ||
"Asia/Shanghai";
console.log(`时区 :${clientTimeZone}`);
config.headers["X-Timezone"] = clientTimeZone;
// get请求映射params参数
console.log(`时区:${clientTimeZone}`)
// get 请求映射 params 参数
if (config.method === "get" && config.params) {
let url = config.url + "?" + tansParams(config.params);
url = url.slice(0, -1);
config.params = {};
config.url = url;
}
if (
!isRepeatSubmit &&
(config.method === "post" || config.method === "put")
@ -77,15 +92,18 @@ service.interceptors.request.use(
: config.data,
time: new Date().getTime(),
};
const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小
const limitSize = 5 * 1024 * 1024; // 限制存放数据5M
const limitSize = 5 * 1024 * 1024; // 限制存放数据 5M
if (requestSize >= limitSize) {
console.warn(
`[${config.url}]: ` +
"请求数据大小超出允许的5M限制,无法进行防重复提交验证。"
"请求数据大小超出允许的 5M 限制,无法进行防重复提交验证。"
);
return config;
}
const sessionObj = cache.session.getJSON("sessionObj");
if (
sessionObj === undefined ||
@ -98,6 +116,7 @@ service.interceptors.request.use(
const s_data = sessionObj.data; // 请求数据
const s_time = sessionObj.time; // 请求时间
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
if (
s_data === requestObj.data &&
requestObj.time - s_time < interval &&
@ -106,16 +125,17 @@ service.interceptors.request.use(
const message = "数据正在处理,请勿重复提交";
console.warn(`[${s_url}]: ` + message);
return Promise.reject(new Error(message));
} else {
cache.session.setJSON("sessionObj", requestObj);
}
cache.session.setJSON("sessionObj", requestObj);
}
}
return config;
},
(error) => {
console.log(error);
Promise.reject(error);
return Promise.reject(error);
}
);
@ -124,8 +144,9 @@ service.interceptors.response.use(
(res) => {
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode["default"];
// 获取错误信息(优先后端返回,支持 i18n)
const msg = res.data.msg || errorCode[code] || errorCode["default"];
// 二进制数据则直接返回
if (
res.request.responseType === "blob" ||
@ -133,15 +154,16 @@ service.interceptors.response.use(
) {
return res.data;
}
if (code === 401) {
if (!isRelogin.show) {
isRelogin.show = true;
MessageBox.confirm(
"登录状态已过期,您可以继续留在该页面,或者重新登录",
"系统提示",
t("request.reloginTip"),
t("common.tips"),
{
confirmButtonText: "重新登录",
cancelButtonText: "取消",
confirmButtonText: t("request.relogin"),
cancelButtonText: t("common.cancel"),
type: "warning",
}
)
@ -155,60 +177,40 @@ service.interceptors.response.use(
isRelogin.show = false;
});
}
return Promise.reject("无效的会话,或者会话已过期,请重新登录。");
} else if (code === 500) {
return Promise.reject(t("request.invalidSession"));
}
if (code === 500) {
Message({ message: msg, type: "error" });
return Promise.reject(new Error(msg));
} else if (code === 601) {
}
if (code === 601) {
Message({ message: msg, type: "warning" });
return Promise.reject("error");
} else if (code !== 200) {
}
if (code !== 200) {
Notification.error({ title: msg });
return Promise.reject("error");
} else {
return res.data;
}
return res.data;
},
(error) => {
console.log("err" + error);
let { message } = error;
if (message == "Network Error") {
message = "后端接口连接异常";
if (message === "Network Error") {
message = t("request.backendConnectError");
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
message = t("request.requestTimeout");
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
message = t("request.apiErrorStatus", { status: message.substr(message.length - 3) });
}
Message({ message: message, type: "error", duration: 5 * 1000 });
Message({ message, type: "error", duration: 5 * 1000 });
return Promise.reject(error);
}
);
// 通用下载方法
// export function download(url, params, filename, config) {
// downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", })
// return service.post(url, params, {
// transformRequest: [(params) => { return tansParams(params) }],
// headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
// responseType: 'blob',
// ...config
// }).then(async (data) => {
// const isBlob = blobValidate(data)
// if (isBlob) {
// const blob = new Blob([data])
// saveAs(blob, filename)
// } else {
// const resText = await data.text()
// const rspObj = JSON.parse(resText)
// const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
// Message.error(errMsg)
// }
// downloadLoadingInstance.close()
// }).catch((r) => {
// console.error(r)
// Message.error('下载文件出现错误,请联系管理员!')
// downloadLoadingInstance.close()
// })
// }
export default service;

367
src/views/device/device/index.vue

@ -2,39 +2,39 @@
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px" @submit.native.prevent="handleQuery">
<el-form-item label="订单号" prop="orderCode">
<el-input v-model="queryParams.orderCode" placeholder="请输入订单号" clearable size="small" @keyup.enter.native="handleQuery" />
<el-form-item :label="$t('device.query.orderCode')" prop="orderCode">
<el-input v-model="queryParams.orderCode" :placeholder="$t('device.placeholder.orderCode')" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="型号" prop="model">
<el-input v-model="queryParams.model" placeholder="请输入型号" clearable size="small" @keyup.enter.native="handleQuery" />
<el-form-item :label="$t('device.query.model')" prop="model">
<el-input v-model="queryParams.model" :placeholder="$t('device.placeholder.model')" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="序列号" prop="sn">
<el-input v-model="queryParams.sn" placeholder="请输入序列号" clearable size="small" @keyup.enter.native="handleQuery" />
<el-form-item :label="$t('device.query.sn')" prop="sn">
<el-input v-model="queryParams.sn" :placeholder="$t('device.placeholder.sn')" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="名称" prop="alias">
<el-input v-model="queryParams.alias" placeholder="请输入名称" clearable size="small" @keyup.enter.native="handleQuery" />
<el-form-item :label="$t('device.query.alias')" prop="alias">
<el-input v-model="queryParams.alias" :placeholder="$t('device.placeholder.alias')" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="地址" prop="lastAddress">
<el-input v-model="queryParams.lastAddress" placeholder="请输入地址" clearable size="small" @keyup.enter.native="handleQuery" />
<el-form-item :label="$t('device.query.lastAddress')" prop="lastAddress">
<el-input v-model="queryParams.lastAddress" :placeholder="$t('device.placeholder.lastAddress')" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="设备备注" prop="remark">
<el-input v-model="queryParams.remark" placeholder="请输入设备备注" clearable size="small" @keyup.enter.native="handleQuery" />
<el-form-item :label="$t('device.query.remark')" prop="remark">
<el-input v-model="queryParams.remark" :placeholder="$t('device.placeholder.remark')" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="是否启用" prop="activationStatus">
<el-select v-model="queryParams.activationStatus" placeholder="请选择启用状态" clearable size="small" @keyup.enter.native="handleQuery">
<el-option label="否" :value="0" />
<el-option label="是" :value="1" />
<el-form-item :label="$t('device.query.activationStatus')" prop="activationStatus">
<el-select v-model="queryParams.activationStatus" :placeholder="$t('device.placeholder.activationStatus')" clearable size="small" @keyup.enter.native="handleQuery">
<el-option :label="$t('device.status.disabled')" :value="0" />
<el-option :label="$t('device.status.enabled')" :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-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t("common.search") }}</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t("common.reset") }}</el-button>
</el-form-item>
</el-form>
@ -52,19 +52,19 @@
</el-col> -->
<!-- 新增导入按钮 -->
<el-col :span="1.5">
<el-button type="info" plain icon="el-icon-upload2" size="mini" @click="handleImport" v-hasPermi="['device:device:import']">导入</el-button>
<el-button type="info" plain icon="el-icon-upload2" size="mini" @click="handleImport" v-hasPermi="['device:device:import']">{{ $t("device.button.import") }}</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-s-claim" size="mini" @click="handleClaimDevice" v-hasPermi="['device:device:claim:batch']">认领设备</el-button>
<el-button type="primary" plain icon="el-icon-s-claim" size="mini" @click="handleClaimDevice" v-hasPermi="['device:device:claim:batch']">{{ $t("device.button.claim") }}</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="el-icon-circle-check" size="mini" :disabled="multiple" v-hasPermi="['device:device:activate:batch']" @click="handleBatchActivate">批量启用</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 :span="1.5">
<el-button type="danger" plain icon="el-icon-circle-close" size="mini" :disabled="multiple" @click="handleBatchDisable">批量禁用</el-button>
<el-button type="danger" plain icon="el-icon-circle-close" size="mini" :disabled="multiple" @click="handleBatchDisable">{{ $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">分配设备</el-button>
<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-col>
@ -93,7 +93,7 @@
>
</el-col> -->
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['device:device:export']">导出</el-button>
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['device:device:export']">{{ $t("device.button.export") }}</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
@ -106,21 +106,21 @@
@sort-change="handleTableSortChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="订单号" align="center" prop="orderCode" />
<el-table-column :label="$t('device.table.orderCode')" align="center" prop="orderCode" />
<el-table-column label="设备状态" align="center" prop="activationStatus">
<el-table-column :label="$t('device.table.deviceStatus')" align="center" prop="activationStatus">
<template slot-scope="scope">
<el-tag :type="getActivationTagType(scope.row.activationStatus)" size="mini">
{{ getActivationStatusLabel(scope.row.activationStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="型号" align="center" prop="model" />
<el-table-column label="序列号" align="center" prop="sn" />
<el-table-column label="名称" align="center" prop="alias" />
<el-table-column label="地址" align="center" prop="lastAddress" min-width="220" />
<el-table-column :label="$t('device.table.model')" align="center" prop="model" />
<el-table-column :label="$t('device.table.sn')" align="center" prop="sn" />
<el-table-column :label="$t('device.table.alias')" align="center" prop="alias" />
<el-table-column :label="$t('device.table.lastAddress')" align="center" prop="lastAddress" min-width="220" />
<el-table-column
label="更新时间"
:label="$t('device.table.updateTime')"
align="center"
prop="lastLocationTime"
min-width="115"
@ -131,7 +131,7 @@
<span>{{ parseTime(scope.row.lastLocationTime, "{y}-{m}-{d} {h}:{i}:{s}") }}</span>
</template>
</el-table-column>
<el-table-column label="经纬度" align="center" >
<el-table-column :label="$t('device.table.coordinates')" align="center" >
<template slot-scope="scope">
<span>{{ formatCoordinates(scope.row.lastLat, scope.row.lastLng) }}</span>
</template>
@ -139,15 +139,15 @@
<el-table-column label="MAC " align="center" prop="mac" />
<!-- <el-table-column label="电量" align="center" prop="battery" /> -->
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column :label="$t('device.table.remark')" align="center" prop="remark" />
<!-- <el-table-column label="绑定企业id" align="center" prop="bindBusinessId" /> -->
<el-table-column label="操作" align="center" width="220" class-name="small-padding fixed-width">
<el-table-column :label="$t('device.table.actions')" align="center" width="220" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEditInfo(scope.row)" v-hasPermi="['device:device:list']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-view" @click="handleDetail(scope.row)">详情</el-button>
<el-button size="mini" type="text" icon="el-icon-location-outline" @click="handleTrajectory(scope.row)">轨迹</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEditInfo(scope.row)" v-hasPermi="['device:device:list']">{{ $t("common.edit") }}</el-button>
<el-button size="mini" type="text" icon="el-icon-view" @click="handleDetail(scope.row)">{{ $t("device.button.detail") }}</el-button>
<el-button size="mini" type="text" icon="el-icon-location-outline" @click="handleTrajectory(scope.row)">{{ $t("device.button.trajectory") }}</el-button>
</template>
</el-table-column>
<!-- <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
@ -175,206 +175,206 @@
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
<!-- Excel导入弹窗仅允许上传1个文件 + 手动输入订单号/备注 -->
<el-dialog title="设备Excel导入" :visible.sync="importOpen" width="550px" append-to-body>
<el-dialog :title="$t('device.dialog.import.title')" :visible.sync="importOpen" width="550px" append-to-body>
<el-form ref="importForm" :model="importForm" :rules="importRules" label-width="90px" @submit.native.prevent>
<!-- Excel文件上传仅1个文件修复核心 -->
<el-form-item label="Excel文件">
<el-form-item :label="$t('device.dialog.import.fileLabel')">
<el-upload ref="upload" :limit="1" :on-exceed="handleExceed" accept=".xlsx, .xls" :auto-upload="false" :on-change="handleFileChange" :file-list="fileList" drag>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将Excel文件拖到此处<em>点击上传</em></div>
<div class="el-upload__text">{{ $t("device.dialog.import.dragText") }}<em>{{ $t("device.dialog.import.clickUpload") }}</em></div>
<div class="el-upload__tip" slot="tip">
<el-icon size="12">
<warning />
</el-icon>
<span>仅支持 .xlsx / .xls 格式文件且只能上传1个文件</span>
<span>{{ $t("device.dialog.import.tip") }}</span>
</div>
</el-upload>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitImport" :loading="importing"> </el-button>
<el-button @click="cancelImport"> </el-button>
<el-button type="primary" @click="submitImport" :loading="importing">{{ $t("common.confirm") }}</el-button>
<el-button @click="cancelImport">{{ $t("common.cancel") }}</el-button>
</div>
</el-dialog>
<el-dialog title="导入结果" :visible.sync="resultOpen" width="700px" append-to-body>
<el-dialog :title="$t('device.dialog.importResult.title')" :visible.sync="resultOpen" width="700px" append-to-body>
<div v-if="latestImportResult">
<el-alert :title="getImportStatusMessage(latestImportResult)" :type="getImportStatusType(latestImportResult.status)" :closable="false" show-icon />
<el-descriptions :column="2" border class="import-result-summary">
<el-descriptions-item label="状态">
<el-descriptions-item :label="$t('device.dialog.importResult.status')">
{{ getImportStatusLabel(latestImportResult.status) }}
</el-descriptions-item>
<el-descriptions-item label="总数">
<el-descriptions-item :label="$t('device.dialog.importResult.total')">
{{ latestImportResult.total }}
</el-descriptions-item>
<el-descriptions-item label="成功数">
<el-descriptions-item :label="$t('device.dialog.importResult.successCount')">
{{ latestImportResult.successCount }}
</el-descriptions-item>
<el-descriptions-item label="失败数">
<el-descriptions-item :label="$t('device.dialog.importResult.failCount')">
{{ latestImportResult.failCount }}
</el-descriptions-item>
<el-descriptions-item label="开始时间">
<el-descriptions-item :label="$t('device.dialog.importResult.startTime')">
{{ parseTime(latestImportResult.startTime) }}
</el-descriptions-item>
<el-descriptions-item label="结束时间">
<el-descriptions-item :label="$t('device.dialog.importResult.finishTime')">
{{ parseTime(latestImportResult.finishTime) }}
</el-descriptions-item>
</el-descriptions>
<div v-if="latestImportResult.requestErrors && latestImportResult.requestErrors.length" class="import-result-block">
<div class="import-result-title">请求错误</div>
<div class="import-result-title">{{ $t("device.dialog.importResult.requestErrors") }}</div>
<div v-for="(item, index) in latestImportResult.requestErrors" :key="'request-error-' + index" class="import-result-text">
{{ index + 1 }}. {{ item }}
</div>
</div>
<div v-if="latestImportResult.errors && latestImportResult.errors.length" class="import-result-block">
<div class="import-result-title">失败明细</div>
<div class="import-result-title">{{ $t("device.dialog.importResult.failDetails") }}</div>
<el-table :data="latestImportResult.errors" size="mini" border max-height="320">
<el-table-column label="行号" prop="rowIndex" width="100" align="center" />
<el-table-column label="错误信息" prop="message" min-width="460" />
<el-table-column :label="$t('device.dialog.importResult.rowIndex')" prop="rowIndex" width="100" align="center" />
<el-table-column :label="$t('device.dialog.importResult.errorMessage')" prop="message" min-width="460" />
</el-table>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="resultOpen = false"> </el-button>
<el-button type="primary" @click="resultOpen = false">{{ $t("device.button.close") }}</el-button>
</div>
</el-dialog>
<!-- 核心必须挂载企业选择组件且绑定正确的开关 -->
<BusinessSelect :visible.sync="businessSelectVisible" @select="handleBusinessSelect" />
<BusinessSelect :visible.sync="searchBusinessSelectVisible" @select="handleSearchBusinessSelect" />
<DeviceClaimDialog :visible.sync="claimDeviceOpen" @success="handleClaimSuccess" />
<el-dialog title="分配设备" :visible.sync="assignDeviceOpen" width="1200px" append-to-body @close="handleAssignDialogClose">
<el-dialog :title="$t('device.dialog.assign.title')" :visible.sync="assignDeviceOpen" width="1200px" append-to-body @close="handleAssignDialogClose">
<UserSelector ref="userSelector" selection-mode @select="handleAssignUserSelect" />
<div slot="footer" class="dialog-footer">
<span class="assign-selected-user">
{{ `已选择员工:${selectedAssignUsers.length}` }}
{{ $t("device.dialog.assign.selectedUsers", { count: selectedAssignUsers.length }) }}
</span>
<el-button @click="handleAssignDialogClose"> </el-button>
<el-button @click="handleAssignDialogClose">{{ $t("common.cancel") }}</el-button>
<el-button type="primary" :loading="assignLoading" @click="handleAssignSubmit">
{{ $t("common.confirm") }}
</el-button>
</div>
</el-dialog>
<el-dialog title="设备详情" :visible.sync="detailOpen" width="720px" append-to-body>
<el-dialog :title="$t('device.dialog.detail.title')" :visible.sync="detailOpen" width="720px" append-to-body>
<div v-loading="detailLoading">
<el-descriptions v-if="detailForm" :column="2" border>
<el-descriptions-item label="ID">{{ detailForm.id || "-" }}</el-descriptions-item>
<el-descriptions-item label="序列号">{{ detailForm.sn || "-" }}</el-descriptions-item>
<el-descriptions-item label="MAC 地址">{{ detailForm.mac || "-" }}</el-descriptions-item>
<el-descriptions-item label="型号">{{ detailForm.model || "-" }}</el-descriptions-item>
<el-descriptions-item label="企业">
<el-descriptions-item :label="$t('device.table.sn')">{{ detailForm.sn || "-" }}</el-descriptions-item>
<el-descriptions-item :label="$t('device.dialog.detail.macAddress')">{{ detailForm.mac || "-" }}</el-descriptions-item>
<el-descriptions-item :label="$t('device.table.model')">{{ detailForm.model || "-" }}</el-descriptions-item>
<el-descriptions-item :label="$t('device.dialog.detail.business')">
{{ detailForm.businessName || detailForm.merchantName || "-" }}
</el-descriptions-item>
<el-descriptions-item label="最后地址名称">
<el-descriptions-item :label="$t('device.dialog.detail.lastAddressName')">
{{ detailForm.lastAddress || "-" }}
</el-descriptions-item>
<el-descriptions-item label="分配员工">
<el-descriptions-item :label="$t('device.dialog.detail.assignedUsers')">
{{ formatAssignedUsers(detailForm.assignedUsers) }}
</el-descriptions-item>
<el-descriptions-item label="最后位置更新时间">
<el-descriptions-item :label="$t('device.dialog.detail.locateUpdateTime')">
{{ detailForm.locateUpdateTime ? parseTime(detailForm.locateUpdateTime, "{y}-{m}-{d} {h}:{i}:{s}") : "-" }}
</el-descriptions-item>
<el-descriptions-item label="最后经纬度">
<el-descriptions-item :label="$t('device.dialog.detail.lastCoordinates')">
{{ formatCoordinates(detailForm.lastLat, detailForm.lastLng) }}
</el-descriptions-item>
<el-descriptions-item label="电量">{{ detailForm.battery || "-" }}</el-descriptions-item>
<el-descriptions-item label="最后上报时间">
<el-descriptions-item :label="$t('device.dialog.detail.battery')">{{ detailForm.battery || "-" }}</el-descriptions-item>
<el-descriptions-item :label="$t('device.dialog.detail.lastReportedTime')">
{{ detailForm.lastReportedTime ? parseTime(detailForm.lastReportedTime, "{y}-{m}-{d} {h}:{i}:{s}") : "-" }}
</el-descriptions-item>
<el-descriptions-item label="最后位置时间">
<el-descriptions-item :label="$t('device.dialog.detail.lastLocationTime')">
{{ detailForm.lastLocationTime ? parseTime(detailForm.lastLocationTime, "{y}-{m}-{d} {h}:{i}:{s}") : "-" }}
</el-descriptions-item>
</el-descriptions>
</div>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="detailOpen = false"> </el-button>
<el-button type="primary" @click="detailOpen = false">{{ $t("device.button.close") }}</el-button>
</div>
</el-dialog>
<el-dialog title="修改设备信息" :visible.sync="editInfoOpen" width="520px" append-to-body @close="cancelEditInfo">
<el-dialog :title="$t('device.dialog.editInfo.title')" :visible.sync="editInfoOpen" width="520px" append-to-body @close="cancelEditInfo">
<el-form ref="editInfoForm" :model="editInfoForm" :rules="editInfoRules" label-width="100px">
<el-form-item label="序列号">
<el-form-item :label="$t('device.table.sn')">
<el-input :value="editInfoForm.sn || '-'" disabled />
</el-form-item>
<el-form-item label="名称" prop="alias">
<el-input v-model="editInfoForm.alias" placeholder="请输入名称" clearable maxlength="64" show-word-limit />
<el-form-item :label="$t('device.table.alias')" prop="alias">
<el-input v-model="editInfoForm.alias" :placeholder="$t('device.placeholder.alias')" clearable maxlength="64" show-word-limit />
</el-form-item>
<el-form-item label="设备备注" prop="remark">
<el-input v-model="editInfoForm.remark" type="textarea" :rows="4" placeholder="请输入设备备注" maxlength="255" show-word-limit />
<el-form-item :label="$t('device.query.remark')" prop="remark">
<el-input v-model="editInfoForm.remark" type="textarea" :rows="4" :placeholder="$t('device.placeholder.remark')" maxlength="255" show-word-limit />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="cancelEditInfo"> </el-button>
<el-button type="primary" :loading="editInfoSubmitting" @click="submitEditInfo"> </el-button>
<el-button @click="cancelEditInfo">{{ $t("common.cancel") }}</el-button>
<el-button type="primary" :loading="editInfoSubmitting" @click="submitEditInfo">{{ $t("common.confirm") }}</el-button>
</div>
</el-dialog>
<DeviceTrajectoryDialog :visible.sync="trajectoryOpen" :device="trajectoryDevice" />
<!-- 添加或修改系统设备主对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="序列号" prop="sn">
<el-input v-model="form.sn" placeholder="请输入序列号" />
<el-form-item :label="$t('device.table.sn')" prop="sn">
<el-input v-model="form.sn" :placeholder="$t('device.placeholder.sn')" />
</el-form-item>
<el-form-item label="MAC 地址" prop="mac">
<el-input v-model="form.mac" placeholder="请输入MAC 地址" />
<el-form-item :label="$t('device.dialog.detail.macAddress')" prop="mac">
<el-input v-model="form.mac" :placeholder="$t('device.placeholder.macAddress')" />
</el-form-item>
<!-- 新增订单号表单项 -->
<el-form-item label="订单号" prop="orderCode">
<el-input v-model="form.orderCode" placeholder="请输入订单号" />
<el-form-item :label="$t('device.table.orderCode')" prop="orderCode">
<el-input v-model="form.orderCode" :placeholder="$t('device.placeholder.orderCode')" />
</el-form-item>
<el-form-item label="私钥" prop="privateKey">
<el-input v-model="form.privateKey" placeholder="请输入私钥" />
<el-form-item :label="$t('device.form.privateKey')" prop="privateKey">
<el-input v-model="form.privateKey" :placeholder="$t('device.placeholder.privateKey')" />
</el-form-item>
<el-form-item label="所属批次" prop="batchNo">
<el-input v-model="form.batchNo" placeholder="请输入所属批次" />
<el-form-item :label="$t('device.form.batchNo')" prop="batchNo">
<el-input v-model="form.batchNo" :placeholder="$t('device.placeholder.batchNo')" />
</el-form-item>
<el-form-item label="设备的唯一哈希 ID" prop="hashid">
<el-input v-model="form.hashid" placeholder="请输入设备的唯一哈希 ID" />
<el-form-item :label="$t('device.form.hashId')" prop="hashid">
<el-input v-model="form.hashid" :placeholder="$t('device.placeholder.hashId')" />
</el-form-item>
<el-form-item label="型号" prop="model">
<el-input v-model="form.model" placeholder="请输入型号" />
<el-form-item :label="$t('device.table.model')" prop="model">
<el-input v-model="form.model" :placeholder="$t('device.placeholder.model')" />
</el-form-item>
<el-form-item label="绑定企业id" prop="bindBusinessId">
<el-input v-model="form.bindBusinessId" placeholder="请输入绑定企业id" />
<el-form-item :label="$t('device.form.bindBusinessId')" prop="bindBusinessId">
<el-input v-model="form.bindBusinessId" :placeholder="$t('device.placeholder.bindBusinessId')" />
</el-form-item>
<!-- 新增备注表单项 -->
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" type="textarea" :rows="3" />
<el-form-item :label="$t('device.table.remark')" prop="remark">
<el-input v-model="form.remark" :placeholder="$t('device.placeholder.remarkSimple')" type="textarea" :rows="3" />
</el-form-item>
<el-form-item label="最后位置更新时间" prop="locateUpdateTime">
<el-date-picker clearable size="small" v-model="form.locateUpdateTime" type="date" value-format="yyyy-MM-dd" placeholder="选择最后位置更新时间">
<el-form-item :label="$t('device.form.locateUpdateTime')" prop="locateUpdateTime">
<el-date-picker clearable size="small" v-model="form.locateUpdateTime" type="date" value-format="yyyy-MM-dd" :placeholder="$t('device.placeholder.locateUpdateTime')">
</el-date-picker>
</el-form-item>
<el-form-item label="最后纬度" prop="lastLat">
<el-input v-model="form.lastLat" placeholder="请输入最后纬度" />
<el-form-item :label="$t('device.form.lastLat')" prop="lastLat">
<el-input v-model="form.lastLat" :placeholder="$t('device.placeholder.lastLat')" />
</el-form-item>
<el-form-item label="最后经度" prop="lastLng">
<el-input v-model="form.lastLng" placeholder="请输入最后经度" />
<el-form-item :label="$t('device.form.lastLng')" prop="lastLng">
<el-input v-model="form.lastLng" :placeholder="$t('device.placeholder.lastLng')" />
</el-form-item>
<el-form-item label="电量" prop="battery">
<el-input v-model="form.battery" placeholder="请输入电量" />
<el-form-item :label="$t('device.dialog.detail.battery')" prop="battery">
<el-input v-model="form.battery" :placeholder="$t('device.placeholder.battery')" />
</el-form-item>
<el-form-item label="最后上报时间" prop="lastReportedTime">
<el-date-picker clearable size="small" v-model="form.lastReportedTime" type="date" value-format="yyyy-MM-dd" placeholder="选择最后上报时间">
<el-form-item :label="$t('device.dialog.detail.lastReportedTime')" prop="lastReportedTime">
<el-date-picker clearable size="small" v-model="form.lastReportedTime" type="date" value-format="yyyy-MM-dd" :placeholder="$t('device.placeholder.lastReportedTime')">
</el-date-picker>
</el-form-item>
<el-form-item label="最后位置时间" prop="lastLocationTime">
<el-date-picker clearable size="small" v-model="form.lastLocationTime" type="date" value-format="yyyy-MM-dd" placeholder="选择最后位置时间">
<el-form-item :label="$t('device.dialog.detail.lastLocationTime')" prop="lastLocationTime">
<el-date-picker clearable size="small" v-model="form.lastLocationTime" type="date" value-format="yyyy-MM-dd" :placeholder="$t('device.placeholder.lastLocationTime')">
</el-date-picker>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker clearable size="small" v-model="form.createTime" type="date" value-format="yyyy-MM-dd" placeholder="选择创建时间">
<el-form-item :label="$t('common.createTime')" prop="createTime">
<el-date-picker clearable size="small" v-model="form.createTime" type="date" value-format="yyyy-MM-dd" :placeholder="$t('device.placeholder.createTime')">
</el-date-picker>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
<el-button type="primary" @click="submitForm">{{ $t("common.confirm") }}</el-button>
<el-button @click="cancel">{{ $t("common.cancel") }}</el-button>
</div>
</el-dialog>
</div>
@ -466,11 +466,11 @@ export default {
//
total: 0,
activationStatusOptions: [{
label: "已启用",
label: this.$t("device.status.enabled"),
value: true
},
{
label: "未启用",
label: this.$t("device.status.disabled"),
value: false
},
],
@ -501,12 +501,12 @@ export default {
editInfoRules: {
alias: [{
max: 64,
message: "名称长度不能超过64个字符",
message: this.$t("device.validation.aliasMax"),
trigger: "blur"
}],
remark: [{
max: 255,
message: "设备备注长度不能超过255个字符",
message: this.$t("device.validation.remarkMax"),
trigger: "blur"
}],
},
@ -535,7 +535,7 @@ export default {
importRules: {
file: [{
required: true,
message: "请选择要上传的Excel文件",
message: this.$t("device.validation.fileRequired"),
trigger: "change"
}],
},
@ -555,42 +555,42 @@ export default {
rules: {
sn: [{
required: true,
message: "序列号不能为空",
message: this.$t("device.validation.snRequired"),
trigger: "blur"
}],
mac: [{
required: true,
message: "MAC 地址不能为空",
message: this.$t("device.validation.macRequired"),
trigger: "blur"
}],
orderCode: [
//
{
required: true,
message: "订单号不能为空",
message: this.$t("device.validation.orderCodeRequired"),
trigger: "blur"
},
],
privateKey: [{
required: true,
message: "私钥不能为空",
message: this.$t("device.validation.privateKeyRequired"),
trigger: "blur"
}],
batchNo: [{
required: true,
message: "所属批次不能为空",
message: this.$t("device.validation.batchNoRequired"),
trigger: "blur"
}],
hashid: [{
required: true,
message: "设备的唯一哈希 ID不能为空",
message: this.$t("device.validation.hashIdRequired"),
trigger: "blur"
}, ],
remark: [
//
{
required: false,
message: "备注不能为空",
message: this.$t("device.validation.remarkRequired"),
trigger: "blur"
},
],
@ -693,17 +693,25 @@ export default {
this.multiple = !selection.length;
},
getActivationStatusLabel(value) {
return value === true || value === 1 || value === "1" ? "启用" : "禁用";
return value === true || value === 1 || value === "1" ? this.$t("device.status.enabled") : this.$t("device.status.disabled");
},
getActivationTagType(value) {
return value === true || value === 1 || value === "1" ? "success" : "info";
},
formatCoordinateValue(value) {
if (value === null || value === undefined || value === "") {
if (value === null || value === undefined) {
return "-";
}
let strValue = String(value);
const [integerPart, decimalPart] = strValue.split(".");
const strValue = String(value).trim();
if (!strValue || strValue === "-") {
return "-";
}
const match = strValue.match(/^([+-]?\d+)(?:\.(\d+))?$/);
if (!match) {
return "-";
}
const integerPart = match[1];
const decimalPart = match[2] || "";
const fixedDecimal = decimalPart.slice(0, 2).padEnd(2, "0");
return `${integerPart}.${fixedDecimal}`;
@ -737,7 +745,7 @@ export default {
/** 打开分配设备弹窗 */
handleAssignDevice() {
if (!this.ids.length) {
this.$message.warning("请先勾选需要分配的设备");
this.$message.warning(this.$t("device.message.selectDeviceForAssign"));
return;
}
this.assignDeviceOpen = true;
@ -764,11 +772,11 @@ export default {
/** 提交分配设备 */
handleAssignSubmit() {
if (!this.ids.length) {
this.$message.warning("请先勾选需要分配的设备");
this.$message.warning(this.$t("device.message.selectDeviceForAssign"));
return;
}
if (!this.selectedAssignUsers.length) {
this.$message.warning("请选择员工账户");
this.$message.warning(this.$t("device.message.selectUserForAssign"));
return;
}
const userIds = Array.from(
@ -780,14 +788,17 @@ export default {
);
const deviceIds = Array.from(new Set(this.ids));
if (!userIds.length) {
this.$message.warning("未获取到有效的员工账号");
this.$message.warning(this.$t("device.message.invalidUserSelection"));
return;
}
this.$confirm(
`确认将选中的 ${deviceIds.length} 台设备分配给 ${userIds.length} 名员工吗?`,
"提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
this.$t("device.message.confirmAssign", {
deviceCount: deviceIds.length,
userCount: userIds.length,
}),
this.$t("common.tips"), {
confirmButtonText: this.$t("common.confirm"),
cancelButtonText: this.$t("common.cancel"),
type: "warning",
}
)
@ -797,7 +808,7 @@ export default {
userIds,
deviceIds,
});
this.$message.success("分配成功");
this.$message.success(this.$t("device.message.assignSuccess"));
this.handleAssignDialogClose();
this.getList();
})
@ -809,19 +820,19 @@ export default {
/** 批量启用设备 */
handleBatchActivate() {
if (!this.ids.length) {
this.$message.warning("请先勾选需要启用的设备");
this.$message.warning(this.$t("device.message.selectDeviceForEnable"));
return;
}
this.$confirm(`确认启用选中的 ${this.ids.length} 台设备吗?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
this.$confirm(this.$t("device.message.confirmBatchEnable", { count: this.ids.length }), this.$t("common.tips"), {
confirmButtonText: this.$t("common.confirm"),
cancelButtonText: this.$t("common.cancel"),
type: "warning",
})
.then(() => {
return batchActivateDevice(this.ids);
})
.then(() => {
this.$message.success("批量启用成功");
this.$message.success(this.$t("device.message.batchEnableSuccess"));
this.getList();
})
.catch(() => {});
@ -829,19 +840,19 @@ export default {
/** 批量禁用设备 */
handleBatchDisable() {
if (!this.ids.length) {
this.$message.warning("请先勾选需要禁用的设备");
this.$message.warning(this.$t("device.message.selectDeviceForDisable"));
return;
}
this.$confirm(`确认禁用选中的 ${this.ids.length} 台设备吗?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
this.$confirm(this.$t("device.message.confirmBatchDisable", { count: this.ids.length }), this.$t("common.tips"), {
confirmButtonText: this.$t("common.confirm"),
cancelButtonText: this.$t("common.cancel"),
type: "warning",
})
.then(() => {
return batchDisableDevice(this.ids);
})
.then(() => {
this.$message.success("批量禁用成功");
this.$message.success(this.$t("device.message.batchDisableSuccess"));
this.getList();
})
.catch(() => {});
@ -883,7 +894,7 @@ export default {
/** 修改名称和备注 */
handleEditInfo(row) {
if (!row || !row.id) {
this.$message.warning("未获取到设备信息");
this.$message.warning(this.$t("device.message.deviceInfoMissing"));
return;
}
this.resetEditInfoForm();
@ -915,7 +926,7 @@ export default {
remark: this.editInfoForm.remark,
})
.then(() => {
this.$message.success("修改成功");
this.$message.success(this.$t("device.message.updateSuccess"));
this.editInfoOpen = false;
this.getList();
})
@ -927,7 +938,7 @@ export default {
/** 查看设备轨迹 */
handleTrajectory(row) {
if (!row || !row.id) {
this.$message.warning("未获取到设备信息");
this.$message.warning(this.$t("device.message.deviceInfoMissing"));
return;
}
this.trajectoryDevice = {
@ -948,7 +959,7 @@ export default {
handleAdd() {
this.reset();
this.open = true;
this.title = "添加系统设备主";
this.title = this.$t("device.dialog.form.addTitle");
},
/** 修改按钮操作 */
handleUpdate(row) {
@ -957,7 +968,7 @@ export default {
getDevice(id).then((response) => {
this.form = response.data;
this.open = true;
this.title = "修改系统设备主";
this.title = this.$t("device.dialog.form.editTitle");
});
},
/** 提交按钮 */
@ -966,13 +977,13 @@ export default {
if (valid) {
if (this.form.id != null) {
updateDevice(this.form).then((response) => {
this.$message.success("修改成功");
this.$message.success(this.$t("device.message.updateSuccess"));
this.open = false;
this.getList();
});
} else {
addDevice(this.form).then((response) => {
this.$message.success("新增成功");
this.$message.success(this.$t("device.message.addSuccess"));
this.open = false;
this.getList();
});
@ -983,9 +994,9 @@ export default {
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id || this.ids;
this.$confirm('是否确认删除系统设备主编号为"' + ids + '"的数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
this.$confirm(this.$t("device.message.confirmDelete", { ids }), this.$t("device.message.warning"), {
confirmButtonText: this.$t("common.confirm"),
cancelButtonText: this.$t("common.cancel"),
type: "warning",
})
.then(function () {
@ -993,16 +1004,16 @@ export default {
})
.then(() => {
this.getList();
this.$message.success("删除成功");
this.$message.success(this.$t("device.message.deleteSuccess"));
})
.catch(() => {});
},
/** 导出按钮操作 */
handleExport() {
const queryParams = this.queryParams;
this.$confirm("是否确认导出所有系统设备主数据项?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
this.$confirm(this.$t("device.message.confirmExport"), this.$t("device.message.warning"), {
confirmButtonText: this.$t("common.confirm"),
cancelButtonText: this.$t("common.cancel"),
type: "warning",
})
.then(function () {
@ -1037,13 +1048,13 @@ export default {
}
})
.catch((err) => {
this.$message.error("获取批次号失败:" + (err.message || "接口异常"));
this.$message.error(this.$t("device.message.fetchBatchNoFailed") + (err.message || this.$t("device.message.apiException")));
});
});
},
/** 限制文件数量:超出1个时提示 */
handleExceed(files, fileList) {
this.$message.warning(`最多只能上传1个Excel文件,当前已选择 ${fileList.length}`);
this.$message.warning(this.$t("device.message.exceedFileLimit", { count: fileList.length }));
},
/** 上传前校验文件格式 */
@ -1053,7 +1064,7 @@ export default {
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
file.type === "application/vnd.ms-excel";
if (!isExcel) {
this.$message.error("只能上传 .xlsx 或 .xls 格式的Excel文件!");
this.$message.error(this.$t("device.message.invalidFileType"));
return false; //
}
return true;
@ -1078,11 +1089,11 @@ export default {
// 2.
if (!this.uploadFile || this.fileList.length === 0) {
this.$message.warning("请选择要上传的Excel文件(仅支持1个文件)!");
this.$message.warning(this.$t("device.message.selectOneExcel"));
return;
}
if (this.fileList.length > 1) {
this.$message.warning("最多只能上传1个Excel文件,请删除多余文件!");
this.$message.warning(this.$t("device.message.removeExtraFiles"));
return;
}
@ -1104,10 +1115,10 @@ export default {
if (this.latestImportResult.failCount > 0 || this.latestImportResult.requestErrors.length > 0) {
this.$message.warning(this.getImportStatusMessage(this.latestImportResult));
} else {
this.$message.success("导入成功!");
this.$message.success(this.$t("device.message.importSuccess"));
}
} catch (error) {
this.$message.error("导入失败:" + (error.message || "服务器异常"));
this.$message.error(this.$t("device.message.importFailed") + (error.message || this.$t("device.message.serverException")));
} finally {
this.importing = false;
}
@ -1170,12 +1181,12 @@ export default {
},
getImportStatusLabel(status) {
const statusMap = {
SUCCESS: "导入成功",
PARTIAL_SUCCESS: "部分成功",
FAILED: "导入失败",
REQUEST_INVALID: "请求校验失败",
SUCCESS: this.$t("device.importStatus.SUCCESS"),
PARTIAL_SUCCESS: this.$t("device.importStatus.PARTIAL_SUCCESS"),
FAILED: this.$t("device.importStatus.FAILED"),
REQUEST_INVALID: this.$t("device.importStatus.REQUEST_INVALID"),
};
return statusMap[status] || status || "未知状态";
return statusMap[status] || status || this.$t("device.importStatus.UNKNOWN");
},
getImportStatusType(status) {
const typeMap = {
@ -1188,7 +1199,11 @@ export default {
},
getImportStatusMessage(result) {
const statusLabel = this.getImportStatusLabel(result.status);
return `${statusLabel},成功 ${result.successCount} 条,失败 ${result.failCount}`;
return this.$t("device.message.importStatusSummary", {
status: statusLabel,
successCount: result.successCount,
failCount: result.failCount,
});
},
},
};

22
src/views/login.vue

@ -145,20 +145,20 @@
<!-- 右侧登录表单区域 -->
<div class="login-right">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">GeoTag 企业客户后台</h3>
<h3 class="title">{{ $t("login.title") }}</h3>
<el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" auto-complete="off" placeholder="账号">
<el-input v-model="loginForm.username" type="text" auto-complete="off" :placeholder="$t('login.usernamePlaceholder')">
<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="密码"
<el-input v-model="loginForm.password" type="password" auto-complete="off" :placeholder="$t('login.passwordPlaceholder')"
@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%"
<el-input v-model="loginForm.code" auto-complete="off" :placeholder="$t('login.googleCodePlaceholder')" style="width: 100%"
@keyup.enter.native="handleLogin">
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
</el-input>
@ -179,8 +179,8 @@
<el-form-item style="width:100%;">
<el-button :loading="loading" size="medium" type="primary" style="width:100%;"
@click.native.prevent="handleLogin">
<span v-if="!loading"> </span>
<span v-else> 中...</span>
<span v-if="!loading">{{ $t("login.login") }}</span>
<span v-else>{{ $t("login.loggingIn") }}</span>
</el-button>
</el-form-item>
</el-form>
@ -202,7 +202,7 @@ export default {
data() {
var checkCode = (rule, value, callback) => {
if (value == false) {
callback(new Error('请进行人机验证'));
callback(new Error(this.$t("login.humanVerifyRequired")));
} else {
callback();
}
@ -221,13 +221,13 @@ export default {
},
loginRules: {
username: [
{ required: true, trigger: "blur", message: "用户名不能为空" }
{ required: true, trigger: "blur", message: this.$t("login.usernameRequired") }
],
password: [
{ required: true, trigger: "blur", message: "密码不能为空" }
{ required: true, trigger: "blur", message: this.$t("login.passwordRequired") }
],
code: [
{ required: true, trigger: "change", message: "验证码不能为空" }
{ required: true, trigger: "change", message: this.$t("login.codeRequired") }
],
validateCode: [
{ validator: checkCode, trigger: 'change' }
@ -611,4 +611,4 @@ export default {
max-width: 400px;
}
}
</style>
</style>

493
src/views/system/menu/index.vue

@ -1,16 +1,22 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch">
<el-form-item label="菜单名称" prop="menuName">
<el-form
ref="queryForm"
:model="queryParams"
size="small"
:inline="true"
v-show="showSearch"
>
<el-form-item :label="$t('menu.menuName')" prop="menuName">
<el-input
v-model="queryParams.menuName"
placeholder="请输入菜单名称"
:placeholder="$t('menu.defaultNamePlaceholder')"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="菜单状态" clearable>
<el-form-item :label="$t('menu.status')" prop="status">
<el-select v-model="queryParams.status" :placeholder="$t('menu.menuStatus')" clearable>
<el-option
v-for="item in normalDisableOptions"
:key="item.value"
@ -20,8 +26,8 @@
</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-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t("menu.search") }}</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t("menu.reset") }}</el-button>
</el-form-item>
</el-form>
@ -34,7 +40,7 @@
size="mini"
@click="handleAdd"
v-hasPermi="['system:menu:add']"
>新增</el-button>
>{{ $t("menu.add") }}</el-button>
</el-col>
<el-col :span="1.5">
<el-button
@ -43,9 +49,9 @@
icon="el-icon-sort"
size="mini"
@click="toggleExpandAll"
>展开/折叠</el-button>
>{{ $t("menu.expandCollapse") }}</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
</el-row>
<el-table
@ -56,26 +62,26 @@
:default-expand-all="isExpandAll"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160" />
<el-table-column prop="icon" label="图标" align="center" width="100">
<el-table-column prop="menuName" :label="$t('menu.menuName')" :show-overflow-tooltip="true" width="180" />
<el-table-column prop="icon" :label="$t('menu.icon')" align="center" width="100">
<template slot-scope="scope">
<svg-icon :icon-class="scope.row.icon" />
</template>
</el-table-column>
<el-table-column prop="orderNum" label="排序" width="60" />
<el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true" />
<el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true" />
<el-table-column prop="status" label="状态" width="80">
<el-table-column prop="orderNum" :label="$t('menu.orderNum')" width="70" />
<el-table-column prop="perms" :label="$t('menu.perms')" :show-overflow-tooltip="true" />
<el-table-column prop="component" :label="$t('menu.component')" :show-overflow-tooltip="true" />
<el-table-column prop="status" :label="$t('menu.status')" width="80">
<template slot-scope="scope">
<dict-tag :options="normalDisableOptions" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime">
<el-table-column :label="$t('menu.createTime')" align="center" prop="createTime" width="160">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column :label="$t('menu.actions')" align="center" class-name="small-padding fixed-width" width="220">
<template slot-scope="scope">
<el-button
size="mini"
@ -83,54 +89,56 @@
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['system:menu:edit']"
>修改</el-button>
>{{ $t("menu.edit") }}</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-plus"
@click="handleAdd(scope.row)"
v-hasPermi="['system:menu:add']"
>新增</el-button>
>{{ $t("menu.add") }}</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['system:menu:remove']"
>删除</el-button>
>{{ $t("menu.delete") }}</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog :title="title" :visible.sync="open" width="680px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-dialog :title="title" :visible.sync="open" width="760px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="110px">
<el-row>
<el-col :span="24">
<el-form-item label="上级菜单" prop="parentId">
<el-form-item :label="$t('menu.parentMenu')" prop="parentId">
<treeselect
v-model="form.parentId"
:options="menuOptions"
:normalizer="normalizer"
:show-count="true"
placeholder="选择上级菜单"
:placeholder="$t('menu.selectParentMenu')"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="菜单类型" prop="menuType">
<el-form-item :label="$t('menu.menuType')" prop="menuType">
<el-radio-group v-model="form.menuType">
<el-radio label="M">目录</el-radio>
<el-radio label="C">菜单</el-radio>
<el-radio label="F">按钮</el-radio>
<el-radio label="M">{{ $t("menu.menuTypeM") }}</el-radio>
<el-radio label="C">{{ $t("menu.menuTypeC") }}</el-radio>
<el-radio label="F">{{ $t("menu.menuTypeF") }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-form-item label="菜单图标" prop="icon">
<el-col :span="12" v-if="form.menuType !== 'F'">
<el-form-item :label="$t('menu.menuIcon')" prop="icon">
<el-popover
placement="bottom-start"
width="460"
@ -138,129 +146,68 @@
@show="$refs.iconSelect.reset()"
>
<IconSelect ref="iconSelect" @selected="selected" :active-icon="form.icon" />
<el-input slot="reference" v-model="form.icon" placeholder="点击选择图标" readonly>
<svg-icon
v-if="form.icon"
slot="prefix"
:icon-class="form.icon"
style="width: 25px;"
/>
<el-input slot="reference" v-model="form.icon" :placeholder="$t('menu.clickSelectIcon')" readonly>
<svg-icon v-if="form.icon" slot="prefix" :icon-class="form.icon" style="width: 25px" />
<i v-else slot="prefix" class="el-icon-search el-input__icon" />
</el-input>
</el-popover>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="显示排序" prop="orderNum">
<el-form-item :label="$t('menu.displayOrder')" prop="orderNum">
<el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="菜单名称" prop="menuName">
<el-input v-model="form.menuName" placeholder="请输入菜单名称" />
<el-form-item :label="$t('menu.defaultName')" prop="menuName">
<el-input v-model="form.menuName" :placeholder="$t('menu.defaultNamePlaceholder')" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item prop="routeName">
<el-input v-model="form.routeName" placeholder="请输入路由名称" />
<span slot="label">
<el-tooltip content="默认不填则和路由地址相同:如地址为:`user`,则名称为`User`(注意:为避免名字的冲突,特殊情况下请自定义,保证唯一性)" placement="top">
<i class="el-icon-question" />
</el-tooltip>
路由名称
</span>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-form-item prop="isFrame">
<span slot="label">
<el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
<i class="el-icon-question" />
</el-tooltip>
是否外链
</span>
<el-radio-group v-model="form.isFrame">
<el-radio label="0"></el-radio>
<el-radio label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-form-item prop="path">
<span slot="label">
<el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
<i class="el-icon-question" />
</el-tooltip>
路由地址
</span>
<el-input v-model="form.path" placeholder="请输入路由地址" />
<el-col :span="12" v-if="form.menuType !== 'F'">
<el-form-item :label="$t('menu.path')" prop="path">
<el-input v-model="form.path" :placeholder="$t('menu.pathPlaceholder')" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item prop="component">
<span slot="label">
<el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
<i class="el-icon-question" />
</el-tooltip>
组件路径
</span>
<el-input v-model="form.component" placeholder="请输入组件路径" />
<el-col :span="12" v-if="form.menuType === 'C'">
<el-form-item :label="$t('menu.componentPath')" prop="component">
<el-input v-model="form.component" :placeholder="$t('menu.componentPathPlaceholder')" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'M'">
<el-form-item prop="perms">
<el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
<span slot="label">
<el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermi('system:user:list')`)" placement="top">
<i class="el-icon-question" />
</el-tooltip>
权限字符
</span>
<el-col :span="12" v-if="form.menuType !== 'M'">
<el-form-item :label="$t('menu.permsLabel')" prop="perms">
<el-input v-model="form.perms" :placeholder="$t('menu.permsPlaceholder')" maxlength="100" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item prop="query">
<el-input v-model="form.query" placeholder="请输入路由参数" maxlength="255" />
<span slot="label">
<el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
<i class="el-icon-question" />
</el-tooltip>
路由参数
</span>
<el-col :span="12" v-if="form.menuType !== 'F'">
<el-form-item :label="$t('menu.isFrame')" prop="isFrame">
<el-radio-group v-model="form.isFrame">
<el-radio label="0">{{ $t("menu.yes") }}</el-radio>
<el-radio label="1">{{ $t("menu.no") }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item prop="isCache">
<span slot="label">
<el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
<i class="el-icon-question" />
</el-tooltip>
是否缓存
</span>
<el-col :span="12" v-if="form.menuType === 'C'">
<el-form-item :label="$t('menu.isCache')" prop="isCache">
<el-radio-group v-model="form.isCache">
<el-radio label="0">缓存</el-radio>
<el-radio label="1">不缓存</el-radio>
<el-radio label="0">{{ $t("menu.cache") }}</el-radio>
<el-radio label="1">{{ $t("menu.noCache") }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-form-item prop="visible">
<span slot="label">
<el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
<i class="el-icon-question" />
</el-tooltip>
显示状态
</span>
<el-col :span="12" v-if="form.menuType !== 'F'">
<el-form-item :label="$t('menu.visible')" prop="visible">
<el-radio-group v-model="form.visible">
<el-radio
v-for="item in showHideOptions"
@ -271,13 +218,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="status">
<span slot="label">
<el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
<i class="el-icon-question" />
</el-tooltip>
菜单状态
</span>
<el-form-item :label="$t('menu.menuStatus')" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="item in normalDisableOptions"
@ -288,34 +229,78 @@
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">{{ $t("menu.i18nTitle") }}</el-divider>
<el-row>
<el-col :span="12">
<el-form-item :label="$t('menu.zhCN')">
<el-input v-model="form.i18nNames['zh-CN']" :placeholder="$t('menu.zhCNPlaceholder')" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('menu.enUS')">
<el-input v-model="form.i18nNames['en-US']" :placeholder="$t('menu.enUSPlaceholder')" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item :label="$t('menu.ptBR')">
<el-input v-model="form.i18nNames['pt-BR']" :placeholder="$t('menu.ptBRPlaceholder')" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('menu.esES')">
<el-input v-model="form.i18nNames['es-ES']" :placeholder="$t('menu.esESPlaceholder')" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item :label="$t('menu.frFR')">
<el-input v-model="form.i18nNames['fr-FR']" :placeholder="$t('menu.frFRPlaceholder')" />
</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>
<el-button type="primary" @click="submitForm">{{ $t("common.confirm") }}</el-button>
<el-button @click="cancel">{{ $t("common.cancel") }}</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listMenu, getMenu, delMenu, addMenu, updateMenu } from "@/api/system/menu"
import Treeselect from "@riophae/vue-treeselect"
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
import IconSelect from "@/components/IconSelect"
import { listMenu, getMenu, delMenu, addMenu, updateMenu } from "@/api/system/menu";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
import IconSelect from "@/components/IconSelect";
const SHOW_HIDE_OPTIONS = [
{ labelKey: "menu.show", value: "0" },
{ labelKey: "menu.hide", value: "1" },
];
const SHOW_HIDE_FALLBACK = [
{ label: "显示", value: "0" },
{ label: "隐藏", value: "1" }
]
const NORMAL_DISABLE_OPTIONS = [
{ labelKey: "menu.normal", value: "0" },
{ labelKey: "menu.disable", value: "1" },
];
const NORMAL_DISABLE_FALLBACK = [
{ label: "正常", value: "0" },
{ label: "停用", value: "1" }
]
const createDefaultI18nNames = () => ({
"zh-CN": "",
"en-US": "",
"fr-FR": "",
"pt-BR": "",
"es-ES": "",
});
export default {
name: "Menu",
dicts: [],
components: { Treeselect, IconSelect },
data() {
return {
@ -329,159 +314,173 @@ export default {
refreshTable: true,
queryParams: {
menuName: undefined,
status: undefined
status: undefined,
},
form: {},
form: this.normalizeMenuForm(),
rules: {
menuName: [
{ required: true, message: "菜单名称不能为空", trigger: "blur" }
],
orderNum: [
{ required: true, message: "菜单顺序不能为空", trigger: "blur" }
],
path: [
{ required: true, message: "路由地址不能为空", trigger: "blur" }
]
}
}
},
computed: {
// showHideOptions() {
// return this.normalizeDictOptions(this.dict.type.sys_show_hide, SHOW_HIDE_FALLBACK)
// },
// normalDisableOptions() {
// return this.normalizeDictOptions(this.dict.type.sys_normal_disable, NORMAL_DISABLE_FALLBACK)
// }
menuName: [{ required: true, message: this.$t("menu.formNameRequired"), trigger: "blur" }],
orderNum: [{ required: true, message: this.$t("menu.formOrderRequired"), trigger: "blur" }],
path: [{ validator: this.validatePath, trigger: "blur" }],
},
showHideOptions: [],
normalDisableOptions: [],
};
},
created() {
this.getList()
this.initDictOptions();
this.getList();
},
methods: {
normalizeDictOptions(options, fallback) {
const source = Array.isArray(options) && options.length ? options : fallback
return source.map(item => ({
...item,
value: String(item.value)
}))
initDictOptions() {
this.showHideOptions = SHOW_HIDE_OPTIONS.map((item) => ({
value: item.value,
label: this.$t(item.labelKey),
}));
this.normalDisableOptions = NORMAL_DISABLE_OPTIONS.map((item) => ({
value: item.value,
label: this.$t(item.labelKey),
}));
},
validatePath(rule, value, callback) {
if (this.form.menuType !== "F" && !value) {
callback(new Error(this.$t("menu.formPathRequired")));
return;
}
callback();
},
normalizeMenuForm(data = {}) {
const i18nNames = {
...createDefaultI18nNames(),
...(data.i18nNames || {}),
};
if (!i18nNames["zh-CN"] && data.menuName) {
i18nNames["zh-CN"] = data.menuName;
}
return {
...data,
menuId: data.menuId,
parentId: data.parentId !== undefined && data.parentId !== null ? data.parentId : 0,
menuName: data.menuName,
icon: data.icon,
menuType: data.menuType !== undefined && data.menuType !== null ? String(data.menuType) : "M",
orderNum: data.orderNum,
path: data.path,
component: data.component,
isFrame: data.isFrame !== undefined && data.isFrame !== null ? String(data.isFrame) : "1",
isCache: data.isCache !== undefined && data.isCache !== null ? String(data.isCache) : "0",
visible: data.visible !== undefined && data.visible !== null ? String(data.visible) : "0",
status: data.status !== undefined && data.status !== null ? String(data.status) : "0"
}
status: data.status !== undefined && data.status !== null ? String(data.status) : "0",
perms: data.perms,
i18nNames,
};
},
selected(name) {
this.form.icon = name
this.form.icon = name;
},
getList() {
this.loading = true
listMenu(this.queryParams).then(response => {
this.menuList = this.handleTree(response.data, "menuId")
this.loading = false
})
this.loading = true;
listMenu(this.queryParams).then((response) => {
this.menuList = this.handleTree(response.data, "menuId");
this.loading = false;
});
},
normalizer(node) {
if (node.children && !node.children.length) {
delete node.children
delete node.children;
}
return {
id: node.menuId,
label: node.menuName,
children: node.children
}
children: node.children,
};
},
getTreeselect() {
listMenu().then(response => {
this.menuOptions = []
const menu = { menuId: 0, menuName: "主类目", children: [] }
menu.children = this.handleTree(response.data, "menuId")
this.menuOptions.push(menu)
})
listMenu().then((response) => {
this.menuOptions = [];
const menu = { menuId: 0, menuName: this.$t("menu.rootCategory"), children: [] };
menu.children = this.handleTree(response.data, "menuId");
this.menuOptions.push(menu);
});
},
cancel() {
this.open = false
this.reset()
this.open = false;
this.reset();
},
reset() {
this.form = this.normalizeMenuForm({
menuId: undefined,
parentId: 0,
menuName: undefined,
icon: undefined,
menuType: "M",
orderNum: undefined,
isFrame: "1",
isCache: "0",
visible: "0",
status: "0"
})
this.resetForm("form")
status: "0",
});
this.resetForm("form");
},
handleQuery() {
this.getList()
this.getList();
},
resetQuery() {
this.resetForm("queryForm")
this.handleQuery()
this.resetForm("queryForm");
this.handleQuery();
},
handleAdd(row) {
this.reset()
this.getTreeselect()
if (row != null && row.menuId) {
this.form.parentId = row.menuId
} else {
this.form.parentId = 0
}
this.open = true
this.title = "添加菜单"
this.reset();
this.getTreeselect();
this.form.parentId = row && row.menuId ? row.menuId : 0;
this.open = true;
this.title = this.$t("menu.titleAdd");
},
toggleExpandAll() {
this.refreshTable = false
this.isExpandAll = !this.isExpandAll
this.refreshTable = false;
this.isExpandAll = !this.isExpandAll;
this.$nextTick(() => {
this.refreshTable = true
})
this.refreshTable = true;
});
},
handleUpdate(row) {
this.reset()
this.getTreeselect()
getMenu(row.menuId).then(response => {
this.form = this.normalizeMenuForm(response.data)
this.open = true
this.title = "修改菜单"
})
this.reset();
this.getTreeselect();
getMenu(row.menuId).then((response) => {
this.form = this.normalizeMenuForm(response.data || {});
this.open = true;
this.title = this.$t("menu.titleEdit");
});
},
submitForm() {
this.$refs.form.validate(valid => {
if (valid) {
if (this.form.menuId !== undefined) {
updateMenu(this.form).then(() => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addMenu(this.form).then(() => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()
})
}
this.$refs.form.validate((valid) => {
if (!valid) {
return;
}
const payload = this.normalizeMenuForm(this.form);
payload.i18nNames["zh-CN"] = payload.menuName || "";
if (payload.menuId !== undefined && payload.menuId !== null) {
updateMenu(payload).then(() => {
this.$modal.msgSuccess(this.$t("menu.msgEditSuccess"));
this.open = false;
this.getList();
});
return;
}
})
addMenu(payload).then(() => {
this.$modal.msgSuccess(this.$t("menu.msgAddSuccess"));
this.open = false;
this.getList();
});
});
},
handleDelete(row) {
this.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?').then(function() {
return delMenu(row.menuId)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
}
}
}
this.$modal
.confirm(this.$t("menu.confirmDelete", { name: row.menuName }))
.then(() => delMenu(row.menuId))
.then(() => {
this.getList();
this.$modal.msgSuccess(this.$t("menu.msgDeleteSuccess"));
})
.catch(() => {});
},
},
};
</script>

Loading…
Cancel
Save