refactor: 重构设备产品详情页面的属性、功能和事件定义
- 将属性、功能和事件定义抽离为单独的组件 - 优化了数据类型、表单类型等选项的管理 - 改进了输出参数的编辑功能 - 统一了保存和取消操作的样式 - 调整了部分UI样式,如描述字段的样式
This commit is contained in:
parent
fde2ec9bae
commit
f8619047c5
|
@ -21,3 +21,93 @@ export const deviceTypeOptions = [
|
||||||
tooltip: '能挂载子设备与平台进行通信的设备',
|
tooltip: '能挂载子设备与平台进行通信的设备',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 数据类型选项
|
||||||
|
export const dataTypeOptions = [
|
||||||
|
{
|
||||||
|
value: 'int',
|
||||||
|
label: 'int(整数型)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'long',
|
||||||
|
label: 'long(长整数型)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'float',
|
||||||
|
label: 'float(单精度浮点型)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'double',
|
||||||
|
label: 'double(双精度浮点数)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'string',
|
||||||
|
label: 'text(字符串)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'boolean',
|
||||||
|
label: 'boolean(布尔型)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'date',
|
||||||
|
label: 'date(时间型)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'enum',
|
||||||
|
label: 'enum(枚举)',
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// value: 'array',
|
||||||
|
// label: 'array(数组)',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: 'object',
|
||||||
|
// label: 'object(结构体)',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: 'file',
|
||||||
|
// label: 'file(文件)',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: 'password',
|
||||||
|
// label: 'password(密码)',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: 'geoPoint',
|
||||||
|
// label: 'geoPoint(地理位置)',
|
||||||
|
// }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 表单类型选项
|
||||||
|
export const formTypeOptions = [
|
||||||
|
{ label: '文本', value: 'input' },
|
||||||
|
{ label: '开关', value: 'switch' },
|
||||||
|
{ label: '数字', value: 'number' },
|
||||||
|
{ label: '进度条', value: 'progress' },
|
||||||
|
{ label: '选择器', value: 'select' },
|
||||||
|
{ label: '时间选择器', value: 'time' },
|
||||||
|
// { label: '文本域', value: 'textarea' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const viewTypeOptions = [
|
||||||
|
{ label: '文本', value: 'input' },
|
||||||
|
{ label: '开关', value: 'switch' },
|
||||||
|
{ label: '选择器', value: 'select' },
|
||||||
|
{ label: '进度条', value: 'progress' },
|
||||||
|
{ label: '图片', value: 'img' },
|
||||||
|
{ label: '折线图', value: 'line' },
|
||||||
|
{ label: '仪表盘', value: 'dashboard' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 读写类型选项
|
||||||
|
export const readWriteTypeOptions = [
|
||||||
|
{ label: '读', value: 'R' },
|
||||||
|
{ label: '写', value: 'W' },
|
||||||
|
{ label: '读写', value: 'RW' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const timeOptions = [
|
||||||
|
{ label: 'yyyy-MM-dd HH:mm:ss', value: 'yyyy-MM-dd HH:mm:ss' },
|
||||||
|
{ label: 'yyyy-MM-dd', value: 'yyyy-MM-dd' },
|
||||||
|
{ label: 'HH:mm:ss', value: 'HH:mm:ss' },
|
||||||
|
];
|
||||||
|
|
|
@ -129,7 +129,7 @@ const productParams = computed(() =>
|
||||||
<DescriptionsItem label="更新时间">
|
<DescriptionsItem label="更新时间">
|
||||||
{{ formatTime(productInfo.updateTime) }}
|
{{ formatTime(productInfo.updateTime) }}
|
||||||
</DescriptionsItem>
|
</DescriptionsItem>
|
||||||
<DescriptionsItem label="描述" :span="2">
|
<DescriptionsItem label="描述">
|
||||||
{{ productInfo.description || '暂无描述' }}
|
{{ productInfo.description || '暂无描述' }}
|
||||||
</DescriptionsItem>
|
</DescriptionsItem>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
|
|
|
@ -0,0 +1,412 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
Drawer,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Popconfirm,
|
||||||
|
Row,
|
||||||
|
Space,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
Textarea,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { dataTypeOptions, formTypeOptions } from '#/constants/dicts';
|
||||||
|
|
||||||
|
import ParameterModal from './ParameterModal.vue';
|
||||||
|
|
||||||
|
interface EventData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
sort: number;
|
||||||
|
description: string;
|
||||||
|
outputs: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
isEdit: boolean;
|
||||||
|
data?: EventData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:open', value: boolean): void;
|
||||||
|
(e: 'save', data: EventData): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.open,
|
||||||
|
set: (value) => emit('update:open', value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const saveLoading = ref(false);
|
||||||
|
const parameterType = ref('add');
|
||||||
|
const parameterDataIndex = ref(-1);
|
||||||
|
|
||||||
|
// 输出参数编辑弹窗相关
|
||||||
|
const outputParamModalVisible = ref(false);
|
||||||
|
|
||||||
|
const outputParamForm = ref<any>(null);
|
||||||
|
|
||||||
|
const outputColumns = ref([
|
||||||
|
{
|
||||||
|
title: '标识符',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数据类型',
|
||||||
|
dataIndex: 'dataType',
|
||||||
|
key: 'dataType',
|
||||||
|
scopedSlots: { customRender: 'dataType' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '是否必填',
|
||||||
|
dataIndex: 'required',
|
||||||
|
key: 'required',
|
||||||
|
scopedSlots: { customRender: 'required' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '表单类型',
|
||||||
|
dataIndex: 'formType',
|
||||||
|
key: 'formType',
|
||||||
|
scopedSlots: { customRender: 'formType' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 100,
|
||||||
|
fixed: 'right',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 默认数据
|
||||||
|
const defaultEventData: EventData = {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
sort: 1,
|
||||||
|
description: '',
|
||||||
|
outputs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const formData = ref<EventData>({ ...defaultEventData });
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const formRules = {
|
||||||
|
id: [
|
||||||
|
{ required: true, message: '请输入标识符', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
pattern: /^[a-z]\w*$/i,
|
||||||
|
message: '请输入以字母开头,只包含字母、数字和下划线的标识符',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||||
|
// 'valueParams.dataType': [
|
||||||
|
// { required: true, message: '请选择数据类型', trigger: 'blur' },
|
||||||
|
// ],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 抽屉标题
|
||||||
|
const drawerTitle = computed(() => {
|
||||||
|
return props.isEdit ? '编辑事件' : '新增事件';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 新增输出参数
|
||||||
|
const handleAddOutputParam = () => {
|
||||||
|
parameterType.value = 'add';
|
||||||
|
parameterDataIndex.value = -1;
|
||||||
|
outputParamForm.value = null;
|
||||||
|
outputParamModalVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除输出参数
|
||||||
|
const handleDeleteOutputParam = (index: number) => {
|
||||||
|
formData.value.outputs.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认输出参数
|
||||||
|
const handleOutputParamsConfirm = (params: any[]) => {
|
||||||
|
if (parameterType.value === 'add') {
|
||||||
|
formData.value.outputs.push(params);
|
||||||
|
} else if (parameterType.value === 'edit') {
|
||||||
|
formData.value.outputs[parameterDataIndex.value] = params;
|
||||||
|
} else {
|
||||||
|
formData.value.outputs = params;
|
||||||
|
}
|
||||||
|
outputParamModalVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
saveLoading.value = true;
|
||||||
|
|
||||||
|
emit('save', { ...formData.value });
|
||||||
|
message.success('保存成功');
|
||||||
|
visible.value = false;
|
||||||
|
} catch {
|
||||||
|
message.error('保存失败');
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭抽屉
|
||||||
|
const handleDrawerClose = () => {
|
||||||
|
visible.value = false;
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
Object.assign(formData.value, JSON.parse(JSON.stringify(defaultEventData)));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听数据变化
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
formData.value = { ...newVal };
|
||||||
|
} else {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Drawer
|
||||||
|
v-model:open="visible"
|
||||||
|
:title="drawerTitle"
|
||||||
|
width="800px"
|
||||||
|
@close="handleDrawerClose"
|
||||||
|
>
|
||||||
|
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
||||||
|
<Row :gutter="24">
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="标识符" name="id">
|
||||||
|
<Input v-model:value="formData.id" placeholder="请输入标识符" />
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="名称" name="name">
|
||||||
|
<Input v-model:value="formData.name" placeholder="请输入名称" />
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<!-- 事件特有字段 -->
|
||||||
|
<!-- 输出参数 -->
|
||||||
|
<Col :span="24">
|
||||||
|
<FormItem label="输出参数" name="outputs">
|
||||||
|
<div class="parameter-preview">
|
||||||
|
<Table
|
||||||
|
:columns="outputColumns"
|
||||||
|
:data-source="formData.outputs"
|
||||||
|
:pagination="false"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record, index }">
|
||||||
|
<template v-if="column.key === 'dataType'">
|
||||||
|
{{
|
||||||
|
dataTypeOptions.find(
|
||||||
|
(item) => item.value === record.valueParams.dataType,
|
||||||
|
)?.label
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'required'">
|
||||||
|
<Tag color="processing">
|
||||||
|
{{ record.required ? '是' : '否' }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'formType'">
|
||||||
|
{{
|
||||||
|
formTypeOptions.find(
|
||||||
|
(item) => item.value === record.valueParams.formType,
|
||||||
|
)?.label
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="handleEditOutputParam(record, index)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
placement="left"
|
||||||
|
title="确认删除?"
|
||||||
|
@confirm="handleDeleteOutputParam(index)"
|
||||||
|
>
|
||||||
|
<Button type="link" size="small" danger> 删除 </Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
<div class="add-button-container">
|
||||||
|
<a-button
|
||||||
|
@click="handleAddOutputParam"
|
||||||
|
type="primary"
|
||||||
|
class="add-button"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
|
新增输出参数
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="排序" name="sort">
|
||||||
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.sort"
|
||||||
|
placeholder="请输入排序"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :span="24">
|
||||||
|
<FormItem label="描述" name="description">
|
||||||
|
<Textarea
|
||||||
|
v-model:value="formData.description"
|
||||||
|
placeholder="请输入描述"
|
||||||
|
:rows="3"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Space>
|
||||||
|
<Button @click="handleDrawerClose">取消</Button>
|
||||||
|
<Button type="primary" @click="handleSave" :loading="saveLoading">
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 输入参数编辑弹窗 -->
|
||||||
|
<ParameterModal
|
||||||
|
v-model:open="outputParamModalVisible"
|
||||||
|
:parameter-type="parameterType"
|
||||||
|
:data="outputParamForm"
|
||||||
|
@confirm="handleOutputParamsConfirm"
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.enum-preview {
|
||||||
|
.enum-items-preview {
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-items {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-preview {
|
||||||
|
.parameter-table {
|
||||||
|
margin-top: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.header-item {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #262626;
|
||||||
|
text-align: center;
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body {
|
||||||
|
.table-row {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: center;
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button-container {
|
||||||
|
padding-top: 12px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.add-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,504 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
Drawer,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
InputPassword,
|
||||||
|
message,
|
||||||
|
Popconfirm,
|
||||||
|
RadioButton,
|
||||||
|
RadioGroup,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
Textarea,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { dataTypeOptions, formTypeOptions } from '#/constants/dicts';
|
||||||
|
|
||||||
|
import ParameterModal from './ParameterModal.vue';
|
||||||
|
|
||||||
|
interface FunctionData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
sort: number;
|
||||||
|
description: string;
|
||||||
|
async: boolean;
|
||||||
|
inputs: any[];
|
||||||
|
expands: {
|
||||||
|
checkType: string;
|
||||||
|
preCheck: boolean;
|
||||||
|
staticPasswordValue: string;
|
||||||
|
};
|
||||||
|
outputs: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
isEdit: boolean;
|
||||||
|
data?: FunctionData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:open', value: boolean): void;
|
||||||
|
(e: 'save', data: FunctionData): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.open,
|
||||||
|
set: (value) => emit('update:open', value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const saveLoading = ref(false);
|
||||||
|
const parameterType = ref('add');
|
||||||
|
const parameterDataIndex = ref(-1);
|
||||||
|
|
||||||
|
// 参数编辑弹窗相关
|
||||||
|
const inputParamModalVisible = ref(false);
|
||||||
|
|
||||||
|
const inputParamForm = ref<any>(null);
|
||||||
|
|
||||||
|
const asyncOptions = ref([
|
||||||
|
{ label: '是', value: true },
|
||||||
|
{ label: '否', value: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const preCheckOptions = ref([
|
||||||
|
{ label: '是', value: true },
|
||||||
|
{ label: '否', value: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const preCheckTypeOptions = ref([
|
||||||
|
{ label: '用户密码', value: 'userPassword' },
|
||||||
|
{ label: '固定密码', value: 'staticPassword' },
|
||||||
|
{ label: '用户短信', value: 'userSms' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const inputColumns = ref([
|
||||||
|
{
|
||||||
|
title: '标识符',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数据类型',
|
||||||
|
dataIndex: 'dataType',
|
||||||
|
key: 'dataType',
|
||||||
|
scopedSlots: { customRender: 'dataType' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '是否必填',
|
||||||
|
dataIndex: 'required',
|
||||||
|
key: 'required',
|
||||||
|
scopedSlots: { customRender: 'required' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '表单类型',
|
||||||
|
dataIndex: 'formType',
|
||||||
|
key: 'formType',
|
||||||
|
scopedSlots: { customRender: 'formType' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 100,
|
||||||
|
fixed: 'right',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 默认数据
|
||||||
|
const defaultFunctionData: FunctionData = {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
sort: 1,
|
||||||
|
description: '',
|
||||||
|
async: false,
|
||||||
|
expands: {
|
||||||
|
// 是否开启前置校验
|
||||||
|
preCheck: false,
|
||||||
|
// 前置校验类型 userPassword用户密码 ,staticPassword 固定密码, userSms 用户短信
|
||||||
|
checkType: 'userPassword',
|
||||||
|
// 固定密码 前端md5加密
|
||||||
|
staticPasswordValue: '',
|
||||||
|
},
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const formData = ref<FunctionData>({ ...defaultFunctionData });
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const formRules = {
|
||||||
|
id: [
|
||||||
|
{ required: true, message: '请输入标识符', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
pattern: /^[a-z]\w*$/i,
|
||||||
|
message: '请输入以字母开头,只包含字母、数字和下划线的标识符',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 抽屉标题
|
||||||
|
const drawerTitle = computed(() => {
|
||||||
|
return props.isEdit ? '编辑功能' : '新增功能';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 新增输入参数
|
||||||
|
const handleAddInputParam = () => {
|
||||||
|
parameterDataIndex.value = -1;
|
||||||
|
parameterType.value = 'add';
|
||||||
|
inputParamForm.value = null;
|
||||||
|
inputParamModalVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑输入参数
|
||||||
|
const handleEditInputParam = (record: any, index: number) => {
|
||||||
|
parameterDataIndex.value = index;
|
||||||
|
parameterType.value = 'edit';
|
||||||
|
inputParamForm.value = JSON.parse(JSON.stringify(record));
|
||||||
|
inputParamModalVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除输入参数
|
||||||
|
const handleDeleteInputParam = (index: number) => {
|
||||||
|
formData.value.inputs.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认输入参数
|
||||||
|
const handleInputParamsConfirm = (params: any) => {
|
||||||
|
if (parameterType.value === 'add') {
|
||||||
|
formData.value.inputs.push(params);
|
||||||
|
} else if (parameterType.value === 'edit') {
|
||||||
|
formData.value.inputs[parameterDataIndex.value] = params;
|
||||||
|
} else {
|
||||||
|
formData.value.inputs = params;
|
||||||
|
}
|
||||||
|
inputParamModalVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
saveLoading.value = true;
|
||||||
|
|
||||||
|
emit('save', { ...formData.value });
|
||||||
|
visible.value = false;
|
||||||
|
} catch {
|
||||||
|
message.error('保存失败');
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭抽屉
|
||||||
|
const handleDrawerClose = () => {
|
||||||
|
visible.value = false;
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
Object.assign(
|
||||||
|
formData.value,
|
||||||
|
JSON.parse(JSON.stringify(defaultFunctionData)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听数据变化
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
formData.value = { ...newVal };
|
||||||
|
} else {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Drawer
|
||||||
|
v-model:open="visible"
|
||||||
|
:title="drawerTitle"
|
||||||
|
width="800px"
|
||||||
|
@close="handleDrawerClose"
|
||||||
|
>
|
||||||
|
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
||||||
|
<Row :gutter="24">
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="标识符" name="id">
|
||||||
|
<Input v-model:value="formData.id" placeholder="请输入标识符" />
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="名称" name="name">
|
||||||
|
<Input v-model:value="formData.name" placeholder="请输入名称" />
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<!-- 输入参数 -->
|
||||||
|
<Col :span="24">
|
||||||
|
<FormItem label="输入参数" name="inputs">
|
||||||
|
<div class="parameter-preview">
|
||||||
|
<Table
|
||||||
|
:columns="inputColumns"
|
||||||
|
:data-source="formData.inputs"
|
||||||
|
:pagination="false"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record, index }">
|
||||||
|
<template v-if="column.key === 'dataType'">
|
||||||
|
{{
|
||||||
|
dataTypeOptions.find(
|
||||||
|
(item) => item.value === record.valueParams.dataType,
|
||||||
|
)?.label
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'required'">
|
||||||
|
<Tag color="processing">
|
||||||
|
{{ record.required ? '是' : '否' }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'formType'">
|
||||||
|
{{
|
||||||
|
formTypeOptions.find(
|
||||||
|
(item) => item.value === record.valueParams.formType,
|
||||||
|
)?.label
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="handleEditInputParam(record, index)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
placement="left"
|
||||||
|
title="确认删除?"
|
||||||
|
@confirm="handleDeleteInputParam(index)"
|
||||||
|
>
|
||||||
|
<Button type="link" size="small" danger> 删除 </Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
<div class="add-button-container">
|
||||||
|
<a-button
|
||||||
|
@click="handleAddInputParam"
|
||||||
|
type="primary"
|
||||||
|
class="add-button"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
|
新增输入参数
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<!-- 功能特有字段 -->
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="是否异步" name="async">
|
||||||
|
<RadioGroup v-model:value="formData.async" button-style="solid">
|
||||||
|
<RadioButton
|
||||||
|
:value="item.value"
|
||||||
|
v-for="item in asyncOptions"
|
||||||
|
:key="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</RadioButton>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="是否开启前置校验" name="expands.preCheck">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="formData.expands.preCheck"
|
||||||
|
button-style="solid"
|
||||||
|
>
|
||||||
|
<RadioButton
|
||||||
|
:value="item.value"
|
||||||
|
v-for="item in preCheckOptions"
|
||||||
|
:key="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</RadioButton>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12" v-if="formData.expands.preCheck">
|
||||||
|
<FormItem label="前置校验类型" name="expands.checkType">
|
||||||
|
<Select
|
||||||
|
v-model:value="formData.expands.checkType"
|
||||||
|
placeholder="请选择前置校验类型"
|
||||||
|
:options="preCheckTypeOptions"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12" v-if="formData.expands.checkType === 'staticPassword'">
|
||||||
|
<FormItem label="固定密码" name="expands.staticPasswordValue">
|
||||||
|
<InputPassword
|
||||||
|
v-model:value="formData.expands.staticPasswordValue"
|
||||||
|
placeholder="请输入固定密码"
|
||||||
|
:visibility-toggle="false"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="排序" name="sort">
|
||||||
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.sort"
|
||||||
|
placeholder="请输入排序"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :span="24">
|
||||||
|
<FormItem label="描述" name="description">
|
||||||
|
<Textarea
|
||||||
|
v-model:value="formData.description"
|
||||||
|
placeholder="请输入描述"
|
||||||
|
:rows="3"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Space>
|
||||||
|
<Button @click="handleDrawerClose">取消</Button>
|
||||||
|
<Button type="primary" @click="handleSave" :loading="saveLoading">
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 输入参数编辑弹窗 -->
|
||||||
|
<ParameterModal
|
||||||
|
v-model:open="inputParamModalVisible"
|
||||||
|
:parameter-type="parameterType"
|
||||||
|
:data="inputParamForm"
|
||||||
|
@confirm="handleInputParamsConfirm"
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.enum-preview {
|
||||||
|
.enum-items-preview {
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-items {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-preview {
|
||||||
|
.parameter-table {
|
||||||
|
margin-top: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.header-item {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #262626;
|
||||||
|
text-align: center;
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body {
|
||||||
|
.table-row {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: center;
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button-container {
|
||||||
|
padding-top: 12px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.add-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -2,28 +2,13 @@
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||||
import {
|
import { Button, message, Popconfirm, Space, Table, Tag } from 'ant-design-vue';
|
||||||
Button,
|
|
||||||
Col,
|
|
||||||
Drawer,
|
|
||||||
Form,
|
|
||||||
FormItem,
|
|
||||||
Input,
|
|
||||||
InputNumber,
|
|
||||||
message,
|
|
||||||
Popconfirm,
|
|
||||||
RadioButton,
|
|
||||||
RadioGroup,
|
|
||||||
Row,
|
|
||||||
Select,
|
|
||||||
Space,
|
|
||||||
Switch,
|
|
||||||
Table,
|
|
||||||
Tag,
|
|
||||||
Textarea,
|
|
||||||
} from 'ant-design-vue';
|
|
||||||
|
|
||||||
import EnumListModal from './EnumListModal.vue';
|
import { dataTypeOptions, readWriteTypeOptions } from '#/constants/dicts';
|
||||||
|
|
||||||
|
import EventDrawer from './EventDrawer.vue';
|
||||||
|
import FunctionDrawer from './FunctionDrawer.vue';
|
||||||
|
import PropertyDrawer from './PropertyDrawer.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
metadata: any[];
|
metadata: any[];
|
||||||
|
@ -38,139 +23,14 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const drawerVisible = ref(false);
|
const drawerVisible = ref(false);
|
||||||
const saveLoading = ref(false);
|
|
||||||
const isEdit = ref(false);
|
|
||||||
const formRef = ref();
|
|
||||||
|
|
||||||
// 枚举列表弹窗相关
|
const isEdit = ref(false);
|
||||||
const enumModalVisible = ref(false);
|
|
||||||
|
const editDataIndex = ref(-1);
|
||||||
|
|
||||||
const tableData = ref<any[]>([]);
|
const tableData = ref<any[]>([]);
|
||||||
|
|
||||||
const defaultPropertiesFormData = {
|
const formData = ref<any>(null);
|
||||||
id: '',
|
|
||||||
name: '',
|
|
||||||
sort: 1,
|
|
||||||
description: '',
|
|
||||||
required: false,
|
|
||||||
expands: {
|
|
||||||
source: 'device',
|
|
||||||
type: 'R',
|
|
||||||
},
|
|
||||||
valueParams: {
|
|
||||||
dataType: 'string',
|
|
||||||
formType: 'input',
|
|
||||||
length: null,
|
|
||||||
viewType: 'input',
|
|
||||||
unit: '',
|
|
||||||
enumConf: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const formData = ref({});
|
|
||||||
|
|
||||||
const formRules = {
|
|
||||||
id: [
|
|
||||||
{ required: true, message: '请输入标识符', trigger: 'blur' },
|
|
||||||
{
|
|
||||||
pattern: /^[a-z]\w*$/i,
|
|
||||||
message: '请输入以字母开头,只包含字母、数字和下划线的标识符',
|
|
||||||
trigger: 'blur',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
|
||||||
valueParams: [{ required: true, message: '请选择数据类型', trigger: 'blur' }],
|
|
||||||
};
|
|
||||||
|
|
||||||
// 数据类型选项
|
|
||||||
const dataTypeOptions = ref([
|
|
||||||
{
|
|
||||||
value: 'int',
|
|
||||||
label: 'int(整数型)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'long',
|
|
||||||
label: 'long(长整数型)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'float',
|
|
||||||
label: 'float(单精度浮点型)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'double',
|
|
||||||
label: 'double(双精度浮点数)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'string',
|
|
||||||
label: 'text(字符串)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'boolean',
|
|
||||||
label: 'boolean(布尔型)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'date',
|
|
||||||
label: 'date(时间型)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'enum',
|
|
||||||
label: 'enum(枚举)',
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// value: 'array',
|
|
||||||
// label: 'array(数组)',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// value: 'object',
|
|
||||||
// label: 'object(结构体)',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// value: 'file',
|
|
||||||
// label: 'file(文件)',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// value: 'password',
|
|
||||||
// label: 'password(密码)',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// value: 'geoPoint',
|
|
||||||
// label: 'geoPoint(地理位置)',
|
|
||||||
// }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 表单类型选项
|
|
||||||
const formTypeOptions = ref([
|
|
||||||
{ label: '文本', value: 'input' },
|
|
||||||
{ label: '开关', value: 'switch' },
|
|
||||||
{ label: '数字', value: 'number' },
|
|
||||||
{ label: '进度条', value: 'progress' },
|
|
||||||
{ label: '选择器', value: 'select' },
|
|
||||||
{ label: '时间选择器', value: 'time' },
|
|
||||||
// { label: '文本域', value: 'textarea' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const viewTypeOptions = ref([
|
|
||||||
{ label: '文本', value: 'input' },
|
|
||||||
{ label: '开关', value: 'switch' },
|
|
||||||
{ label: '选择器', value: 'select' },
|
|
||||||
{ label: '进度条', value: 'progress' },
|
|
||||||
{ label: '图片', value: 'img' },
|
|
||||||
{ label: '折线图', value: 'line' },
|
|
||||||
{ label: '仪表盘', value: 'dashboard' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 读写类型选项
|
|
||||||
const readWriteTypeOptions = ref([
|
|
||||||
{ label: '读', value: 'R' },
|
|
||||||
{ label: '写', value: 'W' },
|
|
||||||
{ label: '读写', value: 'RW' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const timeOptions = ref([
|
|
||||||
{ label: 'yyyy-MM-dd HH:mm:ss', value: 'yyyy-MM-dd HH:mm:ss' },
|
|
||||||
{ label: 'yyyy-MM-dd', value: 'yyyy-MM-dd' },
|
|
||||||
{ label: 'HH:mm:ss', value: 'HH:mm:ss' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 表格列配置
|
// 表格列配置
|
||||||
const columns = computed(() => {
|
const columns = computed(() => {
|
||||||
|
@ -273,11 +133,6 @@ const getTypeLabel = () => {
|
||||||
return labels[props.type];
|
return labels[props.type];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 抽屉标题
|
|
||||||
const drawerTitle = computed(() => {
|
|
||||||
return isEdit.value ? `编辑${getTypeLabel()}` : `新增${getTypeLabel()}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
@ -299,9 +154,10 @@ const handleAdd = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 编辑
|
// 编辑
|
||||||
const handleEdit = (record: any) => {
|
const handleEdit = (record: any, index: number) => {
|
||||||
|
editDataIndex.value = index;
|
||||||
isEdit.value = true;
|
isEdit.value = true;
|
||||||
formData.value = structuredClone(record.value);
|
formData.value = JSON.parse(JSON.stringify(record));
|
||||||
drawerVisible.value = true;
|
drawerVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -321,54 +177,30 @@ const handleDelete = async (record: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存
|
// 保存
|
||||||
const handleSave = async () => {
|
const handleSave = async (data: any) => {
|
||||||
try {
|
try {
|
||||||
await formRef.value.validate();
|
|
||||||
saveLoading.value = true;
|
|
||||||
|
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
// 编辑模式
|
// 编辑模式
|
||||||
const index = tableData.value.findIndex(
|
if (editDataIndex.value === -1) {
|
||||||
(item) => item.id === formData.value.id,
|
tableData.value.push({ ...data });
|
||||||
);
|
} else {
|
||||||
if (index !== -1) {
|
tableData.value[editDataIndex.value] = { ...data };
|
||||||
tableData.value[index] = { ...formData.value };
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 新增模式
|
// 新增模式
|
||||||
tableData.value.push({ ...formData.value });
|
tableData.value.push({ ...data });
|
||||||
}
|
}
|
||||||
|
|
||||||
message.success('保存成功');
|
|
||||||
drawerVisible.value = false;
|
drawerVisible.value = false;
|
||||||
emit('change', tableData.value);
|
emit('change', tableData.value);
|
||||||
} catch {
|
} catch {
|
||||||
message.error('保存失败');
|
message.error('保存失败');
|
||||||
} finally {
|
|
||||||
saveLoading.value = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 关闭抽屉
|
|
||||||
const handleDrawerClose = () => {
|
|
||||||
drawerVisible.value = false;
|
|
||||||
resetForm();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 重置表单
|
// 重置表单
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
Object.assign(formData.value, structuredClone(defaultPropertiesFormData));
|
formData.value = null;
|
||||||
};
|
|
||||||
|
|
||||||
// 打开枚举列表编辑弹窗
|
|
||||||
const handleEditEnum = () => {
|
|
||||||
enumModalVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 确认枚举列表
|
|
||||||
const handleEnumConfirm = (enumList: any[]) => {
|
|
||||||
formData.value.valueParams.enumConf = enumList;
|
|
||||||
enumModalVisible.value = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听metadata变化
|
// 监听metadata变化
|
||||||
|
@ -407,13 +239,13 @@ onMounted(() => {
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record, index }">
|
||||||
<template v-if="column.key === 'action'">
|
<template v-if="column.key === 'action'">
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
@click="handleEdit(record)"
|
@click="handleEdit(record, index)"
|
||||||
v-access:code="['device:product:edit']"
|
v-access:code="['device:product:edit']"
|
||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
|
@ -461,247 +293,31 @@ onMounted(() => {
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<!-- 新增/编辑抽屉 -->
|
<!-- 属性定义抽屉 -->
|
||||||
<Drawer
|
<PropertyDrawer
|
||||||
|
v-if="type === 'properties'"
|
||||||
v-model:open="drawerVisible"
|
v-model:open="drawerVisible"
|
||||||
:title="drawerTitle"
|
:is-edit="isEdit"
|
||||||
width="800px"
|
:data="formData"
|
||||||
@close="handleDrawerClose"
|
@save="handleSave"
|
||||||
>
|
|
||||||
<Form
|
|
||||||
ref="formRef"
|
|
||||||
:model="formData"
|
|
||||||
:rules="formRules"
|
|
||||||
layout="vertical"
|
|
||||||
>
|
|
||||||
<Row :gutter="24">
|
|
||||||
<Col :span="12">
|
|
||||||
<FormItem label="标识符" name="id">
|
|
||||||
<Input v-model:value="formData.id" placeholder="请输入标识符" />
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col :span="12">
|
|
||||||
<FormItem label="名称" name="name">
|
|
||||||
<Input v-model:value="formData.name" placeholder="请输入名称" />
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<!-- 值参数配置 -->
|
|
||||||
<Col :span="24">
|
|
||||||
<FormItem label="数据类型" name="dataType">
|
|
||||||
<Select
|
|
||||||
v-model:value="formData.valueParams.dataType"
|
|
||||||
placeholder="请选择数据类型"
|
|
||||||
:options="dataTypeOptions"
|
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col :span="12">
|
|
||||||
<FormItem label="表单类型" name="valueParams.formType">
|
|
||||||
<Select
|
|
||||||
v-model:value="formData.valueParams.formType"
|
|
||||||
placeholder="请选择表单类型"
|
|
||||||
:options="formTypeOptions"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col :span="12">
|
|
||||||
<FormItem label="展示类型" name="valueParams.viewType">
|
|
||||||
<Select
|
|
||||||
v-model:value="formData.valueParams.viewType"
|
|
||||||
placeholder="请选择展示类型"
|
|
||||||
:options="viewTypeOptions"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col :span="24" v-if="formData.valueParams.formType === 'switch'">
|
|
||||||
<Row
|
|
||||||
:gutter="24"
|
|
||||||
style="
|
|
||||||
padding-top: 16px;
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
border: 1px solid #f0f0f0;
|
|
||||||
border-radius: 4px;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Col :span="12">
|
|
||||||
<FormItem label="开关打开名称" name="valueParams.trueText">
|
|
||||||
<Input
|
|
||||||
v-model:value="formData.valueParams.trueText"
|
|
||||||
default-value="是"
|
|
||||||
placeholder="请输入开关打开名称"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col :span="12">
|
|
||||||
<FormItem label="开关打开值" name="valueParams.trueValue">
|
|
||||||
<Input
|
|
||||||
v-model:value="formData.valueParams.trueValue"
|
|
||||||
default-value="true"
|
|
||||||
placeholder="请输入开关打开值"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col :span="12">
|
|
||||||
<FormItem label="开关关闭名称" name="valueParams.falseText">
|
|
||||||
<Input
|
|
||||||
v-model:value="formData.valueParams.falseText"
|
|
||||||
default-value="否"
|
|
||||||
placeholder="请输入开关关闭名称"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col :span="12">
|
|
||||||
<FormItem label="开关关闭值" name="valueParams.falseValue">
|
|
||||||
<Input
|
|
||||||
v-model:value="formData.valueParams.falseValue"
|
|
||||||
default-value="false"
|
|
||||||
placeholder="请输入开关关闭值"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
<Col :span="24" v-if="formData.valueParams.formType === 'select'">
|
<!-- 功能定义抽屉 -->
|
||||||
<FormItem label="枚举列表" name="valueParams.enumList">
|
<FunctionDrawer
|
||||||
<a-button type="primary" @click="handleEditEnum">
|
v-if="type === 'functions'"
|
||||||
编辑枚举列表
|
v-model:open="drawerVisible"
|
||||||
</a-button>
|
:is-edit="isEdit"
|
||||||
</FormItem>
|
:data="formData"
|
||||||
</Col>
|
@save="handleSave"
|
||||||
|
/>
|
||||||
|
|
||||||
<Col
|
<!-- 事件定义抽屉 -->
|
||||||
:span="12"
|
<EventDrawer
|
||||||
v-if="
|
v-if="type === 'events'"
|
||||||
formData.valueParams.formType === 'number' ||
|
v-model:open="drawerVisible"
|
||||||
formData.valueParams.formType === 'progress'
|
:is-edit="isEdit"
|
||||||
"
|
:data="formData"
|
||||||
>
|
@save="handleSave"
|
||||||
<FormItem label="最小值" name="valueParams.min">
|
|
||||||
<InputNumber
|
|
||||||
style="width: 100%"
|
|
||||||
v-model:value="formData.valueParams.min"
|
|
||||||
placeholder="请输入最小值"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col
|
|
||||||
:span="12"
|
|
||||||
v-if="
|
|
||||||
formData.valueParams.formType === 'number' ||
|
|
||||||
formData.valueParams.formType === 'progress'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<FormItem label="最大值" name="valueParams.max">
|
|
||||||
<InputNumber
|
|
||||||
style="width: 100%"
|
|
||||||
v-model:value="formData.valueParams.max"
|
|
||||||
placeholder="请输入最大值"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col :span="12" v-if="formData.valueParams.formType === 'input'">
|
|
||||||
<FormItem label="长度" name="valueParams.length">
|
|
||||||
<InputNumber
|
|
||||||
style="width: 100%"
|
|
||||||
v-model:value="formData.valueParams.length"
|
|
||||||
placeholder="请输入长度"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col
|
|
||||||
:span="12"
|
|
||||||
v-if="
|
|
||||||
formData.valueParams.formType === 'number' ||
|
|
||||||
formData.valueParams.formType === 'progress'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<FormItem label="小数位" name="valueParams.scale">
|
|
||||||
<InputNumber
|
|
||||||
style="width: 100%"
|
|
||||||
v-model:value="formData.valueParams.scale"
|
|
||||||
placeholder="请输入小数位"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col :span="12">
|
|
||||||
<FormItem label="单位" name="valueParams.unit">
|
|
||||||
<Input
|
|
||||||
v-model:value="formData.valueParams.unit"
|
|
||||||
placeholder="请输入单位"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col :span="12" v-if="formData.valueParams.formType === 'time'">
|
|
||||||
<FormItem label="时间格式" name="valueParams.format">
|
|
||||||
<Select
|
|
||||||
v-model:value="formData.valueParams.format"
|
|
||||||
placeholder="请选择时间格式"
|
|
||||||
:options="timeOptions"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col :span="12">
|
|
||||||
<!-- 属性特有字段 -->
|
|
||||||
<FormItem label="是否必填" name="required">
|
|
||||||
<Switch v-model:checked="formData.required" />
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col :span="12">
|
|
||||||
<!-- 属性特有字段 -->
|
|
||||||
<FormItem label="读写类型" name="type">
|
|
||||||
<RadioGroup
|
|
||||||
v-model:value="formData.expands.type"
|
|
||||||
button-style="solid"
|
|
||||||
>
|
|
||||||
<RadioButton
|
|
||||||
:value="item.value"
|
|
||||||
v-for="item in readWriteTypeOptions"
|
|
||||||
:key="item.value"
|
|
||||||
>
|
|
||||||
{{ item.label }}
|
|
||||||
</RadioButton>
|
|
||||||
</RadioGroup>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
<Col :span="12">
|
|
||||||
<FormItem label="排序" name="sort">
|
|
||||||
<InputNumber
|
|
||||||
style="width: 100%"
|
|
||||||
v-model:value="formData.sort"
|
|
||||||
placeholder="请输入排序"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
<Col :span="24">
|
|
||||||
<FormItem label="描述" name="description">
|
|
||||||
<Textarea
|
|
||||||
v-model:value="formData.description"
|
|
||||||
placeholder="请输入描述"
|
|
||||||
:rows="3"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Form>
|
|
||||||
<template #footer>
|
|
||||||
<Space>
|
|
||||||
<Button @click="handleDrawerClose">取消</Button>
|
|
||||||
<Button type="primary" @click="handleSave" :loading="saveLoading">
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</template>
|
|
||||||
</Drawer>
|
|
||||||
|
|
||||||
<!-- 枚举列表编辑弹窗 -->
|
|
||||||
<EnumListModal
|
|
||||||
v-model:open="enumModalVisible"
|
|
||||||
:enum-conf="formData.valueParams?.enumConf"
|
|
||||||
@confirm="handleEnumConfirm"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,387 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Col,
|
||||||
|
Descriptions,
|
||||||
|
DescriptionsItem,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
Switch,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { dataTypeOptions, formTypeOptions } from '#/constants/dicts';
|
||||||
|
|
||||||
|
import EnumListModal from './EnumListModal.vue';
|
||||||
|
|
||||||
|
interface ParameterItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
dataType: string;
|
||||||
|
required: boolean;
|
||||||
|
formType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
parameterType: 'add' | 'edit';
|
||||||
|
data?: ParameterItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:open', value: boolean): void;
|
||||||
|
(e: 'confirm', value: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.open,
|
||||||
|
set: (value) => emit('update:open', value),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 默认数据
|
||||||
|
const defaultPropertyData: PropertyData = {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
sort: 1,
|
||||||
|
description: '',
|
||||||
|
required: false,
|
||||||
|
expands: {
|
||||||
|
source: 'device',
|
||||||
|
type: 'R',
|
||||||
|
},
|
||||||
|
valueParams: {
|
||||||
|
dataType: 'string',
|
||||||
|
formType: 'input',
|
||||||
|
length: null,
|
||||||
|
viewType: 'input',
|
||||||
|
unit: '',
|
||||||
|
enumConf: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const loading = ref(false);
|
||||||
|
const enumModalVisible = ref(false);
|
||||||
|
|
||||||
|
const formData = ref<ParameterItem>({ ...defaultPropertyData });
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const formRules = {
|
||||||
|
id: [
|
||||||
|
{ required: true, message: '请输入标识符', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
pattern: /^[a-z]\w*$/i,
|
||||||
|
message: '请输入以字母开头,只包含字母、数字和下划线的标识符',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||||
|
dataType: [{ required: true, message: '请选择数据类型', trigger: 'blur' }],
|
||||||
|
formType: [{ required: true, message: '请选择表单类型', trigger: 'blur' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
Object.assign(
|
||||||
|
formData.value,
|
||||||
|
JSON.parse(JSON.stringify(defaultPropertyData)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开枚举列表编辑弹窗
|
||||||
|
const handleEditEnum = () => {
|
||||||
|
enumModalVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认枚举列表
|
||||||
|
const handleEnumConfirm = (enumList: any[]) => {
|
||||||
|
formData.value.valueParams.enumConf = enumList;
|
||||||
|
enumModalVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 弹窗标题
|
||||||
|
const modalTitle = computed(() => {
|
||||||
|
return props.parameterType === 'add' ? '新增参数' : '编辑参数';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证数据
|
||||||
|
const validateData = (): boolean => {
|
||||||
|
try {
|
||||||
|
formRef.value.validate();
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认
|
||||||
|
const handleOk = async () => {
|
||||||
|
if (!validateData()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
emit('confirm', JSON.parse(JSON.stringify(formData.value)));
|
||||||
|
visible.value = false;
|
||||||
|
message.success('保存成功');
|
||||||
|
resetForm();
|
||||||
|
} catch {
|
||||||
|
message.error('保存失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消
|
||||||
|
const handleCancel = () => {
|
||||||
|
visible.value = false;
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听data变化,初始化数据
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal === null) {
|
||||||
|
// 新增模式,重置表单
|
||||||
|
resetForm();
|
||||||
|
} else {
|
||||||
|
// 如果有现有参数,可以用于编辑模式
|
||||||
|
formData.value = JSON.parse(JSON.stringify(newVal));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
v-model:open="visible"
|
||||||
|
:title="modalTitle"
|
||||||
|
width="600px"
|
||||||
|
@ok="handleOk"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
:confirm-loading="loading"
|
||||||
|
>
|
||||||
|
<div class="parameter-form">
|
||||||
|
<Form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
|
<FormItem label="标识符" name="id">
|
||||||
|
<Input v-model:value="formData.id" placeholder="请输入标识符" />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="名称" name="name">
|
||||||
|
<Input v-model:value="formData.name" placeholder="请输入名称" />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="数据类型" name="valueParams.dataType">
|
||||||
|
<Select
|
||||||
|
v-model:value="formData.valueParams.dataType"
|
||||||
|
placeholder="请选择数据类型"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<SelectOption
|
||||||
|
v-for="option in dataTypeOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</SelectOption>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="表单类型" name="valueParams.formType">
|
||||||
|
<Select
|
||||||
|
v-model:value="formData.valueParams.formType"
|
||||||
|
placeholder="请选择表单类型"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<SelectOption
|
||||||
|
v-for="option in formTypeOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</SelectOption>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<!-- 开关类型配置 -->
|
||||||
|
<Row
|
||||||
|
:gutter="24"
|
||||||
|
style="
|
||||||
|
padding-top: 16px;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
"
|
||||||
|
v-if="formData.valueParams.formType === 'switch'"
|
||||||
|
>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="开关打开名称" name="valueParams.trueText">
|
||||||
|
<Input
|
||||||
|
v-model:value="formData.valueParams.trueText"
|
||||||
|
default-value="是"
|
||||||
|
placeholder="请输入开关打开名称"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="开关打开值" name="valueParams.trueValue">
|
||||||
|
<Input
|
||||||
|
v-model:value="formData.valueParams.trueValue"
|
||||||
|
default-value="true"
|
||||||
|
placeholder="请输入开关打开值"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="开关关闭名称" name="valueParams.falseText">
|
||||||
|
<Input
|
||||||
|
v-model:value="formData.valueParams.falseText"
|
||||||
|
default-value="否"
|
||||||
|
placeholder="请输入开关关闭名称"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="开关关闭值" name="valueParams.falseValue">
|
||||||
|
<Input
|
||||||
|
v-model:value="formData.valueParams.falseValue"
|
||||||
|
default-value="false"
|
||||||
|
placeholder="请输入开关关闭值"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<!-- 选择器类型配置 -->
|
||||||
|
<FormItem
|
||||||
|
label="枚举列表"
|
||||||
|
name="valueParams.enumConf"
|
||||||
|
v-if="formData.valueParams.formType === 'select'"
|
||||||
|
>
|
||||||
|
<div class="enum-preview">
|
||||||
|
<a-button type="primary" @click="handleEditEnum">
|
||||||
|
编辑枚举列表
|
||||||
|
</a-button>
|
||||||
|
<Descriptions
|
||||||
|
v-if="
|
||||||
|
formData.valueParams.enumConf &&
|
||||||
|
formData.valueParams.enumConf.length > 0
|
||||||
|
"
|
||||||
|
:column="4"
|
||||||
|
bordered
|
||||||
|
title=" "
|
||||||
|
>
|
||||||
|
<DescriptionsItem
|
||||||
|
:label="`${item.text} :`"
|
||||||
|
v-for="item in formData.valueParams.enumConf"
|
||||||
|
:key="item.value"
|
||||||
|
>
|
||||||
|
{{ item.value }}
|
||||||
|
</DescriptionsItem>
|
||||||
|
</Descriptions>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<Row
|
||||||
|
:gutter="24"
|
||||||
|
v-if="
|
||||||
|
formData.valueParams.formType === 'number' ||
|
||||||
|
formData.valueParams.formType === 'progress'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="最小值" name="valueParams.min">
|
||||||
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.valueParams.min"
|
||||||
|
placeholder="请输入最小值"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="最大值" name="valueParams.max">
|
||||||
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.valueParams.max"
|
||||||
|
placeholder="请输入最大值"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<FormItem
|
||||||
|
label="小数位"
|
||||||
|
name="valueParams.scale"
|
||||||
|
v-if="
|
||||||
|
formData.valueParams.formType === 'number' ||
|
||||||
|
formData.valueParams.formType === 'progress'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.valueParams.scale"
|
||||||
|
placeholder="请输入小数位"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
label="长度"
|
||||||
|
name="valueParams.length"
|
||||||
|
v-if="formData.valueParams.formType === 'input'"
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.valueParams.length"
|
||||||
|
placeholder="请输入长度"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
label="时间格式"
|
||||||
|
name="valueParams.format"
|
||||||
|
v-if="formData.valueParams.formType === 'time'"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
v-model:value="formData.valueParams.format"
|
||||||
|
placeholder="请选择时间格式"
|
||||||
|
:options="timeOptions"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="是否必填" name="required">
|
||||||
|
<Switch
|
||||||
|
v-model:checked="formData.required"
|
||||||
|
checked-children="是"
|
||||||
|
un-checked-children="否"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<EnumListModal
|
||||||
|
v-model:open="enumModalVisible"
|
||||||
|
:enum-conf="formData.valueParams?.enumConf"
|
||||||
|
@confirm="handleEnumConfirm"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.parameter-form {
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,468 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
Descriptions,
|
||||||
|
DescriptionsItem,
|
||||||
|
Drawer,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
RadioButton,
|
||||||
|
RadioGroup,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Switch,
|
||||||
|
Textarea,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
dataTypeOptions,
|
||||||
|
formTypeOptions,
|
||||||
|
readWriteTypeOptions,
|
||||||
|
timeOptions,
|
||||||
|
viewTypeOptions,
|
||||||
|
} from '#/constants/dicts';
|
||||||
|
|
||||||
|
import EnumListModal from './EnumListModal.vue';
|
||||||
|
|
||||||
|
interface PropertyData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
sort: number;
|
||||||
|
description: string;
|
||||||
|
required: boolean;
|
||||||
|
expands: {
|
||||||
|
source: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
valueParams: {
|
||||||
|
dataType: string;
|
||||||
|
enumConf: any[];
|
||||||
|
falseText?: string;
|
||||||
|
falseValue?: string;
|
||||||
|
format?: string;
|
||||||
|
formType: string;
|
||||||
|
length: any;
|
||||||
|
max?: number;
|
||||||
|
min?: number;
|
||||||
|
scale?: number;
|
||||||
|
trueText?: string;
|
||||||
|
trueValue?: string;
|
||||||
|
unit: string;
|
||||||
|
viewType: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
isEdit: boolean;
|
||||||
|
data?: PropertyData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:open', value: boolean): void;
|
||||||
|
(e: 'save', data: PropertyData): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.open,
|
||||||
|
set: (value) => emit('update:open', value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const saveLoading = ref(false);
|
||||||
|
const enumModalVisible = ref(false);
|
||||||
|
|
||||||
|
// 默认数据
|
||||||
|
const defaultPropertyData: PropertyData = {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
sort: 1,
|
||||||
|
description: '',
|
||||||
|
required: false,
|
||||||
|
expands: {
|
||||||
|
source: 'device',
|
||||||
|
type: 'R',
|
||||||
|
},
|
||||||
|
valueParams: {
|
||||||
|
dataType: 'string',
|
||||||
|
formType: 'input',
|
||||||
|
length: null,
|
||||||
|
viewType: 'input',
|
||||||
|
unit: '',
|
||||||
|
enumConf: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const formData = ref<PropertyData>({ ...defaultPropertyData });
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const formRules = {
|
||||||
|
id: [
|
||||||
|
{ required: true, message: '请输入标识符', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
pattern: /^[a-z]\w*$/i,
|
||||||
|
message: '请输入以字母开头,只包含字母、数字和下划线的标识符',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||||
|
// 'valueParams.dataType': [
|
||||||
|
// { required: true, message: '请选择数据类型', trigger: 'blur' },
|
||||||
|
// ],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 抽屉标题
|
||||||
|
const drawerTitle = computed(() => {
|
||||||
|
return props.isEdit ? '编辑属性' : '新增属性';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
Object.assign(
|
||||||
|
formData.value,
|
||||||
|
JSON.parse(JSON.stringify(defaultPropertyData)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开枚举列表编辑弹窗
|
||||||
|
const handleEditEnum = () => {
|
||||||
|
enumModalVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认枚举列表
|
||||||
|
const handleEnumConfirm = (enumList: any[]) => {
|
||||||
|
formData.value.valueParams.enumConf = enumList;
|
||||||
|
enumModalVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
saveLoading.value = true;
|
||||||
|
|
||||||
|
emit('save', { ...formData.value });
|
||||||
|
visible.value = false;
|
||||||
|
} catch {
|
||||||
|
message.error('保存失败');
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭抽屉
|
||||||
|
const handleDrawerClose = () => {
|
||||||
|
visible.value = false;
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听数据变化
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal === null) {
|
||||||
|
resetForm();
|
||||||
|
} else {
|
||||||
|
formData.value = { ...newVal };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Drawer
|
||||||
|
v-model:open="visible"
|
||||||
|
:title="drawerTitle"
|
||||||
|
width="800px"
|
||||||
|
@close="handleDrawerClose"
|
||||||
|
>
|
||||||
|
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
||||||
|
<Row :gutter="24">
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="标识符" name="id">
|
||||||
|
<Input v-model:value="formData.id" placeholder="请输入标识符" />
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="名称" name="name">
|
||||||
|
<Input v-model:value="formData.name" placeholder="请输入名称" />
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<!-- 值参数配置 -->
|
||||||
|
<Col :span="24">
|
||||||
|
<FormItem label="数据类型" name="valueParams.dataType">
|
||||||
|
<Select
|
||||||
|
v-model:value="formData.valueParams.dataType"
|
||||||
|
placeholder="请选择数据类型"
|
||||||
|
:options="dataTypeOptions"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="表单类型" name="valueParams.formType">
|
||||||
|
<Select
|
||||||
|
v-model:value="formData.valueParams.formType"
|
||||||
|
placeholder="请选择表单类型"
|
||||||
|
:options="formTypeOptions"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="展示类型" name="valueParams.viewType">
|
||||||
|
<Select
|
||||||
|
v-model:value="formData.valueParams.viewType"
|
||||||
|
placeholder="请选择展示类型"
|
||||||
|
:options="viewTypeOptions"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<!-- 开关类型配置 -->
|
||||||
|
<Col :span="24" v-if="formData.valueParams.formType === 'switch'">
|
||||||
|
<Row
|
||||||
|
:gutter="24"
|
||||||
|
style="
|
||||||
|
padding-top: 16px;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="开关打开名称" name="valueParams.trueText">
|
||||||
|
<Input
|
||||||
|
v-model:value="formData.valueParams.trueText"
|
||||||
|
default-value="是"
|
||||||
|
placeholder="请输入开关打开名称"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="开关打开值" name="valueParams.trueValue">
|
||||||
|
<Input
|
||||||
|
v-model:value="formData.valueParams.trueValue"
|
||||||
|
default-value="true"
|
||||||
|
placeholder="请输入开关打开值"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="开关关闭名称" name="valueParams.falseText">
|
||||||
|
<Input
|
||||||
|
v-model:value="formData.valueParams.falseText"
|
||||||
|
default-value="否"
|
||||||
|
placeholder="请输入开关关闭名称"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="开关关闭值" name="valueParams.falseValue">
|
||||||
|
<Input
|
||||||
|
v-model:value="formData.valueParams.falseValue"
|
||||||
|
default-value="false"
|
||||||
|
placeholder="请输入开关关闭值"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<!-- 选择器类型配置 -->
|
||||||
|
<Col :span="24" v-if="formData.valueParams.formType === 'select'">
|
||||||
|
<FormItem label="枚举列表" name="valueParams.enumConf">
|
||||||
|
<div class="enum-preview">
|
||||||
|
<a-button type="primary" @click="handleEditEnum">
|
||||||
|
编辑枚举列表
|
||||||
|
</a-button>
|
||||||
|
<Descriptions
|
||||||
|
v-if="
|
||||||
|
formData.valueParams.enumConf &&
|
||||||
|
formData.valueParams.enumConf.length > 0
|
||||||
|
"
|
||||||
|
:column="4"
|
||||||
|
bordered
|
||||||
|
title=" "
|
||||||
|
>
|
||||||
|
<DescriptionsItem
|
||||||
|
:label="`${item.text} :`"
|
||||||
|
v-for="item in formData.valueParams.enumConf"
|
||||||
|
:key="item.value"
|
||||||
|
>
|
||||||
|
{{ item.value }}
|
||||||
|
</DescriptionsItem>
|
||||||
|
</Descriptions>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<!-- 数字类型配置 -->
|
||||||
|
<Col
|
||||||
|
:span="12"
|
||||||
|
v-if="
|
||||||
|
formData.valueParams.formType === 'number' ||
|
||||||
|
formData.valueParams.formType === 'progress'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<FormItem label="最小值" name="valueParams.min">
|
||||||
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.valueParams.min"
|
||||||
|
placeholder="请输入最小值"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
:span="12"
|
||||||
|
v-if="
|
||||||
|
formData.valueParams.formType === 'number' ||
|
||||||
|
formData.valueParams.formType === 'progress'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<FormItem label="最大值" name="valueParams.max">
|
||||||
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.valueParams.max"
|
||||||
|
placeholder="请输入最大值"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12" v-if="formData.valueParams.formType === 'input'">
|
||||||
|
<FormItem label="长度" name="valueParams.length">
|
||||||
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.valueParams.length"
|
||||||
|
placeholder="请输入长度"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
:span="12"
|
||||||
|
v-if="
|
||||||
|
formData.valueParams.formType === 'number' ||
|
||||||
|
formData.valueParams.formType === 'progress'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<FormItem label="小数位" name="valueParams.scale">
|
||||||
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.valueParams.scale"
|
||||||
|
placeholder="请输入小数位"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="单位" name="valueParams.unit">
|
||||||
|
<Input
|
||||||
|
v-model:value="formData.valueParams.unit"
|
||||||
|
placeholder="请输入单位"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12" v-if="formData.valueParams.formType === 'time'">
|
||||||
|
<FormItem label="时间格式" name="valueParams.format">
|
||||||
|
<Select
|
||||||
|
v-model:value="formData.valueParams.format"
|
||||||
|
placeholder="请选择时间格式"
|
||||||
|
:options="timeOptions"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<!-- 属性特有字段 -->
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="是否必填" name="required">
|
||||||
|
<Switch v-model:checked="formData.required" />
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="读写类型" name="type">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="formData.expands.type"
|
||||||
|
button-style="solid"
|
||||||
|
>
|
||||||
|
<RadioButton
|
||||||
|
:value="item.value"
|
||||||
|
v-for="item in readWriteTypeOptions"
|
||||||
|
:key="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</RadioButton>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="排序" name="sort">
|
||||||
|
<InputNumber
|
||||||
|
style="width: 100%"
|
||||||
|
v-model:value="formData.sort"
|
||||||
|
placeholder="请输入排序"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :span="24">
|
||||||
|
<FormItem label="描述" name="description">
|
||||||
|
<Textarea
|
||||||
|
v-model:value="formData.description"
|
||||||
|
placeholder="请输入描述"
|
||||||
|
:rows="3"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Space>
|
||||||
|
<Button @click="handleDrawerClose">取消</Button>
|
||||||
|
<Button type="primary" @click="handleSave" :loading="saveLoading">
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 枚举列表编辑弹窗 -->
|
||||||
|
<EnumListModal
|
||||||
|
v-model:open="enumModalVisible"
|
||||||
|
:enum-conf="formData.valueParams?.enumConf"
|
||||||
|
@confirm="handleEnumConfirm"
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.enum-preview {
|
||||||
|
.enum-items-preview {
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-items {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in New Issue