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

This commit is contained in:
leiqiaochu 2023-02-23 10:59:26 +08:00
commit 8bb756e5e6
14 changed files with 695 additions and 32 deletions

View File

@ -0,0 +1,8 @@
import server from '@/utils/request';
export const modify = (id: string, data: any) => server.put(`/scene/${id}`, data)
export const save = (data: any) => server.post(`/scene`, data)
export const detail = (id: string) => server.get(`/scene/${id}`)

View File

@ -0,0 +1,168 @@
<template>
<Tooltip ref="tooltipRef" placement="top" v-bind="props.tooltip">
<template v-if="props.tooltip" #title>
<slot></slot>
<slot name="tooltip"></slot>
</template>
<span
ref="triggerRef"
v-bind="triggerAttrs()"
@click="handleClickRef"
@mouseenter="
[
props.expandTrigger === 'click'
? getTooltipDisabled()
: undefined,
]
"
>
<slot></slot>
</span>
</Tooltip>
</template>
<script lang="ts" setup>
import { Tooltip, TooltipProps } from 'ant-design-vue';
import { computed, mergeProps, PropType, ref, useAttrs } from 'vue';
// define class name
const jEllipsis = 'j-ellipsis';
const jEllipsisCursorClass = 'j-ellipsis-cursor';
const jEllipsisLineClampClass = 'j-ellipsis-line-clamp';
const props = defineProps({
/** expand by */
expandTrigger: {
type: String as PropType<'click'>,
default: undefined,
},
/** multiline ellipsis */
lineClamp: {
type: [Number, String] as PropType<string | number>,
default: 1,
},
/** a-tooltip props */
tooltip: {
type: [Boolean, Object] as PropType<TooltipProps | boolean>,
default: true,
},
});
const attrs = useAttrs();
function triggerAttrs() {
return {
...mergeProps(attrs, {
class: [
jEllipsis,
props.lineClamp !== undefined
? jEllipsisLineClampClass
: undefined,
props.expandTrigger === 'click'
? jEllipsisCursorClass
: undefined,
],
style: ellipsisStyleRef.value,
}),
};
}
const expandedRef = ref(false);
const tooltipRef = ref<HTMLElement | null>(null);
const triggerRef = ref<HTMLElement | null>(null);
const ellipsisStyleRef = computed(() => {
const { lineClamp } = props;
const { value: expanded } = expandedRef;
if (lineClamp !== undefined) {
return {
textOverflow: '',
'-webkit-line-clamp': expanded ? '' : lineClamp,
};
} else {
return {
textOverflow: expanded ? '' : 'ellipsis',
'-webkit-line-clamp': '',
};
}
});
function syncCursorStyle(trigger: HTMLElement, tooltipDisabled: boolean): void {
if (props.expandTrigger === 'click' && !tooltipDisabled) {
syncTriggerClass(trigger, jEllipsisCursorClass, 'add');
} else {
syncTriggerClass(trigger, jEllipsisCursorClass, 'remove');
}
}
function getTooltipDisabled(): boolean {
let tooltipDisabled = false;
const { value: expanded } = expandedRef;
if (expanded) return true;
const { value: trigger } = triggerRef;
if (trigger) {
syncEllipsisStyle(trigger);
tooltipDisabled = trigger.scrollHeight <= trigger.offsetHeight;
syncCursorStyle(trigger, tooltipDisabled);
}
return tooltipDisabled;
}
const handleClickRef = computed(() => {
return props.expandTrigger === 'click'
? () => {
const { value: expanded } = expandedRef;
expandedRef.value = !expanded;
}
: undefined;
});
function syncEllipsisStyle(trigger: HTMLElement): void {
if (!trigger) return;
const latestStyle = ellipsisStyleRef.value;
const lineClampClass = jEllipsisLineClampClass;
if (props.lineClamp !== undefined) {
syncTriggerClass(trigger, lineClampClass, 'add');
} else {
syncTriggerClass(trigger, lineClampClass, 'remove');
}
for (const key in latestStyle) {
if ((trigger.style as any)[key] !== (latestStyle as any)[key]) {
(trigger.style as any)[key] = (latestStyle as any)[key];
}
}
}
function syncTriggerClass(
trigger: HTMLElement,
styleClass: string,
action: 'add' | 'remove',
): void {
if (action === 'add') {
if (!trigger.classList.contains(styleClass)) {
trigger.classList.add(styleClass);
}
} else {
if (trigger.classList.contains(styleClass)) {
trigger.classList.remove(styleClass);
}
}
}
</script>
<style scoped lang='less'>
.j-ellipsis {
overflow: hidden;
vertical-align: bottom;
}
.j-ellipsis-cursor {
cursor: pointer;
}
.j-ellipsis-line-clamp {
display: -webkit-box;
-webkit-box-orient: vertical;
}
</style>

