!364 feat(@vben/web-antdv-next): sync Antdv Next compatibility fixes and BPM attachment support

Merge pull request !364 from XuZhiqiang/feat-antdv-next
pull/366/head^2
芋道源码 2026-06-18 23:17:31 +00:00 committed by Gitee
commit e26f91d9fa
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
113 changed files with 1585 additions and 1835 deletions

View File

@ -0,0 +1,86 @@
import type { Component } from 'vue';
import type { Recordable } from '@vben/types';
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
const RawDatePicker = defineAsyncComponent(
() => import('antdv-next/dist/date-picker/index'),
);
const RawRangePicker = defineAsyncComponent(() =>
import('antdv-next/dist/date-picker/index').then(
(res) => res.DateRangePicker,
),
);
const TIMESTAMP_VALUE_FORMATS = new Set(['x', 'X']);
function isTimestampValueFormat(valueFormat: unknown) {
return (
typeof valueFormat === 'string' && TIMESTAMP_VALUE_FORMATS.has(valueFormat)
);
}
function normalizeTimestampPickerValue(value: any, valueFormat: unknown): any {
if (!isTimestampValueFormat(valueFormat)) {
return value;
}
if (Array.isArray(value)) {
return value.map((item) => normalizeTimestampPickerValue(item, valueFormat));
}
return typeof value === 'number' ? String(value) : value;
}
function withTimestampValueFormat(
component: Component,
name: 'DatePicker' | 'RangePicker',
) {
return defineComponent({
name,
inheritAttrs: false,
setup(_, { attrs, expose, slots }) {
const innerRef = ref();
expose(
new Proxy(
{},
{
get: (_target, key) => innerRef.value?.[key],
has: (_target, key) => key in (innerRef.value || {}),
},
),
);
return () => {
const pickerAttrs: Recordable<any> = { ...attrs };
if (
'value-format' in pickerAttrs &&
!Reflect.has(pickerAttrs, 'valueFormat')
) {
pickerAttrs.valueFormat = pickerAttrs['value-format'];
}
const valueFormat = pickerAttrs.valueFormat;
for (const key of [
'value',
'defaultValue',
'pickerValue',
'defaultPickerValue',
]) {
if (Reflect.has(pickerAttrs, key)) {
pickerAttrs[key] = normalizeTimestampPickerValue(
pickerAttrs[key],
valueFormat,
);
}
}
return h(component, { ...pickerAttrs, ref: innerRef }, slots);
};
},
});
}
const DatePicker = withTimestampValueFormat(RawDatePicker, 'DatePicker');
const RangePicker = withTimestampValueFormat(RawRangePicker, 'RangePicker');
export { DatePicker, RangePicker };

View File

