Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev

This commit is contained in:
blp 2023-01-10 11:11:04 +08:00
parent 3dbe4ea2af
commit af223a16b8
33 changed files with 3719 additions and 1252 deletions

11
components.d.ts vendored
View File

@ -23,12 +23,23 @@ declare module '@vue/runtime-core' {
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASpin: typeof import('ant-design-vue/es')['Spin']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AInput: typeof import('ant-design-vue/es')['Input']
AInputGroup: typeof import('ant-design-vue/es')['InputGroup']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AModal: typeof import('ant-design-vue/es')['Modal']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
AUpload: typeof import('ant-design-vue/es')['Upload']
BadgeStatus: typeof import('./src/components/BadgeStatus/index.vue')['default']
CardBox: typeof import('./src/components/CardBox/index.vue')['default']
GeoComponent: typeof import('./src/components/GeoComponent/index.vue')['default']
MonacoEditor: typeof import('./src/components/MonacoEditor/index.vue')['default']
PermissionButton: typeof import('./src/components/PermissionButton/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ValueItem: typeof import('./src/components/ValueItem/index.vue')['default']

1145
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,10 +11,12 @@
"prettier": "prettier --write"
},
"dependencies": {
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@vuemap/vue-amap": "^1.1.20",
"ant-design-vue": "^3.2.15",
"axios": "^1.2.1",
"js-cookie": "^3.0.1",
"echarts": "^5.4.1",
"less": "^4.1.3",
"less-loader": "^11.1.0",
"lodash-es": "^4.17.21",
@ -24,7 +26,8 @@
"unplugin-auto-import": "^0.12.1",
"unplugin-vue-components": "^0.22.12",
"vue": "^3.2.45",
"vue-router": "^4.1.6"
"vue-router": "^4.1.6",
"jetlinks-store": "^0.0.3"
},
"devDependencies": {
"@types/lodash-es": "^4.17.6",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 551 KiB

View File

@ -0,0 +1,3 @@
<svg width="18" height="15" viewBox="0 0 18 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 1.12527V13.5414C0 14.2185 0.476821 14.6667 1.20199 14.6667H16.798C17.5132 14.6667 18 14.2185 18 13.5414V1.12527C18 0.448201 17.5232 0 16.798 0H1.20199C0.486755 0 0 0.448201 0 1.12527ZM1.20199 1.12527H16.798V4.51062H1.20199V1.12527ZM16.798 13.5414H1.20199V10.156H16.798V13.5414ZM1.20199 9.03077V5.63589H16.798V9.02124L1.20199 9.03077ZM13.798 2.25054H15C15.3576 2.25054 15.596 2.47941 15.596 2.81318C15.596 3.15648 15.3576 3.37581 15 3.37581H13.798C13.4404 3.37581 13.202 3.14694 13.202 2.81318C13.202 2.47941 13.4404 2.25054 13.798 2.25054ZM15.596 7.33333C15.596 7.67664 15.3576 7.89597 15 7.89597H13.798C13.4404 7.89597 13.202 7.6671 13.202 7.33333C13.202 6.99957 13.4404 6.7707 13.798 6.7707H15C15.3576 6.7707 15.596 6.99003 15.596 7.33333ZM13.202 11.844C13.202 11.5006 13.4404 11.2813 13.798 11.2813H15C15.3576 11.2813 15.596 11.5102 15.596 11.844C15.596 12.1873 15.3576 12.4066 15 12.4066H13.798C13.4404 12.4161 13.202 12.1873 13.202 11.844Z" fill="#597EF7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,3 @@
<svg width="12" height="16" viewBox="0 0 12 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.49722 15.875C3.66823 15.875 4.61944 14.9238 4.61944 13.7528C4.61944 12.8205 4.01688 12.0285 3.17936 11.7424V4.39228C4.01688 4.10616 4.61944 3.31412 4.61944 2.38186C4.61944 1.21085 3.66823 0.259636 2.49722 0.259636C1.32621 0.259636 0.375 1.21085 0.375 2.38186C0.375 3.31412 0.977559 4.10805 1.81508 4.39228V11.7424C0.977559 12.0285 0.375 12.8205 0.375 13.7528C0.375 14.9238 1.32621 15.875 2.49722 15.875ZM1.86055 14.384C1.69051 14.2174 1.59256 13.9908 1.5877 13.7528C1.59256 13.5148 1.69051 13.2882 1.86055 13.1216C2.03059 12.9549 2.25916 12.8616 2.49722 12.8616C2.73527 12.8616 2.96385 12.9549 3.13388 13.1216C3.30392 13.2882 3.40188 13.5148 3.40674 13.7528C3.40188 13.9908 3.30392 14.2174 3.13388 14.384C2.96385 14.5506 2.73527 14.6439 2.49722 14.6439C2.25916 14.6439 2.03059 14.5506 1.86055 14.384ZM3.13388 1.75252C3.30392 1.91913 3.40188 2.14574 3.40674 2.38375C3.40188 2.62176 3.30392 2.84838 3.13388 3.01498C2.96385 3.18158 2.73527 3.2749 2.49722 3.2749C2.25916 3.2749 2.03059 3.18158 1.86055 3.01498C1.69051 2.84838 1.59256 2.62176 1.5877 2.38375C1.59256 2.14574 1.69051 1.91913 1.86055 1.75252C2.03059 1.58592 2.25916 1.49261 2.49722 1.49261C2.73527 1.49261 2.96385 1.58592 3.13388 1.75252ZM11.3016 12.8713H9.84711V11.2344H9.84952V4.25811C10.687 3.97199 11.2896 3.17994 11.2896 2.24768C11.2896 1.07667 10.3384 0.125463 9.16738 0.125463C7.99637 0.125463 7.04516 1.07667 7.04516 2.24768C7.04516 3.17994 7.64772 3.97388 8.48524 4.25811V10.4632L8.48351 10.4633L8.48524 10.8587V11.2344H8.48688L8.49405 12.8734H7.04163C6.91267 12.8734 6.8429 13.0214 6.92113 13.1208L9.05219 15.8163C9.0663 15.8346 9.0844 15.8494 9.1051 15.8595C9.12581 15.8697 9.14857 15.875 9.17164 15.875C9.19471 15.875 9.21747 15.8697 9.23818 15.8595C9.25889 15.8494 9.27699 15.8346 9.29109 15.8163L11.4222 13.1187C11.5004 13.0193 11.4285 12.8713 11.3016 12.8713ZM10.0769 2.2485C10.072 2.0105 9.97405 1.78388 9.80401 1.61727C9.63397 1.45067 9.4054 1.35736 9.16735 1.35736C8.92929 1.35736 8.70072 1.45067 8.53068 1.61727C8.36064 1.78388 8.26268 2.0105 8.25782 2.2485C8.26268 2.48651 8.36064 2.71313 8.53068 2.87973C8.70072 3.04634 8.92929 3.13965 9.16735 3.13965C9.4054 3.13965 9.63397 3.04634 9.80401 2.87973C9.97405 2.71313 10.072 2.48651 10.0769 2.2485Z" fill="#597EF7"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,61 @@
<template>
<template v-if="isPermission">
<template v-if="popConfirm">
<a-popconfirm v-bind="popConfirm" :disabled="!isPermission || props.disabled">
<a-tooltip v-if="tooltip" v-bind="tooltip">
<slot v-if="noButton"></slot>
<a-button v-else v-bind="buttonProps" :disabled="_isPermission"></a-button>
</a-tooltip>
<a-button v-else v-bind="buttonProps" :disabled="_isPermission"></a-button>
</a-popconfirm>
</template>
<template v-else-if="tooltip">
<a-tooltip v-bind="tooltip">
<slot v-if="noButton"></slot>
<a-button v-else v-bind="buttonProps" :disabled="_isPermission"></a-button>
</a-tooltip>
</template>
<template v-else>
<slot v-if="noButton"></slot>
<a-button v-else v-bind="buttonProps" :disabled="_isPermission"></a-button>
</template>
</template>
<a-tooltip v-else title="没有权限">
<slot v-if="noButton"></slot>
<a-button v-else v-bind="buttonProps" :disabled="_isPermission"></a-button>
</a-tooltip>
</template>
<script setup lang="ts">
import type { ButtonProps, TooltipProps, PopconfirmProps } from 'ant-design-vue'
import { usePermissionStore } from '@/store/permission';
interface PermissionButtonProps extends ButtonProps {
tooltip?: TooltipProps;
popConfirm?: PopconfirmProps;
hasPermission?: string | Array<string>;
noButton?: boolean;
}
const props = withDefaults(defineProps<PermissionButtonProps>(), {
noButton: false
})
const { tooltip, popConfirm, hasPermission, noButton, ...buttonProps } = props;
const permissionStore = usePermissionStore()
const isPermission = computed(() => {
if (!props.hasPermission) {
return true
}
return permissionStore.hasPermission(props.hasPermission)
})
const _isPermission = computed(() =>
'hasPermission' in props && isPermission
? 'disabled' in buttonProps
? buttonProps.disabled
: false
: true
)
</script>
<style scoped lang="scss">
</style>

View File

@ -1,8 +1,10 @@
import type { App } from 'vue'
import AIcon from './AIcon'
import PermissionButton from './PermissionButton/index.vue'
export default {
install(app: App) {
app.component('AIcon', AIcon)
app.component('PermissionButton', PermissionButton)
}
}

View File

@ -28,9 +28,26 @@ export default [
path: '/demo',
component: () => import('@/views/demo/index.vue')
},
{
path: '/bind',
component: () => import('@/views/account/Center/bind/index.vue')
},
{
path: '/iot/home',
component: () => import('@/views/iot/home/index.vue')
},
// end: 测试用, 可删除
{
path: '/link/log',
component: () => import('@/views/link/Log/index.vue')
},
{
path: '/link/certificate',
component: () => import('@/views/link/Certificate/index.vue')
},
{
path: '/link/certificate/detail/add',
component: () => import('@/views/link/Certificate/Detail/index.vue')
},
]

View File

@ -28,3 +28,39 @@ export const StatusColorEnum = {
'warning': 'warning',
'default': 'default',
}
class SystemConst {
static API_BASE = 'api';
static SYSTEM_NAME = 'Jetlinks';
static LOGIN = 'LOGIN-STATUS';
static DOC_URL = 'http://doc.jetlinks.cn';
static BASE_CURD_MODAL_VISIBLE = 'BASE_CURD_MODAL_VISIBLE';
static BASE_CURD_CURRENT = 'BASE_CURD_CURRENT';
static BASE_CURD_MODEL = 'BASE_CURD_MODEL';
static BASE_UPDATE_DATA = 'BASE_UPDATE_DATA';
static GLOBAL_WEBSOCKET = 'GLOBAL-WEBSOCKET';
static BIND_USER_STATE = 'false';
static REFRESH_METADATA = 'refresh_metadata';
static REFRESH_METADATA_TABLE = 'refresh_metadata_table';
static GET_METADATA = 'get_metadata';
static REFRESH_DEVICE = 'refresh_device';
static AMAP_KEY = 'amap_key';
static Version_Code = 'version_code';
}
export default SystemConst;

View File

@ -66,10 +66,10 @@ export const patch = function(url: string, data = {}) {
* GET method request
* @param {String} url
* @param {Object} [params]
* @param {Object} [ext],
* @param {Object} [ext]
* @returns {AxiosInstance}
*/
export const get = function(url: string, params = {}, ext: any) {
export const get = function(url: string, params = {}, ext?: any) {
return request({
method: 'GET',
url,
@ -82,10 +82,10 @@ export const get = function(url: string, params = {}, ext: any) {
* DELETE method request
* @param {String} url
* @param {Object} [params]
* @param {Object} [ext],
* @param {Object} [ext]
* @returns {AxiosInstance}
*/
export const remove = function(url: string, params = {}, ext: any) {
export const remove = function(url: string, params = {}, ext?: any) {
return request({
method: 'DELETE',
url,

View File

@ -0,0 +1,259 @@
<!-- 第三方账户绑定 -->
<template>
<div class="page-container">
<div class="content">
<div class="title">第三方账户绑定</div>
<!-- 已登录-绑定三方账号 -->
<template v-if="false">
<div class="info">
<a-card style="width: 280px">
<template #title>
<div class="info-head">
<img :src="getImage('/bind/Rectangle.png')" />
<span>个人信息</span>
</div>
</template>
<div class="info-body">
<img :src="getImage('/bind/jetlinksLogo.png')" />
<p>账号admin</p>
<p>用户名超级管理员</p>
</div>
</a-card>
<img :src="getImage('/bind/Vector.png')" />
<a-card style="width: 280px">
<template #title>
<div class="info-head">
<img :src="getImage('/bind/Rectangle.png')" />
<span>三方账户信息</span>
</div>
</template>
<div class="info-body">
<img :src="getImage('/bind/wechat-webapp.png')" />
<p>用户名-</p>
<p>名称微信昵称</p>
</div>
</a-card>
</div>
<div class="btn">
<a-button type="primary">立即绑定</a-button>
</div>
</template>
<!-- 未登录-绑定三方账号 -->
<template v-else>
<div class="not-login">
<div class="logo">
<img :src="getImage('/bind/jetlinksLogo.png')" />
<img
class="arrow"
:src="getImage('/bind/Vector.png')"
/>
<img :src="getImage('/bind/wechat-webapp.png')" />
</div>
<div class="desc">
你已通过微信授权,完善以下登录信息即可以完成绑定
</div>
<div class="login-form">
<a-form layout="vertical">
<a-form-item
label="账户"
v-bind="validateInfos.username"
>
<a-input
v-model:value="formData.username"
placeholder="请输入账户"
/>
</a-form-item>
<a-form-item
label="密码"
v-bind="validateInfos.password"
>
<a-input-password
v-model:value="formData.password"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item
label="验证码"
v-bind="validateInfos.captcha"
>
<a-input
v-model:value="formData.captcha"
placeholder="请输入验证码"
>
<template #addonAfter>
<span style="cursor: pointer">
图形验证码
</span>
</template>
</a-input>
</a-form-item>
<a-form-item>
<a-button
type="primary"
@click="handleSubmit"
style="width: 100%"
>
登录并绑定账户
</a-button>
</a-form-item>
</a-form>
</div>
</div>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { getImage } from '@/utils/comm';
import { Form } from 'ant-design-vue';
const useForm = Form.useForm;
interface formData {
username: string;
password: string;
captcha: string;
}
const formData = ref<formData>({
username: '',
password: '',
captcha: '',
});
const formRules = ref({
username: [
{
required: true,
message: '请输入账户',
},
],
password: [
{
required: true,
message: '请输入密码',
},
],
captcha: [
{
required: true,
message: '请输入验证码',
},
],
});
const { resetFields, validate, validateInfos } = useForm(
formData.value,
formRules.value,
);
/**
* 登录并绑定账户
*/
const handleSubmit = () => {
validate()
.then(() => {
console.log('toRaw:', toRaw(formData.value));
console.log('formData.value:', formData.value);
})
.catch((err) => {
console.log('error', err);
});
};
</script>
<style lang="less" scoped>
.page-container:deep {
.ant-form-item-label
> label.ant-form-item-required:not(
.ant-form-item-required-mark-optional
)::before {
display: none;
}
.ant-form-item-label > label {
font-weight: bold;
}
}
.page-container {
width: 100%;
height: 100vh;
background: url(/images/bind/bindPage.png) 0% 0% / 100% 100% no-repeat;
display: flex;
align-items: center;
justify-content: center;
.content {
box-sizing: border-box;
width: 850px;
min-height: 510px;
background: #fff;
border: 1px solid #e0e4e8;
border-radius: 2px;
.title {
margin: 30px 0;
color: #0f1222;
font-weight: 400;
font-size: 20px;
font-family: 'PingFang SC';
font-style: normal;
line-height: 25px;
text-align: center;
}
// -
.info {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
&-head {
display: flex;
align-items: baseline;
gap: 10px;
}
&-body {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
img {
width: 70px;
height: 70px;
}
}
}
.btn {
display: flex;
justify-content: center;
margin-top: 30px;
}
//
.not-login {
display: flex;
flex-direction: column;
align-items: center;
.logo {
display: flex;
align-items: center;
gap: 20px;
img {
width: 50px;
height: 50px;
}
.arrow {
width: 15px;
height: 15px;
}
}
.desc {
margin-top: 30px;
margin-bottom: 30px;
font-size: 14px;
font-family: 'PingFang SC';
font-style: normal;
line-height: 14px;
opacity: 0.75;
mix-blend-mode: normal;
}
}
}
}
</style>

View File

@ -2,7 +2,7 @@
<template>
<div class="page-container">
父级: {{ testValue }}
<ViewItem v-model="testValue" />
<ValueItem v-model="testValue" />
<!-- 卡片 -->
<br />卡片组件
<a-row :gutter="20">
@ -41,7 +41,6 @@
</template>
<script setup lang="ts">
import ViewItem from '@/components/ValueItem/index.vue';
import CardBox from '@/components/CardBox/index.vue';
import { StatusColorEnum } from '@/utils/consts';
import { getImage } from '@/utils/comm';

View File

@ -13,20 +13,34 @@
<div class="box-item">
<div class="label">CPU使用率</div>
<div class="value">{{ cpu }}</div>
<div class="chart" ref="cpuChart"></div>
<Pie
class="chart"
chart-ref="cpuChart"
:value="20"
:color-arr="['#ebebeb', '#d3adf7']"
image="/images/home/top-3.svg"
/>
</div>
<div class="box-item">
<div class="label">JVM内存</div>
<div class="value">{{ jvm }}</div>
<div class="chart" ref="jvmChart"></div>
<Pie
class="chart"
chart-ref="jvmChart"
:value="31"
:color-arr="['#d6e4ff', '#85a5ff']"
image="/images/home/top-4.svg"
/>
</div>
</div>
</a-card>
</template>
<script setup lang="ts">
const cpu = ref('0%');
const jvm = ref('10%');
import Pie from './Pie.vue';
const cpu = ref('20%');
const jvm = ref('31%');
const jumpPage = () => {};
</script>

View File

@ -6,12 +6,16 @@
<div class="box">
<div
class="box-item"
v-for="(item,index) in cardData"
v-for="(item, index) in cardData"
@click="jumpPage(item)"
>
<div class="item-english">{{ item.english }}</div>
<div class="item-title">{{ item.label }}</div>
<img class="item-image" :src="`/images/home/${index + 1}.png`" alt="" />
<img
class="item-image"
:src="`/images/home/${index + 1}.png`"
alt=""
/>
</div>
</div>
</a-card>
@ -25,7 +29,7 @@ type configItem = {
link: string;
english: string;
label: string;
save?: boolean;
params?: object;
};
const router = useRouter();
@ -35,13 +39,25 @@ const props = defineProps({
});
const { cardData, cardTitle } = toRefs(props);
const jumpPage = (linkItem: configItem): void => {
if (linkItem.auth && linkItem.link) {
router.push(`${linkItem.link}${linkItem.save ? '?save=true' : ''}`);
const jumpPage = (row: configItem): void => {
if (row.auth && row.link) {
router.push(`${row.link}${objToParams(row.params || {})}`);
} else {
message.warning('暂无权限,请联系管理员');
}
};
const objToParams = (source: object): string => {
if (Object.prototype.toString.call(source) === '[object Object]') {
const paramsArr = <any>[];
Object.entries(source).forEach(([prop, value]) => {
if (typeof value === 'object') value = JSON.stringify(value);
paramsArr.push(`${prop}=${value}`);
});
if (paramsArr.length > 0) return '?' + paramsArr.join('&');
}
return '';
};
</script>
<style lang="less" scoped>

View File

@ -0,0 +1,137 @@
<template>
<a-card class="boot-card-container" :bordered="false">
<template #title>
<h5 class="title">{{ cardTitle }}</h5>
</template>
<div class="box">
<div
class="box-item"
v-for="(item, index) in cardData"
@click="jumpPage(item)"
>
<div class="item-content">
<div class="icon">
<img :src="item.image" alt="" />
</div>
<div class="intro">
<div class="item-english">{{ item.english }}</div>
<div class="item-label">{{ item.label }}</div>
</div>
</div>
<div class="right-bj">
<img :src="`/images/home/home-${index + 1}.png`" alt="" />
</div>
</div>
</div>
</a-card>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue';
type configItem = {
auth: boolean;
link: string;
english: string;
label: string;
params?: object;
image: string;
};
const router = useRouter();
const props = defineProps({
cardData: Array<configItem>,
cardTitle: String,
});
const { cardData, cardTitle } = toRefs(props);
const jumpPage = (row: configItem): void => {
if (row.auth && row.link) {
router.push(`${row.link}${objToParams(row.params || {})}`);
} else {
message.warning('暂无权限,请联系管理员');
}
};
const objToParams = (source: object): string => {
if (Object.prototype.toString.call(source) === '[object Object]') {
const paramsArr = <any>[];
// 使for ints
Object.entries(source).forEach(([prop, value]) => {
if (typeof value === 'object') value = JSON.stringify(value);
paramsArr.push(`${prop}=${value}`);
});
if (paramsArr.length > 0) return '?' + paramsArr.join('&');
}
return '';
};
</script>
<style lang="less" scoped>
.boot-card-container {
:deep(.ant-card-body) {
padding-top: 0;
}
.title {
position: relative;
z-index: 2;
display: flex;
justify-content: space-between;
margin-bottom: 12px;
padding-left: 18px;
font-weight: 700;
font-size: 18px;
&::before {
position: absolute;
top: 50%;
left: 0;
width: 8px;
height: 8px;
background-color: #1d39c4;
border: 1px solid #b4c0da;
transform: translateY(-50%);
content: ' ';
}
}
.box {
font-size: 14px;
.box-item {
cursor: pointer;
position: relative;
border-width: 1px 1px 1px 2px;
border-style: solid;
border-color: rgb(238, 238, 238) rgb(238, 238, 238)
rgb(238, 238, 238) rgb(133, 165, 255);
padding: 11px;
background: linear-gradient(
135.62deg,
#f6f7fd 22.27%,
rgba(255, 255, 255, 0.86) 91.82%
);
border-radius: 2px;
box-shadow: 0 4px 18px #efefef;
&:not(:first-child) {
margin-top: 12px;
}
.item-content {
display: flex;
align-items: center;
.item-label {
color: #252526;
font-size: 18px;
font-weight: 700;
}
}
.right-bj {
position: absolute;
right: 10%;
bottom: 0;
width: 37px;
}
}
}
}
</style>

View File

@ -0,0 +1,190 @@
<template>
<div class="comprehensive-home-conatiner">
<a-row :gutter="24" class="top" style="margin-bottom: 24px">
<a-col :span="6" class="left">
<BootCardSmall
:cardData="devBootConfig"
cardTitle="物联网引导"
/>
<div style="width: 100%; height: 24px"></div>
<BootCardSmall :cardData="opsBootConfig" cardTitle="运维引导" />
</a-col>
<a-col :span="18" class="right">
<a-row :gutter="24">
<a-col :span="12"><DeviceCountCard /></a-col>
<a-col :span="12"><BasicCountCard /></a-col>
<a-col :span="24" style="margin-top: 24px">
<PlatformPicCard image="/images/home/content1.png" />
</a-col>
</a-row>
</a-col>
</a-row>
<StepCard
cardTitle="设备接入推荐步骤"
tooltip="不同的设备因为通信协议的不同,存在接入步骤的差异"
:dataList="devStepDetails"
style="margin-bottom: 24px"
/>
<StepCard
cardTitle="运维管理推荐步骤"
tooltip="请根据业务需要对下述步骤进行选择性操作。"
:dataList="opsStepDetails"
/>
</div>
</template>
<script setup lang="ts" name="ComprehensiveHome">
import BootCardSmall from '../BootCardSmall.vue';
import DeviceCountCard from '../DeviceCountCard.vue';
import BasicCountCard from '../BasicCountCard.vue';
import PlatformPicCard from '../PlatformPicCard.vue';
import { recommendList } from '../../index';
import StepCard from '../StepCard.vue';
// -
const devBootConfig = [
{
english: 'STEP1',
label: '创建产品',
link: '/a',
auth: true,
save: true,
image: '/images/home/guide-home1.png',
},
{
english: 'STEP2',
label: '创建设备',
link: '/b',
auth: true,
save: true,
image: '/images/home/guide-home2.png',
},
{
english: 'STEP3',
label: '规则引擎',
link: '/c',
auth: false,
save: true,
image: '/images/home/guide-home3.png',
},
];
// -
const opsBootConfig = [
{
english: 'STEP1',
label: '创建产品',
link: '/a',
auth: true,
save: true,
image: '/images/home/guide-home4.png',
},
{
english: 'STEP2',
label: '创建设备',
link: '/b',
auth: true,
save: true,
image: '/images/home/guide-home5.png',
},
{
english: 'STEP3',
label: '规则引擎',
link: '/c',
auth: false,
save: true,
image: '/images/home/guide-home6.png',
},
];
// -
const devStepDetails = [
{
title: '创建产品',
details:
'产品是设备的集合,通常指一组具有相同功能的设备。物联设备必须通过产品进行接入方式配置。',
iconUrl: '/images/home/bottom-4.png',
linkUrl: '/a',
auth: true,
},
{
title: '配置产品接入方式',
details:
'通过产品对同一类型的设备进行统一的接入方式配置。请参照设备铭牌说明选择匹配的接入方式。',
iconUrl: '/images/home/bottom-1.png',
linkUrl: '/a',
auth: true,
dialogTag: 'accessMethod',
},
{
title: '添加测试设备',
details: '添加单个设备,用于验证产品模型是否配置正确。',
iconUrl: '/images/home/bottom-5.png',
linkUrl: '/a',
auth: true,
},
{
title: '功能调试',
details:
'对添加的测试设备进行功能调试,验证能否连接到平台,设备功能是否配置正确。',
iconUrl: '/images/home/bottom-2.png',
linkUrl: '/a',
auth: true,
dialogTag: 'funcTest',
},
{
title: '批量添加设备',
details: '批量添加同一产品下的设备',
iconUrl: '/images/home/bottom-3.png',
linkUrl: '/a',
auth: false,
},
] as recommendList[];
// -
const opsStepDetails = [
{
title: '协议管理',
details:
'根据业务需求自定义开发对应的产品(设备模型)接入协议,并上传到平台。',
iconUrl: '/images/home/bottom-1.png',
linkUrl: '/a',
auth: true,
params: {
a: 1,
save: true,
},
},
{
title: '证书管理',
details: '统一维护平台内的证书,用于数据通信加密。',
iconUrl: '/images/home/bottom-6.png',
linkUrl: '/a',
auth: true,
params: {
a: 1,
save: false,
},
},
{
title: '网络组件',
details: '根据不同的传输类型配置平台底层网络组件相关参数。',
iconUrl: '/images/home/bottom-3.png',
linkUrl: '/a',
auth: true,
},
{
title: '设备接入网关',
details: '根据不同的传输类型,关联消息协议,配置设备接入网关相关参数。',
iconUrl: '/images/home/bottom-4.png',
linkUrl: '/a',
auth: true,
},
{
title: '日志管理',
details: '监控系统日志,及时处理系统异常。',
iconUrl: '/images/home/bottom-5.png',
linkUrl: '/a',
auth: false,
},
];
</script>

View File

@ -26,7 +26,6 @@ import BootCard from '../BootCard.vue';
import DeviceCountCard from '../DeviceCountCard.vue';
import PlatformPicCard from '../PlatformPicCard.vue';
import StepCard from '../StepCard.vue';
// import {getImage} from '@/utils/comm'
// -
@ -36,21 +35,21 @@ const bootConfig = [
label: '创建产品',
link: '/a',
auth: true,
save: true,
params: {},
},
{
english: 'STEP2',
label: '创建设备',
link: '/b',
auth: true,
save: true,
params: {},
},
{
english: 'STEP3',
label: '规则引擎',
link: '/c',
auth: false,
save: true,
params: {},
},
];
// -

View File

@ -2,12 +2,43 @@
<div class="init-home-container">
<div class="title">请选择首页视图</div>
<div class="choose-view">
<a-row class="view-content" :gutter="24">
<a-col
:span="8"
class="select-item"
:class="{ selected: selectId === '1' }"
@click="selectId = '1'"
>
<img src="/images/home/device.png" alt="" />
</a-col>
<a-col
:span="8"
class="select-item"
:class="{ selected: selectId === '2' }"
@click="selectId = '2'"
>
<img src="/images/home/ops.png" alt="" />
</a-col>
<a-col
:span="8"
class="select-item"
:class="{ selected: selectId === '3' }"
@click="selectId = '3'"
>
<img src="/images/home/comprehensive.png" alt="" />
</a-col>
</a-row>
<a-button type="primary" class="btn" @click="confirm">确定</a-button>
</div>
</div>
</template>
<script lang="ts" setup></script>
<script lang="ts" setup>
const selectId = ref('1');
const confirm = ()=>{}
</script>
<style lang="less" scoped>
.init-home-container {
@ -26,5 +57,29 @@
font-size: 26px;
text-align: center;
}
.choose-view {
width: 100%;
.view-content {
display: flex;
flex-flow: row wrap;
.select-item {
border: 2px solid transparent;
img {
width: 100%;
}
&.selected {
border-color: #10239e;
}
}
}
.btn {
display: block;
margin: 48px auto;
margin-bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,64 @@
<template>
<div :ref="props.chartRef"></div>
</template>
<script setup lang="ts">
import * as echarts from 'echarts';
const { proxy } = getCurrentInstance();
const props = defineProps({
chartRef: String,
value: Number,
image: String,
colorArr: Array<string>,
});
const options = computed(() => ({
color: props.colorArr || ['#D3ADF7', '#979AFF'],
graphic: [
{
type: 'image',
style: {
image: props.image || '',
width: 16,
height: 16,
},
left: 'center',
top: '41%',
},
],
series: [
{
type: 'pie',
radius: ['100%', '60%'],
center: ['50%', '50%'],
label: {
show: false,
},
data: [100 - (props.value || 0), props.value],
itemStyle: {
borderColor: '#fff',
borderWidth: 2,
},
},
],
}));
watch(options, () => {
initChart();
});
const initChart = () => {
nextTick(() => {
const myChart = echarts.init(proxy.$refs[props.chartRef]);
myChart.clear();
myChart.setOption(options.value);
window.addEventListener('resize', () => {
myChart.resize();
});
});
};
initChart();
</script>

View File

@ -4,11 +4,19 @@
<span>平台架构图</span>
<p>PLATFORM ARCHITECTURE DIAGRAM</p>
</div>
<img src="/images/home/content.png" class="bj" alt="" />
<img
:src="props.image || '/images/home/content.png'"
class="bj"
alt=""
/>
</a-card>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
const props = defineProps({
image: String,
});
</script>
<style lang="less" scoped>
:deep(.ant-card-body) {
@ -18,7 +26,7 @@
position: relative;
width: 100%;
overflow: hidden;
min-height: 480px;
border-bottom: 1px solid #2f54eb;
.bj {
display: block;
@ -36,12 +44,12 @@
padding: 24px 40px;
font-weight: 700;
font-size: 18px;
p{
p {
position: absolute;
top: 50px;
width: 100%;
opacity: .3;
opacity: 0.3;
font-size: 12px;
}

View File

@ -23,37 +23,54 @@
<div class="box-details">{{ item.details }}</div>
</div>
</div>
<div class="dialogs">
<AccessMethodDialog :open-number="openAccess" />
</div>
</a-card>
</template>
<script setup lang="ts">
import { PropType } from 'vue';
import { QuestionCircleOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
interface listConfig {
title: string;
details: string;
iconUrl: string;
linkUrl: string;
params?: object;
auth: boolean;
}
import AccessMethodDialog from './dialogs/AccessMethodDialog.vue';
import { recommendList } from '../index';
const props = defineProps({
cardTitle: String,
tooltip: String,
dataList: Array<listConfig>,
dataList: Array as PropType<recommendList[]>,
});
const router = useRouter();
const { cardTitle, tooltip, dataList } = toRefs(props);
const openAccess = ref<number>(0);
const openFunc = ref<number>(0);
const jumpPage = (row: listConfig) => {
if (row.auth && row.linkUrl) {
router.push(`${row.linkUrl}${row.params ? '?save=true' : ''}`);
} else {
message.warning('暂无权限,请联系管理员');
const jumpPage = (row: recommendList) => {
if (!row.auth) return message.warning('暂无权限,请联系管理员');
else if (row.dialogTag == 'accessMethod') return (openAccess.value += 1);
else if (row.dialogTag === 'funcTest') return (openFunc.value += 1);
else if (row.linkUrl) {
router.push(`${row.linkUrl}${objToParams(row.params || {})}`);
}
};
const objToParams = (source: object): string => {
if (Object.prototype.toString.call(source) === '[object Object]') {
const paramsArr = <any>[];
// 使for ints
Object.entries(source).forEach(([prop, value]) => {
if (typeof value === 'object') value = JSON.stringify(value);
paramsArr.push(`${prop}=${value}`);
});
if (paramsArr.length > 0) return '?' + paramsArr.join('&');
}
return '';
};
</script>
<style lang="less" scoped>

View File

@ -0,0 +1,55 @@
<template>
<a-modal
v-model:visible="visible"
title="选择产品"
style="width: 500px"
@ok="handleOk"
show-search
:filter-option="filterOption"
>
<a-select
v-model:value="productId"
style="width: 100%"
:options="productList"
>
</a-select>
<template #footer>
<a-button key="back" @click="visible = false">取消</a-button>
<a-button key="submit" type="primary" @click="handleOk"
>确认</a-button
>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { productItem } from '../../index';
const props = defineProps({
openNumber: Number,
});
const emits = defineEmits(['confirm']);
const { openNumber } = toRefs(props);
const visible = ref<boolean>(false);
const productId = ref<string>('');
const productList = ref<[productItem] | []>([]);
const getOptions = () => {
productList.value = [];
};
const handleOk = () => {
emits('confirm', productId.value);
visible.value = false;
};
const filterOption = (input: string, option: any) => {
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
watch(openNumber, () => {
visible.value = true;
productId.value = '';
getOptions();
});
</script>
<style scoped></style>

15
src/views/iot/home/index.d.ts vendored Normal file
View File

@ -0,0 +1,15 @@
// 推荐步骤每项
export interface recommendList {
title: string;
details: string;
iconUrl: string;
linkUrl: string;
params?: object;
auth: boolean;
dialogTag?: 'accessMethod' | 'funcTest';
}
// 产品列表里的每项
export interface productItem {
label: string;
value: string
}

View File

@ -4,7 +4,9 @@
<div class="left"></div>
<div class="content iot-home-container">
<!-- <InitHome /> -->
<DeviceHome />
<!-- <DeviceHome /> -->
<!-- <DevOpsHome /> -->
<ComprehensiveHome />
</div>
</div>
</template>
@ -12,6 +14,9 @@
<script lang="ts" setup>
import InitHome from './components/InitHome/index.vue';
import DeviceHome from './components/DeviceHome/index.vue';
import DevOpsHome from './components/DevOpsHome/index.vue';
import ComprehensiveHome from './components/ComprehensiveHome/index.vue';
</script>
<style lang="less" scoped>
@ -30,14 +35,12 @@ import DeviceHome from './components/DeviceHome/index.vue';
background: #352d85;
width: 210px;
min-height: calc(100vh - 48px);
}
.content {
margin: 24px;
width: calc(100vw - 258px);
width: calc(100vw - 280px);
overflow: hidden;
min-height: calc(100vh - 96px);
}
}
// .iot-home-container {
// }
</style>

View File

@ -0,0 +1,88 @@
<template>
<a-spin :spinning="loading">
<div>
<a-textarea
:rows="4"
@change="textChange"
v-model="keystoreBase64"
:placeholder=placeholder
/>
<a-upload
accept=".pem"
listType="text"
:action="action"
:headers="headers"
:showUploadList="false"
@change="handleChange"
>
<a-button style="margin-top: 10px">
<upload-outlined></upload-outlined>
上传文件</a-button>
</a-upload>
</div>
</a-spin>
</template>
<script>
// import storage from 'store'
// import { ACCESS_TOKEN } from '@/store/mutation-types'
// import { ACCESS_TOKEN_KEY } from '@/utils/consts'
import { UploadOutlined } from '@ant-design/icons-vue';
export default {
name: 'CertificateFile',
data () {
return {
keystoreBase64: '',
loading: false,
action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76',
headers:{
authorization: 'authorization-text',
}
// action: process.env.VUE_APP_BASE_API + `/network/certificate/upload`,
// headers: {
// [ACCESS_TOKEN_KEY]: storage.get(ACCESS_TOKEN)
// }
}
},
model: {
prop: 'value',
event: 'change'
},
props: {
value: {
type: String,
default: () => ''
},
placeholder: {
type: String,
default: () => ''
}
},
watch: {
value: {
handler (v) {
this.keystoreBase64 = v
}
}
},
methods: {
handleChange (info) {
this.loading = true
if (info.file.status === 'done') {
this.$message.success('上传成功!')
const result = info.file.response?.result
this.loading = false
this.$emit('change', result)
}
},
textChange (val) {
this.$emit('change', val)
}
}
}
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,207 @@
<template>
<a-card>
<a-row :gutter="[24, 24]" style="padding: 24px">
<a-col :span="12">
<a-form
class="form"
layout="vertical"
:model="formData"
name="basic"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
autocomplete="off"
@finish="onFinish"
@finishFailed="onFinishFailed"
>
<a-form-item
label="证书标准"
name="type"
:rules="[
{
required: true,
message: '请选择证书标准',
},
]"
>
<a-radio-group v-model:value="formData.type">
<a-radio-button
class="form-radio-button"
value="common"
>
<img :src="getImage('/certificate.png')" />
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item
label="证书名称"
name="name"
:rules="[
{
required: true,
message: '请输入证书名称',
},
]"
>
<a-input
placeholder="请输入证书名称"
v-model:value="formData.name"
/>
</a-form-item>
<a-form-item
label="证书文件"
name="cert"
:rules="[
{
required: true,
message: '上传证书文件',
},
]"
>
<CertificateFile
placeholder='证书格式以"-----BEGIN CERTIFICATE-----"开头,以"-----END CERTIFICATE-----"结尾"'
/>
</a-form-item>
<a-form-item
label="证书私钥"
name="key"
:rules="[
{
required: true,
message: '请上传证书私钥',
},
]"
>
<CertificateFile
placeholder='证书私钥格式以"-----BEGIN (RSA|EC) PRIVATE KEY-----"开头,以"-----END(RSA|EC) PRIVATE KEY-----"结尾。'
/>
</a-form-item>
<a-form-item label="说明" name="description">
<a-textarea
placeholder="请输入说明"
v-model:value="formData.description"
:maxlength="200"
:rows="3"
showCount
/>
</a-form-item>
<a-form-item>
<a-button
class="form-submit"
html-type="submit"
type="primary"
>保存</a-button
>
</a-form-item>
</a-form>
</a-col>
<a-col :span="12">
<div class="doc">
<h1>1. 概述</h1>
<div>
证书由受信任的数字证书颁发机构CA在验证服务器身份后颁发具有服务器身份验证和数据传输加密功能保障设备与平台间的数据传输安全配置后可被网络组件引用
</div>
<h1>2. 配置说明</h1>
<h2>1证书文件</h2>
<div>
您可以使用文本编辑工具打开PEM或者CRT格式的证书文件复制其中的内容并粘贴到该文本框或者单击该文本框下的上传并选择存储在本地计算机的证书文件将文件内容上传到文本框
</div>
<h2>2证书私钥</h2>
<div>
填写证书私钥内容的PEM编码
您可以使用文本编辑工具打开KEY格式的证书私钥文件复制其中的内容并粘贴到该文本框或者单击该文本框下的上传并选择存储在本地计算机的证书私钥文件将文件内容上传到文本框
</div>
</div>
</a-col>
</a-row>
</a-card>
</template>
<!-- export const ACCESS_TOKEN_KEY = 'X-Access-Token' -->
<!-- export const ACCESS_TOKEN = 'device_token' -->
<script lang="ts" setup name="CertificateDetail">
import { message } from 'ant-design-vue';
import { getImage } from '@/utils/comm';
import CertificateFile from './CertificateFile.vue';
const formData = reactive({
type: 'common',
name: '',
configs: {
cert: '',
key: '',
},
description: '',
});
const onFinish = (values: any) => {
console.log('Success:', values);
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
const headers = {
authorization: 'authorization-text',
};
const handleChange = (info: any) => {
if (info.file.status !== 'uploading') {
console.log(info.file, info.fileList);
}
if (info.file.status === 'done') {
message.success(`${info.file.name} file uploaded successfully`);
} else if (info.file.status === 'error') {
message.error(`${info.file.name} file upload failed.`);
}
};
const fileList = ref([]);
</script>
<style lang="less" scoped>
.form {
.form-radio-button {
width: 148px;
height: 80px;
padding: 0;
img {
width: 100%;
height: 100%;
}
}
.form-upload-button {
margin-top: 10px;
}
.form-submit {
background-color: @primary-color !important;
}
}
.doc {
height: 100%;
padding: 0 24px;
overflow-y: auto;
color: rgba(#000, 0.8);
font-size: 14px;
background-color: #fff;
h1 {
margin: 16px 0;
color: rgba(#000, 0.85);
font-weight: bold;
font-size: 14px;
&:first-child {
margin-top: 0;
}
}
h2 {
margin: 6px 10px;
color: rgba(0, 0, 0, 0.8);
font-weight: 400;
font-size: 14px;
}
}
</style>

View File

@ -0,0 +1,11 @@
<template>
<a-button type="primary" @click="handlAdd">新增</a-button>
</template>
<script lang="ts" setup name="CertificatePage">
const handlAdd = (e: any) => {
console.log(111,e);
}
</script>

View File

@ -0,0 +1,6 @@
<template>
<div>访问日志</div>
</template>
<script lang="ts" setup name="SystemLog">
</script>

View File

@ -0,0 +1,6 @@
<template>
<div>系统日志</div>
</template>
<script lang="ts" setup name="AccessLog">
</script>

View File

@ -0,0 +1,17 @@
<template>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" tab="访问日志">
<AccessLog />
</a-tab-pane>
<a-tab-pane key="2" tab="系统日志" force-render>
<SystemLog />
</a-tab-pane>
</a-tabs>
</template>
<script lang="ts" setup name="LogPage">
import { defineComponent, ref } from 'vue';
import AccessLog from './Access/index.vue';
import SystemLog from './System/index.vue';
const activeKey = ref('1');
</script>

View File

@ -1,5 +1,6 @@
import {defineConfig, loadEnv} from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import Components from 'unplugin-vue-components/vite'
import {AntDesignVueResolver} from 'unplugin-vue-components/resolvers'
import AutoImport from 'unplugin-auto-import/vite'
@ -46,6 +47,7 @@ export default defineConfig(({ mode}) => {
},
plugins: [
vue(),
vueJsx(),
Components({
resolvers: [AntDesignVueResolver({ importStyle: 'less' }), VueAmapResolver()],
directoryAsNamespace: true

2424
yarn.lock

File diff suppressed because it is too large Load Diff