mirror of
https://github.com/yudaocode/yudao-ui-admin-vue3.git
synced 2026-05-06 00:09:34 +00:00
perf:【IoT 物联网】场景联动触发器优化
This commit is contained in:
@@ -1,242 +0,0 @@
|
||||
<!-- CRON 可视化构建器组件 -->
|
||||
<!-- TODO @puhui999:看看能不能复用全局的 cron 组件 -->
|
||||
<template>
|
||||
<div class="cron-builder">
|
||||
<div class="builder-header">
|
||||
<span class="header-title">可视化 CRON 编辑器</span>
|
||||
</div>
|
||||
|
||||
<div class="builder-content">
|
||||
<!-- 快捷选项 -->
|
||||
<div class="quick-options">
|
||||
<span class="options-label">常用配置:</span>
|
||||
<el-button
|
||||
v-for="option in quickOptions"
|
||||
:key="option.label"
|
||||
size="small"
|
||||
@click="applyQuickOption(option)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 详细配置 -->
|
||||
<div class="detailed-config">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="4">
|
||||
<el-form-item label="秒">
|
||||
<el-select v-model="cronParts.second" @change="updateCronExpression">
|
||||
<el-option label="每秒" value="*" />
|
||||
<el-option label="0秒" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item label="分钟">
|
||||
<el-select v-model="cronParts.minute" @change="updateCronExpression">
|
||||
<el-option label="每分钟" value="*" />
|
||||
<el-option
|
||||
v-for="i in 60"
|
||||
:key="i - 1"
|
||||
:label="`${i - 1}分`"
|
||||
:value="String(i - 1)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item label="小时">
|
||||
<el-select v-model="cronParts.hour" @change="updateCronExpression">
|
||||
<el-option label="每小时" value="*" />
|
||||
<el-option
|
||||
v-for="i in 24"
|
||||
:key="i - 1"
|
||||
:label="`${i - 1}时`"
|
||||
:value="String(i - 1)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item label="日">
|
||||
<el-select v-model="cronParts.day" @change="updateCronExpression">
|
||||
<el-option label="每日" value="*" />
|
||||
<el-option v-for="i in 31" :key="i" :label="`${i}日`" :value="String(i)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item label="月">
|
||||
<el-select v-model="cronParts.month" @change="updateCronExpression">
|
||||
<el-option label="每月" value="*" />
|
||||
<el-option
|
||||
v-for="(month, index) in months"
|
||||
:key="index"
|
||||
:label="month"
|
||||
:value="String(index + 1)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item label="周">
|
||||
<el-select v-model="cronParts.week" @change="updateCronExpression">
|
||||
<el-option label="每周" value="*" />
|
||||
<el-option
|
||||
v-for="(week, index) in weeks"
|
||||
:key="index"
|
||||
:label="week"
|
||||
:value="String(index)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
|
||||
/** CRON 可视化构建器组件 */
|
||||
defineOptions({ name: 'CronBuilder' })
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const localValue = useVModel(props, 'modelValue', emit)
|
||||
|
||||
// CRON 各部分
|
||||
const cronParts = reactive({
|
||||
second: '0',
|
||||
minute: '0',
|
||||
hour: '12',
|
||||
day: '*',
|
||||
month: '*',
|
||||
week: '?'
|
||||
})
|
||||
|
||||
// 常量数据
|
||||
const months = [
|
||||
'1月',
|
||||
'2月',
|
||||
'3月',
|
||||
'4月',
|
||||
'5月',
|
||||
'6月',
|
||||
'7月',
|
||||
'8月',
|
||||
'9月',
|
||||
'10月',
|
||||
'11月',
|
||||
'12月'
|
||||
]
|
||||
const weeks = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
|
||||
// 快捷选项
|
||||
const quickOptions = [
|
||||
{ label: '每分钟', cron: '0 * * * * ?' },
|
||||
{ label: '每小时', cron: '0 0 * * * ?' },
|
||||
{ label: '每天中午', cron: '0 0 12 * * ?' },
|
||||
{ label: '每天凌晨', cron: '0 0 0 * * ?' },
|
||||
{ label: '工作日9点', cron: '0 0 9 * * MON-FRI' },
|
||||
{ label: '每周一', cron: '0 0 9 * * MON' }
|
||||
]
|
||||
|
||||
// 方法
|
||||
const updateCronExpression = () => {
|
||||
localValue.value = `${cronParts.second} ${cronParts.minute} ${cronParts.hour} ${cronParts.day} ${cronParts.month} ${cronParts.week}`
|
||||
emit('validate', { valid: true, message: 'CRON表达式验证通过' })
|
||||
}
|
||||
|
||||
const applyQuickOption = (option: any) => {
|
||||
localValue.value = option.cron
|
||||
parseCronExpression()
|
||||
emit('validate', { valid: true, message: 'CRON表达式验证通过' })
|
||||
}
|
||||
|
||||
const parseCronExpression = () => {
|
||||
if (!localValue.value) return
|
||||
|
||||
const parts = localValue.value.split(' ')
|
||||
if (parts.length >= 6) {
|
||||
cronParts.second = parts[0] || '0'
|
||||
cronParts.minute = parts[1] || '0'
|
||||
cronParts.hour = parts[2] || '12'
|
||||
cronParts.day = parts[3] || '*'
|
||||
cronParts.month = parts[4] || '*'
|
||||
cronParts.week = parts[5] || '?'
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
if (localValue.value) {
|
||||
parseCronExpression()
|
||||
} else {
|
||||
updateCronExpression()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cron-builder {
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 6px;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.builder-header {
|
||||
padding: 12px 16px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.builder-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.quick-options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.options-label {
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detailed-config {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -1,141 +0,0 @@
|
||||
<!-- CRON 表达式输入组件 -->
|
||||
<!-- TODO @puhui999:看看能不能复用全局的 cron 组件 -->
|
||||
<template>
|
||||
<div class="cron-input">
|
||||
<el-input
|
||||
v-model="localValue"
|
||||
placeholder="请输入 CRON 表达式,如:0 0 12 * * ?"
|
||||
@blur="handleBlur"
|
||||
@input="handleInput"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-tooltip content="CRON 表达式帮助" placement="top">
|
||||
<Icon icon="ep:question-filled" class="input-help" @click="showHelp = !showHelp" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<!-- 帮助信息 -->
|
||||
<div v-if="showHelp" class="cron-help">
|
||||
<el-alert title="CRON 表达式格式:秒 分 时 日 月 周" type="info" :closable="false" show-icon>
|
||||
<template #default>
|
||||
<div class="help-content">
|
||||
<p><strong>示例:</strong></p>
|
||||
<ul>
|
||||
<li><code>0 0 12 * * ?</code> - 每天中午12点执行</li>
|
||||
<li><code>0 */5 * * * ?</code> - 每5分钟执行一次</li>
|
||||
<li><code>0 0 9-17 * * MON-FRI</code> - 工作日9-17点每小时执行</li>
|
||||
</ul>
|
||||
<p><strong>特殊字符:</strong></p>
|
||||
<ul>
|
||||
<li><code>*</code> - 匹配任意值</li>
|
||||
<li><code>?</code> - 不指定值(用于日和周)</li>
|
||||
<li><code>/</code> - 间隔触发,如 */5 表示每5个单位</li>
|
||||
<li><code>-</code> - 范围,如 9-17 表示9到17</li>
|
||||
<li><code>,</code> - 列举,如 MON,WED,FRI</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { validateCronExpression } from '../../utils/validation'
|
||||
|
||||
/** CRON 表达式输入组件 */
|
||||
defineOptions({ name: 'CronInput' })
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const localValue = useVModel(props, 'modelValue', emit)
|
||||
|
||||
// 状态
|
||||
const showHelp = ref(false)
|
||||
|
||||
// 事件处理
|
||||
const handleInput = () => {
|
||||
validateExpression()
|
||||
}
|
||||
|
||||
const handleBlur = () => {
|
||||
validateExpression()
|
||||
}
|
||||
|
||||
const validateExpression = () => {
|
||||
if (!localValue.value) {
|
||||
emit('validate', { valid: false, message: '请输入CRON表达式' })
|
||||
return
|
||||
}
|
||||
|
||||
const isValid = validateCronExpression(localValue.value)
|
||||
if (isValid) {
|
||||
emit('validate', { valid: true, message: 'CRON表达式验证通过' })
|
||||
} else {
|
||||
emit('validate', { valid: false, message: 'CRON表达式格式不正确' })
|
||||
}
|
||||
}
|
||||
|
||||
// 监听值变化
|
||||
watch(
|
||||
() => localValue.value,
|
||||
() => {
|
||||
validateExpression()
|
||||
}
|
||||
)
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
if (localValue.value) {
|
||||
validateExpression()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cron-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-help {
|
||||
color: var(--el-text-color-placeholder);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.input-help:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.cron-help {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.help-content ul {
|
||||
margin: 8px 0 0 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.help-content li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.help-content code {
|
||||
background: var(--el-fill-color-light);
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
</style>
|
||||
@@ -1,178 +0,0 @@
|
||||
<!-- 场景描述输入组件 -->
|
||||
<template>
|
||||
<div class="relative w-full">
|
||||
<el-input
|
||||
ref="inputRef"
|
||||
v-model="localValue"
|
||||
type="textarea"
|
||||
placeholder="请输入场景描述(可选)"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
resize="none"
|
||||
@input="handleInput"
|
||||
/>
|
||||
|
||||
<!-- 描述模板 -->
|
||||
<teleport to="body">
|
||||
<div v-if="showTemplates" ref="templateDropdownRef" class="fixed z-1000 bg-white border border-[var(--el-border-color-light)] rounded-6px shadow-[var(--el-box-shadow)] min-w-300px max-w-400px" :style="dropdownStyle">
|
||||
<div class="flex items-center justify-between p-12px border-b border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-light)]">
|
||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">描述模板</span>
|
||||
<el-button type="text" size="small" @click="showTemplates = false">
|
||||
<Icon icon="ep:close" />
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="max-h-300px overflow-y-auto">
|
||||
<div
|
||||
v-for="template in descriptionTemplates"
|
||||
:key="template.title"
|
||||
class="p-12px border-b border-[var(--el-border-color-lighter)] cursor-pointer transition-colors duration-200 hover:bg-[var(--el-fill-color-light)] last:border-b-0"
|
||||
@click="applyTemplate(template)"
|
||||
>
|
||||
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-4px">{{ template.title }}</div>
|
||||
<div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">{{ template.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
|
||||
<!-- TODO @puhui999:不用模版哈,简单点。。。 -->
|
||||
<!-- 模板按钮 -->
|
||||
<div v-if="!localValue && !showTemplates" class="absolute top-2px right-2px">
|
||||
<el-button type="text" size="small" @click="toggleTemplates">
|
||||
<Icon icon="ep:document" class="mr-1" />
|
||||
使用模板
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
|
||||
/** 场景描述输入组件 */
|
||||
defineOptions({ name: 'DescriptionInput' })
|
||||
|
||||
interface Props {
|
||||
modelValue?: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const localValue = useVModel(props, 'modelValue', emit, {
|
||||
defaultValue: ''
|
||||
})
|
||||
|
||||
const showTemplates = ref(false)
|
||||
const templateDropdownRef = ref()
|
||||
const inputRef = ref()
|
||||
const dropdownStyle = ref({})
|
||||
|
||||
// 描述模板
|
||||
const descriptionTemplates = [
|
||||
{
|
||||
title: '温度控制场景',
|
||||
content: '当环境温度超过设定阈值时,自动启动空调降温设备,确保环境温度保持在舒适范围内。'
|
||||
},
|
||||
{
|
||||
title: '设备监控场景',
|
||||
content: '实时监控关键设备的运行状态,当设备出现异常或离线时,立即发送告警通知相关人员。'
|
||||
},
|
||||
{
|
||||
title: '节能控制场景',
|
||||
content: '根据时间段和环境条件,自动调节设备功率或关闭非必要设备,实现智能节能管理。'
|
||||
},
|
||||
{
|
||||
title: '安防联动场景',
|
||||
content: '当检测到异常情况时,自动触发安防设备联动,包括报警器、摄像头录制等安全措施。'
|
||||
},
|
||||
{
|
||||
title: '定时任务场景',
|
||||
content: '按照预设的时间计划,定期执行设备检查、数据备份或系统维护等自动化任务。'
|
||||
}
|
||||
]
|
||||
|
||||
// 计算下拉框位置
|
||||
const calculateDropdownPosition = () => {
|
||||
if (!inputRef.value) return
|
||||
|
||||
const inputElement = inputRef.value.$el || inputRef.value
|
||||
const rect = inputElement.getBoundingClientRect()
|
||||
const viewportHeight = window.innerHeight
|
||||
const dropdownHeight = 300 // 预估下拉框高度
|
||||
|
||||
let top = rect.bottom + 4
|
||||
let left = rect.left
|
||||
|
||||
// 如果下方空间不够,显示在上方
|
||||
if (top + dropdownHeight > viewportHeight) {
|
||||
top = rect.top - dropdownHeight - 4
|
||||
}
|
||||
|
||||
// 确保不超出左右边界
|
||||
const maxLeft = window.innerWidth - 400 // 下拉框最大宽度
|
||||
if (left > maxLeft) {
|
||||
left = maxLeft
|
||||
}
|
||||
if (left < 10) {
|
||||
left = 10
|
||||
}
|
||||
|
||||
dropdownStyle.value = {
|
||||
top: `${top}px`,
|
||||
left: `${left}px`
|
||||
}
|
||||
}
|
||||
|
||||
const handleInput = (value: string) => {
|
||||
if (value.length > 0) {
|
||||
showTemplates.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const applyTemplate = (template: any) => {
|
||||
localValue.value = template.content
|
||||
showTemplates.value = false
|
||||
}
|
||||
|
||||
const toggleTemplates = () => {
|
||||
showTemplates.value = !showTemplates.value
|
||||
if (showTemplates.value) {
|
||||
nextTick(() => {
|
||||
calculateDropdownPosition()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 点击外部关闭下拉框
|
||||
const handleClickOutside = (event: Event) => {
|
||||
if (
|
||||
templateDropdownRef.value &&
|
||||
!templateDropdownRef.value.contains(event.target as Node) &&
|
||||
inputRef.value &&
|
||||
!inputRef.value.$el.contains(event.target as Node)
|
||||
) {
|
||||
showTemplates.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听窗口大小变化和点击事件
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', calculateDropdownPosition)
|
||||
window.addEventListener('scroll', calculateDropdownPosition)
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', calculateDropdownPosition)
|
||||
window.removeEventListener('scroll', calculateDropdownPosition)
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
<!-- 场景名称输入组件 -->
|
||||
<template>
|
||||
<div class="relative w-full">
|
||||
<el-input
|
||||
v-model="localValue"
|
||||
placeholder="请输入场景名称"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
clearable
|
||||
@blur="handleBlur"
|
||||
@input="handleInput"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon icon="ep:edit" class="text-[var(--el-text-color-placeholder)]" />
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<!-- 智能提示 -->
|
||||
<!-- TODO @puhui999:暂时不用考虑智能推荐哈。用途不大 -->
|
||||
<div v-if="showSuggestions && suggestions.length > 0" class="absolute top-full left-0 right-0 z-1000 bg-white border border-[var(--el-border-color-light)] rounded-4px shadow-[var(--el-box-shadow-light)] mt-4px">
|
||||
<div class="p-8px px-12px border-b border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-light)]">
|
||||
<span class="text-12px text-[var(--el-text-color-secondary)] font-500">推荐名称</span>
|
||||
</div>
|
||||
<div class="max-h-200px overflow-y-auto">
|
||||
<div
|
||||
v-for="suggestion in suggestions"
|
||||
:key="suggestion"
|
||||
class="p-8px px-12px cursor-pointer transition-colors duration-200 text-14px text-[var(--el-text-color-primary)] hover:bg-[var(--el-fill-color-light)] last:border-b-0"
|
||||
@click="applySuggestion(suggestion)"
|
||||
>
|
||||
{{ suggestion }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
|
||||
/** 场景名称输入组件 */
|
||||
defineOptions({ name: 'NameInput' })
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const localValue = useVModel(props, 'modelValue', emit)
|
||||
|
||||
// 智能提示相关
|
||||
const showSuggestions = ref(false)
|
||||
const suggestions = ref<string[]>([])
|
||||
|
||||
// 常用场景名称模板
|
||||
const nameTemplates = [
|
||||
'温度过高自动降温',
|
||||
'设备离线告警通知',
|
||||
'湿度异常自动调节',
|
||||
'夜间安防模式启动',
|
||||
'能耗超标自动关闭',
|
||||
'故障设备自动重启',
|
||||
'定时设备状态检查',
|
||||
'环境数据异常告警'
|
||||
]
|
||||
|
||||
const handleInput = (value: string) => {
|
||||
if (value.length > 0 && value.length < 10) {
|
||||
// 根据输入内容过滤建议
|
||||
suggestions.value = nameTemplates
|
||||
.filter(
|
||||
(template) =>
|
||||
template.includes(value) || (value.includes('温度') && template.includes('温度'))
|
||||
)
|
||||
.slice(0, 5)
|
||||
showSuggestions.value = suggestions.value.length > 0
|
||||
} else {
|
||||
showSuggestions.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleBlur = () => {
|
||||
// 延迟隐藏建议,允许点击建议项
|
||||
setTimeout(() => {
|
||||
showSuggestions.value = false
|
||||
}, 200)
|
||||
}
|
||||
|
||||
const applySuggestion = (suggestion: string) => {
|
||||
localValue.value = suggestion
|
||||
showSuggestions.value = false
|
||||
}
|
||||
|
||||
// 监听外部点击隐藏建议
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (!target.closest('.name-input')) {
|
||||
showSuggestions.value = false
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
<!-- 场景状态选择组件 -->
|
||||
<template>
|
||||
<div class="status-radio">
|
||||
<el-radio-group
|
||||
v-model="localValue"
|
||||
@change="handleChange"
|
||||
>
|
||||
<el-radio :value="0" class="status-option">
|
||||
<div class="status-content">
|
||||
<div class="status-indicator enabled"></div>
|
||||
<div class="status-info">
|
||||
<div class="status-label">启用</div>
|
||||
<div class="status-desc">场景规则生效,满足条件时自动执行</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-radio>
|
||||
|
||||
<el-radio :value="1" class="status-option">
|
||||
<div class="status-content">
|
||||
<div class="status-indicator disabled"></div>
|
||||
<div class="status-info">
|
||||
<div class="status-label">禁用</div>
|
||||
<div class="status-desc">场景规则暂停,不会触发任何执行动作</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
|
||||
/** 场景状态选择组件 */
|
||||
defineOptions({ name: 'StatusRadio' })
|
||||
|
||||
interface Props {
|
||||
modelValue: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: number): void
|
||||
(e: 'change', value: number): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const localValue = useVModel(props, 'modelValue', emit)
|
||||
|
||||
const handleChange = (value: number) => {
|
||||
emit('change', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.status-radio {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.status-radio :deep(.el-radio) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-radio :deep(.el-radio:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-radio-group) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
:deep(.el-radio) {
|
||||
margin-right: 0;
|
||||
width: auto;
|
||||
flex: 1;
|
||||
height: auto;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.status-option {
|
||||
width: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:deep(.el-radio__input) {
|
||||
margin-top: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.el-radio__label) {
|
||||
width: 100%;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.status-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
:deep(.el-radio.is-checked) .status-content {
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.status-content:hover {
|
||||
border-color: var(--el-color-primary-light-3);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-top: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-indicator.enabled {
|
||||
background: var(--el-color-success);
|
||||
box-shadow: 0 0 0 2px var(--el-color-success-light-8);
|
||||
}
|
||||
|
||||
.status-indicator.disabled {
|
||||
background: var(--el-color-danger);
|
||||
box-shadow: 0 0 0 2px var(--el-color-danger-light-8);
|
||||
}
|
||||
|
||||
.status-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.status-desc {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user