Merge branch 'dev' of https://gitee.com/yudaocode/yudao-ui-admin-vben into dev
This commit is contained in:
@@ -7,13 +7,29 @@ import { requestClient } from '#/api/request';
|
|||||||
export namespace BpmTaskApi {
|
export namespace BpmTaskApi {
|
||||||
/** 流程任务 */
|
/** 流程任务 */
|
||||||
export interface Task {
|
export interface Task {
|
||||||
id: number; // 编号
|
id: string; // 编号
|
||||||
name: string; // 监听器名字
|
name: string; // 任务名字
|
||||||
type: string; // 监听器类型
|
status: number; // 任务状态
|
||||||
status: number; // 监听器状态
|
createTime: number; // 创建时间
|
||||||
event: string; // 监听事件
|
endTime: number; // 结束时间
|
||||||
valueType: string; // 监听器值类型
|
durationInMillis: number; // 持续时间
|
||||||
processInstance?: BpmProcessInstanceApi.ProcessInstance; // 流程实例
|
reason: string; // 审批理由
|
||||||
|
ownerUser: any; // 负责人
|
||||||
|
assigneeUser: any; // 处理人
|
||||||
|
taskDefinitionKey: string; // 任务定义的标识
|
||||||
|
processInstanceId: string; // 流程实例id
|
||||||
|
processInstance: BpmProcessInstanceApi.ProcessInstance; // 流程实例
|
||||||
|
parentTaskId: any; // 父任务id
|
||||||
|
children: any; // 子任务
|
||||||
|
formId: any; // 表单id
|
||||||
|
formName: any; // 表单名称
|
||||||
|
formConf: any; // 表单配置
|
||||||
|
formFields: any; // 表单字段
|
||||||
|
formVariables: any; // 表单变量
|
||||||
|
buttonsSetting: any; // 按钮设置
|
||||||
|
signEnable: any; // 签名设置
|
||||||
|
reasonRequire: any; // 原因设置
|
||||||
|
nodeType: any; // 节点类型
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export namespace InfraFileConfigApi {
|
|||||||
accessSecret?: string;
|
accessSecret?: string;
|
||||||
pathStyle?: boolean;
|
pathStyle?: boolean;
|
||||||
enablePublicAccess?: boolean;
|
enablePublicAccess?: boolean;
|
||||||
|
region?: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ export enum CodecTypeEnum {
|
|||||||
ALINK = 'Alink', // 阿里云 Alink 协议
|
ALINK = 'Alink', // 阿里云 Alink 协议
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** IOT 产品状态枚举类 */
|
||||||
|
export enum ProductStatusEnum {
|
||||||
|
UNPUBLISHED = 0, // 开发中
|
||||||
|
PUBLISHED = 1, // 已发布
|
||||||
|
}
|
||||||
|
|
||||||
/** 查询产品分页 */
|
/** 查询产品分页 */
|
||||||
export function getProductPage(params: PageParam) {
|
export function getProductPage(params: PageParam) {
|
||||||
return requestClient.get<PageResult<IotProductApi.Product>>(
|
return requestClient.get<PageResult<IotProductApi.Product>>(
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export namespace MallCombinationActivityApi {
|
|||||||
limitDuration?: number; // 限制时长
|
limitDuration?: number; // 限制时长
|
||||||
combinationPrice?: number; // 拼团价格
|
combinationPrice?: number; // 拼团价格
|
||||||
products: CombinationProduct[]; // 商品列表
|
products: CombinationProduct[]; // 商品列表
|
||||||
|
picUrl?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 拼团活动所需属性 */
|
/** 拼团活动所需属性 */
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export namespace MallSeckillActivityApi {
|
|||||||
totalStock?: number; // 秒杀总库存
|
totalStock?: number; // 秒杀总库存
|
||||||
seckillPrice?: number; // 秒杀价格
|
seckillPrice?: number; // 秒杀价格
|
||||||
products?: SeckillProduct[]; // 秒杀商品列表
|
products?: SeckillProduct[]; // 秒杀商品列表
|
||||||
|
picUrl?: any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export namespace SystemSocialClientApi {
|
|||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
|
publicKey?: string;
|
||||||
status: number;
|
status: number;
|
||||||
createTime?: Date;
|
createTime?: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,24 +9,6 @@ const routes: RouteRecordRaw[] = [
|
|||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
|
||||||
path: 'task',
|
|
||||||
name: 'BpmTask',
|
|
||||||
meta: {
|
|
||||||
title: '审批中心',
|
|
||||||
icon: 'ant-design:history-outlined',
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'my',
|
|
||||||
name: 'BpmTaskMy',
|
|
||||||
component: () => import('#/views/bpm/processInstance/index.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '我的流程',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'process-instance/detail',
|
path: 'process-instance/detail',
|
||||||
component: () => import('#/views/bpm/processInstance/detail/index.vue'),
|
component: () => import('#/views/bpm/processInstance/detail/index.vue'),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default {
|
|||||||
'Append Gateway': '追加网关',
|
'Append Gateway': '追加网关',
|
||||||
'Append Task': '追加任务',
|
'Append Task': '追加任务',
|
||||||
'Append Intermediate/Boundary Event': '追加中间抛出事件/边界事件',
|
'Append Intermediate/Boundary Event': '追加中间抛出事件/边界事件',
|
||||||
|
TextAnnotation: '文本注释',
|
||||||
'Activate the global connect tool': '激活全局连接工具',
|
'Activate the global connect tool': '激活全局连接工具',
|
||||||
'Append {type}': '添加 {type}',
|
'Append {type}': '添加 {type}',
|
||||||
'Add Lane above': '在上面添加道',
|
'Add Lane above': '在上面添加道',
|
||||||
@@ -31,10 +31,16 @@ export default {
|
|||||||
'Create expanded SubProcess': '创建扩展子过程',
|
'Create expanded SubProcess': '创建扩展子过程',
|
||||||
'Create IntermediateThrowEvent/BoundaryEvent': '创建中间抛出事件/边界事件',
|
'Create IntermediateThrowEvent/BoundaryEvent': '创建中间抛出事件/边界事件',
|
||||||
'Create Pool/Participant': '创建池/参与者',
|
'Create Pool/Participant': '创建池/参与者',
|
||||||
'Parallel Multi Instance': '并行多重事件',
|
'Participant Multiplicity': '参与者多重性',
|
||||||
'Sequential Multi Instance': '时序多重事件',
|
'Empty pool/participant (removes content)': '清空池/参与者(移除内容)',
|
||||||
|
'Empty pool/participant': '收缩池/参与者',
|
||||||
|
'Expanded pool/participant': '展开池/参与者',
|
||||||
|
'Parallel Multi-Instance': '并行多重事件',
|
||||||
|
'Sequential Multi-Instance': '时序多重事件',
|
||||||
DataObjectReference: '数据对象参考',
|
DataObjectReference: '数据对象参考',
|
||||||
DataStoreReference: '数据存储参考',
|
DataStoreReference: '数据存储参考',
|
||||||
|
'Data object reference': '数据对象引用 ',
|
||||||
|
'Data store reference': '数据存储引用 ',
|
||||||
Loop: '循环',
|
Loop: '循环',
|
||||||
'Ad-hoc': '即席',
|
'Ad-hoc': '即席',
|
||||||
'Create {type}': '创建 {type}',
|
'Create {type}': '创建 {type}',
|
||||||
@@ -49,6 +55,9 @@ export default {
|
|||||||
'Call Activity': '调用活动',
|
'Call Activity': '调用活动',
|
||||||
'Sub-Process (collapsed)': '子流程(折叠的)',
|
'Sub-Process (collapsed)': '子流程(折叠的)',
|
||||||
'Sub-Process (expanded)': '子流程(展开的)',
|
'Sub-Process (expanded)': '子流程(展开的)',
|
||||||
|
'Ad-hoc sub-process': '即席子流程',
|
||||||
|
'Ad-hoc sub-process (collapsed)': '即席子流程(折叠的)',
|
||||||
|
'Ad-hoc sub-process (expanded)': '即席子流程(展开的)',
|
||||||
'Start Event': '开始事件',
|
'Start Event': '开始事件',
|
||||||
StartEvent: '开始事件',
|
StartEvent: '开始事件',
|
||||||
'Intermediate Throw Event': '中间事件',
|
'Intermediate Throw Event': '中间事件',
|
||||||
@@ -111,10 +120,10 @@ export default {
|
|||||||
'Parallel Gateway': '并行网关',
|
'Parallel Gateway': '并行网关',
|
||||||
'Inclusive Gateway': '相容网关',
|
'Inclusive Gateway': '相容网关',
|
||||||
'Complex Gateway': '复杂网关',
|
'Complex Gateway': '复杂网关',
|
||||||
'Event based Gateway': '事件网关',
|
'Event-based Gateway': '事件网关',
|
||||||
Transaction: '转运',
|
Transaction: '转运',
|
||||||
'Sub Process': '子流程',
|
'sub-process': '子流程',
|
||||||
'Event Sub Process': '事件子流程',
|
'Event sub-process': '事件子流程',
|
||||||
'Collapsed Pool': '折叠池',
|
'Collapsed Pool': '折叠池',
|
||||||
'Expanded Pool': '展开池',
|
'Expanded Pool': '展开池',
|
||||||
|
|
||||||
|
|||||||
@@ -390,8 +390,9 @@ watch(() => props.businessObject, syncFromBusinessObject, { deep: true });
|
|||||||
<template #extra>
|
<template #extra>
|
||||||
<IconifyIcon icon="ep:timer" />
|
<IconifyIcon icon="ep:timer" />
|
||||||
</template>
|
</template>
|
||||||
|
<!-- 相关 issue:https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ICNRW2 -->
|
||||||
<TimeEventConfig
|
<TimeEventConfig
|
||||||
:business-object="bpmnElement.value?.businessObject"
|
:business-object="elementBusinessObject"
|
||||||
:key="elementId"
|
:key="elementId"
|
||||||
/>
|
/>
|
||||||
</CollapsePanel>
|
</CollapsePanel>
|
||||||
|
|||||||
@@ -75,8 +75,14 @@ const assignEmptyUserIds = ref<any>();
|
|||||||
|
|
||||||
// 操作按钮
|
// 操作按钮
|
||||||
const buttonsSettingEl = ref<any>();
|
const buttonsSettingEl = ref<any>();
|
||||||
const { btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
|
const { btnDisplayNameEdit, changeBtnDisplayName } = useButtonsSetting();
|
||||||
useButtonsSetting();
|
const btnDisplayNameBlurEvent = (index: number) => {
|
||||||
|
btnDisplayNameEdit.value[index] = false;
|
||||||
|
const buttonItem = buttonsSettingEl.value[index];
|
||||||
|
buttonItem.displayName =
|
||||||
|
buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!;
|
||||||
|
updateElementExtensions();
|
||||||
|
};
|
||||||
|
|
||||||
// 字段权限
|
// 字段权限
|
||||||
const fieldsPermissionEl = ref<any[]>([]);
|
const fieldsPermissionEl = ref<any[]>([]);
|
||||||
@@ -172,7 +178,7 @@ const resetCustomConfigList = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 操作按钮
|
// 操作按钮
|
||||||
buttonsSettingEl.value = elExtensionElements.value.values?.find(
|
buttonsSettingEl.value = elExtensionElements.value.values?.filter(
|
||||||
(ex: any) => ex.$type === `${prefix}:ButtonsSetting`,
|
(ex: any) => ex.$type === `${prefix}:ButtonsSetting`,
|
||||||
);
|
);
|
||||||
if (buttonsSettingEl.value.length === 0) {
|
if (buttonsSettingEl.value.length === 0) {
|
||||||
@@ -189,7 +195,7 @@ const resetCustomConfigList = () => {
|
|||||||
|
|
||||||
// 字段权限
|
// 字段权限
|
||||||
if (formType.value === BpmModelFormType.NORMAL) {
|
if (formType.value === BpmModelFormType.NORMAL) {
|
||||||
const fieldsPermissionList = elExtensionElements.value.values?.find(
|
const fieldsPermissionList = elExtensionElements.value.values?.filter(
|
||||||
(ex: any) => ex.$type === `${prefix}:FieldsPermission`,
|
(ex: any) => ex.$type === `${prefix}:FieldsPermission`,
|
||||||
);
|
);
|
||||||
fieldsPermissionEl.value = [];
|
fieldsPermissionEl.value = [];
|
||||||
@@ -358,24 +364,14 @@ function useButtonsSetting() {
|
|||||||
const changeBtnDisplayName = (index: number) => {
|
const changeBtnDisplayName = (index: number) => {
|
||||||
btnDisplayNameEdit.value[index] = true;
|
btnDisplayNameEdit.value[index] = true;
|
||||||
};
|
};
|
||||||
const btnDisplayNameBlurEvent = (index: number) => {
|
|
||||||
btnDisplayNameEdit.value[index] = false;
|
|
||||||
const buttonItem = buttonsSetting.value?.[index];
|
|
||||||
if (buttonItem) {
|
|
||||||
buttonItem.displayName =
|
|
||||||
buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return {
|
return {
|
||||||
buttonsSetting,
|
buttonsSetting,
|
||||||
btnDisplayNameEdit,
|
btnDisplayNameEdit,
|
||||||
changeBtnDisplayName,
|
changeBtnDisplayName,
|
||||||
btnDisplayNameBlurEvent,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 批量更新权限 */
|
/** 批量更新权限 */
|
||||||
// TODO @lesan:这个页面,有一些 idea 红色报错,咱要不要 fix 下!
|
|
||||||
const updatePermission = (type: string) => {
|
const updatePermission = (type: string) => {
|
||||||
fieldsPermissionEl.value.forEach((field: any) => {
|
fieldsPermissionEl.value.forEach((field: any) => {
|
||||||
if (type === 'READ') {
|
if (type === 'READ') {
|
||||||
@@ -532,7 +528,10 @@ onMounted(async () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-setting-item-label">
|
<div class="button-setting-item-label">
|
||||||
<Switch v-model:checked="item.enable" />
|
<Switch
|
||||||
|
v-model:checked="item.enable"
|
||||||
|
@change="updateElementExtensions"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,7 +61,10 @@ const bpmnInstances = () => (window as any)?.bpmnInstances;
|
|||||||
|
|
||||||
const resetListenersList = () => {
|
const resetListenersList = () => {
|
||||||
bpmnElement.value = bpmnInstances().bpmnElement;
|
bpmnElement.value = bpmnInstances().bpmnElement;
|
||||||
otherExtensionList.value = [];
|
otherExtensionList.value =
|
||||||
|
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||||
|
(ex: any) => ex.$type !== `${prefix}:ExecutionListener`,
|
||||||
|
) ?? []; // 保留非监听器类型的扩展属性,避免移除监听器时清空其他配置(如审批人等)。相关案例:https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ICMSYC
|
||||||
bpmnElementListeners.value =
|
bpmnElementListeners.value =
|
||||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||||
(ex: any) => ex.$type === `${prefix}:ExecutionListener`,
|
(ex: any) => ex.$type === `${prefix}:ExecutionListener`,
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject, nextTick, ref, watch } from 'vue';
|
import { inject, nextTick, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, useVbenDrawer, useVbenModal } from '@vben/common-ui';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { cloneDeep } from '@vben/utils';
|
import { cloneDeep } from '@vben/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Divider,
|
Divider,
|
||||||
Drawer,
|
|
||||||
Form,
|
Form,
|
||||||
FormItem,
|
FormItem,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
|
||||||
Select,
|
Select,
|
||||||
SelectOption,
|
SelectOption,
|
||||||
Table,
|
|
||||||
TableColumn,
|
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import ProcessListenerDialog from '#/views/bpm/components/bpmn-process-designer/package/penal/listeners/ProcessListenerDialog.vue';
|
import ProcessListenerDialog from '#/views/bpm/components/bpmn-process-designer/package/penal/listeners/ProcessListenerDialog.vue';
|
||||||
|
|
||||||
import { createListenerObject, updateElementExtensions } from '../../utils';
|
import { createListenerObject, updateElementExtensions } from '../../utils';
|
||||||
@@ -40,59 +38,46 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const prefix = inject<string>('prefix');
|
const prefix = inject<string>('prefix');
|
||||||
const width = inject<number>('width');
|
|
||||||
const elementListenersList = ref<any[]>([]);
|
const elementListenersList = ref<any[]>([]);
|
||||||
const listenerEventTypeObject = ref(eventType);
|
const listenerEventTypeObject = ref(eventType);
|
||||||
const listenerTypeObject = ref(listenerType);
|
const listenerTypeObject = ref(listenerType);
|
||||||
const listenerFormModelVisible = ref(false);
|
|
||||||
const listenerForm = ref<any>({});
|
const listenerForm = ref<any>({});
|
||||||
const fieldTypeObject = ref(fieldType);
|
const fieldTypeObject = ref(fieldType);
|
||||||
const fieldsListOfListener = ref<any[]>([]);
|
const fieldsListOfListener = ref<any[]>([]);
|
||||||
const listenerFieldFormModelVisible = ref(false); // 监听器 注入字段表单弹窗 显示状态
|
const editingListenerIndex = ref(-1);
|
||||||
const editingListenerIndex = ref(-1); // 监听器所在下标,-1 为新增
|
const editingListenerFieldIndex = ref<any>(-1);
|
||||||
const editingListenerFieldIndex = ref<any>(-1); // 字段所在下标,-1 为新增
|
const listenerFieldForm = ref<any>({});
|
||||||
const listenerFieldForm = ref<any>({}); // 监听器 注入字段 详情表单
|
|
||||||
const bpmnElement = ref<any>();
|
const bpmnElement = ref<any>();
|
||||||
const bpmnElementListeners = ref<any[]>([]);
|
const bpmnElementListeners = ref<any[]>([]);
|
||||||
const otherExtensionList = ref<any[]>([]);
|
const otherExtensionList = ref<any[]>([]);
|
||||||
const listenerFormRef = ref<any>({});
|
const listenerFormRef = ref<any>({});
|
||||||
const listenerFieldFormRef = ref<any>({});
|
const listenerFieldFormRef = ref<any>({});
|
||||||
|
|
||||||
interface BpmnInstances {
|
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||||
bpmnElement: any;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
bpmnInstances?: BpmnInstances;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const bpmnInstances = () => window.bpmnInstances;
|
|
||||||
|
|
||||||
const resetListenersList = () => {
|
const resetListenersList = () => {
|
||||||
// console.log(
|
|
||||||
// bpmnInstances().bpmnElement,
|
|
||||||
// 'window.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElement',
|
|
||||||
// );
|
|
||||||
bpmnElement.value = bpmnInstances()?.bpmnElement;
|
bpmnElement.value = bpmnInstances()?.bpmnElement;
|
||||||
otherExtensionList.value = [];
|
otherExtensionList.value =
|
||||||
|
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||||
|
(ex: any) => ex.$type !== `${prefix}:TaskListener`,
|
||||||
|
) ?? [];
|
||||||
bpmnElementListeners.value =
|
bpmnElementListeners.value =
|
||||||
bpmnElement.value.businessObject?.extensionElements?.values.filter(
|
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||||
(ex: any) => ex.$type === `${prefix}:TaskListener`,
|
(ex: any) => ex.$type === `${prefix}:TaskListener`,
|
||||||
) ?? [];
|
) ?? [];
|
||||||
elementListenersList.value = bpmnElementListeners.value.map((listener) =>
|
elementListenersList.value = bpmnElementListeners.value.map((listener) =>
|
||||||
initListenerType(listener),
|
initListenerType(listener),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openListenerForm = (listener: any, index?: number) => {
|
const openListenerForm = (listener: any, index?: number) => {
|
||||||
if (listener) {
|
if (listener) {
|
||||||
listenerForm.value = initListenerForm(listener);
|
listenerForm.value = initListenerForm(listener);
|
||||||
editingListenerIndex.value = index || -1;
|
editingListenerIndex.value = index || -1;
|
||||||
} else {
|
} else {
|
||||||
listenerForm.value = {};
|
listenerForm.value = {};
|
||||||
editingListenerIndex.value = -1; // 标记为新增
|
editingListenerIndex.value = -1;
|
||||||
}
|
}
|
||||||
if (listener && listener.fields) {
|
if (listener && listener.fields) {
|
||||||
fieldsListOfListener.value = listener.fields.map((field: any) => ({
|
fieldsListOfListener.value = listener.fields.map((field: any) => ({
|
||||||
@@ -103,37 +88,32 @@ const openListenerForm = (listener: any, index?: number) => {
|
|||||||
fieldsListOfListener.value = [];
|
fieldsListOfListener.value = [];
|
||||||
listenerForm.value.fields = [];
|
listenerForm.value.fields = [];
|
||||||
}
|
}
|
||||||
// 打开侧边栏并清楚验证状态
|
listenerDrawerApi.open();
|
||||||
listenerFormModelVisible.value = true;
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (listenerFormRef.value) listenerFormRef.value.clearValidate();
|
if (listenerFormRef.value) listenerFormRef.value.clearValidate();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
// 移除监听器
|
|
||||||
const removeListener = (_: any, index: number) => {
|
const removeListener = (_: any, index: number) => {
|
||||||
// console.log(listener, 'listener');
|
confirm({
|
||||||
Modal.confirm({
|
|
||||||
title: '提示',
|
title: '提示',
|
||||||
content: '确认移除该监听器吗?',
|
content: '确认移除该监听器吗?',
|
||||||
okText: '确 认',
|
}).then(() => {
|
||||||
cancelText: '取 消',
|
bpmnElementListeners.value.splice(index, 1);
|
||||||
onOk() {
|
elementListenersList.value.splice(index, 1);
|
||||||
bpmnElementListeners.value.splice(index, 1);
|
updateElementExtensions(bpmnElement.value, [
|
||||||
elementListenersList.value.splice(index, 1);
|
...otherExtensionList.value,
|
||||||
updateElementExtensions(bpmnElement.value, [
|
...bpmnElementListeners.value,
|
||||||
...otherExtensionList.value,
|
]);
|
||||||
...bpmnElementListeners.value,
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
onCancel() {
|
|
||||||
// console.info('操作取消');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
// 保存监听器
|
|
||||||
const saveListenerConfig = async () => {
|
async function saveListenerConfig() {
|
||||||
const validateStatus = await listenerFormRef.value.validate();
|
try {
|
||||||
if (!validateStatus) return; // 验证不通过直接返回
|
await listenerFormRef.value.validate();
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const listenerObject = createListenerObject(listenerForm.value, true, prefix);
|
const listenerObject = createListenerObject(listenerForm.value, true, prefix);
|
||||||
if (editingListenerIndex.value === -1) {
|
if (editingListenerIndex.value === -1) {
|
||||||
bpmnElementListeners.value.push(listenerObject);
|
bpmnElementListeners.value.push(listenerObject);
|
||||||
@@ -150,7 +130,6 @@ const saveListenerConfig = async () => {
|
|||||||
listenerForm.value,
|
listenerForm.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// 保存其他配置
|
|
||||||
otherExtensionList.value =
|
otherExtensionList.value =
|
||||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||||
(ex: any) => ex.$type !== `${prefix}:TaskListener`,
|
(ex: any) => ex.$type !== `${prefix}:TaskListener`,
|
||||||
@@ -159,63 +138,92 @@ const saveListenerConfig = async () => {
|
|||||||
...otherExtensionList.value,
|
...otherExtensionList.value,
|
||||||
...bpmnElementListeners.value,
|
...bpmnElementListeners.value,
|
||||||
]);
|
]);
|
||||||
// 4. 隐藏侧边栏
|
listenerDrawerApi.close();
|
||||||
listenerFormModelVisible.value = false;
|
|
||||||
listenerForm.value = {};
|
listenerForm.value = {};
|
||||||
};
|
}
|
||||||
|
|
||||||
// 打开监听器字段编辑弹窗
|
|
||||||
const openListenerFieldForm = (field: any, index?: number) => {
|
const openListenerFieldForm = (field: any, index?: number) => {
|
||||||
listenerFieldForm.value = field ? cloneDeep(field) : {};
|
listenerFieldForm.value = field ? cloneDeep(field) : {};
|
||||||
editingListenerFieldIndex.value = field ? index : -1;
|
editingListenerFieldIndex.value = field ? index : -1;
|
||||||
listenerFieldFormModelVisible.value = true;
|
fieldModalApi.open();
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (listenerFieldFormRef.value) listenerFieldFormRef.value.clearValidate();
|
if (listenerFieldFormRef.value) listenerFieldFormRef.value.clearValidate();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
// 保存监听器注入字段
|
|
||||||
const saveListenerFiled = async () => {
|
const [ListenerGrid, listenerGridApi] = useVbenVxeGrid({
|
||||||
const validateStatus = await listenerFieldFormRef.value.validate();
|
gridOptions: {
|
||||||
if (!validateStatus) return; // 验证不通过直接返回
|
columns: [
|
||||||
if (editingListenerFieldIndex.value === -1) {
|
{ type: 'seq', width: 50, title: '序号' },
|
||||||
fieldsListOfListener.value.push(listenerFieldForm.value);
|
{
|
||||||
listenerForm.value.fields.push(listenerFieldForm.value);
|
field: 'event',
|
||||||
} else {
|
title: '事件类型',
|
||||||
fieldsListOfListener.value.splice(
|
minWidth: 80,
|
||||||
editingListenerFieldIndex.value,
|
formatter: ({ cellValue }: { cellValue: string }) =>
|
||||||
1,
|
(listenerEventTypeObject.value as Record<string, any>)[cellValue],
|
||||||
listenerFieldForm.value,
|
},
|
||||||
);
|
{ field: 'id', title: '事件id', minWidth: 80, showOverflow: true },
|
||||||
listenerForm.value.fields.splice(
|
{
|
||||||
editingListenerFieldIndex.value,
|
field: 'listenerType',
|
||||||
1,
|
title: '监听器类型',
|
||||||
listenerFieldForm.value,
|
minWidth: 80,
|
||||||
);
|
formatter: ({ cellValue }: { cellValue: string }) =>
|
||||||
}
|
(listenerTypeObject.value as Record<string, any>)[cellValue],
|
||||||
listenerFieldFormModelVisible.value = false;
|
},
|
||||||
nextTick(() => {
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 120,
|
||||||
|
slots: { default: 'action' },
|
||||||
|
fixed: 'right',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
border: true,
|
||||||
|
showOverflow: true,
|
||||||
|
height: 'auto',
|
||||||
|
toolbarConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function saveListenerField() {
|
||||||
|
try {
|
||||||
|
await listenerFieldFormRef.value.validate();
|
||||||
|
if (editingListenerFieldIndex.value === -1) {
|
||||||
|
fieldsListOfListener.value.push(cloneDeep(listenerFieldForm.value));
|
||||||
|
listenerForm.value.fields.push(cloneDeep(listenerFieldForm.value));
|
||||||
|
} else {
|
||||||
|
fieldsListOfListener.value.splice(
|
||||||
|
editingListenerFieldIndex.value,
|
||||||
|
1,
|
||||||
|
cloneDeep(listenerFieldForm.value),
|
||||||
|
);
|
||||||
|
listenerForm.value.fields.splice(
|
||||||
|
editingListenerFieldIndex.value,
|
||||||
|
1,
|
||||||
|
cloneDeep(listenerFieldForm.value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fieldModalApi.close();
|
||||||
listenerFieldForm.value = {};
|
listenerFieldForm.value = {};
|
||||||
});
|
} catch (error) {
|
||||||
};
|
console.error(error);
|
||||||
// 移除监听器字段
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const removeListenerField = (_: any, index: number) => {
|
const removeListenerField = (_: any, index: number) => {
|
||||||
// console.log(field, 'field');
|
confirm({
|
||||||
Modal.confirm({
|
|
||||||
title: '提示',
|
title: '提示',
|
||||||
content: '确认移除该字段吗?',
|
content: '确认移除该字段吗?',
|
||||||
okText: '确 认',
|
}).then(() => {
|
||||||
cancelText: '取 消',
|
fieldsListOfListener.value.splice(index, 1);
|
||||||
onOk() {
|
listenerForm.value.fields.splice(index, 1);
|
||||||
fieldsListOfListener.value.splice(index, 1);
|
|
||||||
listenerForm.value.fields.splice(index, 1);
|
|
||||||
},
|
|
||||||
onCancel() {
|
|
||||||
// console.info('操作取消');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开监听器弹窗
|
|
||||||
const processListenerDialogRef = ref<any>();
|
const processListenerDialogRef = ref<any>();
|
||||||
const openProcessListenerDialog = async () => {
|
const openProcessListenerDialog = async () => {
|
||||||
processListenerDialogRef.value.open('task');
|
processListenerDialogRef.value.open('task');
|
||||||
@@ -226,7 +234,6 @@ const selectProcessListener = (listener: any) => {
|
|||||||
bpmnElementListeners.value.push(listenerObject);
|
bpmnElementListeners.value.push(listenerObject);
|
||||||
elementListenersList.value.push(listenerForm);
|
elementListenersList.value.push(listenerForm);
|
||||||
|
|
||||||
// 保存其他配置
|
|
||||||
otherExtensionList.value =
|
otherExtensionList.value =
|
||||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||||
(ex: any) => ex.$type !== `${prefix}:TaskListener`,
|
(ex: any) => ex.$type !== `${prefix}:TaskListener`,
|
||||||
@@ -237,6 +244,69 @@ const selectProcessListener = (listener: any) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [ListenerDrawer, listenerDrawerApi] = useVbenDrawer({
|
||||||
|
title: '任务监听器',
|
||||||
|
destroyOnClose: true,
|
||||||
|
onConfirm: saveListenerConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [FieldModal, fieldModalApi] = useVbenModal({
|
||||||
|
title: '字段配置',
|
||||||
|
onConfirm: saveListenerField,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [FieldsGrid, fieldsGridApi] = useVbenVxeGrid({
|
||||||
|
gridOptions: {
|
||||||
|
columns: [
|
||||||
|
{ type: 'seq', width: 50, title: '序号' },
|
||||||
|
{ field: 'name', title: '字段名称', minWidth: 100 },
|
||||||
|
{
|
||||||
|
field: 'fieldType',
|
||||||
|
title: '字段类型',
|
||||||
|
width: 80,
|
||||||
|
formatter: ({ cellValue }: { cellValue: string }) =>
|
||||||
|
fieldTypeObject.value[cellValue as keyof typeof fieldType],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '字段值/表达式',
|
||||||
|
width: 100,
|
||||||
|
formatter: ({ row }: { row: any }) => row.string || row.expression,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 120,
|
||||||
|
slots: { default: 'action' },
|
||||||
|
fixed: 'right',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
border: true,
|
||||||
|
showOverflow: true,
|
||||||
|
minHeight: 200,
|
||||||
|
toolbarConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
elementListenersList,
|
||||||
|
(val) => {
|
||||||
|
listenerGridApi.setGridOptions({ data: val });
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
fieldsListOfListener,
|
||||||
|
(val) => {
|
||||||
|
fieldsGridApi.setGridOptions({ data: val });
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.id,
|
() => props.id,
|
||||||
(val) => {
|
(val) => {
|
||||||
@@ -250,257 +320,218 @@ watch(
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="panel-tab__content">
|
<div class="-mx-2">
|
||||||
<Table :data="elementListenersList" size="small" bordered>
|
<ListenerGrid>
|
||||||
<TableColumn title="序号" width="50px" type="index" />
|
<template #action="{ row, rowIndex }">
|
||||||
<TableColumn
|
<Button
|
||||||
title="事件类型"
|
size="small"
|
||||||
width="80px"
|
type="link"
|
||||||
:ellipsis="{ showTitle: true }"
|
@click="openListenerForm(row, rowIndex)"
|
||||||
:custom-render="
|
>
|
||||||
({ record }: any) =>
|
编辑
|
||||||
listenerEventTypeObject[record.event as keyof typeof eventType]
|
</Button>
|
||||||
"
|
<Divider type="vertical" />
|
||||||
/>
|
<Button
|
||||||
<TableColumn
|
size="small"
|
||||||
title="事件id"
|
type="link"
|
||||||
width="80px"
|
danger
|
||||||
data-index="id"
|
@click="removeListener(row, rowIndex)"
|
||||||
:ellipsis="{ showTitle: true }"
|
>
|
||||||
/>
|
移除
|
||||||
<TableColumn
|
</Button>
|
||||||
title="监听器类型"
|
</template>
|
||||||
width="80px"
|
</ListenerGrid>
|
||||||
:ellipsis="{ showTitle: true }"
|
<div class="mt-1 flex w-full items-center justify-center gap-2 px-2">
|
||||||
:custom-render="
|
<Button
|
||||||
({ record }: any) =>
|
class="flex flex-1 items-center justify-center"
|
||||||
listenerTypeObject[record.listenerType as keyof typeof listenerType]
|
size="small"
|
||||||
"
|
type="primary"
|
||||||
/>
|
@click="openListenerForm(null)"
|
||||||
<TableColumn title="操作" width="90px">
|
>
|
||||||
<template #default="{ record, index }">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="link"
|
|
||||||
@click="openListenerForm(record, index)"
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
<Divider type="vertical" />
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="link"
|
|
||||||
danger
|
|
||||||
@click="removeListener(record, index)"
|
|
||||||
>
|
|
||||||
移除
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
</TableColumn>
|
|
||||||
</Table>
|
|
||||||
<div class="element-drawer__button">
|
|
||||||
<Button size="small" type="primary" @click="openListenerForm(null)">
|
|
||||||
<template #icon> <IconifyIcon icon="ep:plus" /></template>
|
<template #icon> <IconifyIcon icon="ep:plus" /></template>
|
||||||
添加监听器
|
添加监听器
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="small" @click="openProcessListenerDialog">
|
<Button
|
||||||
|
class="flex flex-1 items-center justify-center"
|
||||||
|
size="small"
|
||||||
|
@click="openProcessListenerDialog"
|
||||||
|
>
|
||||||
<template #icon> <IconifyIcon icon="ep:select" /></template>
|
<template #icon> <IconifyIcon icon="ep:select" /></template>
|
||||||
选择监听器
|
选择监听器
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 监听器 编辑/创建 部分 -->
|
<!-- 监听器 编辑/创建 部分 -->
|
||||||
<Drawer
|
<ListenerDrawer class="w-2/5">
|
||||||
v-model:open="listenerFormModelVisible"
|
<template #default>
|
||||||
title="任务监听器"
|
<Form
|
||||||
:width="width"
|
:label-col="{ span: 6 }"
|
||||||
:destroy-on-close="true"
|
:model="listenerForm"
|
||||||
>
|
:wrapper-col="{ span: 18 }"
|
||||||
<Form :model="listenerForm" ref="listenerFormRef">
|
ref="listenerFormRef"
|
||||||
<FormItem
|
|
||||||
label="事件类型"
|
|
||||||
name="event"
|
|
||||||
:rules="[{ required: true, message: '请选择事件类型' }]"
|
|
||||||
>
|
>
|
||||||
<Select v-model:value="listenerForm.event">
|
|
||||||
<SelectOption
|
|
||||||
v-for="i in Object.keys(listenerEventTypeObject)"
|
|
||||||
:key="i"
|
|
||||||
:value="i"
|
|
||||||
>
|
|
||||||
{{ listenerEventTypeObject[i as keyof typeof eventType] }}
|
|
||||||
</SelectOption>
|
|
||||||
</Select>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem
|
|
||||||
label="监听器ID"
|
|
||||||
name="id"
|
|
||||||
:rules="[{ required: true, message: '请输入监听器ID' }]"
|
|
||||||
>
|
|
||||||
<Input v-model:value="listenerForm.id" allow-clear />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem
|
|
||||||
label="监听器类型"
|
|
||||||
name="listenerType"
|
|
||||||
:rules="[{ required: true, message: '请选择监听器类型' }]"
|
|
||||||
>
|
|
||||||
<Select v-model:value="listenerForm.listenerType">
|
|
||||||
<SelectOption
|
|
||||||
v-for="i in Object.keys(listenerTypeObject)"
|
|
||||||
:key="i"
|
|
||||||
:value="i"
|
|
||||||
>
|
|
||||||
{{ listenerTypeObject[i as keyof typeof listenerType] }}
|
|
||||||
</SelectOption>
|
|
||||||
</Select>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem
|
|
||||||
v-if="listenerForm.listenerType === 'classListener'"
|
|
||||||
label="Java类"
|
|
||||||
name="class"
|
|
||||||
key="listener-class"
|
|
||||||
:rules="[{ required: true, message: '请输入Java类' }]"
|
|
||||||
>
|
|
||||||
<Input v-model:value="listenerForm.class" allow-clear />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem
|
|
||||||
v-if="listenerForm.listenerType === 'expressionListener'"
|
|
||||||
label="表达式"
|
|
||||||
name="expression"
|
|
||||||
key="listener-expression"
|
|
||||||
:rules="[{ required: true, message: '请输入表达式' }]"
|
|
||||||
>
|
|
||||||
<Input v-model:value="listenerForm.expression" allow-clear />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem
|
|
||||||
v-if="listenerForm.listenerType === 'delegateExpressionListener'"
|
|
||||||
label="代理表达式"
|
|
||||||
name="delegateExpression"
|
|
||||||
key="listener-delegate"
|
|
||||||
:rules="[{ required: true, message: '请输入代理表达式' }]"
|
|
||||||
>
|
|
||||||
<Input v-model:value="listenerForm.delegateExpression" allow-clear />
|
|
||||||
</FormItem>
|
|
||||||
<template v-if="listenerForm.listenerType === 'scriptListener'">
|
|
||||||
<FormItem
|
<FormItem
|
||||||
label="脚本格式"
|
label="事件类型"
|
||||||
name="scriptFormat"
|
name="event"
|
||||||
key="listener-script-format"
|
:rules="[{ required: true, message: '请选择事件类型' }]"
|
||||||
:rules="[{ required: true, message: '请填写脚本格式' }]"
|
|
||||||
>
|
>
|
||||||
<Input v-model:value="listenerForm.scriptFormat" allow-clear />
|
<Select v-model:value="listenerForm.event">
|
||||||
</FormItem>
|
<SelectOption
|
||||||
<FormItem
|
v-for="i in Object.keys(listenerEventTypeObject)"
|
||||||
label="脚本类型"
|
:key="i"
|
||||||
name="scriptType"
|
:value="i"
|
||||||
key="listener-script-type"
|
>
|
||||||
:rules="[{ required: true, message: '请选择脚本类型' }]"
|
{{ listenerEventTypeObject[i as keyof typeof eventType] }}
|
||||||
>
|
</SelectOption>
|
||||||
<Select v-model:value="listenerForm.scriptType">
|
|
||||||
<SelectOption value="inlineScript">内联脚本</SelectOption>
|
|
||||||
<SelectOption value="externalScript">外部脚本</SelectOption>
|
|
||||||
</Select>
|
</Select>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem
|
<FormItem
|
||||||
v-if="listenerForm.scriptType === 'inlineScript'"
|
label="监听器ID"
|
||||||
label="脚本内容"
|
name="id"
|
||||||
name="value"
|
:rules="[{ required: true, message: '请输入监听器ID' }]"
|
||||||
key="listener-script"
|
|
||||||
:rules="[{ required: true, message: '请填写脚本内容' }]"
|
|
||||||
>
|
>
|
||||||
<Input v-model:value="listenerForm.value" allow-clear />
|
<Input v-model:value="listenerForm.id" allow-clear />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem
|
<FormItem
|
||||||
v-if="listenerForm.scriptType === 'externalScript'"
|
label="监听器类型"
|
||||||
label="资源地址"
|
name="listenerType"
|
||||||
name="resource"
|
:rules="[{ required: true, message: '请选择监听器类型' }]"
|
||||||
key="listener-resource"
|
|
||||||
:rules="[{ required: true, message: '请填写资源地址' }]"
|
|
||||||
>
|
>
|
||||||
<Input v-model:value="listenerForm.resource" allow-clear />
|
<Select v-model:value="listenerForm.listenerType">
|
||||||
</FormItem>
|
<SelectOption
|
||||||
</template>
|
v-for="i in Object.keys(listenerTypeObject)"
|
||||||
|
:key="i"
|
||||||
<template v-if="listenerForm.event === 'timeout'">
|
:value="i"
|
||||||
<FormItem
|
>
|
||||||
label="定时器类型"
|
{{ listenerTypeObject[i as keyof typeof listenerType] }}
|
||||||
name="eventDefinitionType"
|
</SelectOption>
|
||||||
key="eventDefinitionType"
|
|
||||||
>
|
|
||||||
<Select v-model:value="listenerForm.eventDefinitionType">
|
|
||||||
<SelectOption value="date">日期</SelectOption>
|
|
||||||
<SelectOption value="duration">持续时长</SelectOption>
|
|
||||||
<SelectOption value="cycle">循环</SelectOption>
|
|
||||||
<SelectOption value="null">无</SelectOption>
|
|
||||||
</Select>
|
</Select>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem
|
<FormItem
|
||||||
v-if="
|
v-if="listenerForm.listenerType === 'classListener'"
|
||||||
!!listenerForm.eventDefinitionType &&
|
label="Java类"
|
||||||
listenerForm.eventDefinitionType !== 'null'
|
name="class"
|
||||||
"
|
key="listener-class"
|
||||||
label="定时器"
|
:rules="[{ required: true, message: '请输入Java类' }]"
|
||||||
name="eventTimeDefinitions"
|
>
|
||||||
key="eventTimeDefinitions"
|
<Input v-model:value="listenerForm.class" allow-clear />
|
||||||
:rules="[{ required: true, message: '请填写定时器配置' }]"
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
v-if="listenerForm.listenerType === 'expressionListener'"
|
||||||
|
label="表达式"
|
||||||
|
name="expression"
|
||||||
|
key="listener-expression"
|
||||||
|
:rules="[{ required: true, message: '请输入表达式' }]"
|
||||||
|
>
|
||||||
|
<Input v-model:value="listenerForm.expression" allow-clear />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
v-if="listenerForm.listenerType === 'delegateExpressionListener'"
|
||||||
|
label="代理表达式"
|
||||||
|
name="delegateExpression"
|
||||||
|
key="listener-delegate"
|
||||||
|
:rules="[{ required: true, message: '请输入代理表达式' }]"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
v-model:value="listenerForm.eventTimeDefinitions"
|
v-model:value="listenerForm.delegateExpression"
|
||||||
allow-clear
|
allow-clear
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</template>
|
<template v-if="listenerForm.listenerType === 'scriptListener'">
|
||||||
</Form>
|
<FormItem
|
||||||
|
label="脚本格式"
|
||||||
<Divider />
|
name="scriptFormat"
|
||||||
<div class="mb-2 flex justify-between">
|
key="listener-script-format"
|
||||||
<span class="flex items-center">
|
:rules="[{ required: true, message: '请填写脚本格式' }]"
|
||||||
<IconifyIcon icon="ep:menu" class="mr-2 text-gray-600" />
|
>
|
||||||
注入字段
|
<Input v-model:value="listenerForm.scriptFormat" allow-clear />
|
||||||
</span>
|
</FormItem>
|
||||||
<Button
|
<FormItem
|
||||||
type="primary"
|
label="脚本类型"
|
||||||
title="添加字段"
|
name="scriptType"
|
||||||
@click="openListenerFieldForm(null)"
|
key="listener-script-type"
|
||||||
>
|
:rules="[{ required: true, message: '请选择脚本类型' }]"
|
||||||
<template #icon>
|
>
|
||||||
<IconifyIcon icon="ep:plus" />
|
<Select v-model:value="listenerForm.scriptType">
|
||||||
|
<SelectOption value="inlineScript">内联脚本</SelectOption>
|
||||||
|
<SelectOption value="externalScript">外部脚本</SelectOption>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
v-if="listenerForm.scriptType === 'inlineScript'"
|
||||||
|
label="脚本内容"
|
||||||
|
name="value"
|
||||||
|
key="listener-script"
|
||||||
|
:rules="[{ required: true, message: '请填写脚本内容' }]"
|
||||||
|
>
|
||||||
|
<Input v-model:value="listenerForm.value" allow-clear />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
v-if="listenerForm.scriptType === 'externalScript'"
|
||||||
|
label="资源地址"
|
||||||
|
name="resource"
|
||||||
|
key="listener-resource"
|
||||||
|
:rules="[{ required: true, message: '请填写资源地址' }]"
|
||||||
|
>
|
||||||
|
<Input v-model:value="listenerForm.resource" allow-clear />
|
||||||
|
</FormItem>
|
||||||
</template>
|
</template>
|
||||||
添加字段
|
|
||||||
</Button>
|
<template v-if="listenerForm.event === 'timeout'">
|
||||||
</div>
|
<FormItem
|
||||||
<Table
|
label="定时器类型"
|
||||||
:data="fieldsListOfListener"
|
name="eventDefinitionType"
|
||||||
size="small"
|
key="eventDefinitionType"
|
||||||
:scroll="{ y: 240 }"
|
>
|
||||||
bordered
|
<Select v-model:value="listenerForm.eventDefinitionType">
|
||||||
style="flex: none"
|
<SelectOption value="date">日期</SelectOption>
|
||||||
>
|
<SelectOption value="duration">持续时长</SelectOption>
|
||||||
<TableColumn title="序号" width="50px" type="index" />
|
<SelectOption value="cycle">循环</SelectOption>
|
||||||
<TableColumn title="字段名称" width="100px" data-index="name" />
|
<SelectOption value="null">无</SelectOption>
|
||||||
<TableColumn
|
</Select>
|
||||||
title="字段类型"
|
</FormItem>
|
||||||
width="80px"
|
<FormItem
|
||||||
:ellipsis="{ showTitle: true }"
|
v-if="
|
||||||
:custom-render="
|
!!listenerForm.eventDefinitionType &&
|
||||||
({ record }: any) =>
|
listenerForm.eventDefinitionType !== 'null'
|
||||||
fieldTypeObject[record.fieldType as keyof typeof fieldType]
|
"
|
||||||
"
|
label="定时器"
|
||||||
/>
|
name="eventTimeDefinitions"
|
||||||
<TableColumn
|
key="eventTimeDefinitions"
|
||||||
title="字段值/表达式"
|
:rules="[{ required: true, message: '请填写定时器配置' }]"
|
||||||
width="100px"
|
>
|
||||||
:ellipsis="{ showTitle: true }"
|
<Input
|
||||||
:custom-render="
|
v-model:value="listenerForm.eventTimeDefinitions"
|
||||||
({ record }: any) => record.string || record.expression
|
allow-clear
|
||||||
"
|
/>
|
||||||
/>
|
</FormItem>
|
||||||
<TableColumn title="操作" width="100px">
|
</template>
|
||||||
<template #default="{ record, index }">
|
</Form>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<div class="mb-2 flex justify-between">
|
||||||
|
<span class="flex items-center">
|
||||||
|
<IconifyIcon icon="ep:menu" class="mr-2 text-gray-600" />
|
||||||
|
注入字段
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
class="flex items-center"
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="openListenerFieldForm(null)"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<IconifyIcon class="size-4" icon="ep:plus" />
|
||||||
|
</template>
|
||||||
|
添加字段
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<FieldsGrid>
|
||||||
|
<template #action="{ row, rowIndex }">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="link"
|
type="link"
|
||||||
@click="openListenerFieldForm(record, index)"
|
@click="openListenerFieldForm(row, rowIndex)"
|
||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
@@ -509,32 +540,23 @@ watch(
|
|||||||
size="small"
|
size="small"
|
||||||
type="link"
|
type="link"
|
||||||
danger
|
danger
|
||||||
@click="removeListenerField(record, index)"
|
@click="removeListenerField(row, rowIndex)"
|
||||||
>
|
>
|
||||||
移除
|
移除
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</TableColumn>
|
</FieldsGrid>
|
||||||
</Table>
|
</template>
|
||||||
|
</ListenerDrawer>
|
||||||
<div class="element-drawer__button">
|
|
||||||
<Button size="small" @click="listenerFormModelVisible = false">
|
|
||||||
取 消
|
|
||||||
</Button>
|
|
||||||
<Button size="small" type="primary" @click="saveListenerConfig">
|
|
||||||
保 存
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Drawer>
|
|
||||||
|
|
||||||
<!-- 注入字段 编辑/创建 部分 -->
|
<!-- 注入字段 编辑/创建 部分 -->
|
||||||
<Modal
|
<FieldModal class="w-3/5">
|
||||||
title="字段配置"
|
<Form
|
||||||
v-model:open="listenerFieldFormModelVisible"
|
:label-col="{ span: 4 }"
|
||||||
:width="600"
|
:wrapper-col="{ span: 18 }"
|
||||||
:destroy-on-close="true"
|
:model="listenerFieldForm"
|
||||||
>
|
ref="listenerFieldFormRef"
|
||||||
<Form :model="listenerFieldForm" ref="listenerFieldFormRef">
|
>
|
||||||
<FormItem
|
<FormItem
|
||||||
label="字段名称:"
|
label="字段名称:"
|
||||||
name="name"
|
name="name"
|
||||||
@@ -576,15 +598,7 @@ watch(
|
|||||||
<Input v-model:value="listenerFieldForm.expression" allow-clear />
|
<Input v-model:value="listenerFieldForm.expression" allow-clear />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Form>
|
</Form>
|
||||||
<template #footer>
|
</FieldModal>
|
||||||
<Button size="small" @click="listenerFieldFormModelVisible = false">
|
|
||||||
取 消
|
|
||||||
</Button>
|
|
||||||
<Button size="small" type="primary" @click="saveListenerFiled">
|
|
||||||
确 定
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 选择弹窗 -->
|
<!-- 选择弹窗 -->
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { IconifyIcon } from '@vben/icons';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Divider,
|
||||||
Form,
|
Form,
|
||||||
FormItem,
|
FormItem,
|
||||||
Input,
|
Input,
|
||||||
@@ -23,13 +24,31 @@ const modelObjectForm = ref<any>({});
|
|||||||
const rootElements = ref();
|
const rootElements = ref();
|
||||||
const messageIdMap = ref();
|
const messageIdMap = ref();
|
||||||
const signalIdMap = ref();
|
const signalIdMap = ref();
|
||||||
|
const editingIndex = ref(-1); // 正在编辑的索引,-1 表示新建
|
||||||
const modelConfig = computed(() => {
|
const modelConfig = computed(() => {
|
||||||
|
const isEdit = editingIndex.value !== -1;
|
||||||
return modelType.value === 'message'
|
return modelType.value === 'message'
|
||||||
? { title: '创建消息', idLabel: '消息ID', nameLabel: '消息名称' }
|
? {
|
||||||
: { title: '创建信号', idLabel: '信号ID', nameLabel: '信号名称' };
|
title: isEdit ? '编辑消息' : '创建消息',
|
||||||
|
idLabel: '消息ID',
|
||||||
|
nameLabel: '消息名称',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
title: isEdit ? '编辑信号' : '创建信号',
|
||||||
|
idLabel: '信号ID',
|
||||||
|
nameLabel: '信号名称',
|
||||||
|
};
|
||||||
});
|
});
|
||||||
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||||
|
|
||||||
|
// 生成规范化的ID
|
||||||
|
const generateStandardId = (type: string): string => {
|
||||||
|
const prefix = type === 'message' ? 'Message_' : 'Signal_';
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const random = Math.random().toString(36).slice(2, 6).toUpperCase();
|
||||||
|
return `${prefix}${timestamp}_${random}`;
|
||||||
|
};
|
||||||
|
|
||||||
const initDataList = () => {
|
const initDataList = () => {
|
||||||
// console.log(window, 'window');
|
// console.log(window, 'window');
|
||||||
rootElements.value = bpmnInstances().modeler.getDefinitions().rootElements;
|
rootElements.value = bpmnInstances().modeler.getDefinitions().rootElements;
|
||||||
@@ -48,35 +67,104 @@ const initDataList = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const openModel = (type: any) => {
|
const openModel = (type: any) => {
|
||||||
modelType.value = type;
|
modelType.value = type;
|
||||||
modelObjectForm.value = {};
|
editingIndex.value = -1;
|
||||||
|
modelObjectForm.value = {
|
||||||
|
id: generateStandardId(type),
|
||||||
|
name: '',
|
||||||
|
};
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openEditModel = (type: any, row: any, index: number) => {
|
||||||
|
modelType.value = type;
|
||||||
|
editingIndex.value = index;
|
||||||
|
modelObjectForm.value = { ...row };
|
||||||
|
dialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
const addNewObject = () => {
|
const addNewObject = () => {
|
||||||
if (modelType.value === 'message') {
|
if (modelType.value === 'message') {
|
||||||
if (messageIdMap.value[modelObjectForm.value.id]) {
|
// 编辑模式
|
||||||
message.error('该消息已存在,请修改id后重新保存');
|
if (editingIndex.value === -1) {
|
||||||
|
// 新建模式
|
||||||
|
if (messageIdMap.value[modelObjectForm.value.id]) {
|
||||||
|
message.error('该消息已存在,请修改id后重新保存');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const messageRef = bpmnInstances().moddle.create(
|
||||||
|
'bpmn:Message',
|
||||||
|
modelObjectForm.value,
|
||||||
|
);
|
||||||
|
rootElements.value.push(messageRef);
|
||||||
|
} else {
|
||||||
|
const targetMessage = messageList.value[editingIndex.value];
|
||||||
|
// 查找 rootElements 中的原始对象
|
||||||
|
const rootMessage = rootElements.value.find(
|
||||||
|
(el: any) => el.$type === 'bpmn:Message' && el.id === targetMessage.id,
|
||||||
|
);
|
||||||
|
if (rootMessage) {
|
||||||
|
rootMessage.id = modelObjectForm.value.id;
|
||||||
|
rootMessage.name = modelObjectForm.value.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const messageRef = bpmnInstances().moddle.create(
|
|
||||||
'bpmn:Message',
|
|
||||||
modelObjectForm.value,
|
|
||||||
);
|
|
||||||
rootElements.value.push(messageRef);
|
|
||||||
} else {
|
} else {
|
||||||
if (signalIdMap.value[modelObjectForm.value.id]) {
|
// 编辑模式
|
||||||
message.error('该信号已存在,请修改id后重新保存');
|
if (editingIndex.value === -1) {
|
||||||
|
// 新建模式
|
||||||
|
if (signalIdMap.value[modelObjectForm.value.id]) {
|
||||||
|
message.error('该信号已存在,请修改id后重新保存');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const signalRef = bpmnInstances().moddle.create(
|
||||||
|
'bpmn:Signal',
|
||||||
|
modelObjectForm.value,
|
||||||
|
);
|
||||||
|
rootElements.value.push(signalRef);
|
||||||
|
} else {
|
||||||
|
const targetSignal = signalList.value[editingIndex.value];
|
||||||
|
// 查找 rootElements 中的原始对象
|
||||||
|
const rootSignal = rootElements.value.find(
|
||||||
|
(el: any) => el.$type === 'bpmn:Signal' && el.id === targetSignal.id,
|
||||||
|
);
|
||||||
|
if (rootSignal) {
|
||||||
|
rootSignal.id = modelObjectForm.value.id;
|
||||||
|
rootSignal.name = modelObjectForm.value.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const signalRef = bpmnInstances().moddle.create(
|
|
||||||
'bpmn:Signal',
|
|
||||||
modelObjectForm.value,
|
|
||||||
);
|
|
||||||
rootElements.value.push(signalRef);
|
|
||||||
}
|
}
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
initDataList();
|
initDataList();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 补充"编辑"、"移除"功能。相关 issue:https://github.com/YunaiV/yudao-cloud/issues/270
|
||||||
|
const removeObject = (type: any, row: any) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '提示',
|
||||||
|
content: `确认移除该${type === 'message' ? '消息' : '信号'}吗?`,
|
||||||
|
okText: '确 认',
|
||||||
|
cancelText: '取 消',
|
||||||
|
onOk() {
|
||||||
|
// 从 rootElements 中移除
|
||||||
|
const targetType = type === 'message' ? 'bpmn:Message' : 'bpmn:Signal';
|
||||||
|
const elementIndex = rootElements.value.findIndex(
|
||||||
|
(el: any) => el.$type === targetType && el.id === row.id,
|
||||||
|
);
|
||||||
|
if (elementIndex !== -1) {
|
||||||
|
rootElements.value.splice(elementIndex, 1);
|
||||||
|
}
|
||||||
|
// 刷新列表
|
||||||
|
initDataList();
|
||||||
|
message.success('移除成功');
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
// console.info('操作取消');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initDataList();
|
initDataList();
|
||||||
});
|
});
|
||||||
@@ -103,6 +191,26 @@ onMounted(() => {
|
|||||||
</TableColumn>
|
</TableColumn>
|
||||||
<TableColumn title="消息ID" data-index="id" />
|
<TableColumn title="消息ID" data-index="id" />
|
||||||
<TableColumn title="消息名称" data-index="name" />
|
<TableColumn title="消息名称" data-index="name" />
|
||||||
|
<TableColumn title="操作" width="110px">
|
||||||
|
<template #default="{ record, index }">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="openEditModel('message', record, index)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Divider type="vertical" />
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
@click="removeObject('message', record)"
|
||||||
|
>
|
||||||
|
移除
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</TableColumn>
|
||||||
</Table>
|
</Table>
|
||||||
<div class="panel-tab__content--title mt-2 border-t border-gray-200 pt-2">
|
<div class="panel-tab__content--title mt-2 border-t border-gray-200 pt-2">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
@@ -124,6 +232,26 @@ onMounted(() => {
|
|||||||
</TableColumn>
|
</TableColumn>
|
||||||
<TableColumn title="信号ID" data-index="id" />
|
<TableColumn title="信号ID" data-index="id" />
|
||||||
<TableColumn title="信号名称" data-index="name" />
|
<TableColumn title="信号名称" data-index="name" />
|
||||||
|
<TableColumn title="操作" width="110px">
|
||||||
|
<template #default="{ record, index }">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="openEditModel('signal', record, index)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Divider type="vertical" />
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
@click="removeObject('signal', record)"
|
||||||
|
>
|
||||||
|
移除
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</TableColumn>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import { Button, Input, Modal } from 'ant-design-vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'HttpHeaderEditor' });
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'save']);
|
||||||
|
|
||||||
|
interface HeaderItem {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogVisible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val),
|
||||||
|
});
|
||||||
|
|
||||||
|
const headerList = ref<HeaderItem[]>([]);
|
||||||
|
|
||||||
|
// 解析请求头字符串为列表
|
||||||
|
const parseHeaders = (headersStr: string): HeaderItem[] => {
|
||||||
|
if (!headersStr || !headersStr.trim()) {
|
||||||
|
return [{ key: '', value: '' }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = headersStr.split('\n').filter((line) => line.trim());
|
||||||
|
const parsed = lines.map((line) => {
|
||||||
|
const colonIndex = line.indexOf(':');
|
||||||
|
if (colonIndex > 0) {
|
||||||
|
return {
|
||||||
|
key: line.slice(0, Math.max(0, colonIndex)).trim(),
|
||||||
|
value: line.slice(Math.max(0, colonIndex + 1)).trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { key: line.trim(), value: '' };
|
||||||
|
});
|
||||||
|
|
||||||
|
return parsed.length > 0 ? parsed : [{ key: '', value: '' }];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将列表转换为请求头字符串
|
||||||
|
const stringifyHeaders = (headers: HeaderItem[]): string => {
|
||||||
|
return headers
|
||||||
|
.filter((item) => item.key.trim())
|
||||||
|
.map((item) => `${item.key}: ${item.value}`)
|
||||||
|
.join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加请求头
|
||||||
|
const addHeader = () => {
|
||||||
|
headerList.value.push({ key: '', value: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移除请求头
|
||||||
|
const removeHeader = (index: number) => {
|
||||||
|
if (headerList.value.length === 1) {
|
||||||
|
// 至少保留一行
|
||||||
|
headerList.value = [{ key: '', value: '' }];
|
||||||
|
} else {
|
||||||
|
headerList.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存
|
||||||
|
const handleSave = () => {
|
||||||
|
const headersStr = stringifyHeaders(headerList.value);
|
||||||
|
emit('save', headersStr);
|
||||||
|
dialogVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭
|
||||||
|
const handleClose = () => {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听对话框打开,初始化数据
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
headerList.value = parseHeaders(props.headers);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
v-model:open="dialogVisible"
|
||||||
|
title="编辑请求头"
|
||||||
|
width="600px"
|
||||||
|
:mask-closable="false"
|
||||||
|
@cancel="handleClose"
|
||||||
|
>
|
||||||
|
<div class="header-editor">
|
||||||
|
<div class="header-list">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in headerList"
|
||||||
|
:key="index"
|
||||||
|
class="header-item"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
v-model:value="item.key"
|
||||||
|
placeholder="请输入参数名"
|
||||||
|
class="header-key"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
<span class="separator">:</span>
|
||||||
|
<Input
|
||||||
|
v-model:value="item.value"
|
||||||
|
placeholder="请输入参数值 (支持表达式 ${变量名})"
|
||||||
|
class="header-value"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
<Button type="text" danger size="small" @click="removeHeader(index)">
|
||||||
|
<template #icon>
|
||||||
|
<IconifyIcon icon="ep:delete" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button type="primary" class="add-btn" @click="addHeader">
|
||||||
|
<template #icon>
|
||||||
|
<IconifyIcon icon="ep:plus" />
|
||||||
|
</template>
|
||||||
|
添加请求头
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button @click="handleClose">取消</Button>
|
||||||
|
<Button type="primary" @click="handleSave">保存</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.header-editor {
|
||||||
|
.header-list {
|
||||||
|
max-height: 400px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.header-key {
|
||||||
|
flex: 0 0 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-value {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,22 @@
|
|||||||
|
<!-- eslint-disable prettier/prettier -->
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
|
import { inject, nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
|
||||||
|
|
||||||
import { FormItem, Input, Select } from 'ant-design-vue';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
RadioButton,
|
||||||
|
RadioGroup,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
Textarea,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { updateElementExtensions } from '../../../utils';
|
||||||
|
import HttpHeaderEditor from './HttpHeaderEditor.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'ServiceTask' });
|
defineOptions({ name: 'ServiceTask' });
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -9,38 +24,281 @@ const props = defineProps({
|
|||||||
type: { type: String, default: '' },
|
type: { type: String, default: '' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultTaskForm = ref({
|
const prefix = (inject('prefix', 'flowable') || 'flowable') as string;
|
||||||
|
const flowableTypeKey = `${prefix}:type`;
|
||||||
|
const flowableFieldType = `${prefix}:Field`;
|
||||||
|
|
||||||
|
const HTTP_FIELD_NAMES = [
|
||||||
|
'requestMethod',
|
||||||
|
'requestUrl',
|
||||||
|
'requestHeaders',
|
||||||
|
'disallowRedirects',
|
||||||
|
'ignoreException',
|
||||||
|
'saveResponseParameters',
|
||||||
|
'resultVariablePrefix',
|
||||||
|
'saveResponseParametersTransient',
|
||||||
|
'saveResponseVariableAsJson',
|
||||||
|
];
|
||||||
|
const HTTP_BOOLEAN_FIELDS = new Set([
|
||||||
|
'disallowRedirects',
|
||||||
|
'ignoreException',
|
||||||
|
'saveResponseParameters',
|
||||||
|
'saveResponseParametersTransient',
|
||||||
|
'saveResponseVariableAsJson',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const DEFAULT_TASK_FORM = {
|
||||||
executeType: '',
|
executeType: '',
|
||||||
class: '',
|
class: '',
|
||||||
expression: '',
|
expression: '',
|
||||||
delegateExpression: '',
|
delegateExpression: '',
|
||||||
});
|
};
|
||||||
|
|
||||||
const serviceTaskForm = ref<any>({});
|
const DEFAULT_HTTP_FORM = {
|
||||||
|
requestMethod: 'GET',
|
||||||
|
requestUrl: '',
|
||||||
|
requestHeaders: 'Content-Type: application/json',
|
||||||
|
resultVariablePrefix: '',
|
||||||
|
disallowRedirects: false,
|
||||||
|
ignoreException: false,
|
||||||
|
saveResponseParameters: false,
|
||||||
|
saveResponseParametersTransient: false,
|
||||||
|
saveResponseVariableAsJson: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const serviceTaskForm = ref({ ...DEFAULT_TASK_FORM });
|
||||||
|
const httpTaskForm = ref<any>({ ...DEFAULT_HTTP_FORM });
|
||||||
const bpmnElement = ref();
|
const bpmnElement = ref();
|
||||||
|
const httpInitializing = ref(false);
|
||||||
|
const showHeaderEditor = ref(false);
|
||||||
|
|
||||||
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||||
|
|
||||||
const resetTaskForm = () => {
|
// 判断字符串是否包含表达式
|
||||||
for (const key in defaultTaskForm.value) {
|
const isExpression = (value: string): boolean => {
|
||||||
const value =
|
if (!value) return false;
|
||||||
// @ts-ignore
|
// 检测 ${...} 或 #{...} 格式的表达式
|
||||||
bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key];
|
return /\$\{[^}]+\}/.test(value) || /#\{[^}]+\}/.test(value);
|
||||||
serviceTaskForm.value[key] = value;
|
};
|
||||||
if (value) {
|
|
||||||
serviceTaskForm.value.executeType = key;
|
const collectHttpExtensionInfo = () => {
|
||||||
|
const businessObject = bpmnElement.value?.businessObject;
|
||||||
|
const extensionElements = businessObject?.extensionElements;
|
||||||
|
const httpFields = new Map<string, string>();
|
||||||
|
const httpFieldTypes = new Map<string, 'expression' | 'string'>();
|
||||||
|
const otherExtensions: any[] = [];
|
||||||
|
|
||||||
|
extensionElements?.values?.forEach((item: any) => {
|
||||||
|
if (
|
||||||
|
item?.$type === flowableFieldType &&
|
||||||
|
HTTP_FIELD_NAMES.includes(item.name)
|
||||||
|
) {
|
||||||
|
const value = item.string ?? item.stringValue ?? item.expression ?? '';
|
||||||
|
const fieldType = item.expression ? 'expression' : 'string';
|
||||||
|
httpFields.set(item.name, value);
|
||||||
|
httpFieldTypes.set(item.name, fieldType);
|
||||||
|
} else {
|
||||||
|
otherExtensions.push(item);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { httpFields, httpFieldTypes, otherExtensions };
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetHttpDefaults = () => {
|
||||||
|
httpInitializing.value = true;
|
||||||
|
httpTaskForm.value = { ...DEFAULT_HTTP_FORM };
|
||||||
|
nextTick(() => {
|
||||||
|
httpInitializing.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetHttpForm = () => {
|
||||||
|
httpInitializing.value = true;
|
||||||
|
const { httpFields } = collectHttpExtensionInfo();
|
||||||
|
const nextForm: any = { ...DEFAULT_HTTP_FORM };
|
||||||
|
|
||||||
|
HTTP_FIELD_NAMES.forEach((name) => {
|
||||||
|
const stored = httpFields.get(name);
|
||||||
|
if (stored !== undefined) {
|
||||||
|
nextForm[name] = HTTP_BOOLEAN_FIELDS.has(name)
|
||||||
|
? stored === 'true'
|
||||||
|
: stored;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
httpTaskForm.value = nextForm;
|
||||||
|
nextTick(() => {
|
||||||
|
httpInitializing.value = false;
|
||||||
|
updateHttpExtensions(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetServiceTaskForm = () => {
|
||||||
|
const businessObject = bpmnElement.value?.businessObject;
|
||||||
|
const nextForm = { ...DEFAULT_TASK_FORM };
|
||||||
|
|
||||||
|
if (businessObject) {
|
||||||
|
if (businessObject.class) {
|
||||||
|
nextForm.class = businessObject.class;
|
||||||
|
nextForm.executeType = 'class';
|
||||||
|
}
|
||||||
|
if (businessObject.expression) {
|
||||||
|
nextForm.expression = businessObject.expression;
|
||||||
|
nextForm.executeType = 'expression';
|
||||||
|
}
|
||||||
|
if (businessObject.delegateExpression) {
|
||||||
|
nextForm.delegateExpression = businessObject.delegateExpression;
|
||||||
|
nextForm.executeType = 'delegateExpression';
|
||||||
|
}
|
||||||
|
if (businessObject.$attrs?.[flowableTypeKey] === 'http') {
|
||||||
|
nextForm.executeType = 'http';
|
||||||
|
} else {
|
||||||
|
// 兜底:如缺少 flowable:type=http,但扩展里已有 HTTP 的字段,也认为是 HTTP
|
||||||
|
const { httpFields } = collectHttpExtensionInfo();
|
||||||
|
if (httpFields.size > 0) {
|
||||||
|
nextForm.executeType = 'http';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceTaskForm.value = nextForm;
|
||||||
|
|
||||||
|
if (nextForm.executeType === 'http') {
|
||||||
|
resetHttpForm();
|
||||||
|
} else {
|
||||||
|
resetHttpDefaults();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateElementTask = () => {
|
const shouldPersistField = (name: string, value: any) => {
|
||||||
const taskAttr = Object.create(null);
|
if (HTTP_BOOLEAN_FIELDS.has(name)) return true;
|
||||||
const type = serviceTaskForm.value.executeType;
|
if (name === 'requestMethod') return true;
|
||||||
for (const key in serviceTaskForm.value) {
|
if (name === 'requestUrl') return !!value;
|
||||||
if (key !== 'executeType' && key !== type) taskAttr[key] = null;
|
return value !== undefined && value !== '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateHttpExtensions = (force = false) => {
|
||||||
|
if (!bpmnElement.value) return;
|
||||||
|
if (
|
||||||
|
!force &&
|
||||||
|
(httpInitializing.value || serviceTaskForm.value.executeType !== 'http')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
taskAttr[type] = serviceTaskForm.value[type] || '';
|
|
||||||
|
const {
|
||||||
|
httpFields: existingFields,
|
||||||
|
httpFieldTypes: existingTypes,
|
||||||
|
otherExtensions,
|
||||||
|
} = collectHttpExtensionInfo();
|
||||||
|
|
||||||
|
const desiredEntries: [string, string][] = [];
|
||||||
|
HTTP_FIELD_NAMES.forEach((name) => {
|
||||||
|
const rawValue = httpTaskForm.value[name];
|
||||||
|
if (!shouldPersistField(name, rawValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const persisted = HTTP_BOOLEAN_FIELDS.has(name)
|
||||||
|
? String(!!rawValue)
|
||||||
|
: (rawValue === undefined
|
||||||
|
? ''
|
||||||
|
: rawValue.toString());
|
||||||
|
|
||||||
|
desiredEntries.push([name, persisted]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查是否有变化:不仅比较值,还要比较字段类型(string vs expression)
|
||||||
|
if (!force && desiredEntries.length === existingFields.size) {
|
||||||
|
let noChange = true;
|
||||||
|
for (const [name, value] of desiredEntries) {
|
||||||
|
const existingValue = existingFields.get(name);
|
||||||
|
const existingType = existingTypes.get(name);
|
||||||
|
const currentType = isExpression(value) ? 'expression' : 'string';
|
||||||
|
if (existingValue !== value || existingType !== currentType) {
|
||||||
|
noChange = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (noChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const moddle = bpmnInstances().moddle;
|
||||||
|
const httpFieldElements = desiredEntries.map(([name, value]) => {
|
||||||
|
// 根据值是否包含表达式来决定使用 string 还是 expression 属性
|
||||||
|
const isExpr = isExpression(value);
|
||||||
|
return moddle.create(flowableFieldType, {
|
||||||
|
name,
|
||||||
|
...(isExpr ? { expression: value } : { string: value }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
updateElementExtensions(bpmnElement.value, [
|
||||||
|
...otherExtensions,
|
||||||
|
...httpFieldElements,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeHttpExtensions = () => {
|
||||||
|
if (!bpmnElement.value) return;
|
||||||
|
const { httpFields, otherExtensions } = collectHttpExtensionInfo();
|
||||||
|
if (httpFields.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otherExtensions.length === 0) {
|
||||||
|
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
|
||||||
|
extensionElements: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateElementExtensions(bpmnElement.value, otherExtensions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateElementTask = () => {
|
||||||
|
if (!bpmnElement.value) return;
|
||||||
|
|
||||||
|
const taskAttr: Record<string, any> = {
|
||||||
|
class: null,
|
||||||
|
expression: null,
|
||||||
|
delegateExpression: null,
|
||||||
|
[flowableTypeKey]: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const type = serviceTaskForm.value.executeType;
|
||||||
|
if (
|
||||||
|
type === 'class' ||
|
||||||
|
type === 'expression' ||
|
||||||
|
type === 'delegateExpression'
|
||||||
|
) {
|
||||||
|
taskAttr[type] = serviceTaskForm.value[type] || null;
|
||||||
|
} else if (type === 'http') {
|
||||||
|
taskAttr[flowableTypeKey] = 'http';
|
||||||
|
}
|
||||||
|
|
||||||
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr);
|
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr);
|
||||||
|
|
||||||
|
if (type === 'http') {
|
||||||
|
updateHttpExtensions(true);
|
||||||
|
} else {
|
||||||
|
removeHttpExtensions();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExecuteTypeChange = (value: any) => {
|
||||||
|
serviceTaskForm.value.executeType = value;
|
||||||
|
if (value === 'http') {
|
||||||
|
resetHttpForm();
|
||||||
|
}
|
||||||
|
updateElementTask();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHeadersSave = (headersStr: string) => {
|
||||||
|
httpTaskForm.value.requestHeaders = headersStr;
|
||||||
};
|
};
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -52,11 +310,19 @@ watch(
|
|||||||
() => {
|
() => {
|
||||||
bpmnElement.value = bpmnInstances().bpmnElement;
|
bpmnElement.value = bpmnInstances().bpmnElement;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
resetTaskForm();
|
resetServiceTaskForm();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => httpTaskForm.value,
|
||||||
|
() => {
|
||||||
|
updateHttpExtensions();
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -68,7 +334,9 @@ watch(
|
|||||||
{ label: 'Java类', value: 'class' },
|
{ label: 'Java类', value: 'class' },
|
||||||
{ label: '表达式', value: 'expression' },
|
{ label: '表达式', value: 'expression' },
|
||||||
{ label: '代理表达式', value: 'delegateExpression' },
|
{ label: '代理表达式', value: 'delegateExpression' },
|
||||||
|
{ label: 'HTTP 调用', value: 'http' },
|
||||||
]"
|
]"
|
||||||
|
@change="handleExecuteTypeChange"
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem
|
<FormItem
|
||||||
@@ -107,5 +375,62 @@ watch(
|
|||||||
@change="updateElementTask"
|
@change="updateElementTask"
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
<template v-if="serviceTaskForm.executeType === 'http'">
|
||||||
|
<FormItem label="请求方法" key="http-method">
|
||||||
|
<RadioGroup v-model:value="httpTaskForm.requestMethod">
|
||||||
|
<RadioButton value="GET">GET</RadioButton>
|
||||||
|
<RadioButton value="POST">POST</RadioButton>
|
||||||
|
<RadioButton value="PUT">PUT</RadioButton>
|
||||||
|
<RadioButton value="DELETE">DELETE</RadioButton>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="请求地址" key="http-url" name="requestUrl">
|
||||||
|
<Input v-model:value="httpTaskForm.requestUrl" allow-clear />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="请求头" key="http-headers">
|
||||||
|
<div class="flex w-full items-start gap-2">
|
||||||
|
<Textarea
|
||||||
|
v-model:value="httpTaskForm.requestHeaders"
|
||||||
|
:auto-size="{ minRows: 4, maxRows: 8 }"
|
||||||
|
readonly
|
||||||
|
placeholder="点击右侧编辑按钮添加请求头"
|
||||||
|
class="min-w-0 flex-1"
|
||||||
|
/>
|
||||||
|
<Button type="primary" @click="showHeaderEditor = true">
|
||||||
|
<template #icon>
|
||||||
|
<IconifyIcon icon="ep:edit" />
|
||||||
|
</template>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="禁止重定向" key="http-disallow-redirects">
|
||||||
|
<Switch v-model:checked="httpTaskForm.disallowRedirects" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="忽略异常" key="http-ignore-exception">
|
||||||
|
<Switch v-model:checked="httpTaskForm.ignoreException" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="保存返回变量" key="http-save-response">
|
||||||
|
<Switch v-model:checked="httpTaskForm.saveResponseParameters" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="是否瞬间变量" key="http-save-transient">
|
||||||
|
<Switch
|
||||||
|
v-model:checked="httpTaskForm.saveResponseParametersTransient"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="返回变量前缀" key="http-result-variable-prefix">
|
||||||
|
<Input v-model:value="httpTaskForm.resultVariablePrefix" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="格式化返回为JSON" key="http-save-json">
|
||||||
|
<Switch v-model:checked="httpTaskForm.saveResponseVariableAsJson" />
|
||||||
|
</FormItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 请求头编辑器 -->
|
||||||
|
<HttpHeaderEditor
|
||||||
|
v-model="showHeaderEditor"
|
||||||
|
:headers="httpTaskForm.requestHeaders"
|
||||||
|
@save="handleHeadersSave"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -146,7 +146,6 @@
|
|||||||
background: url('./svg/simple-process-bg.svg') 0 0 repeat;
|
background: url('./svg/simple-process-bg.svg') 0 0 repeat;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
transform-origin: 50% 0 0;
|
transform-origin: 50% 0 0;
|
||||||
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
|
||||||
// 节点容器 定义节点宽度
|
// 节点容器 定义节点宽度
|
||||||
.node-container {
|
.node-container {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
|||||||
@@ -259,9 +259,11 @@ async function validateAllSteps() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveLoading = ref<boolean>(false);
|
||||||
/** 保存操作 */
|
/** 保存操作 */
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
try {
|
try {
|
||||||
|
saveLoading.value = true;
|
||||||
// 保存前校验所有步骤的数据
|
// 保存前校验所有步骤的数据
|
||||||
const result = await validateAllSteps();
|
const result = await validateAllSteps();
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@@ -309,9 +311,12 @@ async function handleSave() {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('保存失败:', error);
|
console.error('保存失败:', error);
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 发布加载中状态
|
||||||
|
const deployLoading = ref<boolean>(false);
|
||||||
/** 发布操作 */
|
/** 发布操作 */
|
||||||
async function handleDeploy() {
|
async function handleDeploy() {
|
||||||
try {
|
try {
|
||||||
@@ -319,6 +324,7 @@ async function handleDeploy() {
|
|||||||
if (!formData.value.id) {
|
if (!formData.value.id) {
|
||||||
await confirm('是否确认发布该流程?');
|
await confirm('是否确认发布该流程?');
|
||||||
}
|
}
|
||||||
|
deployLoading.value = true;
|
||||||
// 1.2 校验所有步骤
|
// 1.2 校验所有步骤
|
||||||
await validateAllSteps();
|
await validateAllSteps();
|
||||||
|
|
||||||
@@ -342,6 +348,8 @@ async function handleDeploy() {
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('发布失败:', error);
|
console.error('发布失败:', error);
|
||||||
message.warning(error.message || '发布失败');
|
message.warning(error.message || '发布失败');
|
||||||
|
} finally {
|
||||||
|
deployLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,11 +456,12 @@ onBeforeUnmount(() => {
|
|||||||
<Button
|
<Button
|
||||||
v-if="actionType === 'update'"
|
v-if="actionType === 'update'"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
:loading="deployLoading"
|
||||||
@click="handleDeploy"
|
@click="handleDeploy"
|
||||||
>
|
>
|
||||||
发 布
|
发 布
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="primary" @click="handleSave">
|
<Button type="primary" @click="handleSave" :loading="saveLoading">
|
||||||
<span v-if="actionType === 'definition'">恢 复</span>
|
<span v-if="actionType === 'definition'">恢 复</span>
|
||||||
<span v-else>保 存</span>
|
<span v-else>保 存</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -228,9 +228,10 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
hoverable
|
hoverable
|
||||||
class="definition-item-card w-full cursor-pointer"
|
class="w-full cursor-pointer"
|
||||||
:class="{
|
:class="{
|
||||||
'search-match': searchName.trim().length > 0,
|
'animate-bounce-once !bg-[rgb(63_115_247_/_10%)]':
|
||||||
|
searchName.trim().length > 0,
|
||||||
}"
|
}"
|
||||||
:body-style="{
|
:body-style="{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -241,10 +242,13 @@ onMounted(() => {
|
|||||||
<img
|
<img
|
||||||
v-if="definition.icon"
|
v-if="definition.icon"
|
||||||
:src="definition.icon"
|
:src="definition.icon"
|
||||||
class="flow-icon-img object-contain"
|
class="size-12 rounded object-contain"
|
||||||
alt="流程图标"
|
alt="流程图标"
|
||||||
/>
|
/>
|
||||||
<div v-else class="flow-icon flex-shrink-0">
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex size-12 flex-shrink-0 items-center justify-center rounded bg-primary"
|
||||||
|
>
|
||||||
<span class="text-xs text-white">
|
<span class="text-xs text-white">
|
||||||
{{ definition.name?.slice(0, 2) }}
|
{{ definition.name?.slice(0, 2) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -283,7 +287,6 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
// @jason:看看能不能通过 tailwindcss 简化下
|
|
||||||
@keyframes bounce {
|
@keyframes bounce {
|
||||||
0%,
|
0%,
|
||||||
50% {
|
50% {
|
||||||
@@ -295,30 +298,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.process-definition-container {
|
.animate-bounce-once {
|
||||||
.definition-item-card {
|
animation: bounce 0.5s ease;
|
||||||
.flow-icon-img {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flow-icon {
|
|
||||||
@apply bg-primary;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.search-match {
|
|
||||||
background-color: rgb(63 115 247 / 10%);
|
|
||||||
border: 1px solid var(--primary);
|
|
||||||
animation: bounce 0.5s ease;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ async function submitForm() {
|
|||||||
// 关闭并提示
|
// 关闭并提示
|
||||||
message.success('发起流程成功');
|
message.success('发起流程成功');
|
||||||
await closeCurrentTab();
|
await closeCurrentTab();
|
||||||
await router.push({ name: 'BpmTaskMy' });
|
await router.push({ name: 'BpmProcessInstanceMy' });
|
||||||
} finally {
|
} finally {
|
||||||
processInstanceStartLoading.value = false;
|
processInstanceStartLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,20 +212,27 @@ watch(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
const loading = ref(false);
|
||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await getDetail();
|
try {
|
||||||
// 获得用户列表
|
loading.value = true;
|
||||||
userOptions.value = await getSimpleUserList();
|
await getDetail();
|
||||||
|
// 获得用户列表
|
||||||
|
userOptions.value = await getSimpleUserList();
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page auto-content-height>
|
<Page auto-content-height v-loading="loading">
|
||||||
<Card
|
<Card
|
||||||
|
class="flex h-full flex-col"
|
||||||
:body-style="{
|
:body-style="{
|
||||||
overflowY: 'auto',
|
flex: 1,
|
||||||
|
overflowY: 'hidden',
|
||||||
paddingTop: '12px',
|
paddingTop: '12px',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
@@ -286,24 +293,16 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 流程操作 -->
|
<!-- 流程操作 -->
|
||||||
<div class="process-tabs-container flex flex-1 flex-col">
|
<div class="flex h-full flex-1 flex-col">
|
||||||
<Tabs v-model:active-key="activeTab" class="mt-0 h-full">
|
<Tabs v-model:active-key="activeTab">
|
||||||
<TabPane tab="审批详情" key="form" class="tab-pane-content">
|
<TabPane tab="审批详情" key="form" class="pb-20 pr-3">
|
||||||
<Row :gutter="[48, 24]" class="h-full">
|
<Row :gutter="[48, 24]">
|
||||||
<Col
|
<Col :xs="24" :sm="24" :md="18" :lg="18" :xl="16">
|
||||||
:xs="24"
|
|
||||||
:sm="24"
|
|
||||||
:md="18"
|
|
||||||
:lg="18"
|
|
||||||
:xl="16"
|
|
||||||
class="h-full"
|
|
||||||
>
|
|
||||||
<!-- 流程表单 -->
|
<!-- 流程表单 -->
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
processDefinition?.formType === BpmModelFormType.NORMAL
|
processDefinition?.formType === BpmModelFormType.NORMAL
|
||||||
"
|
"
|
||||||
class="h-full"
|
|
||||||
>
|
>
|
||||||
<form-create
|
<form-create
|
||||||
v-model="detailForm.value"
|
v-model="detailForm.value"
|
||||||
@@ -316,13 +315,12 @@ onMounted(async () => {
|
|||||||
v-else-if="
|
v-else-if="
|
||||||
processDefinition?.formType === BpmModelFormType.CUSTOM
|
processDefinition?.formType === BpmModelFormType.CUSTOM
|
||||||
"
|
"
|
||||||
class="h-full"
|
|
||||||
>
|
>
|
||||||
<BusinessFormComponent :id="processInstance?.businessKey" />
|
<BusinessFormComponent :id="processInstance?.businessKey" />
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :xs="24" :sm="24" :md="6" :lg="6" :xl="8" class="h-full">
|
<Col :xs="24" :sm="24" :md="6" :lg="6" :xl="8">
|
||||||
<div class="mt-4 h-full">
|
<div class="mt-4">
|
||||||
<ProcessInstanceTimeline :activity-nodes="activityNodes" />
|
<ProcessInstanceTimeline :activity-nodes="activityNodes" />
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -331,44 +329,35 @@ onMounted(async () => {
|
|||||||
<TabPane
|
<TabPane
|
||||||
tab="流程图"
|
tab="流程图"
|
||||||
key="diagram"
|
key="diagram"
|
||||||
class="tab-pane-content"
|
class="pb-20 pr-3"
|
||||||
:force-render="true"
|
:force-render="true"
|
||||||
>
|
>
|
||||||
<div class="h-full">
|
<ProcessInstanceSimpleViewer
|
||||||
<ProcessInstanceSimpleViewer
|
v-show="
|
||||||
v-show="
|
processDefinition.modelType &&
|
||||||
processDefinition.modelType &&
|
processDefinition.modelType === BpmModelType.SIMPLE
|
||||||
processDefinition.modelType === BpmModelType.SIMPLE
|
"
|
||||||
"
|
:loading="processInstanceLoading"
|
||||||
:loading="processInstanceLoading"
|
:model-view="processModelView"
|
||||||
:model-view="processModelView"
|
/>
|
||||||
/>
|
<ProcessInstanceBpmnViewer
|
||||||
<ProcessInstanceBpmnViewer
|
v-show="
|
||||||
v-show="
|
processDefinition.modelType &&
|
||||||
processDefinition.modelType &&
|
processDefinition.modelType === BpmModelType.BPMN
|
||||||
processDefinition.modelType === BpmModelType.BPMN
|
"
|
||||||
"
|
:loading="processInstanceLoading"
|
||||||
:loading="processInstanceLoading"
|
:model-view="processModelView"
|
||||||
:model-view="processModelView"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane tab="流转记录" key="record" class="tab-pane-content">
|
<TabPane tab="流转记录" key="record" class="pb-20 pr-3">
|
||||||
<div class="h-full">
|
<BpmProcessInstanceTaskList
|
||||||
<BpmProcessInstanceTaskList
|
ref="taskListRef"
|
||||||
ref="taskListRef"
|
:loading="processInstanceLoading"
|
||||||
:loading="processInstanceLoading"
|
:id="id"
|
||||||
:id="id"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<!-- TODO 待开发 -->
|
<!-- TODO 待开发 -->
|
||||||
<TabPane
|
<TabPane tab="流转评论" key="comment" v-if="false" class="pr-3">
|
||||||
tab="流转评论"
|
|
||||||
key="comment"
|
|
||||||
v-if="false"
|
|
||||||
class="tab-pane-content"
|
|
||||||
>
|
|
||||||
<div class="h-full">待开发</div>
|
<div class="h-full">待开发</div>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -396,35 +385,18 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
// @jason:看看能不能通过 tailwindcss 简化下
|
|
||||||
.ant-tabs-content {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.process-tabs-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ant-tabs) {
|
:deep(.ant-tabs) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ant-tabs-content) {
|
.ant-tabs-content {
|
||||||
flex: 1;
|
height: 100%;
|
||||||
overflow-y: auto;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ant-tabs-tabpane) {
|
:deep(.ant-tabs-tabpane) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
overflow-y: auto;
|
||||||
|
|
||||||
.tab-pane-content {
|
|
||||||
height: calc(100vh - 420px);
|
|
||||||
padding-right: 12px;
|
|
||||||
overflow: hidden auto;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useVbenModal } from '@vben/common-ui';
|
|||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { base64ToFile } from '@vben/utils';
|
import { base64ToFile } from '@vben/utils';
|
||||||
|
|
||||||
import { Button, Space, Tooltip } from 'ant-design-vue';
|
import { Button, Tooltip } from 'ant-design-vue';
|
||||||
import Vue3Signature from 'vue3-signature';
|
import Vue3Signature from 'vue3-signature';
|
||||||
|
|
||||||
import { uploadFile } from '#/api/infra/file';
|
import { uploadFile } from '#/api/infra/file';
|
||||||
@@ -36,30 +36,29 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal title="流程签名" class="w-3/5">
|
<Modal title="流程签名" class="w-3/5">
|
||||||
<div class="mb-2 flex justify-end">
|
<div class="flex h-[50vh] flex-col">
|
||||||
<Space>
|
<div class="mb-2 flex justify-end gap-2">
|
||||||
<Tooltip title="撤销上一步操作">
|
<Tooltip title="撤销上一步操作">
|
||||||
<Button @click="signature?.undo()">
|
<Button @click="signature?.undo()" size="small">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconifyIcon icon="lucide:undo" class="mb-1 size-4" />
|
<IconifyIcon icon="lucide:undo" class="mb-1 size-3" />
|
||||||
</template>
|
</template>
|
||||||
撤销
|
撤销
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="清空画布">
|
<Tooltip title="清空画布">
|
||||||
<Button @click="signature?.clear()">
|
<Button @click="signature?.clear()" size="small">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconifyIcon icon="lucide:trash" class="mb-1 size-4" />
|
<IconifyIcon icon="lucide:trash" class="mb-1 size-3" />
|
||||||
</template>
|
</template>
|
||||||
<span>清除</span>
|
<span>清除</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</div>
|
||||||
|
<Vue3Signature
|
||||||
|
class="h-full flex-1 border border-solid border-gray-300"
|
||||||
|
ref="signature"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Vue3Signature
|
|
||||||
class="mx-auto !h-80 border border-solid border-gray-300"
|
|
||||||
ref="signature"
|
|
||||||
/>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||||||
field: 'approver',
|
field: 'approver',
|
||||||
title: '审批人',
|
title: '审批人',
|
||||||
slots: {
|
slots: {
|
||||||
default: ({ row }: { row: BpmTaskApi.TaskManager }) => {
|
default: ({ row }: { row: BpmTaskApi.Task }) => {
|
||||||
return row.assigneeUser?.nickname || row.ownerUser?.nickname;
|
return row.assigneeUser?.nickname || row.ownerUser?.nickname;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -106,7 +106,7 @@ function handleRefresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 显示表单详情 */
|
/** 显示表单详情 */
|
||||||
async function handleShowFormDetail(row: BpmTaskApi.TaskManager) {
|
async function handleShowFormDetail(row: BpmTaskApi.Task) {
|
||||||
// 设置表单配置和表单字段
|
// 设置表单配置和表单字段
|
||||||
taskForm.value = {
|
taskForm.value = {
|
||||||
rule: [],
|
rule: [],
|
||||||
@@ -141,7 +141,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
keepSource: true,
|
keepSource: true,
|
||||||
showFooter: true,
|
showFooter: true,
|
||||||
border: true,
|
border: true,
|
||||||
height: 'auto',
|
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
ajax: {
|
ajax: {
|
||||||
query: async () => {
|
query: async () => {
|
||||||
@@ -159,7 +158,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
toolbarConfig: {
|
toolbarConfig: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
} as VxeTableGridOptions<BpmTaskApi.TaskManager>,
|
} as VxeTableGridOptions<BpmTaskApi.Task>,
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@@ -168,7 +167,7 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col">
|
<div>
|
||||||
<Grid>
|
<Grid>
|
||||||
<template #slot-reason="{ row }">
|
<template #slot-reason="{ row }">
|
||||||
<div class="flex flex-wrap items-center justify-center">
|
<div class="flex flex-wrap items-center justify-center">
|
||||||
@@ -188,13 +187,13 @@ defineExpose({
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Modal class="w-[800px]">
|
|
||||||
<form-create
|
|
||||||
ref="formRef"
|
|
||||||
v-model="taskForm.value"
|
|
||||||
:option="taskForm.option"
|
|
||||||
:rule="taskForm.rule"
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Modal class="w-3/5">
|
||||||
|
<form-create
|
||||||
|
ref="formRef"
|
||||||
|
v-model="taskForm.value"
|
||||||
|
:option="taskForm.option"
|
||||||
|
:rule="taskForm.rule"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,22 +8,22 @@ import { z } from '#/adapter/form';
|
|||||||
|
|
||||||
export const EVENT_EXECUTION_OPTIONS = [
|
export const EVENT_EXECUTION_OPTIONS = [
|
||||||
{
|
{
|
||||||
label: 'start',
|
label: '开始',
|
||||||
value: 'start',
|
value: 'start',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'end',
|
label: '结束',
|
||||||
value: 'end',
|
value: 'end',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const EVENT_OPTIONS = [
|
export const EVENT_OPTIONS = [
|
||||||
{ label: 'create', value: 'create' },
|
{ label: '创建', value: 'create' },
|
||||||
{ label: 'assignment', value: 'assignment' },
|
{ label: '指派', value: 'assignment' },
|
||||||
{ label: 'complete', value: 'complete' },
|
{ label: '完成', value: 'complete' },
|
||||||
{ label: 'delete', value: 'delete' },
|
{ label: '删除', value: 'delete' },
|
||||||
{ label: 'update', value: 'update' },
|
{ label: '更新', value: 'update' },
|
||||||
{ label: 'timeout', value: 'timeout' },
|
{ label: '超时', value: 'timeout' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 新增/修改的表单 */
|
/** 新增/修改的表单 */
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { useGridColumns, useGridFormSchema } from './data';
|
|||||||
defineOptions({ name: 'BpmDoneTask' });
|
defineOptions({ name: 'BpmDoneTask' });
|
||||||
|
|
||||||
/** 查看历史 */
|
/** 查看历史 */
|
||||||
function handleHistory(row: BpmTaskApi.TaskManager) {
|
function handleHistory(row: BpmTaskApi.Task) {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'BpmProcessInstanceDetail',
|
name: 'BpmProcessInstanceDetail',
|
||||||
query: {
|
query: {
|
||||||
@@ -26,7 +26,7 @@ function handleHistory(row: BpmTaskApi.TaskManager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 撤回任务 */
|
/** 撤回任务 */
|
||||||
async function handleWithdraw(row: BpmTaskApi.TaskManager) {
|
async function handleWithdraw(row: BpmTaskApi.Task) {
|
||||||
const hideLoading = message.loading({
|
const hideLoading = message.loading({
|
||||||
content: '正在撤回中...',
|
content: '正在撤回中...',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
@@ -67,7 +67,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
refresh: true,
|
refresh: true,
|
||||||
search: true,
|
search: true,
|
||||||
},
|
},
|
||||||
} as VxeTableGridOptions<BpmTaskApi.TaskManager>,
|
} as VxeTableGridOptions<BpmTaskApi.Task>,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useGridColumns, useGridFormSchema } from './data';
|
|||||||
defineOptions({ name: 'BpmManagerTask' });
|
defineOptions({ name: 'BpmManagerTask' });
|
||||||
|
|
||||||
/** 查看历史 */
|
/** 查看历史 */
|
||||||
function handleHistory(row: BpmTaskApi.TaskManager) {
|
function handleHistory(row: BpmTaskApi.Task) {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'BpmProcessInstanceDetail',
|
name: 'BpmProcessInstanceDetail',
|
||||||
query: {
|
query: {
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ function handleRowCheckboxChange({
|
|||||||
}: {
|
}: {
|
||||||
records: InfraDataSourceConfigApi.DataSourceConfig[];
|
records: InfraDataSourceConfigApi.DataSourceConfig[];
|
||||||
}) {
|
}) {
|
||||||
checkedIds.value = records.map((item) => item.id!);
|
// 过滤掉id为 0 的主数据源
|
||||||
|
checkedIds.value = records.map((item) => item.id!).filter((id) => id !== 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
@@ -140,6 +141,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
type: 'link',
|
type: 'link',
|
||||||
icon: ACTION_ICON.EDIT,
|
icon: ACTION_ICON.EDIT,
|
||||||
auth: ['infra:data-source-config:update'],
|
auth: ['infra:data-source-config:update'],
|
||||||
|
disabled: row.id === 0,
|
||||||
onClick: handleEdit.bind(null, row),
|
onClick: handleEdit.bind(null, row),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -148,6 +150,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
danger: true,
|
danger: true,
|
||||||
icon: ACTION_ICON.DELETE,
|
icon: ACTION_ICON.DELETE,
|
||||||
auth: ['infra:data-source-config:delete'],
|
auth: ['infra:data-source-config:delete'],
|
||||||
|
disabled: row.id === 0,
|
||||||
popConfirm: {
|
popConfirm: {
|
||||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||||
confirm: handleDelete.bind(null, row),
|
confirm: handleDelete.bind(null, row),
|
||||||
|
|||||||
@@ -229,6 +229,18 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
},
|
},
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'config.region',
|
||||||
|
label: '区域',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请填写区域,一般仅 AWS 需要填写',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['storage'],
|
||||||
|
show: (formValues) => formValues.storage === 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
// 通用
|
// 通用
|
||||||
{
|
{
|
||||||
fieldName: 'config.domain',
|
fieldName: 'config.domain',
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import { deleteDeviceGroup, getDeviceGroupPage } from '#/api/iot/device/group';
|
|||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useGridColumns, useGridFormSchema } from './data';
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
import DeviceGroupForm from './modules/device-group-form.vue';
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTDeviceGroup' });
|
defineOptions({ name: 'IoTDeviceGroup' });
|
||||||
|
|
||||||
const [FormModal, formModalApi] = useVbenModal({
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
connectedComponent: DeviceGroupForm,
|
connectedComponent: Form,
|
||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import type { IotDeviceGroupApi } from '#/api/iot/device/group';
|
import type { IotDeviceGroupApi } from '#/api/iot/device/group';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
@@ -17,17 +17,9 @@ import { $t } from '#/locales';
|
|||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTDeviceGroupForm' });
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
// TODO @haohao:web-antd/src/views/iot/product/category/modules/product-category-form.vue 类似问题
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
success: [];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const formData = ref<IotDeviceGroupApi.DeviceGroup>();
|
const formData = ref<IotDeviceGroupApi.DeviceGroup>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
const modalTitle = computed(() => {
|
|
||||||
return formData.value?.id
|
return formData.value?.id
|
||||||
? $t('ui.actionTitle.edit', ['设备分组'])
|
? $t('ui.actionTitle.edit', ['设备分组'])
|
||||||
: $t('ui.actionTitle.create', ['设备分组']);
|
: $t('ui.actionTitle.create', ['设备分组']);
|
||||||
@@ -40,11 +32,9 @@ const [Form, formApi] = useVbenForm({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
schema: useFormSchema(),
|
schema: useFormSchema(),
|
||||||
showCollapseButton: false,
|
|
||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO @haohao:参考别的 form;1)文件的命名可以简化;2)代码可以在简化下;
|
|
||||||
const [Modal, modalApi] = useVbenModal({
|
const [Modal, modalApi] = useVbenModal({
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
const { valid } = await formApi.validate();
|
const { valid } = await formApi.validate();
|
||||||
@@ -52,17 +42,13 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = (await formApi.getValues()) as IotDeviceGroupApi.DeviceGroup;
|
||||||
try {
|
try {
|
||||||
const values = await formApi.getValues();
|
|
||||||
|
|
||||||
await (formData.value?.id
|
await (formData.value?.id
|
||||||
? updateDeviceGroup({
|
? updateDeviceGroup(data)
|
||||||
...values,
|
: createDeviceGroup(data));
|
||||||
id: formData.value.id,
|
// 关闭并提示
|
||||||
} as IotDeviceGroupApi.DeviceGroup)
|
|
||||||
: createDeviceGroup(values as IotDeviceGroupApi.DeviceGroup));
|
|
||||||
|
|
||||||
await modalApi.close();
|
await modalApi.close();
|
||||||
emit('success');
|
emit('success');
|
||||||
message.success($t('ui.actionMessage.operationSuccess'));
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
@@ -70,28 +56,20 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
modalApi.unlock();
|
modalApi.unlock();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async onOpenChange(isOpen: boolean) {
|
async onOpenChange(isOpen: boolean) {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formData.value = undefined;
|
formData.value = undefined;
|
||||||
await formApi.resetForm();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 加载数据
|
||||||
// 重置表单
|
|
||||||
await formApi.resetForm();
|
|
||||||
|
|
||||||
const data = modalApi.getData<IotDeviceGroupApi.DeviceGroup>();
|
const data = modalApi.getData<IotDeviceGroupApi.DeviceGroup>();
|
||||||
// 如果没有数据或没有 id,表示是新增
|
|
||||||
if (!data || !data.id) {
|
if (!data || !data.id) {
|
||||||
formData.value = undefined;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 编辑模式:加载数据
|
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
try {
|
try {
|
||||||
formData.value = await getDeviceGroup(data.id);
|
formData.value = await getDeviceGroup(data.id);
|
||||||
|
// 设置到 values
|
||||||
await formApi.setValues(formData.value);
|
await formApi.setValues(formData.value);
|
||||||
} finally {
|
} finally {
|
||||||
modalApi.unlock();
|
modalApi.unlock();
|
||||||
@@ -101,7 +79,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal :title="modalTitle" class="w-2/5">
|
<Modal :title="getTitle">
|
||||||
<Form class="mx-4" />
|
<Form class="mx-4" />
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
@@ -14,12 +14,12 @@ import {
|
|||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useGridColumns, useGridFormSchema } from './data';
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
import ProductCategoryForm from './modules/product-category-form.vue';
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTProductCategory' });
|
defineOptions({ name: 'IoTProductCategory' });
|
||||||
|
|
||||||
const [FormModal, formModalApi] = useVbenModal({
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
connectedComponent: ProductCategoryForm,
|
connectedComponent: Form,
|
||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import { $t } from '#/locales';
|
|||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
// TODO @haohao:应该是 form.vue,不用前缀;
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
const formData = ref<IotProductCategoryApi.ProductCategory>();
|
const formData = ref<IotProductCategoryApi.ProductCategory>();
|
||||||
const getTitle = computed(() => {
|
const getTitle = computed(() => {
|
||||||
@@ -40,7 +38,6 @@ const [Form, formApi] = useVbenForm({
|
|||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO @haohao:参考 apps/web-antd/src/views/system/dept/modules/form.vue 简化 useVbenModal 里的代码;
|
|
||||||
const [Modal, modalApi] = useVbenModal({
|
const [Modal, modalApi] = useVbenModal({
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
const { valid } = await formApi.validate();
|
const { valid } = await formApi.validate();
|
||||||
@@ -66,25 +63,19 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
async onOpenChange(isOpen: boolean) {
|
async onOpenChange(isOpen: boolean) {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formData.value = undefined;
|
formData.value = undefined;
|
||||||
formApi.resetForm();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 加载数据
|
||||||
// 重置表单
|
|
||||||
await formApi.resetForm();
|
|
||||||
|
|
||||||
const data = modalApi.getData<IotProductCategoryApi.ProductCategory>();
|
const data = modalApi.getData<IotProductCategoryApi.ProductCategory>();
|
||||||
// 如果没有数据或没有 id,表示是新增
|
|
||||||
if (!data || !data.id) {
|
if (!data || !data.id) {
|
||||||
formData.value = undefined;
|
|
||||||
// 新增模式:设置默认值
|
// 新增模式:设置默认值
|
||||||
|
formData.value = undefined;
|
||||||
await formApi.setValues({
|
await formApi.setValues({
|
||||||
sort: 0,
|
sort: 0,
|
||||||
status: 1,
|
status: 1,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 编辑模式:加载数据
|
// 编辑模式:加载数据
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
try {
|
try {
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { IotProductCategoryApi } from '#/api/iot/product/category';
|
||||||
|
|
||||||
import { h, ref } from 'vue';
|
import { h } from 'vue';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
@@ -10,7 +11,17 @@ import { Button } from 'ant-design-vue';
|
|||||||
|
|
||||||
import { z } from '#/adapter/form';
|
import { z } from '#/adapter/form';
|
||||||
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
|
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
|
||||||
import { getProductPage } from '#/api/iot/product/product';
|
|
||||||
|
/** 产品分类列表缓存 */
|
||||||
|
let categoryList: IotProductCategoryApi.ProductCategory[] = [];
|
||||||
|
|
||||||
|
/** 加载产品分类数据 */
|
||||||
|
async function loadCategoryData() {
|
||||||
|
categoryList = await getSimpleProductCategoryList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化加载分类数据
|
||||||
|
loadCategoryData();
|
||||||
|
|
||||||
/** 新增/修改产品的表单 */
|
/** 新增/修改产品的表单 */
|
||||||
export function useFormSchema(formApi?: any): VbenFormSchema[] {
|
export function useFormSchema(formApi?: any): VbenFormSchema[] {
|
||||||
@@ -134,7 +145,7 @@ export function useFormSchema(formApi?: any): VbenFormSchema[] {
|
|||||||
label: '产品状态',
|
label: '产品状态',
|
||||||
component: 'RadioGroup',
|
component: 'RadioGroup',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
options: getDictOptions(DICT_TYPE.IOT_PRODUCT_STATUS, 'number'),
|
||||||
buttonStyle: 'solid',
|
buttonStyle: 'solid',
|
||||||
optionType: 'button',
|
optionType: 'button',
|
||||||
},
|
},
|
||||||
@@ -283,7 +294,7 @@ export function useBasicFormSchema(formApi?: any): VbenFormSchema[] {
|
|||||||
label: '产品状态',
|
label: '产品状态',
|
||||||
component: 'RadioGroup',
|
component: 'RadioGroup',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
options: getDictOptions(DICT_TYPE.IOT_PRODUCT_STATUS, 'number'),
|
||||||
buttonStyle: 'solid',
|
buttonStyle: 'solid',
|
||||||
optionType: 'button',
|
optionType: 'button',
|
||||||
},
|
},
|
||||||
@@ -308,11 +319,10 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
|
|||||||
{
|
{
|
||||||
fieldName: 'icon',
|
fieldName: 'icon',
|
||||||
label: '产品图标',
|
label: '产品图标',
|
||||||
component: 'IconPicker', // 用这个组件 产品卡片列表 可以根据这个显示 否则就显示默认的
|
component: 'IconPicker',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请选择产品图标',
|
placeholder: '请选择产品图标',
|
||||||
prefix: 'carbon',
|
prefix: 'carbon',
|
||||||
autoFetchApi: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -333,31 +343,6 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 列表的搜索表单 */
|
|
||||||
// TODO @haohao:貌似用不上?
|
|
||||||
export function useGridFormSchema(): VbenFormSchema[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
fieldName: 'name',
|
|
||||||
label: '产品名称',
|
|
||||||
component: 'Input',
|
|
||||||
componentProps: {
|
|
||||||
placeholder: '请输入产品名称',
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'productKey',
|
|
||||||
label: 'ProductKey',
|
|
||||||
component: 'Input',
|
|
||||||
componentProps: {
|
|
||||||
placeholder: '请输入产品标识',
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 列表的字段 */
|
/** 列表的字段 */
|
||||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
return [
|
return [
|
||||||
@@ -375,7 +360,8 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||||||
field: 'categoryId',
|
field: 'categoryId',
|
||||||
title: '品类',
|
title: '品类',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
slots: { default: 'category' },
|
formatter: ({ cellValue }) =>
|
||||||
|
categoryList.find((c) => c.id === cellValue)?.name || '未分类',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'deviceType',
|
field: 'deviceType',
|
||||||
@@ -390,13 +376,17 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||||||
field: 'icon',
|
field: 'icon',
|
||||||
title: '产品图标',
|
title: '产品图标',
|
||||||
width: 100,
|
width: 100,
|
||||||
slots: { default: 'icon' },
|
cellRender: {
|
||||||
|
name: 'CellImage',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'picUrl',
|
field: 'picUrl',
|
||||||
title: '产品图片',
|
title: '产品图片',
|
||||||
width: 100,
|
width: 100,
|
||||||
slots: { default: 'picUrl' },
|
cellRender: {
|
||||||
|
name: 'CellImage',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'createTime',
|
field: 'createTime',
|
||||||
@@ -413,35 +403,6 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询产品列表 */
|
|
||||||
// TODO @haohao:貌似可以删除?
|
|
||||||
export async function queryProductList({ page }: any, searchParams: any) {
|
|
||||||
return await getProductPage({
|
|
||||||
pageNo: page.currentPage,
|
|
||||||
pageSize: page.pageSize,
|
|
||||||
...searchParams,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 创建图片预览状态 */
|
|
||||||
// TODO @haohao:可能不一定用的上;
|
|
||||||
export function useImagePreview() {
|
|
||||||
const previewVisible = ref(false);
|
|
||||||
const previewImage = ref('');
|
|
||||||
|
|
||||||
function handlePreviewImage(url: string) {
|
|
||||||
previewImage.value = url;
|
|
||||||
previewVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
previewVisible,
|
|
||||||
previewImage,
|
|
||||||
handlePreviewImage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @haohao:放到对应的 form 里
|
|
||||||
/** 生成 ProductKey(包含大小写字母和数字) */
|
/** 生成 ProductKey(包含大小写字母和数字) */
|
||||||
export function generateProductKey(): string {
|
export function generateProductKey(): string {
|
||||||
const chars =
|
const chars =
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { IotProductCategoryApi } from '#/api/iot/product/category';
|
||||||
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
@@ -8,7 +10,7 @@ import { Page, useVbenModal } from '@vben/common-ui';
|
|||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
|
||||||
import { Button, Card, Image, Input, message, Space } from 'ant-design-vue';
|
import { Button, Card, Input, message, Space } from 'ant-design-vue';
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
|
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
|
||||||
@@ -19,14 +21,14 @@ import {
|
|||||||
} from '#/api/iot/product/product';
|
} from '#/api/iot/product/product';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useGridColumns, useImagePreview } from './data';
|
import { useGridColumns } from './data';
|
||||||
import ProductCardView from './modules/product-card-view.vue';
|
import ProductCardView from './modules/card-view.vue';
|
||||||
import ProductForm from './modules/product-form.vue';
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTProduct' });
|
defineOptions({ name: 'IoTProduct' });
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const categoryList = ref<any[]>([]); // TODO @haohao:category 类型
|
const categoryList = ref<IotProductCategoryApi.ProductCategory[]>([]);
|
||||||
const viewMode = ref<'card' | 'list'>('card');
|
const viewMode = ref<'card' | 'list'>('card');
|
||||||
const cardViewRef = ref();
|
const cardViewRef = ref();
|
||||||
const searchParams = ref({
|
const searchParams = ref({
|
||||||
@@ -34,10 +36,8 @@ const searchParams = ref({
|
|||||||
productKey: '',
|
productKey: '',
|
||||||
}); // 搜索参数
|
}); // 搜索参数
|
||||||
|
|
||||||
const { previewVisible, previewImage, handlePreviewImage } = useImagePreview();
|
|
||||||
|
|
||||||
const [FormModal, formModalApi] = useVbenModal({
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
connectedComponent: ProductForm,
|
connectedComponent: Form,
|
||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,13 +46,6 @@ async function loadCategories() {
|
|||||||
categoryList.value = await getSimpleProductCategoryList();
|
categoryList.value = await getSimpleProductCategoryList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取分类名称 */
|
|
||||||
function getCategoryNameByValue(categoryId: number) {
|
|
||||||
const category = categoryList.value.find((c: any) => c.id === categoryId);
|
|
||||||
return category?.name || '未分类';
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @haohao:要不要改成 handleRefresh,注释改成“刷新表格”,更加统一。
|
|
||||||
/** 搜索产品 */
|
/** 搜索产品 */
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
if (viewMode.value === 'list') {
|
if (viewMode.value === 'list') {
|
||||||
@@ -128,10 +121,6 @@ async function handleDelete(row: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
// TODO @haohao:这个不用,可以删除掉的
|
|
||||||
formOptions: {
|
|
||||||
schema: [],
|
|
||||||
},
|
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
columns: useGridColumns(),
|
columns: useGridColumns(),
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
@@ -155,7 +144,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
refresh: true,
|
refresh: true,
|
||||||
search: true,
|
search: true,
|
||||||
},
|
},
|
||||||
} as VxeTableGridOptions, // TODO @haohao:这里有个 <> 泛型
|
} as VxeTableGridOptions<IotProductApi.Product>,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
@@ -172,24 +161,22 @@ onMounted(() => {
|
|||||||
<Card :body-style="{ padding: '16px' }" class="mb-4">
|
<Card :body-style="{ padding: '16px' }" class="mb-4">
|
||||||
<!-- 搜索表单 -->
|
<!-- 搜索表单 -->
|
||||||
<div class="mb-3 flex items-center gap-3">
|
<div class="mb-3 flex items-center gap-3">
|
||||||
<!-- TODO @haohao:tindwind -->
|
|
||||||
<Input
|
<Input
|
||||||
v-model:value="searchParams.name"
|
v-model:value="searchParams.name"
|
||||||
placeholder="请输入产品名称"
|
placeholder="请输入产品名称"
|
||||||
allow-clear
|
allow-clear
|
||||||
style="width: 220px"
|
class="w-[220px]"
|
||||||
@press-enter="handleSearch"
|
@press-enter="handleSearch"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<span class="text-gray-400">产品名称</span>
|
<span class="text-gray-400">产品名称</span>
|
||||||
</template>
|
</template>
|
||||||
</Input>
|
</Input>
|
||||||
<!-- TODO @haohao:tindwind -->
|
|
||||||
<Input
|
<Input
|
||||||
v-model:value="searchParams.productKey"
|
v-model:value="searchParams.productKey"
|
||||||
placeholder="请输入产品标识"
|
placeholder="请输入产品标识"
|
||||||
allow-clear
|
allow-clear
|
||||||
style="width: 220px"
|
class="w-[220px]"
|
||||||
@press-enter="handleSearch"
|
@press-enter="handleSearch"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
@@ -207,18 +194,22 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<Space :size="12">
|
<TableAction
|
||||||
<Button type="primary" @click="handleCreate">
|
:actions="[
|
||||||
<!-- TODO @haohao:按钮使用中立的,ACTION_ICON.ADD -->
|
{
|
||||||
<IconifyIcon icon="ant-design:plus-outlined" class="mr-1" />
|
label: '新增产品',
|
||||||
新增产品
|
type: 'primary',
|
||||||
</Button>
|
icon: ACTION_ICON.ADD,
|
||||||
<Button type="primary" @click="handleExport">
|
onClick: handleCreate,
|
||||||
<!-- TODO @haohao:按钮使用中立的,ACTION_ICON.EXPORT -->
|
},
|
||||||
<IconifyIcon icon="ant-design:download-outlined" class="mr-1" />
|
{
|
||||||
导出
|
label: '导出',
|
||||||
</Button>
|
type: 'primary',
|
||||||
</Space>
|
icon: ACTION_ICON.DOWNLOAD,
|
||||||
|
onClick: handleExport,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
<!-- 视图切换 -->
|
<!-- 视图切换 -->
|
||||||
<Space :size="4">
|
<Space :size="4">
|
||||||
<Button
|
<Button
|
||||||
@@ -238,53 +229,18 @@ onMounted(() => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Grid v-show="viewMode === 'list'">
|
<Grid v-show="viewMode === 'list'">
|
||||||
<!-- TODO @haohao:这里貌似可以删除掉 -->
|
|
||||||
<template #toolbar-tools>
|
|
||||||
<div></div>
|
|
||||||
</template>
|
|
||||||
<!-- 产品分类列 -->
|
|
||||||
<!-- TODO @haohao:这里应该可以拿到 data.ts,参考别的模块;类似 apps/web-antd/src/views/ai/image/manager/data.ts 里,里面查询 category ,和自己渲染-->
|
|
||||||
<template #category="{ row }">
|
|
||||||
<span>{{ getCategoryNameByValue(row.categoryId) }}</span>
|
|
||||||
</template>
|
|
||||||
<!-- 产品图标列 -->
|
|
||||||
<!-- TODO @haohao:直接用 Image 组件,就 ok 了呀。在 data.ts 里 -->
|
|
||||||
<template #icon="{ row }">
|
|
||||||
<Button
|
|
||||||
v-if="row.icon"
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
@click="handlePreviewImage(row.icon)"
|
|
||||||
>
|
|
||||||
<IconifyIcon icon="ant-design:eye-outlined" class="text-lg" />
|
|
||||||
</Button>
|
|
||||||
<span v-else class="text-gray-400">-</span>
|
|
||||||
</template>
|
|
||||||
<!-- TODO @haohao:直接用 Image 组件,就 ok 了呀。在 data.ts 里 -->
|
|
||||||
<!-- 产品图片列 -->
|
|
||||||
<template #picUrl="{ row }">
|
|
||||||
<Button
|
|
||||||
v-if="row.picUrl"
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
@click="handlePreviewImage(row.picUrl)"
|
|
||||||
>
|
|
||||||
<IconifyIcon icon="ant-design:eye-outlined" class="text-lg" />
|
|
||||||
</Button>
|
|
||||||
<span v-else class="text-gray-400">-</span>
|
|
||||||
</template>
|
|
||||||
<template #actions="{ row }">
|
<template #actions="{ row }">
|
||||||
<TableAction
|
<TableAction
|
||||||
:actions="[
|
:actions="[
|
||||||
{
|
{
|
||||||
label: '详情',
|
label: '详情',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
onClick: openProductDetail.bind(null, row.id),
|
onClick: openProductDetail.bind(null, row.id!),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '物模型',
|
label: '物模型',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
onClick: openThingModel.bind(null, row.id),
|
onClick: openThingModel.bind(null, row.id!),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t('common.edit'),
|
label: $t('common.edit'),
|
||||||
@@ -320,42 +276,11 @@ onMounted(() => {
|
|||||||
@thing-model="openThingModel"
|
@thing-model="openThingModel"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 图片预览 -->
|
|
||||||
<!-- TODO @haohao:tindwind -->
|
|
||||||
<div style="display: none">
|
|
||||||
<!-- TODO @haohao:是不是通过 Image 直接实现预览 -->
|
|
||||||
<Image.PreviewGroup
|
|
||||||
:preview="{
|
|
||||||
visible: previewVisible,
|
|
||||||
onVisibleChange: (visible) => (previewVisible = visible),
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<Image :src="previewImage" />
|
|
||||||
</Image.PreviewGroup>
|
|
||||||
</div>
|
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/** TODO @haohao:貌似这 2 个 css 没啥用? */
|
|
||||||
:deep(.vxe-toolbar div) {
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏 VxeGrid 自带的搜索表单区域 */
|
/* 隐藏 VxeGrid 自带的搜索表单区域 */
|
||||||
:deep(.vxe-grid--form-wrapper) {
|
:deep(.vxe-grid--form-wrapper) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* 控制图片预览的大小 */
|
|
||||||
.ant-image-preview-img {
|
|
||||||
max-width: 80% !important;
|
|
||||||
max-height: 80% !important;
|
|
||||||
object-fit: contain !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-image-preview-operations {
|
|
||||||
background: rgb(0 0 0 / 70%) !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
Col,
|
Col,
|
||||||
Empty,
|
Empty,
|
||||||
|
Image,
|
||||||
Pagination,
|
Pagination,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
Row,
|
Row,
|
||||||
@@ -19,10 +20,13 @@ import {
|
|||||||
|
|
||||||
import { getProductPage } from '#/api/iot/product/product';
|
import { getProductPage } from '#/api/iot/product/product';
|
||||||
|
|
||||||
// TODO @haohao:应该是 card-view.vue;
|
interface Props {
|
||||||
|
categoryList: any[];
|
||||||
// TODO @haohao:命名不太对;可以简化下;
|
searchParams?: {
|
||||||
defineOptions({ name: 'ProductCardView' });
|
name: string;
|
||||||
|
productKey: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
@@ -34,14 +38,6 @@ const emit = defineEmits<{
|
|||||||
thingModel: [productId: number];
|
thingModel: [productId: number];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
interface Props {
|
|
||||||
categoryList: any[];
|
|
||||||
searchParams?: {
|
|
||||||
name: string;
|
|
||||||
productKey: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const list = ref<any[]>([]);
|
const list = ref<any[]>([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
@@ -50,14 +46,13 @@ const queryParams = ref({
|
|||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO @haohao:注释的优化;
|
/** 获取分类名称 */
|
||||||
// 获取分类名称
|
|
||||||
function getCategoryName(categoryId: number) {
|
function getCategoryName(categoryId: number) {
|
||||||
const category = props.categoryList.find((c: any) => c.id === categoryId);
|
const category = props.categoryList.find((c: any) => c.id === categoryId);
|
||||||
return category?.name || '未分类';
|
return category?.name || '未分类';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取产品列表
|
/** 获取产品列表 */
|
||||||
async function getList() {
|
async function getList() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -72,14 +67,14 @@ async function getList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理页码变化
|
/** 处理页码变化 */
|
||||||
function handlePageChange(page: number, pageSize: number) {
|
function handlePageChange(page: number, pageSize: number) {
|
||||||
queryParams.value.pageNo = page;
|
queryParams.value.pageNo = page;
|
||||||
queryParams.value.pageSize = pageSize;
|
queryParams.value.pageSize = pageSize;
|
||||||
getList();
|
getList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取设备类型颜色
|
/** 获取设备类型颜色 */
|
||||||
function getDeviceTypeColor(deviceType: number) {
|
function getDeviceTypeColor(deviceType: number) {
|
||||||
const colors: Record<number, string> = {
|
const colors: Record<number, string> = {
|
||||||
0: 'blue',
|
0: 'blue',
|
||||||
@@ -114,17 +109,17 @@ onMounted(() => {
|
|||||||
:sm="12"
|
:sm="12"
|
||||||
:md="12"
|
:md="12"
|
||||||
:lg="6"
|
:lg="6"
|
||||||
class="mb-4"
|
|
||||||
>
|
>
|
||||||
<!-- TODO @haohao:卡片之间的上下距离,太宽了。 -->
|
<Card
|
||||||
<Card :body-style="{ padding: '20px' }" class="product-card h-full">
|
:body-style="{ padding: '16px' }"
|
||||||
|
class="product-card h-full rounded-lg transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
||||||
|
>
|
||||||
<!-- 顶部标题区域 -->
|
<!-- 顶部标题区域 -->
|
||||||
<div class="mb-4 flex items-start">
|
<div class="mb-3 flex items-center">
|
||||||
<!-- TODO @haohao:图标太大了;看看是不是参考 vue3 + element-plus 搞小点;然后标题居中。 -->
|
|
||||||
<div class="product-icon">
|
<div class="product-icon">
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
:icon="item.icon || 'ant-design:inbox-outlined'"
|
:icon="item.icon || 'lucide:box'"
|
||||||
class="text-3xl"
|
class="text-xl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 min-w-0 flex-1">
|
<div class="ml-3 min-w-0 flex-1">
|
||||||
@@ -132,7 +127,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<div class="mb-4 flex items-start">
|
<div class="mb-3 flex items-start">
|
||||||
<div class="info-list flex-1">
|
<div class="info-list flex-1">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">产品分类</span>
|
<span class="info-label">产品分类</span>
|
||||||
@@ -156,20 +151,25 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">产品标识</span>
|
<span class="info-label">产品标识</span>
|
||||||
<!-- TODO @haohao:展示 ?有点奇怪,要不小手? -->
|
|
||||||
<Tooltip :title="item.productKey || item.id" placement="top">
|
<Tooltip :title="item.productKey || item.id" placement="top">
|
||||||
<span class="info-value product-key">
|
<span class="info-value product-key cursor-pointer">
|
||||||
{{ item.productKey || item.id }}
|
{{ item.productKey || item.id }}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- TODO @haohao:这里是不是有 image?然后默认 icon -->
|
<!-- 产品图片 -->
|
||||||
<!-- TODO @haohao:高度太高了。建议和左侧(产品分类 + 产品类型 + 产品标识)高度保持一致 -->
|
<div class="product-image">
|
||||||
<div class="product-3d-icon">
|
<Image
|
||||||
|
v-if="item.picUrl"
|
||||||
|
:src="item.picUrl"
|
||||||
|
:preview="true"
|
||||||
|
class="size-full rounded object-cover"
|
||||||
|
/>
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
icon="ant-design:box-plot-outlined"
|
v-else
|
||||||
class="text-2xl"
|
icon="lucide:image"
|
||||||
|
class="text-2xl opacity-50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,8 +180,7 @@ onMounted(() => {
|
|||||||
class="action-btn action-btn-edit"
|
class="action-btn action-btn-edit"
|
||||||
@click="emit('edit', item)"
|
@click="emit('edit', item)"
|
||||||
>
|
>
|
||||||
<!-- TODO @haohao:按钮尽量用中立的按钮,方便迁移 ele; -->
|
<IconifyIcon icon="lucide:edit" class="mr-1" />
|
||||||
<IconifyIcon icon="ant-design:edit-outlined" class="mr-1" />
|
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -189,7 +188,7 @@ onMounted(() => {
|
|||||||
class="action-btn action-btn-detail"
|
class="action-btn action-btn-detail"
|
||||||
@click="emit('detail', item.id)"
|
@click="emit('detail', item.id)"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="ant-design:eye-outlined" class="mr-1" />
|
<IconifyIcon icon="lucide:eye" class="mr-1" />
|
||||||
详情
|
详情
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -197,23 +196,17 @@ onMounted(() => {
|
|||||||
class="action-btn action-btn-model"
|
class="action-btn action-btn-model"
|
||||||
@click="emit('thingModel', item.id)"
|
@click="emit('thingModel', item.id)"
|
||||||
>
|
>
|
||||||
<IconifyIcon
|
<IconifyIcon icon="lucide:git-branch" class="mr-1" />
|
||||||
icon="ant-design:apartment-outlined"
|
|
||||||
class="mr-1"
|
|
||||||
/>
|
|
||||||
物模型
|
物模型
|
||||||
</Button>
|
</Button>
|
||||||
<Tooltip v-if="item.status === 1" title="启用状态的产品不能删除">
|
<Tooltip v-if="item.status === 1" title="已发布的产品不能删除">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
danger
|
danger
|
||||||
disabled
|
disabled
|
||||||
class="action-btn action-btn-delete !w-8"
|
class="action-btn action-btn-delete !w-8"
|
||||||
>
|
>
|
||||||
<IconifyIcon
|
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
||||||
icon="ant-design:delete-outlined"
|
|
||||||
class="text-sm"
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
@@ -226,10 +219,7 @@ onMounted(() => {
|
|||||||
danger
|
danger
|
||||||
class="action-btn action-btn-delete !w-8"
|
class="action-btn action-btn-delete !w-8"
|
||||||
>
|
>
|
||||||
<IconifyIcon
|
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
||||||
icon="ant-design:delete-outlined"
|
|
||||||
class="text-sm"
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,8 +231,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<!-- TODO @haohao:放到最右侧好点 -->
|
<div v-if="list.length > 0" class="flex justify-end">
|
||||||
<div v-if="list.length > 0" class="flex justify-center">
|
|
||||||
<Pagination
|
<Pagination
|
||||||
v-model:current="queryParams.pageNo"
|
v-model:current="queryParams.pageNo"
|
||||||
v-model:page-size="queryParams.pageSize"
|
v-model:page-size="queryParams.pageSize"
|
||||||
@@ -258,18 +247,9 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
/** TODO @haohao:看看哪些可以 tindwind 掉 */
|
|
||||||
.product-card-view {
|
.product-card-view {
|
||||||
.product-card {
|
.product-card {
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 8px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ant-card-body) {
|
:deep(.ant-card-body) {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -283,8 +263,8 @@ onMounted(() => {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 48px;
|
width: 36px;
|
||||||
height: 48px;
|
height: 36px;
|
||||||
color: white;
|
color: white;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -294,9 +274,9 @@ onMounted(() => {
|
|||||||
.product-title {
|
.product-title {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.5;
|
line-height: 36px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +285,7 @@ onMounted(() => {
|
|||||||
.info-item {
|
.info-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
@@ -338,7 +318,6 @@ onMounted(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: help;
|
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,18 +327,17 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3D 图标
|
// 产品图片
|
||||||
.product-3d-icon {
|
.product-image {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100px;
|
width: 80px;
|
||||||
height: 100px;
|
height: 80px;
|
||||||
color: #667eea;
|
color: #667eea;
|
||||||
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
opacity: 0.8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按钮组
|
// 按钮组
|
||||||
@@ -420,10 +398,6 @@ onMounted(() => {
|
|||||||
html.dark {
|
html.dark {
|
||||||
.product-card-view {
|
.product-card-view {
|
||||||
.product-card {
|
.product-card {
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 4px 16px rgb(0 0 0 / 30%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-title {
|
.product-title {
|
||||||
color: rgb(255 255 255 / 85%);
|
color: rgb(255 255 255 / 85%);
|
||||||
}
|
}
|
||||||
@@ -442,7 +416,7 @@ html.dark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-3d-icon {
|
.product-image {
|
||||||
color: #8b9cff;
|
color: #8b9cff;
|
||||||
background: linear-gradient(135deg, #667eea25 0%, #764ba225 100%);
|
background: linear-gradient(135deg, #667eea25 0%, #764ba225 100%);
|
||||||
}
|
}
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
<!-- IoT 产品选择器,使用弹窗展示 -->
|
|
||||||
<script setup lang="ts">
|
|
||||||
// TODO @haohao:这个貌似暂时没看到,在哪里用?
|
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
|
||||||
|
|
||||||
import { reactive, ref } from 'vue';
|
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
|
||||||
|
|
||||||
import { Button, Form, Input, message } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
|
||||||
import { getProductPage } from '#/api/iot/product/product';
|
|
||||||
|
|
||||||
defineOptions({ name: 'IoTProductTableSelect' });
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
multiple: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
success: [product: IotProductApi.Product | IotProductApi.Product[]];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
multiple?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [Modal, modalApi] = useVbenModal({
|
|
||||||
title: '产品选择器',
|
|
||||||
// TODO @haohao:handleConfirm 直接放到这里,不用单独声明
|
|
||||||
onConfirm: handleConfirm,
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedProducts = ref<IotProductApi.Product[]>([]);
|
|
||||||
const selectedRowKeys = ref<number[]>([]);
|
|
||||||
|
|
||||||
// 搜索参数
|
|
||||||
const queryParams = reactive({
|
|
||||||
name: '',
|
|
||||||
productKey: '',
|
|
||||||
});
|
|
||||||
// TODO @haohao:是不是 form 应该也在 Grid 里;
|
|
||||||
|
|
||||||
// 配置表格
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
|
||||||
gridOptions: {
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
type: props.multiple ? 'checkbox' : 'radio',
|
|
||||||
width: 50,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'name',
|
|
||||||
title: '产品名称',
|
|
||||||
minWidth: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'productKey',
|
|
||||||
title: 'ProductKey',
|
|
||||||
minWidth: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'categoryName',
|
|
||||||
title: '品类',
|
|
||||||
minWidth: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'deviceType',
|
|
||||||
title: '设备类型',
|
|
||||||
minWidth: 100,
|
|
||||||
cellRender: {
|
|
||||||
name: 'CellDict',
|
|
||||||
props: { type: 'iot_product_device_type' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'createTime',
|
|
||||||
title: '创建时间',
|
|
||||||
minWidth: 180,
|
|
||||||
formatter: 'formatDateTime',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
checkboxConfig: {
|
|
||||||
reserve: true,
|
|
||||||
highlight: true,
|
|
||||||
},
|
|
||||||
radioConfig: {
|
|
||||||
reserve: true,
|
|
||||||
highlight: true,
|
|
||||||
},
|
|
||||||
proxyConfig: {
|
|
||||||
ajax: {
|
|
||||||
query: async ({ page }: any) => {
|
|
||||||
return await getProductPage({
|
|
||||||
pageNo: page.currentPage,
|
|
||||||
pageSize: page.pageSize,
|
|
||||||
...queryParams,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 打开选择器
|
|
||||||
async function open() {
|
|
||||||
selectedProducts.value = [];
|
|
||||||
selectedRowKeys.value = [];
|
|
||||||
modalApi.open();
|
|
||||||
gridApi.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索
|
|
||||||
function handleSearch() {
|
|
||||||
gridApi.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置搜索
|
|
||||||
function handleReset() {
|
|
||||||
queryParams.name = '';
|
|
||||||
queryParams.productKey = '';
|
|
||||||
gridApi.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认选择
|
|
||||||
async function handleConfirm() {
|
|
||||||
const grid = gridApi.grid;
|
|
||||||
if (!grid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.multiple) {
|
|
||||||
const checkboxRecords = grid.getCheckboxRecords();
|
|
||||||
if (checkboxRecords.length === 0) {
|
|
||||||
message.warning('请至少选择一个产品');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
emit('success', checkboxRecords);
|
|
||||||
} else {
|
|
||||||
const radioRecord = grid.getRadioRecord();
|
|
||||||
if (!radioRecord) {
|
|
||||||
message.warning('请选择一个产品');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
emit('success', radioRecord);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Modal class="!w-[900px]">
|
|
||||||
<div class="mb-4">
|
|
||||||
<Form layout="inline" :model="queryParams">
|
|
||||||
<Form.Item label="产品名称">
|
|
||||||
<Input
|
|
||||||
v-model:value="queryParams.name"
|
|
||||||
placeholder="请输入产品名称"
|
|
||||||
allow-clear
|
|
||||||
class="!w-[200px]"
|
|
||||||
@press-enter="handleSearch"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="ProductKey">
|
|
||||||
<Input
|
|
||||||
v-model:value="queryParams.productKey"
|
|
||||||
placeholder="请输入产品标识"
|
|
||||||
allow-clear
|
|
||||||
class="!w-[200px]"
|
|
||||||
@press-enter="handleSearch"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" @click="handleSearch">
|
|
||||||
<template #icon>
|
|
||||||
<IconifyIcon icon="ant-design:search-outlined" />
|
|
||||||
</template>
|
|
||||||
搜索
|
|
||||||
</Button>
|
|
||||||
<Button class="ml-2" @click="handleReset">
|
|
||||||
<template #icon>
|
|
||||||
<IconifyIcon icon="ant-design:reload-outlined" />
|
|
||||||
</template>
|
|
||||||
重置
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Grid />
|
|
||||||
</Modal>
|
|
||||||
</template>
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
// TODO @haohao:detail 挪到 yudao-ui-admin-vben-v5/apps/web-antd/src/views/iot/product/product/detail 下。独立一个,不放在 modules 里。
|
|
||||||
import { onMounted, provide, ref } from 'vue';
|
import { onMounted, provide, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
@@ -13,8 +12,8 @@ import { getDeviceCount } from '#/api/iot/device/device';
|
|||||||
import { getProduct } from '#/api/iot/product/product';
|
import { getProduct } from '#/api/iot/product/product';
|
||||||
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
|
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
|
||||||
|
|
||||||
import ProductDetailsHeader from './product-details-header.vue';
|
import ProductDetailsHeader from './modules/header.vue';
|
||||||
import ProductDetailsInfo from './product-details-info.vue';
|
import ProductDetailsInfo from './modules/info.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTProductDetail' });
|
defineOptions({ name: 'IoTProductDetail' });
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
// TODO @haohao:放到 detail/modules 里。然后名字就是 header.vue
|
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Button, Card, Descriptions, message } from 'ant-design-vue';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
import { updateProductStatus } from '#/api/iot/product/product';
|
import { Button, Card, Descriptions, message, Modal } from 'ant-design-vue';
|
||||||
|
|
||||||
import ProductForm from '../product-form.vue';
|
import {
|
||||||
|
ProductStatusEnum,
|
||||||
|
updateProductStatus,
|
||||||
|
} from '#/api/iot/product/product';
|
||||||
|
|
||||||
|
import Form from '../../form.vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
product: IotProductApi.Product;
|
product: IotProductApi.Product;
|
||||||
@@ -25,7 +28,11 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const formRef = ref();
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
/** 复制到剪贴板 */
|
/** 复制到剪贴板 */
|
||||||
async function copyToClipboard(text: string) {
|
async function copyToClipboard(text: string) {
|
||||||
@@ -46,59 +53,63 @@ function goToDeviceList(productId: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 打开编辑表单 */
|
/** 打开编辑表单 */
|
||||||
function openForm(type: string, id?: number) {
|
function openEditForm(row: IotProductApi.Product) {
|
||||||
formRef.value?.open(type, id);
|
formModalApi.setData(row).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发布产品 */
|
/** 发布产品 */
|
||||||
async function confirmPublish(id: number) {
|
function handlePublish(product: IotProductApi.Product) {
|
||||||
// TODO @haohao:最好类似;async function handleDeleteBatch() { 的做法:1)有个 confirm;2)有个 loading
|
Modal.confirm({
|
||||||
try {
|
title: '确认发布',
|
||||||
await updateProductStatus(id, 1); // TODO @好好】:1 和 0,最好用枚举;
|
content: `确认要发布产品「${product.name}」吗?`,
|
||||||
message.success('发布成功');
|
async onOk() {
|
||||||
emit('refresh');
|
await updateProductStatus(product.id!, ProductStatusEnum.PUBLISHED);
|
||||||
} catch {
|
message.success('发布成功');
|
||||||
message.error('发布失败');
|
emit('refresh');
|
||||||
}
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 撤销发布 */
|
/** 撤销发布 */
|
||||||
async function confirmUnpublish(id: number) {
|
function handleUnpublish(product: IotProductApi.Product) {
|
||||||
// TODO @haohao:最好类似;async function handleDeleteBatch() { 的做法:1)有个 confirm;2)有个 loading
|
Modal.confirm({
|
||||||
try {
|
title: '确认撤销发布',
|
||||||
await updateProductStatus(id, 0);
|
content: `确认要撤销发布产品「${product.name}」吗?`,
|
||||||
message.success('撤销发布成功');
|
async onOk() {
|
||||||
emit('refresh');
|
await updateProductStatus(product.id!, ProductStatusEnum.UNPUBLISHED);
|
||||||
} catch {
|
message.success('撤销发布成功');
|
||||||
message.error('撤销发布失败');
|
emit('refresh');
|
||||||
}
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
|
<FormModal @success="emit('refresh')" />
|
||||||
|
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">{{ product.name }}</h2>
|
<h2 class="text-xl font-bold">{{ product.name }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-x-2">
|
<div class="space-x-2">
|
||||||
<Button
|
<Button
|
||||||
:disabled="product.status === 1"
|
:disabled="product.status === ProductStatusEnum.PUBLISHED"
|
||||||
@click="openForm('update', product.id)"
|
@click="openEditForm(product)"
|
||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-if="product.status === 0"
|
v-if="product.status === ProductStatusEnum.UNPUBLISHED"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="confirmPublish(product.id!)"
|
@click="handlePublish(product)"
|
||||||
>
|
>
|
||||||
发布
|
发布
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-if="product.status === 1"
|
v-if="product.status === ProductStatusEnum.PUBLISHED"
|
||||||
danger
|
danger
|
||||||
@click="confirmUnpublish(product.id!)"
|
@click="handleUnpublish(product)"
|
||||||
>
|
>
|
||||||
撤销发布
|
撤销发布
|
||||||
</Button>
|
</Button>
|
||||||
@@ -127,9 +138,5 @@ async function confirmUnpublish(id: number) {
|
|||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- 表单弹窗 -->
|
|
||||||
<!-- TODO @haohao:弹不出来;另外,应该用 index.vue 里,Form 的声明方式哈。 -->
|
|
||||||
<ProductForm ref="formRef" @success="emit('refresh')" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
// TODO @haohao:放到 detail/modules 里。然后名字就是 info.vue
|
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
@@ -24,7 +23,6 @@ function formatDate(date?: Date | string) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card title="产品信息">
|
<Card title="产品信息">
|
||||||
<!-- TODO @haohao:看看是不是用 description 组件 -->
|
|
||||||
<Descriptions bordered :column="3" size="small">
|
<Descriptions bordered :column="3" size="small">
|
||||||
<Descriptions.Item label="产品名称">
|
<Descriptions.Item label="产品名称">
|
||||||
{{ product.name }}
|
{{ product.name }}
|
||||||
@@ -48,7 +46,7 @@ function formatDate(date?: Date | string) {
|
|||||||
{{ product.codecType || '-' }}
|
{{ product.codecType || '-' }}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="产品状态">
|
<Descriptions.Item label="产品状态">
|
||||||
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" />
|
<DictTag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" />
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item
|
<Descriptions.Item
|
||||||
v-if="
|
v-if="
|
||||||
142
apps/web-antd/src/views/iot/product/product/modules/form.vue
Normal file
142
apps/web-antd/src/views/iot/product/product/modules/form.vue
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Collapse, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import {
|
||||||
|
createProduct,
|
||||||
|
getProduct,
|
||||||
|
updateProduct,
|
||||||
|
} from '#/api/iot/product/product';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import {
|
||||||
|
generateProductKey,
|
||||||
|
useAdvancedFormSchema,
|
||||||
|
useBasicFormSchema,
|
||||||
|
} from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<IotProductApi.Product>();
|
||||||
|
const activeKey = ref<string[]>([]);
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['产品'])
|
||||||
|
: $t('ui.actionTitle.create', ['产品']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: { class: 'w-full' },
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: [],
|
||||||
|
showDefaultActions: false,
|
||||||
|
wrapperClass: 'grid-cols-2',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [AdvancedForm, advancedFormApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: { class: 'w-full' },
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useAdvancedFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
wrapperClass: 'grid-cols-2',
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 基础表单需要 formApi 引用,所以通过 setState 设置 schema */
|
||||||
|
formApi.setState({ schema: useBasicFormSchema(formApi) });
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
/** 提交表单 */
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 合并两个表单的值
|
||||||
|
const basicValues = await formApi.getValues();
|
||||||
|
const advancedValues = activeKey.value.includes('advanced')
|
||||||
|
? await advancedFormApi.getValues()
|
||||||
|
: formData.value?.id
|
||||||
|
? {
|
||||||
|
icon: formData.value.icon,
|
||||||
|
picUrl: formData.value.picUrl,
|
||||||
|
description: formData.value.description,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
const data = {
|
||||||
|
...basicValues,
|
||||||
|
...advancedValues,
|
||||||
|
} as IotProductApi.Product;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id ? updateProduct(data) : createProduct(data));
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** 弹窗打开/关闭 */
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
activeKey.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<IotProductApi.Product>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
// 新增:设置默认值
|
||||||
|
await formApi.setValues({
|
||||||
|
productKey: generateProductKey(),
|
||||||
|
status: 0,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 编辑:加载数据
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getProduct(data.id);
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
// 设置高级表单(不等待)
|
||||||
|
advancedFormApi.setValues({
|
||||||
|
icon: formData.value.icon,
|
||||||
|
picUrl: formData.value.picUrl,
|
||||||
|
description: formData.value.description,
|
||||||
|
});
|
||||||
|
// 有高级字段时自动展开
|
||||||
|
if (
|
||||||
|
formData.value.icon ||
|
||||||
|
formData.value.picUrl ||
|
||||||
|
formData.value.description
|
||||||
|
) {
|
||||||
|
activeKey.value = ['advanced'];
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle" class="w-2/5">
|
||||||
|
<div class="mx-4">
|
||||||
|
<Form />
|
||||||
|
<Collapse v-model:active-key="activeKey" class="mt-4">
|
||||||
|
<Collapse.Panel key="advanced" header="更多设置">
|
||||||
|
<AdvancedForm />
|
||||||
|
</Collapse.Panel>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
|
|
||||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
|
||||||
|
|
||||||
import { Collapse, message } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import {
|
|
||||||
createProduct,
|
|
||||||
getProduct,
|
|
||||||
updateProduct,
|
|
||||||
} from '#/api/iot/product/product';
|
|
||||||
import { $t } from '#/locales';
|
|
||||||
|
|
||||||
import {
|
|
||||||
generateProductKey,
|
|
||||||
useAdvancedFormSchema,
|
|
||||||
useBasicFormSchema,
|
|
||||||
} from '../data';
|
|
||||||
|
|
||||||
// TODO @haohao:应该是 form.vue;
|
|
||||||
|
|
||||||
defineOptions({ name: 'IoTProductForm' });
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
|
||||||
|
|
||||||
const CollapsePanel = Collapse.Panel;
|
|
||||||
|
|
||||||
const formData = ref<any>();
|
|
||||||
const getTitle = computed(() => {
|
|
||||||
return formData.value?.id ? '编辑产品' : '新增产品';
|
|
||||||
});
|
|
||||||
const activeKey = ref<string[]>([]); // 折叠面板的激活 key,默认不展开
|
|
||||||
|
|
||||||
// TODO @haohao:每一行一个;
|
|
||||||
const [Form, formApi] = useVbenForm({
|
|
||||||
commonConfig: {
|
|
||||||
componentProps: {
|
|
||||||
class: 'w-full',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wrapperClass: 'grid-cols-2',
|
|
||||||
layout: 'horizontal',
|
|
||||||
schema: [],
|
|
||||||
showDefaultActions: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO @haohao:每一行一个;
|
|
||||||
const [AdvancedForm, advancedFormApi] = useVbenForm({
|
|
||||||
commonConfig: {
|
|
||||||
componentProps: {
|
|
||||||
class: 'w-full',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wrapperClass: 'grid-cols-2',
|
|
||||||
layout: 'horizontal',
|
|
||||||
schema: [],
|
|
||||||
showDefaultActions: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO @haohao:看看是不是可以参考别的 form 模块,优化表单这块的逻辑;从 61 到 156 行。体感有点冗余、以及代码风格,不够统一;
|
|
||||||
formApi.setState({ schema: useBasicFormSchema(formApi) });
|
|
||||||
advancedFormApi.setState({ schema: useAdvancedFormSchema() });
|
|
||||||
|
|
||||||
const [Modal, modalApi] = useVbenModal({
|
|
||||||
async onConfirm() {
|
|
||||||
// 只验证基础表单
|
|
||||||
const { valid: basicValid } = await formApi.validate();
|
|
||||||
if (!basicValid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
modalApi.lock();
|
|
||||||
try {
|
|
||||||
// 提交表单 - 合并两个表单的值
|
|
||||||
const basicValues = await formApi.getValues();
|
|
||||||
|
|
||||||
// 如果折叠面板展开,则获取高级表单的值,否则保留原有值(编辑时)或使用空值(新增时)
|
|
||||||
let advancedValues: any = {};
|
|
||||||
if (activeKey.value.includes('advanced')) {
|
|
||||||
advancedValues = await advancedFormApi.getValues();
|
|
||||||
} else if (formData.value?.id) {
|
|
||||||
// 编辑时保留原有的高级字段值
|
|
||||||
advancedValues = {
|
|
||||||
icon: formData.value.icon,
|
|
||||||
picUrl: formData.value.picUrl,
|
|
||||||
description: formData.value.description,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const values = {
|
|
||||||
...basicValues,
|
|
||||||
...advancedValues,
|
|
||||||
} as IotProductApi.Product;
|
|
||||||
const data = formData.value?.id
|
|
||||||
? { ...values, id: formData.value.id }
|
|
||||||
: values;
|
|
||||||
|
|
||||||
await (formData.value?.id ? updateProduct(data) : createProduct(data));
|
|
||||||
// 关闭并提示
|
|
||||||
await modalApi.close();
|
|
||||||
emit('success');
|
|
||||||
message.success($t('ui.actionMessage.operationSuccess'));
|
|
||||||
} finally {
|
|
||||||
modalApi.unlock();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async onOpenChange(isOpen: boolean) {
|
|
||||||
if (!isOpen) {
|
|
||||||
formData.value = undefined;
|
|
||||||
// 重置折叠面板状态
|
|
||||||
activeKey.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 加载数据
|
|
||||||
const data = modalApi.getData<any>();
|
|
||||||
if (!data || !data.id) {
|
|
||||||
// 设置默认值
|
|
||||||
await formApi.setValues({
|
|
||||||
productKey: generateProductKey(), // 自动生成 ProductKey
|
|
||||||
// deviceType: 0, // 默认直连设备
|
|
||||||
// codecType: 'Alink', // 默认 Alink
|
|
||||||
// dataFormat: 1, // 默认 JSON
|
|
||||||
// validateType: 1, // 默认设备密钥
|
|
||||||
status: 0, // 默认启用
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
formData.value = await getProduct(data.id);
|
|
||||||
// 设置基础表单
|
|
||||||
await formApi.setValues(formData.value);
|
|
||||||
|
|
||||||
// 先设置高级表单的值(不等待)
|
|
||||||
advancedFormApi.setValues({
|
|
||||||
icon: formData.value.icon,
|
|
||||||
picUrl: formData.value.picUrl,
|
|
||||||
description: formData.value.description,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 如果有图标、图片或描述,自动展开折叠面板以便显示
|
|
||||||
if (
|
|
||||||
formData.value.icon ||
|
|
||||||
formData.value.picUrl ||
|
|
||||||
formData.value.description
|
|
||||||
) {
|
|
||||||
activeKey.value = ['advanced'];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载产品数据失败:', error);
|
|
||||||
message.error('加载产品数据失败,请重试');
|
|
||||||
} finally {
|
|
||||||
modalApi.unlock();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Modal :title="getTitle" class="w-2/5">
|
|
||||||
<div class="mx-4">
|
|
||||||
<Form />
|
|
||||||
<Collapse v-model:active-key="activeKey" class="mt-4">
|
|
||||||
<CollapsePanel key="advanced" header="更多设置">
|
|
||||||
<AdvancedForm />
|
|
||||||
</CollapsePanel>
|
|
||||||
</Collapse>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
|
||||||
@@ -2,8 +2,8 @@ import type { Ref } from 'vue';
|
|||||||
|
|
||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeGridProps, VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeGridProps, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
|
||||||
import type { MallCategoryApi } from '#/api/mall/product/category';
|
import type { MallCategoryApi } from '#/api/mall/product/category';
|
||||||
|
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||||
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,13 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
placeholder: '请输入最大砍价金额',
|
placeholder: '请输入最大砍价金额',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO @puhui999:这里交互不太对,可以对比下 element-plus 版本呢
|
||||||
|
{
|
||||||
|
fieldName: 'spuId',
|
||||||
|
label: '砍价商品',
|
||||||
|
component: 'Input',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
updateBargainActivity,
|
updateBargainActivity,
|
||||||
} from '#/api/mall/promotion/bargain/bargainActivity';
|
} from '#/api/mall/promotion/bargain/bargainActivity';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
import { SpuShowcase } from '#/views/mall/product/spu/components';
|
||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ defineOptions({ name: 'PromotionBargainActivityForm' });
|
|||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
const formData = ref<MallBargainActivityApi.BargainActivity>();
|
const formData = ref<Partial<MallBargainActivityApi.BargainActivity>>({});
|
||||||
const getTitle = computed(() => {
|
const getTitle = computed(() => {
|
||||||
return formData.value?.id
|
return formData.value?.id
|
||||||
? $t('ui.actionTitle.edit', ['砍价活动'])
|
? $t('ui.actionTitle.edit', ['砍价活动'])
|
||||||
@@ -49,8 +50,11 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
}
|
}
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
// 提交表单
|
// 提交表单
|
||||||
const data =
|
const values = await formApi.getValues();
|
||||||
(await formApi.getValues()) as MallBargainActivityApi.BargainActivity;
|
const data = {
|
||||||
|
...values,
|
||||||
|
spuId: formData.value.spuId,
|
||||||
|
} as MallBargainActivityApi.BargainActivity;
|
||||||
try {
|
try {
|
||||||
await (formData.value?.id
|
await (formData.value?.id
|
||||||
? updateBargainActivity(data)
|
? updateBargainActivity(data)
|
||||||
@@ -65,7 +69,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
},
|
},
|
||||||
async onOpenChange(isOpen: boolean) {
|
async onOpenChange(isOpen: boolean) {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formData.value = undefined;
|
formData.value = {};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 加载数据
|
// 加载数据
|
||||||
@@ -87,6 +91,11 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal class="w-2/5" :title="getTitle">
|
<Modal class="w-2/5" :title="getTitle">
|
||||||
<Form class="mx-4" />
|
<Form class="mx-4">
|
||||||
|
<!-- 自定义插槽:商品选择 -->
|
||||||
|
<template #spuId>
|
||||||
|
<SpuShowcase v-model="formData.spuId" :limit="1" />
|
||||||
|
</template>
|
||||||
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -105,11 +105,12 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO @puhui999:这里交互不太对,可以对比下 element-plus 版本呢
|
||||||
{
|
{
|
||||||
// TODO
|
|
||||||
fieldName: 'spuId',
|
fieldName: 'spuId',
|
||||||
label: '拼团商品',
|
label: '拼团商品',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
rules: 'required',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,16 @@ import {
|
|||||||
updateCombinationActivity,
|
updateCombinationActivity,
|
||||||
} from '#/api/mall/promotion/combination/combinationActivity';
|
} from '#/api/mall/promotion/combination/combinationActivity';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
import { SpuShowcase } from '#/views/mall/product/spu/components';
|
||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
defineOptions({ name: 'CombinationActivityForm' });
|
defineOptions({ name: 'CombinationActivityForm' });
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
const formData = ref<MallCombinationActivityApi.CombinationActivity>();
|
const formData = ref<Partial<MallCombinationActivityApi.CombinationActivity>>(
|
||||||
|
{},
|
||||||
|
);
|
||||||
const getTitle = computed(() => {
|
const getTitle = computed(() => {
|
||||||
return formData.value?.id
|
return formData.value?.id
|
||||||
? $t('ui.actionTitle.edit', ['拼团活动'])
|
? $t('ui.actionTitle.edit', ['拼团活动'])
|
||||||
@@ -47,8 +50,11 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
}
|
}
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
// 提交表单
|
// 提交表单
|
||||||
const data =
|
const values = await formApi.getValues();
|
||||||
(await formApi.getValues()) as MallCombinationActivityApi.CombinationActivity;
|
const data = {
|
||||||
|
...values,
|
||||||
|
spuId: formData.value.spuId,
|
||||||
|
} as MallCombinationActivityApi.CombinationActivity;
|
||||||
try {
|
try {
|
||||||
await (formData.value?.id
|
await (formData.value?.id
|
||||||
? updateCombinationActivity(data)
|
? updateCombinationActivity(data)
|
||||||
@@ -63,7 +69,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
},
|
},
|
||||||
async onOpenChange(isOpen: boolean) {
|
async onOpenChange(isOpen: boolean) {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formData.value = undefined;
|
formData.value = {};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 加载数据
|
// 加载数据
|
||||||
@@ -86,6 +92,11 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal class="w-3/5" :title="getTitle">
|
<Modal class="w-3/5" :title="getTitle">
|
||||||
<Form />
|
<Form>
|
||||||
|
<!-- 自定义插槽:商品选择 -->
|
||||||
|
<template #spuId>
|
||||||
|
<SpuShowcase v-model="formData.spuId" :limit="1" />
|
||||||
|
</template>
|
||||||
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ function emitActivityChange() {
|
|||||||
>
|
>
|
||||||
<Tooltip :title="activity.name">
|
<Tooltip :title="activity.name">
|
||||||
<div class="relative h-full w-full">
|
<div class="relative h-full w-full">
|
||||||
<!-- TODO @芋艿 -->
|
|
||||||
<Image
|
<Image
|
||||||
:src="activity.picUrl"
|
:src="activity.picUrl"
|
||||||
class="h-full w-full rounded-lg object-cover"
|
class="h-full w-full rounded-lg object-cover"
|
||||||
|
|||||||
@@ -133,9 +133,8 @@ function handleSliderChange(prop: string) {
|
|||||||
</TabPane>
|
</TabPane>
|
||||||
|
|
||||||
<!-- 每个组件的通用内容 -->
|
<!-- 每个组件的通用内容 -->
|
||||||
<!-- TODO @xingyu:这里的样式,貌似没 ele 版本的好看。 -->
|
|
||||||
<TabPane tab="样式" key="style" force-render>
|
<TabPane tab="样式" key="style" force-render>
|
||||||
<p class="text-lg font-bold">组件样式:</p>
|
<div class="mb-2 bg-gray-100 p-2 text-sm">组件样式:</div>
|
||||||
<div class="flex flex-col gap-2 rounded-md p-4 shadow-lg">
|
<div class="flex flex-col gap-2 rounded-md p-4 shadow-lg">
|
||||||
<Form :model="formData">
|
<Form :model="formData">
|
||||||
<FormItem
|
<FormItem
|
||||||
@@ -181,7 +180,7 @@ function handleSliderChange(prop: string) {
|
|||||||
class="mb-0 w-full"
|
class="mb-0 w-full"
|
||||||
>
|
>
|
||||||
<Row>
|
<Row>
|
||||||
<Col :span="11">
|
<Col :span="19">
|
||||||
<Slider
|
<Slider
|
||||||
v-model:value="
|
v-model:value="
|
||||||
formData[dataRef.prop as keyof ComponentStyle]
|
formData[dataRef.prop as keyof ComponentStyle]
|
||||||
@@ -192,8 +191,9 @@ function handleSliderChange(prop: string) {
|
|||||||
class="mr-4"
|
class="mr-4"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="2">
|
<Col :span="4">
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
class="w-[50px]"
|
||||||
:max="100"
|
:max="100"
|
||||||
:min="0"
|
:min="0"
|
||||||
v-model:value="
|
v-model:value="
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const handleDeleteComponent = () => {
|
|||||||
<component :is="component.id" :property="component.property" />
|
<component :is="component.id" :property="component.property" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="component-wrap absolute -bottom-1 -left-0.5 -right-0.5 -top-1 block h-full w-full"
|
class="component-wrap absolute -bottom-1 -left-0.5 -right-0.5 block h-full w-full"
|
||||||
>
|
>
|
||||||
<!-- 左侧:组件名(悬浮的小贴条) -->
|
<!-- 左侧:组件名(悬浮的小贴条) -->
|
||||||
<div class="component-name" v-if="component.name">
|
<div class="component-name" v-if="component.name">
|
||||||
@@ -109,8 +109,6 @@ const handleDeleteComponent = () => {
|
|||||||
class="component-toolbar"
|
class="component-toolbar"
|
||||||
v-if="showToolbar && component.name && active"
|
v-if="showToolbar && component.name && active"
|
||||||
>
|
>
|
||||||
<!-- TODO @xingyu:按钮少的时候,会存在遮住的情况; -->
|
|
||||||
<!-- TODO @xingyu:貌似中间的选中框框,没全部框柱。上面多了点,下面少了点。 -->
|
|
||||||
<VerticalButtonGroup size="small">
|
<VerticalButtonGroup size="small">
|
||||||
<Button
|
<Button
|
||||||
:disabled="!canMoveUp"
|
:disabled="!canMoveUp"
|
||||||
@@ -171,7 +169,6 @@ const handleDeleteComponent = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
$active-border-width: 2px;
|
$active-border-width: 2px;
|
||||||
$hover-border-width: 1px;
|
$hover-border-width: 1px;
|
||||||
|
|||||||
@@ -100,5 +100,4 @@ function handleCloneComponent(component: DiyComponent<any>) {
|
|||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</div>
|
</div>
|
||||||
<!-- TODO @xingyu:ele 里面有一些 style,看看是不是都迁移完了;特别是 drag-area 是全局样式; -->
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</Carousel>
|
</Carousel>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
// Ant Design Vue Carousel 样式调整
|
// Ant Design Vue Carousel 样式调整
|
||||||
:deep(.ant-carousel .ant-carousel-dots) {
|
:deep(.ant-carousel .ant-carousel-dots) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ defineProps<{ property: UserCardProperty }>();
|
|||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="flex items-center justify-between px-4 py-6">
|
<div class="flex items-center justify-between px-4 py-6">
|
||||||
<div class="flex flex-1 items-center gap-4">
|
<div class="flex flex-1 items-center gap-4">
|
||||||
<Avatar :size="60">
|
<Avatar :size="60" class="flex items-center">
|
||||||
<IconifyIcon icon="ep:avatar" :size="60" />
|
<IconifyIcon icon="ep:avatar" :size="60" />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<span class="text-[18px] font-bold">芋道源码</span>
|
<span class="text-[18px] font-bold">芋道源码</span>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emits = defineEmits(['reset', 'save', 'update:modelValue']); // 工具栏操作
|
const emits = defineEmits(['reset', 'save', 'update:modelValue']); // 工具栏操作
|
||||||
|
|
||||||
// TODO @xingyu:要不要加这个?
|
// TODO @xingyu:要不要加这个?ele 里是有这个的。
|
||||||
// const qrcode = useQRCode(props.previewUrl, {
|
// const qrcode = useQRCode(props.previewUrl, {
|
||||||
// errorCorrectionLevel: 'H',
|
// errorCorrectionLevel: 'H',
|
||||||
// margin: 4,
|
// margin: 4,
|
||||||
@@ -175,8 +175,7 @@ function handleComponentSelected(
|
|||||||
index: number = -1,
|
index: number = -1,
|
||||||
) {
|
) {
|
||||||
// 使用深拷贝避免响应式追踪循环警告
|
// 使用深拷贝避免响应式追踪循环警告
|
||||||
// TODO @xingyu:这个是必须的么?ele 没有哈。
|
selectedComponent.value = component;
|
||||||
selectedComponent.value = cloneDeep(component);
|
|
||||||
selectedComponentIndex.value = index;
|
selectedComponentIndex.value = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +343,7 @@ onMounted(() => {
|
|||||||
<!-- 中心:设计区域(ComponentContainer) -->
|
<!-- 中心:设计区域(ComponentContainer) -->
|
||||||
<Col :span="12">
|
<Col :span="12">
|
||||||
<div
|
<div
|
||||||
class="relative flex max-h-[calc(80vh)] w-full flex-1 flex-col justify-center overflow-y-auto"
|
class="editor-center relative flex max-h-[calc(80vh)] w-full flex-1 flex-col overflow-y-auto"
|
||||||
@click="handlePageSelected"
|
@click="handlePageSelected"
|
||||||
>
|
>
|
||||||
<!-- 手机顶部 -->
|
<!-- 手机顶部 -->
|
||||||
@@ -378,20 +377,20 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<!-- 手机页面编辑区域 -->
|
<!-- 手机页面编辑区域 -->
|
||||||
<div
|
<div
|
||||||
class="min-h-full w-full"
|
class="mx-auto min-h-full w-96 bg-no-repeat"
|
||||||
:style="{
|
:style="{
|
||||||
// backgroundColor: pageConfigComponent.property.backgroundColor,
|
// backgroundColor: pageConfigComponent.property.backgroundColor,
|
||||||
backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`,
|
backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="relative mx-auto my-0 min-h-full w-96 items-center justify-center bg-auto bg-no-repeat"
|
class="relative my-0 min-h-full w-full items-center justify-center bg-auto bg-no-repeat"
|
||||||
>
|
>
|
||||||
<draggable
|
<draggable
|
||||||
v-model="pageComponents"
|
v-model="pageComponents"
|
||||||
:animation="200"
|
:animation="200"
|
||||||
:force-fallback="false"
|
:force-fallback="false"
|
||||||
class="min-h-full w-full"
|
class="min-h-[70vh] w-full"
|
||||||
filter=".component-toolbar"
|
filter=".component-toolbar"
|
||||||
ghost-class="draggable-ghost"
|
ghost-class="draggable-ghost"
|
||||||
group="component"
|
group="component"
|
||||||
@@ -508,5 +507,4 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</PreviewModal>
|
</PreviewModal>
|
||||||
</Page>
|
</Page>
|
||||||
<!-- TODO @xingyu:这里改造完后,类似 web-ele/src/views/mall/promotion/components/diy-editor/index.vue 里的全局样式(递推到子组件)里的就没没了,类似 property-group -->
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Space } from 'ant-design-vue';
|
import { Space } from 'ant-design-vue';
|
||||||
|
|
||||||
// TODO @芋艿、@xingyu:貌似上下移动的按钮,被遮住了!
|
|
||||||
/**
|
/**
|
||||||
* 垂直按钮组
|
* 垂直按钮组
|
||||||
* Ant Design Vue 的按钮组,通过 Space 实现垂直布局
|
* Ant Design Vue 的按钮组,通过 Space 实现垂直布局
|
||||||
|
|||||||
@@ -60,14 +60,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
rules: 'required',
|
rules: 'required',
|
||||||
defaultValue: PromotionProductScopeEnum.ALL.scope,
|
defaultValue: PromotionProductScopeEnum.ALL.scope,
|
||||||
},
|
},
|
||||||
// TODO @puhui999: 商品选择器优化
|
|
||||||
{
|
{
|
||||||
fieldName: 'productSpuIds',
|
fieldName: 'productSpuIds',
|
||||||
label: '商品',
|
label: '商品',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
componentProps: {
|
|
||||||
placeholder: '请选择商品',
|
|
||||||
},
|
|
||||||
dependencies: {
|
dependencies: {
|
||||||
triggerFields: ['productScope', 'productScopeValues'],
|
triggerFields: ['productScope', 'productScopeValues'],
|
||||||
show: (model) =>
|
show: (model) =>
|
||||||
@@ -84,14 +80,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
},
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
// TODO @puhui999: 商品分类选择器优化
|
|
||||||
{
|
{
|
||||||
fieldName: 'productCategoryIds',
|
fieldName: 'productCategoryIds',
|
||||||
label: '商品分类',
|
label: '商品分类',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
componentProps: {
|
|
||||||
placeholder: '请选择商品分类',
|
|
||||||
},
|
|
||||||
dependencies: {
|
dependencies: {
|
||||||
triggerFields: ['productScope', 'productScopeValues'],
|
triggerFields: ['productScope', 'productScopeValues'],
|
||||||
show: (model) =>
|
show: (model) =>
|
||||||
|
|||||||
@@ -16,11 +16,18 @@ import {
|
|||||||
updateCouponTemplate,
|
updateCouponTemplate,
|
||||||
} from '#/api/mall/promotion/coupon/couponTemplate';
|
} from '#/api/mall/promotion/coupon/couponTemplate';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
import { ProductCategorySelect } from '#/views/mall/product/category/components';
|
||||||
|
import { SpuShowcase } from '#/views/mall/product/spu/components';
|
||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
const formData = ref<MallCouponTemplateApi.CouponTemplate>();
|
const formData = ref<
|
||||||
|
Partial<MallCouponTemplateApi.CouponTemplate> & {
|
||||||
|
productCategoryIds?: number | number[];
|
||||||
|
productSpuIds?: number[];
|
||||||
|
}
|
||||||
|
>({});
|
||||||
const getTitle = computed(() => {
|
const getTitle = computed(() => {
|
||||||
return formData.value?.id
|
return formData.value?.id
|
||||||
? $t('ui.actionTitle.edit', ['优惠券模板'])
|
? $t('ui.actionTitle.edit', ['优惠券模板'])
|
||||||
@@ -64,7 +71,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
},
|
},
|
||||||
async onOpenChange(isOpen: boolean) {
|
async onOpenChange(isOpen: boolean) {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formData.value = undefined;
|
formData.value = {};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 加载数据
|
// 加载数据
|
||||||
@@ -75,7 +82,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
try {
|
try {
|
||||||
formData.value = await getCouponTemplate(data.id);
|
formData.value = await getCouponTemplate(data.id);
|
||||||
const processedData = await processLoadData(formData.value);
|
const processedData = await processLoadData(formData.value as any);
|
||||||
// 设置到表单
|
// 设置到表单
|
||||||
await formApi.setValues(processedData);
|
await formApi.setValues(processedData);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -144,6 +151,15 @@ async function processLoadData(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal :title="getTitle" class="w-2/5">
|
<Modal :title="getTitle" class="w-2/5">
|
||||||
<Form class="mx-4" />
|
<Form class="mx-4">
|
||||||
|
<!-- 自定义插槽:商品选择 -->
|
||||||
|
<template #productSpuIds>
|
||||||
|
<SpuShowcase v-model="formData.productSpuIds" />
|
||||||
|
</template>
|
||||||
|
<!-- 自定义插槽:分类选择 -->
|
||||||
|
<template #productCategoryIds>
|
||||||
|
<ProductCategorySelect v-model="formData.productCategoryIds" />
|
||||||
|
</template>
|
||||||
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -67,8 +67,15 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
placeholder: '请输入备注',
|
placeholder: '请输入备注',
|
||||||
rows: 4,
|
rows: 4,
|
||||||
},
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'spuIds',
|
||||||
|
label: '活动商品',
|
||||||
|
component: 'Input',
|
||||||
|
rules: 'required',
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
},
|
},
|
||||||
// TODO
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,18 @@ import {
|
|||||||
updateDiscountActivity,
|
updateDiscountActivity,
|
||||||
} from '#/api/mall/promotion/discount/discountActivity';
|
} from '#/api/mall/promotion/discount/discountActivity';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
import { SpuShowcase } from '#/views/mall/product/spu/components';
|
||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
defineOptions({ name: 'DiscountActivityForm' });
|
defineOptions({ name: 'DiscountActivityForm' });
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
const formData = ref<MallDiscountActivityApi.DiscountActivity>();
|
const formData = ref<
|
||||||
|
Partial<MallDiscountActivityApi.DiscountActivity> & {
|
||||||
|
spuIds?: number[];
|
||||||
|
}
|
||||||
|
>({});
|
||||||
const getTitle = computed(() => {
|
const getTitle = computed(() => {
|
||||||
return formData.value?.id
|
return formData.value?.id
|
||||||
? $t('ui.actionTitle.edit', ['限时折扣活动'])
|
? $t('ui.actionTitle.edit', ['限时折扣活动'])
|
||||||
@@ -69,7 +74,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
},
|
},
|
||||||
async onOpenChange(isOpen: boolean) {
|
async onOpenChange(isOpen: boolean) {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formData.value = undefined;
|
formData.value = {};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 加载数据
|
// 加载数据
|
||||||
@@ -91,6 +96,11 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal class="w-3/5" :title="getTitle">
|
<Modal class="w-3/5" :title="getTitle">
|
||||||
<Form />
|
<Form>
|
||||||
|
<!-- 自定义插槽:商品选择 -->
|
||||||
|
<template #spuIds>
|
||||||
|
<SpuShowcase v-model="formData.spuIds" />
|
||||||
|
</template>
|
||||||
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -27,34 +27,28 @@ const route = useRoute();
|
|||||||
const { refreshTab } = useTabs();
|
const { refreshTab } = useTabs();
|
||||||
|
|
||||||
const domain = import.meta.env.VITE_MALL_H5_DOMAIN;
|
const domain = import.meta.env.VITE_MALL_H5_DOMAIN;
|
||||||
// 特殊:存储 reset 重置时,当前 selectedTemplateItem 值,从而进行恢复
|
const DIY_PAGE_INDEX_KEY = 'diy_page_index'; // 特殊:存储 reset 重置时,当前 selectedTemplateItem 值,从而进行恢复
|
||||||
const DIY_PAGE_INDEX_KEY = 'diy_page_index';
|
|
||||||
|
|
||||||
const selectedTemplateItem = ref(0);
|
const selectedTemplateItem = ref(0);
|
||||||
// 左上角工具栏操作按钮
|
|
||||||
const templateItems = ref([
|
const templateItems = ref([
|
||||||
{ key: 0, name: '基础设置', icon: 'lucide:settings' },
|
{ key: 0, name: '基础设置', icon: 'lucide:settings' },
|
||||||
{ key: 1, name: '首页', icon: 'lucide:home' },
|
{ key: 1, name: '首页', icon: 'lucide:home' },
|
||||||
{ key: 2, name: '我的', icon: 'lucide:user' },
|
{ key: 2, name: '我的', icon: 'lucide:user' },
|
||||||
]);
|
]); // 左上角工具栏操作按钮
|
||||||
|
|
||||||
const formData = ref<MallDiyTemplateApi.DiyTemplateProperty>();
|
const formData = ref<MallDiyTemplateApi.DiyTemplateProperty>();
|
||||||
// 当前编辑的属性
|
|
||||||
const currentFormData = ref<
|
const currentFormData = ref<
|
||||||
MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty
|
MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty
|
||||||
>({
|
>({
|
||||||
property: '',
|
property: '',
|
||||||
} as MallDiyPageApi.DiyPage);
|
} as MallDiyPageApi.DiyPage); // 当前编辑的属性
|
||||||
// templateItem 对应的缓存
|
|
||||||
const currentFormDataMap = ref<
|
const currentFormDataMap = ref<
|
||||||
Map<string, MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty>
|
Map<string, MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty>
|
||||||
>(new Map());
|
>(new Map()); // templateItem 对应的缓存
|
||||||
// 商城 H5 预览地址
|
|
||||||
const previewUrl = ref('');
|
const previewUrl = ref(''); // 商城 H5 预览地址
|
||||||
// 模板组件库
|
const templateLibs = [] as DiyComponentLibrary[]; // 模板组件库
|
||||||
const templateLibs = [] as DiyComponentLibrary[];
|
const libs = ref<DiyComponentLibrary[]>(templateLibs); // 当前组件库
|
||||||
// 当前组件库
|
|
||||||
const libs = ref<DiyComponentLibrary[]>(templateLibs);
|
|
||||||
|
|
||||||
/** 获取详情 */
|
/** 获取详情 */
|
||||||
async function getPageDetail(id: any) {
|
async function getPageDetail(id: any) {
|
||||||
@@ -73,23 +67,23 @@ async function getPageDetail(id: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 模板选项切换 */
|
/** 模板选项切换 */
|
||||||
function handleTemplateItemChange(val: any) {
|
function handleTemplateItemChange(valObj: any) {
|
||||||
const changeValue = val.target.value;
|
const val = valObj.target.value;
|
||||||
// 缓存模版编辑数据
|
// 缓存模版编辑数据
|
||||||
currentFormDataMap.value.set(
|
currentFormDataMap.value.set(
|
||||||
templateItems.value[changeValue]!.name,
|
templateItems.value[selectedTemplateItem.value]?.name || '',
|
||||||
currentFormData.value!,
|
currentFormData.value!,
|
||||||
);
|
);
|
||||||
// 切换模版
|
|
||||||
selectedTemplateItem.value = changeValue;
|
|
||||||
|
|
||||||
// 读取模版缓存
|
// 读取模版缓存
|
||||||
const data = currentFormDataMap.value.get(
|
const data = currentFormDataMap.value.get(
|
||||||
templateItems.value[changeValue]!.name,
|
templateItems.value[val]?.name || '',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 切换模版
|
||||||
|
selectedTemplateItem.value = val;
|
||||||
|
|
||||||
// 情况一:编辑模板
|
// 情况一:编辑模板
|
||||||
if (changeValue === 0) {
|
if (val === 0) {
|
||||||
libs.value = templateLibs;
|
libs.value = templateLibs;
|
||||||
currentFormData.value = (isEmpty(data) ? formData.value : data) as
|
currentFormData.value = (isEmpty(data) ? formData.value : data) as
|
||||||
| MallDiyPageApi.DiyPage
|
| MallDiyPageApi.DiyPage
|
||||||
@@ -103,7 +97,7 @@ function handleTemplateItemChange(val: any) {
|
|||||||
isEmpty(data)
|
isEmpty(data)
|
||||||
? formData.value!.pages.find(
|
? formData.value!.pages.find(
|
||||||
(page: MallDiyPageApi.DiyPage) =>
|
(page: MallDiyPageApi.DiyPage) =>
|
||||||
page.name === templateItems.value[changeValue]!.name,
|
page.name === templateItems.value[val]?.name,
|
||||||
)
|
)
|
||||||
: data
|
: data
|
||||||
) as MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty;
|
) as MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty;
|
||||||
|
|||||||
@@ -126,15 +126,13 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
},
|
},
|
||||||
formItemClass: 'col-span-2',
|
formItemClass: 'col-span-2',
|
||||||
},
|
},
|
||||||
|
// TODO @puhui999:商品图太大了。
|
||||||
{
|
{
|
||||||
fieldName: 'spuId',
|
fieldName: 'spuId',
|
||||||
label: '活动商品',
|
label: '活动商品',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
formItemClass: 'col-span-2',
|
formItemClass: 'col-span-2',
|
||||||
renderComponentContent: () => ({
|
|
||||||
default: () => null,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ function handleEdit(row: MallRewardActivityApi.RewardActivity) {
|
|||||||
|
|
||||||
/** 关闭满减送活动 */
|
/** 关闭满减送活动 */
|
||||||
async function handleClose(row: MallRewardActivityApi.RewardActivity) {
|
async function handleClose(row: MallRewardActivityApi.RewardActivity) {
|
||||||
// TODO @puhui999:这个国际化,需要加下哈;closing、closeSuccess;
|
|
||||||
const hideLoading = message.loading({
|
const hideLoading = message.loading({
|
||||||
content: $t('ui.actionMessage.closing', [row.name]),
|
content: $t('ui.actionMessage.closing', [row.name]),
|
||||||
duration: 0,
|
duration: 0,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const emit = defineEmits(['success']);
|
|||||||
|
|
||||||
const formData = ref<Partial<MallRewardActivityApi.RewardActivity>>({
|
const formData = ref<Partial<MallRewardActivityApi.RewardActivity>>({
|
||||||
conditionType: PromotionConditionTypeEnum.PRICE.type,
|
conditionType: PromotionConditionTypeEnum.PRICE.type,
|
||||||
|
productScope: PromotionProductScopeEnum.ALL.scope,
|
||||||
rules: [],
|
rules: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -102,46 +102,53 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div>
|
||||||
<Button type="link" class="pl-0" @click="handleSelect">添加优惠券</Button>
|
<!-- 已选优惠券列表 -->
|
||||||
|
<div v-if="list.length > 0" class="mb-2 flex flex-col gap-2">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in list"
|
v-for="(item, index) in list"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="coupon-list-item mb-2 flex justify-between rounded-lg border border-dashed border-gray-300 p-2"
|
class="flex items-center justify-between rounded-md border border-gray-200 bg-white px-3 py-2 transition-all hover:border-blue-400 hover:shadow-sm"
|
||||||
>
|
>
|
||||||
<div class="coupon-list-item-left flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<div>优惠券名称:{{ item.name }}</div>
|
<span class="font-medium text-gray-800">{{ item.name }}</span>
|
||||||
<div>
|
<span class="flex items-center gap-1 text-sm text-gray-500">
|
||||||
范围:
|
<DictTag
|
||||||
<DictTag
|
:type="DICT_TYPE.PROMOTION_PRODUCT_SCOPE"
|
||||||
:type="DICT_TYPE.PROMOTION_PRODUCT_SCOPE"
|
:value="item.productScope"
|
||||||
:value="item.productScope"
|
/>
|
||||||
/>
|
</span>
|
||||||
|
<span class="flex items-center gap-1 text-sm text-gray-500">
|
||||||
|
<DictTag
|
||||||
|
:type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE"
|
||||||
|
:value="item.discountType"
|
||||||
|
/>
|
||||||
|
{{ discountFormat(item) }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex shrink-0 items-center gap-2">
|
||||||
优惠:
|
<span class="text-gray-500">送</span>
|
||||||
<DictTag
|
<Input
|
||||||
:type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE"
|
v-model:value="item.giveCount"
|
||||||
:value="item.discountType"
|
class="!w-20"
|
||||||
|
placeholder=""
|
||||||
|
type="number"
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
{{ discountFormat(item) }}
|
<span class="text-gray-500">张</span>
|
||||||
|
<Button type="link" danger size="small" @click="handleDelete(index)">
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="coupon-list-item-right flex items-center gap-2">
|
|
||||||
<span>送</span>
|
|
||||||
<Input
|
|
||||||
v-model:value="item.giveCount"
|
|
||||||
class="!w-150px"
|
|
||||||
placeholder=""
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
<span>张</span>
|
|
||||||
<Button type="link" danger @click="handleDelete(index)">删除</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 优惠券选择 -->
|
<!-- 添加按钮 -->
|
||||||
|
<Button type="link" class="!pl-0" @click="handleSelect">
|
||||||
|
+ 添加优惠券
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- 优惠券选择弹窗 -->
|
||||||
<CouponSelect
|
<CouponSelect
|
||||||
ref="selectRef"
|
ref="selectRef"
|
||||||
:take-type="CouponTemplateTakeTypeEnum.ADMIN.type"
|
:take-type="CouponTemplateTakeTypeEnum.ADMIN.type"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { PromotionConditionTypeEnum } from '@vben/constants';
|
|||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Card,
|
||||||
Col,
|
Col,
|
||||||
Form,
|
Form,
|
||||||
FormItem,
|
FormItem,
|
||||||
@@ -61,76 +62,92 @@ function handleDelete(ruleIndex: number) {
|
|||||||
<Row :gutter="[16, 16]">
|
<Row :gutter="[16, 16]">
|
||||||
<template v-if="formData.rules">
|
<template v-if="formData.rules">
|
||||||
<Col v-for="(rule, index) in formData.rules" :key="index" :span="24">
|
<Col v-for="(rule, index) in formData.rules" :key="index" :span="24">
|
||||||
<!-- 规则标题 -->
|
<Card size="small" class="rounded-lg">
|
||||||
<div class="mb-4 flex items-center">
|
<!-- 规则标题 -->
|
||||||
<span class="text-base font-bold">活动层级 {{ index + 1 }}</span>
|
<template #title>
|
||||||
<Button
|
<div class="flex items-center">
|
||||||
v-if="index !== 0"
|
<span class="text-base font-medium">
|
||||||
type="link"
|
活动层级 {{ index + 1 }}
|
||||||
danger
|
</span>
|
||||||
class="ml-2"
|
|
||||||
@click="handleDelete(index)"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form :model="rule" layout="horizontal">
|
|
||||||
<!-- 优惠门槛 -->
|
|
||||||
<FormItem label="优惠门槛" :label-col="{ span: 4 }">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span>满</span>
|
|
||||||
<InputNumber
|
|
||||||
v-if="isPriceCondition"
|
|
||||||
v-model:value="rule.limit"
|
|
||||||
:min="0"
|
|
||||||
:precision="2"
|
|
||||||
:step="0.1"
|
|
||||||
class="!w-40"
|
|
||||||
placeholder="请输入金额"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
v-else
|
|
||||||
v-model:value="rule.limit"
|
|
||||||
:min="0"
|
|
||||||
class="!w-40"
|
|
||||||
placeholder="请输入数量"
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
<span>{{ isPriceCondition ? '元' : '件' }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</template>
|
||||||
<!-- 优惠内容 -->
|
<template v-if="index !== 0" #extra>
|
||||||
<FormItem
|
<Button
|
||||||
label="优惠内容"
|
type="link"
|
||||||
:label-col="{ span: 4 }"
|
danger
|
||||||
:wrapper-col="{ span: 20 }"
|
size="small"
|
||||||
>
|
@click="handleDelete(index)"
|
||||||
<div class="flex flex-col gap-2">
|
>
|
||||||
<span>订单金额优惠</span>
|
删除
|
||||||
<div class="flex items-center gap-2">
|
</Button>
|
||||||
<span>减</span>
|
</template>
|
||||||
|
|
||||||
|
<Form :model="rule" layout="horizontal">
|
||||||
|
<!-- 优惠门槛 -->
|
||||||
|
<FormItem label="优惠门槛:" :colon="false" class="mb-3">
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 rounded-md bg-gray-50 px-3 py-2"
|
||||||
|
>
|
||||||
|
<span>满</span>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
v-model:value="rule.discountPrice"
|
v-if="isPriceCondition"
|
||||||
|
v-model:value="rule.limit"
|
||||||
:min="0"
|
:min="0"
|
||||||
:precision="2"
|
:precision="2"
|
||||||
:step="0.1"
|
:step="0.1"
|
||||||
class="!w-32"
|
class="!w-40"
|
||||||
placeholder="请输入金额"
|
placeholder="请输入金额"
|
||||||
/>
|
/>
|
||||||
<span>元</span>
|
<Input
|
||||||
</div>
|
v-else
|
||||||
<div class="flex items-center gap-2">
|
v-model:value="rule.limit"
|
||||||
<span>包邮:</span>
|
:min="0"
|
||||||
<Switch
|
class="!w-40"
|
||||||
v-model:checked="rule.freeDelivery"
|
placeholder="请输入数量"
|
||||||
checked-children="是"
|
type="number"
|
||||||
un-checked-children="否"
|
|
||||||
/>
|
/>
|
||||||
|
<span>{{ isPriceCondition ? '元' : '件' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</FormItem>
|
||||||
<div>送积分:</div>
|
<!-- 优惠内容 -->
|
||||||
<div class="mt-2 flex items-center gap-2">
|
<FormItem label="优惠内容:" :colon="false" class="!mb-0">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<!-- 订单金额优惠 -->
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 rounded-md bg-gray-50 px-3 py-2"
|
||||||
|
>
|
||||||
|
<span class="!w-21 shrink-0 text-sm text-gray-500">
|
||||||
|
订单金额优惠
|
||||||
|
</span>
|
||||||
|
<span>减</span>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="rule.discountPrice"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.1"
|
||||||
|
class="!w-32"
|
||||||
|
placeholder="请输入金额"
|
||||||
|
/>
|
||||||
|
<span>元</span>
|
||||||
|
</div>
|
||||||
|
<!-- 包邮 -->
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 rounded-md bg-gray-50 px-3 py-2"
|
||||||
|
>
|
||||||
|
<span class="w-20 shrink-0 text-sm text-gray-500">包邮</span>
|
||||||
|
<Switch
|
||||||
|
v-model:checked="rule.freeDelivery"
|
||||||
|
checked-children="是"
|
||||||
|
un-checked-children="否"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 送积分 -->
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 rounded-md bg-gray-50 px-3 py-2"
|
||||||
|
>
|
||||||
|
<span class="w-20 shrink-0 text-sm text-gray-500">
|
||||||
|
送积分
|
||||||
|
</span>
|
||||||
<span>送</span>
|
<span>送</span>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
v-model:value="rule.point"
|
v-model:value="rule.point"
|
||||||
@@ -140,17 +157,24 @@ function handleDelete(ruleIndex: number) {
|
|||||||
/>
|
/>
|
||||||
<span>积分</span>
|
<span>积分</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 送优惠券 -->
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-start gap-2 rounded-md bg-gray-50 px-3 py-2"
|
||||||
|
>
|
||||||
|
<span class="w-20 shrink-0 text-sm text-gray-500">
|
||||||
|
送优惠券
|
||||||
|
</span>
|
||||||
|
<RewardRuleCouponSelect
|
||||||
|
:model-value="rule"
|
||||||
|
@update:model-value="
|
||||||
|
(val) => (formData.rules![index] = val)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
</FormItem>
|
||||||
<span class="w-20">送优惠券:</span>
|
</Form>
|
||||||
<RewardRuleCouponSelect
|
</Card>
|
||||||
:model-value="rule"
|
|
||||||
@update:model-value="(val) => (formData.rules![index] = val)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
</Form>
|
|
||||||
</Col>
|
</Col>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import { useVbenModal } from '@vben/common-ui';
|
|||||||
import { Button, message } from 'ant-design-vue';
|
import { Button, message } from 'ant-design-vue';
|
||||||
|
|
||||||
import { useVbenForm } from '#/adapter/form';
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { getSpu } from '#/api/mall/product/spu';
|
||||||
import {
|
import {
|
||||||
createSeckillActivity,
|
createSeckillActivity,
|
||||||
getSeckillActivity,
|
getSeckillActivity,
|
||||||
updateSeckillActivity,
|
updateSeckillActivity,
|
||||||
} from '#/api/mall/promotion/seckill/seckillActivity';
|
} from '#/api/mall/promotion/seckill/seckillActivity';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
import { SpuSkuSelect } from '#/views/mall/product/spu/components';
|
||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
@@ -31,25 +33,37 @@ const spuId = ref<number>();
|
|||||||
const spuName = ref<string>('');
|
const spuName = ref<string>('');
|
||||||
const skuTableData = ref<any[]>([]);
|
const skuTableData = ref<any[]>([]);
|
||||||
|
|
||||||
// 选择商品(占位函数,实际需要对接商品选择组件)
|
const spuSkuSelectRef = ref(); // 商品选择弹窗 Ref
|
||||||
|
|
||||||
|
/** 打开商品选择弹窗 */
|
||||||
const handleSelectProduct = () => {
|
const handleSelectProduct = () => {
|
||||||
message.info('商品选择功能需要对接商品选择组件');
|
spuSkuSelectRef.value?.open();
|
||||||
// TODO: 打开商品选择弹窗
|
|
||||||
// 实际使用时需要:
|
|
||||||
// 1. 打开商品选择弹窗
|
|
||||||
// 2. 选择商品后调用以下逻辑设置数据:
|
|
||||||
// spuId.value = selectedSpu.id;
|
|
||||||
// spuName.value = selectedSpu.name;
|
|
||||||
// skuTableData.value = selectedSkus.map(sku => ({
|
|
||||||
// skuId: sku.id,
|
|
||||||
// skuName: sku.name || '',
|
|
||||||
// picUrl: sku.picUrl || selectedSpu.picUrl || '',
|
|
||||||
// price: sku.price || 0,
|
|
||||||
// stock: 0,
|
|
||||||
// seckillPrice: 0,
|
|
||||||
// }));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 选择商品后的回调 */
|
||||||
|
async function handleSpuSelected(selectedSpuId: number, skuIds?: number[]) {
|
||||||
|
const spu = await getSpu(selectedSpuId);
|
||||||
|
if (!spu) return;
|
||||||
|
|
||||||
|
spuId.value = spu.id;
|
||||||
|
spuName.value = spu.name || '';
|
||||||
|
|
||||||
|
// 筛选指定的 SKU
|
||||||
|
const selectedSkus = skuIds
|
||||||
|
? spu.skus?.filter((sku) => skuIds.includes(sku.id!))
|
||||||
|
: spu.skus;
|
||||||
|
|
||||||
|
skuTableData.value =
|
||||||
|
selectedSkus?.map((sku) => ({
|
||||||
|
skuId: sku.id!,
|
||||||
|
skuName: sku.name || '',
|
||||||
|
picUrl: sku.picUrl || spu.picUrl || '',
|
||||||
|
price: sku.price || 0,
|
||||||
|
stock: 0,
|
||||||
|
seckillPrice: 0,
|
||||||
|
})) || [];
|
||||||
|
}
|
||||||
|
|
||||||
// ================= end =================
|
// ================= end =================
|
||||||
|
|
||||||
const [Form, formApi] = useVbenForm({
|
const [Form, formApi] = useVbenForm({
|
||||||
@@ -137,10 +151,30 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
await formApi.setValues(formData.value);
|
await formApi.setValues(formData.value);
|
||||||
|
|
||||||
// TODO: 加载商品和 SKU 信息
|
// 加载商品和 SKU 信息
|
||||||
// 需要调用商品 API 获取 SPU 详情
|
if (formData.value.spuId) {
|
||||||
// spuId.value = formData.value.spuId;
|
const spu = await getSpu(formData.value.spuId);
|
||||||
// await loadProductDetails(formData.value.spuId, formData.value.products);
|
if (spu) {
|
||||||
|
spuId.value = spu.id;
|
||||||
|
spuName.value = spu.name || '';
|
||||||
|
// 回填 SKU 配置
|
||||||
|
const products = formData.value.products || [];
|
||||||
|
skuTableData.value =
|
||||||
|
spu.skus
|
||||||
|
?.filter((sku) => products.some((p) => p.skuId === sku.id))
|
||||||
|
.map((sku) => {
|
||||||
|
const product = products.find((p) => p.skuId === sku.id);
|
||||||
|
return {
|
||||||
|
skuId: sku.id!,
|
||||||
|
skuName: sku.name || '',
|
||||||
|
picUrl: sku.picUrl || spu.picUrl || '',
|
||||||
|
price: sku.price || 0,
|
||||||
|
stock: product?.stock || 0,
|
||||||
|
seckillPrice: (product?.seckillPrice || 0) / 100, // 分转元
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
modalApi.unlock();
|
modalApi.unlock();
|
||||||
}
|
}
|
||||||
@@ -217,4 +251,11 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<!-- 商品选择器弹窗 -->
|
||||||
|
<SpuSkuSelect
|
||||||
|
ref="spuSkuSelectRef"
|
||||||
|
:is-select-sku="true"
|
||||||
|
@select="handleSpuSelected"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -133,7 +133,6 @@ function emitActivityChange() {
|
|||||||
class="flex h-[60px] w-[60px] cursor-pointer items-center justify-center rounded-lg border border-dashed border-gray-300 hover:border-blue-400"
|
class="flex h-[60px] w-[60px] cursor-pointer items-center justify-center rounded-lg border border-dashed border-gray-300 hover:border-blue-400"
|
||||||
@click="handleOpenActivitySelect"
|
@click="handleOpenActivitySelect"
|
||||||
>
|
>
|
||||||
<!-- TODO @芋艿:等待和 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/views/mall/product/spu/components/spu-showcase.vue 进一步统一 -->
|
|
||||||
<IconifyIcon icon="lucide:plus" class="text-xl text-gray-400" />
|
<IconifyIcon icon="lucide:plus" class="text-xl text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ defineOptions({ name: 'TabNews' });
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: Reply;
|
modelValue: Reply;
|
||||||
newsType: NewsType;
|
newsType?: NewsType;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { MpMaterialApi } from '#/api/mp/material';
|
|||||||
|
|
||||||
import { watch } from 'vue';
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
import { openWindow } from '@vben/utils';
|
import { openWindow } from '@vben/utils';
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
@@ -81,7 +82,7 @@ watch(
|
|||||||
onClick: () => openWindow(row.url),
|
onClick: () => openWindow(row.url),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '删除',
|
label: $t('common.delete'),
|
||||||
type: 'link',
|
type: 'link',
|
||||||
danger: true,
|
danger: true,
|
||||||
icon: ACTION_ICON.DELETE,
|
icon: ACTION_ICON.DELETE,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { MpMaterialApi } from '#/api/mp/material';
|
|||||||
|
|
||||||
import { watch } from 'vue';
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
import { openWindow } from '@vben/utils';
|
import { openWindow } from '@vben/utils';
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
@@ -81,7 +82,7 @@ watch(
|
|||||||
onClick: () => openWindow(row.url),
|
onClick: () => openWindow(row.url),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '删除',
|
label: $t('common.delete'),
|
||||||
type: 'link',
|
type: 'link',
|
||||||
danger: true,
|
danger: true,
|
||||||
icon: ACTION_ICON.DELETE,
|
icon: ACTION_ICON.DELETE,
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ function handlePageChange(page: number, pageSize: number) {
|
|||||||
function showTotal(total: number) {
|
function showTotal(total: number) {
|
||||||
return `共 ${total} 条`;
|
return `共 ${total} 条`;
|
||||||
}
|
}
|
||||||
|
// TODO @dylan:是不是应该都用 Grid 哈:1)message-table 大部分合并到 index.vue;2)message-table 的 schema 放到 data.ts 里;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -82,6 +82,18 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
values.socialType === SystemUserSocialTypeEnum.WECHAT_ENTERPRISE.type,
|
values.socialType === SystemUserSocialTypeEnum.WECHAT_ENTERPRISE.type,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'publicKey',
|
||||||
|
label: 'publicKey',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入 publicKey 公钥',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['socialType'],
|
||||||
|
show: (values) => values.socialType === 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'status',
|
fieldName: 'status',
|
||||||
label: '状态',
|
label: '状态',
|
||||||
|
|||||||
@@ -7,27 +7,16 @@ import { requestClient } from '#/api/request';
|
|||||||
export namespace BpmTaskApi {
|
export namespace BpmTaskApi {
|
||||||
/** 流程任务 */
|
/** 流程任务 */
|
||||||
export interface Task {
|
export interface Task {
|
||||||
id: number; // 编号
|
|
||||||
name: string; // 监听器名字
|
|
||||||
type: string; // 监听器类型
|
|
||||||
status: number; // 监听器状态
|
|
||||||
event: string; // 监听事件
|
|
||||||
valueType: string; // 监听器值类型
|
|
||||||
processInstance?: BpmProcessInstanceApi.ProcessInstance; // 流程实例
|
|
||||||
}
|
|
||||||
|
|
||||||
// 流程任务
|
|
||||||
export interface TaskManager {
|
|
||||||
id: string; // 编号
|
id: string; // 编号
|
||||||
name: string; // 任务名称
|
name: string; // 任务名字
|
||||||
|
status: number; // 任务状态
|
||||||
createTime: number; // 创建时间
|
createTime: number; // 创建时间
|
||||||
endTime: number; // 结束时间
|
endTime: number; // 结束时间
|
||||||
durationInMillis: number; // 持续时间
|
durationInMillis: number; // 持续时间
|
||||||
status: number; // 状态
|
reason: string; // 审批理由
|
||||||
reason: string; // 原因
|
|
||||||
ownerUser: any; // 负责人
|
ownerUser: any; // 负责人
|
||||||
assigneeUser: any; // 处理人
|
assigneeUser: any; // 处理人
|
||||||
taskDefinitionKey: string; // 任务定义key
|
taskDefinitionKey: string; // 任务定义的标识
|
||||||
processInstanceId: string; // 流程实例id
|
processInstanceId: string; // 流程实例id
|
||||||
processInstance: BpmProcessInstanceApi.ProcessInstance; // 流程实例
|
processInstance: BpmProcessInstanceApi.ProcessInstance; // 流程实例
|
||||||
parentTaskId: any; // 父任务id
|
parentTaskId: any; // 父任务id
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export namespace InfraFileConfigApi {
|
|||||||
accessSecret?: string;
|
accessSecret?: string;
|
||||||
pathStyle?: boolean;
|
pathStyle?: boolean;
|
||||||
enablePublicAccess?: boolean;
|
enablePublicAccess?: boolean;
|
||||||
|
region?: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export namespace SystemSocialClientApi {
|
|||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
|
publicKey?: string;
|
||||||
status: number;
|
status: number;
|
||||||
createTime?: Date;
|
createTime?: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,24 +9,6 @@ const routes: RouteRecordRaw[] = [
|
|||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
|
||||||
path: 'task',
|
|
||||||
name: 'BpmTask',
|
|
||||||
meta: {
|
|
||||||
title: '审批中心',
|
|
||||||
icon: 'ant-design:history-outlined',
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'my',
|
|
||||||
name: 'BpmTaskMy',
|
|
||||||
component: () => import('#/views/bpm/processInstance/index.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '我的流程',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'process-instance/detail',
|
path: 'process-instance/detail',
|
||||||
component: () => import('#/views/bpm/processInstance/detail/index.vue'),
|
component: () => import('#/views/bpm/processInstance/detail/index.vue'),
|
||||||
|
|||||||
@@ -146,7 +146,6 @@
|
|||||||
background: url('./svg/simple-process-bg.svg') 0 0 repeat;
|
background: url('./svg/simple-process-bg.svg') 0 0 repeat;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
transform-origin: 50% 0 0;
|
transform-origin: 50% 0 0;
|
||||||
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
|
||||||
// 节点容器 定义节点宽度
|
// 节点容器 定义节点宽度
|
||||||
.node-container {
|
.node-container {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ async function validateAllSteps() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveLoading = ref<boolean>(false);
|
||||||
/** 保存操作 */
|
/** 保存操作 */
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
try {
|
try {
|
||||||
@@ -272,7 +273,7 @@ async function handleSave() {
|
|||||||
const modelData = {
|
const modelData = {
|
||||||
...formData.value,
|
...formData.value,
|
||||||
};
|
};
|
||||||
|
saveLoading.value = true;
|
||||||
switch (actionType) {
|
switch (actionType) {
|
||||||
case 'copy': {
|
case 'copy': {
|
||||||
// 情况三:复制场景
|
// 情况三:复制场景
|
||||||
@@ -309,9 +310,12 @@ async function handleSave() {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('保存失败:', error);
|
console.error('保存失败:', error);
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 发布加载中状态
|
||||||
|
const deployLoading = ref<boolean>(false);
|
||||||
/** 发布操作 */
|
/** 发布操作 */
|
||||||
async function handleDeploy() {
|
async function handleDeploy() {
|
||||||
try {
|
try {
|
||||||
@@ -322,6 +326,8 @@ async function handleDeploy() {
|
|||||||
// 1.2 校验所有步骤
|
// 1.2 校验所有步骤
|
||||||
await validateAllSteps();
|
await validateAllSteps();
|
||||||
|
|
||||||
|
deployLoading.value = true;
|
||||||
|
|
||||||
// 2.1 更新表单数据
|
// 2.1 更新表单数据
|
||||||
const modelData = {
|
const modelData = {
|
||||||
...formData.value,
|
...formData.value,
|
||||||
@@ -342,6 +348,8 @@ async function handleDeploy() {
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('发布失败:', error);
|
console.error('发布失败:', error);
|
||||||
ElMessage.warning(error.message || '发布失败');
|
ElMessage.warning(error.message || '发布失败');
|
||||||
|
} finally {
|
||||||
|
deployLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,11 +456,12 @@ onBeforeUnmount(() => {
|
|||||||
<ElButton
|
<ElButton
|
||||||
v-if="actionType === 'update'"
|
v-if="actionType === 'update'"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
:loading="deployLoading"
|
||||||
@click="handleDeploy"
|
@click="handleDeploy"
|
||||||
>
|
>
|
||||||
发 布
|
发 布
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton type="primary" @click="handleSave">
|
<ElButton type="primary" @click="handleSave" :loading="saveLoading">
|
||||||
<span v-if="actionType === 'definition'">恢 复</span>
|
<span v-if="actionType === 'definition'">恢 复</span>
|
||||||
<span v-else>保 存</span>
|
<span v-else>保 存</span>
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
|||||||
@@ -234,9 +234,10 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<ElCard
|
<ElCard
|
||||||
shadow="hover"
|
shadow="hover"
|
||||||
class="definition-item-card w-full cursor-pointer"
|
class="w-full cursor-pointer"
|
||||||
:class="{
|
:class="{
|
||||||
'search-match': searchName.trim().length > 0,
|
'animate-bounce-once !bg-[rgb(63_115_247_/_10%)]':
|
||||||
|
searchName.trim().length > 0,
|
||||||
}"
|
}"
|
||||||
:body-style="{
|
:body-style="{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -247,10 +248,13 @@ onMounted(() => {
|
|||||||
<img
|
<img
|
||||||
v-if="definition.icon"
|
v-if="definition.icon"
|
||||||
:src="definition.icon"
|
:src="definition.icon"
|
||||||
class="flow-icon-img object-contain"
|
class="size-12 rounded object-contain"
|
||||||
alt="流程图标"
|
alt="流程图标"
|
||||||
/>
|
/>
|
||||||
<div v-else class="flow-icon flex-shrink-0">
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex size-12 flex-shrink-0 items-center justify-center rounded bg-primary"
|
||||||
|
>
|
||||||
<span class="text-xs text-white">
|
<span class="text-xs text-white">
|
||||||
{{ definition.name?.slice(0, 2) }}
|
{{ definition.name?.slice(0, 2) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -301,31 +305,8 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.process-definition-container {
|
.animate-bounce-once {
|
||||||
.definition-item-card {
|
animation: bounce 0.5s ease;
|
||||||
.flow-icon-img {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flow-icon {
|
|
||||||
@apply bg-primary;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.search-match {
|
|
||||||
background-color: rgb(63 115 247 / 10%);
|
|
||||||
border: 1px solid var(--primary);
|
|
||||||
animation: bounce 0.5s ease;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-tabs__content) {
|
:deep(.el-tabs__content) {
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ async function submitForm() {
|
|||||||
// 关闭并提示
|
// 关闭并提示
|
||||||
ElMessage.success('发起流程成功');
|
ElMessage.success('发起流程成功');
|
||||||
await closeCurrentTab();
|
await closeCurrentTab();
|
||||||
await router.push({ name: 'BpmTaskMy' });
|
await router.push({ name: 'BpmProcessInstanceMy' });
|
||||||
} finally {
|
} finally {
|
||||||
processInstanceStartLoading.value = false;
|
processInstanceStartLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,17 +220,22 @@ watch(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
const loading = ref(false);
|
||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await getDetail();
|
loading.value = true;
|
||||||
// 获得用户列表
|
try {
|
||||||
userOptions.value = await getSimpleUserList();
|
await getDetail();
|
||||||
|
// 获得用户列表
|
||||||
|
userOptions.value = await getSimpleUserList();
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page auto-content-height>
|
<Page auto-content-height v-loading="loading">
|
||||||
<ElCard
|
<ElCard
|
||||||
class="flex h-full flex-col"
|
class="flex h-full flex-col"
|
||||||
:body-style="{
|
:body-style="{
|
||||||
@@ -339,24 +344,22 @@ onMounted(async () => {
|
|||||||
</ElRow>
|
</ElRow>
|
||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
<ElTabPane label="流程图" name="diagram" class="pb-20 pr-3">
|
<ElTabPane label="流程图" name="diagram" class="pb-20 pr-3">
|
||||||
<div>
|
<ProcessInstanceSimpleViewer
|
||||||
<ProcessInstanceSimpleViewer
|
v-show="
|
||||||
v-show="
|
processDefinition.modelType &&
|
||||||
processDefinition.modelType &&
|
processDefinition.modelType === BpmModelType.SIMPLE
|
||||||
processDefinition.modelType === BpmModelType.SIMPLE
|
"
|
||||||
"
|
:loading="processInstanceLoading"
|
||||||
:loading="processInstanceLoading"
|
:model-view="processModelView"
|
||||||
:model-view="processModelView"
|
/>
|
||||||
/>
|
<ProcessInstanceBpmnViewer
|
||||||
<ProcessInstanceBpmnViewer
|
v-show="
|
||||||
v-show="
|
processDefinition.modelType &&
|
||||||
processDefinition.modelType &&
|
processDefinition.modelType === BpmModelType.BPMN
|
||||||
processDefinition.modelType === BpmModelType.BPMN
|
"
|
||||||
"
|
:loading="processInstanceLoading"
|
||||||
:loading="processInstanceLoading"
|
:model-view="processModelView"
|
||||||
:model-view="processModelView"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
<ElTabPane label="流转记录" name="record" class="pb-20 pr-3">
|
<ElTabPane label="流转记录" name="record" class="pb-20 pr-3">
|
||||||
<BpmProcessInstanceTaskList
|
<BpmProcessInstanceTaskList
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ async function openPopover(type: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Object.keys(popOverVisible.value).forEach((item) => {
|
Object.keys(popOverVisible.value).forEach((item) => {
|
||||||
if (popOverVisible.value[item]) popOverVisible.value[item] = item === type;
|
popOverVisible.value[item] = item === type;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,14 +704,6 @@ function handleSignFinish(url: string) {
|
|||||||
approveFormRef.value?.validateField('signPicUrl');
|
approveFormRef.value?.validateField('signPicUrl');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 处理弹窗可见性 */
|
|
||||||
function handlePopoverVisible(visible: boolean) {
|
|
||||||
if (!visible) {
|
|
||||||
// 拦截关闭事件
|
|
||||||
popOverVisible.value.approve = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ loadTodoTask });
|
defineExpose({ loadTodoTask });
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
@@ -720,11 +712,10 @@ defineExpose({ loadTodoTask });
|
|||||||
<!-- z-index 设置为300 避免覆盖签名弹窗 -->
|
<!-- z-index 设置为300 避免覆盖签名弹窗 -->
|
||||||
<ElSpace size="default">
|
<ElSpace size="default">
|
||||||
<ElPopover
|
<ElPopover
|
||||||
v-model:visible="popOverVisible.approve"
|
:visible="popOverVisible.approve"
|
||||||
placement="top"
|
placement="top"
|
||||||
:popper-style="{ minWidth: '400px', zIndex: 300 }"
|
:popper-style="{ minWidth: '400px', zIndex: 300 }"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@open-change="handlePopoverVisible"
|
|
||||||
v-if="
|
v-if="
|
||||||
runningTask &&
|
runningTask &&
|
||||||
isHandleTaskStatus() &&
|
isHandleTaskStatus() &&
|
||||||
@@ -825,7 +816,7 @@ defineExpose({ loadTodoTask });
|
|||||||
|
|
||||||
<!-- 【拒绝】按钮 -->
|
<!-- 【拒绝】按钮 -->
|
||||||
<ElPopover
|
<ElPopover
|
||||||
v-model:visible="popOverVisible.reject"
|
:visible="popOverVisible.reject"
|
||||||
placement="top"
|
placement="top"
|
||||||
:popper-style="{ minWidth: '400px' }"
|
:popper-style="{ minWidth: '400px' }"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@@ -885,7 +876,7 @@ defineExpose({ loadTodoTask });
|
|||||||
|
|
||||||
<!-- 【抄送】按钮 -->
|
<!-- 【抄送】按钮 -->
|
||||||
<ElPopover
|
<ElPopover
|
||||||
v-model:visible="popOverVisible.copy"
|
:visible="popOverVisible.copy"
|
||||||
placement="top"
|
placement="top"
|
||||||
:popper-style="{ width: '400px' }"
|
:popper-style="{ width: '400px' }"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@@ -960,7 +951,7 @@ defineExpose({ loadTodoTask });
|
|||||||
|
|
||||||
<!-- 【转办】按钮 -->
|
<!-- 【转办】按钮 -->
|
||||||
<ElPopover
|
<ElPopover
|
||||||
v-model:visible="popOverVisible.transfer"
|
:visible="popOverVisible.transfer"
|
||||||
placement="top"
|
placement="top"
|
||||||
:popper-style="{ width: '400px' }"
|
:popper-style="{ width: '400px' }"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@@ -1036,7 +1027,7 @@ defineExpose({ loadTodoTask });
|
|||||||
|
|
||||||
<!-- 【委派】按钮 -->
|
<!-- 【委派】按钮 -->
|
||||||
<ElPopover
|
<ElPopover
|
||||||
v-model:visible="popOverVisible.delegate"
|
:visible="popOverVisible.delegate"
|
||||||
placement="top"
|
placement="top"
|
||||||
:popper-style="{ width: '400px' }"
|
:popper-style="{ width: '400px' }"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@@ -1112,7 +1103,7 @@ defineExpose({ loadTodoTask });
|
|||||||
|
|
||||||
<!-- 【加签】按钮 当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
|
<!-- 【加签】按钮 当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
|
||||||
<ElPopover
|
<ElPopover
|
||||||
v-model:visible="popOverVisible.addSign"
|
:visible="popOverVisible.addSign"
|
||||||
placement="top"
|
placement="top"
|
||||||
:popper-style="{ width: '400px' }"
|
:popper-style="{ width: '400px' }"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@@ -1200,7 +1191,7 @@ defineExpose({ loadTodoTask });
|
|||||||
|
|
||||||
<!-- 【减签】按钮 -->
|
<!-- 【减签】按钮 -->
|
||||||
<ElPopover
|
<ElPopover
|
||||||
v-model:visible="popOverVisible.deleteSign"
|
:visible="popOverVisible.deleteSign"
|
||||||
placement="top"
|
placement="top"
|
||||||
:popper-style="{ width: '400px' }"
|
:popper-style="{ width: '400px' }"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@@ -1268,7 +1259,7 @@ defineExpose({ loadTodoTask });
|
|||||||
|
|
||||||
<!-- 【退回】按钮 -->
|
<!-- 【退回】按钮 -->
|
||||||
<ElPopover
|
<ElPopover
|
||||||
v-model:visible="popOverVisible.return"
|
:visible="popOverVisible.return"
|
||||||
placement="top"
|
placement="top"
|
||||||
:popper-style="{ width: '400px' }"
|
:popper-style="{ width: '400px' }"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@@ -1342,7 +1333,7 @@ defineExpose({ loadTodoTask });
|
|||||||
|
|
||||||
<!--【取消】按钮 这个对应发起人的取消, 只有发起人可以取消 -->
|
<!--【取消】按钮 这个对应发起人的取消, 只有发起人可以取消 -->
|
||||||
<ElPopover
|
<ElPopover
|
||||||
v-model:visible="popOverVisible.cancel"
|
:visible="popOverVisible.cancel"
|
||||||
placement="top"
|
placement="top"
|
||||||
:popper-style="{ width: '460px' }"
|
:popper-style="{ width: '460px' }"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
|
|||||||
@@ -35,30 +35,30 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal title="流程签名" class="w-3/5">
|
<Modal title="流程签名" class="w-3/5">
|
||||||
<div class="mb-2 flex justify-end">
|
<div class="flex h-[50vh] flex-col">
|
||||||
<ElSpace>
|
<div class="mb-2 flex justify-end">
|
||||||
<ElTooltip content="撤销上一步操作">
|
<ElTooltip content="撤销上一步操作">
|
||||||
<ElButton @click="signature?.undo()">
|
<ElButton @click="signature?.undo()" size="small">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconifyIcon icon="lucide:undo" class="mb-1 size-4" />
|
<IconifyIcon icon="lucide:undo" class="size-4" />
|
||||||
</template>
|
</template>
|
||||||
撤销
|
撤销
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</ElTooltip>
|
</ElTooltip>
|
||||||
<ElTooltip content="清空画布">
|
<ElTooltip content="清空画布">
|
||||||
<ElButton @click="signature?.clear()">
|
<ElButton @click="signature?.clear()" size="small">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconifyIcon icon="lucide:trash" class="mb-1 size-4" />
|
<IconifyIcon icon="lucide:trash" class="size-4" />
|
||||||
</template>
|
</template>
|
||||||
<span>清除</span>
|
<span>清除</span>
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</ElTooltip>
|
</ElTooltip>
|
||||||
</ElSpace>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Vue3Signature
|
<Vue3Signature
|
||||||
class="mx-auto !h-80 border border-solid border-gray-300"
|
class="h-full flex-1 border border-solid border-gray-300"
|
||||||
ref="signature"
|
ref="signature"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||||||
field: 'approver',
|
field: 'approver',
|
||||||
title: '审批人',
|
title: '审批人',
|
||||||
slots: {
|
slots: {
|
||||||
default: ({ row }: { row: BpmTaskApi.TaskManager }) => {
|
default: ({ row }: { row: BpmTaskApi.Task }) => {
|
||||||
return row.assigneeUser?.nickname || row.ownerUser?.nickname;
|
return row.assigneeUser?.nickname || row.ownerUser?.nickname;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -104,7 +104,7 @@ function handleRefresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 显示表单详情 */
|
/** 显示表单详情 */
|
||||||
async function handleShowFormDetail(row: BpmTaskApi.TaskManager) {
|
async function handleShowFormDetail(row: BpmTaskApi.Task) {
|
||||||
// 设置表单配置和表单字段
|
// 设置表单配置和表单字段
|
||||||
taskForm.value = {
|
taskForm.value = {
|
||||||
rule: [],
|
rule: [],
|
||||||
@@ -156,7 +156,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
toolbarConfig: {
|
toolbarConfig: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
} as VxeTableGridOptions<BpmTaskApi.TaskManager>,
|
} as VxeTableGridOptions<BpmTaskApi.Task>,
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@@ -186,7 +186,7 @@ defineExpose({
|
|||||||
</template>
|
</template>
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</div>
|
||||||
<Modal class="w-[800px]">
|
<Modal class="w-3/5">
|
||||||
<FormCreate
|
<FormCreate
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
v-model="taskForm.value"
|
v-model="taskForm.value"
|
||||||
|
|||||||
@@ -8,22 +8,22 @@ import { z } from '#/adapter/form';
|
|||||||
|
|
||||||
export const EVENT_EXECUTION_OPTIONS = [
|
export const EVENT_EXECUTION_OPTIONS = [
|
||||||
{
|
{
|
||||||
label: 'start',
|
label: '开始',
|
||||||
value: 'start',
|
value: 'start',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'end',
|
label: '结束',
|
||||||
value: 'end',
|
value: 'end',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const EVENT_OPTIONS = [
|
export const EVENT_OPTIONS = [
|
||||||
{ label: 'create', value: 'create' },
|
{ label: '创建', value: 'create' },
|
||||||
{ label: 'assignment', value: 'assignment' },
|
{ label: '指派', value: 'assignment' },
|
||||||
{ label: 'complete', value: 'complete' },
|
{ label: '完成', value: 'complete' },
|
||||||
{ label: 'delete', value: 'delete' },
|
{ label: '删除', value: 'delete' },
|
||||||
{ label: 'update', value: 'update' },
|
{ label: '更新', value: 'update' },
|
||||||
{ label: 'timeout', value: 'timeout' },
|
{ label: '超时', value: 'timeout' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 新增/修改的表单 */
|
/** 新增/修改的表单 */
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { useGridColumns, useGridFormSchema } from './data';
|
|||||||
defineOptions({ name: 'BpmDoneTask' });
|
defineOptions({ name: 'BpmDoneTask' });
|
||||||
|
|
||||||
/** 查看历史 */
|
/** 查看历史 */
|
||||||
function handleHistory(row: BpmTaskApi.TaskManager) {
|
function handleHistory(row: BpmTaskApi.Task) {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'BpmProcessInstanceDetail',
|
name: 'BpmProcessInstanceDetail',
|
||||||
query: {
|
query: {
|
||||||
@@ -26,7 +26,7 @@ function handleHistory(row: BpmTaskApi.TaskManager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 撤回任务 */
|
/** 撤回任务 */
|
||||||
async function handleWithdraw(row: BpmTaskApi.TaskManager) {
|
async function handleWithdraw(row: BpmTaskApi.Task) {
|
||||||
const loadingInstance = ElLoading.service({
|
const loadingInstance = ElLoading.service({
|
||||||
text: '正在撤回中...',
|
text: '正在撤回中...',
|
||||||
});
|
});
|
||||||
@@ -66,7 +66,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
refresh: true,
|
refresh: true,
|
||||||
search: true,
|
search: true,
|
||||||
},
|
},
|
||||||
} as VxeTableGridOptions<BpmTaskApi.TaskManager>,
|
} as VxeTableGridOptions<BpmTaskApi.Task>,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useGridColumns, useGridFormSchema } from './data';
|
|||||||
defineOptions({ name: 'BpmManagerTask' });
|
defineOptions({ name: 'BpmManagerTask' });
|
||||||
|
|
||||||
/** 查看历史 */
|
/** 查看历史 */
|
||||||
function handleHistory(row: BpmTaskApi.TaskManager) {
|
function handleHistory(row: BpmTaskApi.Task) {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'BpmProcessInstanceDetail',
|
name: 'BpmProcessInstanceDetail',
|
||||||
query: {
|
query: {
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ function handleRowCheckboxChange({
|
|||||||
}: {
|
}: {
|
||||||
records: InfraDataSourceConfigApi.DataSourceConfig[];
|
records: InfraDataSourceConfigApi.DataSourceConfig[];
|
||||||
}) {
|
}) {
|
||||||
checkedIds.value = records.map((item) => item.id!);
|
// 过滤掉id为 0 的主数据源
|
||||||
|
checkedIds.value = records.map((item) => item.id!).filter((id) => id !== 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
@@ -138,6 +139,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
link: true,
|
link: true,
|
||||||
icon: ACTION_ICON.EDIT,
|
icon: ACTION_ICON.EDIT,
|
||||||
auth: ['infra:data-source-config:update'],
|
auth: ['infra:data-source-config:update'],
|
||||||
|
disabled: row.id === 0,
|
||||||
onClick: handleEdit.bind(null, row),
|
onClick: handleEdit.bind(null, row),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -146,6 +148,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
link: true,
|
link: true,
|
||||||
icon: ACTION_ICON.DELETE,
|
icon: ACTION_ICON.DELETE,
|
||||||
auth: ['infra:data-source-config:delete'],
|
auth: ['infra:data-source-config:delete'],
|
||||||
|
disabled: row.id === 0,
|
||||||
popConfirm: {
|
popConfirm: {
|
||||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||||
confirm: handleDelete.bind(null, row),
|
confirm: handleDelete.bind(null, row),
|
||||||
|
|||||||
@@ -225,6 +225,18 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
},
|
},
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'config.region',
|
||||||
|
label: '区域',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请填写区域,一般仅 AWS 需要填写',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['storage'],
|
||||||
|
show: (formValues) => formValues.storage === 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
// 通用
|
// 通用
|
||||||
{
|
{
|
||||||
fieldName: 'config.domain',
|
fieldName: 'config.domain',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const props = defineProps<{
|
|||||||
takeType?: number; // 领取方式
|
takeType?: number; // 领取方式
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
// TODO @puhui999:这个也要调整,和 antd 保持统一。
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
|||||||
@@ -145,5 +145,6 @@ async function processLoadData(
|
|||||||
<template>
|
<template>
|
||||||
<Modal :title="getTitle" class="w-2/5">
|
<Modal :title="getTitle" class="w-2/5">
|
||||||
<Form class="mx-4" />
|
<Form class="mx-4" />
|
||||||
|
<!-- TODO @puhui999:这里需要同步下 -->
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { MallDiyPageApi } from '#/api/mall/promotion/diy/page';
|
|||||||
import type { MallDiyTemplateApi } from '#/api/mall/promotion/diy/template';
|
import type { MallDiyTemplateApi } from '#/api/mall/promotion/diy/template';
|
||||||
import type { DiyComponentLibrary } from '#/views/mall/promotion/components'; // 商城的 DIY 组件,在 DiyEditor 目录下
|
import type { DiyComponentLibrary } from '#/views/mall/promotion/components'; // 商城的 DIY 组件,在 DiyEditor 目录下
|
||||||
|
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import { useTabs } from '@vben/hooks';
|
import { useTabs } from '@vben/hooks';
|
||||||
@@ -35,7 +35,7 @@ const { refreshTab } = useTabs();
|
|||||||
const DIY_PAGE_INDEX_KEY = 'diy_page_index'; // 特殊:存储 reset 重置时,当前 selectedTemplateItem 值,从而进行恢复
|
const DIY_PAGE_INDEX_KEY = 'diy_page_index'; // 特殊:存储 reset 重置时,当前 selectedTemplateItem 值,从而进行恢复
|
||||||
|
|
||||||
const selectedTemplateItem = ref(0);
|
const selectedTemplateItem = ref(0);
|
||||||
const templateItems = reactive([
|
const templateItems = ref([
|
||||||
{ name: '基础设置', icon: 'ep:iphone' },
|
{ name: '基础设置', icon: 'ep:iphone' },
|
||||||
{ name: '首页', icon: 'ep:home-filled' },
|
{ name: '首页', icon: 'ep:home-filled' },
|
||||||
{ name: '我的', icon: 'ep:user-filled' },
|
{ name: '我的', icon: 'ep:user-filled' },
|
||||||
@@ -77,11 +77,13 @@ async function getPageDetail(id: any) {
|
|||||||
function handleTemplateItemChange(val: any) {
|
function handleTemplateItemChange(val: any) {
|
||||||
// 缓存模版编辑数据
|
// 缓存模版编辑数据
|
||||||
currentFormDataMap.value.set(
|
currentFormDataMap.value.set(
|
||||||
templateItems[selectedTemplateItem.value]?.name || '',
|
templateItems.value[selectedTemplateItem.value]?.name || '',
|
||||||
currentFormData.value!,
|
currentFormData.value!,
|
||||||
);
|
);
|
||||||
// 读取模版缓存
|
// 读取模版缓存
|
||||||
const data = currentFormDataMap.value.get(templateItems[val]?.name || '');
|
const data = currentFormDataMap.value.get(
|
||||||
|
templateItems.value[val]?.name || '',
|
||||||
|
);
|
||||||
|
|
||||||
// 切换模版
|
// 切换模版
|
||||||
selectedTemplateItem.value = val;
|
selectedTemplateItem.value = val;
|
||||||
@@ -101,7 +103,7 @@ function handleTemplateItemChange(val: any) {
|
|||||||
isEmpty(data)
|
isEmpty(data)
|
||||||
? formData.value!.pages.find(
|
? formData.value!.pages.find(
|
||||||
(page: MallDiyPageApi.DiyPage) =>
|
(page: MallDiyPageApi.DiyPage) =>
|
||||||
page.name === templateItems[val]?.name,
|
page.name === templateItems.value[val]?.name,
|
||||||
)
|
)
|
||||||
: data
|
: data
|
||||||
) as MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty;
|
) as MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty;
|
||||||
@@ -114,7 +116,7 @@ async function submitForm() {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
// 对所有的 templateItems 都进行保存,有缓存则保存缓存,解决都有修改时只保存了当前所编辑的 templateItem,导致装修效果存在差异
|
// 对所有的 templateItems 都进行保存,有缓存则保存缓存,解决都有修改时只保存了当前所编辑的 templateItem,导致装修效果存在差异
|
||||||
for (const [i, templateItem] of templateItems.entries()) {
|
for (const [i, templateItem] of templateItems.value.entries()) {
|
||||||
const data = currentFormDataMap.value.get(templateItem.name) as any;
|
const data = currentFormDataMap.value.get(templateItem.name) as any;
|
||||||
// 情况一:基础设置
|
// 情况一:基础设置
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
@@ -188,7 +190,7 @@ onMounted(async () => {
|
|||||||
:show-navigation-bar="selectedTemplateItem !== 0"
|
:show-navigation-bar="selectedTemplateItem !== 0"
|
||||||
:show-page-config="selectedTemplateItem !== 0"
|
:show-page-config="selectedTemplateItem !== 0"
|
||||||
:show-tab-bar="selectedTemplateItem === 0"
|
:show-tab-bar="selectedTemplateItem === 0"
|
||||||
:title="templateItems[selectedTemplateItem]?.name || ''"
|
:title="templateItems[selectedTemplateItem]?.name ?? ''"
|
||||||
@reset="handleEditorReset"
|
@reset="handleEditorReset"
|
||||||
@save="submitForm"
|
@save="submitForm"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,26 +1,19 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MpMaterialApi } from '#/api/mp/material';
|
||||||
|
|
||||||
|
import { reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { NewsType } from '@vben/constants';
|
import { NewsType } from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { formatTime } from '@vben/utils';
|
|
||||||
|
|
||||||
import {
|
import { ElButton, ElPagination, ElRow } from 'element-plus';
|
||||||
ElButton,
|
|
||||||
ElPagination,
|
|
||||||
ElRow,
|
|
||||||
ElTable,
|
|
||||||
ElTableColumn,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
import * as MpDraftApi from '#/api/mp/draft';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import * as MpFreePublishApi from '#/api/mp/freePublish';
|
import { getDraftPage } from '#/api/mp/draft';
|
||||||
import * as MpMaterialApi from '#/api/mp/material';
|
import { getFreePublishPage } from '#/api/mp/freePublish';
|
||||||
import News from '#/views/mp/components/wx-news/wx-news.vue';
|
import { getMaterialPage } from '#/api/mp/material';
|
||||||
import VideoPlayer from '#/views/mp/components/wx-video-play/wx-video-play.vue';
|
import { WxNews, WxVideoPlayer, WxVoicePlayer } from '#/views/mp/components';
|
||||||
import VoicePlayer from '#/views/mp/components/wx-voice-play/wx-voice-play.vue';
|
|
||||||
|
|
||||||
// TODO @hw:代码风格,看看 antd 和 ele 是不是统一下; 等antd此组件修改完再调整
|
|
||||||
|
|
||||||
/** 微信素材选择 */
|
/** 微信素材选择 */
|
||||||
defineOptions({ name: 'WxMaterialSelect' });
|
defineOptions({ name: 'WxMaterialSelect' });
|
||||||
@@ -49,33 +42,163 @@ const queryParams = reactive({
|
|||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
}); // 查询参数
|
}); // 查询参数
|
||||||
|
|
||||||
/** 选择素材 */
|
const voiceGridColumns: VxeTableGridOptions<MpMaterialApi.Material>['columns'] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
field: 'mediaId',
|
||||||
|
title: '编号',
|
||||||
|
align: 'center',
|
||||||
|
minWidth: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '文件名',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'voice',
|
||||||
|
title: '语音',
|
||||||
|
minWidth: 200,
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'voice' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '上传时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 140,
|
||||||
|
fixed: 'right',
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const videoGridColumns: VxeTableGridOptions<MpMaterialApi.Material>['columns'] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
field: 'mediaId',
|
||||||
|
title: '编号',
|
||||||
|
minWidth: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '文件名',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'title',
|
||||||
|
title: '标题',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'introduction',
|
||||||
|
title: '介绍',
|
||||||
|
minWidth: 220,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'video',
|
||||||
|
title: '视频',
|
||||||
|
minWidth: 220,
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'video' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '上传时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 140,
|
||||||
|
fixed: 'right',
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const [VoiceGrid, voiceGridApi] = useVbenVxeGrid({
|
||||||
|
gridOptions: {
|
||||||
|
border: true,
|
||||||
|
columns: voiceGridColumns,
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: true,
|
||||||
|
pageSize: 10,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, { accountId }) => {
|
||||||
|
const finalAccountId = accountId ?? queryParams.accountId;
|
||||||
|
if (!finalAccountId) {
|
||||||
|
return { list: [], total: 0 };
|
||||||
|
}
|
||||||
|
return await getMaterialPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
accountId: finalAccountId,
|
||||||
|
type: 'voice',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'mediaId',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<MpMaterialApi.Material>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [VideoGrid, videoGridApi] = useVbenVxeGrid({
|
||||||
|
gridOptions: {
|
||||||
|
border: true,
|
||||||
|
columns: videoGridColumns,
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: true,
|
||||||
|
pageSize: 10,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, { accountId }) => {
|
||||||
|
const finalAccountId = accountId ?? queryParams.accountId;
|
||||||
|
if (finalAccountId === undefined || finalAccountId === null) {
|
||||||
|
return { list: [], total: 0 };
|
||||||
|
}
|
||||||
|
return await getMaterialPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
accountId: finalAccountId,
|
||||||
|
type: 'video',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'mediaId',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<MpMaterialApi.Material>,
|
||||||
|
});
|
||||||
|
|
||||||
function selectMaterialFun(item: any) {
|
function selectMaterialFun(item: any) {
|
||||||
emit('selectMaterial', item);
|
emit('selectMaterial', item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取分页数据 */
|
|
||||||
async function getPage() {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
if (props.type === 'news' && props.newsType === NewsType.Published) {
|
|
||||||
// 【图文】+ 【已发布】
|
|
||||||
await getFreePublishPageFun();
|
|
||||||
} else if (props.type === 'news' && props.newsType === NewsType.Draft) {
|
|
||||||
// 【图文】+ 【草稿】
|
|
||||||
await getDraftPageFun();
|
|
||||||
} else {
|
|
||||||
// 【素材】
|
|
||||||
await getMaterialPageFun();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取素材分页 */
|
|
||||||
async function getMaterialPageFun() {
|
async function getMaterialPageFun() {
|
||||||
const data = await MpMaterialApi.getMaterialPage({
|
const data = await getMaterialPage({
|
||||||
...queryParams,
|
...queryParams,
|
||||||
type: props.type,
|
type: props.type,
|
||||||
});
|
});
|
||||||
@@ -83,9 +206,8 @@ async function getMaterialPageFun() {
|
|||||||
total.value = data.total;
|
total.value = data.total;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取已发布图文分页 */
|
|
||||||
async function getFreePublishPageFun() {
|
async function getFreePublishPageFun() {
|
||||||
const data = await MpFreePublishApi.getFreePublishPage(queryParams);
|
const data = await getFreePublishPage(queryParams);
|
||||||
data.list.forEach((item: any) => {
|
data.list.forEach((item: any) => {
|
||||||
const articles = item.content.newsItem;
|
const articles = item.content.newsItem;
|
||||||
articles.forEach((article: any) => {
|
articles.forEach((article: any) => {
|
||||||
@@ -96,9 +218,8 @@ async function getFreePublishPageFun() {
|
|||||||
total.value = data.total;
|
total.value = data.total;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取草稿图文分页 */
|
|
||||||
async function getDraftPageFun() {
|
async function getDraftPageFun() {
|
||||||
const data = await MpDraftApi.getDraftPage(queryParams);
|
const data = await getDraftPage(queryParams);
|
||||||
data.list.forEach((draft: any) => {
|
data.list.forEach((draft: any) => {
|
||||||
const articles = draft.content.newsItem;
|
const articles = draft.content.newsItem;
|
||||||
articles.forEach((article: any) => {
|
articles.forEach((article: any) => {
|
||||||
@@ -109,9 +230,57 @@ async function getDraftPageFun() {
|
|||||||
total.value = data.total;
|
total.value = data.total;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
async function getPage() {
|
||||||
getPage();
|
if (props.type === 'voice') {
|
||||||
});
|
await voiceGridApi.reload({ accountId: queryParams.accountId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (props.type === 'video') {
|
||||||
|
await videoGridApi.reload({ accountId: queryParams.accountId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (props.type === 'news' && props.newsType === NewsType.Published) {
|
||||||
|
await getFreePublishPageFun();
|
||||||
|
} else if (props.type === 'news' && props.newsType === NewsType.Draft) {
|
||||||
|
await getDraftPageFun();
|
||||||
|
} else {
|
||||||
|
await getMaterialPageFun();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.accountId,
|
||||||
|
(accountId) => {
|
||||||
|
queryParams.accountId = accountId;
|
||||||
|
queryParams.pageNo = 1;
|
||||||
|
getPage();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.type,
|
||||||
|
() => {
|
||||||
|
queryParams.pageNo = 1;
|
||||||
|
getPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.newsType,
|
||||||
|
() => {
|
||||||
|
if (props.type === 'news') {
|
||||||
|
queryParams.pageNo = 1;
|
||||||
|
getPage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -152,90 +321,31 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<!-- 类型:voice -->
|
<!-- 类型:voice -->
|
||||||
<div v-else-if="props.type === 'voice'">
|
<div v-else-if="props.type === 'voice'">
|
||||||
<!-- 列表 -->
|
<VoiceGrid>
|
||||||
<ElTable v-loading="loading" :data="list">
|
<template #voice="{ row }">
|
||||||
<ElTableColumn label="编号" align="center" prop="mediaId" />
|
<WxVoicePlayer :url="row.url" />
|
||||||
<ElTableColumn label="文件名" align="center" prop="name" />
|
</template>
|
||||||
<ElTableColumn label="语音" align="center">
|
<template #actions="{ row }">
|
||||||
<template #default="scope">
|
<ElButton type="primary" link @click="selectMaterialFun(row)">
|
||||||
<VoicePlayer :url="scope.row.url" />
|
选择
|
||||||
</template>
|
<IconifyIcon icon="lucide:plus" />
|
||||||
</ElTableColumn>
|
</ElButton>
|
||||||
<ElTableColumn
|
</template>
|
||||||
label="上传时间"
|
</VoiceGrid>
|
||||||
align="center"
|
|
||||||
prop="createTime"
|
|
||||||
width="180"
|
|
||||||
:formatter="
|
|
||||||
(row: any) => formatTime(row.createTime, 'YYYY-MM-DD HH:mm:ss')
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<ElTableColumn label="操作" align="center" fixed="right">
|
|
||||||
<template #default="scope">
|
|
||||||
<ElButton type="primary" link @click="selectMaterialFun(scope.row)">
|
|
||||||
选择
|
|
||||||
<IconifyIcon icon="lucide:plus" />
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
</ElTable>
|
|
||||||
<!-- 分页组件 -->
|
|
||||||
<ElPagination
|
|
||||||
background
|
|
||||||
layout="prev, pager, next, sizes, total"
|
|
||||||
:total="total"
|
|
||||||
v-model:current-page="queryParams.pageNo"
|
|
||||||
v-model:page-size="queryParams.pageSize"
|
|
||||||
@current-change="getPage"
|
|
||||||
@size-change="getPage"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 类型:video -->
|
<!-- 类型:video -->
|
||||||
<div v-else-if="props.type === 'video'">
|
<div v-else-if="props.type === 'video'">
|
||||||
<!-- 列表 -->
|
<VideoGrid>
|
||||||
<ElTable v-loading="loading" :data="list">
|
<template #video="{ row }">
|
||||||
<ElTableColumn label="编号" align="center" prop="mediaId" />
|
<WxVideoPlayer :url="row.url" />
|
||||||
<ElTableColumn label="文件名" align="center" prop="name" />
|
</template>
|
||||||
<ElTableColumn label="标题" align="center" prop="title" />
|
<template #actions="{ row }">
|
||||||
<ElTableColumn label="介绍" align="center" prop="introduction" />
|
<ElButton type="primary" link @click="selectMaterialFun(row)">
|
||||||
<ElTableColumn label="视频" align="center">
|
选择
|
||||||
<template #default="scope">
|
<IconifyIcon icon="lucide:circle-plus" />
|
||||||
<VideoPlayer :url="scope.row.url" />
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</VideoGrid>
|
||||||
<ElTableColumn
|
|
||||||
label="上传时间"
|
|
||||||
align="center"
|
|
||||||
prop="createTime"
|
|
||||||
width="180"
|
|
||||||
:formatter="
|
|
||||||
(row: any) => formatTime(row.createTime, 'YYYY-MM-DD HH:mm:ss')
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<ElTableColumn
|
|
||||||
label="操作"
|
|
||||||
align="center"
|
|
||||||
fixed="right"
|
|
||||||
class-name="small-padding fixed-width"
|
|
||||||
>
|
|
||||||
<template #default="scope">
|
|
||||||
<ElButton type="primary" link @click="selectMaterialFun(scope.row)">
|
|
||||||
选择
|
|
||||||
<IconifyIcon icon="lucide:circle-plus" />
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
</ElTable>
|
|
||||||
<!-- 分页组件 -->
|
|
||||||
<ElPagination
|
|
||||||
background
|
|
||||||
layout="prev, pager, next, sizes, total"
|
|
||||||
:total="total"
|
|
||||||
v-model:current-page="queryParams.pageNo"
|
|
||||||
v-model:page-size="queryParams.pageSize"
|
|
||||||
@current-change="getMaterialPageFun"
|
|
||||||
@size-change="getMaterialPageFun"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 类型:news -->
|
<!-- 类型:news -->
|
||||||
<div v-else-if="props.type === 'news'">
|
<div v-else-if="props.type === 'news'">
|
||||||
@@ -249,7 +359,7 @@ onMounted(async () => {
|
|||||||
:key="item.mediaId"
|
:key="item.mediaId"
|
||||||
>
|
>
|
||||||
<div v-if="item.content && item.content.newsItem">
|
<div v-if="item.content && item.content.newsItem">
|
||||||
<News :articles="item.content.newsItem" />
|
<WxNews :articles="item.content.newsItem" />
|
||||||
<ElRow class="flex justify-center pt-2.5">
|
<ElRow class="flex justify-center pt-2.5">
|
||||||
<ElButton type="success" @click="selectMaterialFun(item)">
|
<ElButton type="success" @click="selectMaterialFun(item)">
|
||||||
选择
|
选择
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ defineOptions({ name: 'TabNews' });
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: Reply;
|
modelValue: Reply;
|
||||||
newsType: NewsType;
|
newsType?: NewsType;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -74,4 +74,3 @@ export const useBeforeUpload = (type: UploadType, maxSizeMB?: number) => {
|
|||||||
|
|
||||||
return fn;
|
return fn;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const total = ref(0); // 总条数
|
|||||||
const accountId = ref(-1);
|
const accountId = ref(-1);
|
||||||
provide('accountId', accountId);
|
provide('accountId', accountId);
|
||||||
|
|
||||||
|
// TODO @AI:这里是不是应该都用 grid;类似 yudao-ui-admin-vben-v5/apps/web-ele/src/views/mp/autoReply/index.vue
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
accountId,
|
accountId,
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
|
|
||||||
const props = defineProps<{ type: UploadType }>();
|
const props = defineProps<{ type: UploadType }>();
|
||||||
|
|
||||||
|
// TODO @dylan:是不是要和 antd 的 props 定义相同哈?这样后续两侧维护方便点
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
uploaded: [v: void];
|
uploaded: [v: void];
|
||||||
}>();
|
}>();
|
||||||
@@ -59,6 +60,7 @@ const customRequest: UploadProps['httpRequest'] = async function (options) {
|
|||||||
|
|
||||||
if (res.code !== 0) {
|
if (res.code !== 0) {
|
||||||
ElMessage.error(`上传出错:${res.msg}`);
|
ElMessage.error(`上传出错:${res.msg}`);
|
||||||
|
// TODO @dylan:这里有个 linter 错误。
|
||||||
onError?.(new Error(res.msg));
|
onError?.(new Error(res.msg));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -104,4 +106,3 @@ const customRequest: UploadProps['httpRequest'] = async function (options) {
|
|||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
|
|
||||||
import { beforeVideoUpload, HEADERS, UPLOAD_URL, UploadType } from './upload';
|
import { beforeVideoUpload, HEADERS, UPLOAD_URL, UploadType } from './upload';
|
||||||
|
|
||||||
|
// TODO @dylan:是不是要和 antd 的 props 定义相同哈?这样后续两侧维护方便点
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
modelValue?: boolean;
|
modelValue?: boolean;
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import type { MpMaterialApi } from '#/api/mp/material';
|
|||||||
|
|
||||||
import { nextTick, onMounted, watch } from 'vue';
|
import { nextTick, onMounted, watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import { useImageGridColumns } from './data';
|
import { useImageGridColumns } from './data';
|
||||||
@@ -89,9 +91,9 @@ onMounted(async () => {
|
|||||||
<TableAction
|
<TableAction
|
||||||
:actions="[
|
:actions="[
|
||||||
{
|
{
|
||||||
label: '删除',
|
label: $t('common.delete'),
|
||||||
type: 'link',
|
type: 'primary',
|
||||||
danger: true,
|
link: true,
|
||||||
icon: ACTION_ICON.DELETE,
|
icon: ACTION_ICON.DELETE,
|
||||||
auth: ['mp:material:delete'],
|
auth: ['mp:material:delete'],
|
||||||
popConfirm: {
|
popConfirm: {
|
||||||
|
|||||||
@@ -44,4 +44,3 @@ export {
|
|||||||
type UploadData,
|
type UploadData,
|
||||||
UploadType,
|
UploadType,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { MpMaterialApi } from '#/api/mp/material';
|
|||||||
|
|
||||||
import { watch } from 'vue';
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
import { openWindow } from '@vben/utils';
|
import { openWindow } from '@vben/utils';
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
@@ -76,14 +77,15 @@ watch(
|
|||||||
:actions="[
|
:actions="[
|
||||||
{
|
{
|
||||||
label: '下载',
|
label: '下载',
|
||||||
type: 'link',
|
type: 'primary',
|
||||||
|
link: true,
|
||||||
icon: ACTION_ICON.DOWNLOAD,
|
icon: ACTION_ICON.DOWNLOAD,
|
||||||
onClick: () => openWindow(row.url),
|
onClick: () => openWindow(row.url),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '删除',
|
label: $t('common.delete'),
|
||||||
type: 'link',
|
type: 'danger',
|
||||||
danger: true,
|
link: true,
|
||||||
icon: ACTION_ICON.DELETE,
|
icon: ACTION_ICON.DELETE,
|
||||||
auth: ['mp:material:delete'],
|
auth: ['mp:material:delete'],
|
||||||
popConfirm: {
|
popConfirm: {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { MpMaterialApi } from '#/api/mp/material';
|
|||||||
|
|
||||||
import { watch } from 'vue';
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
import { openWindow } from '@vben/utils';
|
import { openWindow } from '@vben/utils';
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
@@ -76,14 +77,15 @@ watch(
|
|||||||
:actions="[
|
:actions="[
|
||||||
{
|
{
|
||||||
label: '下载',
|
label: '下载',
|
||||||
type: 'link',
|
type: 'primary',
|
||||||
|
link: true,
|
||||||
icon: ACTION_ICON.DOWNLOAD,
|
icon: ACTION_ICON.DOWNLOAD,
|
||||||
onClick: () => openWindow(row.url),
|
onClick: () => openWindow(row.url),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '删除',
|
label: $t('common.delete'),
|
||||||
type: 'link',
|
type: 'danger',
|
||||||
danger: true,
|
link: true,
|
||||||
icon: ACTION_ICON.DELETE,
|
icon: ACTION_ICON.DELETE,
|
||||||
auth: ['mp:material:delete'],
|
auth: ['mp:material:delete'],
|
||||||
popConfirm: {
|
popConfirm: {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user