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 interface Task {
|
||||
id: number; // 编号
|
||||
name: string; // 监听器名字
|
||||
type: string; // 监听器类型
|
||||
status: number; // 监听器状态
|
||||
event: string; // 监听事件
|
||||
valueType: string; // 监听器值类型
|
||||
processInstance?: BpmProcessInstanceApi.ProcessInstance; // 流程实例
|
||||
id: string; // 编号
|
||||
name: string; // 任务名字
|
||||
status: number; // 任务状态
|
||||
createTime: number; // 创建时间
|
||||
endTime: number; // 结束时间
|
||||
durationInMillis: number; // 持续时间
|
||||
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;
|
||||
pathStyle?: boolean;
|
||||
enablePublicAccess?: boolean;
|
||||
region?: string;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,12 @@ export enum CodecTypeEnum {
|
||||
ALINK = 'Alink', // 阿里云 Alink 协议
|
||||
}
|
||||
|
||||
/** IOT 产品状态枚举类 */
|
||||
export enum ProductStatusEnum {
|
||||
UNPUBLISHED = 0, // 开发中
|
||||
PUBLISHED = 1, // 已发布
|
||||
}
|
||||
|
||||
/** 查询产品分页 */
|
||||
export function getProductPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<IotProductApi.Product>>(
|
||||
|
||||
@@ -21,6 +21,7 @@ export namespace MallCombinationActivityApi {
|
||||
limitDuration?: number; // 限制时长
|
||||
combinationPrice?: number; // 拼团价格
|
||||
products: CombinationProduct[]; // 商品列表
|
||||
picUrl?: any;
|
||||
}
|
||||
|
||||
/** 拼团活动所需属性 */
|
||||
|
||||
@@ -31,6 +31,7 @@ export namespace MallSeckillActivityApi {
|
||||
totalStock?: number; // 秒杀总库存
|
||||
seckillPrice?: number; // 秒杀价格
|
||||
products?: SeckillProduct[]; // 秒杀商品列表
|
||||
picUrl?: any;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export namespace SystemSocialClientApi {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
agentId?: string;
|
||||
publicKey?: string;
|
||||
status: number;
|
||||
createTime?: Date;
|
||||
}
|
||||
|
||||
@@ -9,24 +9,6 @@ const routes: RouteRecordRaw[] = [
|
||||
hideInMenu: true,
|
||||
},
|
||||
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',
|
||||
component: () => import('#/views/bpm/processInstance/detail/index.vue'),
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
'Append Gateway': '追加网关',
|
||||
'Append Task': '追加任务',
|
||||
'Append Intermediate/Boundary Event': '追加中间抛出事件/边界事件',
|
||||
|
||||
TextAnnotation: '文本注释',
|
||||
'Activate the global connect tool': '激活全局连接工具',
|
||||
'Append {type}': '添加 {type}',
|
||||
'Add Lane above': '在上面添加道',
|
||||
@@ -31,10 +31,16 @@ export default {
|
||||
'Create expanded SubProcess': '创建扩展子过程',
|
||||
'Create IntermediateThrowEvent/BoundaryEvent': '创建中间抛出事件/边界事件',
|
||||
'Create Pool/Participant': '创建池/参与者',
|
||||
'Parallel Multi Instance': '并行多重事件',
|
||||
'Sequential Multi Instance': '时序多重事件',
|
||||
'Participant Multiplicity': '参与者多重性',
|
||||
'Empty pool/participant (removes content)': '清空池/参与者(移除内容)',
|
||||
'Empty pool/participant': '收缩池/参与者',
|
||||
'Expanded pool/participant': '展开池/参与者',
|
||||
'Parallel Multi-Instance': '并行多重事件',
|
||||
'Sequential Multi-Instance': '时序多重事件',
|
||||
DataObjectReference: '数据对象参考',
|
||||
DataStoreReference: '数据存储参考',
|
||||
'Data object reference': '数据对象引用 ',
|
||||
'Data store reference': '数据存储引用 ',
|
||||
Loop: '循环',
|
||||
'Ad-hoc': '即席',
|
||||
'Create {type}': '创建 {type}',
|
||||
@@ -49,6 +55,9 @@ export default {
|
||||
'Call Activity': '调用活动',
|
||||
'Sub-Process (collapsed)': '子流程(折叠的)',
|
||||
'Sub-Process (expanded)': '子流程(展开的)',
|
||||
'Ad-hoc sub-process': '即席子流程',
|
||||
'Ad-hoc sub-process (collapsed)': '即席子流程(折叠的)',
|
||||
'Ad-hoc sub-process (expanded)': '即席子流程(展开的)',
|
||||
'Start Event': '开始事件',
|
||||
StartEvent: '开始事件',
|
||||
'Intermediate Throw Event': '中间事件',
|
||||
@@ -111,10 +120,10 @@ export default {
|
||||
'Parallel Gateway': '并行网关',
|
||||
'Inclusive Gateway': '相容网关',
|
||||
'Complex Gateway': '复杂网关',
|
||||
'Event based Gateway': '事件网关',
|
||||
'Event-based Gateway': '事件网关',
|
||||
Transaction: '转运',
|
||||
'Sub Process': '子流程',
|
||||
'Event Sub Process': '事件子流程',
|
||||
'sub-process': '子流程',
|
||||
'Event sub-process': '事件子流程',
|
||||
'Collapsed Pool': '折叠池',
|
||||
'Expanded Pool': '展开池',
|
||||
|
||||
|
||||
@@ -390,8 +390,9 @@ watch(() => props.businessObject, syncFromBusinessObject, { deep: true });
|
||||
<template #extra>
|
||||
<IconifyIcon icon="ep:timer" />
|
||||
</template>
|
||||
<!-- 相关 issue:https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ICNRW2 -->
|
||||
<TimeEventConfig
|
||||
:business-object="bpmnElement.value?.businessObject"
|
||||
:business-object="elementBusinessObject"
|
||||
:key="elementId"
|
||||
/>
|
||||
</CollapsePanel>
|
||||
|
||||
@@ -75,8 +75,14 @@ const assignEmptyUserIds = ref<any>();
|
||||
|
||||
// 操作按钮
|
||||
const buttonsSettingEl = ref<any>();
|
||||
const { btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
|
||||
useButtonsSetting();
|
||||
const { btnDisplayNameEdit, changeBtnDisplayName } = 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[]>([]);
|
||||
@@ -172,7 +178,7 @@ const resetCustomConfigList = () => {
|
||||
});
|
||||
|
||||
// 操作按钮
|
||||
buttonsSettingEl.value = elExtensionElements.value.values?.find(
|
||||
buttonsSettingEl.value = elExtensionElements.value.values?.filter(
|
||||
(ex: any) => ex.$type === `${prefix}:ButtonsSetting`,
|
||||
);
|
||||
if (buttonsSettingEl.value.length === 0) {
|
||||
@@ -189,7 +195,7 @@ const resetCustomConfigList = () => {
|
||||
|
||||
// 字段权限
|
||||
if (formType.value === BpmModelFormType.NORMAL) {
|
||||
const fieldsPermissionList = elExtensionElements.value.values?.find(
|
||||
const fieldsPermissionList = elExtensionElements.value.values?.filter(
|
||||
(ex: any) => ex.$type === `${prefix}:FieldsPermission`,
|
||||
);
|
||||
fieldsPermissionEl.value = [];
|
||||
@@ -358,24 +364,14 @@ function useButtonsSetting() {
|
||||
const changeBtnDisplayName = (index: number) => {
|
||||
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 {
|
||||
buttonsSetting,
|
||||
btnDisplayNameEdit,
|
||||
changeBtnDisplayName,
|
||||
btnDisplayNameBlurEvent,
|
||||
};
|
||||
}
|
||||
|
||||
/** 批量更新权限 */
|
||||
// TODO @lesan:这个页面,有一些 idea 红色报错,咱要不要 fix 下!
|
||||
const updatePermission = (type: string) => {
|
||||
fieldsPermissionEl.value.forEach((field: any) => {
|
||||
if (type === 'READ') {
|
||||
@@ -532,7 +528,10 @@ onMounted(async () => {
|
||||
</Button>
|
||||
</div>
|
||||
<div class="button-setting-item-label">
|
||||
<Switch v-model:checked="item.enable" />
|
||||
<Switch
|
||||
v-model:checked="item.enable"
|
||||
@change="updateElementExtensions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,10 @@ const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||
|
||||
const resetListenersList = () => {
|
||||
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 =
|
||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||
(ex: any) => ex.$type === `${prefix}:ExecutionListener`,
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import { inject, nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { confirm, useVbenDrawer, useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Drawer,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
SelectOption,
|
||||
Table,
|
||||
TableColumn,
|
||||
} 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 { createListenerObject, updateElementExtensions } from '../../utils';
|
||||
@@ -40,59 +38,46 @@ interface Props {
|
||||
}
|
||||
|
||||
const prefix = inject<string>('prefix');
|
||||
const width = inject<number>('width');
|
||||
|
||||
const elementListenersList = ref<any[]>([]);
|
||||
const listenerEventTypeObject = ref(eventType);
|
||||
const listenerTypeObject = ref(listenerType);
|
||||
const listenerFormModelVisible = ref(false);
|
||||
const listenerForm = ref<any>({});
|
||||
const fieldTypeObject = ref(fieldType);
|
||||
const fieldsListOfListener = ref<any[]>([]);
|
||||
const listenerFieldFormModelVisible = ref(false); // 监听器 注入字段表单弹窗 显示状态
|
||||
const editingListenerIndex = ref(-1); // 监听器所在下标,-1 为新增
|
||||
const editingListenerFieldIndex = ref<any>(-1); // 字段所在下标,-1 为新增
|
||||
const listenerFieldForm = ref<any>({}); // 监听器 注入字段 详情表单
|
||||
const editingListenerIndex = ref(-1);
|
||||
const editingListenerFieldIndex = ref<any>(-1);
|
||||
const listenerFieldForm = ref<any>({});
|
||||
const bpmnElement = ref<any>();
|
||||
const bpmnElementListeners = ref<any[]>([]);
|
||||
const otherExtensionList = ref<any[]>([]);
|
||||
const listenerFormRef = ref<any>({});
|
||||
const listenerFieldFormRef = ref<any>({});
|
||||
|
||||
interface BpmnInstances {
|
||||
bpmnElement: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
bpmnInstances?: BpmnInstances;
|
||||
}
|
||||
}
|
||||
|
||||
const bpmnInstances = () => window.bpmnInstances;
|
||||
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||
|
||||
const resetListenersList = () => {
|
||||
// console.log(
|
||||
// bpmnInstances().bpmnElement,
|
||||
// 'window.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElement',
|
||||
// );
|
||||
bpmnElement.value = bpmnInstances()?.bpmnElement;
|
||||
otherExtensionList.value = [];
|
||||
otherExtensionList.value =
|
||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||
(ex: any) => ex.$type !== `${prefix}:TaskListener`,
|
||||
) ?? [];
|
||||
bpmnElementListeners.value =
|
||||
bpmnElement.value.businessObject?.extensionElements?.values.filter(
|
||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||
(ex: any) => ex.$type === `${prefix}:TaskListener`,
|
||||
) ?? [];
|
||||
elementListenersList.value = bpmnElementListeners.value.map((listener) =>
|
||||
initListenerType(listener),
|
||||
);
|
||||
};
|
||||
|
||||
const openListenerForm = (listener: any, index?: number) => {
|
||||
if (listener) {
|
||||
listenerForm.value = initListenerForm(listener);
|
||||
editingListenerIndex.value = index || -1;
|
||||
} else {
|
||||
listenerForm.value = {};
|
||||
editingListenerIndex.value = -1; // 标记为新增
|
||||
editingListenerIndex.value = -1;
|
||||
}
|
||||
if (listener && listener.fields) {
|
||||
fieldsListOfListener.value = listener.fields.map((field: any) => ({
|
||||
@@ -103,37 +88,32 @@ const openListenerForm = (listener: any, index?: number) => {
|
||||
fieldsListOfListener.value = [];
|
||||
listenerForm.value.fields = [];
|
||||
}
|
||||
// 打开侧边栏并清楚验证状态
|
||||
listenerFormModelVisible.value = true;
|
||||
listenerDrawerApi.open();
|
||||
nextTick(() => {
|
||||
if (listenerFormRef.value) listenerFormRef.value.clearValidate();
|
||||
});
|
||||
};
|
||||
// 移除监听器
|
||||
|
||||
const removeListener = (_: any, index: number) => {
|
||||
// console.log(listener, 'listener');
|
||||
Modal.confirm({
|
||||
confirm({
|
||||
title: '提示',
|
||||
content: '确认移除该监听器吗?',
|
||||
okText: '确 认',
|
||||
cancelText: '取 消',
|
||||
onOk() {
|
||||
bpmnElementListeners.value.splice(index, 1);
|
||||
elementListenersList.value.splice(index, 1);
|
||||
updateElementExtensions(bpmnElement.value, [
|
||||
...otherExtensionList.value,
|
||||
...bpmnElementListeners.value,
|
||||
]);
|
||||
},
|
||||
onCancel() {
|
||||
// console.info('操作取消');
|
||||
},
|
||||
}).then(() => {
|
||||
bpmnElementListeners.value.splice(index, 1);
|
||||
elementListenersList.value.splice(index, 1);
|
||||
updateElementExtensions(bpmnElement.value, [
|
||||
...otherExtensionList.value,
|
||||
...bpmnElementListeners.value,
|
||||
]);
|
||||
});
|
||||
};
|
||||
// 保存监听器
|
||||
const saveListenerConfig = async () => {
|
||||
const validateStatus = await listenerFormRef.value.validate();
|
||||
if (!validateStatus) return; // 验证不通过直接返回
|
||||
|
||||
async function saveListenerConfig() {
|
||||
try {
|
||||
await listenerFormRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const listenerObject = createListenerObject(listenerForm.value, true, prefix);
|
||||
if (editingListenerIndex.value === -1) {
|
||||
bpmnElementListeners.value.push(listenerObject);
|
||||
@@ -150,7 +130,6 @@ const saveListenerConfig = async () => {
|
||||
listenerForm.value,
|
||||
);
|
||||
}
|
||||
// 保存其他配置
|
||||
otherExtensionList.value =
|
||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||
(ex: any) => ex.$type !== `${prefix}:TaskListener`,
|
||||
@@ -159,63 +138,92 @@ const saveListenerConfig = async () => {
|
||||
...otherExtensionList.value,
|
||||
...bpmnElementListeners.value,
|
||||
]);
|
||||
// 4. 隐藏侧边栏
|
||||
listenerFormModelVisible.value = false;
|
||||
listenerDrawerApi.close();
|
||||
listenerForm.value = {};
|
||||
};
|
||||
}
|
||||
|
||||
// 打开监听器字段编辑弹窗
|
||||
const openListenerFieldForm = (field: any, index?: number) => {
|
||||
listenerFieldForm.value = field ? cloneDeep(field) : {};
|
||||
editingListenerFieldIndex.value = field ? index : -1;
|
||||
listenerFieldFormModelVisible.value = true;
|
||||
fieldModalApi.open();
|
||||
nextTick(() => {
|
||||
if (listenerFieldFormRef.value) listenerFieldFormRef.value.clearValidate();
|
||||
});
|
||||
};
|
||||
// 保存监听器注入字段
|
||||
const saveListenerFiled = async () => {
|
||||
const validateStatus = await listenerFieldFormRef.value.validate();
|
||||
if (!validateStatus) return; // 验证不通过直接返回
|
||||
if (editingListenerFieldIndex.value === -1) {
|
||||
fieldsListOfListener.value.push(listenerFieldForm.value);
|
||||
listenerForm.value.fields.push(listenerFieldForm.value);
|
||||
} else {
|
||||
fieldsListOfListener.value.splice(
|
||||
editingListenerFieldIndex.value,
|
||||
1,
|
||||
listenerFieldForm.value,
|
||||
);
|
||||
listenerForm.value.fields.splice(
|
||||
editingListenerFieldIndex.value,
|
||||
1,
|
||||
listenerFieldForm.value,
|
||||
);
|
||||
}
|
||||
listenerFieldFormModelVisible.value = false;
|
||||
nextTick(() => {
|
||||
|
||||
const [ListenerGrid, listenerGridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '序号' },
|
||||
{
|
||||
field: 'event',
|
||||
title: '事件类型',
|
||||
minWidth: 80,
|
||||
formatter: ({ cellValue }: { cellValue: string }) =>
|
||||
(listenerEventTypeObject.value as Record<string, any>)[cellValue],
|
||||
},
|
||||
{ field: 'id', title: '事件id', minWidth: 80, showOverflow: true },
|
||||
{
|
||||
field: 'listenerType',
|
||||
title: '监听器类型',
|
||||
minWidth: 80,
|
||||
formatter: ({ cellValue }: { cellValue: string }) =>
|
||||
(listenerTypeObject.value as Record<string, any>)[cellValue],
|
||||
},
|
||||
{
|
||||
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 = {};
|
||||
});
|
||||
};
|
||||
// 移除监听器字段
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const removeListenerField = (_: any, index: number) => {
|
||||
// console.log(field, 'field');
|
||||
Modal.confirm({
|
||||
confirm({
|
||||
title: '提示',
|
||||
content: '确认移除该字段吗?',
|
||||
okText: '确 认',
|
||||
cancelText: '取 消',
|
||||
onOk() {
|
||||
fieldsListOfListener.value.splice(index, 1);
|
||||
listenerForm.value.fields.splice(index, 1);
|
||||
},
|
||||
onCancel() {
|
||||
// console.info('操作取消');
|
||||
},
|
||||
}).then(() => {
|
||||
fieldsListOfListener.value.splice(index, 1);
|
||||
listenerForm.value.fields.splice(index, 1);
|
||||
});
|
||||
};
|
||||
|
||||
// 打开监听器弹窗
|
||||
const processListenerDialogRef = ref<any>();
|
||||
const openProcessListenerDialog = async () => {
|
||||
processListenerDialogRef.value.open('task');
|
||||
@@ -226,7 +234,6 @@ const selectProcessListener = (listener: any) => {
|
||||
bpmnElementListeners.value.push(listenerObject);
|
||||
elementListenersList.value.push(listenerForm);
|
||||
|
||||
// 保存其他配置
|
||||
otherExtensionList.value =
|
||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||
(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(
|
||||
() => props.id,
|
||||
(val) => {
|
||||
@@ -250,257 +320,218 @@ watch(
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="panel-tab__content">
|
||||
<Table :data="elementListenersList" size="small" bordered>
|
||||
<TableColumn title="序号" width="50px" type="index" />
|
||||
<TableColumn
|
||||
title="事件类型"
|
||||
width="80px"
|
||||
:ellipsis="{ showTitle: true }"
|
||||
:custom-render="
|
||||
({ record }: any) =>
|
||||
listenerEventTypeObject[record.event as keyof typeof eventType]
|
||||
"
|
||||
/>
|
||||
<TableColumn
|
||||
title="事件id"
|
||||
width="80px"
|
||||
data-index="id"
|
||||
:ellipsis="{ showTitle: true }"
|
||||
/>
|
||||
<TableColumn
|
||||
title="监听器类型"
|
||||
width="80px"
|
||||
:ellipsis="{ showTitle: true }"
|
||||
:custom-render="
|
||||
({ record }: any) =>
|
||||
listenerTypeObject[record.listenerType as keyof typeof listenerType]
|
||||
"
|
||||
/>
|
||||
<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)">
|
||||
<div class="-mx-2">
|
||||
<ListenerGrid>
|
||||
<template #action="{ row, rowIndex }">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openListenerForm(row, rowIndex)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
@click="removeListener(row, rowIndex)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</ListenerGrid>
|
||||
<div class="mt-1 flex w-full items-center justify-center gap-2 px-2">
|
||||
<Button
|
||||
class="flex flex-1 items-center justify-center"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="openListenerForm(null)"
|
||||
>
|
||||
<template #icon> <IconifyIcon icon="ep:plus" /></template>
|
||||
添加监听器
|
||||
</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>
|
||||
选择监听器
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 监听器 编辑/创建 部分 -->
|
||||
<Drawer
|
||||
v-model:open="listenerFormModelVisible"
|
||||
title="任务监听器"
|
||||
:width="width"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<Form :model="listenerForm" ref="listenerFormRef">
|
||||
<FormItem
|
||||
label="事件类型"
|
||||
name="event"
|
||||
:rules="[{ required: true, message: '请选择事件类型' }]"
|
||||
<ListenerDrawer class="w-2/5">
|
||||
<template #default>
|
||||
<Form
|
||||
:label-col="{ span: 6 }"
|
||||
:model="listenerForm"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
ref="listenerFormRef"
|
||||
>
|
||||
<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
|
||||
label="脚本格式"
|
||||
name="scriptFormat"
|
||||
key="listener-script-format"
|
||||
:rules="[{ required: true, message: '请填写脚本格式' }]"
|
||||
label="事件类型"
|
||||
name="event"
|
||||
:rules="[{ required: true, message: '请选择事件类型' }]"
|
||||
>
|
||||
<Input v-model:value="listenerForm.scriptFormat" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="脚本类型"
|
||||
name="scriptType"
|
||||
key="listener-script-type"
|
||||
:rules="[{ required: true, message: '请选择脚本类型' }]"
|
||||
>
|
||||
<Select v-model:value="listenerForm.scriptType">
|
||||
<SelectOption value="inlineScript">内联脚本</SelectOption>
|
||||
<SelectOption value="externalScript">外部脚本</SelectOption>
|
||||
<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
|
||||
v-if="listenerForm.scriptType === 'inlineScript'"
|
||||
label="脚本内容"
|
||||
name="value"
|
||||
key="listener-script"
|
||||
:rules="[{ required: true, message: '请填写脚本内容' }]"
|
||||
label="监听器ID"
|
||||
name="id"
|
||||
:rules="[{ required: true, message: '请输入监听器ID' }]"
|
||||
>
|
||||
<Input v-model:value="listenerForm.value" allow-clear />
|
||||
<Input v-model:value="listenerForm.id" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="listenerForm.scriptType === 'externalScript'"
|
||||
label="资源地址"
|
||||
name="resource"
|
||||
key="listener-resource"
|
||||
:rules="[{ required: true, message: '请填写资源地址' }]"
|
||||
label="监听器类型"
|
||||
name="listenerType"
|
||||
:rules="[{ required: true, message: '请选择监听器类型' }]"
|
||||
>
|
||||
<Input v-model:value="listenerForm.resource" allow-clear />
|
||||
</FormItem>
|
||||
</template>
|
||||
|
||||
<template v-if="listenerForm.event === 'timeout'">
|
||||
<FormItem
|
||||
label="定时器类型"
|
||||
name="eventDefinitionType"
|
||||
key="eventDefinitionType"
|
||||
>
|
||||
<Select v-model:value="listenerForm.eventDefinitionType">
|
||||
<SelectOption value="date">日期</SelectOption>
|
||||
<SelectOption value="duration">持续时长</SelectOption>
|
||||
<SelectOption value="cycle">循环</SelectOption>
|
||||
<SelectOption value="null">无</SelectOption>
|
||||
<Select 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.eventDefinitionType &&
|
||||
listenerForm.eventDefinitionType !== 'null'
|
||||
"
|
||||
label="定时器"
|
||||
name="eventTimeDefinitions"
|
||||
key="eventTimeDefinitions"
|
||||
:rules="[{ required: true, message: '请填写定时器配置' }]"
|
||||
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.eventTimeDefinitions"
|
||||
v-model:value="listenerForm.delegateExpression"
|
||||
allow-clear
|
||||
/>
|
||||
</FormItem>
|
||||
</template>
|
||||
</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
|
||||
type="primary"
|
||||
title="添加字段"
|
||||
@click="openListenerFieldForm(null)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:plus" />
|
||||
<template v-if="listenerForm.listenerType === 'scriptListener'">
|
||||
<FormItem
|
||||
label="脚本格式"
|
||||
name="scriptFormat"
|
||||
key="listener-script-format"
|
||||
:rules="[{ required: true, message: '请填写脚本格式' }]"
|
||||
>
|
||||
<Input v-model:value="listenerForm.scriptFormat" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="脚本类型"
|
||||
name="scriptType"
|
||||
key="listener-script-type"
|
||||
:rules="[{ required: true, message: '请选择脚本类型' }]"
|
||||
>
|
||||
<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>
|
||||
添加字段
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
:data="fieldsListOfListener"
|
||||
size="small"
|
||||
:scroll="{ y: 240 }"
|
||||
bordered
|
||||
style="flex: none"
|
||||
>
|
||||
<TableColumn title="序号" width="50px" type="index" />
|
||||
<TableColumn title="字段名称" width="100px" data-index="name" />
|
||||
<TableColumn
|
||||
title="字段类型"
|
||||
width="80px"
|
||||
:ellipsis="{ showTitle: true }"
|
||||
:custom-render="
|
||||
({ record }: any) =>
|
||||
fieldTypeObject[record.fieldType as keyof typeof fieldType]
|
||||
"
|
||||
/>
|
||||
<TableColumn
|
||||
title="字段值/表达式"
|
||||
width="100px"
|
||||
:ellipsis="{ showTitle: true }"
|
||||
:custom-render="
|
||||
({ record }: any) => record.string || record.expression
|
||||
"
|
||||
/>
|
||||
<TableColumn title="操作" width="100px">
|
||||
<template #default="{ record, index }">
|
||||
|
||||
<template v-if="listenerForm.event === 'timeout'">
|
||||
<FormItem
|
||||
label="定时器类型"
|
||||
name="eventDefinitionType"
|
||||
key="eventDefinitionType"
|
||||
>
|
||||
<Select v-model:value="listenerForm.eventDefinitionType">
|
||||
<SelectOption value="date">日期</SelectOption>
|
||||
<SelectOption value="duration">持续时长</SelectOption>
|
||||
<SelectOption value="cycle">循环</SelectOption>
|
||||
<SelectOption value="null">无</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
!!listenerForm.eventDefinitionType &&
|
||||
listenerForm.eventDefinitionType !== 'null'
|
||||
"
|
||||
label="定时器"
|
||||
name="eventTimeDefinitions"
|
||||
key="eventTimeDefinitions"
|
||||
:rules="[{ required: true, message: '请填写定时器配置' }]"
|
||||
>
|
||||
<Input
|
||||
v-model:value="listenerForm.eventTimeDefinitions"
|
||||
allow-clear
|
||||
/>
|
||||
</FormItem>
|
||||
</template>
|
||||
</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
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openListenerFieldForm(record, index)"
|
||||
@click="openListenerFieldForm(row, rowIndex)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
@@ -509,32 +540,23 @@ watch(
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
@click="removeListenerField(record, index)"
|
||||
@click="removeListenerField(row, rowIndex)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</TableColumn>
|
||||
</Table>
|
||||
|
||||
<div class="element-drawer__button">
|
||||
<Button size="small" @click="listenerFormModelVisible = false">
|
||||
取 消
|
||||
</Button>
|
||||
<Button size="small" type="primary" @click="saveListenerConfig">
|
||||
保 存
|
||||
</Button>
|
||||
</div>
|
||||
</Drawer>
|
||||
</FieldsGrid>
|
||||
</template>
|
||||
</ListenerDrawer>
|
||||
|
||||
<!-- 注入字段 编辑/创建 部分 -->
|
||||
<Modal
|
||||
title="字段配置"
|
||||
v-model:open="listenerFieldFormModelVisible"
|
||||
:width="600"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<Form :model="listenerFieldForm" ref="listenerFieldFormRef">
|
||||
<FieldModal class="w-3/5">
|
||||
<Form
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
:model="listenerFieldForm"
|
||||
ref="listenerFieldFormRef"
|
||||
>
|
||||
<FormItem
|
||||
label="字段名称:"
|
||||
name="name"
|
||||
@@ -576,15 +598,7 @@ watch(
|
||||
<Input v-model:value="listenerFieldForm.expression" allow-clear />
|
||||
</FormItem>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button size="small" @click="listenerFieldFormModelVisible = false">
|
||||
取 消
|
||||
</Button>
|
||||
<Button size="small" type="primary" @click="saveListenerFiled">
|
||||
确 定
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</FieldModal>
|
||||
</div>
|
||||
|
||||
<!-- 选择弹窗 -->
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
@@ -23,13 +24,31 @@ const modelObjectForm = ref<any>({});
|
||||
const rootElements = ref();
|
||||
const messageIdMap = ref();
|
||||
const signalIdMap = ref();
|
||||
const editingIndex = ref(-1); // 正在编辑的索引,-1 表示新建
|
||||
const modelConfig = computed(() => {
|
||||
const isEdit = editingIndex.value !== -1;
|
||||
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;
|
||||
|
||||
// 生成规范化的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 = () => {
|
||||
// console.log(window, 'window');
|
||||
rootElements.value = bpmnInstances().modeler.getDefinitions().rootElements;
|
||||
@@ -48,35 +67,104 @@ const initDataList = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const openModel = (type: any) => {
|
||||
modelType.value = type;
|
||||
modelObjectForm.value = {};
|
||||
editingIndex.value = -1;
|
||||
modelObjectForm.value = {
|
||||
id: generateStandardId(type),
|
||||
name: '',
|
||||
};
|
||||
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 = () => {
|
||||
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 {
|
||||
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;
|
||||
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(() => {
|
||||
initDataList();
|
||||
});
|
||||
@@ -103,6 +191,26 @@ onMounted(() => {
|
||||
</TableColumn>
|
||||
<TableColumn title="消息ID" data-index="id" />
|
||||
<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>
|
||||
<div class="panel-tab__content--title mt-2 border-t border-gray-200 pt-2">
|
||||
<span class="flex items-center">
|
||||
@@ -124,6 +232,26 @@ onMounted(() => {
|
||||
</TableColumn>
|
||||
<TableColumn title="信号ID" data-index="id" />
|
||||
<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>
|
||||
|
||||
<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>
|
||||
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' });
|
||||
const props = defineProps({
|
||||
@@ -9,38 +24,281 @@ const props = defineProps({
|
||||
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: '',
|
||||
class: '',
|
||||
expression: '',
|
||||
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 httpInitializing = ref(false);
|
||||
const showHeaderEditor = ref(false);
|
||||
|
||||
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||
|
||||
const resetTaskForm = () => {
|
||||
for (const key in defaultTaskForm.value) {
|
||||
const value =
|
||||
// @ts-ignore
|
||||
bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key];
|
||||
serviceTaskForm.value[key] = value;
|
||||
if (value) {
|
||||
serviceTaskForm.value.executeType = key;
|
||||
// 判断字符串是否包含表达式
|
||||
const isExpression = (value: string): boolean => {
|
||||
if (!value) return false;
|
||||
// 检测 ${...} 或 #{...} 格式的表达式
|
||||
return /\$\{[^}]+\}/.test(value) || /#\{[^}]+\}/.test(value);
|
||||
};
|
||||
|
||||
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 taskAttr = Object.create(null);
|
||||
const type = serviceTaskForm.value.executeType;
|
||||
for (const key in serviceTaskForm.value) {
|
||||
if (key !== 'executeType' && key !== type) taskAttr[key] = null;
|
||||
const shouldPersistField = (name: string, value: any) => {
|
||||
if (HTTP_BOOLEAN_FIELDS.has(name)) return true;
|
||||
if (name === 'requestMethod') return true;
|
||||
if (name === 'requestUrl') return !!value;
|
||||
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);
|
||||
|
||||
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(() => {
|
||||
@@ -52,11 +310,19 @@ watch(
|
||||
() => {
|
||||
bpmnElement.value = bpmnInstances().bpmnElement;
|
||||
nextTick(() => {
|
||||
resetTaskForm();
|
||||
resetServiceTaskForm();
|
||||
});
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => httpTaskForm.value,
|
||||
() => {
|
||||
updateHttpExtensions();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -68,7 +334,9 @@ watch(
|
||||
{ label: 'Java类', value: 'class' },
|
||||
{ label: '表达式', value: 'expression' },
|
||||
{ label: '代理表达式', value: 'delegateExpression' },
|
||||
{ label: 'HTTP 调用', value: 'http' },
|
||||
]"
|
||||
@change="handleExecuteTypeChange"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
@@ -107,5 +375,62 @@ watch(
|
||||
@change="updateElementTask"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -146,7 +146,6 @@
|
||||
background: url('./svg/simple-process-bg.svg') 0 0 repeat;
|
||||
transform: scale(1);
|
||||
transform-origin: 50% 0 0;
|
||||
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
// 节点容器 定义节点宽度
|
||||
.node-container {
|
||||
width: 200px;
|
||||
|
||||
@@ -259,9 +259,11 @@ async function validateAllSteps() {
|
||||
return true;
|
||||
}
|
||||
|
||||
const saveLoading = ref<boolean>(false);
|
||||
/** 保存操作 */
|
||||
async function handleSave() {
|
||||
try {
|
||||
saveLoading.value = true;
|
||||
// 保存前校验所有步骤的数据
|
||||
const result = await validateAllSteps();
|
||||
if (!result) {
|
||||
@@ -309,9 +311,12 @@ async function handleSave() {
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('保存失败:', error);
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 发布加载中状态
|
||||
const deployLoading = ref<boolean>(false);
|
||||
/** 发布操作 */
|
||||
async function handleDeploy() {
|
||||
try {
|
||||
@@ -319,6 +324,7 @@ async function handleDeploy() {
|
||||
if (!formData.value.id) {
|
||||
await confirm('是否确认发布该流程?');
|
||||
}
|
||||
deployLoading.value = true;
|
||||
// 1.2 校验所有步骤
|
||||
await validateAllSteps();
|
||||
|
||||
@@ -342,6 +348,8 @@ async function handleDeploy() {
|
||||
} catch (error: any) {
|
||||
console.error('发布失败:', error);
|
||||
message.warning(error.message || '发布失败');
|
||||
} finally {
|
||||
deployLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,11 +456,12 @@ onBeforeUnmount(() => {
|
||||
<Button
|
||||
v-if="actionType === 'update'"
|
||||
type="primary"
|
||||
:loading="deployLoading"
|
||||
@click="handleDeploy"
|
||||
>
|
||||
发 布
|
||||
</Button>
|
||||
<Button type="primary" @click="handleSave">
|
||||
<Button type="primary" @click="handleSave" :loading="saveLoading">
|
||||
<span v-if="actionType === 'definition'">恢 复</span>
|
||||
<span v-else>保 存</span>
|
||||
</Button>
|
||||
|
||||
@@ -228,9 +228,10 @@ onMounted(() => {
|
||||
>
|
||||
<Card
|
||||
hoverable
|
||||
class="definition-item-card w-full cursor-pointer"
|
||||
class="w-full cursor-pointer"
|
||||
:class="{
|
||||
'search-match': searchName.trim().length > 0,
|
||||
'animate-bounce-once !bg-[rgb(63_115_247_/_10%)]':
|
||||
searchName.trim().length > 0,
|
||||
}"
|
||||
:body-style="{
|
||||
width: '100%',
|
||||
@@ -241,10 +242,13 @@ onMounted(() => {
|
||||
<img
|
||||
v-if="definition.icon"
|
||||
:src="definition.icon"
|
||||
class="flow-icon-img object-contain"
|
||||
class="size-12 rounded object-contain"
|
||||
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">
|
||||
{{ definition.name?.slice(0, 2) }}
|
||||
</span>
|
||||
@@ -283,7 +287,6 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// @jason:看看能不能通过 tailwindcss 简化下
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
50% {
|
||||
@@ -295,30 +298,7 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.process-definition-container {
|
||||
.definition-item-card {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
.animate-bounce-once {
|
||||
animation: bounce 0.5s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -104,7 +104,7 @@ async function submitForm() {
|
||||
// 关闭并提示
|
||||
message.success('发起流程成功');
|
||||
await closeCurrentTab();
|
||||
await router.push({ name: 'BpmTaskMy' });
|
||||
await router.push({ name: 'BpmProcessInstanceMy' });
|
||||
} finally {
|
||||
processInstanceStartLoading.value = false;
|
||||
}
|
||||
|
||||
@@ -212,20 +212,27 @@ watch(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const loading = ref(false);
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await getDetail();
|
||||
// 获得用户列表
|
||||
userOptions.value = await getSimpleUserList();
|
||||
try {
|
||||
loading.value = true;
|
||||
await getDetail();
|
||||
// 获得用户列表
|
||||
userOptions.value = await getSimpleUserList();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Page auto-content-height v-loading="loading">
|
||||
<Card
|
||||
class="flex h-full flex-col"
|
||||
:body-style="{
|
||||
overflowY: 'auto',
|
||||
flex: 1,
|
||||
overflowY: 'hidden',
|
||||
paddingTop: '12px',
|
||||
}"
|
||||
>
|
||||
@@ -286,24 +293,16 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<!-- 流程操作 -->
|
||||
<div class="process-tabs-container flex flex-1 flex-col">
|
||||
<Tabs v-model:active-key="activeTab" class="mt-0 h-full">
|
||||
<TabPane tab="审批详情" key="form" class="tab-pane-content">
|
||||
<Row :gutter="[48, 24]" class="h-full">
|
||||
<Col
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="18"
|
||||
:lg="18"
|
||||
:xl="16"
|
||||
class="h-full"
|
||||
>
|
||||
<div class="flex h-full flex-1 flex-col">
|
||||
<Tabs v-model:active-key="activeTab">
|
||||
<TabPane tab="审批详情" key="form" class="pb-20 pr-3">
|
||||
<Row :gutter="[48, 24]">
|
||||
<Col :xs="24" :sm="24" :md="18" :lg="18" :xl="16">
|
||||
<!-- 流程表单 -->
|
||||
<div
|
||||
v-if="
|
||||
processDefinition?.formType === BpmModelFormType.NORMAL
|
||||
"
|
||||
class="h-full"
|
||||
>
|
||||
<form-create
|
||||
v-model="detailForm.value"
|
||||
@@ -316,13 +315,12 @@ onMounted(async () => {
|
||||
v-else-if="
|
||||
processDefinition?.formType === BpmModelFormType.CUSTOM
|
||||
"
|
||||
class="h-full"
|
||||
>
|
||||
<BusinessFormComponent :id="processInstance?.businessKey" />
|
||||
</div>
|
||||
</Col>
|
||||
<Col :xs="24" :sm="24" :md="6" :lg="6" :xl="8" class="h-full">
|
||||
<div class="mt-4 h-full">
|
||||
<Col :xs="24" :sm="24" :md="6" :lg="6" :xl="8">
|
||||
<div class="mt-4">
|
||||
<ProcessInstanceTimeline :activity-nodes="activityNodes" />
|
||||
</div>
|
||||
</Col>
|
||||
@@ -331,44 +329,35 @@ onMounted(async () => {
|
||||
<TabPane
|
||||
tab="流程图"
|
||||
key="diagram"
|
||||
class="tab-pane-content"
|
||||
class="pb-20 pr-3"
|
||||
:force-render="true"
|
||||
>
|
||||
<div class="h-full">
|
||||
<ProcessInstanceSimpleViewer
|
||||
v-show="
|
||||
processDefinition.modelType &&
|
||||
processDefinition.modelType === BpmModelType.SIMPLE
|
||||
"
|
||||
:loading="processInstanceLoading"
|
||||
:model-view="processModelView"
|
||||
/>
|
||||
<ProcessInstanceBpmnViewer
|
||||
v-show="
|
||||
processDefinition.modelType &&
|
||||
processDefinition.modelType === BpmModelType.BPMN
|
||||
"
|
||||
:loading="processInstanceLoading"
|
||||
:model-view="processModelView"
|
||||
/>
|
||||
</div>
|
||||
<ProcessInstanceSimpleViewer
|
||||
v-show="
|
||||
processDefinition.modelType &&
|
||||
processDefinition.modelType === BpmModelType.SIMPLE
|
||||
"
|
||||
:loading="processInstanceLoading"
|
||||
:model-view="processModelView"
|
||||
/>
|
||||
<ProcessInstanceBpmnViewer
|
||||
v-show="
|
||||
processDefinition.modelType &&
|
||||
processDefinition.modelType === BpmModelType.BPMN
|
||||
"
|
||||
:loading="processInstanceLoading"
|
||||
:model-view="processModelView"
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="流转记录" key="record" class="tab-pane-content">
|
||||
<div class="h-full">
|
||||
<BpmProcessInstanceTaskList
|
||||
ref="taskListRef"
|
||||
:loading="processInstanceLoading"
|
||||
:id="id"
|
||||
/>
|
||||
</div>
|
||||
<TabPane tab="流转记录" key="record" class="pb-20 pr-3">
|
||||
<BpmProcessInstanceTaskList
|
||||
ref="taskListRef"
|
||||
:loading="processInstanceLoading"
|
||||
:id="id"
|
||||
/>
|
||||
</TabPane>
|
||||
<!-- TODO 待开发 -->
|
||||
<TabPane
|
||||
tab="流转评论"
|
||||
key="comment"
|
||||
v-if="false"
|
||||
class="tab-pane-content"
|
||||
>
|
||||
<TabPane tab="流转评论" key="comment" v-if="false" class="pr-3">
|
||||
<div class="h-full">待开发</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
@@ -396,35 +385,18 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// @jason:看看能不能通过 tailwindcss 简化下
|
||||
.ant-tabs-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.process-tabs-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-content) {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
.ant-tabs-content {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-tabpane) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tab-pane-content {
|
||||
height: calc(100vh - 420px);
|
||||
padding-right: 12px;
|
||||
overflow: hidden auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
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 { uploadFile } from '#/api/infra/file';
|
||||
@@ -36,30 +36,29 @@ const [Modal, modalApi] = useVbenModal({
|
||||
|
||||
<template>
|
||||
<Modal title="流程签名" class="w-3/5">
|
||||
<div class="mb-2 flex justify-end">
|
||||
<Space>
|
||||
<div class="flex h-[50vh] flex-col">
|
||||
<div class="mb-2 flex justify-end gap-2">
|
||||
<Tooltip title="撤销上一步操作">
|
||||
<Button @click="signature?.undo()">
|
||||
<Button @click="signature?.undo()" size="small">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:undo" class="mb-1 size-4" />
|
||||
<IconifyIcon icon="lucide:undo" class="mb-1 size-3" />
|
||||
</template>
|
||||
撤销
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="清空画布">
|
||||
<Button @click="signature?.clear()">
|
||||
<Button @click="signature?.clear()" size="small">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:trash" class="mb-1 size-4" />
|
||||
<IconifyIcon icon="lucide:trash" class="mb-1 size-3" />
|
||||
</template>
|
||||
<span>清除</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</div>
|
||||
<Vue3Signature
|
||||
class="h-full flex-1 border border-solid border-gray-300"
|
||||
ref="signature"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Vue3Signature
|
||||
class="mx-auto !h-80 border border-solid border-gray-300"
|
||||
ref="signature"
|
||||
/>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -44,7 +44,7 @@ function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
field: 'approver',
|
||||
title: '审批人',
|
||||
slots: {
|
||||
default: ({ row }: { row: BpmTaskApi.TaskManager }) => {
|
||||
default: ({ row }: { row: BpmTaskApi.Task }) => {
|
||||
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 = {
|
||||
rule: [],
|
||||
@@ -141,7 +141,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
keepSource: true,
|
||||
showFooter: true,
|
||||
border: true,
|
||||
height: 'auto',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
@@ -159,7 +158,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
} as VxeTableGridOptions<BpmTaskApi.TaskManager>,
|
||||
} as VxeTableGridOptions<BpmTaskApi.Task>,
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
@@ -168,7 +167,7 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<div>
|
||||
<Grid>
|
||||
<template #slot-reason="{ row }">
|
||||
<div class="flex flex-wrap items-center justify-center">
|
||||
@@ -188,13 +187,13 @@ defineExpose({
|
||||
</div>
|
||||
</template>
|
||||
</Grid>
|
||||
<Modal class="w-[800px]">
|
||||
<form-create
|
||||
ref="formRef"
|
||||
v-model="taskForm.value"
|
||||
:option="taskForm.option"
|
||||
:rule="taskForm.rule"
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
<Modal class="w-3/5">
|
||||
<form-create
|
||||
ref="formRef"
|
||||
v-model="taskForm.value"
|
||||
:option="taskForm.option"
|
||||
:rule="taskForm.rule"
|
||||
/>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -8,22 +8,22 @@ import { z } from '#/adapter/form';
|
||||
|
||||
export const EVENT_EXECUTION_OPTIONS = [
|
||||
{
|
||||
label: 'start',
|
||||
label: '开始',
|
||||
value: 'start',
|
||||
},
|
||||
{
|
||||
label: 'end',
|
||||
label: '结束',
|
||||
value: 'end',
|
||||
},
|
||||
];
|
||||
|
||||
export const EVENT_OPTIONS = [
|
||||
{ label: 'create', value: 'create' },
|
||||
{ label: 'assignment', value: 'assignment' },
|
||||
{ label: 'complete', value: 'complete' },
|
||||
{ label: 'delete', value: 'delete' },
|
||||
{ label: 'update', value: 'update' },
|
||||
{ label: 'timeout', value: 'timeout' },
|
||||
{ label: '创建', value: 'create' },
|
||||
{ label: '指派', value: 'assignment' },
|
||||
{ label: '完成', value: 'complete' },
|
||||
{ label: '删除', value: 'delete' },
|
||||
{ label: '更新', value: 'update' },
|
||||
{ label: '超时', value: 'timeout' },
|
||||
];
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useGridColumns, useGridFormSchema } from './data';
|
||||
defineOptions({ name: 'BpmDoneTask' });
|
||||
|
||||
/** 查看历史 */
|
||||
function handleHistory(row: BpmTaskApi.TaskManager) {
|
||||
function handleHistory(row: BpmTaskApi.Task) {
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
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({
|
||||
content: '正在撤回中...',
|
||||
duration: 0,
|
||||
@@ -67,7 +67,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<BpmTaskApi.TaskManager>,
|
||||
} as VxeTableGridOptions<BpmTaskApi.Task>,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useGridColumns, useGridFormSchema } from './data';
|
||||
defineOptions({ name: 'BpmManagerTask' });
|
||||
|
||||
/** 查看历史 */
|
||||
function handleHistory(row: BpmTaskApi.TaskManager) {
|
||||
function handleHistory(row: BpmTaskApi.Task) {
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: {
|
||||
|
||||
@@ -78,7 +78,8 @@ function handleRowCheckboxChange({
|
||||
}: {
|
||||
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({
|
||||
@@ -140,6 +141,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:data-source-config:update'],
|
||||
disabled: row.id === 0,
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
@@ -148,6 +150,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:data-source-config:delete'],
|
||||
disabled: row.id === 0,
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
|
||||
@@ -229,6 +229,18 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'config.region',
|
||||
label: '区域',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请填写区域,一般仅 AWS 需要填写',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) => formValues.storage === 20,
|
||||
},
|
||||
},
|
||||
// 通用
|
||||
{
|
||||
fieldName: 'config.domain',
|
||||
|
||||
@@ -11,12 +11,12 @@ import { deleteDeviceGroup, getDeviceGroupPage } from '#/api/iot/device/group';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import DeviceGroupForm from './modules/device-group-form.vue';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'IoTDeviceGroup' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: DeviceGroupForm,
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import type { IotDeviceGroupApi } from '#/api/iot/device/group';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
@@ -17,17 +17,9 @@ import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
defineOptions({ name: 'IoTDeviceGroupForm' });
|
||||
|
||||
// TODO @haohao:web-antd/src/views/iot/product/category/modules/product-category-form.vue 类似问题
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<IotDeviceGroupApi.DeviceGroup>();
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['设备分组'])
|
||||
: $t('ui.actionTitle.create', ['设备分组']);
|
||||
@@ -40,11 +32,9 @@ const [Form, formApi] = useVbenForm({
|
||||
},
|
||||
},
|
||||
schema: useFormSchema(),
|
||||
showCollapseButton: false,
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// TODO @haohao:参考别的 form;1)文件的命名可以简化;2)代码可以在简化下;
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
@@ -52,17 +42,13 @@ const [Modal, modalApi] = useVbenModal({
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as IotDeviceGroupApi.DeviceGroup;
|
||||
try {
|
||||
const values = await formApi.getValues();
|
||||
|
||||
await (formData.value?.id
|
||||
? updateDeviceGroup({
|
||||
...values,
|
||||
id: formData.value.id,
|
||||
} as IotDeviceGroupApi.DeviceGroup)
|
||||
: createDeviceGroup(values as IotDeviceGroupApi.DeviceGroup));
|
||||
|
||||
? updateDeviceGroup(data)
|
||||
: createDeviceGroup(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
@@ -70,28 +56,20 @@ const [Modal, modalApi] = useVbenModal({
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
await formApi.resetForm();
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
await formApi.resetForm();
|
||||
|
||||
// 加载数据
|
||||
const data = modalApi.getData<IotDeviceGroupApi.DeviceGroup>();
|
||||
// 如果没有数据或没有 id,表示是新增
|
||||
if (!data || !data.id) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
// 编辑模式:加载数据
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDeviceGroup(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
@@ -101,7 +79,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="modalTitle" class="w-2/5">
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -14,12 +14,12 @@ import {
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import ProductCategoryForm from './modules/product-category-form.vue';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'IoTProductCategory' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: ProductCategoryForm,
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -17,8 +17,6 @@ import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
// TODO @haohao:应该是 form.vue,不用前缀;
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<IotProductCategoryApi.ProductCategory>();
|
||||
const getTitle = computed(() => {
|
||||
@@ -40,7 +38,6 @@ const [Form, formApi] = useVbenForm({
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// TODO @haohao:参考 apps/web-antd/src/views/system/dept/modules/form.vue 简化 useVbenModal 里的代码;
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
@@ -66,25 +63,19 @@ const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
formApi.resetForm();
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
await formApi.resetForm();
|
||||
|
||||
// 加载数据
|
||||
const data = modalApi.getData<IotProductCategoryApi.ProductCategory>();
|
||||
// 如果没有数据或没有 id,表示是新增
|
||||
if (!data || !data.id) {
|
||||
formData.value = undefined;
|
||||
// 新增模式:设置默认值
|
||||
formData.value = undefined;
|
||||
await formApi.setValues({
|
||||
sort: 0,
|
||||
status: 1,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 编辑模式:加载数据
|
||||
modalApi.lock();
|
||||
try {
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
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 { getDictOptions } from '@vben/hooks';
|
||||
@@ -10,7 +11,17 @@ import { Button } from 'ant-design-vue';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
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[] {
|
||||
@@ -134,7 +145,7 @@ export function useFormSchema(formApi?: any): VbenFormSchema[] {
|
||||
label: '产品状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
options: getDictOptions(DICT_TYPE.IOT_PRODUCT_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
@@ -283,7 +294,7 @@ export function useBasicFormSchema(formApi?: any): VbenFormSchema[] {
|
||||
label: '产品状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
options: getDictOptions(DICT_TYPE.IOT_PRODUCT_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
@@ -308,11 +319,10 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
|
||||
{
|
||||
fieldName: 'icon',
|
||||
label: '产品图标',
|
||||
component: 'IconPicker', // 用这个组件 产品卡片列表 可以根据这个显示 否则就显示默认的
|
||||
component: 'IconPicker',
|
||||
componentProps: {
|
||||
placeholder: '请选择产品图标',
|
||||
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'] {
|
||||
return [
|
||||
@@ -375,7 +360,8 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
field: 'categoryId',
|
||||
title: '品类',
|
||||
minWidth: 120,
|
||||
slots: { default: 'category' },
|
||||
formatter: ({ cellValue }) =>
|
||||
categoryList.find((c) => c.id === cellValue)?.name || '未分类',
|
||||
},
|
||||
{
|
||||
field: 'deviceType',
|
||||
@@ -390,13 +376,17 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
field: 'icon',
|
||||
title: '产品图标',
|
||||
width: 100,
|
||||
slots: { default: 'icon' },
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '产品图片',
|
||||
width: 100,
|
||||
slots: { default: 'picUrl' },
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
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(包含大小写字母和数字) */
|
||||
export function generateProductKey(): string {
|
||||
const chars =
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
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 { useRouter } from 'vue-router';
|
||||
@@ -8,7 +10,7 @@ import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
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 { getSimpleProductCategoryList } from '#/api/iot/product/category';
|
||||
@@ -19,14 +21,14 @@ import {
|
||||
} from '#/api/iot/product/product';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useImagePreview } from './data';
|
||||
import ProductCardView from './modules/product-card-view.vue';
|
||||
import ProductForm from './modules/product-form.vue';
|
||||
import { useGridColumns } from './data';
|
||||
import ProductCardView from './modules/card-view.vue';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'IoTProduct' });
|
||||
|
||||
const router = useRouter();
|
||||
const categoryList = ref<any[]>([]); // TODO @haohao:category 类型
|
||||
const categoryList = ref<IotProductCategoryApi.ProductCategory[]>([]);
|
||||
const viewMode = ref<'card' | 'list'>('card');
|
||||
const cardViewRef = ref();
|
||||
const searchParams = ref({
|
||||
@@ -34,10 +36,8 @@ const searchParams = ref({
|
||||
productKey: '',
|
||||
}); // 搜索参数
|
||||
|
||||
const { previewVisible, previewImage, handlePreviewImage } = useImagePreview();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: ProductForm,
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
@@ -46,13 +46,6 @@ async function loadCategories() {
|
||||
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() {
|
||||
if (viewMode.value === 'list') {
|
||||
@@ -128,10 +121,6 @@ async function handleDelete(row: any) {
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
// TODO @haohao:这个不用,可以删除掉的
|
||||
formOptions: {
|
||||
schema: [],
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
@@ -155,7 +144,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions, // TODO @haohao:这里有个 <> 泛型
|
||||
} as VxeTableGridOptions<IotProductApi.Product>,
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
@@ -172,24 +161,22 @@ onMounted(() => {
|
||||
<Card :body-style="{ padding: '16px' }" class="mb-4">
|
||||
<!-- 搜索表单 -->
|
||||
<div class="mb-3 flex items-center gap-3">
|
||||
<!-- TODO @haohao:tindwind -->
|
||||
<Input
|
||||
v-model:value="searchParams.name"
|
||||
placeholder="请输入产品名称"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
class="w-[220px]"
|
||||
@press-enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<span class="text-gray-400">产品名称</span>
|
||||
</template>
|
||||
</Input>
|
||||
<!-- TODO @haohao:tindwind -->
|
||||
<Input
|
||||
v-model:value="searchParams.productKey"
|
||||
placeholder="请输入产品标识"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
class="w-[220px]"
|
||||
@press-enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
@@ -207,18 +194,22 @@ onMounted(() => {
|
||||
</div>
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<Space :size="12">
|
||||
<Button type="primary" @click="handleCreate">
|
||||
<!-- TODO @haohao:按钮使用中立的,ACTION_ICON.ADD -->
|
||||
<IconifyIcon icon="ant-design:plus-outlined" class="mr-1" />
|
||||
新增产品
|
||||
</Button>
|
||||
<Button type="primary" @click="handleExport">
|
||||
<!-- TODO @haohao:按钮使用中立的,ACTION_ICON.EXPORT -->
|
||||
<IconifyIcon icon="ant-design:download-outlined" class="mr-1" />
|
||||
导出
|
||||
</Button>
|
||||
</Space>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增产品',
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: '导出',
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<!-- 视图切换 -->
|
||||
<Space :size="4">
|
||||
<Button
|
||||
@@ -238,53 +229,18 @@ onMounted(() => {
|
||||
</Card>
|
||||
|
||||
<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 }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '详情',
|
||||
type: 'link',
|
||||
onClick: openProductDetail.bind(null, row.id),
|
||||
onClick: openProductDetail.bind(null, row.id!),
|
||||
},
|
||||
{
|
||||
label: '物模型',
|
||||
type: 'link',
|
||||
onClick: openThingModel.bind(null, row.id),
|
||||
onClick: openThingModel.bind(null, row.id!),
|
||||
},
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
@@ -320,42 +276,11 @@ onMounted(() => {
|
||||
@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>
|
||||
</template>
|
||||
<style scoped>
|
||||
/** TODO @haohao:貌似这 2 个 css 没啥用? */
|
||||
:deep(.vxe-toolbar div) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 隐藏 VxeGrid 自带的搜索表单区域 */
|
||||
:deep(.vxe-grid--form-wrapper) {
|
||||
display: none !important;
|
||||
}
|
||||
</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 { DICT_TYPE } from '@vben/constants';
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Card,
|
||||
Col,
|
||||
Empty,
|
||||
Image,
|
||||
Pagination,
|
||||
Popconfirm,
|
||||
Row,
|
||||
@@ -19,10 +20,13 @@ import {
|
||||
|
||||
import { getProductPage } from '#/api/iot/product/product';
|
||||
|
||||
// TODO @haohao:应该是 card-view.vue;
|
||||
|
||||
// TODO @haohao:命名不太对;可以简化下;
|
||||
defineOptions({ name: 'ProductCardView' });
|
||||
interface Props {
|
||||
categoryList: any[];
|
||||
searchParams?: {
|
||||
name: string;
|
||||
productKey: string;
|
||||
};
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
@@ -34,14 +38,6 @@ const emit = defineEmits<{
|
||||
thingModel: [productId: number];
|
||||
}>();
|
||||
|
||||
interface Props {
|
||||
categoryList: any[];
|
||||
searchParams?: {
|
||||
name: string;
|
||||
productKey: string;
|
||||
};
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<any[]>([]);
|
||||
const total = ref(0);
|
||||
@@ -50,14 +46,13 @@ const queryParams = ref({
|
||||
pageSize: 12,
|
||||
});
|
||||
|
||||
// TODO @haohao:注释的优化;
|
||||
// 获取分类名称
|
||||
/** 获取分类名称 */
|
||||
function getCategoryName(categoryId: number) {
|
||||
const category = props.categoryList.find((c: any) => c.id === categoryId);
|
||||
return category?.name || '未分类';
|
||||
}
|
||||
|
||||
// 获取产品列表
|
||||
/** 获取产品列表 */
|
||||
async function getList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -72,14 +67,14 @@ async function getList() {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理页码变化
|
||||
/** 处理页码变化 */
|
||||
function handlePageChange(page: number, pageSize: number) {
|
||||
queryParams.value.pageNo = page;
|
||||
queryParams.value.pageSize = pageSize;
|
||||
getList();
|
||||
}
|
||||
|
||||
// 获取设备类型颜色
|
||||
/** 获取设备类型颜色 */
|
||||
function getDeviceTypeColor(deviceType: number) {
|
||||
const colors: Record<number, string> = {
|
||||
0: 'blue',
|
||||
@@ -114,17 +109,17 @@ onMounted(() => {
|
||||
:sm="12"
|
||||
:md="12"
|
||||
:lg="6"
|
||||
class="mb-4"
|
||||
>
|
||||
<!-- TODO @haohao:卡片之间的上下距离,太宽了。 -->
|
||||
<Card :body-style="{ padding: '20px' }" class="product-card h-full">
|
||||
<Card
|
||||
: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">
|
||||
<!-- TODO @haohao:图标太大了;看看是不是参考 vue3 + element-plus 搞小点;然后标题居中。 -->
|
||||
<div class="mb-3 flex items-center">
|
||||
<div class="product-icon">
|
||||
<IconifyIcon
|
||||
:icon="item.icon || 'ant-design:inbox-outlined'"
|
||||
class="text-3xl"
|
||||
:icon="item.icon || 'lucide:box'"
|
||||
class="text-xl"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3 min-w-0 flex-1">
|
||||
@@ -132,7 +127,7 @@ onMounted(() => {
|
||||
</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-item">
|
||||
<span class="info-label">产品分类</span>
|
||||
@@ -156,20 +151,25 @@ onMounted(() => {
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">产品标识</span>
|
||||
<!-- TODO @haohao:展示 ?有点奇怪,要不小手? -->
|
||||
<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 }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO @haohao:这里是不是有 image?然后默认 icon -->
|
||||
<!-- TODO @haohao:高度太高了。建议和左侧(产品分类 + 产品类型 + 产品标识)高度保持一致 -->
|
||||
<div class="product-3d-icon">
|
||||
<!-- 产品图片 -->
|
||||
<div class="product-image">
|
||||
<Image
|
||||
v-if="item.picUrl"
|
||||
:src="item.picUrl"
|
||||
:preview="true"
|
||||
class="size-full rounded object-cover"
|
||||
/>
|
||||
<IconifyIcon
|
||||
icon="ant-design:box-plot-outlined"
|
||||
class="text-2xl"
|
||||
v-else
|
||||
icon="lucide:image"
|
||||
class="text-2xl opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,8 +180,7 @@ onMounted(() => {
|
||||
class="action-btn action-btn-edit"
|
||||
@click="emit('edit', item)"
|
||||
>
|
||||
<!-- TODO @haohao:按钮尽量用中立的按钮,方便迁移 ele; -->
|
||||
<IconifyIcon icon="ant-design:edit-outlined" class="mr-1" />
|
||||
<IconifyIcon icon="lucide:edit" class="mr-1" />
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
@@ -189,7 +188,7 @@ onMounted(() => {
|
||||
class="action-btn action-btn-detail"
|
||||
@click="emit('detail', item.id)"
|
||||
>
|
||||
<IconifyIcon icon="ant-design:eye-outlined" class="mr-1" />
|
||||
<IconifyIcon icon="lucide:eye" class="mr-1" />
|
||||
详情
|
||||
</Button>
|
||||
<Button
|
||||
@@ -197,23 +196,17 @@ onMounted(() => {
|
||||
class="action-btn action-btn-model"
|
||||
@click="emit('thingModel', item.id)"
|
||||
>
|
||||
<IconifyIcon
|
||||
icon="ant-design:apartment-outlined"
|
||||
class="mr-1"
|
||||
/>
|
||||
<IconifyIcon icon="lucide:git-branch" class="mr-1" />
|
||||
物模型
|
||||
</Button>
|
||||
<Tooltip v-if="item.status === 1" title="启用状态的产品不能删除">
|
||||
<Tooltip v-if="item.status === 1" title="已发布的产品不能删除">
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
disabled
|
||||
class="action-btn action-btn-delete !w-8"
|
||||
>
|
||||
<IconifyIcon
|
||||
icon="ant-design:delete-outlined"
|
||||
class="text-sm"
|
||||
/>
|
||||
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
@@ -226,10 +219,7 @@ onMounted(() => {
|
||||
danger
|
||||
class="action-btn action-btn-delete !w-8"
|
||||
>
|
||||
<IconifyIcon
|
||||
icon="ant-design:delete-outlined"
|
||||
class="text-sm"
|
||||
/>
|
||||
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
@@ -241,8 +231,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<!-- TODO @haohao:放到最右侧好点 -->
|
||||
<div v-if="list.length > 0" class="flex justify-center">
|
||||
<div v-if="list.length > 0" class="flex justify-end">
|
||||
<Pagination
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
@@ -258,18 +247,9 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/** TODO @haohao:看看哪些可以 tindwind 掉 */
|
||||
.product-card-view {
|
||||
.product-card {
|
||||
height: 100%;
|
||||
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) {
|
||||
display: flex;
|
||||
@@ -283,8 +263,8 @@ onMounted(() => {
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 8px;
|
||||
@@ -294,9 +274,9 @@ onMounted(() => {
|
||||
.product-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
line-height: 36px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -305,7 +285,7 @@ onMounted(() => {
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
|
||||
&:last-child {
|
||||
@@ -338,7 +318,6 @@ onMounted(() => {
|
||||
font-size: 12px;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
cursor: help;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
@@ -348,18 +327,17 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// 3D 图标
|
||||
.product-3d-icon {
|
||||
// 产品图片
|
||||
.product-image {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: #667eea;
|
||||
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
||||
border-radius: 8px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
// 按钮组
|
||||
@@ -420,10 +398,6 @@ onMounted(() => {
|
||||
html.dark {
|
||||
.product-card-view {
|
||||
.product-card {
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgb(0 0 0 / 30%);
|
||||
}
|
||||
|
||||
.product-title {
|
||||
color: rgb(255 255 255 / 85%);
|
||||
}
|
||||
@@ -442,7 +416,7 @@ html.dark {
|
||||
}
|
||||
}
|
||||
|
||||
.product-3d-icon {
|
||||
.product-image {
|
||||
color: #8b9cff;
|
||||
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">
|
||||
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 { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
@@ -13,8 +12,8 @@ import { getDeviceCount } from '#/api/iot/device/device';
|
||||
import { getProduct } from '#/api/iot/product/product';
|
||||
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
|
||||
|
||||
import ProductDetailsHeader from './product-details-header.vue';
|
||||
import ProductDetailsInfo from './product-details-info.vue';
|
||||
import ProductDetailsHeader from './modules/header.vue';
|
||||
import ProductDetailsInfo from './modules/info.vue';
|
||||
|
||||
defineOptions({ name: 'IoTProductDetail' });
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
// TODO @haohao:放到 detail/modules 里。然后名字就是 header.vue
|
||||
<script lang="ts" setup>
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
|
||||
import { ref } from 'vue';
|
||||
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 {
|
||||
product: IotProductApi.Product;
|
||||
@@ -25,7 +28,11 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const formRef = ref();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 复制到剪贴板 */
|
||||
async function copyToClipboard(text: string) {
|
||||
@@ -46,59 +53,63 @@ function goToDeviceList(productId: number) {
|
||||
}
|
||||
|
||||
/** 打开编辑表单 */
|
||||
function openForm(type: string, id?: number) {
|
||||
formRef.value?.open(type, id);
|
||||
function openEditForm(row: IotProductApi.Product) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 发布产品 */
|
||||
async function confirmPublish(id: number) {
|
||||
// TODO @haohao:最好类似;async function handleDeleteBatch() { 的做法:1)有个 confirm;2)有个 loading
|
||||
try {
|
||||
await updateProductStatus(id, 1); // TODO @好好】:1 和 0,最好用枚举;
|
||||
message.success('发布成功');
|
||||
emit('refresh');
|
||||
} catch {
|
||||
message.error('发布失败');
|
||||
}
|
||||
function handlePublish(product: IotProductApi.Product) {
|
||||
Modal.confirm({
|
||||
title: '确认发布',
|
||||
content: `确认要发布产品「${product.name}」吗?`,
|
||||
async onOk() {
|
||||
await updateProductStatus(product.id!, ProductStatusEnum.PUBLISHED);
|
||||
message.success('发布成功');
|
||||
emit('refresh');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 撤销发布 */
|
||||
async function confirmUnpublish(id: number) {
|
||||
// TODO @haohao:最好类似;async function handleDeleteBatch() { 的做法:1)有个 confirm;2)有个 loading
|
||||
try {
|
||||
await updateProductStatus(id, 0);
|
||||
message.success('撤销发布成功');
|
||||
emit('refresh');
|
||||
} catch {
|
||||
message.error('撤销发布失败');
|
||||
}
|
||||
function handleUnpublish(product: IotProductApi.Product) {
|
||||
Modal.confirm({
|
||||
title: '确认撤销发布',
|
||||
content: `确认要撤销发布产品「${product.name}」吗?`,
|
||||
async onOk() {
|
||||
await updateProductStatus(product.id!, ProductStatusEnum.UNPUBLISHED);
|
||||
message.success('撤销发布成功');
|
||||
emit('refresh');
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-4">
|
||||
<FormModal @success="emit('refresh')" />
|
||||
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">{{ product.name }}</h2>
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
:disabled="product.status === 1"
|
||||
@click="openForm('update', product.id)"
|
||||
:disabled="product.status === ProductStatusEnum.PUBLISHED"
|
||||
@click="openEditForm(product)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
v-if="product.status === 0"
|
||||
v-if="product.status === ProductStatusEnum.UNPUBLISHED"
|
||||
type="primary"
|
||||
@click="confirmPublish(product.id!)"
|
||||
@click="handlePublish(product)"
|
||||
>
|
||||
发布
|
||||
</Button>
|
||||
<Button
|
||||
v-if="product.status === 1"
|
||||
v-if="product.status === ProductStatusEnum.PUBLISHED"
|
||||
danger
|
||||
@click="confirmUnpublish(product.id!)"
|
||||
@click="handleUnpublish(product)"
|
||||
>
|
||||
撤销发布
|
||||
</Button>
|
||||
@@ -127,9 +138,5 @@ async function confirmUnpublish(id: number) {
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<!-- 表单弹窗 -->
|
||||
<!-- TODO @haohao:弹不出来;另外,应该用 index.vue 里,Form 的声明方式哈。 -->
|
||||
<ProductForm ref="formRef" @success="emit('refresh')" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
// TODO @haohao:放到 detail/modules 里。然后名字就是 info.vue
|
||||
<script lang="ts" setup>
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
@@ -24,7 +23,6 @@ function formatDate(date?: Date | string) {
|
||||
|
||||
<template>
|
||||
<Card title="产品信息">
|
||||
<!-- TODO @haohao:看看是不是用 description 组件 -->
|
||||
<Descriptions bordered :column="3" size="small">
|
||||
<Descriptions.Item label="产品名称">
|
||||
{{ product.name }}
|
||||
@@ -48,7 +46,7 @@ function formatDate(date?: Date | string) {
|
||||
{{ product.codecType || '-' }}
|
||||
</Descriptions.Item>
|
||||
<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
|
||||
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 { VxeGridProps, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
import type { MallCategoryApi } from '#/api/mall/product/category';
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
|
||||
@@ -133,6 +133,13 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
placeholder: '请输入最大砍价金额',
|
||||
},
|
||||
},
|
||||
// TODO @puhui999:这里交互不太对,可以对比下 element-plus 版本呢
|
||||
{
|
||||
fieldName: 'spuId',
|
||||
label: '砍价商品',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
updateBargainActivity,
|
||||
} from '#/api/mall/promotion/bargain/bargainActivity';
|
||||
import { $t } from '#/locales';
|
||||
import { SpuShowcase } from '#/views/mall/product/spu/components';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
@@ -21,7 +22,7 @@ defineOptions({ name: 'PromotionBargainActivityForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formData = ref<MallBargainActivityApi.BargainActivity>();
|
||||
const formData = ref<Partial<MallBargainActivityApi.BargainActivity>>({});
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['砍价活动'])
|
||||
@@ -49,8 +50,11 @@ const [Modal, modalApi] = useVbenModal({
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as MallBargainActivityApi.BargainActivity;
|
||||
const values = await formApi.getValues();
|
||||
const data = {
|
||||
...values,
|
||||
spuId: formData.value.spuId,
|
||||
} as MallBargainActivityApi.BargainActivity;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateBargainActivity(data)
|
||||
@@ -65,7 +69,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
formData.value = {};
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
@@ -87,6 +91,11 @@ const [Modal, modalApi] = useVbenModal({
|
||||
|
||||
<template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -105,11 +105,12 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||
},
|
||||
},
|
||||
// TODO @puhui999:这里交互不太对,可以对比下 element-plus 版本呢
|
||||
{
|
||||
// TODO
|
||||
fieldName: 'spuId',
|
||||
label: '拼团商品',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -13,13 +13,16 @@ import {
|
||||
updateCombinationActivity,
|
||||
} from '#/api/mall/promotion/combination/combinationActivity';
|
||||
import { $t } from '#/locales';
|
||||
import { SpuShowcase } from '#/views/mall/product/spu/components';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
defineOptions({ name: 'CombinationActivityForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallCombinationActivityApi.CombinationActivity>();
|
||||
const formData = ref<Partial<MallCombinationActivityApi.CombinationActivity>>(
|
||||
{},
|
||||
);
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['拼团活动'])
|
||||
@@ -47,8 +50,11 @@ const [Modal, modalApi] = useVbenModal({
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as MallCombinationActivityApi.CombinationActivity;
|
||||
const values = await formApi.getValues();
|
||||
const data = {
|
||||
...values,
|
||||
spuId: formData.value.spuId,
|
||||
} as MallCombinationActivityApi.CombinationActivity;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateCombinationActivity(data)
|
||||
@@ -63,7 +69,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
formData.value = {};
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
@@ -86,6 +92,11 @@ const [Modal, modalApi] = useVbenModal({
|
||||
|
||||
<template>
|
||||
<Modal class="w-3/5" :title="getTitle">
|
||||
<Form />
|
||||
<Form>
|
||||
<!-- 自定义插槽:商品选择 -->
|
||||
<template #spuId>
|
||||
<SpuShowcase v-model="formData.spuId" :limit="1" />
|
||||
</template>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -111,7 +111,6 @@ function emitActivityChange() {
|
||||
>
|
||||
<Tooltip :title="activity.name">
|
||||
<div class="relative h-full w-full">
|
||||
<!-- TODO @芋艿 -->
|
||||
<Image
|
||||
:src="activity.picUrl"
|
||||
class="h-full w-full rounded-lg object-cover"
|
||||
|
||||
@@ -133,9 +133,8 @@ function handleSliderChange(prop: string) {
|
||||
</TabPane>
|
||||
|
||||
<!-- 每个组件的通用内容 -->
|
||||
<!-- TODO @xingyu:这里的样式,貌似没 ele 版本的好看。 -->
|
||||
<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">
|
||||
<Form :model="formData">
|
||||
<FormItem
|
||||
@@ -181,7 +180,7 @@ function handleSliderChange(prop: string) {
|
||||
class="mb-0 w-full"
|
||||
>
|
||||
<Row>
|
||||
<Col :span="11">
|
||||
<Col :span="19">
|
||||
<Slider
|
||||
v-model:value="
|
||||
formData[dataRef.prop as keyof ComponentStyle]
|
||||
@@ -192,8 +191,9 @@ function handleSliderChange(prop: string) {
|
||||
class="mr-4"
|
||||
/>
|
||||
</Col>
|
||||
<Col :span="2">
|
||||
<Col :span="4">
|
||||
<InputNumber
|
||||
class="w-[50px]"
|
||||
:max="100"
|
||||
:min="0"
|
||||
v-model:value="
|
||||
|
||||
@@ -98,7 +98,7 @@ const handleDeleteComponent = () => {
|
||||
<component :is="component.id" :property="component.property" />
|
||||
</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">
|
||||
@@ -109,8 +109,6 @@ const handleDeleteComponent = () => {
|
||||
class="component-toolbar"
|
||||
v-if="showToolbar && component.name && active"
|
||||
>
|
||||
<!-- TODO @xingyu:按钮少的时候,会存在遮住的情况; -->
|
||||
<!-- TODO @xingyu:貌似中间的选中框框,没全部框柱。上面多了点,下面少了点。 -->
|
||||
<VerticalButtonGroup size="small">
|
||||
<Button
|
||||
:disabled="!canMoveUp"
|
||||
@@ -171,7 +169,6 @@ const handleDeleteComponent = () => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$active-border-width: 2px;
|
||||
$hover-border-width: 1px;
|
||||
|
||||
@@ -100,5 +100,4 @@ function handleCloneComponent(component: DiyComponent<any>) {
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
<!-- TODO @xingyu:ele 里面有一些 style,看看是不是都迁移完了;特别是 drag-area 是全局样式; -->
|
||||
</template>
|
||||
|
||||
@@ -106,7 +106,6 @@ watch(
|
||||
</div>
|
||||
</Carousel>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
// Ant Design Vue Carousel 样式调整
|
||||
:deep(.ant-carousel .ant-carousel-dots) {
|
||||
|
||||
@@ -14,7 +14,7 @@ defineProps<{ property: UserCardProperty }>();
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center justify-between px-4 py-6">
|
||||
<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" />
|
||||
</Avatar>
|
||||
<span class="text-[18px] font-bold">芋道源码</span>
|
||||
|
||||
@@ -38,7 +38,7 @@ const props = defineProps({
|
||||
|
||||
const emits = defineEmits(['reset', 'save', 'update:modelValue']); // 工具栏操作
|
||||
|
||||
// TODO @xingyu:要不要加这个?
|
||||
// TODO @xingyu:要不要加这个?ele 里是有这个的。
|
||||
// const qrcode = useQRCode(props.previewUrl, {
|
||||
// errorCorrectionLevel: 'H',
|
||||
// margin: 4,
|
||||
@@ -175,8 +175,7 @@ function handleComponentSelected(
|
||||
index: number = -1,
|
||||
) {
|
||||
// 使用深拷贝避免响应式追踪循环警告
|
||||
// TODO @xingyu:这个是必须的么?ele 没有哈。
|
||||
selectedComponent.value = cloneDeep(component);
|
||||
selectedComponent.value = component;
|
||||
selectedComponentIndex.value = index;
|
||||
}
|
||||
|
||||
@@ -344,7 +343,7 @@ onMounted(() => {
|
||||
<!-- 中心:设计区域(ComponentContainer) -->
|
||||
<Col :span="12">
|
||||
<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"
|
||||
>
|
||||
<!-- 手机顶部 -->
|
||||
@@ -378,20 +377,20 @@ onMounted(() => {
|
||||
</div>
|
||||
<!-- 手机页面编辑区域 -->
|
||||
<div
|
||||
class="min-h-full w-full"
|
||||
class="mx-auto min-h-full w-96 bg-no-repeat"
|
||||
:style="{
|
||||
// backgroundColor: pageConfigComponent.property.backgroundColor,
|
||||
backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`,
|
||||
}"
|
||||
>
|
||||
<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
|
||||
v-model="pageComponents"
|
||||
:animation="200"
|
||||
:force-fallback="false"
|
||||
class="min-h-full w-full"
|
||||
class="min-h-[70vh] w-full"
|
||||
filter=".component-toolbar"
|
||||
ghost-class="draggable-ghost"
|
||||
group="component"
|
||||
@@ -508,5 +507,4 @@ onMounted(() => {
|
||||
</div>
|
||||
</PreviewModal>
|
||||
</Page>
|
||||
<!-- TODO @xingyu:这里改造完后,类似 web-ele/src/views/mall/promotion/components/diy-editor/index.vue 里的全局样式(递推到子组件)里的就没没了,类似 property-group -->
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Space } from 'ant-design-vue';
|
||||
|
||||
// TODO @芋艿、@xingyu:貌似上下移动的按钮,被遮住了!
|
||||
/**
|
||||
* 垂直按钮组
|
||||
* Ant Design Vue 的按钮组,通过 Space 实现垂直布局
|
||||
|
||||
@@ -60,14 +60,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
rules: 'required',
|
||||
defaultValue: PromotionProductScopeEnum.ALL.scope,
|
||||
},
|
||||
// TODO @puhui999: 商品选择器优化
|
||||
{
|
||||
fieldName: 'productSpuIds',
|
||||
label: '商品',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请选择商品',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['productScope', 'productScopeValues'],
|
||||
show: (model) =>
|
||||
@@ -84,14 +80,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
// TODO @puhui999: 商品分类选择器优化
|
||||
{
|
||||
fieldName: 'productCategoryIds',
|
||||
label: '商品分类',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请选择商品分类',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['productScope', 'productScopeValues'],
|
||||
show: (model) =>
|
||||
|
||||
@@ -16,11 +16,18 @@ import {
|
||||
updateCouponTemplate,
|
||||
} from '#/api/mall/promotion/coupon/couponTemplate';
|
||||
import { $t } from '#/locales';
|
||||
import { ProductCategorySelect } from '#/views/mall/product/category/components';
|
||||
import { SpuShowcase } from '#/views/mall/product/spu/components';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallCouponTemplateApi.CouponTemplate>();
|
||||
const formData = ref<
|
||||
Partial<MallCouponTemplateApi.CouponTemplate> & {
|
||||
productCategoryIds?: number | number[];
|
||||
productSpuIds?: number[];
|
||||
}
|
||||
>({});
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['优惠券模板'])
|
||||
@@ -64,7 +71,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
formData.value = {};
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
@@ -75,7 +82,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getCouponTemplate(data.id);
|
||||
const processedData = await processLoadData(formData.value);
|
||||
const processedData = await processLoadData(formData.value as any);
|
||||
// 设置到表单
|
||||
await formApi.setValues(processedData);
|
||||
} finally {
|
||||
@@ -144,6 +151,15 @@ async function processLoadData(
|
||||
|
||||
<template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -67,8 +67,15 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
placeholder: '请输入备注',
|
||||
rows: 4,
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
},
|
||||
{
|
||||
fieldName: 'spuIds',
|
||||
label: '活动商品',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
formItemClass: 'col-span-2',
|
||||
},
|
||||
// TODO
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -13,13 +13,18 @@ import {
|
||||
updateDiscountActivity,
|
||||
} from '#/api/mall/promotion/discount/discountActivity';
|
||||
import { $t } from '#/locales';
|
||||
import { SpuShowcase } from '#/views/mall/product/spu/components';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
defineOptions({ name: 'DiscountActivityForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallDiscountActivityApi.DiscountActivity>();
|
||||
const formData = ref<
|
||||
Partial<MallDiscountActivityApi.DiscountActivity> & {
|
||||
spuIds?: number[];
|
||||
}
|
||||
>({});
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['限时折扣活动'])
|
||||
@@ -69,7 +74,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
formData.value = {};
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
@@ -91,6 +96,11 @@ const [Modal, modalApi] = useVbenModal({
|
||||
|
||||
<template>
|
||||
<Modal class="w-3/5" :title="getTitle">
|
||||
<Form />
|
||||
<Form>
|
||||
<!-- 自定义插槽:商品选择 -->
|
||||
<template #spuIds>
|
||||
<SpuShowcase v-model="formData.spuIds" />
|
||||
</template>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -27,34 +27,28 @@ const route = useRoute();
|
||||
const { refreshTab } = useTabs();
|
||||
|
||||
const domain = import.meta.env.VITE_MALL_H5_DOMAIN;
|
||||
// 特殊:存储 reset 重置时,当前 selectedTemplateItem 值,从而进行恢复
|
||||
const DIY_PAGE_INDEX_KEY = 'diy_page_index';
|
||||
const DIY_PAGE_INDEX_KEY = 'diy_page_index'; // 特殊:存储 reset 重置时,当前 selectedTemplateItem 值,从而进行恢复
|
||||
|
||||
const selectedTemplateItem = ref(0);
|
||||
// 左上角工具栏操作按钮
|
||||
const templateItems = ref([
|
||||
{ key: 0, name: '基础设置', icon: 'lucide:settings' },
|
||||
{ key: 1, name: '首页', icon: 'lucide:home' },
|
||||
{ key: 2, name: '我的', icon: 'lucide:user' },
|
||||
]);
|
||||
]); // 左上角工具栏操作按钮
|
||||
|
||||
const formData = ref<MallDiyTemplateApi.DiyTemplateProperty>();
|
||||
// 当前编辑的属性
|
||||
const currentFormData = ref<
|
||||
MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty
|
||||
>({
|
||||
property: '',
|
||||
} as MallDiyPageApi.DiyPage);
|
||||
// templateItem 对应的缓存
|
||||
} as MallDiyPageApi.DiyPage); // 当前编辑的属性
|
||||
const currentFormDataMap = ref<
|
||||
Map<string, MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty>
|
||||
>(new Map());
|
||||
// 商城 H5 预览地址
|
||||
const previewUrl = ref('');
|
||||
// 模板组件库
|
||||
const templateLibs = [] as DiyComponentLibrary[];
|
||||
// 当前组件库
|
||||
const libs = ref<DiyComponentLibrary[]>(templateLibs);
|
||||
>(new Map()); // templateItem 对应的缓存
|
||||
|
||||
const previewUrl = ref(''); // 商城 H5 预览地址
|
||||
const templateLibs = [] as DiyComponentLibrary[]; // 模板组件库
|
||||
const libs = ref<DiyComponentLibrary[]>(templateLibs); // 当前组件库
|
||||
|
||||
/** 获取详情 */
|
||||
async function getPageDetail(id: any) {
|
||||
@@ -73,23 +67,23 @@ async function getPageDetail(id: any) {
|
||||
}
|
||||
|
||||
/** 模板选项切换 */
|
||||
function handleTemplateItemChange(val: any) {
|
||||
const changeValue = val.target.value;
|
||||
function handleTemplateItemChange(valObj: any) {
|
||||
const val = valObj.target.value;
|
||||
// 缓存模版编辑数据
|
||||
currentFormDataMap.value.set(
|
||||
templateItems.value[changeValue]!.name,
|
||||
templateItems.value[selectedTemplateItem.value]?.name || '',
|
||||
currentFormData.value!,
|
||||
);
|
||||
// 切换模版
|
||||
selectedTemplateItem.value = changeValue;
|
||||
|
||||
// 读取模版缓存
|
||||
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;
|
||||
currentFormData.value = (isEmpty(data) ? formData.value : data) as
|
||||
| MallDiyPageApi.DiyPage
|
||||
@@ -103,7 +97,7 @@ function handleTemplateItemChange(val: any) {
|
||||
isEmpty(data)
|
||||
? formData.value!.pages.find(
|
||||
(page: MallDiyPageApi.DiyPage) =>
|
||||
page.name === templateItems.value[changeValue]!.name,
|
||||
page.name === templateItems.value[val]?.name,
|
||||
)
|
||||
: data
|
||||
) as MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty;
|
||||
|
||||
@@ -126,15 +126,13 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
},
|
||||
// TODO @puhui999:商品图太大了。
|
||||
{
|
||||
fieldName: 'spuId',
|
||||
label: '活动商品',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
formItemClass: 'col-span-2',
|
||||
renderComponentContent: () => ({
|
||||
default: () => null,
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ function handleEdit(row: MallRewardActivityApi.RewardActivity) {
|
||||
|
||||
/** 关闭满减送活动 */
|
||||
async function handleClose(row: MallRewardActivityApi.RewardActivity) {
|
||||
// TODO @puhui999:这个国际化,需要加下哈;closing、closeSuccess;
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.closing', [row.name]),
|
||||
duration: 0,
|
||||
|
||||
@@ -29,6 +29,7 @@ const emit = defineEmits(['success']);
|
||||
|
||||
const formData = ref<Partial<MallRewardActivityApi.RewardActivity>>({
|
||||
conditionType: PromotionConditionTypeEnum.PRICE.type,
|
||||
productScope: PromotionProductScopeEnum.ALL.scope,
|
||||
rules: [],
|
||||
});
|
||||
|
||||
|
||||
@@ -102,46 +102,53 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<Button type="link" class="pl-0" @click="handleSelect">添加优惠券</Button>
|
||||
|
||||
<div
|
||||
v-for="(item, index) in list"
|
||||
:key="item.id"
|
||||
class="coupon-list-item mb-2 flex justify-between rounded-lg border border-dashed border-gray-300 p-2"
|
||||
>
|
||||
<div class="coupon-list-item-left flex flex-wrap items-center gap-2">
|
||||
<div>优惠券名称:{{ item.name }}</div>
|
||||
<div>
|
||||
范围:
|
||||
<DictTag
|
||||
:type="DICT_TYPE.PROMOTION_PRODUCT_SCOPE"
|
||||
:value="item.productScope"
|
||||
/>
|
||||
<div>
|
||||
<!-- 已选优惠券列表 -->
|
||||
<div v-if="list.length > 0" class="mb-2 flex flex-col gap-2">
|
||||
<div
|
||||
v-for="(item, index) in list"
|
||||
:key="item.id"
|
||||
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="flex flex-wrap items-center gap-3">
|
||||
<span class="font-medium text-gray-800">{{ item.name }}</span>
|
||||
<span class="flex items-center gap-1 text-sm text-gray-500">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.PROMOTION_PRODUCT_SCOPE"
|
||||
: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 class="flex items-center">
|
||||
优惠:
|
||||
<DictTag
|
||||
:type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE"
|
||||
:value="item.discountType"
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<span class="text-gray-500">送</span>
|
||||
<Input
|
||||
v-model:value="item.giveCount"
|
||||
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 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>
|
||||
|
||||
<!-- 优惠券选择 -->
|
||||
<!-- 添加按钮 -->
|
||||
<Button type="link" class="!pl-0" @click="handleSelect">
|
||||
+ 添加优惠券
|
||||
</Button>
|
||||
|
||||
<!-- 优惠券选择弹窗 -->
|
||||
<CouponSelect
|
||||
ref="selectRef"
|
||||
:take-type="CouponTemplateTakeTypeEnum.ADMIN.type"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PromotionConditionTypeEnum } from '@vben/constants';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Form,
|
||||
FormItem,
|
||||
@@ -61,76 +62,92 @@ function handleDelete(ruleIndex: number) {
|
||||
<Row :gutter="[16, 16]">
|
||||
<template v-if="formData.rules">
|
||||
<Col v-for="(rule, index) in formData.rules" :key="index" :span="24">
|
||||
<!-- 规则标题 -->
|
||||
<div class="mb-4 flex items-center">
|
||||
<span class="text-base font-bold">活动层级 {{ index + 1 }}</span>
|
||||
<Button
|
||||
v-if="index !== 0"
|
||||
type="link"
|
||||
danger
|
||||
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>
|
||||
<Card size="small" class="rounded-lg">
|
||||
<!-- 规则标题 -->
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
<span class="text-base font-medium">
|
||||
活动层级 {{ index + 1 }}
|
||||
</span>
|
||||
</div>
|
||||
</FormItem>
|
||||
<!-- 优惠内容 -->
|
||||
<FormItem
|
||||
label="优惠内容"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 20 }"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span>订单金额优惠</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>减</span>
|
||||
</template>
|
||||
<template v-if="index !== 0" #extra>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
@click="handleDelete(index)"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</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
|
||||
v-model:value="rule.discountPrice"
|
||||
v-if="isPriceCondition"
|
||||
v-model:value="rule.limit"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="!w-32"
|
||||
class="!w-40"
|
||||
placeholder="请输入金额"
|
||||
/>
|
||||
<span>元</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>包邮:</span>
|
||||
<Switch
|
||||
v-model:checked="rule.freeDelivery"
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
<Input
|
||||
v-else
|
||||
v-model:value="rule.limit"
|
||||
:min="0"
|
||||
class="!w-40"
|
||||
placeholder="请输入数量"
|
||||
type="number"
|
||||
/>
|
||||
<span>{{ isPriceCondition ? '元' : '件' }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div>送积分:</div>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
</FormItem>
|
||||
<!-- 优惠内容 -->
|
||||
<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>
|
||||
<InputNumber
|
||||
v-model:value="rule.point"
|
||||
@@ -140,17 +157,24 @@ function handleDelete(ruleIndex: number) {
|
||||
/>
|
||||
<span>积分</span>
|
||||
</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 class="flex items-center gap-2">
|
||||
<span class="w-20">送优惠券:</span>
|
||||
<RewardRuleCouponSelect
|
||||
:model-value="rule"
|
||||
@update:model-value="(val) => (formData.rules![index] = val)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Card>
|
||||
</Col>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -8,12 +8,14 @@ import { useVbenModal } from '@vben/common-ui';
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getSpu } from '#/api/mall/product/spu';
|
||||
import {
|
||||
createSeckillActivity,
|
||||
getSeckillActivity,
|
||||
updateSeckillActivity,
|
||||
} from '#/api/mall/promotion/seckill/seckillActivity';
|
||||
import { $t } from '#/locales';
|
||||
import { SpuSkuSelect } from '#/views/mall/product/spu/components';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
@@ -31,25 +33,37 @@ const spuId = ref<number>();
|
||||
const spuName = ref<string>('');
|
||||
const skuTableData = ref<any[]>([]);
|
||||
|
||||
// 选择商品(占位函数,实际需要对接商品选择组件)
|
||||
const spuSkuSelectRef = ref(); // 商品选择弹窗 Ref
|
||||
|
||||
/** 打开商品选择弹窗 */
|
||||
const handleSelectProduct = () => {
|
||||
message.info('商品选择功能需要对接商品选择组件');
|
||||
// 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,
|
||||
// }));
|
||||
spuSkuSelectRef.value?.open();
|
||||
};
|
||||
|
||||
/** 选择商品后的回调 */
|
||||
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 =================
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
@@ -137,10 +151,30 @@ const [Modal, modalApi] = useVbenModal({
|
||||
await nextTick();
|
||||
await formApi.setValues(formData.value);
|
||||
|
||||
// TODO: 加载商品和 SKU 信息
|
||||
// 需要调用商品 API 获取 SPU 详情
|
||||
// spuId.value = formData.value.spuId;
|
||||
// await loadProductDetails(formData.value.spuId, formData.value.products);
|
||||
// 加载商品和 SKU 信息
|
||||
if (formData.value.spuId) {
|
||||
const spu = await getSpu(formData.value.spuId);
|
||||
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 {
|
||||
modalApi.unlock();
|
||||
}
|
||||
@@ -217,4 +251,11 @@ const [Modal, modalApi] = useVbenModal({
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!-- 商品选择器弹窗 -->
|
||||
<SpuSkuSelect
|
||||
ref="spuSkuSelectRef"
|
||||
:is-select-sku="true"
|
||||
@select="handleSpuSelected"
|
||||
/>
|
||||
</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"
|
||||
@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" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@@ -14,7 +14,7 @@ defineOptions({ name: 'TabNews' });
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Reply;
|
||||
newsType: NewsType;
|
||||
newsType?: NewsType;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { MpMaterialApi } from '#/api/mp/material';
|
||||
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
@@ -81,7 +82,7 @@ watch(
|
||||
onClick: () => openWindow(row.url),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { MpMaterialApi } from '#/api/mp/material';
|
||||
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
@@ -81,7 +82,7 @@ watch(
|
||||
onClick: () => openWindow(row.url),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
|
||||
@@ -102,6 +102,7 @@ function handlePageChange(page: number, pageSize: number) {
|
||||
function showTotal(total: number) {
|
||||
return `共 ${total} 条`;
|
||||
}
|
||||
// TODO @dylan:是不是应该都用 Grid 哈:1)message-table 大部分合并到 index.vue;2)message-table 的 schema 放到 data.ts 里;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -82,6 +82,18 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
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',
|
||||
label: '状态',
|
||||
|
||||
@@ -7,27 +7,16 @@ import { requestClient } from '#/api/request';
|
||||
export namespace BpmTaskApi {
|
||||
/** 流程任务 */
|
||||
export interface Task {
|
||||
id: number; // 编号
|
||||
name: string; // 监听器名字
|
||||
type: string; // 监听器类型
|
||||
status: number; // 监听器状态
|
||||
event: string; // 监听事件
|
||||
valueType: string; // 监听器值类型
|
||||
processInstance?: BpmProcessInstanceApi.ProcessInstance; // 流程实例
|
||||
}
|
||||
|
||||
// 流程任务
|
||||
export interface TaskManager {
|
||||
id: string; // 编号
|
||||
name: string; // 任务名称
|
||||
name: string; // 任务名字
|
||||
status: number; // 任务状态
|
||||
createTime: number; // 创建时间
|
||||
endTime: number; // 结束时间
|
||||
durationInMillis: number; // 持续时间
|
||||
status: number; // 状态
|
||||
reason: string; // 原因
|
||||
reason: string; // 审批理由
|
||||
ownerUser: any; // 负责人
|
||||
assigneeUser: any; // 处理人
|
||||
taskDefinitionKey: string; // 任务定义key
|
||||
taskDefinitionKey: string; // 任务定义的标识
|
||||
processInstanceId: string; // 流程实例id
|
||||
processInstance: BpmProcessInstanceApi.ProcessInstance; // 流程实例
|
||||
parentTaskId: any; // 父任务id
|
||||
|
||||
@@ -17,6 +17,7 @@ export namespace InfraFileConfigApi {
|
||||
accessSecret?: string;
|
||||
pathStyle?: boolean;
|
||||
enablePublicAccess?: boolean;
|
||||
region?: string;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export namespace SystemSocialClientApi {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
agentId?: string;
|
||||
publicKey?: string;
|
||||
status: number;
|
||||
createTime?: Date;
|
||||
}
|
||||
|
||||
@@ -9,24 +9,6 @@ const routes: RouteRecordRaw[] = [
|
||||
hideInMenu: true,
|
||||
},
|
||||
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',
|
||||
component: () => import('#/views/bpm/processInstance/detail/index.vue'),
|
||||
|
||||
@@ -146,7 +146,6 @@
|
||||
background: url('./svg/simple-process-bg.svg') 0 0 repeat;
|
||||
transform: scale(1);
|
||||
transform-origin: 50% 0 0;
|
||||
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
// 节点容器 定义节点宽度
|
||||
.node-container {
|
||||
width: 200px;
|
||||
|
||||
@@ -259,6 +259,7 @@ async function validateAllSteps() {
|
||||
return true;
|
||||
}
|
||||
|
||||
const saveLoading = ref<boolean>(false);
|
||||
/** 保存操作 */
|
||||
async function handleSave() {
|
||||
try {
|
||||
@@ -272,7 +273,7 @@ async function handleSave() {
|
||||
const modelData = {
|
||||
...formData.value,
|
||||
};
|
||||
|
||||
saveLoading.value = true;
|
||||
switch (actionType) {
|
||||
case 'copy': {
|
||||
// 情况三:复制场景
|
||||
@@ -309,9 +310,12 @@ async function handleSave() {
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('保存失败:', error);
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 发布加载中状态
|
||||
const deployLoading = ref<boolean>(false);
|
||||
/** 发布操作 */
|
||||
async function handleDeploy() {
|
||||
try {
|
||||
@@ -322,6 +326,8 @@ async function handleDeploy() {
|
||||
// 1.2 校验所有步骤
|
||||
await validateAllSteps();
|
||||
|
||||
deployLoading.value = true;
|
||||
|
||||
// 2.1 更新表单数据
|
||||
const modelData = {
|
||||
...formData.value,
|
||||
@@ -342,6 +348,8 @@ async function handleDeploy() {
|
||||
} catch (error: any) {
|
||||
console.error('发布失败:', error);
|
||||
ElMessage.warning(error.message || '发布失败');
|
||||
} finally {
|
||||
deployLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,11 +456,12 @@ onBeforeUnmount(() => {
|
||||
<ElButton
|
||||
v-if="actionType === 'update'"
|
||||
type="primary"
|
||||
:loading="deployLoading"
|
||||
@click="handleDeploy"
|
||||
>
|
||||
发 布
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="handleSave">
|
||||
<ElButton type="primary" @click="handleSave" :loading="saveLoading">
|
||||
<span v-if="actionType === 'definition'">恢 复</span>
|
||||
<span v-else>保 存</span>
|
||||
</ElButton>
|
||||
|
||||
@@ -234,9 +234,10 @@ onMounted(() => {
|
||||
>
|
||||
<ElCard
|
||||
shadow="hover"
|
||||
class="definition-item-card w-full cursor-pointer"
|
||||
class="w-full cursor-pointer"
|
||||
:class="{
|
||||
'search-match': searchName.trim().length > 0,
|
||||
'animate-bounce-once !bg-[rgb(63_115_247_/_10%)]':
|
||||
searchName.trim().length > 0,
|
||||
}"
|
||||
:body-style="{
|
||||
width: '100%',
|
||||
@@ -247,10 +248,13 @@ onMounted(() => {
|
||||
<img
|
||||
v-if="definition.icon"
|
||||
:src="definition.icon"
|
||||
class="flow-icon-img object-contain"
|
||||
class="size-12 rounded object-contain"
|
||||
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">
|
||||
{{ definition.name?.slice(0, 2) }}
|
||||
</span>
|
||||
@@ -301,31 +305,8 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.process-definition-container {
|
||||
.definition-item-card {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
.animate-bounce-once {
|
||||
animation: bounce 0.5s ease;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
|
||||
@@ -112,7 +112,7 @@ async function submitForm() {
|
||||
// 关闭并提示
|
||||
ElMessage.success('发起流程成功');
|
||||
await closeCurrentTab();
|
||||
await router.push({ name: 'BpmTaskMy' });
|
||||
await router.push({ name: 'BpmProcessInstanceMy' });
|
||||
} finally {
|
||||
processInstanceStartLoading.value = false;
|
||||
}
|
||||
|
||||
@@ -220,17 +220,22 @@ watch(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const loading = ref(false);
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await getDetail();
|
||||
// 获得用户列表
|
||||
userOptions.value = await getSimpleUserList();
|
||||
loading.value = true;
|
||||
try {
|
||||
await getDetail();
|
||||
// 获得用户列表
|
||||
userOptions.value = await getSimpleUserList();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Page auto-content-height v-loading="loading">
|
||||
<ElCard
|
||||
class="flex h-full flex-col"
|
||||
:body-style="{
|
||||
@@ -339,24 +344,22 @@ onMounted(async () => {
|
||||
</ElRow>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="流程图" name="diagram" class="pb-20 pr-3">
|
||||
<div>
|
||||
<ProcessInstanceSimpleViewer
|
||||
v-show="
|
||||
processDefinition.modelType &&
|
||||
processDefinition.modelType === BpmModelType.SIMPLE
|
||||
"
|
||||
:loading="processInstanceLoading"
|
||||
:model-view="processModelView"
|
||||
/>
|
||||
<ProcessInstanceBpmnViewer
|
||||
v-show="
|
||||
processDefinition.modelType &&
|
||||
processDefinition.modelType === BpmModelType.BPMN
|
||||
"
|
||||
:loading="processInstanceLoading"
|
||||
:model-view="processModelView"
|
||||
/>
|
||||
</div>
|
||||
<ProcessInstanceSimpleViewer
|
||||
v-show="
|
||||
processDefinition.modelType &&
|
||||
processDefinition.modelType === BpmModelType.SIMPLE
|
||||
"
|
||||
:loading="processInstanceLoading"
|
||||
:model-view="processModelView"
|
||||
/>
|
||||
<ProcessInstanceBpmnViewer
|
||||
v-show="
|
||||
processDefinition.modelType &&
|
||||
processDefinition.modelType === BpmModelType.BPMN
|
||||
"
|
||||
:loading="processInstanceLoading"
|
||||
:model-view="processModelView"
|
||||
/>
|
||||
</ElTabPane>
|
||||
<ElTabPane label="流转记录" name="record" class="pb-20 pr-3">
|
||||
<BpmProcessInstanceTaskList
|
||||
|
||||
@@ -275,7 +275,7 @@ async function openPopover(type: string) {
|
||||
}
|
||||
}
|
||||
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');
|
||||
}
|
||||
|
||||
/** 处理弹窗可见性 */
|
||||
function handlePopoverVisible(visible: boolean) {
|
||||
if (!visible) {
|
||||
// 拦截关闭事件
|
||||
popOverVisible.value.approve = true;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ loadTodoTask });
|
||||
</script>
|
||||
<template>
|
||||
@@ -720,11 +712,10 @@ defineExpose({ loadTodoTask });
|
||||
<!-- z-index 设置为300 避免覆盖签名弹窗 -->
|
||||
<ElSpace size="default">
|
||||
<ElPopover
|
||||
v-model:visible="popOverVisible.approve"
|
||||
:visible="popOverVisible.approve"
|
||||
placement="top"
|
||||
:popper-style="{ minWidth: '400px', zIndex: 300 }"
|
||||
trigger="click"
|
||||
@open-change="handlePopoverVisible"
|
||||
v-if="
|
||||
runningTask &&
|
||||
isHandleTaskStatus() &&
|
||||
@@ -825,7 +816,7 @@ defineExpose({ loadTodoTask });
|
||||
|
||||
<!-- 【拒绝】按钮 -->
|
||||
<ElPopover
|
||||
v-model:visible="popOverVisible.reject"
|
||||
:visible="popOverVisible.reject"
|
||||
placement="top"
|
||||
:popper-style="{ minWidth: '400px' }"
|
||||
trigger="click"
|
||||
@@ -885,7 +876,7 @@ defineExpose({ loadTodoTask });
|
||||
|
||||
<!-- 【抄送】按钮 -->
|
||||
<ElPopover
|
||||
v-model:visible="popOverVisible.copy"
|
||||
:visible="popOverVisible.copy"
|
||||
placement="top"
|
||||
:popper-style="{ width: '400px' }"
|
||||
trigger="click"
|
||||
@@ -960,7 +951,7 @@ defineExpose({ loadTodoTask });
|
||||
|
||||
<!-- 【转办】按钮 -->
|
||||
<ElPopover
|
||||
v-model:visible="popOverVisible.transfer"
|
||||
:visible="popOverVisible.transfer"
|
||||
placement="top"
|
||||
:popper-style="{ width: '400px' }"
|
||||
trigger="click"
|
||||
@@ -1036,7 +1027,7 @@ defineExpose({ loadTodoTask });
|
||||
|
||||
<!-- 【委派】按钮 -->
|
||||
<ElPopover
|
||||
v-model:visible="popOverVisible.delegate"
|
||||
:visible="popOverVisible.delegate"
|
||||
placement="top"
|
||||
:popper-style="{ width: '400px' }"
|
||||
trigger="click"
|
||||
@@ -1112,7 +1103,7 @@ defineExpose({ loadTodoTask });
|
||||
|
||||
<!-- 【加签】按钮 当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
|
||||
<ElPopover
|
||||
v-model:visible="popOverVisible.addSign"
|
||||
:visible="popOverVisible.addSign"
|
||||
placement="top"
|
||||
:popper-style="{ width: '400px' }"
|
||||
trigger="click"
|
||||
@@ -1200,7 +1191,7 @@ defineExpose({ loadTodoTask });
|
||||
|
||||
<!-- 【减签】按钮 -->
|
||||
<ElPopover
|
||||
v-model:visible="popOverVisible.deleteSign"
|
||||
:visible="popOverVisible.deleteSign"
|
||||
placement="top"
|
||||
:popper-style="{ width: '400px' }"
|
||||
trigger="click"
|
||||
@@ -1268,7 +1259,7 @@ defineExpose({ loadTodoTask });
|
||||
|
||||
<!-- 【退回】按钮 -->
|
||||
<ElPopover
|
||||
v-model:visible="popOverVisible.return"
|
||||
:visible="popOverVisible.return"
|
||||
placement="top"
|
||||
:popper-style="{ width: '400px' }"
|
||||
trigger="click"
|
||||
@@ -1342,7 +1333,7 @@ defineExpose({ loadTodoTask });
|
||||
|
||||
<!--【取消】按钮 这个对应发起人的取消, 只有发起人可以取消 -->
|
||||
<ElPopover
|
||||
v-model:visible="popOverVisible.cancel"
|
||||
:visible="popOverVisible.cancel"
|
||||
placement="top"
|
||||
:popper-style="{ width: '460px' }"
|
||||
trigger="click"
|
||||
|
||||
@@ -35,30 +35,30 @@ const [Modal, modalApi] = useVbenModal({
|
||||
|
||||
<template>
|
||||
<Modal title="流程签名" class="w-3/5">
|
||||
<div class="mb-2 flex justify-end">
|
||||
<ElSpace>
|
||||
<div class="flex h-[50vh] flex-col">
|
||||
<div class="mb-2 flex justify-end">
|
||||
<ElTooltip content="撤销上一步操作">
|
||||
<ElButton @click="signature?.undo()">
|
||||
<ElButton @click="signature?.undo()" size="small">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:undo" class="mb-1 size-4" />
|
||||
<IconifyIcon icon="lucide:undo" class="size-4" />
|
||||
</template>
|
||||
撤销
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="清空画布">
|
||||
<ElButton @click="signature?.clear()">
|
||||
<ElButton @click="signature?.clear()" size="small">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:trash" class="mb-1 size-4" />
|
||||
<IconifyIcon icon="lucide:trash" class="size-4" />
|
||||
</template>
|
||||
<span>清除</span>
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
</ElSpace>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Vue3Signature
|
||||
class="mx-auto !h-80 border border-solid border-gray-300"
|
||||
ref="signature"
|
||||
/>
|
||||
<Vue3Signature
|
||||
class="h-full flex-1 border border-solid border-gray-300"
|
||||
ref="signature"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -42,7 +42,7 @@ function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
field: 'approver',
|
||||
title: '审批人',
|
||||
slots: {
|
||||
default: ({ row }: { row: BpmTaskApi.TaskManager }) => {
|
||||
default: ({ row }: { row: BpmTaskApi.Task }) => {
|
||||
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 = {
|
||||
rule: [],
|
||||
@@ -156,7 +156,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
} as VxeTableGridOptions<BpmTaskApi.TaskManager>,
|
||||
} as VxeTableGridOptions<BpmTaskApi.Task>,
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
@@ -186,7 +186,7 @@ defineExpose({
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
<Modal class="w-[800px]">
|
||||
<Modal class="w-3/5">
|
||||
<FormCreate
|
||||
ref="formRef"
|
||||
v-model="taskForm.value"
|
||||
|
||||
@@ -8,22 +8,22 @@ import { z } from '#/adapter/form';
|
||||
|
||||
export const EVENT_EXECUTION_OPTIONS = [
|
||||
{
|
||||
label: 'start',
|
||||
label: '开始',
|
||||
value: 'start',
|
||||
},
|
||||
{
|
||||
label: 'end',
|
||||
label: '结束',
|
||||
value: 'end',
|
||||
},
|
||||
];
|
||||
|
||||
export const EVENT_OPTIONS = [
|
||||
{ label: 'create', value: 'create' },
|
||||
{ label: 'assignment', value: 'assignment' },
|
||||
{ label: 'complete', value: 'complete' },
|
||||
{ label: 'delete', value: 'delete' },
|
||||
{ label: 'update', value: 'update' },
|
||||
{ label: 'timeout', value: 'timeout' },
|
||||
{ label: '创建', value: 'create' },
|
||||
{ label: '指派', value: 'assignment' },
|
||||
{ label: '完成', value: 'complete' },
|
||||
{ label: '删除', value: 'delete' },
|
||||
{ label: '更新', value: 'update' },
|
||||
{ label: '超时', value: 'timeout' },
|
||||
];
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useGridColumns, useGridFormSchema } from './data';
|
||||
defineOptions({ name: 'BpmDoneTask' });
|
||||
|
||||
/** 查看历史 */
|
||||
function handleHistory(row: BpmTaskApi.TaskManager) {
|
||||
function handleHistory(row: BpmTaskApi.Task) {
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
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({
|
||||
text: '正在撤回中...',
|
||||
});
|
||||
@@ -66,7 +66,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<BpmTaskApi.TaskManager>,
|
||||
} as VxeTableGridOptions<BpmTaskApi.Task>,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useGridColumns, useGridFormSchema } from './data';
|
||||
defineOptions({ name: 'BpmManagerTask' });
|
||||
|
||||
/** 查看历史 */
|
||||
function handleHistory(row: BpmTaskApi.TaskManager) {
|
||||
function handleHistory(row: BpmTaskApi.Task) {
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: {
|
||||
|
||||
@@ -76,7 +76,8 @@ function handleRowCheckboxChange({
|
||||
}: {
|
||||
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({
|
||||
@@ -138,6 +139,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:data-source-config:update'],
|
||||
disabled: row.id === 0,
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
@@ -146,6 +148,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:data-source-config:delete'],
|
||||
disabled: row.id === 0,
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
|
||||
@@ -225,6 +225,18 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'config.region',
|
||||
label: '区域',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请填写区域,一般仅 AWS 需要填写',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) => formValues.storage === 20,
|
||||
},
|
||||
},
|
||||
// 通用
|
||||
{
|
||||
fieldName: 'config.domain',
|
||||
|
||||
@@ -15,6 +15,7 @@ const props = defineProps<{
|
||||
takeType?: number; // 领取方式
|
||||
}>();
|
||||
|
||||
// TODO @puhui999:这个也要调整,和 antd 保持统一。
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
|
||||
@@ -145,5 +145,6 @@ async function processLoadData(
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-2/5">
|
||||
<Form class="mx-4" />
|
||||
<!-- TODO @puhui999:这里需要同步下 -->
|
||||
</Modal>
|
||||
</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 { 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 { useTabs } from '@vben/hooks';
|
||||
@@ -35,7 +35,7 @@ const { refreshTab } = useTabs();
|
||||
const DIY_PAGE_INDEX_KEY = 'diy_page_index'; // 特殊:存储 reset 重置时,当前 selectedTemplateItem 值,从而进行恢复
|
||||
|
||||
const selectedTemplateItem = ref(0);
|
||||
const templateItems = reactive([
|
||||
const templateItems = ref([
|
||||
{ name: '基础设置', icon: 'ep:iphone' },
|
||||
{ name: '首页', icon: 'ep:home-filled' },
|
||||
{ name: '我的', icon: 'ep:user-filled' },
|
||||
@@ -77,11 +77,13 @@ async function getPageDetail(id: any) {
|
||||
function handleTemplateItemChange(val: any) {
|
||||
// 缓存模版编辑数据
|
||||
currentFormDataMap.value.set(
|
||||
templateItems[selectedTemplateItem.value]?.name || '',
|
||||
templateItems.value[selectedTemplateItem.value]?.name || '',
|
||||
currentFormData.value!,
|
||||
);
|
||||
// 读取模版缓存
|
||||
const data = currentFormDataMap.value.get(templateItems[val]?.name || '');
|
||||
const data = currentFormDataMap.value.get(
|
||||
templateItems.value[val]?.name || '',
|
||||
);
|
||||
|
||||
// 切换模版
|
||||
selectedTemplateItem.value = val;
|
||||
@@ -101,7 +103,7 @@ function handleTemplateItemChange(val: any) {
|
||||
isEmpty(data)
|
||||
? formData.value!.pages.find(
|
||||
(page: MallDiyPageApi.DiyPage) =>
|
||||
page.name === templateItems[val]?.name,
|
||||
page.name === templateItems.value[val]?.name,
|
||||
)
|
||||
: data
|
||||
) as MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty;
|
||||
@@ -114,7 +116,7 @@ async function submitForm() {
|
||||
});
|
||||
try {
|
||||
// 对所有的 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;
|
||||
// 情况一:基础设置
|
||||
if (i === 0) {
|
||||
@@ -188,7 +190,7 @@ onMounted(async () => {
|
||||
:show-navigation-bar="selectedTemplateItem !== 0"
|
||||
:show-page-config="selectedTemplateItem !== 0"
|
||||
:show-tab-bar="selectedTemplateItem === 0"
|
||||
:title="templateItems[selectedTemplateItem]?.name || ''"
|
||||
:title="templateItems[selectedTemplateItem]?.name ?? ''"
|
||||
@reset="handleEditorReset"
|
||||
@save="submitForm"
|
||||
>
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
<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 { IconifyIcon } from '@vben/icons';
|
||||
import { formatTime } from '@vben/utils';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElPagination,
|
||||
ElRow,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
} from 'element-plus';
|
||||
import { ElButton, ElPagination, ElRow } from 'element-plus';
|
||||
|
||||
import * as MpDraftApi from '#/api/mp/draft';
|
||||
import * as MpFreePublishApi from '#/api/mp/freePublish';
|
||||
import * as MpMaterialApi from '#/api/mp/material';
|
||||
import News from '#/views/mp/components/wx-news/wx-news.vue';
|
||||
import VideoPlayer from '#/views/mp/components/wx-video-play/wx-video-play.vue';
|
||||
import VoicePlayer from '#/views/mp/components/wx-voice-play/wx-voice-play.vue';
|
||||
|
||||
// TODO @hw:代码风格,看看 antd 和 ele 是不是统一下; 等antd此组件修改完再调整
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDraftPage } from '#/api/mp/draft';
|
||||
import { getFreePublishPage } from '#/api/mp/freePublish';
|
||||
import { getMaterialPage } from '#/api/mp/material';
|
||||
import { WxNews, WxVideoPlayer, WxVoicePlayer } from '#/views/mp/components';
|
||||
|
||||
/** 微信素材选择 */
|
||||
defineOptions({ name: 'WxMaterialSelect' });
|
||||
@@ -49,33 +42,163 @@ const queryParams = reactive({
|
||||
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) {
|
||||
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() {
|
||||
const data = await MpMaterialApi.getMaterialPage({
|
||||
const data = await getMaterialPage({
|
||||
...queryParams,
|
||||
type: props.type,
|
||||
});
|
||||
@@ -83,9 +206,8 @@ async function getMaterialPageFun() {
|
||||
total.value = data.total;
|
||||
}
|
||||
|
||||
/** 获取已发布图文分页 */
|
||||
async function getFreePublishPageFun() {
|
||||
const data = await MpFreePublishApi.getFreePublishPage(queryParams);
|
||||
const data = await getFreePublishPage(queryParams);
|
||||
data.list.forEach((item: any) => {
|
||||
const articles = item.content.newsItem;
|
||||
articles.forEach((article: any) => {
|
||||
@@ -96,9 +218,8 @@ async function getFreePublishPageFun() {
|
||||
total.value = data.total;
|
||||
}
|
||||
|
||||
/** 获取草稿图文分页 */
|
||||
async function getDraftPageFun() {
|
||||
const data = await MpDraftApi.getDraftPage(queryParams);
|
||||
const data = await getDraftPage(queryParams);
|
||||
data.list.forEach((draft: any) => {
|
||||
const articles = draft.content.newsItem;
|
||||
articles.forEach((article: any) => {
|
||||
@@ -109,9 +230,57 @@ async function getDraftPageFun() {
|
||||
total.value = data.total;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
getPage();
|
||||
});
|
||||
async function 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>
|
||||
|
||||
<template>
|
||||
@@ -152,90 +321,31 @@ onMounted(async () => {
|
||||
</div>
|
||||
<!-- 类型:voice -->
|
||||
<div v-else-if="props.type === 'voice'">
|
||||
<!-- 列表 -->
|
||||
<ElTable v-loading="loading" :data="list">
|
||||
<ElTableColumn label="编号" align="center" prop="mediaId" />
|
||||
<ElTableColumn label="文件名" align="center" prop="name" />
|
||||
<ElTableColumn label="语音" align="center">
|
||||
<template #default="scope">
|
||||
<VoicePlayer :url="scope.row.url" />
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<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">
|
||||
<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"
|
||||
/>
|
||||
<VoiceGrid>
|
||||
<template #voice="{ row }">
|
||||
<WxVoicePlayer :url="row.url" />
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<ElButton type="primary" link @click="selectMaterialFun(row)">
|
||||
选择
|
||||
<IconifyIcon icon="lucide:plus" />
|
||||
</ElButton>
|
||||
</template>
|
||||
</VoiceGrid>
|
||||
</div>
|
||||
<!-- 类型:video -->
|
||||
<div v-else-if="props.type === 'video'">
|
||||
<!-- 列表 -->
|
||||
<ElTable v-loading="loading" :data="list">
|
||||
<ElTableColumn label="编号" align="center" prop="mediaId" />
|
||||
<ElTableColumn label="文件名" align="center" prop="name" />
|
||||
<ElTableColumn label="标题" align="center" prop="title" />
|
||||
<ElTableColumn label="介绍" align="center" prop="introduction" />
|
||||
<ElTableColumn label="视频" align="center">
|
||||
<template #default="scope">
|
||||
<VideoPlayer :url="scope.row.url" />
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<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"
|
||||
/>
|
||||
<VideoGrid>
|
||||
<template #video="{ row }">
|
||||
<WxVideoPlayer :url="row.url" />
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<ElButton type="primary" link @click="selectMaterialFun(row)">
|
||||
选择
|
||||
<IconifyIcon icon="lucide:circle-plus" />
|
||||
</ElButton>
|
||||
</template>
|
||||
</VideoGrid>
|
||||
</div>
|
||||
<!-- 类型:news -->
|
||||
<div v-else-if="props.type === 'news'">
|
||||
@@ -249,7 +359,7 @@ onMounted(async () => {
|
||||
:key="item.mediaId"
|
||||
>
|
||||
<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">
|
||||
<ElButton type="success" @click="selectMaterialFun(item)">
|
||||
选择
|
||||
|
||||
@@ -14,7 +14,7 @@ defineOptions({ name: 'TabNews' });
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Reply;
|
||||
newsType: NewsType;
|
||||
newsType?: NewsType;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -74,4 +74,3 @@ export const useBeforeUpload = (type: UploadType, maxSizeMB?: number) => {
|
||||
|
||||
return fn;
|
||||
};
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ const total = ref(0); // 总条数
|
||||
const accountId = ref(-1);
|
||||
provide('accountId', accountId);
|
||||
|
||||
// TODO @AI:这里是不是应该都用 grid;类似 yudao-ui-admin-vben-v5/apps/web-ele/src/views/mp/autoReply/index.vue
|
||||
const queryParams = reactive({
|
||||
accountId,
|
||||
pageNo: 1,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
|
||||
const props = defineProps<{ type: UploadType }>();
|
||||
|
||||
// TODO @dylan:是不是要和 antd 的 props 定义相同哈?这样后续两侧维护方便点
|
||||
const emit = defineEmits<{
|
||||
uploaded: [v: void];
|
||||
}>();
|
||||
@@ -59,6 +60,7 @@ const customRequest: UploadProps['httpRequest'] = async function (options) {
|
||||
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(`上传出错:${res.msg}`);
|
||||
// TODO @dylan:这里有个 linter 错误。
|
||||
onError?.(new Error(res.msg));
|
||||
return;
|
||||
}
|
||||
@@ -104,4 +106,3 @@ const customRequest: UploadProps['httpRequest'] = async function (options) {
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
|
||||
import { beforeVideoUpload, HEADERS, UPLOAD_URL, UploadType } from './upload';
|
||||
|
||||
// TODO @dylan:是不是要和 antd 的 props 定义相同哈?这样后续两侧维护方便点
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: boolean;
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { MpMaterialApi } from '#/api/mp/material';
|
||||
|
||||
import { nextTick, onMounted, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
import { useImageGridColumns } from './data';
|
||||
@@ -89,9 +91,9 @@ onMounted(async () => {
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '删除',
|
||||
type: 'link',
|
||||
danger: true,
|
||||
label: $t('common.delete'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['mp:material:delete'],
|
||||
popConfirm: {
|
||||
|
||||
@@ -44,4 +44,3 @@ export {
|
||||
type UploadData,
|
||||
UploadType,
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { MpMaterialApi } from '#/api/mp/material';
|
||||
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
@@ -76,14 +77,15 @@ watch(
|
||||
:actions="[
|
||||
{
|
||||
label: '下载',
|
||||
type: 'link',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
onClick: () => openWindow(row.url),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
type: 'link',
|
||||
danger: true,
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['mp:material:delete'],
|
||||
popConfirm: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { MpMaterialApi } from '#/api/mp/material';
|
||||
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
@@ -76,14 +77,15 @@ watch(
|
||||
:actions="[
|
||||
{
|
||||
label: '下载',
|
||||
type: 'link',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
onClick: () => openWindow(row.url),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
type: 'link',
|
||||
danger: true,
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['mp:material:delete'],
|
||||
popConfirm: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user