Merge remote-tracking branch 'yudao/dev' into dev

This commit is contained in:
jason
2025-11-30 00:12:03 +08:00
70 changed files with 2621 additions and 255 deletions

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

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

@@ -74,9 +74,16 @@ const assignEmptyUserIdsEl = ref<any>();
const assignEmptyUserIds = ref<any>();
// 操作按钮
// TODO @puhui999这块迁移有点问题按钮不能操作另外label 也没展示。
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[]>([]);
@@ -358,19 +365,10 @@ 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,
};
}
@@ -532,7 +530,7 @@ 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

@@ -77,9 +77,12 @@ const resetListenersList = () => {
// '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`,
) ?? []; // 保留非监听器类型的扩展属性避免移除监听器时清空其他配置如审批人等。相关案例https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ICMSYC
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) =>

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).substring(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;
overflow-y: auto;
margin-bottom: 16px;
}
.header-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
.header-key {
flex: 0 0 180px;
}
.separator {
color: #606266;
font-weight: 500;
}
.header-value {
flex: 1;
}
}
.add-btn {
width: 100%;
}
}
</style>

View File

@@ -1,7 +1,21 @@
<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 +23,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
? ''
: String(rawValue));
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 +309,19 @@ watch(
() => {
bpmnElement.value = bpmnInstances().bpmnElement;
nextTick(() => {
resetTaskForm();
resetServiceTaskForm();
});
},
{ immediate: true },
);
watch(
() => httpTaskForm.value,
() => {
updateHttpExtensions();
},
{ deep: true },
);
</script>
<template>
@@ -68,7 +333,9 @@ watch(
{ label: 'Java类', value: 'class' },
{ label: '表达式', value: 'expression' },
{ label: '代理表达式', value: 'delegateExpression' },
{ label: 'HTTP 调用', value: 'http' },
]"
@change="handleExecuteTypeChange"
/>
</FormItem>
<FormItem
@@ -107,5 +374,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

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

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

