zhuangyunsheng 4 hónapja
szülő
commit
53a096bc5d

+ 5 - 4
.env.development

@@ -2,12 +2,13 @@
 NODE_ENV = development
 
 # 标题
-VUE_APP_TITLE = EasyDo车辆冲洗
+VUE_APP_TITLE = EasyDo_AI盒子
 
 # 接口地址
-VUE_APP_API_BASEURL = http://www.qdeasydo.com/api
-VUE_APP_ZEROAPI_BASEURL = http://www.qdeasydo.com
-# VUE_APP_API_BASEURL = http://192.168.101.93:8802
+# VUE_APP_API_BASEURL = http://www.qdeasydo.com/api
+# VUE_APP_ZEROAPI_BASEURL = http://www.qdeasydo.com
+VUE_APP_API_BASEURL = http://192.168.101.179:30127
+VUE_APP_ZEROAPI_BASEURL = http://192.168.101.179:30127
 
 # 本地端口
 VUE_APP_PORT = 6060

+ 1 - 1
package.json

@@ -1,5 +1,5 @@
 {
-    "name": "easydo-carwash",
+    "name": "easydo-aibox",
     "version": "1.0.0",
     "private": true,
     "scripts": {

+ 1 - 1
src/App.vue

@@ -49,7 +49,7 @@
 
 		created() {
 			// 设置主题颜色
-			const app_color = this.$CONFIG.COLOR || this.$TOOL.data.get("APP_COLOR")
+			const app_color = this.$TOOL.data.get("APP_COLOR") || this.$CONFIG.COLOR
 			if (app_color) {
 				document.documentElement.style.setProperty("--el-color-primary", app_color);
 				for (let i = 1; i <= 9; i++) {

+ 21 - 0
src/api/model/auth.js

@@ -0,0 +1,21 @@
+import config from "@/config"
+import tool from '@/utils/tool'
+import http from "@/utils/request"
+
+export default {
+	token: {
+		url: `${config.API_URL}/zeroapi/v2/cwashm/box/_login`,
+		name: "登录获取TOKEN",
+		post: async function (expands = {}) {
+			return await http.post(this.url, { expands });
+		}
+	},
+
+    updatePassword: {
+		url: `${config.API_URL}/zeroapi/v2/cwashm/box/_reseta`,
+		name: "修改密码",
+		post: async function (expands = {}) {
+			return await http.post(this.url, { expands });
+		}
+	}
+}

+ 18 - 9
src/api/model/carwash.js

@@ -2,25 +2,34 @@ import config from "@/config"
 import http from "@/utils/request"
 
 export default {
-    device: {
-        url: `${config.API_URL}/zeroapi/v2/cwashm/device`,
+    boxDev: {
+        name: "查询设备",
+        url: `${config.API_URL}/zeroapi/v2/cwashm/box/dev`,
         get: async function (data = {}) {
-            return await http.post(`${this.url}/fetch`, data);
+            return await http.post(`${this.url}f`, data);
+        },
+
+        child: async function (data = {}) {
+            return await http.post(`${this.url}fs`, data);
         },
 
         add: async function (querys = []) {
-			return await http.post(`${this.url}/add`, { querys });
+			return await http.post(`${this.url}add`, { querys });
 		},
 
         edit: async function (querys = []) {
-			return await http.post(`${this.url}/up`, { querys });
+			return await http.post(`${this.url}up`, { querys });
 		},
 
         del: async function (querys = []) {
-			return await http.post(`${this.url}/rm`, { querys });
-		},
+			return await http.post(`${this.url}rm`, { querys });
+		}
+    },
 
-        // name: "查询在线状态",
+    
+    device: {
+        name: "查询在线状态",
+        url: `${config.API_URL}/zeroapi/v2/cwashm/device`,
         state: async function (data = {}) {
             return await http.post(`${this.url}/state`, data);
         },
@@ -35,7 +44,7 @@ export default {
     },
 
     journal: {
-        url: `${config.API_URL}/zeroapi/v2/cwashm/journal/fetch`,
+        url: `${config.API_URL}/zeroapi/v2/cwashm/box/jfetch`,
         name: "监测记录-图片",
         get: async function (data = {}) {
             return await http.post(this.url, data);

+ 0 - 32
src/api/model/system.js

@@ -1,32 +0,0 @@
-import config from "@/config"
-import http from "@/utils/request"
-
-export default {
-	project: {
-		url: `${config.API_URL}/api/factory/getFactoryProject`,
-		name: "获取项目",
-		get: async function (data = {}) {
-			return await http.post(this.url, data);
-		}
-	},
-
-    device: {
-		url: `${config.API_URL}/api/deviceStock/getDevicePage`,
-		name: "设备查询",
-        get: async function (data = {}) {
-			return await http.post(this.url, data);
-		}
-	},
-
-    user: {
-        url: `${config.API_URL}/api/users`,
-		name: "用户信息",
-        get: async function (params = {}) {
-			return await http.get(this.url, params);
-		},
-
-		edit: async function (data = {}) {
-			return await http.put(this.url, data);
-		}
-    }
-}

+ 1 - 0
src/components/scTable/index.vue

@@ -162,6 +162,7 @@ const gridOptions = ref({
         ...props.editConfig
     },
     pagerConfig: {
+        queryType: "limit", // 页码筛选类型
         className: "vxe-table-pager",
         pageSizes: config.pageSizes,
         layouts: config.layouts,

+ 11 - 13
src/config/table.js

@@ -9,11 +9,9 @@ export default {
     
     framework: {
         common: {
-            queryData: function ({ formConfig: { data }, pagerConfig: { currentPage, pageSize } }, paramsColumns) {
-                const query = {
-                    current: currentPage,
-                    size: pageSize
-                }
+            queryData: function ({ formConfig: { data }, pagerConfig: { queryType, currentPage, pageSize } }, paramsColumns) {
+                const query = queryType == "limit" ? { current: currentPage, size: pageSize } : {}
+
                 XEUtils.arrayEach(XEUtils.filter(paramsColumns, item => !valueIsNull(data, item.field || item.column)), item => XEUtils.set(query, item.column, XEUtils.get(data, item.field || item.column)))
 
                 return XEUtils.omit(query, val => XEUtils.isEmpty(val) && !XEUtils.isNumber(val))
@@ -27,12 +25,12 @@ export default {
         },
 
         zeroLiteOld: {
-            queryData: function ({ formConfig: { data }, pagerConfig: { currentPage, pageSize } }, paramsColumns) {
-                const expands = {}
+            queryData: function ({ formConfig: { data }, pagerConfig: { queryType, currentPage, pageSize } }, paramsColumns) {
+                const expands = queryType == "expands" ? { length: pageSize, start: (currentPage - 1) * pageSize } : {}
                 XEUtils.arrayEach(XEUtils.filter(paramsColumns, item => item.type == "expands" && !valueIsNull(data, item.field || item.column)), item => XEUtils.set(expands, item.field || item.column, data[item.field || item.column]))
 
                 const query = {
-                    limit: { length: pageSize, start: (currentPage - 1) * pageSize },
+                    limit: queryType == "limit" ? { length: pageSize, start: (currentPage - 1) * pageSize } : {},
                     columns: XEUtils.get(XEUtils.find(paramsColumns, item => item.type == "columns"), "field", [])
                 }
                 XEUtils.arrayEach(XEUtils.filter(paramsColumns, item => item.type == "limit"), item => XEUtils.set(query, `limit.${item.column}`, item.field))
@@ -58,7 +56,7 @@ export default {
                 if (relation.length == 1) XEUtils.set(query, "condition", XEUtils.first(relation))
                 if (relation.length > 1) XEUtils.set(query, "condition", { symbol: "AND", relation })
 
-                return { querys: [XEUtils.omit(query, item => XEUtils.isEmpty(item))], expands }
+                return { querys: XEUtils.isEmpty(XEUtils.omit(query, item => XEUtils.isEmpty(item))) ? []: [XEUtils.omit(query, item => XEUtils.isEmpty(item))], expands }
             },
             parseData: function (res) {
                 return {
@@ -69,12 +67,12 @@ export default {
         },
 
         zeroLite: {
-            queryData: function ({ formConfig: { data }, pagerConfig: { currentPage, pageSize } }, paramsColumns) {
-                const expands = {}
+            queryData: function ({ formConfig: { data }, pagerConfig: { queryType, currentPage, pageSize } }, paramsColumns) {
+                const expands = queryType == "expands" ? { length: pageSize, start: (currentPage - 1) * pageSize } : {}
                 XEUtils.arrayEach(XEUtils.filter(paramsColumns, item => item.type == "expands" && !valueIsNull(data, item.field || item.column)), item => XEUtils.set(expands, item.field || item.column, data[item.field || item.column]))
 
                 const query = {
-                    limit: { length: pageSize, start: (currentPage - 1) * pageSize },
+                    limit: queryType == "limit" ? { length: pageSize, start: (currentPage - 1) * pageSize } : {},
                     orderby: XEUtils.map(XEUtils.filter(paramsColumns, item => item.type == "orderby"), item => ({ column: item.column, seq: item.symbol.toUpperCase() })),
                     columns: XEUtils.get(XEUtils.find(paramsColumns, item => item.type == "columns"), "field", [])
                 }
@@ -96,7 +94,7 @@ export default {
                 if (relation.length == 1) XEUtils.set(query, "condition", XEUtils.first(relation))
                 if (relation.length > 1) XEUtils.set(query, "condition", { symbol: "AND", relation })
 
-                return { querys: [XEUtils.omit(query, item => XEUtils.isEmpty(item))], expands }
+                return { querys: XEUtils.isEmpty(XEUtils.omit(query, item => XEUtils.isEmpty(item))) ? [] : [XEUtils.omit(query, item => XEUtils.isEmpty(item))], expands }
             },
             parseData: function (res) {
                 const data = XEUtils.get(res, "datas", XEUtils.get(res, "expands.connects", []))

+ 87 - 0
src/layout/components/password.vue

@@ -0,0 +1,87 @@
+<template>
+    <el-dialog v-model="visible" title="修改密码" width="480">
+        <el-form ref="dialogForm" :model="form" :rules="rules" label-width="110px">
+            <el-form-item label="当前密码:" prop="auth">
+                <el-input v-model="form.auth" type="password" show-password placeholder="请输入当前密码"></el-input>
+				<div class="el-form-item-msg">必须提供当前登录用户密码才能进行更改</div>
+            </el-form-item>
+            <el-form-item label="新密码:" prop="n">
+                <el-input v-model="form.n" type="password" show-password placeholder="请输入新密码"></el-input>
+				<sc-password-strength v-model="form.n"></sc-password-strength>
+            </el-form-item>
+            <el-form-item label="确认新密码:" prop="cn">
+				<el-input v-model="form.cn" type="password" show-password placeholder="请再次输入新密码"></el-input>
+			</el-form-item>
+        </el-form>
+
+        <template #footer>
+            <el-button @click="cancel">取 消</el-button>
+            <el-button type="primary" :loading="isSaveing" @click="save">确 定</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import scPasswordStrength from "@/components/scPasswordStrength";
+
+const router = useRouter();
+
+const visible = ref(false);
+const isSaveing = ref(false);
+const form = ref({
+    auth: "",
+    n: "",
+    cn: ""
+});
+const rules = reactive({
+    auth: [{ required: true, message: '请输入当前密码'}],
+    n: [{ required: true, message: '请输入新密码'}],
+    cn: [
+        { required: true, message: "请再次输入新密码" },
+        { validator: (rule, value, callback) => {
+            if (value !== form.value.n) {
+                callback(new Error("两次输入密码不一致"));
+            } else {
+                callback();
+            }
+        }}
+    ]
+});
+const dialogForm = ref();
+const open = () => {
+    visible.value = true;
+}
+const save = () => {
+    dialogForm.value.validate(valid => {
+        if (valid) {
+            isSaveing.value = true;
+            API.auth.updatePassword.post(form.value).then(() => {
+                ElNotification.success({
+                    title: "提示",
+                    message: "密码修改成功,请重新登录",
+                    duration: 1500
+                });
+                
+                setTimeout(() => {
+                    isSaveing.value = false;
+                    TOOL.cookie.remove("TOKEN");
+                    TOOL.data.remove("USER_INFO");
+                    router.replace({ path: "/login" });
+                }, 1500);
+            }).catch(() => isSaveing.value = false);
+        } else {
+            return false
+        }
+    })
+}
+
+defineExpose({
+    open
+})
+</script>
+
+<style lang="scss" scoped>
+.el-form {padding-right: calc(var(--el-dialog-padding-primary) + var(--el-message-close-size, 16px));}
+</style>

+ 73 - 0
src/layout/components/setting.vue

@@ -0,0 +1,73 @@
+<template>
+	<el-form ref="form" label-width="120px" label-position="left" style="padding:0 20px;">
+		<el-form-item :label="$t('user.nightmode')">
+			<el-switch v-model="dark"></el-switch>
+		</el-form-item>
+		<el-form-item :label="$t('user.language')">
+			<el-select v-model="lang">
+				<el-option label="简体中文" value="zh-cn"></el-option>
+				<el-option label="English" value="en"></el-option>
+			</el-select>
+		</el-form-item>
+		<el-divider></el-divider>
+		<el-form-item label="主题颜色">
+			<el-color-picker v-model="colorPrimary" :predefine="colorList">></el-color-picker>
+		</el-form-item>
+		<el-divider></el-divider>
+		<el-form-item label="折叠菜单">
+			<el-switch v-model="menuIsCollapse"></el-switch>
+		</el-form-item>
+		<el-divider></el-divider>
+	</el-form>
+</template>
+
+<script>
+import { VxeUI } from "vxe-table";
+import colorTool from "@/utils/color";
+
+export default {
+    data() {
+        return {
+            menuIsCollapse: this.$store.state.global.menuIsCollapse,
+            lang: this.$TOOL.data.get("APP_LANG") || this.$CONFIG.LANG,
+            dark: this.$TOOL.data.get("APP_DARK") || false,
+            colorList: ["#409EFF", "#009688", "#536dfe", "#ff5c93", "#c62f2f", "#fd726d"],
+            colorPrimary: this.$TOOL.data.get("APP_COLOR") || this.$CONFIG.COLOR || "#409EFF"
+        }
+    },
+    watch: {
+        menuIsCollapse(){
+            this.$store.commit("TOGGLE_menuIsCollapse")
+        },
+        dark(val) {
+            if (val) {
+                document.documentElement.classList.add("dark");
+                this.$TOOL.data.set("APP_DARK", val);
+                VxeUI.setTheme("dark");
+            } else {
+                document.documentElement.classList.remove("dark");
+                this.$TOOL.data.remove("APP_DARK");
+                VxeUI.setTheme("light");
+            }
+        },
+        lang(val){
+            this.$i18n.locale = val
+            this.$TOOL.data.set("APP_LANG", val);
+        },
+        colorPrimary(val){
+            if(!val){
+                val = "#409EFF"
+                this.colorPrimary = "#409EFF"
+            }
+            document.documentElement.style.setProperty("--el-color-primary", val);
+            for (let i = 1; i <= 9; i++) {
+                document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, colorTool.lighten(val,i/10));
+            }
+            for (let i = 1; i <= 9; i++) {
+                document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, colorTool.darken(val,i/10));
+            }
+            this.$TOOL.data.set("APP_COLOR", val);
+        }
+    }
+}
+</script>

+ 1 - 6
src/layout/components/sideM.vue

@@ -9,7 +9,7 @@
             <el-main>
                 <el-scrollbar>
                     <el-menu :default-active="$route.meta.active || $route.fullPath" router @select="select">
-                        <NavMenu :navMenus="menu"></NavMenu>
+                        <NavMenu :navMenus="$TOOL.data.get('MENU') || []"></NavMenu>
                     </el-menu>
                 </el-scrollbar>
             </el-main>
@@ -30,11 +30,6 @@ export default {
             nav: false
         }
     },
-    computed:{
-        menu() {
-            return (this.$TOOL.data.get("MENU") || []).filter(item => item.path !== this.$CONFIG.DASHBOARD_URL && !item.meta?.hidden);
-        }
-    },
 
     methods: {
         showMobileNav(e) {

+ 0 - 250
src/layout/components/tags.vue

@@ -1,250 +0,0 @@
-<template>
-	<div class="adminui-tags">
-		<ul ref="tags">
-			<li v-for="tag in tagList" v-bind:key="tag" :class="[isActive(tag) && 'active', tag.meta.affix && 'affix']" @contextmenu.prevent="openContextMenu($event, tag)">
-                <router-link v-if="!tag.meta?.hidden" :to="tag">
-                    <sc-iconify v-if="tag.meta?.icon" style="margin-left: 0;margin-right: 5px;" :icon="tag.meta.icon" size="16"></sc-iconify>
-                    <scTooltip :content="tag.meta.title"></scTooltip>
-				    <el-icon v-if="!tag.meta.affix" @click.prevent.stop='closeSelectedTag(tag)'><el-icon-close /></el-icon>
-				</router-link>
-			</li>
-		</ul>
-	</div>
-
-	<transition name="el-zoom-in-top">
-		<ul v-if="contextMenuVisible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu" id="contextmenu">
-			<li @click="refreshTab()"><el-icon><el-icon-refresh /></el-icon>刷新</li>
-			<hr>
-			<li @click="closeTabs()" :class="contextMenuItem.meta.affix && 'disabled'"><el-icon><el-icon-close /></el-icon>关闭标签</li>
-			<li @click="closeOtherTabs()"><el-icon><el-icon-folder-delete /></el-icon>关闭其他标签</li>
-		</ul>
-	</transition>
-</template>
-
-<script>
-import Sortable from "sortablejs"
-import XEUtils from "xe-utils";
-
-export default {
-    name: "tags",
-    data() {
-        return {
-            contextMenuVisible: false,
-            contextMenuItem: null,
-            left: 0,
-            top: 0,
-            tagList: this.$store.state.viewTags.viewTags,
-            tipDisplayed: false
-        }
-    },
-    watch: {
-        $route(e) {
-            this.addViewTags(e);
-            // 判断标签容器是否出现滚动条
-            this.$nextTick(() => {
-                const tags = this.$refs.tags
-                if(tags && tags.scrollWidth > tags.clientWidth) {
-                    // 确保当前标签在可视范围内
-                    let targetTag = tags.querySelector(".active")
-                    targetTag.scrollIntoView()
-                    // 显示提示
-                    if (!this.tipDisplayed) {
-                        this.$msgbox({
-                            type: "warning",
-                            center: true,
-                            title: "提示",
-                            message: "当前标签数量过多,可通过鼠标滚轴滚动标签栏。关闭标签数量可减少系统性能消耗。",
-                            confirmButtonText: "知道了"
-                        })
-                        this.tipDisplayed = true
-                    }
-                }
-            })
-        },
-        contextMenuVisible(value) {
-            const cm = e => {
-                const sp = document.getElementById("contextmenu");
-                if (sp && !sp.contains(e.target)) this.closeMenu();
-            }
-            if (value) document.body.addEventListener("click", e => cm(e));
-            else document.body.removeEventListener("click", e => cm(e));
-        }
-    },
-    created() {
-        let dashboardRoute = XEUtils.findTree(this.$TOOL.data.get("MENU"), item => item.path == this.$CONFIG.DASHBOARD_URL)?.item;
-        if (dashboardRoute) {
-            dashboardRoute.fullPath = dashboardRoute.path;
-            this.addViewTags(dashboardRoute);
-            this.addViewTags(this.$route);
-        }
-    },
-    mounted() {
-        this.tagDrop();
-        this.scrollInit();
-    },
-    methods: {
-        // 标签拖拽排序
-        tagDrop() {
-            Sortable.create(this.$refs.tags, {
-                draggable: "li",
-                animation: 300
-            })
-        },
-        // 增加tag
-        addViewTags(route) {
-            if (route.name && !route.meta.fullpage) {
-                this.$store.commit("pushViewTags", route);
-                this.$store.commit("pushKeepLive", route.name);
-            }
-        },
-        //高亮tag
-        isActive(route) {
-            return route.fullPath === this.$route.fullPath;
-        },
-        // 关闭tag
-        closeSelectedTag(tag, autoPushLatestView = true) {
-            const nowTagIndex = this.tagList.findIndex(item => item.fullPath == tag.fullPath);
-            this.$store.commit("removeViewTags", tag);
-            this.$store.commit("removeIframeList", tag);
-            this.$store.commit("removeKeepLive", tag.name);
-            if (autoPushLatestView && this.isActive(tag)) {
-                const leftView = this.tagList[nowTagIndex - 1];
-                if (leftView) this.$router.push(leftView);
-                else this.$router.push("/");
-            }
-        },
-        // tag右键
-        openContextMenu(e, tag) {
-            this.contextMenuItem = tag;
-            this.contextMenuVisible = true;
-            this.left = e.clientX + 1;
-            this.top = e.clientY + 1;
-
-            // FIX 右键菜单边缘化位置处理
-            this.$nextTick(() => {
-                let sp = document.getElementById("contextmenu");
-                if(document.body.offsetWidth - e.clientX < sp.offsetWidth) {
-                    this.left = document.body.offsetWidth - sp.offsetWidth + 1;
-                    this.top = e.clientY + 1;
-                }
-            })
-        },
-        // 关闭右键菜单
-        closeMenu() {
-            this.contextMenuItem = null;
-            this.contextMenuVisible = false;
-        },
-        // TAB 刷新
-        refreshTab() {
-            this.contextMenuVisible = false;
-            const nowTag = this.contextMenuItem;
-            //判断是否当前路由,否的话跳转
-            if (this.$route.fullPath !== nowTag.fullPath) {
-                this.$router.push({
-                    path: nowTag.fullPath,
-                    query: nowTag.query
-                })
-            }
-            
-            this.$store.commit("refreshIframe", nowTag);
-            setTimeout(() => {
-                this.$store.commit("removeKeepLive", nowTag.name);
-                this.$store.commit("setRouteShow", false);
-                this.$nextTick(() => {
-                    this.$store.commit("pushKeepLive", nowTag.name);
-                    this.$store.commit("setRouteShow", true);
-                })
-            }, 0);
-        },
-        // TAB 关闭
-        closeTabs(){
-            const nowTag = this.contextMenuItem;
-            if (!nowTag.meta.affix) {
-                this.closeSelectedTag(nowTag);
-                this.contextMenuVisible = false;
-            }
-        },
-        // TAB 关闭其他
-        closeOtherTabs() {
-            const nowTag = this.contextMenuItem;
-            // 判断是否当前路由,否的话跳转
-            if (this.$route.fullPath != nowTag.fullPath) {
-                this.$router.push({
-                    path: nowTag.fullPath,
-                    query: nowTag.query
-                })
-            }
-
-            [...this.tagList].forEach(tag => {
-                if(tag?.meta?.affix || nowTag.fullPath == tag.fullPath) return true;
-                else this.closeSelectedTag(tag, false);
-            })
-            this.contextMenuVisible = false;
-        },
-        // 横向滚动
-        scrollInit() {
-            const scrollDiv = this.$refs.tags;
-            scrollDiv.addEventListener("mousewheel", handler, false) || scrollDiv.addEventListener("DOMMouseScroll", handler, false)
-            function handler(event) {
-                const detail = event.wheelDelta || event.detail;
-                //火狐上滚键值-3 下滚键值3,其他内核上滚键值120 下滚键值-120
-                const moveForwardStep = 1;
-                const moveBackStep = -1;
-                let step = 0;
-                if (detail == 3 ||  detail < 0 && detail != -3) {
-                    step = moveForwardStep * 50;
-                } else {
-                    step = moveBackStep * 50;
-                }
-                scrollDiv.scrollLeft += step;
-            }
-        }
-    }
-}
-</script>
-
-<style>
-	.contextmenu {
-		position: fixed;
-		width: 200px;
-		margin:0;
-		border-radius: 0px;
-		background: var(--el-bg-color-overlay);
-		border: 1px solid var(--el-border-color-light);
-		box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
-		z-index: 3000;
-		list-style-type: none;
-		padding: 10px 0;
-	}
-	.contextmenu hr {
-		margin:5px 0;
-		border: none;
-		height: 1px;
-		font-size: 0px;
-		background-color: var(--el-border-color-light);
-	}
-	.contextmenu li {
-		display: flex;
-		align-items: center;
-		margin:0;
-		cursor: pointer;
-		line-height: 30px;
-		padding: 0 17px;
-		color: #606266;
-	}
-	.contextmenu li i {
-		font-size: 14px;
-		margin-right: 10px;
-	}
-	.contextmenu li:hover {
-		background-color: #ecf5ff;
-		color: #66b1ff;
-	}
-	.contextmenu li.disabled {
-		cursor: not-allowed;
-		color: #bbb;
-		background: transparent;
-	}
-
-	.dark .contextmenu li {color: var(--el-text-color-primary);}
-</style>

+ 71 - 0
src/layout/components/userbar.vue

@@ -0,0 +1,71 @@
+<template>
+	<div class="user-bar">
+		<el-dropdown class="user panel-item" trigger="click" @command="handleUser">
+			<div class="user-avatar">
+				<el-avatar :size="30">{{ $TOOL.data.get("USER_INFO")?.nickName?.substring(0, 1).toLocaleUpperCase() }}</el-avatar>
+				<label>{{ $TOOL.data.get("USER_INFO")?.nickName }}</label>
+				<el-icon class="el-icon--right"><el-icon-arrow-down /></el-icon>
+			</div>
+			<template #dropdown>
+				<el-dropdown-menu>
+					<el-dropdown-item command="layout">布局设置</el-dropdown-item>
+					<el-dropdown-item command="updatePwd">修改密码</el-dropdown-item>
+					<el-dropdown-item divided command="outLogin">退出登录</el-dropdown-item>
+				</el-dropdown-menu>
+			</template>
+		</el-dropdown>
+	</div>
+
+    <password v-if="passwordVisible" ref="passwordDialog"></password>
+
+    <el-drawer title="布局实时演示" v-model="settingDialog" :size="400" append-to-body destroy-on-close>
+		<setting></setting>
+	</el-drawer>
+</template>
+
+<script>
+import setting from "./setting";
+import password from "./password";
+
+export default {
+    components: { setting, password },
+    data() {
+        return {
+            passwordVisible: false,
+            settingDialog: false,
+        }
+    },
+
+    methods: {
+        //个人信息
+        handleUser(command) {
+            if (command == "layout") this.settingDialog = true;
+            if (command == "updatePwd") {
+                this.passwordVisible = true;
+                nextTick(() => this.$refs.passwordDialog.open());
+            }
+            if (command == "outLogin") {
+                this.$confirm("确认是否退出当前用户?", "温馨提示", {
+                    type: "warning",
+                    confirmButtonText: "退出"
+                }).then(() => {
+                    this.$TOOL.cookie.remove("TOKEN");
+                    this.$TOOL.data.remove("USER_INFO");
+                    this.$TOOL.data.remove("MENU");
+                    this.$router.replace({ path: "/login" });
+                }).catch(() => {})
+            }
+        }
+    }
+}
+</script>
+
+<style scoped>
+	.user-bar {display: flex;align-items: center;height: 100%;}
+	.user-bar .panel-item {padding: 0 10px;cursor: pointer;height: 100%;display: flex;align-items: center;}
+	.user-bar .panel-item i {font-size: 16px;}
+	.user-bar .panel-item:hover {background: rgba(255, 255, 255, 0.1);}
+    
+	.user-bar .user-avatar {height:49px;display: flex;align-items: center;color: #fff;}
+	.user-bar .user-avatar label {display: inline-block;margin-left:5px;font-size: 12px;cursor:pointer;}
+</style>

+ 6 - 7
src/layout/index.vue

@@ -5,6 +5,7 @@
             <img class="logo" src="img/logo.png" />
             <span>{{ $CONFIG.APP_NAME }}</span>
         </div>
+        <userbar></userbar>
     </header>
     <section class="aminui-wrapper">
         <div v-if="!ismobile" :class="['aminui-side', menuIsCollapse && 'isCollapse']">
@@ -19,13 +20,11 @@
                 <el-icon><el-icon-expand v-if="menuIsCollapse" /><el-icon-fold v-else /></el-icon>
             </div>
         </div>
-        <Side-m v-if="ismobile"></Side-m>
+        <Side-m v-else></Side-m>
         <div class="aminui-body el-container">
             <div class="aminui-main" id="aminui-main">
                 <router-view v-slot="{ Component }">
-                    <keep-alive :include="this.$store.state.keepAlive.keepLiveRoute">
-                        <component :is="Component" :key="$route.fullPath" v-if="$store.state.keepAlive.routeShow"/>
-                    </keep-alive>
+                    <component :is="Component" :key="$route.fullPath" />
                 </router-view>
             </div>
         </div>
@@ -36,15 +35,15 @@
 import XEUtils from "xe-utils";
 
 import SideM from "./components/sideM";
-import Tags from "./components/tags";
 import NavMenu from "./components/NavMenu";
+import userbar from "./components/userbar";
     
 export default {
     name: "index",
     components: {
         SideM,
-        Tags,
-        NavMenu
+        NavMenu,
+        userbar
     },
     data() {
         return {}

+ 12 - 10
src/locales/lang/en.js

@@ -1,13 +1,15 @@
 export default {
 	login: {
-		formTitle: 'User login',
-		rememberMe: 'Remember me',
-		signIn: 'Sign in',
-		userPlaceholder: 'Please input a user name',
-		userError: 'Please input a user name',
-		PWPlaceholder: 'Please input a password',
-		PWError: 'Please input a password',
-		codePlaceholder: 'Please input the verification code',
-		codeError: 'Please input the verification code'
+		formTitle: "User login",
+		signIn: "Sign in",
+		userPlaceholder: "Please input a user name",
+		userError: "Please input a user name",
+		PWPlaceholder: "Please input a password",
+		PWError: "Please input a password"
+	},
+
+    user: {
+		nightmode: "night mode",
+		language: "language"
 	}
-}
+}

+ 12 - 10
src/locales/lang/zh-cn.js

@@ -1,13 +1,15 @@
 export default {
 	login: {
-		formTitle: '用户登录',
-		rememberMe: '24小时免登录',
-		signIn: '登 录',
-		userPlaceholder: '请输入用户名',
-		userError: '请输入用户名',
-		PWPlaceholder: '请输入密码',
-		PWError: '请输入密码',
-		codePlaceholder: '请输入验证码',
-		codeError: '请输入验证码'
+		formTitle: "用户登录",
+		signIn: "登 录",
+		userPlaceholder: "请输入用户名",
+		userError: "请输入用户名",
+		PWPlaceholder: "请输入密码",
+		PWError: "请输入密码"
+	},
+
+    user: {
+		nightmode: "黑夜模式",
+		language: "语言"
 	}
-}
+}

+ 21 - 0
src/router/index.js

@@ -33,6 +33,27 @@ router.beforeEach(async (to, from, next) => {
 	// 动态标题
 	document.title = to.meta.title ? `${to.meta.title} - ${config.APP_NAME}` : `${config.APP_NAME}`
 
+    let token = tool.cookie.get("TOKEN");
+    
+	if (to.path === "/login") {
+		// 删除路由(替换当前layout路由)
+		router.addRoute(routes[0])
+		// 删除路由(404)
+		routes_404_r()
+		isGetRouter = false;
+		if (token) next(from.fullPath);
+        else {
+            to.redirectedFrom = from;
+            next();
+        }
+		return false;
+	}
+
+	if (!token) {
+		next({ path: "/login" });
+		return false;
+	}
+
 	// 整页路由处理
 	if (to.meta.fullpage) to.matched = [to.matched[to.matched.length - 1]];
     

+ 2 - 10
src/router/scrollBehavior.js

@@ -1,19 +1,11 @@
 import store from "@/store"
 
-export function beforeEach(to, from) {
+export function beforeEach() {
 	const adminMain = document.querySelector("#aminui-main")
 	if (!adminMain) return false
-	store.commit("updateViewTags", {
-		fullPath: from.fullPath,
-		scrollTop: adminMain.scrollTop
-	})
 }
 
-export function afterEach(to) {
+export function afterEach() {
 	const adminMain = document.querySelector("#aminui-main")
 	if (!adminMain) return false
-	nextTick(() => {
-		const beforeRoute = store.state.viewTags.viewTags.filter(v => v.fullPath == to.fullPath)[0]
-		if (beforeRoute) adminMain.scrollTop = beforeRoute.scrollTop || 0
-	})
 }

+ 8 - 1
src/router/systemRouter.js

@@ -6,8 +6,15 @@ const routes = [
 		name: "layout",
 		path: "/",
 		component: () => import(/* webpackChunkName: "layout" */ "@/layout"),
-		redirect: config.DASHBOARD_URL || "/home",
+		redirect: config.DASHBOARD_URL || "/device",
 		children: []
+	},
+	{
+		path: "/login",
+		component: () => import(/* webpackChunkName: "login" */ "@/views/login"),
+		meta: {
+			title: "登录"
+		}
 	}
 ]
 

+ 0 - 5
src/store/modules/global.js

@@ -6,8 +6,6 @@ export default {
 		ismobile: false,
 		//菜单是否折叠 toggle
 		menuIsCollapse: config.MENU_IS_COLLAPSE,
-		// 多标签栏
-		layoutTags: config.LAYOUT_TAGS,
 		// 主题
 		theme: config.THEME
 	},
@@ -20,9 +18,6 @@ export default {
 		},
 		TOGGLE_menuIsCollapse(state) {
 			state.menuIsCollapse = !state.menuIsCollapse
-		},
-		TOGGLE_layoutTags(state) {
-			state.layoutTags = !state.layoutTags
 		}
 	}
 }

+ 0 - 38
src/store/modules/iframe.js

@@ -1,38 +0,0 @@
-export default {
-	state: {
-		iframeList: []
-	},
-	mutations: {
-		setIframeList(state, route){
-			state.iframeList = []
-			state.iframeList.push(route)
-		},
-		pushIframeList(state, route){
-			let target = state.iframeList.find((item) => item.path === route.path)
-			if(!target){
-				state.iframeList.push(route)
-			}
-		},
-		removeIframeList(state, route){
-			state.iframeList.forEach((item, index) => {
-				if (item.path === route.path){
-					state.iframeList.splice(index, 1)
-				}
-			})
-		},
-		refreshIframe(state, route){
-			state.iframeList.forEach((item) => {
-				if (item.path == route.path){
-					var url = route.meta.url;
-					item.meta.url = '';
-					setTimeout(function() {
-						item.meta.url = url
-					}, 200);
-				}
-			})
-		},
-		clearIframeList(state){
-			state.iframeList = []
-		}
-	}
-}

+ 0 - 34
src/store/modules/keepAlive.js

@@ -1,34 +0,0 @@
-export default {
-	state: {
-		keepLiveRoute: [],
-		routeKey: null,
-		routeShow: true
-	},
-	mutations: {
-		pushKeepLive(state, component){
-			if(!state.keepLiveRoute.includes(component)){
-				state.keepLiveRoute.push(component)
-			}
-		},
-		removeKeepLive(state, component){
-			var index = state.keepLiveRoute.indexOf(component);
-			if(index !== -1){
-				state.keepLiveRoute.splice(index, 1);
-			}
-		},
-		clearKeepLive(state){
-			state.keepLiveRoute = []
-		},
-		setRouteKey(state, key){
-			state.routeKey = key
-		},
-		setRouteShow(state, key){
-			state.routeShow = key
-		}
-	},
-	actions: {
-		setRouteKey({ commit }, key) {
-			commit('setRouteKey', key);
-		}
-	}
-}

+ 0 - 46
src/store/modules/viewTags.js

@@ -1,46 +0,0 @@
-import router from '@/router'
-
-export default {
-	state: {
-		viewTags: []
-	},
-	mutations: {
-		pushViewTags(state, route){
-			let backPathIndex = state.viewTags.findIndex(item => item.fullPath == router.options.history.state.back)
-			let target = state.viewTags.find((item) => item.fullPath === route.fullPath)
-			let isName = route.name
-			if(!target && isName){
-				if(backPathIndex == -1){
-					state.viewTags.push(route)
-				}else{
-					state.viewTags.splice(backPathIndex+1, 0, route)
-				}
-			}
-		},
-		removeViewTags(state, route){
-			state.viewTags.forEach((item, index) => {
-				if (item.fullPath === route.fullPath){
-					state.viewTags.splice(index, 1)
-				}
-			})
-		},
-		updateViewTags(state, route){
-			state.viewTags.forEach((item) => {
-				if (item.fullPath == route.fullPath){
-					item = Object.assign(item, route)
-				}
-			})
-		},
-		updateViewTagsTitle(state, title=''){
-			const nowFullPath = location.hash.substring(1)
-			state.viewTags.forEach((item) => {
-				if (item.fullPath == nowFullPath){
-					item.meta.title = title
-				}
-			})
-		},
-		clearViewTags(state){
-			state.viewTags = []
-		}
-	}
-}

+ 1 - 10
src/style/app.scss

@@ -34,9 +34,6 @@ a,button,input,textarea{-webkit-tap-highlight-color:rgba(0,0,0,0);box-sizing: bo
 .aminui-side {display: flex;flex-flow: column;flex-shrink:0;width:210px;background: #fff;box-shadow: 2px 0 8px 0 rgba(29,35,41,.05);border-right: 1px solid #e6e6e6;transition:width 0.3s;}
 .aminui-side-scroll {overflow: auto;overflow-x:hidden;flex: 1;}
 
-.el-menu, .side-menu-popper .el-menu .el-menu-item {--el-menu-active-color: var(--el-fill-color-blank);--el-menu-hover-bg-color: var(--el-menu-bg-color);--el-menu-item-hover-fill: var(--el-menu-bg-color);}
-.el-menu .el-menu-item.is-active, .el-menu .el-menu-item.is-active:hover, .el-menu--collapse .el-sub-menu.is-active .el-sub-menu__title {background: var(--el-color-primary);}
-
 .aminui-side-bottom {border-top: 1px solid #ebeef5;height:51px;cursor: pointer;display: flex;align-items: center;justify-content: center;}
 .aminui-side-bottom i {font-size: 16px;}
 .aminui-side-bottom:hover {color: var(--el-color-primary);}
@@ -94,10 +91,4 @@ a,button,input,textarea{-webkit-tap-highlight-color:rgba(0,0,0,0);box-sizing: bo
 .vxe-table-slot--top__form .el-form-item {margin-bottom: 0;padding-bottom: .6em;}
 .vxe-table-slot--top__form .el-form-item__label, .vxe-table-slot--top__form .el-form-item__content {font-size: var(--el-font-size-medium);color: #000;}
 
-.el-dialog .vxe-grid.size--mini .vxe-table-query {background-color: unset;}
-
-.download-button-popper {
-    width: unset !important;min-width: unset !important;max-width: 200px;text-align: center;
-    .el-button {justify-content: unset;max-width: 100%;height: var(--vxe-ui-button-height-mini);font-size: inherit;font-weight: 400;}
-    .el-button > span {max-width: 100%;}
-}
+.el-dialog .vxe-grid.size--mini .vxe-table-query {background-color: unset;}

+ 3 - 4
src/style/dark.scss

@@ -25,8 +25,6 @@ html.dark {
 	.aminui-side-bottom {border-color: var(--el-border-color-light);}
 	.aminui-main {background: var(--el-bg-color);}
 	.drawerBG {background: var(--el-bg-color);}
-	.aminui-header-menu .el-menu {--el-menu-bg-color:var(--el-bg-color-overlay) !important;--el-menu-hover-bg-color: #171819 !important;}
-	.aminui-header-menu .el-menu .el-sub-menu__title {background-color:transparent !important;}
 
 	//全局滚动条样式
 	::-webkit-scrollbar-thumb {background-color: rgba(163, 166, 173, 0.3);}
@@ -36,6 +34,7 @@ html.dark {
 	.el-header, .el-main.nopadding, .el-footer {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);}
 	.el-main {background: var(--el-bg-color);}
 	.el-aside {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);}
-	.el-table .el-table__body-wrapper {background: var(--el-bg-color);}
-	.el-table th.is-sortable:hover {background: #111;}
+
+    .scPageHeader {background: var(--el-bg-color);}
+    .scPageHeader .el-page-header__header .el-page-header__extra .extra-title {color: #fff;}
 }

+ 8 - 0
src/utils/basicDic.js

@@ -1,5 +1,13 @@
 import XEUtils from "xe-utils";
 
+export const loginDic = {
+    token: "Bearer easydo_aibox",
+    user: {
+        id: 1,
+        nickName: "admin"
+    }
+}
+
 export const nodeTypeDic = {
     master: "监控主机",
     box: "AI盒子"

+ 0 - 60
src/utils/useTabs.js

@@ -1,60 +0,0 @@
-import NProgress from 'nprogress'
-import 'nprogress/nprogress.css'
-import router from '@/router'
-import store from '@/store'
-
-export default {
-	//刷新标签
-	refresh() {
-		NProgress.start()
-		const route = router.currentRoute.value
-		store.commit("removeKeepLive", route.name)
-		store.commit("setRouteShow", false)
-		nextTick(() => {
-			store.commit("pushKeepLive", route.name)
-			store.commit("setRouteShow", true)
-			NProgress.done()
-		})
-	},
-	//关闭标签
-	close(tag) {
-		const route = tag || router.currentRoute.value
-		store.commit("removeViewTags", route)
-		store.commit("removeIframeList", route)
-		store.commit("removeKeepLive", route.name)
-		const tagList = store.state.viewTags.viewTags
-		const latestView = tagList.slice(-1)[0]
-		if (latestView) {
-			router.push(latestView)
-		} else {
-			router.push('/')
-		}
-	},
-	//关闭标签后处理
-	closeNext(next) {
-		const route = router.currentRoute.value
-		store.commit("removeViewTags", route)
-		store.commit("removeIframeList", route)
-		store.commit("removeKeepLive", route.name)
-		if(next){
-			const tagList = store.state.viewTags.viewTags
-			next(tagList)
-		}
-	},
-	//关闭其他
-	closeOther() {
-		const route = router.currentRoute.value
-		const tagList = [...store.state.viewTags.viewTags]
-		tagList.forEach(tag => {
-			if(tag.meta&&tag.meta.affix || route.fullPath==tag.fullPath){
-				return true
-			}else{
-				this.close(tag)
-			}
-		})
-	},
-	//设置标题
-	setTitle(title){
-		store.commit("updateViewTagsTitle", title)
-	}
-}

+ 4 - 3
src/views/device/index.vue

@@ -17,8 +17,8 @@ import moment from "moment";
 
 const formatOnline = seconds => {
     const hour = XEUtils.floor(XEUtils.divide(seconds, 3600));
-    const minute = XEUtils.floor(XEUtils.divide(XEUtils.subtract(seconds, XEUtils.multiply(hour, 3600)), 60));
-    const second = XEUtils.subtract(XEUtils.subtract(seconds, XEUtils.multiply(hour, 3600)), XEUtils.multiply(minute, 60))
+    const minute = XEUtils.floor(XEUtils.divide(seconds, 60)) % 60;
+    const second = seconds % 60;
 
     if (hour >= 1) return hour + "时" + minute + "分" + second + "秒";
     if (minute >= 1) return minute + "分" + second + "秒";
@@ -32,11 +32,12 @@ const toolbarConfig = reactive({
 });
 
 const pagerConfig = reactive({
+    queryType: "none",
     layouts: ["Total"],
     slots: {
         "total": params => h("div", { class: "table-slot-total" }, ["在线设备", h("span", null, params.total)])
     }
-})
+});
 
 const columns = reactive([
     { type: "seq", width: 60 },

+ 12 - 13
src/views/group/camera.vue

@@ -5,12 +5,11 @@
             <el-button class="m-l-10" type="primary" @click="table_add">新增</el-button>
         </template>
         
-        <scTable ref="xGridTable" min-height="108" max-height="600" :apiObj="$API.carwash.device" framework="zeroLite" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns">
+        <scTable ref="xGridTable" min-height="108" max-height="600" :apiObj="$API.carwash.boxDev" apiKey="child" framework="zeroLite" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns" :pagerConfig="pagerConfig">
             <template #action="{ row }">
                 <el-button type="primary" link @click="table_snap(row)">
                     <template #icon><sc-iconify icon="mingcute:pic-line"></sc-iconify></template>设备抓拍
                 </el-button>
-
                 <el-button type="primary" link @click="table_edit(row)">
                     <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
                 </el-button>
@@ -21,7 +20,7 @@
         </scTable>
     </el-dialog>
 
-    <device-detail v-if="dialog.detail" ref="deviceRef" :nodeType="formConfig.data.nodeType" @success="refreshTable" @closed="dialog.detail = false"></device-detail>
+    <device-detail v-if="dialog.detail" ref="deviceRef" :nodeType="formConfig.data.masterId" @success="refreshTable" @closed="dialog.detail = false"></device-detail>
     <snap-detail v-if="dialog.snap" ref="snapRef" @closed="dialog.snap = false"></snap-detail>
 </template>
 
@@ -29,31 +28,31 @@
 import XEUtils from "xe-utils";
 import API from "@/api";
 import TOOL from "@/utils/tool";
-import { mapFormItemInput } from "@/components/scTable/helper";
 import deviceDetail from "./detail";
 import snapDetail from "./snap";
 
 const visible = ref(false);
 const open = row => {
     visible.value = true;
-    XEUtils.set(formConfig.data, "nodeType", row.id);
+    XEUtils.set(formConfig.data, "masterId", row.id);
 }
 
 const formConfig = reactive({
-    data: {},
-    items: [
-        mapFormItemInput("deviceCode", "设备编号")
-    ]
+    data: {}
 })
 
 const paramsColums = reactive([
-    { type: "relation", column: "nodeType", symbol: "eq" },
-    { type: "relation", column: "deviceCode", symbol: "like" },
-    { type: "orderby", column: "createTime", symbol: "desc" }
+    { type: "expands", column: "masterId" }
 ]);
 
+const pagerConfig = reactive({
+    enabled: false,
+    queryType: "none"
+});
+
 const columns = reactive([
     { type: "html", field: "deviceCode", title: "设备编号", minWidth: 160, sortable: true },
+    { type: "html", field: "features.name", title: "设备名称", minWidth: 160, sortable: true },
     { type: "html", field: "deviceType", title: "设备类型", minWidth: 160, sortable: true },
     { type: "html", field: "CreateTime", title: "创建时间", minWidth: 160, sortable: true, formatter: ({ cellValue, row }) => cellValue || TOOL.dateFormat(row.createTime) },
     { type: "html", field: "UpdateTime", title: "更新时间", minWidth: 160, sortable: true, formatter: ({ cellValue, row }) => cellValue || TOOL.dateFormat(row.updateTime) },
@@ -95,7 +94,7 @@ const table_del = ({ id }) => {
         confirmButtonText: "确定",
         cancelButtonText: "取消"
     }).then(() => {
-        API.carwash.device.del([{ id }]).then(res => {
+        API.carwash.boxDev.del([{ id }]).then(res => {
             if (res.code == 200) {
                 ElMessage.success("操作成功");
                 refreshTable();

+ 31 - 42
src/views/group/detail.vue

@@ -4,7 +4,7 @@
             <el-row>
                 <el-col :md="12" :xs="24">
                     <el-form-item label="设备编号" prop="deviceCode">
-                        <el-input v-model="form.deviceCode" placeholder="请输入设备编号"></el-input>
+                        <el-input v-model="form.deviceCode" :readonly="mode == 'edit'" placeholder="请输入设备编号"></el-input>
                     </el-form-item>
                 </el-col>
                 <el-col :md="12" :xs="24">
@@ -19,47 +19,37 @@
                 </el-col>
                 <template v-if="!props.nodeType">
                     <el-col :md="12" :xs="24">
-                        <el-form-item label="节点类型" prop="nodeType">
-                            <el-select v-model="form.nodeType" placeholder="请选择节点类型">
-                                <el-option v-for="(label, key) in nodeTypeDic" :key="key" :label="label" :value="key"></el-option>
-                            </el-select>
+                        <el-form-item label="冲洗时长" prop="features.minStagnantTime">
+                            <el-input-number v-model="form.features.minStagnantTime" :min="60" :controls="false" placeholder="请输入冲洗时长">
+                                <template #suffix>秒</template>
+                            </el-input-number>
                         </el-form-item>
                     </el-col>
-
-                    <template v-if="form.nodeType == 'master'">
-                        <el-col :md="12" :xs="24">
-                            <el-form-item label="冲洗时长" prop="features.minStagnantTime">
-                                <el-input-number v-model="form.features.minStagnantTime" :min="60" :controls="false" placeholder="请输入冲洗时长">
-                                    <template #suffix>秒</template>
-                                </el-input-number>
-                            </el-form-item>
-                        </el-col>
-                        <el-col :md="12" :xs="24">
-                            <el-form-item label="图片采集间隔" prop="features.minPictureSaveTime">
-                                <el-input-number v-model="form.features.minPictureSaveTime" :min="180" :controls="false" placeholder="请输入图片采集间隔">
-                                    <template #suffix>秒</template>
-                                </el-input-number>
-                            </el-form-item>
-                        </el-col>
-                        <el-col :md="12" :xs="24">
-                            <el-form-item label="超时等待时间" prop="features.maxWaitingTime">
-                                <el-input-number v-model="form.features.maxWaitingTime" :min="60" :controls="false" placeholder="请输入超时等待时间">
-                                    <template #suffix>秒</template>
-                                </el-input-number>
-				                <div class="el-form-item-msg">应为上报时间的3倍</div>
-                            </el-form-item>
-                        </el-col>
-                        <el-col :xs="24">
-                            <el-form-item class="periods-slider-item" label="工作时段">
-                                <el-form-item v-for="index in 4" :key="index" :prop="`periodsSlider.${index - 1}`" :rules="{ validator: validateSlider }" label-width="auto">
-                                    <template #label>
-                                        <el-checkbox v-model="periods.checkbox[index - 1]">时段{{ index }}</el-checkbox>
-                                    </template>
-                                    <el-slider v-model="periods.slider[index - 1]" :format-tooltip="formatTooltip" :marks="periods.marks[index - 1]" range :max="1440" @change="changeSlider(index - 1)" />
-                                </el-form-item>
+                    <el-col :md="12" :xs="24">
+                        <el-form-item label="图片采集间隔" prop="features.minPictureSaveTime">
+                            <el-input-number v-model="form.features.minPictureSaveTime" :min="180" :controls="false" placeholder="请输入图片采集间隔">
+                                <template #suffix>秒</template>
+                            </el-input-number>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :md="12" :xs="24">
+                        <el-form-item label="超时等待时间" prop="features.maxWaitingTime">
+                            <el-input-number v-model="form.features.maxWaitingTime" :min="60" :controls="false" placeholder="请输入超时等待时间">
+                                <template #suffix>秒</template>
+                            </el-input-number>
+                            <div class="el-form-item-msg">应为上报时间的3倍</div>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :xs="24">
+                        <el-form-item class="periods-slider-item" label="工作时段">
+                            <el-form-item v-for="index in 4" :key="index" :prop="`periodsSlider.${index - 1}`" :rules="{ validator: validateSlider }" label-width="auto">
+                                <template #label>
+                                    <el-checkbox v-model="periods.checkbox[index - 1]">时段{{ index }}</el-checkbox>
+                                </template>
+                                <el-slider v-model="periods.slider[index - 1]" :format-tooltip="formatTooltip" :marks="periods.marks[index - 1]" range :max="1440" @change="changeSlider(index - 1)" />
                             </el-form-item>
-                        </el-col>
-                    </template>
+                        </el-form-item>
+                    </el-col>
                 </template>
                 <el-col :xs="24">
                     <el-form-item label="描述">
@@ -105,7 +95,7 @@ const form = ref({
     id: null,
     deviceCode: null,
     deviceType: "easydo.cwashm.v202507",
-    nodeType: null,
+    nodeType: "master",
     features: {
         name: "",
         deviceId: "",
@@ -120,7 +110,6 @@ const rules = reactive({
     deviceType: [{ required: true }],
     deviceCode: [{ required: true, message: "请输入设备编号" }],
     "features.name": [{ required: true, message: "请输入设备名称" }],
-    nodeType: [{ required: true, message: "请选择节点类型" }],
     "features.minStagnantTime": [{ required: true, message: "请输入冲洗时长" }],
     "features.minPictureSaveTime": [{ required: true, message: "请输入图片采集间隔" }],
     "features.maxWaitingTime": [{ required: true, message: "请输入超时等待时间" }]
@@ -203,7 +192,7 @@ const submit = () => {
             form.value.features.workingPeriods = !workingPeriods.length ? ["00:00-23:59"] : workingPeriods;
 
             isSaving.value = true;
-            API.carwash.device[mode.value]([form.value]).then(res => {
+            API.carwash.boxDev[mode.value]([form.value]).then(res => {
                 if (res.code == 200) {
                     isSaving.value = false;
                     ElMessage.success("操作成功");

+ 24 - 30
src/views/group/index.vue

@@ -1,18 +1,15 @@
 <template>
 	<el-container class="is-vertical">
-        <sc-page-header @filter="toggleFormEnabled" @add="table_add"></sc-page-header>
+        <sc-page-header @add="table_add"></sc-page-header>
         
-        <scTable ref="xGridTable" :apiObj="$API.carwash.device" framework="zeroLite" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns">
+        <scTable ref="xGridTable" :apiObj="$API.carwash.boxDev" framework="zeroLite" :toolbarConfig="toolbarConfig" :columns="columns" :pagerConfig="pagerConfig">
             <template #action="{ row }">
-                <template v-if="row.nodeType == 'master'">
-                    <el-button type="primary" link @click="table_camera(row)">
-                        <template #icon><sc-iconify icon="material-symbols:speed-camera-outline"></sc-iconify></template>摄像头
-                    </el-button>
-                    <el-button type="primary" link @click="table_snap(row)">
-                        <template #icon><sc-iconify icon="mingcute:pic-line"></sc-iconify></template>设备抓拍
-                    </el-button>
-                </template>
-
+                <el-button type="primary" link @click="table_camera(row)">
+                    <template #icon><sc-iconify icon="material-symbols:speed-camera-outline"></sc-iconify></template>摄像头
+                </el-button>
+                <el-button type="primary" link @click="table_snap(row)">
+                    <template #icon><sc-iconify icon="mingcute:pic-line"></sc-iconify></template>设备抓拍
+                </el-button>
                 <el-button type="primary" link @click="table_edit(row)">
                     <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
                 </el-button>
@@ -32,8 +29,8 @@
 import XEUtils from "xe-utils";
 import API from "@/api";
 import TOOL from "@/utils/tool";
-import { mapFormItemInput } from "@/components/scTable/helper";
 import { nodeTypeDic } from "@/utils/basicDic";
+
 import deviceDetail from "./detail";
 import cameraDetail from "./camera";
 import snapDetail from "./snap";
@@ -43,26 +40,18 @@ const toolbarConfig = reactive({
     print: false
 });
 
-const formConfig = reactive({
-    data: {
-        nodeType: XEUtils.keys(nodeTypeDic)
-    },
-    items: [
-        mapFormItemInput("deviceCode", "设备编号")
-    ]
+const pagerConfig = reactive({
+    queryType: "expands"
 });
 
-const paramsColums = reactive([
-    { type: "relation", column: "nodeType", symbol: "or" },
-    { type: "relation", column: "deviceCode", symbol: "like" },
-    { type: "orderby", column: "createTime", symbol: "desc" }
-])
-
 const columns = reactive([
     { type: "seq", width: 60 },
     { type: "html", field: "deviceCode", title: "设备编号", minWidth: 160, sortable: true },
+    { type: "html", field: "features.name", title: "设备名称", minWidth: 160, sortable: true },
     { type: "html", field: "deviceType", title: "设备类型", minWidth: 160, sortable: true },
-    { type: "html", field: "nodeType", title: "节点类型", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(nodeTypeDic, cellValue, cellValue) },
+    { type: "html", field: "features.minStagnantTime", title: "冲洗时长", minWidth: 100, sortable: true },
+    { type: "html", field: "features.minPictureSaveTime", title: "图片采集间隔", minWidth: 120, sortable: true },
+    { type: "html", field: "features.maxWaitingTime", title: "超时等待时间", minWidth: 120, sortable: true },
     { type: "html", field: "CreateTime", title: "创建时间", minWidth: 160, sortable: true, formatter: ({ cellValue, row }) => cellValue || TOOL.dateFormat(row.createTime) },
     { type: "html", field: "UpdateTime", title: "更新时间", minWidth: 160, sortable: true, formatter: ({ cellValue, row }) => cellValue || TOOL.dateFormat(row.updateTime) },
     { title: "操作", fixed: "right", width: 300, align: "center", slots: { default: "action" } }
@@ -70,7 +59,6 @@ const columns = reactive([
 
 // 显示隐藏 筛选表单
 const xGridTable = ref();
-const toggleFormEnabled = () => xGridTable.value.toggleFormEnabled();
 const refreshTable = (mode = "add") => {
     xGridTable.value.reloadColumn(columns);
     xGridTable.value.searchData(mode);
@@ -111,10 +99,16 @@ const table_del = ({ id }) => {
         confirmButtonText: "确定",
         cancelButtonText: "取消"
     }).then(() => {
-        API.carwash.device.del([{ id }]).then(res => {
+        API.carwash.boxDev.child({ expands: { masterId: id } }).then(res => {
             if (res.code == 200) {
-                ElMessage.success("操作成功");
-                refreshTable();
+                if (!(res.datas || []).length) {
+                    API.carwash.boxDev.del([{ id }]).then(res => {
+                        if (res.code == 200) {
+                            ElMessage.success("操作成功");
+                            refreshTable();
+                        } else throw res.message;
+                    }).catch(error => ElMessage.error(error));
+                } else ElMessage.warning("当前设备存在摄像头,请移除后再试");
             } else throw res.message;
         }).catch(error => ElMessage.error(error));
     });

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 133 - 0
src/views/login/index.vue


+ 6 - 73
src/views/record/index.vue

@@ -1,8 +1,8 @@
 <template>
 	<el-container class="is-vertical">
-        <sc-page-header @filter="toggleFormEnabled"></sc-page-header>
+        <sc-page-header></sc-page-header>
         
-        <scTable ref="xGridTable" :apiObj="$API.carwash.journal" framework="zeroLite" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns">
+        <scTable :apiObj="$API.carwash.journal" framework="zeroLite" :toolbarConfig="toolbarConfig" :columns="columns" :pagerConfig="pagerConfig">
             <template #action="{ row }">
                 <el-button type="primary" link @click="table_process(row)">
                     <template #icon><sc-iconify icon="material-symbols:view-timeline-outline"></sc-iconify></template>过程记录
@@ -15,12 +15,10 @@
 </template>
 
 <script setup>
-import moment from "moment";
 import XEUtils from "xe-utils";
-import API from "@/api";
 import TOOL from "@/utils/tool";
-import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker } from "@/components/scTable/helper";
-import { stateDic, objectToArray } from "@/utils/basicDic";
+
+import { stateDic } from "@/utils/basicDic";
 import processDetail from "./process";
 
 const toolbarConfig = reactive({
@@ -28,66 +26,10 @@ const toolbarConfig = reactive({
     print: false
 });
 
-const monthConfig = reactive({
-    span: 3,
-    showTitle: false,
-    props: {
-        type: "month",
-        valueFormat: "YYYY-MM-01",
-        clearable: false
-    },
-    resetValue: moment().format("YYYY-MM-01"),
-    events: {
-        change: ({ field, data }) => XEUtils.merge(formConfig.data, { ...data, createTime: [moment(data.zone).startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment(data.zone).endOf("month").format("YYYY-MM-DD HH:mm:ss")] })
-    }
-})
-
-const daterangeConfig = reactive({
-    span: 9,
-    showTitle: false,
-    resetValue: [moment().startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")],
-    props: {
-        type: "datetimerange",
-        startPlaceholder: "开始时间",
-        endPlaceholder: "结束时间",
-        format: "YYYY-MM-DD HH:mm",
-        disabledDate: date => moment(date).diff(moment(formConfig.data.zone).endOf("month")) > 0 || moment(formConfig.data.zone).startOf("month").diff(moment(date)) > 0
-    }
-})
-
-const stateConfig = reactive({
-    options: objectToArray(stateDic),
-    events: {
-        change: data => XEUtils.merge(formConfig.data, data)
-    }
-})
-
-const formConfig = reactive({
-    data: {
-        zone: moment().format("YYYY-MM-01"),
-        createTime: [moment().startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")]
-    },
-    items: [
-        mapFormItemDatePicker("zone", "", monthConfig),
-        mapFormItemDatePicker("createTime", "", daterangeConfig),
-        mapFormItemInput("deviceCode", "设备编号"),
-        mapFormItemInput("plate", "车牌号"),
-        mapFormItemInput("carType", "车辆类型"),
-        mapFormItemSelect("state", "状态", stateConfig)
-    ]
+const pagerConfig = reactive({
+    queryType: "expands"
 });
 
-const paramsColums = reactive([
-    { type: "relation", column: "deviceCode", symbol: "like" },
-    { type: "relation", column: "plate", symbol: "like" },
-    { type: "relation", column: "carType", symbol: "like" },
-    { type: "relation", column: "state", symbol: "eq" },
-    { type: "relation", column: "createTime", symbol: "eqgt", field: "createTime[0]", formatValue: value => moment(value).format("YYYY-MM-DDTHH:mm:ss[Z]") },
-    { type: "relation", column: "createTime", symbol: "eqlt", field: "createTime[1]", formatValue: value => moment(value).format("YYYY-MM-DDTHH:mm:ss[Z]") },
-    { type: "orderby", column: "createTime", symbol: "desc" },
-    { type: "expands", column: "zone" }
-])
-
 const columns = reactive([
     { type: "seq", width: 60 },
     { type: "html", field: "deviceCode", title: "设备编号", minWidth: 160, sortable: true },
@@ -101,15 +43,6 @@ const columns = reactive([
     { title: "操作", fixed: "right", minWidth: 100, align: "center", slots: { default: "action" } }
 ])
 
-// 显示隐藏 筛选表单
-const xGridTable = ref();
-const toggleFormEnabled = () => xGridTable.value.toggleFormEnabled();
-
-const refreshTable = () => {
-    xGridTable.value.reloadColumn(columns);
-    xGridTable.value.searchData();
-}
-
 const processRef = ref();
 const dialog = ref(false);
 const table_process = row => {

+ 8 - 3
src/views/record/process.vue

@@ -6,7 +6,10 @@
                 <div class="timeline-item" v-for="(item, index) in eventTable" :key="`${item.id}.${index}`">
                     <el-image :src="imagePrefix + item.path" :preview-src-list="XEUtils.map(eventTable, e => imagePrefix + e.path)"></el-image>
                     <div class="timeline-item__content">
-                        <el-image v-for="snapshot in item.snapshots" :key="`${item.id}.${index}.${snapshot.eventId}`" :src="imagePrefix + snapshot.path" :preview-src-list="XEUtils.map(item.snapshots, s => imagePrefix + s.path)"></el-image>
+                        <div class="snapshot-item" v-for="snapshot in item.snapshots" :key="`${item.id}.${index}.${snapshot.eventId}`" >
+                            <el-image :src="imagePrefix + snapshot.path" :preview-src-list="XEUtils.map(item.snapshots, s => imagePrefix + s.path)"></el-image>
+                            <div>{{ snapshot.name }}</div>
+                        </div>
                     </div>
                     <div class="timeline-item__timestamp">{{ item.payload.time }}</div>
                 </div>
@@ -17,7 +20,6 @@
 
 <script setup>
 import XEUtils from "xe-utils";
-import API from "@/api";
 
 const visible = ref(false);
 const imagePrefix = ref("/zeroapi/v1/ossminiv2/store/cwashmstore/");
@@ -53,6 +55,9 @@ defineExpose({
 .timeline-item .el-image {width: 100%;overflow: unset;}
 .timeline-item .timeline-item__timestamp {line-height: 1;font-size: 13px;color: #909399;}
 .timeline-item .timeline-item__content {display: flex;align-items: center;overflow: auto;padding: 10px 0;}
-.timeline-item .timeline-item__content .el-image {flex-shrink: 0;width: unset;height: 40px;margin-right: 10px;}
+.timeline-item .timeline-item__content .snapshot-item {text-align: center;font-size: 13px;}
+.timeline-item .timeline-item__content .el-image {flex-shrink: 0;width: unset;height: 40px;}
+
 .timeline-item + .timeline-item {margin-left: 15px;}
+.snapshot-item + .snapshot-item {margin-left: 10px;}
 </style>

+ 2 - 2
vue.config.js

@@ -2,8 +2,8 @@ const { defineConfig } = require("@vue/cli-service")
 
 module.exports = defineConfig({
 	//设置为空打包后不分更目录还是多级目录
-	publicPath: '/easydo/carwash',
-    outputDir: 'easydo/carwash',
+	publicPath: '/easydo/aibox',
+    outputDir: 'easydo/aibox',
 	//build编译后存放静态文件的目录
 	// assetsDir: "static",