页面完成

This commit is contained in:
likaikai 2025-05-23 14:14:05 +08:00
parent c0df5c0869
commit 665b1efdae
73 changed files with 6740 additions and 550 deletions

View File

@ -1,5 +1,5 @@
# 页面标题
VITE_APP_TITLE = 若依管理系统
VITE_APP_TITLE = 测偏仪后台管理系统
# 开发环境配置
VITE_APP_ENV = 'development'

View File

@ -1,11 +1,11 @@
# 页面标题
VITE_APP_TITLE = 若依管理系统
VITE_APP_TITLE = 测偏仪后台管理系统
# 生产环境配置
VITE_APP_ENV = 'production'
# 若依管理系统/生产环境
VITE_APP_BASE_API = '/prod-api'
VITE_APP_BASE_API = '/api'
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS = gzip

49
.gitignore vendored
View File

@ -1,26 +1,33 @@
# ---> Java
# Compiled class file
*.class
.DS_Store
.history
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
**/*.log
# Log file
*.log
tests/**/coverage/
tests/e2e/reports
selenium-debug.log
# BlueJ files
*.ctxt
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.local
# Mobile Tools for Java (J2ME)
.mtj.tmp/
package-lock.json
yarn.lock
pnpm-lock.yaml
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
# 编译生成的文件
auto-imports.d.ts
components.d.ts
# 项目配置文件
vite.config.ts
dist.zip

83
package-lock.json generated
View File

@ -24,8 +24,11 @@
"nprogress": "0.2.0",
"pinia": "2.1.7",
"splitpanes": "3.1.5",
"typescript": "^5.8.3",
"vue": "3.4.31",
"vue-clipboard3": "^2.0.0",
"vue-cropper": "1.1.1",
"vue-i18n": "^11.1.3",
"vue-router": "4.4.0",
"vuedraggable": "4.1.0"
},
@ -529,6 +532,47 @@
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@intlify/core-base": {
"version": "11.1.3",
"resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.1.3.tgz",
"integrity": "sha512-cMuHunYO7LE80azTitcvEbs1KJmtd6g7I5pxlApV3Jo547zdO3h31/0uXpqHc+Y3RKt1wo2y68RGSx77Z1klyA==",
"dependencies": {
"@intlify/message-compiler": "11.1.3",
"@intlify/shared": "11.1.3"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "11.1.3",
"resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.1.3.tgz",
"integrity": "sha512-7rbqqpo2f5+tIcwZTAG/Ooy9C8NDVwfDkvSeDPWUPQW+Dyzfw2o9H103N5lKBxO7wxX9dgCDjQ8Umz73uYw3hw==",
"dependencies": {
"@intlify/shared": "11.1.3",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "11.1.3",
"resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.1.3.tgz",
"integrity": "sha512-pTFBgqa/99JRA2H1qfyqv97MKWJrYngXBA/I0elZcYxvJgcCw3mApAoPW3mJ7vx3j+Ti0FyKUFZ4hWxdjKaxvA==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -6566,6 +6610,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/ufo": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
@ -7014,12 +7070,39 @@
}
}
},
"node_modules/vue-clipboard3": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/vue-clipboard3/-/vue-clipboard3-2.0.0.tgz",
"integrity": "sha512-Q9S7dzWGax7LN5iiSPcu/K1GGm2gcBBlYwmMsUc5/16N6w90cbKow3FnPmPs95sungns4yvd9/+JhbAznECS2A==",
"dependencies": {
"clipboard": "^2.0.6"
}
},
"node_modules/vue-cropper": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/vue-cropper/-/vue-cropper-1.1.1.tgz",
"integrity": "sha512-WsqKMpaBf9Osi1LQlE/5AKdD0nHWOy1asLXocaG8NomOWO07jiZi968+/PbMmnD0QbPJOumDQaGuGa13qys85A==",
"license": "ISC"
},
"node_modules/vue-i18n": {
"version": "11.1.3",
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.1.3.tgz",
"integrity": "sha512-Pcylh9z9S5+CJAqgbRZ3EKxFIBIrtY5YUppU722GIT65+Nukm0TCqiQegZnNLCZkXGthxe0cpqj0AoM51H+6Gw==",
"dependencies": {
"@intlify/core-base": "11.1.3",
"@intlify/shared": "11.1.3",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-router": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.0.tgz",

View File

@ -31,8 +31,11 @@
"nprogress": "0.2.0",
"pinia": "2.1.7",
"splitpanes": "3.1.5",
"typescript": "^5.8.3",
"vue": "3.4.31",
"vue-clipboard3": "^2.0.0",
"vue-cropper": "1.1.1",
"vue-i18n": "^11.1.3",
"vue-router": "4.4.0",
"vuedraggable": "4.1.0"
},

92
src/api/customer/index.js Normal file
View File

@ -0,0 +1,92 @@
import request from '@/utils/request';
// 获取客户列表
export function getCustomerListApi(query) {
return request({
url: '/system/dept/customerList',
method: 'get',
params: query
})
}
// 获取客户列表
export function getDepListApi() {
return request({
url: '/system/dept/list',
method: 'get'
})
}
//新增客户-部门管理
export function addDepartmentApi(data) {
return request({
url: '/system/dept',
method: 'post',
data
})
}
//编辑客户-部门管理
export function editDepartmentApi(data) {
return request({
url: '/system/dept',
method: 'put',
data
})
}
//查询客户-部门管理详情
export function getDepartmentDetailApi(id) {
return request({
url: '/system/dept/' + id,
method: 'get'
})
}
//删除客户-部门管理
export function deleteDepartmentApi(id) {
return request({
url: '/system/dept/' + id,
method: 'delete'
})
}
//获取客户-部门管理下拉树结构
export function getDepartmentTreeApi() {
return request({
url: '/system/user/deptTree',
method: 'get'
})
}
//获取客户-部门-岗位关联列表
export function getDepartmentPostListApi(query) {
return request({
url: '/customer/dept/post/rel/list',
method: 'get',
params: query
})
}
//新增客户-部门-岗位关联
export function addDepartmentPostApi(data) {
return request({
url: '/customer/dept/post/rel',
method: 'post',
data
})
}
//编辑客户-部门-岗位关联
export function editDepartmentPostApi(data) {
return request({
url: '/customer/dept/post/rel',
method: 'put',
data
})
}
//删除客户-部门-岗位关联
export function deleteDepartmentPostApi(id) {
return request({
url: '/customer/dept/post/rel/' + id,
method: 'delete'
})
}
//获取客户-部门-岗位关联详情
export function getDepartmentPostDetailApi(id) {
return request({
url: '/customer/dept/post/rel/' + id,
method: 'get'
})
}

22
src/api/dashboard.js Normal file
View File

@ -0,0 +1,22 @@
import request from '@/utils/request'
//获取数据总览
export function getDashboardApi() {
return request({
url: '/index/overview',
method: 'get'
})
}
// 维保分析
export function getMaintainAnalysisApi() {
return request({
url: '/index/maintenanceAnalysis',
method: 'get'
})
}
//维保汇总
export function getMaintainSummaryApi() {
return request({
url: '/index/maintenanceOverview',
method: 'get'
})
}

View File

@ -0,0 +1,99 @@
import request from '@/utils/request';
import {getDicts} from "@/api/system/dict/data.js";
//获取设备型号列表
export function getDeviceModelListApi(query) {
return request({
url: '/device/model/list',
method: 'get',
params: query
})
}
//新增设备型号
export function addDeviceModelApi(data) {
return request({
url: '/device/model',
method: 'post',
data
})
}
//编辑设备型号
export function editDeviceModelApi(data) {
return request({
url: '/device/model',
method: 'put',
data
})
}
//查询设备型号详情
export function getDeviceModelDetailApi(id) {
return request({
url: '/device/model/' + id,
method: 'get'
})
}
//删除设备型号
export function deleteDeviceModelApi(id) {
return request({
url: '/device/model/' + id,
method: 'delete'
})
}
//获取设备信息列表
export function getDeviceListApi(query) {
return request({
url: '/device/info/list',
method: 'get',
params: query
})
}
//新增设备信息
export function addDeviceApi(data) {
return request({
url: '/device/info',
method: 'post',
data
})
}
//编辑设备信息
export function editDeviceApi(data) {
return request({
url: '/device/info',
method: 'put',
data
})
}
//查询设备信息详情
export function getDeviceDetailApi(id) {
return request({
url: '/device/info/' + id,
method: 'get'
})
}
//删除设备信息
export function deleteDeviceApi(id) {
return request({
url: '/device/info/' + id,
method: 'delete'
})
}
//获取设备下拉列表
export function getDeviceSelectApi() {
return request({
url: '/device/info/dropList',
method: 'get'
})
}
//获取用户下拉列表
export function getUserSelectApi() {
return request({
url: '/system/user/dropList',
method: 'get'
})
}
//查看鉴权信息
export function getAuthInfoApi(id) {
return request({
url: '/emqx/auth/' + id,
method: 'get'
})
}

View File

@ -57,4 +57,16 @@ export function getCodeImg() {
method: 'get',
timeout: 20000
})
}
export function changeLanguage(lang){
return request({
url: '/changeLanguage',
method: 'get',
headers: {
isToken: false,
},
params: {
lang: lang
}
})
}

View File

@ -0,0 +1,47 @@
import request from '@/utils/request';
// 获取设备维保记录
export function getMaintainListApi(query) {
return request({
url: '/maintain/record/list',
method: 'get',
params: query
})
}
//新增维保记录
export function addMaintainApi(data) {
return request({
url: '/maintain/record',
method: 'post',
data
})
}
//编辑维保记录
export function editMaintainApi(data) {
return request({
url: '/maintain/record',
method: 'put',
data
})
}
//查询维保记录详情
export function getMaintainDetailApi(id) {
return request({
url: '/maintain/record/' + id,
method: 'get'
})
}
//获取维保类型
export function getMaintainTypeApi() {
return request({
url: '/system/dict/data/type/' + 'tg_maintain_type',
method: 'get'
})
}
//获取设备编码列表
export function getDeviceCodeListApi() {
return request({
url: '/device/info/dropList',
method: 'get'
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -52,6 +52,7 @@
left: 0;
position: relative;
margin: 0 auto;
top: 40px;
}
// refine element ui upload

View File

@ -9,10 +9,14 @@ $yellow: #FEC171;
$panGreen: #30B08F;
// 默认主题变量
$menuText: #bfcbd9;
$menuActiveText: #409eff;
$menuBg: #304156;
$menuHover: #263445;
//$menuText: #bfcbd9;
$menuText: #000;
$menuActiveText: #fff;
//$menuActiveText: #409eff;
//$menuBg: #304156;
$menuBg: #ffffff;
//$menuHover: #263445;
$menuHover: #f0f1f5;
// 浅色主题theme-light
$menuLightBg: #ffffff;
@ -21,15 +25,20 @@ $menuLightText: #303133;
$menuLightActiveText: #409EFF;
// 基础变量
$base-sidebar-width: 200px;
$sideBarWidth: 200px;
$base-sidebar-width: 180px;
$sideBarWidth: 180px;
// 菜单暗色变量
//$base-menu-color: #bfcbd9;
//$base-menu-color-active: #f4f4f5;
//$base-menu-background: #304156;
//$base-sub-menu-background: #1f2d3d;
//$base-sub-menu-hover: #001528;
$base-menu-color: #bfcbd9;
$base-menu-color-active: #f4f4f5;
$base-menu-background: #304156;
$base-sub-menu-background: #1f2d3d;
$base-sub-menu-hover: #001528;
$base-menu-color-active: #409EFF;
$base-menu-background: #409EFF;
$base-sub-menu-background:#fff;
$base-sub-menu-hover:#f4f4f5;
// 组件变量
$--color-primary: #409EFF;

View File

@ -0,0 +1,97 @@
<template>
<div ref="chartRef" style="width: 100%; height: 400px"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
const props = defineProps({
chartData: {
type: Array,
default: () => []
},
seriesData: {
type: Array,
default: () => []
}
})
const chartRef = ref(null)
let myChart = null
const initChart = () => {
if (myChart) {
myChart.dispose()
}
nextTick(() => {
myChart = echarts.init(chartRef.value)
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: props.seriesData.map(item => item.name) //
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data:Array.isArray(props.chartData) ? props.chartData.map(item => item.name || '') : [],
axisLabel: {
interval: 0,
rotate: 30 //
}
},
yAxis: {
type: 'value',
minInterval: 1, // 1
axisLabel: {
formatter: '{value}' // 使
}
},
series: props.seriesData.map(item => ({
name: item.name,
type: 'bar',
stack: 'total',
emphasis: {
focus: 'series'
},
data: item.data || [],
itemStyle: {
borderRadius: [0, 0, 0, 0]
}
}))
}
myChart.setOption(option, true) //
})
}
watch(
() => [props.chartData, props.seriesData],
() => {
console.log(props.chartData)
initChart()
},
{ deep: true }
)
//
const handleResize = () => {
myChart && myChart.resize()
}
//
onMounted(() => {
initChart()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
myChart && myChart.dispose()
})
</script>

View File

