UNIVPLMDataIntegration/Web/src/views/system/reportConfig/component/editReportConfig.vue
2025-06-25 11:37:37 +08:00

595 lines
20 KiB
Vue

<template>
<div>
<el-dialog v-model="state.isShowDialog" draggable :close-on-click-modal="false" width="90%">
<template #header>
<div style="color: #fff">
<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-Edit /> </el-icon>
<span> {{ props.title }} </span>
</div>
</template>
<el-form :model="state.ruleForm" ref="ruleFormRef" label-width="130" :rules="rules">
<el-row :gutter="10">
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8" class="mb20">
<el-form-item :label="$t('名称')" prop="name">
<el-input v-model="state.ruleForm.name" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8" class="mb20">
<el-form-item :label="$t('分组')" prop="groupId">
<el-select v-model="state.ruleForm.groupId" filterable clearable>
<el-option v-for="item in state.groupList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8" class="mb20">
<el-form-item :label="$t('描述')" prop="description">
<el-input v-model="state.ruleForm.description" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8" class="mb20">
<el-form-item :label="$t('数据源类型')" prop="dsType">
<g-sys-dict v-model="state.ruleForm.dsType" code="ReportConfigDsTypeEnum" render-as="select" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8" class="mb20" v-if="state.ruleForm.dsType === ReportConfigDsTypeEnum.NUMBER_0">
<el-form-item :label="$t('数据源')" prop="dataSource">
<el-select v-model="state.ruleForm.dataSource" class="w100">
<el-option v-for="item in state.dataSourceList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20" :style="[state.ruleForm.dsType === ReportConfigDsTypeEnum.NUMBER_0 ? '' : 'display:none;']">
<el-form-item :label="$t('脚本语句')" prop="sqlScript">
<div ref="sqlScriptMonacoEditorRef" style="width: 100%; height: 300px; border: 1px solid var(--el-border-color)"></div>
<el-button ref="parseSqlButtonRef" type="success" plain style="margin-top: 8px" @click="parseSqlClick">{{ $t('解析Sql') }}</el-button>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8" class="mb20" v-if="state.ruleForm.dsType === ReportConfigDsTypeEnum.NUMBER_1">
<el-form-item :label="$t('接口地址')" prop="apiUrl">
<el-input v-model="state.ruleForm.apiUrl" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8" class="mb20" v-if="state.ruleForm.dsType === ReportConfigDsTypeEnum.NUMBER_1">
<el-form-item :label="$t('接口请求方式')" prop="apiHttpMethod">
<el-select v-model="state.ruleForm.apiHttpMethod" class="w100">
<el-option key="Get" label="Get" value="Get" />
<el-option key="Post" label="Post" value="Post" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20" :style="[state.ruleForm.dsType === ReportConfigDsTypeEnum.NUMBER_1 ? '' : 'display:none;']">
<el-form-item :label="$t('接口参数')" prop="apiParams">
<div ref="apiParamsMonacoEditorRef" style="width: 100%; height: 300px; border: 1px solid var(--el-border-color)"></div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-tabs v-model="state.activeName" class="report-config-tab">
<el-tab-pane :label="$t('字段')" name="field">
<vxe-grid ref="vFieldGrid" v-bind="fieldConfig.gridOptions" :data="state.fieldListData">
<template #toolbar_buttons>
<el-space>
<el-button type="primary" size="small" icon="ele-Plus" @click="() => add(vFieldGrid!, beforeAddField)">{{ $t('新增') }}</el-button>
<el-button size="small" @click="() => insert(vFieldGrid!)">{{ $t('插入') }}</el-button>
<el-button type="danger" size="small" icon="ele-Minus" plain @click="() => remove(vFieldGrid!)">{{ $t('删除') }}</el-button>
</el-space>
</template>
<template #toolbar_tools> </template>
<template #row_isSummary="{ row }">
{{ row.isSummary ? '√' : '' }}
</template>
<template #row_visible="{ row }">
{{ row.visible ? '√' : '' }}
</template>
</vxe-grid>
</el-tab-pane>
<el-tab-pane :label="$t('参数')" name="param">
<div style="display: flex; height: 100%">
<vxe-grid ref="vParamGrid" style="flex: 1" v-bind="paramConfig.gridOptions" :data="state.paramListData">
<template #toolbar_buttons>
<el-space>
<el-button type="primary" size="small" icon="ele-Plus" @click="() => add(vParamGrid!)">{{ $t('新增') }}</el-button>
<el-button size="small" @click="() => insert(vParamGrid!)">{{ $t('插入') }}</el-button>
<el-button type="danger" size="small" icon="ele-Minus" plain @click="() => remove(vParamGrid!)">{{ $t('删除') }}</el-button>
</el-space>
</template>
<template #toolbar_tools> </template>
</vxe-grid>
<el-card style="margin-left: 4px" :header="$t('内置参数')" headerClass="sys-report-config-param-build-in-card-header">
<el-row style="width: 240px" v-for="(item, index) in buildInParams" :key="index">
<el-col :span="11">{{ item.name }}</el-col>
<el-col :span="13">{{ item.description }}</el-col>
</el-row>
</el-card>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<span class="dialog-footer">
<el-button icon="ele-CircleCloseFilled" @click="cancel">{{ $t(' ') }}</el-button>
<el-button type="primary" icon="ele-CircleCheckFilled" @click="submit">{{ $t(' ') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="sysReportConfigEditForm">
import { computed, onMounted, reactive, ref } from 'vue';
import { ElInput, ElMessage } from 'element-plus';
import { getAPI } from '/@/utils/axios-utils';
import { ReportConfigDsTypeEnum, ReportDataSourceOutput, SysReportField, SysReportGroup, SysReportParam, UpdateReportConfigInput } from '/@/api-services';
import { SysReportConfigApi, SysReportDataSourceApi, SysReportGroupApi } from '/@/api-services/api';
import { VxeGrid } from 'vxe-table';
import { useI18n } from 'vue-i18n';
import * as monaco from 'monaco-editor';
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import { format } from 'sql-formatter';
import { useVxeTable } from '/@/hooks/useVxeTableOptionsHook';
import { sqlSugarDbTypes } from '../inputCtrlType';
const { t } = useI18n();
const props = defineProps({
title: String,
});
const emits = defineEmits(['handleQuery']);
const ruleFormRef = ref();
const vFieldGrid = ref<InstanceType<typeof VxeGrid>>();
const vParamGrid = ref<InstanceType<typeof VxeGrid>>();
const sqlScriptMonacoEditorRef = ref();
const apiParamsMonacoEditorRef = ref();
const parseSqlButtonRef = ref<InstanceType<typeof ElInput>>();
// TODO: monaco 封装成组件
// 设置 monaco worker
self.MonacoEnvironment = {
getWorker: function (workerId: string, label: string) {
if (label === 'json') {
return new jsonWorker();
}
return new editorWorker();
},
};
// 注册 monaco sql 格式化
monaco.languages.registerDocumentFormattingEditProvider('sql', {
provideDocumentFormattingEdits: (model: monaco.editor.ITextModel): monaco.languages.ProviderResult<monaco.languages.TextEdit[]> => {
return [
{
text: formatSql(model.getValue()),
range: model.getFullModelRange(),
},
];
},
});
/** 格式化 Sql */
const formatSql = (value: string): string => {
return format(value, {
tabWidth: 4,
useTabs: false,
keywordCase: 'upper',
dataTypeCase: 'upper',
functionCase: 'upper',
logicalOperatorNewline: 'before',
language: 'transactsql',
});
};
// 初始化monacoEditor对象
var sqlScriptMonacoEditor: monaco.editor.IStandaloneCodeEditor;
const initSqlScriptMonacoEditor = () => {
sqlScriptMonacoEditor = monaco.editor.create(sqlScriptMonacoEditorRef.value, {
theme: 'vs', // 主题 vs vs-dark hc-black
value: '', // 默认显示的值
language: 'sql',
formatOnPaste: true,
wordWrap: 'on', //自动换行,注意大小写
wrappingIndent: 'indent',
folding: true, // 是否折叠
foldingHighlight: true, // 折叠等高线
foldingStrategy: 'indentation', // 折叠方式 auto | indentation
showFoldingControls: 'always', // 是否一直显示折叠 always | mouSEOver
disableLayerHinting: true, // 等宽优化
emptySelectionClipboard: false, // 空选择剪切板
selectionClipboard: false, // 选择剪切板
automaticLayout: true, // 自动布局
codeLens: false, // 代码镜头
scrollBeyondLastLine: false, // 滚动完最后一行后再滚动一屏幕
colorDecorators: true, // 颜色装饰器
accessibilitySupport: 'auto', // 辅助功能支持 "auto" | "off" | "on"
lineNumbers: 'on', // 行号 取值: "on" | "off" | "relative" | "interval" | function
lineNumbersMinChars: 5, // 行号最小字符 number
//enableSplitViewResizing: false,
readOnly: false, //是否只读 取值 true | false
renderLineHighlight: 'none', // 当前行突出显示方式 "all" | "line" | "none" | "gutter"
minimap: {
// 缩略图
size: 'fill',
enabled: false,
},
});
// 添加按钮
sqlScriptMonacoEditor.addOverlayWidget({
getId: function () {
return 'monaco.editor.neuz.parseSqlButton';
},
getDomNode: function () {
return parseSqlButtonRef.value?.$el;
},
getPosition: function () {
return {
preference: monaco.editor.OverlayWidgetPositionPreference.TOP_RIGHT_CORNER,
};
},
});
};
// 初始化monacoEditor对象
var apiParamsMonacoEditor: monaco.editor.IStandaloneCodeEditor;
const initApiParamsMonacoEditor = () => {
apiParamsMonacoEditor = monaco.editor.create(apiParamsMonacoEditorRef.value, {
theme: 'vs', // 主题 vs vs-dark hc-black
value: '', // 默认显示的值
language: 'json',
formatOnPaste: true,
wordWrap: 'on', //自动换行,注意大小写
wrappingIndent: 'indent',
folding: true, // 是否折叠
foldingHighlight: true, // 折叠等高线
foldingStrategy: 'indentation', // 折叠方式 auto | indentation
showFoldingControls: 'always', // 是否一直显示折叠 always | mouSEOver
disableLayerHinting: true, // 等宽优化
emptySelectionClipboard: false, // 空选择剪切板
selectionClipboard: false, // 选择剪切板
automaticLayout: true, // 自动布局
codeLens: false, // 代码镜头
scrollBeyondLastLine: false, // 滚动完最后一行后再滚动一屏幕
colorDecorators: true, // 颜色装饰器
accessibilitySupport: 'auto', // 辅助功能支持 "auto" | "off" | "on"
lineNumbers: 'on', // 行号 取值: "on" | "off" | "relative" | "interval" | function
lineNumbersMinChars: 5, // 行号最小字符 number
//enableSplitViewResizing: false,
readOnly: false, //是否只读 取值 true | false
renderLineHighlight: 'none', // 当前行突出显示方式 "all" | "line" | "none" | "gutter"
});
};
/** 后端内置参数 */
const buildInParams = [
{ name: '@curTenantId', description: '当前租户Id' },
{ name: '@curUserId', description: '当前用户Id' },
];
const fieldConfig = reactive({
gridOptions: useVxeTable<SysReportField>(
{
id: 'sysReportConfigEditForm_fields',
columns: [
{ type: 'checkbox', width: 36, fixed: 'left' },
{ field: 'fieldName', title: t('字段名'), width: 200, editRender: { name: 'ElInput', props: { size: 'small' } }, dragSort: true },
{ field: 'title', title: t('字段标题'), width: 200, editRender: { name: 'ElInput', props: { size: 'small' } } },
{ field: 'isSummary', title: t('是否合计'), width: 90, editRender: { name: 'ElSwitch', props: { size: 'small' } }, slots: { default: 'row_isSummary' } },
{ field: 'visible', title: t('是否显示'), width: 90, editRender: { name: 'ElSwitch', props: { size: 'small' } }, slots: { default: 'row_visible' } },
{ field: 'groupTitle', title: t('分组标题'), width: 200, editRender: { name: 'ElInput', props: { size: 'small' } } },
{ field: 'width', title: t('列宽'), width: 80, editRender: { name: 'ElInputNumber', props: { size: 'small', min: 0, controlsPosition: 'right' } } },
],
},
{
align: 'left',
columnConfig: {
width: 130, // 列默认宽度
},
toolbarConfig: {
refresh: false,
import: false,
export: false,
print: false,
zoom: false,
custom: false,
},
rowConfig: {
drag: true,
},
editConfig: {
trigger: 'click',
mode: 'row',
// showStatus: true,
autoClear: false,
},
keyboardConfig: {
isArrow: true,
},
editRules: {
fieldName: [{ required: true, message: t('字段名不能为空') }],
},
}
),
});
const paramConfig = reactive({
gridOptions: useVxeTable<SysReportField>(
{
id: 'sysReportConfigEditForm_param',
columns: [
{ type: 'checkbox', width: 36, fixed: 'left' },
{ field: 'paramName', title: t('参数名'), editRender: { name: 'ElInput', props: { size: 'small' } } },
{ field: 'title', title: t('参数标题'), editRender: { name: 'ElInput', props: { size: 'small' } } },
{
field: 'inputCtrl',
title: t('输入控件类型'),
editRender: {
name: 'ElSelect',
options: sqlSugarDbTypes,
props: { size: 'small', filterable: true },
},
},
{ field: 'defaultValue', title: t('默认值'), editRender: { name: 'ElInput', props: { size: 'small' } } },
],
},
{
align: 'left',
columnConfig: {
width: 130, // 列默认宽度
},
toolbarConfig: {
refresh: false,
import: false,
export: false,
print: false,
zoom: false,
custom: false,
},
editConfig: {
trigger: 'click',
mode: 'row',
// showStatus: true,
autoClear: false,
},
keyboardConfig: {
isArrow: true,
},
editRules: {
paramName: [{ required: true, message: t('参数名不能为空') }],
title: [{ required: true, message: t('参数标题不能为空') }],
inputCtrl: [{ required: true, message: t('输入控件类型不能为空') }],
},
}
),
});
const state = reactive({
isShowDialog: false,
ruleForm: {} as UpdateReportConfigInput,
activeName: 'field',
/** 字段列表数据 */
fieldListData: [] as SysReportField[],
/** 参数列表数据 */
paramListData: [] as SysReportParam[],
/** 数据源列表数据 */
dataSourceList: [] as ReportDataSourceOutput[],
/** 分组列表数据 */
groupList: [] as SysReportGroup[],
});
const rules = computed(() => {
const data = {
name: [{ required: true, message: t('名称不能为空') }],
dsType: [{ required: true, message: t('数据源类型不能为空') }],
} as any;
if (state.ruleForm.dsType == ReportConfigDsTypeEnum.NUMBER_0) {
data.dataSource = [{ required: true, message: t('数据源不能为空') }];
} else {
data.apiUrl = [{ required: true, message: t('接口地址不能为空') }];
data.apiHttpMethod = [{ required: true, message: t('接口请求方式不能为空') }];
}
return data;
});
onMounted(() => {
getAPI(SysReportDataSourceApi)
.apiSysReportDataSourceGetDataSourceListGet()
.then((result) => {
if (result.data.type !== 'success') {
return;
}
state.dataSourceList = result.data.result!;
});
getAPI(SysReportGroupApi)
.apiSysReportGroupGetListGet()
.then((res) => {
state.groupList = res.data.result!;
});
});
// 打开弹窗
const openDialog = (row: any) => {
state.ruleForm = JSON.parse(JSON.stringify(row));
state.fieldListData = state.ruleForm.fields ? JSON.parse(state.ruleForm.fields) : [];
state.paramListData = state.ruleForm.params ? JSON.parse(state.ruleForm.params) : [];
state.isShowDialog = true;
ruleFormRef.value?.resetFields();
const defaultSqlValue = `-- SELECT * FROM xxx WHERE number = @number AND createTime >= @time AND tenantId=@curTenantId
-- sp_name`;
// 延迟拿值防止取不到
setTimeout(() => {
if (sqlScriptMonacoEditor == null || sqlScriptMonacoEditor == undefined) initSqlScriptMonacoEditor();
sqlScriptMonacoEditor!.setValue(state.ruleForm.sqlScript == undefined || state.ruleForm.sqlScript == null || state.ruleForm.sqlScript == '' ? defaultSqlValue : state.ruleForm.sqlScript);
if (apiParamsMonacoEditor == null || apiParamsMonacoEditor == undefined) initApiParamsMonacoEditor();
apiParamsMonacoEditor!.setValue(state.ruleForm.apiParams ?? '');
}, 100);
};
// 关闭弹窗
const closeDialog = () => {
emits('handleQuery');
state.isShowDialog = false;
};
// 取消
const cancel = () => {
state.isShowDialog = false;
};
// 提交
const submit = () => {
ruleFormRef.value.validate(async (valid: boolean) => {
if (!valid) return;
// 构造提交数据
const fieldFullData = vFieldGrid.value!.getFullData();
const paramFullData = vParamGrid.value!.getFullData();
const record = state.ruleForm;
record.sqlScript = sqlScriptMonacoEditor.getValue();
record.apiParams = apiParamsMonacoEditor.getValue();
record.fields = JSON.stringify(fieldFullData);
record.params = JSON.stringify(paramFullData);
if (state.ruleForm.id != undefined && state.ruleForm.id > 0) {
await getAPI(SysReportConfigApi).apiSysReportConfigUpdatePost(record);
} else {
await getAPI(SysReportConfigApi).apiSysReportConfigAddPost(record);
}
closeDialog();
});
};
/** 在新增字段前执行 */
const beforeAddField = (row: SysReportField): Promise<void> => {
row.isSummary = false;
row.visible = true;
row.width = 0;
return Promise.resolve();
};
/** 解析SQL */
const parseSqlClick = () => {
const paramFullData = vParamGrid.value!.getFullData();
const execParams: Record<string, string> = {};
for (const item of paramFullData) {
if (item.paramName) {
execParams[item.paramName] = '';
}
}
return getAPI(SysReportConfigApi)
.apiSysReportConfigParseSqlPost({ dataSource: state.ruleForm.dataSource, sqlScript: sqlScriptMonacoEditor.getValue(), execParams: execParams })
.then((res) => {
ElMessage.success('解析成功');
const fieldList = vFieldGrid.value!.getFullData();
const fieldNames = res.data.result?.fieldNames ?? [];
fieldNames.forEach((fieldName: string) => {
if (fieldList.some((field) => field.fieldName === fieldName)) return;
const field: SysReportField = { fieldName: fieldName, isSummary: false, visible: true, width: 0 };
vFieldGrid.value!.insertAt(field, -1);
});
});
};
/** 新增行 */
const add = async (grid: InstanceType<typeof VxeGrid>, beforeAdd: ((row: any) => Promise<void>) | undefined = undefined): Promise<void> => {
try {
const newRow = {};
beforeAdd && (await beforeAdd(newRow));
const { row } = await grid.insertAt(newRow, -1);
await grid.setEditRow(row, true);
} catch (err: any) {
console.log(err);
ElMessage.error(err.message || err);
}
};
/** 插入行 */
const insert = async (grid: InstanceType<typeof VxeGrid>): Promise<void> => {
try {
const newRow = {};
const curRow = grid.getCurrentRecord();
const { row } = await grid.insertAt(newRow, curRow);
await grid.setEditRow(row, true);
} catch (err: any) {
console.log(err);
ElMessage.error(err.message || err);
}
};
/** 删除行 */
const remove = async (grid: InstanceType<typeof VxeGrid>): Promise<void> => {
try {
let checkedRows = grid.getCheckboxRecords();
if (checkedRows === null || checkedRows.length === 0) {
if (checkedRows === null) {
checkedRows = [];
}
const curRow = grid.getCurrentRecord();
if (curRow !== null) checkedRows.push(curRow);
}
if (!checkedRows || checkedRows.length === 0) return;
await Promise.all(
checkedRows.map(async (row: any): Promise<void> => {
await grid.remove(row);
})
);
await grid.clearCheckboxRow();
await grid.clearCurrentRow();
} catch (err: any) {
console.log(err);
ElMessage.error(err.message || err);
}
};
// 导出对象
defineExpose({ openDialog });
</script>
<style lang="scss" scoped>
:deep(.el-dialog__body) {
height: calc(100vh - 18px) !important;
display: flex;
flex-direction: column;
}
:deep(.el-tabs) {
height: 100%;
display: flex;
.el-tabs__content {
flex: 1;
.el-tab-pane {
height: 100%;
display: flex;
flex-direction: column;
}
}
}
.report-config-tab {
flex: 1;
}
</style>
<style lang="scss">
.sys-report-config-param-build-in-card-header {
padding: 8px;
}
</style>