View File

@ -10,6 +10,7 @@ import NormalUpload from './NormalUpload/index.vue'
import FileFormat from './FileFormat/index.vue'
import JUpload from './JUpload/index.vue'
import { BasicLayoutPage, BlankLayoutPage, PageContainer } from './Layout'
import Ellipsis from './Ellipsis/index.vue'
export default {
install(app: App) {
@ -26,5 +27,6 @@ export default {
.component('BasicLayoutPage', BasicLayoutPage)
.component('BlankLayoutPage', BlankLayoutPage)
.component('PageContainer', PageContainer)
.component('Ellipsis', Ellipsis)
}
}

View File

@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import { queryOwnThree } from '@/api/system/menu'
import { filterAsnycRouter, MenuItem } from '@/utils/menu'
import { isArray } from 'lodash-es'
import { usePermissionStore } from './permission'
import router from '@/router'
const defaultOwnParams = [
@ -45,26 +46,15 @@ export const useMenuStore = defineStore({
}),
getters: {
hasPermission(state) {
return (code: string | string[]) => {
if (!code) {
return (menuCode: string | string[]) => {
if (!menuCode) {
return true
}
if (!!Object.keys(state.menus).length) {
let codes: string[] = []
if (typeof code === 'string') {
codes.push(code)
} else {
codes = code
if (typeof menuCode === 'string') {
return !!this.menus[menuCode]
}
return codes.some(_c => {
const menu_code = _c.split(':')
if (menu_code.length > 1) {
return !!this.menus[menu_code[0]]?.buttons?.includes(menu_code[1])
}
return false
})
return menuCode.some(code => !!this.menus[code])
}
return false
}
@ -95,6 +85,8 @@ export const useMenuStore = defineStore({
//过滤非集成的菜单
const resp = await queryOwnThree({ paging: false, terms: defaultOwnParams })
if (resp.success) {
const permission = usePermissionStore()
permission.permissions = {}
const { menusData, silderMenus } = filterAsnycRouter(resp.result)
this.menus = {}
const handleMenuItem = (menu: any) => {
@ -104,6 +96,7 @@ export const useMenuStore = defineStore({
path: menuItem.path,
buttons: menuItem.meta.buttons
}
permission.permissions[menuItem.name] = menuItem.meta.buttons
if (menuItem.children && menuItem.children.length) {
handleMenuItem(menuItem.children)
}

132
src/store/scene.ts Normal file
View File

@ -0,0 +1,132 @@
import { defineStore } from 'pinia'
import type { BranchesType, FormModelType, SceneItem } from '@/views/rule-engine/Scene/typings'
import { detail } from '@/api/rule-engine/scene'
import { cloneDeep, isArray } from 'lodash-es'
import { randomString } from '@/utils/utils'
type DataType = {
data: FormModelType | any
productCache: any
}
const assignmentKey = (data: any[]): any[] => {
const onlyKey = ['when', 'then', 'terms', 'actions'];
if (!data) return [];
return data.map((item: any) => {
if (item) {
item.key = randomString();
Object.keys(item).some((key) => {
if (onlyKey.includes(key) && isArray(item[key])) {
item[key] = assignmentKey(item[key]);
}
});
}
return item;
});
};
export const defaultBranches = [
{
when: [
{
terms: [
{
column: undefined,
value: undefined,
termType: undefined,
key: 'params_1',
type: 'and',
},
],
type: 'and',
key: 'terms_1',
},
],
key: 'branches_1',
shakeLimit: {
enabled: false,
time: 1,
threshold: 1,
alarmFirst: false,
},
then: [],
},
];
const defaultOptions = {
trigger: {},
when: [
{
terms: [
{
terms: [],
},
],
},
],
};
export const useSceneStore = defineStore({
id: 'scene',
state: (): DataType => {
return {
data: {
trigger: { type: ''},
options: defaultOptions,
branches: defaultBranches,
description: ''
},
productCache: {}
}
},
actions: {
/**
*
*/
initData() {
},
/**
*
* @param id
*/
async getDetail(id: string) {
const resp = await detail(id)
if (resp.success) {
const result = resp.result as SceneItem
const triggerType = result.triggerType
let branches: any[] = result.branches
if (!branches) {
branches = cloneDeep(defaultBranches)
if (triggerType === 'device') {
branches.push(null)
}
} else {
const branchesLength = branches.length;
if (
triggerType === 'device' &&
((branchesLength === 1 && !!branches[0]?.when?.length) || // 有一组数据并且when有值
(branchesLength > 1 && !branches[branchesLength - 1]?.when?.length)) // 有多组否则数据并且最后一组when有值
) {
branches.push(null);
}
}
this.data = {
...result,
trigger: result.trigger || {},
branches: cloneDeep(assignmentKey(branches)),
options: {...defaultOptions, ...result.options },
}
}
},
getProduct() {
}
},
getters: {
}
})

View File

@ -13,6 +13,7 @@
v-model:value="formData.type"
placeholder="请选择通知方式"
:disabled="!!formData.id"
@change="handleTypeChange"
>
<a-select-option
v-for="(item, index) in NOTICE_METHOD"
@ -37,6 +38,7 @@
<RadioCard
:options="msgType"
v-model="formData.provider"
@change="handleProviderChange"
/>
</a-form-item>
<!-- 钉钉 -->
@ -331,22 +333,24 @@ watch(
(val) => {
msgType.value = MSG_TYPE[val];
formData.value.provider = msgType.value[0].value;
formData.value.provider =
formData.value.provider !== ':id'
? formData.value.provider
: msgType.value[0].value;
formData.value.configuration =
CONFIG_FIELD_MAP[val][formData.value.provider];
// formData.value.configuration =
// CONFIG_FIELD_MAP[val][formData.value.provider];
clearValid();
// clearValid();
},
);
watch(
() => formData.value.provider,
(val) => {
formData.value.configuration =
CONFIG_FIELD_MAP[formData.value.type][val];
clearValid();
// formData.value.configuration =
// CONFIG_FIELD_MAP[formData.value.type][val];
// clearValid();
},
);
@ -429,10 +433,31 @@ const getDetail = async () => {
const res = await configApi.detail(route.params.id as string);
// formData.value = res.result;
Object.assign(formData.value, res.result);
// console.log('res.result: ', res.result);
// console.log('formData.value: ', formData.value);
};
getDetail();
/**
* 通知方式改变
*/
const handleTypeChange = () => {
setTimeout(() => {
formData.value.configuration =
CONFIG_FIELD_MAP[formData.value.type][formData.value.provider];
// resetPublicFiles();
}, 0);
};
/**
* 通知类型改变
*/
const handleProviderChange = () => {
formData.value.configuration =
CONFIG_FIELD_MAP[formData.value.type][formData.value.provider];
// resetPublicFiles();
};
/**
* 表单提交
*/

View File

@ -48,10 +48,10 @@
<template #img>
<slot name="img">
<img
style="width: 80px; height: 80px"
:src="
getLogo(slotProps.type, slotProps.provider)
"
class="logo"
/>
</slot>
</template>
@ -429,3 +429,9 @@ const getActions = (
}
};
</script>
<style lang="less" scoped>
.logo {
width: 88px;
height: 88px;
}
</style>

View File

@ -51,6 +51,7 @@
:src="
getLogo(slotProps.type, slotProps.provider)
"
class="logo"
/>
</slot>
</template>
@ -388,3 +389,10 @@ const getActions = (
return actions;
};
</script>
<style lang="less" scoped>
.logo {
width: 88px;
height: 88px;
}
</style>

View File

@ -0,0 +1,14 @@
import { getImage } from '@/utils/comm'
export const TriggerHeaderIcon = {
time: getImage('/scene/trigger-type-icon/timing.png'),
manual: getImage('/scene/trigger-type-icon/manual.png'),
device: getImage('/scene/trigger-type-icon/device.png')
}
export const TriggerListIcon = {
time: getImage('/scene/scene-timer.png'),
manual: getImage('/scene/scene-hand.png'),
device: getImage('/scene/scene-device.png')
}

View File

@ -0,0 +1,119 @@
<template>
<div :class='classNames'>
<div
v-for='item in options'
:key='item.value'
:class='["trigger-way-item", modelValue === item.value ? "active" : "" ]'
@click='handleClick(item.value)'
>
<div class='way-item-title'>
<p>{{ item.label }}</p>
<span>{{ item.tip}}</span>
</div>
<div class='way-item-image'>
<img width='40' :src='item.image' />
</div>
</div>
</div>
</template>
<script lang='ts' setup name='TriggerWay'>
import { getImage } from '@/utils/comm'
type Emit = {
(e: 'update:modelValue', data: string): void
}
const options = [
{ value: 'device', label: '设备触发', tip: '适用于设备数据或行为满足触发条件时,执行指定的动作', image: getImage('/device-trigger.png') },
{ value: 'manual', label: '手动触发', tip: '适用于第三方平台向物联网平台下发指令控制设备', image: getImage('/manual-trigger.png') },
{ value: 'timing', label: '定时触发', tip: '适用于定期执行固定任务', image: getImage('/timing-trigger.png') },
]
const props = defineProps({
modelValue: {
type: String,
default: ''
},
className: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
}
})
const emit = defineEmits<Emit>()
const classNames = computed(() => {
return {
[props.className]: true,
'scene-trigger-way-warp': true,
disabled: props.disabled
}
})
const handleClick = (type: string) => {
emit('update:modelValue', type)
}
</script>
<style scoped lang='less'>
@import 'ant-design-vue/es/style/themes/default.less';
.scene-trigger-way-warp {display: flex;
flex-wrap: wrap;
gap: 16px 24px;
width: 100%;
.trigger-way-item {
display: flex;
justify-content: space-between;
padding: 16px;
border: 1px solid #e0e4e8;
border-radius: 2px;
cursor: pointer;
transition: all 0.3s;
width: 204px;
.way-item-title {
p {
margin-bottom: 8px;
font-weight: bold;
font-size: 16px;
}
span {
color: rgba(#000, 0.35);
font-size: 12px;
}
}
.way-item-image {
display: flex;
align-items: center;
height: 100%;
margin: 0 !important;
opacity: 0.6;
}
&:hover {
color: @primary-color-hover;
.way-item-image {
opacity: 0.8;
}
}
&.active {
border-color: @primary-color-active;
.way-item-image {
opacity: 1;
}
}
}
}
</style>

View File

@ -1,13 +1,62 @@
<template>
<page-container>
<div class='scene-warp'>
<div class='header'>
<div class='type'>
<img :src='TriggerHeaderIcon[data.triggerType]' />
{{ keyByLabel[data.triggerType] }}
</div>
</div>
<a-form ref='sceneForm' :model='data'>
</a-form>
<PermissionButton
type='primary'
hasPermission='rule-engine/Scene:update'
>
保存
</PermissionButton>
<!-- <a-button type='primary' :loading='loading'>保存</a-button>-->
</div>
</page-container>
</template>
<script>
export default {
name: 'index'
}
<script setup lang='ts' name='Scene'>
import { useSceneStore } from '@/store/scene'
import { TriggerHeaderIcon } from './asstes'
import { keyByLabel } from '../typings'
const { getDetail, data } = useSceneStore()
const route = useRoute();
const loading = ref(false)
getDetail(route.query.id as string)
</script>
<style scoped>
<style scoped lang='less'>
.scene-warp {
padding: 24px;
background-color: #fff;
.header {
display: flex;
align-items: center;
justify-content: flex-start;
margin-bottom: 16px;
.type {
display: flex;
align-items: center;
min-width: 100px;
margin-left: 16px;
padding: 4px 8px;
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 2px;
}
}
}
</style>

View File

@ -0,0 +1,104 @@
<template>
<a-modal
visible
:title='title'
:width='750'
:confirm-loading='loading'
:maskClosable='false'
@cancel='emit("close")'
@ok='handleOk'
>
<a-form
layout='vertical'
name='scene-save'
ref="formRef"
:model='formModel'
>
<a-form-item
name='name'
label='名称'
:rules="[
{ required: true, message: '请输入名称' },
{ max: 64, message: '最多输入64个字符' }
]"
>
<a-input v-model:value='formModel.name' placeholder='请输入名称' />
</a-form-item>
<a-form-item
:name='["trigger", "type"]'
label='触发方式'
:rules="[{ required: true, message: '请选择触发方式' }]"
>
<TriggerWay v-model:modelValue='formModel.trigger.type' :disabled='disabled' />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang='ts'>
import { SceneItem } from '@/views/rule-engine/Scene/typings'
import TriggerWay from './components/TriggerWay.vue'
import type { PropType } from 'vue'
import type { FormInstance } from 'ant-design-vue';
import { save, modify } from '@/api/rule-engine/scene'
import { useMenuStore } from 'store/menu'
type Emit = {
(e: 'close'): void
}
const loading = ref(false)
const menuStory = useMenuStore()
const formModel = reactive({
name: '',
trigger: {
type: 'device'
}
})
const formRef = ref<FormInstance>()
const props = defineProps({
data: {
type: Object as PropType<Partial<SceneItem>>,
default: () => ({})
}
})
const emit = defineEmits<Emit>()
const title = computed(() => {
return props.data?.id ? '编辑' : '新增'
})
const disabled = computed(() => {
return !!props.data?.id
})
const handleOk = async () => {
if (formRef.value) {
const values = await formRef.value.validateFields()
let modelObj = { ...values }
if (props.data.id) {
modelObj = {
...props.data,
name: values.name
}
}
loading.value = true
const resp = props.data.id ? await modify(props.data.id, modelObj) : await save(modelObj)
loading.value = false
if (resp.success) {
emit('close')
const _id = props.data?.id || (resp.result as any).id
menuStory.jumpPage('rule-engine/Scene/Save', {}, { triggerType: values.trigger.type, id: _id })
}
}
}
</script>
<style scoped>
</style>

View File

@ -5,13 +5,24 @@
/>
<j-table
:columns='columns'
/>
>
<template #headerTitle>
<a-space>
<a-button type="primary" @click="visible = true">新增</a-button>
</a-space>
</template>
</j-table>
<SaveModal v-if='visible' @close='visible = false'/>
</page-container>
</template>
<script setup lang='ts'>
import SaveModal from './Save/save.vue'
import type { SceneItem } from './typings'
import { useMenuStore } from 'store/menu'
const menuStory = useMenuStore()
const visible = ref<boolean>(false)
const columns = [
{
@ -52,6 +63,24 @@ const columns = [
}
}
]
/**
* 编辑
* @param id
* @param triggerType 触发类型
*/
const handleEdit = (id: string, triggerType: string) => {
menuStory.jumpPage('Scene/Save', { }, { triggerType: triggerType, id, type: 'edit' })
}
/**
* 查看
* @param id
* @param triggerType 触发类型
*/
const handleView = (id: string, triggerType: string) => {
menuStory.jumpPage('Scene/Save', { }, { triggerType: triggerType, id, type: 'view' })
};
</script>
<style scoped>

View File

@ -1,3 +1,9 @@
export const keyByLabel = {
manual: '手动触发',
timer: '定时触发',
device: '设备触发',
}
type State = {
value: string;
text: string;