feat: 设备导入

This commit is contained in:
100011797 2023-06-29 09:54:17 +08:00
parent 8307a1353e
commit 5f15f21ff2
42 changed files with 331 additions and 826 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

View File

@ -99,7 +99,7 @@ export const templateDownload = (productId: string, type: string) => server.get(
* @param type
* @returns
*/
export const deviceImport = (productId: string, fileUrl: string, autoDeploy: boolean) => `${BASE_API_PATH}/device-instance/${productId}/import?fileUrl=${fileUrl}&autoDeploy=${autoDeploy}&:X_Access_Token=${LocalStore.get(TOKEN_KEY)}`
export const deviceImport = (productId: string, fileUrl: string, autoDeploy: boolean) => `${BASE_API_PATH}/device-instance/${productId}/import/_withlog?fileUrl=${fileUrl}&autoDeploy=${autoDeploy}&:X_Access_Token=${LocalStore.get(TOKEN_KEY)}`
/**
*

View File

@ -0,0 +1,9 @@
<template>
123
</template>
<script lang="ts" setup>
const props = defineProps({
// options:
})
</script>

View File

@ -37,7 +37,7 @@
:src="
iconMap.get(
bindUser?.applicationProvider,
) || getImage('/apply/provider1.png')
) || getImage('/apply/internal-standalone.png')
"
/>
<p>账号{{ bindUser?.result?.userId || '-' }}</p>
@ -153,8 +153,8 @@ interface formData {
const iconMap = new Map()
iconMap.set('dingtalk-ent-app', getImage('/notice/dingtalk.png'))
iconMap.set('wechat-webapp', getImage('/notice/wechat.png'))
iconMap.set('internal-standalone', getImage('/apply/provider1.png'))
iconMap.set('third-party', getImage('/apply/provider5.png'))
iconMap.set('internal-standalone', getImage('/apply/internal-standalone.png'))
iconMap.set('third-party', getImage('/apply/third-party.png'))
const token = computed(() => LocalStore.get(TOKEN_KEY))

View File

@ -51,8 +51,8 @@ const bindList = ref<any[]>([]);
const bindIcon = {
'dingtalk-ent-app': '/notice/dingtalk.png',
'wechat-webapp': '/notice/wechat.png',
'internal-standalone': '/apply/provider1.png',
'third-party': '/apply/provider5.png',
'internal-standalone': '/apply/internal-standalone.png',
'third-party': '/apply/third-party.png',
};
const unBind = (id: string) => {
unBind_api(id).then((resp) => {

View File

@ -1,34 +1,31 @@
<template>
<div class="choose-view">
<j-row class="view-content" :gutter="24">
<j-col
<div class="view-content">
<div
:span="8"
class="select-item"
:class="{ selected: currentView === 'device' }"
@click="currentView = 'device'"
>
<img :src="getImage('/home/device.png')" alt="" />
</j-col>
<j-col
<img :src="getImage(`/home/home-view/device${currentView === 'device' ? '-active' : ''}.png`)" alt="" />
</div>
<div
:span="8"
class="select-item"
:class="{ selected: currentView === 'ops' }"
@click="currentView = 'ops'"
>
<img :src="getImage('/home/ops.png')" alt="" />
</j-col>
<j-col
<img :src="getImage(`/home/home-view/ops${currentView === 'ops' ? '-active' : ''}.png`)" alt="" />
</div>
<div
:span="8"
class="select-item"
:class="{
selected: currentView === 'comprehensive',
}"
@click="currentView = 'comprehensive'"
>
<img :src="getImage('/home/comprehensive.png')" alt="" />
</j-col>
</j-row>
<j-button type="primary" class="btn" @click="confirm">确定</j-button>
<img :src="getImage(`/home/home-view/comprehensive${currentView === 'comprehensive' ? '-active' : ''}.png`)" alt="" />
</div>
</div>
<div class="btn">
<j-button type="primary" @click="confirm">保存修改</j-button>
</div>
</div>
</template>
@ -76,28 +73,26 @@ onMounted(() => {
<style lang="less" scoped>
.choose-view {
width: 100%;
margin-top: 30px;
padding: 48px 150px;
padding: 48px 90px;
box-sizing: border-box;
.view-content {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
.select-item {
border: 2px solid transparent;
img {
width: 100%;
background-size: cover;
}
cursor: pointer;
// width: 312px;
&.selected {
border-color: #10239e;
img {
width: 312px;
background-size: cover;
}
}
}
.btn {
display: block;
margin: 48px auto;
display: flex;
justify-content: flex-end;
margin-top: 68px;
}
}
</style>

View File

@ -19,8 +19,6 @@
<div class="box-item">
<div class="box-item-img">
<j-dropdown placement="top" :trigger="['click']">
<!-- :visible="show?.[slotProps?.id]"
@visibleChange="onVisibleChange(slotProps)" -->
<div>
<img
:src="
@ -28,6 +26,7 @@
slotProps?.channelProvider,
)
"
style="width: 60px;"
/>
<div
:class="{
@ -37,13 +36,6 @@
}"
></div>
</div>
<!-- v-if="
notifyChannels?.includes(
slotProps?.id,
) &&
slotProps?.channelProvider !==
'inside-mail'
" -->
<template #overlay>
<j-menu>
<j-menu-item
@ -97,13 +89,13 @@
</j-menu>
</template>
</j-dropdown>
<div class="box-item-checked">
<!-- <div class="box-item-checked">
<j-checkbox
:checked="
notifyChannels?.includes(slotProps?.id)
"
></j-checkbox>
</div>
</div> -->
</div>
<div class="box-item-text">
{{ slotProps?.name }}
@ -140,12 +132,12 @@ import { useUserInfo } from '@/store/userInfo';
import EditInfo from '../../EditInfo/index.vue';
const iconMap = new Map();
iconMap.set('notifier-dingTalk', getImage('/notice/dingtalk.png'));
iconMap.set('notifier-weixin', getImage('/notice/wechat.png'));
iconMap.set('notifier-email', getImage('/notice/email.png'));
iconMap.set('notifier-voice', getImage('/notice/voice.png'));
iconMap.set('notifier-sms', getImage('/notice/sms.png'));
iconMap.set('inside-mail', getImage('/notice/inside-mail.png'));
iconMap.set('notifier-dingTalk', getImage('/notice-rule/dingtalk.png'));
iconMap.set('notifier-weixin', getImage('/notice-rule/wechat.png'));
iconMap.set('notifier-email', getImage('/notice-rule/email.png'));
iconMap.set('notifier-voice', getImage('/notice-rule/voice.png'));
iconMap.set('notifier-sms', getImage('/notice-rule/sms.png'));
iconMap.set('inside-mail', getImage('/notice-rule/inside-mail.png'));
const current = ref<any>({});
const visible = ref<boolean>(false);
@ -303,30 +295,21 @@ const onSave = () => {
.box-item {
margin-left: 10px;
.box-item-img {
width: 48px;
height: 48px;
width: 60px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
img {
width: 100%;
z-index: 1;
}
.box-item-checked {
position: absolute;
top: -10px;
right: -10px;
z-index: 3;
}
.disabled {
background-color: rgba(#000, 0.38);
position: absolute;
width: 48px;
height: 48px;
width: 60px;
height: 60px;
z-index: 2;
top: 0;
left: 0;

View File

@ -1,482 +0,0 @@
<template>
<page-container>
<div class="center-container">
<div class="card">
<div class="content" style="margin-top: 0">
<div
class="content-item flex-item"
style="width: 350px; justify-content: center"
>
<img
v-if="userInfo.avatar"
:src="userInfo.avatar"
style="width: 140px; border-radius: 70px"
alt=""
/>
<div class="default-avatar" v-else>
<AIcon type="UserOutlined" />
</div>
<div
style="
width: 100%;
text-align: center;
margin-top: 20px;
"
>
<j-upload
v-model:file-list="upload.fileList"
accept=".jpg,.png,.jfif,.pjp,.pjpeg,.jpeg"
:maxCount="1"
:show-upload-list="false"
:headers="{
[TOKEN_KEY]: LocalStore.get(TOKEN_KEY),
}"
:action="`${BASE_API_PATH}/file/static`"
@change="upload.changeBackUpload"
:beforeUpload="upload.beforeUpload"
>
<j-button>
<AIcon type="UploadOutlined" />
更换头像
</j-button>
</j-upload>
</div>
</div>
<div
class="content-item flex-item"
style="flex: 1; padding: 15px 0"
>
<div class="info-card">
<p>用户名</p>
<p>{{ userInfo.username }}</p>
</div>
<div class="info-card">
<p>账号ID</p>
<p>{{ userInfo.id }}</p>
</div>
<div class="info-card">
<p>注册时间</p>
<p>
{{
userInfo.createTime ? moment(userInfo.createTime).format(
'YYYY-MM-DD HH:mm:ss',
) : '-'
}}
</p>
</div>
<div class="info-card">
<p>电话</p>
<p>{{ userInfo.telephone || '-' }}</p>
</div>
<div class="info-card">
<p>姓名</p>
<p>{{ userInfo.name }}</p>
</div>
<div class="info-card">
<p>角色</p>
<p>
{{
(userInfo.roleList &&
userInfo.roleList
.map((item) => item.name)
.join(',')) ||
'-'
}}
</p>
</div>
<div class="info-card">
<p>组织</p>
<p>
{{
(userInfo.orgList &&
userInfo.orgList
.map((item) => item.name)
.join(',')) ||
'-'
}}
</p>
</div>
<div class="info-card">
<p>邮箱</p>
<p>{{ userInfo.email || '-' }}</p>
</div>
</div>
<AIcon
type="EditOutlined"
class="edit"
style="right: 40px"
@click="editInfoVisible = true"
/>
</div>
</div>
<div class="card" v-if='updatePassword'>
<h3>修改密码</h3>
<div class="content">
<div class="content" style="align-items: flex-end">
<AIcon
type="LockOutlined"
style="color: #1d39c4; font-size: 70px"
/>
<span
style="margin-left: 5px; color: rgba(0, 0, 0, 0.55)"
>安全性高的密码可以使帐号更安全建议您定期更换密码,设置一个包含字母,符号或数字中至少两项且长度超过8位的密码</span
>
</div>
<span class="edit">
<PermissionButton
:uhasPermission="`${permission}:update`"
type="link"
@click="editPasswordVisible = true"
>
<AIcon type="EditOutlined" style="color: #1d39c4" />
</PermissionButton>
</span>
</div>
</div>
<!-- 社区版不显示 -->
<div class="card" v-if="isNoCommunity">
<h3>绑定三方账号</h3>
<div class="content">
<div class="account-card" v-for="item in bindList" :key="item.id">
<img
:src="item.logoUrl || getImage(bindIcon[item.provider])"
style="height: 50px;width: 50px"
width='50px'
height='50px'
alt=""
/>
<Ellipsis style="width: 150px; font-size: 22px">
<div v-if="item.bound">
<div>绑定名{{ item.others.name }}</div>
<div>
绑定时间{{
moment(item.bindTime).format(
'YYYY-MM-DD HH:mm:ss',
)
}}
</div>
</div>
<div v-else>{{ item.name }}未绑定</div>
</Ellipsis>
<j-popconfirm
v-if="item.bound"
title="确认解除绑定嘛?"
@confirm="() => unBind(item.id)"
>
<j-button>解除绑定</j-button>
</j-popconfirm>
<j-button
v-else
type="primary"
@click="clickBind(item.id)"
>立即绑定</j-button
>
</div>
</div>
</div>
<!-- 第三方用户不显示 -->
<div class="card" v-if="!isApiUser">
<h3>首页视图</h3>
<div class="choose-view">
<j-row class="view-content" :gutter="24">
<j-col
:span="6"
class="select-item"
:class="{ selected: currentView === 'device' }"
@click="currentView = 'device'"
>
<img :src="getImage('/home/device.png')" alt="" />
</j-col>
<j-col
:span="6"
class="select-item"
:class="{ selected: currentView === 'ops' }"
@click="currentView = 'ops'"
>
<img :src="getImage('/home/ops.png')" alt="" />
</j-col>
<j-col
:span="6"
class="select-item"
:class="{
selected: currentView === 'comprehensive',
}"
@click="currentView = 'comprehensive'"
>
<img
:src="getImage('/home/comprehensive.png')"
alt=""
/>
</j-col>
</j-row>
<j-button type="primary" class="btn" @click="confirm"
>确定</j-button
>
</div>
</div>
<EditInfoDialog
v-if="editInfoVisible"
v-model:visible="editInfoVisible"
:data="{ ...userInfo }"
@ok="getUserInfo"
/>
<EditPasswordDialog
v-if="editPasswordVisible"
v-model:visible="editPasswordVisible"
/>
</div>
</page-container>
</template>
<script setup lang="ts" name="Center">
import PermissionButton from '@/components/PermissionButton/index.vue';
import EditInfoDialog from './components/EditInfoDialog.vue';
import EditPasswordDialog from './components/EditPasswordDialog.vue';
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { LocalStore, getImage, onlyMessage } from '@/utils/comm'
import { message, UploadChangeParam, UploadFile } from 'ant-design-vue';
import {
getMeInfo_api,
getSsoBinds_api,
unBind_api,
updateMeInfo_api
} from '@/api/account/center';
import moment from 'moment';
import { getMe_api, getView_api, setView_api } from '@/api/home';
import { isNoCommunity } from '@/utils/utils';
import { userInfoType } from './typing';
import { usePermissionStore } from 'store/permission'
const btnHasPermission = usePermissionStore().hasPermission;
const updatePassword = btnHasPermission('account-center:user-center-passwd-update')
const permission = 'system/User';
const userInfo = ref<userInfoType>({} as any);
//
const bindList = ref<any[]>([]);
const bindIcon = {
'dingtalk-ent-app': '/notice/dingtalk.png',
'wechat-webapp': '/notice/wechat.png',
'internal-standalone': '/apply/provider1.png',
'third-party': '/apply/provider5.png',
};
const unBind = (id: string) => {
unBind_api(id).then((resp) => {
if (resp.status === 200) {
message.success('解绑成功');
getSsoBinds();
}
});
};
const clickBind = (id: string) => {
window.open(
`${BASE_API_PATH}/application/sso/${id}/login?autoCreateUser=false`,
);
localStorage.setItem('onBind', 'false');
localStorage.setItem('onLogin', 'yes');
window.onstorage = (e) => {
if (e.newValue) {
getSsoBinds();
}
};
};
const upload = reactive({
fileList: [] as any[],
uploadLoading: false,
changeBackUpload: (info: UploadChangeParam<UploadFile<any>>) => {
if (info.file.status === 'uploading') {
upload.uploadLoading = true;
} else if (info.file.status === 'done') {
info.file.url = info.file.response?.result;
upload.uploadLoading = false;
userInfo.value.avatar = info.file.response?.result;
updateMeInfo_api(userInfo.value).then(res => {
if(res.success) {
onlyMessage('上传成功')
}
})
} else if (info.file.status === 'error') {
upload.uploadLoading = false;
onlyMessage('logo上传失败请稍后再试', 'error');
}
},
beforeUpload: ({ size, type }: File) => {
const imageTypes = ['jpg', 'png', 'jfif', 'pjp', 'pjpeg', 'jpeg'];
const typeBool =
imageTypes.filter((typeStr) => type.includes(typeStr)).length > 0;
const sizeBool = size < 4 * 1024 * 1024;
(typeBool && sizeBool) || message.error('请上传正确格式的图片');
return typeBool && sizeBool;
},
});
//
const isApiUser = ref<boolean>();
const currentView = ref<string>('');
const confirm = () => {
setView_api({
name: 'view',
content: currentView.value,
}).then(() => message.success('保存成功'));
};
const editInfoVisible = ref<boolean>(false);
const editPasswordVisible = ref<boolean>(false);
init();
function init() {
getUserInfo();
isNoCommunity && getSsoBinds();
getViews();
}
/**
* 获取用户信息
*/
function getUserInfo() {
getMeInfo_api().then((resp) => {
userInfo.value = resp.result as userInfoType;
});
}
/**
* 获取绑定第三方账号
*/
function getSsoBinds() {
getSsoBinds_api().then((resp: any) => {
if (resp.status === 200) bindList.value = resp.result;
});
}
/**
* 获取首页视图
*/
function getViews() {
// api
getMe_api()
.then((resp: any) => {
if (resp && resp.status === 200) {
isApiUser.value = resp.result.dimensions.find(
(item: any) =>
item.type === 'api-client' ||
item.type.id === 'api-client',
);
if (!isApiUser.value) return getView_api();
}
})
.then((resp: any) => {
if (resp?.status === 200) {
if (resp.result) currentView.value = resp.result?.content;
else if (resp.result?.username === 'admin') {
currentView.value = 'comprehensive';
} else currentView.value = 'init';
}
});
}
</script>
<style lang="less" scoped>
.center-container {
background-color: #f0f2f5;
min-height: 100vh;
.card {
margin: 16px 0;
padding: 24px;
background-color: #fff;
position: relative;
h3 {
font-size: 22px;
&::before {
display: inline-block;
width: 3px;
height: 0.7em;
content: '';
background-color: #2f54eb;
margin: 0 8px;
}
}
.content {
display: flex;
margin-top: 24px;
flex-wrap: wrap;
gap: 24px;
.content-item {
margin-right: 24px;
.default-avatar {
background-color: #ccc;
color: #fff;
border-radius: 50%;
font-size: 70px;
width: 140px;
height: 140px;
display: flex;
justify-content: center;
align-items: center;
}
.info-card {
width: 25%;
:first-child {
font-weight: bold;
}
:last-child {
color: #666363d9;
}
}
&.flex-item {
display: flex;
flex-wrap: wrap;
}
}
.edit {
position: absolute;
cursor: pointer;
top: 30px;
right: 24px;
color: #1d39c4;
}
.account-card {
width: 415px;
background-image: url(/images/notice/dingtalk-background.png);
border-right: 1px solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px;
}
}
.choose-view {
width: 100%;
margin-top: 48px;
.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

@ -16,69 +16,41 @@
</div>
<div class="person-header-item-info-right">
<div class="person-header-item-info-right-top">
<span>{{ _org }}部门 · {{ _role }}角色</span>
Hi, {{ user.userInfos?.name }}
</div>
<div class="person-header-item-info-right-info">
<div>用户名 {{ user.userInfos?.username }}</div>
<div>账号ID {{ user.userInfos?.id }}</div>
<span>{{ _org }}部门 · {{ _role }}角色</span>
</div>
</div>
</div>
<div class="person-header-item-action">
<div class="person-header-item-action-left">
<j-space>
<j-button
@click="onActivated(item.key)"
v-for="item in list"
:type="
user.tabKey === item.key
? 'primary'
: 'default'
"
:key="item.key"
>{{ item.title }}</j-button
>
</j-space>
</div>
<div class="person-header-item-action-right">
<j-space :size="24">
<j-tooltip title="查看详情"
><j-button
@click="visible = true"
shape="circle"
><AIcon
style="font-size: 24px"
type="FileSearchOutlined" /></j-button
></j-tooltip>
<j-tooltip title="编辑资料"
><j-button
shape="circle"
@click="editInfoVisible = true"
><AIcon
style="font-size: 24px"
type="FormOutlined" /></j-button
></j-tooltip>
<PermissionButton
shape="circle"
v-if="permission"
:tooltip="{
title: '修改密码'
}"
@click="editPasswordVisible = true"
>
<AIcon
style="font-size: 24px"
type="LockOutlined"
/>
</PermissionButton>
</j-space>
</div>
<j-space :size="24">
<j-button type="primary" @click="visible = true"
>查看详情</j-button
>
<j-button @click="editInfoVisible = true"
>编辑资料</j-button
>
<PermissionButton
v-if="permission"
@click="editPasswordVisible = true"
>
修改密码
</PermissionButton>
</j-space>
</div>
</div>
</div>
<div class="person-content">
<div class="person-content-item">
<FullPage>
<j-tabs v-model:activeKey="user.tabKey" type="card">
<j-tab-pane
v-for="item in list"
:key="item.key"
:tab="item.title"
/>
</j-tabs>
<div class="person-content-item-content">
<component :is="tabs[user.tabKey]" />
</div>
@ -114,9 +86,9 @@ import { updateMeInfo_api } from '@/api/account/center';
import { onlyMessage } from '@/utils/comm';
import { useRouterParams } from '@/utils/hooks/useParams';
import {
USER_CENTER_MENU_BUTTON_CODE,
USER_CENTER_MENU_CODE
} from '@/utils/consts'
USER_CENTER_MENU_BUTTON_CODE,
USER_CENTER_MENU_CODE,
} from '@/utils/consts';
import { usePermissionStore } from '@/store/permission';
const imageTypes = reactive([
@ -165,7 +137,8 @@ const editInfoVisible = ref<boolean>(false);
const editPasswordVisible = ref<boolean>(false);
const hasPermission = usePermissionStore().hasPermission;
const permission = () => hasPermission(`${USER_CENTER_MENU_CODE}:${USER_CENTER_MENU_BUTTON_CODE}`);
const permission = () =>
hasPermission(`${USER_CENTER_MENU_CODE}:${USER_CENTER_MENU_BUTTON_CODE}`);
const onActivated = (_key: KeyType) => {
user.tabKey = _key;
@ -219,15 +192,16 @@ watchEffect(() => {
.person {
.person-header {
width: 100%;
height: 150px;
height: 156px;
padding: 0 150px;
background-color: rgba(2, 125, 180, 0.368);
background-color: #fff;
.person-header-item {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
.person-header-item-info {
padding-top: 30px;
display: flex;
.person-header-item-info-left {
margin-right: 30px;
@ -238,61 +212,43 @@ watchEffect(() => {
flex-direction: column;
justify-content: space-between;
.person-header-item-info-right-top {
span {
background-color: rgba(
255,
255,
128,
0.43137254901960786
);
border-radius: 5px;
padding: 0 10px;
}
display: flex;
font-size: 26px;
color: #1d2129;
font-weight: 500;
// > :not(:last-child) {
// margin-right: 20px;
// }
}
.person-header-item-info-right-info {
color: #fff;
display: flex;
font-size: 16px;
> :not(:last-child) {
margin-right: 20px;
span {
background-color: #f7f8fa;
border-radius: 5px;
padding: 0 10px;
}
}
}
}
.person-header-item-action {
position: absolute;
width: 100%;
height: 50px;
z-index: 2;
left: 0;
bottom: -25px;
padding: 0 50px;
display: flex;
justify-content: space-between;
align-items: center;
.person-header-item-action-left {
button {
height: 35px;
padding: 0 40px;
}
}
.person-header-item-action-right {
:deep(button) {
height: 50px;
width: 50px;
}
button {
width: 110px;
}
}
}
}
.person-content-item {
padding: 10px;
background-color: #fff;
}
.person-content {
width: 100%;
padding: 0 150px;
padding: 0 260px;
// margin-top: 15px;
.person-content-item-content {
padding: 20px;
padding: 0 20px;
}
}
}

View File

@ -83,6 +83,10 @@ watch(
{ immediate: true, deep: true },
);
const productName = computed(() => {
return productList.value.find(item => item.id === modelRef.product)?.name || ''
})
const handleOk = async () => {
const params = encodeQuery(props.data);
// downloadFile(
@ -97,7 +101,7 @@ const handleOk = async () => {
if (res) {
const blob = new Blob([res], { type: modelRef.fileType });
const url = URL.createObjectURL(blob);
downloadFileByUrl(url, `设备实例`, modelRef.fileType);
downloadFileByUrl(url, `${productName.value ? (productName.value + '下设备') : '设备实例'}`, modelRef.fileType);
emit('close');
}
};

View File

@ -1,70 +1,68 @@
<template>
<div class='file'>
<j-form layout='vertical'>
<!-- <j-form-item label='文件格式' >
<div class='file-type-label'>
<a-radio-group class='file-type-radio' v-model:value="modelRef.file.fileType" >
<a-radio-button value="xlsx">xlsx</a-radio-button>
<a-radio-button value="csv">csv</a-radio-button>
</a-radio-group>
<a-checkbox v-model:checked="modelRef.file.autoDeploy">自动启用</a-checkbox>
<div class="file">
<j-form layout="vertical">
<j-form-item label="下载模板">
<div class="file-download">
<j-button @click="downFile('xlsx')">模板格式.xlsx</j-button>
<j-button @click="downFile('csv')">模板格式.csv</j-button>
</div>
</j-form-item>
<j-form-item label="文件上传">
<!-- 导入系统已存在的设备数据不会更改已存在设备的所属产品信息 -->
<a-upload-dragger
v-model:fileList="modelRef.upload"
name="file"
:action="FILE_UPLOAD"
:headers="{
'X-Access-Token': LocalStore.get(TOKEN_KEY),
}"
:maxCount="1"
:showUploadList="false"
@change="uploadChange"
:accept="
modelRef?.file?.fileType
? `.${modelRef?.file?.fileType}`
: '.xlsx'
"
:before-upload="beforeUpload"
:disabled="disabled"
@drop="handleDrop"
>
<div
style="
height: 115px;
line-height: 115px;
color: #666666;
"
>
<!-- <AIcon style="font-size: 20px;" type="PlusCircleOutlined" /> -->
点击或拖拽上传文件
</div>
</a-upload-dragger>
</j-form-item>
<div style="margin-bottom: 16px">
<j-checkbox v-model:checked="modelRef.file.autoDeploy"
>导入并启用</j-checkbox
>
</div>
</j-form>
<div v-if="importLoading" class="result">
<div v-if="flag">
<j-spin size="small" style="margin-right: 10px" />正在导入
</div>
<div v-else>
<AIcon
style="color: #08e21e; margin-right: 10px; font-size: 16px"
type="CheckCircleOutlined"
/>
</div>
<div>导入成功{{ count }} </div>
<div>
导入失败<span style="color: #ff595e">{{ errCount }}</span>
<a v-if="errMessage && !flag && errCount > 0" style="margin-left: 20px" @click="downError">下载</a>
</div>
</div>
</j-form-item> -->
<j-form-item label='下载模板'>
<div class='file-download'>
<j-button @click="downFile('xlsx')">模板格式.xlsx</j-button>
<j-button @click="downFile('csv')">模板格式.csv</j-button>
</div>
</j-form-item>
<j-row type="flex">
<j-col :flex="600">
<j-form-item label="文件上传">
<a-upload-dragger
v-model:fileList="modelRef.upload"
name="file"
:action="FILE_UPLOAD"
:headers="{
'X-Access-Token': LocalStore.get(TOKEN_KEY),
}"
:maxCount="1"
:showUploadList="false"
@change="uploadChange"
:accept="
modelRef?.file?.fileType ? `.${modelRef?.file?.fileType}` : '.xlsx'
"
:before-upload="beforeUpload"
:disabled='disabled'
@drop="handleDrop"
>
<div style="height: 115px; line-height: 115px;">
<template v-if="!modelRef.upload.length">
点击或拖拽上传文件
</template>
<template v-else>
重传
</template>
</div>
</a-upload-dragger>
</j-form-item>
</j-col>
<j-col flex="auto">
<j-form-item>
<a-checkbox style="margin: 30px 0 0 30px;" v-model:checked="modelRef.file.autoDeploy">导入并启用</a-checkbox>
</j-form-item>
</j-col>
</j-row>
</j-form>
<div v-if="importLoading">
<!-- <j-badge v-if="flag" status="processing" text="正在导入" />
<j-badge v-else status="success" text="导入完成" /> -->
<div v-if="flag">正在导入</div>
<div v-else>导入完成</div>
<!-- <span>总数量{{ count }}</span>
<p style="color: red">{{ errMessage }}</p> -->
<div>导入成功{{ count }} </div>
<div>导入失败{{ count }} <a style="margin-left: 20px;">下载</a></div>
</div>
</div>
</template>
<script setup lang='ts' name='DeviceImportFile'>
@ -72,129 +70,143 @@ import { FILE_UPLOAD } from '@/api/comm';
import { TOKEN_KEY } from '@/utils/variable';
import { LocalStore, onlyMessage } from '@/utils/comm';
import { downloadFileByUrl } from '@/utils/utils';
import {
deviceImport,
templateDownload,
} from '@/api/device/instance';
import { deviceImport, templateDownload } from '@/api/device/instance';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { message } from 'jetlinks-ui-components'
import { message } from 'jetlinks-ui-components';
const props = defineProps({
product: {
type: String,
default: undefined
}
})
product: {
type: String,
default: undefined,
},
});
const modelRef = reactive({
product: props.product,
upload: [],
file: {
fileType: 'xlsx',
autoDeploy: false,
},
product: props.product,
upload: [],
file: {
fileType: 'xlsx',
autoDeploy: false,
},
});
const importLoading = ref<boolean>(false);
const flag = ref<boolean>(false);
const count = ref<number>(0);
const errCount = ref<number>(0);
const errMessage = ref<string>('');
const disabled = ref(false)
const disabled = ref(false);
const downFile = async (type: string) => {
const res: any = await templateDownload(props.product!, type);
if (res) {
const blob = new Blob([res], { type: type });
const url = URL.createObjectURL(blob);
downloadFileByUrl(url, `设备导入模板`, type);
}
const res: any = await templateDownload(props.product!, type);
if (res) {
const blob = new Blob([res], { type: type });
const url = URL.createObjectURL(blob);
downloadFileByUrl(url, `设备导入模板`, type);
}
};
const beforeUpload = (_file: any) => {
const fileType = modelRef.file?.fileType === 'csv' ? 'csv' : 'xlsx';
const isCsv = _file.type === 'text/csv';
const isXlsx = _file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
if (!isCsv && fileType !== 'xlsx') {
onlyMessage('请上传.csv格式文件', 'warning');
}
if (!isXlsx && fileType !== 'csv') {
onlyMessage('请上传.xlsx格式文件', 'warning');
}
return (isCsv && fileType !== 'xlsx') || (isXlsx && fileType !== 'csv');
const fileType = modelRef.file?.fileType === 'csv' ? 'csv' : 'xlsx';
const isCsv = _file.type === 'text/csv';
const isXlsx =
_file.type ===
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
if (!isCsv && fileType !== 'xlsx') {
onlyMessage('请上传.csv格式文件', 'warning');
}
if (!isXlsx && fileType !== 'csv') {
onlyMessage('请上传.xlsx格式文件', 'warning');
}
return (isCsv && fileType !== 'xlsx') || (isXlsx && fileType !== 'csv');
};
const handleDrop = () => {
const handleDrop = () => {};
const downError = () => {
window.open(errMessage.value)
}
const submitData = async (fileUrl: string) => {
if (!!fileUrl) {
count.value = 0;
errMessage.value = '';
const autoDeploy = !!modelRef?.file?.autoDeploy || false;
importLoading.value = true;
let dt = 0;
const source = new EventSourcePolyfill(
deviceImport(props.product!, fileUrl, autoDeploy),
);
source.onmessage = (e: any) => {
const res = JSON.parse(e.data);
if (res.success) {
const temp = res.result.total;
dt += temp;
count.value = dt;
} else {
errMessage.value = res.message || '失败';
}
disabled.value = false
};
source.onerror = (e: { status: number }) => {
if (e.status === 403) errMessage.value = '暂无权限,请联系管理员';
flag.value = false;
disabled.value = false
source.close();
};
source.onopen = () => {};
} else {
message.error('请先上传文件');
}
if (!!fileUrl) {
count.value = 0;
errCount.value = 0;
const autoDeploy = !!modelRef?.file?.autoDeploy || false;
importLoading.value = true;
let dt = 0;
let et = 0;
const source = new EventSourcePolyfill(
deviceImport(props.product!, fileUrl, autoDeploy),
);
source.onmessage = (e: any) => {
const res = JSON.parse(e.data);
if (res.success) {
const temp = res.result.total;
dt += temp;
count.value = dt;
} else {
if (res.detailFile) {
errMessage.value = res.detailFile
} else {
et += 1;
errCount.value = et;
}
}
disabled.value = false;
};
source.onerror = (e: { status: number }) => {
if (e.status === 403) errMessage.value = '暂无权限,请联系管理员';
flag.value = false;
disabled.value = false;
source.close();
};
source.onopen = () => {};
} else {
message.error('请先上传文件');
}
};
const uploadChange = async (info: Record<string, any>) => {
disabled.value = true
// console.log(info.file)
if (info.file.status === 'done') {
const resp: any = info.file.response || { result: '' };
await submitData(resp?.result || '');
}
disabled.value = true;
if (info.file.status === 'done') {
const resp: any = info.file.response || { result: '' };
await submitData(resp?.result || '');
}
};
</script>
<style scoped lang='less'>
.file {
.file-type-label {
display: flex;
gap: 16px;
align-items: center;
.file-type-label {
display: flex;
gap: 16px;
align-items: center;
.file-type-radio {
display: flex;
flex-grow: 1;
.file-type-radio {
display: flex;
flex-grow: 1;
:deep(.ant-radio-button-wrapper) {
width: 50%;
}
:deep(.ant-radio-button-wrapper) {
width: 50%;
}
}
}
}
.file-download {
display: flex;
gap: 16px;
>button {
flex: 1;
min-width: 0;
.file-download {
display: flex;
gap: 16px;
> button {
flex: 1;
min-width: 0;
}
}
.result {
div {
margin: 5px 0;
color: #605e5c;
}
}
}
}
</style>

View File

@ -285,6 +285,14 @@ const getProtocol = async () => {
if (productStore.current?.accessProvider === 'plugin_gateway') {
list.value.push({ key: 'MetadataMap', tab: '物模型映射'})
}
// if (
// instanceStore.current?.features?.find(
// (item: any) => item?.id === 'diffMetadataSameProduct',
// ) &&
// !keys.includes('MetadataMap')
// ) {
// list.value.push({ key: 'MetadataMap', tab: ''});
// }
}
};
/**

View File

@ -172,7 +172,7 @@ const saveImage = (url: string) => {
width: @with;
height: @height;
overflow: hidden;
//border-radius: 50%;
border-radius: 5px;
// border: @border;
transition: all 0.3s;
@ -197,9 +197,9 @@ const saveImage = (url: string) => {
flex-direction: column;
width: 100%;
height: 100%;
background-color: rgba(#000, 0.06);
// background-color: rgba(#000, 0.06);
cursor: pointer;
padding: 8px;
// padding: 8px;
.upload-image-mask {
.flex-center();
@ -211,7 +211,7 @@ const saveImage = (url: string) => {
width: 100%;
height: 100%;
color: #fff;
font-size: 16px;
font-size: 14px;
background-color: @mask-color;
}

View File

@ -47,7 +47,7 @@ const props = defineProps({
},
photoUrl: {
type: String,
default: getImage('/apply/provider1.png'),
default: getImage('/apply/internal-standalone.png'),
},
options: {
type: Array as PropType<any[]>,
@ -58,12 +58,12 @@ const props = defineProps({
const emit = defineEmits(['update:value', 'update:photoUrl']);
const defaultImg = {
'internal-standalone': getImage('/apply/provider1.png'),
'internal-integrated': getImage('/apply/provider2.png'),
'wechat-webapp': getImage('/apply/provider4.png'),
'dingtalk-ent-app': getImage('/apply/provider3.png'),
'third-party': getImage('/apply/provider5.png'),
'wechat-miniapp': getImage('/apply/provider1.png'),
'internal-standalone': getImage('/apply/internal-standalone.png'),
'internal-integrated': getImage('/apply/internal-integrated.png'),
'wechat-webapp': getImage('/apply/wechat-webapp.png'),
'dingtalk-ent-app': getImage('/apply/dingtalk-ent-app.png'),
'third-party': getImage('/apply/third-party.png'),
'wechat-miniapp': getImage('/apply/wechat-miniapp.png'),
}
const urlValue = ref<any>({...defaultImg});

View File

@ -1427,7 +1427,7 @@ const loading = ref<boolean>(false);
const initForm: formType = {
name: '',
provider: 'internal-standalone',
logoUrl: getImage('/apply/provider1.png'),
logoUrl: getImage('/apply/internal-standalone.png'),
integrationModes: [],
description: '',
page: {

View File

@ -1,5 +1,5 @@
<template>
<div class="child-item">
<div class="child-item" :class="{'border': !isLast}">
<div class="child-item-left">
<div style="font-weight: 600">
{{ data?.name }}
@ -156,6 +156,10 @@ const props = defineProps({
type: Object,
default: () => {},
},
isLast: {
type: Boolean,
default: false,
}
});
const emits = defineEmits(['refresh']);
@ -347,7 +351,6 @@ const onSave = (_data: any) => {
.child-item {
padding: 10px 20px;
margin: 5px;
background: #f7f7f7;
display: flex;
justify-content: space-between;
align-items: center;
@ -410,5 +413,9 @@ const onSave = (_data: any) => {
}
}
}
&.border {
box-shadow: 0px 1px 0px 0px #E2E2E2;
}
}
</style>

View File

@ -1,12 +1,12 @@
import { getImage } from "@/utils/comm";
const iconMap = new Map();
iconMap.set('notifier-dingTalk', getImage('/notice/dingtalk.png'));
iconMap.set('notifier-weixin', getImage('/notice/wechat.png'));
iconMap.set('notifier-email', getImage('/notice/email.png'));
iconMap.set('notifier-voice', getImage('/notice/voice.png'));
iconMap.set('notifier-sms', getImage('/notice/sms.png'));
iconMap.set('inside-mail', getImage('/notice/inside-mail.png'));
iconMap.set('notifier-dingTalk', getImage('/notice-rule/dingtalk.png'));
iconMap.set('notifier-weixin', getImage('/notice-rule/wechat.png'));
iconMap.set('notifier-email', getImage('/notice-rule/email.png'));
iconMap.set('notifier-voice', getImage('/notice-rule/voice.png'));
iconMap.set('notifier-sms', getImage('/notice-rule/sms.png'));
iconMap.set('inside-mail', getImage('/notice-rule/inside-mail.png'));
const noticeType = new Map();
noticeType.set('notifier-dingTalk', 'dingTalk');

View File

@ -30,12 +30,13 @@
>
<div class="child">
<template
v-for="child in item.children"
v-for="(child, index) in item.children"
:key="child.provider"
>
<Item
:data="data.find(i => i?.provider === child?.provider)"
@refresh="onRefresh"
:isLast="index === item.children?.length"
/>
</template>
</div>
@ -156,13 +157,18 @@ onMounted(() => {
background-color: #f6f6f6;
}
.custom {
background: #f7f7f7;
// background: #f7f7f7;
border-radius: 4px;
border: 0;
overflow: hidden;
}
.child {
background-color: white;
padding: 10px;
}
.content-collapse {
:deep(.ant-collapse-content > .ant-collapse-content-box) {
padding: 0;
}
}
</style>

View File

@ -239,12 +239,13 @@ const loading = ref(false);
const bindings = ref<any[]>();
// const basis = ref<any>({});
const defaultImg = getImage('/apply/provider1.png');
const defaultImg = getImage('/apply/internal-standalone.png');
const iconMap = new Map();
iconMap.set('dingtalk-ent-app', getImage('/bind/dingtalk.png'));
iconMap.set('wechat-webapp', getImage('/bind/wechat-webapp.png'));
iconMap.set('internal-standalone', getImage('/apply/provider1.png'));
iconMap.set('third-party', getImage('/apply/provider5.png'));
iconMap.set('internal-standalone', getImage('/apply/internal-standalone.png'));
iconMap.set('third-party', getImage('/apply/third-party.png'));
iconMap.set('wechat-miniapp', getImage('/apply/wechat-miniapp.png'));
const onFinish = async () => {
try {

View File

@ -3825,8 +3825,8 @@ jetlinks-store@^0.0.3:
jetlinks-ui-components@^1.0.24:
version "1.0.24"
resolved "http://registry.jetlinks.cn/jetlinks-ui-components/-/jetlinks-ui-components-1.0.24.tgz#97580bed720526b50b3244440c7ae16d3d0a26c0"
integrity sha512-7ccv/eu9moZZFzCRuBa8Pe4NLd/knDARWwJaivH+qgkPSIIdij0Wax27zFwoRqivsDzbOAs2iRButcwSNvR9AQ==
resolved "http://registry.jetlinks.cn/jetlinks-ui-components/-/jetlinks-ui-components-1.0.24.tgz#747dbd3c6d8af389d4eb02ffc37e9ee9a8da94c5"
integrity sha512-gJ+IJ90p6Q8YQJxfBjep3BCBHg/conxYOHv3v0aiGrzKzdidSk075OcXxOx7bIeAWoqBbY1uCNhYfnhFWCwVIQ==
dependencies:
"@vueuse/core" "^9.12.0"
"@vueuse/router" "^9.13.0"
@ -3834,6 +3834,7 @@ jetlinks-ui-components@^1.0.24:
colorpicker-v3 "^2.10.2"
lodash-es "^4.17.21"
monaco-editor "^0.35.0"
sortablejs "^1.15.0"
vuedraggable "^4.1.0"
js-cookie@^3.0.1:
@ -6168,6 +6169,11 @@ sortablejs@1.14.0:
resolved "http://registry.jetlinks.cn/sortablejs/-/sortablejs-1.14.0.tgz#6d2e17ccbdb25f464734df621d4f35d4ab35b3d8"
integrity sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==
sortablejs@^1.15.0:
version "1.15.0"
resolved "http://registry.jetlinks.cn/sortablejs/-/sortablejs-1.15.0.tgz#53230b8aa3502bb77a29e2005808ffdb4a5f7e2a"
integrity sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"