@ -75,6 +75,9 @@ import { message, Modal, notification } from 'antdv-next';
import { uploadFile as uploadFileApi } from '#/api/infra/file';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { FileUpload, ImageUpload } from '#/components/upload';
import { DatePicker, RangePicker } from './date-picker';
type AdapterUploadProps = UploadProps & {
aspectRatio?: string;
crop?: boolean;
@ -97,9 +100,6 @@ const Checkbox = defineAsyncComponent(
const CheckboxGroup = defineAsyncComponent(() =>
import('antdv-next/dist/checkbox/index').then((res) => res.CheckboxGroup),
);
const DatePicker = defineAsyncComponent(
() => import('antdv-next/dist/date-picker/index'),
);
const Divider = defineAsyncComponent(
() => import('antdv-next/dist/divider/index'),
);
@ -117,11 +117,6 @@ const Radio = defineAsyncComponent(() => import('antdv-next/dist/radio/index'));
const RadioGroup = defineAsyncComponent(() =>
import('antdv-next/dist/radio/index').then((res) => res.RadioGroup),
);
const RangePicker = defineAsyncComponent(() =>
import('antdv-next/dist/date-picker/index').then(
(res) => res.DateRangePicker,
),
);
const Rate = defineAsyncComponent(() => import('antdv-next/dist/rate/index'));
const Select = defineAsyncComponent(
() => import('antdv-next/dist/select/index'),
@ -794,4 +789,4 @@ async function initComponentAdapter() {
});
}
export { initComponentAdapter };
export { DatePicker, initComponentAdapter, RangePicker };

View File

@ -83,6 +83,7 @@ export namespace BpmProcessInstanceApi {
reason: string;
signPicUrl: string;
status: number;
attachments?: string[];
}
/** 抄送流程实例 */

View File

@ -1,4 +1,4 @@
import type { Rule } from 'antdv-next/es/form';
import type { FormItemProps } from 'antdv-next';
import type { PageParam, PageResult } from '@vben/request';
@ -6,6 +6,8 @@ import { isEmpty } from '@vben/utils';
import { requestClient } from '#/api/request';
type FormItemRule = NonNullable<FormItemProps['rules']>[number];
export namespace ThingModelApi {
/** IoT 物模型数据 */
export interface ThingModel {
@ -95,46 +97,42 @@ export namespace ThingModelApi {
/** 生成「必填 + 数字」类校验器:拼到 size / length / 枚举值上 */
function buildRequiredNumberValidator(label: string) {
return (_rule: any, value: any, callback: any) => {
return (_rule: any, value: any) => {
if (isEmpty(value)) {
callback(new Error(`${label}不能为空`));
return;
return Promise.reject(new Error(`${label}不能为空`));
}
if (Number.isNaN(Number(value))) {
callback(new Error(`${label}必须是数字`));
return;
return Promise.reject(new Error(`${label}必须是数字`));
}
callback();
return Promise.resolve();
};
}
/** 生成「标识符样式」名称校验器:开头需为中文 / 英文 / 数字,整体仅允许中文、英文、数字、下划线、短划线,长度 ≤ 20 */
export function buildIdentifierLikeNameValidator(label: string) {
return (_rule: any, value: string, callback: any) => {
return (_rule: any, value: string) => {
if (isEmpty(value)) {
callback(new Error(`${label}不能为空`));
return;
return Promise.reject(new Error(`${label}不能为空`));
}
if (!/^[一-龥A-Za-z0-9]/.test(value)) {
callback(new Error(`${label}必须以中文、英文字母或数字开头`));
return;
return Promise.reject(
new Error(`${label}必须以中文、英文字母或数字开头`),
);
}
if (!/^[一-龥A-Za-z0-9][\w一-龥-]*$/.test(value)) {
callback(
return Promise.reject(
new Error(`${label}只能包含中文、英文字母、数字、下划线和短划线`),
);
return;
}
if (value.length > 20) {
callback(new Error(`${label}长度不能超过 20 个字符`));
return;
return Promise.reject(new Error(`${label}长度不能超过 20 个字符`));
}
callback();
return Promise.resolve();
};
}
/** IoT 物模型表单校验规则 */
export const ThingModelFormRules: Record<string, Rule[]> = {
export const ThingModelFormRules: Record<string, FormItemRule[]> = {
name: [
{ required: true, message: '功能名称不能为空', trigger: 'blur' },
{
@ -153,7 +151,7 @@ export const ThingModelFormRules: Record<string, Rule[]> = {
trigger: 'blur',
},
{
validator: (_rule: any, value: string, callback: any) => {
validator: (_rule: any, value: string) => {
const reservedKeywords = [
'set',
'get',
@ -164,18 +162,16 @@ export const ThingModelFormRules: Record<string, Rule[]> = {
'value',
];
if (reservedKeywords.includes(value)) {
callback(
return Promise.reject(
new Error(
'set, get, post, property, event, time, value 是系统保留字段,不能用于标识符定义',
),
);
return;
}
if (/^\d+$/.test(value)) {
callback(new Error('标识符不能是纯数字'));
return;
return Promise.reject(new Error('标识符不能是纯数字'));
}
callback();
return Promise.resolve();
},
trigger: 'blur',
},

View File

@ -8,6 +8,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue';
import {
Button,
Form,
FormItem,
Input,
InputNumber,
message,
@ -417,22 +418,21 @@ function inputChange() {
@input="inputChange"
>
<template #addonAfter>
<Select v-model:value="select" placeholder="生成器" class="w-36">
<SelectOption value="0 * * * * ?">每分钟</SelectOption>
<SelectOption value="0 0 * * * ?">每小时</SelectOption>
<SelectOption value="0 0 0 * * ?">每天零点</SelectOption>
<SelectOption value="0 0 0 1 * ?">每月一号零点</SelectOption>
<SelectOption value="0 0 0 L * ?">每月最后一天零点</SelectOption>
<SelectOption value="0 0 0 ? * 1">每周星期日零点</SelectOption>
<SelectOption
v-for="(item, index) in shortcuts"
:key="index"
:value="item.value"
>
{{ item.text }}
</SelectOption>
<SelectOption value="custom">自定义</SelectOption>
</Select>
<Select
v-model:value="select"
placeholder="生成器"
class="w-36"
:options="[
{ label: '每分钟', value: '0 * * * * ?' },
{ label: '每小时', value: '0 0 * * * ?' },
{ label: '每天零点', value: '0 0 0 * * ?' },
{ label: '每月一号零点', value: '0 0 0 1 * ?' },
{ label: '每月最后一天零点', value: '0 0 0 L * ?' },
{ label: '每周星期日零点', value: '0 0 0 ? * 1' },
...shortcuts.map((item) => ({ label: item.text, value: item.value })),
{ label: '自定义', value: 'custom' },
]"
/>
</template>
</Input>
@ -496,14 +496,10 @@ function inputChange() {
v-model:value="cronValue.second.appoint"
mode="multiple"
style="width: 100%"
>
<SelectOption
v-for="(item, index) in data.second"
:key="index"
:label="item"
:value="item"
/>
</Select>
:options="
data.second.map((item) => ({ label: item, value: item }))
"
/>
</FormItem>
</Form>
</TabPane>
@ -560,14 +556,10 @@ function inputChange() {
v-model:value="cronValue.minute.appoint"
mode="multiple"
style="width: 100%"
>
<SelectOption
v-for="(item, index) in data.minute"
:key="index"
:label="item"
:value="item"
/>
</Select>
:options="
data.minute.map((item) => ({ label: item, value: item }))
"
/>
</FormItem>
</Form>
</TabPane>
@ -624,14 +616,10 @@ function inputChange() {
v-model:value="cronValue.hour.appoint"
mode="multiple"
style="width: 100%"
>
<SelectOption
v-for="(item, index) in data.hour"
:key="index"
:label="item"
:value="item"
/>
</Select>
:options="
data.hour.map((item) => ({ label: item, value: item }))
"
/>
</FormItem>
</Form>
</TabPane>
@ -690,14 +678,10 @@ function inputChange() {
v-model:value="cronValue.day.appoint"
mode="multiple"
style="width: 100%"
>
<SelectOption
v-for="(item, index) in data.day"
:key="index"
:label="item"
:value="item"
/>
</Select>
:options="
data.day.map((item) => ({ label: item, value: item }))
"
/>
</FormItem>
</Form>
</TabPane>
@ -754,14 +738,10 @@ function inputChange() {
v-model:value="cronValue.month.appoint"
mode="multiple"
style="width: 100%"
>
<SelectOption
v-for="(item, index) in data.month"
:key="index"
:label="item"
:value="item"
/>
</Select>
:options="
data.month.map((item) => ({ label: item, value: item }))
"
/>
</FormItem>
</Form>
</TabPane>
@ -785,23 +765,15 @@ function inputChange() {
</RadioGroup>
</FormItem>
<FormItem v-if="cronValue.week.type === '1'" label="范围">
<Select v-model:value="cronValue.week.range.start">
<SelectOption
v-for="(item, index) in data.week"
:key="index"
:label="item.label"
:value="item.value"
/>
</Select>
<Select
v-model:value="cronValue.week.range.start"
:options="data.week"
/>
<span style="padding: 0 15px">-</span>
<Select v-model:value="cronValue.week.range.end">
<SelectOption
v-for="(item, index) in data.week"
:key="index"
:label="item.label"
:value="item.value"
/>
</Select>
<Select
v-model:value="cronValue.week.range.end"
:options="data.week"
/>
</FormItem>
<FormItem v-if="cronValue.week.type === '2'" label="间隔">
@ -812,14 +784,10 @@ function inputChange() {
controls-position="right"
/>
周的星期
<Select v-model:value="cronValue.week.loop.end">
<SelectOption
v-for="(item, index) in data.week"
:key="index"
:label="item.label"
:value="item.value"
/>
</Select>
<Select
v-model:value="cronValue.week.loop.end"
:options="data.week"
/>
执行一次
</FormItem>
<FormItem v-if="cronValue.week.type === '3'" label="指定">
@ -827,24 +795,14 @@ function inputChange() {
v-model:value="cronValue.week.appoint"
mode="multiple"
style="width: 100%"
>
<SelectOption
v-for="(item, index) in data.week"
:key="index"
:label="item.label"
:value="item.value"
/>
</Select>
:options="data.week"
/>
</FormItem>
<FormItem v-if="cronValue.week.type === '4'" label="最后一周">
<Select v-model:value="cronValue.week.last">
<SelectOption
v-for="(item, index) in data.week"
:key="index"
:label="item.label"
:value="item.value"
/>
</Select>
<Select
v-model:value="cronValue.week.last"
:options="data.week"
/>
</FormItem>
</Form>
</TabPane>
@ -895,14 +853,10 @@ function inputChange() {
v-model:value="cronValue.year.appoint"
mode="multiple"
style="width: 100%"
>
<SelectOption
v-for="(item, index) in data.year"
:key="index"
:label="item"
:value="item"
/>
</Select>
:options="
data.year.map((item) => ({ label: item, value: item }))
"
/>
</FormItem>
</Form>
</TabPane>

View File

@ -9,7 +9,7 @@ import { computed, defineComponent, ref, unref, useAttrs } from 'vue';
import { get, getNestedValue, isFunction } from '@vben/utils';
import { Card, Descriptions } from 'antdv-next';
import { Card, Descriptions, DescriptionsItem } from 'antdv-next';
const props = {
bordered: { default: true, type: Boolean },

View File

@ -6,14 +6,7 @@ import { computed, useAttrs } from 'vue';
import { getDictOptions } from '@vben/hooks';
import {
Checkbox,
CheckboxGroup,
Radio,
RadioGroup,
Select,
SelectOption,
} from 'antdv-next';
import { Checkbox, CheckboxGroup, Radio, RadioGroup, Select } from 'antdv-next';
defineOptions({ name: 'DictSelect' });
@ -41,18 +34,22 @@ const getDictOption = computed(() => {
}
}
});
const selectOptions = computed(() =>
getDictOption.value.map((dict) => ({
label: dict.label,
value: dict.value as any,
})),
);
</script>
<template>
<Select v-if="selectType === 'select'" class="w-full" v-bind="attrs">
<SelectOption
v-for="(dict, index) in getDictOption"
:key="index"
:value="dict.value"
>
{{ dict.label }}
</SelectOption>
</Select>
<Select
v-if="selectType === 'select'"
class="w-full"
v-bind="attrs"
:options="selectOptions"
/>
<RadioGroup v-if="selectType === 'radio'" class="w-full" v-bind="attrs">
<Radio
v-for="(dict, index) in getDictOption"

View File

@ -5,14 +5,7 @@ import { defineComponent, onMounted, ref, useAttrs } from 'vue';
import { useUserStore } from '@vben/stores';
import { isEmpty } from '@vben/utils';
import {
Checkbox,
CheckboxGroup,
Radio,
RadioGroup,
Select,
SelectOption,
} from 'antdv-next';
import { Checkbox, CheckboxGroup, Radio, RadioGroup, Select } from 'antdv-next';
import { requestClient } from '#/api/request';
@ -256,18 +249,11 @@ export function useApiSelect(option: ApiSelectProps) {
onUpdate:value={onUpdateModelValue as any}
value={modelValue as any}
{...restAttrs}
options={options.value}
// TODO @xingyu remote 对等实现, 还是说没作用
// remote={props.remote}
{...(props.remote && { remoteMethod })}
>
{options.value.map(
(item: { label: any; value: any }, index: any) => (
<SelectOption key={index} value={item.value}>
{item.label}
</SelectOption>
),
)}
</Select>
/>
);
}
return (
@ -277,18 +263,11 @@ export function useApiSelect(option: ApiSelectProps) {
onUpdate:value={onUpdateModelValue as any}
value={modelValue as any}
{...restAttrs}
options={options.value}
// TODO: @xingyu remote 对等实现, 还是说没作用
// remote={props.remote}
{...(props.remote && { remoteMethod })}
>
{options.value.map(
(item: { label: any; value: any }, index: any) => (
<SelectOption key={index} value={item.value}>
{item.label}
</SelectOption>
),
)}
</Select>
/>
);
};
const buildCheckbox = () => {

View File

@ -4,7 +4,7 @@ import { nextTick, reactive, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Form, Input, Select, Space } from 'antdv-next';
import { Button, Form, FormItem, Input, Select, Space } from 'antdv-next';
import { loadBaiduMapSdk } from './utils';

View File

@ -91,7 +91,6 @@ onMounted(() => {
<DateRangePicker
v-model:value="times"
:format="rangePickerProps.format"
:value-format="rangePickerProps.valueFormat"
:placeholder="rangePickerProps.placeholder"
:presets="rangePickerProps.presets"
class="!w-full !max-w-96"

View File

@ -188,8 +188,8 @@ watch(
<Dropdown v-if="getDropdownList.length > 0" :trigger="['hover']">
<slot name="more">
<Button type="link">
{{ $t('page.action.more') }}
<template #icon>
{{ $t('page.action.more') }}
<IconifyIcon icon="lucide:ellipsis-vertical" />
</template>
</Button>

View File

@ -1,6 +1,5 @@
<script lang="ts" setup>
import type { UploadFile, UploadProps } from 'antdv-next';
import type { UploadRequestOption } from 'antdv-next/lib/vc-upload/interface';
import type { FileUploadProps } from './typing';
@ -17,6 +16,8 @@ import { Button, message, Upload } from 'antdv-next';
import { UploadResultStatus } from './typing';
import { useUpload, useUploadType } from './use-upload';
type UploadRequestOption = any;
defineOptions({ name: 'FileUpload', inheritAttrs: false });
const props = withDefaults(defineProps<FileUploadProps>(), {
@ -34,6 +35,7 @@ const props = withDefaults(defineProps<FileUploadProps>(), {
resultField: '',
returnText: false,
showDescription: false,
showDownloadIcon: true,
});
const emit = defineEmits([
'change',
@ -295,7 +297,7 @@ function getValue() {
:show-upload-list="{
showPreviewIcon: true,
showRemoveIcon: true,
showDownloadIcon: true,
showDownloadIcon,
}"
@remove="handleRemove"
@preview="handlePreview"

View File

@ -29,5 +29,6 @@ export interface FileUploadProps {
resultField?: string; // support xxx.xxx.xx
returnText?: boolean; // 是否返回文件文本内容
showDescription?: boolean; // 是否显示下面的描述
showDownloadIcon?: boolean; // 是否显示下载按钮
value?: string | string[];
}

View File

@ -1,7 +1,16 @@
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { $t } from '#/locales';
type DateRangeTuple = [Dayjs, Dayjs];
type StringRangeTuple = [string, string];
function dateRange(start: Dayjs, end: Dayjs): DateRangeTuple {
return [start, end];
}
/** 时间段选择器拓展 */
export function getRangePickerDefaultProps() {
return {
@ -13,55 +22,55 @@ export function getRangePickerDefaultProps() {
placeholder: [
$t('utils.rangePicker.beginTime'),
$t('utils.rangePicker.endTime'),
],
] as StringRangeTuple,
// 快捷时间范围
presets: [
{
label: $t('utils.rangePicker.today'),
value: [dayjs().startOf('day'), dayjs().endOf('day')],
value: dateRange(dayjs().startOf('day'), dayjs().endOf('day')),
},
{
label: $t('utils.rangePicker.last7Days'),
value: [
value: dateRange(
dayjs().subtract(7, 'day').startOf('day'),
dayjs().endOf('day'),
],
),
},
{
label: $t('utils.rangePicker.last30Days'),
value: [
value: dateRange(
dayjs().subtract(30, 'day').startOf('day'),
dayjs().endOf('day'),
],
),
},
{
label: $t('utils.rangePicker.yesterday'),
value: [
value: dateRange(
dayjs().subtract(1, 'day').startOf('day'),
dayjs().subtract(1, 'day').endOf('day'),
],
),
},
{
label: $t('utils.rangePicker.thisWeek'),
value: [dayjs().startOf('week'), dayjs().endOf('day')],
value: dateRange(dayjs().startOf('week'), dayjs().endOf('day')),
},
{
label: $t('utils.rangePicker.thisMonth'),
value: [dayjs().startOf('month'), dayjs().endOf('day')],
value: dateRange(dayjs().startOf('month'), dayjs().endOf('day')),
},
{
label: $t('utils.rangePicker.lastWeek'),
value: [
value: dateRange(
dayjs().subtract(1, 'week').startOf('day'),
dayjs().endOf('day'),
],
),
},
],
showTime: {
defaultValue: [
defaultValue: dateRange(
dayjs('00:00:00', 'HH:mm:ss'),
dayjs('23:59:59', 'HH:mm:ss'),
],
),
format: 'HH:mm:ss',
},
};

View File

@ -147,15 +147,9 @@ defineExpose({ settingValues });
size="large"
class="!w-80"
@change="handlerPlatformChange"
>
<SelectOption
v-for="item in OtherPlatformEnum"
:key="item.key"
:value="item.key"
>
{{ item.name }}
</SelectOption>
</Select>
:options="OtherPlatformEnum"
:field-names="{ label: 'name', value: 'key' }"
/>
</Space>
</div>
@ -169,15 +163,9 @@ defineExpose({ settingValues });
placeholder="Select"
size="large"
class="!w-80"
>
<SelectOption
v-for="item in platformModels"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
:options="platformModels"
:field-names="{ label: 'name', value: 'id' }"
/>
</Space>
</div>

View File

@ -214,15 +214,8 @@ defineExpose({ settingValues });
class="!w-80"
allow-clear
placeholder="请选择版本"
>
<SelectOption
v-for="item in versionList"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</SelectOption>
</Select>
:options="versionList"
/>
</Space>
</div>

View File

@ -168,15 +168,9 @@ defineExpose({ settingValues });
placeholder="Select"
size="large"
class="!w-80"
>
<SelectOption
v-for="item in StableDiffusionSamplers"
:key="item.key"
:value="item.key"
>
{{ item.name }}
</SelectOption>
</Select>
:options="StableDiffusionSamplers"
:field-names="{ label: 'name', value: 'key' }"
/>
</Space>
</div>
@ -189,15 +183,9 @@ defineExpose({ settingValues });
placeholder="Select"
size="large"
class="!w-80"
>
<SelectOption
v-for="item in StableDiffusionClipGuidancePresets"
:key="item.key"
:value="item.key"
>
{{ item.name }}
</SelectOption>
</Select>
:options="StableDiffusionClipGuidancePresets"
:field-names="{ label: 'name', value: 'key' }"
/>
</Space>
</div>
@ -210,16 +198,9 @@ defineExpose({ settingValues });
placeholder="Select"
size="large"
class="!w-80"
>
<SelectOption
v-for="item in StableDiffusionStylePresets"
:key="item.key"
:label="item.name"
:value="item.key"
>
{{ item.name }}
</SelectOption>
</Select>
:options="StableDiffusionStylePresets"
:field-names="{ label: 'name', value: 'key' }"
/>
</Space>
</div>

View File

@ -10,6 +10,7 @@ import {
Dropdown,
Empty,
Form,
FormItem,
InputNumber,
Menu,
MenuItem,

View File

@ -10,7 +10,7 @@ import { computed, getCurrentInstance, inject, onMounted, ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { generateAcceptedFileTypes } from '@vben/utils';
import { Button, Form, message, UploadDragger } from 'antdv-next';
import { Button, Form, FormItem, message, UploadDragger } from 'antdv-next';
import { useUpload } from '#/components/upload/use-upload';
type UploadRequestOption = any;

View File

@ -47,9 +47,8 @@ defineExpose({
v-model:value="formData.version"
class="w-full"
placeholder="请选择"
>
<SelectOption
v-for="item in [
:options="[
...[
{
value: '3',
label: 'V3',
@ -58,13 +57,9 @@ defineExpose({
value: '2',
label: 'V2',
},
]"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</SelectOption>
</Select>
].map((item) => ({ label: item.label, value: item.value })),
]"
/>
</Title>
</div>
</template>

View File

@ -3,7 +3,7 @@ import type { Nullable, Recordable } from '@vben/types';
import { ref, unref } from 'vue';
import { Button, Card } from 'antdv-next';
import { Button, Card, RadioButton, RadioGroup } from 'antdv-next';
import desc from './desc.vue';
import lyric from './lyric.vue';

View File

@ -80,9 +80,8 @@ defineExpose({
v-model:value="formData.version"
class="w-full"
placeholder="请选择"
>
<SelectOption
v-for="item in [
:options="[
...[
{
value: '3',
label: 'V3',
@ -91,13 +90,9 @@ defineExpose({
value: '2',
label: 'V2',
},
]"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</SelectOption>
</Select>
].map((item) => ({ label: item.label, value: item.value })),
]"
/>
</Title>
</div>
</template>

View File

@ -4,7 +4,7 @@ import { ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Form, Input, Select, TextArea } from 'antdv-next';
import { Form, FormItem, Input, Select, TextArea } from 'antdv-next';
type Rule = any;
const modelData = defineModel<any>(); //
@ -53,15 +53,13 @@ defineExpose({ validate });
v-model:value="modelData.status"
allow-clear
placeholder="请选择状态"
>
<SelectOption
v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS, 'number')"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</SelectOption>
</Select>
:options="
getDictOptions(DICT_TYPE.COMMON_STATUS, 'number').map((dict) => ({
label: dict.label,
value: dict.value as any,
}))
"
/>
</FormItem>
<FormItem label="流程描述" name="description" class="mb-5">
<TextArea v-model:value="modelData.description" allow-clear />

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { Ref } from 'vue';
import { inject, ref } from 'vue';
import { computed, inject, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
@ -20,6 +20,15 @@ const tinyflowRef = ref<InstanceType<typeof Tinyflow> | null>(null);
const workflowData = inject('workflowData') as Ref;
const params4Test = ref<any[]>([]);
const paramsOfStartNode = ref<any>({});
const startNodeParamOptions = computed(() =>
Object.entries(paramsOfStartNode.value).map(
([key, value]: [string, any]) => ({
label: value?.description || key,
value: key,
disabled: !!value?.disabled,
}),
),
);
const testResult = ref(null);
const loading = ref(false);
const error = ref(null);
@ -231,16 +240,12 @@ defineExpose({ validate });
v-for="(param, index) in params4Test"
:key="index"
>
<Select class="w-48" v-model="param.key" placeholder="参数名">
<SelectOption
v-for="(value, key) in paramsOfStartNode"
:key="key"
:value="key"
:disabled="!!value?.disabled"
>
{{ value?.description || key }}
</SelectOption>
</Select>
<Select
v-model="param.key"
:options="startNodeParamOptions"
class="w-48"
placeholder="参数名"
/>
<Input
class="mx-2 w-48"
v-model:value="param.value"

View File

@ -4,7 +4,7 @@ import { nextTick, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import { Collapse } from 'antdv-next';
import { Collapse, CollapsePanel } from 'antdv-next';
import ElementCustomConfig from '#/views/bpm/components/bpmn-process-designer/package/penal/custom-config/ElementCustomConfig.vue';
import ElementForm from '#/views/bpm/components/bpmn-process-designer/package/penal/form/ElementForm.vue';
@ -57,8 +57,6 @@ const props = defineProps({
}, //
});
const CollapsePanel = CollapsePanel;
const activeTab = ref('base');
const elementId = ref('');
const elementType = ref<any>('');

View File

@ -10,7 +10,6 @@ import {
RadioGroup,
Row,
Select,
SelectOption,
Switch,
TypographyText,
} from 'antdv-next';
@ -277,18 +276,10 @@ watch(
<Col>
<Select
v-model:value="timeUnit"
:options="TIME_UNIT_TYPES"
class="mr-2 !w-24"
@change="onTimeUnitChange"
>
<SelectOption
v-for="item in TIME_UNIT_TYPES"
:key="item.value"
:label="item.label"
:value="item.value"
>
{{ item.label }}
</SelectOption>
</Select>
/>
<TypographyText class="mr-2 mt-2 inline-flex text-sm">
未处理
</TypographyText>

View File

@ -20,11 +20,11 @@ import { IconifyIcon } from '@vben/icons';
import {
Button,
Divider,
FormItem,
Input,
Radio,
RadioGroup,
Select,
SelectOption,
Switch,
} from 'antdv-next';
@ -487,15 +487,9 @@ onMounted(async () => {
style="width: 100%"
@change="updateReturnNodeId"
placeholder="请选择驳回节点"
>
<SelectOption
v-for="item in returnTaskList"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
:options="returnTaskList"
:field-names="{ label: 'name', value: 'id' }"
/>
</FormItem>
<Divider orientation="left">审批人为空时</Divider>
@ -524,15 +518,9 @@ onMounted(async () => {
mode="multiple"
style="width: 100%"
@change="updateAssignEmptyUserIds"
>
<SelectOption
v-for="item in userOptions"
:key="item.id"
:value="item.id"
>
{{ item.nickname }}
</SelectOption>
</Select>
:options="userOptions"
:field-names="{ label: 'nickname', value: 'id' }"
/>
</FormItem>
<Divider orientation="left">审批人与提交人为同一人时</Divider>

View File

@ -157,21 +157,28 @@ watch(
:wrapper-col="{ span: 18 }"
>
<FormItem label="流转类型">
<Select v-model:value="flowConditionForm.type" @change="updateFlowType">
<SelectOption value="normal">普通流转路径</SelectOption>
<SelectOption value="default">默认流转路径</SelectOption>
<SelectOption value="condition">条件流转路径</SelectOption>
</Select>
<Select
v-model:value="flowConditionForm.type"
@change="updateFlowType"
:options="[
{ label: '普通流转路径', value: 'normal' },
{ label: '默认流转路径', value: 'default' },
{ label: '条件流转路径', value: 'condition' },
]"
/>
</FormItem>
<FormItem
label="条件格式"
v-if="flowConditionForm.type === 'condition'"
key="condition"
>
<Select v-model:value="flowConditionForm.conditionType">
<SelectOption value="expression">表达式</SelectOption>
<SelectOption value="script">脚本</SelectOption>
</Select>
<Select
v-model:value="flowConditionForm.conditionType"
:options="[
{ label: '表达式', value: 'expression' },
{ label: '脚本', value: 'script' },
]"
/>
</FormItem>
<FormItem
label="表达式"
@ -202,10 +209,13 @@ watch(
/>
</FormItem>
<FormItem label="脚本类型" key="scriptType">
<Select v-model:value="flowConditionForm.scriptType">
<SelectOption value="inlineScript">内联脚本</SelectOption>
<SelectOption value="externalScript">外部脚本</SelectOption>
</Select>
<Select
v-model:value="flowConditionForm.scriptType"
:options="[
{ label: '内联脚本', value: 'inlineScript' },
{ label: '外部脚本', value: 'externalScript' },
]"
/>
</FormItem>
<FormItem
label="脚本"

View File

@ -319,14 +319,13 @@ watch(
<FormItem label="业务标识">
<Select
v-model:value="businessKey"
:options="[
...fieldList.map((i) => ({ label: i.label, value: i.id })),
{ label: '无', value: '' },
]"
@change="_updateElementBusinessKey"
allow-clear
>
<SelectOption v-for="i in fieldList" :key="i.id" :value="i.id">
{{ i.label }}
</SelectOption>
<SelectOption value=""></SelectOption>
</Select>
/>
</FormItem>
</Form>
@ -388,7 +387,6 @@ watch(
<!-- allowClear-->
<!-- @change="changeFieldTypeType"-->
<!-- >-->
<!-- <SelectOption v-for="(value, key) of fieldType" :key="key" :value="key">{{ value }}</SelectOption>-->
<!-- </Select>-->
<!-- </FormItem>-->
<!-- <FormItem label="类型名称" v-if="formFieldForm.typeType === 'custom'">-->

View File

@ -5,15 +5,7 @@ import { confirm, useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import {
Button,
Divider,
Form,
FormItem,
Input,
Select,
SelectOption,
} from 'antdv-next';
import { Button, Divider, Form, FormItem, Input, Select } from 'antdv-next';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ProcessListenerSelectModal } from '#/views/bpm/processListener/components';
@ -392,10 +384,13 @@ watch(
},
]"
>
<Select v-model:value="listenerForm.event">
<SelectOption value="start">start</SelectOption>
<SelectOption value="end">end</SelectOption>
</Select>
<Select
v-model:value="listenerForm.event"
:options="[
{ label: 'start', value: 'start' },
{ label: 'end', value: 'end' },
]"
/>
</FormItem>
<FormItem
label="监听器类型"
@ -408,15 +403,15 @@ watch(
},
]"
>
<Select v-model:value="listenerForm.listenerType">
<SelectOption
v-for="i in Object.keys(listenerTypeObject)"
:key="i"
:value="i"
>
{{ listenerTypeObject[i as keyof typeof listenerType] }}
</SelectOption>
</Select>
<Select
v-model:value="listenerForm.listenerType"
:options="
Object.keys(listenerTypeObject).map((i) => ({
label: listenerTypeObject[i as keyof typeof listenerType],
value: i,
}))
"
/>
</FormItem>
<FormItem
v-if="listenerForm.listenerType === 'classListener'"
@ -490,10 +485,13 @@ watch(
},
]"
>
<Select v-model:value="listenerForm.scriptType">
<SelectOption value="inlineScript">内联脚本</SelectOption>
<SelectOption value="externalScript">外部脚本</SelectOption>
</Select>
<Select
v-model:value="listenerForm.scriptType"
:options="[
{ label: '内联脚本', value: 'inlineScript' },
{ label: '外部脚本', value: 'externalScript' },
]"
/>
</FormItem>
<FormItem
v-if="listenerForm.scriptType === 'inlineScript'"

View File

@ -3,7 +3,7 @@ import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Form, FormItem, Input, Select, SelectOption } from 'antdv-next';
import { Form, FormItem, Input, Select } from 'antdv-next';
import { fieldType } from './utilSelf';
@ -72,15 +72,15 @@ const [Modal, modalApi] = useVbenModal({
},
]"
>
<Select v-model:value="form.fieldType">
<SelectOption
v-for="i in Object.keys(fieldTypeObject)"
:key="i"
:value="i"
>
{{ fieldTypeObject[i as keyof typeof fieldType] }}
</SelectOption>
</Select>
<Select
v-model:value="form.fieldType"
:options="[
...Object.keys(fieldTypeObject).map((i) => ({
label: fieldTypeObject[i as keyof typeof fieldType],
value: i,
})),
]"
/>
</FormItem>
<FormItem
v-if="form.fieldType === 'string'"

View File

@ -5,15 +5,7 @@ import { confirm, useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import {
Button,
Divider,
Form,
FormItem,
Input,
Select,
SelectOption,
} from 'antdv-next';
import { Button, Divider, Form, FormItem, Input, Select } from 'antdv-next';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ProcessListenerSelectModal } from '#/views/bpm/processListener/components';
@ -379,15 +371,15 @@ watch(
name="event"
:rules="[{ required: true, message: '请选择事件类型' }]"
>
<Select v-model:value="listenerForm.event">
<SelectOption
v-for="i in Object.keys(listenerEventTypeObject)"
:key="i"
:value="i"
>
{{ listenerEventTypeObject[i as keyof typeof eventType] }}
</SelectOption>
</Select>
<Select
v-model:value="listenerForm.event"
:options="[
...Object.keys(listenerEventTypeObject).map((i) => ({
label: listenerEventTypeObject[i as keyof typeof eventType],
value: i,
})),
]"
/>
</FormItem>
<FormItem
label="监听器ID"
@ -401,15 +393,15 @@ watch(
name="listenerType"
:rules="[{ required: true, message: '请选择监听器类型' }]"
>
<Select v-model:value="listenerForm.listenerType">
<SelectOption
v-for="i in Object.keys(listenerTypeObject)"
:key="i"
:value="i"
>
{{ listenerTypeObject[i as keyof typeof listenerType] }}
</SelectOption>
</Select>
<Select
v-model:value="listenerForm.listenerType"
:options="[
...Object.keys(listenerTypeObject).map((i) => ({
label: listenerTypeObject[i as keyof typeof listenerType],
value: i,
})),
]"
/>
</FormItem>
<FormItem
v-if="listenerForm.listenerType === 'classListener'"
@ -456,10 +448,13 @@ watch(
key="listener-script-type"
:rules="[{ required: true, message: '请选择脚本类型' }]"
>
<Select v-model:value="listenerForm.scriptType">
<SelectOption value="inlineScript">内联脚本</SelectOption>
<SelectOption value="externalScript">外部脚本</SelectOption>
</Select>
<Select
v-model:value="listenerForm.scriptType"
:options="[
{ label: '内联脚本', value: 'inlineScript' },
{ label: '外部脚本', value: 'externalScript' },
]"
/>
</FormItem>
<FormItem
v-if="listenerForm.scriptType === 'inlineScript'"
@ -487,12 +482,15 @@ watch(
name="eventDefinitionType"
key="eventDefinitionType"
>
<Select v-model:value="listenerForm.eventDefinitionType">
<SelectOption value="date">日期</SelectOption>
<SelectOption value="duration">持续时长</SelectOption>
<SelectOption value="cycle">循环</SelectOption>
<SelectOption value="null"></SelectOption>
</Select>
<Select
v-model:value="listenerForm.eventDefinitionType"
:options="[
{ label: '日期', value: 'date' },
{ label: '持续时长', value: 'duration' },
{ label: '循环', value: 'cycle' },
{ label: '无', value: 'null' },
]"
/>
</FormItem>
<FormItem
v-if="

View File

@ -447,15 +447,12 @@ watch(
<Select
v-model:value="loopCharacteristics"
@change="changeLoopCharacteristicsType"
>
<SelectOption value="ParallelMultiInstance">
并行多重事件
</SelectOption>
<SelectOption value="SequentialMultiInstance">
时序多重事件
</SelectOption>
<SelectOption value="Null"></SelectOption>
</Select>
:options="[
{ label: '并行多重事件', value: 'ParallelMultiInstance' },
{ label: '时序多重事件', value: 'SequentialMultiInstance' },
{ label: '无', value: 'Null' },
]"
/>
</FormItem>
<template
v-if="

View File

@ -11,7 +11,6 @@ import {
FormItem,
Input,
Select,
SelectOption,
Switch,
} from 'antdv-next';
@ -269,16 +268,9 @@ onMounted(async () => {
placeholder="请选择子流程"
allow-clear
@change="handleChildProcessChange"
>
<SelectOption
v-for="item in childProcessOptions"
:key="item.key"
:value="item.key"
:label="item.name"
>
{{ item.name }}
</SelectOption>
</Select>
:options="childProcessOptions"
:field-names="{ label: 'name', value: 'key' }"
/>
</FormItem>
<FormItem label="继承变量">

View File

@ -4,7 +4,7 @@ import { nextTick, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Button, message, Select, SelectOption } from 'antdv-next';
import { Button, message, Select } from 'antdv-next';
import SignalMessageModal from '../../signal-message/SignalMessageModal.vue';
@ -118,17 +118,15 @@ watch(
<span class="w-20 text-foreground">消息实例:</span>
<Select
v-model:value="bindMessageId"
:options="
Object.keys(messageMap).map((key) => ({
label: messageMap[key],
value: key,
}))
"
class="w-full"
@change="(value: any) => updateTaskMessage(value)"
>
<SelectOption
v-for="key in Object.keys(messageMap)"
:key="key"
:value="key"
>
{{ messageMap[key] }}
</SelectOption>
</Select>
/>
</div>
<Modal @confirm="handleConfirm" />
</div>

View File

@ -1,14 +1,7 @@
<script lang="ts" setup>
import { nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
import {
Form,
FormItem,
Input,
Select,
SelectOption,
TextArea,
} from 'antdv-next';
import { Form, FormItem, Input, Select, TextArea } from 'antdv-next';
defineOptions({ name: 'ScriptTask' });
const props = defineProps({
@ -86,10 +79,13 @@ watch(
</FormItem>
<!-- TODO scriptType 外部资源 内联脚本 flowable 文档 https://www.flowable.com/open-source/docs/bpmn/ch07b-BPMN-Constructs#script-task -->
<FormItem label="脚本类型">
<Select v-model:value="scriptTaskForm.scriptType">
<SelectOption value="inline">内联脚本</SelectOption>
<SelectOption value="external">外部资源</SelectOption>
</Select>
<Select
v-model:value="scriptTaskForm.scriptType"
:options="[
{ label: '内联脚本', value: 'inline' },
{ label: '外部资源', value: 'external' },
]"
/>
</FormItem>
<FormItem label="脚本" v-show="scriptTaskForm.scriptType === 'inline'">
<TextArea

View File

@ -26,7 +26,6 @@ import {
Form,
FormItem,
Select,
SelectOption,
TextArea,
TreeSelect,
} from 'antdv-next';
@ -356,15 +355,8 @@ onBeforeUnmount(() => {
allow-clear
style="width: 100%"
@change="changeCandidateStrategy"
>
<SelectOption
v-for="(dict, index) in CANDIDATE_STRATEGY"
:key="index"
:value="dict.value"
>
{{ dict.label }}
</SelectOption>
</Select>
:options="CANDIDATE_STRATEGY"
/>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.ROLE"
@ -377,15 +369,9 @@ onBeforeUnmount(() => {
mode="multiple"
style="width: 100%"
@change="updateElementTask"
>
<SelectOption
v-for="item in roleOptions"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
:options="roleOptions"
:field-names="{ label: 'name', value: 'id' }"
/>
</FormItem>
<FormItem
v-if="
@ -419,15 +405,9 @@ onBeforeUnmount(() => {
mode="multiple"
style="width: 100%"
@change="updateElementTask"
>
<SelectOption
v-for="item in postOptions"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
:options="postOptions"
:field-names="{ label: 'name', value: 'id' }"
/>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.USER"
@ -440,15 +420,9 @@ onBeforeUnmount(() => {
mode="multiple"
style="width: 100%"
@change="updateElementTask"
>
<SelectOption
v-for="item in userOptions"
:key="item.id"
:value="item.id"
>
{{ item.nickname }}
</SelectOption>
</Select>
:options="userOptions"
:field-names="{ label: 'nickname', value: 'id' }"
/>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.USER_GROUP"
@ -461,15 +435,9 @@ onBeforeUnmount(() => {
mode="multiple"
style="width: 100%"
@change="updateElementTask"
>
<SelectOption
v-for="item in userGroupOptions"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
:options="userGroupOptions"
:field-names="{ label: 'name', value: 'id' }"
/>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.FORM_USER"
@ -478,19 +446,17 @@ onBeforeUnmount(() => {
>
<Select
v-model:value="userTaskForm.candidateParam"
:options="
userFieldOnFormOptions.map((item) => ({
label: item.title,
value: item.field,
disabled: !item.required,
}))
"
allow-clear
style="width: 100%"
@change="handleFormUserChange"
>
<SelectOption
v-for="(item, idx) in userFieldOnFormOptions"
:key="idx"
:value="item.field"
:disabled="!item.required"
>
{{ item.title }}
</SelectOption>
</Select>
/>
</FormItem>
<FormItem
v-if="
@ -504,16 +470,14 @@ onBeforeUnmount(() => {
allow-clear
style="width: 100%"
@change="updateElementTask"
>
<SelectOption
v-for="(item, idx) in deptFieldOnFormOptions"
:key="idx"
:value="item.field"
:disabled="!item.required"
>
{{ item.title }}
</SelectOption>
</Select>
:options="[
...deptFieldOnFormOptions.map((item) => ({
label: item.title,
value: item.field,
disabled: !item.required,
})),
]"
/>
</FormItem>
<FormItem
v-if="
@ -528,15 +492,12 @@ onBeforeUnmount(() => {
:label="deptLevelLabel!"
name="deptLevel"
>
<Select v-model:value="deptLevel" allow-clear @change="updateElementTask">
<SelectOption
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
:value="item.value"
>
{{ item.label }}
</SelectOption>
</Select>
<Select
v-model:value="deptLevel"
allow-clear
@change="updateElementTask"
:options="MULTI_LEVEL_DEPT"
/>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.EXPRESSION"

View File

@ -9,6 +9,7 @@ import {
Input,
InputNumber,
Radio,
RadioGroup,
TabPane,
Tabs,
} from 'antdv-next';

View File

@ -21,7 +21,6 @@ import {
RadioGroup,
Row,
Select,
SelectOption,
Switch,
} from 'antdv-next';
@ -432,15 +431,9 @@ onMounted(async () => {
v-model:value="configForm.calledProcessDefinitionKey"
allow-clear
@change="handleCalledElementChange"
>
<SelectOption
v-for="(item, index) in childProcessOptions"
:key="index"
:value="item.key"
>
{{ item.name }}
</SelectOption>
</Select>
:options="childProcessOptions"
:field-names="{ label: 'name', value: 'key' }"
/>
</FormItem>
<FormItem
label="是否自动跳过子流程发起节点"
@ -464,41 +457,39 @@ onMounted(async () => {
<div class="mr-2">
<FormItem
:name="['inVariables', index, 'source']"
:rules="{
required: true,
message: '变量不能为空',
trigger: 'blur',
}"
:rules="[
{
required: true,
message: '变量不能为空',
trigger: 'blur',
},
]"
>
<Select class="!w-40" v-model:value="item.source">
<SelectOption
v-for="(field, fIdx) in formFieldOptions"
:key="fIdx"
:value="field.field"
>
{{ field.title }}
</SelectOption>
</Select>
<Select
class="!w-40"
v-model:value="item.source"
:options="formFieldOptions"
:field-names="{ label: 'title', value: 'field' }"
/>
</FormItem>
</div>
<div class="mr-2">
<FormItem
:name="['inVariables', index, 'target']"
:rules="{
required: true,
message: '变量不能为空',
trigger: 'blur',
}"
:rules="[
{
required: true,
message: '变量不能为空',
trigger: 'blur',
},
]"
>
<Select class="!w-40" v-model:value="item.target">
<SelectOption
v-for="(field, fIdx) in childFormFieldOptions"
:key="fIdx"
:value="field.field"
>
{{ field.title }}
</SelectOption>
</Select>
<Select
class="!w-40"
v-model:value="item.target"
:options="childFormFieldOptions"
:field-names="{ label: 'title', value: 'field' }"
/>
</FormItem>
</div>
<div class="mr-1 flex h-8 items-center">
@ -534,41 +525,39 @@ onMounted(async () => {
<div class="mr-2">
<FormItem
:name="['outVariables', index, 'source']"
:rules="{
required: true,
message: '变量不能为空',
trigger: 'blur',
}"
:rules="[
{
required: true,
message: '变量不能为空',
trigger: 'blur',
},
]"
>
<Select class="!w-40" v-model:value="item.source">
<SelectOption
v-for="(field, fIdx) in childFormFieldOptions"
:key="fIdx"
:value="field.field"
>
{{ field.title }}
</SelectOption>
</Select>
<Select
class="!w-40"
v-model:value="item.source"
:options="childFormFieldOptions"
:field-names="{ label: 'title', value: 'field' }"
/>
</FormItem>
</div>
<div class="mr-2">
<FormItem
:name="['outVariables', index, 'target']"
:rules="{
required: true,
message: '变量不能为空',
trigger: 'blur',
}"
:rules="[
{
required: true,
message: '变量不能为空',
trigger: 'blur',
},
]"
>
<Select class="!w-40" v-model:value="item.target">
<SelectOption
v-for="(field, fIdx) in formFieldOptions"
:key="fIdx"
:value="field.field"
>
{{ field.title }}
</SelectOption>
</Select>
<Select
class="!w-40"
v-model:value="item.target"
:options="formFieldOptions"
:field-names="{ label: 'title', value: 'field' }"
/>
</FormItem>
</div>
<div class="mr-1 flex h-8 items-center">
@ -609,16 +598,12 @@ onMounted(async () => {
label="子流程发起人字段"
name="startUserFormField"
>
<Select v-model:value="configForm.startUserFormField" allow-clear>
<SelectOption
v-for="(field, fIdx) in startUserFormFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
>
{{ field.title }}
</SelectOption>
</Select>
<Select
v-model:value="configForm.startUserFormField"
allow-clear
:options="startUserFormFieldOptions"
:field-names="{ label: 'title', value: 'field' }"
/>
</FormItem>
<FormItem
v-if="
@ -682,16 +667,11 @@ onMounted(async () => {
</FormItem>
</Col>
<Col>
<Select v-model:value="configForm.timeUnit" class="w-24">
<SelectOption
v-for="item in TIME_UNIT_TYPES"
:key="item.value"
:label="item.label"
:value="item.value"
>
{{ item.label }}
</SelectOption>
</Select>
<Select
v-model:value="configForm.timeUnit"
class="w-24"
:options="TIME_UNIT_TYPES"
/>
</Col>
<Col>
<span class="inline-flex h-8 items-center">后进入下一节点</span>
@ -774,16 +754,8 @@ onMounted(async () => {
<Select
v-model:value="configForm.multiInstanceSourceType"
@change="handleMultiInstanceSourceTypeChange"
>
<SelectOption
v-for="item in CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE"
:key="item.value"
:label="item.label"
:value="item.value"
>
{{ item.label }}
</SelectOption>
</Select>
:options="CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE"
/>
</FormItem>
<FormItem
v-if="
@ -795,11 +767,13 @@ onMounted(async () => {
label-align="left"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 12 }"
:rules="{
required: true,
message: '固定数量不能为空',
trigger: 'change',
}"
:rules="[
{
required: true,
message: '固定数量不能为空',
trigger: 'change',
},
]"
>
<InputNumber
v-model:value="configForm.multiInstanceSource"
@ -816,22 +790,19 @@ onMounted(async () => {
label-align="left"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 12 }"
:rules="{
required: true,
message: '数字表单字段不能为空',
trigger: 'change',
}"
:rules="[
{
required: true,
message: '数字表单字段不能为空',
trigger: 'change',
},
]"
>
<Select v-model:value="configForm.multiInstanceSource">
<SelectOption
v-for="(field, fIdx) in digitalFormFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
>
{{ field.title }}
</SelectOption>
</Select>
<Select
v-model:value="configForm.multiInstanceSource"
:options="digitalFormFieldOptions"
:field-names="{ label: 'title', value: 'field' }"
/>
</FormItem>
<FormItem
v-if="
@ -843,22 +814,19 @@ onMounted(async () => {
label-align="left"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 12 }"
:rules="{
required: true,
message: '多选表单字段不能为空',
trigger: 'change',
}"
:rules="[
{
required: true,
message: '多选表单字段不能为空',
trigger: 'change',
},
]"
>
<Select v-model:value="configForm.multiInstanceSource">
<SelectOption
v-for="(field, fIdx) in multiFormFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
>
{{ field.title }}
</SelectOption>
</Select>
<Select
v-model:value="configForm.multiInstanceSource"
:options="multiFormFieldOptions"
:field-names="{ label: 'title', value: 'field' }"
/>
</FormItem>
</div>
</Form>

View File

@ -19,7 +19,6 @@ import {
RadioGroup,
Row,
Select,
SelectOption,
TabPane,
Tabs,
TextArea,
@ -259,18 +258,11 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
>
<Select
v-model:value="configForm.roleIds"
clearable
allow-clear
mode="multiple"
>
<SelectOption
v-for="item in roleOptions"
:key="item.id"
:label="item.name"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
:options="roleOptions"
:field-names="{ label: 'name', value: 'id' }"
/>
</FormItem>
<FormItem
v-if="
@ -306,18 +298,11 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
>
<Select
v-model:value="configForm.postIds"
clearable
allow-clear
mode="multiple"
>
<SelectOption
v-for="item in postOptions"
:key="item.id"
:label="item.name"
:value="item.id!"
>
{{ item.name }}
</SelectOption>
</Select>
:options="postOptions"
:field-names="{ label: 'name', value: 'id' }"
/>
</FormItem>
<FormItem
v-if="configForm.candidateStrategy === CandidateStrategy.USER"
@ -326,18 +311,11 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
>
<Select
v-model:value="configForm.userIds"
clearable
allow-clear
mode="multiple"
>
<SelectOption
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
>
{{ item.nickname }}
</SelectOption>
</Select>
:options="userOptions"
:field-names="{ label: 'nickname', value: 'id' }"
/>
</FormItem>
<FormItem
v-if="
@ -348,18 +326,11 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
>
<Select
v-model:value="configForm.userGroups"
clearable
allow-clear
mode="multiple"
>
<SelectOption
v-for="item in userGroupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
:options="userGroupOptions"
:field-names="{ label: 'name', value: 'id' }"
/>
</FormItem>
<FormItem
v-if="
@ -368,17 +339,17 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
label="表单内用户字段"
name="formUser"
>
<Select v-model:value="configForm.formUser" clearable>
<SelectOption
v-for="(item, idx) in userFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled="!item.required"
>
{{ item.title }}
</SelectOption>
</Select>
<Select
v-model:value="configForm.formUser"
:options="
userFieldOnFormOptions.map((item) => ({
label: item.title,
value: item.field,
disabled: !item.required,
}))
"
allow-clear
/>
</FormItem>
<FormItem
v-if="
@ -388,17 +359,17 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
label="表单内部门字段"
name="formDept"
>
<Select v-model:value="configForm.formDept" clearable>
<SelectOption
v-for="(item, idx) in deptFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled="!item.required"
>
{{ item.title }}
</SelectOption>
</Select>
<Select
v-model:value="configForm.formDept"
allow-clear
:options="[
...deptFieldOnFormOptions.map((item) => ({
label: item.title,
value: item.field,
disabled: !item.required,
})),
]"
/>
</FormItem>
<FormItem
v-if="
@ -414,16 +385,11 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
:label="deptLevelLabel!"
name="deptLevel"
>
<Select v-model:value="configForm.deptLevel" clearable>
<SelectOption
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
:label="item.label"
:value="item.value"
>
{{ item.label }}
</SelectOption>
</Select>
<Select
v-model:value="configForm.deptLevel"
allow-clear
:options="MULTI_LEVEL_DEPT"
/>
</FormItem>
<FormItem
v-if="

View File

@ -18,7 +18,6 @@ import {
RadioGroup,
Row,
Select,
SelectOption,
} from 'antdv-next';
import {
@ -207,15 +206,11 @@ defineExpose({ openDrawer }); // 暴露方法给父组件
</FormItem>
</Col>
<Col>
<Select v-model:value="configForm.timeUnit" class="w-28">
<SelectOption
v-for="item in TIME_UNIT_TYPES"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</SelectOption>
</Select>
<Select
v-model:value="configForm.timeUnit"
class="w-28"
:options="TIME_UNIT_TYPES"
/>
</Col>
<Col>
<span class="inline-flex h-8 items-center">后进入下一节点</span>

View File

@ -17,7 +17,6 @@ import {
RadioGroup,
Row,
Select,
SelectOption,
Space,
Switch,
TextArea,
@ -69,6 +68,14 @@ const conditionConfigTypes = computed(() => {
/** 条件规则可选择的表单字段 */
const fieldOptions = useFormFieldsAndStartUser();
const fieldSelectOptions = computed(() =>
fieldOptions.map((field) => ({
label: field.title,
value: field.field,
disabled: !field.required,
raw: field,
})),
);
//
const formRules: Record<string, Rule[]> = reactive({
@ -172,10 +179,12 @@ defineExpose({ validate });
/>
</div>
</div>
<Space orientation="vertical" size="small" class="w-11/12 pl-1">
<template #split>
{{ condition.conditionGroups.and ? '且' : '或' }}
</template>
<Space
orientation="vertical"
size="small"
class="w-11/12 pl-1"
:separator="condition.conditionGroups.and ? '且' : '或'"
>
<Card
class="group relative w-full hover:border-blue-500"
v-for="(equation, cIdx) in condition.conditionGroups.conditions"
@ -190,7 +199,10 @@ defineExpose({ validate });
icon="lucide:circle-x"
class="size-4"
@click="
deleteConditionGroup(condition.conditionGroups.conditions, cIdx)
deleteConditionGroup(
condition.conditionGroups.conditions,
Number(cIdx),
)
"
/>
</div>
@ -224,47 +236,39 @@ defineExpose({ validate });
rIdx,
'leftSide',
]"
:rules="{
required: true,
message: '左值不能为空',
trigger: 'change',
}"
:rules="[
{
required: true,
message: '左值不能为空',
trigger: 'change',
},
]"
>
<Select
v-model:value="rule.leftSide"
allow-clear
placeholder="请选择表单字段"
:options="fieldSelectOptions"
>
<SelectOption
v-for="(field, fIdx) in fieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
>
<template #optionRender="{ option }">
<Tooltip
title="表单字段非必填时不能作为流程分支条件"
placement="right"
v-if="!field.required"
v-if="!option.data.raw.required"
>
<span>{{ field.title }}</span>
<span>{{ option.data.raw.title }}</span>
</Tooltip>
<template v-else>{{ field.title }}</template>
</SelectOption>
<template v-else>{{ option.data.raw.title }}</template>
</template>
</Select>
</FormItem>
</Col>
<Col :span="6">
<Select v-model:value="rule.opCode" placeholder="请选择操作符">
<SelectOption
v-for="operator in COMPARISON_OPERATORS"
:key="operator.value"
:label="operator.label"
:value="operator.value"
>
{{ operator.label }}
</SelectOption>
</Select>
<Select
v-model:value="rule.opCode"
placeholder="请选择操作符"
:options="COMPARISON_OPERATORS"
/>
</Col>
<Col :span="7">
<FormItem
@ -276,11 +280,13 @@ defineExpose({ validate });
rIdx,
'rightSide',
]"
:rules="{
required: true,
message: '右值不能为空',
trigger: ['blur', 'change'],
}"
:rules="[
{
required: true,
message: '右值不能为空',
trigger: ['blur', 'change'],
},
]"
>
<Input
v-model:value="rule.rightSide"
@ -293,11 +299,11 @@ defineExpose({ validate });
<Trash2
v-if="equation.rules.length > 1"
class="mr-2 size-4 cursor-pointer text-red-500"
@click="deleteConditionRule(equation, rIdx)"
@click="deleteConditionRule(equation, Number(rIdx))"
/>
<Plus
class="size-4 cursor-pointer text-blue-500"
@click="addConditionRule(equation, rIdx)"
@click="addConditionRule(equation, Number(rIdx))"
/>
</div>
</Col>

View File

@ -3,15 +3,7 @@ import type { HttpRequestParam } from '../../../consts';
import { IconifyIcon } from '@vben/icons';
import {
Button,
Col,
FormItem,
Input,
Row,
Select,
SelectOption,
} from 'antdv-next';
import { Button, Col, FormItem, Input, Row, Select } from 'antdv-next';
import {
BPM_HTTP_REQUEST_PARAM_TYPES,
@ -65,59 +57,59 @@ function deleteHttpRequestParam(arr: HttpRequestParam[], index: number) {
<Col :span="7">
<FormItem
:name="[bind, 'header', index, 'key']"
:rules="{
required: true,
message: '参数名不能为空',
trigger: ['blur', 'change'],
}"
:rules="[
{
required: true,
message: '参数名不能为空',
trigger: ['blur', 'change'],
},
]"
>
<Input placeholder="参数名不能为空" v-model:value="item.key" />
</FormItem>
</Col>
<Col :span="5">
<Select v-model:value="item.type">
<SelectOption
v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES"
:key="types.value"
:label="types.label"
:value="types.value"
>
{{ types.label }}
</SelectOption>
</Select>
<Select
v-model:value="item.type"
:options="BPM_HTTP_REQUEST_PARAM_TYPES"
/>
</Col>
<Col :span="10">
<FormItem
:name="[bind, 'header', index, 'value']"
:rules="{
required: true,
message: '参数值不能为空',
trigger: ['blur', 'change'],
}"
:rules="[
{
required: true,
message: '参数值不能为空',
trigger: ['blur', 'change'],
},
]"
v-if="item.type === BpmHttpRequestParamTypeEnum.FIXED_VALUE"
>
<Input placeholder="请求头" v-model:value="item.value" />
</FormItem>
<FormItem
:name="[bind, 'header', index, 'value']"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'change',
}"
:rules="[
{
required: true,
message: '参数值不能为空',
trigger: 'change',
},
]"
v-if="item.type === BpmHttpRequestParamTypeEnum.FROM_FORM"
>
<Select v-model:value="item.value" placeholder="请选择表单字段">
<SelectOption
v-for="(field, fIdx) in formFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
>
{{ field.title }}
</SelectOption>
</Select>
<Select
v-model:value="item.value"
:options="
formFieldOptions.map((field) => ({
label: field.title,
value: field.field,
disabled: !field.required,
}))
"
placeholder="请选择表单字段"
/>
</FormItem>
</Col>
<Col :span="2">
@ -150,59 +142,59 @@ function deleteHttpRequestParam(arr: HttpRequestParam[], index: number) {
<Col :span="7">
<FormItem
:name="[bind, 'body', index, 'key']"
:rules="{
required: true,
message: '参数名不能为空',
trigger: ['blur', 'change'],
}"
:rules="[
{
required: true,
message: '参数名不能为空',
trigger: ['blur', 'change'],
},
]"
>
<Input placeholder="参数名" v-model:value="item.key" />
</FormItem>
</Col>
<Col :span="5">
<Select v-model:value="item.type">
<SelectOption
v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES"
:key="types.value"
:label="types.label"
:value="types.value"
>
{{ types.label }}
</SelectOption>
</Select>
<Select
v-model:value="item.type"
:options="BPM_HTTP_REQUEST_PARAM_TYPES"
/>
</Col>
<Col :span="10">
<FormItem
:name="[bind, 'body', index, 'value']"
:rules="{
required: true,
message: '参数值不能为空',
trigger: ['blur', 'change'],
}"
:rules="[
{
required: true,
message: '参数值不能为空',
trigger: ['blur', 'change'],
},
]"
v-if="item.type === BpmHttpRequestParamTypeEnum.FIXED_VALUE"
>
<Input placeholder="参数值" v-model:value="item.value" />
</FormItem>
<FormItem
:name="[bind, 'body', index, 'value']"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'change',
}"
:rules="[
{
required: true,
message: '参数值不能为空',
trigger: 'change',
},
]"
v-if="item.type === BpmHttpRequestParamTypeEnum.FROM_FORM"
>
<Select v-model:value="item.value" placeholder="请选择表单字段">
<SelectOption
v-for="(field, fIdx) in formFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
>
{{ field.title }}
</SelectOption>
</Select>
<Select
v-model:value="item.value"
:options="
formFieldOptions.map((field) => ({
label: field.title,
value: field.field,
disabled: !field.required,
}))
"
placeholder="请选择表单字段"
/>
</FormItem>
</Col>
<Col :span="2">

View File

@ -3,16 +3,7 @@ import { toRefs, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import {
Alert,
Button,
Col,
FormItem,
Input,
Row,
Select,
SelectOption,
} from 'antdv-next';
import { Alert, Button, Col, FormItem, Input, Row, Select } from 'antdv-next';
import { useFormFields } from '../../../helpers';
import HttpRequestParamSetting from './http-request-param-setting.vue';
@ -79,11 +70,13 @@ function deleteHttpResponseSetting(
:label-col="{ span: 24 }"
:wrapper-col="{ span: 24 }"
:name="[formItemPrefix, 'url']"
:rules="{
required: true,
message: '请求地址不能为空',
trigger: ['blur', 'change'],
}"
:rules="[
{
required: true,
message: '请求地址不能为空',
trigger: ['blur', 'change'],
},
]"
>
<Input v-model:value="setting.url" placeholder="请输入请求地址" />
</FormItem>
@ -117,37 +110,38 @@ function deleteHttpResponseSetting(
<Col :span="10">
<FormItem
:name="[formItemPrefix, 'response', index, 'key']"
:rules="{
required: true,
message: '表单字段不能为空',
trigger: ['blur', 'change'],
}"
:rules="[
{
required: true,
message: '表单字段不能为空',
trigger: ['blur', 'change'],
},
]"
>
<Select
v-model:value="item.key"
placeholder="请选择表单字段"
allow-clear
>
<SelectOption
v-for="(field, fIdx) in formFields"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
>
{{ field.title }}
</SelectOption>
</Select>
:options="[
...formFields.map((field) => ({
label: field.title,
value: field.field,
disabled: !field.required,
})),
]"
/>
</FormItem>
</Col>
<Col :span="12">
<FormItem
:name="[formItemPrefix, 'response', index, 'value']"
:rules="{
required: true,
message: '请求返回字段不能为空',
trigger: ['blur', 'change'],
}"
:rules="[
{
required: true,
message: '请求返回字段不能为空',
trigger: ['blur', 'change'],
},
]"
>
<Input v-model:value="item.value" placeholder="请求返回字段" />
</FormItem>
@ -157,7 +151,9 @@ function deleteHttpResponseSetting(
<IconifyIcon
class="size-4 cursor-pointer text-red-500"
icon="lucide:trash-2"
@click="deleteHttpResponseSetting(setting.response!, index)"
@click="
deleteHttpResponseSetting(setting.response!, Number(index))
"
/>
</div>
</Col>

View File

@ -90,11 +90,13 @@ defineExpose({ validate });
<FormItem
label="请求地址"
:name="`task${listener.type}ListenerPath`"
:rules="{
required: true,
message: '请求地址不能为空',
trigger: ['blur', 'change'],
}"
:rules="[
{
required: true,
message: '请求地址不能为空',
trigger: ['blur', 'change'],
},
]"
>
<Input
v-model:value="configForm[`task${listener.type}ListenerPath`]"

View File

@ -19,7 +19,6 @@ import {
message,
Row,
Select,
SelectOption,
} from 'antdv-next';
import { ConditionType } from '../../consts';
@ -237,25 +236,20 @@ defineExpose({ openDrawer }); // 暴露方法给父组件
<FormItem
class="mb-0 ml-4 inline-block w-48"
:name="['routerGroups', index, 'nodeId']"
:rules="{
required: true,
message: '路由目标节点不能为空',
trigger: 'change',
}"
:rules="[
{
required: true,
message: '路由目标节点不能为空',
trigger: 'change',
},
]"
>
<Select
v-model:value="item.nodeId"
placeholder="请选择路由目标节点"
allow-clear
>
<SelectOption
v-for="node in nodeOptions"
:key="node.value"
:value="node.value"
>
{{ node.label }}
</SelectOption>
</Select>
:options="nodeOptions"
/>
</FormItem>
</div>
<Button

View File

@ -25,7 +25,6 @@ import {
message,
Row,
Select,
SelectOption,
Tag,
} from 'antdv-next';
@ -412,16 +411,11 @@ onMounted(() => {
:rules="formRules"
>
<FormItem label="触发器类型" name="type">
<Select v-model:value="configForm.type" @change="changeTriggerType">
<SelectOption
v-for="(item, index) in TRIGGER_TYPES"
:key="index"
:value="item.value"
:label="item.label"
>
{{ item.label }}
</SelectOption>
</Select>
<Select
v-model:value="configForm.type"
@change="changeTriggerType"
:options="TRIGGER_TYPES"
/>
</FormItem>
<!-- HTTP 请求触发器 -->
<div
@ -509,17 +503,9 @@ onMounted(() => {
placeholder="请选择表单字段"
:disabled="key !== ''"
allow-clear
>
<SelectOption
v-for="(field, fIdx) in optionalUpdateFormFields"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="field.disabled"
>
{{ field.title }}
</SelectOption>
</Select>
:options="optionalUpdateFormFields"
:field-names="{ label: 'title', value: 'field' }"
/>
</FormItem>
</Col>
<Col :span="4">
@ -528,11 +514,13 @@ onMounted(() => {
<Col :span="10">
<FormItem
:name="['formSettings', index, 'updateFormFields', key]"
:rules="{
required: true,
message: '值不能为空',
trigger: 'blur',
}"
:rules="[
{
required: true,
message: '值不能为空',
trigger: 'blur',
},
]"
>
<Input
v-model:value="formSetting.updateFormFields![key]"
@ -651,16 +639,9 @@ onMounted(() => {
mode="multiple"
placeholder="请选择要删除的字段"
class="w-full"
>
<SelectOption
v-for="field in formFields"
:key="field.field"
:label="field.title"
:value="field.field"
>
{{ field.title }}
</SelectOption>
</Select>
:options="formFields"
:field-names="{ label: 'title', value: 'field' }"
/>
</div>
</Card>
</div>

View File

@ -28,7 +28,6 @@ import {
RadioGroup,
Row,
Select,
SelectOption,
Switch,
TabPane,
Tabs,
@ -148,6 +147,20 @@ const deptFieldOnFormOptions = computed(() => {
return formFieldOptions.filter((item) => item.type === 'DeptSelect');
});
const userFieldSelectOptions = computed(() =>
userFieldOnFormOptions.value.map((item) => ({
...item,
disabled: !item.required,
})),
);
const deptFieldSelectOptions = computed(() =>
deptFieldOnFormOptions.value.map((item) => ({
...item,
disabled: !item.required,
})),
);
//
const {
buttonsSetting,
@ -697,18 +710,11 @@ onMounted(() => {
>
<Select
v-model:value="configForm.roleIds"
clearable
:options="roleOptions"
:field-names="{ label: 'name', value: 'id' }"
allow-clear
mode="multiple"
>
<SelectOption
v-for="item in roleOptions"
:key="item.id"
:label="item.name"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
/>
</FormItem>
<FormItem
v-if="
@ -744,18 +750,11 @@ onMounted(() => {
>
<Select
v-model:value="configForm.postIds"
clearable
:options="postOptions"
:field-names="{ label: 'name', value: 'id' }"
allow-clear
mode="multiple"
>
<SelectOption
v-for="item in postOptions"
:key="item.id"
:label="item.name"
:value="item.id!"
>
{{ item.name }}
</SelectOption>
</Select>
/>
</FormItem>
<FormItem
v-if="configForm.candidateStrategy === CandidateStrategy.USER"
@ -764,18 +763,11 @@ onMounted(() => {
>
<Select
v-model:value="configForm.userIds"
clearable
:options="userOptions"
:field-names="{ label: 'nickname', value: 'id' }"
allow-clear
mode="multiple"
>
<SelectOption
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
>
{{ item.nickname }}
</SelectOption>
</Select>
/>
</FormItem>
<FormItem
v-if="
@ -786,18 +778,11 @@ onMounted(() => {
>
<Select
v-model:value="configForm.userGroups"
clearable
:options="userGroupOptions"
:field-names="{ label: 'name', value: 'id' }"
allow-clear
mode="multiple"
>
<SelectOption
v-for="item in userGroupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
/>
</FormItem>
<FormItem
v-if="
@ -806,17 +791,12 @@ onMounted(() => {
label="表单内用户字段"
name="formUser"
>
<Select v-model:value="configForm.formUser" clearable>
<SelectOption
v-for="(item, idx) in userFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled="!item.required"
>
{{ item.title }}
</SelectOption>
</Select>
<Select
v-model:value="configForm.formUser"
:options="userFieldSelectOptions"
:field-names="{ label: 'title', value: 'field' }"
allow-clear
/>
</FormItem>
<FormItem
v-if="
@ -826,17 +806,12 @@ onMounted(() => {
label="表单内部门字段"
name="formDept"
>
<Select v-model:value="configForm.formDept" clearable>
<SelectOption
v-for="(item, idx) in deptFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled="!item.required"
>
{{ item.title }}
</SelectOption>
</Select>
<Select
v-model:value="configForm.formDept"
:options="deptFieldSelectOptions"
:field-names="{ label: 'title', value: 'field' }"
allow-clear
/>
</FormItem>
<FormItem
v-if="
@ -852,16 +827,11 @@ onMounted(() => {
:label="deptLevelLabel!"
name="deptLevel"
>
<Select v-model:value="configForm.deptLevel" clearable>
<SelectOption
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
:label="item.label"
:value="item.value"
>
{{ item.label }}
</SelectOption>
</Select>
<Select
v-model:value="configForm.deptLevel"
:options="MULTI_LEVEL_DEPT"
allow-clear
/>
</FormItem>
<FormItem
v-if="
@ -943,16 +913,12 @@ onMounted(() => {
label="驳回节点"
name="returnNodeId"
>
<Select v-model:value="configForm.returnNodeId" clearable>
<SelectOption
v-for="item in returnTaskList"
:key="item.id"
:label="item.name"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
<Select
v-model:value="configForm.returnNodeId"
:options="returnTaskList"
:field-names="{ label: 'name', value: 'id' }"
allow-clear
/>
</FormItem>
</div>
@ -1021,19 +987,11 @@ onMounted(() => {
<Col>
<Select
v-model:value="timeUnit"
:options="TIME_UNIT_TYPES"
class="mr-2"
:style="{ width: '100px' }"
@change="timeUnitChange"
>
<SelectOption
v-for="item in TIME_UNIT_TYPES"
:key="item.value"
:label="item.label"
:value="item.value"
>
{{ item.label }}
</SelectOption>
</Select>
/>
<TypographyText class="mr-2 mt-2 inline-flex text-sm">
未处理
</TypographyText>
@ -1087,18 +1045,11 @@ onMounted(() => {
>
<Select
v-model:value="configForm.assignEmptyHandlerUserIds"
clearable
:options="userOptions"
:field-names="{ label: 'nickname', value: 'id' }"
allow-clear
mode="multiple"
>
<SelectOption
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
>
{{ item.nickname }}
</SelectOption>
</Select>
/>
</FormItem>
<div v-if="currentNode.type === BpmNodeTypeEnum.USER_TASK_NODE">

View File

@ -18,8 +18,10 @@ import {
Avatar,
Button,
Form,
FormItem,
Input,
Radio,
RadioGroup,
Select,
TextArea,
Tooltip,
@ -68,20 +70,18 @@ const rules: Record<string, Rule[]> = {
key: [
{ required: true, message: '流程标识不能为空', trigger: 'blur' },
{
validator: (_rule: any, value: string, callback: any) => {
validator: (_rule: any, value: string) => {
if (!value) {
callback();
return;
return Promise.resolve();
}
if (!/^[a-z_][-\w.$]*$/i.test(value)) {
callback(
return Promise.reject(
new Error(
'只能包含字母、数字、下划线、连字符和点号,且必须以字母或下划线开头',
),
);
return;
}
callback();
return Promise.resolve();
},
trigger: 'blur',
},
@ -281,16 +281,10 @@ defineExpose({ validate });
class="w-full"
v-model:value="modelData.category"
allow-clear
:field-names="{ label: 'name', value: 'code' }"
:options="categoryList"
placeholder="请选择流程分类"
>
<SelectOption
v-for="category in categoryList"
:key="category.code"
:value="category.code"
>
{{ category.name }}
</SelectOption>
</Select>
/>
</FormItem>
<FormItem label="流程图标">
<ImageUpload v-model:value="modelData.icon" />
@ -326,13 +320,14 @@ defineExpose({ validate });
<FormItem label="谁可以发起" name="startUserType">
<Select
v-model:value="modelData.startUserType"
:options="[
{ label: '全员', value: 0 },
{ label: '指定人员', value: 1 },
{ label: '指定部门', value: 2 },
]"
placeholder="请选择谁可以发起"
@change="handleStartUserTypeChange"
>
<SelectOption :value="0">全员</SelectOption>
<SelectOption :value="1">指定人员</SelectOption>
<SelectOption :value="2">指定部门</SelectOption>
</Select>
/>
<div
v-if="modelData.startUserType === 1"
class="mt-2 flex flex-wrap gap-1"

View File

@ -15,7 +15,6 @@ import {
Radio,
RadioGroup,
Select,
SelectOption,
Tooltip,
} from 'antdv-next';
@ -107,16 +106,12 @@ defineExpose({ validate });
name="formId"
class="mb-5"
>
<Select v-model:value="modelData.formId" allow-clear>
<SelectOption
v-for="form in props.formList"
:key="form.id"
:value="form.id"
>
{{ form.name }}
</SelectOption>
>
</Select>
<Select
v-model:value="modelData.formId"
allow-clear
:options="props.formList"
:field-names="{ label: 'name', value: 'id' }"
/>
</FormItem>
<FormItem
v-if="modelData.formType === BpmModelFormType.CUSTOM"

View File

@ -18,6 +18,7 @@ import {
Button,
Card,
Collapse,
CollapsePanel,
Dropdown,
Menu,
MenuItem,

View File

@ -34,7 +34,6 @@ import {
message,
Popover,
Select,
SelectOption,
Space,
TextArea,
} from 'antdv-next';
@ -55,6 +54,7 @@ import {
transferTask,
} from '#/api/bpm/task';
import { setConfAndFields2 } from '#/components/form-create';
import { FileUpload } from '#/components/upload';
import { $t } from '#/locales';
import Signature from './signature.vue';
@ -120,6 +120,7 @@ const approveReasonForm: any = reactive({
reason: '',
signPicUrl: '',
nextAssignees: {},
attachments: [],
});
const approveReasonRule: Record<string, any> = computed(() => {
return {
@ -140,7 +141,8 @@ const approveReasonRule: Record<string, any> = computed(() => {
});
const rejectFormRef = ref<FormInstance>();
const rejectReasonForm = reactive({
const rejectReasonForm = reactive<{ attachments: string[]; reason: string }>({
attachments: [],
reason: '',
}); //
const rejectReasonRule: any = computed(() => {
@ -290,6 +292,14 @@ function closePopover(type: string, formRef: any | FormInstance) {
if (formRef) {
formRef.resetFields();
}
if (type === 'approve') {
approveReasonForm.reason = '';
approveReasonForm.attachments = [];
approveReasonForm.signPicUrl = '';
} else if (type === 'reject') {
rejectReasonForm.reason = '';
rejectReasonForm.attachments = [];
}
if (popOverVisible.value[type]) popOverVisible.value[type] = false;
nextAssigneesActivityNode.value = [];
// Timeline
@ -401,6 +411,7 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
const data = {
id: runningTask.value.id,
reason: approveReasonForm.reason,
attachments: approveReasonForm.attachments,
variables, // ,
nextAssignees: approveReasonForm.nextAssignees, //
} as any;
@ -414,6 +425,9 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
await formCreateApi.validate();
}
await approveTask(data);
approveReasonForm.reason = '';
approveReasonForm.attachments = [];
approveReasonForm.signPicUrl = '';
popOverVisible.value.approve = false;
nextAssigneesActivityNode.value = [];
// Timeline
@ -425,9 +439,12 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
//
const data = {
id: runningTask.value.id,
attachments: rejectReasonForm.attachments,
reason: rejectReasonForm.reason,
};
await rejectTask(data);
rejectReasonForm.reason = '';
rejectReasonForm.attachments = [];
popOverVisible.value.reject = false;
message.success('审批不通过成功');
}
@ -748,6 +765,38 @@ function handleSignFinish(url: string) {
approveFormRef.value?.validateFields(['signPicUrl']);
}
/** 附件图片预览 */
const imagePreviewOpen = ref(false);
const imagePreviewUrl = ref('');
/** 判断文件是否为图片类型 */
function isImageUrl(url: string) {
return /\.(bmp|gif|jpe?g|png|svg|webp)$/i.test(url);
}
/** 处理文件预览 */
function handleFilePreview(file: any) {
if (!file?.url && !file?.response) {
message.warning('文件地址不存在,无法预览');
return;
}
const url = file.url || file?.response?.url || file?.response;
if (!url) {
message.warning('文件地址不存在,无法预览');
return;
}
if (isImageUrl(url)) {
imagePreviewUrl.value = url;
imagePreviewOpen.value = true;
} else {
window.open(url, '_blank');
}
}
function handleImagePreviewOpenChange(open: boolean) {
imagePreviewOpen.value = open;
}
/** 处理弹窗可见性 */
function handlePopoverVisible(visible: boolean) {
if (!visible) {
@ -843,6 +892,16 @@ defineExpose({ loadTodoTask });
:rows="4"
/>
</FormItem>
<FormItem label="上传附件/图片" name="attachments">
<FileUpload
v-model:value="approveReasonForm.attachments"
:max-number="10"
:multiple="true"
:show-download-icon="false"
help-text="支持多文件/图片上传"
@preview="handleFilePreview"
/>
</FormItem>
<FormItem>
<Space>
<Button
@ -900,6 +959,15 @@ defineExpose({ loadTodoTask });
:rows="4"
/>
</FormItem>
<FormItem label="上传附件/图片" name="attachments">
<FileUpload
v-model:value="rejectReasonForm.attachments"
:max-number="10"
:multiple="true"
help-text="支持多文件/图片上传"
@preview="handleFilePreview"
/>
</FormItem>
<FormItem>
<Button
:disabled="formLoading"
@ -956,16 +1024,9 @@ defineExpose({ loadTodoTask });
mode="multiple"
placeholder="请选择抄送人"
class="w-full"
>
<SelectOption
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
>
{{ item.nickname }}
</SelectOption>
</Select>
:options="userOptions"
:field-names="{ label: 'nickname', value: 'id' }"
/>
</FormItem>
<FormItem label="抄送意见" name="copyReason">
<TextArea
@ -1026,16 +1087,9 @@ defineExpose({ loadTodoTask });
v-model:value="transferForm.assigneeUserId"
:allow-clear="true"
style="width: 100%"
>
<SelectOption
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
>
{{ item.nickname }}
</SelectOption>
</Select>
:options="userOptions"
:field-names="{ label: 'nickname', value: 'id' }"
/>
</FormItem>
<FormItem label="审批意见" name="reason">
<TextArea
@ -1099,16 +1153,9 @@ defineExpose({ loadTodoTask });
v-model:value="delegateForm.delegateUserId"
:allow-clear="true"
style="width: 100%"
>
<SelectOption
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
>
{{ item.nickname }}
</SelectOption>
</Select>
:options="userOptions"
:field-names="{ label: 'nickname', value: 'id' }"
/>
</FormItem>
<FormItem label="审批意见" name="reason">
<TextArea
@ -1173,16 +1220,9 @@ defineExpose({ loadTodoTask });
:allow-clear="true"
mode="multiple"
style="width: 100%"
>
<SelectOption
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
>
{{ item.nickname }}
</SelectOption>
</Select>
:options="userOptions"
:field-names="{ label: 'nickname', value: 'id' }"
/>
</FormItem>
<FormItem label="审批意见" name="reason">
<TextArea
@ -1250,18 +1290,15 @@ defineExpose({ loadTodoTask });
<FormItem label="减签人员" name="deleteSignTaskId">
<Select
v-model:value="deleteSignForm.deleteSignTaskId"
:options="
(runningTask.children as any[]).map((item) => ({
label: getDeleteSignUserLabel(item),
value: item.id,
}))
"
:allow-clear="true"
style="width: 100%"
>
<SelectOption
v-for="item in runningTask.children"
:key="item.id"
:label="getDeleteSignUserLabel(item)"
:value="item.id"
>
{{ getDeleteSignUserLabel(item) }}
</SelectOption>
</Select>
/>
</FormItem>
<FormItem label="审批意见" name="reason">
<TextArea
@ -1323,16 +1360,9 @@ defineExpose({ loadTodoTask });
v-model:value="returnForm.targetTaskDefinitionKey"
:allow-clear="true"
style="width: 100%"
>
<SelectOption
v-for="item in returnList"
:key="item.taskDefinitionKey"
:label="item.name"
:value="item.taskDefinitionKey"
>
{{ item.name }}
</SelectOption>
</Select>
:options="returnList"
:field-names="{ label: 'name', value: 'taskDefinitionKey' }"
/>
</FormItem>
<FormItem label="退回理由" name="returnReason">
<TextArea
@ -1444,4 +1474,14 @@ defineExpose({ loadTodoTask });
<!-- 签名弹窗 -->
<SignatureModal @success="handleSignFinish" />
<!-- 图片预览隐藏的 Image 组件仅用于附件预览弹窗 -->
<Image
:preview="{
open: imagePreviewOpen,
onOpenChange: handleImagePreviewOpenChange,
}"
:src="imagePreviewUrl"
style="display: none"
/>
</template>

View File

@ -201,16 +201,25 @@ function shouldShowCustomUserSelect(
);
}
/** 判断是否需要显示审批意见 */
function shouldShowApprovalReason(task: any, nodeType: BpmNodeTypeEnum) {
/** 判断是否需要显示审批意见和附件 */
function shouldShowReasonAndAttachment(task: any, nodeType: BpmNodeTypeEnum) {
return (
task.reason &&
(task.reason || task.attachments?.length > 0) &&
[BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
nodeType,
)
);
}
function getAttachmentName(url: string) {
return decodeURIComponent(url.slice(url.lastIndexOf('/') + 1));
}
function isImageAttachment(url: string) {
const ext = url.split('.').pop()?.toLowerCase();
return ['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp'].includes(ext || '');
}
/** 用户选择弹窗关闭 */
function handleUserSelectClosed() {
selectedUsers.value = [];
@ -410,29 +419,74 @@ defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers });
</div>
</div>
<!-- 审批意见和签名 -->
<!-- 审批意见附件和签名 -->
<div
v-if="shouldShowApprovalReason(task, activity.nodeType)"
v-if="shouldShowReasonAndAttachment(task, activity.nodeType)"
class="mt-1 w-full rounded-md bg-gray-100 p-2 text-sm text-gray-500"
>
审批意见{{ task.reason }}
</div>
<div v-if="task.reason">{{ task.reason }}</div>
<div
v-if="
task.signPicUrl &&
activity.nodeType === BpmNodeTypeEnum.USER_TASK_NODE
"
class="mt-1 flex w-full items-center rounded-md bg-gray-100 p-2 text-sm text-gray-500"
v-if="(task.attachments?.length || 0) > 0"
:class="{
'mt-2 border-t border-dashed border-gray-300 pt-2':
task.reason,
}"
>
签名
<Image
class="ml-2"
:width="180"
:height="60"
:src="task.signPicUrl"
:preview="{ src: task.signPicUrl }"
/>
<div class="mb-1 text-xs font-semibold text-gray-400">
附件列表
</div>
<div class="flex flex-col gap-1.5">
<template
v-for="(attachment, attachmentIndex) in task.attachments"
:key="attachmentIndex"
>
<div class="flex items-center gap-2">
<IconifyIcon
:icon="
isImageAttachment(attachment)
? 'lucide:image'
: 'lucide:file-text'
"
class="text-gray-400"
/>
<Image
v-if="isImageAttachment(attachment)"
:width="32"
:height="32"
class="rounded border border-solid border-gray-200 object-cover"
:src="attachment"
:preview="{ src: attachment }"
/>
<a
v-else
:href="attachment"
target="_blank"
class="max-w-[240px] truncate text-blue-500 hover:text-blue-600 hover:underline"
:title="getAttachmentName(attachment)"
>
{{ getAttachmentName(attachment) }}
</a>
</div>
</template>
</div>
</div>
</div>
<div
v-if="
task.signPicUrl &&
activity.nodeType === BpmNodeTypeEnum.USER_TASK_NODE
"
class="mt-1 flex w-full items-center rounded-md bg-gray-100 p-2 text-sm text-gray-500"
>
签名
<Image
class="ml-2"
:width="180"
:height="60"
:src="task.signPicUrl"
:preview="{ src: task.signPicUrl }"
/>
</div>
</div>
<!-- 情况二遍历每个审批节点下的候选的task 任务 -->

View File

@ -90,15 +90,11 @@ function filterDictTypeOption(input: string, option: any) {
<!-- Java 类型 -->
<template #javaType="{ row, column }">
<Select v-model:value="row.javaType" style="width: 100%">
<SelectOption
v-for="option in column.params.options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</SelectOption>
</Select>
<Select
v-model:value="row.javaType"
style="width: 100%"
:options="column.params.options"
/>
</template>
<!-- Java 属性 -->
<template #javaField="{ row }">
@ -124,15 +120,11 @@ function filterDictTypeOption(input: string, option: any) {
<!-- 查询方式 -->
<template #listOperationCondition="{ row, column }">
<Select v-model:value="row.listOperationCondition" class="w-full">
<SelectOption
v-for="option in column.params.options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</SelectOption>
</Select>
<Select
v-model:value="row.listOperationCondition"
class="w-full"
:options="column.params.options"
/>
</template>
<!-- 允许空 -->
@ -142,15 +134,11 @@ function filterDictTypeOption(input: string, option: any) {
<!-- 显示类型 -->
<template #htmlType="{ row, column }">
<Select v-model:value="row.htmlType" class="w-full">
<SelectOption
v-for="option in column.params.options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</SelectOption>
</Select>
<Select
v-model:value="row.htmlType"
class="w-full"
:options="column.params.options"
/>
</template>
<!-- 字典类型 -->
@ -161,15 +149,9 @@ function filterDictTypeOption(input: string, option: any) {
allow-clear
show-search
:filter-option="filterDictTypeOption"
>
<SelectOption
v-for="option in dictTypeOptions"
:key="option.type"
:value="option.type"
>
{{ option.name }}
</SelectOption>
</Select>
:options="dictTypeOptions"
:field-names="{ label: 'name', value: 'type' }"
/>
</template>
<!-- 示例 -->

View File

@ -26,6 +26,7 @@ const loading = ref(false);
const fileTree = ref<FileNode[]>([]);
const previewFiles = ref<InfraCodegenApi.CodegenPreview[]>([]);
const activeKey = ref<string>('');
const selectedKeys = ref<string[]>([]);
/** 代码地图 */
const codeMap = ref<Map<string, string>>(new Map<string, string>());
@ -53,31 +54,35 @@ function removeCodeMapKey(targetKey: any) {
/** 复制代码 */
async function copyCode() {
const { copy } = useClipboard();
const file = previewFiles.value.find(
(item) => item.filePath === activeKey.value,
);
const file = findPreviewFile(activeKey.value);
if (file) {
await copy(file.code);
message.success('复制成功');
}
}
/** 文件节点点击事件 */
function handleNodeClick(_: any[], e: any) {
if (!e.node.isLeaf) {
return;
}
activeKey.value = e.node.key;
const file = previewFiles.value.find((item) => {
const list = activeKey.value.split('.');
function findPreviewFile(fileKey: string) {
return previewFiles.value.find((item) => {
const list = fileKey.split('.');
// -
if (list.length > 2) {
const lang = list.pop();
return item.filePath === `${list.join('/')}.${lang}`;
}
return item.filePath === activeKey.value;
return item.filePath === fileKey;
});
}
/** 文件节点点击事件 */
function handleNodeClick(_: any[], e: any) {
if (!e.node.isLeaf) {
selectedKeys.value = activeKey.value ? [activeKey.value] : [];
return;
}
activeKey.value = String(e.node.key);
selectedKeys.value = [activeKey.value];
const file = findPreviewFile(activeKey.value);
if (!file) {
return;
}
@ -179,6 +184,7 @@ const [Modal, modalApi] = useVbenModal({
if (!isOpen) {
//
codeMap.value.clear();
selectedKeys.value = [];
return;
}
@ -197,6 +203,7 @@ const [Modal, modalApi] = useVbenModal({
fileTree.value = handleFiles(data);
if (data.length > 0) {
activeKey.value = data[0]?.filePath || '';
selectedKeys.value = activeKey.value ? [activeKey.value] : [];
const code = data[0]?.code || '';
setCodeMap(activeKey.value, code);
}
@ -217,7 +224,7 @@ const [Modal, modalApi] = useVbenModal({
<DirectoryTree
v-if="fileTree.length > 0"
default-expand-all
v-model:active-key="activeKey"
v-model:selected-keys="selectedKeys"
@select="handleNodeClick"
:tree-data="fileTree"
/>
@ -240,7 +247,7 @@ const [Modal, modalApi] = useVbenModal({
>
<CodeEditor
class="max-h-200"
:value="codeMap.get(activeKey)"
:value="codeMap.get(key)"
mode="application/json"
:readonly="true"
:bordered="true"

View File

@ -18,6 +18,7 @@ import {
import {
Button,
Form,
FormItem,
Input,
message,
Pagination,
@ -180,18 +181,12 @@ onMounted(() => {
placeholder="请选择性别"
allow-clear
class="w-full"
>
<SelectOption
v-for="dict in getDictOptions(
DICT_TYPE.SYSTEM_USER_SEX,
'number',
)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</SelectOption>
</Select>
:options="[
...getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number').map(
(dict) => ({ label: dict.label, value: dict.value as any }),
),
]"
/>
</FormItem>
<FormItem label="创建时间" name="createTime">
<RangePicker

View File

@ -7,15 +7,9 @@ import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import {
DatePicker,
Form,
Input,
message,
Radio,
RadioGroup,
} from 'antdv-next';
import { Form, FormItem, Input, message, Radio, RadioGroup } from 'antdv-next';
import { DatePicker } from '#/adapter/component/date-picker';
import {
createDemo01Contact,
getDemo01Contact,

View File

@ -13,7 +13,14 @@ import {
isEmpty,
} from '@vben/utils';
import { Button, DateRangePicker, Form, Input, message } from 'antdv-next';
import {
Button,
DateRangePicker,
Form,
FormItem,
Input,
message,
} from 'antdv-next';
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import {

View File

@ -6,7 +6,7 @@ import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { handleTree } from '@vben/utils';
import { Form, Input, message, TreeSelect } from 'antdv-next';
import { Form, FormItem, Input, message, TreeSelect } from 'antdv-next';
import {
createDemo02Category,

View File

@ -18,6 +18,7 @@ import {
import {
Button,
Form,
FormItem,
Input,
message,
Pagination,
@ -191,18 +192,12 @@ onMounted(() => {
placeholder="请选择性别"
allow-clear
class="w-full"
>
<SelectOption
v-for="dict in getDictOptions(
DICT_TYPE.SYSTEM_USER_SEX,
'number',
)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</SelectOption>
</Select>
:options="[
...getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number').map(
(dict) => ({ label: dict.label, value: dict.value as any }),
),
]"
/>
</FormItem>
<FormItem label="创建时间" name="createTime">
<RangePicker

View File

@ -5,7 +5,7 @@ import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Form, Input, message } from 'antdv-next';
import { Form, FormItem, Input, message } from 'antdv-next';
import {
createDemo03Course,

View File

@ -11,6 +11,7 @@ import { cloneDeep, formatDateTime, isEmpty } from '@vben/utils';
import {
Button,
Form,
FormItem,
Input,
message,
Pagination,

View File

@ -5,7 +5,7 @@ import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Form, Input, message } from 'antdv-next';
import { Form, FormItem, Input, message } from 'antdv-next';
import {
createDemo03Grade,

View File

@ -11,6 +11,7 @@ import { cloneDeep, formatDateTime, isEmpty } from '@vben/utils';
import {
Button,
Form,
FormItem,
Input,
message,
Pagination,

View File

@ -7,15 +7,9 @@ import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import {
DatePicker,
Form,
Input,
message,
Radio,
RadioGroup,
} from 'antdv-next';
import { Form, FormItem, Input, message, Radio, RadioGroup } from 'antdv-next';
import { DatePicker } from '#/adapter/component/date-picker';
import {
createDemo03Student,
getDemo03Student,

View File

@ -18,6 +18,7 @@ import {
import {
Button,
Form,
FormItem,
Input,
message,
Pagination,
@ -184,18 +185,12 @@ onMounted(() => {
placeholder="请选择性别"
allow-clear
class="w-full"
>
<SelectOption
v-for="dict in getDictOptions(
DICT_TYPE.SYSTEM_USER_SEX,
'number',
)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</SelectOption>
</Select>
:options="[
...getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number').map(
(dict) => ({ label: dict.label, value: dict.value as any }),
),
]"
/>
</FormItem>
<FormItem label="创建时间" name="createTime">
<RangePicker

View File

@ -3,7 +3,7 @@ import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
import { nextTick, ref, watch } from 'vue';
import { Form, Input } from 'antdv-next';
import { Form, FormItem, Input } from 'antdv-next';
import { getDemo03GradeByStudentId } from '#/api/infra/demo/demo03/normal';

View File

@ -8,8 +8,8 @@ import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import {
DatePicker,
Form,
FormItem,
Input,
message,
Radio,
@ -17,6 +17,7 @@ import {
Tabs,
} from 'antdv-next';
import { DatePicker } from '#/adapter/component/date-picker';
import {
createDemo03Student,
getDemo03Student,

View File

@ -18,6 +18,7 @@ import {
import {
Button,
Form,
FormItem,
Input,
message,
Pagination,
@ -181,18 +182,12 @@ onMounted(() => {
placeholder="请选择性别"
allow-clear
class="w-full"
>
<SelectOption
v-for="dict in getDictOptions(
DICT_TYPE.SYSTEM_USER_SEX,
'number',
)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</SelectOption>
</Select>
:options="[
...getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number').map(
(dict) => ({ label: dict.label, value: dict.value as any }),
),
]"
/>
</FormItem>
<FormItem label="创建时间" name="createTime">
<RangePicker

View File

@ -3,7 +3,7 @@ import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
import { nextTick, ref, watch } from 'vue';
import { Form, Input } from 'antdv-next';
import { Form, FormItem, Input } from 'antdv-next';
import { getDemo03GradeByStudentId } from '#/api/infra/demo/demo03/normal';

View File

@ -8,8 +8,8 @@ import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import {
DatePicker,
Form,
FormItem,
Input,
message,
Radio,
@ -17,6 +17,7 @@ import {
Tabs,
} from 'antdv-next';
import { DatePicker } from '#/adapter/component/date-picker';
import {
createDemo03Student,
getDemo03Student,

View File

@ -100,7 +100,15 @@ watchEffect(() => {
/** 发送消息 */
const sendText = ref(''); //
const sendUserId = ref('all'); //
const sendUserId = ref<'all' | number>('all'); //
const sendUserOptions = computed(() => [
{ label: '所有人', value: 'all', raw: null },
...userList.value.map((user) => ({
label: user.nickname,
value: user.id,
raw: user,
})),
]);
function handlerSend() {
if (!sendText.value.trim()) {
message.warning('消息内容不能为空');
@ -228,26 +236,20 @@ onMounted(async () => {
size="large"
placeholder="请选择接收人"
:disabled="!getIsOpen"
:options="sendUserOptions"
>
<SelectOption key="" value="" label="所有人">
<div class="flex items-center">
<template #optionRender="{ option }">
<div class="flex items-center" v-if="!option.data.raw">
<Avatar size="small" class="mr-2"></Avatar>
<span>所有人</span>
</div>
</SelectOption>
<SelectOption
v-for="user in userList"
:key="user.id"
:value="user.id"
:label="user.nickname"
>
<div class="flex items-center">
<div class="flex items-center" v-else>
<Avatar size="small" class="mr-2">
{{ user.nickname.slice(0, 1) }}
{{ option.data.raw.nickname?.slice(0, 1) }}
</Avatar>
<span>{{ user.nickname }}</span>
<span>{{ option.data.raw.nickname }}</span>
</div>
</SelectOption>
</template>
</Select>
<TextArea

View File

@ -15,7 +15,7 @@ import { DICT_TYPE, IotDeviceMessageMethodEnum } from '@vben/constants';
import { IconifyIcon } from '@vben/icons';
import { formatDateTime } from '@vben/utils';
import { Button, Select, SelectOption, Space, Switch, Tag } from 'antdv-next';
import { Button, Select, Space, Switch, Tag } from 'antdv-next';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDeviceMessagePage } from '#/api/iot/device/device';
@ -195,25 +195,18 @@ defineExpose({
allow-clear
placeholder="所有方法"
style="width: 160px"
>
<SelectOption
v-for="item in methodOptions"
:key="item.value"
:label="item.label"
:value="item.value"
>
{{ item.label }}
</SelectOption>
</Select>
:options="methodOptions"
/>
<Select
v-model:value="queryParams.upstream"
allow-clear
placeholder="上行/下行"
style="width: 160px"
>
<SelectOption label="上行" value="true">上行</SelectOption>
<SelectOption label="下行" value="false">下行</SelectOption>
</Select>
:options="[
{ label: '上行', value: 'true' },
{ label: '下行', value: 'false' },
]"
/>
<Space>
<Button type="primary" @click="handleQuery">
<IconifyIcon icon="ep:search" class="mr-[5px]" /> 搜索

View File

@ -14,14 +14,7 @@ import {
import { IconifyIcon } from '@vben/icons';
import { formatDateTime } from '@vben/utils';
import {
Button,
DateRangePicker,
Select,
SelectOption,
Space,
Tag,
} from 'antdv-next';
import { Button, DateRangePicker, Select, Space, Tag } from 'antdv-next';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
@ -217,15 +210,13 @@ defineExpose({
allow-clear
placeholder="请选择事件标识符"
style="width: 240px"
>
<SelectOption
v-for="event in eventThingModels"
:key="event.identifier"
:value="event.identifier!"
>
{{ event.name }}({{ event.identifier }})
</SelectOption>
</Select>
:options="[
...eventThingModels.map((event) => ({
label: `${event.name}(${event.identifier})`,
value: event.identifier!,
})),
]"
/>
</div>
<div class="flex items-center gap-2">
<span>时间范围</span>

View File

@ -14,14 +14,7 @@ import {
import { IconifyIcon } from '@vben/icons';
import { formatDateTime } from '@vben/utils';
import {
Button,
DateRangePicker,
Select,
SelectOption,
Space,
Tag,
} from 'antdv-next';
import { Button, DateRangePicker, Select, Space, Tag } from 'antdv-next';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
@ -231,15 +224,13 @@ defineExpose({
allow-clear
placeholder="请选择服务标识符"
style="width: 240px"
>
<SelectOption
v-for="service in serviceThingModels"
:key="service.identifier"
:value="service.identifier!"
>
{{ service.name }}({{ service.identifier }})
</SelectOption>
</Select>
:options="[
...serviceThingModels.map((service) => ({
label: `${service.name}(${service.identifier})`,
value: service.identifier!,
})),
]"
/>
</div>
<div class="flex items-center gap-2">
<span>时间范围</span>

View File

@ -15,16 +15,7 @@ import { getDictOptions } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons';
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
import {
Button,
Card,
Input,
message,
Select,
SelectOption,
Space,
Tag,
} from 'antdv-next';
import { Button, Card, Input, message, Select, Space, Tag } from 'antdv-next';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
@ -292,15 +283,9 @@ onMounted(async () => {
placeholder="请选择产品"
allow-clear
style="width: 200px"
>
<SelectOption
v-for="product in products"
:key="product.id"
:value="product.id"
>
{{ product.name }}
</SelectOption>
</Select>
:options="products"
:field-names="{ label: 'name', value: 'id' }"
/>
<Input
v-model:value="queryParams.deviceName"
placeholder="请输入 DeviceName"
@ -317,49 +302,34 @@ onMounted(async () => {
/>
<Select
v-model:value="queryParams.deviceType"
:options="
getDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE, 'number').map(
(dict) => ({ label: dict.label, value: dict.value as any }),
)
"
placeholder="请选择设备类型"
allow-clear
style="width: 200px"
>
<SelectOption
v-for="dict in getDictOptions(
DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE,
'number',
)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</SelectOption>
</Select>
/>
<Select
v-model:value="queryParams.status"
placeholder="请选择设备状态"
allow-clear
style="width: 200px"
>
<SelectOption
v-for="dict in getDictOptions(DICT_TYPE.IOT_DEVICE_STATE, 'number')"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</SelectOption>
</Select>
:options="
getDictOptions(DICT_TYPE.IOT_DEVICE_STATE, 'number').map(
(dict) => ({ label: dict.label, value: dict.value as any }),
)
"
/>
<Select
v-model:value="queryParams.groupId"
placeholder="请选择设备分组"
allow-clear
style="width: 200px"
>
<SelectOption
v-for="group in deviceGroups"
:key="group.id"
:value="group.id"
>
{{ group.name }}
</SelectOption>
</Select>
:options="deviceGroups"
:field-names="{ label: 'name', value: 'id' }"
/>
<Button type="primary" @click="handleSearch">
<IconifyIcon icon="ant-design:search-outlined" class="mr-1" />
{{ $t('common.search') }}

View File

@ -5,7 +5,7 @@ import { computed, onMounted, ref, watch } from 'vue';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { FormItem, Input, Select, SelectOption, TextArea } from 'antdv-next';
import { FormItem, Input, Select, TextArea } from 'antdv-next';
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
@ -67,10 +67,14 @@ onMounted(() => {
>
<Input v-model:value="urlPath" placeholder="请输入请求地址">
<template #addonBefore>
<Select v-model:value="urlPrefix" class="w-[100px]">
<SelectOption value="http://">http://</SelectOption>
<SelectOption value="https://">https://</SelectOption>
</Select>
<Select
v-model:value="urlPrefix"
class="w-[100px]"
:options="[
{ label: 'http://', value: 'http://' },
{ label: 'https://', value: 'https://' },
]"
/>
</template>
</Input>
</FormItem>
@ -81,12 +85,16 @@ onMounted(() => {
]"
label="请求方法"
>
<Select v-model:value="config.method" placeholder="请选择请求方法">
<SelectOption value="GET">GET</SelectOption>
<SelectOption value="POST">POST</SelectOption>
<SelectOption value="PUT">PUT</SelectOption>
<SelectOption value="DELETE">DELETE</SelectOption>
</Select>
<Select
v-model:value="config.method"
placeholder="请选择请求方法"
:options="[
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' },
]"
/>
</FormItem>
<FormItem label="请求头">
<KeyValueEditor v-model="config.headers" add-button-text="" />

View File

@ -4,14 +4,7 @@ import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import {
FormItem,
Input,
InputNumber,
Select,
SelectOption,
Switch,
} from 'antdv-next';
import { FormItem, Input, InputNumber, Select, Switch } from 'antdv-next';
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
@ -120,10 +113,14 @@ onMounted(() => {
]"
label="数据格式"
>
<Select v-model:value="config.dataFormat" placeholder="请选择数据格式">
<SelectOption value="JSON">JSON</SelectOption>
<SelectOption value="BINARY">BINARY</SelectOption>
</Select>
<Select
v-model:value="config.dataFormat"
placeholder="请选择数据格式"
:options="[
{ label: 'JSON', value: 'JSON' },
{ label: 'BINARY', value: 'BINARY' },
]"
/>
</FormItem>
<FormItem :name="['config', 'heartbeatIntervalMs']" label="心跳间隔(ms)">
<InputNumber

View File

@ -9,7 +9,6 @@ import {
Input,
InputNumber,
Select,
SelectOption,
Switch,
TextArea,
} from 'antdv-next';
@ -127,10 +126,14 @@ onMounted(() => {
]"
label="数据格式"
>
<Select v-model:value="config.dataFormat" placeholder="请选择数据格式">
<SelectOption value="JSON">JSON</SelectOption>
<SelectOption value="TEXT">TEXT</SelectOption>
</Select>
<Select
v-model:value="config.dataFormat"
placeholder="请选择数据格式"
:options="[
{ label: 'JSON', value: 'JSON' },
{ label: 'TEXT', value: 'TEXT' },
]"
/>
</FormItem>
<FormItem :name="['config', 'reconnectIntervalMs']" label="重连间隔(ms)">
<InputNumber

View File

@ -3,7 +3,7 @@
import { onMounted, ref } from 'vue';
import { useVModel } from '@vueuse/core';
import { FormItem, Select, SelectOption } from 'antdv-next';
import { FormItem, Select } from 'antdv-next';
import { getSimpleAlertConfigList } from '#/api/iot/alert/config';
@ -49,20 +49,15 @@ onMounted(() => {
<FormItem label="告警配置" required>
<Select
v-model:value="localValue"
:options="alertConfigs"
:field-names="{ label: 'name', value: 'id' }"
placeholder="请选择告警配置"
show-search
allow-clear
@change="handleChange"
class="w-full"
:loading="loading"
>
<SelectOption
v-for="config in alertConfigs"
:key="config.id"
:label="config.name"
:value="config.id"
/>
</Select>
/>
</FormItem>
</div>
</template>

View File

@ -12,7 +12,7 @@ import {
} from '@vben/constants';
import { useVModel } from '@vueuse/core';
import { Col, FormItem, Row, Select, SelectOption } from 'antdv-next';
import { Col, FormItem, Row, Select } from 'antdv-next';
import ValueInput from '../inputs/value-input.vue';
import DeviceSelector from '../selectors/device-selector.vue';
@ -163,6 +163,7 @@ function handleOperatorChange() {
<FormItem label="条件类型" required>
<Select
:value="condition.type"
:options="getConditionTypeOptions()"
@change="
(value: any) => {
updateConditionField('type', value);
@ -171,15 +172,7 @@ function handleOperatorChange() {
"
placeholder="请选择条件类型"
class="w-full"
>
<SelectOption
v-for="option in getConditionTypeOptions()"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</SelectOption>
</Select>
/>
</FormItem>
</Col>
</Row>
@ -225,18 +218,11 @@ function handleOperatorChange() {
<FormItem label="操作符" required>
<Select
:value="condition.operator"
:options="statusOperatorOptions"
@change="(value: any) => updateConditionField('operator', value)"
placeholder="请选择操作符"
class="w-full"
>
<SelectOption
v-for="option in statusOperatorOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</SelectOption>
</Select>
/>
</FormItem>
</Col>
@ -245,18 +231,11 @@ function handleOperatorChange() {
<FormItem label="设备状态" required>
<Select
:value="condition.param"
:options="deviceStatusOptions"
@change="(value: any) => updateConditionField('param', value)"
placeholder="请选择设备状态"
class="w-full"
>
<SelectOption
v-for="option in deviceStatusOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</SelectOption>
</Select>
/>
</FormItem>
</Col>
</Row>

View File

@ -1,7 +1,5 @@
<!-- 当前时间条件配置组件 -->
<script setup lang="ts">
import type { Dayjs } from 'dayjs';
import type { RuleSceneApi } from '#/api/iot/rule/scene';
import { computed, watch } from 'vue';
@ -17,7 +15,6 @@ import {
FormItem,
Row,
Select,
SelectOption,
Tag,
TimePicker,
} from 'antdv-next';
@ -130,7 +127,7 @@ function updateConditionField(field: any, value: any) {
* 处理第一个时间值变化
* @param value 时间值
*/
function handleTimeValueChange(value: Dayjs | null | string) {
function handleTimeValueChange(value: any) {
const normalized = formatDayjs(value);
const currentParams = condition.value.param
? condition.value.param.split(',')
@ -147,7 +144,7 @@ function handleTimeValueChange(value: Dayjs | null | string) {
* 处理第二个时间值变化
* @param value 时间值
*/
function handleTimeValue2Change(value: Dayjs | null | string) {
function handleTimeValue2Change(value: any) {
const normalized = formatDayjs(value);
const currentParams = condition.value.param
? condition.value.param.split(',')
@ -187,23 +184,22 @@ watch(
"
placeholder="请选择时间条件"
class="w-full"
:options="timeOperatorOptions"
>
<SelectOption
v-for="option in timeOperatorOptions"
:key="option.value"
:label="option.label"
:value="option.value"
>
<template #optionRender="{ option }">
<div class="flex w-full items-center justify-between">
<div class="flex items-center gap-2">
<IconifyIcon :icon="option.icon" :class="option.iconClass" />
<span>{{ option.label }}</span>
<IconifyIcon
:icon="option.data.icon"
:class="option.data.iconClass"
/>
<span>{{ option.data.label }}</span>
</div>
<Tag :color="option.tag">
{{ option.category }}
<Tag :color="option.data.tag">
{{ option.data.category }}
</Tag>
</div>
</SelectOption>
</template>
</Select>
</FormItem>
</Col>

View File

@ -13,7 +13,7 @@ import {
import { isObject } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Col, FormItem, Row, Select, SelectOption, Tag } from 'antdv-next';
import { Col, FormItem, Row, Select, Tag } from 'antdv-next';
import { getThingModelTSLByProductId } from '#/api/iot/thingmodel';
@ -39,6 +39,13 @@ const loadingThingModel = ref(false); // 物模型加载状态
const selectedService = ref<null | ThingModelApi.Service>(null); //
const serviceList = ref<ThingModelApi.Service[]>([]); //
const loadingServices = ref(false); //
const serviceOptions = computed(() =>
serviceList.value.map((service) => ({
label: service.name,
value: service.identifier,
raw: service,
})),
);
//
const paramsValue = computed({
@ -346,23 +353,22 @@ watch(
allow-clear
class="w-full"
:loading="loadingServices"
:options="serviceOptions"
option-filter-prop="label"
@change="handleServiceChange"
>
<SelectOption
v-for="service in serviceList"
:key="service.identifier"
:label="service.name"
:value="service.identifier"
>
<template #optionRender="{ option }">
<div class="flex items-center justify-between">
<span>{{ service.name }}</span>
<span>{{ option.data.raw.name }}</span>
<Tag
:color="service.callType === 'sync' ? 'processing' : 'success'"
:color="
option.data.raw.callType === 'sync' ? 'processing' : 'success'
"
>
{{ service.callType === 'sync' ? '同步' : '异步' }}
{{ option.data.raw.callType === 'sync' ? '同步' : '异步' }}
</Tag>
</div>
</SelectOption>
</template>
</Select>
</FormItem>

View File

@ -12,7 +12,7 @@ import {
} from '@vben/constants';
import { useVModel } from '@vueuse/core';
import { Col, FormItem, Input, Row, Select, SelectOption } from 'antdv-next';
import { Col, FormItem, Input, Row, Select } from 'antdv-next';
import JsonParamsInput from '../inputs/json-params-input.vue';
import ValueInput from '../inputs/value-input.vue';
@ -171,18 +171,11 @@ function handlePropertyChange(propertyInfo: any) {
<FormItem label="触发事件类型" required>
<Select
:value="triggerType"
:options="triggerTypeOptions"
@change="(value: any) => handleTriggerTypeChange(value)"
placeholder="请选择触发事件类型"
class="w-full"
>
<SelectOption
v-for="option in triggerTypeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</SelectOption>
</Select>
/>
</FormItem>
<!-- 设备属性条件配置 -->
@ -317,38 +310,31 @@ function handlePropertyChange(propertyInfo: any) {
<FormItem label="操作符" required>
<Select
:value="condition.operator"
:options="[
{
label:
IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS
.name,
value:
IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS
.value,
},
]"
@change="(value: any) => updateConditionField('operator', value)"
placeholder="请选择操作符"
class="w-full"
>
<SelectOption
:value="
IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
"
>
{{
IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.name
}}
</SelectOption>
</Select>
/>
</FormItem>
</Col>
<Col :span="6">
<FormItem label="参数" required>
<Select
:value="condition.value"
:options="deviceStatusChangeOptions"
@change="(value: any) => updateConditionField('value', value)"
placeholder="请选择操作符"
class="w-full"
>
<SelectOption
v-for="option in deviceStatusChangeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</SelectOption>
</Select>
/>
</FormItem>
</Col>
</Row>

View File

@ -14,7 +14,6 @@ import {
Input,
InputNumber,
Select,
SelectOption,
Tag,
Tooltip,
} from 'antdv-next';
@ -209,10 +208,11 @@ watch(
v-model:value="localValue"
placeholder="请选择布尔值"
class="w-full!"
>
<SelectOption value="true"> (true)</SelectOption>
<SelectOption value="false"> (false)</SelectOption>
</Select>
:options="[
{ label: '真 (true)', value: 'true' },
{ label: '假 (false)', value: 'false' },
]"
/>
<!-- 枚举值选择 -->
<Select
@ -220,17 +220,10 @@ watch(
propertyType === IoTDataSpecsDataTypeEnum.ENUM && enumOptions.length > 0
"
v-model:value="localValue"
:options="enumOptions"
placeholder="请选择枚举值"
class="w-full!"
>
<SelectOption
v-for="option in enumOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</SelectOption>
</Select>
/>
<!-- 范围输入 (between 操作符) -->
<div

View File

@ -11,15 +11,7 @@ import { IconifyIcon } from '@vben/icons';
import { getStableObjectKey } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import {
Button,
Card,
Empty,
FormItem,
Select,
SelectOption,
Tag,
} from 'antdv-next';
import { Button, Card, Empty, FormItem, Select, Tag } from 'antdv-next';
import AlertConfig from '../configs/alert-config.vue';
import DeviceControlConfig from '../configs/device-control-config.vue';
@ -239,18 +231,11 @@ function onActionTypeChange(action: RuleSceneApi.Action, type: number) {
<FormItem label="执行类型" required>
<Select
:value="action.type"
:options="getActionTypeOptions()"
@change="(value) => updateActionType(index, value as number)"
placeholder="请选择执行类型"
class="w-full"
>
<SelectOption
v-for="option in getActionTypeOptions()"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</SelectOption>
</Select>
/>
</FormItem>
</div>

View File

@ -1,10 +1,10 @@
<!-- 设备选择器组件 -->
<script setup lang="ts">
import { ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { DEVICE_SELECTOR_OPTIONS, DICT_TYPE } from '@vben/constants';
import { Select, SelectOption } from 'antdv-next';
import { Select } from 'antdv-next';
import { getDeviceListByProductId } from '#/api/iot/device/device';
import { DictTag } from '#/components/dict-tag';
@ -24,6 +24,13 @@ const emit = defineEmits<{
const deviceLoading = ref(false); //
const deviceList = ref<any[]>([]); //
const deviceOptions = computed(() =>
deviceList.value.map((device) => ({
label: device.deviceName,
value: device.id,
raw: device,
})),
);
/** 处理选择变化事件 */
function handleChange(value: any) {
@ -90,28 +97,28 @@ watch(
allow-clear
class="w-full"
option-label-prop="label"
option-filter-prop="label"
:loading="deviceLoading"
:disabled="!productId"
:options="deviceOptions"
>
<SelectOption
v-for="device in deviceList"
:key="device.id"
:label="device.deviceName"
:value="device.id"
>
<template #optionRender="{ option }">
<div class="py-[4px] flex w-full items-center justify-between">
<div class="flex-1">
<div class="text-[14px] font-medium mb-[2px] text-foreground">
{{ device.deviceName }}
{{ option.data.raw.deviceName }}
</div>
<div class="text-[12px] text-muted-foreground">
{{ device.deviceKey }}
{{ option.data.raw.deviceKey }}
</div>
</div>
<div class="gap-[4px] flex items-center" v-if="device.id > 0">
<DictTag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="device.state" />
<div class="gap-[4px] flex items-center" v-if="option.data.raw.id > 0">
<DictTag
:type="DICT_TYPE.IOT_DEVICE_STATE"
:value="option.data.raw.state"
/>
</div>
</div>
</SelectOption>
</template>
</Select>
</template>

View File

@ -8,7 +8,7 @@ import {
} from '@vben/constants';
import { useVModel } from '@vueuse/core';
import { Select, SelectOption } from 'antdv-next';
import { Select } from 'antdv-next';
/** 操作符选择器组件 */
defineOptions({ name: 'OperatorSelector' });
@ -204,6 +204,13 @@ const availableOperators = computed(() => {
(op.supportedTypes as any[]).includes(props.propertyType || ''),
);
});
const availableOperatorOptions = computed(() =>
availableOperators.value.map((operator) => ({
label: operator.label,
value: operator.value,
raw: operator,
})),
);
//
const selectedOperator = computed(() => {
@ -244,29 +251,25 @@ watch(
@change="handleChange"
class="w-full"
option-label-prop="label"
:options="availableOperatorOptions"
>
<SelectOption
v-for="operator in availableOperators"
:key="operator.value"
:label="operator.label"
:value="operator.value"
>
<template #optionRender="{ option }">
<div class="py-[4px] flex w-full items-center justify-between">
<div class="gap-[8px] flex items-center">
<div class="text-[14px] font-medium text-foreground">
{{ operator.label }}
{{ option.data.raw.label }}
</div>
<div
class="text-[12px] px-[6px] py-[2px] rounded-[4px] bg-primary-light-9 font-mono text-primary"
>
{{ operator.symbol }}
{{ option.data.raw.symbol }}
</div>
</div>
<div class="text-[12px] text-muted-foreground">
{{ operator.description }}
{{ option.data.raw.description }}
</div>
</div>
</SelectOption>
</template>
</Select>
</div>
</template>

View File

@ -1,10 +1,10 @@
<!-- 产品选择器组件 -->
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { Select, SelectOption } from 'antdv-next';
import { Select } from 'antdv-next';
import { getSimpleProductList } from '#/api/iot/product/product';
import { DictTag } from '#/components/dict-tag';
@ -23,6 +23,13 @@ const emit = defineEmits<{
const productLoading = ref(false); //
const productList = ref<any[]>([]); //
const productOptions = computed(() =>
productList.value.map((product) => ({
label: product.name,
value: product.id,
raw: product,
})),
);
/**
* 处理选择变化事件
@ -62,25 +69,25 @@ onMounted(() => {
allow-clear
class="w-full"
option-label-prop="label"
option-filter-prop="label"
:loading="productLoading"
:options="productOptions"
>
<SelectOption
v-for="product in productList"
:key="product.id"
:label="product.name"
:value="product.id"
>
<template #optionRender="{ option }">
<div class="py-[4px] flex w-full items-center justify-between">
<div class="flex-1">
<div class="text-[14px] font-medium mb-[2px] text-foreground">
{{ product.name }}
{{ option.data.raw.name }}
</div>
<div class="text-[12px] text-muted-foreground">
{{ product.productKey }}
{{ option.data.raw.productKey }}
</div>
</div>
<DictTag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" />
<DictTag
:type="DICT_TYPE.IOT_PRODUCT_STATUS"
:value="option.data.raw.status"
/>
</div>
</SelectOption>
</template>
</Select>
</template>

View File

@ -16,14 +16,7 @@ import {
import { IconifyIcon } from '@vben/icons';
import { useVModel } from '@vueuse/core';
import {
Button,
Popover,
Select,
SelectOptGroup,
SelectOption,
Tag,
} from 'antdv-next';
import { Button, Popover, Select, Tag } from 'antdv-next';
import { getThingModelTSLByProductId } from '#/api/iot/thingmodel';
@ -99,6 +92,16 @@ const propertyGroups = computed(() => {
);
return options.length > 0 ? [{ label: config.label, options }] : [];
});
const propertySelectOptions = computed(() =>
propertyGroups.value.map((group) => ({
label: group.label,
options: group.options.map((property) => ({
label: property.name,
value: property.identifier,
raw: property,
})),
})),
);
/** 当前选中的属性 */
const selectedProperty = computed(() =>
@ -224,31 +227,20 @@ watch(
@change="handleChange"
class="!w-[150px]"
option-label-prop="label"
option-filter-prop="label"
:loading="loading"
:options="propertySelectOptions"
>
<SelectOptGroup
v-for="group in propertyGroups"
:key="group.label"
:label="group.label"
>
<SelectOption
v-for="property in group.options"
:key="property.identifier"
:label="property.name"
:value="property.identifier"
>
<div class="py-[2px] flex w-full items-center justify-between">
<span
class="text-[14px] font-medium flex-1 truncate text-foreground"
>
{{ property.name }}
</span>
<Tag class="ml-[8px] flex-shrink-0">
{{ property.identifier }}
</Tag>
</div>
</SelectOption>
</SelectOptGroup>
<template #optionRender="{ option }">
<div class="py-[2px] flex w-full items-center justify-between">
<span class="text-[14px] font-medium flex-1 truncate text-foreground">
{{ option.data.raw.name }}
</span>
<Tag class="ml-[8px] flex-shrink-0">
{{ option.data.raw.identifier }}
</Tag>
</div>
</template>
</Select>
<!-- 属性详情弹出层 -->

View File

@ -122,23 +122,21 @@ function normalizeFormData(result: any): RuleSceneApi.SceneRule {
}
/** 触发器校验 */
function validateTriggers(_rule: any, value: any, callback: any) {
function validateTriggers(_rule: any, value: any) {
const error = validateSceneRuleTriggers(value);
if (error) {
callback(new Error(error));
return;
return Promise.reject(new Error(error));
}
callback();
return Promise.resolve();
}
/** 执行器校验 */
function validateActions(_rule: any, value: any, callback: any) {
function validateActions(_rule: any, value: any) {
const error = validateSceneRuleActions(value);
if (error) {
callback(new Error(error));
return;
return Promise.reject(new Error(error));
}
callback();
return Promise.resolve();
}
const formRules = reactive({

View File

@ -35,51 +35,44 @@ function deleteEnum(index: number) {
}
/** 校验单项枚举值:必填、数字、不重复 */
function validateEnumValue(_rule: any, value: any, callback: any) {
function validateEnumValue(_rule: any, value: any) {
if (isEmpty(value)) {
callback(new Error('枚举值不能为空'));
return;
return Promise.reject(new Error('枚举值不能为空'));
}
if (Number.isNaN(Number(value))) {
callback(new Error('枚举值必须是数字'));
return;
return Promise.reject(new Error('枚举值必须是数字'));
}
const sameCount = dataSpecsList.value.filter(
(it) => it.value === value,
).length;
if (sameCount > 1) {
callback(new Error('枚举值不能重复'));
return;
return Promise.reject(new Error('枚举值不能重复'));
}
callback();
return Promise.resolve();
}
/** 校验整个枚举列表:非空、无空项、无非法数字、无重复 */
function validateEnumList(_rule: any, _value: any, callback: any) {
function validateEnumList(_rule: any, _value: any) {
if (isEmpty(dataSpecsList.value)) {
callback(new Error('请至少添加一个枚举项'));
return;
return Promise.reject(new Error('请至少添加一个枚举项'));
}
const hasEmpty = dataSpecsList.value.some(
(item) => isEmpty(item.value) || isEmpty(item.name),
);
if (hasEmpty) {
callback(new Error('存在未填写的枚举值或描述'));
return;
return Promise.reject(new Error('存在未填写的枚举值或描述'));
}
const hasInvalidNumber = dataSpecsList.value.some((item) =>
Number.isNaN(Number(item.value)),
);
if (hasInvalidNumber) {
callback(new Error('存在非数字的枚举值'));
return;
return Promise.reject(new Error('存在非数字的枚举值'));
}
const values = dataSpecsList.value.map((item) => item.value);
if (new Set(values).size !== values.length) {
callback(new Error('存在重复的枚举值'));
return;
return Promise.reject(new Error('存在重复的枚举值'));
}
callback();
return Promise.resolve();
}
</script>

View File

@ -10,7 +10,7 @@ import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { useVModel } from '@vueuse/core';
import { FormItem, Input, Select, SelectOption } from 'antdv-next';
import { FormItem, Input, Select } from 'antdv-next';
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
@ -36,53 +36,46 @@ function unitChange(unitSpecs: any) {
}
/** 校验最小值 */
function validateMin(_rule: any, _value: any, callback: any) {
function validateMin(_rule: any, _value: any) {
const min = Number(dataSpecs.value.min);
const max = Number(dataSpecs.value.max);
if (Number.isNaN(min)) {
callback(new Error('请输入有效的数值'));
return;
return Promise.reject(new Error('请输入有效的数值'));
}
if (!Number.isNaN(max) && min >= max) {
callback(new Error('最小值必须小于最大值'));
return;
return Promise.reject(new Error('最小值必须小于最大值'));
}
callback();
return Promise.resolve();
}
/** 校验最大值 */
function validateMax(_rule: any, _value: any, callback: any) {
function validateMax(_rule: any, _value: any) {
const min = Number(dataSpecs.value.min);
const max = Number(dataSpecs.value.max);
if (Number.isNaN(max)) {
callback(new Error('请输入有效的数值'));
return;
return Promise.reject(new Error('请输入有效的数值'));
}
if (!Number.isNaN(min) && max <= min) {
callback(new Error('最大值必须大于最小值'));
return;
return Promise.reject(new Error('最大值必须大于最小值'));
}
callback();
return Promise.resolve();
}
/** 校验步长 */
function validateStep(_rule: any, _value: any, callback: any) {
function validateStep(_rule: any, _value: any) {
const step = Number(dataSpecs.value.step);
if (Number.isNaN(step)) {
callback(new Error('请输入有效的数值'));
return;
return Promise.reject(new Error('请输入有效的数值'));
}
if (step <= 0) {
callback(new Error('步长必须大于 0'));
return;
return Promise.reject(new Error('步长必须大于 0'));
}
const min = Number(dataSpecs.value.min);
const max = Number(dataSpecs.value.max);
if (!Number.isNaN(min) && !Number.isNaN(max) && step > max - min) {
callback(new Error('步长不能大于最大值与最小值的差值'));
return;
return Promise.reject(new Error('步长不能大于最大值与最小值的差值'));
}
callback();
return Promise.resolve();
}
</script>
@ -133,15 +126,13 @@ function validateStep(_rule: any, _value: any, callback: any) {
placeholder="请选择单位"
class="w-full"
@change="unitChange"
>
<SelectOption
v-for="(item, index) in unitOptions"
:key="index"
:value="`${item.label}-${item.value}`"
>
{{ `${item.label}-${item.value}` }}
</SelectOption>
</Select>
:options="[
...unitOptions.map((item) => ({
label: `{{ \`${item.label}-${item.value}\` }}`,
value: `${item.label}-${item.value}`,
})),
]"
/>
</FormItem>
</template>

View File

@ -33,12 +33,11 @@ const structFormRef = ref();
const formData = ref<any>(buildEmptyFormData());
/** 校验结构体属性对象非空 */
function validateStructSpecsList(_rule: any, _value: any, callback: any) {
function validateStructSpecsList(_rule: any, _value: any) {
if (isEmpty(dataSpecsList.value)) {
callback(new Error('请至少添加一个结构体属性对象'));
return;
return Promise.reject(new Error('请至少添加一个结构体属性对象'));
}
callback();
return Promise.resolve();
}
const [Modal, modalApi] = useVbenModal({

View File

@ -14,14 +14,7 @@ import {
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import {
FormItem,
Input,
Radio,
RadioGroup,
Select,
SelectOption,
} from 'antdv-next';
import { FormItem, Input, Radio, RadioGroup, Select } from 'antdv-next';
import { ThingModelFormRules, validateBoolName } from '#/api/iot/thingmodel';
@ -118,16 +111,13 @@ if (!props.isStructDataSpecs && !props.isParams) {
v-model:value="property.dataType"
placeholder="请选择数据类型"
@change="handleChange"
>
<!-- ARRAY STRUCT 类型数据相互嵌套时最多支持递归嵌套 2 父和子 -->
<SelectOption
v-for="option in dataTypeOptions"
:key="option.value"
:value="option.value"
>
{{ `${option.value}(${option.label})` }}
</SelectOption>
</Select>
:options="[
...dataTypeOptions.map((option) => ({
label: `{{ \`${option.value}(${option.label})\` }}`,
value: option.value,
})),
]"
/>
</FormItem>
<!-- 数值型配置 -->
<ThingModelNumberDataSpecs

View File

@ -204,15 +204,9 @@ async function getAttributeOptions(propertyId: number) {
@blur="handleInputConfirm(index, attribute.id)"
@change="handleInputConfirm(index, attribute.id)"
@keyup.enter="handleInputConfirm(index, attribute.id)"
>
<SelectOption
v-for="item2 in attributeOptions"
:key="item2.id"
:value="item2.name"
>
{{ item2.name }}
</SelectOption>
</Select>
:options="attributeOptions"
:field-names="{ label: 'name', value: 'name' }"
/>
<Tag
v-show="!inputVisible(index)"
@click="showInput(index)"

View File

@ -6,7 +6,7 @@ import { ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import { Collapse } from 'antdv-next';
import { Collapse, CollapsePanel } from 'antdv-next';
import draggable from 'vuedraggable';
import { componentConfigs } from './mobile/index';

View File

@ -11,7 +11,6 @@ import {
RadioButton,
RadioGroup,
Select,
SelectOption,
} from 'antdv-next';
import UploadImg from '#/components/upload/image-upload.vue';
@ -29,6 +28,11 @@ defineOptions({ name: 'TabBarProperty' });
const props = defineProps<{ modelValue: TabBarProperty }>();
const emit = defineEmits(['update:modelValue']);
const formData = useVModel(props, 'modelValue', emit);
const themeOptions = THEME_LIST.map((theme) => ({
label: theme.name,
value: theme.id,
raw: theme,
}));
//
component.property.items = formData.value.items;
@ -51,18 +55,20 @@ const handleThemeChange = () => {
:wrapper-col="{ span: 18 }"
>
<FormItem label="主题" name="theme">
<Select v-model:value="formData!.theme" @change="handleThemeChange">
<SelectOption
v-for="(theme, index) in THEME_LIST"
:key="index"
:label="theme.name"
:value="theme.id"
>
<Select
v-model:value="formData!.theme"
:options="themeOptions"
@change="handleThemeChange"
>
<template #optionRender="{ option }">
<div class="flex items-center justify-between">
<IconifyIcon :icon="theme.icon" :color="theme.color" />
<span>{{ theme.name }}</span>
<IconifyIcon
:icon="option.data.raw.icon"
:color="option.data.raw.color"
/>
<span>{{ option.data.raw.name }}</span>
</div>
</SelectOption>
</template>
</Select>
</FormItem>
<FormItem label="默认颜色">

View File

@ -11,7 +11,7 @@ import { IconifyIcon } from '@vben/icons';
import { useAccessStore } from '@vben/stores';
import { isEmpty } from '@vben/utils';
import { message, RadioGroup } from 'antdv-next';
import { message, RadioButton, RadioGroup } from 'antdv-next';
import { updateDiyPageProperty } from '#/api/mall/promotion/diy/page';
import {

View File

@ -54,8 +54,17 @@ function handleSearch(value: string) {
}
/** 处理节点点击:支持点击同一节点取消选中 */
function handleSelect(_selectedKeys: any[], info: any) {
const row = info.node.dataRef as MesMdItemTypeApi.ItemType;
function handleSelect(selectedNodeKeys: any[], info: any) {
const selectedKey = selectedNodeKeys[0] ?? info.node?.id ?? info.node?.key;
const row = itemTypeList.value.find(
(item) => String(item.id) === String(selectedKey),
);
if (!row) {
currentNodeId.value = undefined;
selectedKeys.value = [];
emit('nodeClick', undefined);
return;
}
if (currentNodeId.value === row.id) {
currentNodeId.value = undefined;
selectedKeys.value = [];

Some files were not shown because too many files have changed in this diff Show More