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

# Conflicts:
#	components.d.ts
#	src/utils/consts.ts
This commit is contained in:
xiongqian 2023-01-10 18:31:05 +08:00
commit 748fac80a7
66 changed files with 8859 additions and 4398 deletions

28
.commitlintrc.cjs Normal file
View File

@ -0,0 +1,28 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'build', // 编译相关修改(新版本发布)
'feat', // 新功能
'fix', // 修复bug
'update', // 更新某功能
'refactor', // 重构
'docs', // 文档
'chore', // 增加依赖或库
'style', // 格式(不影响代码变动)
'revert', // 撤销commit 回滚上一版本
'perf', // 性能优化
]
],
'type-case': [0],
'type-empty': [0],
'scope-empty': [0],
'scope-case': [0],
'subject-full-stop': [0, 'never'],
'subject-case': [0, 'never'],
'header-max-length': [0, 'always', 72]
}
}

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ node_modules
dist dist
dist-ssr dist-ssr
*.local *.local
components.d.ts
# Editor directories and files # Editor directories and files
.vscode .vscode

4
.husky/commit-msg Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit ${1}

4020
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,12 +8,18 @@
"build": "vite build --mode production", "build": "vite build --mode production",
"preview": "vite preview", "preview": "vite preview",
"eslint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src", "eslint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
"prettier": "prettier --write" "lint": "eslint src --fix --ext .ts,.tsx,.vue,.js,.jsx",
"prettier": "prettier --write",
"prepare": "husky install"
}, },
"dependencies": { "dependencies": {
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@vuemap/vue-amap": "^1.1.20", "@vuemap/vue-amap": "^1.1.20",
"ant-design-vue": "^3.2.15", "ant-design-vue": "^3.2.15",
"axios": "^1.2.1", "axios": "^1.2.1",
"echarts": "^5.4.1",
"jetlinks-store": "^0.0.3",
"js-cookie": "^3.0.1",
"less": "^4.1.3", "less": "^4.1.3",
"less-loader": "^11.1.0", "less-loader": "^11.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@ -26,16 +32,32 @@
"vue-router": "^4.1.6" "vue-router": "^4.1.6"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.4.1",
"@commitlint/config-conventional": "^17.4.0",
"@types/lodash-es": "^4.17.6", "@types/lodash-es": "^4.17.6",
"@types/moment": "^2.13.0", "@types/moment": "^2.13.0",
"@types/node": "^18.11.17", "@types/node": "^18.11.17",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.0.0",
"@vuemap/unplugin-resolver": "^1.0.4", "@vuemap/unplugin-resolver": "^1.0.4",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"commitlint": "^17.4.1",
"husky": "^8.0.0",
"lint-staged": "^13.1.0",
"mrm": "^4.1.13",
"prettier": "^2.8.1", "prettier": "^2.8.1",
"typescript": "^4.9.3", "typescript": "^4.9.3",
"vite": "^4.0.0", "vite": "^4.0.0",
"vite-plugin-html": "^3.2.0", "vite-plugin-html": "^3.2.0",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue-tsc": "^1.0.11" "vue-tsc": "^1.0.11"
},
"lint-staged": {
"**/*.{vue,js,jsx,ts,tsx}": [
"npm run lint",
"prettier --write"
],
"**/*.{html,css,less,md}": [
"prettier --write"
]
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

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

BIN
public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

4
src/api/bind.ts Normal file
View File

@ -0,0 +1,4 @@
import { get } from '@/utils/request'
// 三方应用账户信息
export const applicationInfo = (code: string) => get(`/application/sso/bind-code/${code}`)

3
src/api/device.ts Normal file
View File

@ -0,0 +1,3 @@
import server from '@/utils/request'
export const deleteMetadata = (deviceId: string) => server.remove(`/device-instance/${deviceId}/metadata`)

8
src/api/home.js Normal file
View File

@ -0,0 +1,8 @@
import server from '@/utils/request';
// 设备数量
export const getDeviceCount_api = () => server.get(`/device/instance/_count`);
// 产品数量
export const getProductCount_api = (data) => server.post(`/device-product/_count`, data);
// 查询产品列表
export const getProductList_api = (data) => server.get(`/device/product/_query/no-paging?paging=false`, data);

View File

@ -0,0 +1,3 @@
import server from '@/utils/request'
export const save = (data) => server.post(`/network/certificate`, data)

15
src/api/login.js Normal file
View File

@ -0,0 +1,15 @@
import server from '@/utils/request'
export const config = () => server.get(`/authorize/captcha/config`)
export const code = () => server.get(`/authorize/captcha/image?width=130&height=30`)
export const authLogin = (data) => server.post(`/authorize/login`, data)
export const getInitSet = () => server.get(`/user/settings/init`)
export const postInitSet = (data) => server.post(`/user/settings/init`, data)
export const systemVersion = () => server.get(`/system/version`)
export const bindInfo = () => server.get(`/application/sso/_all`)

View File

@ -0,0 +1,6 @@
import server from '@/utils/request';
// 获取tree数据-第一层
export const getTreeOne_api = () => server.get(`/v3/api-docs/swagger-config`);
// 获取tree数据-第二层
export const getTreeTwo_api = (name:string) => server.get(`/v3/api-docs/${name}`);

View File

@ -0,0 +1,13 @@
<template>
<div class=''>
</div>
</template>
<script setup type='ts' name='FormBuilder'>
const data = reactive({})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,3 @@
import FormBuilder from './FormBuilder.vue'
export default FormBuilder

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

@ -0,0 +1,168 @@
<template>
<div class="jtable-body">
<div class="jtable-body-header">
<div class="jtable-body-header-left">
<slot name="headerTitle"></slot>
</div>
<div class="jtable-body-header-right">
<div class="jtable-setting-item" :class="[ModelEnum.CARD === model ? 'active' : '']" @click="modelChange(ModelEnum.CARD)">
<AppstoreOutlined />
</div>
<div class="jtable-setting-item" :class="[ModelEnum.TABLE === model ? 'active' : '']" @click="modelChange(ModelEnum.TABLE)">
<UnorderedListOutlined />
</div>
</div>
</div>
<div class="jtable-content">
<div v-if="model === ModelEnum.CARD" class="jtable-card">
<div
v-if="dataSource.length"
class="jtable-card-items"
:style="{gridTemplateColumns: `repeat(${column}, 1fr)`}"
>
<div
class="jtable-card-item"
v-for="(item, index) in dataSource"
:key="index"
>
<slot name="cardRender" :item="item" :index="index"></slot>
</div>
</div>
<div v-else>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
</div>
</div>
<div v-else>
<a-table :columns="columns" :dataSource="dataSource" :pagination="false" />
</div>
</div>
<div class="jtable-pagination" v-if="dataSource.length">
<a-pagination
size="small"
:total="50"
:show-total="total => `第 ${1} - ${1} 条/总共 ${total} 条`"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { UnorderedListOutlined, AppstoreOutlined } from '@ant-design/icons-vue'
import type { TableProps } from 'ant-design-vue/es/table'
import { Empty } from 'ant-design-vue'
enum ModelEnum {
TABLE = 'TABLE',
CARD = 'CARD',
}
export declare type RequestData = {
code: string;
result: {
data: Record<string, any>[] | undefined;
pageIndex: number;
pageSize: number;
total: number;
};
status: number;
} & Record<string, any>;
interface JTableProps extends TableProps{
request: (params: Record<string, any> & {
pageSize: number;
pageIndex: number;
}) => Promise<Partial<RequestData>>;
cardBodyClass?: string;
columns: Record<string, any>[];
params: Record<string, any> & {
pageSize: number;
pageIndex: number;
}
}
// propsemit
const emit = defineEmits(["modelChange"]);
const props = withDefaults(defineProps<JTableProps>(), {
cardBodyClass: '',
request: undefined
})
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE
const model = ref<keyof typeof ModelEnum>(ModelEnum.CARD); //
const column = ref<number>(4);
const dataSource = ref<Record<string, any>[]>([])
console.log(props)
//
//
const modelChange = (type: keyof typeof ModelEnum) => {
model.value = type
}
//
const handleSearch = async (params1?: Record<string, any>) => {
const resp = await props.request({
pageSize: 10,
pageIndex: 1,
...params1
})
if(resp.status === 200){
dataSource.value = resp.result?.data || []
}
}
watchEffect(() => {
handleSearch(props.params)
})
</script>
<style lang="less" scoped>
.jtable-body {
width: 100%;
padding: 0 24px 24px;
background-color: white;
.jtable-body-header {
padding: 16px 0;
display: flex;
justify-content: space-between;
align-items: center;
.jtable-body-header-right {
display: flex;
gap: 8px;
.jtable-setting-item {
color: rgba(0, 0, 0, 0.75);
font-size: 16px;
cursor: pointer;
&:hover {
color: @primary-color-hover;
}
&.active {
color: @primary-color-active;
}
}
}
}
.jtable-content {
.jtable-card {
.jtable-card-items {
display: grid;
grid-gap: 26px;
// grid-template-columns: repeat(4, 1fr);
.jtable-card-item {
display: flex;
}
}
}
}
.jtable-pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
// position: absolute;
// right: 24px;
// bottom: 24px;
}
}
</style>

View File

@ -0,0 +1,45 @@
.jtable-body {
width: 100%;
padding: 0 24px 24px;
background-color: white;
.jtable-body-header {
padding: 16px 0;
display: flex;
justify-content: space-between;
align-items: center;
.jtable-body-header-right {
display: flex;
gap: 8px;
.jtable-setting-item {
color: rgba(0, 0, 0, 0.75);
font-size: 16px;
cursor: pointer;
&:hover {
color: @primary-color-hover;
}
&.active {
color: @primary-color-active;
}
}
}
}
.jtable-content {
.jtable-card {
.jtable-card-items {
display: grid;
grid-gap: 26px;
// grid-template-columns: repeat(4, 1fr);
.jtable-card-item {
display: flex;
}
}
}
}
.jtable-pagination {
position: absolute;
right: 24px;
bottom: 24px;
}
}

View File

@ -0,0 +1,168 @@
import { UnorderedListOutlined, AppstoreOutlined } from '@ant-design/icons-vue'
import styles from './index.module.less'
import { Pagination, Table, Empty } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue/es/table'
enum ModelEnum {
TABLE = 'TABLE',
CARD = 'CARD',
}
export declare type RequestData = {
code: string;
result: {
data: any[] | undefined;
pageIndex: number;
pageSize: number;
total: number;
};
status: number;
} & Record<string, any>;
interface JTableProps extends TableProps{
request: (params: Record<string, any> & {
pageSize: number;
pageIndex: number;
}) => Promise<Partial<RequestData>>;
cardBodyClass: string;
}
const JTable = defineComponent<JTableProps>({
name: 'JTable',
slots: [
'headerTitle', // 顶部左边插槽
'cardRender', // 卡片内容
],
emits: [
'modelChange', // 切换卡片和表格
],
props: {
cardBodyClass: '',
request: undefined,
columns: []
} as any,
setup(props ,{ slots, emit }){
const model = ref<keyof typeof ModelEnum>(ModelEnum.CARD); // 模式切换
const column = ref<number>(3);
console.log(props.columns, props.request)
const dataSource = ref<any[]>([
{
key: '1',
name: '胡彦斌',
age: 32,
address: '西湖区湖底公园1号',
},
{
key: '2',
name: '胡彦祖1',
age: 42,
address: '西湖区湖底公园1号',
},
{
key: '3',
name: '胡彦斌',
age: 32,
address: '西湖区湖底公园1号',
},
{
key: '4',
name: '胡彦祖1',
age: 42,
address: '西湖区湖底公园1号',
},
{
key: '5',
name: '胡彦斌',
age: 32,
address: '西湖区湖底公园1号',
},
{
key: '6',
name: '胡彦祖1',
age: 42,
address: '西湖区湖底公园1号',
},
])
// 请求数据
onMounted(() => {
})
return () => <div class={styles["jtable-body"]}>
<div class={styles["jtable-body-header"]}>
<div class={styles["jtable-body-header-left"]}>
{/* 顶部左边插槽 */}
{slots.headerTitle && slots.headerTitle()}
</div>
<div class={styles["jtable-body-header-right"]}>
{/* <Space> */}
<div class={[styles["jtable-setting-item"], ModelEnum.CARD === model.value ? styles['active'] : '']} onClick={() => {
model.value = ModelEnum.CARD
}}>
<AppstoreOutlined />
</div>
<div class={[styles["jtable-setting-item"], ModelEnum.TABLE === model.value ? styles['active'] : '']} onClick={() => {
model.value = ModelEnum.TABLE
}}>
<UnorderedListOutlined />
</div>
{/* </Space> */}
</div>
</div>
{/* content */}
<div class={styles['jtable-content']}>
{
model.value === ModelEnum.CARD ?
<div class={styles['jtable-card']}>
{
dataSource.value.length ?
<div
class={styles['jtable-card-items']}
style={{gridTemplateColumns: `repeat(${column.value}, 1fr)`}}
>
{
dataSource.value.map(item => slots.cardRender ?
<div class={[styles['jtable-card-item'], props.cardBodyClass]}>{slots.cardRender(item)}</div>
: null)
}
</div> :
<div><Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /></div>
}
</div> :
<div>
<Table
dataSource={dataSource.value}
columns={props.columns}
pagination={false}
/>
</div>
}
</div>
{/* 分页 */}
{
dataSource.value.length &&
<div class={styles['jtable-pagination']}>
<Pagination
size="small"
total={50}
showTotal={(total) => {
const min = 1
const max = 1
return `${min} - ${max} 条/总共 ${total}`
}}
onChange={() => {
}}
onShowSizeChange={() => {
}}
/>
</div>
}
</div>
}
})
export default JTable

View File

@ -0,0 +1,41 @@
<template>
<div class="title">
<div class="title-before"></div>
<span>{{ data }}</span>
<slot name="extra"></slot>
</div>
</template>
<script>
export default {
name: "TitleComponent",
props: {
data: {
type: String,
default: ""
}
},
};
</script>
<style lang="less" scoped>
.title {
position: relative;
width: 100%;
margin-bottom: 10px;
padding-left: 10px;
color: rgba(0, 0, 0, 0.8);
font-weight: 600;
font-size: 16px;
}
.title-before {
position: absolute;
top: 0;
left: 0;
width: 4px;
height: calc(100% - 2px);
background-color: @primary-color;
border-radius: 0 3px 3px 0;
}
</style>

View File

@ -1,8 +1,16 @@
import type { App } from 'vue' import type { App } from 'vue'
import AIcon from './AIcon' import AIcon from './AIcon'
import PermissionButton from './PermissionButton/index.vue'
import JTable from './Table/index.vue'
import TitleComponent from "./TitleComponent/index.vue";
import Form from './Form'
export default { export default {
install(app: App) { install(app: App) {
app.component('AIcon', AIcon) app.component('AIcon', AIcon)
.component('PermissionButton', PermissionButton)
.component('JTable', JTable)
.component('TitleComponent', TitleComponent)
.component('Form', Form)
} }
} }

View File

@ -10,7 +10,6 @@ const router = createRouter({
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const token = LocalStore.get(TOKEN_KEY) const token = LocalStore.get(TOKEN_KEY)
next() // 测试用, 可删除
if (token) { if (token) {
next() next()
} else { } else {

View File

@ -20,13 +20,47 @@ export default [
// } // }
// start: 测试用, 可删除 // start: 测试用, 可删除
{
path: '/login',
component: () => import('@/views/user/Login/index.vue')
},
{ {
path: '/demo', path: '/demo',
component: () => import('@/views/demo/index.vue') component: () => import('@/views/demo/index.vue')
}, },
{
path: '/account/center/bind',
component: () => import('@/views/account/Center/bind/index.vue')
},
{ {
path: '/iot/home', path: '/iot/home',
component: () => import('@/views/iot/home/index.vue') component: () => import('@/views/home/index.vue')
},
{
path: '/table',
component: () => import('@/views/demo/table/index.vue')
},
{
path: '/form',
component: () => import('@/views/demo/Form.vue')
}, },
// end: 测试用, 可删除 // end: 测试用, 可删除
// link 运维管理
{
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')
},
{
path: '/link/accessConfig/detail/add',
component: () => import('@/views/link/AccessConfig/Detail/index.vue')
},
] ]

41
src/store/permission.ts Normal file
View File

@ -0,0 +1,41 @@
import { defineStore } from "pinia";
export const usePermissionStore = defineStore({
id: 'permission',
state: () => ({
permissions: {} as {[key: string]: string},
}),
getters: {
check(state) {
return (permissionCode: string) => {
if (!permissionCode) {
return true
}
if (!permissionCode.includes(":")) {
return false
}
const code = permissionCode.split(":")[0]
const value = permissionCode.split(":")[1]
const _buttonArray = state.permissions[code]
if (!_buttonArray) {
return false
}
return _buttonArray.includes(value)
}
},
hasPermission(state) {
return (permissionCode: string | string[]) => {
if (!permissionCode) {
return true
}
if (!!Object.keys(state.permissions).length) {
if (typeof permissionCode === 'string') {
return this.check(permissionCode)
}
return permissionCode.some(_permissionCode => this.check(_permissionCode))
}
return false
}
}
}
})

42
src/store/userInfo.ts Normal file
View File

@ -0,0 +1,42 @@
import { defineStore } from 'pinia';
import { authLogin } from '@/api/login';
import { LocalStore } from '@/utils/comm';
import { TOKEN_KEY } from '@/utils/variable';
/**
*
* @methods setUserInfos
*/
export const useUserInfo = defineStore('userInfo', {
state: () => ({
userInfos: {
id: '',
username: '',
isAdmin: true,
currentAuthority: [],
expires: 0,
permissions: [],
roles: [],
token: '',
user: {},
},
}),
actions: {
login(userInfo: any) {
const username = userInfo.userName.trim();
const password = userInfo.password;
const verifyCode = userInfo.verifyCode;
return new Promise((resolve: any, reject: any) => {
authLogin({ username, password, verifyCode })
.then((res: any) => {
Object.assign(this.userInfos, res.result);
LocalStore.set(TOKEN_KEY, res?.result.token);
resolve(res);
})
.catch((error: any) => {
reject(error);
});
});
},
},
});

View File

@ -29,5 +29,38 @@ export const StatusColorEnum = {
'default': 'default', '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 * GET method request
* @param {String} url * @param {String} url
* @param {Object} [params] * @param {Object} [params]
* @param {Object} [ext], * @param {Object} [ext]
* @returns {AxiosInstance} * @returns {AxiosInstance}
*/ */
export const get = function(url: string, params = {}, ext: any) { export const get = function(url: string, params = {}, ext?: any) {
return request({ return request({
method: 'GET', method: 'GET',
url, url,
@ -82,10 +82,10 @@ export const get = function(url: string, params = {}, ext: any) {
* DELETE method request * DELETE method request
* @param {String} url * @param {String} url
* @param {Object} [params] * @param {Object} [params]
* @param {Object} [ext], * @param {Object} [ext]
* @returns {AxiosInstance} * @returns {AxiosInstance}
*/ */
export const remove = function(url: string, params = {}, ext: any) { export const remove = function(url: string, params = {}, ext?: any) {
return request({ return request({
method: 'DELETE', method: 'DELETE',
url, url,
@ -162,7 +162,7 @@ request.interceptors.request.use(config => {
// 让每个请求携带自定义 token 请根据实际情况自行修改 // 让每个请求携带自定义 token 请根据实际情况自行修改
const token = LocalStore.get(TOKEN_KEY) const token = LocalStore.get(TOKEN_KEY)
// const token = store.$state.tokenAlias // const token = store.$state.tokenAlias
if (token) { if (!token) {
setTimeout(() => { setTimeout(() => {
router.replace({ router.replace({
path: LoginPath path: LoginPath

View File

@ -1,3 +1,7 @@
export const BASE_API_PATH = import.meta.env.VITE_APP_BASE_API export const BASE_API_PATH = import.meta.env.VITE_APP_BASE_API
export const TOKEN_KEY = 'X-Access-Token' export const TOKEN_KEY = 'X-Access-Token'
export const Version_Code = 'version_code'
export const NETWORK_CERTIFICATE_UPLOAD = '/network/certificate/upload'

View File

@ -0,0 +1,286 @@
<!-- 第三方账户绑定 -->
<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';
import { applicationInfo } from '@/api/bind';
const useForm = Form.useForm;
interface formData {
username: string;
password: string;
captcha: string;
}
//
const getAppInfo = async () => {
const code: string = '73ab60c88979a1475963a5dde31e374b';
const res = await applicationInfo(code);
console.log('getAppInfo: ', res);
};
getAppInfo();
//
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);
});
};
/**
* 绑定成功跳转至页面url的: redirect
*/
const goRedirect = () => {
const urlParams = new URLSearchParams(window.location.hash);
const redirectUrl =
urlParams.get('redirect') ||
window.location.href.split('redirect=')?.[1];
console.log('redirectUrl: ', redirectUrl);
//
if (redirectUrl && redirectUrl.indexOf('account/center/bind') === -1) {
window.location.href = decodeURIComponent(redirectUrl);
}
};
</script>
<style lang="less" scoped>
:deep(
.ant-form-item-label
> label.ant-form-item-required:not(
.ant-form-item-required-mark-optional
)::before
) {
display: none;
}
:deep(.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>

11
src/views/demo/Form.vue Normal file
View File

@ -0,0 +1,11 @@
<template>
<Form />
</template>
<script setup name='FormDemo'>
const data = reactive({})
</script>
<style scoped>
</style>

View File

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

View File

@ -0,0 +1,51 @@
<template>
<div class="box">
<JTable
:columns="[
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '年龄',
dataIndex: 'age',
key: 'age',
},
{
title: '住址',
dataIndex: 'address',
key: 'address',
}
]"
:request="request"
>
<template #headerTitle>
<a-button type="primary">新增</a-button>
</template>
<template #cardRender="slotProps">
<CardBox>
<template #content>
{{slotProps.item.name}}
</template>
</CardBox>
</template>
</JTable>
</div>
</template>
<script setup lang="ts">
import server from "@/utils/request";
import CardBox from '@/components/CardBox/index.vue';
const request = (data: any) => server.post(`/device-product/_query`, data)
</script>
<style lang="less" scoped>
.box {
padding: 20px;
background: #f0f2f5;
}
</style>

View File

@ -13,20 +13,38 @@
<div class="box-item"> <div class="box-item">
<div class="label">CPU使用率</div> <div class="label">CPU使用率</div>
<div class="value">{{ 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>
<div class="box-item"> <div class="box-item">
<div class="label">JVM内存</div> <div class="label">JVM内存</div>
<div class="value">{{ 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>
</div> </div>
</a-card> </a-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const cpu = ref('0%'); import Pie from './Pie.vue';
const jvm = ref('10%');
const cpu = ref('20%');
const jvm = ref('31%');
const getData = ()=>{
}
const jumpPage = () => {}; const jumpPage = () => {};
</script> </script>

View File

@ -6,12 +6,16 @@
<div class="box"> <div class="box">
<div <div
class="box-item" class="box-item"
v-for="(item,index) in cardData" v-for="(item, index) in cardData"
@click="jumpPage(item)" @click="jumpPage(item)"
> >
<div class="item-english">{{ item.english }}</div> <div class="item-english">{{ item.english }}</div>
<div class="item-title">{{ item.label }}</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>
</div> </div>
</a-card> </a-card>
@ -25,7 +29,7 @@ type configItem = {
link: string; link: string;
english: string; english: string;
label: string; label: string;
save?: boolean; params?: object;
}; };
const router = useRouter(); const router = useRouter();
@ -35,13 +39,25 @@ const props = defineProps({
}); });
const { cardData, cardTitle } = toRefs(props); const { cardData, cardTitle } = toRefs(props);
const jumpPage = (linkItem: configItem): void => { const jumpPage = (row: configItem): void => {
if (linkItem.auth && linkItem.link) { if (row.auth && row.link) {
router.push(`${linkItem.link}${linkItem.save ? '?save=true' : ''}`); router.push(`${row.link}${objToParams(row.params || {})}`);
} else { } else {
message.warning('暂无权限,请联系管理员'); 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> </script>
<style lang="less" scoped> <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,
},
] as recommendList[];
</script>

View File

@ -27,6 +27,7 @@ import BasicCountCard from '../BasicCountCard.vue';
import PlatformPicCard from '../PlatformPicCard.vue'; import PlatformPicCard from '../PlatformPicCard.vue';
import StepCard from '../StepCard.vue'; import StepCard from '../StepCard.vue';
import {recommendList} from '../../index'
// import {getImage} from '@/utils/comm' // import {getImage} from '@/utils/comm'
// - // -
@ -93,7 +94,7 @@ const stepDetails = [
linkUrl: '/a', linkUrl: '/a',
auth: false, auth: false,
}, },
]; ] as recommendList[];
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -25,9 +25,22 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { getDeviceCount_api, getProductCount_api } from '@/api/home';
const projectNum = ref(0); const projectNum = ref(0);
const deviceNum = ref(0); const deviceNum = ref(0);
onMounted(() => {
getData();
});
const getData = () => {
getDeviceCount_api().then((resp) => {
deviceNum.value = resp.result;
});
getProductCount_api().then((resp) => {
projectNum.value = resp.result;
});
};
const jumpPage = () => {}; const jumpPage = () => {};
</script> </script>

View File

@ -27,6 +27,8 @@ import DeviceCountCard from '../DeviceCountCard.vue';
import PlatformPicCard from '../PlatformPicCard.vue'; import PlatformPicCard from '../PlatformPicCard.vue';
import StepCard from '../StepCard.vue'; import StepCard from '../StepCard.vue';
import {recommendList} from '../../index'
// import {getImage} from '@/utils/comm' // import {getImage} from '@/utils/comm'
// - // -
@ -36,21 +38,21 @@ const bootConfig = [
label: '创建产品', label: '创建产品',
link: '/a', link: '/a',
auth: true, auth: true,
save: true, params: {},
}, },
{ {
english: 'STEP2', english: 'STEP2',
label: '创建设备', label: '创建设备',
link: '/b', link: '/b',
auth: true, auth: true,
save: true, params: {},
}, },
{ {
english: 'STEP3', english: 'STEP3',
label: '规则引擎', label: '规则引擎',
link: '/c', link: '/c',
auth: false, auth: false,
save: true, params: {},
}, },
]; ];
// - // -
@ -91,7 +93,7 @@ const stepDetails = [
linkUrl: '/a', linkUrl: '/a',
auth: false, auth: false,
}, },
]; ] as recommendList[];
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -0,0 +1,85 @@
<template>
<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>
const selectId = ref('1');
const confirm = ()=>{}
</script>
<style lang="less" scoped>
.init-home-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: calc(100vh - 150px);
padding: 20px;
background-color: white;
.title {
margin-top: 28px;
margin-bottom: 48px;
font-weight: 400;
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,65 @@
<template>
<div :ref="props.chartRef"></div>
</template>
<script setup lang="ts">
import * as echarts from 'echarts';
import { ComponentInternalInstance } from 'vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
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 as string] as HTMLElement);
myChart.clear();
myChart.setOption(options.value);
window.addEventListener('resize', () => {
myChart.resize();
});
});
};
initChart();
</script>

View File

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

View File

@ -11,49 +11,80 @@
</h5> </h5>
<div class="box-list"> <div class="box-list">
<div <div class="list-item" v-for="item in dataList">
class="list-item" <div class="box-top" @click="jumpPage(item)">
v-for="item in dataList"
@click="jumpPage(item)"
>
<div class="box-top">
<span class="top-title">{{ item.title }}</span> <span class="top-title">{{ item.title }}</span>
<img :src="item.iconUrl" alt="" /> <img :src="item.iconUrl" alt="" />
</div> </div>
<div class="box-details">{{ item.details }}</div> <div class="box-details">{{ item.details }}</div>
</div> </div>
</div> </div>
<div class="dialogs">
<AccessMethodDialog
:open-number="openAccess"
@confirm="againJumpPage"
/>
</div>
</a-card> </a-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { PropType } from 'vue';
import { QuestionCircleOutlined } from '@ant-design/icons-vue'; import { QuestionCircleOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
interface listConfig { import AccessMethodDialog from './dialogs/AccessMethodDialog.vue';
title: string;
details: string; import { recommendList } from '../index';
iconUrl: string;
type rowType = {
params: object;
linkUrl: string; linkUrl: string;
params?: object; };
auth: boolean;
}
const props = defineProps({ const props = defineProps({
cardTitle: String, cardTitle: String,
tooltip: String, tooltip: String,
dataList: Array<listConfig>, dataList: Array as PropType<recommendList[]>,
}); });
const router = useRouter(); const router = useRouter();
const { cardTitle, tooltip, dataList } = toRefs(props); const { cardTitle, tooltip, dataList } = toRefs(props);
const openAccess = ref<number>(0);
const jumpPage = (row: listConfig) => { const openFunc = ref<number>(0);
if (row.auth && row.linkUrl) { let selectRow: recommendList | rowType = {
router.push(`${row.linkUrl}${row.params ? '?save=true' : ''}`); params: {},
} else { linkUrl: '',
message.warning('暂无权限,请联系管理员'); };
//
const jumpPage = (row: recommendList) => {
if (!row.auth) return message.warning('暂无权限,请联系管理员');
selectRow = row; // 使
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 againJumpPage = (paramsSource: object) => {
const params = { ...(selectRow.params || {}), ...paramsSource };
router.push(`${selectRow.linkUrl}${objToParams(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> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -0,0 +1,98 @@
<template>
<div ref="modal" class="access-method-dialog-container"></div>
<a-modal
v-model:visible="visible"
title="选择产品"
style="width: 700px"
@ok="handleOk"
show-search
:filter-option="filterOption"
:getContainer="getContainer"
:maskClosable="false"
>
<a-form :model="form" name="basic" autocomplete="off" layout="vertical">
<a-form-item
label="产品"
name="productId"
:rules="[{ required: true, message: '该字段是必填字段' }]"
>
<a-select
v-model:value="form.productId"
style="width: 100%"
:options="productList"
>
</a-select>
</a-form-item>
</a-form>
<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 { ComponentInternalInstance } from 'vue';
import { getProductList_api } from '@/api/home';
import { productItem } from '../../index';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const props = defineProps({
openNumber: Number,
});
const emits = defineEmits(['confirm']);
const visible = ref<boolean>(false);
const form = ref({
productId: '',
});
const productList = ref<[productItem] | []>([]);
const getContainer = () => proxy?.$refs.modal as HTMLElement;
const getOptions = () => {
getProductList_api().then((resp) => {
productList.value = resp.result
.filter((i: any) => !i?.accessId)
.map((item: { name: any; id: any }) => ({
label: item.name,
value: item.id,
})) as [productItem];
});
};
const handleOk = () => {
emits('confirm', form.value);
visible.value = false;
};
const filterOption = (input: string, option: any) => {
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
watch(
() => props.openNumber,
() => {
visible.value = true;
form.value.productId = '';
getOptions();
},
);
</script>
<style lang="less" scoped>
.access-method-dialog-container {
:deep(.ant-modal-body) {
.ant-form {
.ant-form-item-label {
color: green;
.ant-form-item-required {
&::before {
position: absolute;
left: 30px;
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,135 @@
<template>
<div ref="modal" class="func-test-dialog-container"></div>
<a-modal
v-model:visible="visible"
title="选择产品"
style="width: 700px"
@ok="handleOk"
:getContainer="getContainer"
:maskClosable="false"
>
<div class="search">
<a-select
v-model:value="form.key"
style="width: 100%"
:options="productList"
/>
<a-select
v-model:value="form.relation"
style="width: 100%"
:options="productList"
/>
<a-input v-model:value="form.keyValue" allow-clear />
<a-button type="primary" @click="clickSearch">
<template #icon><SearchOutlined /></template>
搜索
</a-button>
<a-button type="primary" @click="clickReset">
<template #icon><reload-outlined /></template>
重置
</a-button>
</div>
<a-table
:columns="columns"
:data-source="tableData"
:row-selection="{
onChange: (selectedRowKeys, selectedRows) =>
(selectItem = selectedRows),
type: 'radio',
}"
>
</a-table>
<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 { ComponentInternalInstance } from 'vue';
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue';
import { productItem, deviceInfo } from '../../index';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const emits = defineEmits(['confirm']);
const props = defineProps({
openNumber: Number,
});
//
const visible = ref<boolean>(false);
const getContainer = () => proxy?.$refs.modal as HTMLElement;
const handleOk = () => {
emits('confirm', form.value);
visible.value = false;
};
watch(
() => props.openNumber,
() => {
clickReset();
getOptions();
clickSearch();
visible.value = true;
},
);
//
const form = ref({
key: '',
relation: '',
keyValue: '',
});
const productList = ref<[productItem] | []>([]);
const getOptions = () => {
productList.value = [];
};
const clickSearch = ()=>{
}
const clickReset = () => {
Object.entries(form.value).forEach(([prop]) => {
form.value[prop] = '';
});
};
//
const columns = [
{
name: 'deviceId',
dataIndex: 'deviceId',
key: 'deviceId',
},
{
name: 'deviceName',
dataIndex: 'deviceName',
key: 'deviceName',
},
{
name: 'productName',
dataIndex: 'productName',
key: 'productName',
},
{
name: 'createTime',
dataIndex: 'createTime',
key: 'createTime',
},
{
name: 'status',
dataIndex: 'status',
key: 'status',
},
];
const tableData = ref<deviceInfo[]>([]);
const selectItem: deviceInfo | {} = {};
const getList = () => {};
</script>
<style lang="scss" scoped></style>

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

@ -0,0 +1,23 @@
// 推荐步骤每项
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
}
export interface deviceInfo {
deviceId: string,
deviceName: string,
productName: string,
createTime: string,
status: boolean
}

View File

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

View File

@ -1,30 +0,0 @@
<template>
<div class="init-home-container">
<div class="title">请选择首页视图</div>
</div>
</template>
<script lang="ts" setup></script>
<style lang="less" scoped>
.init-home-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: calc(100vh - 150px);
padding: 20px;
background-color: white;
.title {
margin-top: 28px;
margin-bottom: 48px;
font-weight: 400;
font-size: 26px;
text-align: center;
}
}
</style>

View File

@ -0,0 +1,99 @@
const MetworkTypeMapping = new Map();
MetworkTypeMapping.set('websocket-server', 'WEB_SOCKET_SERVER');
MetworkTypeMapping.set('http-server-gateway', 'HTTP_SERVER');
MetworkTypeMapping.set('udp-device-gateway', 'UDP');
MetworkTypeMapping.set('coap-server-gateway', 'COAP_SERVER');
MetworkTypeMapping.set('mqtt-client-gateway', 'MQTT_CLIENT');
MetworkTypeMapping.set('mqtt-server-gateway', 'MQTT_SERVER');
MetworkTypeMapping.set('tcp-server-gateway', 'TCP_SERVER');
const ProcotoleMapping = new Map();
ProcotoleMapping.set('websocket-server', 'WebSocket');
ProcotoleMapping.set('http-server-gateway', 'HTTP');
ProcotoleMapping.set('udp-device-gateway', 'UDP');
ProcotoleMapping.set('coap-server-gateway', 'CoAP');
ProcotoleMapping.set('mqtt-client-gateway', 'MQTT');
ProcotoleMapping.set('mqtt-server-gateway', 'MQTT');
ProcotoleMapping.set('tcp-server-gateway', 'TCP');
ProcotoleMapping.set('child-device', '');
const descriptionList = {
'udp-device-gateway':
'UDP可以让设备无需建立连接就可以与平台传输数据。在允许一定程度丢包的情况下提供轻量化且简单的连接。',
'tcp-server-gateway':
'TCP服务是一种面向连接的、可靠的、基于字节流的传输层通信协议。设备可通过TCP服务与平台进行长链接实时更新状态并发送消息。可自定义多种粘拆包规则处理传输过程中可能发生的粘拆包问题。',
'websocket-server':
'WebSocket是一种在单个TCP连接上进行全双工通信的协议允许服务端主动向客户端推送数据。设备通过WebSocket服务与平台进行长链接实时更新状态并发送消息且可以发布订阅消息',
'mqtt-client-gateway':
'MQTT是ISO 标准下基于发布/订阅范式的消息协议具有轻量、简单、开放和易于实现的特点。平台使用指定的ID接入其他远程平台订阅消息。也可添加用户名和密码校验。可设置最大消息长度。可统一设置共享的订阅前缀。',
'http-server-gateway':
'HTTP服务是一个简单的请求-响应的基于TCP的无状态协议。设备通过HTTP服务与平台进行灵活的短链接通信仅支持设备和平台之间单对单的请求-响应模式',
'mqtt-server-gateway':
'MQTT是ISO 标准下基于发布/订阅范式的消息协议具有轻量、简单、开放和易于实现的特点。提供MQTT的服务端以供设备以长链接的方式接入平台。设备使用唯一的ID也可添加用户名和密码校验。可设置最大消息长度。',
'coap-server-gateway':
'CoAP是针对只有少量的内存空间和有限的计算能力提供的一种基于UDP的协议。便于低功耗或网络受限的设备与平台通信仅支持设备和平台之间单对单的请求-响应模式。',
};
const columnsMQTT = [
{
title: '分组',
dataIndex: 'group',
key: 'group',
ellipsis: true,
align: 'center',
width: 100,
scopedSlots: { customRender: 'group' },
},
{
title: 'topic',
dataIndex: 'topic',
key: 'topic',
scopedSlots: { customRender: 'topic' },
},
{
title: '上下行',
dataIndex: 'stream',
key: 'stream',
ellipsis: true,
align: 'center',
width: 100,
scopedSlots: { customRender: 'stream' },
},
{
title: '说明',
dataIndex: 'description',
key: 'description',
scopedSlots: { customRender: 'description' },
},
]
const columnsHTTP = [
{
title: '分组',
dataIndex: 'group',
key: 'group',
ellipsis: true,
width: 100,
scopedSlots: { customRender: 'group' },
},
{
title: '地址',
dataIndex: 'address',
key: 'address',
scopedSlots: { customRender: 'address' },
},
{
title: '示例',
dataIndex: 'example',
key: 'example',
scopedSlots: { customRender: 'example' },
},
{
title: '说明',
dataIndex: 'description',
key: 'description',
scopedSlots: { customRender: 'description' }
},
]
export { MetworkTypeMapping, ProcotoleMapping, descriptionList, columnsMQTT, columnsHTTP };

View File

@ -0,0 +1,135 @@
<template>
<a-card :bordered="false">
<TitleComponent data="自定义设备接入"></TitleComponent>
<div>
<a-row :gutter="[24, 24]">
<a-col :span="12" v-for="item in items" :key="item.id">
<div class="provider">
<div class="box">
<div class="left">
<div class="images">
<img :src="backMap.get(item.id)" />
</div>
<div class="context">
<div class="title">{{ item.name }}</div>
<div class="desc">
<a-tooltip :title="item.description">
{{ item.description || '' }}
</a-tooltip>
</div>
</div>
</div>
<div class="right">
<a-button
type="primary"
@click="goProviders(item)"
>接入</a-button
>
</div>
</div>
</div>
</a-col>
</a-row>
</div>
</a-card>
</template>
<script lang="ts" setup name="AccessConfigDetail">
import { getImage } from '@/utils/comm';
import TitleComponent from '@/components/TitleComponent/index.vue';
const items = [
{ id: 'mqtt-server-gateway', name: '测试1', description: '测试1' },
{ id: 'websocket-server', name: '测试2', description: '测试' },
{ id: 'coap-server-gateway', name: '测试3', description: '测试' },
];
const backMap = new Map();
backMap.set('mqtt-server-gateway', getImage('/access/mqtt.png'));
backMap.set('websocket-server', getImage('/access/websocket.png'));
backMap.set('coap-server-gateway', getImage('/access/coap.png'));
backMap.set('tcp-server-gateway', getImage('/access/tcp.png'));
backMap.set('child-device', getImage('/access/child-device.png'));
backMap.set('http-server-gateway', getImage('/access/http.png'));
backMap.set('udp-device-gateway', getImage('/access/udp.png'));
backMap.set('mqtt-client-gateway', getImage('/access/mqtt-broke.png'));
const goProviders = (value: object) => {
console.log(111, value);
};
</script>
<style lang="less" scoped>
.provider {
position: relative;
width: 100%;
padding: 20px;
background: url('/public/images/access/background.png') no-repeat;
background-size: 100% 100%;
border: 1px solid #e6e6e6;
&::before {
position: absolute;
top: 0;
left: 40px;
display: block;
width: 15%;
min-width: 64px;
height: 2px;
background-image: url('/public/images/access/rectangle.png');
background-repeat: no-repeat;
background-size: 100% 100%;
// border: 1px #8da1f4 solid;
// border-bottom-left-radius: 10%;
// border-bottom-right-radius: 10%;
content: ' ';
}
&:hover {
box-shadow: 0 0 24px rgba(#000, 0.1);
}
}
.box {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.left {
display: flex;
width: calc(100% - 70px);
.images {
width: 64px;
height: 64px;
img {
width: 100%;
}
}
.context {
width: calc(100% - 84px);
margin: 10px;
.title {
font-weight: 600;
}
.desc {
width: 100%;
margin-top: 10px;
overflow: hidden;
color: rgba(0, 0, 0, 0.55);
font-weight: 400;
font-size: 13px;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.right {
width: 70px;
}
}
</style>

View File

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

View File

@ -0,0 +1,91 @@
<template>
<a-spin :spinning="loading">
<div>
<a-textarea
:rows="4"
@change="textChange"
v-model:value="keystoreBase64"
:placeholder="placeholder"
/>
<a-upload
accept=".pem"
listType="text"
:action="`${BASE_API_PATH}${NETWORK_CERTIFICATE_UPLOAD}`"
:headers="{
[TOKEN_KEY]: LocalStore.get(TOKEN_KEY),
}"
:showUploadList="false"
@change="handleChange"
>
<a-button style="margin-top: 10px">
<upload-outlined />
上传文件</a-button
>
</a-upload>
</div>
</a-spin>
</template>
<script setup lang="ts" name="CertificateFile">
import { UploadOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import type { UploadChangeParam } from 'ant-design-vue';
import { LocalStore } from '@/utils/comm';
import {
BASE_API_PATH,
TOKEN_KEY,
NETWORK_CERTIFICATE_UPLOAD,
} from '@/utils/variable';
import type { UploadProps } from 'ant-design-vue';
const emit = defineEmits(['update:modelValue', 'change']);
const props = defineProps({
name: {
type: String,
default: () => '',
},
modelValue: {
type: String,
default: () => '',
},
placeholder: {
type: String,
default: () => '',
},
});
const keystoreBase64 = ref(props.modelValue);
const loading = ref(false);
const handleChange = (info: UploadChangeParam) => {
loading.value = true;
if (info.file.status === 'done') {
message.success('上传成功!');
const result = info.file.response?.result;
keystoreBase64.value = result;
console.log(1114, result);
loading.value = false;
emit('change', result);
emit('update:modelValue', result);
}
};
const textChange = (val: any) => {
val.name = props.name;
emit('change', val);
// emit('update:modelValue', val);
};
watch(
() => props.modelValue,
(v) => {
keystoreBase64.value = v;
},
{
deep: true,
immediate: true,
},
);
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,204 @@
<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"
:rules="formRules"
>
<a-form-item label="证书标准" name="type">
<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">
<a-input
placeholder="请输入证书名称"
v-model:value="formData.name"
/>
</a-form-item>
<a-form-item label="证书文件" name="cert">
<CertificateFile
name="cert"
v-model:modelValue="formData.cert"
@change="changeFileValue"
placeholder='证书格式以"-----BEGIN CERTIFICATE-----"开头,以"-----END CERTIFICATE-----"结尾"'
/>
</a-form-item>
<a-form-item label="证书私钥" name="key">
<CertificateFile
name="key"
v-model:modelValue="formData.key"
@change="changeFileValue"
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>
<script lang="ts" setup name="CertificateDetail">
import { message } from 'ant-design-vue';
import { getImage } from '@/utils/comm';
import CertificateFile from './CertificateFile.vue';
import type { UploadChangeParam } from 'ant-design-vue';
import { LocalStore } from '@/utils/comm';
import {
BASE_API_PATH,
TOKEN_KEY,
NETWORK_CERTIFICATE_UPLOAD,
} from '@/utils/variable';
import { save } from '@/api/link/certificate';
const loading = ref(false);
const formData = reactive({
type: 'common',
name: '',
cert: '',
key: '',
// configs: {
// cert: '',
// key: '',
// },
description: '',
});
const formRules = {
type: [{ required: true, message: '请选择证书标准', trigger: 'blur' }],
name: [
{ required: true, message: '请输入证书名称', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' },
],
cert: [{ required: true, message: '请输入或上传文件', trigger: 'blur' }],
key: [{ required: true, message: '请输入或上传文件', trigger: 'blur' }],
description: [{ max: 200, message: '最多可输入200个字符' }],
};
const onFinish = async (values: any) => {
values.configs = {
cert: formData.cert,
key: formData.key,
};
delete values.cert;
delete values.key;
const response = await save(values)
if (response.status === 200) {
message.success('操作成功')
}
};
const changeFileValue = (v: any) => {
formData[v.name] = v.data;
};
const handleChange = (info: UploadChangeParam) => {
loading.value = true;
if (info.file.status === 'done') {
message.success('上传成功!');
const result = info.file.response?.result;
formData.cert = result;
loading.value = false;
}
};
</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

@ -0,0 +1,71 @@
<template>
<a-tree
:tree-data="treeData"
@select="clickSelectItem"
showLine
class="left-tree-container"
>
<template #title="{ name }">
{{ name }}
</template>
</a-tree>
</template>
<script setup lang="ts">
import { TreeProps } from 'ant-design-vue';
import { getTreeOne_api, getTreeTwo_api } from '@/api/system/apiPage';
type treeNodeTpye = {
name: string;
url: string;
children?: treeNodeTpye[];
};
const emits = defineEmits(['select']);
const treeData: TreeProps['treeData'] = ref([]);
const getTreeData = () => {
let tree: treeNodeTpye[] = [];
getTreeOne_api().then((resp) => {
tree = resp.urls.map((item) => ({
...item,
key: item.url,
}));
const allPromise = tree.map((item) => getTreeTwo_api(item.name));
Promise.all(allPromise).then((values) => {
values.forEach((item, i) => {
tree[i].children = combData(item.paths);
});
console.log(tree);
treeData.value = tree
});
});
};
const clickSelectItem = (key, { node }) => {
emits('select', node);
};
onMounted(() => {
getTreeData();
});
const combData = (dataSource: object): object[] => {
const apiList: object[] = [];
const keys = Object.keys(dataSource);
keys.forEach((key) => {
const method = Object.keys(dataSource[key] || {})[0];
const name = dataSource[key][method].tags[0];
let apiObj = apiList.find((item) => item.name === name);
if (!apiObj) {
apiObj = { name, link: key, methods: dataSource[key], key };
apiList.push(apiObj);
}
});
return apiList;
};
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,15 @@
<template>
<a-card class="api-page-container" >
<LeftTree />
</a-card>
</template>
<script setup lang="ts">
import LeftTree from './components/LeftTree.vue';
</script>
<style scoped>
.api-page-container {
height: 100%;
}
</style>

View File

@ -0,0 +1,534 @@
<template>
<div>
<a-spin :spinning="loading" :delay="500">
<div class="container">
<div class="left">
<img
style="width: 100%; height: 100%"
:src="getImage('/login.png')"
/>
</div>
<div class="right">
<div class="lang" data-lang=""></div>
<div class="content">
<div class="top">
<div class="header">
<!-- <link to="/"> -->
<img
alt="logo"
class="logo"
:src="getImage('/logo.png')"
/>
<!-- </link> -->
</div>
<div class="desc">物联网平台</div>
<div class="main">
<a-form
layout="vertical"
:model="form"
class="login-form"
@finish="onFinish"
>
<a-form-item
label="账号"
name="username"
:rules="[
{
required: true,
message: '请输入账号!',
},
]"
>
<a-input
v-model:value="form.username"
placeholder="请输入账号"
:maxlength="64"
></a-input>
</a-form-item>
<a-form-item
label="密码"
name="password"
:rules="[
{
required: true,
message: '请输入密码!',
},
]"
>
<a-input-password
v-model:value="form.password"
placeholder="请输入密码"
:maxlength="64"
></a-input-password>
</a-form-item>
<a-form-item
v-if="codeConfig"
class="verifyCode"
label="验证码"
name="verifyCode"
:rules="[
{
required: true,
message: '请输入验证码!',
},
]"
>
<a-input
class="login-code-input"
v-model:value="form.verifyCode"
autocomplete="off"
:maxlength="64"
placeholder="请输入验证码"
></a-input>
<div class="login-code">
<img
:src="codeUrl"
@click="getCode()"
class="login-code-img"
/>
</div>
</a-form-item>
<a-form-item
name="remember"
style="text-align: left"
>
<a-checkbox
v-model:checked="form.remember"
>记住密码</a-checkbox
>
</a-form-item>
<a-form-item>
<a-button
:loading="loading"
type="primary"
html-type="submit"
class="login-form-button"
block
>
登录
</a-button>
</a-form-item>
</a-form>
<div style="margin-top: 20px">
<a-divider plain style="height: 12px">
<div
style="color: #807676d9, font-size: 12px"
>
其他方式登录
</div>
</a-divider>
<div
style="position: relative, bottom: 10px; text-align: center"
>
<a-button
v-for="(item, index) in bindings"
:key="index"
type="link"
@Click="handleClickOther(item)"
>
<img
style="width: 32px, height: 33px"
:alt="item.name"
:src="
iconMap.get(
item.provider,
) || defaultImg
"
/>
</a-button>
</div>
</div>
</div>
</div>
</div>
<div class="bottom">
<div class="view">
JETLINKS团队全新力作可视化大屏系统
</div>
<div class="url">
<div style="height: 33px">
<img :src="viewLogo" />
</div>
<a
href="https://view.jetlinks.cn/"
target="_blank"
rel="noopener noreferrer"
>
体验DEMO
</a>
</div>
</div>
</div>
</div>
</a-spin>
</div>
</template>
<script setup lang="ts">
import { getImage } from '@/utils/comm';
import {
config,
code,
authLogin,
getInitSet,
systemVersion,
bindInfo,
} from '@/api/login';
import Cookies from 'js-cookie';
import { useUserInfo } from '@/store/userInfo';
import { LocalStore } from '@/utils/comm';
import { BASE_API_PATH, TOKEN_KEY, Version_Code } from '@/utils/variable';
const store = useUserInfo();
const router = useRouter();
const bgImage = getImage('/logo.png');
const viewLogo = getImage('/view-logo.png');
const LoginWarpStyle = reactive({
backgroundImage: `url(${bgImage})`,
});
const screenWidth = ref(document.body.clientWidth);
const screenHeight = ref(document.body.clientHeight);
const form = reactive({
username: '',
password: '',
remember: false,
expires: 3600000,
verifyCode: '',
verifyKey: '',
});
const codeUrl = ref('');
const codeConfig = ref(false);
const loading = ref(false);
const bindings = ref<any[]>();
const defaultImg = getImage('/apply/provider1.png');
const iconMap = new Map();
iconMap.set('dingtalk-ent-app', getImage('/bind/dingtalk.png'));
iconMap.set('wechat-webapp', getImage('/bind/wechat-webapp.png'));
const onFinish = async () => {
form.remember
? Cookies.set('user', encodeURIComponent(JSON.stringify(form)), {
expires: 7,
})
: Cookies.remove('user');
Cookies.set('username', form.username, { expires: 30 });
try {
loading.value = true;
const res: any = await authLogin(form);
if (res.success) {
store.$patch({
...res.result,
username: form.username,
});
LocalStore.set(TOKEN_KEY, res?.result.token);
const resp: any = await getInitSet();
if (resp.success) {
router.push('/demo');
}
}
} catch (error) {
form.verifyCode = '';
getCode();
loading.value = false;
}
};
const getCode = async () => {
const configRes: any = await config();
if (!configRes.success || (configRes.success && !configRes.result.enabled))
return;
codeConfig.value = true;
const res: any = await code();
if (res.success) {
codeUrl.value = res.result.base64;
form.verifyKey = res.result.key;
}
};
const getCookie = () => {
// form.username = Cookies.get('username');
if (!Cookies.get('user')) return;
const user = JSON.parse(decodeURIComponent(Cookies.get('user')));
form.username = user.username;
form.password = user.password;
form.remember = user.remember || false;
};
const getOpen = () => {
systemVersion().then((res: any) => {
if (res.success && res.result) {
LocalStore.set(Version_Code, res.result.edition);
if (res.result.edition !== 'community') {
bindInfo().then((res: any) => {
if (res.success) {
bindings.value = res.result;
}
});
}
}
});
};
const handleClickOther = (item: any) => {
LocalStore.set('onLogin', 'no');
window.open(`${BASE_API_PATH}/application/sso/${item.id}/login`);
window.onstorage = (e) => {
if (e.newValue) {
window.location.href = '/';
}
};
};
const screenRotation = (width: number, height: number) => {
LoginWarpStyle.backgroundImage = `url(${bgImage})`;
};
window.onresize = () => {
return (() => {
screenWidth.value = document.body.clientWidth;
screenHeight.value = document.body.clientHeight;
})();
};
watch(
[() => screenWidth.value, () => screenHeight.value],
(value) => {
screenRotation(value[0], value[1]);
},
{ deep: true },
);
getOpen();
getCode();
getCookie();
screenRotation(screenWidth.value, screenHeight.value);
</script>
<style scoped lang="less">
@import 'ant-design-vue/es/style/themes/default.less';
.container {
display: flex;
height: 100vh;
overflow: auto;
background: @layout-body-background;
.left {
width: 73%;
height: 100%;
}
.right {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 27%;
background: #fff;
:deep(.ant-layout-footer) {
background: #fff;
}
.lang {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
:global(.ant-dropdown-trigger) {
margin-right: 24px;
}
}
.content {
display: flex;
flex-direction: row-reverse;
justify-content: center;
padding: 0 0 15% 0;
.top {
width: 100%;
// text-align: center;
.header {
height: 44px;
line-height: 44px;
text-align: center;
a {
text-decoration: none;
}
.logo {
height: 44px;
margin-right: 16px;
vertical-align: top;
}
.title {
position: relative;
top: 2px;
color: @heading-color;
font-weight: 600;
font-size: 33px;
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica,
sans-serif;
}
}
.desc {
margin-top: 12px;
margin-bottom: 40px;
// color: @heading-color;
color: rgb(0 0 0 / 70%);
font-weight: 600;
font-size: 22px;
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica,
sans-serif;
text-align: center;
}
.main {
width: 70%;
margin: 60px auto 0;
@media screen and (max-width: @screen-sm) {
width: 95%;
max-width: 328px;
}
// ::v-deep {
// .@{ant-prefix}-tabs-nav-list {
// margin: auto;
// font-size: 16px;
// }
// // .ant-formily-item-size-large .ant-formily-item-help {
// // text-align: left;
// // }
// }
.icon {
margin-left: 16px;
color: rgba(0, 0, 0, 0.2);
font-size: 24px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: @primary-color;
}
}
.other {
margin-top: 24px;
line-height: 22px;
text-align: left;
.register {
float: right;
}
}
.prefixIcon {
color: @primary-color;
font-size: @font-size-base;
}
.remember {
display: flex;
flex-direction: row;
margin-bottom: 30px;
}
.verifyCode {
.login-code-input {
width: 70%;
float: left;
}
.login-code {
width: 30%;
height: 32px;
float: left;
background-color: #e4e6e7;
img {
cursor: pointer;
vertical-align: middle;
}
.login-code-img {
width: 100%;
height: 100%;
}
}
}
}
}
}
}
.bottom {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 11%;
border-top: 1px solid #e0e4e8;
.view {
width: 247px;
height: 20px;
margin-right: 10%;
margin-bottom: 10px;
padding-top: 2px;
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
font-family: 'PingFang SC';
}
.url {
display: flex;
align-items: center;
margin-right: 10%;
img {
width: 100%;
height: 100%;
}
a {
position: relative;
left: 60px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 2px 7px;
line-height: 20px;
border: 1px solid #2f54eb;
border-radius: 2px;
}
}
}
}
@media (min-width: @screen-md-min) {
.container {
//background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
.content {
padding: 32px 0 24px;
}
}
</style>

View File

@ -18,8 +18,9 @@
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
}, },
"types": ["ant-design-vue/typings/global"] "types": ["ant-design-vue/typings/global"],
"suppressImplicitAnyIndexErrors": true
}, },
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }],
} }

View File

@ -1,11 +1,13 @@
import {defineConfig, loadEnv} from 'vite' import {defineConfig, loadEnv} from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'
import {AntDesignVueResolver} from 'unplugin-vue-components/resolvers' import {AntDesignVueResolver} from 'unplugin-vue-components/resolvers'
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'
import { createHtmlPlugin } from 'vite-plugin-html' import { createHtmlPlugin } from 'vite-plugin-html'
import Config from './config/config' import Config from './config/config'
import {VueAmapResolver} from '@vuemap/unplugin-resolver' import {VueAmapResolver} from '@vuemap/unplugin-resolver'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
import * as path from 'path' import * as path from 'path'
@ -46,6 +48,7 @@ export default defineConfig(({ mode}) => {
}, },
plugins: [ plugins: [
vue(), vue(),
vueJsx(),
Components({ Components({
resolvers: [AntDesignVueResolver({ importStyle: 'less' }), VueAmapResolver()], resolvers: [AntDesignVueResolver({ importStyle: 'less' }), VueAmapResolver()],
directoryAsNamespace: true directoryAsNamespace: true
@ -65,7 +68,8 @@ export default defineConfig(({ mode}) => {
favicon: `<link rel="icon" type="image/svg+xml" href="${Config.logo}" />` favicon: `<link rel="icon" type="image/svg+xml" href="${Config.logo}" />`
} }
} }
}) }),
VueSetupExtend()
], ],
server: { server: {
host:'0.0.0.0', host:'0.0.0.0',
@ -74,9 +78,10 @@ export default defineConfig(({ mode}) => {
[env.VITE_APP_BASE_API]: { [env.VITE_APP_BASE_API]: {
// target: 'http://192.168.33.22:8800', // target: 'http://192.168.33.22:8800',
// target: 'http://192.168.32.244:8881', // target: 'http://192.168.32.244:8881',
target: 'http://47.112.135.104:5096', // opcua // target: 'http://47.112.135.104:5096', // opcua
target: 'http://47.108.63.174:8845', // 测试
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace('^'+env.VITE_APP_BASE_API, '') rewrite: (path) => path.replace(/^\/api/, '')
} }
} }
}, },

5941
yarn.lock

File diff suppressed because it is too large Load Diff