This commit is contained in:
dylanmay
2025-12-05 09:37:41 +08:00
115 changed files with 2321 additions and 1781 deletions

View File

@@ -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; // 节点类型
}
}

View File

@@ -17,6 +17,7 @@ export namespace InfraFileConfigApi {
accessSecret?: string;
pathStyle?: boolean;
enablePublicAccess?: boolean;
region?: string;
domain: string;
}

View File

@@ -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>>(

View File

@@ -21,6 +21,7 @@ export namespace MallCombinationActivityApi {
limitDuration?: number; // 限制时长
combinationPrice?: number; // 拼团价格
products: CombinationProduct[]; // 商品列表
picUrl?: any;
}
/** 拼团活动所需属性 */

View File

@@ -31,6 +31,7 @@ export namespace MallSeckillActivityApi {
totalStock?: number; // 秒杀总库存
seckillPrice?: number; // 秒杀价格
products?: SeckillProduct[]; // 秒杀商品列表
picUrl?: any;
}
}

View File

@@ -12,6 +12,7 @@ export namespace SystemSocialClientApi {
clientId: string;
clientSecret: string;
agentId?: string;
publicKey?: string;
status: number;
createTime?: Date;
}

View File

@@ -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'),

View File

@@ -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': '展开池',

View File

@@ -390,8 +390,9 @@ watch(() => props.businessObject, syncFromBusinessObject, { deep: true });
<template #extra>
<IconifyIcon icon="ep:timer" />
</template>
<!-- 相关 issuehttps://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ICNRW2 -->
<TimeEventConfig
:business-object="bpmnElement.value?.businessObject"
:business-object="elementBusinessObject"
:key="elementId"
/>
</CollapsePanel>

View File

@@ -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>

View File

@@ -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`,

View File

@@ -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>
<!-- 选择弹窗 -->

View File

@@ -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();
};
// 补充"编辑"、"移除"功能。相关 issuehttps://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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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' },
];
/** 新增/修改的表单 */

View File

@@ -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>

View File

@@ -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: {

View File

@@ -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),

View File

@@ -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',

View File

@@ -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,
});

View File

@@ -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 @haohaoweb-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 form12
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>

View File

@@ -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,
});

View File

@@ -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 {

View File

@@ -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 =

View File

@@ -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 @haohaocategory 类型
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 @haohaotindwind -->
<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 @haohaotindwind -->
<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 @haohaotindwind -->
<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>

View File

@@ -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%);
}

View File

@@ -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 @haohaohandleConfirm 直接放到这里,不用单独声明
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>

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import type { IotProductApi } from '#/api/iot/product/product';
// TODO @haohaodetail 挪到 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' });

View File

@@ -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 @haohaoasync function handleDeleteBatch() { 1 confirm2 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 @haohaoasync function handleDeleteBatch() { 1 confirm2 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>

View File

@@ -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="

View 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>

View File

@@ -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>

View File

@@ -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';

View File

@@ -133,6 +133,13 @@ export function useFormSchema(): VbenFormSchema[] {
placeholder: '请输入最大砍价金额',
},
},
// TODO @puhui999这里交互不太对可以对比下 element-plus 版本呢
{
fieldName: 'spuId',
label: '砍价商品',
component: 'Input',
rules: 'required',
},
];
}

View File

@@ -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>

View File

@@ -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',
},
];
}

View File

@@ -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>

View File

@@ -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"

View File

@@ -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="

View File

@@ -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;

View File

@@ -100,5 +100,4 @@ function handleCloneComponent(component: DiyComponent<any>) {
</Collapse.Panel>
</Collapse>
</div>
<!-- TODO @xingyuele 里面有一些 style看看是不是都迁移完了特别是 drag-area 是全局样式 -->
</template>

View File

@@ -106,7 +106,6 @@ watch(
</div>
</Carousel>
</template>
<style lang="scss">
// Ant Design Vue Carousel 样式调整
:deep(.ant-carousel .ant-carousel-dots) {

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import { Space } from 'ant-design-vue';
// TODO @芋艿、@xingyu貌似上下移动的按钮被遮住了
/**
* 垂直按钮组
* Ant Design Vue 的按钮组,通过 Space 实现垂直布局

View File

@@ -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) =>

View File

@@ -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>

View File

@@ -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
];
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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,
}),
},
];
}

View File

@@ -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,

View File

@@ -29,6 +29,7 @@ const emit = defineEmits(['success']);
const formData = ref<Partial<MallRewardActivityApi.RewardActivity>>({
conditionType: PromotionConditionTypeEnum.PRICE.type,
productScope: PromotionProductScopeEnum.ALL.scope,
rules: [],
});

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -14,7 +14,7 @@ defineOptions({ name: 'TabNews' });
const props = defineProps<{
modelValue: Reply;
newsType: NewsType;
newsType?: NewsType;
}>();
const emit = defineEmits<{

View File

@@ -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,

View File

@@ -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,

View File

@@ -102,6 +102,7 @@ function handlePageChange(page: number, pageSize: number) {
function showTotal(total: number) {
return `${total}`;
}
// TODO @dylan是不是应该都用 Grid 哈1message-table 大部分合并到 index.vue2message-table 的 schema 放到 data.ts 里;
</script>
<template>

View File

@@ -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: '状态',

View File

@@ -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

View File

@@ -17,6 +17,7 @@ export namespace InfraFileConfigApi {
accessSecret?: string;
pathStyle?: boolean;
enablePublicAccess?: boolean;
region?: string;
domain: string;
}

View File

@@ -12,6 +12,7 @@ export namespace SystemSocialClientApi {
clientId: string;
clientSecret: string;
agentId?: string;
publicKey?: string;
status: number;
createTime?: Date;
}

View File

@@ -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'),

View File

@@ -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;

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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审批向后加签BA审批完需要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"

View File

@@ -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>

View File

@@ -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"

View File

@@ -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' },
];
/** 新增/修改的表单 */

View File

@@ -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>

View File

@@ -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: {

View File

@@ -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),

View File

@@ -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',

View File

@@ -15,6 +15,7 @@ const props = defineProps<{
takeType?: number; // 领取方式
}>();
// TODO @puhui999这个也要调整和 antd 保持统一。
const emit = defineEmits(['success']);
const [Grid, gridApi] = useVbenVxeGrid({

View File

@@ -145,5 +145,6 @@ async function processLoadData(
<template>
<Modal :title="getTitle" class="w-2/5">
<Form class="mx-4" />
<!-- TODO @puhui999这里需要同步下 -->
</Modal>
</template>

View File

@@ -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"
>

View File

@@ -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)">
选择

View File

@@ -14,7 +14,7 @@ defineOptions({ name: 'TabNews' });
const props = defineProps<{
modelValue: Reply;
newsType: NewsType;
newsType?: NewsType;
}>();
const emit = defineEmits<{

View File

@@ -74,4 +74,3 @@ export const useBeforeUpload = (type: UploadType, maxSizeMB?: number) => {
return fn;
};

View File

@@ -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,

View File

@@ -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>

View File

@@ -20,6 +20,7 @@ import {
import { beforeVideoUpload, HEADERS, UPLOAD_URL, UploadType } from './upload';
// TODO @dylan是不是要和 antd 的 props 定义相同哈?这样后续两侧维护方便点
withDefaults(
defineProps<{
modelValue?: boolean;

View File

@@ -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: {

View File

@@ -44,4 +44,3 @@ export {
type UploadData,
UploadType,
};

View File

@@ -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: {

View File

@@ -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