@ -0,0 +1,62 @@
<template>
<div ref="chartRef" style="width: 100%; height: 300px"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as echarts from 'echarts'
const props = defineProps({
chartData: {
type: Object,
default: () => ({})
}
})
const chartRef = ref(null)
let myChart = null
const initChart = () => {
const option = {
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
right: '4%',
bottom: '3%'
},
series: [
{
type: 'pie',
radius: '50%',
data: (props.chartData.data || []).filter(item => item.value > 0),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
myChart = echarts.init(chartRef.value)
myChart.setOption(option)
}
// watch
watch(() => props.chartData.data, (newVal) => {
if (newVal && newVal.length > 0) {
initChart()
}
}, { deep: true, immediate: true })
onMounted(() => {
initChart()
window.addEventListener('resize', () => myChart?.resize())
})
onUnmounted(() => {
window.removeEventListener('resize', () => myChart?.resize())
myChart?.dispose()
})
</script>

View File

@ -0,0 +1,40 @@
<template>
<el-dropdown trigger="click" @command="handleLanguageChange">
<div class="lang-select--style">
<svg-icon icon-class="language" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :disabled="appStore.language === 'zh_CN'" command="zh_CN"> 中文 </el-dropdown-item>
<el-dropdown-item :disabled="appStore.language === 'en_US'" command="en_US"> English </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { useAppStore } from '@/store/modules/app';
import SvgIcon from '@/components/SvgIcon/index.vue';
import {ElMessage} from "element-plus";
const appStore = useAppStore();
const { locale } = useI18n();
const message: any = {
zh_CN: '切换语言成功!',
en_US: 'Switch Language Successful!'
};
const handleLanguageChange = (lang: any) => {
locale.value = lang;
appStore.changeLanguage(lang);
ElMessage.success(message[lang] || '切换语言成功!');
};
</script>
<style lang="scss" scoped>
.lang-select--style {
font-size: 28px;
margin-top:10px;
}
</style>

View File

@ -16,7 +16,7 @@
</template>
<script setup>
import useAppStore from "@/store/modules/app";
import {useAppStore} from "@/store/modules/app";
const appStore = useAppStore();
const size = computed(() => appStore.size);

View File

@ -35,7 +35,7 @@
<script setup>
import { constantRoutes } from "@/router"
import { isHttp } from '@/utils/validate'
import useAppStore from '@/store/modules/app'
import {useAppStore} from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'

View File

@ -0,0 +1,5 @@
export enum LanguageEnum {
zh_CN = 'zh_CN',
en_US = 'en_US'
}

15
src/enums/MenuTypeEnum.ts Normal file
View File

@ -0,0 +1,15 @@
export enum MenuTypeEnum {
/**
*
*/
M = 'M',
/**
*
*/
C = 'C',
/**
*
*/
F = 'F'
}

90
src/enums/RespEnum.ts Normal file
View File

@ -0,0 +1,90 @@
export enum HttpStatus {
/**
*
*/
SUCCESS = 200,
/**
*
*/
CREATED = 201,
/**
*
*/
ACCEPTED = 202,
/**
*
*/
NO_CONTENT = 204,
/**
*
*/
MOVED_PERM = 301,
/**
*
*/
SEE_OTHER = 303,
/**
*
*/
NOT_MODIFIED = 304,
/**
*
*/
PARAM_ERROR = 400,
/**
*
*/
UNAUTHORIZED = 401,
/**
* 访
*/
FORBIDDEN = 403,
/**
*
*/
NOT_FOUND = 404,
/**
* http方法
*/
BAD_METHOD = 405,
/**
*
*/
CONFLICT = 409,
/**
*
*/
UNSUPPORTED_TYPE = 415,
/**
*
*/
SERVER_ERROR = 500,
/**
*
*/
NOT_IMPLEMENTED = 501,
/**
*
*/
BAD_GATEWAY = 502,
/**
*
*/
GATEWAY_TIMEOUT = 504,
/**
*
*/
UNKNOWN_ERROR = 520,
/**
*
*/
SERVICE_ERROR = 521,
/**
*
*/
DATABASE_ERROR = 522,
/**
*
*/
WARN = 601
}

View File

@ -0,0 +1,4 @@
export enum SideThemeEnum {
DARK = 'theme-dark',
LIGHT = 'theme-light'
}

View File

@ -0,0 +1,17 @@
export enum AllocationTypeEnum {
USER = 'user',
CANDIDATE = 'candidate',
YOURSELF = 'yourself',
SPECIFY = 'specify'
}
export enum SpecifyDescEnum {
SPECIFY_MULTIPLE = 'specifyMultiple',
SPECIFY_SINGLE = 'specifySingle'
}
export enum MultiInstanceTypeEnum {
SERIAL = 'serial',
PARALLEL = 'parallel',
NONE = 'none'
}

View File

@ -0,0 +1,4 @@
export enum ThemeEnum {
DARK = 'theme-dark',
LIGHT = 'theme-light'
}

240
src/lang/en_US.js Normal file
View File

@ -0,0 +1,240 @@
export default {
route: {
dashboard: 'Dashboard',
document: 'Document'
},
login: {
selectPlaceholder: 'Please select/enter company name',
username: 'Username',
password: 'Password',
loginTitle: 'Welcome',
loginTip: 'Deflection Meter Management System',
login: 'Login',
logging: 'Logging in...',
code: 'Verification Code',
rememberPassword: 'Remember Password',
switchRegisterPage: 'Register Now',
rule: {
tenantId: {
required: 'Please enter your tenant ID'
},
username: {
required: 'Please enter your username'
},
password: {
required: 'Please enter your password'
},
code: {
required: 'Please enter verification code'
}
},
social: {
wechat: 'WeChat Login',
maxkey: 'MaxKey Login',
topiam: 'TopIam Login',
gitee: 'Gitee Login',
github: 'Github Login'
}
},
register: {
selectPlaceholder: 'Please select/enter company name',
username: 'Username',
password: 'Password',
confirmPassword: 'Confirm Password',
register: 'Register',
registering: 'Registering...',
registerSuccess: 'Congratulations, your account {username} has been registered successfully!',
code: 'Verification Code',
switchLoginPage: 'Login with existing account',
rule: {
tenantId: {
required: 'Please enter your tenant ID'
},
username: {
required: 'Please enter your username',
length: 'Username length must be between {min} and {max}'
},
password: {
required: 'Please enter your password',
length: 'Password length must be between {min} and {max}',
pattern: 'Cannot contain illegal characters: {strings}'
},
code: {
required: 'Please enter verification code'
},
confirmPassword: {
required: 'Please enter your password again',
equalToPassword: 'The two passwords do not match'
}
}
},
navbar: {
full: 'Full Screen',
language: 'Language',
dashboard: 'Dashboard',
document: 'Document',
message: 'Message',
layoutSize: 'Layout Size',
selectTenant: 'Select Tenant',
layoutSetting: 'Layout Settings',
personalCenter: 'Personal Center',
logout: 'Logout'
},
maintenance: {
deviceName: 'Device Name',
deviceCode: 'Device Code',
deviceModel: 'Device Model',
maintenanceType: 'Maintenance Type',
maintenanceDate: 'Maintenance Date',
maintenanceUser: 'Maintenance User',
inputMaintenanceUser: 'Please enter maintenance user',
inputDeviceName: 'Please enter device name',
inputDeviceCode: 'Please enter device code',
inputMaintenanceType: 'Please enter maintenance type',
inputMaintenanceDate: 'Please enter maintenance date',
},
// 客户管理
customer: {
customerName: 'Customer Name',
customerCode: 'Customer Code',
customerType: 'Customer Type',
customerAddress: 'Customer Address',
customerPhone: 'Customer Phone',
inputCustomerName: 'Please enter customer name',
inputCustomerCode: 'Please enter customer code',
inputCustomerType: 'Please enter customer type',
inputCustomerAddress: 'Please enter customer address',
inputCustomerPhone: 'Please enter customer phone',
addCustomer: 'Add Customer',
editCustomer: 'Edit Customer',
contact:'Contact Method',
inputContact:'Please enter contact method',
leader:'Person in Charge',
inputLeader:'Please enter Person in Charge',
contactAddress:'Contact Address',
inputContactAddress:'Please enter contact address',
createTime:'Creation Time',
createBy:'Created by',
checkDevice: 'View Device',
},
// 部门管理
department: {
departmentName: 'Department Name',
inputDepartmentName: 'Please enter department name',
addDepartment: 'Add Department',
editDepartment: 'Edit Department',
allCustomer: 'All Customers',
leader:'Department Leader',
inputLeader:'Please enter department leader',
},
// 岗位管理
position: {
positionName:'Position Name',
inputPositionName:'Please enter position name',
addPosition:'Add Position',
editPosition:'Edit Position',
department:'Department Affiliation',
},
// 设备
equipment: {
deviceName:'Device Name',
inputDeviceName:'Please enter device name',
deviceCode:'Device Code',
inputDeviceCode:'Please enter device code',
deviceModel:'Device Model',
inputDeviceModel:'Please enter device model',
addDevice:'Add Device',
editDevice:'Edit Device',
deviceStatus:'Device Status',
serviceStatus:'Service Status',
lifespan:'Consumables lifespanh',
usedTime:'Used Time',
viewAuth:'View Authorization',
nickName:'nickName',
userName:'userName',
userStatus:'userStatus',
EMQConnect:'EMQ Connection Details',
localEMQ: 'Local EMQ Username',
localPassword: 'Local Password',
localEMQPassword: 'Cloud EMQ Username',
localEMQUser: 'Cloud Password',
deviceImage:'Device Image',
inputDeviceImage:'Please enter device image',
uploadImage:'Upload Image',
limitImage:'Only one JPG/PNG file can be uploaded, and the file size cannot exceed 50kb',
Ray:'Ray',
imagePage:'Image Page',
reporting:'Generate Report',
deviceShare:'Device Sharing',
sketch:'Sketch',
historyData:'History Data',
deviceInfo:'Device Info',
paramsSetting:'Parameter Setting',
deviceTemperature:'Device Temperature',
warningInfo:'Alarm Information',
imageView:'Image Preview',
trendChart:'Trend Chart',
},
// 首页
dashboard: {
deviceCount:'Device Count',
customerCount:'Customer Count',
maintenanceCount:'Maintenance Count',
onlineDeviceCount:'Online Device Count',
offlineDeviceCount: 'Offline Device Count',
dataOverview:'Data Overview',
maintenanceAnalysis:'Maintenance Analysis',
maintenanceSummary:'Maintenance Summary',
},
button: {
add: 'Add',
edit: 'Edit',
delete: 'Delete',
save: 'Save',
cancel: 'Cancel',
search: 'Search',
reset: 'Reset',
import: 'Import',
export: 'Export',
check: 'View',
details: 'Details',
return:'Return',
viewDetails:'View Details',
submit:'Submit',
},
common: {
sort: 'Index',
customerAffiliation: 'Customer Affiliation',
userAffiliation: 'User Affiliation',
comments: 'Comments',
inputComments:'Please enter comments',
action: 'Action',
select:'Please select',
selectMul:'Please select(supports multiple selections)',
selectDate:'Please select date',
inputText:'Please enter text',
formValidateError:'Form validation failed',
warning:'Reminder',
deleteItem:'Are you sure to delete?',
normal:'Normal',
deactivate:'Deactivate',
online:'online',
offline:'offline',
open:'open',
close:'close',
personalCenter:'Personal Center',
logout:'Logout',
logoutSystem:'Exit the system?',
personalInfo:'Personal Information',
userName:'Username',
phoneNumber:'Phone Number',
userEmail:'User Email',
role:'Role Affiliation',
baseInfo:'Basic Information',
editPassword:'Edit Password',
editAvatar:'Edit Avatar',
choose:'Choose',
modifySuccess:'Modification Successful',
sex:'Sex'
}
}

36
src/lang/index.js Normal file
View File

@ -0,0 +1,36 @@
// 自定义国际化配置
import { createI18n } from 'vue-i18n'
import { useStorage } from "@vueuse/core"
import zh_CN from '@/lang/zh_CN.js'
import en_US from '@/lang/en_US.js'
// 语言枚举
export const LanguageEnum = {
zh_CN: 'zh_CN',
en_US: 'en_US'
}
/**
* 获取当前语言
* @returns {string} zh_CN|en_US
*/
export const getLanguage = () => {
const language = useStorage('language', LanguageEnum.zh_CN)
if (language.value) {
return language.value
}
return LanguageEnum.zh_CN
}
const i18n = createI18n({
globalInjection: true,
allowComposition: true,
legacy: false,
locale: getLanguage(),
messages: {
zh_CN: zh_CN,
en_US: en_US
}
})
export default i18n

247
src/lang/zh_CN.js Normal file
View File

@ -0,0 +1,247 @@
export default {
// 路由国际化
route: {
dashboard: '首页',
document: '项目文档'
},
// 登录页面国际化
login: {
selectPlaceholder: '请选择/输入公司名称',
username: '用户名',
password: '密码',
loginTitle:'欢迎登录',
loginTip:'测偏仪后台管理系统',
login: '登 录',
logging: '登 录 中...',
code: '验证码',
rememberPassword: '记住密码',
switchRegisterPage: '立即注册',
rule: {
tenantId: {
required: '请输入您的租户编号'
},
username: {
required: '请输入您的账号'
},
password: {
required: '请输入您的密码'
},
code: {
required: '请输入验证码'
}
},
social: {
wechat: '微信登录',
maxkey: 'MaxKey登录',
topiam: 'TopIam登录',
gitee: 'Gitee登录',
github: 'Github登录'
}
},
// 注册页面国际化
register: {
selectPlaceholder: '请选择/输入公司名称',
username: '用户名',
password: '密码',
confirmPassword: '确认密码',
register: '注 册',
registering: '注 册 中...',
registerSuccess: '恭喜你,您的账号 {username} 注册成功!',
code: '验证码',
switchLoginPage: '使用已有账户登录',
rule: {
tenantId: {
required: '请输入您的租户编号'
},
username: {
required: '请输入您的账号',
length: '用户账号长度必须介于 {min} 和 {max} 之间'
},
password: {
required: '请输入您的密码',
length: '用户密码长度必须介于 {min} 和 {max} 之间',
pattern: '不能包含非法字符:{strings}'
},
code: {
required: '请输入验证码'
},
confirmPassword: {
required: '请再次输入您的密码',
equalToPassword: '两次输入的密码不一致'
}
}
},
// 导航栏国际化
navbar: {
full: '全屏',
language: '语言',
dashboard: '首页',
document: '项目文档',
message: '消息',
layoutSize: '布局大小',
selectTenant: '选择租户',
layoutSetting: '布局设置',
personalCenter: '个人中心',
logout: '退出登录'
},
//维保管理
maintenance: {
deviceName:'设备名称',
deviceCode:'设备编码',
deviceModel:'设备型号',
maintenanceType:'维保类型',
maintenanceDate:'维保日期',
maintenanceUser:'维护人',
inputMaintenanceUser:'请输入维护人',
inputDeviceName:'请输入设备名称',
inputDeviceCode:'请输入设备编码',
inputMaintenanceType:'请输入维保类型',
inputMaintenanceDate:'请输入维保日期',
},
// 客户管理
customer: {
customerName:'客户名称',
customerCode:'客户编码',
customerAddress:'客户地址',
customerPhone:'客户电话',
customerEmail:'客户邮箱',
inputCustomerName:'请输入客户名称',
inputCustomerCode:'请输入客户编码',
inputCustomerAddress:'请输入客户地址',
inputCustomerPhone:'请输入客户电话',
inputCustomerEmail:'请输入客户邮箱',
addCustomer:'新建客户',
editCustomer:'编辑客户',
contact:'联系方式',
inputContact:'请输入联系方式',
leader:'负责人',
inputLeader:'请输入负责人',
contactAddress:'联系地址',
proxy:'请输入联系方式',
createTime:'创建时间',
createBy:'创建人',
checkDevice:'查看设备',
},
// 部门管理
department: {
departmentName:'部门名称',
inputDepartmentName:'请输入部门名称',
addDepartment:'新建部门',
editDepartment:'编辑部门',
leader:'部门负责人',
inputLeader:'请输入部门负责人',
allCustomer:'全部客户'
},
// 岗位管理
position: {
positionName:'岗位名称',
inputPositionName:'请输入岗位名称',
addPosition:'新建岗位',
editPosition:'编辑岗位',
department:'所属部门',
},
// 设备
equipment: {
deviceName: '设备名称',
inputDeviceName: '请输入设备名称',
deviceCode: '设备编码',
inputDeviceCode: '请输入设备编码',
deviceModel: '设备型号',
inputDeviceModel: '请输入设备型号',
addDevice: '新增设备',
editDevice: '编辑设备',
deviceStatus:'设备状态',
serviceStatus:'服务状态',
lifespan:'耗材寿命h',
usedTime:'已使用时长',
viewAuth:'查看鉴权',
nickName:'用户昵称',
userName:'登陆账户',
userStatus:'用户状态',
EMQConnect:'EMQ连接详情',
localEMQ:'本地EMQ用户名',
localPassword:'本地密码',
localEMQPassword:'云EMQ用户名',
localEMQUser:'云密码',
deviceImage:'设备图片',
inputDeviceImage:'请输入设备图片',
uploadImage:'上传图片',
limitImage:'只能上传一张jpg/png格式文件文件不能超过50kb',
Ray:'射线',
imagePage:'图像页',
reporting:'生成报告',
deviceShare:'设备分享',
sketch:'示意图',
historyData:'历史数据',
deviceInfo:'设备信息',
paramsSetting:'参数设定',
deviceTemperature:'设备温度',
warningInfo:'报警信息',
imageView:'图像预览',
trendChart:'趋势图',
},
// 首页
dashboard: {
deviceCount:'设备数量',
customerCount:'客户数量',
maintenanceCount:'用户数量',
onlineDeviceCount:'在线设备数量',
offlineDeviceCount:'离线设备数量',
dataOverview:'数据总览',
maintenanceAnalysis: '维保分析',
maintenanceSummary: '维保汇总',
},
// 按钮
button: {
add:'新 增',
edit:'编 辑',
delete:'删除',
save:'确 定',
cancel:'取 消',
search:'查 询',
reset:'重 置',
import:'导 入',
export:'导 出',
check:'查 看',
details:'详 情',
return:'返 回',
viewDetails:'查看详情',
submit:'提 交',
},
common: {
sort:'序号',
customerAffiliation:'所属客户',
userAffiliation:'所属用户',
comments:'备注',
inputComments:'请输入备注',
action:'操作',
select:'请选择',
selectMul:'请选择(支持多选)',
selectDate:'请选择日期',
inputText:'请输入文字',
formValidateError:'表单验证失败',
warning:'警告',
deleteItem:'是否确认删除?',
normal:'正常',
deactivate:'停用',
online:'在线',
offline:'离线',
open:'开启',
close:'关闭',
personalCenter: '个人中心',
logout:'退出登录',
logoutSystem:'是否确认退出系统?',
personalInfo:'个人信息',
userName:'用户名称',
phoneNumber:'手机号',
inputPhoneNumber:'请输入手机号',
userEmail:'用户邮箱',
role:'所属角色',
baseInfo:'基本资料',
editPassword:'修改密码',
editAvatar:'修改头像',
choose:'选择',
modifySuccess:'修改成功',
sex:'性别',
}
};

View File

@ -22,7 +22,7 @@ onMounted(() => {
addIframe()
})
watch((route) => {
watchEffect((route) => {
addIframe()
})

View File

@ -6,27 +6,30 @@
<div class="right-menu">
<template v-if="appStore.device !== 'mobile'">
<header-search id="header-search" class="right-menu-item" />
<!-- <header-search id="header-search" class="right-menu-item" />-->
<el-tooltip content="源码地址" effect="dark" placement="bottom">
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
</el-tooltip>
<!-- <el-tooltip content="源码地址" effect="dark" placement="bottom">-->
<!-- <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />-->
<!-- </el-tooltip>-->
<el-tooltip content="文档地址" effect="dark" placement="bottom">
<ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
</el-tooltip>
<!-- <el-tooltip content="文档地址" effect="dark" placement="bottom">-->
<!-- <ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />-->
<!-- </el-tooltip>-->
<screenfull id="screenfull" class="right-menu-item hover-effect" />
<!-- <screenfull id="screenfull" class="right-menu-item hover-effect" />-->
<el-tooltip content="主题模式" effect="dark" placement="bottom">
<div class="right-menu-item hover-effect theme-switch-wrapper" @click="toggleTheme">
<svg-icon v-if="settingsStore.isDark" icon-class="sunny" />
<svg-icon v-if="!settingsStore.isDark" icon-class="moon" />
</div>
</el-tooltip>
<!-- <el-tooltip content="主题模式" effect="dark" placement="bottom">-->
<!-- <div class="right-menu-item hover-effect theme-switch-wrapper" @click="toggleTheme">-->
<!-- <svg-icon v-if="settingsStore.isDark" icon-class="sunny" />-->
<!-- <svg-icon v-if="!settingsStore.isDark" icon-class="moon" />-->
<!-- </div>-->
<!-- </el-tooltip>-->
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
<!-- <el-tooltip content="布局大小" effect="dark" placement="bottom">-->
<!-- <size-select id="size-select" class="right-menu-item hover-effect" />-->
<!-- </el-tooltip>-->
<el-tooltip :content="proxy.$t('navbar.language')" effect="dark" placement="bottom">
<LangSelect id="lang-select" class="right-menu-item hover-effect" />
</el-tooltip>
</template>
<div class="avatar-container">
@ -38,13 +41,13 @@
<template #dropdown>
<el-dropdown-menu>
<router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item>{{proxy.$t('common.personalCenter')}}</el-dropdown-item>
</router-link>
<el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
<span>布局设置</span>
</el-dropdown-item>
<!-- <el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">-->
<!-- <span>布局设置</span>-->
<!-- </el-dropdown-item>-->
<el-dropdown-item divided command="logout">
<span>退出登录</span>
<span>{{proxy.$t('common.logout')}}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
@ -59,23 +62,25 @@ import { ElMessageBox } from 'element-plus'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger'
import LangSelect from '@/components/LangSelect/index.vue'
import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
import HeaderSearch from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc'
import useAppStore from '@/store/modules/app'
import {useAppStore} from '@/store/modules/app'
import useUserStore from '@/store/modules/user'
import useSettingsStore from '@/store/modules/settings'
import { useI18n } from 'vue-i18n';
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const appStore = useAppStore()
const userStore = useUserStore()
const settingsStore = useSettingsStore()
function toggleSideBar() {
appStore.toggleSideBar()
appStore.toggleSideBar(false)
}
function handleCommand(command) {
switch (command) {
case "setLayout":
@ -90,11 +95,14 @@ function handleCommand(command) {
}
function logout() {
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
ElMessageBox.confirm(proxy.$t('common.logoutSystem'),
proxy.$t('common.warning'),
{
confirmButtonText: proxy.$t('button.save'),
cancelButtonText: proxy.$t('button.cancel'),
type: "warning"
}
).then(() => {
userStore.logOut().then(() => {
location.href = '/index';
})

View File

@ -83,7 +83,7 @@ import variables from '@/assets/styles/variables.module.scss'
import axios from 'axios'
import { ElLoading, ElMessage } from 'element-plus'
import { useDynamicTitle } from '@/utils/dynamicTitle'
import useAppStore from '@/store/modules/app'
import {useAppStore} from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
import { handleThemeStyle } from '@/utils/theme'

View File

@ -2,12 +2,12 @@
<div class="sidebar-logo-container" :class="{ 'collapse': collapse }">
<transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 v-else class="sidebar-title">{{ title }}</h1>
<!-- <img v-if="logo" :src="logo" class="sidebar-logo" />-->
<h1 class="sidebar-title">TIANGONIX</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">{{ title }}</h1>
<!-- <img v-if="logo" :src="logo" class="sidebar-logo" />-->
<h1 class="sidebar-title">TIANGONIX</h1>
</router-link>
</transition>
</div>
@ -81,10 +81,11 @@ const getLogoTextColor = computed(() => {
& .sidebar-title {
display: inline-block;
margin: 0;
color: v-bind(getLogoTextColor);
color:#F59537;
//color: v-bind(getLogoTextColor);
font-weight: 600;
line-height: 50px;
font-size: 14px;
font-size: 18px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
vertical-align: middle;
}

View File

@ -28,7 +28,7 @@
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/assets/styles/variables.module.scss'
import useAppStore from '@/store/modules/app'
import {useAppStore} from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'

View File

@ -19,7 +19,7 @@ import Sidebar from './components/Sidebar/index.vue'
import { AppMain, Navbar, Settings, TagsView } from './components'
import defaultSettings from '@/settings'
import useAppStore from '@/store/modules/app'
import {useAppStore} from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
const settingsStore = useSettingsStore()

View File

@ -13,7 +13,7 @@ import App from './App'
import store from './store'
import router from './router'
import directive from './directive' // directive
import { createPinia } from 'pinia'
// 注册指令
import plugins from './plugins' // plugins
import { download } from '@/utils/request'
@ -27,7 +27,8 @@ import './permission' // permission control
import { useDict } from '@/utils/dict'
import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
// 国际化
import i18n from '@/lang/index.js';
// 分页组件
import Pagination from '@/components/Pagination'
// 自定义表格工具组件
@ -42,9 +43,13 @@ import ImageUpload from "@/components/ImageUpload"
import ImagePreview from "@/components/ImagePreview"
// 字典标签组件
import DictTag from '@/components/DictTag'
import { useAppStore } from '@/store/modules/app'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
const appStore = useAppStore()
// 全局方法挂载
app.config.globalProperties.useDict = useDict
app.config.globalProperties.download = download
@ -68,15 +73,15 @@ app.use(router)
app.use(store)
app.use(plugins)
app.use(elementIcons)
app.use(i18n)
app.component('svg-icon', SvgIcon)
directive(app)
// 使用element-plus 并且设置全局的大小
app.use(ElementPlus, {
locale: locale,
// 支持 large、default、small
size: Cookies.get('size') || 'default'
locale: appStore.locale,
size: appStore.size
})
// app.use(ElementPlus)
app.mount('#app')

View File

@ -1,3 +1,4 @@
import { LanguageEnum } from '@/enums/LanguageEnum';
export default {
/**
* 网页标题
@ -43,5 +44,6 @@ export default {
* The default is only used in the production env
* If you want to also use it in dev, you can pass ['production', 'development']
*/
errorLog: 'production'
errorLog: 'production',
language: LanguageEnum.zh_CN,
}

View File

@ -1,6 +1,11 @@
import zhCN from 'element-plus/es/locale/lang/zh-cn';
import enUS from 'element-plus/es/locale/lang/en';
import { defineStore } from 'pinia';
import { useStorage } from '@vueuse/core';
import { ref, reactive, computed } from 'vue';
import Cookies from 'js-cookie'
const useAppStore = defineStore(
export const useAppStore = defineStore(
'app',
{
state: () => ({
@ -10,8 +15,20 @@ const useAppStore = defineStore(
hide: false
},
device: 'desktop',
size: Cookies.get('size') || 'default'
size: Cookies.get('size') || 'default',
// 添加语言相关状态
language: useStorage('language', 'zh_CN'),
languageObj: {
en_US: enUS,
zh_CN: zhCN
}
}),
getters: {
locale: (state) => {
console.log(state,'===')
return state.languageObj[state.language]
}
},
actions: {
toggleSideBar(withoutAnimation) {
if (this.sidebar.hide) {
@ -39,8 +56,10 @@ const useAppStore = defineStore(
},
toggleSideBarHide(status) {
this.sidebar.hide = status
},
// 添加修改语言的方法
changeLanguage(val) {
this.language = val
}
}
})
export default useAppStore

15
src/utils/i18n.js Normal file
View File

@ -0,0 +1,15 @@
import i18n from '@/lang/index.js'
/**
* 获取国际化路由如果不存在则原生返回
* @param {string} title 路由名称
* @returns {string}
*/
export const translateRouteTitle = (title) => {
const hasKey = i18n.global.te('route.' + title)
if (hasKey) {
const translatedTitle = i18n.global.t('route.' + title)
return translatedTitle
}
return title
}

View File

@ -10,7 +10,12 @@ import useUserStore from '@/store/modules/user'
let downloadLoadingInstance;
// 是否显示重新登录
export let isRelogin = { show: false };
export const globalHeaders = () => {
return {
Authorization: 'Bearer ' + getToken(),
clientid: import.meta.env.VITE_APP_CLIENT_ID
};
};
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({

View File

@ -0,0 +1,145 @@
<template>
<el-dialog
:title="title"
v-model="visible"
style="width: 460px"
append-to-body
:before-close="cancel"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
:label-width="appStore.language === 'zh_CN' ? '100px' : '160px'"
label-position="right"
>
<el-form-item :label="proxy.$t('customer.customerName')" prop="deptName">
<el-input v-model="form.deptName" :placeholder="proxy.$t('customer.inputCustomerName')" style="width:300px"/>
</el-form-item>
<el-form-item :label="proxy.$t('customer.leader')" prop="leader">
<el-input v-model="form.leader" :placeholder="proxy.$t('customer.inputLeader')" style="width:300px"/>
</el-form-item>
<el-form-item :label="proxy.$t('customer.contact')" prop="phone">
<el-input v-model="form.phone" :placeholder="proxy.$t('customer.inputContact')" style="width:300px"/>
</el-form-item>
<el-form-item :label="proxy.$t('customer.contactAddress')" prop="address">
<el-input
v-model="form.address"
type="textarea"
:placeholder="proxy.$t('customer.inputContactAddress')"
style="width:300px"
:rows="4"
maxlength="200"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">{{proxy.$t('button.save')}}</el-button>
<el-button @click="cancel">{{proxy.$t('button.cancel')}}</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive,onMounted } from 'vue'
import {addDepartmentApi, editDepartmentApi} from '@/api/customer'
import {ElMessage} from "element-plus";
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
const { proxy } = getCurrentInstance();
const props = defineProps({
title: {
type: String,
default: '',
},
formData: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue', 'success'])
const visible = ref(false)
const formRef = ref()
const form = reactive({
deptName: '',
leader: '',
phone: '',
address: '',
orderNum: 0,
status:'0',
parentId:100,
ancestors: '',
deptId: undefined,
})
const rules = {
deptName: [{ required: true, message: proxy.$t('customer.inputCustomerName'), trigger: 'blur' }],
leader: [{ required: true, message: proxy.$t('customer.inputLeader'), trigger: 'blur' }],
phone: [{ required: true, message: proxy.$t('customer.inputContact'), trigger: 'blur' }],
address: [{ required: true, message: proxy.$t('customer.contactAddress'), trigger: 'blur' }]
}
const submitForm = async () => {
try {
await formRef.value.validate()
//
const res = props.title === proxy.$t('customer.addCustomer') ? await addDepartmentApi(form) : await editDepartmentApi(form)
if(res.code === 200) {
ElMessage.success(res.msg)
} else {
ElMessage.error(res.msg)
}
emit('success')
cancel()
} catch (error) {
console.error(proxy.$t('common.formValidateError'), error)
}
}
const cancel = () => {
resetForm()
emit('update:modelValue', false)
}
const resetForm = () => {
formRef.value?.resetFields()
if (props.title === proxy.$t('customer.addCustomer')) {
//
Object.keys(form).forEach(key => {
form[key] = ''
})
}
}
watch(
[() => props.title, () => props.formData],
([newTitle, newFormData]) => {
if (newTitle === proxy.$t('customer.editCustomer') && Object.keys(newFormData).length > 0) {
Object.keys(form).forEach(key => {
form[key] = newFormData[key]
})
}
},
{ immediate: true, deep: true }
)
defineExpose({
visible
})
</script>
<style scoped>
.dialog-footer {
text-align: center;
}
:deep(.el-form-item__label) {
justify-content: flex-end;
}
</style>

View File

@ -0,0 +1,182 @@
<template>
<el-dialog
:title="title"
v-model="visible"
style="width: 460px"
append-to-body
:before-close="cancel"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
:label-width="appStore.language === 'zh_CN' ? '100px' : '160px'"
label-position="right"
>
<el-form-item :label="proxy.$t('common.customerAffiliation')" prop="parentName">
<el-select v-model="form.parentName" style="width:300px">
<el-option
v-for="item in deptList"
:key="item.deptId"
:label="item.deptName"
:value="item.deptId"
/>
</el-select>
</el-form-item>
<el-form-item :label="proxy.$t('department.departmentName')" prop="deptName">
<el-input v-model="form.deptName" :placeholder="proxy.$t('department.inputDepartmentName')" style="width:300px"/>
</el-form-item>
<el-form-item :label="proxy.$t('department.leader')" prop="leader">
<el-input v-model="form.leader" :placeholder="proxy.$t('department.inputLeader')" style="width:300px"/>
</el-form-item>
<el-form-item :label="proxy.$t('customer.contact')" prop="phone">
<el-input v-model="form.phone" :placeholder="proxy.$t('customer.inputContact')" style="width:300px"/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">{{proxy.$t('button.save')}}</el-button>
<el-button @click="cancel">{{proxy.$t('button.cancel')}}</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive } from 'vue'
import {addDepartmentApi, editDepartmentApi} from '@/api/customer'
import {ElMessage} from "element-plus";
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
const { proxy } = getCurrentInstance();
const props = defineProps({
title: {
type: String,
default: '新建部门'
},
formData: {
type: Object,
default: () => ({})
},
deptList: {
type: Array,
default: () => []
},
selectedName: {
type: String,
default: ''
},
selectedId: {
type: Number,
default: 0
},
orderNum: {
type: Number,
default: 0
}
})
const emit = defineEmits(['update:modelValue', 'success'])
const visible = ref(false)
const formRef = ref()
const form = reactive({
deptName: '',
leader: '',
phone: '',
address: '',
orderNum: 0,
status:'0',
parentId:undefined,
ancestors: '',
deptId: undefined,
parentName:undefined
})
const rules = {
parentName: [{ required: true, message: proxy.$t('common.select'), trigger: 'change' }],
deptName: [{ required: true, message: proxy.$t('department.inputDepartmentName'), trigger: 'blur' }],
leader: [{ required: true, message: proxy.$t('department.inputLeader'), trigger: 'blur' }],
phone: [{ required: true, message: proxy.$t('customer.inputContact'), trigger: 'blur' }],
}
const submitForm = async () => {
try {
await formRef.value.validate()
//
form.orderNum = 0
const res = props.title === proxy.$t('department.addDepartment') ? await addDepartmentApi(form) : await editDepartmentApi(form)
if(res.code === 200) {
ElMessage.success(res.msg)
} else {
ElMessage.error(res.msg)
}
emit('success')
cancel()
} catch (error) {
console.error( proxy.$t('common.formValidateError'), error)
}
}
const cancel = () => {
resetForm()
emit('update:modelValue', false)
}
const resetForm = () => {
formRef.value?.resetFields()
if(props.title === proxy.$t('department.addDepartment')) {
Object.keys(form).forEach(key => {
if (key !== 'parentId' && key !== 'parentName'&& key !== 'orderNum') {
form[key] = ''
}
})
}else {
Object.keys(form).forEach(key => {
form[key] = ''
})
}
}
watch(
[() => props.title, () => props.formData],
([newTitle, newFormData]) => {
if (newTitle === proxy.$t('department.editDepartment') && Object.keys(newFormData).length > 0) {
Object.keys(form).forEach(key => {
form[key] = newFormData[key]
})
}
},
{ immediate: true, deep: true }
)
watchEffect(() => {
if (props.selectedName) {
form.parentName = props.selectedName
}
})
watchEffect(() => {
if (props.selectedId) {
form.parentId = props.selectedId
}
})
watchEffect(() => {
if (props.orderNum) {
form.orderNum = props.orderNum
}
})
defineExpose({
visible
})
</script>
<style scoped>
.dialog-footer {
text-align: center;
}
:deep(.el-form-item__label) {
justify-content: flex-end;
}
</style>

View File

@ -0,0 +1,208 @@
<template>
<el-dialog
:title="title"
v-model="visible"
style="width: 460px"
append-to-body
:before-close="cancel"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
:label-width="appStore.language === 'zh_CN' ? '100px' : '200px'"
label-position="right"
>
<el-form-item :label="proxy.$t('position.positionName')" prop="postName">
<el-input v-model="form.postName" :placeholder="proxy.$t('position.inputPositionName')" style="width:300px"/>
</el-form-item>
<el-form-item :label="proxy.$t('common.customerAffiliation')" prop="customerId">
<el-select v-model="form.customerId" style="width:300px" @change="handleCustomerChange">
<el-option
v-for="item in deptList"
:key="item.deptId"
:label="item.deptName"
:value="item.deptId"
/>
</el-select>
</el-form-item>
<el-form-item :label="proxy.$t('position.department')" prop="deptId">
<el-select v-model="form.deptId" style="width:300px" :disabled="!form.customerName">
<el-option
v-for="item in postList"
:key="item.deptId"
:label="item.deptName"
:value="item.deptId"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">{{proxy.$t('button.save')}}</el-button>
<el-button @click="cancel">{{proxy.$t('button.cancel')}}</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive,onMounted } from 'vue'
import {
addDepartmentPostApi,
editDepartmentPostApi,getCustomerListApi
} from '@/api/customer'
import {ElMessage} from "element-plus";
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
const { proxy } = getCurrentInstance();
const props = defineProps({
title: {
type: String,
default: '新建岗位'
},
formData: {
type: Object,
default: () => ({})
},
selectId: {
type: String,
default: ''
},
selectName: {
type: String,
default: ''
},
parentId: {
type: String,
default: ''
}
})
const deptList = ref([])
const postList = ref([])
const emit = defineEmits(['update:modelValue', 'success'])
const visible = ref(false)
const formRef = ref()
const form = reactive({
deptId:'',
deptName: '',
postId:'',
postName: '',
customerId:'',
customerName: '',
id:''
})
const queryParams = reactive({
pageNum: 1,
pageSize: 100000000,
deptName: undefined,
parentId: 100,
})
const rules = {
deptId: [{ required: true, message: proxy.$t('common.select'), trigger: 'change' }],
postName: [{ required: true, message: proxy.$t('position.inputPositionName'), trigger: 'blur' }],
customerId: [{ required: true, message: proxy.$t('common.select'), trigger: 'change' }],
}
const getDepList = async() => {
const res = await getCustomerListApi(queryParams);
deptList.value = res.rows || []
}
const submitForm = async () => {
try {
await formRef.value.validate()
//
console.log(props.selectId,'id---')
const res = props.title === proxy.$t('position.addPosition') ? await addDepartmentPostApi(form) : await editDepartmentPostApi(form)
if(res.code === 200) {
ElMessage.success(res.msg)
} else {
ElMessage.error(res.msg)
}
emit('success')
cancel()
} catch (error) {
console.error(proxy.$t('common.formValidateError') , error)
}
}
const cancel = () => {
resetForm()
emit('update:modelValue', false)
emit('update:formData', {})
}
const handleCustomerChange = async (value) => {
form.deptName = ''
if (value) {
queryParams.parentId = value
getPostList()
} else {
postList.value = []
}
}
const getPostList = async () => {
const res = await getCustomerListApi(queryParams);
if (res.code === 200) {
postList.value = res.rows || []
}
}
const resetForm = () => {
formRef.value?.resetFields()
if(props.title === proxy.$t('position.addPosition')) {
form.postId = ''
form.postName = ''
}else {
Object.keys(form).forEach(key => {
form[key] = ''
})
}
}
watch(
[() => props.title, () => props.formData],
([newTitle, newFormData]) => {
if (newTitle === proxy.$t('position.editPosition') && Object.keys(newFormData).length > 0) {
Object.keys(form).forEach(key => {
form[key] = newFormData[key]
})
}
},
{ immediate: true, deep: true }
)
watchEffect(() => {
if (props.selectId && postList.value.length > 0) {
nextTick(() => {
form.deptId = props.selectId
})
}
})
watchEffect(() => {
if (props.parentId) {
form.customerId = props.parentId
queryParams.parentId = props.parentId
getPostList()
// const selectedDept = deptList.value.find(item => item.deptId === props.parentId)
// if (selectedDept) {
// form.customerName = selectedDept.deptName
// form.customerId = props.parentId
// }
}
})
onMounted(() => {
getDepList()
})
defineExpose({
visible
})
</script>
<style scoped>
.dialog-footer {
text-align: center;
}
:deep(.el-form-item__label) {
justify-content: flex-end;
}
</style>

View File

@ -0,0 +1,233 @@
<template>
<div class="contentBox">
<el-card class="card" shadow="hover">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
<el-form-item :label="proxy.$t('customer.customerName')" prop="customerName">
<el-input
v-model="queryParams.deptName"
:placeholder="proxy.$t('customer.inputCustomerName')"
clearable
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">{{proxy.$t('button.search')}}</el-button>
<el-button icon="Refresh" @click="resetQuery">{{proxy.$t('button.reset')}}</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
>{{proxy.$t('customer.addCustomer')}}</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="customerList" boreder>
<el-table-column :label="proxy.$t('common.sort')" type="index" width="60" align="center" />
<el-table-column :label="proxy.$t('customer.customerName')" align="center" prop="deptName" />
<el-table-column :label="proxy.$t('customer.leader')" align="center" prop="leader" />
<el-table-column :label="proxy.$t('customer.contact')" align="center" prop="phone" />
<el-table-column :label="proxy.$t('customer.contactAddress')" align="center" prop="address" show-overflow-tooltip />
<el-table-column :label="proxy.$t('customer.createBy')" align="center" prop="createBy" />
<el-table-column :label="proxy.$t('customer.createTime')" align="center" prop="createTime" width="160" />
<el-table-column :label="proxy.$t('common.action')" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
link
type="primary"
icon="Edit"
@click="handleUpdate(scope.row)"
>{{proxy.$t('button.edit')}}</el-button>
<el-button
link
type="primary"
icon="Delete"
@click="handleDelete(scope.row)"
>{{proxy.$t('button.delete')}}</el-button>
<el-button
link
type="primary"
icon="View"
@click="handleViewDevices(scope.row)"
>{{ proxy.$t('customer.checkDevice') }}</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-if="total > 0"
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 30, 50]"
layout="total, sizes, prev, pager, next, jumper"
@pagination="getList"
/>
</div>
</el-card>
<CustomerDialog
ref="dialogRef"
:title="dialog.title"
:formData="dialog.form"
v-model="dialog.visible"
@success="handleSuccess"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n';
import CustomerDialog from './common/CustomerDialog.vue';
import {getCustomerListApi, getDepartmentDetailApi, deleteDepartmentApi} from "@/api/customer/index.js";
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const router = useRouter()
const loading = ref(false)
const showSearch = ref(true)
const total = ref(0)
//
const dialog = reactive({
title: proxy.$t('customer.addCustomer'),
visible: false,
form: {
deptName: '',
leader: '',
phone: '',
address: '',
orderNum: 0,
status:"0",
parentId:100,
ancestors: '',
deptId: undefined,
}
})
const dialogRef = ref()
//
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
deptName: undefined,
parentId: 100,
})
//
const customerList = ref([])
//
const getList = async() => {
loading.value = true
// API
const res = await getCustomerListApi(queryParams);
if(res.code === 200) {
customerList.value = res.rows || []
total.value = res.total
} else {
ElMessage.error(res.msg)
}
setTimeout(() => {
loading.value = false
}, 500)
}
//
const handleQuery = () => {
queryParams.pageNum = 1
getList()
}
//
const resetQuery = () => {
queryParams.deptName = undefined
handleQuery()
}
//
const handleAdd = () => {
dialog.title = proxy.$t('customer.addCustomer')
dialog.visible = true
}
//
const handleUpdate = async(row) => {
console.log(row)
const res = await getDepartmentDetailApi(row.deptId)
if(res.code === 200) {
dialog.title = proxy.$t('customer.editCustomer')
dialog.visible = true
dialog.form.deptName = res.data.deptName
dialog.form.leader = res.data.leader
dialog.form.phone = res.data.phone
dialog.form.address = res.data.address
dialog.form.parentId = res.data.parentId
dialog.form.orderNum = res.data.orderNum
dialog.form.status = res.data.status
dialog.form.deptId = res.data.deptId
dialog.form.ancestors = res.data.ancestors
} else {
ElMessage.error(res.msg)
}
}
//
const handleDelete = (row) => {
ElMessageBox.confirm(
proxy.$t('common.deleteItem'),
proxy.$t('common.warning'),
{
confirmButtonText: proxy.$t('button.save'),
cancelButtonText: proxy.$t('button.cancel'),
type: "warning"
}
).then(async () => {
// API
const res = await deleteDepartmentApi(row.deptId)
if(res.code === 200) {
ElMessage.success(res.msg)
} else {
ElMessage.error(res.msg)
}
getList()
}).catch(() => {})
}
//
const handleViewDevices = (row) => {
router.push({ path: '/equipment/equipmentManage', query: { id: row.deptId } });
}
//
const handleSuccess = () => {
getList()
}
onMounted(() => {
getList()
})
</script>
<style scoped lang="scss">
.contentBox {
height:calc(100vh - 130px);
margin:20px;
.card {
width:100%;
height: 100%;
overflow-y: scroll;
}
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
.mb8 {
margin-bottom: 8px;
}
</style>

View File

@ -0,0 +1,330 @@
<template>
<div class="contentBox">
<div class="contentLeft">
<el-card>
<el-input
v-model="queryParams.deptName"
:placeholder="proxy.$t('customer.inputCustomerName')"
class="filter-input"
clearable
@input="handleFilter"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button style="margin:20px 0 10px" type="primary" @click="handleAll" link>{{proxy.$t('department.allCustomer')}}</el-button>
<el-divider style="margin:0"/>
<ul class="customer-container">
<li :class="{ active: selectedId === item.deptId }" @click="handleNodeClick(item)" class="customer-item" v-for="(item,index) in customerList" :key="index">
{{item.deptName}}
</li>
</ul>
</el-card>
</div>
<div class="contentRight">
<el-card>
<el-form :model="queryParams" ref="queryForm" :inline="true">
<el-form-item :label="proxy.$t('department.departmentName')" prop="deptName">
<el-input
v-model="queryParams.deptName"
:placeholder="proxy.$t('department.inputDepartmentName')"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">{{proxy.$t('button.search')}}</el-button>
<el-button icon="Refresh" @click="resetQuery">{{proxy.$t('button.reset')}}</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
>{{proxy.$t('department.addDepartment')}}</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="deptList" border>
<el-table-column :label="proxy.$t('common.sort')" type="index" width="60" align="center" />
<el-table-column :label="proxy.$t('department.departmentName')" align="center" prop="deptName" />
<el-table-column :label="proxy.$t('department.leader')" align="center" prop="leader" />
<el-table-column :label="proxy.$t('common.customerAffiliation')" align="center" prop="parentName" />
<el-table-column :label="proxy.$t('customer.contact')" align="center" prop="phone" />
<el-table-column :label="proxy.$t('customer.createTime')" align="center" prop="createTime" width="160" />
<el-table-column :label="proxy.$t('common.action')" align="center" width="150">
<template #default="scope">
<el-button
link
type="primary"
icon="Edit"
@click="handleUpdate(scope.row)"
>{{proxy.$t('button.edit')}}</el-button>
<el-button
link
type="primary"
icon="Delete"
@click="handleDelete(scope.row)"
>{{proxy.$t('button.delete')}}</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-if="total > 0"
v-model:current-page="queryParamsNoPage.pageNum"
v-model:page-size="queryParamsNoPage.pageSize"
:total="total"
:page-sizes="[10, 20, 30, 50]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
<DepartmentDialog
ref="dialogRef"
:title="dialog.title"
:dept-list="customerList"
:selected-name="selectName"
:selected-id="selectedId"
:order-num="0"
:formData="dialog.form"
v-model="dialog.visible"
@success="handleSuccess"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import DepartmentDialog from "@/views/customer/common/DepartmentDialog.vue";
import {getDepartmentDetailApi, getCustomerListApi, deleteDepartmentApi} from '@/api/customer'
const loading = ref(false)
const { proxy } = getCurrentInstance();
const selectName = ref('')
const customerList = ref([])
const originalList = ref([])
const total = ref(0)
//
const dialog = reactive({
title: proxy.$t('department.addDepartment'),
visible: false,
form: {
deptName: '',
leader: '',
phone: '',
address: '',
orderNum: 0,
status:"0",
parentId:100,
ancestors: '',
deptId: undefined,
parentName:undefined
}
})
const dialogRef = ref()
const selectedId = ref(null)
//
const queryParams = reactive({
pageNum: 1,
pageSize: 10000000000,
parentId: 100,
deptName: '',
customerId: undefined
})
const queryParamsNoPage = reactive({
pageNum: 1,
pageSize: 10,
parentId: -1,
deptName: '',
customerId: undefined
})
//
const deptList = ref([])
//
const handleAll = () => {
selectedId.value = null
queryParamsNoPage.parentId = -1
selectName.value = ''
//
getList()
}
//
const handleNodeClick = (item) => {
selectedId.value = item.deptId
selectName.value = item.deptName
queryParamsNoPage.parentId = selectedId.value
getList()
}
const handleFilter = () => {
if (!queryParams.deptName) {
customerList.value = originalList.value
return
}
customerList.value = originalList.value.filter(item =>
item.deptName.toLowerCase().includes(queryParams.deptName.toLowerCase())
)
}
//
const getCustomerList = async() => {
loading.value = true
// API
const res = await getCustomerListApi(queryParams);
if(res.code === 200) {
customerList.value = res.rows || []
originalList.value = [...customerList.value]
} else {
ElMessage.error(res.msg)
}
setTimeout(() => {
loading.value = false
}, 500)
}
//
const getList = async() => {
loading.value = true
const res = await getCustomerListApi(queryParamsNoPage);
if(res.code === 200) {
deptList.value = res.rows || []
total.value = res.total
} else {
ElMessage.error(res.msg)
}
setTimeout(() => {
loading.value = false
}, 500)
}
const handleSizeChange = (val) => {
queryParamsNoPage.pageSize = val
getList()
}
const handleCurrentChange = (val) => {
queryParamsNoPage.pageNum = val
getList()
}
//
const handleSuccess = () => {
getList()
}
//
const handleQuery = () => {
getList()
}
//
const resetQuery = () => {
queryParams.deptName = ''
handleQuery()
}
//
const handleAdd = () => {
//
dialog.title = proxy.$t('department.addDepartment')
dialog.visible = true
}
//
const handleUpdate = async(row) => {
//
const res = await getDepartmentDetailApi(row.deptId)
if(res.code === 200) {
dialog.title = proxy.$t('department.editDepartment')
dialog.visible = true
dialog.form.deptName = res.data.deptName
dialog.form.leader = res.data.leader
dialog.form.phone = res.data.phone
dialog.form.address = res.data.address
dialog.form.parentId = res.data.parentId
dialog.form.orderNum = res.data.orderNum
dialog.form.status = res.data.status
dialog.form.deptId = res.data.deptId
dialog.form.ancestors = res.data.ancestors
dialog.form.parentName = res.data.parentName
} else {
ElMessage.error(res.msg)
}
}
//
const handleDelete = (row) => {
ElMessageBox.confirm(
proxy.$t('common.deleteItem'),
proxy.$t('common.warning'),
{
confirmButtonText: proxy.$t('button.save'),
cancelButtonText: proxy.$t('button.cancel'),
type: "warning"
}
).then(async () => {
const res = await deleteDepartmentApi(row.deptId)
if (res.code === 200) {
ElMessage.success(res.msg)
} else {
ElMessage.error(res.msg)
}
getList()
}).catch(() => {})
}
onMounted(() => {
getCustomerList()
getList()
})
</script>
<style scoped lang="scss">
.contentBox {
height:calc(100vh - 130px);
margin:20px;
display: flex;
.contentLeft {
width:24%;
margin-right:14px;
}
.contentRight {
flex:1;
}
.contentLeft,.contentRight {
height:100%;
overflow-y: scroll;
padding-bottom:10px;
box-sizing: border-box;
}
.card {
width:100%;
overflow-y: scroll;
}
}
.customer-item {
color: #606266;
padding:12px 0;
cursor: pointer;
border-bottom: 1px solid #ccc;
}
.customer-item:hover {
background-color: #F5F7FA;
}
.customer-item.active {
color: #409EFF;
background-color: #f5f7fa;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,282 @@
<template>
<div class="contentBox">
<div class="contentLeft">
<el-card>
<el-input v-model="deptName" :placeholder="proxy.$t('department.inputDepartmentName')" clearable>
<template #append>
<el-button :icon="Search" />
</template>
</el-input>
<div style="margin-top:10px">
<el-tree :data="deptOption" :props="{ label: 'label', children: 'children' }" :expand-on-click-node="false" :filter-node-method="filterNode" ref="deptTreeRef" node-key="id" highlight-current default-expand-all @node-click="handleNodeClick" />
</div>
</el-card>
</div>
<div class="contentRight">
<el-card>
<el-form :model="queryParams" ref="queryForm" :inline="true">
<el-form-item :label="proxy.$t('position.positionName')" prop="positionName">
<el-input
v-model="queryParams.positionName"
:placeholder="proxy.$t('position.inputPositionName')"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">{{proxy.$t('button.search')}}</el-button>
<el-button icon="Refresh" @click="resetQuery">{{proxy.$t('button.reset')}}</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
>{{proxy.$t('position.addPosition')}}</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="positionList" border>
<el-table-column :label="proxy.$t('common.sort')" type="index" width="60" align="center" />
<el-table-column :label="proxy.$t('position.positionName')" align="center" prop="postName" />
<el-table-column :label="proxy.$t('common.customerAffiliation')" align="center" prop="customerName" />
<el-table-column :label="proxy.$t('position.department')" align="center" prop="deptName" />
<el-table-column :label="proxy.$t('customer.createBy')" align="center" prop="createBy" />
<el-table-column :label="proxy.$t('customer.createTime')" align="center" prop="createTime" width="160" />
<el-table-column :label="proxy.$t('common.action')" align="center" width="150">
<template #default="scope">
<el-button
link
type="primary"
icon="Edit"
@click="handleUpdate(scope.row)"
>{{proxy.$t('button.edit')}}</el-button>
<el-button
link
type="primary"
icon="Delete"
@click="handleDelete(scope.row)"
>{{proxy.$t('button.delete')}}</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-if="total > 0"
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 30, 50]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
<PostDialog
ref="dialogRef"
:title="dialog.title"
:parent-id="parentId"
:select-id="queryParams.deptId"
:select-name="selectedName"
:formData="dialog.form"
v-model="dialog.visible"
@success="handleSuccess"
/>
</div>
</template>
<script setup>
import {Search} from "@element-plus/icons-vue";
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {getDepartmentPostListApi, getDepartmentTreeApi, deleteDepartmentPostApi,getDepartmentPostDetailApi} from '@/api/customer'
import PostDialog from './common/PostDialog.vue';
const { proxy } = getCurrentInstance();
const loading = ref(false)
const total = ref(0)
const dialog = reactive({
title: proxy.$t('position.addPosition'),
visible: false,
form: {
customerId:'',
customerName:'',
deptId: '',
deptName:'',
postName:'',
postId:'',
id:''
}
})
//
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
postName: undefined,
deptId: undefined,
})
const deptName = ref("");
const deptOption = ref(undefined);
const filterNode = (value, data) => {
if (!value) return true;
return data.label.indexOf(value) !== -1;
};
const dialogRef = ref()
const selectedName = ref('')
const parentId = ref(null)
const handleNodeClick = (data) => {
console.log(data,'===data')
queryParams.deptId = data.id;
selectedName.value = data.label;
parentId.value = data.parentId;
getList();
};
watch(deptName, val => {
proxy.$refs["deptTreeRef"].filter(val);
});
//
const positionList = ref([])
//
const handleQuery = () => {
queryParams.pageNum = 1
getList()
}
//
const resetQuery = () => {
queryParams.deptId = undefined;
proxy.$refs.deptTreeRef.setCurrentKey(null);
queryParams.postName = undefined
handleQuery()
}
//
const handleAdd = () => {
//
dialog.title = proxy.$t('position.addPosition')
dialog.visible = true
}
//
const handleUpdate = async(row) => {
//
console.log(row,'===')
const res = await getDepartmentPostDetailApi(row.id)
if(res.code === 200) {
dialog.title = proxy.$t('position.editPosition')
dialog.visible = true
dialog.form.postName = res.data.postName
dialog.form.customerId = res.data.customerId
dialog.form.deptId = res.data.deptId
dialog.form.customerName = res.data.customerName
dialog.form.deptName = res.data.deptName
dialog.form.postId = res.data.postId
dialog.form.id = res.data.id
} else {
ElMessage.error(res.msg)
}
}
//
const handleSuccess = () => {
getList()
}
//
const handleDelete = (row) => {
console.log(row)
ElMessageBox.confirm(
proxy.$t('common.deleteItem'),
proxy.$t('common.warning'),
{
confirmButtonText: proxy.$t('button.save'),
cancelButtonText: proxy.$t('button.cancel'),
type: "warning"
}
).then(async () => {
// API
const res = await deleteDepartmentPostApi(row.id)
if (res.code === 200) {
ElMessage.success('删除成功')
} else {
ElMessage.error(res.msg)
}
getList()
}).catch(() => {})
}
//
const getDeptTree = async() => {
// API
const res = await getDepartmentTreeApi();
if(res.code === 200) {
deptOption.value = res.data
} else {
ElMessage.error(res.msg)
}
}
//
const getList = async() => {
loading.value = true
// API
const res = await getDepartmentPostListApi(queryParams);
console.log(res,'000')
if(res.code === 200) {
positionList.value = res.rows
total.value = res.total
} else {
ElMessage.error(res.msg)
}
setTimeout(() => {
loading.value = false
}, 500)
}
const handleSizeChange = (val) => {
queryParams.pageSize = val
getList()
}
const handleCurrentChange = (val) => {
queryParams.pageNum = val
getList()
}
onMounted(() => {
getList()
getDeptTree()
})
</script>
<style scoped lang="scss">
.contentBox {
height:calc(100vh - 130px);
margin:20px;
display: flex;
.contentLeft {
width:24%;
margin-right:14px;
}
.contentRight {
flex:1;
}
.contentLeft,.contentRight {
height:100%;
overflow-y: scroll;
padding-bottom:10px;
box-sizing: border-box;
}
.card {
width:100%;
overflow-y: scroll;
}
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,156 @@
<template>
<div class="circle-chart">
<div ref="chartRef" style="width: 400px; height: 400px;"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as echarts from 'echarts'
const props = defineProps({
innerData: {
type: Array,
default: () => [0.68, 0.85, 1.07, 1.18, 1.13, 0.97, 0.67, 0.98]
},
middleData: {
type: Array,
default: () => [9.52, 9.95, 10.53, 11.04, 11.34, 11.15, 9.66, 10.40]
},
outerData: {
type: Array,
default: () => [1.15, 1.19, 1.21, 1.17, 1.05, 0.96, 1.06, 0.79]
},
errorRange: {
type: Array,
default: () => [180, 225] //
}
})
const chartRef = ref(null)
let chart = null
const initChart = () => {
if (chart) {
chart.dispose()
}
nextTick(() => {
chart = echarts.init(chartRef.value)
const option = {
color: ['#67F9D8', '#FFE434', '#56A3F1', '#FF917C'],
legend: {
show: false
},
radar: [
{
indicator: [
{ text: `${props.outerData[0]}\n${props.middleData[0]}\n${props.innerData[0]}`, max: 100 },
{ text: `${props.outerData[1]}\n${props.middleData[1]}\n${props.innerData[1]}`, max: 100 },
{ text: `${props.outerData[2]}\n${props.middleData[2]}\n${props.innerData[2]}`, max: 100 },
{ text: `${props.outerData[3]}\n${props.middleData[3]}\n${props.innerData[3]}`, max: 100 },
{ text: `${props.outerData[4]}\n${props.middleData[4]}\n${props.innerData[4]}`, max: 100 },
{ text: `${props.outerData[5]}\n${props.middleData[5]}\n${props.innerData[5]}`, max: 100 },
{ text: `${props.outerData[6]}\n${props.middleData[6]}\n${props.innerData[6]}`, max: 100 },
{ text: `${props.outerData[7]}\n${props.middleData[7]}\n${props.innerData[7]}`, max: 100 }
],
center: ['50%', '50%'], //
radius: '60%', //
startAngle: 90,
splitNumber: 1, //
shape: 'circle',
axisName: {
show: true,
color: '#333',
fontSize: 12,
rich: {
value: {
lineHeight: 20,
align: 'center'
}
}
},
splitArea: {
show: false
},
axisLine: {
lineStyle: {
color: '#999'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#999',
type: 'dashed'
},
formatter: function(params) {
// 45°135°225°315°线
const angles = [45, 135, 225, 315]
return angles.includes(params.value) ? params.value : ''
}
}
}
],
series: [
{
type: 'radar',
data: [
{
value: [80, 70, 90, 85, 75, 88, 82, 78],
symbol: 'none', //
lineStyle: {
color: '#1890ff',
width: 2
},
areaStyle: {
color: 'rgba(24, 144, 255, 0.2)'
},
smooth: true, // 线
smoothMonotone: 'cw', //
emphasis: {
focus: 'series'
}
}
]
}
]
}
chart.setOption(option)
})
}
//
// watch([() => props.innerData, () => props.middleData, () => props.outerData], () => {
// initChart()
// }, { deep: true })
//
const refresh = () => {
initChart()
}
onMounted(() => {
initChart()
window.addEventListener('resize', () => chart?.resize())
})
onUnmounted(() => {
window.removeEventListener('resize', () => chart?.resize())
chart?.dispose()
})
defineExpose({
refresh
})
</script>
<style scoped lang="scss">
.circle-chart {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
background: #fff;
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,157 @@
<template>
<div class="curve-chart">
<div class="chart-title">{{title}}</div>
<div ref="chartRef" style="width: 100%; height: 400px"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
const props = defineProps({
title: {
type: String,
default: '',
},
color: {
type: String,
default: '#ff4444'
},
})
const chartRef = ref(null)
let chart = null
const initChart = () => {
if (chart) {
chart.dispose()
}
nextTick(() => {
chart = echarts.init(chartRef.value)
const option = {
grid: {
left: '12%', //
right: '5%',
top: '8%', //
bottom: '12%', //
containLabel: true //
},
xAxis: {
type: 'value',
min: 0,
max: 120,
interval: 15,
axisLine: {
show: true
},
axisLabel: {
margin: 12, // 线
formatter: '{value}'
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
},
yAxis: {
type: 'value',
min: 0,
max: 48000,
interval: 6000,
axisLine: {
show: true
},
axisLabel: {
margin: 16, // 线
formatter: '{value}'
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
},
series: [{
type: 'line',
data: generateData(),
symbol: 'none',
smooth: 0.2, //
smoothMonotone: 'x', //
lineStyle: {
color: props.color,
width: 2
},
animation: false
}]
}
chart.setOption(option)
})
}
// resize
const resize = () => {
nextTick(() => {
chart?.resize()
})
}
//
defineExpose({
resize
})
const generateData = () => {
const data = []
for (let i = 0; i <= 120; i += 0.5) { //
let value
if (i <= 45) {
value = 40000
} else if (i > 45 && i <= 60) {
// 使线
const progress = (i - 45) / 15
value = 40000 - (34000 * Math.pow(progress, 1.2))
} else if (i > 60 && i <= 75) {
value = 6000
} else if (i > 75 && i <= 90) {
// 使线
const progress = (i - 75) / 15
value = 6000 + (34000 * Math.pow(progress, 0.8))
} else {
value = 40000
}
data.push([i, value])
}
return data
}
onMounted(() => {
initChart()
window.addEventListener('resize', () => chart?.resize())
})
onUnmounted(() => {
window.removeEventListener('resize', () => chart?.resize())
chart?.dispose()
})
</script>
<style scoped lang="scss">
.curve-chart {
width:100%;
height:100%;
padding: 20px;
background: #fff;
border-radius: 4px;
.chart-title {
text-align: center;
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
}
}
</style>

View File

@ -0,0 +1,32 @@
<template>
<div class="data-table">
<el-table :data="tableData" border>
<el-table-column prop="name" label="数据项" width="180" />
<el-table-column prop="arm1" label="扫描臂1">
<template #default="scope">
<el-tag v-if="scope.row.type === 'status'" :type="scope.row.arm1.type">
{{ scope.row.arm1.value }}
</el-tag>
<span v-else>{{ scope.row.arm1 }}</span>
</template>
</el-table-column>
<el-table-column prop="arm2" label="扫描臂2">
<template #default="scope">
<el-tag v-if="scope.row.type === 'status'" :type="scope.row.arm2.type">
{{ scope.row.arm2.value }}
</el-tag>
<span v-else>{{ scope.row.arm2 }}</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
defineProps({
tableData: {
type: Array,
required: true
}
})
</script>

View File

@ -0,0 +1,216 @@
<template>
<el-dialog
:title="title"
v-model="visible"
width="600px"
append-to-body
:before-close="cancel"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
:label-width="appStore.language === 'zh_CN' ? '140px' : '200px'"
label-position="right"
>
<el-form-item :label="proxy.$t('equipment.deviceName')" prop="deviceName">
<el-input v-model="form.deviceName" :placeholder="proxy.$t('equipment.inputDeviceName')" />
</el-form-item>
<el-form-item :label="proxy.$t('equipment.deviceCode')" prop="deviceCode">
<el-input v-model="form.deviceCode" :placeholder="proxy.$t('equipment.inputDeviceCode')" />
</el-form-item>
<el-form-item :label="proxy.$t('equipment.deviceModel')" prop="deviceModel">
<el-select v-model="form.deviceModel" :placeholder="proxy.$t('equipment.inputDeviceModel')">
<el-option
v-for="item in typeOptions"
:key="item.id"
:label="item.deviceModel"
:value="item.deviceModel"
/>
</el-select>
</el-form-item>
<el-form-item :label="proxy.$t('common.customerAffiliation')" prop="customerId">
<el-select v-model="form.customerId" :placeholder="proxy.$t('common.select')">
<el-option
v-for="item in customerList"
:key="item.deptId"
:label="item.deptName"
:value="item.deptId"
/>
</el-select>
</el-form-item>
<el-form-item :label="proxy.$t('common.userAffiliation')" prop="userIdList">
<el-select v-model="form.userIdList" :placeholder="proxy.$t('common.selectMul')" multiple>
<el-option
v-for="item in userList"
:key="item.userId"
:label="item.nickName"
:value="item.userId"
/>
</el-select>
</el-form-item>
<el-form-item :label="proxy.$t('equipment.localEMQ')" v-if="title === proxy.$t('button.add')" prop="emqxAuth.localUserName" >
<el-input v-model="form.emqxAuth.localUserName" :placeholder="proxy.$t('common.select')" />
</el-form-item>
<el-form-item :label="proxy.$t('equipment.localPassword')" v-if="title === proxy.$t('button.add')" prop="emqxAuth.localPass">
<el-input v-model="form.emqxAuth.localPass" :placeholder="proxy.$t('common.select')" />
</el-form-item>
<el-form-item :label="proxy.$t('equipment.localEMQPassword')" v-if="title === proxy.$t('button.add')" prop="emqxAuth.cloudUserName">
<el-input v-model="form.emqxAuth.cloudUserName" :placeholder="proxy.$t('common.select')" />
</el-form-item>
<el-form-item :label="proxy.$t('equipment.localEMQUser')" v-if="title === proxy.$t('button.add')" prop="emqxAuth.cloudPass">
<el-input v-model="form.emqxAuth.cloudPass" :placeholder="proxy.$t('common.select')" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">{{ proxy.$t('button.save') }}</el-button>
<el-button @click="cancel">{{proxy.$t('button.cancel')}}</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive,watch } from 'vue'
import { ElMessage } from 'element-plus'
import {getDeviceModelListApi, addDeviceApi, editDeviceApi} from "@/api/equipment/index.js";
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
const { proxy } = getCurrentInstance();
const props = defineProps({
title: {
type: String,
default: '新建设备'
},
formData: {
type: Object,
default: () => ({})
},
customerList: {
type: Array,
default: () => []
},
userList: {
type: Array,
default: () => []
},
})
const emit = defineEmits(['update:modelValue', 'success'])
const visible = ref(false)
const formRef = ref()
const form = reactive({
deviceName: '',
deviceCode: '',
deviceModel: '',
customerId: '',
userIdList: [],
emqxAuth: {
localEmqUser: '',
localPassword: '',
cloudEmqUser: '',
cloudPassword: ''
},
id:''
})
const rules = {
deviceName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
deviceCode: [{ required: true, message: '请输入设备编码', trigger: 'blur' }],
deviceModel: [{ required: true, message: '请选择设备型号', trigger: 'change' }],
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
emqxAuth: {
localUserName: [{ required: true, message: '请输入本地EMQ用户名', trigger: 'blur' }],
localPass: [{ required: true, message: '请输入本地密码', trigger: 'blur' }],
cloudUserName: [{ required: true, message: '请输入云EMQ用户名', trigger: 'blur' }],
cloudPass: [{ required: true, message: '请输入云密码', trigger: 'blur' }]
}
}
const query = {
pageNum: 1,
pageSize: 100000000
}
//
const typeOptions = ref([])
//
const getDeviceTypeList = async () => {
// API
const res = await getDeviceModelListApi(query)
if (res.code === 200) {
typeOptions.value = res.rows || []
} else {
ElMessage.error(res.msg)
}
}
const submitForm = async () => {
try {
await formRef.value.validate()
const res = props.title==='新增' ? await addDeviceApi(form): await editDeviceApi({deviceName: form.deviceName,
deviceCode: form.deviceCode,
deviceModel: form.deviceModel,
customerId: form.customerId,
userIdList: form.userIdList,
id:form.id
})
if (res.code === 200) {
emit('success', form)
ElMessage.success(res.msg)
} else {
ElMessage.error(res.msg)
}
cancel()
} catch (error) {
console.error('表单验证失败', error)
}
}
const cancel = () => {
formRef.value?.resetFields()
Object.keys(form).forEach(key => {
if (Array.isArray(form[key])) {
form[key] = []
} else {
form[key] = ''
}
})
emit('update:modelValue', false)
}
watch(
() => props.formData,
(newVal) => {
if (props.title === '编辑' && Object.keys(newVal).length > 0) {
Object.keys(form).forEach(key => {
if (key === 'userIdList') {
form[key] = Array.isArray(newVal[key]) ? [...newVal[key]] : []
} else if (key === 'emqxAuth') {
form[key] = { ...newVal[key] }
} else {
form[key] = newVal[key]
}
})
}
},
{ immediate: true, deep: true }
)
defineExpose({
visible
})
onMounted(() => {
getDeviceTypeList()
})
</script>
<style scoped>
.dialog-footer {
text-align: center;
padding-top: 20px;
}
:deep(.el-form-item__label) {
justify-content: flex-end;
}
</style>

View File

@ -0,0 +1,88 @@
<template>
<div class="device-settings">
<el-form :model="formData" label-width="150px">
<el-form-item v-for="item in settingItems" :key="item.key" :label="item.label">
<div class="setting-item">
<el-input
v-model="formData[item.key]"
:suffix="item.unit"
style="width: 200px"
>
<template #append>
<el-icon><Edit /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="handleSet(item)">设定</el-button>
</div>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Edit } from '@element-plus/icons-vue'
const formData = ref({
oilTemp: '45.0',
waterTemp: '40.0',
airTemp: '40.0',
boardTemp: '45.0',
outTemp: '30.0',
waterFlow: '0.0',
scanSpeed: '40',
interval: '10',
voltage1: '55000',
current1: '1800',
filament1: '3000',
preheat1: '1000',
voltage2: '55000'
})
const settingItems = [
{ label: '油温上限', key: 'oilTemp', unit: '℃' },
{ label: '水温报警线', key: 'waterTemp', unit: '℃' },
{ label: '气温报警线', key: 'airTemp', unit: '℃' },
{ label: '采集板温度报警线', key: 'boardTemp', unit: '℃' },
{ label: '出风设定温度', key: 'outTemp', unit: '℃' },
{ label: '水流下限', key: 'waterFlow', unit: 'L/min' },
{ label: '扫描臂速度', key: 'scanSpeed', unit: 'mm/s' },
{ label: '自定义间隔', key: 'interval', unit: 'min' },
{ label: '射线电源设定电压1', key: 'voltage1', unit: 'V' },
{ label: '射线电源设定电流1', key: 'current1', unit: 'μA' },
{ label: '灯丝设定电流1', key: 'filament1', unit: 'mA' },
{ label: '灯丝预热设定电流1', key: 'preheat1', unit: 'mA' },
{ label: '射线电源设定电压2', key: 'voltage2', unit: 'mA' }
]
const emit = defineEmits(['update'])
const handleSet = (item) => {
console.log(item)
emit('update', {
key: item.key,
value: formData.value[item.key]
})
}
</script>
<style scoped lang="scss">
.device-settings {
padding: 20px;
.setting-item {
display: flex;
gap: 10px;
align-items: center;
}
:deep(.el-input-group__append) {
padding: 0 10px;
cursor: pointer;
}
.el-form-item {
margin-bottom: 15px;
}
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<div class="device-status-table">
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="name" label="参数" align="center" />
<el-table-column prop="arm1" label="扫描臂1" align="center">
<template #default="scope">
<template v-if="scope.row.type === 'tag'">
<el-tag :type="scope.row.arm1.type" effect="plain">
{{ scope.row.arm1.value }}
</el-tag>
</template>
<template v-else>
{{ scope.row.arm1 }}
</template>
</template>
</el-table-column>
<el-table-column prop="arm2" label="扫描臂2" align="center">
<template #default="scope">
<template v-if="scope.row.type === 'tag'">
<el-tag :type="scope.row.arm2.type" effect="plain">
{{ scope.row.arm2.value }}
</el-tag>
</template>
<template v-else>
{{ scope.row.arm2 }}
</template>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
statusData: {
type: Object,
required: true
}
})
const tableData = computed(() => [
{ name: '通信状态', arm1: props.statusData.arm1.communication, arm2: props.statusData.arm2.communication },
{ name: '故障状态', arm1: props.statusData.arm1.fault, arm2: props.statusData.arm2.fault },
{ name: '使能状态', arm1: props.statusData.arm1.enable, arm2: props.statusData.arm2.enable },
{ name: '当前位置', arm1: props.statusData.arm1.position, arm2: props.statusData.arm2.position },
{ name: '当前速度[r/min]', arm1: props.statusData.arm1.speed, arm2: props.statusData.arm2.speed },
{
name: '射线电源开关',
type: 'tag',
arm1: props.statusData.arm1.powerSwitch,
arm2: props.statusData.arm2.powerSwitch
},
{ name: '射线电源安全锁', arm1: props.statusData.arm1.safetyLock, arm2: props.statusData.arm2.safetyLock },
{ name: '射线电源设定电压[V]', arm1: props.statusData.arm1.voltageSet, arm2: props.statusData.arm2.voltageSet },
{ name: '射线电源反馈电压[V]', arm1: props.statusData.arm1.voltageFeedback, arm2: props.statusData.arm2.voltageFeedback },
{ name: '射线电源反馈电流[μA]', arm1: props.statusData.arm1.currentFeedback, arm2: props.statusData.arm2.currentFeedback },
{ name: '射线电源设定电流[μA]', arm1: props.statusData.arm1.currentSet, arm2: props.statusData.arm2.currentSet }
])
</script>
<style scoped lang="scss">
.device-status-table {
:deep(.el-table) {
.el-tag {
width: 100%;
justify-content: center;
&.el-tag--danger {
background-color: #fef0f0;
}
}
.cell {
padding: 8px 0;
}
}
}
</style>

View File

@ -0,0 +1,207 @@
<template>
<div class="dual-channel-chart">
<div class="chart-title">{{ title }}</div>
<div ref="chartRef" style="height: 200px"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
const props = defineProps({
title: String,
chartData: {
type: Object,
required: true
}
})
const chartRef = ref(null)
let chart = null
// const initChart = () => {
// chart = echarts.init(chartRef.value)
// const option = {
// grid: {
// top: 30,
// right: 10,
// bottom: 20,
// left: 40
// },
// legend: {
// data: ['a', 'b'],
// right: 0,
// top: 0
// },
// xAxis: {
// type: 'value',
// min: 0,
// max: 100
// },
// yAxis: {
// type: 'value',
// min: 0,
// max: 10000
// },
// series: [
// {
// name: 'a',
// type: 'line',
// data: props.chartData.a,
// color: '#ff4444',
// smooth: true
// },
// {
// name: 'b',
// type: 'line',
// data: props.chartData.b,
// color: '#67c23a',
// smooth: true
// }
// ]
// }
// chart = echarts.init(chartRef.value)
// chart.setOption(option)
// }
const initChart = () => {
const option = {
grid: {
left: '10%', //
right: '5%',
top: '10%', //
bottom: '10%', //
containLabel: true
},
legend: {
data: ['a通道', 'b通道'],
right: 10,
top: 0
},
xAxis: {
type: 'value',
min: 0,
max: 90,
interval: 15,
axisLine: {
show: true
},
splitLine: { // 线
show: true,
lineStyle: {
type: 'dashed'
}
},
axisLabel: { //
margin: 12, // 线
formatter: '{value}'
}
},
yAxis: {
type: 'value',
min: 0,
max: 9000,
interval: 1500,
axisLine: {
show: true
},
splitLine: { // 线
show: true,
lineStyle: {
type: 'dashed'
}
},
axisLabel: { //
margin: 16, // 线
formatter: '{value}'
}
},
series: [
{
name: 'a通道',
type: 'line',
data: generateData('a'),
symbol: 'none',
lineStyle: {
color: '#ff4444',
width: 2
},
smooth: true
},
{
name: 'b通道',
type: 'line',
data: generateData('b'),
symbol: 'none',
lineStyle: {
color: '#67c23a',
width: 2
},
smooth: true
}
]
}
chart = echarts.init(chartRef.value)
chart.setOption(option)
}
const generateData = (type) => {
const data = []
for (let i = 0; i <= 90; i++) {
let value
if (i < 45) {
value = type === 'a' ? 8500 : 7500
} else if (i >= 45 && i < 60) {
//
const progress = (i - 45) / 15
value = type === 'a' ?
8500 - (8000 * progress) :
7500 - (7000 * progress)
} else if (i >= 60 && i < 75) {
//
const progress = (i - 60) / 15
value = type === 'a' ?
500 + (8000 * progress) :
500 + (7000 * progress)
} else {
value = type === 'a' ? 8500 : 7500
}
data.push([i, value])
}
return data
}
onMounted(() => {
initChart()
window.addEventListener('resize', () => chart?.resize())
})
onUnmounted(() => {
window.removeEventListener('resize', () => chart?.resize())
chart?.dispose()
})
const resize = () => {
nextTick(() => {
chart?.resize()
})
}
defineExpose({
resize
})
</script>
<style scoped lang="scss">
.dual-channel-chart {
width: 100%;
//height: 100%;
background: #fff;
border-radius: 4px;
padding: 15px;
.chart-title {
margin-bottom: 10px;
font-weight: bold;
}
}
</style>

View File

@ -0,0 +1,60 @@
<template>
<el-dialog
:title="proxy.$t('equipment.EMQConnect')"
v-model="visible"
width="600px"
append-to-body
:before-close="handleCancel"
>
<el-descriptions :column="2" border>
<el-descriptions-item :label="proxy.$t('equipment.deviceName')">{{ formData.deviceName }}</el-descriptions-item>
<el-descriptions-item :label="proxy.$t('equipment.deviceCode')">{{ formData.deviceCode }}</el-descriptions-item>
<el-descriptions-item :label="proxy.$t('equipment.localEMQ')">{{ formData.localUserName }}</el-descriptions-item>
<el-descriptions-item :label="proxy.$t('equipment.localPassword')">{{ formData.localPass }}</el-descriptions-item>
<el-descriptions-item :label="proxy.$t('equipment.localEMQPassword')">{{ formData.cloudUserName }}</el-descriptions-item>
<el-descriptions-item :label="proxy.$t('equipment.localEMQUser')">{{ formData.cloudPass }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<div class="dialog-footer">
<!-- <el-button type="primary" @click="handleConfirm">确定</el-button>-->
<el-button @click="handleCancel">{{proxy.$t('button.cancel')}}</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref } from 'vue'
const { proxy } = getCurrentInstance();
const props = defineProps({
formData: {
type: Object,
default: () => ({})
}
})
const visible = ref(false)
const handleCancel = () => {
visible.value = false
}
defineExpose({
visible
})
</script>
<style scoped>
.dialog-footer {
text-align: center;
padding-top: 20px;
}
:deep(.el-descriptions__label) {
width: 120px;
justify-content: flex-end;
color: var(--el-text-color-regular);
}
</style>

View File

@ -0,0 +1,108 @@
<template>
<el-dialog
v-model="visible"
title="选择历史数据"
width="600px"
:before-close="handleClose"
>
<div class="history-data">
<el-form :model="form" label-width="80px">
<el-form-item label="选择日期" required>
<el-date-picker
v-model="form.date"
type="datetime"
placeholder="选择日期+小时"
format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-form>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="index" label="序号" width="80" />
<el-table-column prop="name" label="字典名称" />
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[20, 50, 100]"
layout="sizes, prev, pager, next, jumper, total"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref } from 'vue'
const visible = ref(false)
const form = ref({
date: ''
})
const tableData = ref([])
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(101)
const emit = defineEmits(['confirm', 'cancel'])
const show = () => {
visible.value = true
}
const handleClose = () => {
visible.value = false
emit('cancel')
}
const handleConfirm = () => {
visible.value = false
emit('confirm', form.value)
}
const handleSizeChange = (val) => {
pageSize.value = val
}
const handleCurrentChange = (val) => {
currentPage.value = val
}
defineExpose({
show
})
</script>
<style scoped lang="scss">
.history-data {
.pagination {
margin-top: 16px;
padding-bottom: 16px;
display: flex;
justify-content: flex-end;
:deep(.el-pagination) {
justify-content: flex-end;
flex-wrap: wrap;
row-gap: 8px;
}
}
}
:deep(.el-dialog__body) {
padding-top: 16px;
padding-bottom: 0;
}
</style>

View File

@ -0,0 +1,217 @@
<template>
<div class="realtime-chart">
<div class="chart-header">
<div class="title">趋势图</div>
<div class="subtitle"> 30min 数据</div>
<div class="actions">
<el-dropdown>
<el-button type="primary">
冷直径
<el-icon class="el-icon--right"><CaretBottom /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="(item,index) in options" :key="index">{{item.label}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button @click="refreshData">
<el-icon><Refresh /></el-icon>
</el-button>
</div>
</div>
<div ref="chartRef" style="width: 100%; height: 300px"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { CaretBottom, Refresh } from '@element-plus/icons-vue'
import * as echarts from 'echarts'
// import { getRealtimeData } from '@/api/scanner'
const chartRef = ref(null)
let myChart = null
let timer = null
const selectedType = ref('coldDiameter')
const options = [
{ label: '冷直径', value: 'coldDiameter' },
{ label: '热外径', value: 'hotDiameter' },
{ label: '内屏蔽层偏心度', value: 'eccentricity' },
{ label: '内屏蔽厚度', value: 'ovality' },
{ label: '外屏蔽层偏心度', value: 'thickness' },
{ label: '外屏蔽层厚度', value: 'innerDiameter' },
{ label: '绝缘层偏心度', value: 'outerDiameter' },
{ label: '绝缘层厚度', value: 'surfaceQuality' }
]
const mockData = {
times: ['13:21', '13:22', '13:23', '13:24', '13:25', '13:26', '13:27', '13:28', '13:29', '13:30',
'13:31', '13:32', '13:33', '13:34', '13:35', '13:36', '13:37', '13:38', '13:39', '13:40', '13:41'],
values: [3.2, 3.1, 3.3, 3.2, 3.4, 3.3, 3.5, 3.6, 3.4, 3.3, 3.2, 3.4, 3.3, 3.2, 3.1, 3.0, 3.2, 2.8, 3.5, 3.6, 3.4]
}
const initChart = () => {
console.log(myChart,'myChart')
if (myChart) {
myChart.dispose()
}
// DOM
nextTick(() => {
myChart = echarts.init(chartRef.value)
const option = {
grid: {
left: '0%',
right: '0%',
bottom: '15%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: mockData.times,
axisLabel: {
interval: 2,
margin: 12 // 线
}
},
yAxis: {
type: 'value',
min: 0,
max: 5,
interval: 1,
splitLine: {
lineStyle: {
type: 'dashed'
}
},
xisLabel: {
margin: 16, // 线
formatter: '{value}'
},
nameTextStyle: {
padding: [0, 0, 0, 40] //
}
},
series: [{
data: mockData.values,
type: 'line',
smooth: true,
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0,
color: 'rgba(64, 158, 255, 0.3)'
}, {
offset: 1,
color: 'rgba(64, 158, 255, 0.1)'
}]
}
},
itemStyle: {
color: '#409EFF'
},
lineStyle: {
width: 1,
color: '#409EFF'
},
symbol: 'none'
}]
}
myChart.setOption(option)
})
}
const fetchData = async () => {
try {
const res = await getRealtimeData({
type: selectedType.value,
minutes: 30
})
if (res.code === 200) {
myChart.setOption({
xAxis: {
data: res.data.times
},
series: [{
data: res.data.values
}]
})
}
} catch (error) {
console.error('获取数据失败', error)
}
}
// const refreshData = () => {
// // fetchData()
// }
// watch(() => selectedType.value, () => {
// // fetchData()
// })
const refreshData = () => {
initChart()
}
onMounted(() => {
initChart()
// fetchData()
//
// timer = setInterval(fetchData, 60000)
window.addEventListener('resize', () => myChart?.resize())
})
onUnmounted(() => {
// clearInterval(timer)
window.removeEventListener('resize', () => myChart?.resize())
myChart?.dispose()
})
//
const resize = () => {
nextTick(() => {
myChart?.resize()
})
}
//
defineExpose({
resize
})
</script>
<style scoped lang="scss">
.realtime-chart {
padding-top: 14px;
background: #fff;
border-radius: 4px;
.chart-header {
display: flex;
align-items: center;
margin-bottom: 20px;
.title {
font-size: 16px;
font-weight: bold;
}
.subtitle {
color: #909399;
margin-left: 10px;
}
.actions {
margin-left: auto;
display: flex;
gap: 10px;
}
}
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<div class="temperature-container">
<div ref="chartRef" style="width: 100%; height: 400px;"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
const props = defineProps({
chartData: {
type: Object,
default: () => ({
arm1: [],
arm2: [],
times: []
})
}
})
const chartRef = ref(null)
let myChart = null
const initChart = () => {
const option = {
title: {
left: 0,
top: 0
},
tooltip: {
trigger: 'axis',
formatter: (params) => {
const time = params[0].axisValue
return `${time}<br/>
扫描臂1: ${params[0].data}<br/>
扫描臂2: ${params[1].data}`
}
},
legend: {
data: ['扫描臂1', '扫描臂2'],
right: 0,
top: 0
},
grid: {
left: 60, //
right: 30, //
top: 40,
bottom: 40,
containLabel: true //
},
xAxis: {
type: 'category',
boundaryGap: false,
data: props.chartData.times,
axisLabel: {
formatter: (value) => {
return value.substring(11, 19)
}
}
},
yAxis: {
type: 'value',
name: '设备温度(℃)',
splitLine: {
lineStyle: {
type: 'dashed'
}
},
axisLabel: {
margin: 16, // 线
formatter: '{value}'
},
min: 0, //
max: 100, //
interval: 20
},
series: [
{
name: '扫描臂1',
type: 'line',
data: props.chartData.arm1,
itemStyle: {
color: '#f56c6c'
},
lineStyle: {
width: 2
},
smooth: true
},
{
name: '扫描臂2',
type: 'line',
data: props.chartData.arm2,
itemStyle: {
color: '#67c23a'
},
lineStyle: {
width: 2
},
smooth: true
}
]
}
myChart = echarts.init(chartRef.value)
myChart.setOption(option)
}
watch(() => props.chartData, () => {
initChart()
}, { deep: true })
onMounted(() => {
initChart()
window.addEventListener('resize', () => myChart?.resize())
})
onUnmounted(() => {
window.removeEventListener('resize', () => myChart?.resize())
myChart?.dispose()
})
</script>
<style scoped>
.temperature-container {
width: 100%;
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<el-dialog
title="分享链接"
v-model="visible"
width="500px"
append-to-body
>
<div class="share-content">
<el-input
v-model="shareUrl"
readonly
:autosize="{ minRows: 2, maxRows: 4 }"
type="textarea"
>
<template #append>
<el-button type="primary" @click="copyUrl">复制链接</el-button>
</template>
</el-input>
<div class="tip-text">点击分享链接生成一个web超链接设备详情页弹出提示条"分享链接已复制!"</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import useClipboard from 'vue-clipboard3'
const { toClipboard } = useClipboard()
const visible = ref(false)
const shareUrl = ref('')
const props = defineProps({
deviceId: {
type: [String, Number],
default: ''
}
})
watch(() => props.deviceId, (newVal) => {
if (newVal) {
//
shareUrl.value = `${window.location.origin}/device/detail/${newVal}`
}
})
const copyUrl = async () => {
try {
await toClipboard(shareUrl.value)
ElMessage.success('分享链接已复制!')
} catch (e) {
ElMessage.error('复制失败,请手动复制')
}
}
const handleClose = () => {
visible.value = false
}
defineExpose({
visible
})
</script>
<style scoped>
.share-content {
padding: 20px 0;
}
.tip-text {
margin-top: 10px;
color: #909399;
font-size: 14px;
}
.dialog-footer {
text-align: center;
}
</style>

View File

@ -0,0 +1,858 @@
<template>
<div class="equipment-container">
<div class="data-overview">
<div class="data-left">
<div class="equipment-name">设备名称设备编码</div>
<el-button type="info" size="small">离线</el-button>
<div style="margin-left:16px;color:#606266">时间显示</div>
</div>
<div class="action-buttons">
<div class="button-group">
<el-button
type="primary"
link
@click="handleShowImage()"
>
{{proxy.$t('equipment.imagePage')}}
</el-button>
<el-button
:type="mode === 'fast' ? 'primary' : ''"
@click="handleModeChange('fast')"
>
<el-icon><Timer /></el-icon>
快速模式
</el-button>
<el-button
:type="xRayStatus ? 'danger' : 'success'"
@click="handleXRayToggle"
>
X{{proxy.$t('equipment.Ray')}}{{ xRayStatus ? proxy.$t('common.open') : proxy.$t('common.close') }}
</el-button>
<el-button
type="primary"
plain
@click="handleGenerateReport"
>
{{proxy.$t('equipment.reporting')}}
</el-button>
<el-button
type="success"
plain
@click="handleDeviceShare"
>
{{proxy.$t('equipment.deviceShare')}}
</el-button>
</div>
</div>
</div>
<div class="chart-container">
<div class="chart-box chart-box-left">
<el-tabs
v-model="activeName"
type="card"
class="demo-tabs"
@tab-click="handleClick"
>
<el-tab-pane label="数据组1" name="first">
<div class="card-container">
<div class="card-list">
<div class="eccentricity-card" v-for="(item, index) in cardList" :key="index">
<div class="card-header">
<span class="title">{{item.title}}</span>
<el-icon class="close-icon"><Close /></el-icon>
</div>
<div class="card-content">
<div class="card-text">
<div>
<div class="difference">
<span class="plus">收缩率</span>
<span class="label">9%</span>
</div>
<div class="difference">
<span class="plus">+</span>
<span class="label">公差</span>
</div>
</div>
<div class="value">
21.15<span class="unit">%</span>
</div>
</div>
<div class="tolerance-section">
<el-button
type="primary"
plain
class="switch-btn left"
@click="handlePrevious"
>
<el-icon><ArrowLeft /></el-icon>
</el-button>
<div class="tolerance-text">
偏心度公差 {{ toleranceList[currentIndex] }}
</div>
<el-button
type="primary"
plain
class="switch-btn right"
@click="handleNext"
>
<el-icon><ArrowRight /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="数据组2" name="second">数据组2</el-tab-pane>
<el-tab-pane label="数据组3" name="third">数据组3</el-tab-pane>
</el-tabs>
<div class="btn">
<el-button type="primary">{{proxy.$t('button.add')}}</el-button>
</div>
</div>
<div class="chart-box chart-box-right">
<el-tabs
v-model="activeTwoName"
type="card"
class="demo-tabs"
@tab-click="handleClick"
>
<el-tab-pane :label="proxy.$t('equipment.sketch')" name="first">
<div class="detection-section">
<CircleDetectionChart
:inner-data="innerLayerData"
:middle-data="middleLayerData"
:outer-data="outerLayerData"
/>
<div class="history-content">
<div>左边指南针</div>
<div class="history-right">
<div>
<el-button v-if="showHistory" type="primary" @click="showHistoryDialog">选择历史数据</el-button>
<el-switch
v-model="showHistory"
size="large"
style="margin-left: 6px"
:inactive-text="proxy.$t('equipment.historyData')"
/>
</div>
<div class="scanItem">扫描时间2025/01/07 04:34:8</div>
<div class="scanItem">2025-01-07_04_30_34_866_direction_1_device2.json</div>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane :label="proxy.$t('equipment.deviceInfo')" name="second">
<div class="device-status">
<DeviceStatus v-if="!showBtn" :status-data="deviceStatus" />
<DeviceSetting v-if="showBtn" @update="handleSettingUpdate" />
</div>
</el-tab-pane>
</el-tabs>
<div class="btn" v-if="activeTwoName === 'second'">
<el-button v-if="!showBtn" type="primary" @click="paramsHandle">{{proxy.$t('equipment.paramsSetting')}}</el-button>
<el-button v-if="showBtn" type="primary" @click="paramsHandle">{{proxy.$t('button.return')}}</el-button>
</div>
</div>
</div>
<div class="chart-container" style="margin-top: 14px">
<div class="chart-box chart-bottom-left">
<el-tabs
v-model="activeThreeName"
type="card"
class="demo-tabs"
@tab-click="handleClick"
>
<el-tab-pane :label="proxy.$t('equipment.deviceTemperature')" name="first">
<TemperatureChart :chart-data="temperatureData"></TemperatureChart>
</el-tab-pane>
<el-tab-pane :label="proxy.$t('equipment.warningInfo')" name="second">
<div class="warning-container">
<div class="warning-item" v-for="(item, index) in warningList" :key="index">
{{item.title}}
</div>
</div>
</el-tab-pane>
<el-tab-pane :label="proxy.$t('equipment.imageView')" name="third">
<div class="controls">
<el-button-group>
<el-button
type="primary"
plain
class="switch-btn right"
@click="handleImagePrev"
>
<el-icon><ArrowLeft /></el-icon>
</el-button>
<el-button
type="primary"
plain
class="switch-btn right switch"
@click="handleImageNext"
>
<el-icon><ArrowRight /></el-icon>
</el-button>
</el-button-group>
</div>
<div style="display: flex">
<div v-if="showRed">
<CurveChart ref="bowlCurveRef" :title="chartTitle.left" :color="colorRed"/>
<CurveChart ref="bowlCurveRef" :title="chartTitle.right" :color="colorRed"/>
</div>
<div v-if="showGreen">
<CurveChart ref="bowlCurveRef" :title="chartTitle.left" :color="colorGreen"/>
<CurveChart ref="bowlCurveRef" :title="chartTitle.right" :color="colorGreen"/>
</div>
<div v-if="showRedGreen">
<DualChannelChart
title="扫描臂1"
ref="dualChart1Ref"
:chart-data="arm1ChartData"
/>
<DualChannelChart
title="扫描臂2"
ref="dualChart2Ref"
:chart-data="arm2ChartData"
/>
</div>
</div>
</el-tab-pane>
<el-tab-pane :label="proxy.$t('equipment.trendChart')" name="four">
<RealtimeChart ref="realtimeChartRef" />
</el-tab-pane>
</el-tabs>
</div>
<div class="chart-box chart-bottom-right">
<div class="header">
<div>
<span>{{proxy.$t('equipment.imagePage')}}</span>
<span class="time">{{ currentTime }}</span>
</div>
<el-divider/>
</div>
<div class="imagePage-container">
<div class="imagePage-left">
<DataTable :table-data="tableData" />
</div>
<div class="charts">
<div class="controls">
<el-button-group>
<el-button type="primary" plain class="switch-btn" @click="handleImagePagePrev"><el-icon><ArrowLeft /></el-icon></el-button>
<el-button type="primary" plain class="switch-btn switch" @click="handleImagePageNext"><el-icon><ArrowRight /></el-icon></el-button>
<el-button type="primary" plain class="switch-btn switch" @click="handleRefresh"><el-icon><Refresh /></el-icon></el-button>
</el-button-group>
</div>
<div v-if="showImagePageRed">
<CurveChart ref="bowlCurvePageRef" :title="chartTitle.left" :color="colorRed"/>
<CurveChart ref="bowlCurvePageRef" :title="chartTitle.right" :color="colorRed"/>
</div>
<div v-if="showImagePageGreen">
<CurveChart ref="bowlCurvePageRef" :title="chartTitle.left" :color="colorGreen"/>
<CurveChart ref="bowlCurvePageRef" :title="chartTitle.right" :color="colorGreen"/>
</div>
<div v-if="showImagePageRedGreen">
<DualChannelChart
title="扫描臂1"
ref="dualChart1PageRef"
:chart-data="arm1ChartData"
/>
<DualChannelChart
title="扫描臂2"
ref="dualChart2PageRef"
:chart-data="arm2ChartData"
/>
</div>
</div>
</div>
</div>
</div>
<!-- 历史数据弹框-->
<HistoryDataDialog
ref="historyDialogRef"
@confirm="handleHistoryConfirm"
@cancel="handleHistoryCancel"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Close, ArrowLeft, ArrowRight, Timer, Refresh } from '@element-plus/icons-vue'
import TemperatureChart from './common/TemperatureChart.vue'
import RealtimeChart from './common/RealtimeChart.vue';
import DeviceStatus from './common/DeviceStatus.vue';
import DeviceSetting from './common/DeviceSetting.vue';
import DataTable from './common/DataTable.vue';
import DualChannelChart from './common/DualChannelChart.vue';
import CurveChart from './common/CurveChart.vue';
import CircleDetectionChart from "./common/CircleDetectionChart.vue";
import HistoryDataDialog from './common/HistoryDataDialog.vue';
import {ElMessageBox} from "element-plus";
const { proxy } = getCurrentInstance();
const toleranceList = ['15%', '20%', '25%']
const showRed = ref(true)
const showGreen = ref(false)
const showRedGreen = ref(false)
const showImagePageRed = ref(true)
const showImagePageGreen = ref(false)
const showImagePageRedGreen = ref(false)
const colorRed = ref('#ff4444')
const colorGreen = ref('#67c23a')
const currentIndex = ref(0)
const showHistory = ref(false)
const mode = ref('normal')
const realtimeChartRef = ref(null)
const dualChart1Ref = ref(null)
const dualChart2Ref = ref(null)
const dualChart1PageRef = ref(null)
const dualChart2PageRef = ref(null)
const bowlCurvePageRef = ref(null)
const xRayStatus = ref(false)
const activeName = ref('first')
const activeTwoName = ref('first')
const activeThreeName = ref('first')
const bowlCurveRef = ref(null)
const showBtn = ref(false)
const historyDialogRef = ref(null)
const currentTime = ref('2021-02-11 13:23:39')
const chartTitle = ref({
left: '扫描臂1 曲线',
right: '扫描臂2 曲线'
})
const cardList = [
{ title: '外屏层偏心度' },
{ title: '内屏层偏心度' },
{ title: '绝缘层偏心度' },
{ title: '外屏层厚度' },
{ title: '内屏层厚度' },
{ title: '绝缘层厚度' }
]
const warningList = ref([
{ title: '设备1离线', status: 'offline' },
{ title: '设备2离线', status: 'offline' },
{ title: '设备3离线', status: 'offline' }
])
//
const innerLayerData = ref([0.68, 0.85, 1.07, 1.18, 1.13, 0.97, 0.67, 0.98])
const middleLayerData = ref([9.52, 9.95, 10.53, 11.04, 11.34, 11.15, 9.66, 10.40])
const outerLayerData = ref([1.15, 1.19, 1.21, 1.17, 1.05, 0.96, 1.06, 0.79])
//
const temperatureData = ref({
arm1: [33, 46, 57, 24, 48, 51, 33],
arm2: [11, 24, 39, 13, 48, 41, 33],
times: [
'2023-01-01 09:00:00',
'2023-01-01 10:00:00',
'2023-01-01 11:00:00',
'2023-01-01 12:00:00',
'2023-01-01 13:00:00',
'2023-01-01 14:00:00'
]
})
const deviceStatus = ref({
arm1: {
communication: '正常',
fault: '正常',
enable: '未使用',
position: '下过渡段',
speed: '0',
powerSwitch: { value: '关闭', type: 'danger' },
safetyLock: '打开',
voltageSet: '55000',
voltageFeedback: '15',
currentFeedback: '2',
currentSet: '1800'
},
arm2: {
communication: '正常',
fault: '正常',
enable: '未使用',
position: '下过渡段',
speed: '0',
powerSwitch: { value: '关闭', type: 'danger' },
safetyLock: '打开',
voltageSet: '55000',
voltageFeedback: '0',
currentFeedback: '2',
currentSet: '1800'
}
})
//
const tableData = ref([
{ name: '射线电源反馈电压[V]', arm1: '15', arm2: '0' },
{ name: '射线电源设定电压[V]', arm1: '55000', arm2: '55000' },
// ...
])
//
const arm1ChartData = ref({
a: Array.from({ length: 100 }, () => Math.random() * 9000),
b: Array.from({ length: 100 }, () => Math.random() * 9000)
})
const arm2ChartData = ref({
a: Array.from({ length: 100 }, () => Math.random() * 9000),
b: Array.from({ length: 100 }, () => Math.random() * 9000)
})
//
const handleImagePageNext = () => {
if (showImagePageRedGreen.value) {
showImagePageRedGreen.value = false
showImagePageRed.value = true
showImagePageGreen.value = false
} else if (showImagePageRed.value) {
showImagePageRedGreen.value = false
showImagePageRed.value = false
showImagePageGreen.value = true
} else if (showImagePageGreen.value) {
showImagePageRedGreen.value = true
showImagePageRed.value = false
showImagePageGreen.value = false
}
}
const handleImagePagePrev = () => {
if (showImagePageRedGreen.value) {
showImagePageRedGreen.value = false
showImagePageRed.value = false
showImagePageGreen.value = true
} else if (showImagePageGreen.value) {
showImagePageRedGreen.value = false
showImagePageRed.value = true
showImagePageGreen.value = false
} else if (showImagePageRed.value) {
showImagePageRedGreen.value = true
showImagePageRed.value = false
showImagePageGreen.value = false
}
}
const handleRefresh = () => {
//
if (showImagePageRedGreen.value) {
dualChart1PageRef.value?.refresh()
dualChart2PageRef.value?.refresh()
} else {
bowlCurvePageRef.value?.refresh()
}
}
//
const showHistoryDialog = () => {
historyDialogRef.value.show()
}
const handleHistoryConfirm = (data) => {
console.log('选择的历史数据:', data)
}
const handleHistoryCancel = () => {
console.log('取消选择')
}
//
const handleImageNext = () => {
if (showRedGreen.value) {
showRedGreen.value = false
showRed.value = true
showGreen.value = false
} else if (showRed.value) {
showRedGreen.value = false
showRed.value = false
showGreen.value = true
} else if (showGreen.value) {
showRedGreen.value = true
showRed.value = false
showGreen.value = false
}
}
const handleImagePrev = () => {
if (showRedGreen.value) {
showRedGreen.value = false
showRed.value = false
showGreen.value = true
} else if (showGreen.value) {
showRedGreen.value = false
showRed.value = true
showGreen.value = false
} else if (showRed.value) {
showRedGreen.value = true
showRed.value = false
showGreen.value = false
}
}
const generateData = (type) => {
const data = []
for (let i = 0; i <= 90; i++) {
let value
if (i < 45) {
value = type === 'a' ? 8500 : 7500
} else if (i >= 45 && i < 60) {
value = 8500 - ((i - 45) * 500)
} else if (i >= 60 && i < 75) {
value = 1000 + ((i - 60) * 500)
} else {
value = type === 'a' ? 8500 : 7500
}
data.push([i, value])
}
arm1ChartData.value = data
}
//
const handleClick = (tab, event) => {
console.log(tab.props.label,'----')
nextTick(() => {
if(tab.props.label === '趋势图') {
realtimeChartRef.value?.resize()
}
if(tab.props.label === '图像预览') {
bowlCurveRef.value?.resize()
}
})
}
const handlePrevious = () => {
if (currentIndex.value > 0) {
currentIndex.value--
}
}
const handleNext = () => {
if (currentIndex.value < toleranceList.length - 1) {
currentIndex.value++
}
}
//
const paramsHandle = () => {
showBtn.value = !showBtn.value
}
//
const handleSettingUpdate = ({ key, value }) => {
console.log('更新设置:', key, value)
//
}
//
const handleDataUpdate = (data) => {
console.log('数据更新:', data)
}
//
const handleShowImage = () => {
// router.push({ path: '/image-page' })
// ElMessage.success('')
}
//
const handleModeChange = (newMode) => {
mode.value = newMode
}
//x线
const handleXRayToggle = () => {
ElMessageBox.confirm(
'是否确认切换X射线状态',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
// X线
xRayStatus.value = !xRayStatus.value
}).catch(() => {
//
})
}
//
const handleGenerateReport = () => {
}
//
const handleDeviceShare = () => {
}
onMounted(() => {
generateData()
})
</script>
<style scoped lang="scss">
.equipment-container {
padding: 20px;
h3 {
margin: 0 0 20px;
font-size: 16px;
font-weight: 500;
}
.data-overview {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
.data-left {
display: flex;
align-items: center;
}
}
.data-card {
flex: 1;
display: flex;
padding: 20px;
background: #fff;
border-radius: 4px;
align-items: center;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
.img-icon {
border:1px dashed #ccc;
margin-right: 10px;
}
.number {
font-size: 28px;
font-weight: bold;
margin-bottom: 10px;
color:#333333;
}
.label {
color: #999;
font-size: 14px;
}
}
.chart-container {
display: flex;
gap: 20px;
.chart-box {
position: relative;
padding: 14px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
.btn {
position: absolute;
top:14px;
right: 14px;
}
}
.chart-box-left {
width:60%;
}
.chart-box-right {
flex: 1;
}
}
}
.action-buttons {
padding: 10px 0;
.button-group {
display: flex;
gap: 10px;
.el-button {
display: flex;
align-items: center;
gap: 5px;
&.el-button--danger {
background-color: #f56c6c;
border-color: #f56c6c;
color: #fff;
&:hover {
background-color: #f78989;
border-color: #f78989;
}
}
}
}
}
.demo-tabs > .el-tabs__content {
padding: 32px;
color: #6b778c;
font-size: 32px;
font-weight: 600;
}
.eccentricity-card {
background-color: #F5F7FA;
border-radius: 4px;
padding: 14px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 10px;
border-bottom:1px solid #ccc;
margin-bottom: 10px;
.title {
font-size: 14px;
color: #333;
font-weight: 600;
}
.close-icon {
cursor: pointer;
color: #999;
}
}
.card-content {
.card-text {
display: flex;
justify-content: space-between;
}
.difference {
margin-bottom: 6px;
font-size: 14px;
.plus {
color: #f56c6c;
margin-right: 5px;
}
.label {
color: #f56c6c;
}
}
.value {
font-size: 32px;
font-weight: bold;
color: red;
margin-bottom: 15px;
.unit {
font-size: 16px;
margin-left: 5px;
}
}
.tolerance-section {
display: flex;
align-items: center;
justify-content: space-between;
background: #E5ECF6 ;
padding: 8px;
border-radius: 4px;
.tolerance-text {
color: #666;
font-size: 14px;
}
}
}
}
.card-container {
.card-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
border-radius: 4px;
}
}
.chart-bottom-left {
width:38%;
}
.chart-bottom-right {
flex:1
}
.warning-container {
padding-left:10px;
.warning-item {
color:red;
margin-bottom: 10px;
}
}
.device-status {
background: #fff;
border-radius: 4px;
height: 346px;
overflow-y: scroll;
:deep(.el-table) {
.el-tag {
width: 100%;
justify-content: center;
&.el-tag--danger {
background-color: #fef0f0;
}
}
.cell {
padding: 8px 0;
}
}
}
.header {
//display: flex;
//justify-content: space-between;
//align-items: center;
//margin-bottom: 14px;
.time {
font-size: 14px;
color: #606266;
margin-left: 6px;
}
}
.imagePage-container {
display: flex;
.imagePage-left {
width: 52%;
}
}
.charts {
//display: flex;
//gap: 20px;
padding: 0 6px;
flex:1;
//
//> div {
// flex: 1;
//}
}
.controls {
display: flex;
justify-content: flex-end;
.switch {
margin-left: 5px;
}
}
.switch-btn {
padding: 5px;
//&:hover {
// background: #f0f9eb;
//}
}
.detection-section {
padding: 20px;
background: #fff;
border-radius: 4px;
}
.history-content {
display: flex;
justify-content: space-between;
align-items: center;
.history-right {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.scanItem {
color:#666;
font-size: 12px;
margin-bottom: 6px;
}
}
</style>

View File

@ -0,0 +1,535 @@
<template>
<div class="contentBox">
<el-card class="card" shadow="hover">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
<el-form-item :label="proxy.$t('maintenance.deviceCode')" prop="deviceCode">
<el-input v-model="queryParams.deviceCode" :placeholder="proxy.$t('maintenance.deviceCode')" clearable />
</el-form-item>
<el-form-item :label="proxy.$t('common.customerAffiliation')" prop="customerId" v-if="customerList.length>1">
<el-select v-model="queryParams.customerId" :placeholder="proxy.$t('common.select')" clearable style="width:200px" >
<el-option
v-for="item in customerList"
:key="item.deptId"
:label="item.deptName"
:value="item.deptId"
/>
</el-select>
</el-form-item>
<el-form-item :label="proxy.$t('common.userAffiliation')" prop="userId" v-if="belongUserList.length>1">
<el-select v-model="queryParams.userId" :placeholder="proxy.$t('common.select')" clearable style="width:200px" >
<el-option
v-for="item in belongUserList"
:key="item.userId"
:label="item.nickName"
:value="item.userId"
/>
</el-select>
</el-form-item>
<el-form-item :label="proxy.$t('equipment.deviceStatus')" prop="deviceStatus">
<el-select v-model="queryParams.deviceStatus" :placeholder="proxy.$t('common.select')" clearable style="width:200px" >
<el-option
v-for="item in statusList"
:key="item.dictValue"
:label="item.dictLabel"
:value="item.dictValue"
/>
</el-select>
</el-form-item>
<el-form-item :label="proxy.$t('equipment.serviceStatus')" prop="serviceStatus">
<el-select v-model="queryParams.serviceStatus" :placeholder="proxy.$t('common.select')" clearable style="width:200px" >
<el-option
v-for="item in statusList"
:key="item.dictValue"
:label="item.dictLabel"
:value="item.dictValue"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">{{proxy.$t('button.search')}}</el-button>
<el-button icon="Refresh" @click="resetQuery">{{proxy.$t('button.reset')}}</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
>{{proxy.$t('button.add')}}</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="primary"
plain
@click="exportDevice"
>{{proxy.$t('button.export')}}</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" style="width: 100%" :data="deviceList" border>
<el-table-column fixed :label="proxy.$t('common.sort')" type="index" width="60" align="center" />
<el-table-column :label="proxy.$t('equipment.deviceName')" align="center" prop="deviceName" width="120"/>
<el-table-column :label="proxy.$t('equipment.deviceModel')" align="center" prop="deviceModel" width="120" />
<el-table-column :label="proxy.$t('equipment.deviceCode')" align="center" prop="deviceCode" width="120"/>
<el-table-column :label="proxy.$t('common.customerAffiliation')" align="center" prop="customerName" width="140"/>
<el-table-column :label="proxy.$t('common.userAffiliation')" align="center" prop="userRelList">
<template #default="scope">
<el-button type="primary" link @click="viewCustomer(scope.row)">{{proxy.$t('button.check')}}</el-button>
</template>
</el-table-column>
<el-table-column :label="proxy.$t('equipment.lifespan')" width="140" align="center" prop="lifespan" />
<el-table-column :label="proxy.$t('equipment.usedTime')" width="140" align="center" prop="usedTime" />
<el-table-column :label="proxy.$t('equipment.deviceStatus')" align="center" prop="deviceStatus" width="100">
<template #default="scope">
{{statusFormat(scope.row.deviceStatus)}}
</template>
</el-table-column>
<el-table-column :label="proxy.$t('equipment.serviceStatus')" align="center" prop="serviceStatus" width="100">
<template #default="scope">
{{statusFormat(scope.row.serviceStatus)}}
</template>
</el-table-column>
<el-table-column :label="proxy.$t('customer.createBy')" align="center" prop="createBy" width="160" />
<el-table-column :label="proxy.$t('customer.createTime')" align="center" prop="createTime" width="200" />
<el-table-column :label="proxy.$t('common.action')" align="center" width="380" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="handleUpdate(scope.row,proxy.$t('button.edit'))"
>{{proxy.$t('button.edit')}}</el-button>
<el-button
link
type="primary"
@click="handleDelete(scope.row)"
>{{proxy.$t('button.delete')}}</el-button>
<el-button
link
type="primary"
@click="handleGo(scope.row)"
>{{ proxy.$t('button.viewDetails') }}</el-button>
<!-- <el-dropdown @command="handleCommand">-->
<!-- <el-button type="primary" link>-->
<!-- 更多<el-icon class="el-icon&#45;&#45;right"><CaretBottom /></el-icon>-->
<!-- </el-button>-->
<!-- <template #dropdown>-->
<!-- <el-dropdown-menu>-->
<!-- <el-dropdown-item :command="{ type: 'stop', row }"-->
<!-- :disabled="row.status === 0">停止服务</el-dropdown-item>-->
<!-- <el-dropdown-item :command="{ type: 'restart', row }"-->
<!-- :disabled="row.status === 0">重启服务</el-dropdown-item>-->
<!-- <el-dropdown-item :command="{ type: 'start', row }"-->
<!-- :disabled="row.status === 1">启动服务</el-dropdown-item>-->
<!-- </el-dropdown-menu>-->
<!-- </template>-->
<!-- </el-dropdown>-->
<!-- <el-dropdown placement="bottom">-->
<!-- <el-button-->
<!-- link-->
<!-- type="primary"-->
<!-- >更多</el-button>-->
<!-- <template #dropdown>-->
<!-- <el-dropdown-menu>-->
<!-- <el-dropdown-item>停止服务</el-dropdown-item>-->
<!-- <el-dropdown-item>重启服务</el-dropdown-item>-->
<!-- <el-dropdown-item>启动服务</el-dropdown-item>-->
<!-- </el-dropdown-menu>-->
<!-- </template>-->
<!-- </el-dropdown>-->
<!-- <el-button-->
<!-- link-->
<!-- type="primary"-->
<!-- @click="handleShare(scope.row)"-->
<!-- >分享</el-button>-->
<el-button
link
type="primary"
@click="handleEmqx(scope.row)"
>{{proxy.$t('equipment.viewAuth')}}</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-if="total > 0"
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 30, 50]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<DeviceDialog
v-model="dialog.visible"
:title="dialog.title"
:customer-list="customerList"
:user-list="belongUserList"
:form-data="dialog.form"
@success="handleSuccess"
/>
<!--查看所属用户列表-->
<el-dialog
:title="proxy.$t('common.userAffiliation')"
v-model="visible"
width="800px"
append-to-body
>
<el-table :data="userList" border>
<el-table-column :label="proxy.$t('common.sort')" type="index" width="80" align="center" />
<el-table-column :label="proxy.$t('equipment.nickName')" prop="nickName" align="center" />
<el-table-column :label="proxy.$t('equipment.userName')" prop="userName" align="center" />
<el-table-column :label="proxy.$t('position.department')" prop="deptName" align="center" />
<el-table-column :label="proxy.$t('equipment.userStatus')" align="center" prop="status">
<template #default="scope">
{{Number(scope.row.status) === 0 ? proxy.$t('common.normal') :proxy.$t('common.deactivate')}}
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false">{{ proxy.$t('button.cancel') }}</el-button>
</div>
</template>
</el-dialog>
<!--查看鉴权-->
<EmqDialog ref="emqDialogRef" :form-data="emqFormData" ></EmqDialog>
<!--分享-->
<ShareDialog ref="shareDialogRef" :form-data="shareDialogRef"></ShareDialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter,useRoute } from 'vue-router'
import {
getDeviceListApi,
deleteDeviceApi,
getDeviceSelectApi,
getUserSelectApi,
getDeviceDetailApi,
getAuthInfoApi
} from '@/api/equipment'
import DeviceDialog from './common/DeviceDialog.vue'
import shareDialog from './common/shareDialog.vue'
import { CaretBottom } from '@element-plus/icons-vue'
import {ElMessage, ElMessageBox} from "element-plus";
import {getDicts} from "@/api/system/dict/data.js";
import { useI18n } from 'vue-i18n';
import {deleteDepartmentPostApi, getCustomerListApi} from "@/api/customer/index.js";
import EmqDialog from './common/EmqDialog.vue'
const emqDialogRef = ref()
const emqFormData = ref({})
const router = useRouter()
const { proxy } = getCurrentInstance();
const queryFormRef = ref()
const loading = ref(false)
const showSearch = ref(true)
const total = ref(0)
const deviceList = ref([])
const { t } = useI18n();
const visible = ref(false)
const belongUserList = ref([])
const userList = ref([])
const statusList = ref([])
const shareDialogRef = ref()
const currentDeviceId = ref('')
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
deviceCode: '',
userId:'',
customerId:'',
deviceStatus:'',
serviceStatus:''
})
const route = useRoute()
//
const dialog = reactive({
title: proxy.$t('button.add'),
visible: false,
form: {
deviceName: '',
deviceCode: '',
deviceModel: '',
customerId: '',
userIdList: [],
id: '',
}
})
const emqVisible = ref(false)
const queryParamsCustomer = reactive({
pageNum: 1,
pageSize: 100000000,
deptName: undefined,
parentId: 100,
})
const customerList = ref([])
const statusFormat = (status) => {
if(status === undefined || status === null) {
return ''
}
return status === 1 ? proxy.$t('common.online') : proxy.$t('common.offline')
}
//
const getStatus = async() => {
const res = await getDicts('tg_device_status')
if(res.code === 200) {
statusList.value = res.data || []
} else {
ElMessage.error(res.msg)
}
}
// const handleCommand = async ({ type, row }) => {
// const actionMap = {
// stop: {
// title: '',
// // api: stopDeviceApi,
// successMsg: ''
// },
// restart: {
// title: '',
// // api: restartDeviceApi,
// successMsg: ''
// },
// start: {
// title: '',
// // api: startDeviceApi,
// successMsg: ''
// }
// }
//
// const action = actionMap[type]
//
// try {
// await ElMessageBox.confirm(
// `${action.title}?`,
// '',
// {
// confirmButtonText: '',
// cancelButtonText: '',
// type: 'warning'
// }
// )
//
// const res = await action.api(row.id)
// if (res.code === 200) {
// ElMessage.success(action.successMsg)
// getList() //
// } else {
// ElMessage.error(res.msg)
// }
// } catch {
// //
// }
// }
//
const exportDevice = async() => {
proxy?.download(
'/device/info/export',
{
...queryParams
},
`equipment_${new Date().getTime()}.xlsx`
);
}
//
const getCustomerList = async() => {
const res = await getCustomerListApi(queryParamsCustomer);
customerList.value = res.rows || []
}
const getList = async() => {
loading.value = true
// API
const res = await getDeviceListApi(queryParams);
if(res.code === 200) {
deviceList.value = res.rows || []
total.value = res.total
}
loading.value = false
}
const handleQuery = () => {
queryParams.pageNum = 1
getList()
}
const resetQuery = () => {
queryParams.deviceCode = ''
queryParams.customerId = ''
queryParams.userId = ''
queryParams.deviceStatus = ''
queryParams.serviceStatus = ''
queryFormRef.value?.resetFields()
handleQuery()
}
//
const viewCustomer = async(row) => {
visible.value = true
if(row.userRelList?.length > 0) {
userList.value = row.userRelList
} else {
userList.value = []
}
}
//
const handleAdd = () => {
dialog.title = proxy.$t('button.add')
dialog.visible = true
}
const handleShare = (row) => {
currentDeviceId.value = row.id
shareDialogRef.value.visible = true
}
//
const handleDelete = (row) => {
ElMessageBox.confirm(
proxy.$t('common.deleteItem'),
proxy.$t('common.warning'),
{
confirmButtonText: proxy.$t('button.save'),
cancelButtonText: proxy.$t('button.cancel'),
type: "warning"
}
).then(async () => {
// API
const res = await deleteDeviceApi(row.id)
if (res.code === 200) {
ElMessage.success('删除成功')
}
getList()
}).catch(() => {})
}
//
const handleSuccess = () => {
getList()
}
//
const handleSizeChange = (val) => {
queryParams.pageSize = val
getList()
}
const handleCurrentChange = (val) => {
queryParams.pageNum = val
getList()
}
//
const handleGo = (row) => {
//
router.push({ path: '/equipment/equipmentDetails', query: { id: row.id } });
}
//
const handleEmqx = async (row) => {
const res = await getAuthInfoApi(row.deviceCode)
emqFormData.value = res.data
emqDialogRef.value.visible = true
}
//
const handleUpdate = async(row) => {
const res = await getDeviceDetailApi(row.id)
console.log(res.data,'===')
if(res.code === 200) {
dialog.title = proxy.$t('button.edit')
dialog.visible = true
dialog.form.deviceName = res.data.deviceName
dialog.form.deviceCode = res.data.deviceCode
dialog.form.deviceModel = res.data.deviceModel
dialog.form.customerId = res.data.customerId
dialog.form.userIdList = res.data.userIdList
dialog.form.id = res.data.id
} else {
ElMessage.error(res.msg)
}
}
//
const getUserSelect = async() => {
const res = await getUserSelectApi()
if(res.code === 200) {
belongUserList.value = res.data || []
} else {
ElMessage.error(res.msg)
}
}
//
// const getDeviceSelect = async() => {
// const res = await getDeviceSelectApi()
// if(res.code === 200) {
// deviceDropList.value = res.data || []
// } else {
// ElMessage.error(res.msg)
// }
// }
watch(
() => route.query,
(newQuery) => {
if (newQuery.id) {
queryParams.customerId = newQuery.id
// ID
if (newQuery.id) {
queryParams.customerId = Number(newQuery.id) //
}
handleQuery()
}
},
{ immediate: true }
)
onMounted(() => {
// 使
if (route.query.id) {
queryParams.customerId = route.query.id
}
if (route.query.id) {
queryParams.customerId = Number(route.query.id)
}
getList()
getCustomerList()
getUserSelect()
getStatus()
})
</script>
<style scoped lang="scss">
.contentBox {
height:calc(100vh - 130px);
margin:20px;
.card {
width:100%;
height: 100%;
overflow-y: scroll;
}
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
.mb8 {
margin-bottom: 8px;
}
.dialog-footer {
text-align: center;
padding-top: 20px;
}
:deep(.el-tag) {
border-radius: 2px;
}
:deep(.reset-style) {
padding: 0;
height: auto;
font-size: inherit;
}
:deep(.el-dropdown-menu__item) {
padding: 5px 20px;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,355 @@
<template>
<div class="contentBox">
<el-card class="card" shadow="hover">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
<el-form-item :label="proxy.$t('equipment.deviceModel')" prop="deviceModel">
<el-input v-model="queryParams.deviceModel" :placeholder="proxy.$t('equipment.inputDeviceModel')" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">{{proxy.$t('button.search')}}</el-button>
<el-button icon="Refresh" @click="resetQuery">{{proxy.$t('button.reset')}}</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
>{{proxy.$t('button.add')}}</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="maintenanceList" border>
<el-table-column :label="proxy.$t('common.sort')" type="index" width="60" align="center" />
<el-table-column :label="proxy.$t('equipment.deviceModel')" align="center" prop="deviceModel" />
<el-table-column :label="proxy.$t('equipment.deviceImage')" align="center" prop="devicePic">
<template #default="scope">
<image-preview :height="50" :src="scope.row.devicePic" :width="50" />
</template>
</el-table-column>
<el-table-column :label="proxy.$t('customer.createBy')" align="center" prop="createBy" />
<el-table-column :label="proxy.$t('customer.createTime')" align="center" prop="createTime" width="160" />
<el-table-column :label="proxy.$t('common.action')" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
link
type="primary"
icon="Edit"
@click="handleUpdate(scope.row)"
>{{proxy.$t('button.edit')}}</el-button>
<el-button
link
type="primary"
icon="delete"
@click="handleDelete(scope.row)"
>{{proxy.$t('button.delete')}}</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-if="total > 0"
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 30, 50]"
layout="total, sizes, prev, pager, next, jumper"
@pagination="getList"
/>
</div>
</el-card>
<el-dialog
:title="title"
v-model="visible"
width="500px"
append-to-body
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
:label-width="appStore.language === 'zh_CN' ? '80px' : '120px'"
label-position="right"
>
<el-form-item :label="proxy.$t('equipment.deviceModel')" prop="deviceModel">
<el-input v-model="form.deviceModel" :placeholder="proxy.$t('common.select')" style="width: 300px" />
</el-form-item>
<el-form-item :label="proxy.$t('equipment.deviceImage')" prop="devicePic">
<el-upload
class="upload-demo"
:action="upload.url"
:headers="upload.headers"
:before-upload="beforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:limit="1"
accept=".jpg,.png"
:show-file-list="false"
>
<template #default>
<div v-if="!form.devicePic" class="upload-box">
<el-icon><Plus /></el-icon>
<div>{{proxy.$t('equipment.uploadImage')}}</div>
</div>
<div v-else class="upload-file">
<el-image
:src="form.devicePic"
class="upload-image"
fit="cover"
:preview-src-list="[form.devicePic]"
:initial-index="0"
preview-teleported
/>
<span class="delete-icon" @click.stop="handleRemove">
<el-icon><Close /></el-icon>
</span>
</div>
</template>
<template #tip>
<div class="el-upload__tip">
{{proxy.$t('equipment.limitImage')}}
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">{{proxy.$t('button.save')}}</el-button>
<el-button @click="cancel">{{proxy.$t('button.cancel')}}</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import {getDeviceModelListApi, deleteDeviceModelApi, getDeviceModelDetailApi, editDeviceModelApi, addDeviceModelApi} from '@/api/equipment'
import {ElMessage, ElMessageBox} from "element-plus";
import { Plus, Close } from '@element-plus/icons-vue'
import { useI18n } from 'vue-i18n';
import { globalHeaders } from '@/utils/request';
import {deleteDepartmentApi} from "@/api/customer/index.js";
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
const { proxy } = getCurrentInstance();
const loading = ref(false)
const showSearch = ref(true)
const total = ref(0)
const maintenanceList = ref([])
const { t } = useI18n();
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
deviceModel:''
})
//
const visible = ref(false)
const title = ref('')
const formRef = ref()
const form = reactive({
deviceModel: '',
devicePic: '',
})
const upload = reactive({
//
headers: globalHeaders(),
//
url: import.meta.env.VITE_APP_BASE_API + '/file/upload'
});
const rules = {
deviceModel: [
{ required: true, message: proxy.$t('equipment.deviceModel'), trigger: 'blur' }
],
devicePic: [
{ required: true, message: proxy.$t('equipment.inputDeviceImage'), trigger: 'change' }
]
}
//
const getList = async() => {
loading.value = true
// API
const res = await getDeviceModelListApi(queryParams);
if(res.code === 200) {
maintenanceList.value = res.rows || []
total.value = res.total
} else {
ElMessage.error(res.msg)
}
loading.value = false
}
const handleQuery = () => {
queryParams.pageNum = 1
getList()
}
const resetQuery = () => {
queryParams.deviceModel = ''
handleQuery()
}
const handleDelete = (row) => {
ElMessageBox.confirm(
proxy.$t('common.deleteItem'),
proxy.$t('common.warning'),
{
confirmButtonText: proxy.$t('button.save'),
cancelButtonText: proxy.$t('button.cancel'),
type: "warning"
}
).then(async () => {
const res = await deleteDeviceModelApi(row.id)
if (res.code === 200) {
ElMessage.success(res.msg)
} else {
ElMessage.error(res.msg)
}
getList()
}).catch(() => {})
}
//
const handleAdd = () => {
visible.value = true
title.value = proxy.$t('button.add')
form.devicePic = ''
form.deviceModel = ''
formRef.value?.resetFields()
}
const beforeUpload = (file) => {
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png'
const isLt50K = file.size / 1024 < 50
console.log(file,'0000')
if (!isJPG) {
ElMessage.error('只能上传 JPG/PNG 格式的图片!')
return false
}
if (!isLt50K) {
ElMessage.error('图片大小不能超过 50KB!')
return false
}
return true
}
const handleUploadSuccess = (response, file) => {
form.devicePic = response.data.url
ElMessage.success('上传成功')
}
const handleUploadError = () => {
ElMessage.error('上传失败')
}
const handleRemove = (file) => {
form.devicePic = ''
}
const submitForm = async () => {
try {
await formRef.value.validate()
//
const res = title.value === proxy.$t('button.add') ? await addDeviceModelApi(form): await editDeviceModelApi(form)
if(res.code === 200) {
ElMessage.success(res.msg)
} else {
ElMessage.error(res.msg)
}
getList()
cancel()
} catch (error) {
console.error(proxy.$t('common.formValidateError'), error)
}
}
const cancel = () => {
visible.value = false
form.devicePic = ''
form.deviceModel = ''
}
//
const handleUpdate = async(row) => {
console.log(row,'---')
const res = await getDeviceModelDetailApi(row.id)
if(res.code === 200) {
form.deviceModel = res.data.deviceModel
form.devicePic = res.data.devicePic
form.id = res.data.id
title.value = proxy.$t('button.edit')
visible.value = true
} else {
ElMessage.error(res.msg)
}
}
onMounted(() => {
getList()
})
</script>
<style scoped lang="scss">
.contentBox {
height:calc(100vh - 130px);
margin:20px;
.card {
width:100%;
height: 100%;
overflow-y: scroll;
}
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
.mb8 {
margin-bottom: 8px;
}
.upload-box, .upload-file {
width:200px;
height: 200px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.upload-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.upload-image {
width: 100%;
height: 100%;
}
.delete-icon {
position: absolute;
top: 8px;
right: 8px;
padding: 4px;
color: #fff;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
cursor: pointer;
z-index: 1;
}
.delete-icon:hover {
background-color: rgba(0, 0, 0, 0.7);
}
:deep(.el-form-item__label) {
justify-content: flex-end;
}
</style>

View File

@ -1,414 +1,191 @@
<template>
<div class="app-container home">
<el-row :gutter="20">
<el-col :sm="24" :lg="12" style="padding-left: 20px">
<h2>测偏移管理框架</h2>
<p>
<b>当前版本:</b> <span>v{{ version }}</span>
</p>
<p>
<el-button
type="primary"
icon="Cloudy"
plain
@click="goTarget('https://gitee.com/y_project/RuoYi-Vue')"
>访问码云</el-button
>
<el-button
icon="HomeFilled"
plain
@click="goTarget('http://ruoyi.vip')"
>访问主页</el-button
>
</p>
</el-col>
<div class="dashboard">
<div class="data-overview">
<h3>{{proxy.$t('dashboard.dataOverview')}}</h3>
<div class="card-list">
<div class="data-card">
<div class="img-icon">
<img src="../assets/dashboard/customerNum.png" alt=""/>
</div>
<div class="data-right">
<div class="number">{{ statistics.customerCount || 0 }}</div>
<div class="label">{{proxy.$t('dashboard.customerCount')}}</div>
</div>
</div>
<div class="data-card">
<div class="img-icon">
<img src="../assets/dashboard/deviceNum.png" alt=""/>
</div>
<div class="data-right">
<div class="number">{{ statistics.deviceCount|| 0 }}</div>
<div class="label">{{proxy.$t('dashboard.customerCount')}}</div>
</div>
</div>
<div class="data-card">
<div class="img-icon">
<img src="../assets/dashboard/maintenanceNum.png" alt=""/>
</div>
<div class="data-right">
<div class="number" >{{ statistics.maintainCount || 0 }}</div>
<div class="label">{{proxy.$t('dashboard.maintenanceCount')}}</div>
</div>
</div>
<div class="data-card">
<div class="img-icon">
<img src="../assets/dashboard/onlineDevice.png" alt=""/>
</div>
<div class="data-right">
<div class="number" >{{ statistics.onlineCount || 0 }}</div>
<div class="label">{{proxy.$t('dashboard.onlineDeviceCount')}}</div>
</div>
</div>
<div class="data-card">
<div class="img-icon">
<img src="../assets/dashboard/offlineDevice.png" alt=""/>
</div>
<div class="data-right">
<div class="number" >{{ statistics.offlineCount || 0 }}</div>
<div class="label">{{proxy.$t('dashboard.offlineDeviceCount')}}</div>
</div>
</div>
</div>
</div>
<el-col :sm="24" :lg="12" style="padding-left: 50px">
<el-row>
<el-col :span="12">
<h2>技术选型</h2>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<h4>后端技术</h4>
<ul>
<li>SpringBoot</li>
<li>Spring Security</li>
<li>JWT</li>
<li>MyBatis</li>
<li>Druid</li>
<li>Fastjson</li>
<li>...</li>
</ul>
</el-col>
<el-col :span="6">
<h4>前端技术</h4>
<ul>
<li>Vue</li>
<li>Vuex</li>
<li>Element-ui</li>
<li>Axios</li>
<li>Sass</li>
<li>Quill</li>
<li>...</li>
</ul>
</el-col>
</el-row>
</el-col>
</el-row>
<el-divider />
<el-row :gutter="20">
<el-col :xs="24" :sm="24" :md="12" :lg="8">
<el-card class="update-log">
<template v-slot:header>
<div class="clearfix">
<span>联系信息</span>
</div>
</template>
<div class="body">
<p>
<i class="el-icon-s-promotion"></i> 官网<el-link
href="http://www.ruoyi.vip"
target="_blank"
>http://www.ruoyi.vip</el-link
>
</p>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="12" :lg="8">
<el-card class="update-log">
<template v-slot:header>
<div class="clearfix">
<span>更新日志</span>
</div>
</template>
<el-collapse accordion>
<el-collapse-item title="v3.8.9 - 2024-12-30">
<ol>
<li>用户管理支持分栏拖动</li>
<li>修改主题样式本地读取</li>
<li>用户头像http(s)链接支持</li>
<li>用户管理过滤掉已禁用部门</li>
<li>支持自定义显示Excel属性列</li>
<li>操作日志记录DELETE请求参数</li>
<li>白名单支持对通配符路径匹配</li>
<li>校检文件名是否包含特殊字符</li>
<li>代码生成创建表屏蔽违规的字符</li>
<li>菜单面包屑导航支持多层级显示</li>
<li>Excel注解支持wrapText是否允许内容换行</li>
<li>代码生成新增配置是否允许文件覆盖到本地</li>
<li>修复角色禁用权限不失效问题</li>
<li>修复代码生成上级菜单显示问题</li>
<li>修复导出子列表对象只能在最后的问题</li>
<li>修复TopNav无法正确获取active的问题</li>
<li>修复默认关闭Tags-Views内链页面打不开</li>
<li>升级oshi到最新版本6.6.5</li>
<li>升级tomcat到最新版本9.0.96</li>
<li>升级fastjson到最新版2.0.53</li>
<li>升级logback到最新版本1.2.13</li>
<li>升级spring-framework到最新版本5.3.39</li>
<li>升级quill到最新版本2.0.2</li>
<li>升级axios到最新版本0.28.1</li>
<li>优化身份证脱敏正则</li>
<li>优化权限更新后同步缓存</li>
<li>优化查询时间范围日期格式</li>
<li>优化参数键值更换为多行文本</li>
<li>优化导入带标题文件关闭清理</li>
<li>优化上传图片带域名不增加前缀</li>
<li>优化特殊字符密码修改失败问题</li>
<li>优化无用户编号不校验数据权限</li>
<li>优化TopNav内链菜单点击没有高亮</li>
<li>优化菜单管理切换Mini布局错乱问题</li>
<li>其他细节优化</li>
</ol>
</el-collapse-item>
<el-collapse-item title="v3.8.8 - 2024-06-30">
<ol>
<li>菜单管理新增路由名称</li>
<li>新增数据脱敏过滤注解</li>
<li>用户密码新增非法字符验证</li>
<li>限制用户操作数据权限范围</li>
<li>代码生成新增创建表结构功能</li>
<li>定时任务白名单配置范围缩小</li>
<li>优化代码生成主子表关联查询方式</li>
<li>Excel注解新增属性comboReadDict</li>
<li>Excel注解ColumnType类型新增文本</li>
<li>新增国际化资源文件配置</li>
<li>升级oshi到最新版本6.6.1</li>
<li>升级druid到最新版本1.2.23</li>
<li>升级core-js到最新版本3.37.1</li>
<li>更新HttpUtils中的User-Agent</li>
<li>更新compressionPlugin到6.1.2以兼容node18+</li>
<li>升级spring-security到安全版本防止漏洞风险</li>
<li>升级spring-framework到安全版本防止漏洞风险</li>
<li>优化自定义XSS注解匹配方式</li>
<li>优化缓存监控键名列表排序显示</li>
<li>优化定时任务日志默认按时间排序</li>
<li>优化默认文件大小超过2G无效的问题</li>
<li>优化查表特殊字符使用反斜杠进行转义</li>
<li>优化定时任务cron表达式小时配置显示错误问题</li>
<li>优化多个自定数据权限使用in查询,避免多次拼接</li>
<li>优化导入Excel时设置dictType属性重复查缓存问题</li>
<li>其他细节优化</li>
</ol>
</el-collapse-item>
<el-collapse-item title="v3.8.7 - 2023-12-08">
<ol>
<li>操作日志记录部门名称</li>
<li>全局数据存储用户编号</li>
<li>新增编程式判断资源访问权限</li>
<li>操作日志列表新增IP地址查询</li>
<li>定时任务新增页去除状态选项</li>
<li>代码生成支持选择前端模板类型</li>
<li>显隐列组件支持复选框弹出类型</li>
<li>通用排序属性orderBy参数限制长度</li>
<li>Excel自定义数据处理器增加单元格/工作簿对象</li>
<li>升级oshi到最新版本6.4.8</li>
<li>升级druid到最新版本1.2.20</li>
<li>升级fastjson到最新版2.0.43</li>
<li>升级pagehelper到最新版1.4.7</li>
<li>升级commons.io到最新版本2.13.0</li>
<li>升级element-ui到最新版本2.15.14</li>
<li>修复五级路由缓存无效问题</li>
<li>修复外链带端口出现的异常</li>
<li>修复树模板父级编码变量错误</li>
<li>修复字典表详情页面搜索问题</li>
<li>修复内链iframe没有传递参数问题</li>
<li>修复自定义字典样式不生效的问题</li>
<li>修复字典缓存删除方法参数错误问题</li>
<li>修复Excel导入数据临时文件无法删除问题</li>
<li>修复未登录带参数访问成功后参数丢失问题</li>
<li>修复HeaderSearch组件跳转query参数丢失问题</li>
<li>修复代码生成导入后必填项与数据库不匹配问题</li>
<li>修复Excels导入时无法获取到dictType字典值问题</li>
<li>优化下载zip方法新增遮罩层</li>
<li>优化头像上传参数新增文件名称</li>
<li>优化字典标签支持自定义分隔符</li>
<li>优化菜单管理类型为按钮状态可选</li>
<li>优化前端防重复提交数据大小限制</li>
<li>优化TopNav菜单没有图标svg不显示</li>
<li>优化数字金额大写转换精度丢失问题</li>
<li>优化富文本Editor组件检验图片格式</li>
<li>优化页签在Firefox浏览器被遮挡的问题</li>
<li>优化个人中心/基本资料修改时数据显示问题</li>
<li>优化缓存监控图表支持跟随屏幕大小自适应调整</li>
<li>其他细节优化</li>
</ol>
</el-collapse-item>
<el-collapse-item title="v3.8.6 - 2023-06-30">
<ol>
<li>支持登录IP黑名单限制</li>
<li>新增监控页面图标显示</li>
<li>操作日志新增消耗时间属性</li>
<li>屏蔽定时任务bean违规的字符</li>
<li>日志管理使用索引提升查询性能</li>
<li>日志注解支持排除指定的请求参数</li>
<li>支持自定义隐藏属性列过滤子对象</li>
<li>升级oshi到最新版本6.4.3</li>
<li>升级druid到最新版本1.2.16</li>
<li>升级fastjson到最新版2.0.34</li>
<li>升级spring-boot到最新版本2.5.15</li>
<li>升级element-ui到最新版本2.15.13</li>
<li>移除apache/commons-fileupload依赖</li>
<li>修复页面切换时布局错乱的问题</li>
<li>修复匿名注解Anonymous空指针问题</li>
<li>修复路由跳转被阻止时内部产生报错信息问题</li>
<li>修复isMatchedIp的参数判断产生空指针的问题</li>
<li>修复用户多角色数据权限可能出现权限抬升的情况</li>
<li>修复开启TopNav后一级菜单路由参数设置无效问题</li>
<li>修复DictTag组件value没有匹配的值时则展示value</li>
<li>优化文件下载出现的异常</li>
<li>优化选择图标组件高亮回显</li>
<li>优化弹窗后导航栏偏移的问题</li>
<li>优化修改密码日志存储明文问题</li>
<li>优化页签栏关闭其他出现的异常问题</li>
<li>优化页签关闭左侧选项排除首页选项</li>
<li>优化关闭当前tab页跳转最右侧tab页</li>
<li>优化缓存列表清除操作提示不变的问题</li>
<li>优化字符未使用下划线不进行驼峰式处理</li>
<li>优化用户导入更新时需获取用户编号问题</li>
<li>优化侧边栏的平台标题与VUE_APP_TITLE保持同步</li>
<li>优化导出Excel时设置dictType属性重复查缓存问题</li>
<li>连接池Druid支持新的配置connectTimeout和socketTimeout</li>
<li>其他细节优化</li>
</ol>
</el-collapse-item>
<el-collapse-item title="v3.8.5 - 2023-01-01">
<ol>
<li>定时任务违规的字符</li>
<li>重置时取消部门选中</li>
<li>新增返回警告消息提示</li>
<li>忽略不必要的属性数据返回</li>
<li>修改参数键名时移除前缓存配置</li>
<li>导入更新用户数据前校验数据权限</li>
<li>兼容Excel下拉框内容过多无法显示的问题</li>
<li>升级echarts到最新版本5.4.0</li>
<li>升级core-js到最新版本3.25.3</li>
<li>升级oshi到最新版本6.4.0</li>
<li>升级kaptcha到最新版2.3.3</li>
<li>升级druid到最新版本1.2.15</li>
<li>升级fastjson到最新版2.0.20</li>
<li>升级pagehelper到最新版1.4.6</li>
<li>优化弹窗内容过多展示不全问题</li>
<li>优化swagger-ui静态资源使用缓存</li>
<li>开启TopNav没有子菜单隐藏侧边栏</li>
<li>删除fuse无效选项maxPatternLength</li>
<li>优化导出对象的子列表为空会出现[]问题</li>
<li>优化编辑头像时透明部分会变成黑色问题</li>
<li>优化小屏幕上修改头像界面布局错位的问题</li>
<li>修复代码生成勾选属性无效问题</li>
<li>修复文件上传组件格式验证问题</li>
<li>修复回显数据字典数组异常问题</li>
<li>修复sheet超出最大行数异常问题</li>
<li>修复Log注解GET请求记录不到参数问题</li>
<li>修复调度日志点击多次数据不变化的问题</li>
<li>修复主题颜色在Drawer组件不会加载问题</li>
<li>修复文件名包含特殊字符的文件无法下载问题</li>
<li>修复table中更多按钮切换主题色未生效修复问题</li>
<li>修复某些特性的环境生成代码变乱码TXT文件问题</li>
<li>修复代码生成图片/文件/单选时选择必填无法校验问题</li>
<li>修复某些特性的情况用户编辑对话框中角色和部门无法修改问题</li>
<li>其他细节优化</li>
</ol>
</el-collapse-item>
<el-collapse-item title="v3.8.4 - 2022-09-26">
<ol>
<li>数据逻辑删除不进行唯一验证</li>
<li>Excel注解支持导出对象的子列表方法</li>
<li>Excel注解支持自定义隐藏属性列</li>
<li>Excel注解支持backgroundColor属性设置背景色</li>
<li>支持配置密码最大错误次数/锁定时间</li>
<li>登录日志新增解锁账户功能</li>
<li>通用下载方法新增config配置选项</li>
<li>支持多权限字符匹配角色数据权限</li>
<li>页面内嵌iframe切换tab不刷新数据</li>
<li>操作日志记录支持排除敏感属性字段</li>
<li>修复多文件上传报错出现的异常问题</li>
<li>修复图片预览组件src属性为null值控制台报错问题</li>
<li>升级oshi到最新版本6.2.2</li>
<li>升级fastjson到最新版2.0.14</li>
<li>升级pagehelper到最新版1.4.3</li>
<li>升级core-js到最新版本3.25.2</li>
<li>升级element-ui到最新版本2.15.10</li>
<li>优化任务过期不执行调度</li>
<li>优化字典数据使用store存取</li>
<li>优化修改资料头像被覆盖的问题</li>
<li>优化修改用户登录账号重复验证</li>
<li>优化代码生成同步后值NULL问题</li>
<li>优化定时任务支持执行父类方法</li>
<li>优化用户个人信息接口防止修改部门</li>
<li>优化布局设置使用el-drawer抽屉显示</li>
<li>优化没有权限的用户编辑部门缺少数据</li>
<li>优化日志注解记录限制请求地址的长度</li>
<li>优化excel/scale属性导出单元格数值类型</li>
<li>优化日志操作中重置按钮时重复查询的问题</li>
<li>优化多个相同角色数据导致权限SQL重复问题</li>
<li>优化表格上右侧工具条搜索按钮显隐&右侧样式凸出</li>
<li>其他细节优化</li>
</ol>
</el-collapse-item>
<el-collapse-item title="v1.0.0 - 2019-10-08">
<ol>
<li>若依前后端分离系统正式发布</li>
</ol>
</el-collapse-item>
</el-collapse>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="12" :lg="8">
<el-card class="update-log">
<template v-slot:header>
<div class="clearfix">
<span>捐赠支持</span>
</div>
</template>
<div class="body">
<img
src="@/assets/images/pay.png"
alt="donate"
style="width:100%"
/>
<span style="display: inline-block; height: 30px; line-height: 30px"
>你可以请作者喝杯咖啡表示鼓励</span
>
</div>
</el-card>
</el-col>
</el-row>
<div class="chart-container">
<div class="chart-box">
<h3>{{proxy.$t('dashboard.maintenanceAnalysis')}}</h3>
<DashChart :chart-data="maintenanceData" :series-data="seriesData"/>
</div>
<div class="chart-box">
<h3>{{proxy.$t('dashboard.maintenanceSummary')}}</h3>
<DashPie :chart-data="maintenanceSummary" />
</div>
</div>
</div>
</template>
<script setup name="Index">
const version = ref('3.8.9')
<script setup>
import { ref, onMounted } from 'vue'
import DashChart from '../components/DashChart.vue'
import DashPie from '../components/DashPie.vue'
import { getDashboardApi, getMaintainSummaryApi, getMaintainAnalysisApi } from '@/api/dashboard'
const { proxy } = getCurrentInstance();
const statistics = ref({})
const maintenanceData = ref({})
const seriesData = ref([])
const maintenanceSummary = ref({
total: 38,
data: []
})
function goTarget(url) {
window.open(url, '__blank')
const getStatistics = async () => {
try {
const res = await getDashboardApi()
if (res.code === 200) {
statistics.value = res.data
}
} catch (error) {
console.error('获取统计数据失败', error)
}
}
const getMaintainAnalysis = async() => {
try {
const res = await getMaintainAnalysisApi()
if (res.code === 200) {
maintenanceData.value =
res.data.customerList.map(item => ({
name: item
}))
console.log(res,'===')
seriesData.value = res.data.maintenanceTypeList.map(item => ({
name: item.maintenanceTypeName,
data: item.dataList
}))
console.log(seriesData.value,'===')
}
} catch (error) {
console.error('获取维保分析数据失败', error)
}
}
//
const getMaintainSummary = async () => {
try {
const res = await getMaintainSummaryApi()
if (res.code === 200) {
maintenanceSummary.value = {
total: res.data.reduce((sum, item) => sum + item.maintenanceTypeCount, 0),
data: res.data.map(item => ({
name: item.maintenanceTypeName,
value: item.maintenanceTypeCount
}))
}
}
} catch (error) {
console.error('获取维保汇总数据失败', error)
}
}
onMounted(() => {
getStatistics()
getMaintainSummary()
getMaintainAnalysis()
})
</script>
<style scoped lang="scss">
.home {
blockquote {
padding: 10px 20px;
.dashboard {
padding: 20px;
h3 {
margin: 0 0 20px;
font-size: 17.5px;
border-left: 5px solid #eee;
font-size: 16px;
font-weight: 500;
}
hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #eee;
}
.col-item {
.data-overview {
margin-bottom: 20px;
}
ul {
padding: 0;
margin: 0;
.card-list {
display: flex;
gap: 20px;
}
font-family: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 13px;
color: #676a6c;
overflow-x: hidden;
.data-card {
flex: 1;
display: flex;
padding: 20px;
background: #fff;
border-radius: 4px;
align-items: center;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
.img-icon {
border:1px dashed #ccc;
margin-right: 10px;
}
.number {
font-size: 28px;
font-weight: bold;
margin-bottom: 10px;
color:#333333;
}
ul {
list-style-type: none;
}
h4 {
margin-top: 0px;
}
h2 {
margin-top: 10px;
font-size: 26px;
font-weight: 100;
}
p {
margin-top: 10px;
b {
font-weight: 700;
.label {
color: #999;
font-size: 14px;
}
}
.update-log {
ol {
display: block;
list-style-type: decimal;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0;
margin-inline-end: 0;
padding-inline-start: 40px;
.chart-container {
display: flex;
gap: 20px;
.chart-box {
flex: 1;
padding: 20px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
}
}
}
</style>
</style>

View File

@ -1,14 +1,15 @@
<template>
<div class="login">
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">{{ title }}</h3>
<h3 class="title-color">{{ proxy.$t('login.loginTitle') }}</h3>
<h3 class="title">{{ proxy.$t('login.loginTip') }}</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
size="large"
style="width:300px"
auto-complete="off"
placeholder="账号"
:placeholder="proxy.$t('login.username')"
>
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
</el-input>
@ -17,30 +18,30 @@
<el-input
v-model="loginForm.password"
type="password"
size="large"
style="width:300px"
auto-complete="off"
placeholder="密码"
:placeholder="proxy.$t('login.password')"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item prop="code" v-if="captchaEnabled">
<el-input
v-model="loginForm.code"
size="large"
auto-complete="off"
placeholder="验证码"
style="width: 63%"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
<!-- <el-form-item prop="code" v-if="captchaEnabled">-->
<!-- <el-input-->
<!-- v-model="loginForm.code"-->
<!-- size="large"-->
<!-- auto-complete="off"-->
<!-- placeholder="请输入密码"-->
<!-- style="width: 63%"-->
<!-- @keyup.enter="handleLogin"-->
<!-- >-->
<!-- <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>-->
<!-- </el-input>-->
<!-- <div class="login-code">-->
<!-- <img :src="codeUrl" @click="getCode" class="login-code-img"/>-->
<!-- </div>-->
<!-- </el-form-item>-->
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">{{ proxy.$t('login.rememberPassword') }}</el-checkbox>
<el-form-item style="width:100%;">
<el-button
:loading="loading"
@ -49,18 +50,18 @@
style="width:100%;"
@click.prevent="handleLogin"
>
<span v-if="!loading"> </span>
<span v-else> 中...</span>
<span v-if="!loading">{{ proxy.$t('login.login') }}</span>
<span v-else>{{ proxy.$t('login.logging') }}</span>
</el-button>
<div style="float: right;" v-if="register">
<router-link class="link-type" :to="'/register'">立即注册</router-link>
<router-link class="link-type" :to="'/register'">{{ proxy.$t('login.switchRegisterPage') }}</router-link>
</div>
</el-form-item>
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © 2018-2025 ruoyi.vip All Rights Reserved.</span>
</div>
<!-- <div class="el-login-footer">-->
<!-- <span>Copyright © 2018-2025 ruoyi.vip All Rights Reserved.</span>-->
<!-- </div>-->
</div>
</template>
@ -69,13 +70,12 @@ import { getCodeImg } from "@/api/login";
import Cookies from "js-cookie";
import { encrypt, decrypt } from "@/utils/jsencrypt";
import useUserStore from '@/store/modules/user'
const title = import.meta.env.VITE_APP_TITLE;
import { useI18n } from 'vue-i18n';
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
const { proxy } = getCurrentInstance();
const { t } = useI18n();
const loginForm = ref({
username: "admin",
password: "admin123",
@ -168,21 +168,22 @@ getCookie();
display: flex;
justify-content: center;
align-items: center;
position: relative;
height: 100%;
background-image: url("../assets/images/login-background.jpg");
background-image: url("../assets/images/loginBg.jpg");
background-size: cover;
}
.title {
margin: 0px auto 30px auto;
text-align: center;
color: #707070;
}
color: #000;
}
.title-color {
color:#F59537;
margin-bottom: 10px;
}
.login-form {
border-radius: 6px;
background: #ffffff;
width: 400px;
padding: 25px 25px 5px 25px;
position: absolute;
right:20%;
.el-input {
height: 40px;
input {

View File

@ -0,0 +1,190 @@
<template>
<el-dialog
:title="title"
v-model="visible"
style="width: 460px"
append-to-body
:before-close="cancel"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
:label-width="appStore.language === 'zh_CN' ? '100px' : '160px'"
label-position="right"
>
<el-form-item :label="proxy.$t('maintenance.deviceCode')" prop="deviceCode">
<el-select v-model="form.deviceCode" :disabled="title === proxy.$t('button.details') " :placeholder="proxy.$t('common.select')" style="width:300px">
<el-option
v-for="item in deviceOptions"
:key="item.deviceCode"
:label="item.deviceCode"
:value="item.deviceCode"
/>
</el-select>
</el-form-item>
<el-form-item :label="proxy.$t('maintenance.maintenanceType')" prop="maintenanceType">
<el-select v-model="form.maintenanceType" :disabled="title === proxy.$t('button.details') " :placeholder="proxy.$t('common.select')" style="width:300px">
<el-option
v-for="item in maintenanceTypeList"
:key="item.dictValue"
:label="item.dictLabel"
:value="item.dictValue"
/>
</el-select>
</el-form-item>
<el-form-item :label="proxy.$t('maintenance.maintenanceUser')" prop="maintenanceUser">
<el-input :disabled="title === proxy.$t('button.details') " v-model="form.maintenanceUser" :placeholder="proxy.$t('maintenance.inputMaintenanceUser')" style="width:300px"/>
</el-form-item>
<el-form-item :label="proxy.$t('maintenance.maintenanceDate')" prop="maintenanceDate">
<el-date-picker
:disabled="title === proxy.$t('button.details') "
style="width:300px"
v-model="form.maintenanceDate"
type="date"
:placeholder="proxy.$t('common.selectDate')"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD HH:mm:ss"
:disabled-date="disabledDate"
/>
</el-form-item>
<el-form-item :label="proxy.$t('common.comments')" prop="comments">
<el-input
v-model="form.comments"
style="width:300px"
:disabled="title === proxy.$t('button.details') "
type="textarea"
:placeholder="proxy.$t('common.inputText')"
:rows="4"
maxlength="200"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" v-if="title!== proxy.$t('button.details') " @click="submitForm">{{proxy.$t('button.save')}}</el-button>
<el-button @click="cancel">{{ proxy.$t('button.cancel') }}</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive,onMounted } from 'vue'
import {addMaintainApi, editMaintainApi,getDeviceCodeListApi} from '@/api/maintenance';
import {ElMessage} from "element-plus";
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
const { proxy } = getCurrentInstance();
const props = defineProps({
title: {
type: String,
default: ''
},
maintenanceTypeList: {
type: Array,
default: () => []
},
formData: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue', 'success'])
const visible = ref(false)
const formRef = ref()
const deviceOptions = ref([
{ value: 'CPY0092', label: 'CPY0092' },
{ value: 'CPY0057', label: 'CPY0057' }
])
const form = reactive({
deviceCode: '',
maintenanceType: '',
maintenanceUser: '',
maintenanceDate: '',
comments: ''
})
const rules = {
deviceCode: [{ required: true, message: proxy.$t('maintenance.inputDeviceCode'), trigger: 'change' }],
maintenanceType: [{ required: true, message: proxy.$t('maintenance.inputMaintenanceType'), trigger: 'change' }],
maintenanceUser: [{ required: true, message: proxy.$t('maintenance.inputMaintenanceUser'), trigger: 'blur' }],
maintenanceDate: [{ required: true, message: proxy.$t('maintenance.inputMaintenanceDate'), trigger: 'change' }],
comments: [{ required: true, message: proxy.$t('common.inputComments'), trigger: 'blur' }]
}
const disabledDate = (time) => {
return time.getTime() > Date.now()
}
const submitForm = async () => {
try {
await formRef.value.validate()
//
const res = props.title === proxy.$t('button.add') ? await addMaintainApi(form) : await editMaintainApi(form)
if(res.code === 200) {
ElMessage.success(res.msg)
} else {
ElMessage.error(res.msg)
}
emit('success')
cancel()
} catch (error) {
console.error(proxy.$t('common.formValidateError'), error)
}
}
const cancel = () => {
resetForm()
emit('update:modelValue', false)
}
const resetForm = () => {
formRef.value?.resetFields()
if (props.title === proxy.$t('button.add')) {
//
Object.keys(form).forEach(key => {
form[key] = ''
})
}
}
//
const getDeviceCode = async () => {
// API
const res = await getDeviceCodeListApi();
if(res.code === 200) {
deviceOptions.value = res.data || []
}
}
watch(
() => props.title,
(newVal) => {
if (newVal === proxy.$t('button.edit') || newVal===proxy.$t('button.details') && Object.keys(props.formData).length > 0) {
// 使
Object.keys(form).forEach(key => {
form[key] = props.formData[key]
})
}
},
{ immediate: true }
)
onMounted(() => {
getDeviceCode()
})
defineExpose({
visible
})
</script>
<style scoped>
:deep(.el-form-item__label) {
justify-content: flex-end;
}
.dialog-footer {
text-align: center;
}
</style>

View File

@ -0,0 +1,233 @@
<template>
<div class="contentBox">
<el-card class="card" shadow="hover">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
<el-form-item :label="proxy.$t('maintenance.deviceName')" prop="deviceName">
<el-input v-model="queryParams.deviceName" :placeholder="proxy.$t('maintenance.deviceName')" clearable />
</el-form-item>
<el-form-item :label="proxy.$t('maintenance.deviceCode')" prop="deviceCode">
<el-input v-model="queryParams.deviceCode" :placeholder="proxy.$t('maintenance.deviceCode')" clearable />
</el-form-item>
<el-form-item :label="proxy.$t('maintenance.maintenanceType')" prop="maintenanceType">
<el-select v-model="queryParams.maintenanceType" :placeholder="proxy.$t('maintenance.maintenanceType')" clearable style="width:200px" >
<el-option
v-for="item in maintenanceTypeList"
:key="item.dictValue"
:label="item.dictLabel"
:value="item.dictValue"
/>
</el-select>
</el-form-item>
<el-form-item :label="proxy.$t('maintenance.maintenanceDate')" prop="maintenanceDate">
<el-date-picker
v-model="queryParams.maintenanceDate"
type="date"
:disabled-date="disabledDate"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD HH:mm:ss"
:placeholder="proxy.$t('maintenance.maintenanceDate')"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">{{proxy.$t('button.search')}}</el-button>
<el-button icon="Refresh" @click="resetQuery">{{proxy.$t('button.reset')}}</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
>{{proxy.$t('button.add')}}</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="maintenanceList" border>
<el-table-column :label="proxy.$t('common.sort')" type="index" width="60" align="center" />
<el-table-column :label="proxy.$t('maintenance.deviceName')" align="center" prop="deviceName" />
<el-table-column :label="proxy.$t('maintenance.deviceModel')" align="center" prop="deviceModel" />
<el-table-column :label="proxy.$t('maintenance.deviceCode')" align="center" prop="deviceCode" />
<el-table-column :label="proxy.$t('common.customerAffiliation')" align="center" prop="customerName" />
<el-table-column :label="proxy.$t('maintenance.maintenanceType')" align="center" prop="maintenanceTypeName" />
<el-table-column :label="proxy.$t('common.comments')" align="center" prop="comments" />
<el-table-column :label="proxy.$t('maintenance.maintenanceUser')" align="center" prop="maintenanceUser" />
<el-table-column :label="proxy.$t('maintenance.maintenanceDate')" align="center" prop="maintenanceDate" width="100" />
<el-table-column :label="proxy.$t('maintenance.createTime')" align="center" prop="createTime" width="160" />
<el-table-column :label="proxy.$t('common.action')" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
link
type="primary"
icon="Edit"
@click="handleUpdate(scope.row,proxy.$t('button.edit'))"
>{{ proxy.$t('button.edit') }}</el-button>
<el-button
link
type="primary"
icon="View"
@click="handleUpdate(scope.row,proxy.$t('button.details'))"
>{{proxy.$t('button.check')}}</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-if="total > 0"
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 30, 50]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<MaintenanceDialog
ref="dialogRef"
:title="dialog.title"
:formData="dialog.form"
:maintenance-type-list="maintenanceTypeList"
v-model="dialog.visible"
@success="handleSuccess"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import {getMaintainListApi,getMaintainTypeApi, getMaintainDetailApi} from '@/api/maintenance/index.js'
import {ElMessage} from "element-plus";
import MaintenanceDialog from "./common/MaintenanceDialog.vue";
import { useI18n } from 'vue-i18n';
const router = useRouter()
const { proxy } = getCurrentInstance();
const queryFormRef = ref()
const loading = ref(false)
const showSearch = ref(true)
const total = ref(0)
const maintenanceTypeList = ref([])
const maintenanceList = ref([])
const { t } = useI18n();
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
deviceName: '',
deviceCode: '',
maintenanceType: '',
maintenanceDate: ''
})
//
const dialog = reactive({
title: proxy.$t('button.add'),
visible: false,
form: {
deviceCode: '',
maintenanceType: '',
maintenanceUser: '',
maintenanceDate: '',
comments: ''
}
})
const dialogRef = ref()
const getList = async() => {
loading.value = true
// API
const res = await getMaintainListApi(queryParams);
if(res.code === 200) {
maintenanceList.value = res.rows || []
total.value = res.total
} else {
ElMessage.error(res.msg)
}
loading.value = false
}
const handleQuery = () => {
queryParams.pageNum = 1
getList()
}
const resetQuery = () => {
queryFormRef.value?.resetFields()
handleQuery()
}
//
const handleAdd = () => {
dialog.title = proxy.$t('button.add')
dialog.visible = true
}
//
const handleSuccess = () => {
getList()
}
//
const handleUpdate = async(row,value) => {
const res = await getMaintainDetailApi(row.id)
if(res.code === 200) {
dialog.title = value
dialog.visible = true
dialog.form.deviceCode = res.data.deviceCode
dialog.form.maintenanceType = res.data.maintenanceType.toString()
dialog.form.maintenanceUser = res.data.maintenanceUser
dialog.form.maintenanceDate = res.data.maintenanceDate
dialog.form.comments = res.data.comments
} else {
ElMessage.error(res.msg)
}
}
const disabledDate = (time) => {
return time.getTime() > Date.now()
}
const handleSizeChange = (val) => {
queryParamsNoPage.pageSize = val
getList()
}
const handleCurrentChange = (val) => {
queryParamsNoPage.pageNum = val
getList()
}
//
const getMaintainType = async() => {
const res = await getMaintainTypeApi()
if(res.code === 200) {
maintenanceTypeList.value = res.data || []
} else {
ElMessage.error(res.msg)
}
}
onMounted(() => {
getList()
getMaintainType()
})
</script>
<style scoped lang="scss">
.contentBox {
height:calc(100vh - 130px);
margin:20px;
.card {
width:100%;
height: 100%;
overflow-y: scroll;
}
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
.mb8 {
margin-bottom: 8px;
}
</style>

View File

@ -1,11 +1,11 @@
<template>
<div class="app-container">
<el-row :gutter="20">
<el-col :span="6" :xs="24">
<el-col :span="8" :xs="24">
<el-card class="box-card">
<template v-slot:header>
<div class="clearfix">
<span>个人信息</span>
<span>{{proxy.$t('common.personalInfo')}}</span>
</div>
</template>
<div>
@ -14,45 +14,45 @@
</div>
<ul class="list-group list-group-striped">
<li class="list-group-item">
<svg-icon icon-class="user" />用户名称
<svg-icon icon-class="user" />{{proxy.$t('common.userName')}}
<div class="pull-right">{{ state.user.userName }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="phone" />手机号码
<svg-icon icon-class="phone" />{{proxy.$t('common.phoneNumber')}}
<div class="pull-right">{{ state.user.phonenumber }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="email" />用户邮箱
<svg-icon icon-class="email" />{{proxy.$t('common.userEmail')}}
<div class="pull-right">{{ state.user.email }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="tree" />所属部门
<svg-icon icon-class="tree" />{{proxy.$t('position.department')}}
<div class="pull-right" v-if="state.user.dept">{{ state.user.dept.deptName }} / {{ state.postGroup }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="peoples" />所属角色
<svg-icon icon-class="peoples" />{{proxy.$t('common.role')}}
<div class="pull-right">{{ state.roleGroup }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="date" />创建日期
<svg-icon icon-class="date" />{{proxy.$t('customer.createTime')}}
<div class="pull-right">{{ state.user.createTime }}</div>
</li>
</ul>
</div>
</el-card>
</el-col>
<el-col :span="18" :xs="24">
<el-col :span="16" :xs="24">
<el-card>
<template v-slot:header>
<div class="clearfix">
<span>基本资料</span>
<span>{{proxy.$t('common.baseInfo')}}</span>
</div>
</template>
<el-tabs v-model="activeTab">
<el-tab-pane label="基本资料" name="userinfo">
<el-tab-pane :label="proxy.$t('common.baseInfo')" name="userinfo">
<userInfo :user="state.user" />
</el-tab-pane>
<el-tab-pane label="修改密码" name="resetPwd">
<el-tab-pane :label="proxy.$t('common.editPassword')" name="resetPwd">
<resetPwd />
</el-tab-pane>
</el-tabs>
@ -67,7 +67,7 @@ import userAvatar from "./userAvatar";
import userInfo from "./userInfo";
import resetPwd from "./resetPwd";
import { getUserProfile } from "@/api/system/user";
const { proxy } = getCurrentInstance();
const activeTab = ref("userinfo");
const state = reactive({
user: {},

View File

@ -1,7 +1,7 @@
<template>
<div class="user-info-head" @click="editCropper()">
<img :src="options.img" title="点击上传头像" class="img-circle img-lg" />
<el-dialog :title="title" v-model="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
<el-dialog :title="proxy.$t('common.editAvatar')" v-model="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
<el-row>
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
<vue-cropper
@ -33,7 +33,7 @@
:before-upload="beforeUpload"
>
<el-button>
选择
{{proxy.$t('common.choose')}}
<el-icon class="el-icon--right"><Upload /></el-icon>
</el-button>
</el-upload>
@ -51,7 +51,7 @@
<el-button icon="RefreshRight" @click="rotateRight()"></el-button>
</el-col>
<el-col :lg="{ span: 2, offset: 6 }" :md="2">
<el-button type="primary" @click="uploadImg()"> </el-button>
<el-button type="primary" @click="uploadImg()">{{proxy.$t('button.submit')}}</el-button>
</el-col>
</el-row>
</el-dialog>
@ -69,7 +69,6 @@ const { proxy } = getCurrentInstance();
const open = ref(false);
const visible = ref(false);
const title = ref("修改头像");
//
const options = reactive({
@ -135,7 +134,7 @@ function uploadImg() {
open.value = false;
options.img = import.meta.env.VITE_APP_BASE_API + response.imgUrl;
userStore.avatar = options.img;
proxy.$modal.msgSuccess("修改成功");
proxy.$modal.msgSuccess(proxy.$t('common.modifySuccess'));
visible.value = false;
});
});

View File

@ -1,23 +1,23 @@
<template>
<el-form ref="userRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="用户昵称" prop="nickName">
<el-form-item :label="proxy.$t('equipment.nickName')" prop="nickName">
<el-input v-model="form.nickName" maxlength="30" />
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-form-item :label="proxy.$t('common.phoneNumber')" prop="phonenumber">
<el-input v-model="form.phonenumber" maxlength="11" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-form-item :label="proxy.$t('common.userEmail')" prop="email">
<el-input v-model="form.email" maxlength="50" />
</el-form-item>
<el-form-item label="性别">
<el-form-item :label="proxy.$t('common.sex')">
<el-radio-group v-model="form.sex">
<el-radio value="0"></el-radio>
<el-radio value="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">保存</el-button>
<el-button type="danger" @click="close">关闭</el-button>
<el-button type="primary" @click="submit">{{proxy.$t('button.save')}}</el-button>
<el-button type="danger" @click="close">{{proxy.$t('button.cancel')}}</el-button>
</el-form-item>
</el-form>
</template>

View File

@ -25,15 +25,16 @@ export default defineConfig(({ mode, command }) => {
},
// vite 相关配置
server: {
port: 80,
port: 90,
host: true,
open: true,
proxy: {
// https://cn.vitejs.dev/config/#server-proxy
'/dev-api': {
target: 'http://localhost:8080',
[env.VITE_APP_BASE_API]: {
target: 'http://124.236.46.74:8104',
// target: 'http://192.168.0.22:8111/',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '')
// rewrite: (p) => p.replace(/^\/dev-api/, '')
rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '/api')
}
}
},

View File

@ -67,6 +67,27 @@
resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz"
integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==
"@intlify/core-base@11.1.3":
version "11.1.3"
resolved "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.1.3.tgz"
integrity sha512-cMuHunYO7LE80azTitcvEbs1KJmtd6g7I5pxlApV3Jo547zdO3h31/0uXpqHc+Y3RKt1wo2y68RGSx77Z1klyA==
dependencies:
"@intlify/message-compiler" "11.1.3"
"@intlify/shared" "11.1.3"
"@intlify/message-compiler@11.1.3":
version "11.1.3"
resolved "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.1.3.tgz"
integrity sha512-7rbqqpo2f5+tIcwZTAG/Ooy9C8NDVwfDkvSeDPWUPQW+Dyzfw2o9H103N5lKBxO7wxX9dgCDjQ8Umz73uYw3hw==
dependencies:
"@intlify/shared" "11.1.3"
source-map-js "^1.0.2"
"@intlify/shared@11.1.3":
version "11.1.3"
resolved "https://registry.npmmirror.com/@intlify/shared/-/shared-11.1.3.tgz"
integrity sha512-pTFBgqa/99JRA2H1qfyqv97MKWJrYngXBA/I0elZcYxvJgcCw3mApAoPW3mJ7vx3j+Ti0FyKUFZ4hWxdjKaxvA==
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz"
@ -654,7 +675,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
clipboard@2.0.11:
clipboard@^2.0.6, clipboard@2.0.11:
version "2.0.11"
resolved "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz"
integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==
@ -2841,7 +2862,7 @@ sortablejs@1.14.0:
resolved "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz"
integrity sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==
source-map-js@^1.2.0, source-map-js@^1.2.1, "source-map-js@>=0.6.2 <2.0.0":
source-map-js@^1.0.2, source-map-js@^1.2.0, source-map-js@^1.2.1, "source-map-js@>=0.6.2 <2.0.0":
version "1.2.1"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
@ -3169,6 +3190,11 @@ typedarray.prototype.slice@^1.0.5:
typed-array-buffer "^1.0.3"
typed-array-byte-offset "^1.0.4"
typescript@*, typescript@^5.8.3, typescript@>=4.4.4:
version "5.8.3"
resolved "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz"
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
ufo@^1.5.4:
version "1.5.4"
resolved "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz"
@ -3317,6 +3343,13 @@ vite@^5.0.0, vite@>=2.0.0, vite@5.3.2:
optionalDependencies:
fsevents "~2.3.3"
vue-clipboard3@^2.0.0:
version "2.0.0"
resolved "https://registry.npmmirror.com/vue-clipboard3/-/vue-clipboard3-2.0.0.tgz"
integrity sha512-Q9S7dzWGax7LN5iiSPcu/K1GGm2gcBBlYwmMsUc5/16N6w90cbKow3FnPmPs95sungns4yvd9/+JhbAznECS2A==
dependencies:
clipboard "^2.0.6"
vue-cropper@1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/vue-cropper/-/vue-cropper-1.1.1.tgz"
@ -3337,6 +3370,15 @@ vue-demi@>=0.14.8:
resolved "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz"
integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
vue-i18n@^11.1.3:
version "11.1.3"
resolved "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.1.3.tgz"
integrity sha512-Pcylh9z9S5+CJAqgbRZ3EKxFIBIrtY5YUppU722GIT65+Nukm0TCqiQegZnNLCZkXGthxe0cpqj0AoM51H+6Gw==
dependencies:
"@intlify/core-base" "11.1.3"
"@intlify/shared" "11.1.3"
"@vue/devtools-api" "^6.5.0"
vue-router@4.4.0:
version "4.4.0"
resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.4.0.tgz"
@ -3344,7 +3386,7 @@ vue-router@4.4.0:
dependencies:
"@vue/devtools-api" "^6.5.1"
"vue@^2.6.14 || ^3.3.0", "vue@^3.0.0-0 || ^2.6.0", vue@^3.0.1, vue@^3.2.0, vue@^3.2.25, vue@^3.2.41, vue@3.4.31:
"vue@^2.6.14 || ^3.3.0", vue@^3.0.0, "vue@^3.0.0-0 || ^2.6.0", vue@^3.0.1, vue@^3.2.0, vue@^3.2.25, vue@^3.2.41, vue@3.4.31:
version "3.4.31"
resolved "https://registry.npmjs.org/vue/-/vue-3.4.31.tgz"
integrity sha512-njqRrOy7W3YLAlVqSKpBebtZpDVg21FPoaq1I7f/+qqBThK9ChAIjkRWgeP6Eat+8C+iia4P3OYqpATP21BCoQ==