fix: 合并冲突

This commit is contained in:
100011797 2023-01-30 18:12:57 +08:00
commit a8ef765702
26 changed files with 1964 additions and 409 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -16,7 +16,7 @@ export const queryTree = (params?: Record<string, any>) => server.post<CategoryI
/** /**
* Id修改 * Id修改
*/ */
export const updateTree = (data: any, id:string) => server.put(`/device/category/${id}`, data) export const updateTree = (id:string,data: any,) => server.put(`/device/category/${id}`, data)
/** /**
* Id删除数据 * Id删除数据

View File

@ -1,5 +1,5 @@
import server from '@/utils/request' import server from '@/utils/request'
import { DeviceMetadata, ProductItem } from '@/views/device/Product/typings' import { DeviceMetadata, ProductItem, DepartmentItem } from '@/views/device/Product/typings'
/** /**
* *
@ -40,13 +40,78 @@ export const detail = (id: string) => server.get<ProductItem>(`/device-product/$
/** /**
* *
* @param data * @param data
*/ */
export const category = (data: any) => server.post('/device/category/_tree', data) export const category = (data: any) => server.post('/device/category/_tree', data)
/**
*
*/
export const getProviders = () => server.get('/gateway/device/providers')
/**
*
* @param params
*/
export const queryOrgThree = (params?: Record<string, any>) => server.post<DepartmentItem>('/organization/_all/tree', params)
/**
*
* @param data
*/
export const queryGatewayList = (data: any) => server.post('/gateway/device/_query/no-paging', data)
/**
* ()
* @param data
*/
export const queryProductList = (data: any) => server.post('/device-product/_query', data)
/**
*
* @param productId ID
* @param data
* @returns
*/
export const _deploy = (productId: string) => server.post(`/device-product/${productId}/deploy`)
/**
*
* @param productId ID
* @param data
* @returns
*/
export const _undeploy = (productId: string) => server.post(`/device-product/${productId}/undeploy`)
/**
*
* @param data
* @returns
*/
export const addProduct = (data:any) => server.post('/device-product',data)
/**
*
* @param id ID
* @param data
* @returns
*/
export const editProduct = (data: any) => server.patch('/device-product', data)
/**
*
* @param id ID
*/
export const deleteProduct = (id: string) => server.patch(`/device-product/${id}`)
/**
* Id唯一性
* @param id ID
*/
export const queryProductId = (id: string) => server.post(`/device-product/${id}/exists`)
/** /**
* *
* @param data * @param data
* @returns * @returns
*/ */
export const saveProductMetadata = (data: Record<string, unknown>) => server.patch('/device-product', data) export const saveProductMetadata = (data: Record<string, unknown>) => server.patch('/device-product', data)

View File