@@ -86,6 +86,7 @@ const [Modal, modalApi] = useVbenModal({
</script>
<template>
<!-- TODO @puhui999这里缺少商品的选择 -->
<Modal class="w-2/5" :title="getTitle">
<Form class="mx-4" />
</Modal>

View File

@@ -106,7 +106,7 @@ export function useFormSchema(): VbenFormSchema[] {
},
},
{
// TODO
// TODO <!-- TODO @puhui999这里缺少商品的选择 -->
fieldName: 'spuId',
label: '拼团商品',
component: 'Input',

View File

@@ -133,7 +133,7 @@ function handleSliderChange(prop: string) {
</TabPane>
<!-- 每个组件的通用内容 -->
<!-- TODO @xingyu这里的样式貌似没 ele 版本的好看 -->
<!-- TODO @xingyu装修这里的样式貌似没 ele 版本的好看 -->
<TabPane tab="样式" key="style" force-render>
<p class="text-lg font-bold">组件样式</p>
<div class="flex flex-col gap-2 rounded-md p-4 shadow-lg">

View File

@@ -109,8 +109,9 @@ const handleDeleteComponent = () => {
class="component-toolbar"
v-if="showToolbar && component.name && active"
>
<!-- TODO @xingyu按钮少的时候会存在遮住的情况 -->
<!-- TODO @xingyu貌似中间的选中框框没全部框柱上面多了点下面少了点 -->
<!-- TODO @xingyu装修按钮少的时候会存在遮住的情况 -->
<!-- TODO @xingyu装修貌似中间的选中框框没全部框柱上面多了点下面少了点 -->
<!-- TODO @xingyu装修从左侧的组件拖拽到中间时没有这个组件的小卡片预览ele 版本是有的 -->
<VerticalButtonGroup size="small">
<Button
:disabled="!canMoveUp"

View File

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

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,7 +175,7 @@ function handleComponentSelected(
index: number = -1,
) {
// 使用深拷贝避免响应式追踪循环警告
// TODO @xingyu这个是必须的么ele 没有哈。
// TODO @xingyu【装修】这个是必须的么ele 没有哈。
selectedComponent.value = cloneDeep(component);
selectedComponentIndex.value = index;
}
@@ -508,5 +508,5 @@ onMounted(() => {
</div>
</PreviewModal>
</Page>
<!-- TODO @xingyu这里改造完后类似 web-ele/src/views/mall/promotion/components/diy-editor/index.vue 里的全局样式递推到子组件里的就没没了类似 property-group -->
<!-- TODO @xingyu装修这里改造完后类似 web-ele/src/views/mall/promotion/components/diy-editor/index.vue 里的全局样式递推到子组件里的就没没了类似 property-group -->
</template>

View File

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

View File

@@ -68,7 +68,7 @@ export function useFormSchema(): VbenFormSchema[] {
rows: 4,
},
},
// TODO
// TODO @puhui999少了商品选择~
];
}

View File

@@ -20,6 +20,7 @@ import {
} from '#/api/mall/promotion/diy/template';
import { DiyEditor, PAGE_LIBS } from '#/views/mall/promotion/components';
// TODO @xingyu【装修】左上角的“基础设施”、“首页”、“我的”切换时中间的编辑器内容没有正确切换。可对比 ele 版本的效果!
/** 装修模板表单 */
defineOptions({ name: 'DiyTemplateDecorate' });

View File

@@ -126,12 +126,14 @@ export function useFormSchema(): VbenFormSchema[] {
},
formItemClass: 'col-span-2',
},
// TODO @puhui999商品图太大了。
{
fieldName: 'spuId',
label: '活动商品',
component: 'Input',
rules: 'required',
formItemClass: 'col-span-2',
// TODO @puhui999貌似 renderComponentContent 不需要哇?
renderComponentContent: () => ({
default: () => null,
}),

View File

@@ -32,6 +32,7 @@ const formData = ref<Partial<MallRewardActivityApi.RewardActivity>>({
rules: [],
});
// TODO @puhui999点击“编辑”后会出现 Cannot read properties of null (reading 'type') 报错;
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['满减送'])

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

@@ -154,6 +154,7 @@ const [Modal, modalApi] = useVbenModal({
<Form />
<!-- 商品选择区域 -->
<!-- TODO @puhui999这里缺少商品的选择 -->
<div class="mt-4">
<div class="mb-2 flex items-center">
<span class="text-sm font-medium">秒杀活动商品:</span>

View File

@@ -43,7 +43,6 @@ const queryParams = reactive({
pageSize: 10,
}); // 查询参数
// TODO @dylan可以把【点击上传】3 个 tab 的按钮,放到右侧的 toolbar 一起,和刷新按钮放在一行;
const voiceGridColumns: VxeTableGridOptions<MpMaterialApi.Material>['columns'] =
[
{

View File

@@ -10,12 +10,12 @@ import { Button, Card, Form, message, Pagination, Tabs } from 'ant-design-vue';
import { deletePermanentMaterial, getMaterialPage } from '#/api/mp/material';
import { WxAccountSelect } from '#/views/mp/components';
import ImageTable from './components/image-table.vue';
import { UploadType } from './components/upload';
import UploadFile from './components/UploadFile.vue';
import UploadVideo from './components/UploadVideo.vue';
import VideoTable from './components/video-table.vue';
import VoiceTable from './components/voice-table.vue';
import ImageTable from './modules/image-table.vue';
import { UploadType } from './modules/upload';
import UploadFile from './modules/UploadFile.vue';
import UploadVideo from './modules/UploadVideo.vue';
import VideoTable from './modules/video-table.vue';
import VoiceTable from './modules/voice-table.vue';
defineOptions({ name: 'MpMaterial' });
@@ -117,19 +117,16 @@ async function handleDelete(id: number) {
图片
</span>
</template>
<UploadFile
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
:type="UploadType.Image"
@uploaded="getList"
>
支持 bmp/png/jpeg/jpg/gif 格式大小不超过 2M
</UploadFile>
<!-- 列表 -->
<ImageTable
:list="list"
:loading="loading"
@delete="handleDelete"
/>
<ImageTable :list="list" :loading="loading" @delete="handleDelete">
<template #toolbar-tools>
<UploadFile
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
:type="UploadType.Image"
@uploaded="getList"
/>
</template>
</ImageTable>
<!-- 分页组件 -->
<div class="mt-4 flex justify-end">
<Pagination
@@ -151,19 +148,16 @@ async function handleDelete(id: number) {
语音
</span>
</template>
<UploadFile
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
:type="UploadType.Voice"
@uploaded="getList"
>
格式支持 mp3/wma/wav/amr文件大小不超过 2M播放长度不超过 60s
</UploadFile>
<!-- 列表 -->
<VoiceTable
:list="list"
:loading="loading"
@delete="handleDelete"
/>
<VoiceTable :list="list" :loading="loading" @delete="handleDelete">
<template #toolbar-tools>
<UploadFile
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
:type="UploadType.Voice"
@uploaded="getList"
/>
</template>
</VoiceTable>
<!-- 分页组件 -->
<div class="mt-4 flex justify-end">
<Pagination
@@ -185,21 +179,20 @@ async function handleDelete(id: number) {
视频
</span>
</template>
<Button
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
type="primary"
@click="showCreateVideo = true"
>
新建视频
</Button>
<!-- 列表 -->
<VideoTable :list="list" :loading="loading" @delete="handleDelete">
<template #toolbar-tools>
<Button
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
type="primary"
@click="showCreateVideo = true"
>
新建视频
</Button>
</template>
</VideoTable>
<!-- 新建视频的弹窗 -->
<UploadVideo v-model:open="showCreateVideo" @uploaded="getList" />
<!-- 列表 -->
<VideoTable
:list="list"
:loading="loading"
@delete="handleDelete"
/>
<!-- 分页组件 -->
<div class="mt-4 flex justify-end">
<Pagination

View File

@@ -86,7 +86,6 @@ const customRequest: UploadProps['customRequest'] = async function (options) {
:file-list="fileList"
:headers="HEADERS"
:multiple="true"
class="mb-4"
>
<Button type="primary">
<IconifyIcon icon="lucide:upload" class="mr-1" />

View File

@@ -1,7 +1,6 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MpMaterialApi } from '#/api/mp/material';
// TODO @dylan看看 ele 要迁移一个么?
/** 视频表格列配置 */
export function useVideoGridColumns(): VxeTableGridOptions<MpMaterialApi.Material>['columns'] {
return [

View File

@@ -31,6 +31,9 @@ const [Grid, gridApi] = useVbenVxeGrid<MpMaterialApi.Material>({
isHover: true,
height: 220,
},
toolbarConfig: {
refresh: true,
},
showOverflow: 'tooltip',
},
});
@@ -70,6 +73,9 @@ onMounted(async () => {
<template>
<Grid class="image-table-grid mt-4 pb-0">
<template #toolbar-tools>
<slot name="toolbar-tools"></slot>
</template>
<template #image="{ row }">
<div class="flex items-center justify-center" style="height: 192px">
<img

View File

@@ -10,6 +10,7 @@ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { WxVideoPlayer } from '#/views/mp/components';
import { useVideoGridColumns } from './data';
import {$t} from '@vben/locales';
const props = defineProps<{
list: MpMaterialApi.Material[];
@@ -34,6 +35,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
showOverflow: 'tooltip',
} as VxeTableGridOptions<MpMaterialApi.Material>,
});
@@ -62,6 +66,9 @@ watch(
<template>
<Grid class="mt-4">
<template #toolbar-tools>
<slot name="toolbar-tools"></slot>
</template>
<template #video="{ row }">
<WxVideoPlayer v-if="row.url" :url="row.url" />
</template>
@@ -75,7 +82,7 @@ watch(
onClick: () => openWindow(row.url),
},
{
label: '删除',
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,

View File

@@ -4,13 +4,13 @@ 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';
import { WxVoicePlayer } from '#/views/mp/components';
import { useVoiceGridColumns } from './data';
// TODO @dylan modules components
const props = defineProps<{
list: MpMaterialApi.Material[];
@@ -35,6 +35,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
showOverflow: 'tooltip',
} as VxeTableGridOptions<MpMaterialApi.Material>,
});
@@ -63,6 +66,9 @@ watch(
<template>
<Grid class="mt-4">
<template #toolbar-tools>
<slot name="toolbar-tools"></slot>
</template>
<template #voice="{ row }">
<WxVoicePlayer v-if="row.url" :url="row.url" />
</template>
@@ -76,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

@@ -9,7 +9,6 @@ import { formatDate2 } from '@vben/utils';
import { Button, Image, Tag } from 'ant-design-vue';
// TODO @dylan看看 ele 要迁移一个么?
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
WxLocation,

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

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

View File

@@ -1,36 +1,41 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
import { MessageType } from '@vben/constants';
// TODO @dylan这个类的代码应该和对应的 antd 是一致的。调整下~看看相关的 vue 是不是也要调整掉。
/** 消息类型枚举 */
export enum MessageType {
IMAGE = 'image', // 图片消息
MPNEWS = 'mpnews', // 公众号图文消息
MUSIC = 'music', // 音乐消息
NEWS = 'news', // 图文消息
TEXT = 'text', // 文本消息
VIDEO = 'video', // 视频消息
VOICE = 'voice', // 语音消息
WXCARD = 'wxcard', // 卡券消息
}
import { requestClient } from '#/api/request';
export namespace MpMessageApi {
/** 消息信息 */
export interface Message {
id?: number;
accountId: number;
type: MessageType;
type: MessageType | string;
openid: string;
content: string;
mediaId?: string;
status: number;
remark?: string;
createTime?: Date;
sendFrom?: number;
userId?: number;
event?: string;
eventKey?: string;
mediaUrl?: string;
recognition?: string;
url?: string;
title?: string;
label?: string;
locationX?: number;
locationY?: number;
thumbMediaUrl?: string;
musicUrl?: string;
hqMusicUrl?: string;
description?: string;
articles?: any[];
}
/** 发送消息请求 */
export interface SendMessageRequest {
export interface MessageSendRequestVO {
accountId: number;
openid: string;
type: MessageType;
@@ -50,6 +55,6 @@ export function getMessagePage(params: PageParam) {
}
/** 发送消息 */
export function sendMessage(data: MpMessageApi.SendMessageRequest) {
export function sendMessage(data: MpMessageApi.MessageSendRequestVO) {
return requestClient.post('/mp/message/send', data);
}

View File

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

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

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

@@ -1,7 +1,7 @@
export { default as WxAccountSelect } from './wx-account-select/wx-account-select.vue';
export { default as WxLocation } from './wx-location/wx-location.vue';
export { default as WxMaterialSelect } from './wx-material-select/wx-material-select.vue';
export { default as WxMsg } from './wx-msg/msg.vue'; // TODO @hw、@dylan貌似和 antd 不同。antd 这里是 export { default as WxMsg } from './wx-msg/wx-msg.vue'; 看看哪个是对的
export { default as WxMsg } from './wx-msg/wx-msg.vue';
export { default as WxMusic } from './wx-music/wx-music.vue';
export { default as WxNews } from './wx-news/wx-news.vue';
export { default as WxReply } from './wx-reply/wx-reply.vue';

View File

@@ -0,0 +1,76 @@
import { ElMessage } from 'element-plus';
import { $t } from '#/locales';
export enum UploadType {
Image = 'image',
Video = 'video',
Voice = 'voice',
}
interface UploadTypeConfig {
allowTypes: string[];
maxSizeMB: number;
i18nKey: string;
}
export interface UploadRawFile {
name: string;
size: number;
type: string;
}
const UPLOAD_CONFIGS: Record<UploadType, UploadTypeConfig> = {
[UploadType.Image]: {
allowTypes: [
'image/jpeg',
'image/png',
'image/gif',
'image/bmp',
'image/jpg',
],
maxSizeMB: 2,
i18nKey: 'mp.upload.image',
},
[UploadType.Video]: {
allowTypes: ['video/mp4'],
maxSizeMB: 10,
i18nKey: 'mp.upload.video',
},
[UploadType.Voice]: {
allowTypes: [
'audio/mp3',
'audio/mpeg',
'audio/wma',
'audio/wav',
'audio/amr',
],
maxSizeMB: 2,
i18nKey: 'mp.upload.voice',
},
};
export const useBeforeUpload = (type: UploadType, maxSizeMB?: number) => {
const fn = (rawFile: UploadRawFile): boolean => {
const config = UPLOAD_CONFIGS[type];
const finalMaxSize = maxSizeMB ?? config.maxSizeMB;
// 格式不正确
if (!config.allowTypes.includes(rawFile.type)) {
const typeName = $t(config.i18nKey);
ElMessage.error($t('mp.upload.invalidFormat', [typeName]));
return false;
}
// 大小不正确
if (rawFile.size / 1024 / 1024 > finalMaxSize) {
const typeName = $t(config.i18nKey);
ElMessage.error($t('mp.upload.maxSize', [typeName, finalMaxSize]));
return false;
}
return true;
};
return fn;
};

View File

@@ -0,0 +1,239 @@
<script lang="ts" setup>
import { provide, reactive, ref } from 'vue';
import { useAccess } from '@vben/access';
import { confirm, DocAlert, Page } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import {
ElButton,
ElCard,
ElForm,
ElFormItem,
ElLoading,
ElMessage,
ElPagination,
ElTabPane,
ElTabs,
} from 'element-plus';
import { deletePermanentMaterial, getMaterialPage } from '#/api/mp/material';
import { WxAccountSelect } from '#/views/mp/components';
import ImageTable from './modules/image-table.vue';
import { UploadType } from './modules/upload';
import UploadFile from './modules/UploadFile.vue';
import UploadVideo from './modules/UploadVideo.vue';
import VideoTable from './modules/video-table.vue';
import VoiceTable from './modules/voice-table.vue';
defineOptions({ name: 'MpMaterial' });
const { hasAccessByCodes } = useAccess();
const type = ref<UploadType>(UploadType.Image); // 素材类型
const loading = ref(false); // 遮罩层
const list = ref<any[]>([]); // 数据列表
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,
pageSize: 10,
permanent: true,
}); // 查询参数
const showCreateVideo = ref(false); // 是否新建视频的弹窗
/** 侦听公众号变化 */
function onAccountChanged(id: number) {
accountId.value = id;
queryParams.accountId = id;
queryParams.pageNo = 1;
getList();
}
/** 查询列表 */
async function getList() {
loading.value = true;
try {
const data = await getMaterialPage({
...queryParams,
type: type.value,
});
list.value = data.list;
total.value = data.total;
} finally {
loading.value = false;
}
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.pageNo = 1;
getList();
}
/** 处理 tab 切换 */
function onTabChange() {
// 提前清空数据,避免 tab 切换后显示垃圾数据
list.value = [];
total.value = 0;
// 从第一页开始查询
handleQuery();
}
/** 处理删除操作 */
async function handleDelete(id: number) {
await confirm('此操作将永久删除该文件, 是否继续?');
const loadingInstance = ElLoading.service({
text: '正在删除...',
lock: true,
});
try {
await deletePermanentMaterial(id);
ElMessage.success('删除成功');
await getList();
} finally {
loadingInstance.close();
}
}
/** 分页改变事件 */
function handlePageChange(page: number) {
queryParams.pageNo = page;
getList();
}
/** 每页条数改变事件 */
function handleSizeChange(pageSize: number) {
queryParams.pageSize = pageSize;
queryParams.pageNo = 1;
getList();
}
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert title="公众号素材" url="https://doc.iocoder.cn/mp/material/" />
</template>
<div class="h-full">
<!-- 搜索工作栏 -->
<ElCard class="h-[10%]" shadow="never">
<ElForm :model="queryParams" :inline="true">
<ElFormItem label="公众号" class="w-52">
<WxAccountSelect @change="onAccountChanged" />
</ElFormItem>
</ElForm>
</ElCard>
<ElCard shadow="never" class="mt-4 h-auto">
<ElTabs v-model="type" @tab-change="onTabChange">
<!-- tab 1图片 -->
<ElTabPane :name="UploadType.Image">
<template #label>
<span class="flex items-center">
<IconifyIcon icon="lucide:image" class="mr-1" />
图片
</span>
</template>
<!-- 列表 -->
<ImageTable :list="list" :loading="loading" @delete="handleDelete">
<template #toolbar-tools>
<UploadFile
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
:type="UploadType.Image"
@uploaded="getList"
/>
</template>
</ImageTable>
<!-- 分页组件 -->
<div class="mt-4 flex justify-end">
<ElPagination
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</ElTabPane>
<!-- tab 2语音 -->
<ElTabPane :name="UploadType.Voice">
<template #label>
<span class="flex items-center">
<IconifyIcon icon="lucide:mic" class="mr-1" />
语音
</span>
</template>
<!-- 列表 -->
<VoiceTable :list="list" :loading="loading" @delete="handleDelete">
<template #toolbar-tools>
<UploadFile
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
:type="UploadType.Voice"
@uploaded="getList"
/>
</template>
</VoiceTable>
<!-- 分页组件 -->
<div class="mt-4 flex justify-end">
<ElPagination
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</ElTabPane>
<!-- tab 3视频 -->
<ElTabPane :name="UploadType.Video">
<template #label>
<span class="flex items-center">
<IconifyIcon icon="lucide:video" class="mr-1" />
视频
</span>
</template>
<!-- 列表 -->
<VideoTable :list="list" :loading="loading" @delete="handleDelete">
<template #toolbar-tools>
<ElButton
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
type="primary"
@click="showCreateVideo = true"
>
新建视频
</ElButton>
</template>
</VideoTable>
<!-- 新建视频的弹窗 -->
<UploadVideo v-model="showCreateVideo" @uploaded="getList" />
<!-- 分页组件 -->
<div class="mt-4 flex justify-end">
<ElPagination
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</ElTabPane>
</ElTabs>
</ElCard>
</div>
</Page>
</template>

View File

@@ -0,0 +1,108 @@
<script lang="ts" setup>
import type { UploadProps } from 'element-plus';
import type { UploadData } from './upload';
import { inject, reactive, ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { ElButton, ElMessage, ElUpload } from 'element-plus';
import {
beforeImageUpload,
beforeVoiceUpload,
HEADERS,
UPLOAD_URL,
UploadType,
} from './upload';
const props = defineProps<{ type: UploadType }>();
// TODO @dylan是不是要和 antd 的 props 定义相同哈?这样后续两侧维护方便点
const emit = defineEmits<{
uploaded: [v: void];
}>();
const accountId = inject<number>('accountId');
const fileList = ref<any[]>([]);
const uploadData: UploadData = reactive({
accountId: accountId!,
introduction: '',
title: '',
type: props.type,
});
/** 上传前检查 */
const onBeforeUpload =
props.type === UploadType.Image ? beforeImageUpload : beforeVoiceUpload;
/** 自定义上传 */
const customRequest: UploadProps['httpRequest'] = async function (options) {
const { file, onError, onSuccess } = options;
const formData = new FormData();
formData.append('file', file as File);
formData.append('type', uploadData.type);
formData.append('title', uploadData.title);
formData.append('introduction', uploadData.introduction);
formData.append('accountId', String(uploadData.accountId));
try {
const response = await fetch(UPLOAD_URL, {
body: formData,
headers: HEADERS,
method: 'POST',
});
const res = await response.json();
if (res.code !== 0) {
ElMessage.error(`上传出错:${res.msg}`);
// TODO @dylan这里有个 linter 错误。
onError?.(new Error(res.msg));
return;
}
// 清空上传时的各种数据
fileList.value = [];
uploadData.title = '';
uploadData.introduction = '';
ElMessage.success('上传成功');
onSuccess?.(res);
emit('uploaded');
} catch (error: any) {
ElMessage.error(`上传失败: ${error.message}`);
onError?.(error);
}
};
</script>
<template>
<ElUpload
:action="UPLOAD_URL"
:before-upload="onBeforeUpload"
:http-request="customRequest"
:file-list="fileList"
:headers="HEADERS"
:multiple="true"
>
<ElButton type="primary">
<IconifyIcon icon="lucide:upload" class="mr-1" />
点击上传
</ElButton>
<template #file="{ file }">
<div class="flex items-center">
<span>{{ file.name }}</span>
<ElButton type="primary" link size="small" @click="fileList = []">
删除
</ElButton>
</div>
</template>
</ElUpload>
<div v-if="$slots.default" class="ml-1 text-sm text-gray-500">
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,166 @@
<script lang="ts" setup>
import type { FormInstance, UploadProps } from 'element-plus';
import type { UploadData } from './upload';
import { inject, reactive, ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import {
ElButton,
ElDialog,
ElDivider,
ElForm,
ElFormItem,
ElInput,
ElMessage,
ElUpload,
} from 'element-plus';
import { beforeVideoUpload, HEADERS, UPLOAD_URL, UploadType } from './upload';
// TODO @dylan是不是要和 antd 的 props 定义相同哈?这样后续两侧维护方便点
withDefaults(
defineProps<{
modelValue?: boolean;
}>(),
{
modelValue: false,
},
);
const emit = defineEmits<{
'update:modelValue': [v: boolean];
uploaded: [v: void];
}>();
const accountId = inject<number>('accountId');
const uploadRules = {
introduction: [
{ message: '请输入描述', required: true, trigger: 'blur' } as const,
],
title: [{ message: '请输入标题', required: true, trigger: 'blur' } as const],
};
function handleCancel() {
emit('update:modelValue', false);
}
const fileList = ref<any[]>([]);
const uploadData: UploadData = reactive({
accountId: accountId!,
introduction: '',
title: '',
type: UploadType.Video,
});
const uploadFormRef = ref<FormInstance | null>(null);
const uploadVideoRef = ref<any>(null);
async function submitVideo() {
await uploadFormRef.value?.validate();
uploadVideoRef.value?.submit();
}
/** 自定义上传 */
const customRequest: UploadProps['httpRequest'] = async function (options) {
const { file, onError, onSuccess } = options;
const formData = new FormData();
formData.append('file', file as File);
formData.append('type', uploadData.type);
formData.append('title', uploadData.title);
formData.append('introduction', uploadData.introduction);
formData.append('accountId', String(uploadData.accountId));
try {
const response = await fetch(UPLOAD_URL, {
body: formData,
headers: HEADERS,
method: 'POST',
});
const res = await response.json();
if (res.code !== 0) {
ElMessage.error(`上传出错:${res.msg}`);
onError?.(new Error(res.msg));
return;
}
// 清空上传时的各种数据
fileList.value = [];
uploadData.title = '';
uploadData.introduction = '';
emit('update:modelValue', false);
ElMessage.success('上传成功');
onSuccess?.(res);
emit('uploaded');
} catch (error: any) {
ElMessage.error(`上传失败: ${error.message}`);
onError?.(error);
}
};
</script>
<template>
<ElDialog
:model-value="modelValue"
title="新建视频"
width="600px"
@close="handleCancel"
>
<template #footer>
<ElButton @click="handleCancel">取消</ElButton>
<ElButton type="primary" @click="submitVideo">确定</ElButton>
</template>
<ElUpload
ref="uploadVideoRef"
:action="UPLOAD_URL"
:auto-upload="false"
:before-upload="beforeVideoUpload"
:http-request="customRequest"
:file-list="fileList"
:headers="HEADERS"
:limit="1"
:multiple="true"
class="mb-4"
>
<ElButton type="primary">
<IconifyIcon icon="lucide:video" class="mr-1" />
选择视频
</ElButton>
</ElUpload>
<div class="mb-4 ml-1 text-sm text-gray-500">
格式支持 MP4文件大小不超过 10MB
</div>
<ElDivider />
<ElForm
ref="uploadFormRef"
:model="uploadData"
:rules="uploadRules"
label-position="top"
>
<ElFormItem label="标题" prop="title">
<ElInput
v-model="uploadData.title"
placeholder="标题将展示在相关播放页面,建议填写清晰、准确、生动的标题"
/>
</ElFormItem>
<ElFormItem label="描述" prop="introduction">
<ElInput
v-model="uploadData.introduction"
type="textarea"
:rows="3"
placeholder="介绍语将展示在相关播放页面,建议填写简洁明确、有信息量的内容"
/>
</ElFormItem>
</ElForm>
</ElDialog>
</template>

View File

@@ -0,0 +1,133 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MpMaterialApi } from '#/api/mp/material';
/** 视频表格列配置 */
export function useVideoGridColumns(): VxeTableGridOptions<MpMaterialApi.Material>['columns'] {
return [
{
field: 'mediaId',
title: '编号',
align: 'center',
width: 160,
},
{
field: 'name',
title: '文件名',
align: 'center',
minWidth: 100,
},
{
field: 'title',
title: '标题',
align: 'center',
minWidth: 200,
},
{
field: 'introduction',
title: '介绍',
align: 'center',
minWidth: 220,
},
{
field: 'video',
title: '视频',
align: 'center',
width: 220,
slots: { default: 'video' },
},
{
field: 'createTime',
title: '上传时间',
align: 'center',
width: 180,
formatter: 'formatDateTime',
},
{
field: 'actions',
title: '操作',
align: 'center',
fixed: 'right',
width: 180,
slots: { default: 'actions' },
},
];
}
/** 语音表格列配置 */
export function useVoiceGridColumns(): VxeTableGridOptions<MpMaterialApi.Material>['columns'] {
return [
{
field: 'mediaId',
title: '编号',
align: 'center',
width: 160,
},
{
field: 'name',
title: '文件名',
align: 'center',
minWidth: 100,
},
{
field: 'voice',
title: '语音',
align: 'center',
width: 220,
slots: { default: 'voice' },
},
{
field: 'createTime',
title: '上传时间',
align: 'center',
width: 180,
formatter: 'formatDateTime',
},
{
field: 'actions',
title: '操作',
align: 'center',
fixed: 'right',
width: 160,
slots: { default: 'actions' },
},
];
}
/** 图片表格列配置 */
export function useImageGridColumns(): VxeTableGridOptions<MpMaterialApi.Material>['columns'] {
return [
{
field: 'mediaId',
title: '编号',
align: 'center',
width: 400,
},
{
field: 'name',
title: '文件名',
align: 'center',
width: 200,
},
{
field: 'url',
title: '图片',
align: 'center',
width: 200,
slots: { default: 'image' },
},
{
field: 'createTime',
title: '上传时间',
align: 'center',
width: 180,
formatter: 'formatDateTime',
},
{
field: 'actions',
title: '操作',
align: 'center',
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@@ -0,0 +1,108 @@
<script lang="ts" setup>
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';
const props = defineProps<{
list: MpMaterialApi.Material[];
loading: boolean;
}>();
const emit = defineEmits<{
delete: [v: number];
}>();
const columns = useImageGridColumns();
const [Grid, gridApi] = useVbenVxeGrid<MpMaterialApi.Material>({
gridOptions: {
border: true,
columns,
keepSource: true,
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
height: 220,
},
toolbarConfig: {
refresh: true,
},
showOverflow: 'tooltip',
},
});
function updateGridData(data: MpMaterialApi.Material[]) {
if (gridApi.grid?.loadData) {
gridApi.grid.loadData(data);
} else {
gridApi.setGridOptions({ data });
}
}
watch(
() => props.list,
async (list: MpMaterialApi.Material[]) => {
const data = Array.isArray(list) ? list : [];
await nextTick();
updateGridData(data);
},
{ flush: 'post' },
);
watch(
() => props.loading,
(loading: boolean) => {
gridApi.setLoading(loading);
},
);
/** 初始化 */
onMounted(async () => {
await nextTick();
updateGridData(Array.isArray(props.list) ? props.list : []);
gridApi.setLoading(props.loading);
});
</script>
<template>
<Grid class="image-table-grid mt-4 pb-0">
<template #toolbar-tools>
<slot name="toolbar-tools"></slot>
</template>
<template #image="{ row }">
<div class="flex items-center justify-center" style="height: 192px">
<img
:src="row.url"
class="object-contain"
style="display: block; max-width: 100%; max-height: 192px"
/>
</div>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.delete'),
type: 'primary',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['mp:material:delete'],
popConfirm: {
title: '确定要删除该图片吗?',
confirm: () => emit('delete', row.id!),
},
},
]"
/>
</template>
</Grid>
</template>

View File

@@ -0,0 +1,46 @@
import type { UploadProps } from 'element-plus';
import type { UploadRawFile } from '#/views/mp/hooks/useUpload';
import { useAccessStore } from '@vben/stores';
import { UploadType, useBeforeUpload } from '#/views/mp/hooks/useUpload';
const accessStore = useAccessStore();
const HEADERS = { Authorization: `Bearer ${accessStore.accessToken}` }; // 请求头
const UPLOAD_URL = `${import.meta.env.VITE_BASE_URL}/admin-api/mp/material/upload-permanent`; // 上传地址
interface UploadData {
accountId: number;
introduction: string;
title: string;
type: UploadType;
}
const beforeImageUpload: UploadProps['beforeUpload'] = function (
rawFile: UploadRawFile,
) {
return useBeforeUpload(UploadType.Image, 2)(rawFile);
};
const beforeVoiceUpload: UploadProps['beforeUpload'] = function (
rawFile: UploadRawFile,
) {
return useBeforeUpload(UploadType.Voice, 2)(rawFile);
};
const beforeVideoUpload: UploadProps['beforeUpload'] = function (
rawFile: UploadRawFile,
) {
return useBeforeUpload(UploadType.Video, 10)(rawFile);
};
export {
beforeImageUpload,
beforeVideoUpload,
beforeVoiceUpload,
HEADERS,
UPLOAD_URL,
type UploadData,
UploadType,
};

View File

@@ -0,0 +1,99 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MpMaterialApi } from '#/api/mp/material';
import { watch } from 'vue';
import { openWindow } from '@vben/utils';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { WxVideoPlayer } from '#/views/mp/components';
import { useVideoGridColumns } from './data';
const props = defineProps<{
list: MpMaterialApi.Material[];
loading: boolean;
}>();
const emit = defineEmits<{
delete: [v: number];
}>();
const columns = useVideoGridColumns();
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
columns,
keepSource: true,
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
showOverflow: 'tooltip',
} as VxeTableGridOptions<MpMaterialApi.Material>,
});
watch(
() => props.list,
(list: MpMaterialApi.Material[]) => {
const data = Array.isArray(list) ? list : [];
if (gridApi.grid?.loadData) {
gridApi.grid.loadData(data);
} else {
gridApi.setGridOptions({ data });
}
},
{ immediate: true },
);
watch(
() => props.loading,
(loading: boolean) => {
gridApi.setLoading(loading);
},
{ immediate: true },
);
</script>
<template>
<Grid class="mt-4">
<template #toolbar-tools>
<slot name="toolbar-tools"></slot>
</template>
<template #video="{ row }">
<WxVideoPlayer v-if="row.url" :url="row.url" />
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '下载',
type: 'primary',
link: true,
icon: ACTION_ICON.DOWNLOAD,
onClick: () => openWindow(row.url),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['mp:material:delete'],
popConfirm: {
title: '确定要删除该视频吗?',
confirm: () => emit('delete', row.id!),
},
},
]"
/>
</template>
</Grid>
</template>

View File

@@ -0,0 +1,100 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
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';
import { WxVoicePlayer } from '#/views/mp/components';
import { useVoiceGridColumns } from './data';
const props = defineProps<{
list: MpMaterialApi.Material[];
loading: boolean;
}>();
const emit = defineEmits<{
delete: [v: number];
}>();
const columns = useVoiceGridColumns();
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
columns,
keepSource: true,
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
showOverflow: 'tooltip',
} as VxeTableGridOptions<MpMaterialApi.Material>,
});
watch(
() => props.list,
(list: MpMaterialApi.Material[]) => {
const data = Array.isArray(list) ? list : [];
if (gridApi.grid?.loadData) {
gridApi.grid.loadData(data);
} else {
gridApi.setGridOptions({ data });
}
},
{ immediate: true },
);
watch(
() => props.loading,
(loading: boolean) => {
gridApi.setLoading(loading);
},
{ immediate: true },
);
</script>
<template>
<Grid class="mt-4">
<template #toolbar-tools>
<slot name="toolbar-tools"></slot>
</template>
<template #voice="{ row }">
<WxVoicePlayer v-if="row.url" :url="row.url" />
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '下载',
type: 'primary',
link: true,
icon: ACTION_ICON.DOWNLOAD,
onClick: () => openWindow(row.url),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['mp:material:delete'],
popConfirm: {
title: '确定要删除该语音吗?',
confirm: () => emit('delete', row.id!),
},
},
]"
/>
</template>
</Grid>
</template>

View File

@@ -0,0 +1,211 @@
<script lang="ts" setup>
import type { Dayjs } from 'dayjs';
import { reactive, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { DICT_TYPE, MpMsgType } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons';
import {
ElButton,
ElDatePicker,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElOption,
ElPagination,
ElSelect,
} from 'element-plus';
import { getMessagePage } from '#/api/mp/message';
import { WxAccountSelect, WxMsg } from '#/views/mp/components';
import MessageTable from './message-table.vue';
defineOptions({ name: 'MpMessage' });
const loading = ref(false);
const total = ref(0); // 数据的总页数
const list = ref<any[]>([]); // 当前页的列表数据
const queryParams = reactive<{
accountId: number;
createTime: [Dayjs, Dayjs] | undefined;
openid: string;
pageNo: number;
pageSize: number;
type: string;
}>({
accountId: -1,
createTime: undefined,
openid: '',
pageNo: 1,
pageSize: 10,
type: MpMsgType.Text,
}); // 搜索参数
const queryFormRef = ref(); // 搜索的表单
// 消息对话框
const messageBoxVisible = ref(false);
const messageBoxUserId = ref(0);
/** 侦听 accountId */
function onAccountChanged(id: number) {
queryParams.accountId = id;
queryParams.pageNo = 1;
handleQuery();
}
/** 查询列表 */
function handleQuery() {
queryParams.pageNo = 1;
getList();
}
async function getList() {
try {
loading.value = true;
const data = await getMessagePage(queryParams);
list.value = data.list;
total.value = data.total;
} finally {
loading.value = false;
}
}
/** 重置按钮操作 */
async function resetQuery() {
// 暂存 accountId并在 reset 后恢复
const accountId = queryParams.accountId;
queryFormRef.value?.resetFields();
queryParams.accountId = accountId;
handleQuery();
}
/** 打开消息发送窗口 */
async function handleSend(userId: number) {
messageBoxUserId.value = userId;
messageBoxVisible.value = true;
}
/** 分页改变事件 */
function handlePageChange(page: number) {
queryParams.pageNo = page;
getList();
}
/** 每页条数改变事件 */
function handleSizeChange(pageSize: number) {
queryParams.pageSize = pageSize;
queryParams.pageNo = 1;
getList();
}
/** 显示总条数 */
function showTotal(total: number) {
return `${total}`;
}
// TODO @dylan是不是应该都用 Grid 哈?
</script>
<template>
<Page auto-content-height class="flex flex-col">
<!-- 搜索工作栏 -->
<div class="mb-4 rounded-lg bg-background p-4">
<ElForm
ref="queryFormRef"
:model="queryParams"
:inline="true"
class="search-form"
>
<ElFormItem label="公众号" prop="accountId">
<WxAccountSelect @change="onAccountChanged" />
</ElFormItem>
<ElFormItem label="消息类型" prop="type">
<ElSelect
v-model="queryParams.type"
placeholder="请选择消息类型"
class="!w-[240px]"
>
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="用户标识" prop="openid">
<ElInput
v-model="queryParams.openid"
placeholder="请输入用户标识"
clearable
class="!w-[240px]"
/>
</ElFormItem>
<ElFormItem label="创建时间" prop="createTime">
<ElDatePicker
v-model="queryParams.createTime"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
class="!w-[240px]"
/>
</ElFormItem>
<ElFormItem>
<ElButton type="primary" @click="handleQuery">
<template #icon>
<IconifyIcon icon="mdi:magnify" />
</template>
搜索
</ElButton>
<ElButton class="ml-2" @click="resetQuery">
<template #icon>
<IconifyIcon icon="mdi:refresh" />
</template>
重置
</ElButton>
</ElFormItem>
</ElForm>
</div>
<!-- 列表 -->
<div class="flex-1 rounded-lg bg-background p-4">
<MessageTable :list="list" :loading="loading" @send="handleSend" />
<div v-show="total > 0" class="mt-4 flex justify-end">
<ElPagination
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:show-total="showTotal"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<!-- 发送消息的弹窗 -->
<ElDialog
v-model="messageBoxVisible"
title="粉丝消息列表"
width="800"
:close-on-click-modal="false"
destroy-on-close
>
<WxMsg :user-id="messageBoxUserId" />
</ElDialog>
</Page>
</template>
<style scoped>
.search-form :deep(.el-form-item) {
margin-bottom: 16px;
}
</style>

View File

@@ -0,0 +1,263 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MpMessageApi } from '#/api/mp/message';
import { onMounted, watch } from 'vue';
import { MpMsgType as MsgType } from '@vben/constants';
import { formatDate2 } from '@vben/utils';
import { ElButton, ElImage, ElTag } from 'element-plus';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
WxLocation,
WxMusic,
WxNews,
WxVideoPlayer,
WxVoicePlayer,
} from '#/views/mp/components';
const props = withDefaults(
defineProps<{
list?: MpMessageApi.Message[];
loading?: boolean;
}>(),
{
list() {
return [];
},
loading: false,
},
);
const emit = defineEmits<{
(e: 'send', userId: number): void;
}>();
const columns: VxeTableGridOptions<MpMessageApi.Message>['columns'] = [
{
field: 'createTime',
title: '发送时间',
width: 180,
align: 'center',
slots: { default: 'createTime' },
},
{
field: 'type',
title: '消息类型',
width: 80,
align: 'center',
},
{
field: 'sendFrom',
title: '发送方',
width: 80,
align: 'center',
slots: { default: 'sendFrom' },
},
{
field: 'openid',
title: '用户标识',
width: 300,
align: 'center',
},
{
field: 'content',
title: '内容',
align: 'left',
minWidth: 320,
slots: { default: 'content' },
},
{
field: 'actions',
title: '操作',
width: 120,
align: 'center',
fixed: 'right',
slots: { default: 'actions' },
},
];
const [Grid, gridApi] = useVbenVxeGrid<MpMessageApi.Message>({
gridOptions: {
border: true,
columns,
keepSource: true,
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
showOverflow: 'tooltip',
},
});
function normalizeList(list?: MpMessageApi.Message[]) {
return Array.isArray(list) ? list : [];
}
function updateGridData(data: MpMessageApi.Message[]) {
if (gridApi.grid?.loadData) {
gridApi.grid.loadData(data);
} else {
gridApi.setGridOptions({ data });
}
}
watch(
() => props.list,
(list) => {
updateGridData(normalizeList(list));
},
{ flush: 'post' },
);
watch(
() => props.loading,
(loading) => {
gridApi.setLoading(loading);
},
);
/** 初始化 */
onMounted(() => {
updateGridData(normalizeList(props.list));
gridApi.setLoading(props.loading);
});
</script>
<template>
<Grid>
<template #createTime="{ row }">
{{ row.createTime ? formatDate2(row.createTime) : '' }}
</template>
<template #sendFrom="{ row }">
<ElTag v-if="row.sendFrom === 1" type="success">粉丝</ElTag>
<ElTag v-else>公众号</ElTag>
</template>
<template #content="{ row }">
<div
v-if="
(row.type as string) === (MsgType.Event as string) &&
(row.event as string) === 'subscribe'
"
>
<ElTag type="success">关注</ElTag>
</div>
<div
v-else-if="
(row.type as string) === (MsgType.Event as string) &&
(row.event as string) === 'unsubscribe'
"
>
<ElTag type="danger">取消关注</ElTag>
</div>
<div
v-else-if="
(row.type as string) === (MsgType.Event as string) &&
(row.event as string) === 'CLICK'
"
>
<ElTag>点击菜单</ElTag>
【{{ row.eventKey }}】
</div>
<div v-else-if="row.type === MsgType.Event && row.event === 'VIEW'">
<ElTag>点击菜单链接</ElTag>
【{{ row.eventKey }}】
</div>
<div
v-else-if="
row.type === MsgType.Event && row.event === 'scancode_waitmsg'
"
>
<ElTag>扫码结果</ElTag>
【{{ row.eventKey }}】
</div>
<div
v-else-if="row.type === MsgType.Event && row.event === 'scancode_push'"
>
<ElTag>扫码结果</ElTag>
【{{ row.eventKey }}】
</div>
<div
v-else-if="row.type === MsgType.Event && row.event === 'pic_sysphoto'"
>
<ElTag>系统拍照发图</ElTag>
</div>
<div
v-else-if="
row.type === MsgType.Event && row.event === 'pic_photo_or_album'
"
>
<ElTag>拍照或者相册</ElTag>
</div>
<div v-else-if="row.type === MsgType.Event && row.event === 'pic_weixin'">
<ElTag>微信相册</ElTag>
</div>
<div
v-else-if="
row.type === MsgType.Event && row.event === 'location_select'
"
>
<ElTag>选择地理位置</ElTag>
</div>
<div v-else-if="row.type === MsgType.Event">
<ElTag type="danger">未知事件类型</ElTag>
</div>
<div v-else-if="row.type === MsgType.Text">{{ row.content }}</div>
<div v-else-if="row.type === MsgType.Voice">
<WxVoicePlayer
:url="row.mediaUrl || ''"
:content="row.recognition || ''"
/>
</div>
<div v-else-if="row.type === MsgType.Image">
<a :href="row.mediaUrl" target="_blank">
<ElImage :src="row.mediaUrl" :width="100" :preview-src-list="[]" />
</a>
</div>
<div v-else-if="row.type === MsgType.Video || row.type === 'shortvideo'">
<WxVideoPlayer :url="row.mediaUrl || ''" class="mt-2" />
</div>
<div v-else-if="row.type === MsgType.Link">
<ElTag>链接</ElTag>
<a :href="row.url" target="_blank">{{ row.title }}</a>
</div>
<div v-else-if="row.type === MsgType.Location">
<WxLocation
:label="row.label || ''"
:location-y="row.locationY || 0"
:location-x="row.locationX || 0"
/>
</div>
<div v-else-if="row.type === MsgType.Music">
<WxMusic
:title="row.title"
:description="row.description"
:thumb-media-url="row.thumbMediaUrl || ''"
:music-url="row.musicUrl"
:hq-music-url="row.hqMusicUrl"
/>
</div>
<div v-else-if="row.type === MsgType.News">
<WxNews :articles="row.articles" />
</div>
<div v-else>
<ElTag type="danger">未知消息类型</ElTag>
</div>
</template>
<template #actions="{ row }">
<ElButton type="primary" link @click="emit('send', row.userId || 0)">
消息
</ElButton>
</template>
</Grid>
</template>

View File

@@ -80,6 +80,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

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

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

@@ -224,6 +224,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

@@ -79,6 +79,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

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

@@ -77,7 +77,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({
variant: 'text',
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({
type: 'danger',
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

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