feat: 场景联动卡片

This commit is contained in:
100011797 2023-02-28 21:32:47 +08:00
parent fdb3323bcc
commit 7822393092
8 changed files with 730 additions and 65 deletions

View File

@ -7,4 +7,15 @@ export const save = (data: any) => server.post(`/scene`, data)
export const detail = (id: string) => server.get(`/scene/${id}`)
export const query = (data: any) => server.post('/scene/_query/',data);
export const query = (data: any) => server.post('/scene/_query/',data);
export const _delete = (id: string) => server.remove(`/scene/${id}/`);
export const _action = (id: string, type: '_disable' | '_enable') => server.put(`/scene/${id}/${type}`);
/**
*
* @param id
* @returns
*/
export const _execute = (id: string) => server.post(`/scene/${id}/_execute`);

View File

@ -60,6 +60,7 @@ const iconKeys = [
'RedoOutlined',
'VideoCameraOutlined',
'HistoryOutlined',
'LikeOutlined'
]
const Icon = (props: {type: string}) => {

View File

@ -158,7 +158,6 @@ const JTable = defineComponent<JTableProps>({
const pageSize = ref<number>(6)
const total = ref<number>(0)
const loading = ref<boolean>(true)
const loading1 = ref<boolean>(true)
const _columns = computed(() => props.columns.filter(i => !(i?.hideInTable)))
@ -240,6 +239,7 @@ const JTable = defineComponent<JTableProps>({
)
onMounted(() => {
windowChange() // 初始化
window.onresize = () => {
windowChange()
}

View File

@ -28,7 +28,7 @@ type Emit = {
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') },
{ value: 'timer', label: '定时触发', tip: '适用于定期执行固定任务', image: getImage('/timing-trigger.png') },
]
const props = defineProps({

View File

@ -66,6 +66,10 @@ const props = defineProps({
}
})
watchEffect(() => {
Object.assign(formModel, props.data)
})
const emit = defineEmits<Emit>()
const title = computed(() => {

View File

@ -0,0 +1,370 @@
<template>
<div class="card">
<div
class="card-warp"
:class="{ active: active ? 'active' : '' }"
@click="handleClick"
>
<div class="card-type">
<div class="card-type-text"><slot name="type"></slot></div>
</div>
<div class="card-content">
<div style="display: flex">
<!-- 图片 -->
<div class="card-item-avatar">
<slot name="img"> </slot>
</div>
<!-- 内容 -->
<div class="card-item-body">
<slot name="title"></slot>
<span class="subTitle">
<slot name="subTitle"></slot>
</span>
</div>
</div>
<!-- 勾选 -->
<div v-if="active" class="checked-icon">
<div>
<AIcon type="CheckOutlined" />
</div>
</div>
<!-- 状态 -->
<div
v-if="showStatus"
class="card-state"
:class="statusNames ? statusNames[status] : ''"
>
<div class="card-state-content">
<BadgeStatus
:status="status"
:text="statusText"
:statusNames="statusNames"
></BadgeStatus>
</div>
</div>
</div>
</div>
<!-- 按钮 -->
<slot name="bottom-tool">
<div
v-if="showTool && actions && actions.length"
class="card-tools"
>
<div
v-for="item in actions"
:key="item.key"
class="card-button"
:class="{
delete: item.key === 'delete',
}"
>
<slot name="actions" v-bind="item"></slot>
</div>
</div>
</slot>
</div>
</template>
<script setup lang="ts">
import BadgeStatus from '@/components/BadgeStatus/index.vue';
import { StatusColorEnum } from '@/utils/consts.ts';
import type { ActionsType } from '@/components/Table/index.vue';
import { PropType } from 'vue';
type EmitProps = {
(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: () => {},
},
showStatus: {
type: Boolean,
default: true,
},
showTool: {
type: Boolean,
default: true,
},
statusText: {
type: String,
default: '正常',
},
status: {
type: [String, Number],
default: 'default',
},
statusNames: {
type: Object,
},
actions: {
type: Array as PropType<TableActionsType[]>,
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;
overflow: hidden;
&:hover {
cursor: pointer;
box-shadow: 0 0 24px rgba(#000, 0.1);
.card-mask {
visibility: visible;
}
}
&.active {
position: relative;
border: 1px solid #2f54eb;
}
.card-type {
position: absolute;
top: 0;
left: -14px;
height: 32px;
padding: 0 30px;
color: rgba(0, 0, 0, 0.65);
line-height: 32px;
background-color: rgba(0, 0, 0, 0.06);
transform: skewX(-45deg);
.card-type-text {
display: flex;
align-items: center;
justify-content: center;
transform: skewX(45deg);
}
}
.card-content {
position: relative;
padding: 43px 12px 19px 30px;
overflow: hidden;
.card-item-avatar {
margin-right: 16px;
}
.card-item-body {
display: flex;
flex-direction: column;
flex-grow: 1;
width: 0;
.subTitle {
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
margin-top: 10px;
}
}
.card-state {
position: absolute;
top: 40px;
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;
font-size: 16px;
font-weight: 700;
color: @primary-color;
width: calc(100% - 100px);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
:deep(.card-item-heard-name) {
font-weight: 700;
font-size: 16px;
margin-bottom: 12px;
}
:deep(.card-item-content-text) {
color: rgba(0, 0, 0, 0.75);
font-size: 12px;
}
}
}
&.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;
}
}
}
}
}
</style>

View File

@ -1,68 +1,340 @@
<template>
<page-container>
<search
:columns='columns'
/>
<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>
<page-container>
<Search :columns="columns" target="scene" @search="handleSearch" />
<JTable
ref="sceneRef"
:columns="columns"
:request="query"
:defaultParams="{ sorts: [{ name: 'createTime', order: 'desc' }] }"
:params="params"
>
<template #headerTitle>
<a-space>
<PermissionButton
type="primary"
@click="handleAdd"
hasPermission="device/Instance:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</PermissionButton>
</a-space>
</template>
<template #card="slotProps">
<SceneCard
:value="slotProps"
@click="handleClick"
:actions="getActions(slotProps, 'card')"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
started: 'success',
disable: 'error',
}"
>
<template #type>
<span
><img
:height="16"
:src="typeMap.get(slotProps.triggerType)?.icon"
style="margin-right: 5px"
/>{{
typeMap.get(slotProps.triggerType)?.text
}}</span
>
</template>
<template #img>
<img :src="typeMap.get(slotProps.triggerType)?.img" />
</template>
<template #title>
<Ellipsis style="width: calc(100% - 100px)">
<span
style="font-size: 16px; font-weight: 600"
@click.stop="handleView(slotProps.id)"
>
{{ slotProps.name }}
</span>
</Ellipsis>
</template>
<template #subTitle>
<Ellipsis :lineClamp="2">
说明{{
slotProps?.description ||
typeMap.get(slotProps.triggerType)?.tip
}}
</Ellipsis>
</template>
<template #actions="item">
<PermissionButton
:disabled="item.disabled"
:popConfirm="item.popConfirm"
:tooltip="{
...item.tooltip,
}"
@click="item.onClick"
:hasPermission="'rule-engine/Scene:' + item.key"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</PermissionButton>
</template>
</SceneCard>
</template>
<template #triggerType="slotProps">
{{ typeMap.get(slotProps.triggerType)?.text }}
</template>
<template #state="slotProps">
<a-badge
:text="slotProps.state?.text"
:status="statusMap.get(slotProps.state?.value)"
/>
</template>
<template #action="slotProps">
<a-space>
<template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
>
<PermissionButton
:disabled="i.disabled"
:popConfirm="i.popConfirm"
:tooltip="{
...i.tooltip,
}"
@click="i.onClick"
type="link"
style="padding: 0px"
:hasPermission="'device/Instance:' + i.key"
>
<template #icon><AIcon :type="i.icon" /></template>
</PermissionButton>
</template>
</a-space>
</template>
</JTable>
<SaveModal v-if="visible" @close="visible = false" :data="current" />
</page-container>
</template>
<script setup lang='ts'>
import SaveModal from './Save/save.vue'
import type { SceneItem } from './typings'
import { useMenuStore } from 'store/menu'
import SaveModal from './Save/save.vue';
import type { SceneItem } from './typings';
import { useMenuStore } from 'store/menu';
import { query, _delete, _action } from '@/api/rule-engine/scene';
import { message } from 'ant-design-vue';
import type { ActionsType } from '@/components/Table';
import { getImage } from '@/utils/comm';
import SceneCard from './SceneCard.vue';
const menuStory = useMenuStore()
const visible = ref<boolean>(false)
const menuStory = useMenuStore();
const visible = ref<boolean>(false);
const current = ref<Record<string, any>>({});
const statusMap = new Map();
statusMap.set('started', 'success');
statusMap.set('disable', 'error');
const params = ref<Record<string, any>>({});
const sceneRef = ref<Record<string, any>>({});
const typeMap = new Map();
typeMap.set('manual', {
text: '手动触发',
img: getImage('/scene/scene-hand.png'),
icon: getImage('/scene/trigger-type-icon/manual.png'),
tip: '适用于第三方平台向物联网平台下发指令控制设备',
});
typeMap.set('timer', {
text: '定时触发',
img: getImage('/scene/scene-timer.png'),
icon: getImage('/scene/trigger-type-icon/timing.png'),
tip: '适用于定期执行固定任务',
});
typeMap.set('device', {
text: '设备触发',
img: getImage('/scene/scene-device.png'),
icon: getImage('/scene/trigger-type-icon/device.png'),
tip: '适用于设备数据或行为满足触发条件时,执行指定的动作',
});
const columns = [
{
dataIndex: 'name',
fixed: 'left',
ellipsis: true,
width: 300,
title: '名称',
search: {
type: 'string'
{
dataIndex: 'name',
fixed: 'left',
ellipsis: true,
width: 300,
title: '名称',
search: {
type: 'string',
},
},
{
dataIndex: 'triggerType',
title: '触发方式',
scopedSlots: true,
search: {
type: 'select',
options: Array.from(typeMap).map((item) => ({
label: item[1],
value: item[0],
})),
},
},
{
dataIndex: 'state',
title: '状态',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '正常', value: 'started' },
{ label: '禁用', value: 'disable' },
],
},
},
{
dataIndex: 'description',
title: '说明',
search: {
type: 'string',
},
scopedSlots: true,
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
scopedSlots: true,
},
];
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
const actions: ActionsType[] = [
{
key: 'update',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
visible.value = true;
current.value = data;
},
},
{
key: 'action',
text: data.state?.value !== 'disable' ? '禁用' : '启用',
tooltip: {
title: !(!!data.triggerType && (data.branches || [])?.length)
? '未配置规则的不能启用'
: data.state?.value !== 'disable'
? '禁用'
: '启用',
},
disabled: !(!!data?.triggerType && (data?.branches || [])?.length),
icon:
data.state.value !== 'disable'
? 'StopOutlined'
: 'CheckCircleOutlined',
popConfirm: {
title: `确认${
data.state.value !== 'disable' ? '禁用' : '启用'
}?`,
onConfirm: async () => {
let response = undefined;
if (data.state.value !== 'disable') {
response = await _action(data.id, '_disable');
} else {
response = await _action(data.id, '_enable');
}
if (response && response.status === 200) {
message.success('操作成功!');
sceneRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
},
{
key: 'delete',
text: '删除',
disabled: data.state?.value !== 'disable',
tooltip: {
title:
data.state.value !== 'disable'
? '请先禁用该场景,再删除'
: '删除',
},
popConfirm: {
title: '确认删除?',
onConfirm: async () => {
const resp = await _delete(data.id);
if (resp.status === 200) {
message.success('操作成功!');
sceneRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'DeleteOutlined',
},
];
if (data.triggerType === 'manual') {
const _item: ActionsType = {
key: 'trigger',
text: '手动触发',
disabled: data.state?.value === 'disable',
tooltip: {
title:
data.state.value !== 'disable'
? '手动触发'
: '未启用,不能手动触发',
},
icon: 'LikeOutlined',
onClick: () => {
// handleView(data.id, data.triggerType);
},
};
actions.splice(1, 0, _item);
}
},
{
dataIndex: 'triggerType',
title: '触发方式',
search: {
type: 'select',
options: [
{ label: '手动触发', value: 'manual'},
{ label: '定时触发', value: 'timer'},
{ label: '设备触发', value: 'device'}
]
if (type === 'table') {
actions.splice(0, 0, {
key: 'view',
text: '查看',
tooltip: {
title: '查看',
},
icon: 'EyeOutlined',
onClick: () => {
handleView(data.id, data.triggerType);
},
});
}
},
{
dataIndex: 'description',
title: '说明',
},
{
dataIndex: 'state',
title: '状态',
search: {
type: 'select',
options: [
{ label: '正常', value: 'started'},
{ label: '禁用', value: 'disable'},
]
}
}
]
return actions;
};
const handleSearch = (_params: any) => {
params.value = _params;
};
const handleAdd = () => {
visible.value = true;
current.value = {};
};
/**
* 编辑
@ -70,8 +342,12 @@ const columns = [
* @param triggerType 触发类型
*/
const handleEdit = (id: string, triggerType: string) => {
menuStory.jumpPage('Scene/Save', { }, { triggerType: triggerType, id, type: 'edit' })
}
menuStory.jumpPage(
'rule-engine/Scene/Save',
{},
{ triggerType: triggerType, id, type: 'edit' },
);
};
/**
* 查看
@ -79,10 +355,13 @@ const handleEdit = (id: string, triggerType: string) => {
* @param triggerType 触发类型
*/
const handleView = (id: string, triggerType: string) => {
menuStory.jumpPage('Scene/Save', { }, { triggerType: triggerType, id, type: 'view' })
menuStory.jumpPage(
'rule-engine/Scene/Save',
{},
{ triggerType: triggerType, id, type: 'view' },
);
};
</script>
<style scoped>
</style>

View File

@ -1906,7 +1906,7 @@ export default [
],
},
{
id: 'tigger',
id: 'trigger',
name: '手动触发',
permissions: [
{
@ -2323,7 +2323,7 @@ export default [
],
},
{
id: 'tigger',
id: 'trigger',
name: '手动触发',
permissions: [
{