@ -7,21 +7,16 @@ import {
provide provide
} from 'vue' } from 'vue'
import type { DefineComponent, ExtractPropTypes, PropType, CSSProperties, Plugin, App } from 'vue' import type { ExtractPropTypes, PropType, CSSProperties} from 'vue'
import { Layout } from 'ant-design-vue' import { Layout } from 'ant-design-vue'
import useConfigInject from 'ant-design-vue/es/_util/hooks/useConfigInject' import { defaultSettingProps } from './defaultSetting'
import { defaultSettingProps, defaultSettings } from './defaultSetting'
import type { PureSettings } from './defaultSetting'
import type { BreadcrumbProps, RouteContextProps } from './RouteContext' import type { BreadcrumbProps, RouteContextProps } from './RouteContext'
import type { import type {
BreadcrumbRender, BreadcrumbRender,
CollapsedButtonRender, CustomRender, CollapsedButtonRender, CustomRender,
FooterRender,
HeaderContentRender,
HeaderRender, HeaderRender,
MenuContentRender, MenuContentRender,
MenuExtraRender, MenuExtraRender,
MenuFooterRender,
MenuHeaderRender, MenuHeaderRender,
MenuItemRender, MenuItemRender,
RightContentRender, RightContentRender,

View File

@ -8,7 +8,6 @@ import { defaultSettingProps } from 'components/Layout/defaultSetting'
import PropTypes from 'ant-design-vue/es/_util/vue-types' import PropTypes from 'ant-design-vue/es/_util/vue-types'
import { CustomRender, MenuDataItem, ProProps, RightContentRender, WithFalse } from 'components/Layout/typings' import { CustomRender, MenuDataItem, ProProps, RightContentRender, WithFalse } from 'components/Layout/typings'
import './index.less' import './index.less'
import { omit } from 'lodash-es'
import { RouteRecordRaw } from 'vue-router' import { RouteRecordRaw } from 'vue-router'
import { clearMenuItem } from 'components/Layout/utils' import { clearMenuItem } from 'components/Layout/utils'

View File

@ -1,13 +1,8 @@
import type { ExtractPropTypes, PropType } from 'vue' import type { ExtractPropTypes } from 'vue'
import PropTypes from 'ant-design-vue/es/_util/vue-types';
import type { MenuDataItem, WithFalse, ProProps, CustomRender, RightContentRender } from '../../typings'
import { siderMenuProps } from '../SiderMenu/SiderMenu'
import { defaultSettingProps } from 'components/Layout/defaultSetting'
import Header, { headerProps } from './Header' import Header, { headerProps } from './Header'
import { useRouteContext } from 'components/Layout/RouteContext' import { useRouteContext } from 'components/Layout/RouteContext'
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
import { clearMenuItem } from 'components/Layout/utils' import { clearMenuItem } from 'components/Layout/utils'
import DefaultSetting from '../../../../../config/config'
import { Layout } from 'ant-design-vue' import { Layout } from 'ant-design-vue'
export const headerViewProps = { export const headerViewProps = {

View File

@ -195,6 +195,7 @@ const PageContainer = defineComponent({
const extra = getSlotVNode<DefaultPropRender>(slots, props, 'extra'); const extra = getSlotVNode<DefaultPropRender>(slots, props, 'extra');
const extraContent = getSlotVNode<DefaultPropRender>(slots, props, 'extraContent'); const extraContent = getSlotVNode<DefaultPropRender>(slots, props, 'extraContent');
const subTitle = getSlotVNode<DefaultPropRender>(slots, props, 'subTitle'); const subTitle = getSlotVNode<DefaultPropRender>(slots, props, 'subTitle');
const title = getSlotVNode<DefaultPropRender>(slots, props, 'title');
// @ts-ignore // @ts-ignore
return ( return (
@ -202,6 +203,7 @@ const PageContainer = defineComponent({
{...props} {...props}
prefixCls={undefined} prefixCls={undefined}
ghost={ghost.value} ghost={ghost.value}
title={title}
subTitle={subTitle} subTitle={subTitle}
content={headerContent} content={headerContent}
// tags={tags} // tags={tags}

View File

@ -18,10 +18,6 @@ import './SiderMenu.less'
import { computed } from 'vue' import { computed } from 'vue'
import { omit } from 'lodash-es' import { omit } from 'lodash-es'
export type PrivateSiderMenuProps = {
matchMenuKeys?: string[];
}
const { Sider } = Layout const { Sider } = Layout
export const defaultRenderLogo = (logo?: CustomRender, logoStyle?: CSSProperties): CustomRender => { export const defaultRenderLogo = (logo?: CustomRender, logoStyle?: CSSProperties): CustomRender => {
@ -89,7 +85,6 @@ const SiderMenu: FunctionalComponent<SiderMenuProps> = (props, { slots, emit}) =
const { const {
collapsed, collapsed,
collapsedWidth = 48, collapsedWidth = 48,
menuExtraRender = false,
menuContentRender = false, menuContentRender = false,
collapsedButtonRender = defaultRenderCollapsedButton, collapsedButtonRender = defaultRenderCollapsedButton,
} = props; } = props;
@ -97,7 +92,6 @@ const SiderMenu: FunctionalComponent<SiderMenuProps> = (props, { slots, emit}) =
const context = useRouteContext(); const context = useRouteContext();
const sSideWidth = computed(() => (props.collapsed ? props.collapsedWidth : props.siderWidth)); const sSideWidth = computed(() => (props.collapsed ? props.collapsedWidth : props.siderWidth));
const extraDom = menuExtraRender && menuExtraRender(props);
const handleSelect = ($event: string[]) => { const handleSelect = ($event: string[]) => {
if (props.onSelect) { if (props.onSelect) {

View File

@ -4,14 +4,14 @@
<a-popconfirm v-bind="popConfirm" :disabled="!isPermission || props.disabled"> <a-popconfirm v-bind="popConfirm" :disabled="!isPermission || props.disabled">
<a-tooltip v-if="tooltip" v-bind="tooltip"> <a-tooltip v-if="tooltip" v-bind="tooltip">
<slot v-if="noButton"></slot> <slot v-if="noButton"></slot>
<a-button v-else v-bind="buttonProps" :disabled="_isPermission"> <a-button v-else v-bind="buttonProps" :disabled="_isPermission" @click="handleClick">
<slot></slot> <slot></slot>
<template #icon> <template #icon>
<slot name="icon"></slot> <slot name="icon"></slot>
</template> </template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<a-button v-else v-bind="buttonProps" :disabled="_isPermission"> <a-button v-else v-bind="buttonProps" :disabled="_isPermission" @click="handleClick">
<slot></slot> <slot></slot>
<template #icon> <template #icon>
<slot name="icon"></slot> <slot name="icon"></slot>
@ -22,7 +22,7 @@
<template v-else-if="tooltip"> <template v-else-if="tooltip">
<a-tooltip v-bind="tooltip"> <a-tooltip v-bind="tooltip">
<slot v-if="noButton"></slot> <slot v-if="noButton"></slot>
<a-button v-else v-bind="buttonProps" :disabled="_isPermission"> <a-button v-else v-bind="buttonProps" :disabled="_isPermission" @click="handleClick">
<slot></slot> <slot></slot>
<template #icon> <template #icon>
<slot name="icon"></slot> <slot name="icon"></slot>
@ -32,7 +32,7 @@
</template> </template>
<template v-else> <template v-else>
<slot v-if="noButton"></slot> <slot v-if="noButton"></slot>
<a-button v-else v-bind="buttonProps" :disabled="_isPermission"> <a-button v-else v-bind="buttonProps" :disabled="_isPermission" @click="handleClick">
<slot></slot> <slot></slot>
<template #icon> <template #icon>
<slot name="icon"></slot> <slot name="icon"></slot>
@ -42,7 +42,7 @@
</template> </template>
<a-tooltip v-else title="没有权限"> <a-tooltip v-else title="没有权限">
<slot v-if="noButton"></slot> <slot v-if="noButton"></slot>
<a-button v-else v-bind="buttonProps" :disabled="_isPermission"> <a-button v-else v-bind="buttonProps" :disabled="_isPermission" @click="handleClick">
<slot></slot> <slot></slot>
<template #icon> <template #icon>
<slot name="icon"></slot> <slot name="icon"></slot>
@ -54,6 +54,12 @@
import type { ButtonProps, TooltipProps, PopconfirmProps } from 'ant-design-vue' import type { ButtonProps, TooltipProps, PopconfirmProps } from 'ant-design-vue'
import { usePermissionStore } from '@/store/permission'; import { usePermissionStore } from '@/store/permission';
interface PermissionButtonEmits {
(e: 'click', data: MouseEvent): void;
}
const emits = defineEmits<PermissionButtonEmits>()
interface PermissionButtonProps extends ButtonProps { interface PermissionButtonProps extends ButtonProps {
tooltip?: TooltipProps; tooltip?: TooltipProps;
popConfirm?: PopconfirmProps; popConfirm?: PopconfirmProps;
@ -80,6 +86,9 @@ const _isPermission = computed(() =>
: false : false
: true : true
) )
const handleClick = (e: MouseEvent) => {
emits('click', e)
}
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -1,6 +1,6 @@
import { createRouter, createWebHashHistory } from 'vue-router' import { createRouter, createWebHashHistory } from 'vue-router'
import menus, { LoginPath } from './menu' import menus, { LoginPath } from './menu'
import { getToken } from '@/utils/comm' import { cleanToken, getToken } from '@/utils/comm'
import { useUserInfo } from '@/store/userInfo' import { useUserInfo } from '@/store/userInfo'
import { useSystem } from '@/store/system' import { useSystem } from '@/store/system'
@ -27,20 +27,25 @@ router.beforeEach((to, from, next) => {
} else { } else {
const userInfo = useUserInfo() const userInfo = useUserInfo()
const system = useSystem() const system = useSystem()
if (!userInfo.$state.userInfos.username) {
userInfo.getUserInfo()
system.getSystemVersion().then((menuData: any[]) => {
menuData.forEach(r => {
router.addRoute('main', r)
})
const redirect = decodeURIComponent((from.query.redirect as string) || to.path)
if(to.path === redirect) {
next({ ...to, replace: true })
} else {
next({ path: redirect })
}
})
if (!userInfo.userInfos.username) {
userInfo.getUserInfo().then(() => {
system.getSystemVersion().then((menuData: any[]) => {
menuData.forEach(r => {
router.addRoute('main', r)
})
const redirect = decodeURIComponent((from.query.redirect as string) || to.path)
if(to.path === redirect) {
next({ ...to, replace: true })
} else {
next({ path: redirect })
}
})
}).catch(() => {
console.log('userInfo', userInfo)
cleanToken()
next({ path: LoginPath })
})
} else { } else {
next() next()
} }

View File

@ -149,5 +149,10 @@ export default [
{ {
path: '/iot/device/Category', path: '/iot/device/Category',
component: () => import('@/views/device/Category/index.vue') component: () => import('@/views/device/Category/index.vue')
} } ,
// 产品
{
path: '/iot/device/Product',
component: () => import('@/views/device/Product/index.vue')
}
] ]

View File

@ -1,7 +1,5 @@
import type { Slots } from 'vue' import type { Slots } from 'vue'
import { TOKEN_KEY } from '@/utils/variable' import { TOKEN_KEY } from '@/utils/variable'
import { Terms } from 'components/Search/types'
import { urlReg } from '@/utils/regular'
/** /**
* *
@ -40,6 +38,10 @@ export const getToken = () => {
return LocalStore.get(TOKEN_KEY) return LocalStore.get(TOKEN_KEY)
} }
export const cleanToken = () => {
LocalStore.remove(TOKEN_KEY)
}
/** /**
* TreeSelect过滤 * TreeSelect过滤
* @param value * @param value

View File

@ -13,23 +13,24 @@
> >
<a-form <a-form
layout="vertical" layout="vertical"
v-model="formModel"
:rules="rules"
ref="formRef" ref="formRef"
:rules="rules"
:model="formModel"
> >
<a-form-item label="名称" name="name" v-bind="validateInfos.name"> <a-form-item label="名称" name="name">
<a-input v-model:value="formModel.name" :maxlength="64" /> <a-input
v-model:value="formModel.name"
:maxlength="64"
placeholder="请输入名称"
/>
</a-form-item> </a-form-item>
<a-form-item <a-form-item label="排序" name="sortIndex">
label="排序"
name="sortIndex"
v-bind="validateInfos.sortIndex"
>
<a-input-number <a-input-number
style="width: 100%" style="width: 100%"
id="inputNumber" id="inputNumber"
v-model:value="formModel.sortIndex" v-model:value="formModel.sortIndex"
:min="1" :min="1"
placeholder="请输入排序"
/> />
</a-form-item> </a-form-item>
<a-form-item label="说明"> <a-form-item label="说明">
@ -37,6 +38,7 @@
v-model:value="formModel.description" v-model:value="formModel.description"
show-count show-count
:maxlength="200" :maxlength="200"
placeholder="请输入说明"
/> />
</a-form-item> </a-form-item>
</a-form> </a-form>
@ -44,10 +46,12 @@
</template> </template>
<script setup lang="ts" name="modifyModal"> <script setup lang="ts" name="modifyModal">
import { PropType } from 'vue'; import { PropType } from 'vue';
import { Form } from 'ant-design-vue'; import { Form, message } from 'ant-design-vue';
import { queryTree } from '@/api/device/category'; import { queryTree, saveTree, updateTree } from '@/api/device/category';
import { ValidateErrorEntity } from 'ant-design-vue/es/form/interface'; import { ValidateErrorEntity } from 'ant-design-vue/es/form/interface';
import { list } from '@/api/iot-card/home'; import { list } from '@/api/iot-card/home';
import { number } from 'echarts';
const emits = defineEmits(['refresh']); const emits = defineEmits(['refresh']);
const formRef = ref(); const formRef = ref();
const useForm = Form.useForm; const useForm = Form.useForm;
@ -65,8 +69,8 @@ const props = defineProps({
default: 0, default: 0,
}, },
isChild: { isChild: {
type: Boolean, type: Number,
default: false, default: 0,
}, },
}); });
interface formState { interface formState {
@ -75,6 +79,11 @@ interface formState {
description: string; description: string;
} }
const listData = ref([]); const listData = ref([]);
const childArr = ref([]);
const arr = ref([]);
const updateObj = ref({});
const addObj = ref({});
const addParams = ref({});
/** /**
* 表单数据 * 表单数据
*/ */
@ -84,8 +93,8 @@ const formModel = ref<formState>({
description: '', description: '',
}); });
const rules = ref({ const rules = ref({
name: [{ required: true, message: '请输入名称' }], name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
sortIndex: [{ required: true, message: '请输入排序' }], sortIndex: [{ required: true, message: '请输入排序', trigger: 'blur' }],
}); });
const visible = ref(false); const visible = ref(false);
const { resetFields, validate, validateInfos } = useForm( const { resetFields, validate, validateInfos } = useForm(
@ -96,53 +105,99 @@ const { resetFields, validate, validateInfos } = useForm(
* 提交数据 * 提交数据
*/ */
const submitData = async () => { const submitData = async () => {
validate() formRef.value.validate().then(async () => {
.then(async () => {}) addParams.value = {};
.catch((error: ValidateErrorEntity<formState>) => {}); if (props.isAdd === 0) {
if (props.isChild === 1) {
addParams.value = {
...formModel.value,
sortIndex:
childArr.value[childArr.value.length - 1].sortIndex + 1,
parentId: addObj.value.id,
};
} else if (props.isChild === 2) {
addParams.value = {
parentId: addObj.value.id,
...formModel.value,
sortIndex: 1,
};
} else if (props.isChild === 3) {
addParams.value = {
...formModel.value,
sortIndex: arr.value[arr.value.length - 1].sortIndex + 1,
};
}
const res = await saveTree(addParams.value);
if (res.status === 200) {
message.success('操作成功!');
visible.value = false;
emits('refresh');
} else {
message.error('操作失败!');
}
} else if (props.isAdd === 2) {
const id = updateObj.value.id;
const updateParams = {
...formModel.value,
id: updateObj.value.id,
key: updateObj.value.key,
parentId: updateObj.value.parentId,
};
const res = await updateTree(id, updateParams);
if (res.status === 200) {
message.success('操作成功!');
visible.value = false;
emits('refresh');
} else {
message.error('操作失败!');
}
}
});
}; };
/** /**
* 显示弹窗 * 显示弹窗
*/ */
const show = (row: any) => { const show = (row: any) => {
//
if (props.isAdd === 0) { if (props.isAdd === 0) {
// if (props.isChild === 1) {
if (props.isChild) { addObj.value = row;
//
if (row.children && row.children.length > 0) { if (row.children && row.children.length > 0) {
let childArr = []; childArr.value = row.children.sort(compare('sortIndex'));
childArr = row.children.sort(compare('sortIndex'));
formModel.value = { formModel.value = {
name: '', name: '',
sortIndex: childArr[childArr.length - 1].sortIndex + 1, sortIndex:
childArr.value[childArr.value.length - 1].sortIndex + 1,
description: '', description: '',
}; };
} else { visible.value = true;
}
} else if (props.isChild === 3) {
arr.value = listData.value.sort(compare('sortIndex'));
if (arr.value.length > 0) {
formModel.value = { formModel.value = {
name: '', name: '',
sortIndex: 1, sortIndex: arr.value[arr.value.length - 1].sortIndex + 1,
description: '', description: '',
}; };
} }
} else { visible.value = true;
let arr = []; } else if (props.isChild === 2) {
arr = listData.value.sort(compare('sortIndex')); if (row.level === 5) {
if (arr.length > 0) { message.warning('树形结构最多添加5层');
formModel.value = { visible.value = false;
name: '',
sortIndex: arr[arr.length - 1].sortIndex + 1,
description: '',
};
} else { } else {
addObj.value = row;
formModel.value = { formModel.value = {
name: '', name: '',
sortIndex: 1, sortIndex: 1,
description: '', description: '',
}; };
visible.value = true;
} }
} }
visible.value = true;
} else if (props.isAdd === 2) { } else if (props.isAdd === 2) {
updateObj.value = row;
// //
formModel.value = { formModel.value = {
name: row.name, name: row.name,
@ -152,10 +207,7 @@ const show = (row: any) => {
visible.value = true; visible.value = true;
} }
}; };
/**
* 判断是新增还是编辑
*/
const judgeIsAdd = () => {};
/** /**
* 排序 * 排序
*/ */
@ -183,7 +235,6 @@ const compare = (property: any) => {
const res = await queryTree(params); const res = await queryTree(params);
if (res.status === 200) { if (res.status === 200) {
listData.value = res.result; listData.value = res.result;
console.log(listData.value, 'listData.value');
} }
}; };
/** /**

View File

@ -1,7 +1,7 @@
<!--产品分类 --> <!--产品分类 -->
<template> <template>
<a-card class="product-category"> <a-card class="product-category">
<Search :columns="query.columns" target="category" /> <Search :columns="query.columns" target="category" @search="search" />
<JTable <JTable
ref="tableRef" ref="tableRef"
:columns="table.columns" :columns="table.columns"
@ -57,7 +57,7 @@
:title="title" :title="title"
:isAdd="isAdd" :isAdd="isAdd"
:isChild="isChild" :isChild="isChild"
@refresh="() => modifyRef.value?.reload()" @refresh="refresh"
/> />
</a-card> </a-card>
</template> </template>
@ -73,34 +73,41 @@ const dataSource = ref([]);
const currentForm = ref({}); const currentForm = ref({});
const title = ref(''); const title = ref('');
const isAdd = ref(0); const isAdd = ref(0);
const isChild = ref(false); const isChild = ref(0);
// //
const query = reactive({ const query = reactive({
columns: [ columns: [
{ {
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
ellipsis: true, key: 'name',
search: {
type: 'string',
},
}, },
{ {
title: '排序', title: '排序',
dataIndex: 'sortIndex', dataIndex: 'sortIndex',
valueType: 'digit', key: 'sortIndex',
sorter: true, search: {
type: 'number',
},
scopedSlots: true,
}, },
{ {
title: '描述', title: '描述',
key: 'description', key: 'description',
ellipsis: true,
dataIndex: 'description', dataIndex: 'description',
filters: true, search: {
onFilter: true, type: 'string',
},
}, },
{ {
title: '操作', title: '操作',
valueType: 'option', key: 'action',
width: 200,
fixed: 'right', fixed: 'right',
width: 250,
scopedSlots: true,
}, },
], ],
params: { params: {
@ -150,8 +157,12 @@ const getActions = (
onClick: () => { onClick: () => {
title.value = '新增子分类'; title.value = '新增子分类';
isAdd.value = 0; isAdd.value = 0;
isChild.value = true;
currentForm.value = {}; currentForm.value = {};
if (data.children && data.children.length > 0) {
isChild.value = 1;
} else {
isChild.value = 2;
}
nextTick(() => { nextTick(() => {
modifyRef.value.show(data); modifyRef.value.show(data);
}); });
@ -187,6 +198,7 @@ const table = reactive({
title: '排序', title: '排序',
dataIndex: 'sortIndex', dataIndex: 'sortIndex',
key: 'sortIndex', key: 'sortIndex',
scopedSlots: true,
}, },
{ {
title: '说明', title: '说明',
@ -207,13 +219,19 @@ const table = reactive({
add: async () => { add: async () => {
title.value = '新增分类'; title.value = '新增分类';
isAdd.value = 0; isAdd.value = 0;
isChild.value = false; isChild.value = 3;
nextTick(() => { nextTick(() => {
modifyRef.value.show(currentForm.value); modifyRef.value.show(currentForm.value);
}); });
}, },
/**
* 刷新表格数据
*/
refresh: () => {
tableRef.value?.reload();
},
}); });
const { add, columns } = toRefs(table); const { add, columns, refresh } = toRefs(table);
/** /**
* 初始化 * 初始化
*/ */

View File

@ -1,12 +1,12 @@
<template> <template>
<JTable <JTable
ref="instanceRef" ref="instanceRef"
:columns="columns" :columns="columns"
:request="query" :request="query"
:defaultParams="{sorts: [{name: 'createTime', order: 'desc'}]}" :defaultParams="{ sorts: [{ name: 'createTime', order: 'desc' }] }"
:rowSelection="{ :rowSelection="{
selectedRowKeys: _selectedRowKeys, selectedRowKeys: _selectedRowKeys,
onChange: onSelectChange onChange: onSelectChange,
}" }"
@cancelSelect="cancelSelect" @cancelSelect="cancelSelect"
:params="params" :params="params"
@ -17,37 +17,79 @@
<a-dropdown> <a-dropdown>
<a-button>批量操作 <AIcon type="DownOutlined" /></a-button> <a-button>批量操作 <AIcon type="DownOutlined" /></a-button>
<template #overlay> <template #overlay>
<a-menu> <a-menu>
<a-menu-item> <a-menu-item>
<a-button @click="exportVisible = true"><AIcon type="ExportOutlined" />批量导出设备</a-button> <a-button @click="exportVisible = true"
</a-menu-item> ><AIcon
<a-menu-item> type="ExportOutlined"
<a-button @click="importVisible = true"><AIcon type="ImportOutlined" />批量导入设备</a-button> /></a-button
</a-menu-item> >
<a-menu-item> </a-menu-item>
<a-popconfirm @confirm="activeAllDevice" title="确认激活全部设备?"> <a-menu-item>
<a-button type="primary" ghost><AIcon type="CheckCircleOutlined" />激活全部设备</a-button> <a-button @click="importVisible = true"
</a-popconfirm> ><AIcon
</a-menu-item> type="ImportOutlined"
<a-menu-item> /></a-button
<a-button @click="syncDeviceStatus" type="primary"><AIcon type="SyncOutlined" />同步设备状态</a-button> >
</a-menu-item> </a-menu-item>
<a-menu-item v-if="_selectedRowKeys.length"> <a-menu-item>
<a-popconfirm @confirm="delSelectedDevice" title="已启用的设备无法删除,确认删除选中的禁用状态设备?"> <a-popconfirm
<a-button type="primary" danger><AIcon type="DeleteOutlined" />删除选中设备</a-button> @confirm="activeAllDevice"
</a-popconfirm> title="确认激活全部设备?"
</a-menu-item> >
<a-menu-item v-if="_selectedRowKeys.length" title="确认激活选中设备?"> <a-button type="primary" ghost
<a-popconfirm @confirm="activeSelectedDevice" > ><AIcon
<a-button type="primary"><AIcon type="CheckOutlined" />激活选中设备</a-button> type="CheckCircleOutlined"
</a-popconfirm> /></a-button
</a-menu-item> >
<a-menu-item v-if="_selectedRowKeys.length"> </a-popconfirm>
<a-popconfirm @confirm="disabledSelectedDevice" title="确认禁用选中设备?"> </a-menu-item>
<a-button type="primary" danger><AIcon type="StopOutlined" />禁用选中设备</a-button> <a-menu-item>
</a-popconfirm> <a-button
</a-menu-item> @click="syncDeviceStatus"
</a-menu> type="primary"
><AIcon
type="SyncOutlined"
/></a-button
>
</a-menu-item>
<a-menu-item v-if="_selectedRowKeys.length">
<a-popconfirm
@confirm="delSelectedDevice"
title="已启用的设备无法删除,确认删除选中的禁用状态设备?"
>
<a-button type="primary" danger
><AIcon
type="DeleteOutlined"
/></a-button
>
</a-popconfirm>
</a-menu-item>
<a-menu-item
v-if="_selectedRowKeys.length"
title="确认激活选中设备?"
>
<a-popconfirm @confirm="activeSelectedDevice">
<a-button type="primary"
><AIcon
type="CheckOutlined"
/></a-button
>
</a-popconfirm>
</a-menu-item>
<a-menu-item v-if="_selectedRowKeys.length">
<a-popconfirm
@confirm="disabledSelectedDevice"
title="确认禁用选中设备?"
>
<a-button type="primary" danger
><AIcon
type="StopOutlined"
/></a-button
>
</a-popconfirm>
</a-menu-item>
</a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
</a-space> </a-space>
@ -69,31 +111,44 @@
> >
<template #img> <template #img>
<slot name="img"> <slot name="img">
<img :src="getImage('/device/instance/device-card.png')" /> <img
:src="getImage('/device/instance/device-card.png')"
/>
</slot> </slot>
</template> </template>
<template #content> <template #content>
<h3 class="card-item-content-title" @click.stop="handleView(slotProps.id)">{{ slotProps.name }}</h3> <h3
class="card-item-content-title"
@click.stop="handleView(slotProps.id)"
>
{{ slotProps.name }}
</h3>
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<div class="card-item-content-text">设备类型</div> <div class="card-item-content-text">设备类型</div>
<div>{{slotProps.deviceType.text}}</div> <div>{{ slotProps.deviceType.text }}</div>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<div class="card-item-content-text">产品名称</div> <div class="card-item-content-text">产品名称</div>
<div>{{slotProps.productName}}</div> <div>{{ slotProps.productName }}</div>
</a-col> </a-col>
</a-row> </a-row>
</template> </template>
<template #actions="item"> <template #actions="item">
<a-tooltip v-bind="item.tooltip" :title="item.disabled && item.tooltip.title"> <a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
>
<a-popconfirm <a-popconfirm
v-if="item.popConfirm" v-if="item.popConfirm"
v-bind="item.popConfirm" v-bind="item.popConfirm"
:disabled="item.disabled" :disabled="item.disabled"
> >
<a-button :disabled="item.disabled"> <a-button :disabled="item.disabled">
<AIcon type="DeleteOutlined" v-if="item.key === 'delete'" /> <AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else> <template v-else>
<AIcon :type="item.icon" /> <AIcon :type="item.icon" />
<span>{{ item.text }}</span> <span>{{ item.text }}</span>
@ -101,8 +156,14 @@
</a-button> </a-button>
</a-popconfirm> </a-popconfirm>
<template v-else> <template v-else>
<a-button :disabled="item.disabled" @click="item.onClick"> <a-button
<AIcon type="DeleteOutlined" v-if="item.key === 'delete'" /> :disabled="item.disabled"
@click="item.onClick"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else> <template v-else>
<AIcon :type="item.icon" /> <AIcon :type="item.icon" />
<span>{{ item.text }}</span> <span>{{ item.text }}</span>
@ -114,7 +175,10 @@
</CardBox> </CardBox>
</template> </template>
<template #state="slotProps"> <template #state="slotProps">
<a-badge :text="slotProps.state.text" :status="statusMap.get(slotProps.state.value)" /> <a-badge
:text="slotProps.state === 1 ? ' 正常' : '禁用'"
:status="statusMap.get(slotProps.state)"
/>
</template> </template>
<template #action="slotProps"> <template #action="slotProps">
<a-space :size="16"> <a-space :size="16">
@ -123,7 +187,11 @@
:key="i.key" :key="i.key"
v-bind="i.tooltip" v-bind="i.tooltip"
> >
<a-popconfirm v-if="i.popConfirm" v-bind="i.popConfirm" :disabled="i.disabled"> <a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
:disabled="i.disabled"
>
<a-button <a-button
:disabled="i.disabled" :disabled="i.disabled"
style="padding: 0" style="padding: 0"
@ -149,44 +217,59 @@
</template> </template>
</JTable> </JTable>
<Import v-if="importVisible" @close="importVisible = false" /> <Import v-if="importVisible" @close="importVisible = false" />
<Export v-if="exportVisible" @close="exportVisible = false" :data="params" /> <Export
<Process v-if="operationVisible" @close="operationVisible = false" :api="api" :type="type" /> v-if="exportVisible"
<Save v-if="visible" :data="current" @close="visible = false" @save="saveBtn" /> @close="exportVisible = false"
:data="params"
/>
<Process
v-if="operationVisible"
@close="operationVisible = false"
:api="api"
:type="type"
/>
<Save v-if="visible" :data="current" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { query, _delete, _deploy, _undeploy, batchUndeployDevice, batchDeployDevice, batchDeleteDevice } from '@/api/device/instance' import {
import type { ActionsType } from '@/components/Table/index.vue' query,
_delete,
_deploy,
_undeploy,
batchUndeployDevice,
batchDeployDevice,
batchDeleteDevice,
} from '@/api/device/instance';
import type { ActionsType } from '@/components/Table/index.vue';
import { getImage, LocalStore } from '@/utils/comm'; import { getImage, LocalStore } from '@/utils/comm';
import { message } from "ant-design-vue"; import { message } from 'ant-design-vue';
import Import from './Import/index.vue' import Import from './Import/index.vue';
import Export from './Export/index.vue' import Export from './Export/index.vue';
import Process from './Process/index.vue' import Process from './Process/index.vue';
import Save from './Save/index.vue' import Save from './Save/index.vue';
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable'; import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
const router = useRouter(); const router = useRouter();
const instanceRef = ref<Record<string, any>>({}); const instanceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({}) const params = ref<Record<string, any>>({});
const _selectedRowKeys = ref<string[]>([]) const _selectedRowKeys = ref<string[]>([]);
const importVisible = ref<boolean>(false) const importVisible = ref<boolean>(false);
const exportVisible = ref<boolean>(false) const exportVisible = ref<boolean>(false);
const visible = ref<boolean>(false) const visible = ref<boolean>(false);
const current = ref<Record<string, any>>({}) const current = ref<Record<string, any>>({});
const operationVisible = ref<boolean>(false) const operationVisible = ref<boolean>(false);
const api = ref<string>('') const api = ref<string>('');
const type = ref<string>('') const type = ref<string>('');
const statusMap = new Map(); const statusMap = new Map();
statusMap.set('online', 'processing'); statusMap.set(1, 'processing');
statusMap.set('offline', 'error'); statusMap.set(0, 'error');
statusMap.set('notActive', 'warning');
const columns = [ const columns = [
{ {
title: 'ID', title: 'ID',
dataIndex: 'id', dataIndex: 'id',
key: 'id' key: 'id',
}, },
{ {
title: '设备名称', title: '设备名称',
@ -202,66 +285,81 @@ const columns = [
title: '创建时间', title: '创建时间',
dataIndex: 'createTime', dataIndex: 'createTime',
key: 'createTime', key: 'createTime',
scopedSlots: true scopedSlots: true,
}, },
{ {
title: '状态', title: '状态',
dataIndex: 'state', dataIndex: 'state',
key: 'state', key: 'state',
scopedSlots: true scopedSlots: true,
}, },
{ {
title: '说明', title: '说明',
dataIndex: 'describe', dataIndex: 'describe',
key: 'describe' key: 'describe',
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
fixed: 'right', fixed: 'right',
width: 250, width: 250,
scopedSlots: true scopedSlots: true,
} },
] ];
const paramsFormat = (config: Record<string, any>, _terms: Record<string, any>, name?: string) => { const paramsFormat = (
if (config?.terms && Array.isArray(config.terms) && config?.terms.length > 0) { config: Record<string, any>,
(config?.terms || []).map((item: Record<string, any>, index: number) => { _terms: Record<string, any>,
if (item?.type) { name?: string,
_terms[`${name ? `${name}.` : ''}terms[${index}].type`] = item.type; ) => {
} if (
paramsFormat(item, _terms, `${name ? `${name}.` : ''}terms[${index}]`); config?.terms &&
}); Array.isArray(config.terms) &&
config?.terms.length > 0
) {
(config?.terms || []).map(
(item: Record<string, any>, index: number) => {
if (item?.type) {
_terms[`${name ? `${name}.` : ''}terms[${index}].type`] =
item.type;
}
paramsFormat(
item,
_terms,
`${name ? `${name}.` : ''}terms[${index}]`,
);
},
);
} else if (!config?.terms && Object.keys(config).length > 0) { } else if (!config?.terms && Object.keys(config).length > 0) {
Object.keys(config).forEach((key) => { Object.keys(config).forEach((key) => {
if (config[key]) { if (config[key]) {
_terms[`${name ? `${name}.` : ''}${key}`] = config[key]; _terms[`${name ? `${name}.` : ''}${key}`] = config[key];
} }
}); });
} }
} };
const handleParams = (config: Record<string, any>) => { const handleParams = (config: Record<string, any>) => {
const _terms: Record<string, any> = {}; const _terms: Record<string, any> = {};
paramsFormat(config, _terms); paramsFormat(config, _terms);
if(Object.keys(_terms._value).length && Object.keys(_terms).length) { if (Object.keys(_terms._value).length && Object.keys(_terms).length) {
const url = new URLSearchParams(); const url = new URLSearchParams();
Object.keys(_terms).forEach((key) => { Object.keys(_terms).forEach((key) => {
url.append(key, _terms[key]); url.append(key, _terms[key]);
}); });
return url.toString(); return url.toString();
} else { } else {
return '' return '';
} }
} };
/** /**
* 新增 * 新增
*/ */
const handleAdd = () => { const handleAdd = () => {
visible.value = true visible.value = true;
current.value = {} current.value = {};
} };
/** /**
* 查看 * 查看
@ -270,138 +368,154 @@ const handleView = (id: string) => {
router.push('/device/instance/detail/' + id) router.push('/device/instance/detail/' + id)
} }
const getActions = (data: Partial<Record<string, any>>, type: 'card' | 'table'): ActionsType[] => { const getActions = (
if(!data) return [] data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
const actions = [ const actions = [
{ {
key: 'view', key: 'view',
text: "查看", text: '查看',
tooltip: { tooltip: {
title: '查看' title: '查看',
}, },
icon: 'EyeOutlined', icon: 'EyeOutlined',
onClick: () => { onClick: () => {
handleView(data.id) handleView(data.id);
} },
}, },
{ {
key: 'edit', key: 'edit',
text: "编辑", text: '编辑',
tooltip: { tooltip: {
title: '编辑' title: '编辑',
}, },
icon: 'EditOutlined', icon: 'EditOutlined',
onClick: () => { onClick: () => {
visible.value = true visible.value = true;
current.value = data current.value = data;
} },
}, },
{ {
key: 'action', key: 'action',
text: data.state.value !== 'notActive' ? "禁用" : "启用", text: data.state.value !== 'notActive' ? '禁用' : '启用',
tooltip: { tooltip: {
title: data.state.value !== 'notActive' ? "禁用" : "启用", title: data.state.value !== 'notActive' ? '禁用' : '启用',
}, },
icon: data.state.value !== 'notActive' ? 'StopOutlined' : 'CheckCircleOutlined', icon:
data.state.value !== 'notActive'
? 'StopOutlined'
: 'CheckCircleOutlined',
popConfirm: { popConfirm: {
title: `确认${data.state.value !== 'notActive' ? "禁用" : "启用"}?`, title: `确认${
data.state.value !== 'notActive' ? '禁用' : '启用'
}?`,
onConfirm: async () => { onConfirm: async () => {
let response = undefined let response = undefined;
if(data.state.value !== 'notActive') { if (data.state.value !== 'notActive') {
response = await _undeploy(data.id) response = await _undeploy(data.id);
} else { } else {
response = await _deploy(data.id) response = await _deploy(data.id);
} }
if(response && response.status === 200) { if (response && response.status === 200) {
message.success('操作成功!') message.success('操作成功!');
instanceRef.value?.reload() instanceRef.value?.reload();
} else { } else {
message.error('操作失败!') message.error('操作失败!');
} }
} },
} },
}, },
{ {
key: 'delete', key: 'delete',
text: "删除", text: '删除',
disabled: data.state.value !== 'notActive', disabled: data.state.value !== 'notActive',
tooltip: { tooltip: {
title: data.state.value !== 'notActive' ? '已启用的设备不能删除' : '删除' title:
data.state.value !== 'notActive'
? '已启用的设备不能删除'
: '删除',
}, },
popConfirm: { popConfirm: {
title: '确认删除?', title: '确认删除?',
onConfirm: async () => { onConfirm: async () => {
const resp = await _delete(data.id) const resp = await _delete(data.id);
if(resp.status === 200) { if (resp.status === 200) {
message.success('操作成功!') message.success('操作成功!');
instanceRef.value?.reload() instanceRef.value?.reload();
} else { } else {
message.error('操作失败!') message.error('操作失败!');
} }
} },
}, },
icon: 'DeleteOutlined' icon: 'DeleteOutlined',
} },
] ];
if(type === 'card') return actions.filter((i: ActionsType) => i.key !== 'view') if (type === 'card')
return actions return actions.filter((i: ActionsType) => i.key !== 'view');
} return actions;
};
const onSelectChange = (keys: string[]) => { const onSelectChange = (keys: string[]) => {
_selectedRowKeys.value = [...keys] _selectedRowKeys.value = [...keys];
} };
const cancelSelect = () => { const cancelSelect = () => {
_selectedRowKeys.value = [] _selectedRowKeys.value = [];
} };
const handleClick = (dt: any) => { const handleClick = (dt: any) => {
if(_selectedRowKeys.value.includes(dt.id)) { if (_selectedRowKeys.value.includes(dt.id)) {
const _index = _selectedRowKeys.value.findIndex(i => i === dt.id) const _index = _selectedRowKeys.value.findIndex((i) => i === dt.id);
_selectedRowKeys.value.splice(_index, 1) _selectedRowKeys.value.splice(_index, 1);
} else { } else {
_selectedRowKeys.value = [..._selectedRowKeys.value, dt.id] _selectedRowKeys.value = [..._selectedRowKeys.value, dt.id];
} }
} };
const activeAllDevice = () => { const activeAllDevice = () => {
type.value = 'active' type.value = 'active';
const activeAPI = `${BASE_API_PATH}/device-instance/deploy?:X_Access_Token=${LocalStore.get(TOKEN_KEY)}&${handleParams(params)}`; const activeAPI = `${BASE_API_PATH}/device-instance/deploy?:X_Access_Token=${LocalStore.get(
api.value = activeAPI TOKEN_KEY,
operationVisible.value = true )}&${handleParams(params)}`;
} api.value = activeAPI;
operationVisible.value = true;
};
const syncDeviceStatus = () => { const syncDeviceStatus = () => {
type.value = 'sync' type.value = 'sync';
const syncAPI = `${BASE_API_PATH}/device-instance/state/_sync?:X_Access_Token=${LocalStore.get(TOKEN_KEY)}&${handleParams(params)}`; const syncAPI = `${BASE_API_PATH}/device-instance/state/_sync?:X_Access_Token=${LocalStore.get(
api.value = syncAPI TOKEN_KEY,
operationVisible.value = true )}&${handleParams(params)}`;
} api.value = syncAPI;
operationVisible.value = true;
};
const delSelectedDevice = async () => { const delSelectedDevice = async () => {
const resp = await batchDeleteDevice(_selectedRowKeys.value) const resp = await batchDeleteDevice(_selectedRowKeys.value);
if(resp.status === 200){ if (resp.status === 200) {
message.success('操作成功!') message.success('操作成功!');
_selectedRowKeys.value = [] _selectedRowKeys.value = [];
instanceRef.value?.reload() instanceRef.value?.reload();
} }
} };
const activeSelectedDevice = async () => { const activeSelectedDevice = async () => {
const resp = await batchDeployDevice(_selectedRowKeys.value) const resp = await batchDeployDevice(_selectedRowKeys.value);
if(resp.status === 200){ if (resp.status === 200) {
message.success('操作成功!') message.success('操作成功!');
_selectedRowKeys.value = [] _selectedRowKeys.value = [];
instanceRef.value?.reload() instanceRef.value?.reload();
} }
} };
const disabledSelectedDevice = async () => { const disabledSelectedDevice = async () => {
const resp = await batchUndeployDevice(_selectedRowKeys.value) const resp = await batchUndeployDevice(_selectedRowKeys.value);
if(resp.status === 200){ if (resp.status === 200) {
message.success('操作成功!') message.success('操作成功!');
_selectedRowKeys.value = [] _selectedRowKeys.value = [];
instanceRef.value?.reload() instanceRef.value?.reload();
} }
} }
@ -409,4 +523,4 @@ const saveBtn = () => {
visible.value = false visible.value = false
instanceRef.value?.reload() instanceRef.value?.reload()
} }
</script> </script>

View File

@ -0,0 +1,313 @@
<template>
<div class="card">
<div
class="card-warp"
:class="{ active: active ? 'active' : '' }"
@click="handleClick"
>
<div class="card-content">
<a-row :gutter="20">
<a-col :span="10">
<!-- 图片 -->
<div class="card-item-avatar">
<slot name="img"> </slot>
</div>
</a-col>
<a-col :span="14">
<!-- 内容 -->
<slot name="content"></slot>
</a-col>
</a-row>
<!-- 勾选 -->
<div v-if="active" class="checked-icon">
<div>
<CheckOutlined />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
SearchOutlined,
CheckOutlined,
DeleteOutlined,
} from '@ant-design/icons-vue';
import { StatusColorEnum } from '@/utils/consts.ts';
import type { ActionsType } from '@/components/Table/index.vue';
import { PropType } from 'vue';
type EmitProps = {
// (e: 'update:modelValue', data: Record<string, any>): void;
(e: 'click', data: Record<string, any>): void;
};
type TableActionsType = Partial<ActionsType>;
const emit = defineEmits<EmitProps>();
const props = defineProps({
value: {
type: Object as PropType<Record<string, any>>,
default: () => {},
},
active: {
type: Boolean,
default: false,
},
});
const handleClick = () => {
emit('click', props.value);
};
</script>
<style lang="less" scoped>
.card {
width: 100%;
background-color: #fff;
.checked-icon {
position: absolute;
right: -22px;
bottom: -22px;
z-index: 2;
width: 44px;
height: 44px;
color: #fff;
background-color: red;
background-color: #2f54eb;
transform: rotate(-45deg);
> div {
position: relative;
height: 100%;
transform: rotate(45deg);
> span {
position: absolute;
top: 6px;
left: 6px;
font-size: 12px;
}
}
}
.card-warp {
position: relative;
border: 1px solid #e6e6e6;
height: 66px;
&.hover {
cursor: pointer;
box-shadow: 0 0 24px rgba(#000, 0.1);
}
&.active {
position: relative;
border: 1px solid #2f54eb;
}
.card-content {
position: relative;
padding: 30px 12px 16px 30px;
overflow: hidden;
position: relative;
top: -16px;
&::before {
position: absolute;
top: 0;
left: 30px + 10px;
display: block;
width: 15%;
min-width: 64px;
height: 2px;
// background-image: url('/images/rectangle.png');
background-repeat: no-repeat;
background-size: 100% 100%;
content: ' ';
}
.card-item-avatar {
// position: relative;
// top: -16px;
}
.card-state {
position: absolute;
top: 30px;
right: -12px;
display: flex;
justify-content: center;
width: 100px;
padding: 2px 0;
background-color: rgba(#5995f5, 0.15);
transform: skewX(45deg);
&.success {
background-color: @success-color-deprecated-bg;
}
&.warning {
background-color: rgba(#ff9000, 0.1);
}
&.error {
background-color: rgba(#e50012, 0.1);
}
.card-state-content {
transform: skewX(-45deg);
}
}
:deep(.card-item-content-title) {
cursor: pointer;
}
}
.card-mask {
position: absolute;
top: 0;
left: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #fff;
background-color: rgba(#000, 0);
visibility: hidden;
cursor: pointer;
transition: all 0.3s;
> div {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 0 !important;
}
&.show {
background-color: rgba(#000, 0.5);
visibility: visible;
}
}
}
&.item-active {
position: relative;
color: #2f54eb;
.checked-icon {
display: block;
}
.card-warp {
border: 1px solid #2f54eb;
}
}
.card-tools {
display: flex;
margin-top: 8px;
.card-button {
display: flex;
flex-grow: 1;
& > :deep(span, button) {
width: 100%;
border-radius: 0;
}
:deep(button) {
width: 100%;
border-radius: 0;
background: #f6f6f6;
border: 1px solid #e6e6e6;
color: #2f54eb;
&:hover {
background-color: @primary-color-hover;
border-color: @primary-color-hover;
span {
color: #fff !important;
}
}
&:active {
background-color: @primary-color-active;
border-color: @primary-color-active;
span {
color: #fff !important;
}
}
}
&:not(:last-child) {
margin-right: 8px;
}
&.delete {
flex-basis: 60px;
flex-grow: 0;
:deep(button) {
background: @error-color-deprecated-bg;
border: 1px solid @error-color-outline;
span {
color: @error-color !important;
}
&:hover {
background-color: @error-color-hover;
span {
color: #fff !important;
}
}
&:active {
background-color: @error-color-active;
span {
color: #fff !important;
}
}
}
}
:deep(button[disabled]) {
background: @disabled-bg;
border-color: @disabled-color;
span {
color: @disabled-color !important;
}
&:hover {
background-color: @disabled-active-bg;
}
&:active {
background-color: @disabled-active-bg;
}
}
// :deep(.ant-tooltip-disabled-compatible-wrapper) {
// width: 100%;
// }
}
}
}
</style>

View File

@ -0,0 +1,386 @@
<!-- 新增编辑产品 -->
<template>
<a-modal
:title="props.title"
:maskClosable="false"
destroy-on-close
v-model:visible="visible"
@ok="submitData"
@cancel="close"
okText="确定"
cancelText="取消"
v-bind="layout"
width="650px"
>
<div style="margin-top: 10px">
<a-form :layout="'vertical'">
<a-row type="flex">
<a-col flex="180px">
<a-form-item>
<div class="upload-image-warp-logo">
<div class="upload-image-border-logo">
<a-upload
name="file"
:action="FILE_UPLOAD"
:headers="headers"
:showUploadList="false"
:beforeUpload="beforeUpload"
@change="handleChange"
:accept="
imageTypes && imageTypes.length
? imageTypes.toString()
: ''
"
>
<div class="upload-image-content-logo">
<div
class="loading-logo"
v-if="logoLoading"
>
<LoadingOutlined
style="font-size: 28px"
/>
</div>
<div
class="upload-image"
v-if="photoValue"
:style="
photoValue
? `background-image: url(${photoValue});`
: ''
"
></div>
<div
v-if="photoValue"
class="upload-image-mask"
>
点击修改
</div>
<div v-else>
<div v-if="logoLoading">
<LoadingOutlined
style="font-size: 28px"
/>
</div>
<div v-else>
<PlusOutlined
style="font-size: 28px"
/>
</div>
</div>
</div>
</a-upload>
<div v-if="logoLoading">
<div class="upload-loading-mask">
<LoadingOutlined
v-if="logoLoading"
style="font-size: 28px"
/>
</div>
</div>
</div>
</div>
</a-form-item>
</a-col>
<a-col flex="auto">
<a-form-item>
<template #label>
<span>ID</span>
<a-tooltip
title="若不填写系统将自动生成唯一ID"
>
<img
class="img-style"
:src="getImage('/init-home/mark.png')"
/>
</a-tooltip>
</template>
<a-input
v-model:value="modelRef.id"
placeholder="请输入ID"
/>
</a-form-item>
<a-form-item label="名称">
<a-input
v-model:value="modelRef.name"
placeholder="请输入名称"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="产品分类">
<a-tree-select
showSearch
v-model:value="modelRef.productId"
placeholder="请选择产品分类"
>
</a-tree-select>
</a-form-item>
<a-form-item label="设备类型">
<a-row :span="24" :gutter="20">
<a-col
:span="8"
v-for="item in deviceList"
:key="item.value"
>
<ChooseCard
:value="item"
v-bind="item"
@click="handleClick"
:active="_selectedRowKeys.includes(item.value)"
>
<template #img>
<slot name="img">
<img
v-if="item.value === 'device'"
:src="
getImage('/device-type-1.png')
"
/>
<img
v-if="
item.value === 'childrenDevice'
"
:src="
getImage('/device-type-2.png')
"
/>
<img
v-if="item.value === 'gateway'"
:src="
getImage(
'/device/device-type-3.png',
)
"
/>
</slot>
</template>
<template #content>
<span
class="card-style"
:style="
_selectedRowKeys.includes(
item.value,
)
? 'color: #10239e'
: ''
"
>{{
item.value === 'device'
? '直连设备'
: item.value ===
'childrenDevice'
? '网关子设备'
: item.value === 'gateway'
? '网关设备'
: ''
}}</span
>
</template>
</ChooseCard>
</a-col>
</a-row>
</a-form-item>
<a-form-item label="说明">
<a-textarea
v-model:value="modelRef.describe"
placeholder="请输入说明"
/>
</a-form-item>
</a-form>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { queryTree } from '@/api/device/category';
import { Form } from 'ant-design-vue';
import { getImage } from '@/utils/comm.ts';
import { message } from 'ant-design-vue';
import ChooseCard from '../ChooseCard/index.vue';
import { FILE_UPLOAD } from '@/api/comm';
const emit = defineEmits(['close', 'save']);
const props = defineProps({
title: {
type: String,
defult: '',
},
isAdd: {
type: Number,
default: 0,
},
});
const treeList = ref<Record<string, any>[]>([]);
const visible = ref(false);
const logoLoading = ref(false);
const useForm = Form.useForm;
const _selectedRowKeys = ref([]);
const photoValue = ref('/images/device-product.png');
const imageTypes = reactive([
'image/jpeg',
'image/png',
'image/jpg',
'image/jfif',
'image/pjp',
'image/pjpeg',
]);
const deviceList = ref([
{
label: '直连设备',
value: 'device',
},
{
label: '网关子设备',
value: 'childrenDevice',
},
{
label: '网关设备',
value: 'gateway',
},
]);
const modelRef = reactive({
id: '',
name: '',
classifiedId: '',
classifiedName: '',
deviceType: '',
describe: '',
photoUrl: '',
});
watch(
() => props.isAdd,
() => {
queryTree({ paging: false }).then((resp) => {
if (resp.status === 200) {
treeList.value = resp.result;
}
});
},
{ immediate: true, deep: true },
);
/**
* 显示弹窗
*/
const show = () => {
visible.value = true;
};
/**
* 关闭弹窗
*/
const close = () => {
visible.value = false;
};
/**
* 卡片点击事件
*/
const handleClick = (dt: any) => {
_selectedRowKeys.value = dt;
};
/**
* 文件上传之前
*/
const beforeUpload = (file: any) => {
const isType: any = imageTypes.includes(file.type);
if (!isType) {
message.error(`请上传.jpg.png.jfif.pjp.pjpeg.jpeg格式的图片`);
return false;
}
const isSize = file.size / 1024 / 1024 < 4;
if (!isSize) {
message.error(`图片大小必须小于${4}M`);
}
return isType && isSize;
};
/**
* 文件改变事件
*/
const handleChange = (info: any) => {
if (info.file.status === 'uploading') {
logoLoading.value = true;
}
if (info.file.status === 'done') {
info.file.url = info.file.response?.result;
logoLoading.value = false;
logoLoading.value = info.file.response?.result;
}
};
defineExpose({
show: show,
});
</script>
<style scoped lang="less">
.card-style {
position: relative;
top: 8px;
}
.upload-image-warp-logo {
display: flex;
justify-content: flex-start;
.upload-image-border-logo {
position: relative;
overflow: hidden;
border: 1px dashed #d9d9d9;
transition: all 0.3s;
width: 160px;
height: 150px;
&:hover {
border: 1px dashed #1890ff;
display: flex;
}
.upload-image-content-logo {
align-items: center;
justify-content: center;
position: relative;
display: flex;
flex-direction: column;
width: 160px;
height: 150px;
padding: 8px;
background-color: rgba(0, 0, 0, 0.06);
cursor: pointer;
.loading-logo {
position: absolute;
top: 50%;
}
.loading-icon {
position: absolute;
}
.upload-image {
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-position: 50%;
background-size: cover;
}
.upload-image-icon {
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-position: 50%;
background-size: inherit;
}
.upload-image-mask {
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
display: none;
width: 100%;
height: 100%;
color: #fff;
font-size: 16px;
background-color: rgba(0, 0, 0, 0.35);
}
&:hover .upload-image-mask {
display: flex;
}
}
}
}
</style>

View File

@ -0,0 +1,498 @@
<template>
<a-card class="device-product">
<Search :columns="query.columns" target="category" />
<JTable :columns="columns" :request="queryProductList" ref="tableRef">
<template #headerTitle>
<a-button type="primary" @click="add"
><plus-outlined />新增</a-button
>
</template>
<template #deviceType="slotProps">
<div>{{ slotProps.deviceType.text }}</div>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
@click="handleClick"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:active="_selectedRowKeys.includes(slotProps.id)"
:status="slotProps.state"
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
:statusNames="{
1: 'success',
0: 'error',
}"
>
<template #img>
<slot name="img">
<img :src="getImage('/device-product.png')" />
</slot>
</template>
<template #content>
<h3>{{ slotProps.name }}</h3>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>直连设备</div>
</a-col>
</a-row>
</template>
<template #actions="item">
<a-popconfirm
v-if="item.popConfirm"
v-bind="item.popConfirm"
>
<a-button :disabled="item.disabled">
<DeleteOutlined v-if="item.key === 'delete'" />
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</template>
</a-button>
</a-popconfirm>
<template v-else>
<a-button :disabled="item.disabled">
<DeleteOutlined v-if="item.key === 'delete'" />
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</template>
</a-button>
</template>
</template>
</CardBox>
</template>
<template #state="slotProps">
<a-badge
:text="slotProps.state === 1 ? '正常' : '禁用'"
:status="statusMap.get(slotProps.state)"
/>
</template>
<template #id="slotProps">
<a>{{ slotProps.id }}</a>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps)"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm v-if="i.popConfirm" v-bind="i.popConfirm">
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
</a-space>
</template>
</JTable>
<!-- 新增编辑 -->
<Save ref="saveRef" />
</a-card>
</template>
<script setup lang="ts">
import server from '@/utils/request';
import type { ActionsType } from '@/components/Table/index.vue';
import { getImage } from '@/utils/comm';
import {
EditOutlined,
DeleteOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import {
getProviders,
category,
queryOrgThree,
queryGatewayList,
queryProductList,
_deploy,
_undeploy,
deleteProduct,
addProduct,
editProduct,
queryProductId,
} from '@/api/device/product';
import { isNoCommunity } from '@/utils/utils';
import { typeOptions } from '@/components/Search/util';
import Save from './Save/index.vue';
/**
* 表格数据
*/
const statusMap = new Map();
statusMap.set(1, 'success');
statusMap.set(0, 'error');
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
scopedSlots: true,
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
},
{
title: '接入方式',
dataIndex: 'accessName',
key: 'accessName',
},
{
title: '设备类型',
dataIndex: 'deviceType',
key: 'deviceType',
scopedSlots: true,
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
},
{
title: '说明',
dataIndex: 'describe',
key: 'describe',
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
scopedSlots: true,
},
];
const _selectedRowKeys = ref<string[]>([]);
const onSelectChange = (keys: string[]) => {
_selectedRowKeys.value = [...keys];
};
const cancelSelect = () => {
_selectedRowKeys.value = [];
};
const handleClick = (dt: any) => {
if (_selectedRowKeys.value.includes(dt.id)) {
const _index = _selectedRowKeys.value.findIndex((i) => i === dt.id);
_selectedRowKeys.value.splice(_index, 1);
} else {
_selectedRowKeys.value = [..._selectedRowKeys.value, dt.id];
}
};
const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
if (!data) {
return [];
}
return [
{
key: 'view',
text: '查看',
tooltip: {
title: '查看',
},
icon: 'EyeOutlined',
},
{
key: 'edit',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
},
{
key: 'download',
text: '导出',
tooltip: {
title: '导出',
},
icon: 'icon-xiazai',
},
{
key: 'action',
text: data.state !== 0 ? '禁用' : '启用',
tooltip: {
title: data.state !== 0 ? '禁用' : '启用',
},
icon: data.state !== 0 ? 'StopOutlined' : 'CheckCircleOutlined',
popConfirm: {
title: `确认${data.state !== 0 ? '禁用' : '启用'}?`,
onConfirm: async () => {
let response = undefined;
if (data.state !== 0) {
response = await _undeploy(data.id);
} else {
response = await _deploy(data.id);
}
if (response && response.status === 200) {
message.success('操作成功!');
tableRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
},
{
key: 'delete',
text: '删除',
disabled: data.state !== 0,
tooltip: {
title: data.state !== 0 ? '已启用的设备不能删除' : '删除',
},
popConfirm: {
title: '确认删除?',
onConfirm: async () => {
const resp = await deleteProduct(data.id);
if (resp.status === 200) {
message.success('操作成功!');
tableRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'DeleteOutlined',
},
];
};
const add = () => {
saveRef.value.show();
};
//
const listData = ref([]);
const typeList = ref([]);
const tableRef = ref<Record<string, any>>({});
const query = reactive({
columns: [
{
title: '名称',
dataIndex: 'name',
key: 'name',
search: {
first: true,
type: 'string',
},
},
{
title: 'ID',
dataIndex: 'id',
key: 'id',
search: {
type: 'string',
},
},
{
title: '网关类型',
key: 'accessProvider',
dataIndex: 'accessProvider',
search: {
type: 'select',
options: async () => {
return new Promise((res) => {
getProviders().then((resp: any) => {
listData.value = [];
// const list = () => {
if (isNoCommunity) {
listData.value = (resp?.result || []).map(
(item: any) => ({
label: item.name,
value: item.id,
}),
);
} else {
listData.value = (resp?.result || [])
.filter((i: any) =>
[
'mqtt-server-gateway',
'http-server-gateway',
'mqtt-client-gateway',
'tcp-server-gateway',
].includes(i.id),
)
.map((item: any) => ({
label: item.name,
value: item.id,
}));
// }
}
res(listData.value);
});
});
},
},
},
{
title: '接入方式',
key: 'accessName',
dataIndex: 'accessName',
search: {
type: 'select',
options: async () => {
return new Promise((res) => {
queryGatewayList({
paging: false,
}).then((resp: any) => {
typeList.value = [];
typeList.value = resp.result.map((item: any) => ({
label: item.name,
value: item.name,
}));
res(typeList.value);
});
});
},
},
},
{
title: '设备类型',
key: 'deviceType',
dataIndex: 'deviceType',
search: {
type: 'select',
options: [
{
label: '直连设备',
value: 'device',
},
{
label: '网关子设备',
value: 'childrenDevice',
},
{
label: '网关设备',
value: 'gateway',
},
],
},
},
{
title: '状态',
key: 'state',
dataIndex: 'state',
search: {
type: 'select',
options: [
{
label: '正常',
value: 1,
},
{
label: '禁用',
value: 0,
},
],
},
},
{
title: '说明',
key: 'describe',
dataIndex: 'describe',
search: {
type: 'string',
},
},
{
title: '分类',
key: 'classified',
dataIndex: 'classifiedId',
search: {
type: 'treeSelect',
options: async () => {
return new Promise((res) => {
category({
paging: false,
}).then((resp) => {
res(resp.result);
});
});
},
},
},
{
title: '所属部门',
key: 'id$dim-assets',
dataIndex: 'id$dim-assets',
search: {
first: true,
type: 'treeSelect',
options: async () => {
return new Promise((res) => {
queryOrgThree({ paging: false }).then((resp: any) => {
const formatValue = (list: any[]) => {
const _list: any[] = [];
list.forEach((item) => {
if (item.children) {
item.children = formatValue(
item.children,
);
}
_list.push({
...item,
value: JSON.stringify({
assetType: 'product',
targets: [
{
type: 'org',
id: item.id,
},
],
}),
});
});
return _list;
};
res(formatValue(resp.result));
});
});
},
},
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
scopedSlots: true,
},
],
});
const saveRef = ref();
</script>
<style lang="less" scoped>
.box {
padding: 20px;
background: #f0f2f5;
}
</style>

View File

@ -177,3 +177,15 @@ type ObserverMetadata = {
subscribe: (data: any) => void; subscribe: (data: any) => void;
next: (data: any) => void; next: (data: any) => void;
}; };
// 部门
export type DepartmentItem = {
id: string;
name: string;
path: string;
sortIndex: number;
level: number;
code: string;
parentId: string;
children: DepartmentItem[];
};

View File

@ -114,6 +114,6 @@ const form = reactive({
model: {} model: {}
}) })
</script> </script>
<style lang="scss" scoped> <style lang="less" scoped>
</style> </style>

View File

@ -96,5 +96,5 @@ const operateLimits = (action: 'add' | 'updata', types: MetadataType) => {
); );
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="less">
</style> </style>

View File

@ -140,7 +140,7 @@ watchEffect(() => {
} }
}) })
</script> </script>
<style scoped lang="scss"> <style scoped lang="less">
.cat-content { .cat-content {
background: #F6F6F6; background: #F6F6F6;

View File

@ -8,8 +8,8 @@
</p> </p>
</div> </div>
<a-form layout="vertical" v-model="formModel"> <a-form layout="vertical" v-model="formModel">
<a-form-item label="导入方式" v-bind="validateInfos.type"> <a-form-item v-if="type === 'product'" label="导入方式" v-bind="validateInfos.type">
<a-select v-if="type === 'product'" v-model:value="formModel.type"> <a-select v-model:value="formModel.type">
<a-select-option value="copy">拷贝产品</a-select-option> <a-select-option value="copy">拷贝产品</a-select-option>
<a-select-option value="import">导入物模型</a-select-option> <a-select-option value="import">导入物模型</a-select-option>
</a-select> </a-select>
@ -32,11 +32,19 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="文件上传" v-bind="validateInfos.upload" v-if="formModel.metadataType === 'file'"> <a-form-item label="文件上传" v-bind="validateInfos.upload" v-if="formModel.metadataType === 'file'">
<a-upload v-model:file-list="formModel.upload" name="files" :before-upload="beforeUpload" accept=".json" <a-input v-model:value="formModel.upload">
:show-upload-list="false"></a-upload> <template #addonAfter>
<label for="uploadFile"><upload-outlined/></label>
</template>
</a-input>
<a-upload v-model:file-list="fileList" name="files" :before-upload="beforeUpload" accept=".json"
:show-upload-list="false" :action="FILE_UPLOAD" @change="fileChange" :headers="{ 'X-Access-Token': token }">
<button id="uploadFile" style="display: none;"></button>
</a-upload>
</a-form-item> </a-form-item>
<a-form-item label="物模型" v-bind="validateInfos.import" v-if="formModel.metadataType === 'script'"> <a-form-item label="物模型" v-bind="validateInfos.import" v-if="formModel.metadataType === 'script'">
<!-- TODO代码编辑器 --> <!-- TODO代码编辑器 -->
<a-textarea v-model:value="formModel.import"></a-textarea>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
@ -46,13 +54,17 @@ import { useForm } from 'ant-design-vue/es/form';
import { saveMetadata } from '@/api/device/instance' import { saveMetadata } from '@/api/device/instance'
import { queryNoPagingPost, convertMetadata, modify } from '@/api/device/product' import { queryNoPagingPost, convertMetadata, modify } from '@/api/device/product'
import type { DefaultOptionType } from 'ant-design-vue/es/select'; import type { DefaultOptionType } from 'ant-design-vue/es/select';
import { UploadProps } from 'ant-design-vue/es'; import type { UploadProps, UploadFile, UploadChangeParam } from 'ant-design-vue/es';
import type { DeviceMetadata, ProductItem } from '@/views/device/Product/typings' import type { DeviceMetadata, ProductItem } from '@/views/device/Product/typings'
import { message } from 'ant-design-vue/es'; import { message } from 'ant-design-vue/es';
import { Store } from 'jetlinks-store'; import { Store } from 'jetlinks-store';
import { SystemConst } from '@/utils/consts'; import { SystemConst } from '@/utils/consts';
import { useInstanceStore } from '@/store/instance' import { useInstanceStore } from '@/store/instance'
import { useProductStore } from '@/store/product'; import { useProductStore } from '@/store/product';
import { UploadOutlined } from '@ant-design/icons-vue';
import { FILE_UPLOAD } from '@/api/comm';
import { LocalStore } from '@/utils/comm';
import { TOKEN_KEY } from '@/utils/variable';
const route = useRoute() const route = useRoute()
const instanceStore = useInstanceStore() const instanceStore = useInstanceStore()
@ -79,6 +91,7 @@ const _visible = computed({
}) })
const close = () => { const close = () => {
console.log(1)
emits('update:visible', false); emits('update:visible', false);
} }
@ -132,6 +145,8 @@ const onSubmit = () => {
}) })
} }
const fileList = ref<UploadFile[]>([])
const token = ref(LocalStore.get(TOKEN_KEY));
const productList = ref<DefaultOptionType[]>([]) const productList = ref<DefaultOptionType[]>([])
@ -157,6 +172,11 @@ const beforeUpload: UploadProps['beforeUpload'] = file => {
formModel.import = json.target?.result; formModel.import = json.target?.result;
}; };
} }
const fileChange = (info: UploadChangeParam) => {
if (info.file.status === 'done') {
console.log(info)
}
}
const operateLimits = (mdata: DeviceMetadata) => { const operateLimits = (mdata: DeviceMetadata) => {
const obj: DeviceMetadata = { ...mdata }; const obj: DeviceMetadata = { ...mdata };
@ -257,7 +277,7 @@ const handleImport = async () => {
// const showProduct = computed(() => formModel.type === 'copy') // const showProduct = computed(() => formModel.type === 'copy')
</script> </script>
<style scoped lang="scss"> <style scoped lang="less">
.import-content { .import-content {
background: rgb(236, 237, 238); background: rgb(236, 237, 238);

View File

@ -1,6 +1,6 @@
<template> <template>
<div class='device-detail-metadata' style="position: relative;"> <div class='device-detail-metadata' style="position: relative;">
<div class="tips" style="width: 40%"> <div class="tips">
<a-tooltip :title="instanceStore.detail?.independentMetadata && type === 'device' <a-tooltip :title="instanceStore.detail?.independentMetadata && type === 'device'
? '该设备已脱离产品物模型,修改产品物模型对该设备无影响' ? '该设备已脱离产品物模型,修改产品物模型对该设备无影响'
: '设备会默认继承产品的物模型,修改设备物模型后将脱离产品物模型'"> : '设备会默认继承产品的物模型,修改设备物模型后将脱离产品物模型'">
@ -14,16 +14,20 @@
</div> </div>
</a-tooltip> </a-tooltip>
</div> </div>
<a-tabs class="metadataNav" destroyInactiveTabPane> <a-tabs class="metadataNav" destroyInactiveTabPane type="card">
<template #rightExtra> <template #rightExtra>
<a-space> <a-space>
<PermissionButton v-if="type === 'device'" :hasPermission="`${permission}:update`" <PermissionButton v-if="type === 'device' && instanceStore.detail?.independentMetadata"
:popConfirm="{ title: '确认重置?', onConfirm: resetMetadata, }" :tooltip="{ title: '重置后将使用产品的物模型配置' }" :hasPermission="`${permission}:update`" :popConfirm="{ title: '确认重置?', onConfirm: resetMetadata, }"
key="reload"> :tooltip="{ title: '重置后将使用产品的物模型配置' }" key="reload">
重置操作 重置操作
</PermissionButton> </PermissionButton>
<PermissionButton :isPermission="`${permission}:update`" @click="visible = true">快速导入</PermissionButton> <PermissionButton
<PermissionButton :isPermission="`${permission}:update`" @click="cat = true">物模型TSL</PermissionButton> :uhasPermission="`${permission}:update`"
@click="visible = true">快速导入</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:update`"
@click="cat = true">物模型TSL</PermissionButton>
</a-space> </a-space>
</template> </template>
@ -40,11 +44,13 @@
<BaseMetadata target={props.type} type="tags" :permission="permission" /> <BaseMetadata target={props.type} type="tags" :permission="permission" />
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
<Import :visible="visible" :type="type" @close="visible = false" /> {{ visible }}
<Cat :visible="cat" @close="cat = false" :type="type" /> <Import v-model:visible="visible" :type="type" @close="visible = false" />
<Cat v-model:visible="cat" @close="cat = false" :type="type" />
</div> </div>
</template> </template>
<script setup lang="ts" name="Metadata"> <script setup lang="ts" name="Metadata">
import { InfoCircleOutlined } from '@ant-design/icons-vue';
import PermissionButton from '@/components/PermissionButton/index.vue' import PermissionButton from '@/components/PermissionButton/index.vue'
import { deleteMetadata } from '@/api/device/instance.js' import { deleteMetadata } from '@/api/device/instance.js'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
@ -80,21 +86,20 @@ const resetMetadata = async () => {
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="less">
.device-detail-metadata { .device-detail-metadata {
.tips { .tips {
width: calc(100% - 670px);
position: absolute; position: absolute;
top: 12px; top: 12px;
z-index: 1; z-index: 1;
margin-left: 330px; margin-left: 380px;
font-weight: 100; font-weight: 100;
} }
.metadataNav { .metadataNav {
:global { :deep(.ant-card-body) {
.ant-card-body { padding: 0;
padding: 0;
}
} }
} }
} }

View File

@ -801,7 +801,7 @@ const rulesFrom = ref({
{ {
required: true, required: true,
message: '请输入系统名称', message: '请输入系统名称',
trigger: 'blur', trigger: 'change',
}, },
], ],
headerTheme: [ headerTheme: [

View File

@ -1,103 +1,125 @@
<template> <template>
<a-modal <div>
v-model:visible="_vis" <a-modal
title="同步用户" v-model:visible="_vis"
:footer="null" title="同步用户"
@cancel="_vis = false" :footer="null"
width="80%" @cancel="_vis = false"
> width="80%"
<a-row :gutter="10"> >
<a-col :span="4"> <a-row :gutter="10">
<a-input <a-col :span="4">
v-model:value="deptName" <a-input
@keyup.enter="getDepartment" v-model:value="deptName"
allowClear @keyup.enter="getDepartment"
placeholder="请输入部门名称" allowClear
style="margin-bottom: 8px" placeholder="请输入部门名称"
> style="margin-bottom: 8px"
<template #addonAfter> >
<AIcon <template #addonAfter>
type="SearchOutlined"
style="cursor: pointer"
@click="getDepartment"
/>
</template>
</a-input>
<a-tree
:tree-data="deptTreeData"
:fieldNames="{ title: 'name', key: 'id' }"
:selectedKeys="[deptId]"
@select="onTreeSelect"
>
</a-tree>
<a-empty v-if="!deptTreeData.length" />
</a-col>
<a-col :span="20">
<JTable
ref="tableRef"
:columns="columns"
:dataSource="dataSource"
:loading="tableLoading"
model="table"
>
<template #headerTitle>
<a-button type="primary" @click="handleAutoBind">
自动绑定
</a-button>
</template>
<template #status="slotProps">
<a-space>
<a-badge
:status="slotProps.status.value"
:text="slotProps.status.text"
></a-badge>
<AIcon <AIcon
v-if="slotProps.status.value === 'error'" type="SearchOutlined"
type="ExclamationCircleOutlined" style="cursor: pointer"
style="color: #1d39c4; cursor: pointer" @click="getDepartment"
@click="handleError(slotProps.errorStack)"
/> />
</a-space> </template>
</template> </a-input>
<template #action="slotProps"> <a-tree
<a-space :size="16"> :tree-data="deptTreeData"
<a-tooltip :fieldNames="{ title: 'name', key: 'id' }"
v-for="i in getActions(slotProps, 'table')" :selectedKeys="[deptId]"
:key="i.key" @select="onTreeSelect"
v-bind="i.tooltip" >
> </a-tree>
<a-popconfirm <a-empty v-if="!deptTreeData.length" />
v-if="i.popConfirm" </a-col>
v-bind="i.popConfirm" <a-col :span="20">
:disabled="i.disabled" <JTable
ref="tableRef"
:columns="columns"
:dataSource="dataSource"
:loading="tableLoading"
model="table"
>
<template #headerTitle>
<a-button type="primary" @click="handleAutoBind">
自动绑定
</a-button>
</template>
<template #status="slotProps">
<a-space>
<a-badge
:status="slotProps.status.value"
:text="slotProps.status.text"
></a-badge>
</a-space>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
v-bind="i.tooltip"
> >
<a-button <a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
:disabled="i.disabled" :disabled="i.disabled"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
style="padding: 0" style="padding: 0"
type="link" type="link"
><AIcon :type="i.icon" v-else
/></a-button> @click="
</a-popconfirm> i.onClick && i.onClick(slotProps)
<a-button "
style="padding: 0" >
type="link" <a-button
v-else :disabled="i.disabled"
@click="i.onClick && i.onClick(slotProps)" style="padding: 0"
> type="link"
<a-button ><AIcon :type="i.icon"
:disabled="i.disabled" /></a-button>
style="padding: 0" </a-button>
type="link" </a-tooltip>
><AIcon :type="i.icon" </a-space>
/></a-button> </template>
</a-button> </JTable>
</a-tooltip> </a-col>
</a-space> </a-row>
</template> </a-modal>
</JTable>
</a-col> <!-- 绑定用户 -->
</a-row> <a-modal
</a-modal> v-model:visible="bindVis"
title="绑定用户"
:maskClosable="false"
:confirm-loading="confirmLoading"
@cancel="handleCancel"
@ok="handleBindSubmit"
>
<a-form layout="vertical">
<a-form-item label="用户" v-bind="validateInfos.userId">
<a-select
v-model:value="formData.userId"
:options="allUserList"
allowClear
show-search
option-filter-prop="children"
:filter-option="filterOption"
placeholder="请选择用户"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template> </template>
<script setup lang="ts" name="SyncUser"> <script setup lang="ts" name="SyncUser">
@ -106,6 +128,9 @@ import { PropType } from 'vue';
import moment from 'moment'; import moment from 'moment';
import { Modal, message } from 'ant-design-vue'; import { Modal, message } from 'ant-design-vue';
import type { ActionsType } from '@/components/Table/index.vue'; import type { ActionsType } from '@/components/Table/index.vue';
import { Form } from 'ant-design-vue';
const useForm = Form.useForm;
type Emits = { type Emits = {
(e: 'update:visible', data: boolean): void; (e: 'update:visible', data: boolean): void;
@ -220,8 +245,7 @@ const getActions = (
}, },
icon: 'EditOutlined', icon: 'EditOutlined',
onClick: () => { onClick: () => {
// visible.value = true; handleBind(data);
// current.value = data;
}, },
}, },
{ {
@ -344,7 +368,17 @@ watch(
* 绑定用户 * 绑定用户
*/ */
const bindVis = ref(false); const bindVis = ref(false);
const formData = ref({}); const confirmLoading = ref(false);
const formData = ref({ userId: '' });
const formRules = ref({
userId: [{ required: true, message: '请选择用户', trigger: 'change' }],
});
const { resetFields, validate, validateInfos, clearValidate } = useForm(
formData.value,
formRules.value,
);
const handleBind = (row: any) => { const handleBind = (row: any) => {
bindVis.value = true; bindVis.value = true;
formData.value = row; formData.value = row;
@ -352,22 +386,55 @@ const handleBind = (row: any) => {
}; };
/** /**
* 查看错误信息 * 绑定用户, 用户下拉筛选
*/ */
const handleError = (e: any) => { const filterOption = (input: string, option: any) => {
Modal.info({ return (
title: '错误信息', option.componentOptions.children[0].text
content: JSON.stringify(e), .toLowerCase()
.indexOf(input.toLowerCase()) >= 0
);
};
/**
* 绑定提交
*/
const handleBindSubmit = () => {
validate().then(async () => {
const params = {
// providerName: formData.value.thirdPartyUserName,
// thirdPartyUserId: formData.value.thirdPartyUserId,
userId: formData.value.userId,
};
confirmLoading.value = true;
if (props.data.type === 'dingTalk') {
configApi
.dingTalkBindUser([params], props.data.id)
.then(() => {
message.success('操作成功');
bindVis.value = false;
getTableData();
})
.finally(() => {
confirmLoading.value = false;
});
} else if (props.data.type === 'weixin') {
configApi
.weChatBindUser([params], props.data.id)
.then(() => {
message.success('操作成功');
bindVis.value = false;
getTableData();
})
.finally(() => {
confirmLoading.value = false;
});
}
}); });
}; };
/** const handleCancel = () => {
* 查看详情 bindVis.value = false;
*/ resetFields()
const handleDetail = (e: any) => {
Modal.info({
title: '详情信息',
content: JSON.stringify(e),
});
}; };
</script> </script>