feat: 场景联动新增Terms组件

This commit is contained in:
xieyonghong 2023-03-07 19:19:32 +08:00
parent ef26b169d4
commit 9a883cf7a9
23 changed files with 1068 additions and 125 deletions

View File

@ -8,213 +8,214 @@ export interface IMatcher {
const matchComponents: IMatcher[] = [
{
pattern: /^Avatar/,
styleDir: 'Avatar',
styleDir: 'Avatar'
},
{
pattern: /^AutoComplete/,
styleDir: 'AutoComplete',
styleDir: 'AutoComplete'
},
{
pattern: /^Anchor/,
styleDir: 'Anchor',
styleDir: 'Anchor'
},
{
pattern: /^Badge/,
styleDir: 'Badge',
styleDir: 'Badge'
},
{
pattern: /^Breadcrumb/,
styleDir: 'Breadcrumb',
styleDir: 'Breadcrumb'
},
{
pattern: /^Button/,
styleDir: 'Button',
styleDir: 'Button'
},
{
pattern: /^Checkbox/,
styleDir: 'Checkbox',
styleDir: 'Checkbox'
},
{
pattern: /^CardSelect/,
styleDir: 'CardSelect'
},
{
pattern: /^Card/,
styleDir: 'Card',
styleDir: 'Card'
},
{
pattern: /^Collapse/,
styleDir: 'Collapse',
styleDir: 'Collapse'
},
{
pattern: /^Descriptions/,
styleDir: 'Descriptions',
styleDir: 'Descriptions'
},
{
pattern: /^RangePicker|^WeekPicker|^MonthPicker/,
styleDir: 'DatePicker',
styleDir: 'DatePicker'
},
{
pattern: /^Dropdown/,
styleDir: 'Dropdown',
styleDir: 'Dropdown'
},
{
pattern: /^Form/,
styleDir: 'Form',
styleDir: 'Form'
},
{
pattern: /^InputNumber/,
styleDir: 'InputNumber',
styleDir: 'InputNumber'
},
{
pattern: /^Input|^Textarea/,
styleDir: 'Input',
styleDir: 'Input'
},
{
pattern: /^Statistic/,
styleDir: 'Statistic',
styleDir: 'Statistic'
},
{
pattern: /^CheckableTag/,
styleDir: 'Tag',
styleDir: 'Tag'
},
{
pattern: /^TimeRangePicker/,
styleDir: 'TimePicker',
styleDir: 'TimePicker'
},
{
pattern: /^Layout/,
styleDir: 'Layout',
styleDir: 'Layout'
},
{
pattern: /^Menu|^SubMenu/,
styleDir: 'Menu',
styleDir: 'Menu'
},
{
pattern: /^Table/,
styleDir: 'Table',
styleDir: 'Table'
},
{
pattern: /^TimePicker|^TimeRangePicker/,
styleDir: 'TimeTicker',
styleDir: 'TimeTicker'
},
{
pattern: /^Radio/,
styleDir: 'Radio',
styleDir: 'Radio'
},
{
pattern: /^Image/,
styleDir: 'Image',
styleDir: 'Image'
},
{
pattern: /^List/,
styleDir: 'List',
styleDir: 'List'
},
{
pattern: /^Tab/,
styleDir: 'Tabs',
styleDir: 'Tabs'
},
{
pattern: /^Mentions/,
styleDir: 'Mentions',
styleDir: 'Mentions'
},
{
pattern: /^Step/,
styleDir: 'Steps',
styleDir: 'Steps'
},
{
pattern: /^Skeleton/,
styleDir: 'Skeleton',
styleDir: 'Skeleton'
},
{
pattern: /^Select/,
styleDir: 'Select',
styleDir: 'Select'
},
{
pattern: /^TreeSelect/,
styleDir: 'TreeSelect',
styleDir: 'TreeSelect'
},
{
pattern: /^Tree|^DirectoryTree/,
styleDir: 'Tree',
styleDir: 'Tree'
},
{
pattern: /^Typography/,
styleDir: 'Typography',
styleDir: 'Typography'
},
{
pattern: /^Timeline/,
styleDir: 'Timeline',
styleDir: 'Timeline'
},
{
pattern: /^Upload/,
styleDir: 'Upload',
styleDir: 'Upload'
},
{
pattern: /^ProTable/,
styleDir: 'ProTable',
styleDir: 'ProTable'
},
{
pattern: /^Search|^AdvancedSearch/,
styleDir: 'Search',
styleDir: 'Search'
},
{
pattern: /^Ellipsis/,
styleDir: 'Ellipsis',
styleDir: 'Ellipsis'
},
{
pattern: /^MonacoEditor/,
styleDir: 'MonacoEditor',
styleDir: 'MonacoEditor'
},
{
pattern: /^ProLayout/,
styleDir: 'ProLayout',
styleDir: 'ProLayout'
},
{
pattern: /^ScrollTable/,
styleDir: 'ScrollTable',
styleDir: 'ScrollTable'
},
{
pattern: /^TableCard/,
styleDir: 'TableCard',
styleDir: 'TableCard'
},
{
pattern: /^Scrollbar/,
styleDir: 'Scrollbar',
styleDir: 'Scrollbar'
},
{
pattern: /^AIcon/,
styleDir: 'AIcon',
},
{
pattern: /^CardSelect/,
styleDir: 'CardSelect',
styleDir: 'AIcon'
},
{
pattern: /^Tooltip/,
styleDir: 'Tooltip',
styleDir: 'Tooltip'
},
{
pattern: /^Empty/,
styleDir: 'Empty',
styleDir: 'Empty'
},
{
pattern: /^Popconfirm/,
styleDir: 'Popconfirm',
styleDir: 'Popconfirm'
},
{
pattern: /^message/,
styleDir: 'Message',
styleDir: 'Message'
},
{
pattern: /^Notification/,
styleDir: 'Notification',
},
styleDir: 'Notification'
}
]
export interface JetlinksVueResolverOptions {
@ -259,7 +260,6 @@ function getStyleDir(compName: string, _isAntd = false): string {
let styleDir
const components = _isAntd ? AntdMatchComponents : matchComponents
const total = components.length
console.log('getStyleDir', compName)
for (let i = 0; i < total; i++) {
const matcher = components[i]
if (compName.match(matcher.pattern)) {
@ -273,10 +273,10 @@ function getStyleDir(compName: string, _isAntd = false): string {
return styleDir
}
function getSideEffects(compName: string, options: JetlinksVueResolverOptions, _isAntd= false): any {
function getSideEffects(compName: string, options: JetlinksVueResolverOptions, _isAntd = false): any {
const {
importStyle = true,
importLess = false,
importLess = false
} = options
if (!importStyle)
@ -286,23 +286,21 @@ function getSideEffects(compName: string, options: JetlinksVueResolverOptions, _
if (importStyle === 'less' || importLess) {
const styleDir = getStyleDir(compName, _isAntd)
console.log('getSideEffects-style-path', `${packageName}/${lib}/${styleDir}/style`)
return `${packageName}/${lib}/${styleDir}/style`
}
else {
} else {
const styleDir = getStyleDir(compName, _isAntd)
return `${packageName}/${lib}/${styleDir}/style/css`
}
}
const filterName = [ 'message', 'Notification', 'AIcon']
const filterName = ['message', 'Notification', 'AIcon']
const primitiveNames = ['Affix', 'Anchor', 'AnchorLink', 'message', 'Notification', 'AutoComplete', 'AutoCompleteOptGroup', 'AutoCompleteOption', 'Alert', 'Avatar', 'AvatarGroup', 'BackTop', 'Badge', 'BadgeRibbon', 'Breadcrumb', 'BreadcrumbItem', 'BreadcrumbSeparator', 'Button', 'ButtonGroup', 'Calendar', 'Card', 'CardGrid', 'CardMeta', 'Collapse', 'CollapsePanel', 'Carousel', 'Cascader', 'Checkbox', 'CheckboxGroup', 'Col', 'Comment', 'ConfigProvider', 'DatePicker', 'MonthPicker', 'WeekPicker', 'RangePicker', 'QuarterPicker', 'Descriptions', 'DescriptionsItem', 'Divider', 'Dropdown', 'DropdownButton', 'Drawer', 'Empty', 'Form', 'FormItem', 'FormItemRest', 'Grid', 'Input', 'InputGroup', 'InputPassword', 'InputSearch', 'Textarea', 'Image', 'ImagePreviewGroup', 'InputNumber', 'Layout', 'LayoutHeader', 'LayoutSider', 'LayoutFooter', 'LayoutContent', 'List', 'ListItem', 'ListItemMeta', 'Menu', 'MenuDivider', 'MenuItem', 'MenuItemGroup', 'SubMenu', 'Mentions', 'MentionsOption', 'Modal', 'Statistic', 'StatisticCountdown', 'PageHeader', 'Pagination', 'Popconfirm', 'Popover', 'Progress', 'Radio', 'RadioButton', 'RadioGroup', 'Rate', 'Result', 'Row', 'Select', 'SelectOptGroup', 'SelectOption', 'Skeleton', 'SkeletonButton', 'SkeletonAvatar', 'SkeletonInput', 'SkeletonImage', 'Slider', 'Space', 'Spin', 'Steps', 'Step', 'Switch', 'Table', 'TableColumn', 'TableColumnGroup', 'TableSummary', 'TableSummaryRow', 'TableSummaryCell', 'Transfer', 'Tree', 'TreeNode', 'DirectoryTree', 'TreeSelect', 'TreeSelectNode', 'Tabs', 'TabPane', 'Tag', 'CheckableTag', 'TimePicker', 'TimeRangePicker', 'Timeline', 'TimelineItem', 'Tooltip', 'Typography', 'TypographyLink', 'TypographyParagraph', 'TypographyText', 'TypographyTitle', 'Upload', 'UploadDragger', 'LocaleProvider', 'ProTable', 'Search', 'AdvancedSearch', 'Ellipsis', 'MonacoEditor', 'ProLayout', 'ScrollTable', 'TableCard', 'Scrollbar', 'CardSelect', 'ColorPicker']
const prefix = 'J'
let jetlinksNames: Set<string>
function genJetlinksNames(primitiveNames: string[]): void {
jetlinksNames = new Set(primitiveNames.map(name => filterName.includes(name) ? name : `${prefix}${name}`))
jetlinksNames = new Set(primitiveNames.map(name => filterName.includes(name) ? name : `${prefix}${name}`))
}
let antdvNames: Set<string>
@ -322,16 +320,14 @@ function isAntdv(compName: string): boolean {
return antdvNames.has(compName)
}
export function JetlinksVueResolver(options: JetlinksVueResolverOptions = {
}): any {
export function JetlinksVueResolver(options: JetlinksVueResolverOptions = {}): any {
return {
type: 'component',
resolve: (name: string) => {
if (options.resolveIcons && name.match(/(Outlined|Filled|TwoTone)$/)) {
return {
name,
from: '@ant-design/icons-vue',
from: '@ant-design/icons-vue'
}
}
const _isJetlinks = isJetlinks(name)
@ -340,13 +336,16 @@ export function JetlinksVueResolver(options: JetlinksVueResolverOptions = {
const importName = filterName.includes(name) ? name : name.slice(1)
options.packageName = _isJetlinks ? 'jetlinks-ui-components' : 'ant-design-vue'
const path = `${options.packageName}/${options.cjs ? 'lib' : 'es'}`
const stylePath = getSideEffects(importName, options, _isAntd)
if (_isJetlinks) {
console.log(name, importName, stylePath)
}
return {
name: importName,
from: path,
sideEffects: getSideEffects(importName, options, _isAntd),
sideEffects: stylePath
}
}
},
}
}
}

View File

@ -21,4 +21,6 @@ export const _action = (id: string, type: '_disable' | '_enable') => server.put(
export const _execute = (id: string) => server.post(`/scene/${id}/_execute`);
// 内置参数
export const queryBuiltInParams = (data: any, params?: any) => server.post(`/scene/parse-variables`, data, params);
export const queryBuiltInParams = (data: any, params?: any) => server.post(`/scene/parse-variables`, data, params);
export const getParseTerm = (data: Record<string, any>) => server.post(`/scene/parse-term-column`, data)

View File

@ -10,7 +10,7 @@ import NormalUpload from './NormalUpload/index.vue'
import FileFormat from './FileFormat/index.vue'
import JProUpload from './JUpload/index.vue'
import { BasicLayoutPage, BlankLayoutPage } from './Layout'
import { PageContainer } from 'jetlinks-ui-components/es/components'
import { PageContainer } from 'jetlinks-ui-components'
import Ellipsis from './Ellipsis/index.vue'
import JEmpty from './Empty/index.vue'
import AMapComponent from './AMapComponent/index.vue'

View File

@ -76,7 +76,7 @@ export const useMenuStore = defineStore({
const path = this.hasMenu(name)
if (path) {
router.push({
name, params, query
name, params, query, state: { params }
})
} else {
onlyMessage('暂无权限,请联系管理员', 'error')

View File

@ -92,7 +92,8 @@ export type MenuItem = {
icon?: string
[key: string]: any
},
component?: any
component?: any,
props?: boolean
};
// 额外子级路由
@ -166,6 +167,7 @@ const findChildrenRoute = (code: string, url: string, routes: any[] = []): MenuI
url: `${url}/${route.code}`,
code: `${code}/${route.code}`,
name: route.name,
props: true,
isShow: false
}
})
@ -182,6 +184,7 @@ const findDetailRouteItem = (code: string, url: string): Partial<MenuItem> | nul
code: `${code}/Detail`,
component: detailComponent,
name: '详情信息',
props: true,
isShow: false
}
}
@ -213,7 +216,8 @@ export function filterAsnycRouter(asyncRouterMap: any, parentCode = '', level =
title: route.name,
hideInMenu: route.isShow === false,
buttons: route.buttons?.map((b: any) => b.id) || []
}
},
props: true,
}
const silder = {..._route}

View File

@ -408,6 +408,7 @@ const { resetFields, validate, validateInfos, clearValidate } = useForm(
);
const getDetail = async () => {
console.log('getDetail', route)
if (route.params.id === ':id') return;
const res = await configApi.detail(route.params.id as string);
// formData.value = res.result;

View File

@ -53,9 +53,9 @@
<script setup lang='ts' name='AddModel'>
import type { PropType } from 'vue'
import type { metadataType, TriggerDevice, TriggerDeviceOptions } from '@/views/rule-engine/Scene/typings'
import type { metadataType, TriggerDevice, TriggerDeviceOptions, SelectorValuesItem } from '@/views/rule-engine/Scene/typings'
import { onlyMessage } from '@/utils/comm'
import { detail as deviceDetail } from '@/api/device/instance'
import { detail as deviceDetail } from '@/api/device/instance'
import Product from './Product.vue'
import DeviceSelect from './DeviceSelect.vue'
import Type from './Type.vue'
@ -66,12 +66,13 @@ type Emit = {
(e: 'save', data: TriggerDevice, options: Record<string, any>): void
}
interface AddModelType extends Omit<TriggerDevice, 'selectorValues'> {
stepNumber: number
deviceKeys: Array<{ label: string, value: string }>
orgId: Array<{ label: string, value: string }>
deviceKeys: SelectorValuesItem[]
orgId: SelectorValuesItem[]
productDetail: any
selectorValues: Array<Record<string, any>>
selectorValues: SelectorValuesItem[]
metadata: metadataType,
operator: TriggerDeviceOptions
}
@ -95,23 +96,21 @@ const props = defineProps({
})
const addModel = reactive<AddModelType>({
productId: '',
selector: 'fixed',
selectorValues: [],
productId: props.value.productId || '',
selector: props.value.selector || 'fixed',
selectorValues: props.value.selectorValues || [],
stepNumber: 0,
deviceKeys: [],
orgId: [],
deviceKeys: props.value.selectorValues || [],
orgId: props.value.selectorValues || [],
productDetail: {},
metadata: {},
operator: {
operator: props.value.operation || {
operator: 'online'
}
})
const optionsCache = ref(props.options)
Object.assign(addModel, props.value)
const handleOptions = (data: TriggerDeviceOptions) => {
const typeIconMap = {
writeProperty: 'icon-bianji1',
@ -230,6 +229,11 @@ const productChange = () => {
addModel.selectorValues = []
}
const getDeviceDetailByMetadata = async (deviceId: string) => {
const resp = await deviceDetail(deviceId)
return resp.result?.metadata
}
const save = async (step?: number) => {
let _step = step !== undefined ? step : addModel.stepNumber
if (_step === 0) {
@ -240,12 +244,8 @@ const save = async (step?: number) => {
return onlyMessage(isFixed ? '请选择设备' : '请选择部门', 'error')
}
//
if (isFixed && addModel.selectorValues?.length === 1) {
const resp = await deviceDetail(addModel.selectorValues[0].value)
handleMetadata(resp.result.metadata)
} else {
handleMetadata(addModel.productDetail?.metadata)
}
const onlyOneDevice = isFixed && addModel.selectorValues?.length === 1
handleMetadata( onlyOneDevice ? await getDeviceDetailByMetadata(addModel.selectorValues[0].value) : addModel.productDetail?.metadata)
addModel.stepNumber = 2
} else {
const typeData = await typeRef.value.vail()
@ -273,6 +273,16 @@ const stepChange = (step: number) => {
}
}
const initQuery = async () => {
if (props.value.selector === 'fixed' && props.value.selectorValues?.length) {
handleMetadata(await getDeviceDetailByMetadata(props.value.selectorValues[0].value))
}
}
nextTick(() => {
initQuery()
})
</script>
<style scoped>

View File

@ -1,5 +1,5 @@
<template>
<Search
<j-advanced-search
:columns="columns"
type='simple'
@search="handleSearch"
@ -7,7 +7,7 @@
target="scene-triggrt-device-device"
/>
<a-divider style='margin: 0' />
<j-table
<j-pro-table
ref='actionRef'
model='CARD'
:columns='columns'
@ -60,7 +60,7 @@
</template>
</CardBox>
</template>
</j-table>
</j-pro-table>
</template>
<script setup lang='ts' name='DeviceSelectList'>
@ -68,17 +68,17 @@ import type { PropType } from 'vue'
import { getImage } from '@/utils/comm'
import { query } from '@/api/device/instance'
import { cloneDeep } from 'lodash-es'
import type { SelectorValuesItem } from '@/views/rule-engine/Scene/typings'
type Emit = {
(e: 'update', data: Array<{ name: string, value: string}>): void
(e: 'update', data: SelectorValuesItem[]): void
}
const actionRef = ref()
const params = ref({})
const context = inject('SceneDeviceAddModel')
const props = defineProps({
rowKeys: {
type: Array as PropType<Array<{ name: string, value: string}>>,
type: Array as PropType<SelectorValuesItem[]>,
default: () => ([])
},
productId: {

View File

@ -12,17 +12,13 @@ import DeviceList from './DeviceList.vue'
import OrgList from './OrgList.vue'
import { getImage } from '@/utils/comm'
import type { PropType } from 'vue'
type ItemType = {
name: string,
value: string
}
import { SelectorValuesItem } from '@/views/rule-engine/Scene/typings'
type Emit = {
(e: 'update:selector', data: string): void
(e: 'update:selectorValues', data: ItemType[]): void
(e: 'update:deviceKeys', data: ItemType[]): void
(e: 'update:orgId', data: ItemType[]): void
(e: 'update:selectorValues', data: SelectorValuesItem[]): void
(e: 'update:deviceKeys', data: SelectorValuesItem[]): void
(e: 'update:orgId', data: SelectorValuesItem[]): void
}
const emit = defineEmits<Emit>()
@ -36,18 +32,22 @@ const props = defineProps({
type: String,
default: ''
},
device: {
type: Array as PropType<ItemType[]>,
selectorValues: {
type: Array as PropType<SelectorValuesItem[]>,
default: () => []
},
deviceKeys: {
type: Array as PropType<SelectorValuesItem[]>,
default: () => []
},
orgId: {
type: Array as PropType<ItemType[]>,
type: Array as PropType<SelectorValuesItem[]>,
default: () => []
}
})
const selectorModel = ref(props.selector)
const devices = ref(props.device)
const devices = ref(props.deviceKeys)
const orgIds = ref(props.orgId)
const typeList = [
@ -69,6 +69,7 @@ const updateDevice = (d: any[]) => {
}
const updateOrg = (d: any[]) => {
console.log('updateOrg', d)
orgIds.value = d
emit('update:orgId', d)
emit('update:selectorValues', d)

View File

@ -1,5 +1,5 @@
<template>
<Search
<j-advanced-search
:columns="columns"
type='simple'
@search="handleSearch"
@ -7,7 +7,7 @@
target="scene-triggrt-device-category"
/>
<a-divider style='margin: 0' />
<JTable
<j-pro-table
ref="instanceRef"
model='TABLE'
type='TREE'
@ -26,9 +26,10 @@
onChange: selectedRowChange
}'
:onChange='tableChange'
@selectCancel='cancelAll'
>
</JTable>
</j-pro-table>
</template>
@ -36,14 +37,15 @@
import type { PropType } from 'vue'
import { getExpandedRowById } from './util'
import { getTreeData_api } from '@/api/system/department'
import { SelectorValuesItem } from '@/views/rule-engine/Scene/typings'
type Emit = {
(e: 'update', data: Array<{ name: string, value: string}>): void
(e: 'update', data: SelectorValuesItem[]): void
}
const props = defineProps({
rowKeys: {
type: Array as PropType<Array<{ name: string, value: string}>>,
type: Array as PropType<SelectorValuesItem[]>,
default: () => ([])
},
productId: {
@ -69,6 +71,9 @@ const columns = [
width: 300,
ellipsis: true,
dataIndex: 'name',
search: {
type: 'string'
}
},
{
title: '排序',
@ -109,10 +114,14 @@ const query = async (p: any) => {
return resp
}
const selectedRowChange = (_: any, selectedRows: any[]) => {
const selectedRowChange = (values: any, selectedRows: any[]) => {
const item = selectedRows[0]
console.log(selectedRows)
emit('update', item ? [{ name: item.name, value: item.id }] : [])
console.log(values, selectedRows)
emit('update', [{ name: item.name, value: item.id }])
}
const cancelAll = () => {
emit('update', [])
}
const expandedRowChange = (keys: string[]) => {

View File

@ -1,5 +1,5 @@
<template>
<Search
<j-advanced-search
:columns="columns"
type='simple'
@search="handleSearch"
@ -7,7 +7,7 @@
target="scene-triggrt-device-device"
/>
<a-divider style='margin: 0' />
<j-table
<j-pro-table
ref='actionRef'
model='CARD'
:columns='columns'
@ -53,7 +53,7 @@
</template>
</CardBox>
</template>
</j-table>
</j-pro-table>
</template>
<script setup lang='ts' name='Product'>
@ -83,6 +83,7 @@ const props = defineProps({
})
const emit = defineEmits<Emit>()
const firstFind = ref(true)
const columns = [
{
@ -230,7 +231,7 @@ const handleSearch = (p: any) => {
params.value = p
}
const productQuery = (p: any) => {
const productQuery = async (p: any) => {
const sorts: any = [];
if (props.rowKey) {
@ -241,7 +242,15 @@ const productQuery = (p: any) => {
}
sorts.push({ name: 'createTime', order: 'desc' });
p.sorts = sorts
return queryProductList(p)
const resp = await queryProductList(p)
if (resp.success && props.rowKey && firstFind.value) {
const productItem = (resp.result as { data: any[]}).data.find((item: any) => item.id === props.rowKey)
emit('update:detail', productItem)
firstFind.value = false
}
return {
...resp
}
}
const handleClick = (detail: any) => {

View File

@ -14,6 +14,7 @@
<Title :options='data.options.trigger' />
</AddButton>
</a-form-item>
<Terms />
<Action />
<AddModel v-if='visible' @cancel='visible = false' @save='save' :value='data.trigger.device' :options='data.options.trigger' />
</div>
@ -26,6 +27,7 @@ import AddModel from './AddModal.vue'
import AddButton from '../components/AddButton.vue'
import Title from '../components/Title.vue'
import Action from '../action/index.vue'
import Terms from '../components/Terms'
import type { TriggerDevice } from '@/views/rule-engine/Scene/typings'
const sceneStore = useSceneStore()

View File

@ -0,0 +1,153 @@
<template>
<j-dropdown class='scene-select' trigger='click'>
<div :class='dropdownButtonClass'>
<span :style='LabelStyle'>
{{ label }}
</span>
</div>
<template #overlay>
<template v-if='options.length'>
<j-menu v-if='component === "select"' @click='menuSelect'>
<j-menu-item v-for='item in options' :key='item.value'>{{ item.label }}</j-menu-item>
</j-menu>
<j-tree
:selectedKeys='selectValue ? [selectValue] : []'
:treeData='options'
@select='treeSelect'
/>
</template>
<div class='scene-select-empty' v-else>
<j-empty />
</div>
</template>
</j-dropdown>
</template>
<script lang='ts' setup name='DropdownButton'>
import type { PropType } from 'vue'
type LabelType = string | number | undefined
type DropdownButtonOptions = {
label: string;
value: string;
children?: DropdownButtonOptions[];
[key: string]: any;
};
type Emit = {
(e: 'update:value', data: string | number): void
(e: 'select', data: DropdownButtonOptions | undefined ): void
}
const props = defineProps({
placeholder: {
type: String,
default: undefined
},
value: {
type: [String, Number],
default: undefined
},
options: {
type: Array as PropType<Array<DropdownButtonOptions>>,
default: () => []
},
type: {
type: String,
default: 'column' // 'column' | 'termType' | 'value' | 'type'
},
component: {
type: String,
default: 'select' // 'select' | 'treeSelect'
}
})
const emit = defineEmits<Emit>()
const label = ref<LabelType>(props.placeholder)
const selectValue = ref(props.value)
const flatMapTree = new Map()
const LabelStyle = computed(() => {
return {
color: selectValue.value ? '#' : '#'
}
})
const dropdownButtonClass = computed(() => ({
'dropdown-button': true,
'column': props.type === 'column',
'termType': props.type === 'termType',
'value': props.type === 'value',
'type': props.type === 'type',
}))
const getOption = (key?: string | number): DropdownButtonOptions | undefined => {
let option
for(let i = props.options.length - 1; i >= 0; i --) {
const cacheOption = props.options[i]
if (cacheOption.value === key) {
option = {
...cacheOption
}
break
}
}
return option
}
const treeSelect = () => {
}
const menuSelect = (v: any) => {
const option = getOption(props.value)
emit('update:value', v.key)
emit('select', option)
}
watch([props.options, props.value], () => {
const option = getOption(props.value)
console.log(props.value)
if (option) {
label.value = option.label
} else {
label.value = props.value || props.placeholder
}
}, { immediate: true })
</script>
<style scoped lang='less'>
.dropdown-button {
display: flex;
align-items: center;
padding: 6px 8px;
border: 1px solid #d9d9d9;
border-radius: 8px;
cursor: pointer;
}
.column {
color: #00a4fe;
background-color: rgba(154, 219, 255, 0.3);
border-color: rgba(0, 164, 254, 0.3);
}
.termType {
color: #2f54eb;
background-color: rgba(163, 202, 255, 0.3);
border-color: rgba(47, 84, 235, 0.3);
}
.value {
color: #692ca7;
background-color: rgba(188, 125, 238, 0.1);
border-color: rgba(188, 125, 238, 0.5);
}
.type {
padding: 5px 10px;
}
</style>

View File

@ -0,0 +1,13 @@
<template>
</template>
<script>
export default {
name: 'ParamsDropdown'
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,135 @@
<template>
<div :class='["actions-terms-warp", props.class]'>
<div class='actions-terms-title'>
{{ isFirst ? '当' : '否则' }}
</div>
<div :class='optionsClass'>
<j-popconfirm
title='确认删除?'
@confirm='onDelete'
>
<div v-if='!isFirst' class='terms-params-delete danger show'>
<AIcon type='DeleteOutlined' />
</div>
</j-popconfirm>
<div
class='actions-terms-list'
@mouseover='mouseover'
@mouseout='mouseout'
>
<j-popconfirm
title='该操作将清空其它所有否则条件,确认删除?'
placement='topRight'
@confirm='onDeleteAll'
>
<AIcon type='CloseOutlined' v-show='showDelete' />
</j-popconfirm>
<div class='actions-terms-list-content'>
<template v-if='showWhen'>
<TermsItem
v-for='(item, index) in data.when'
:key='item.key'
:isFirst='index === 0'
:isLast='index === data.when.length -1'
:branchName='name'
:whenName='index'
:data='item'
:isFrist='index === 0'
/>
</template>
<span v-else class='when-add' @click='addWhen' :style='{ padding: isFirst ? "16px 0" : 0 }'>
<AIcon type='PlusCircleOutlined' style='padding: 4px' />
添加过滤条件
</span>
</div>
</div>
<div class='actions-branches'>
<j-form-item></j-form-item>
</div>
</div>
</div>
</template>
<script lang='ts' setup name='Branchs'>
import type { PropType } from 'vue'
import type { ActionBranchesProps } from '@/views/rule-engine/Scene/typings'
import TermsItem from './TermsItem.vue'
import { storeToRefs } from 'pinia';
import { useSceneStore } from 'store/scene'
const sceneStore = useSceneStore()
const { data: FormModel } = storeToRefs(sceneStore)
const props = defineProps({
isFirst: {
type: Boolean,
default: true
},
data: {
type: Object as PropType<ActionBranchesProps>,
default: () => ({
when: [],
shakeLimit: {},
then: []
})
},
class: {
type: String,
default: ''
},
name: {
type: Number,
default: 0
}
})
const showDelete = ref(false)
const error = ref(false)
const showWhen = computed(() => {
return props.data.when.length
})
const onDelete = () => {
}
const onDeleteAll = () => {
}
const mouseover = () => {
if (props.isFirst && props.data.when.length){
showDelete.value = true
}
}
const mouseout = () => {
if (props.isFirst && props.data.when.length){
showDelete.value = false
}
}
const addWhen = () => {
}
const optionsClass = computed(() => {
return {
'actions-terms-options': true,
border: !props.isFirst,
error: error
}
})
</script>
<style scoped lang='less'>
.when-add {
font-size: 14px;
color: #2F54EB;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,142 @@
<template>
<div class='terms-params-item'>
<div v-if='!isFirst' class='term-type-warp'>
<DropdownButton
:options='[
{ label: "并且", value: "and" },
{ label: "或者", value: "or" },
]'
type='type'
v-model:value='paramsValue.type'
/>
</div>
<div
class='params-item_button'
@mouseover='mouseover'
@mouseout='mouseout'
>
<DropdownButton
:options='options'
type='column'
placeholder='请选择参数'
v-model:value='paramsValue.column'
component='treeSelect'
@select='columnSelect'
/>
<DropdownButton
:options='termTypeOptions'
type="termType"
placeholder="操作符"
v-model:value='paramsValue.termsType'
@select='termsTypeSelect'
/>
<termplate v-if='showDouble'>
</termplate>
<j-popconfirm title='确认删除?' @confirm='onDelete'>
<div v-show='showDelete' class='button-delete'> <AIcon type='CloseOutlined' /></div>
</j-popconfirm>
</div>
<div class='term-add' @click.stop='termAdd'>
<div class='terms-content'>
<AIcon type='PlusOutlined' style='font-size: 12px' />
</div>
</div>
</div>
</template>
<script setup lang='ts' name='ParamsItem'>
import type { PropType } from 'vue'
import type { TermsType } from '@/views/rule-engine/Scene/typings'
import DropdownButton from '../DropdownButton.vue'
import { inject } from 'vue'
import { ContextKey } from './util'
const props = defineProps({
isFirst: {
type: Boolean,
default: true
},
isLast: {
type: Boolean,
default: true
},
name: {
type: Number,
default: 0
},
value: {
type: Object as PropType<TermsType>,
default: () => ({
column: '',
type: '',
termsType: undefined,
value: undefined
})
}
})
const paramsValue = reactive<TermsType>({
column: '',
type: '',
termType: undefined,
value: undefined
})
const showDelete = ref(false)
const columnOptions = inject(ContextKey)
const options = computed(() => {
function handleOptions() {
}
return []
})
const termTypeOptions = computed(() => {
return []
})
const showDouble = computed(() => {
return paramsValue.termType ? ['nbtw', 'btw', 'in', 'nin'].includes(paramsValue.termType) : false
})
const mouseover = () => {
if (props.name !== 0){
showDelete.value = true
}
}
const mouseout = () => {
if (props.name !== 0){
showDelete.value = false
}
}
const columnSelect = () => {
}
const termsTypeSelect = () => {
}
const termAdd = () => {
}
const onDelete = () => {
}
onMounted(() => {
Object.assign(paramsValue, props.value)
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,121 @@
<template>
<div class='actions-terms'>
<TitleComponent data='触发条件' style='font-size: 14px;' >
<template #extra>
<j-switch
:checked='open'
@change='change'
checkedChildren='开'
unCheckedChildren='关'
style='margin-left: 4px;'
/>
</template>
</TitleComponent>
<template v-if='open'>
<template v-for='(item, index) in data.branches'>
<Branches
v-if='!!item'
:data='item'
:isFirst='index === 0'
:name='index'
:key='item.key'
@delete='branchesDelete'
@deleteAll='branchesDeleteAll'
/>
<div v-else class='actions-terms-warp' :style='{ marginTop: data.branches.length === 2 ? 0 : 24 }'>
<div class='actions-terms-title' style='padding: 0'>
否则
</div>
<div class='actions-terms-options no-when'>
<AIcon type='PlusOutlined' class='when-add-button' @click='addBranches' />
</div>
</div>
</template>
</template>
<j-form-item
v-else
:name='["branches", 0, "then"]'
:rules='rules'
>
</j-form-item>
</div>
</template>
<script setup lang='ts' name='Terms'>
import { storeToRefs } from 'pinia';
import { useSceneStore } from 'store/scene'
import { cloneDeep } from 'lodash-es'
import { provide } from 'vue'
import { ContextKey } from './util'
import { getParseTerm } from '@/api/rule-engine/scene'
import type { FormModelType } from '@/views/rule-engine/Scene/typings'
import Branches from './Branchs.vue'
const sceneStore = useSceneStore()
const { data } = storeToRefs(sceneStore)
const open = ref(false)
const columnOptions = ref<any[]>([])
provide(ContextKey, columnOptions)
const change = (e: boolean) => {
open.value = e
}
const rules = [{
validator(_: string, value: any) {
if (!value || (value && !value.length)) {
return Promise.reject('至少配置一个执行动作')
} else {
const isActions = value.some((item: any) => item.actions && item.actions.length)
return isActions ? Promise.resolve() : Promise.reject('至少配置一个执行动作');
}
return Promise.resolve();
}
}]
const queryColumn = (dataModel: FormModelType) => {
const cloneDevice = cloneDeep(dataModel)
cloneDevice.branches = cloneDevice.branches?.filter(item => !!item)
getParseTerm(cloneDevice).then(res => {
columnOptions.value = res as any
})
}
const addBranches = () => {
}
const branchesDelete = (index: number) => {
if ((data as FormModelType).branches?.length === 2) {
(data as FormModelType).branches?.splice(index, 1, null as any)
} else {
(data as FormModelType).branches?.splice(index, 1)
}
(data as FormModelType).options?.when?.splice(index, 1)
}
const branchesDeleteAll = () => {
}
watchEffect(() => {
if ((data as FormModelType).trigger?.device) {
queryColumn((data as FormModelType))
}
})
watchEffect(() => {
open.value = !(
(data as FormModelType).branches &&
(data as FormModelType).branches?.length === 1
)
})
</script>
<style scoped lang='less'>
</style>

View File

@ -0,0 +1,126 @@
<template>
<div class='terms-params'>
<div class='terms-params-warp'>
<div v-if='!isFirst' class='term-type-warp'>
<DropdownButton
:options='[
{ label: "并且", value: "and" },
{ label: "或者", value: "or" },
]'
type='type'
v-model:value='formModel.branches[branchName].when[whenName].type'
/>
</div>
<div
class='terms-params-content'
@mouseover='mouseover'
@mouseout='mouseout'
>
<j-popconfirm
title='确认删除?'
@confirm='onDelete'
>
<div v-show='showDelete' class='terms-params-delete'>
<AIcon type='CloseOutlined' />
</div>
</j-popconfirm>
<j-form-item
v-for='(item, index) in data.terms'
:key='item.key'
:name='["branches", branchName, "when", whenName, "terms", index]'
>
<ParamsItem
v-model:value='formModel.branches[branchName].when[whenName].terms[index]'
:isFirst='index === 0'
:isLast='index === data.terms.length - 1'
:name='index'
@change='paramsChange'
@delete='paramsDelete'
@add='paramsAdd'
/>
</j-form-item>
</div>
</div>
</div>
</template>
<script setup lang='ts' name='TermsItem'>
import type { PropType } from 'vue'
import type { TermsType } from '@/views/rule-engine/Scene/typings'
import DropdownButton from '../DropdownButton.vue'
import { storeToRefs } from 'pinia';
import { useSceneStore } from 'store/scene'
import ParamsItem from './ParamsItem.vue'
const sceneStore = useSceneStore()
const { data: formModel } = storeToRefs(sceneStore)
const props = defineProps({
isFirst: {
type: Boolean,
default: true
},
data: {
type: Object as PropType<TermsType>,
default: () => ({
when: [],
shakeLimit: {},
then: []
})
},
class: {
type: String,
default: ''
},
branchName: {
type: Number,
default: 0
},
whenName: {
type: Number,
default: 0
}
})
const showDelete = ref(false)
const mouseover = () => {
console.log(props.whenName)
if (props.whenName !== 0){
showDelete.value = true
}
}
const mouseout = () => {
console.log(props.whenName)
if (props.whenName !== 0){
showDelete.value = false
}
}
const onDelete = () => {
}
const onDeleteAll = () => {
}
const paramsChange = () => {
}
const paramsDelete = () => {
}
const paramsAdd = () => {
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,204 @@
.add-button() {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: rgba(0, 0, 0, 0.3);
background-color: #fff;
border: 1px dashed rgba(0, 0, 0, 0.3);
border-radius: 50%;
cursor: pointer;
}
.deleteBtn() {
position: absolute;
top: -10px;
right: -10px;
display: none;
width: 20px;
height: 20px;
color: #999;
line-height: 20px;
text-align: center;
background-color: #f1f1f1;
border-radius: 50%;
cursor: pointer;
&.show {
display: block;
}
&:hover {
background-color: #f3f3f3;
}
}
.actions-terms {
.actions-terms-warp {
display: flex;
width: 66.66%;
margin-bottom: 24px;
&.first-children {
width: 100%;
.actions-branches {
width: 66.66%;
}
}
&.first-children,
&:last-child {
margin-bottom: 0;
}
.when-add-button {
.add-button();
}
.actions-terms-title {
width: 40px;
padding-top: 16px;
color: #6968be;
font-weight: 800;
font-size: 16px;
}
.actions-terms-options {
position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
width: 0;
&.border {
padding: 10px 18px 0 18px;
border: 1px dashed #999;
border-radius: 2px;
}
&.no-when {
flex: none;
}
.actions-terms-list {
position: relative;
margin-bottom: 16px;
.ant-form-item-has-error {
.params-item_button {
border-color: @error-color;
}
}
.actions-terms-list-content {
display: flex;
padding-top: 10px;
overflow-x: auto;
overflow-y: visible;
row-gap: 16px;
}
}
}
}
}
.terms-params {
display: flex;
flex-shrink: 0;
.terms-params-warp {
display: flex;
align-items: baseline;
}
.terms-params-content {
position: relative;
display: flex;
// flex-wrap: wrap;
padding: 8px;
padding-bottom: 0;
border: 1px dashed #e0e0e0;
//background-color: #fafafa;
border-radius: 6px;
row-gap: 16px;
.terms-params-item {
display: flex;
align-items: center;
}
.ant-form-item {
margin-bottom: 8px;
&:not(:first-child) {
.ant-form-item-explain-error {
padding-left: 80px !important;
}
}
}
}
.terms-group-add {
width: 66px;
margin-left: 16px;
padding: 2px 8px;
color: rgba(0, 0, 0, 0.3);
background: #fff;
border: 1px dashed rgba(0, 0, 0, 0.3);
border-radius: 30px;
cursor: pointer;
.terms-content {
display: flex;
align-items: center;
}
}
.term-type-warp {
width: 50px;
margin: 0 16px;
.term-type {
padding-top: 4px;
padding-bottom: 4px;
border-radius: 2px;
}
}
}
.terms-params-item {
.params-button {
padding: 6px 8px;
border: 1px solid #d9d9d9;
border-radius: 8px;
cursor: pointer;
}
.params-item_button {
position: relative;
display: flex;
gap: 2px;
.button-delete {
.deleteBtn();
}
}
.term-add {
margin-left: 16px;
.add-button();
}
}
.terms-params-delete {
.deleteBtn();
&.danger {
color: #e50012;
background-color: rgba(229, 0, 18, 0.1);
}
&.filter-terms-params-delete {
transform: translateY(6px);
}
}

View File

@ -0,0 +1,4 @@
import Terms from './Terms.vue'
import './index.less'
export default Terms

View File

@ -0,0 +1,2 @@
export const ContextKey = 'columnOptions'

View File

@ -122,13 +122,18 @@ export interface TriggerDeviceOptions {
functionParameters?: Record<string, any>[];
}
export type SelectorValuesItem = {
name: string
value: any
}
/**
*
*/
export interface TriggerDevice {
productId: string;
selector: string;
selectorValues?: Record<string, any>[];
selectorValues?: SelectorValuesItem[];
operation?: TriggerDeviceOptions;
}

View File

@ -10,6 +10,7 @@ import VueSetupExtend from 'vite-plugin-vue-setup-extend'
import { createStyleImportPlugin, AndDesignVueResolve } from 'vite-plugin-style-import'
import * as path from 'path'
import monacoEditorPlugin from 'vite-plugin-monaco-editor';
// import { JetlinksVueResolver } from 'jetlinks-ui-components/lib/plugin/resolve'
import { JetlinksVueResolver } from './plugin/jetlinks'
import copy from 'rollup-plugin-copy';