Merge remote-tracking branch 'remote/master'
# Conflicts: # packages/effects/plugins/src/echarts/use-echarts.tspull/175/head
commit
d2a2227c4c
|
@ -19,11 +19,9 @@ Project maintainers have the right and responsibility to remove, edit, or reject
|
|||
- Checkout a topic branch from the relevant branch, e.g. main, and merge back against that branch.
|
||||
|
||||
- If adding a new feature:
|
||||
|
||||
- Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it.
|
||||
|
||||
- If fixing bug:
|
||||
|
||||
- Provide a detailed description of the bug in the PR. Live demo preferred.
|
||||
|
||||
- It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.
|
||||
|
|
|
@ -8,13 +8,7 @@ import type { Component } from 'vue';
|
|||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import {
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
ref,
|
||||
} from 'vue';
|
||||
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
@ -85,16 +79,15 @@ const withDefaultPlaceholder = <T extends Component>(
|
|||
$t(`ui.placeholder.${type}`);
|
||||
// 透传组件暴露的方法
|
||||
const innerRef = ref();
|
||||
const publicApi: Recordable<any> = {};
|
||||
expose(publicApi);
|
||||
const instance = getCurrentInstance();
|
||||
instance?.proxy?.$nextTick(() => {
|
||||
for (const key in innerRef.value) {
|
||||
if (typeof innerRef.value[key] === 'function') {
|
||||
publicApi[key] = innerRef.value[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
expose(
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, key) => innerRef.value?.[key],
|
||||
has: (_target, key) => key in (innerRef.value || {}),
|
||||
},
|
||||
),
|
||||
);
|
||||
return () =>
|
||||
h(
|
||||
component,
|
||||
|
|
|
@ -7,6 +7,8 @@ export namespace BpmProcessDefinitionApi {
|
|||
export interface ProcessDefinition {
|
||||
id: string;
|
||||
version: number;
|
||||
name: string;
|
||||
description: string;
|
||||
deploymentTime: number;
|
||||
suspensionState: number;
|
||||
modelType: number;
|
||||
|
@ -15,6 +17,7 @@ export namespace BpmProcessDefinitionApi {
|
|||
bpmnXml?: string;
|
||||
simpleModel?: string;
|
||||
formFields?: string[];
|
||||
icon?: string;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ export namespace BpmProcessInstanceApi {
|
|||
candidateStrategy?: BpmCandidateStrategyEnum;
|
||||
candidateUsers?: User[];
|
||||
endTime?: Date;
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
nodeType: BpmNodeTypeEnum;
|
||||
startTime?: Date;
|
||||
|
|
|
@ -186,6 +186,12 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
|||
});
|
||||
|
||||
const buildSelect = () => {
|
||||
const {
|
||||
modelValue,
|
||||
'onUpdate:modelValue': onUpdateModelValue,
|
||||
...restAttrs
|
||||
} = attrs;
|
||||
|
||||
if (props.multiple) {
|
||||
// fix:多写此步是为了解决 multiple 属性问题
|
||||
return (
|
||||
|
@ -193,7 +199,9 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
|||
class="w-full"
|
||||
loading={loading.value}
|
||||
mode="multiple"
|
||||
{...attrs}
|
||||
onUpdate:value={onUpdateModelValue as any}
|
||||
value={modelValue as any}
|
||||
{...restAttrs}
|
||||
// TODO: remote 对等实现
|
||||
// remote={props.remote}
|
||||
{...(props.remote && { remoteMethod })}
|
||||
|
@ -212,7 +220,9 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
|||
<Select
|
||||
class="w-full"
|
||||
loading={loading.value}
|
||||
{...attrs}
|
||||
onUpdate:value={onUpdateModelValue as any}
|
||||
value={modelValue as any}
|
||||
{...restAttrs}
|
||||
// TODO: @dhb52 remote 对等实现, 还是说没作用
|
||||
// remote={props.remote}
|
||||
{...(props.remote && { remoteMethod })}
|
||||
|
@ -228,6 +238,11 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
|||
);
|
||||
};
|
||||
const buildCheckbox = () => {
|
||||
const {
|
||||
modelValue,
|
||||
'onUpdate:modelValue': onUpdateModelValue,
|
||||
...restAttrs
|
||||
} = attrs;
|
||||
if (isEmpty(options.value)) {
|
||||
options.value = [
|
||||
{ label: '选项1', value: '选项1' },
|
||||
|
@ -235,7 +250,12 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
|||
];
|
||||
}
|
||||
return (
|
||||
<CheckboxGroup class="w-full" {...attrs}>
|
||||
<CheckboxGroup
|
||||
class="w-full"
|
||||
onUpdate:value={onUpdateModelValue as any}
|
||||
value={modelValue as any}
|
||||
{...restAttrs}
|
||||
>
|
||||
{options.value.map(
|
||||
(item: { label: any; value: any }, index: any) => (
|
||||
<Checkbox key={index} value={item.value}>
|
||||
|
@ -247,6 +267,11 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
|||
);
|
||||
};
|
||||
const buildRadio = () => {
|
||||
const {
|
||||
modelValue,
|
||||
'onUpdate:modelValue': onUpdateModelValue,
|
||||
...restAttrs
|
||||
} = attrs;
|
||||
if (isEmpty(options.value)) {
|
||||
options.value = [
|
||||
{ label: '选项1', value: '选项1' },
|
||||
|
@ -254,7 +279,12 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
|||
];
|
||||
}
|
||||
return (
|
||||
<RadioGroup class="w-full" {...attrs}>
|
||||
<RadioGroup
|
||||
class="w-full"
|
||||
onUpdate:value={onUpdateModelValue as any}
|
||||
value={modelValue as any}
|
||||
{...restAttrs}
|
||||
>
|
||||
{options.value.map(
|
||||
(item: { label: any; value: any }, index: any) => (
|
||||
<Radio key={index} value={item.value}>
|
||||
|
|
|
@ -22,7 +22,7 @@ const md = new MarkdownIt({
|
|||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
const copyHtml = `<div id="copy" data-copy='${str}' style="position: absolute; right: 10px; top: 5px; color: #fff;cursor: pointer;">复制</div>`;
|
||||
return `<pre style="position: relative;">${copyHtml}<code class="hljs">${hljs.highlight(lang, str, true).value}</code></pre>`;
|
||||
return `<pre style="position: relative;">${copyHtml}<code class="hljs">${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}</code></pre>`;
|
||||
} catch {}
|
||||
}
|
||||
return ``;
|
||||
|
|
|
@ -0,0 +1,875 @@
|
|||
<script setup lang="ts">
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
|
||||
import type { IOParameter, SimpleFlowNode } from '../../consts';
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputNumber,
|
||||
Radio,
|
||||
RadioButton,
|
||||
RadioGroup,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
Switch,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { getFormDetail } from '#/api/bpm/form';
|
||||
import { getModelList } from '#/api/bpm/model';
|
||||
import { BpmNodeTypeEnum } from '#/utils';
|
||||
|
||||
import {
|
||||
CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE,
|
||||
CHILD_PROCESS_START_USER_EMPTY_TYPE,
|
||||
CHILD_PROCESS_START_USER_TYPE,
|
||||
ChildProcessMultiInstanceSourceTypeEnum,
|
||||
ChildProcessStartUserEmptyTypeEnum,
|
||||
ChildProcessStartUserTypeEnum,
|
||||
DELAY_TYPE,
|
||||
DelayTypeEnum,
|
||||
TIME_UNIT_TYPES,
|
||||
TimeUnitType,
|
||||
} from '../../consts';
|
||||
import {
|
||||
parseFormFields,
|
||||
useFormFields,
|
||||
useNodeName,
|
||||
useWatchNode,
|
||||
} from '../../helpers';
|
||||
import { convertTimeUnit } from './utils';
|
||||
|
||||
defineOptions({ name: 'ChildProcessNodeConfig' });
|
||||
|
||||
const props = defineProps<{
|
||||
flowNode: SimpleFlowNode;
|
||||
}>();
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
header: true,
|
||||
closable: true,
|
||||
title: '',
|
||||
onConfirm() {
|
||||
saveConfig();
|
||||
},
|
||||
});
|
||||
|
||||
// 当前节点
|
||||
const currentNode = useWatchNode(props);
|
||||
/** 节点名称配置 */
|
||||
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
|
||||
useNodeName(BpmNodeTypeEnum.CHILD_PROCESS_NODE);
|
||||
// 激活的 Tab 标签页
|
||||
const activeTabName = ref('child');
|
||||
// 子流程表单配置
|
||||
const formRef = ref(); // 表单 Ref
|
||||
// 表单校验规则
|
||||
const formRules: Record<string, Rule[]> = reactive({
|
||||
async: [{ required: true, message: '是否异步不能为空', trigger: 'change' }],
|
||||
calledProcessDefinitionKey: [
|
||||
{ required: true, message: '子流程不能为空', trigger: 'change' },
|
||||
],
|
||||
skipStartUserNode: [
|
||||
{
|
||||
required: true,
|
||||
message: '是否自动跳过子流程发起节点不能为空',
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
startUserType: [
|
||||
{ required: true, message: '子流程发起人不能为空', trigger: 'change' },
|
||||
],
|
||||
startUserEmptyType: [
|
||||
{
|
||||
required: true,
|
||||
message: '当子流程发起人为空时不能为空',
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
startUserFormField: [
|
||||
{ required: true, message: '子流程发起人字段不能为空', trigger: 'change' },
|
||||
],
|
||||
timeoutEnable: [
|
||||
{ required: true, message: '超时设置是否开启不能为空', trigger: 'change' },
|
||||
],
|
||||
timeoutType: [
|
||||
{ required: true, message: '超时设置时间不能为空', trigger: 'change' },
|
||||
],
|
||||
timeDuration: [
|
||||
{ required: true, message: '超时设置时间不能为空', trigger: 'change' },
|
||||
],
|
||||
dateTime: [
|
||||
{ required: true, message: '超时设置时间不能为空', trigger: 'change' },
|
||||
],
|
||||
multiInstanceEnable: [
|
||||
{ required: true, message: '多实例设置不能为空', trigger: 'change' },
|
||||
],
|
||||
sequential: [
|
||||
{ required: true, message: '是否串行不能为空', trigger: 'change' },
|
||||
],
|
||||
multiInstanceSourceType: [
|
||||
{ required: true, message: '实例数量不能为空', trigger: 'change' },
|
||||
],
|
||||
approveRatio: [
|
||||
{ required: true, message: '完成比例不能为空', trigger: 'change' },
|
||||
],
|
||||
});
|
||||
|
||||
type ChildProcessFormType = {
|
||||
approveRatio: number;
|
||||
async: boolean;
|
||||
calledProcessDefinitionKey: string;
|
||||
dateTime: string;
|
||||
inVariables?: IOParameter[];
|
||||
multiInstanceEnable: boolean;
|
||||
multiInstanceSource: string;
|
||||
multiInstanceSourceType: ChildProcessMultiInstanceSourceTypeEnum;
|
||||
outVariables?: IOParameter[];
|
||||
sequential: boolean;
|
||||
skipStartUserNode: boolean;
|
||||
startUserEmptyType: ChildProcessStartUserEmptyTypeEnum;
|
||||
startUserFormField: string;
|
||||
startUserType: ChildProcessStartUserTypeEnum;
|
||||
timeDuration: number;
|
||||
timeoutEnable: boolean;
|
||||
timeoutType: DelayTypeEnum;
|
||||
timeUnit: TimeUnitType;
|
||||
};
|
||||
|
||||
const configForm = ref<ChildProcessFormType>({
|
||||
async: false,
|
||||
calledProcessDefinitionKey: '',
|
||||
skipStartUserNode: false,
|
||||
inVariables: [],
|
||||
outVariables: [],
|
||||
startUserType: ChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER,
|
||||
startUserEmptyType:
|
||||
ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER,
|
||||
startUserFormField: '',
|
||||
timeoutEnable: false,
|
||||
timeoutType: DelayTypeEnum.FIXED_TIME_DURATION,
|
||||
timeDuration: 1,
|
||||
timeUnit: TimeUnitType.HOUR,
|
||||
dateTime: '',
|
||||
multiInstanceEnable: false,
|
||||
sequential: false,
|
||||
approveRatio: 100,
|
||||
multiInstanceSourceType:
|
||||
ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY,
|
||||
multiInstanceSource: '',
|
||||
});
|
||||
|
||||
const childProcessOptions = ref<any[]>([]);
|
||||
// 主流程表单字段选项
|
||||
const formFieldOptions = useFormFields();
|
||||
/** 子流程发起人表单可选项 : 只有用户选择组件字段才能被选择 */
|
||||
const startUserFormFieldOptions = computed(() => {
|
||||
return formFieldOptions.filter((item) => item.type === 'UserSelect');
|
||||
});
|
||||
// 数字表单字段选项
|
||||
const digitalFormFieldOptions = computed(() => {
|
||||
return formFieldOptions.filter((item) => item.type === 'inputNumber');
|
||||
});
|
||||
// 多选表单字段选项
|
||||
const multiFormFieldOptions = computed(() => {
|
||||
return formFieldOptions.filter(
|
||||
(item) => item.type === 'select' || item.type === 'checkbox',
|
||||
);
|
||||
});
|
||||
const childFormFieldOptions = ref<any[]>([]);
|
||||
|
||||
/** 保存配置 */
|
||||
const saveConfig = async () => {
|
||||
activeTabName.value = 'child';
|
||||
if (!formRef.value) return false;
|
||||
|
||||
const valid = await formRef.value.validate().catch(() => false);
|
||||
if (!valid) return false;
|
||||
|
||||
const childInfo = childProcessOptions.value.find(
|
||||
(option) => option.key === configForm.value.calledProcessDefinitionKey,
|
||||
);
|
||||
|
||||
currentNode.value.name = nodeName.value!;
|
||||
if (currentNode.value.childProcessSetting) {
|
||||
// 1. 是否异步
|
||||
currentNode.value.childProcessSetting.async = configForm.value.async;
|
||||
// 2. 调用流程
|
||||
currentNode.value.childProcessSetting.calledProcessDefinitionKey =
|
||||
childInfo.key;
|
||||
currentNode.value.childProcessSetting.calledProcessDefinitionName =
|
||||
childInfo.name;
|
||||
// 3. 是否跳过发起人
|
||||
currentNode.value.childProcessSetting.skipStartUserNode =
|
||||
configForm.value.skipStartUserNode;
|
||||
// 4. 主->子变量
|
||||
currentNode.value.childProcessSetting.inVariables =
|
||||
configForm.value.inVariables;
|
||||
// 5. 子->主变量
|
||||
currentNode.value.childProcessSetting.outVariables =
|
||||
configForm.value.outVariables;
|
||||
// 6. 发起人设置
|
||||
currentNode.value.childProcessSetting.startUserSetting.type =
|
||||
configForm.value.startUserType;
|
||||
currentNode.value.childProcessSetting.startUserSetting.emptyType =
|
||||
configForm.value.startUserEmptyType;
|
||||
currentNode.value.childProcessSetting.startUserSetting.formField =
|
||||
configForm.value.startUserFormField;
|
||||
// 7. 超时设置
|
||||
currentNode.value.childProcessSetting.timeoutSetting = {
|
||||
enable: configForm.value.timeoutEnable,
|
||||
};
|
||||
if (configForm.value.timeoutEnable) {
|
||||
currentNode.value.childProcessSetting.timeoutSetting.type =
|
||||
configForm.value.timeoutType;
|
||||
if (configForm.value.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
||||
currentNode.value.childProcessSetting.timeoutSetting.timeExpression =
|
||||
getIsoTimeDuration();
|
||||
}
|
||||
if (configForm.value.timeoutType === DelayTypeEnum.FIXED_DATE_TIME) {
|
||||
currentNode.value.childProcessSetting.timeoutSetting.timeExpression =
|
||||
configForm.value.dateTime;
|
||||
}
|
||||
}
|
||||
// 8. 多实例设置
|
||||
currentNode.value.childProcessSetting.multiInstanceSetting = {
|
||||
enable: configForm.value.multiInstanceEnable,
|
||||
};
|
||||
if (configForm.value.multiInstanceEnable) {
|
||||
currentNode.value.childProcessSetting.multiInstanceSetting.sequential =
|
||||
configForm.value.sequential;
|
||||
currentNode.value.childProcessSetting.multiInstanceSetting.approveRatio =
|
||||
configForm.value.approveRatio;
|
||||
currentNode.value.childProcessSetting.multiInstanceSetting.sourceType =
|
||||
configForm.value.multiInstanceSourceType;
|
||||
currentNode.value.childProcessSetting.multiInstanceSetting.source =
|
||||
configForm.value.multiInstanceSource;
|
||||
}
|
||||
}
|
||||
|
||||
currentNode.value.showText = `调用子流程:${childInfo.name}`;
|
||||
drawerApi.close();
|
||||
return true;
|
||||
};
|
||||
|
||||
// 显示子流程节点配置, 由父组件传过来
|
||||
const showChildProcessNodeConfig = (node: SimpleFlowNode) => {
|
||||
nodeName.value = node.name;
|
||||
if (node.childProcessSetting) {
|
||||
// 1. 是否异步
|
||||
configForm.value.async = node.childProcessSetting.async;
|
||||
// 2. 调用流程
|
||||
configForm.value.calledProcessDefinitionKey =
|
||||
node.childProcessSetting?.calledProcessDefinitionKey;
|
||||
// 3. 是否跳过发起人
|
||||
configForm.value.skipStartUserNode =
|
||||
node.childProcessSetting.skipStartUserNode;
|
||||
// 4. 主->子变量
|
||||
configForm.value.inVariables = node.childProcessSetting.inVariables ?? [];
|
||||
// 5. 子->主变量
|
||||
configForm.value.outVariables = node.childProcessSetting.outVariables ?? [];
|
||||
// 6. 发起人设置
|
||||
configForm.value.startUserType =
|
||||
node.childProcessSetting.startUserSetting.type;
|
||||
configForm.value.startUserEmptyType =
|
||||
node.childProcessSetting.startUserSetting.emptyType ??
|
||||
ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER;
|
||||
configForm.value.startUserFormField =
|
||||
node.childProcessSetting.startUserSetting.formField ?? '';
|
||||
// 7. 超时设置
|
||||
configForm.value.timeoutEnable =
|
||||
node.childProcessSetting.timeoutSetting.enable ?? false;
|
||||
if (configForm.value.timeoutEnable) {
|
||||
configForm.value.timeoutType =
|
||||
node.childProcessSetting.timeoutSetting.type ??
|
||||
DelayTypeEnum.FIXED_TIME_DURATION;
|
||||
// 固定时长
|
||||
if (configForm.value.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
||||
const strTimeDuration =
|
||||
node.childProcessSetting.timeoutSetting.timeExpression ?? '';
|
||||
const parseTime = strTimeDuration.slice(2, -1);
|
||||
const parseTimeUnit = strTimeDuration.slice(-1);
|
||||
configForm.value.timeDuration = Number.parseInt(parseTime);
|
||||
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit);
|
||||
}
|
||||
// 固定日期时间
|
||||
if (configForm.value.timeoutType === DelayTypeEnum.FIXED_DATE_TIME) {
|
||||
configForm.value.dateTime =
|
||||
node.childProcessSetting.timeoutSetting.timeExpression ?? '';
|
||||
}
|
||||
}
|
||||
// 8. 多实例设置
|
||||
configForm.value.multiInstanceEnable =
|
||||
node.childProcessSetting.multiInstanceSetting.enable ?? false;
|
||||
if (configForm.value.multiInstanceEnable) {
|
||||
configForm.value.sequential =
|
||||
node.childProcessSetting.multiInstanceSetting.sequential ?? false;
|
||||
configForm.value.approveRatio =
|
||||
node.childProcessSetting.multiInstanceSetting.approveRatio ?? 100;
|
||||
configForm.value.multiInstanceSourceType =
|
||||
node.childProcessSetting.multiInstanceSetting.sourceType ??
|
||||
ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY;
|
||||
configForm.value.multiInstanceSource =
|
||||
node.childProcessSetting.multiInstanceSetting.source ?? '';
|
||||
}
|
||||
}
|
||||
loadFormInfo();
|
||||
drawerApi.open();
|
||||
};
|
||||
|
||||
/** 暴露方法给父组件 */
|
||||
defineExpose({ showChildProcessNodeConfig });
|
||||
|
||||
const addVariable = (arr?: IOParameter[]) => {
|
||||
arr?.push({
|
||||
source: '',
|
||||
target: '',
|
||||
});
|
||||
};
|
||||
|
||||
const deleteVariable = (index: number, arr?: IOParameter[]) => {
|
||||
arr?.splice(index, 1);
|
||||
};
|
||||
|
||||
const handleCalledElementChange = () => {
|
||||
configForm.value.inVariables = [];
|
||||
configForm.value.outVariables = [];
|
||||
loadFormInfo();
|
||||
};
|
||||
|
||||
const loadFormInfo = async () => {
|
||||
const childInfo = childProcessOptions.value.find(
|
||||
(option) => option.key === configForm.value.calledProcessDefinitionKey,
|
||||
);
|
||||
if (!childInfo) return;
|
||||
|
||||
const formInfo = await getFormDetail(childInfo.formId);
|
||||
childFormFieldOptions.value = [];
|
||||
if (formInfo.fields) {
|
||||
formInfo.fields.forEach((fieldStr: string) => {
|
||||
parseFormFields(JSON.parse(fieldStr), childFormFieldOptions.value);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getIsoTimeDuration = () => {
|
||||
let strTimeDuration = 'PT';
|
||||
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
|
||||
strTimeDuration += `${configForm.value.timeDuration}M`;
|
||||
}
|
||||
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
|
||||
strTimeDuration += `${configForm.value.timeDuration}H`;
|
||||
}
|
||||
if (configForm.value.timeUnit === TimeUnitType.DAY) {
|
||||
strTimeDuration += `${configForm.value.timeDuration}D`;
|
||||
}
|
||||
return strTimeDuration;
|
||||
};
|
||||
|
||||
const handleMultiInstanceSourceTypeChange = () => {
|
||||
configForm.value.multiInstanceSource = '';
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
childProcessOptions.value = await getModelList(undefined);
|
||||
} catch (error) {
|
||||
console.error('获取模型列表失败', error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer class="w-1/3">
|
||||
<template #title>
|
||||
<div class="config-header">
|
||||
<Input
|
||||
v-if="showInput"
|
||||
ref="inputRef"
|
||||
type="text"
|
||||
class="focus:border-blue-500 focus:shadow-[0_0_0_2px_rgba(24,144,255,0.2)] focus:outline-none"
|
||||
@blur="changeNodeName()"
|
||||
@press-enter="changeNodeName()"
|
||||
v-model:value="nodeName"
|
||||
:placeholder="nodeName"
|
||||
/>
|
||||
<div v-else class="node-name">
|
||||
{{ nodeName }}
|
||||
<IconifyIcon class="ml-1" icon="lucide:edit-3" @click="clickIcon()" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="configForm"
|
||||
:label-wrap="true"
|
||||
:label-col="{ span: 24 }"
|
||||
:wrapper-col="{ span: 24 }"
|
||||
:rules="formRules"
|
||||
>
|
||||
<FormItem
|
||||
label="是否异步执行"
|
||||
name="async"
|
||||
label-align="left"
|
||||
:label-col="{ span: 8 }"
|
||||
:wrapper-col="{ span: 4 }"
|
||||
>
|
||||
<Switch
|
||||
v-model:checked="configForm.async"
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="选择子流程" name="calledProcessDefinitionKey">
|
||||
<Select
|
||||
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>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="是否自动跳过子流程发起节点"
|
||||
name="skipStartUserNode"
|
||||
label-align="left"
|
||||
:label-col="{ span: 12 }"
|
||||
:wrapper-col="{ span: 4 }"
|
||||
>
|
||||
<Switch
|
||||
v-model:checked="configForm.skipStartUserNode"
|
||||
checked-children="跳过"
|
||||
un-checked-children="不跳过"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="主→子变量传递" name="inVariables">
|
||||
<div
|
||||
class="flex"
|
||||
v-for="(item, index) in configForm.inVariables"
|
||||
:key="index"
|
||||
>
|
||||
<div class="mr-2">
|
||||
<FormItem
|
||||
:name="['inVariables', index, 'source']"
|
||||
: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>
|
||||
</FormItem>
|
||||
</div>
|
||||
<div class="mr-2">
|
||||
<FormItem
|
||||
:name="['inVariables', index, 'target']"
|
||||
: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>
|
||||
</FormItem>
|
||||
</div>
|
||||
<div class="mr-1 flex h-8 items-center">
|
||||
<IconifyIcon
|
||||
icon="lucide:trash-2"
|
||||
:size="18"
|
||||
class="cursor-pointer text-red-500"
|
||||
@click="deleteVariable(index, configForm.inVariables)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="link"
|
||||
@click="addVariable(configForm.inVariables)"
|
||||
class="flex items-center"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon class="size-4" icon="lucide:plus" />
|
||||
</template>
|
||||
添加一行
|
||||
</Button>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="configForm.async === false"
|
||||
label="子→主变量传递"
|
||||
name="outVariables"
|
||||
>
|
||||
<div
|
||||
class="flex"
|
||||
v-for="(item, index) in configForm.outVariables"
|
||||
:key="index"
|
||||
>
|
||||
<div class="mr-2">
|
||||
<FormItem
|
||||
:name="['outVariables', index, 'source']"
|
||||
: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>
|
||||
</FormItem>
|
||||
</div>
|
||||
<div class="mr-2">
|
||||
<FormItem
|
||||
:name="['outVariables', index, 'target']"
|
||||
: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>
|
||||
</FormItem>
|
||||
</div>
|
||||
<div class="mr-1 flex h-8 items-center">
|
||||
<IconifyIcon
|
||||
icon="lucide:trash-2"
|
||||
:size="18"
|
||||
class="cursor-pointer text-red-500"
|
||||
@click="deleteVariable(index, configForm.outVariables)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="link"
|
||||
@click="addVariable(configForm.outVariables)"
|
||||
class="flex items-center"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon class="size-4" icon="lucide:plus" />
|
||||
</template>
|
||||
添加一行
|
||||
</Button>
|
||||
</FormItem>
|
||||
<FormItem label="子流程发起人" name="startUserType">
|
||||
<RadioGroup v-model:value="configForm.startUserType">
|
||||
<Radio
|
||||
v-for="item in CHILD_PROCESS_START_USER_TYPE"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
configForm.startUserType === ChildProcessStartUserTypeEnum.FROM_FORM
|
||||
"
|
||||
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>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
configForm.startUserType === ChildProcessStartUserTypeEnum.FROM_FORM
|
||||
"
|
||||
label="当子流程发起人为空时"
|
||||
name="startUserEmptyType"
|
||||
>
|
||||
<RadioGroup v-model:value="configForm.startUserEmptyType">
|
||||
<Radio
|
||||
v-for="item in CHILD_PROCESS_START_USER_EMPTY_TYPE"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
|
||||
<Divider>超时设置</Divider>
|
||||
<FormItem
|
||||
label="启用开关"
|
||||
name="timeoutEnable"
|
||||
label-align="left"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 4 }"
|
||||
>
|
||||
<Switch
|
||||
v-model:checked="configForm.timeoutEnable"
|
||||
checked-children="开启"
|
||||
un-checked-children="关闭"
|
||||
/>
|
||||
</FormItem>
|
||||
<div v-if="configForm.timeoutEnable">
|
||||
<FormItem name="timeoutType">
|
||||
<RadioGroup v-model:value="configForm.timeoutType">
|
||||
<RadioButton
|
||||
v-for="item in DELAY_TYPE"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</RadioButton>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="configForm.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION"
|
||||
>
|
||||
<Row :gutter="8">
|
||||
<Col>
|
||||
<span class="inline-flex h-8 items-center"> 当超过 </span>
|
||||
</Col>
|
||||
<Col>
|
||||
<FormItem name="timeDuration">
|
||||
<InputNumber
|
||||
class="w-24"
|
||||
v-model:value="configForm.timeDuration"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</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>
|
||||
</Col>
|
||||
<Col>
|
||||
<span class="inline-flex h-8 items-center">后进入下一节点</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="configForm.timeoutType === DelayTypeEnum.FIXED_DATE_TIME"
|
||||
name="dateTime"
|
||||
>
|
||||
<Row :gutter="8">
|
||||
<Col>
|
||||
<DatePicker
|
||||
class="mr-2"
|
||||
v-model:value="configForm.dateTime"
|
||||
type="date"
|
||||
show-time
|
||||
placeholder="请选择日期和时间"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<span class="inline-flex h-8 items-center">
|
||||
后进入下一节点
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<Divider>多实例设置</Divider>
|
||||
<FormItem
|
||||
label="启用开关"
|
||||
label-align="left"
|
||||
name="multiInstanceEnable"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 4 }"
|
||||
>
|
||||
<Switch
|
||||
v-model:checked="configForm.multiInstanceEnable"
|
||||
checked-children="开启"
|
||||
un-checked-children="关闭"
|
||||
/>
|
||||
</FormItem>
|
||||
<div v-if="configForm.multiInstanceEnable">
|
||||
<FormItem
|
||||
name="sequential"
|
||||
label="是否串行"
|
||||
label-align="left"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 4 }"
|
||||
>
|
||||
<Switch
|
||||
v-model:checked="configForm.sequential"
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="approveRatio"
|
||||
label="完成比例(%)"
|
||||
label-align="left"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 4 }"
|
||||
>
|
||||
<InputNumber
|
||||
v-model:value="configForm.approveRatio"
|
||||
:min="10"
|
||||
:max="100"
|
||||
:step="10"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="multiInstanceSourceType"
|
||||
label="实例数量"
|
||||
label-align="left"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 12 }"
|
||||
>
|
||||
<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>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
configForm.multiInstanceSourceType ===
|
||||
ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY
|
||||
"
|
||||
name="multiInstanceSource"
|
||||
label="固定数量"
|
||||
label-align="left"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 12 }"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '固定数量不能为空',
|
||||
trigger: 'change',
|
||||
}"
|
||||
>
|
||||
<InputNumber
|
||||
v-model:value="configForm.multiInstanceSource"
|
||||
:min="1"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
configForm.multiInstanceSourceType ===
|
||||
ChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM
|
||||
"
|
||||
name="multiInstanceSource"
|
||||
label="数字表单"
|
||||
label-align="left"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 12 }"
|
||||
: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>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
configForm.multiInstanceSourceType ===
|
||||
ChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM
|
||||
"
|
||||
name="multiInstanceSource"
|
||||
label="多选表单"
|
||||
label-align="left"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 12 }"
|
||||
: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>
|
||||
</FormItem>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</Drawer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
|
@ -62,9 +62,15 @@ const [Modal, modalApi] = useVbenModal({
|
|||
},
|
||||
});
|
||||
|
||||
// TODO xingyu 暴露 modalApi 给父组件是否合适? trigger-node-config.vue 会有多个 conditionDialog 实例
|
||||
// 不用暴露啊,用 useVbenModal 就可以了
|
||||
defineExpose({ modalApi });
|
||||
/**
|
||||
* 打开条件配置弹窗,不暴露 modalApi 给父组件
|
||||
*/
|
||||
function openModal(conditionObj: any) {
|
||||
modalApi.setData(conditionObj).open();
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({ openModal });
|
||||
</script>
|
||||
<template>
|
||||
<Modal class="w-1/2">
|
||||
|
|
|
@ -200,8 +200,8 @@ function addFormSettingCondition(
|
|||
formSetting: FormTriggerSetting,
|
||||
) {
|
||||
const conditionDialog = proxy.$refs[`condition-${index}`][0];
|
||||
// 使用modalApi来打开模态框并传递数据
|
||||
conditionDialog.modalApi.setData(formSetting).open();
|
||||
// 打开模态框并传递数据
|
||||
conditionDialog.openModal(formSetting);
|
||||
}
|
||||
|
||||
/** 删除条件配置 */
|
||||
|
@ -215,8 +215,8 @@ function openFormSettingCondition(
|
|||
formSetting: FormTriggerSetting,
|
||||
) {
|
||||
const conditionDialog = proxy.$refs[`condition-${index}`][0];
|
||||
// 使用 modalApi 来打开模态框并传递数据
|
||||
conditionDialog.modalApi.setData(formSetting).open();
|
||||
// 打开模态框并传递数据
|
||||
conditionDialog.openModal(formSetting);
|
||||
}
|
||||
|
||||
/** 处理条件配置保存 */
|
||||
|
|
|
@ -601,7 +601,7 @@ onMounted(() => {
|
|||
});
|
||||
</script>
|
||||
<template>
|
||||
<Drawer class="w-1/3">
|
||||
<Drawer class="w-2/5">
|
||||
<template #title>
|
||||
<div class="config-header">
|
||||
<Input
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
<script setup lang="ts">
|
||||
import type { SimpleFlowNode } from '../../consts';
|
||||
|
||||
import { inject, ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Input } from 'ant-design-vue';
|
||||
|
||||
import { BpmNodeTypeEnum } from '#/utils';
|
||||
|
||||
import { NODE_DEFAULT_TEXT } from '../../consts';
|
||||
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
|
||||
import ChildProcessNodeConfig from '../nodes-config/child-process-node-config.vue';
|
||||
import NodeHandler from './node-handler.vue';
|
||||
|
||||
defineOptions({ name: 'ChildProcessNode' });
|
||||
|
||||
const props = defineProps<{
|
||||
flowNode: SimpleFlowNode;
|
||||
}>();
|
||||
|
||||
/** 定义事件,更新父组件。 */
|
||||
const emits = defineEmits<{
|
||||
'update:flowNode': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
|
||||
/** 监控节点的变化 */
|
||||
const currentNode = useWatchNode(props);
|
||||
|
||||
/** 节点名称编辑 */
|
||||
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
|
||||
currentNode,
|
||||
BpmNodeTypeEnum.CHILD_PROCESS_NODE,
|
||||
);
|
||||
|
||||
// 节点配置 Ref
|
||||
const nodeConfigRef = ref();
|
||||
|
||||
/** 打开节点配置 */
|
||||
const openNodeConfig = () => {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
nodeConfigRef.value.showChildProcessNodeConfig(currentNode.value);
|
||||
};
|
||||
|
||||
/** 删除节点。更新当前节点为孩子节点 */
|
||||
const deleteNode = () => {
|
||||
emits('update:flowNode', currentNode.value.childNode);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="node-wrapper">
|
||||
<div class="node-container">
|
||||
<div
|
||||
class="node-box"
|
||||
:class="[
|
||||
{ 'node-config-error': !currentNode.showText },
|
||||
`${useTaskStatusClass(currentNode?.activityStatus)}`,
|
||||
]"
|
||||
>
|
||||
<div class="node-title-container">
|
||||
<div
|
||||
:class="`node-title-icon ${currentNode.childProcessSetting?.async === true ? 'async-child-process' : 'child-process'}`"
|
||||
>
|
||||
<span
|
||||
:class="`iconfont ${currentNode.childProcessSetting?.async === true ? 'icon-async-child-process' : 'icon-child-process'}`"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
ref="inputRef"
|
||||
v-if="!readonly && showInput"
|
||||
type="text"
|
||||
class="editable-title-input"
|
||||
@blur="changeNodeName()"
|
||||
@press-enter="changeNodeName()"
|
||||
v-model:value="currentNode.name"
|
||||
:placeholder="currentNode.name"
|
||||
/>
|
||||
<div v-else class="node-title" @click="clickTitle">
|
||||
{{ currentNode.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-content" @click="openNodeConfig">
|
||||
<div
|
||||
class="node-text"
|
||||
:title="currentNode.showText"
|
||||
v-if="currentNode.showText"
|
||||
>
|
||||
{{ currentNode.showText }}
|
||||
</div>
|
||||
<div class="node-text" v-else>
|
||||
{{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.CHILD_PROCESS_NODE) }}
|
||||
</div>
|
||||
<IconifyIcon v-if="!readonly" icon="lucide:chevron-right" />
|
||||
</div>
|
||||
<div v-if="!readonly" class="node-toolbar">
|
||||
<div class="toolbar-icon">
|
||||
<IconifyIcon
|
||||
color="#0089ff"
|
||||
icon="lucide:circle-x"
|
||||
:size="18"
|
||||
@click="deleteNode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加节点组件。会在子节点前面添加节点 -->
|
||||
<NodeHandler
|
||||
v-if="currentNode"
|
||||
v-model:child-node="currentNode.childNode"
|
||||
:current-node="currentNode"
|
||||
/>
|
||||
</div>
|
||||
<ChildProcessNodeConfig
|
||||
v-if="!readonly && currentNode"
|
||||
ref="nodeConfigRef"
|
||||
:flow-node="currentNode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
|
@ -16,25 +16,6 @@ import { NODE_DEFAULT_TEXT } from '../../consts';
|
|||
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
|
||||
import UserTaskNodeConfig from '../nodes-config/user-task-node-config.vue';
|
||||
import TaskListModal from './modules/task-list-modal.vue';
|
||||
// // 使用useVbenVxeGrid
|
||||
// const [Grid, gridApi] = useVbenVxeGrid({
|
||||
// gridOptions: {
|
||||
// columns: columns.value,
|
||||
// keepSource: true,
|
||||
// border: true,
|
||||
// height: 'auto',
|
||||
// data: selectTasks.value,
|
||||
// rowConfig: {
|
||||
// keyField: 'id',
|
||||
// },
|
||||
// pagerConfig: {
|
||||
// enabled: false,
|
||||
// },
|
||||
// toolbarConfig: {
|
||||
// enabled: false,
|
||||
// },
|
||||
// } as VxeTableGridOptions<any>,
|
||||
// });
|
||||
import NodeHandler from './node-handler.vue';
|
||||
|
||||
defineOptions({ name: 'UserTaskNode' });
|
||||
|
@ -155,7 +136,7 @@ function findReturnTaskNodes(
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
|
||||
<!-- 添加节点组件。会在子节点前面添加节点 -->
|
||||
<NodeHandler
|
||||
v-if="currentNode"
|
||||
v-model:child-node="currentNode.childNode"
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { SimpleFlowNode } from '../consts';
|
|||
import { BpmNodeTypeEnum } from '#/utils';
|
||||
|
||||
import { useWatchNode } from '../helpers';
|
||||
import ChildProcessNode from './nodes/child-process-node.vue';
|
||||
import CopyTaskNode from './nodes/copy-task-node.vue';
|
||||
import DelayTimerNode from './nodes/delay-timer-node.vue';
|
||||
import EndEventNode from './nodes/end-event-node.vue';
|
||||
|
@ -140,11 +141,13 @@ function recursiveFindParentNode(
|
|||
@update:flow-node="handleModelValueUpdate"
|
||||
/>
|
||||
<!-- 子流程节点 -->
|
||||
<!-- <ChildProcessNode
|
||||
v-if="currentNode && currentNode.type === NodeType.CHILD_PROCESS_NODE"
|
||||
<ChildProcessNode
|
||||
v-if="
|
||||
currentNode && currentNode.type === BpmNodeTypeEnum.CHILD_PROCESS_NODE
|
||||
"
|
||||
:flow-node="currentNode"
|
||||
@update:flow-node="handleModelValueUpdate"
|
||||
/> -->
|
||||
/>
|
||||
<!-- 递归显示孩子节点 -->
|
||||
<ProcessNodeTree
|
||||
v-if="currentNode && currentNode.childNode"
|
||||
|
|
|
@ -126,7 +126,8 @@ function updateModel() {
|
|||
name: '发起人',
|
||||
type: BpmNodeTypeEnum.START_USER_NODE,
|
||||
id: NodeId.START_USER_NODE_ID,
|
||||
showText: '默认配置',
|
||||
// 默认为空,需要进行配置
|
||||
showText: '',
|
||||
childNode: {
|
||||
id: NodeId.END_EVENT_NODE_ID,
|
||||
name: '结束',
|
||||
|
|
|
@ -852,7 +852,7 @@ export const CHILD_PROCESS_START_USER_TYPE = [
|
|||
label: '同主流程发起人',
|
||||
value: ChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER,
|
||||
},
|
||||
{ label: '表单', value: ChildProcessStartUserTypeEnum.FROM_FORM },
|
||||
{ label: '从表单中获取', value: ChildProcessStartUserTypeEnum.FROM_FORM },
|
||||
];
|
||||
|
||||
export const CHILD_PROCESS_START_USER_EMPTY_TYPE = [
|
||||
|
|
|
@ -60,39 +60,14 @@ function isIfShow(action: ActionItem): boolean {
|
|||
|
||||
/** 处理按钮 actions */
|
||||
const getActions = computed(() => {
|
||||
return (props.actions || [])
|
||||
.filter((action: ActionItem) => isIfShow(action))
|
||||
.map((action: ActionItem) => {
|
||||
const { popConfirm } = action;
|
||||
return {
|
||||
type: action.type || 'link',
|
||||
...action,
|
||||
...popConfirm,
|
||||
onConfirm: popConfirm?.confirm,
|
||||
onCancel: popConfirm?.cancel,
|
||||
enable: !!popConfirm,
|
||||
};
|
||||
});
|
||||
return (props.actions || []).filter((action: ActionItem) => isIfShow(action));
|
||||
});
|
||||
|
||||
/** 处理下拉菜单 actions */
|
||||
const getDropdownList = computed(() => {
|
||||
return (props.dropDownActions || [])
|
||||
.filter((action: ActionItem) => isIfShow(action))
|
||||
.map((action: ActionItem, index: number) => {
|
||||
const { label, popConfirm } = action;
|
||||
const processedAction = { ...action };
|
||||
delete processedAction.icon;
|
||||
return {
|
||||
...processedAction,
|
||||
...popConfirm,
|
||||
onConfirm: popConfirm?.confirm,
|
||||
onCancel: popConfirm?.cancel,
|
||||
text: label,
|
||||
divider:
|
||||
index < props.dropDownActions.length - 1 ? props.divider : false,
|
||||
};
|
||||
});
|
||||
return (props.dropDownActions || []).filter((action: ActionItem) =>
|
||||
isIfShow(action),
|
||||
);
|
||||
});
|
||||
|
||||
/** Space 组件的 size */
|
||||
|
@ -103,18 +78,27 @@ const spaceSize = computed(() => {
|
|||
});
|
||||
|
||||
/** 获取 PopConfirm 属性 */
|
||||
function getPopConfirmProps(attrs: PopConfirm) {
|
||||
const originAttrs: any = { ...attrs };
|
||||
delete originAttrs.icon;
|
||||
if (attrs.confirm && isFunction(attrs.confirm)) {
|
||||
originAttrs.onConfirm = attrs.confirm;
|
||||
delete originAttrs.confirm;
|
||||
function getPopConfirmProps(popConfirm: PopConfirm) {
|
||||
if (!popConfirm) return {};
|
||||
|
||||
const attrs: Record<string, any> = {};
|
||||
|
||||
// 复制基本属性,排除函数
|
||||
Object.keys(popConfirm).forEach((key) => {
|
||||
if (key !== 'confirm' && key !== 'cancel' && key !== 'icon') {
|
||||
attrs[key] = popConfirm[key as keyof PopConfirm];
|
||||
}
|
||||
});
|
||||
|
||||
// 单独处理事件函数
|
||||
if (popConfirm.confirm && isFunction(popConfirm.confirm)) {
|
||||
attrs.onConfirm = popConfirm.confirm;
|
||||
}
|
||||
if (attrs.cancel && isFunction(attrs.cancel)) {
|
||||
originAttrs.onCancel = attrs.cancel;
|
||||
delete originAttrs.cancel;
|
||||
if (popConfirm.cancel && isFunction(popConfirm.cancel)) {
|
||||
attrs.onCancel = popConfirm.cancel;
|
||||
}
|
||||
return originAttrs;
|
||||
|
||||
return attrs;
|
||||
}
|
||||
|
||||
/** 获取 Button 属性 */
|
||||
|
@ -146,6 +130,13 @@ function handleMenuClick(e: any) {
|
|||
function getActionKey(action: ActionItem, index: number) {
|
||||
return `${action.label || ''}-${action.type || ''}-${index}`;
|
||||
}
|
||||
|
||||
/** 处理按钮点击 */
|
||||
function handleButtonClick(action: ActionItem) {
|
||||
if (action.onClick && isFunction(action.onClick)) {
|
||||
action.onClick();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -172,7 +163,10 @@ function getActionKey(action: ActionItem, index: number) {
|
|||
</Tooltip>
|
||||
</Popconfirm>
|
||||
<Tooltip v-else v-bind="getTooltipProps(action.tooltip)">
|
||||
<Button v-bind="getButtonProps(action)" @click="action.onClick">
|
||||
<Button
|
||||
v-bind="getButtonProps(action)"
|
||||
@click="handleButtonClick(action)"
|
||||
>
|
||||
<template v-if="action.icon" #icon>
|
||||
<IconifyIcon :icon="action.icon" />
|
||||
</template>
|
||||
|
@ -184,7 +178,7 @@ function getActionKey(action: ActionItem, index: number) {
|
|||
|
||||
<Dropdown v-if="getDropdownList.length > 0" :trigger="['hover']">
|
||||
<slot name="more">
|
||||
<Button :type="getDropdownList[0]?.type">
|
||||
<Button type="link">
|
||||
<template #icon>
|
||||
{{ $t('page.action.more') }}
|
||||
<IconifyIcon icon="lucide:ellipsis-vertical" />
|
||||
|
@ -213,7 +207,7 @@ function getActionKey(action: ActionItem, index: number) {
|
|||
>
|
||||
<IconifyIcon v-if="action.icon" :icon="action.icon" />
|
||||
<span :class="action.icon ? 'ml-1' : ''">
|
||||
{{ action.text }}
|
||||
{{ action.label }}
|
||||
</span>
|
||||
</div>
|
||||
</Popconfirm>
|
||||
|
|
|
@ -10,6 +10,7 @@ import { AuthenticationLogin, Verification, z } from '@vben/common-ui';
|
|||
import { isCaptchaEnable, isTenantEnable } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { getUrlValue } from '@vben/utils';
|
||||
|
||||
import {
|
||||
checkCaptcha,
|
||||
|
@ -124,12 +125,6 @@ async function handleVerifySuccess({ captchaVerification }: any) {
|
|||
}
|
||||
}
|
||||
|
||||
/** tricky: 配合 login.vue 中,redirectUri 需要对参数进行 encode,需要在回调后进行decode */
|
||||
function getUrlValue(key: string): string {
|
||||
const url = new URL(decodeURIComponent(location.href));
|
||||
return url.searchParams.get(key) ?? '';
|
||||
}
|
||||
|
||||
/** 组件挂载时获取租户信息 */
|
||||
onMounted(async () => {
|
||||
await fetchTenantList();
|
||||
|
|
|
@ -115,7 +115,7 @@ async function handelUpload({
|
|||
所属岗位
|
||||
</div>
|
||||
</template>
|
||||
{{ profile.posts.map((post) => post.name).join(',') }}
|
||||
{{ profile.posts && profile.posts.length > 0 ? profile.posts.map(post => post.name).join(',') : '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem>
|
||||
<template #label>
|
||||
|
|
|
@ -6,6 +6,7 @@ import { computed, onMounted, ref } from 'vue';
|
|||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { confirm } from '@vben/common-ui';
|
||||
import { getUrlValue } from '@vben/utils';
|
||||
|
||||
import { Button, Card, Image, message } from 'ant-design-vue';
|
||||
|
||||
|
@ -149,13 +150,6 @@ async function bindSocial() {
|
|||
window.history.replaceState({}, '', location.pathname);
|
||||
}
|
||||
|
||||
// TODO @芋艿:后续搞到 util 里;
|
||||
// 双层 encode 需要在回调后进行 decode
|
||||
function getUrlValue(key: string): string {
|
||||
const url = new URL(decodeURIComponent(location.href));
|
||||
return url.searchParams.get(key) ?? '';
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
bindSocial();
|
||||
|
|
|
@ -2,7 +2,12 @@ import type { VbenFormSchema } from '#/adapter/form';
|
|||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
import {
|
||||
CommonStatusEnum,
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
|
@ -97,7 +102,15 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||
allowClear: true,
|
||||
},
|
||||
},
|
||||
// TODO 创建时间 等通用方法完善后加
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -335,7 +335,7 @@ defineExpose({ validate });
|
|||
<div
|
||||
v-for="user in selectedStartUsers"
|
||||
:key="user.id"
|
||||
class="relative flex h-9 items-center rounded-full pr-2 hover:bg-gray-200"
|
||||
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2 hover:bg-gray-200 dark:border dark:border-gray-500 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
>
|
||||
<Avatar
|
||||
class="m-1"
|
||||
|
@ -346,7 +346,9 @@ defineExpose({ validate });
|
|||
<Avatar class="m-1" :size="28" v-else>
|
||||
{{ user.nickname?.substring(0, 1) }}
|
||||
</Avatar>
|
||||
{{ user.nickname }}
|
||||
<span class="text-gray-700 dark:text-gray-200">
|
||||
{{ user.nickname }}
|
||||
</span>
|
||||
<IconifyIcon
|
||||
icon="lucide:x"
|
||||
class="ml-2 size-4 cursor-pointer text-gray-400 hover:text-red-500"
|
||||
|
@ -371,10 +373,12 @@ defineExpose({ validate });
|
|||
<div
|
||||
v-for="dept in selectedStartDepts"
|
||||
:key="dept.id"
|
||||
class="relative flex h-9 items-center rounded-full pr-2 shadow-sm hover:bg-gray-200"
|
||||
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2 shadow-sm hover:bg-gray-200 dark:border dark:border-gray-500 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
>
|
||||
<IconifyIcon icon="lucide:building" class="size-6 px-1" />
|
||||
{{ dept.name }}
|
||||
<span class="text-gray-700 dark:text-gray-200">
|
||||
{{ dept.name }}
|
||||
</span>
|
||||
<IconifyIcon
|
||||
icon="lucide:x"
|
||||
class="ml-2 size-4 cursor-pointer text-gray-400 hover:text-red-500"
|
||||
|
@ -398,7 +402,7 @@ defineExpose({ validate });
|
|||
<div
|
||||
v-for="user in selectedManagerUsers"
|
||||
:key="user.id"
|
||||
class="hover:bg-primary-500 relative flex h-9 items-center rounded-full pr-2"
|
||||
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2 hover:bg-gray-200 dark:border dark:border-gray-500 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
>
|
||||
<Avatar
|
||||
class="m-1"
|
||||
|
@ -409,7 +413,9 @@ defineExpose({ validate });
|
|||
<Avatar class="m-1" :size="28" v-else>
|
||||
{{ user.nickname?.substring(0, 1) }}
|
||||
</Avatar>
|
||||
{{ user.nickname }}
|
||||
<span class="text-gray-700 dark:text-gray-200">
|
||||
{{ user.nickname }}
|
||||
</span>
|
||||
<IconifyIcon
|
||||
icon="lucide:x"
|
||||
class="ml-2 size-4 cursor-pointer text-gray-400 hover:text-red-500"
|
||||
|
@ -432,6 +438,7 @@ defineExpose({ validate });
|
|||
|
||||
<!-- 用户选择弹窗 -->
|
||||
<UserSelectModalComp
|
||||
class="w-3/5"
|
||||
v-model:value="selectedUsers"
|
||||
:multiple="true"
|
||||
title="选择用户"
|
||||
|
@ -441,6 +448,7 @@ defineExpose({ validate });
|
|||
/>
|
||||
<!-- 部门选择对话框 -->
|
||||
<DeptSelectModalComp
|
||||
class="w-3/5"
|
||||
title="发起人部门选择"
|
||||
:check-strictly="true"
|
||||
@confirm="handleDeptSelectConfirm"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import type { BpmCategoryApi } from '#/api/bpm/category';
|
||||
import type { BpmProcessDefinitionApi } from '#/api/bpm/definition';
|
||||
|
||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
@ -146,6 +146,11 @@ function handleQuery() {
|
|||
// 如果没有搜索关键字,恢复所有数据
|
||||
isSearching.value = false;
|
||||
filteredProcessDefinitionList.value = processDefinitionList.value;
|
||||
|
||||
// 恢复到第一个可用分类
|
||||
if (availableCategories.value.length > 0) {
|
||||
activeCategory.value = availableCategories.value[0].code;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -178,7 +183,8 @@ const processDefinitionGroup = computed(() => {
|
|||
});
|
||||
|
||||
/** 通过分类 code 获取对应的名称 */
|
||||
function getCategoryName(categoryCode: string) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function _getCategoryName(categoryCode: string) {
|
||||
return categoryList.value?.find((ctg: any) => ctg.code === categoryCode)
|
||||
?.name;
|
||||
}
|
||||
|
@ -215,11 +221,28 @@ const availableCategories = computed(() => {
|
|||
});
|
||||
|
||||
/** 获取 tab 的位置 */
|
||||
|
||||
const tabPosition = computed(() => {
|
||||
return window.innerWidth < 768 ? 'top' : 'left';
|
||||
});
|
||||
|
||||
/** 监听可用分类变化,自动设置正确的活动分类 */
|
||||
watch(
|
||||
availableCategories,
|
||||
(newCategories) => {
|
||||
if (newCategories.length > 0) {
|
||||
// 如果当前活动分类不在可用分类中,切换到第一个可用分类
|
||||
const currentCategoryExists = newCategories.some(
|
||||
(category: BpmCategoryApi.Category) =>
|
||||
category.code === activeCategory.value,
|
||||
);
|
||||
if (!currentCategoryExists) {
|
||||
activeCategory.value = newCategories[0].code;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList();
|
||||
|
@ -240,10 +263,10 @@ onMounted(() => {
|
|||
:loading="loading"
|
||||
>
|
||||
<template #extra>
|
||||
<div class="flex items-end">
|
||||
<div class="flex h-full items-center justify-center">
|
||||
<InputSearch
|
||||
v-model:value="searchName"
|
||||
class="!w-50% mb-4"
|
||||
class="!w-50%"
|
||||
placeholder="请输入流程名称检索"
|
||||
allow-clear
|
||||
@input="handleQuery"
|
||||
|
@ -259,15 +282,15 @@ onMounted(() => {
|
|||
:key="category.code"
|
||||
:tab="category.name"
|
||||
>
|
||||
<Row :gutter="[16, 16]">
|
||||
<Row :gutter="[16, 16]" :wrap="true">
|
||||
<Col
|
||||
v-for="definition in processDefinitionGroup[category.code]"
|
||||
:key="definition.id"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:md="8"
|
||||
:lg="6"
|
||||
:xl="4"
|
||||
:lg="8"
|
||||
:xl="6"
|
||||
@click="handleSelect(definition)"
|
||||
>
|
||||
<Card
|
||||
|
@ -278,10 +301,10 @@ onMounted(() => {
|
|||
}"
|
||||
:body-style="{
|
||||
width: '100%',
|
||||
padding: '16px',
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<!-- TODO @ziye:icon、name 会告警~~ -->
|
||||
<img
|
||||
v-if="definition.icon"
|
||||
:src="definition.icon"
|
||||
|
@ -290,16 +313,14 @@ onMounted(() => {
|
|||
/>
|
||||
|
||||
<div v-else class="flow-icon flex-shrink-0">
|
||||
<Tooltip :title="definition.name">
|
||||
<span class="text-xs text-white">
|
||||
{{ definition.name?.slice(0, 2) }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<span class="text-xs text-white">
|
||||
{{ definition.name?.slice(0, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="ml-3 flex-1 truncate text-base">
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
:title="`${definition.name}`"
|
||||
:title="`${definition.description}`"
|
||||
>
|
||||
{{ definition.name }}
|
||||
</Tooltip>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ApiAttrs } from '@form-create/ant-design-vue/types/config';
|
||||
|
||||
import type { BpmProcessDefinitionApi } from '#/api/bpm/definition';
|
||||
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
|
||||
|
@ -22,7 +21,6 @@ import {
|
|||
BpmModelFormType,
|
||||
BpmModelType,
|
||||
BpmNodeIdEnum,
|
||||
BpmNodeTypeEnum,
|
||||
decodeFields,
|
||||
setConfAndFields2,
|
||||
} from '#/utils';
|
||||
|
@ -41,22 +39,6 @@ interface UserTask {
|
|||
name: string;
|
||||
}
|
||||
|
||||
interface ApprovalNodeInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
candidateStrategy: BpmCandidateStrategyEnum;
|
||||
candidateUsers?: Array<{
|
||||
avatar: string;
|
||||
id: number;
|
||||
nickname: string;
|
||||
}>;
|
||||
endTime?: Date;
|
||||
nodeType: BpmNodeTypeEnum;
|
||||
startTime?: Date;
|
||||
status: number;
|
||||
tasks: any[];
|
||||
}
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceCreateForm' });
|
||||
|
||||
const props = defineProps({
|
||||
|
@ -80,7 +62,7 @@ const detailForm = ref<ProcessFormData>({
|
|||
value: {},
|
||||
});
|
||||
|
||||
const fApi = ref<ApiAttrs>();
|
||||
const fApi = ref<any>();
|
||||
const startUserSelectTasks = ref<UserTask[]>([]);
|
||||
const startUserSelectAssignees = ref<Record<string, string[]>>({});
|
||||
const tempStartUserSelectAssignees = ref<Record<string, string[]>>({});
|
||||
|
@ -88,9 +70,8 @@ const bpmnXML = ref<string | undefined>(undefined);
|
|||
const simpleJson = ref<string | undefined>(undefined);
|
||||
const timelineRef = ref<any>();
|
||||
const activeTab = ref('form');
|
||||
const activityNodes = ref<ApprovalNodeInfo[]>([]);
|
||||
const activityNodes = ref<BpmProcessInstanceApi.ApprovalNodeInfo[]>([]);
|
||||
const processInstanceStartLoading = ref(false);
|
||||
|
||||
/** 提交按钮 */
|
||||
async function submitForm() {
|
||||
if (!fApi.value || !props.selectProcessDefinition) {
|
||||
|
@ -127,7 +108,6 @@ async function submitForm() {
|
|||
|
||||
await router.push({ path: '/bpm/task/my' });
|
||||
} catch (error) {
|
||||
message.error('发起流程失败');
|
||||
console.error('发起流程失败:', error);
|
||||
} finally {
|
||||
processInstanceStartLoading.value = false;
|
||||
|
@ -219,7 +199,7 @@ async function getApprovalDetail(row: {
|
|||
}
|
||||
|
||||
// 获取审批节点
|
||||
activityNodes.value = data.activityNodes as unknown as ApprovalNodeInfo[];
|
||||
activityNodes.value = data.activityNodes;
|
||||
|
||||
// 获取发起人自选的任务
|
||||
startUserSelectTasks.value = (data.activityNodes?.filter(
|
||||
|
@ -330,7 +310,12 @@ defineExpose({ initProcessInfo });
|
|||
</Row>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane tab="流程图" key="flow" class="flex flex-1 overflow-hidden">
|
||||
<Tabs.TabPane
|
||||
tab="流程图"
|
||||
key="flow"
|
||||
class="flex flex-1 overflow-hidden"
|
||||
:force-render="true"
|
||||
>
|
||||
<div class="w-full">
|
||||
<ProcessInstanceSimpleViewer
|
||||
:simple-json="simpleJson"
|
||||
|
@ -343,7 +328,12 @@ defineExpose({ initProcessInfo });
|
|||
<template #actions>
|
||||
<template v-if="activeTab === 'form'">
|
||||
<Space wrap class="flex w-full justify-center">
|
||||
<Button plain type="primary" @click="submitForm">
|
||||
<Button
|
||||
plain
|
||||
type="primary"
|
||||
@click="submitForm"
|
||||
:loading="processInstanceStartLoading"
|
||||
>
|
||||
<IconifyIcon icon="lucide:check" />
|
||||
发起
|
||||
</Button>
|
||||
|
|
|
@ -657,8 +657,7 @@ async function validateNormalForm() {
|
|||
function getUpdatedProcessInstanceVariables() {
|
||||
const variables: any = {};
|
||||
props.writableFields.forEach((field: string) => {
|
||||
if (field && variables[field])
|
||||
variables[field] = props.normalFormApi.getValue(field);
|
||||
variables[field] = props.normalFormApi.getValue(field);
|
||||
});
|
||||
return variables;
|
||||
}
|
||||
|
@ -736,6 +735,7 @@ defineExpose({ loadTodoTask });
|
|||
<ProcessInstanceTimeline
|
||||
:activity-nodes="nextAssigneesActivityNode"
|
||||
:show-status-icon="false"
|
||||
:use-next-assignees="true"
|
||||
@select-user-confirm="selectNextAssigneesConfirm"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,7 @@ import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
|||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime, isEmpty } from '@vben/utils';
|
||||
|
||||
|
@ -19,13 +20,15 @@ import {
|
|||
|
||||
defineOptions({ name: 'BpmProcessInstanceTimeline' });
|
||||
|
||||
withDefaults(
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
activityNodes: BpmProcessInstanceApi.ApprovalNodeInfo[]; // 审批节点信息
|
||||
showStatusIcon?: boolean; // 是否显示头像右下角状态图标
|
||||
useNextAssignees?: boolean; // 是否用于下一个节点审批人选择
|
||||
}>(),
|
||||
{
|
||||
showStatusIcon: true, // 默认值为 true
|
||||
useNextAssignees: false, // 默认值为 false
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -102,7 +105,7 @@ const nodeTypeSvgMap = {
|
|||
color: '#14bb83',
|
||||
icon: 'icon-park-outline:tree-diagram',
|
||||
},
|
||||
};
|
||||
} as Record<BpmNodeTypeEnum, { color: string; icon: string }>;
|
||||
|
||||
// 只有状态是 -1、0、1 才展示头像右小角状态小icon
|
||||
const onlyStatusIconShow = [-1, 0, 1];
|
||||
|
@ -150,21 +153,27 @@ function getApprovalNodeTime(node: BpmProcessInstanceApi.ApprovalNodeInfo) {
|
|||
}
|
||||
|
||||
// 选择自定义审批人
|
||||
const userSelectFormRef = ref();
|
||||
const [UserSelectModalComp, userSelectModalApi] = useVbenModal({
|
||||
connectedComponent: UserSelectModal,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
const selectedActivityNodeId = ref<string>();
|
||||
const customApproveUsers = ref<Record<string, any[]>>({}); // key:activityId,value:用户列表
|
||||
|
||||
// 打开选择用户弹窗
|
||||
const handleSelectUser = (activityId: string, selectedList: any[]) => {
|
||||
selectedActivityNodeId.value = activityId;
|
||||
userSelectFormRef.value.open(
|
||||
selectedList?.length ? selectedList.map((item) => item.id) : [],
|
||||
);
|
||||
userSelectModalApi
|
||||
.setData({ userIds: selectedList.map((item) => item.id) })
|
||||
.open();
|
||||
};
|
||||
|
||||
// 选择用户完成
|
||||
const selectedUsers = ref<number[]>([]);
|
||||
function handleUserSelectConfirm(userList: any[]) {
|
||||
if (!selectedActivityNodeId.value) {
|
||||
return;
|
||||
}
|
||||
customApproveUsers.value[selectedActivityNodeId.value] = userList || [];
|
||||
|
||||
emit('selectUserConfirm', selectedActivityNodeId.value, userList);
|
||||
|
@ -189,8 +198,9 @@ function shouldShowCustomUserSelect(
|
|||
isEmpty(activity.candidateUsers) &&
|
||||
(BpmCandidateStrategyEnum.START_USER_SELECT ===
|
||||
activity.candidateStrategy ||
|
||||
BpmCandidateStrategyEnum.APPROVE_USER_SELECT ===
|
||||
activity.candidateStrategy)
|
||||
(BpmCandidateStrategyEnum.APPROVE_USER_SELECT ===
|
||||
activity.candidateStrategy &&
|
||||
props.useNextAssignees))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -289,8 +299,12 @@ function handleUserSelectCancel() {
|
|||
type="primary"
|
||||
size="middle"
|
||||
ghost
|
||||
class="flex items-center justify-center"
|
||||
@click="
|
||||
handleSelectUser(activity.id, customApproveUsers[activity.id])
|
||||
handleSelectUser(
|
||||
activity.id,
|
||||
customApproveUsers[activity.id] ?? [],
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
|
@ -330,9 +344,7 @@ function handleUserSelectCancel() {
|
|||
v-if="task.assigneeUser || task.ownerUser"
|
||||
>
|
||||
<!-- 信息:头像昵称 -->
|
||||
<div
|
||||
class="relative flex h-8 items-center rounded-3xl bg-gray-100 pr-2 dark:bg-gray-600"
|
||||
>
|
||||
<div class="relative flex h-8 items-center rounded-3xl pr-2">
|
||||
<template
|
||||
v-if="
|
||||
task.assigneeUser?.avatar || task.assigneeUser?.nickname
|
||||
|
@ -414,7 +426,7 @@ function handleUserSelectCancel() {
|
|||
<div
|
||||
v-for="(user, userIndex) in activity.candidateUsers"
|
||||
:key="userIndex"
|
||||
class="relative flex h-8 items-center rounded-3xl bg-gray-100 pr-2 dark:bg-gray-600"
|
||||
class="relative flex h-8 items-center rounded-3xl pr-2"
|
||||
>
|
||||
<Avatar
|
||||
class="!m-1"
|
||||
|
@ -447,8 +459,8 @@ function handleUserSelectCancel() {
|
|||
</Timeline>
|
||||
|
||||
<!-- 用户选择弹窗 -->
|
||||
<UserSelectModal
|
||||
ref="userSelectFormRef"
|
||||
<UserSelectModalComp
|
||||
class="w-3/5"
|
||||
v-model:value="selectedUsers"
|
||||
:multiple="true"
|
||||
title="选择用户"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { SystemDeptApi } from '#/api/system/dept';
|
||||
import type { SystemMenuApi } from '#/api/system/menu';
|
||||
import type { SystemRoleApi } from '#/api/system/role';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
@ -21,7 +21,7 @@ import { useAssignMenuFormSchema } from '../data';
|
|||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const menuTree = ref<SystemDeptApi.Dept[]>([]); // 菜单树
|
||||
const menuTree = ref<SystemMenuApi.Menu[]>([]); // 菜单树
|
||||
const menuLoading = ref(false); // 加载菜单列表
|
||||
const isAllSelected = ref(false); // 全选状态
|
||||
const isExpanded = ref(false); // 展开状态
|
||||
|
@ -90,7 +90,7 @@ async function loadMenuTree() {
|
|||
menuLoading.value = true;
|
||||
try {
|
||||
const data = await getMenuList();
|
||||
menuTree.value = handleTree(data) as SystemDeptApi.Dept[];
|
||||
menuTree.value = handleTree(data) as SystemMenuApi.Menu[];
|
||||
} finally {
|
||||
menuLoading.value = false;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemDeptApi } from '#/api/system/dept';
|
||||
import type { SystemMenuApi } from '#/api/system/menu';
|
||||
import type { SystemTenantPackageApi } from '#/api/system/tenant-package';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
@ -27,7 +27,7 @@ const getTitle = computed(() => {
|
|||
? $t('ui.actionTitle.edit', ['套餐'])
|
||||
: $t('ui.actionTitle.create', ['套餐']);
|
||||
});
|
||||
const menuTree = ref<SystemDeptApi.Dept[]>([]); // 菜单树
|
||||
const menuTree = ref<SystemMenuApi.Menu[]>([]); // 菜单树
|
||||
const menuLoading = ref(false); // 加载菜单列表
|
||||
const isAllSelected = ref(false); // 全选状态
|
||||
const isExpanded = ref(false); // 展开状态
|
||||
|
@ -95,7 +95,7 @@ async function loadMenuTree() {
|
|||
menuLoading.value = true;
|
||||
try {
|
||||
const data = await getMenuList();
|
||||
menuTree.value = handleTree(data) as SystemDeptApi.Dept[];
|
||||
menuTree.value = handleTree(data) as SystemMenuApi.Menu[];
|
||||
} finally {
|
||||
menuLoading.value = false;
|
||||
}
|
||||
|
@ -134,7 +134,6 @@ function getAllNodeIds(nodes: any[], ids: number[] = []): number[] {
|
|||
<Modal :title="getTitle" class="w-2/5">
|
||||
<Form class="mx-6">
|
||||
<template #menuIds="slotProps">
|
||||
<!-- TODO @芋艿:可优化,使用 antd 的 tree?原因是,更原生 -->
|
||||
<VbenTree
|
||||
class="max-h-96 overflow-y-auto"
|
||||
:loading="menuLoading"
|
||||
|
|
|
@ -26,8 +26,8 @@
|
|||
"#/*": "./src/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@form-create/designer": "^3.2.6",
|
||||
"@form-create/element-ui": "^3.2.11",
|
||||
"@form-create/designer": "catalog:",
|
||||
"@form-create/element-ui": "catalog:",
|
||||
"@tinymce/tinymce-vue": "catalog:",
|
||||
"@vben/access": "workspace:*",
|
||||
"@vben/common-ui": "workspace:*",
|
||||
|
|
|
@ -8,13 +8,7 @@ import type { Component } from 'vue';
|
|||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import {
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
ref,
|
||||
} from 'vue';
|
||||
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
@ -148,16 +142,15 @@ const withDefaultPlaceholder = <T extends Component>(
|
|||
$t(`ui.placeholder.${type}`);
|
||||
// 透传组件暴露的方法
|
||||
const innerRef = ref();
|
||||
const publicApi: Recordable<any> = {};
|
||||
expose(publicApi);
|
||||
const instance = getCurrentInstance();
|
||||
instance?.proxy?.$nextTick(() => {
|
||||
for (const key in innerRef.value) {
|
||||
if (typeof innerRef.value[key] === 'function') {
|
||||
publicApi[key] = innerRef.value[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
expose(
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, key) => innerRef.value?.[key],
|
||||
has: (_target, key) => key in (innerRef.value || {}),
|
||||
},
|
||||
),
|
||||
);
|
||||
return () =>
|
||||
h(
|
||||
component,
|
||||
|
|
|
@ -10,6 +10,7 @@ import { AuthenticationLogin, Verification, z } from '@vben/common-ui';
|
|||
import { isCaptchaEnable, isTenantEnable } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { getUrlValue } from '@vben/utils';
|
||||
|
||||
import {
|
||||
checkCaptcha,
|
||||
|
@ -124,12 +125,6 @@ async function handleVerifySuccess({ captchaVerification }: any) {
|
|||
}
|
||||
}
|
||||
|
||||
/** tricky: 配合 login.vue 中,redirectUri 需要对参数进行 encode,需要在回调后进行decode */
|
||||
function getUrlValue(key: string): string {
|
||||
const url = new URL(decodeURIComponent(location.href));
|
||||
return url.searchParams.get(key) ?? '';
|
||||
}
|
||||
|
||||
/** 组件挂载时获取租户信息 */
|
||||
onMounted(async () => {
|
||||
await fetchTenantList();
|
||||
|
|
|
@ -6,6 +6,7 @@ import { computed, onMounted, ref } from 'vue';
|
|||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { confirm } from '@vben/common-ui';
|
||||
import { getUrlValue } from '@vben/utils';
|
||||
|
||||
import { ElButton, ElCard, ElImage, ElMessage } from 'element-plus';
|
||||
|
||||
|
@ -149,13 +150,6 @@ async function bindSocial() {
|
|||
window.history.replaceState({}, '', location.pathname);
|
||||
}
|
||||
|
||||
// TODO @芋艿:后续搞到 util 里;
|
||||
// 双层 encode 需要在回调后进行 decode
|
||||
function getUrlValue(key: string): string {
|
||||
const url = new URL(decodeURIComponent(location.href));
|
||||
return url.searchParams.get(key) ?? '';
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
bindSocial();
|
||||
|
|
|
@ -8,13 +8,7 @@ import type { Component } from 'vue';
|
|||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import {
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
ref,
|
||||
} from 'vue';
|
||||
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
@ -85,16 +79,15 @@ const withDefaultPlaceholder = <T extends Component>(
|
|||
$t(`ui.placeholder.${type}`);
|
||||
// 透传组件暴露的方法
|
||||
const innerRef = ref();
|
||||
const publicApi: Recordable<any> = {};
|
||||
expose(publicApi);
|
||||
const instance = getCurrentInstance();
|
||||
instance?.proxy?.$nextTick(() => {
|
||||
for (const key in innerRef.value) {
|
||||
if (typeof innerRef.value[key] === 'function') {
|
||||
publicApi[key] = innerRef.value[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
expose(
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, key) => innerRef.value?.[key],
|
||||
has: (_target, key) => key in (innerRef.value || {}),
|
||||
},
|
||||
),
|
||||
);
|
||||
return () =>
|
||||
h(
|
||||
component,
|
||||
|
|
|
@ -10,6 +10,7 @@ import { AuthenticationLogin, Verification, z } from '@vben/common-ui';
|
|||
import { isCaptchaEnable, isTenantEnable } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { getUrlValue } from '@vben/utils';
|
||||
|
||||
import {
|
||||
checkCaptcha,
|
||||
|
@ -124,12 +125,6 @@ async function handleVerifySuccess({ captchaVerification }: any) {
|
|||
}
|
||||
}
|
||||
|
||||
/** tricky: 配合 login.vue 中,redirectUri 需要对参数进行 encode,需要在回调后进行decode */
|
||||
function getUrlValue(key: string): string {
|
||||
const url = new URL(decodeURIComponent(location.href));
|
||||
return url.searchParams.get(key) ?? '';
|
||||
}
|
||||
|
||||
/** 组件挂载时获取租户信息 */
|
||||
onMounted(async () => {
|
||||
await fetchTenantList();
|
||||
|
|
|
@ -6,6 +6,7 @@ import { computed, onMounted, ref } from 'vue';
|
|||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { confirm } from '@vben/common-ui';
|
||||
import { getUrlValue } from '@vben/utils';
|
||||
|
||||
import { NButton, NCard, NImage } from 'naive-ui';
|
||||
|
||||
|
@ -150,13 +151,6 @@ async function bindSocial() {
|
|||
window.history.replaceState({}, '', location.pathname);
|
||||
}
|
||||
|
||||
// TODO @芋艿:后续搞到 util 里;
|
||||
// 双层 encode 需要在回调后进行 decode
|
||||
function getUrlValue(key: string): string {
|
||||
const url = new URL(decodeURIComponent(location.href));
|
||||
return url.searchParams.get(key) ?? '';
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
bindSocial();
|
||||
|
@ -201,7 +195,7 @@ onMounted(() => {
|
|||
<NButton
|
||||
:disabled="!!item.socialUser"
|
||||
size="small"
|
||||
type="link"
|
||||
text
|
||||
@click="onBind(item)"
|
||||
>
|
||||
{{ item.socialUser ? '已绑定' : '绑定' }}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
defineOptions({
|
||||
name: 'FormModelDemo',
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: 'field1',
|
||||
label: '字段1',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: 'field2',
|
||||
label: '字段2',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '选项1', value: '1' },
|
||||
{ label: '选项2', value: '2' },
|
||||
],
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: 'field3',
|
||||
label: '字段3',
|
||||
rules: 'required',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
await formApi.validateAndSubmitForm();
|
||||
// modalApi.close();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
const { values } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
formApi.setValues(values);
|
||||
}
|
||||
}
|
||||
},
|
||||
title: '内嵌表单示例',
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Modal>
|
||||
<Form />
|
||||
</Modal>
|
||||
</template>
|
|
@ -22,7 +22,7 @@ outline: deep
|
|||
|
||||
## 基础用法
|
||||
|
||||
使用 `useVbenDrawer` 创建最基础的模态框。
|
||||
使用 `useVbenDrawer` 创建最基础的抽屉。
|
||||
|
||||
<DemoPreview dir="demos/vben-drawer/basic" />
|
||||
|
||||
|
@ -52,7 +52,7 @@ Drawer 内的内容一般业务中,会比较复杂,所以我们可以将 dra
|
|||
|
||||
::: info 注意
|
||||
|
||||
- `VbenDrawer` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
||||
- `VbenDrawer` 组件对于参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
||||
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
|
||||
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
|
||||
- 如果抽屉的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultDrawerProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。
|
||||
|
@ -77,7 +77,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
|||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
||||
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
|
||||
| connectedComponent | 连接另一个Drawer组件 | `Component` | - |
|
||||
| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
|
||||
| title | 标题 | `string\|slot` | - |
|
||||
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
||||
|
@ -96,7 +96,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
|||
| cancelText | 取消按钮文本 | `string\|slot` | `取消` |
|
||||
| placement | 抽屉弹出位置 | `'left'\|'right'\|'top'\|'bottom'` | `right` |
|
||||
| showCancelButton | 显示取消按钮 | `boolean` | `true` |
|
||||
| showConfirmButton | 显示确认按钮文本 | `boolean` | `true` |
|
||||
| showConfirmButton | 显示确认按钮 | `boolean` | `true` |
|
||||
| class | modal的class,宽度通过这个配置 | `string` | - |
|
||||
| contentClass | modal内容区域的class | `string` | - |
|
||||
| footerClass | modal底部区域的class | `string` | - |
|
||||
|
|
|
@ -324,6 +324,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
|||
| submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
|
||||
| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
|
||||
| compact | 是否紧凑模式(忽略为校验信息所预留的空间) | `boolean` | false |
|
||||
| scrollToFirstError | 表单验证失败时是否自动滚动到第一个错误字段 | `boolean` | false |
|
||||
|
||||
::: tip handleValuesChange
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ const [Form] = useVbenForm({
|
|||
handleSubmit: onSubmit,
|
||||
// 垂直布局,label和input在不同行,值为vertical
|
||||
// 水平布局,label和input在同一行
|
||||
scrollToFirstError: true,
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
- If you want to contribute code to the project, please ensure your code complies with the project's coding standards.
|
||||
- If you are using `vscode`, you need to install the following plugins:
|
||||
|
||||
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - Script code checking
|
||||
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - Code formatting
|
||||
- [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - Word syntax checking
|
||||
|
@ -157,7 +156,6 @@ The most effective solution is to perform Lint checks locally before committing.
|
|||
The project defines corresponding hooks inside `lefthook.yml`:
|
||||
|
||||
- `pre-commit`: Runs before commit, used for code formatting and checking
|
||||
|
||||
- `code-workspace`: Updates VSCode workspace configuration
|
||||
- `lint-md`: Formats Markdown files
|
||||
- `lint-vue`: Formats and checks Vue files
|
||||
|
@ -167,7 +165,6 @@ The project defines corresponding hooks inside `lefthook.yml`:
|
|||
- `lint-json`: Formats other JSON files
|
||||
|
||||
- `post-merge`: Runs after merge, used for automatic dependency installation
|
||||
|
||||
- `install`: Runs `pnpm install` to install new dependencies
|
||||
|
||||
- `commit-msg`: Runs during commit, used for checking commit message format
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
### 友情链接
|
||||
|
||||
- 在您的网站上添加我们的友情链接,链接如下:
|
||||
|
||||
- 名称:Vben Admin
|
||||
- 链接:https://www.vben.pro
|
||||
- 描述:Vben Admin 企业级开箱即用的中后台前端解决方案
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
- 如果你想向项目贡献代码,请确保你的代码符合项目的代码规范。
|
||||
- 如果你使用的是 `vscode`,需要安装以下插件:
|
||||
|
||||
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - 脚本代码检查
|
||||
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - 代码格式化
|
||||
- [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - 单词语法检查
|
||||
|
@ -157,7 +156,6 @@ git hook 一般结合各种 lint,在 git 提交代码的时候进行代码风
|
|||
项目在 `lefthook.yml` 内部定义了相应的 hooks:
|
||||
|
||||
- `pre-commit`: 在提交前运行,用于代码格式化和检查
|
||||
|
||||
- `code-workspace`: 更新 VSCode 工作区配置
|
||||
- `lint-md`: 格式化 Markdown 文件
|
||||
- `lint-vue`: 格式化并检查 Vue 文件
|
||||
|
@ -167,7 +165,6 @@ git hook 一般结合各种 lint,在 git 提交代码的时候进行代码风
|
|||
- `lint-json`: 格式化其他 JSON 文件
|
||||
|
||||
- `post-merge`: 在合并后运行,用于自动安装依赖
|
||||
|
||||
- `install`: 运行 `pnpm install` 安装新依赖
|
||||
|
||||
- `commit-msg`: 在提交时运行,用于检查提交信息格式
|
||||
|
|
|
@ -98,7 +98,7 @@
|
|||
"node": ">=20.10.0",
|
||||
"pnpm": ">=9.12.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0",
|
||||
"packageManager": "pnpm@10.12.4",
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
|
|
|
@ -42,3 +42,13 @@ export function getNestedValue<T>(obj: T, path: string): any {
|
|||
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 URL 参数值
|
||||
* @param key - 参数键
|
||||
* @returns 参数值,或者未找到时返回空字符串
|
||||
*/
|
||||
export function getUrlValue(key: string): string {
|
||||
const url = new URL(decodeURIComponent(location.href));
|
||||
return url.searchParams.get(key) ?? '';
|
||||
}
|
||||
|
|
|
@ -48,13 +48,18 @@ const queryFormStyle = computed(() => {
|
|||
async function handleSubmit(e: Event) {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
const { valid } = await form.validate();
|
||||
const props = unref(rootProps);
|
||||
if (!props.formApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { valid } = await props.formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const values = toRaw(await unref(rootProps).formApi?.getValues());
|
||||
await unref(rootProps).handleSubmit?.(values);
|
||||
const values = toRaw(await props.formApi.getValues());
|
||||
await props.handleSubmit?.(values);
|
||||
}
|
||||
|
||||
async function handleReset(e: Event) {
|
||||
|
|
|
@ -39,6 +39,7 @@ function getDefaultState(): VbenFormProps {
|
|||
layout: 'horizontal',
|
||||
resetButtonOptions: {},
|
||||
schema: [],
|
||||
scrollToFirstError: false,
|
||||
showCollapseButton: false,
|
||||
showDefaultActions: true,
|
||||
submitButtonOptions: {},
|
||||
|
@ -253,6 +254,41 @@ export class FormApi {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到第一个错误字段
|
||||
* @param errors 验证错误对象
|
||||
*/
|
||||
scrollToFirstError(errors: Record<string, any> | string) {
|
||||
// https://github.com/logaretm/vee-validate/discussions/3835
|
||||
const firstErrorFieldName =
|
||||
typeof errors === 'string' ? errors : Object.keys(errors)[0];
|
||||
|
||||
if (!firstErrorFieldName) {
|
||||
return;
|
||||
}
|
||||
|
||||
let el = document.querySelector(
|
||||
`[name="${firstErrorFieldName}"]`,
|
||||
) as HTMLElement;
|
||||
|
||||
// 如果通过 name 属性找不到,尝试通过组件引用查找, 正常情况下不会走到这,怕哪天 vee-validate 改了 name 属性有个兜底的
|
||||
if (!el) {
|
||||
const componentRef = this.getFieldComponentRef(firstErrorFieldName);
|
||||
if (componentRef && componentRef.$el instanceof HTMLElement) {
|
||||
el = componentRef.$el;
|
||||
}
|
||||
}
|
||||
|
||||
if (el) {
|
||||
// 滚动到错误字段,添加一些偏移量以确保字段完全可见
|
||||
el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async setFieldValue(field: string, value: any, shouldValidate?: boolean) {
|
||||
const form = await this.getForm();
|
||||
form.setFieldValue(field, value, shouldValidate);
|
||||
|
@ -389,14 +425,21 @@ export class FormApi {
|
|||
|
||||
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
|
||||
console.error('validate error', validateResult?.errors);
|
||||
|
||||
if (this.state?.scrollToFirstError) {
|
||||
this.scrollToFirstError(validateResult.errors);
|
||||
}
|
||||
}
|
||||
return validateResult;
|
||||
}
|
||||
|
||||
async validateAndSubmitForm() {
|
||||
const form = await this.getForm();
|
||||
const { valid } = await form.validate();
|
||||
const { valid, errors } = await form.validate();
|
||||
if (!valid) {
|
||||
if (this.state?.scrollToFirstError) {
|
||||
this.scrollToFirstError(errors);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return await this.submitForm();
|
||||
|
@ -408,6 +451,10 @@ export class FormApi {
|
|||
|
||||
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
|
||||
console.error('validate error', validateResult?.errors);
|
||||
|
||||
if (this.state?.scrollToFirstError) {
|
||||
this.scrollToFirstError(fieldName);
|
||||
}
|
||||
}
|
||||
return validateResult;
|
||||
}
|
||||
|
|
|
@ -389,6 +389,12 @@ export interface VbenFormProps<
|
|||
*/
|
||||
resetButtonOptions?: ActionButtonOptions;
|
||||
|
||||
/**
|
||||
* 验证失败时是否自动滚动到第一个错误字段
|
||||
* @default false
|
||||
*/
|
||||
scrollToFirstError?: boolean;
|
||||
|
||||
/**
|
||||
* 是否显示默认操作按钮
|
||||
* @default true
|
||||
|
|
|
@ -105,10 +105,17 @@ const shouldDraggable = computed(
|
|||
() => draggable.value && !shouldFullscreen.value && header.value,
|
||||
);
|
||||
|
||||
const getAppendTo = computed(() => {
|
||||
return appendToMain.value
|
||||
? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
|
||||
: undefined;
|
||||
});
|
||||
|
||||
const { dragging, transform } = useModalDraggable(
|
||||
dialogRef,
|
||||
headerRef,
|
||||
shouldDraggable,
|
||||
getAppendTo,
|
||||
);
|
||||
|
||||
const firstOpened = ref(false);
|
||||
|
@ -198,11 +205,6 @@ function handleFocusOutside(e: Event) {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
const getAppendTo = computed(() => {
|
||||
return appendToMain.value
|
||||
? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
|
||||
: undefined;
|
||||
});
|
||||
|
||||
const getForceMount = computed(() => {
|
||||
return !unref(destroyOnClose) && unref(firstOpened);
|
||||
|
@ -224,7 +226,8 @@ function handleClosed() {
|
|||
:append-to="getAppendTo"
|
||||
:class="
|
||||
cn(
|
||||
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0 sm:rounded-[var(--radius)]',
|
||||
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0',
|
||||
shouldFullscreen ? 'sm:rounded-none' : 'sm:rounded-[var(--radius)]',
|
||||
modalClass,
|
||||
{
|
||||
'border-border border': bordered,
|
||||
|
|
|
@ -13,6 +13,7 @@ export function useModalDraggable(
|
|||
targetRef: Ref<HTMLElement | undefined>,
|
||||
dragRef: Ref<HTMLElement | undefined>,
|
||||
draggable: ComputedRef<boolean>,
|
||||
containerSelector?: ComputedRef<string | undefined>,
|
||||
) {
|
||||
const transform = reactive({
|
||||
offsetX: 0,
|
||||
|
@ -30,20 +31,36 @@ export function useModalDraggable(
|
|||
}
|
||||
|
||||
const targetRect = targetRef.value.getBoundingClientRect();
|
||||
|
||||
const { offsetX, offsetY } = transform;
|
||||
const targetLeft = targetRect.left;
|
||||
const targetTop = targetRect.top;
|
||||
const targetWidth = targetRect.width;
|
||||
const targetHeight = targetRect.height;
|
||||
const docElement = document.documentElement;
|
||||
const clientWidth = docElement.clientWidth;
|
||||
const clientHeight = docElement.clientHeight;
|
||||
|
||||
const minLeft = -targetLeft + offsetX;
|
||||
const minTop = -targetTop + offsetY;
|
||||
const maxLeft = clientWidth - targetLeft - targetWidth + offsetX;
|
||||
const maxTop = clientHeight - targetTop - targetHeight + offsetY;
|
||||
let containerRect: DOMRect | null = null;
|
||||
|
||||
if (containerSelector?.value) {
|
||||
const container = document.querySelector(containerSelector.value);
|
||||
if (container) {
|
||||
containerRect = container.getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
|
||||
let maxLeft, maxTop, minLeft, minTop;
|
||||
if (containerRect) {
|
||||
minLeft = containerRect.left - targetLeft + offsetX;
|
||||
maxLeft = containerRect.right - targetLeft - targetWidth + offsetX;
|
||||
minTop = containerRect.top - targetTop + offsetY;
|
||||
maxTop = containerRect.bottom - targetTop - targetHeight + offsetY;
|
||||
} else {
|
||||
const docElement = document.documentElement;
|
||||
const clientWidth = docElement.clientWidth;
|
||||
const clientHeight = docElement.clientHeight;
|
||||
minLeft = -targetLeft + offsetX;
|
||||
minTop = -targetTop + offsetY;
|
||||
maxLeft = clientWidth - targetLeft - targetWidth + offsetX;
|
||||
maxTop = clientHeight - targetTop - targetHeight + offsetY;
|
||||
}
|
||||
|
||||
const onMousemove = (e: MouseEvent) => {
|
||||
let moveX = offsetX + e.clientX - downX;
|
||||
|
|
|
@ -23,6 +23,7 @@ const props = withDefaults(defineProps<TreeProps>(), {
|
|||
defaultExpandedKeys: () => [],
|
||||
defaultExpandedLevel: 0,
|
||||
disabled: false,
|
||||
disabledField: 'disabled',
|
||||
expanded: () => [],
|
||||
iconField: 'icon',
|
||||
labelField: 'label',
|
||||
|
@ -101,16 +102,37 @@ function updateTreeValue() {
|
|||
if (val === undefined) {
|
||||
treeValue.value = undefined;
|
||||
} else {
|
||||
treeValue.value = Array.isArray(val)
|
||||
? val.map((v) => getItemByValue(v))
|
||||
: getItemByValue(val);
|
||||
if (Array.isArray(val)) {
|
||||
const filteredValues = val.filter((v) => {
|
||||
const item = getItemByValue(v);
|
||||
return item && !get(item, props.disabledField);
|
||||
});
|
||||
treeValue.value = filteredValues.map((v) => getItemByValue(v));
|
||||
|
||||
if (filteredValues.length !== val.length) {
|
||||
modelValue.value = filteredValues;
|
||||
}
|
||||
} else {
|
||||
const item = getItemByValue(val);
|
||||
if (item && !get(item, props.disabledField)) {
|
||||
treeValue.value = item;
|
||||
} else {
|
||||
treeValue.value = undefined;
|
||||
modelValue.value = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateModelValue(val: Arrayable<Recordable<any>>) {
|
||||
modelValue.value = Array.isArray(val)
|
||||
? val.map((v) => get(v, props.valueField))
|
||||
: get(val, props.valueField);
|
||||
if (Array.isArray(val)) {
|
||||
const filteredVal = val.filter((v) => !get(v, props.disabledField));
|
||||
modelValue.value = filteredVal.map((v) => get(v, props.valueField));
|
||||
} else {
|
||||
if (val && !get(val, props.disabledField)) {
|
||||
modelValue.value = get(val, props.valueField);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function expandToLevel(level: number) {
|
||||
|
@ -149,10 +171,18 @@ function collapseAll() {
|
|||
expanded.value = [];
|
||||
}
|
||||
|
||||
function isNodeDisabled(item: FlattenedItem<Recordable<any>>) {
|
||||
return props.disabled || get(item.value, props.disabledField);
|
||||
}
|
||||
|
||||
function onToggle(item: FlattenedItem<Recordable<any>>) {
|
||||
emits('expand', item);
|
||||
}
|
||||
function onSelect(item: FlattenedItem<Recordable<any>>, isSelected: boolean) {
|
||||
if (isNodeDisabled(item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!props.checkStrictly &&
|
||||
props.multiple &&
|
||||
|
@ -224,34 +254,44 @@ defineExpose({
|
|||
:class="
|
||||
cn('cursor-pointer', getNodeClass?.(item), {
|
||||
'data-[selected]:bg-accent': !multiple,
|
||||
'cursor-not-allowed': disabled,
|
||||
'cursor-not-allowed': isNodeDisabled(item),
|
||||
})
|
||||
"
|
||||
v-bind="
|
||||
Object.assign(item.bind, {
|
||||
onfocus: disabled ? 'this.blur()' : undefined,
|
||||
onfocus: isNodeDisabled(item) ? 'this.blur()' : undefined,
|
||||
disabled: isNodeDisabled(item),
|
||||
})
|
||||
"
|
||||
@select="
|
||||
(event) => {
|
||||
(event: any) => {
|
||||
if (isNodeDisabled(item)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
if (event.detail.originalEvent.type === 'click') {
|
||||
event.preventDefault();
|
||||
}
|
||||
!disabled && onSelect(item, event.detail.isSelected);
|
||||
onSelect(item, event.detail.isSelected);
|
||||
}
|
||||
"
|
||||
@toggle="
|
||||
(event) => {
|
||||
(event: any) => {
|
||||
if (event.detail.originalEvent.type === 'click') {
|
||||
event.preventDefault();
|
||||
}
|
||||
!disabled && onToggle(item);
|
||||
!isNodeDisabled(item) && onToggle(item);
|
||||
}
|
||||
"
|
||||
class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2"
|
||||
>
|
||||
<ChevronRight
|
||||
v-if="item.hasChildren"
|
||||
v-if="
|
||||
item.hasChildren &&
|
||||
Array.isArray(item.value[childrenField]) &&
|
||||
item.value[childrenField].length > 0
|
||||
"
|
||||
class="size-4 cursor-pointer transition"
|
||||
:class="{ 'rotate-90': isExpanded }"
|
||||
@click.stop="
|
||||
|
@ -266,24 +306,32 @@ defineExpose({
|
|||
</div>
|
||||
<Checkbox
|
||||
v-if="multiple"
|
||||
:checked="isSelected"
|
||||
:disabled="disabled"
|
||||
:indeterminate="isIndeterminate"
|
||||
:checked="isSelected && !isNodeDisabled(item)"
|
||||
:disabled="isNodeDisabled(item)"
|
||||
:indeterminate="isIndeterminate && !isNodeDisabled(item)"
|
||||
@click="
|
||||
() => {
|
||||
!disabled && handleSelect();
|
||||
// onSelect(item, !isSelected);
|
||||
(event: MouseEvent) => {
|
||||
if (isNodeDisabled(item)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
handleSelect();
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div
|
||||
class="flex items-center gap-1 pl-2"
|
||||
@click="
|
||||
(_event) => {
|
||||
// $event.stopPropagation();
|
||||
// $event.preventDefault();
|
||||
!disabled && handleSelect();
|
||||
// onSelect(item, !isSelected);
|
||||
(event: MouseEvent) => {
|
||||
if (isNodeDisabled(item)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
handleSelect();
|
||||
}
|
||||
"
|
||||
>
|
||||
|
|
|
@ -22,6 +22,8 @@ export interface TreeProps {
|
|||
defaultValue?: Arrayable<number | string>;
|
||||
/** 禁用 */
|
||||
disabled?: boolean;
|
||||
/** 禁用字段名 */
|
||||
disabledField?: string;
|
||||
/** 自定义节点类名 */
|
||||
getNodeClass?: (item: FlattenedItem<Recordable<any>>) => string;
|
||||
iconField?: string;
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
"@vueuse/core": "catalog:",
|
||||
"@vueuse/integrations": "catalog:",
|
||||
"crypto-js": "catalog:",
|
||||
"json-bigint": "catalog:",
|
||||
"qrcode": "catalog:",
|
||||
"tippy.js": "catalog:",
|
||||
"vue": "catalog:",
|
||||
|
|
|
@ -3,6 +3,7 @@ export { default as PointSelectionCaptchaCard } from './point-selection-captcha/
|
|||
|
||||
export { default as SliderCaptcha } from './slider-captcha/index.vue';
|
||||
export { default as SliderRotateCaptcha } from './slider-rotate-captcha/index.vue';
|
||||
export { default as SliderTranslateCaptcha } from './slider-translate-captcha/index.vue';
|
||||
export type * from './types';
|
||||
|
||||
export { default as Verification } from './verification/index.vue';
|
||||
|
|
|
@ -0,0 +1,311 @@
|
|||
<script setup lang="ts">
|
||||
import type {
|
||||
CaptchaVerifyPassingData,
|
||||
SliderCaptchaActionType,
|
||||
SliderRotateVerifyPassingData,
|
||||
SliderTranslateCaptchaProps,
|
||||
} from '../types';
|
||||
|
||||
import {
|
||||
computed,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref,
|
||||
unref,
|
||||
useTemplateRef,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import SliderCaptcha from '../slider-captcha/index.vue';
|
||||
|
||||
const props = withDefaults(defineProps<SliderTranslateCaptchaProps>(), {
|
||||
defaultTip: '',
|
||||
canvasWidth: 420,
|
||||
canvasHeight: 280,
|
||||
squareLength: 42,
|
||||
circleRadius: 10,
|
||||
src: '',
|
||||
diffDistance: 3,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [CaptchaVerifyPassingData];
|
||||
}>();
|
||||
|
||||
const PI: number = Math.PI;
|
||||
enum CanvasOpr {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
Clip = 'clip',
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
Fill = 'fill',
|
||||
}
|
||||
|
||||
const modalValue = defineModel<boolean>({ default: false });
|
||||
|
||||
const slideBarRef = useTemplateRef<SliderCaptchaActionType>('slideBarRef');
|
||||
const puzzleCanvasRef = useTemplateRef<HTMLCanvasElement>('puzzleCanvasRef');
|
||||
const pieceCanvasRef = useTemplateRef<HTMLCanvasElement>('pieceCanvasRef');
|
||||
|
||||
const state = reactive({
|
||||
dragging: false,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
pieceX: 0,
|
||||
pieceY: 0,
|
||||
moveDistance: 0,
|
||||
isPassing: false,
|
||||
showTip: false,
|
||||
});
|
||||
|
||||
const left = ref('0');
|
||||
|
||||
const pieceStyle = computed(() => {
|
||||
return {
|
||||
left: left.value,
|
||||
};
|
||||
});
|
||||
|
||||
function setLeft(val: string) {
|
||||
left.value = val;
|
||||
}
|
||||
|
||||
const verifyTip = computed(() => {
|
||||
return state.isPassing
|
||||
? $t('ui.captcha.sliderTranslateSuccessTip', [
|
||||
((state.endTime - state.startTime) / 1000).toFixed(1),
|
||||
])
|
||||
: $t('ui.captcha.sliderTranslateFailTip');
|
||||
});
|
||||
function handleStart() {
|
||||
state.startTime = Date.now();
|
||||
}
|
||||
|
||||
function handleDragBarMove(data: SliderRotateVerifyPassingData) {
|
||||
state.dragging = true;
|
||||
const { moveX } = data;
|
||||
state.moveDistance = moveX;
|
||||
setLeft(`${moveX}px`);
|
||||
}
|
||||
|
||||
function handleDragEnd() {
|
||||
const { pieceX } = state;
|
||||
const { diffDistance } = props;
|
||||
|
||||
if (Math.abs(pieceX - state.moveDistance) >= (diffDistance || 3)) {
|
||||
setLeft('0');
|
||||
state.moveDistance = 0;
|
||||
} else {
|
||||
checkPass();
|
||||
}
|
||||
state.showTip = true;
|
||||
state.dragging = false;
|
||||
}
|
||||
|
||||
function checkPass() {
|
||||
state.isPassing = true;
|
||||
state.endTime = Date.now();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => state.isPassing,
|
||||
(isPassing) => {
|
||||
if (isPassing) {
|
||||
const { endTime, startTime } = state;
|
||||
const time = (endTime - startTime) / 1000;
|
||||
emit('success', { isPassing, time: time.toFixed(1) });
|
||||
}
|
||||
modalValue.value = isPassing;
|
||||
},
|
||||
);
|
||||
|
||||
function resetCanvas() {
|
||||
const { canvasWidth, canvasHeight } = props;
|
||||
const puzzleCanvas = unref(puzzleCanvasRef);
|
||||
const pieceCanvas = unref(pieceCanvasRef);
|
||||
if (!puzzleCanvas || !pieceCanvas) return;
|
||||
pieceCanvas.width = canvasWidth;
|
||||
const puzzleCanvasCtx = puzzleCanvas.getContext('2d');
|
||||
// Canvas2D: Multiple readback operations using getImageData
|
||||
// are faster with the willReadFrequently attribute set to true.
|
||||
// See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently (anonymous)
|
||||
const pieceCanvasCtx = pieceCanvas.getContext('2d', {
|
||||
willReadFrequently: true,
|
||||
});
|
||||
if (!puzzleCanvasCtx || !pieceCanvasCtx) return;
|
||||
puzzleCanvasCtx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||
pieceCanvasCtx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
const { canvasWidth, canvasHeight, squareLength, circleRadius, src } = props;
|
||||
const puzzleCanvas = unref(puzzleCanvasRef);
|
||||
const pieceCanvas = unref(pieceCanvasRef);
|
||||
if (!puzzleCanvas || !pieceCanvas) return;
|
||||
const puzzleCanvasCtx = puzzleCanvas.getContext('2d');
|
||||
// Canvas2D: Multiple readback operations using getImageData
|
||||
// are faster with the willReadFrequently attribute set to true.
|
||||
// See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently (anonymous)
|
||||
const pieceCanvasCtx = pieceCanvas.getContext('2d', {
|
||||
willReadFrequently: true,
|
||||
});
|
||||
if (!puzzleCanvasCtx || !pieceCanvasCtx) return;
|
||||
const img = new Image();
|
||||
// 解决跨域
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.src = src;
|
||||
img.addEventListener('load', () => {
|
||||
draw(puzzleCanvasCtx, pieceCanvasCtx);
|
||||
puzzleCanvasCtx.drawImage(img, 0, 0, canvasWidth, canvasHeight);
|
||||
pieceCanvasCtx.drawImage(img, 0, 0, canvasWidth, canvasHeight);
|
||||
const pieceLength = squareLength + 2 * circleRadius + 3;
|
||||
const sx = state.pieceX;
|
||||
const sy = state.pieceY - 2 * circleRadius - 1;
|
||||
const imageData = pieceCanvasCtx.getImageData(
|
||||
sx,
|
||||
sy,
|
||||
pieceLength,
|
||||
pieceLength,
|
||||
);
|
||||
pieceCanvas.width = pieceLength;
|
||||
pieceCanvasCtx.putImageData(imageData, 0, sy);
|
||||
setLeft('0');
|
||||
});
|
||||
}
|
||||
|
||||
function getRandomNumberByRange(start: number, end: number) {
|
||||
return Math.round(Math.random() * (end - start) + start);
|
||||
}
|
||||
|
||||
// 绘制拼图
|
||||
function draw(ctx1: CanvasRenderingContext2D, ctx2: CanvasRenderingContext2D) {
|
||||
const { canvasWidth, canvasHeight, squareLength, circleRadius } = props;
|
||||
state.pieceX = getRandomNumberByRange(
|
||||
squareLength + 2 * circleRadius,
|
||||
canvasWidth - (squareLength + 2 * circleRadius),
|
||||
);
|
||||
state.pieceY = getRandomNumberByRange(
|
||||
3 * circleRadius,
|
||||
canvasHeight - (squareLength + 2 * circleRadius),
|
||||
);
|
||||
drawPiece(ctx1, state.pieceX, state.pieceY, CanvasOpr.Fill);
|
||||
drawPiece(ctx2, state.pieceX, state.pieceY, CanvasOpr.Clip);
|
||||
}
|
||||
|
||||
// 绘制拼图切块
|
||||
function drawPiece(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
opr: CanvasOpr,
|
||||
) {
|
||||
const { squareLength, circleRadius } = props;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.arc(
|
||||
x + squareLength / 2,
|
||||
y - circleRadius + 2,
|
||||
circleRadius,
|
||||
0.72 * PI,
|
||||
2.26 * PI,
|
||||
);
|
||||
ctx.lineTo(x + squareLength, y);
|
||||
ctx.arc(
|
||||
x + squareLength + circleRadius - 2,
|
||||
y + squareLength / 2,
|
||||
circleRadius,
|
||||
1.21 * PI,
|
||||
2.78 * PI,
|
||||
);
|
||||
ctx.lineTo(x + squareLength, y + squareLength);
|
||||
ctx.lineTo(x, y + squareLength);
|
||||
ctx.arc(
|
||||
x + circleRadius - 2,
|
||||
y + squareLength / 2,
|
||||
circleRadius + 0.4,
|
||||
2.76 * PI,
|
||||
1.24 * PI,
|
||||
true,
|
||||
);
|
||||
ctx.lineTo(x, y);
|
||||
ctx.lineWidth = 2;
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)';
|
||||
ctx.stroke();
|
||||
opr === CanvasOpr.Clip ? ctx.clip() : ctx.fill();
|
||||
ctx.globalCompositeOperation = 'destination-over';
|
||||
}
|
||||
|
||||
function resume() {
|
||||
state.showTip = false;
|
||||
const basicEl = unref(slideBarRef);
|
||||
if (!basicEl) {
|
||||
return;
|
||||
}
|
||||
state.dragging = false;
|
||||
state.isPassing = false;
|
||||
state.pieceX = 0;
|
||||
state.pieceY = 0;
|
||||
|
||||
basicEl.resume();
|
||||
resetCanvas();
|
||||
initCanvas();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCanvas();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex flex-col items-center">
|
||||
<div
|
||||
class="border-border relative flex cursor-pointer overflow-hidden border shadow-md"
|
||||
>
|
||||
<canvas
|
||||
ref="puzzleCanvasRef"
|
||||
:width="canvasWidth"
|
||||
:height="canvasHeight"
|
||||
@click="resume"
|
||||
></canvas>
|
||||
<canvas
|
||||
ref="pieceCanvasRef"
|
||||
:width="canvasWidth"
|
||||
:height="canvasHeight"
|
||||
:style="pieceStyle"
|
||||
class="absolute"
|
||||
@click="resume"
|
||||
></canvas>
|
||||
<div
|
||||
class="h-15 absolute bottom-3 left-0 z-10 block w-full text-center text-xs leading-[30px] text-white"
|
||||
>
|
||||
<div
|
||||
v-if="state.showTip"
|
||||
:class="{
|
||||
'bg-success/80': state.isPassing,
|
||||
'bg-destructive/80': !state.isPassing,
|
||||
}"
|
||||
>
|
||||
{{ verifyTip }}
|
||||
</div>
|
||||
<div v-if="!state.dragging" class="bg-black/30">
|
||||
{{ defaultTip || $t('ui.captcha.sliderTranslateDefaultTip') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SliderCaptcha
|
||||
ref="slideBarRef"
|
||||
v-model="modalValue"
|
||||
class="mt-5"
|
||||
is-slot
|
||||
@end="handleDragEnd"
|
||||
@move="handleDragBarMove"
|
||||
@start="handleStart"
|
||||
>
|
||||
<template v-for="(_, key) in $slots" :key="key" #[key]="slotProps">
|
||||
<slot :name="key" v-bind="slotProps"></slot>
|
||||
</template>
|
||||
</SliderCaptcha>
|
||||
</div>
|
||||
</template>
|
|
@ -159,6 +159,42 @@ export interface SliderRotateCaptchaProps {
|
|||
defaultTip?: string;
|
||||
}
|
||||
|
||||
export interface SliderTranslateCaptchaProps {
|
||||
/**
|
||||
* @description 拼图的宽度
|
||||
* @default 420
|
||||
*/
|
||||
canvasWidth?: number;
|
||||
/**
|
||||
* @description 拼图的高度
|
||||
* @default 280
|
||||
*/
|
||||
canvasHeight?: number;
|
||||
/**
|
||||
* @description 切块上正方形的长度
|
||||
* @default 42
|
||||
*/
|
||||
squareLength?: number;
|
||||
/**
|
||||
* @description 切块上圆形的半径
|
||||
* @default 10
|
||||
*/
|
||||
circleRadius?: number;
|
||||
/**
|
||||
* @description 图片的地址
|
||||
*/
|
||||
src?: string;
|
||||
/**
|
||||
* @description 允许的最大差距
|
||||
* @default 3
|
||||
*/
|
||||
diffDistance?: number;
|
||||
/**
|
||||
* @description 默认提示文本
|
||||
*/
|
||||
defaultTip?: string;
|
||||
}
|
||||
|
||||
export interface CaptchaVerifyPassingData {
|
||||
isPassing: boolean;
|
||||
time: number | string;
|
||||
|
|
|
@ -29,7 +29,7 @@ function close() {
|
|||
<div
|
||||
role="alert"
|
||||
v-if="isDocAlertEnable() && isVisible"
|
||||
class="border-primary bg-primary/10 relative my-2 flex h-8 w-full items-center gap-2 rounded-md border p-2"
|
||||
class="border-primary bg-primary/10 relative m-3 my-2 flex h-8 items-center gap-2 rounded-md border p-2"
|
||||
>
|
||||
<span class="grid shrink-0 place-items-center">
|
||||
<VbenIcon icon="mdi:information-outline" class="text-primary size-5" />
|
||||
|
|
|
@ -76,6 +76,12 @@ const keyword = ref('');
|
|||
const keywordDebounce = refDebounced(keyword, 300);
|
||||
const innerIcons = ref<string[]>([]);
|
||||
|
||||
/* 当检索关键词变化时,重置分页 */
|
||||
watch(keywordDebounce, () => {
|
||||
currentPage.value = 1;
|
||||
setCurrentPage(1);
|
||||
});
|
||||
|
||||
watchDebounced(
|
||||
() => props.prefix,
|
||||
async (prefix) => {
|
||||
|
|
|
@ -18,6 +18,7 @@ export {
|
|||
VbenAvatar,
|
||||
VbenButton,
|
||||
VbenButtonGroup,
|
||||
VbenCheckbox,
|
||||
VbenCheckButtonGroup,
|
||||
VbenCountToAnimator,
|
||||
VbenFullScreen,
|
||||
|
@ -25,6 +26,7 @@ export {
|
|||
VbenLoading,
|
||||
VbenLogo,
|
||||
VbenPinInput,
|
||||
VbenSelect,
|
||||
VbenSpinner,
|
||||
VbenTree,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
|
|
@ -18,6 +18,9 @@ import { $t } from '@vben/locales';
|
|||
|
||||
import { isBoolean } from '@vben-core/shared/utils';
|
||||
|
||||
// @ts-ignore
|
||||
import JsonBigint from 'json-bigint';
|
||||
|
||||
defineOptions({ name: 'JsonViewer' });
|
||||
|
||||
const props = withDefaults(defineProps<JsonViewerProps>(), {
|
||||
|
@ -68,6 +71,20 @@ function handleClick(event: MouseEvent) {
|
|||
emit('click', event);
|
||||
}
|
||||
|
||||
// 支持显示 bigint 数据,如较长的订单号
|
||||
const jsonData = computed<Record<string, any>>(() => {
|
||||
if (typeof props.value !== 'string') {
|
||||
return props.value || {};
|
||||
}
|
||||
|
||||
try {
|
||||
return JsonBigint({ storeAsString: true }).parse(props.value);
|
||||
} catch (error) {
|
||||
console.error('JSON parse error:', error);
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
const bindProps = computed<Recordable<any>>(() => {
|
||||
const copyable = {
|
||||
copyText: $t('ui.jsonViewer.copy'),
|
||||
|
@ -79,6 +96,7 @@ const bindProps = computed<Recordable<any>>(() => {
|
|||
return {
|
||||
...props,
|
||||
...attrs,
|
||||
value: jsonData.value,
|
||||
onCopied: (event: JsonViewerAction) => emit('copied', event),
|
||||
onKeyclick: (key: string) => emit('keyClick', key),
|
||||
onClick: (event: MouseEvent) => handleClick(event),
|
||||
|
|
|
@ -3,6 +3,8 @@ import type { AuthenticationProps } from './types';
|
|||
|
||||
import { computed, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { useVbenModal } from '@vben-core/popup-ui';
|
||||
import { Slot, VbenAvatar } from '@vben-core/shadcn-ui';
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { Arrayable, MaybeElementRef } from '@vueuse/core';
|
|||
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { computed, onUnmounted, ref, unref, watch } from 'vue';
|
||||
import { computed, effectScope, onUnmounted, ref, unref, watch } from 'vue';
|
||||
|
||||
import { isFunction } from '@vben/utils';
|
||||
|
||||
|
@ -20,12 +20,12 @@ const DEFAULT_ENTER_DELAY = 0; // 鼠标进入延迟时间,默认为 0(立
|
|||
|
||||
/**
|
||||
* 监测鼠标是否在元素内部,如果在元素内部则返回 true,否则返回 false
|
||||
* @param refElement 所有需要检测的元素。如果提供了一个数组,那么鼠标在任何一个元素内部都会返回 true
|
||||
* @param refElement 所有需要检测的元素。支持单个元素、元素数组或响应式引用的元素数组。如果鼠标在任何一个元素内部都会返回 true
|
||||
* @param delay 延迟更新状态的时间,可以是数字或包含进入/离开延迟的配置对象
|
||||
* @returns 返回一个数组,第一个元素是一个 ref,表示鼠标是否在元素内部,第二个元素是一个控制器,可以通过 enable 和 disable 方法来控制监听器的启用和禁用
|
||||
*/
|
||||
export function useHoverToggle(
|
||||
refElement: Arrayable<MaybeElementRef>,
|
||||
refElement: Arrayable<MaybeElementRef> | Ref<HTMLElement[] | null>,
|
||||
delay: (() => number) | HoverDelayOptions | number = DEFAULT_LEAVE_DELAY,
|
||||
) {
|
||||
// 兼容旧版本API
|
||||
|
@ -38,20 +38,58 @@ export function useHoverToggle(
|
|||
...delay,
|
||||
};
|
||||
|
||||
const isHovers: Array<Ref<boolean>> = [];
|
||||
const value = ref(false);
|
||||
const enterTimer = ref<ReturnType<typeof setTimeout> | undefined>();
|
||||
const leaveTimer = ref<ReturnType<typeof setTimeout> | undefined>();
|
||||
const refs = Array.isArray(refElement) ? refElement : [refElement];
|
||||
refs.forEach((refEle) => {
|
||||
const eleRef = computed(() => {
|
||||
const ele = unref(refEle);
|
||||
return ele instanceof Element ? ele : (ele?.$el as Element);
|
||||
});
|
||||
const isHover = useElementHover(eleRef);
|
||||
isHovers.push(isHover);
|
||||
const hoverScopes = ref<ReturnType<typeof effectScope>[]>([]);
|
||||
|
||||
// 使用计算属性包装 refElement,使其响应式变化
|
||||
const refs = computed(() => {
|
||||
const raw = unref(refElement);
|
||||
if (raw === null) return [];
|
||||
return Array.isArray(raw) ? raw : [raw];
|
||||
});
|
||||
const isOutsideAll = computed(() => isHovers.every((v) => !v.value));
|
||||
// 存储所有 hover 状态
|
||||
const isHovers = ref<Array<Ref<boolean>>>([]);
|
||||
|
||||
// 更新 hover 监听的函数
|
||||
function updateHovers() {
|
||||
// 停止并清理之前的作用域
|
||||
hoverScopes.value.forEach((scope) => scope.stop());
|
||||
hoverScopes.value = [];
|
||||
|
||||
isHovers.value = refs.value.map((refEle) => {
|
||||
if (!refEle) {
|
||||
return ref(false);
|
||||
}
|
||||
const eleRef = computed(() => {
|
||||
const ele = unref(refEle);
|
||||
return ele instanceof Element ? ele : (ele?.$el as Element);
|
||||
});
|
||||
|
||||
// 为每个元素创建独立的作用域
|
||||
const scope = effectScope();
|
||||
const hoverRef = scope.run(() => useElementHover(eleRef)) || ref(false);
|
||||
hoverScopes.value.push(scope);
|
||||
|
||||
return hoverRef;
|
||||
});
|
||||
}
|
||||
|
||||
// 监听元素数量变化,避免过度执行
|
||||
const elementsCount = computed(() => {
|
||||
const raw = unref(refElement);
|
||||
if (raw === null) return 0;
|
||||
return Array.isArray(raw) ? raw.length : 1;
|
||||
});
|
||||
|
||||
// 初始设置
|
||||
updateHovers();
|
||||
|
||||
// 只在元素数量变化时重新设置监听器
|
||||
const stopWatcher = watch(elementsCount, updateHovers, { deep: false });
|
||||
|
||||
const isOutsideAll = computed(() => isHovers.value.every((v) => !v.value));
|
||||
|
||||
function clearTimers() {
|
||||
if (enterTimer.value) {
|
||||
|
@ -96,7 +134,7 @@ export function useHoverToggle(
|
|||
}
|
||||
}
|
||||
|
||||
const watcher = watch(
|
||||
const hoverWatcher = watch(
|
||||
isOutsideAll,
|
||||
(val) => {
|
||||
setValueDelay(!val);
|
||||
|
@ -106,15 +144,19 @@ export function useHoverToggle(
|
|||
|
||||
const controller = {
|
||||
enable() {
|
||||
watcher.resume();
|
||||
hoverWatcher.resume();
|
||||
},
|
||||
disable() {
|
||||
watcher.pause();
|
||||
hoverWatcher.pause();
|
||||
},
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimers();
|
||||
// 停止监听器
|
||||
stopWatcher();
|
||||
// 停止所有剩余的作用域
|
||||
hoverScopes.value.forEach((scope) => scope.stop());
|
||||
});
|
||||
|
||||
return [value, controller] as [typeof value, typeof controller];
|
||||
|
|
|
@ -62,21 +62,23 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
|
|||
</template>
|
||||
</AuthenticationFormView>
|
||||
|
||||
<!-- 头部 Logo 和应用名称 -->
|
||||
<div
|
||||
v-if="logo || appName"
|
||||
class="absolute left-0 top-0 z-10 flex flex-1"
|
||||
@click="clickLogo"
|
||||
>
|
||||
<slot name="logo">
|
||||
<!-- 头部 Logo 和应用名称 -->
|
||||
<div
|
||||
class="text-foreground lg:text-foreground ml-4 mt-4 flex flex-1 items-center sm:left-6 sm:top-6"
|
||||
v-if="logo || appName"
|
||||
class="absolute left-0 top-0 z-10 flex flex-1"
|
||||
@click="clickLogo"
|
||||
>
|
||||
<img v-if="logo" :alt="appName" :src="logo" class="mr-2" width="42" />
|
||||
<p v-if="appName" class="m-0 text-xl font-medium">
|
||||
{{ appName }}
|
||||
</p>
|
||||
<div
|
||||
class="text-foreground lg:text-foreground ml-4 mt-4 flex flex-1 items-center sm:left-6 sm:top-6"
|
||||
>
|
||||
<img v-if="logo" :alt="appName" :src="logo" class="mr-2" width="42" />
|
||||
<p v-if="appName" class="m-0 text-xl font-medium">
|
||||
{{ appName }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<!-- 系统介绍 -->
|
||||
<div v-if="!authPanelCenter" class="relative hidden w-0 flex-1 lg:block">
|
||||
|
|
|
@ -86,18 +86,20 @@ const [Modal, modalApi] = useVbenModal({
|
|||
</VbenButton>
|
||||
</VbenButtonGroup>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex justify-start">
|
||||
<p class="w-24 p-2">软件外包:</p>
|
||||
<img
|
||||
src="/wx-xingyu.png"
|
||||
alt="数舵科技"
|
||||
class="cursor-pointer"
|
||||
width="80%"
|
||||
@click="openWindow('https://shuduokeji.com')"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-2 flex justify-center pt-4 text-sm italic">
|
||||
本项目采用 <Badge class="mx-2" variant="destructive">MIT</Badge>
|
||||
开源协议,个人与企业可100% 免费使用。
|
||||
开源协议,个人与企业可100% 免费使用
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
@ -56,8 +56,10 @@ async function handleChange(id: number | undefined) {
|
|||
class="hover:bg-accent ml-1 mr-2 h-8 w-32 cursor-pointer rounded-full p-1.5"
|
||||
>
|
||||
<IconifyIcon icon="lucide:align-justify" class="mr-4" />
|
||||
{{ $t('page.tenant.placeholder') }}
|
||||
<!-- {{ tenants.find((item) => item.id === visitTenantId)?.name }} -->
|
||||
{{
|
||||
tenants.find((item) => item.id === visitTenantId)?.name ||
|
||||
$t('page.tenant.placeholder')
|
||||
}}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-40 p-0 pb-1">
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from '@vueuse/core';
|
||||
|
||||
import echarts from './echarts';
|
||||
// TODO @xingyu:有 500kb,china.json 会影响打包么?
|
||||
import chinaMap2 from './map/china2.json';
|
||||
import chinaMap from './map/china.json';
|
||||
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import type { VxeGridSlots, VxeGridSlotTypes } from 'vxe-table';
|
||||
|
||||
import type { SlotsType } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@vben-core/form-ui';
|
||||
|
||||
import type { ExtendedVxeGridApi, VxeGridProps } from './types';
|
||||
|
@ -9,6 +13,12 @@ import { useStore } from '@vben-core/shared/store';
|
|||
import { VxeGridApi } from './api';
|
||||
import VxeGrid from './use-vxe-grid.vue';
|
||||
|
||||
type FilteredSlots<T> = {
|
||||
[K in keyof VxeGridSlots<T> as K extends 'form'
|
||||
? never
|
||||
: K]: VxeGridSlots<T>[K];
|
||||
};
|
||||
|
||||
export function useVbenVxeGrid<
|
||||
T extends Record<string, any> = any,
|
||||
D extends BaseFormComponentType = BaseFormComponentType,
|
||||
|
@ -31,6 +41,16 @@ export function useVbenVxeGrid<
|
|||
{
|
||||
name: 'VbenVxeGrid',
|
||||
inheritAttrs: false,
|
||||
slots: Object as SlotsType<
|
||||
{
|
||||
// 表格标题
|
||||
'table-title': undefined;
|
||||
// 工具栏左侧部分
|
||||
'toolbar-actions': VxeGridSlotTypes.DefaultSlotParams<T>;
|
||||
// 工具栏右侧部分
|
||||
'toolbar-tools': VxeGridSlotTypes.DefaultSlotParams<T>;
|
||||
} & FilteredSlots<T>
|
||||
>,
|
||||
},
|
||||
);
|
||||
// Add reactivity support
|
||||
|
|
|
@ -46,8 +46,11 @@
|
|||
"sliderDefaultText": "Slider and drag",
|
||||
"alt": "Supports img tag src attribute value",
|
||||
"sliderRotateDefaultTip": "Click picture to refresh",
|
||||
"sliderTranslateDefaultTip": "Click picture to refresh",
|
||||
"sliderRotateFailTip": "Validation failed",
|
||||
"sliderRotateSuccessTip": "Validation successful, time {0} seconds",
|
||||
"sliderTranslateFailTip": "Validation failed",
|
||||
"sliderTranslateSuccessTip": "Validation successful, time {0} seconds",
|
||||
"refreshAriaLabel": "Refresh captcha",
|
||||
"confirmAriaLabel": "Confirm selection",
|
||||
"confirm": "Confirm",
|
||||
|
|
|
@ -45,8 +45,11 @@
|
|||
"sliderSuccessText": "验证通过",
|
||||
"sliderDefaultText": "请按住滑块拖动",
|
||||
"sliderRotateDefaultTip": "点击图片可刷新",
|
||||
"sliderTranslateDefaultTip": "点击图片可刷新",
|
||||
"sliderRotateFailTip": "验证失败",
|
||||
"sliderRotateSuccessTip": "验证成功,耗时{0}秒",
|
||||
"sliderTranslateFailTip": "验证失败",
|
||||
"sliderTranslateSuccessTip": "验证成功,耗时{0}秒",
|
||||
"alt": "支持img标签src属性值",
|
||||
"refreshAriaLabel": "刷新验证码",
|
||||
"confirmAriaLabel": "确认选择",
|
||||
|
|
|
@ -8,13 +8,7 @@ import type { Component } from 'vue';
|
|||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import {
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
ref,
|
||||
} from 'vue';
|
||||
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
@ -82,16 +76,24 @@ const withDefaultPlaceholder = <T extends Component>(
|
|||
$t(`ui.placeholder.${type}`);
|
||||
// 透传组件暴露的方法
|
||||
const innerRef = ref();
|
||||
const publicApi: Recordable<any> = {};
|
||||
expose(publicApi);
|
||||
const instance = getCurrentInstance();
|
||||
instance?.proxy?.$nextTick(() => {
|
||||
for (const key in innerRef.value) {
|
||||
if (typeof innerRef.value[key] === 'function') {
|
||||
publicApi[key] = innerRef.value[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
// const publicApi: Recordable<any> = {};
|
||||
expose(
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, key) => innerRef.value?.[key],
|
||||
has: (_target, key) => key in (innerRef.value || {}),
|
||||
},
|
||||
),
|
||||
);
|
||||
// const instance = getCurrentInstance();
|
||||
// instance?.proxy?.$nextTick(() => {
|
||||
// for (const key in innerRef.value) {
|
||||
// if (typeof innerRef.value[key] === 'function') {
|
||||
// publicApi[key] = innerRef.value[key];
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
return () =>
|
||||
h(
|
||||
component,
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"custom": "Custom Component",
|
||||
"api": "Api",
|
||||
"merge": "Merge Form",
|
||||
"scrollToError": "Scroll to Error Field",
|
||||
"upload-error": "Partial file upload failed",
|
||||
"upload-urls": "Urls after file upload",
|
||||
"file": "file",
|
||||
|
@ -41,6 +42,7 @@
|
|||
"pointSelection": "Point Selection Captcha",
|
||||
"sliderCaptcha": "Slider Captcha",
|
||||
"sliderRotateCaptcha": "Rotate Captcha",
|
||||
"sliderTranslateCaptcha": "Translate Captcha",
|
||||
"captchaCardTitle": "Please complete the security verification",
|
||||
"pageDescription": "Verify user identity by clicking on specific locations in the image.",
|
||||
"pageTitle": "Captcha Component Example",
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"custom": "自定义组件",
|
||||
"api": "Api",
|
||||
"merge": "合并表单",
|
||||
"scrollToError": "滚动到错误字段",
|
||||
"upload-error": "部分文件上传失败",
|
||||
"upload-urls": "文件上传后的网址",
|
||||
"file": "文件",
|
||||
|
@ -44,6 +45,7 @@
|
|||
"pointSelection": "点选验证",
|
||||
"sliderCaptcha": "滑块验证",
|
||||
"sliderRotateCaptcha": "旋转验证",
|
||||
"sliderTranslateCaptcha": "拼图滑块验证",
|
||||
"captchaCardTitle": "请完成安全验证",
|
||||
"pageDescription": "通过点击图片中的特定位置来验证用户身份。",
|
||||
"pageTitle": "验证码组件示例",
|
||||
|
|
|
@ -85,6 +85,15 @@ const routes: RouteRecordRaw[] = [
|
|||
title: $t('examples.form.merge'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FormScrollToErrorExample',
|
||||
path: '/examples/form/scroll-to-error-test',
|
||||
component: () =>
|
||||
import('#/views/examples/form/scroll-to-error-test.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.scrollToError'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -196,6 +205,15 @@ const routes: RouteRecordRaw[] = [
|
|||
title: $t('examples.captcha.sliderRotateCaptcha'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'TranslateVerifyExample',
|
||||
path: '/examples/captcha/slider-translate',
|
||||
component: () =>
|
||||
import('#/views/examples/captcha/slider-translate-captcha.vue'),
|
||||
meta: {
|
||||
title: $t('examples.captcha.sliderTranslateCaptcha'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'CaptchaPointSelectionExample',
|
||||
path: '/examples/captcha/point-selection',
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import { Page, SliderTranslateCaptcha } from '@vben/common-ui';
|
||||
|
||||
import { Card, message } from 'ant-design-vue';
|
||||
|
||||
function handleSuccess() {
|
||||
message.success('success!');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
description="用于前端简单的拼图滑块水平拖动校验场景"
|
||||
title="拼图滑块校验"
|
||||
>
|
||||
<Card class="mb-5" title="基本示例">
|
||||
<div class="flex items-center justify-center p-4">
|
||||
<SliderTranslateCaptcha
|
||||
src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/pro-avatar.webp"
|
||||
:canvas-width="420"
|
||||
:canvas-height="420"
|
||||
@success="handleSuccess"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
|
@ -0,0 +1,183 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, Switch } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
defineOptions({
|
||||
name: 'ScrollToErrorTest',
|
||||
});
|
||||
|
||||
const scrollEnabled = ref(true);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
scrollToFirstError: scrollEnabled.value,
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户名',
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: '用户名',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入邮箱',
|
||||
},
|
||||
fieldName: 'email',
|
||||
label: '邮箱',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入手机号',
|
||||
},
|
||||
fieldName: 'phone',
|
||||
label: '手机号',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入地址',
|
||||
},
|
||||
fieldName: 'address',
|
||||
label: '地址',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入公司名称',
|
||||
},
|
||||
fieldName: 'company',
|
||||
label: '公司名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入职位',
|
||||
},
|
||||
fieldName: 'position',
|
||||
label: '职位',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '男', value: 'male' },
|
||||
{ label: '女', value: 'female' },
|
||||
],
|
||||
placeholder: '请选择性别',
|
||||
},
|
||||
fieldName: 'gender',
|
||||
label: '性别',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// 测试 validateAndSubmitForm(验证并提交)
|
||||
async function testValidateAndSubmit() {
|
||||
await formApi.validateAndSubmitForm();
|
||||
}
|
||||
|
||||
// 测试 validate(手动验证整个表单)
|
||||
async function testValidate() {
|
||||
await formApi.validate();
|
||||
}
|
||||
|
||||
// 测试 validateField(验证单个字段)
|
||||
async function testValidateField() {
|
||||
await formApi.validateField('username');
|
||||
}
|
||||
|
||||
// 切换滚动功能
|
||||
function toggleScrollToError() {
|
||||
formApi.setState({ scrollToFirstError: scrollEnabled.value });
|
||||
}
|
||||
|
||||
// 填充部分数据测试
|
||||
async function fillPartialData() {
|
||||
await formApi.resetForm();
|
||||
await formApi.setFieldValue('username', '测试用户');
|
||||
await formApi.setFieldValue('email', 'test@example.com');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
description="测试表单验证失败时自动滚动到错误字段的功能"
|
||||
title="滚动到错误字段测试"
|
||||
>
|
||||
<Card title="功能测试">
|
||||
<template #extra>
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
v-model:checked="scrollEnabled"
|
||||
@change="toggleScrollToError"
|
||||
/>
|
||||
<span>启用滚动到错误字段</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded bg-blue-50 p-4">
|
||||
<h3 class="mb-2 font-medium">测试说明:</h3>
|
||||
<ul class="list-inside list-disc space-y-1 text-sm">
|
||||
<li>所有验证方法在验证失败时都会自动滚动到第一个错误字段</li>
|
||||
<li>可以通过右上角的开关控制是否启用自动滚动功能</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="rounded border p-4">
|
||||
<h4 class="mb-3 font-medium">验证方法测试:</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button type="primary" @click="testValidateAndSubmit">
|
||||
测试 validateAndSubmitForm()
|
||||
</Button>
|
||||
<Button @click="testValidate"> 测试 validate() </Button>
|
||||
<Button @click="testValidateField"> 测试 validateField() </Button>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-500">
|
||||
<p>• validateAndSubmitForm(): 验证表单并提交</p>
|
||||
<p>• validate(): 手动验证整个表单</p>
|
||||
<p>• validateField(): 验证单个字段(这里测试用户名字段)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded border p-4">
|
||||
<h4 class="mb-3 font-medium">数据填充测试:</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button @click="fillPartialData"> 填充部分数据 </Button>
|
||||
<Button @click="() => formApi.resetForm()"> 清空表单 </Button>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-500">
|
||||
<p>• 填充部分数据后验证,会滚动到第一个错误字段</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form />
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
5439
pnpm-lock.yaml
5439
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -12,121 +12,123 @@ packages:
|
|||
- scripts/*
|
||||
- docs
|
||||
- playground
|
||||
|
||||
catalog:
|
||||
'@ast-grep/napi': ^0.37.0
|
||||
'@changesets/changelog-github': ^0.5.1
|
||||
'@changesets/cli': ^2.29.2
|
||||
'@changesets/cli': ^2.29.5
|
||||
'@changesets/git': ^3.0.4
|
||||
'@clack/prompts': ^0.10.1
|
||||
'@commitlint/cli': ^19.8.0
|
||||
'@commitlint/config-conventional': ^19.8.0
|
||||
'@commitlint/cli': ^19.8.1
|
||||
'@commitlint/config-conventional': ^19.8.1
|
||||
'@ctrl/tinycolor': ^4.1.0
|
||||
'@eslint/js': ^9.26.0
|
||||
'@faker-js/faker': ^9.7.0
|
||||
'@iconify/json': ^2.2.334
|
||||
'@eslint/js': ^9.30.1
|
||||
'@faker-js/faker': ^9.9.0
|
||||
'@iconify/json': ^2.2.354
|
||||
'@iconify/tailwind': ^1.2.0
|
||||
'@iconify/vue': ^5.0.0
|
||||
'@intlify/core-base': ^11.1.3
|
||||
'@intlify/core-base': ^11.1.7
|
||||
'@intlify/unplugin-vue-i18n': ^6.0.8
|
||||
'@jspm/generator': ^2.5.1
|
||||
'@jspm/generator': ^2.6.2
|
||||
'@manypkg/get-packages': ^3.0.0
|
||||
'@microsoft/fetch-event-source': ^2.0.1
|
||||
'@nolebase/vitepress-plugin-git-changelog': ^2.17.0
|
||||
'@playwright/test': ^1.52.0
|
||||
'@pnpm/workspace.read-manifest': ^1000.1.4
|
||||
'@stylistic/stylelint-plugin': ^3.1.2
|
||||
'@nolebase/vitepress-plugin-git-changelog': ^2.18.0
|
||||
'@playwright/test': ^1.53.2
|
||||
'@pnpm/workspace.read-manifest': ^1000.2.0
|
||||
'@stylistic/stylelint-plugin': ^3.1.3
|
||||
'@tailwindcss/nesting': 0.0.0-insiders.565cd3e
|
||||
'@tailwindcss/typography': ^0.5.16
|
||||
'@tanstack/vue-query': ^5.75.1
|
||||
'@tanstack/vue-store': ^0.7.0
|
||||
'@tanstack/vue-query': ^5.81.5
|
||||
'@tanstack/vue-store': ^0.7.1
|
||||
'@tinymce/tinymce-vue': ^6.1.0
|
||||
'@form-create/ant-design-vue': ^3.2.22
|
||||
'@ant-design/icons-vue': ^7.0.1
|
||||
'@form-create/antd-designer': ^3.2.11
|
||||
'@form-create/naive-ui': ^3.2.22
|
||||
'@form-create/ant-design-vue': ^3.2.27
|
||||
'@form-create/antd-designer': ^3.3.0
|
||||
'@form-create/designer': ^3.3.0
|
||||
'@form-create/naive-ui': ^3.2.27
|
||||
'@form-create/element-ui': ^3.2.27
|
||||
'@types/archiver': ^6.0.3
|
||||
'@types/eslint': ^9.6.1
|
||||
'@types/html-minifier-terser': ^7.0.2
|
||||
'@types/json-bigint': ^1.0.4
|
||||
'@types/jsonwebtoken': ^9.0.9
|
||||
'@types/jsonwebtoken': ^9.0.10
|
||||
'@types/lodash.clonedeep': ^4.5.9
|
||||
'@types/lodash.get': ^4.4.9
|
||||
'@types/lodash.isequal': ^4.5.8
|
||||
'@types/lodash.set': ^4.3.9
|
||||
'@types/markdown-it': ^14.1.2
|
||||
'@types/node': ^22.15.3
|
||||
'@types/node': ^22.16.0
|
||||
'@types/nprogress': ^0.2.3
|
||||
'@types/postcss-import': ^14.0.3
|
||||
'@types/qrcode': ^1.5.5
|
||||
'@types/qs': ^6.9.18
|
||||
'@types/qs': ^6.14.0
|
||||
'@types/sortablejs': ^1.15.8
|
||||
'@types/crypto-js': ^4.2.2
|
||||
'@typescript-eslint/eslint-plugin': ^8.31.1
|
||||
'@typescript-eslint/parser': ^8.31.1
|
||||
'@vee-validate/zod': ^4.15.0
|
||||
'@typescript-eslint/eslint-plugin': ^8.35.1
|
||||
'@typescript-eslint/parser': ^8.35.1
|
||||
'@vee-validate/zod': ^4.15.1
|
||||
'@vite-pwa/vitepress': ^1.0.0
|
||||
'@vitejs/plugin-vue': ^5.2.3
|
||||
'@vitejs/plugin-vue-jsx': ^4.1.2
|
||||
'@vue/reactivity': ^3.5.13
|
||||
'@vue/shared': ^3.5.13
|
||||
'@vitejs/plugin-vue': ^5.2.4
|
||||
'@vitejs/plugin-vue-jsx': ^4.2.0
|
||||
'@vue/reactivity': ^3.5.17
|
||||
'@vue/shared': ^3.5.17
|
||||
'@vue/test-utils': ^2.4.6
|
||||
'@vueuse/core': ^13.1.0
|
||||
'@vueuse/integrations': ^13.1.0
|
||||
'@vueuse/core': ^13.4.0
|
||||
'@vueuse/integrations': ^13.4.0
|
||||
'@vueuse/motion': ^3.0.3
|
||||
ant-design-vue: ^4.2.6
|
||||
archiver: ^7.0.1
|
||||
autoprefixer: ^10.4.21
|
||||
axios: ^1.9.0
|
||||
axios: ^1.10.0
|
||||
axios-mock-adapter: ^2.1.0
|
||||
cac: ^6.7.14
|
||||
chalk: ^5.4.1
|
||||
cheerio: ^1.0.0
|
||||
cheerio: ^1.1.0
|
||||
circular-dependency-scanner: ^2.3.0
|
||||
class-variance-authority: ^0.7.1
|
||||
clsx: ^2.1.1
|
||||
commitlint-plugin-function-rules: ^4.0.1
|
||||
commitlint-plugin-function-rules: ^4.0.2
|
||||
consola: ^3.4.2
|
||||
cross-env: ^7.0.3
|
||||
cropperjs: ^1.6.2
|
||||
crypto-js: ^4.2.0
|
||||
cspell: ^8.18.3
|
||||
cssnano: ^7.0.6
|
||||
cz-git: ^1.11.1
|
||||
cspell: ^8.19.4
|
||||
cssnano: ^7.0.7
|
||||
cz-git: ^1.11.2
|
||||
czg: ^1.11.1
|
||||
dayjs: ^1.11.13
|
||||
defu: ^6.1.4
|
||||
depcheck: ^1.4.7
|
||||
dotenv: ^16.5.0
|
||||
dotenv: ^16.6.1
|
||||
echarts: ^5.6.0
|
||||
element-plus: ^2.9.9
|
||||
eslint: ^9.26.0
|
||||
eslint-config-turbo: ^2.5.2
|
||||
eslint-plugin-command: ^3.2.0
|
||||
element-plus: ^2.10.2
|
||||
eslint: ^9.30.1
|
||||
eslint-config-turbo: ^2.5.4
|
||||
eslint-plugin-command: ^3.3.1
|
||||
eslint-plugin-eslint-comments: ^3.2.0
|
||||
eslint-plugin-import-x: ^4.11.0
|
||||
eslint-plugin-jsdoc: ^50.6.11
|
||||
eslint-plugin-jsonc: ^2.20.0
|
||||
eslint-plugin-n: ^17.17.0
|
||||
eslint-plugin-import-x: ^4.16.1
|
||||
eslint-plugin-jsdoc: ^50.8.0
|
||||
eslint-plugin-jsonc: ^2.20.1
|
||||
eslint-plugin-n: ^17.20.0
|
||||
eslint-plugin-no-only-tests: ^3.3.0
|
||||
eslint-plugin-perfectionist: ^4.12.3
|
||||
eslint-plugin-prettier: ^5.2.6
|
||||
eslint-plugin-regexp: ^2.7.0
|
||||
eslint-plugin-unicorn: ^59.0.0
|
||||
eslint-plugin-perfectionist: ^4.15.0
|
||||
eslint-plugin-prettier: ^5.5.1
|
||||
eslint-plugin-regexp: ^2.9.0
|
||||
eslint-plugin-unicorn: ^59.0.1
|
||||
eslint-plugin-unused-imports: ^4.1.4
|
||||
eslint-plugin-vitest: ^0.5.4
|
||||
eslint-plugin-vue: ^10.1.0
|
||||
execa: ^9.5.2
|
||||
eslint-plugin-vue: ^10.2.0
|
||||
execa: ^9.6.0
|
||||
find-up: ^7.0.0
|
||||
get-port: ^7.1.0
|
||||
globals: ^16.0.0
|
||||
globals: ^16.3.0
|
||||
h3: ^1.15.3
|
||||
happy-dom: ^17.4.6
|
||||
happy-dom: ^17.6.3
|
||||
html-minifier-terser: ^7.2.0
|
||||
is-ci: ^4.1.0
|
||||
json-bigint: ^1.0.0
|
||||
jsonc-eslint-parser: ^2.4.0
|
||||
jsonwebtoken: ^9.0.2
|
||||
lefthook: ^1.11.12
|
||||
lefthook: ^1.11.14
|
||||
lodash.clonedeep: ^4.5.0
|
||||
lodash.get: ^4.4.2
|
||||
lodash.isequal: ^4.5.0
|
||||
|
@ -138,74 +140,74 @@ catalog:
|
|||
markmap-toolbar: ^0.17.0
|
||||
markmap-view: ^0.16.0
|
||||
medium-zoom: ^1.1.0
|
||||
naive-ui: ^2.41.0
|
||||
nitropack: ^2.11.11
|
||||
naive-ui: ^2.42.0
|
||||
nitropack: ^2.11.13
|
||||
nprogress: ^0.2.0
|
||||
ora: ^8.2.0
|
||||
pinia: ^3.0.2
|
||||
pinia-plugin-persistedstate: ^4.2.0
|
||||
pkg-types: ^2.1.0
|
||||
playwright: ^1.52.0
|
||||
postcss: ^8.5.3
|
||||
pinia: ^3.0.3
|
||||
pinia-plugin-persistedstate: ^4.4.1
|
||||
pkg-types: ^2.2.0
|
||||
playwright: ^1.53.2
|
||||
postcss: ^8.5.6
|
||||
postcss-antd-fixes: ^0.2.0
|
||||
postcss-html: ^1.8.0
|
||||
postcss-import: ^16.1.0
|
||||
postcss-preset-env: ^10.1.6
|
||||
postcss-import: ^16.1.1
|
||||
postcss-preset-env: ^10.2.4
|
||||
postcss-scss: ^4.0.9
|
||||
prettier: ^3.5.3
|
||||
prettier-plugin-tailwindcss: ^0.6.11
|
||||
prettier: ^3.6.2
|
||||
prettier-plugin-tailwindcss: ^0.6.13
|
||||
publint: ^0.3.12
|
||||
qrcode: ^1.5.4
|
||||
qs: ^6.14.0
|
||||
radix-vue: ^1.9.17
|
||||
resolve.exports: ^2.0.3
|
||||
rimraf: ^6.0.1
|
||||
rollup: ^4.40.1
|
||||
rollup: ^4.44.1
|
||||
rollup-plugin-visualizer: ^5.14.0
|
||||
sass: ^1.87.0
|
||||
sass: ^1.89.2
|
||||
secure-ls: ^2.0.0
|
||||
sortablejs: ^1.15.6
|
||||
stylelint: ^16.19.1
|
||||
stylelint-config-recess-order: ^6.0.0
|
||||
stylelint: ^16.21.0
|
||||
stylelint-config-recess-order: ^6.1.0
|
||||
stylelint-config-recommended: ^16.0.0
|
||||
stylelint-config-recommended-scss: ^14.1.0
|
||||
stylelint-config-recommended-vue: ^1.6.0
|
||||
stylelint-config-recommended-vue: ^1.6.1
|
||||
stylelint-config-standard: ^38.0.0
|
||||
stylelint-order: ^7.0.0
|
||||
stylelint-prettier: ^5.0.3
|
||||
stylelint-scss: ^6.11.1
|
||||
stylelint-scss: ^6.12.1
|
||||
tailwind-merge: ^2.6.0
|
||||
tailwindcss: ^3.4.17
|
||||
tailwindcss-animate: ^1.0.7
|
||||
theme-colors: ^0.1.0
|
||||
tippy.js: ^6.3.7
|
||||
turbo: ^2.5.2
|
||||
turbo: ^2.5.4
|
||||
typescript: ^5.8.3
|
||||
unbuild: ^3.5.0
|
||||
unplugin-element-plus: ^0.10.0
|
||||
vee-validate: ^4.15.0
|
||||
vite: ^6.3.4
|
||||
vee-validate: ^4.15.1
|
||||
vite: ^6.3.5
|
||||
vite-plugin-compression: ^0.5.1
|
||||
vite-plugin-dts: ^4.5.3
|
||||
vite-plugin-dts: ^4.5.4
|
||||
vite-plugin-html: ^3.2.2
|
||||
vite-plugin-lazy-import: ^1.0.7
|
||||
vite-plugin-pwa: ^1.0.0
|
||||
vite-plugin-vue-devtools: ^7.7.6
|
||||
vite-plugin-pwa: ^1.0.1
|
||||
vite-plugin-vue-devtools: ^7.7.7
|
||||
vitepress: ^1.6.3
|
||||
vitepress-plugin-group-icons: ^1.5.2
|
||||
vitest: ^3.1.2
|
||||
vue: ^3.5.13
|
||||
vue-dompurify-html: ^5.2.0
|
||||
vue-eslint-parser: ^10.1.3
|
||||
vue-i18n: ^11.1.3
|
||||
vitepress-plugin-group-icons: ^1.6.1
|
||||
vitest: ^3.2.4
|
||||
vue: ^3.5.17
|
||||
vue-dompurify-html: ^5.3.0
|
||||
vue-eslint-parser: ^10.2.0
|
||||
vue-i18n: ^11.1.7
|
||||
vue-json-viewer: ^3.0.4
|
||||
vue-router: ^4.5.1
|
||||
vue-tippy: ^6.7.0
|
||||
vue-tippy: ^6.7.1
|
||||
vue-tsc: 2.2.10
|
||||
vxe-pc-ui: ^4.5.35
|
||||
vxe-table: ^4.13.16
|
||||
watermark-js-plus: ^1.6.0
|
||||
zod: ^3.24.3
|
||||
vxe-pc-ui: ^4.6.42
|
||||
vxe-table: ^4.13.51
|
||||
watermark-js-plus: ^1.6.2
|
||||
zod: ^3.25.67
|
||||
zod-defaults: ^0.1.3
|
||||
highlight.js: ^11.11.1
|
||||
vue3-signature: ^0.2.4
|
||||
|
|
Loading…
Reference in New Issue