From 2dfc8dc4bcd3d3cd4aed1b6054e073e140eba9c8 Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Mon, 19 May 2025 16:05:50 +0800
Subject: [PATCH] Merge branch 'master' into jiangwu
---
src/views/device/airport.vue | 623 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 files changed, 600 insertions(+), 23 deletions(-)
diff --git a/src/views/device/airport.vue b/src/views/device/airport.vue
index 5b630a2..e751aec 100644
--- a/src/views/device/airport.vue
+++ b/src/views/device/airport.vue
@@ -57,43 +57,89 @@
<template #firmware_status="{ row }">
<div
v-if="row.firmware_status == 1"
- style="display: flex; flex-direction: row; align-items: center"
+ style="display: flex; flex-direction: column; align-items: center"
>
- <div style="background-color: #f1f1f1; border-radius: 4px">无需升级</div>
+ <el-tag type="info">无需升级</el-tag>
+ <el-tag type="primary" effect="dark" class="active-element" @click="rollFirmware(row)"
+ >版本管理</el-tag
+ >
+ <!-- <div style="background-color: #f1f1f1; border-radius: 4px">无需升级</div> -->
</div>
<div
v-if="row.firmware_status == 2"
- style="display: flex; flex-direction: row; align-items: center"
+ style="display: flex; flex-direction: column; align-items: center"
>
- <div @click="updateFirmware(row)" class="firmware_status">升级固件</div>
+ <!-- <el-tag type="success" effect="dark" class="active-element" @click="updateFirmware(row)"
+ >升级固件</el-tag
+ > -->
+ <el-tag type="primary" effect="dark" class="active-element" @click="rollFirmware(row)"
+ >版本管理</el-tag
+ >
+ <!-- <div @click="updateFirmware(row)" class="firmware_status">升级固件</div> -->
</div>
<div
v-if="row.firmware_status == 3"
style="display: flex; flex-direction: row; align-items: center"
>
- <span style="margin-left: 5px">一致性升级</span>
+ <el-tag type="success" effect="dark" class="active-element" @click="updateFirmware(row)"
+ >一致性升级</el-tag
+ >
+ <!-- <div @click="updateFirmware(row)" class="firmware_status">一致性升级</div> -->
</div>
<div
v-if="row.firmware_status == 4"
style="display: flex; flex-direction: row; align-items: center"
>
- <div style="background-color: #a8cbfb; border-radius: 4px">
- 升级中
- <el-progress v-if="row.firmware_progress>0 && row.firmware_progress<100" :percentage="row.firmware_progress"></el-progress>
- </div>
+ <el-tag effect="dark" class="active-element" @click="updateFirmware(row)">升级中</el-tag>
+ <el-progress
+ v-if="row.firmware_progress > 0 && row.firmware_progress < 100"
+ :percentage="row.firmware_progress"
+ ></el-progress>
</div>
+ </template>
+ <template #mode_code="{ row }">
+ <span
+ class="text"
+ v-if="row.domain == 3"
+ :style="row.mode_code != '-1' ? 'color: #00ee8b' : 'color: #de5e5e'"
+ >
+ {{ getDockModeText(row.mode_code) }}
+ </span>
</template>
<template #menu="scope">
<el-button
type="primary"
text
- icon="el-icon-collection-tag"
- v-if="permission.deviceFirmware_manage"
- @click.stop="handleOpenFirmwarm(scope.row, scope.index)"
- >固件管理
+ icon="el-icon-paperclip"
+ v-if="permission.oss_set"
+ @click.stop="handleOpenOssSet(scope.row, scope.index)"
+ >存储配置
+ </el-button>
+ <el-button
+ type="primary"
+ text
+ icon="el-icon-share"
+ v-if="permission.per_share && scope.row.domain == 3"
+ @click.stop="handleOpenDevicePerShare(scope.row, scope.index)"
+ >机场授权
+ </el-button>
+
+ <el-button
+ type="primary"
+ text
+ icon="el-icon-position"
+ :disabled="!scope.row.status"
+ v-if="permission.rang_con && scope.row.domain == 3"
+ @click.stop="handleOpenRemoteDebugging(scope.row, scope.index)"
+ >远程调试
</el-button>
</template>
+ <!-- 添加行政区划显示模板 -->
+ <template #area_code="{ row }">
+ <span>{{ row.area_name }}</span>
+ </template>
</avue-crud>
+
<el-dialog title="固件升级" append-to-body v-model="firmwareBox" width="455px">
<div>升级固件版本:{{ firmwareVersion }}</div>
<template #footer>
@@ -103,14 +149,84 @@
</span>
</template>
</el-dialog>
+
+ <el-dialog
+ title="存储配置"
+ append-to-body
+ v-model="ossSetBox"
+ @close="handleOssSetClose"
+ width="600px"
+ >
+ <div style="display: flex; flex-direction: row; align-items: center">
+ 存储对象配置:
+ <el-select v-model="ossEnd" clearable placeholder="请选择存储对象" style="width: 450px">
+ <el-option
+ v-for="item in ossList"
+ :key="item.id"
+ :label="item.name + ': ' + item.endpoint"
+ :value="item.id"
+ >
+ <span style="float: left">{{ item.name + ': ' + item.endpoint }}</span>
+ <span v-if="item.status == 1" style="float: right; color: #8492a6; font-size: 13px">{{
+ item.categoryName
+ }}</span>
+ <span v-if="item.status == 2" style="float: right; color: #40cb8b; font-size: 13px">{{
+ item.categoryName
+ }}</span>
+ </el-option>
+ </el-select>
+ </div>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="cancelOssSet">取 消</el-button>
+ <el-button type="danger" @click="cancelOssSetConfirm">取消配置</el-button>
+ <el-button type="primary" @click="ossSetConfirm">配置</el-button>
+ </span>
+ </template>
+ </el-dialog>
+
+ <el-dialog title="固件版本管理" append-to-body v-model="rollFirmwareBox" width="455px">
+ <div style="display: flex; flex-direction: row; align-items: center">
+ 固件版本升级/回退:
+ <el-select
+ v-model="rollFirmVersion"
+ clearable
+ placeholder="请选择固件版本"
+ style="width: 240px"
+ >
+ <el-option
+ v-for="item in firmList"
+ :key="item.firmware_version"
+ :label="item.firmware_version"
+ :value="item.firmware_version"
+ />
+ </el-select>
+ </div>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="rollFirmwareBox = false">取 消</el-button>
+ <el-button type="primary" @click="rollFirmwareConfirm">确 定</el-button>
+ </span>
+ </template>
+ </el-dialog>
+
<el-drawer
- title="固件管理"
+ title="机场授权管理"
size="60%"
append-to-body
- v-model="firmwareManageVisible"
+ v-model="devicePerShareVisible"
:direction="'rtl'"
>
- <FirmwareManage ref="firmwareManage" />
+ <DevicePerShare ref="devicePerShare" />
+ </el-drawer>
+
+ <el-drawer :title="operateTitle" append-to-body v-model="remoteDebuggingShow" size="40%">
+ <DockControlPanel
+ v-if="remoteDebuggingShow"
+ :sn="curDeviceInfo.device_sn"
+ :deviceInfo="curDeviceInfo"
+ >
+ </DockControlPanel>
</el-drawer>
</basic-container>
</template>
@@ -124,15 +240,45 @@
getDetail,
getDeviceUpgradeInfo,
ota,
+ getDeviceFirmwareList,
+ getDevices,
} from '@/api/device/device';
+import { deleteByOssId, addOrUpdate as addOrUpdateOssBind } from '@/api/device/ossBind';
+import { getListPage as getOssList } from '@/api/resource/oss';
+import { getLazyTree } from '@/api/base/region';
+import { EDockModeText, EDockModeCode } from '@/types/device';
import { mapGetters } from 'vuex';
import FirmwareManage from './components/firmwareManage.vue';
+import DevicePerShare from './components/devicePerShare.vue';
+import DockControlPanel from './components/DockControlPanel.vue';
+import { getWebsocketUrl } from '@/websocket/util/config';
+import ConnectWebSocket from '@/websocket';
export default {
components: {
FirmwareManage,
+ DevicePerShare,
+ DockControlPanel,
},
data() {
return {
+ bindOssId: null,
+ deviceSn: '',
+ ossSetBox: false,
+ ossList: [],
+ ossEnd: '',
+ operateTitle: '',
+ rollFirmVersion: '',
+ firmList: [],
+ rollFirmwareBox: false,
+ // 用于记录已经创建的websocket,防止重复创建
+ webSocketIdSet: new Set(),
+ // 用于保存多组websocket
+ websocketMap: new Map(),
+ scheduleNum: 0,
+ webSocket: '',
+ devicePerShareVisible: false,
+ remoteDebuggingShow: false,
+ curDeviceInfo: {},
firmwareManageVisible: false,
percentageNum: 0,
firmwareBox: false,
@@ -152,7 +298,7 @@
tree: true,
height: '460',
calcHeight: 32,
- dialogWidth: 950,
+ dialogWidth: 1050,
tip: false,
searchShow: true,
searchMenuSpan: 6,
@@ -199,6 +345,7 @@
label: '机巢编号',
prop: 'machine_nest_sn',
labelWidth: 130,
+ editDisabled: true,
rules: [
{
required: false,
@@ -306,6 +453,74 @@
],
},
{
+ label: '行政区划',
+ prop: 'area_code',
+ type: 'cascader',
+ labelWidth: 130,
+ hide: true,
+ props: {
+ label: 'title',
+ value: 'value',
+ emitPath: false,
+ checkStrictly: true,
+ multiple: false,
+ expandTrigger: 'hover',
+ },
+ dataType: 'string',
+ rules: [
+ {
+ required: true,
+ message: '请选择行政区划',
+ trigger: 'change',
+ },
+ ],
+ change: ({ value }) => {
+ if (!value) {
+ this.form.area_code = null;
+ } else {
+ if (typeof value === 'string' && value.includes(',')) {
+ const codes = value.split(',');
+ this.form.area_code = codes[codes.length - 1];
+ } else {
+ this.form.area_code = value;
+ }
+ }
+ },
+ lazy: true,
+ lazyLoad(node, resolve) {
+ let level = node.level;
+ let list = [];
+ let callback = () => {
+ resolve(
+ (list || []).map(ele => ({
+ ...ele,
+ value: ele.value,
+ leaf: level >= 2,
+ }))
+ );
+ };
+
+ if (level === 0) {
+ getLazyTree('000000000000').then(res => {
+ list = res.data.data;
+ callback();
+ });
+ } else if (level === 1) {
+ getLazyTree(node.value).then(res => {
+ list = res.data.data;
+ callback();
+ });
+ } else if (level === 2) {
+ getLazyTree(node.value).then(res => {
+ list = res.data.data;
+ callback();
+ });
+ } else {
+ callback();
+ }
+ },
+ },
+ {
label: '在线时间',
prop: 'login_time',
type: 'date',
@@ -336,6 +551,22 @@
{
required: true,
message: '请输入在线状态',
+ trigger: 'blur',
+ },
+ ],
+ },
+ {
+ label: '机场状态',
+ prop: 'mode_code',
+ addDisplay: false,
+ editDisplay: false,
+ labelWidth: 130,
+ slot: true,
+ width: 100,
+ rules: [
+ {
+ required: true,
+ message: '请输入机场状态',
trigger: 'blur',
},
],
@@ -375,9 +606,109 @@
return ids.join(',');
},
},
+ destroyed() {
+ this.websocketMap.forEach((k, v) => {
+ if (null != v) {
+ v.close();
+ this.websocketMap.delete(k);
+ this.webSocketIdSet.delete(k);
+ }
+ });
+ },
methods: {
+ getDockModeText(value) {
+ return EDockModeText[value] || '';
+ },
+ // 关闭所有的webscoket
+ closeAllWebsoket() {
+ this.websocketMap.forEach((k, v) => {
+ if (null != v) {
+ v.close();
+ this.websocketMap.delete(k);
+ this.webSocketIdSet.delete(k);
+ }
+ });
+ },
+ // 连接webSocket
+ connectWebSocket(data) {
+ const webSocketId = data.workspace_id;
+ if (!this.webSocketIdSet.has(webSocketId)) {
+ // 防止重复连接
+ this.webSocketIdSet.add(webSocketId);
+ const webSorketUrl = getWebsocketUrl() + '&workspace-id=' + data.workspace_id;
+ // 监听ws 消息
+ this.useConnectWebSocket(webSocketId, webSorketUrl);
+ }
+ },
+ useConnectWebSocket(webSocketId, url) {
+ const websocket = new ConnectWebSocket(url);
+ // 加入 webscoket map
+ this.websocketMap.set(webSocketId, websocket);
+ websocket?.registerMessageHandler(this.messageHandler);
+ websocket?.initSocket();
+ },
+ async messageHandler(payload) {
+ if (!payload) {
+ return;
+ }
+ if (payload.biz_code != 'ota_progress') {
+ return;
+ }
+ var data = payload.data;
+ this.data.forEach(e => {
+ if (e.device_sn == data.sn && data.output.status == 'in_progress') {
+ e.firmware_progress = data.output.progress.percent;
+ }
+
+ if (e.device_sn == data.sn && data.output.status == 'ok') {
+ // 升级完成修改状态
+ e.firmware_status = 1;
+ const webscoket = this.websocketMap.get(e.workspace_id);
+ // 关闭
+ webscoket?.close();
+ //删除对应的webscoket
+ this.websocketMap.delete(e.workspace_id);
+ this.webSocketIdSet.delete(e.workspace_id);
+ }
+ });
+ },
init() {
- this.onLoad(this.page);
+ (this.page = {
+ pageSize: 10,
+ currentPage: 1,
+ total: 0,
+ }),
+ this.onLoad(this.page);
+ },
+ // 打开权限分享页面
+ handleOpenDevicePerShare(row) {
+ var that = this;
+ this.devicePerShareVisible = true;
+ this.$nextTick(() => {
+ that.$refs.devicePerShare.init(row);
+ });
+ },
+ // 打开远程调试
+ handleOpenRemoteDebugging(row) {
+ this.curDeviceInfo = row;
+ this.remoteDebuggingShow = true;
+ this.operateTitle = row.nickname + ' - ' + row.device_sn;
+ },
+ // 打开存储对象配置页面
+ handleOpenOssSet(row) {
+ var that = this;
+ this.deviceSn = row.device_sn;
+ this.ossSetBox = true;
+ this.bindOssId = row.oss_id;
+ this.ossEnd = row.oss_id;
+ // 查询下拉列表
+ const param = {
+ categoryKeys: "1,3,6"
+ }
+ getOssList(1, 50,param).then(res => {
+ const data = res.data.data;
+ that.ossList = data.records;
+ });
},
// 打开固件管理页面
handleOpenFirmwarm(row) {
@@ -385,6 +716,144 @@
this.firmwareManageVisible = true;
this.$nextTick(() => {
that.$refs.firmwareManage.init(row);
+ });
+ },
+ // 版本回退点击事件
+ rollFirmware(row) {
+ if (row.status == 0) {
+ this.$message({
+ type: 'warning',
+ message: '设备不在线!',
+ });
+ return;
+ }
+ // 如果是机场,查询飞机对应的信息
+ if (row.domain === 3) {
+ // 查询子设备名称
+ getDevices(row.workspace_id, row.child_sn).then(res => {
+ const data = res.data.data;
+ that.firmwareInfo['child_device_name'] = data.device_name;
+ that.firmwareInfo['child_version'] = data.firmware_version;
+ that.firmwareInfo['child_status'] = data.status;
+ });
+ }
+ var that = this;
+ this.rollFirmwareBox = true;
+ const param = {
+ device_name: row.device_name,
+ };
+ // 获取固件最新版本信息
+ getDeviceFirmwareList(1, 20, param).then(res => {
+ that.firmList = [];
+ const data = res.data.data.records;
+ if (data.length === 0) {
+ return;
+ }
+ data.forEach(e => {
+ if (e.firmware_version != row.firmware_version) {
+ that.firmList.push(e);
+ }
+ });
+ that.firmwareInfo['device_name'] = row.device_name;
+ that.firmwareInfo['sn'] = row.device_sn;
+ // 3-普通升级,2-一致性升级;
+ that.firmwareInfo['firmware_upgrade_type'] = 3;
+ that.firmwareInfo['workspaceId'] = row.workspace_id;
+ that.firmwareInfo['firmware_version'] = row.firmware_version;
+ that.firmwareInfo['status'] = row.status;
+ // domain 0:飞机 3:机场
+ that.firmwareInfo['domain'] = row.domain;
+ that.firmwareInfo['child_sn'] = row.child_sn;
+ that.firmwareInfo['now_version'] = row.firmware_version;
+ });
+ },
+ // 比较版本大小
+ compare(version1, version2) {
+ let arr1 = version1.split('.');
+ let arr2 = version2.split('.');
+ let length = Math.max(arr1.length, arr2.length);
+ for (let i = 0; i < length; i++) {
+ const n1 = Number(arr1[i] || 0);
+ const n2 = Number(arr2[i] || 0);
+ // version1 > version2 返回1,如果 version1 < version2 返回-1,不然返回0
+ if (n1 > n2) return 1;
+ if (n1 < n2) return -1;
+ }
+ return 0;
+ },
+ // 版本回退
+ rollFirmwareConfirm() {
+ // 无人机升级/回滚
+ if (this.rollFirmVersion == '') {
+ this.$message({
+ type: 'warning',
+ message: '请先选择回退的固件版本!',
+ });
+ return;
+ }
+ var that = this;
+ var arr = [];
+ that.firmwareInfo['product_version'] = this.rollFirmVersion;
+ // 机场信息设置
+ arr.push(that.firmwareInfo);
+ // 判断是机场升级还是飞机升级,如果是机场升级则把无人机信息一起带过去
+ if (that.firmwareInfo.domain === 3) {
+ // 判断当前机场和无人机固件版本是否一致
+ var dockFlag = this.compare(
+ this.firmwareInfo.firmware_version,
+ this.firmwareInfo.child_version
+ );
+ // 机场和无人机版本一致情况
+ if (dockFlag == 0) {
+ if (!this.firmwareInfo.status || !this.firmwareInfo.child_status) {
+ this.$message({
+ type: 'warning',
+ message: '请先更新无人机固件版本后再进行机场固件版本更新!',
+ });
+ return;
+ }
+ // 只有同时在线并且是升级才能操作
+ // 判断升级还是降级,不能降级
+ var upDownFlag = this.compare(this.rollFirmVersion, this.firmwareInfo.firmware_version);
+ if (upDownFlag == -1) {
+ this.$message({
+ type: 'warning',
+ message: '请先回滚无人机固件版本后再进行机场固件版本更新!',
+ });
+ return;
+ }
+ // 同时升级操作
+ // 无人机信息设置
+ arr.push({
+ device_name: that.firmwareInfo.child_device_name,
+ sn: that.firmwareInfo.child_sn,
+ product_version: this.rollFirmVersion,
+ firmware_upgrade_type: 3,
+ });
+ } else {
+ // 版本不一致情况
+ // 比对需要改变的版本号和无人机版本号是否一致,不一致提示不能操作
+ var verFlag = this.compare(this.rollFirmVersion, this.firmwareInfo.child_version);
+ if (verFlag == -1 || verFlag == 1) {
+ this.$message({
+ type: 'warning',
+ message: '更新版本与当前无人机固件版本不一致,请更新和无人机一致的版本!',
+ });
+ return;
+ }
+ // 需要改变的版本号和无人机版本号如果是一致,进行单独的机场升级/回滚操作
+ }
+ }
+ ota(that.firmwareInfo.workspaceId, arr).then(res => {
+ that.firmwareVersion = '';
+ that.firmwareInfo = {};
+ that.rollFirmwareBox = false;
+ this.$message({
+ type: 'success',
+ message: '操作成功!',
+ });
+ this.onLoad(this.page);
+ done();
});
},
// 升级固件按钮事件
@@ -411,7 +880,7 @@
that.firmwareInfo['device_name'] = row.device_name;
that.firmwareInfo['sn'] = row.device_sn;
that.firmwareInfo['product_version'] = data[0].product_version;
- // 1-普通升级,2-一致性升级;
+ // 3-普通升级,2-一致性升级;
that.firmwareInfo['firmware_upgrade_type'] =
row.firmware_status === that.DeviceFirmwareStatusEnum.ToUpgraded
? that.DeviceFirmwareTypeEnum.ToUpgraded
@@ -436,7 +905,75 @@
done();
});
},
+ handleOssSetClose() {
+ this.ossEnd = '';
+ this.deviceSn = '';
+ this.bindOssId = null;
+ },
+ // 存储对象配置确定
+ ossSetConfirm() {
+ var that = this;
+ // 判断是否已选择
+ if (!this.ossEnd) {
+ this.$message({
+ type: 'warning',
+ message: '请选择对于的存储对象!',
+ });
+ return;
+ }
+ // 新增或者修改操作
+ const data = {
+ oss_id: this.ossEnd,
+ sn: this.deviceSn,
+ };
+ // 新增
+ addOrUpdateOssBind(data).then(res => {
+ this.$message({
+ type: 'success',
+ message: '操作成功!',
+ });
+ that.ossSetBox = false;
+ this.onLoad(this.page);
+ done();
+ });
+ },
+ // 取消配置
+ cancelOssSetConfirm() {
+ var that = this;
+ // 判断是否已有配置
+ if (!this.bindOssId) {
+ this.$message({
+ type: 'warning',
+ message: '当前机场尚未配置存储对象信息!',
+ });
+ return;
+ }
+ deleteByOssId(this.bindOssId).then(() => {
+ that.ossSetBox = false;
+ this.onLoad(this.page);
+ this.$message({
+ type: 'success',
+ message: '操作成功!',
+ });
+ });
+ },
+ // 取消按钮 oss
+ cancelOssSet() {
+ this.ossEnd = '';
+ this.deviceSn = '';
+ this.bindOssId = null;
+ this.ossSetBox = false;
+ },
rowSave(row, done, loading) {
+ let areaCode = row.area_code;
+ if (Array.isArray(areaCode)) {
+ areaCode = areaCode[areaCode.length - 1];
+ } else if (typeof areaCode === 'string' && areaCode.includes(',')) {
+ const codes = areaCode.split(',');
+ areaCode = codes[codes.length - 1];
+ }
+ row.area_code = areaCode || null;
+
add(row).then(
() => {
this.onLoad(this.page);
@@ -453,7 +990,12 @@
);
},
rowUpdate(row, index, done, loading) {
- update(row).then(
+ const submitData = {
+ ...row,
+ area_code: row.area_code.split(',').pop(),
+ };
+
+ update(submitData).then(
() => {
this.onLoad(this.page);
this.$message({
@@ -524,10 +1066,32 @@
this.$refs.crud.toggleSelection();
});
},
+ getFullAreaCode(areaCode) {
+ if (!areaCode) return '';
+
+ const code = areaCode.toString();
+
+ if (code.includes(',')) return code;
+
+ if (code.endsWith('0000000000')) {
+ return code;
+ } else if (code.endsWith('00000000') && !code.endsWith('0000000000')) {
+ const provinceCode = code.substring(0, 2) + '0000000000';
+ return `${provinceCode},${code}`;
+ } else {
+ const provinceCode = code.substring(0, 2) + '0000000000';
+ const cityCode = code.substring(0, 4) + '00000000';
+ return `${provinceCode},${cityCode},${code}`;
+ }
+ },
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
- this.form = res.data.data;
+ const data = res.data.data;
+ this.form = {
+ ...data,
+ area_code: this.getFullAreaCode(data.area_code),
+ };
});
}
done();
@@ -542,6 +1106,7 @@
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
+ // this.closeAllWebsoket();
const { releaseTimeRange } = this.query;
let values = {
...params,
@@ -562,6 +1127,9 @@
this.page.total = data.total;
data.records.forEach(e => {
e['hasChildren'] = e.has_children;
+ if (e.firmware_status == 4) {
+ this.connectWebSocket(e);
+ }
});
this.data = data.records;
this.loading = false;
@@ -573,14 +1141,15 @@
childSn: tree.child_sn,
};
getList(1, 10, params).then(res => {
- resolve(res.data.data.records);
+ const data = res.data.data.records;
+ resolve(data);
});
},
},
};
</script>
-<style>
+<style lang="scss" scoped>
.firmware_status {
background-color: aqua;
border-radius: 4px;
@@ -589,4 +1158,12 @@
cursor: pointer;
/* color: aqua; */
}
+
+.active-element {
+ cursor: pointer;
+ &:hover {
+ background-color: #409eff;
+ border-color: #409eff;
+ }
+}
</style>
--
Gitblit v1.9.3