diff --git a/.bk.development.env b/.bk.development.env index 0e2a3b613..7ba0299e6 100644 --- a/.bk.development.env +++ b/.bk.development.env @@ -29,3 +29,9 @@ BK_API_GATEWAY_ORIGIN = // 静态资源前缀 BK_STATIC_URL = + +// ai 白名单 +BK_AI_WHITE_LIST = + +// bk-data 授权码 +BK_DATA_DATA_TOKEN = diff --git a/.bk.env b/.bk.env index 9e51aeb46..db873e974 100644 --- a/.bk.env +++ b/.bk.env @@ -6,5 +6,3 @@ BK_APP_PORT = 5000 // 接口路径地址 BK_AJAX_URL_PREFIX = '/api' -// 请求用户接口路径 -BK_USER_INFO_URL = '/user' diff --git a/.bk.production.env b/.bk.production.env index a4e4920ae..728dddcdb 100644 --- a/.bk.production.env +++ b/.bk.production.env @@ -26,3 +26,9 @@ BK_STATIC_URL = {{ BK_STATIC_URL }} // itsm 地址 BK_ITSM_URL = + +// bkvision 网关接口地址 +BK_VISION_API_URL = + +// 跳转到bkvision平台的链接 +BK_VISION_WEB_URL = diff --git a/lib/client/index.html b/lib/client/index.html index f5783c27d..6a0a2da89 100644 --- a/lib/client/index.html +++ b/lib/client/index.html @@ -14,7 +14,7 @@ - + 可视化开发平台 | 腾讯蓝鲸智云 diff --git a/lib/client/src/components/flow-form-comp/form/fields/serial.vue b/lib/client/src/components/flow-form-comp/form/fields/serial.vue new file mode 100644 index 000000000..70684ab1e --- /dev/null +++ b/lib/client/src/components/flow-form-comp/form/fields/serial.vue @@ -0,0 +1,22 @@ + + + + + + diff --git a/lib/client/src/components/flow-form-comp/form/index.vue b/lib/client/src/components/flow-form-comp/form/index.vue index 2d6aad665..8bdf6035c 100644 --- a/lib/client/src/components/flow-form-comp/form/index.vue +++ b/lib/client/src/components/flow-form-comp/form/index.vue @@ -4,6 +4,7 @@ ({}) - } + }, + disabled: Boolean }, data () { return { fieldsCopy: cloneDeep(this.fields), - localValue: {} + localValue: {}, + computeConfigFields: [] } }, watch: { @@ -73,6 +77,7 @@ initFormValue () { const fieldsValue = {} const fieldsWithRules = [] + this.computeConfigFields = [] this.fields.forEach((item) => { let value if (item.key in this.value) { @@ -90,6 +95,14 @@ if (item.meta.default_val_config) { fieldsWithRules.push(item) } + if (item.meta.compute_config_info) { + this.computeConfigFields.push(item) + } + // 隐藏自动编号字段 + if (item.type === 'SERIAL') { + item.isHide = true + } + // 储存各个字段对应的初始值 fieldsValue[item.key] = value }) @@ -115,6 +128,56 @@ }) this.$emit('change', this.localValue) }, + // 初始化计算组件数据 + initComputeData (computeConfigFields) { + computeConfigFields.forEach((computeField) => { + this.localValue[computeField.key] = this.changeComputeDefalutValue(computeField) + }) + }, + // 修改计算控件的值 + changeComputeDefalutValue (computeField) { + const { type, dateTime, numberComput } = computeField.meta.compute_config_info + if (type === 'dateTime') { + // 计算时间间隔 + if (this.changeDateTime(dateTime)) { + return computDateDiff(computeField.meta.compute_config_info) + } + return computeField.default + } else { + this.setBindFieldValue(numberComput) + return computeNumberResult(numberComput) + } + }, + // 修改日期计算组件的开始或结束日期的值 + changeDateTime (dateTime) { + let isChange = false + // 日期计算 + const dateKeys = ['creation_date', 'update_date', 'specify_date'] + const dateTimeKeys = ['startDate', 'endDate'] + dateTimeKeys.forEach((strItem) => { + const key = dateTime[strItem].key + if (!dateKeys.includes(key)) { + // 找到对应的日期字段的值 + dateTime[strItem].value = this.localValue[key] + isChange = true + } + }) + return isChange + }, + // 设置绑定的字段值 + setBindFieldValue (numberComput) { + let fieldsKey = 'computeFields' + if (numberComput.formula === 'customize') { + fieldsKey = 'customizeFormula' + } + numberComput[fieldsKey] = numberComput[fieldsKey].map((item) => { + const key = item.key || item + return { + key, + value: this.localValue[key] + } + }) + }, // 解析是否有表单依赖变化的表单项,如果有则更新数据源配置,触发重新拉取数据逻辑 parseDataSourceRelation (key, val) { this.fieldsCopy.forEach(field => { @@ -166,6 +229,8 @@ } } }) + this.initComputeData(this.computeConfigFields) + // this.initSerialDefaultValue() }, // 获取关联规则中包含当前字段key的字段列表 getValAssociatedFields (key) { diff --git a/lib/client/src/components/flow-form-comp/form/util/index.js b/lib/client/src/components/flow-form-comp/form/util/index.js index 999b1c93e..fe6036e55 100644 --- a/lib/client/src/components/flow-form-comp/form/util/index.js +++ b/lib/client/src/components/flow-form-comp/form/util/index.js @@ -1,3 +1,4 @@ +import dayjs from 'dayjs' export function deepClone (obj) { if (obj === null) return null if (['string', 'number', 'boolean', 'undefined', 'symbol'].includes(typeof obj)) { @@ -22,3 +23,163 @@ export function getComBaseDefault (fieldesType, type) { } return '' } + +export function computDateDiff (computConfigInfo) { + const startDate = computConfigInfo.dateTime.startDate + const endDate = computConfigInfo.dateTime.endDate + const { startDateValue, endDateValue } = setDateAccuracy(startDate.value, endDate.value, computConfigInfo) + let value = '--' + if (startDateValue && endDateValue && (dayjs(endDateValue).diff(dayjs(startDateValue)) > 0)) { + // 结束日期-开始日期 + const startDate = dayjs(startDateValue) + const endDate = dayjs(endDateValue) + const days = endDate.diff(startDate, 'day') + const hours = parseInt(endDate.diff(startDate, 'hour') - (days * 24)) + const minutes = parseInt(endDate.diff(startDate, 'minute') - (days * 24 * 60 + hours * 60)) + value = `${days}天` + const accuracyResult = computConfigInfo.dateTime.accuracyResult + if (accuracyResult !== 'day') { + value += `${hours}小时` + if (accuracyResult === 'minutes') { + value += `${minutes}分钟` + } + } + } + return value +} +// 检查日期精度 +export function checkAccuracy (startDateValue, endDateValue) { + if (!startDateValue || !endDateValue) { + return true + } + const startDate = dayjs(startDateValue).format('YYYY-MM-DD HH:mm:ss') + const endDate = dayjs(endDateValue).format('YYYY-MM-DD HH:mm:ss') + // 时间格式字符串以00:00:00则表示精度为天 + if (startDate.includes('00:00:00') || endDate.includes('00:00:00')) { + return true + } + return false +} +// 设置时间精度 +function setDateAccuracy (startDateValue, endDateValue, computConfigInfo) { + // 如果开始和结束日期的精度不是时分秒,且精度选择时或分 + if (checkAccuracy(startDateValue, endDateValue) && computConfigInfo.dateTime.accuracyResult !== 'day') { + const defaultTime = computConfigInfo.dateTime.defaultTime + if (defaultTime) { + startDateValue = `${dayjs(startDateValue).format('YYYY-MM-DD ')} ${defaultTime}` + endDateValue = `${dayjs(endDateValue).format('YYYY-MM-DD ')} ${defaultTime}` + } else { + startDateValue = '' + endDateValue = '' + } + } + return { startDateValue, endDateValue } +} +// 获取真实公式 +export function getRealFormula (customizeFormula) { + let formula = '' + customizeFormula.forEach((item) => { + const chars = ['+', '-', '*', '/', '(', ')'] + let value = item.key + if (!chars.includes(item.key)) { + value = item.value + } + formula += value + }) + return formula +} +// 获取自定义公式计算结果 +export function evalFun (formulaStr) { + const safetyChars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '-', '*', '/', '(', ')'] + let res = '' + const isSafety = [...formulaStr].some((char) => { + return !safetyChars.includes(char) + }) + if (!isSafety) { + // 表示不存在危险字符 + try { + // eslint-disable-next-line no-eval + res = eval(formulaStr) + } catch (error) { + const err = { msg: '请设置正确的计算公式' } + throw err + } + } else { + const err = { msg: '无法确定表达式的安全性' } + throw err + } + return res +} +// 计算公式 +export const computeFormulas = { + sum (fields) { + let sum = 0 + fields.forEach((item) => { + sum += Number(item.value) + }) + return sum + }, + averageValue (fields) { + const leng = fields.length + const res = this.sum(fields) / leng + return res + }, + median (fields) { + const leng = fields.length + let res = 0 + const fieldsNumber = fields.map((item) => { + return Number(item.value) + }) + fieldsNumber.sort((a, b) => { + return a - b + }) + if (leng % 2 === 0) { + const index = leng / 2 + res = (fieldsNumber[index] + fieldsNumber[index + 1]) / 2 + } else { + res = fieldsNumber[parseInt((leng / 2))] + } + return res + }, + product (fields) { + let res = 1 + fields.forEach((item) => { + res *= Number(item.value) + }) + return res + }, + maxVlaue (fields) { + const fieldsNumber = fields.map((item) => { + return item.value + }) + return Math.max(...fieldsNumber) + }, + minVlaue (fields) { + const fieldsNumber = fields.map((item) => { + return item.value + }) + return Math.min(...fieldsNumber) + } +} +// 计算数值结果 +export function computeNumberResult (numberComput) { + let res = 0 + // 数值计算 + if (numberComput.formula) { + if (numberComput.formula === 'customize') { + const realFormula = getRealFormula(numberComput.customizeFormula) + res = evalFun(realFormula) + } else { + res = computeFormulas[numberComput.formula](numberComput.computeFields) + } + res = Number(res || 0).toFixed(numberComput.decimal) + } else { + res = '--' + } + if (numberComput.unit.position === 'prefix') { + res = `${numberComput.unit.value}${res}` + } else { + res = `${res}${numberComput.unit.value}` + } + return res +} diff --git a/lib/client/src/components/flow/flow-canvas/index.vue b/lib/client/src/components/flow/flow-canvas/index.vue index 16bde41bc..c56da1177 100644 --- a/lib/client/src/components/flow/flow-canvas/index.vue +++ b/lib/client/src/components/flow/flow-canvas/index.vue @@ -268,7 +268,7 @@ axis: { x, y } } } - this.$store.dispatch('nocode/flow/updateNodePos', params) + this.$store.dispatch('nocode/flow/patchNodeData', params) } catch (e) { console.error(e) } diff --git a/lib/client/src/components/flow/nodeConfig/components/node-actions.vue b/lib/client/src/components/flow/nodeConfig/components/node-actions.vue index 6b9343ddf..1d966cdd7 100644 --- a/lib/client/src/components/flow/nodeConfig/components/node-actions.vue +++ b/lib/client/src/components/flow/nodeConfig/components/node-actions.vue @@ -72,59 +72,12 @@ } }, methods: { - // 表单字段保存到itsm - saveItsmFields () { - const fields = this.formConfig.content.map(item => { - const field = cloneDeep(item) - if (typeof item.id !== 'number') { - field.id = null // itsm新建的字段需要传null - } - if (field.source_type === 'WORKSHEET') { - field.source_type = 'CUSTOM_API' - field.meta.data_config.source_type = 'WORKSHEET' - } - field.workflow = this.serviceData.workflow_id - field.state = this.nodeData.id - field.meta.columnId = field.columnId // 表单字段需要保存columnId,itsm不支持直接添加,存到meta里 - delete field.api_instance_id - delete field.columnId - return field - }) - const deletedIds = [] - this.initialFieldIds.forEach(id => { - if (!fields.find(item => item.id === id)) { - deletedIds.push(id) - } - }) - const params = { - fields, - state_id: this.nodeData.id, - delete_ids: deletedIds - } - return this.$store.dispatch('nocode/flow/batchSaveFields', params) - }, - // 表单配置保存到form表 - saveFormConfig (pageId = null) { - const params = { - pageId, - id: this.flowConfig.id, - nodeId: this.nodeData.id, - projectId: this.projectId, - versionId: this.versionId, - formData: this.formConfig - } - return this.$store.dispatch('nocode/flow/editFlowNode', params) - }, - // 更新流程提单页面pageId - updateFlowPageId (pageId) { - const params = { - pageId, - id: this.flowConfig.id - } - return this.$store.dispatch('nocode/flow/editFlow', params) + // 删除流程提单页 + deleteCreateTicketPage () { + return this.$store.dispatch('page/delete', { pageId: this.delCreateTicketPageId }) }, // 更新itsm节点数据 - updateItsmNode (formId) { + updateItsmNode () { const data = cloneDeep(this.nodeData) // 流程服务校验desc字段不为空,节点上没有可配置desc的地方,故先删除 delete data.desc @@ -133,17 +86,6 @@ if (!data.is_multi) { data.finish_condition = {} } - } else if (data.type === 'NORMAL') { - const formFieldsId = this.formConfig.content.map(field => field.id) - data.fields = [...formFieldsId] - // itsm新建服务时,提单节点默认生成一个标题字段,需要保留,默认放到第一个 - if (this.nodeData.is_first_state) { - data.fields.unshift(this.nodeData.fields[0]) - } - data.extras.formConfig = { - id: formId, - type: this.formConfig.type - } } else if (data.type === 'TASK') { if (METHODS_WITHOUT_DATA.includes(data.extras.api_info.method)) { data.extras.api_info.body = { @@ -158,17 +100,13 @@ } return this.$store.dispatch('nocode/flow/updateNode', data) }, - // 更新表单的名称 - updateFormName () { + // 更新流程提单页面pageId + updateFlowPageId (pageId) { const params = { - id: this.formConfig.id, - formName: this.formConfig.formName + pageId, + id: this.flowConfig.id } - return this.$store.dispatch('nocode/form/updateForm', params) - }, - // 删除流程提单页 - deleteCreateTicketPage () { - return this.$store.dispatch('page/delete', { pageId: this.delCreateTicketPageId }) + return this.$store.dispatch('nocode/flow/editFlow', params) }, async handleSaveClick (createPage = false) { try { @@ -178,49 +116,24 @@ } if (createPage) { this.createPagePending = true - } else { - this.savePending = true + this.$refs.createPageDialog.isShow = true + return } - if (this.nodeData.type === 'NORMAL') { - const itsmFields = await this.saveItsmFields() - const content = [] - itsmFields.forEach(field => { - if (this.nodeData.is_first_state && field.id === this.nodeData.fields[0]) { - return - } - field.columnId = field.meta.columnId - field.disabled = true - delete field.meta.columnId - if (field.meta.data_config?.source_type === 'WORKSHEET') { - field.source_type = 'WORKSHEET' - } - content.push(field) - }) - this.$store.commit('nocode/nodeConfig/setFormConfig', { content }) - this.$store.commit('nocode/nodeConfig/setInitialFieldIds', itsmFields) - const res = await this.saveFormConfig(this.flowConfig.pageId) - this.$store.commit('nocode/nodeConfig/setFormConfig', { id: res.formId }) - this.$store.commit('nocode/flow/setFlowNodeFormId', { nodeId: this.nodeData.id, formId: res.formId }) - await this.updateItsmNode(this.formConfig.id) - await this.updateFormName() - if (createPage) { - this.$refs.createPageDialog.isShow = true - return - } else if (this.delCreateTicketPageId) { // 流程提单页被删除 - await this.deleteCreateTicketPage() - await this.updateFlowPageId(0) - this.$store.commit('nocode/flow/setDeletedPageId', null) - this.$store.commit('nocode/flow/setFlowConfig', { pageId: 0 }) - } - } else { - await this.updateItsmNode() + this.savePending = true + // 流程提单页被删除 + if (this.nodeData.type === 'NORMAL' && this.delCreateTicketPageId) { + await this.deleteCreateTicketPage() + await this.updateFlowPageId(0) + this.$store.commit('nocode/flow/setDeletedPageId', null) + this.$store.commit('nocode/flow/setFlowConfig', { pageId: 0 }) } + await this.updateItsmNode() await this.$store.dispatch('nocode/flow/editFlow', { id: this.flowConfig.id, deployed: 0 }) this.$store.commit('nocode/flow/setFlowConfig', { deployed: 0 }) this.$store.commit('nocode/nodeConfig/setNodeDataChangeStatus', false) this.$bkMessage({ - message: this.nodeData.type === 'NORMAL' ? '节点保存成功,表单配置关联数据表变更成功' : '节点保存成功', + message: '节点保存成功', theme: 'success' }) } catch (e) { @@ -233,13 +146,13 @@ // 创建提单页 async handleCreatePageConfirm () { try { - const pageId = await this.$refs.createPageDialog.save() - if (pageId) { - this.$store.commit('nocode/flow/setFlowConfig', { pageId }) - await this.updateFlowPageId(pageId) + const pageData = await this.$refs.createPageDialog.save() + if (pageData) { + this.$store.commit('nocode/flow/setFlowConfig', { pageId: pageData.id }) + await this.updateFlowPageId(pageData.id) await this.$store.dispatch('nocode/flow/editFlow', { id: this.flowConfig.id, deployed: 0 }) - this.$store.dispatch('route/getProjectPageRoute', { projectId: this.projectId, versionId: this.versionId }) this.$store.commit('nocode/flow/setFlowConfig', { deployed: 0 }) + this.$store.commit('nocode/nodeConfig/setCreateTicketPageData', pageData) this.$refs.createPageDialog.isShow = false this.$bkMessage({ diff --git a/lib/client/src/components/flow/nodeConfig/index.vue b/lib/client/src/components/flow/nodeConfig/index.vue index 58ec5be97..b84d12cd1 100644 --- a/lib/client/src/components/flow/nodeConfig/index.vue +++ b/lib/client/src/components/flow/nodeConfig/index.vue @@ -8,22 +8,10 @@ + :workflow-id="serviceData.workflow_id" + @close="goToFlow"> - - - + @@ -35,7 +23,6 @@ import DataProcessNode from './nodes/data-process-node.vue' import ApiNode from './nodes/api-node/index.vue' import ApprovalNode from './nodes/approval-node.vue' - import FormSection from './components/form-section.vue' import NodeActions from './components/node-actions.vue' export default { @@ -45,7 +32,6 @@ DataProcessNode, ApiNode, ApprovalNode, - FormSection, NodeActions }, props: { @@ -53,10 +39,6 @@ serviceData: { type: Object, default: () => ({}) - }, - editable: { - type: Boolean, - default: true } }, data () { @@ -74,7 +56,7 @@ }, computed: { ...mapGetters('projectVersion', { versionId: 'currentVersionId' }), - ...mapState('nocode/nodeConfig', ['nodeData', 'isNodeDataChanged']), + ...mapState('nocode/nodeConfig', ['nodeData']), typeName () { if (this.nodeData.type) { return NODE_TYPE_LIST.find(item => item.type === this.nodeData.type).name @@ -115,18 +97,19 @@ return true }, handleClose () { - if (!this.isNodeDataChanged) { - this.$emit('close') - return - } this.$bkInfo({ title: '此操作会导致您的编辑没有保存,确认吗?', type: 'warning', width: 500, confirmFn: () => { - this.$emit('close') + this.goToFlow() } }) + }, + goToFlow () { + const { projectId, flowId } = this.$route.params + this.$router.push({ name: 'flowConfig', params: { projectId, flowId } }) + this.$emit('close') } } } @@ -157,24 +140,6 @@ padding: 24px; height: calc(100% - 48px); overflow: auto; - .extend-setting-btn { - margin: 16px 0 24px; - padding: 0 130px; - } - .trigger-area { - display: inline-flex; - align-items: center; - font-size: 12px; - color: #3a84ff; - cursor: pointer; - i { - font-size: 18px; - transition: transform 0.2s ease; - &.opened { - transform: rotate(-180deg); - } - } - } } .actions-wrapper { margin-top: 24px; diff --git a/lib/client/src/components/flow/nodeConfig/nodes/normal-node/index.vue b/lib/client/src/components/flow/nodeConfig/nodes/normal-node/index.vue index 6d2fbf2fe..b07a38e40 100644 --- a/lib/client/src/components/flow/nodeConfig/nodes/normal-node/index.vue +++ b/lib/client/src/components/flow/nodeConfig/nodes/normal-node/index.vue @@ -25,8 +25,8 @@ @@ -47,6 +47,9 @@ Processors, NodeFormSetting }, + props: { + workflowId: Number + }, data () { return { formContentLoading: false, diff --git a/lib/client/src/components/flow/nodeConfig/nodes/normal-node/node-form-setting/breadcrumb-nav.vue b/lib/client/src/components/flow/nodeConfig/nodes/normal-node/node-form-setting/breadcrumb-nav.vue index e771b3f23..a32a14c49 100644 --- a/lib/client/src/components/flow/nodeConfig/nodes/normal-node/node-form-setting/breadcrumb-nav.vue +++ b/lib/client/src/components/flow/nodeConfig/nodes/normal-node/node-form-setting/breadcrumb-nav.vue @@ -19,12 +19,12 @@ @@ -36,6 +36,10 @@ flowConfig: { type: Object, default: () => ({}) + }, + editable: { + type: Boolean, + default: true } }, data () { diff --git a/lib/client/src/components/flow/nodeConfig/nodes/normal-node/node-form-setting/edit-form-panel.vue b/lib/client/src/components/flow/nodeConfig/nodes/normal-node/node-form-setting/edit-form-panel.vue index edf82fbb9..6007d2de8 100644 --- a/lib/client/src/components/flow/nodeConfig/nodes/normal-node/node-form-setting/edit-form-panel.vue +++ b/lib/client/src/components/flow/nodeConfig/nodes/normal-node/node-form-setting/edit-form-panel.vue @@ -3,22 +3,27 @@
- + + :hide-preview="!isCreateTicketPage" + :hide-func="!isCreateTicketPage" + :hide-clear="isUseForm" + :custom-loading="savePending" + @save="handleSave">
@@ -28,7 +33,7 @@ v-show="operationType === 'edit'" page-type="FLOW" :content="formConfig.content" - :disabled="formConfig.type === 'USE_FORM'"> + :disabled="isUseForm"> @@ -38,6 +43,8 @@
diff --git a/lib/client/src/components/patch/widget-vant-date-time-picker/index.js b/lib/client/src/components/patch/widget-vant-date-time-picker/index.js new file mode 100644 index 000000000..39b7f7099 --- /dev/null +++ b/lib/client/src/components/patch/widget-vant-date-time-picker/index.js @@ -0,0 +1,17 @@ +/** + * Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community Edition) available. + * Copyright (C) 2017-2019 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +import widgetTimePicker from './index.vue' +import setInstaller from '@/common/component-installer.js' + +setInstaller(widgetTimePicker) + +export default widgetTimePicker diff --git a/lib/client/src/components/patch/widget-vant-date-time-picker/index.vue b/lib/client/src/components/patch/widget-vant-date-time-picker/index.vue new file mode 100644 index 000000000..ff7cf5c98 --- /dev/null +++ b/lib/client/src/components/patch/widget-vant-date-time-picker/index.vue @@ -0,0 +1,79 @@ + + + diff --git a/lib/client/src/components/project/create-page-dialog.vue b/lib/client/src/components/project/create-page-dialog.vue index 380235fb2..f800b5704 100644 --- a/lib/client/src/components/project/create-page-dialog.vue +++ b/lib/client/src/components/project/create-page-dialog.vue @@ -319,7 +319,7 @@ }) } } - return res + return { id: res, pageName: this.formData.pageName } } catch (e) { console.error(e) } finally { diff --git a/lib/client/src/components/render-nocode/form/components/form-edit/association-value/components/rateValueRule.vue b/lib/client/src/components/render-nocode/form/components/form-edit/association-value/components/rateValueRule.vue index b3ac2d2ac..d1a177b8b 100644 --- a/lib/client/src/components/render-nocode/form/components/form-edit/association-value/components/rateValueRule.vue +++ b/lib/client/src/components/render-nocode/form/components/form-edit/association-value/components/rateValueRule.vue @@ -11,6 +11,7 @@ size="small" placeholder="表单字段" :loading="!isCurrentTable && formListLoading" + :disabled="disabled" @change="ruleChange"> 的值处于
- + ~ - + 区间值为 - +
@@ -47,7 +48,8 @@ relFieldList: { type: Array, default: () => [] - } + }, + disabled: Boolean }, data () { return { diff --git a/lib/client/src/components/render-nocode/form/components/form-edit/association-value/linkage.vue b/lib/client/src/components/render-nocode/form/components/form-edit/association-value/linkage.vue index 2ae402edb..b746198c5 100644 --- a/lib/client/src/components/render-nocode/form/components/form-edit/association-value/linkage.vue +++ b/lib/client/src/components/render-nocode/form/components/form-edit/association-value/linkage.vue @@ -15,11 +15,12 @@ - 本表字段 - 他表字段 + 本表字段 + 他表字段 + value="createTicketTime" + :disabled="disabled"> 默认为提交{{ `${field.type === 'DATE' ? '日期' : '时间'}` }} @@ -29,7 +30,8 @@ style="width: 50%;" size="small" v-model="configData.tableName" - :loading="formListLoading"> + :loading="formListLoading" + :disabled="disabled">
- +
- + + + + + + + + +
@@ -304,6 +318,7 @@ :title="fieldData.name" :show.sync="readerOnlyShow" :value="fieldData.read_only_conditions" + :disabled="disabled" @confirm="(val) => onConfirm('read_only_conditions',val)">
@@ -350,6 +369,8 @@ import ConfigDescCompValueDialog from './configDescCompValueDialog' import TableHeaderSetting from './tableHeaderSetting.vue' import RichText from '@/components/flow-form-comp/form/fields/richText.vue' + import ComputeEdit from './components/computeEdit/index.vue' + import SerialEdit from './components/serialEdit.vue' import { FIELDS_FULL_LAYOUT, FIELDS_SHOW_DEFAULT_VALUE, @@ -372,7 +393,9 @@ ShowTypeDialog, DataSourceDialog, ConfigDescCompValueDialog, - RichText + RichText, + ComputeEdit, + SerialEdit }, model: { prop: 'value', @@ -696,6 +719,10 @@ /deep/ .bk-form-checkbox .bk-checkbox-text{ font-size: 12px; } + .serial-tips { + font-size: 12px; + margin: 10px 0 5px; + } } /deep/ .bk-form-control { & > .bk-form-radio, diff --git a/lib/client/src/components/render-nocode/form/components/form-edit/readOnlyDialog.vue b/lib/client/src/components/render-nocode/form/components/form-edit/readOnlyDialog.vue index 4ffff6d8c..d02bd6714 100644 --- a/lib/client/src/components/render-nocode/form/components/form-edit/readOnlyDialog.vue +++ b/lib/client/src/components/render-nocode/form/components/form-edit/readOnlyDialog.vue @@ -10,7 +10,7 @@ :value="show" @confirm="onConfirm" @cancel="$emit('update:show', false)"> - + @@ -50,7 +50,8 @@ fieldList: { type: Array, default: () => [] - } + }, + disabled: Boolean }, data () { return { @@ -59,6 +60,10 @@ }, methods: { onConfirm () { + if (this.disabled) { + this.$emit('update:show', false) + return + } this.$emit('confirm', this.localValue) }, handleChangeValue (val) { diff --git a/lib/client/src/components/render-nocode/form/components/form-edit/requireDialog.vue b/lib/client/src/components/render-nocode/form/components/form-edit/requireDialog.vue index 34093d52a..ce6951133 100644 --- a/lib/client/src/components/render-nocode/form/components/form-edit/requireDialog.vue +++ b/lib/client/src/components/render-nocode/form/components/form-edit/requireDialog.vue @@ -10,7 +10,7 @@ :value="show" @confirm="onConfirm" @cancel="$emit('update:show', false)"> - + @@ -50,7 +50,8 @@ fieldList: { type: Array, default: () => [] - } + }, + disabled: Boolean }, data () { return { @@ -59,6 +60,10 @@ }, methods: { onConfirm () { + if (this.disabled) { + this.$emit('update:show', false) + return + } this.$emit('confirm', this.localValue) }, handleChangeValue (val) { diff --git a/lib/client/src/components/render-nocode/form/components/form-edit/showTypeDialog.vue b/lib/client/src/components/render-nocode/form/components/form-edit/showTypeDialog.vue index a21edb0ff..4ad1a7f43 100644 --- a/lib/client/src/components/render-nocode/form/components/form-edit/showTypeDialog.vue +++ b/lib/client/src/components/render-nocode/form/components/form-edit/showTypeDialog.vue @@ -10,7 +10,7 @@ :value="show" @confirm="onConfirm" @cancel="$emit('update:show', false)"> - + @@ -50,7 +50,8 @@ fieldList: { type: Array, default: () => [] - } + }, + disabled: Boolean }, data () { return { @@ -59,6 +60,10 @@ }, methods: { onConfirm () { + if (this.disabled) { + this.$emit('update:show', false) + return + } if (this.validate()) { this.$emit('confirm', this.localValue) } diff --git a/lib/client/src/components/render-nocode/form/components/left-panel/index.vue b/lib/client/src/components/render-nocode/form/components/left-panel/index.vue index d75cbd43e..a9e193f7a 100644 --- a/lib/client/src/components/render-nocode/form/components/left-panel/index.vue +++ b/lib/client/src/components/render-nocode/form/components/left-panel/index.vue @@ -80,10 +80,10 @@
  • {{ field.name }} @@ -100,6 +100,7 @@ import { FIELDS_TYPES } from '@/components/flow-form-comp/form/constants/forms' import _ from 'lodash' const LAYOUT_GROUP = ['DESC', 'DIVIDER'] + const ADVANCED_GROUP = ['COMPUTE', 'SERIAL'] export default { components: { draggable @@ -131,17 +132,27 @@ name: '基础控件', items: [], isFolded: false + }, + { + name: '高级控件', + items: [], + isFolded: false } ] fieldsArr.forEach(item => { if (LAYOUT_GROUP.includes(item.type)) { group[0].items.push(item) + } else if (ADVANCED_GROUP.includes(item.type)) { + group[2].items.push(item) } else { group[1].items.push(item) } }) return group }, + isFieldDisable (type) { + return this.pageType === 'FLOW' && [...LAYOUT_GROUP, ...ADVANCED_GROUP, 'RATE'].includes(type) + }, handleMove () { this.$emit('move') }, @@ -271,10 +282,11 @@ .fields-list-container { height: calc(100% - 56px); - overflow: hidden; + overflow: auto; width: 100%; background: #FFFFFF; box-shadow: 1px 0 0 0 #DCDEE5; + @mixin scroller; } .group-name { diff --git a/lib/client/src/components/render/mobile/common/simulator-mobile.vue b/lib/client/src/components/render/mobile/common/simulator-mobile.vue index 4f4b62374..1772ff94d 100644 --- a/lib/client/src/components/render/mobile/common/simulator-mobile.vue +++ b/lib/client/src/components/render/mobile/common/simulator-mobile.vue @@ -17,6 +17,8 @@ diff --git a/lib/client/src/preview/router.js b/lib/client/src/preview/router.js index 48bcef26e..be8f73e65 100644 --- a/lib/client/src/preview/router.js +++ b/lib/client/src/preview/router.js @@ -81,7 +81,7 @@ module.exports = (routeGroup, projectPageRouteList, projectRouteList, projectId, // 后端生成页面信息的时候发生错误 routeConifg.component = BkError } else if (route.pageId !== -1) { - // 判断是从storage读取数据还是数据库 + // 判断是从storage读取数据还是数据库 const pagePreviewId = projectId + route.pageCode + versionId const source = pagePreviewId === editPageData.id ? editPageData.source : route.content // 生成页面 diff --git a/lib/client/src/router/index.js b/lib/client/src/router/index.js index 6504e2eeb..965292e4b 100644 --- a/lib/client/src/router/index.js +++ b/lib/client/src/router/index.js @@ -98,13 +98,14 @@ const OperationStatsProject = () => import(/* webpackChunkName: 'operation-stats const OperationStatsFunc = () => import(/* webpackChunkName: 'operation-stats' */'@/views/system/operation/stats/func/index.vue') const OperationStatsComp = () => import(/* webpackChunkName: 'operation-stats' */'@/views/system/operation/stats/comp/index.vue') -// 流程列表 +// 流程管理 const FlowManage = () => import(/* webpackChunkName: 'flow' */'@/views/project/flow-manage/index.vue') -const FlowList = () => import(/* webpackChunkName: 'flow' */'@/views/project/flow-manage/flow-list.vue') -const FlowArchivedList = () => import(/* webpackChunkName: 'flow' */'@/views/project/flow-manage/archived-list.vue') +const FlowList = () => import(/* webpackChunkName: 'flow' */'@/views/project/flow-manage/list/flow-list.vue') +const FlowArchivedList = () => import(/* webpackChunkName: 'flow' */'@/views/project/flow-manage/list/archived-list.vue') const FlowEdit = () => import(/* webpackChunkName: 'flow' */'@/views/project/flow-manage/edit/index.vue') -const FlowConfig = () => import(/* webpackChunkName: 'flow' */'@/views/project/flow-manage/edit/flowConfig.vue') -const FlowAdvancedConfig = () => import(/* webpackChunkName: 'flow' */'@/views/project/flow-manage/edit/advancedConfig.vue') +const FlowConfig = () => import(/* webpackChunkName: 'flow' */'@/views/project/flow-manage/edit/flow-config.vue') +const FlowAdvancedConfig = () => import(/* webpackChunkName: 'flow' */'@/views/project/flow-manage/edit/advanced-config.vue') +const CreateTicketPageEdit = () => import(/* webpackChunkName: 'flow' */'@/views/project/flow-manage/create-ticket-page-edit/index.vue') const routes = [ { @@ -373,6 +374,7 @@ const routes = [ component: FlowEdit, redirect: { name: 'flowConfig' }, children: [ + // 流程设计 { path: '', name: 'flowConfig', @@ -381,6 +383,7 @@ const routes = [ hideSideNav: true } }, + // 流程设计 { path: 'advanced', name: 'flowAdvancedConfig', @@ -390,6 +393,15 @@ const routes = [ } } ] + }, + // 流程提单页编辑 + { + path: ':flowId/page/:pageId', + name: 'createTicketPageEdit', + component: CreateTicketPageEdit, + meta: { + hideSideNav: true + } } ] }, diff --git a/lib/client/src/store/index.js b/lib/client/src/store/index.js index 489f572c5..990b310e8 100644 --- a/lib/client/src/store/index.js +++ b/lib/client/src/store/index.js @@ -33,6 +33,7 @@ import projectVersion from './modules/project-version' import dataSource from './modules/data-source' import api from './modules/api' import iam from './modules/iam' +import ai from './modules/ai' import nocode from './modules/nocode' import file from './modules/file' @@ -73,7 +74,8 @@ const store = new Vuex.Store({ api, nocode, file, - iam + iam, + ai }, // 公共 store state: { diff --git a/lib/client/src/store/modules/ai.js b/lib/client/src/store/modules/ai.js new file mode 100644 index 000000000..aaada97f3 --- /dev/null +++ b/lib/client/src/store/modules/ai.js @@ -0,0 +1,40 @@ +/** + * Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community Edition) available. + * Copyright (C) 2017-2019 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +import http from '@/api' +const perfix = '/ai' + +export default { + namespaced: true, + state: { + isAiAvailable: false + }, + mutations: { + setAiAvailable (state, data) { + state.isAiAvailable = data + } + }, + getters: { + isAiAvailable: state => state.isAiAvailable + }, + actions: { + generatePage (_, data) { + return http.post(`${perfix}/generate-page`, data).then((res = {}) => { + return res.data + }) + }, + checkAiAvailable ({ commit }) { + return http.get(`${perfix}/ai-available`).then((res = {}) => { + commit('setAiAvailable', res.data) + return res.data + }) + } + } +} diff --git a/lib/client/src/store/modules/nocode/flow.js b/lib/client/src/store/modules/nocode/flow.js index 8525f78d7..0843ba08c 100644 --- a/lib/client/src/store/modules/nocode/flow.js +++ b/lib/client/src/store/modules/nocode/flow.js @@ -86,7 +86,7 @@ export default { }, // 部署流程 deployFlow ({ state }, id) { - return http.post(`${prefix}/serviceDeploy`, { id }).then(response => response.data) + return http.post(`${prefix}/serviceDeploy`, { id }, { globalError: false }).then(response => response.data) }, // 归档/恢复流程 archiveFlow ({ state }, params) { @@ -105,7 +105,7 @@ export default { // 更新流程服务配置 updateServiceData ({ state }, params) { const { id, data } = params - return http.post(`${prefix}/service/${id}/save_configs/`, data).then(response => response.data) + return http.post(`${prefix}/service/${id}/save_configs/`, data, { globalError: false }).then(response => response.data) }, // 获取流程图节点 getFlowNodes ({ state }, params) { @@ -134,7 +134,7 @@ export default { }) }, // 更新节点数据 - updateNodePos ({ state }, params) { + patchNodeData ({ state }, params) { return http.patch(`${prefix}/state/${params.id}`, params.data).then(response => response.data) }, // 批量保存表单字段到流程服务 diff --git a/lib/client/src/store/modules/nocode/node-config.js b/lib/client/src/store/modules/nocode/node-config.js index 236e5368c..a1b26c553 100644 --- a/lib/client/src/store/modules/nocode/node-config.js +++ b/lib/client/src/store/modules/nocode/node-config.js @@ -23,6 +23,7 @@ export default { content: [], code: '' }, + createTicketPage: {}, // 提单节点上绑定的提单页配置 initialFieldIds: [] // 已保存的表单项的id,itsm批量更新表单字段接口需要传被删除的表单项id }, mutations: { @@ -129,6 +130,9 @@ export default { }, setNodeDataChangeStatus (state, val) { state.isNodeDataChanged = val + }, + setCreateTicketPageData (state, val) { + state.createTicketPage = val } }, getters: { diff --git a/lib/client/src/views/edit-nocode/components/action-tool/components/save.vue b/lib/client/src/views/edit-nocode/components/action-tool/components/save.vue index 59825ce87..deb2a916b 100644 --- a/lib/client/src/views/edit-nocode/components/action-tool/components/save.vue +++ b/lib/client/src/views/edit-nocode/components/action-tool/components/save.vue @@ -1,9 +1,9 @@ @@ -11,21 +11,27 @@ import { mapGetters } from 'vuex' import html2canvas from 'html2canvas' import MenuItem from '@/views/index/components/action-tool/components/menu-item' + import { checkAccuracy } from '@/components/flow-form-comp/form/util/index.js' + import { bus } from '@/common/bus' export default { components: { MenuItem }, props: { - custom: Boolean // 是否需要自定义保存逻辑 + custom: Boolean, // 是否需要自定义保存逻辑 + customLoading: Boolean, + disabled: Boolean, + saveTips: String }, data () { return { - isLoading: false, + loading: false, isLocked: false, item: { icon: 'bk-drag-icon bk-drag-save', text: '保存', + tips: this.saveTips, func: this.handleSubmit } } @@ -40,10 +46,16 @@ }, projectId () { return this.$route.params.projectId + }, + isLoading () { + return this.customLoading || this.loading } }, methods: { async handleSubmit () { + if (this.disabled) { + return + } if (this.custom) { if (this.validateForm()) { this.$emit('save', this.$store.state.nocode.formSetting.fieldsList) @@ -85,7 +97,7 @@ Object.assign(formData, { id: this.pageDetail.formId }) } try { - this.isLoading = true + this.loading = true const res = await this.$store.dispatch(`form/${action}`, formData) if (res && res.id) { this.savePreviewImg() @@ -102,7 +114,7 @@ } catch (e) { console.error(e) } finally { - this.isLoading = false + this.loading = false } }, // 保存页面content @@ -112,7 +124,7 @@ content } try { - this.isLoading = true + this.loading = true const res = await this.$store.dispatch('page/update', { data: { pageData, @@ -130,7 +142,7 @@ } catch (e) { console.error(e) } finally { - this.isLoading = false + this.loading = false } }, // 校验表单配置 @@ -160,9 +172,24 @@ }) return true } + if (this.checkAccuracy(field)) { + this.$bkMessage({ + theme: 'error', + message: `字段【${field.name}】未设置默认时间精度` + }) + isKeyValid = false + return true + } }) return message === '' }, + // 检查计算组件的默认精度值 + checkAccuracy (field) { + const computConfigInfo = field.meta.compute_config_info + if (computConfigInfo && computConfigInfo.type === 'dateTime' && computConfigInfo.dateTime.accuracyResult !== 'day' && !computConfigInfo.dateTime.defaultTime) { + return checkAccuracy(computConfigInfo.dateTime.startDate.value, computConfigInfo.dateTime.endDate.value) + } + }, // 保存导航数据 async saveTemplate () { const { @@ -213,3 +240,9 @@ } } + diff --git a/lib/client/src/views/edit-nocode/components/action-tool/index.vue b/lib/client/src/views/edit-nocode/components/action-tool/index.vue index 21e1af042..c9bf10085 100644 --- a/lib/client/src/views/edit-nocode/components/action-tool/index.vue +++ b/lib/client/src/views/edit-nocode/components/action-tool/index.vue @@ -2,7 +2,12 @@
    - +
    @@ -22,7 +27,9 @@ }, props: { customSave: Boolean, - hideSave: Boolean, + customLoading: Boolean, + saveDisabled: Boolean, + saveTips: String, hidePreview: Boolean, hideFunc: Boolean, hideClear: Boolean diff --git a/lib/client/src/views/index/components/action-tool/components/ai.vue b/lib/client/src/views/index/components/action-tool/components/ai.vue new file mode 100644 index 000000000..0020afa97 --- /dev/null +++ b/lib/client/src/views/index/components/action-tool/components/ai.vue @@ -0,0 +1,176 @@ + + + diff --git a/lib/client/src/views/index/components/action-tool/components/menu-item.vue b/lib/client/src/views/index/components/action-tool/components/menu-item.vue index 5c3b72669..8782ed00c 100644 --- a/lib/client/src/views/index/components/action-tool/components/menu-item.vue +++ b/lib/client/src/views/index/components/action-tool/components/menu-item.vue @@ -5,7 +5,8 @@ v-bk-tooltips="{ placement: 'bottom', content: item.tips, - disabled: !item.tips + disabled: !item.tips, + hideOnClick: false }" @click="item.func"> diff --git a/lib/client/src/views/index/components/action-tool/index.vue b/lib/client/src/views/index/components/action-tool/index.vue index f69a39b99..b41567839 100644 --- a/lib/client/src/views/index/components/action-tool/index.vue +++ b/lib/client/src/views/index/components/action-tool/index.vue @@ -7,6 +7,7 @@ +
    diff --git a/lib/client/src/views/index/index.vue b/lib/client/src/views/index/index.vue index 97ce31b72..4797a9897 100644 --- a/lib/client/src/views/index/index.vue +++ b/lib/client/src/views/index/index.vue @@ -139,8 +139,6 @@ LC.addEventListener('update', this.handleUpdatePreviewContent) // 更新预览区域数据 LC.addEventListener('ready', this.initPerviewData) - // 卸载的时候,清除 storage 数据 - LC.addEventListener('unload', this.clearPerviewData) }) // 获取并设置当前版本信息 @@ -196,6 +194,8 @@ target: '#editPageSwitchPage' } ] + + window.addEventListener('beforeunload', this.clearPerviewData) }, beforeDestroy () { // 路由离开的时候注销相关事件 @@ -203,7 +203,8 @@ // 更新预览区域数据 LC.removeEventListener('ready', this.initPerviewData) // 卸载的时候,清除 storage 数据 - LC.removeEventListener('unload', this.clearPerviewData) + this.clearPerviewData() + window.removeEventListener('beforeunload', this.clearPerviewData) window.removeEventListener('beforeunload', this.beforeunloadConfirm) }, beforeRouteLeave (to, from, next) { diff --git a/lib/client/src/views/project/data-source-manage/data-table/common/field-table.tsx b/lib/client/src/views/project/data-source-manage/data-table/common/field-table.tsx index fd74bcafd..04b83c332 100644 --- a/lib/client/src/views/project/data-source-manage/data-table/common/field-table.tsx +++ b/lib/client/src/views/project/data-source-manage/data-table/common/field-table.tsx @@ -177,6 +177,15 @@ export default defineComponent({ return ['text', 'json'].includes(props?.row?.type) } }, + { + name: '唯一约束', + type: 'checkbox', + prop: 'unique', + width: '100px', + isReadonly (item, props) { + return ['text', 'json'].includes(props?.row?.type) + } + }, { name: '可空', type: 'checkbox', diff --git a/lib/client/src/views/project/data-source-manage/data-table/common/import.vue b/lib/client/src/views/project/data-source-manage/data-table/common/import.vue index 3d9a5805b..93b772ab2 100644 --- a/lib/client/src/views/project/data-source-manage/data-table/common/import.vue +++ b/lib/client/src/views/project/data-source-manage/data-table/common/import.vue @@ -15,24 +15,37 @@ class="bk-icon icon-info" > + +
    文件类型
    + + + XLSX + + + SQL + + + + + - 支持 XLSX,SQL 文件格式, - - XLSX 模板 - - - - SQL 模板 + 支持 {{ fileType }} 文件格式, + + {{ fileType }} 模板 @@ -59,12 +72,21 @@ + \ No newline at end of file diff --git a/lib/client/src/views/project/flow-manage/edit/advancedConfig.vue b/lib/client/src/views/project/flow-manage/edit/advanced-config.vue similarity index 84% rename from lib/client/src/views/project/flow-manage/edit/advancedConfig.vue rename to lib/client/src/views/project/flow-manage/edit/advanced-config.vue index 9e47ec561..2f5a86a12 100644 --- a/lib/client/src/views/project/flow-manage/edit/advancedConfig.vue +++ b/lib/client/src/views/project/flow-manage/edit/advanced-config.vue @@ -198,42 +198,57 @@ this.advancedData.notify = notify this.advancedData.notify_rule = val.length > 0 ? 'ONCE' : 'NONE' }, + // 保存流程配置数据 + async updateItsmServiceData () { + const { + id, + notify, + notify_freq, + notify_rule, + revoke_config, + show_all_workflow, + show_my_create_workflow + } = this.advancedData + const serviceConfig = { + workflow_config: { + notify, + notify_freq, + notify_rule, + revoke_config, + is_revocable: revoke_config.type !== 0, + show_all_workflow, + show_my_create_workflow, + // 以下为流程服务必需字段 + is_supervise_needed: false, + supervise_type: 'EMPTY', + supervisor: '', + is_auto_approve: false + }, + // 以下为流程服务必需字段 + can_ticket_agency: false, + display_type: 'INVISIBLE' + } + return new Promise((resolve, reject) => { + this.$store.dispatch('nocode/flow/updateServiceData', { id, data: serviceConfig }) + .then(() => { + resolve() + }).catch((error) => { + const h = this.$createElement + this.$bkMessage({ + theme: 'error', + ellipsisLine: 3, + message: error.message + }) + reject(error.message) + }) + }) + }, handleSave () { this.$refs.advancedForm.validate().then(async () => { - try { + try{ this.advancedPending = true - const { - id, - name, - notify, - notify_freq, - notify_rule, - revoke_config, - show_all_workflow, - show_my_create_workflow - } = this.advancedData - const isRevocable = revoke_config.type !== 0 - const serviceConfig = { - workflow_config: { - notify, - notify_freq, - notify_rule, - revoke_config, - is_revocable: isRevocable, - show_all_workflow, - show_my_create_workflow, - // 以下为流程服务必需字段 - is_supervise_needed: false, - supervise_type: 'EMPTY', - supervisor: '', - is_auto_approve: false - }, - // 以下为流程服务必需字段 - can_ticket_agency: false, - display_type: 'INVISIBLE' - } - await this.$store.dispatch('nocode/flow/updateServiceData', { id, data: serviceConfig }) - await this.$store.dispatch('nocode/flow/editFlow', { id: this.flowConfig.id, flowName: name }) + await this.updateItsmServiceData() + await this.$store.dispatch('nocode/flow/editFlow', { id: this.flowConfig.id, flowName: this.advancedData.name }) this.$router.push({ name: 'flowList' }) } catch (e) { console.error(e.message || e) diff --git a/lib/client/src/views/project/flow-manage/edit/components/flow-selector.vue b/lib/client/src/views/project/flow-manage/edit/components/flow-selector.vue index 1e33688fb..a07fcd7a0 100644 --- a/lib/client/src/views/project/flow-manage/edit/components/flow-selector.vue +++ b/lib/client/src/views/project/flow-manage/edit/components/flow-selector.vue @@ -40,7 +40,7 @@
  • diff --git a/lib/server/project-template/project-init-code/lib/client/src/components/flow-form-comp/form/fields/serial.vue b/lib/server/project-template/project-init-code/lib/client/src/components/flow-form-comp/form/fields/serial.vue new file mode 100644 index 000000000..70684ab1e --- /dev/null +++ b/lib/server/project-template/project-init-code/lib/client/src/components/flow-form-comp/form/fields/serial.vue @@ -0,0 +1,22 @@ + + + + + + diff --git a/lib/server/project-template/project-init-code/lib/client/src/components/flow-form-comp/form/index.vue b/lib/server/project-template/project-init-code/lib/client/src/components/flow-form-comp/form/index.vue index 2d6aad665..d0b107594 100644 --- a/lib/server/project-template/project-init-code/lib/client/src/components/flow-form-comp/form/index.vue +++ b/lib/server/project-template/project-init-code/lib/client/src/components/flow-form-comp/form/index.vue @@ -16,6 +16,7 @@ import { debounce, isEqual, cloneDeep } from 'lodash' import dayjs from 'dayjs' import conditionMixins from './condition-mixins' + import { computDateDiff, computeNumberResult } from './util/index.js' import FieldFormItem from './fieldItem.vue' export default { @@ -45,7 +46,8 @@ data () { return { fieldsCopy: cloneDeep(this.fields), - localValue: {} + localValue: {}, + computeConfigFields: [] } }, watch: { @@ -73,6 +75,8 @@ initFormValue () { const fieldsValue = {} const fieldsWithRules = [] + this.computeConfigFields = [] + this.fields.forEach((item) => { let value if (item.key in this.value) { @@ -90,6 +94,13 @@ if (item.meta.default_val_config) { fieldsWithRules.push(item) } + if (item.meta.compute_config_info) { + this.computeConfigFields.push(item) + } + // 隐藏自动编号字段 + if (item.type === 'SERIAL') { + item.isHide = true + } // 储存各个字段对应的初始值 fieldsValue[item.key] = value }) @@ -115,6 +126,56 @@ }) this.$emit('change', this.localValue) }, + // 初始化计算组件数据 + initComputeData (computeConfigFields) { + computeConfigFields.forEach((computeField) => { + this.localValue[computeField.key] = this.changeComputeDefalutValue(computeField) + }) + }, + // 修改计算控件的值 + changeComputeDefalutValue (computeField) { + const { type, dateTime, numberComput } = computeField.meta.compute_config_info + if (type === 'dateTime') { + // 计算时间间隔 + if (this.changeDateTime(dateTime)) { + return computDateDiff(computeField.meta.compute_config_info) + } + return computeField.default + } else { + this.setBindFieldValue(numberComput) + return computeNumberResult(numberComput) + } + }, + // 修改日期计算组件的开始或结束日期的值 + changeDateTime (dateTime) { + let isChange = false + // 日期计算 + const dateKeys = ['creation_date', 'update_date', 'specify_date'] + const dateTimeKeys = ['startDate', 'endDate'] + dateTimeKeys.forEach((strItem) => { + const key = dateTime[strItem].key + if (!dateKeys.includes(key)) { + // 找到对应的日期字段的值 + dateTime[strItem].value = this.localValue[key] + isChange = true + } + }) + return isChange + }, + // 设置绑定的字段值 + setBindFieldValue (numberComput) { + let fieldsKey = 'computeFields' + if (numberComput.formula === 'customize') { + fieldsKey = 'customizeFormula' + } + numberComput[fieldsKey] = numberComput[fieldsKey].map((item) => { + const key = item.key || item + return { + key, + value: this.localValue[key] + } + }) + }, // 解析是否有表单依赖变化的表单项,如果有则更新数据源配置,触发重新拉取数据逻辑 parseDataSourceRelation (key, val) { this.fieldsCopy.forEach(field => { @@ -166,6 +227,7 @@ } } }) + this.initComputeData(this.computeConfigFields) }, // 获取关联规则中包含当前字段key的字段列表 getValAssociatedFields (key) { diff --git a/lib/server/project-template/project-init-code/lib/client/src/components/flow-form-comp/form/util/index.js b/lib/server/project-template/project-init-code/lib/client/src/components/flow-form-comp/form/util/index.js index 06137aada..fe6036e55 100644 --- a/lib/server/project-template/project-init-code/lib/client/src/components/flow-form-comp/form/util/index.js +++ b/lib/server/project-template/project-init-code/lib/client/src/components/flow-form-comp/form/util/index.js @@ -1,3 +1,4 @@ +import dayjs from 'dayjs' export function deepClone (obj) { if (obj === null) return null if (['string', 'number', 'boolean', 'undefined', 'symbol'].includes(typeof obj)) { @@ -15,3 +16,170 @@ export function deepClone (obj) { } return clone } +export function getComBaseDefault (fieldesType, type) { + const fielde = fieldesType.find(item => item.type === type) || {} + if (fielde.hasOwnProperty('default')) { + return fielde.default + } + return '' +} + +export function computDateDiff (computConfigInfo) { + const startDate = computConfigInfo.dateTime.startDate + const endDate = computConfigInfo.dateTime.endDate + const { startDateValue, endDateValue } = setDateAccuracy(startDate.value, endDate.value, computConfigInfo) + let value = '--' + if (startDateValue && endDateValue && (dayjs(endDateValue).diff(dayjs(startDateValue)) > 0)) { + // 结束日期-开始日期 + const startDate = dayjs(startDateValue) + const endDate = dayjs(endDateValue) + const days = endDate.diff(startDate, 'day') + const hours = parseInt(endDate.diff(startDate, 'hour') - (days * 24)) + const minutes = parseInt(endDate.diff(startDate, 'minute') - (days * 24 * 60 + hours * 60)) + value = `${days}天` + const accuracyResult = computConfigInfo.dateTime.accuracyResult + if (accuracyResult !== 'day') { + value += `${hours}小时` + if (accuracyResult === 'minutes') { + value += `${minutes}分钟` + } + } + } + return value +} +// 检查日期精度 +export function checkAccuracy (startDateValue, endDateValue) { + if (!startDateValue || !endDateValue) { + return true + } + const startDate = dayjs(startDateValue).format('YYYY-MM-DD HH:mm:ss') + const endDate = dayjs(endDateValue).format('YYYY-MM-DD HH:mm:ss') + // 时间格式字符串以00:00:00则表示精度为天 + if (startDate.includes('00:00:00') || endDate.includes('00:00:00')) { + return true + } + return false +} +// 设置时间精度 +function setDateAccuracy (startDateValue, endDateValue, computConfigInfo) { + // 如果开始和结束日期的精度不是时分秒,且精度选择时或分 + if (checkAccuracy(startDateValue, endDateValue) && computConfigInfo.dateTime.accuracyResult !== 'day') { + const defaultTime = computConfigInfo.dateTime.defaultTime + if (defaultTime) { + startDateValue = `${dayjs(startDateValue).format('YYYY-MM-DD ')} ${defaultTime}` + endDateValue = `${dayjs(endDateValue).format('YYYY-MM-DD ')} ${defaultTime}` + } else { + startDateValue = '' + endDateValue = '' + } + } + return { startDateValue, endDateValue } +} +// 获取真实公式 +export function getRealFormula (customizeFormula) { + let formula = '' + customizeFormula.forEach((item) => { + const chars = ['+', '-', '*', '/', '(', ')'] + let value = item.key + if (!chars.includes(item.key)) { + value = item.value + } + formula += value + }) + return formula +} +// 获取自定义公式计算结果 +export function evalFun (formulaStr) { + const safetyChars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '-', '*', '/', '(', ')'] + let res = '' + const isSafety = [...formulaStr].some((char) => { + return !safetyChars.includes(char) + }) + if (!isSafety) { + // 表示不存在危险字符 + try { + // eslint-disable-next-line no-eval + res = eval(formulaStr) + } catch (error) { + const err = { msg: '请设置正确的计算公式' } + throw err + } + } else { + const err = { msg: '无法确定表达式的安全性' } + throw err + } + return res +} +// 计算公式 +export const computeFormulas = { + sum (fields) { + let sum = 0 + fields.forEach((item) => { + sum += Number(item.value) + }) + return sum + }, + averageValue (fields) { + const leng = fields.length + const res = this.sum(fields) / leng + return res + }, + median (fields) { + const leng = fields.length + let res = 0 + const fieldsNumber = fields.map((item) => { + return Number(item.value) + }) + fieldsNumber.sort((a, b) => { + return a - b + }) + if (leng % 2 === 0) { + const index = leng / 2 + res = (fieldsNumber[index] + fieldsNumber[index + 1]) / 2 + } else { + res = fieldsNumber[parseInt((leng / 2))] + } + return res + }, + product (fields) { + let res = 1 + fields.forEach((item) => { + res *= Number(item.value) + }) + return res + }, + maxVlaue (fields) { + const fieldsNumber = fields.map((item) => { + return item.value + }) + return Math.max(...fieldsNumber) + }, + minVlaue (fields) { + const fieldsNumber = fields.map((item) => { + return item.value + }) + return Math.min(...fieldsNumber) + } +} +// 计算数值结果 +export function computeNumberResult (numberComput) { + let res = 0 + // 数值计算 + if (numberComput.formula) { + if (numberComput.formula === 'customize') { + const realFormula = getRealFormula(numberComput.customizeFormula) + res = evalFun(realFormula) + } else { + res = computeFormulas[numberComput.formula](numberComput.computeFields) + } + res = Number(res || 0).toFixed(numberComput.decimal) + } else { + res = '--' + } + if (numberComput.unit.position === 'prefix') { + res = `${numberComput.unit.value}${res}` + } else { + res = `${res}${numberComput.unit.value}` + } + return res +} diff --git a/lib/server/project-template/project-init-code/lib/client/src/components/patch/vant-widget/index.js b/lib/server/project-template/project-init-code/lib/client/src/components/patch/vant-widget/index.js new file mode 100644 index 000000000..542b1aaa3 --- /dev/null +++ b/lib/server/project-template/project-init-code/lib/client/src/components/patch/vant-widget/index.js @@ -0,0 +1,5 @@ +import Vue from 'vue' +import '@/common/vant' +import widgetVanDateTimePicker from './widget-vant-date-time-picker.vue' + +Vue.component('widget-van-date-time-picker', widgetVanDateTimePicker) diff --git a/lib/server/project-template/project-init-code/lib/client/src/components/patch/vant-widget/widget-vant-date-time-picker.vue b/lib/server/project-template/project-init-code/lib/client/src/components/patch/vant-widget/widget-vant-date-time-picker.vue new file mode 100644 index 000000000..ff7cf5c98 --- /dev/null +++ b/lib/server/project-template/project-init-code/lib/client/src/components/patch/vant-widget/widget-vant-date-time-picker.vue @@ -0,0 +1,79 @@ + + + diff --git a/lib/server/project-template/project-init-code/lib/client/src/components/patch/widget-bk-vision.vue b/lib/server/project-template/project-init-code/lib/client/src/components/patch/widget-bk-vision.vue new file mode 100644 index 000000000..0e0046072 --- /dev/null +++ b/lib/server/project-template/project-init-code/lib/client/src/components/patch/widget-bk-vision.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/lib/server/project-template/project-init-code/lib/client/src/main.js b/lib/server/project-template/project-init-code/lib/client/src/main.js index 6ce6d676f..31747f0f1 100644 --- a/lib/server/project-template/project-init-code/lib/client/src/main.js +++ b/lib/server/project-template/project-init-code/lib/client/src/main.js @@ -20,6 +20,7 @@ import 'mavon-editor/dist/css/index.css' import renderHtml from '@/components/html' import widgetTableColumn from '@/components/patch/widget-table-column.vue' import widgetBkTable from '@/components/patch/widget-bk-table.vue' +import widgetBkVision from '@/components/patch/widget-bk-vision.vue' ${importElementLib} import pureAxios from '@/api/pureAxios.js' ${importVantLib} @@ -31,6 +32,7 @@ Vue.prototype.$http = pureAxios Vue.component('app-exception', Exception) Vue.component('render-html', renderHtml) +Vue.component('widget-bk-vision', widgetBkVision) Vue.component('widget-table-column', widgetTableColumn) Vue.component('widget-bk-table', widgetBkTable) Vue.component('auth-button', AuthButton) diff --git a/lib/server/project-template/project-init-code/lib/client/src/store/index.js b/lib/server/project-template/project-init-code/lib/client/src/store/index.js index a2d00c3a9..fc381563c 100644 --- a/lib/server/project-template/project-init-code/lib/client/src/store/index.js +++ b/lib/server/project-template/project-init-code/lib/client/src/store/index.js @@ -63,7 +63,7 @@ const store = new Vuex.Store({ * @return {Promise} promise 对象 */ userInfo (context, config = {}) { - return http.get(process.env.BK_USER_INFO_URL).then(response => { + return http.get('/user/getUser').then(response => { const userData = response.data || {} context.commit('updateUser', userData) return userData diff --git a/lib/server/project-template/project-init-code/lib/server/app.browser.js b/lib/server/project-template/project-init-code/lib/server/app.browser.js index 6a76f435f..be286d26f 100644 --- a/lib/server/project-template/project-init-code/lib/server/app.browser.js +++ b/lib/server/project-template/project-init-code/lib/server/app.browser.js @@ -45,7 +45,7 @@ const SESSION_CONFIG = { } async function startServer () { - ${grantApiGWPermissionForItsm} + ${grantApiGWPermissionForApps} const IS_DEV = process.env.NODE_ENV === 'development' const PORT = IS_DEV ? process.env.BK_APP_PORT - 1 : process.env.BK_APP_PORT diff --git a/lib/server/project-template/project-init-code/lib/server/conf/open-api.json b/lib/server/project-template/project-init-code/lib/server/conf/open-api.json index 0e9cff036..aea66cbf0 100644 --- a/lib/server/project-template/project-init-code/lib/server/conf/open-api.json +++ b/lib/server/project-template/project-init-code/lib/server/conf/open-api.json @@ -2,7 +2,7 @@ "swagger": "2.0", "basePath": "/", "info": { - "version": "0.0.1", + "version": "0.0.3", "title": "API Gateway Resources", "description": "Generated by bk-lesscode" }, @@ -133,6 +133,37 @@ "descriptionEn": null } } + }, + "/execQuerySql": { + "post": { + "operationId": "execQuerySql", + "description": "执行查询sql返回数据表数据", + "tags": [], + "responses": { + "default": { + "description": "" + } + }, + "x-bk-apigateway-resource": { + "isPublic": true, + "allowApplyPermission": true, + "matchSubpath": false, + "backend": { + "type": "HTTP", + "method": "post", + "path": "/{env.subpath}api/open-api/execQuerySql", + "matchSubpath": false, + "timeout": 0, + "upstreams": {}, + "transformHeaders": {} + }, + "authConfig": { + "userVerifiedRequired": false + }, + "disabledStages": [], + "descriptionEn": null + } + } } } } \ No newline at end of file diff --git a/lib/server/project-template/project-init-code/lib/server/controller/bkvision.js b/lib/server/project-template/project-init-code/lib/server/controller/bkvision.js new file mode 100644 index 000000000..0e1051803 --- /dev/null +++ b/lib/server/project-template/project-init-code/lib/server/controller/bkvision.js @@ -0,0 +1,75 @@ +/** + * Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community Edition) available. + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +import { + Controller, + Get, + All, + BodyParams, + QueryParams, + Ctx +} from '../decorator' +import { getAuthQuerySubfix, getAuthPostParams } from '../service/open-api' + +@Controller('/api/bkvision') +export default class bkVisionController { + @Get('/api/v1/(panel|meta)/*') + async proxyApi ( + @Ctx() ctx, + @Ctx({ name: 'captures' }) captures, + @QueryParams() query + ) { + // 鉴权信息 + const authSubfix = getAuthQuerySubfix(ctx.cookies) + let queryStr = '' + for (const key in query) { + queryStr += `${key}=${query[key]}&` + } + queryStr += authSubfix + + const url = process.env.BK_VISION_API_URL + '/api/v1/' + captures.join('/') + '?' + queryStr + + console.log(url, 'url', captures) + + const res = await ctx.http.get(url) + ctx.body = res.data + } + + @All('/api/v1/(variable|datasource)/*') + async proxyPostApi ( + @Ctx() ctx, + @Ctx({ name: 'captures' }) captures, + @QueryParams() query, + @BodyParams() body + ) { + // 鉴权信息 + const authParams = getAuthPostParams(ctx.cookies) + + // 需要处理替换body里面的env信息,使bkvision去找对应环境的数据 + if (body.queries && process.env.BKPAAS_ENVIRONMENT) { + (body.queries || []).forEach(item => { + item.env = process.env.BKPAAS_ENVIRONMENT + }) + } + + const data = Object.assign({}, body, authParams) + + let queryStr = '' + for (const key in query) { + queryStr += `${key}=${query[key]}&` + } + const url = process.env.BK_VISION_API_URL + '/api/v1/' + captures.join('/') + '?' + queryStr + + console.log(url, 'url', data) + + const res = await ctx.http.post(url, data) + ctx.body = res.data + } +} diff --git a/lib/server/project-template/project-init-code/lib/server/controller/data-source.js b/lib/server/project-template/project-init-code/lib/server/controller/data-source.js index 89b699b80..7699b2a96 100644 --- a/lib/server/project-template/project-init-code/lib/server/controller/data-source.js +++ b/lib/server/project-template/project-init-code/lib/server/controller/data-source.js @@ -122,7 +122,7 @@ export default class DataSourceController { @BodyParams() data ) { // 入库校验 - validate(formId, data) + await validate(formId, data) // 入库 const result = await dataService.add(tableName, transferData(formId, data)) return result @@ -137,7 +137,7 @@ export default class DataSourceController { @BodyParams() data ) { // 入库校验 - validate(formId, data) + await validate(formId, data) // 入库 const result = await dataService.add(tableName, transferData(formId, data)) return result @@ -152,7 +152,7 @@ export default class DataSourceController { @BodyParams() data ) { // 入库校验 - validate(formId, data) + await validate(formId, data) // 入库 const result = await dataService.update(tableName, transferData(formId, data)) return result @@ -167,7 +167,7 @@ export default class DataSourceController { @BodyParams() data ) { // 入库校验 - validate(formId, data) + await validate(formId, data) // 入库 const result = await dataService.update(tableName, transferData(formId, data)) return result diff --git a/lib/server/project-template/project-init-code/lib/server/controller/open-api.js b/lib/server/project-template/project-init-code/lib/server/controller/open-api.js index 577914ee4..0c979b4d2 100644 --- a/lib/server/project-template/project-init-code/lib/server/controller/open-api.js +++ b/lib/server/project-template/project-init-code/lib/server/controller/open-api.js @@ -149,4 +149,24 @@ export default class OpenApiController { throw new global.BusinessError('api调用失败', -1, 500) } } + + // 执行sql获取某张表下数据 + @OutputJson() + @Post('/execQuerySql') + async execQuerySql ( + @BodyParams({ name: 'sql' }) sql + ) { + try { + // 涉及到表变更sql的关键词 + const sqlKeywords = ['DROP DATABASE', 'TRRUNCATE TABLE', 'DROP TABLE', 'DELETE FROM', 'CREATE TABLE', 'ALTER TABLE', 'INSERT INTO'] + const upperCaseSql = sql && sql.toUpperCase() + // 此接口只能执行查询语句,禁止执行任何涉及表变更的语句 + if (!upperCaseSql || !upperCaseSql.startsWith('SELECT') || sqlKeywords.indexOf(upperCaseSql) !== -1) { + throw new global.BusinessError('sql语句未以SELECT开头或含有变更数据表的危险关键词', -1, 400) + } + return await dataService.execSql(sql) + } catch (e) { + throw new global.BusinessError(e.sqlMessage || e.message || '执行查询sql失败', -1, 500) + } + } } diff --git a/lib/server/project-template/project-init-code/lib/server/controller/user.js b/lib/server/project-template/project-init-code/lib/server/controller/user.js index 662c8302a..9905e7929 100644 --- a/lib/server/project-template/project-init-code/lib/server/controller/user.js +++ b/lib/server/project-template/project-init-code/lib/server/controller/user.js @@ -4,6 +4,7 @@ import { Ctx, OutputJson } from '../decorator' +import token from '../conf/token' const axios = require('axios') const querystring = require('querystring') const https = require('https') @@ -25,8 +26,8 @@ export default class UserController { const urlPrefix = process.env.BK_COMPONENT_API_URL const bkToken = ctx.cookies.get('bk_token') const params = querystring.stringify({ - bk_app_code: process.env.BKPAAS_APP_ID, - bk_app_secret: process.env.BKPAAS_APP_SECRET, + bk_app_code: token.bk_app_code, + bk_app_secret: token.bk_app_secret, bk_token: bkToken }) const ret = await axios({ diff --git a/lib/server/project-template/project-init-code/lib/server/service/data-service.js b/lib/server/project-template/project-init-code/lib/server/service/data-service.js index 1f3720748..eae2c568f 100644 --- a/lib/server/project-template/project-init-code/lib/server/service/data-service.js +++ b/lib/server/project-template/project-init-code/lib/server/service/data-service.js @@ -218,7 +218,16 @@ export function getDataService (name = 'default', customEntityMap) { const [list, count] = await repository.findAndCount(queryObject) return { list, count } }, - + /** + * 获取数量 + * @param {*} tableFileName 表名 + * @param {*} query 查询参数 + * @returns Number 数量 + */ + count (tableFileName, query = { deleteFlag: 0 }) { + const repository = getRepositoryByName(tableFileName) + return repository.count(transformQuery(query)) + }, /** * 获取数据详情 * @param {*} tableFileName 表模型的文件名 @@ -379,6 +388,16 @@ export function getDataService (name = 'default', customEntityMap) { } } return res + }, + + /** + * 执行sql + * @param {*} sqls sql语句 + */ + async execSql (sql) { + const manager = getManager() + const res = await manager.query(sql) + return res } } } diff --git a/lib/server/project-template/project-init-code/lib/server/service/data-source.js b/lib/server/project-template/project-init-code/lib/server/service/data-source.js index d5f7a05f1..e445ba8c1 100644 --- a/lib/server/project-template/project-init-code/lib/server/service/data-source.js +++ b/lib/server/project-template/project-init-code/lib/server/service/data-source.js @@ -13,19 +13,22 @@ import { import dataService, { Like } from './data-service' import dayjs from 'dayjs' import DayJSUtcPlugin from 'dayjs/plugin/utc' + dayjs.extend(DayJSUtcPlugin) // 数据源入库校验 -export const validate = (formId, data) => { +export const validate = async (formId, data) => { // 如果有 formId,执行 nocode 校验 if (formId) { - const formContent = formMap[formId] - if (!formContent) { + const formData = formMap[formId] + if (!formData) { throw new global.BusinessError(`暂未查询到【ID: ${formId}】的 Form 数据,请修改后再试`, 404, 404) } - const validateResult = validateData( - formContent, - data + const validateResult = await validateData( + formData.content, + data, + formData, + dataService ) if (!validateResult.result) { throw new global.BusinessError(`数据入库校验失败:【${validateResult.errorMsg}】,请修改后再试`, 400, 400) diff --git a/lib/server/project-template/project-init-code/lib/server/service/form.js b/lib/server/project-template/project-init-code/lib/server/service/form.js index b815a05c0..5ae7fec8c 100644 --- a/lib/server/project-template/project-init-code/lib/server/service/form.js +++ b/lib/server/project-template/project-init-code/lib/server/service/form.js @@ -121,7 +121,7 @@ export const filterTableDataWithConditions = async (conditions, tableName, group } // ApiGateWay 上给 ITSM 授权 -export const grantApiGWPermissionForItsm = async () => { +export const grantApiGWPermissionForApps = async () => { // dev 模式下不创建网关 if (process.env.NODE_ENV === 'development') return Promise.resolve() @@ -226,6 +226,18 @@ export const grantApiGWPermissionForItsm = async () => { 'prod' ) console.log(`为itsm主动授权完成,appCode为${global.ITSM_APP_CODE}`) + + // 为bkvision主动授权接口权限 + // await grantPermissions( + // apiName, + // { + // target_app_code: 'bkvision', + // grant_dimension: 'api' + // }, + // token, + // 'prod' + // ) + // console.log(`为bkvision主动授权完成`) } catch (e) { console.log('发布网关失败', e.message || e) } diff --git a/lib/server/project-template/project-init-code/lib/server/service/open-api.js b/lib/server/project-template/project-init-code/lib/server/service/open-api.js index 1662c80a5..d8433fdb3 100644 --- a/lib/server/project-template/project-init-code/lib/server/service/open-api.js +++ b/lib/server/project-template/project-init-code/lib/server/service/open-api.js @@ -10,3 +10,15 @@ export const getUserFromApiGW = async (jwt) => { } return user } + +export const getAuthQuerySubfix = (cookies) => { + return `bk_app_code=${bkToken?.bk_app_code}&bk_app_secret=${encodeURI(bkToken?.bk_app_secret)}&${global.AUTH_NAME}=${cookies.get(global.AUTH_NAME)}` +} + +export const getAuthPostParams = (cookies) => { + return { + bk_app_code: bkToken?.bk_app_code, + bk_app_secret: bkToken?.bk_app_secret, + [global.AUTH_NAME]: cookies.get(global.AUTH_NAME) + } +} diff --git a/lib/server/project-template/project-init-code/lib/shared/form/validate.js b/lib/server/project-template/project-init-code/lib/shared/form/validate.js index 657ff4057..9ad1664d7 100644 --- a/lib/server/project-template/project-init-code/lib/shared/form/validate.js +++ b/lib/server/project-template/project-init-code/lib/shared/form/validate.js @@ -1,3 +1,5 @@ + +import { MoreThanOrEqual } from 'typeorm' const dayjs = require('dayjs') // 有校验规则的组件类型 @@ -33,7 +35,6 @@ const ruleNameMap = { 'AFTER_TIME': '系统时间之后', 'BEFORE_TIME': '系统时间之前' } - // 规则key-正则映射 const ruleRegexMap = { 'NUM': /^[0-9]$/g, @@ -57,6 +58,7 @@ const ruleRegexMap = { 'IP': /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/g, 'QQ': /^[1-9][0-9]{4,10}$/g } +let validateDataMsg = { result: true, errorMsg: '' } const isPassValidate = (regexKey, value) => { // 重置正则得 lastIndex @@ -85,29 +87,122 @@ const returnValidataMsg = (regexKey) => { } } +function setSerialFielVlaue (serialNumber, serialRules, fieldData, data) { + fieldData.value = serialRules.map(item => { + if (item.type === 'formField') { + const formField = data.find((formValue) => { + return formValue.key === item.configValue + }) || {} + if (formField.choice?.length) { + const values = formField.value.split(',') + return formField.choice.filter((choiceItem) => { + return values.includes(choiceItem.key) + }).map((choiceItem) => { + return choiceItem.name + }).join(',') + } + return formField.value + } else if (item.type === 'serialNumber') { + return setSerialNumber(serialNumber, item.configValue) + } + return item.serialValue + }).join('-') +} +// 查询周期内的表单数据 +async function queryofrmDataList (resetCycle, formData, dataService) { + let serialNumber = 0 + // 查询数据添加时间大于等于presentCycleStartDateTime的表单数据 + const dateTime = getResetCycleDate(resetCycle) + const leng = await dataService.count(formData.tableName, { + createTime: MoreThanOrEqual(dayjs(dateTime).format()) + }) + if (leng) { + serialNumber = leng + 1 + } + return serialNumber +} +// 获取周期时间 +function getResetCycleDate (resetCycle) { +// 当前周期开始时间 + let presentCycleStartDateTime = 0 + const presentYear = new Date().getFullYear() + const presentMonth = new Date().getMonth() + 1 + let presentWeek = new Date().getDay() + const presentDay = new Date().getDate() + + if (presentWeek === 0) { + // 由于周期要求认定一周的开始是从周一开始的,所以周日时需要减7 + presentWeek = -7 + } + // 按重置周期查询当前周期内数据的条数,有数据则流水号延续,没有则使用初始值为流水号 + switch (resetCycle) { + case 'year': + presentCycleStartDateTime = new Date(`${presentYear}/01/01`).getTime() + break + case 'month': + presentCycleStartDateTime = new Date(`${presentYear}/${presentMonth}/01`).getTime() + break + case 'week': + presentCycleStartDateTime = new Date(presentYear, presentMonth, presentDay - presentWeek + 1).getTime() + break + case 'day': + presentCycleStartDateTime = new Date(`${presentYear}/${presentMonth}/${presentDay}`).getTime() + break + default: + break + } + return presentCycleStartDateTime +} +// 获取流水号 +async function getSerialNumber (resetCycle, formData, dataService) { + const num = await queryofrmDataList(resetCycle, formData, dataService) + return num +} +// 获取流水号 +function setSerialNumber (serialNumber, number) { + const serialNumberStr = `${serialNumber}` + if (number > serialNumberStr.length) { + serialNumber = serialNumberStr.padStart(number, '0') + } else if (number < serialNumberStr.length) { + validateDataMsg = { + result: false, errorMsg: '当前周期内编号位数已超过流水号所设置的位数值' + } + } + return serialNumber +} +// const isValueInOption = (field, val) => { +// // 无值的时候 默认为true(必填在提交的时候校验) +// console.log(field.choice.map(item => item.key), val) +// return val ? true : field.choice.some(option => option.key === val) +// } /** * nocode表单校验 * @param {[]fields} fields 字段列表 * @returns validateDataMsg {result:'' , errorMsg:''} */ -export const validateData = (fields) => { - let validateDataMsg = { result: true, errorMsg: '' } - const len = fields.length || 0 +export const validateData = async (fields, data, formData, dataService) => { + const len = fields.length for (let i = 0; i < len; i++) { - if (hasValidateRulesType.includes(fields[i].type) && fields[i].regex !== 'EMPTY') { - const regexKey = fields[i].regex - const value = fields[i].type === 'INT' ? Number(fields[i].value) : fields[i].value - if (!timeValidateKeys.includes(regexKey)) { - console.log('isPassValidate', isPassValidate(regexKey, value)) - console.log('! isPassValidate', !isPassValidate(regexKey, value)) - if (!isPassValidate(regexKey, value)) { - validateDataMsg = returnValidataMsg(regexKey) + const field = fields[i] + const fieldData = data.find(item => item.key === field.key) + const val = fieldData?.value || '' + const { type, regex, meta } = field + if (type === 'SERIAL' && meta.serial_config_info) { + const { resetCycle, initNumber, serialRules } = meta.serial_config_info + const serialNumber = await getSerialNumber(resetCycle, formData, dataService) || initNumber + setSerialFielVlaue(serialNumber, serialRules, fieldData, data) + } + if (hasValidateRulesType.includes(type) && regex !== 'EMPTY') { + const value = type === 'INT' ? Number(val) : val + if (!timeValidateKeys.includes(regex)) { + if (!isPassValidate(regex, value)) { + validateDataMsg = returnValidataMsg(regex) break } } else { - const isPassTimeValidate = timeValidate(regexKey, value) + const isPassTimeValidate = timeValidate(regex, value) if (!isPassTimeValidate) { - validateDataMsg = returnValidataMsg(regexKey) + validateDataMsg = returnValidataMsg(regex) break } } diff --git a/lib/server/project-template/project-init-code/lib/shared/util.js b/lib/server/project-template/project-init-code/lib/shared/util.js index 514a9b29c..e9593be7d 100644 --- a/lib/server/project-template/project-init-code/lib/shared/util.js +++ b/lib/server/project-template/project-init-code/lib/shared/util.js @@ -58,3 +58,44 @@ export const isEmpty = value => { } return false } + +/** + * 生成 uuid + * + * @param {Number} len 长度 + * @param {Number} radix 基数 + * + * @return {string} uuid + */ + export const uuid = (len = 8, radix = 16) => { + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('') + const uuid = [] + radix = radix || chars.length + + if (len) { + let i + // Compact form + for (i = 0; i < len; i++) { + uuid[i] = chars[0 | Math.random() * radix] + } + } else { + // rfc4122, version 4 form + let r + + // rfc4122 requires these characters + uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-' + uuid[14] = '4' + + let i + // Fill in random data. At i==19 set the high bits of clock sequence as + // per rfc4122, sec. 4.1.5 + for (i = 0; i < 36; i++) { + if (!uuid[i]) { + r = 0 | Math.random() * 16 + uuid[i] = chars[(i === 19) ? (r & 0x3) | 0x8 : r] + } + } + } + + return uuid.join('') +} diff --git a/lib/server/router/open-api.config.js b/lib/server/router/open-api.config.js index 59a7c69b6..158858619 100644 --- a/lib/server/router/open-api.config.js +++ b/lib/server/router/open-api.config.js @@ -10,20 +10,26 @@ */ const { - ping, + getProjectTables, + getProjectTableCols, + getMyProjectList, getProjectReleases, getProjectReleasePackage, getProjectByBindApp, - createProjectByBindApp + createProjectByBindApp, + execQuerySql } = require('../controller/open-api') module.exports = { prefix: '/api/open', routes: { - PING: ['get', '/ping', ping], + GET_MY_PROJECT_LIST: ['get', '/get_project_list', getMyProjectList], + GET_PROJECT_TABLES: ['get', '/get_project_tables', getProjectTables], + GET_PROJECT_TABLE_COLS: ['get', '/get_project_table_cols', getProjectTableCols], PROJECT_RELEASES: ['get', '/project/releases', getProjectReleases], PROJECT_RELEASE_PACKAGE: ['get', '/project/release/package', getProjectReleasePackage, { accessControl: ['project'] }], FIND_PROJECT_BY_APP: ['get', '/find-project-by-app', getProjectByBindApp], - CREATE_PROJECT_BY_APP: ['post', '/create-project-by-app', createProjectByBindApp] + CREATE_PROJECT_BY_APP: ['post', '/create-project-by-app', createProjectByBindApp], + EXEC_QUERY_SQL: ['post', '/exec_query_sql', execQuerySql] } } diff --git a/lib/server/router/open-api.js b/lib/server/router/open-api.js index 6824ecb2e..8dd218297 100644 --- a/lib/server/router/open-api.js +++ b/lib/server/router/open-api.js @@ -42,7 +42,7 @@ router.use('*', async (ctx, next) => { const { debug } = ctx.request.query let user = {} if (process.env.NODE_ENV === 'development' && debug) { - user = { username: 'lesscode1' } + user = { username: 'admin' } } else if (!jwt) { ctx.throw(401, 'Not Found JWT header', { code: 30001 }) } else { diff --git a/lib/server/service/business/data-source.js b/lib/server/service/business/data-source.js index 461dde4fd..68115ae9e 100644 --- a/lib/server/service/business/data-source.js +++ b/lib/server/service/business/data-source.js @@ -16,6 +16,8 @@ import { } from '../../util' import dayjs from 'dayjs' import DayJSUtcPlugin from 'dayjs/plugin/utc' +import { getPreviewDataService } from './preview-db-service' + dayjs.extend(DayJSUtcPlugin) // 数据源入库校验 @@ -32,9 +34,12 @@ export const validate = async (formId, data) => { if (!formData) { throw new global.BusinessError(`暂未查询到【ID: ${formId}】的 Form 数据,请修改后再试`, 404, 404) } - const validateResult = validateData( + const dataService = await getPreviewDataService(formData.projectId) + const validateResult = await validateData( JSON.parse(formData.content), - data + data, + formData, + dataService ) if (!validateResult.result) { throw new global.BusinessError(`数据入库校验失败:【${validateResult.errorMsg}】,请修改后再试`, 400, 400) diff --git a/lib/server/service/business/preview-db-service.js b/lib/server/service/business/preview-db-service.js index 5e65b08d2..1c161274b 100644 --- a/lib/server/service/business/preview-db-service.js +++ b/lib/server/service/business/preview-db-service.js @@ -195,6 +195,14 @@ export const getTables = async (projectId, page, pageSize) => { return result } +export const getTableDetail = async (projectId, tableName) => { + const result = await LCDataService.findOne(TABLE_FILE_NAME.DATA_TABLE, { projectId, deleteFlag: 0, tableName }) || {} + if (result.columns) { + result.columns = JSON.parse(result.columns) + } + return result +} + /** * 获取有权限的数据 * @param {*} projectId 项目id diff --git a/lib/server/service/business/v3-service.js b/lib/server/service/business/v3-service.js index 76c5df317..a17a50458 100644 --- a/lib/server/service/business/v3-service.js +++ b/lib/server/service/business/v3-service.js @@ -18,6 +18,18 @@ const fse = require('fs-extra') const DIR_PATH = '.' const STATIC_URL = `${DIR_PATH}/lib/server/project-template/` +export const getAuthQuerySubfix = (cookies) => { + return `bk_app_code=${v3Config.APP_ID}&bk_app_secret=${encodeURI(v3Config.APP_SECRET)}&${global.AUTH_NAME}=${cookies.get(global.AUTH_NAME)}` +} + +export const getAuthPostParams = (cookies) => { + return { + bk_app_code: v3Config.APP_ID, + bk_app_secret: v3Config.APP_SECRET, + [global.AUTH_NAME]: cookies.get(global.AUTH_NAME) + } +} + export const getReleaseInfo = async (projectId, appCode, moduleCode, env, bkTicket, bindInfo) => { const url = `${v3Config.URL_PREFIX}/bkapps/applications/${appCode}/modules/${moduleCode}/envs/${env}/released_state/?bk_app_code=${v3Config.APP_ID}&bk_app_secret=${encodeURI(v3Config.APP_SECRET)}&${global.AUTH_NAME}=${bkTicket}` return new Promise((resolve, reject) => { diff --git a/lib/server/service/common/ai.js b/lib/server/service/common/ai.js new file mode 100644 index 000000000..a5147cfec --- /dev/null +++ b/lib/server/service/common/ai.js @@ -0,0 +1,79 @@ +import { + execApiGateWay +} from '@bkui/apigateway-nodejs-sdk'; +import { + isEmpty +} from '../../../shared/util'; +import httpConf from '../../conf/http'; +import v3Conf from '../../conf/v3'; + +const authorization = { + bk_app_code: v3Conf.APP_ID, + bk_app_secret: v3Conf.APP_SECRET +} + +/** + * 通过 ai 生成相应的 json + * @param {*} bkToken + * @param {*} userName 当前用户名 + * @param {*} content 自然语言 + * @returns open ai 返回 + */ +export const prompt = async (bkToken, userName, content) => { + if (!isInWhiteList(userName)) { + throw new Error('当前用户不在白名单内,无法使用 AI 相关功能') + } + const { + result, + data, + message + } = await execApiGateWay({ + apiName: 'bk-data', + path: '/v3/aiops/serving/processing/aiops_openai_service/execute/', + method: 'post', + authorization: { + ...authorization, + [global.AUTH_NAME]: bkToken + }, + data: { + bkdata_authentication_method: 'token', + bkdata_data_token: process.env.BK_DATA_DATA_TOKEN, + bk_app_code: authorization.bk_app_code, + bk_app_secret: authorization.bk_app_secret, + [global.AUTH_NAME]: bkToken, + data: { + inputs: [ + { + timestamp: +new Date(), + action: 'create', + object: 'ChatCompletion', + input: JSON.stringify(content) + } + ] + }, + config: { + timeout: 600, + predict_args: { + service_params: "{\"temperature\": 0, \"max_tokens\": 2048, \"top_p\": 1.0, \"frequency_penalty\": 0.0, \"presence_penalty\": 0.0, \"stop\": [\"###\"], \"model\": \"gpt-3.5-turbo\"}" + } + } + }, + apiUrlTemp: httpConf.apiGateWayUrlTmpl, + stageName: httpConf.stageName === 'prod' ? 'dev' : 'prod' + }) + if (result) { + if (data?.data?.status === 'success') { + return data?.data?.data?.[0] || {} + } else { + throw new Error(data?.message || '调用 aiops 接口失败') + } + } else { + throw new Error(message || '调用 aiops 接口失败') + } +} + +// 是否在白名单中 +export const isInWhiteList = (userName) => { + const whiteList = process.env?.BK_AI_WHITE_LIST?.split(';') || [] + return whiteList.includes(userName) && !isEmpty(userName) +} diff --git a/lib/server/service/common/data-service.js b/lib/server/service/common/data-service.js index ce3df6dcb..ae3c3eb29 100644 --- a/lib/server/service/common/data-service.js +++ b/lib/server/service/common/data-service.js @@ -301,7 +301,6 @@ export function getDataService (name = 'default', customEntityMap) { const repository = getRepositoryByName(tableFileName) return repository.findOne(transformQuery(query)) || {} }, - /** * 添加 * @param {*} tableFileName 表模型的文件名 diff --git a/lib/server/service/common/db-engine-service.js b/lib/server/service/common/db-engine-service.js index 3e0a18276..f83cb0d52 100644 --- a/lib/server/service/common/db-engine-service.js +++ b/lib/server/service/common/db-engine-service.js @@ -57,6 +57,7 @@ class DBEngineService { /** * 执行多条sql语句 + * 自动嵌套事务 * @param sqls */ async execMultSql (sqls) { @@ -65,9 +66,18 @@ class DBEngineService { // connect and exec sql const pool = this.getPoolPromise() const res = [] - for (const sql of sqlArr) { - const [execResult] = await pool.query(sql) - res.push(execResult) + try { + // 嵌套事务 + await pool.query('START TRANSACTION;') + for (const sql of sqlArr) { + const [execResult] = await pool.query(sql) + res.push(execResult) + } + await pool.query('COMMIT;') + } catch (error) { + // 失败需要手动回滚 + await pool.query('ROLLBACK;') + throw new Error(error.message || error) } this.close() return res diff --git a/lib/server/service/common/online-db-service.js b/lib/server/service/common/online-db-service.js index 822e67e17..af393e728 100644 --- a/lib/server/service/common/online-db-service.js +++ b/lib/server/service/common/online-db-service.js @@ -57,6 +57,7 @@ export default class OnlineService { TABLE_COLLATION, COLUMN_NAME, COLUMN_TYPE, + COLUMN_KEY, DATA_TYPE, COLUMN_DEFAULT, COLUMN_COMMENT, @@ -68,7 +69,7 @@ export default class OnlineService { } = cur const normalizeColumnIndex = () => { - return indexList.findIndex(x => x.TABLE_NAME === TABLE_NAME && x.COLUMN_NAME === COLUMN_NAME) > -1 + return indexList.findIndex(x => x.TABLE_NAME === TABLE_NAME && x.COLUMN_NAME === COLUMN_NAME && x.NON_UNIQUE === 1) > -1 } const normalizeColumnLength = () => { @@ -87,6 +88,7 @@ export default class OnlineService { name: COLUMN_NAME, type: DATA_TYPE, index: normalizeColumnIndex(), + unique: COLUMN_KEY === 'UNI', nullable: IS_NULLABLE !== 'NO', default: COLUMN_DEFAULT, comment: COLUMN_COMMENT, diff --git a/lib/server/system-conf/open-api.json b/lib/server/system-conf/open-api.json index 01cdb7ad1..fc76032a5 100644 --- a/lib/server/system-conf/open-api.json +++ b/lib/server/system-conf/open-api.json @@ -2,7 +2,7 @@ "swagger": "2.0", "basePath": "/", "info": { - "version": "1.1.0", + "version": "1.1.9", "title": "bk-lesscode", "description": "bk-lesscode 对外接口" }, @@ -257,6 +257,130 @@ "descriptionEn": null } } + }, + "/get_project_list": { + "get": { + "operationId": "get_my_project_list", + "description": "获取有应用开发权限的应用列表", + "tags": [], + "responses": { + "default": { + "description": "" + } + }, + "x-bk-apigateway-resource": { + "isPublic": true, + "allowApplyPermission": true, + "matchSubpath": false, + "backend": { + "type": "HTTP", + "method": "get", + "path": "/{env.subpath}api/open/get_project_list", + "matchSubpath": false, + "timeout": 0, + "upstreams": {}, + "transformHeaders": {} + }, + "authConfig": { + "userVerifiedRequired": true + }, + "disabledStages": [], + "descriptionEn": null + } + } + }, + "/get_project_tables": { + "get": { + "operationId": "get_project_tables", + "description": "根据projctId获取lesscode应用下数据表列表", + "tags": [], + "responses": { + "default": { + "description": "" + } + }, + "x-bk-apigateway-resource": { + "isPublic": true, + "allowApplyPermission": true, + "matchSubpath": false, + "backend": { + "type": "HTTP", + "method": "get", + "path": "/{env.subpath}api/open/get_project_tables", + "matchSubpath": false, + "timeout": 0, + "upstreams": {}, + "transformHeaders": {} + }, + "authConfig": { + "userVerifiedRequired": true + }, + "disabledStages": [], + "descriptionEn": null + } + } + }, + "/get_project_table_cols": { + "get": { + "operationId": "get_project_table_cols", + "description": "根据projctId和tableName获取数据表的字段列表", + "tags": [], + "responses": { + "default": { + "description": "" + } + }, + "x-bk-apigateway-resource": { + "isPublic": true, + "allowApplyPermission": true, + "matchSubpath": false, + "backend": { + "type": "HTTP", + "method": "get", + "path": "/{env.subpath}api/open/get_project_table_cols", + "matchSubpath": false, + "timeout": 0, + "upstreams": {}, + "transformHeaders": {} + }, + "authConfig": { + "userVerifiedRequired": true + }, + "disabledStages": [], + "descriptionEn": null + } + } + }, + "/exec_query_sql": { + "post": { + "operationId": "exec_query_sql", + "description": "根据projectId和sql获取应用预览环境数据表数据", + "tags": [], + "responses": { + "default": { + "description": "" + } + }, + "x-bk-apigateway-resource": { + "isPublic": true, + "allowApplyPermission": true, + "matchSubpath": false, + "backend": { + "type": "HTTP", + "method": "post", + "path": "/{env.subpath}api/open/exec_query_sql", + "matchSubpath": false, + "timeout": 0, + "upstreams": {}, + "transformHeaders": {} + }, + "authConfig": { + "userVerifiedRequired": false + }, + "disabledStages": [], + "descriptionEn": null + } + } } } } \ No newline at end of file diff --git a/lib/shared/data-source/constant.js b/lib/shared/data-source/constant.js index 8065ba10c..7ee0db270 100644 --- a/lib/shared/data-source/constant.js +++ b/lib/shared/data-source/constant.js @@ -66,6 +66,7 @@ export const ORM_KEYS = [ 'primary', 'index', 'nullable', + 'unique', 'default', 'comment', 'createDate', @@ -133,6 +134,8 @@ export const NO_LENGTH_ORM_KEY = [ */ export const DATA_MODIFY_TYPE = { INSERT: 'insert', + UPDATE_INSERT: 'update_insert', + DE_DUPLICATION_INSERT: 'de_duplication_insert', UPDATE: 'update', DELETE: 'delete' } @@ -145,6 +148,14 @@ export const INDEX_MODIFY_TYPE = { ADD: 'ADD' } +/** + * 唯一性约束的修改类型 + */ +export const UNIQUE_MODIFY_TYPE = { + DROP: 'DROP', + ADD: 'ADD' +} + /** * 字段的修改类型 */ @@ -247,3 +258,28 @@ export const DATA_SOURCE_TYPE = { PREVIEW: 'preview', BK_BASE: 'bk-base' } + +// 数据源导入导出文件类型 +export const DATA_FILE_TYPE = { + SQL: 'sql', + XLSX: 'xlsx' +} + +// 数据导入操作类型 +export const DATA_IMPORT_OPERATION_TYPE = { + ALL_INSERT: { + ID: 'ALL_NEW', + NAME: '新增导入', + TIPS: '1. 所有导入的数据以新增的方式插入数据库
    2. 新增数据的唯一性约束字段不能和已有数据重复,否则新增失败' + }, + UPDATE_INSERT: { + ID: 'UPDATE_NEW', + NAME: '更新导入', + TIPS: '1. 导入的数据中,如果某条数据已经在数据库中存在则更新这条数据。如果不存在则新增这条数据
    2. 通过表结构中的唯一性约束字段或者ID字段判断是否在数据库中存在该条数据' + }, + DE_DUPLICATION_INSERT: { + ID: 'DE_DUPLICATION_NEW', + NAME: '去重导入', + TIPS: '1. 导入的数据中,如果某条数据已经在数据库中存在则忽略这条数据。如果不存在则新增这条数据
    2. 通过表结构中的唯一性约束字段或者ID字段判断是否在数据库中存在该条数据' + } +} diff --git a/lib/shared/data-source/data-parse/common.js b/lib/shared/data-source/data-parse/common.js index a7d411d6e..6adfdd59b 100644 --- a/lib/shared/data-source/data-parse/common.js +++ b/lib/shared/data-source/data-parse/common.js @@ -13,7 +13,9 @@ import { TABLE_MODIFY_TYPE, FIELD_MODIFY_TYPE, INDEX_MODIFY_TYPE, - DATA_MODIFY_TYPE + UNIQUE_MODIFY_TYPE, + DATA_MODIFY_TYPE, + DATA_IMPORT_OPERATION_TYPE } from '../constant' /** @@ -29,7 +31,7 @@ function diffColumns (originColumns, finalColumns) { const sameColumnName = originColumns.find(originColumn => originColumn.name === finalColumn.name) const originColumn = sameColumnId || sameColumnName || {} // 如果不一样,表示该行有修改或者是新增行 - if (ORM_KEYS.some(key => key !== 'index' && finalColumn[key] !== originColumn[key])) { + if (ORM_KEYS.some(key => !['index', 'unique'].includes(key) && finalColumn[key] !== originColumn[key])) { if (originColumn.name) { const type = originColumn.name === finalColumn.name ? FIELD_MODIFY_TYPE.MODIFY_COLUMN @@ -65,6 +67,23 @@ function diffIndex (originColumns = [], finalColumns) { ] } +/** + * 对表的唯一性进行对比 + * @param {*} originColumns 修改前的字段 + * @param {*} finalColumns 修改后的字段 + */ +function diffUnique (originColumns = [], finalColumns) { + const getDifference = (a1, a2) => { + return a1.filter((a) => (a.unique && !a2.some(b => (b.unique && a.name === b.name)))) + } + const dropIndex = getDifference(originColumns, finalColumns).map(data => ({ type: UNIQUE_MODIFY_TYPE.DROP, data })) + const addIndex = getDifference(finalColumns, originColumns).map(data => ({ type: UNIQUE_MODIFY_TYPE.ADD, data })) + return [ + ...dropIndex, + ...addIndex + ] +} + /** * 对比导入前后的表结构,得到导入后影响的数据 * @param {*} originDatas 导入前的数据 @@ -76,6 +95,7 @@ export const diff = (originDatas, finalDatas) => { const finalData = finalDatas[index] const originData = originDatas.find((originData) => (finalData.id && originData.id === finalData.id)) || {} const indexData = diffIndex(originData.columns, finalData.columns) + const uniqueData = diffUnique(originData.columns, finalData.columns) if (originData.tableName) { if (originData.tableName !== finalData.tableName) { result.push({ type: TABLE_MODIFY_TYPE.RENAME, data: finalData, originData }) @@ -86,15 +106,15 @@ export const diff = (originDatas, finalDatas) => { } const modifyColumns = diffColumns(originData.columns, finalData.columns) - if (modifyColumns.length || indexData.length) { + if (modifyColumns.length || indexData.length || uniqueData.length) { const data = { ...finalData, columns: modifyColumns } - result.push({ type: TABLE_MODIFY_TYPE.MODIFY, data, index: indexData }) + result.push({ type: TABLE_MODIFY_TYPE.MODIFY, data, index: indexData, unique: uniqueData }) } } else { - result.push({ type: TABLE_MODIFY_TYPE.CREATE, data: finalData, index: indexData }) + result.push({ type: TABLE_MODIFY_TYPE.CREATE, data: finalData, index: indexData, unique: uniqueData }) } } // 删除的表 @@ -124,6 +144,10 @@ export const diffDatas = (originTableDatas, finalTableDatas) => { if (!isSameData) { diffResult.push({ type: DATA_MODIFY_TYPE.UPDATE, data: finalData, tableName: finalTableData.tableName }) } + } else if (finalData._dataImportOperationType === DATA_IMPORT_OPERATION_TYPE.DE_DUPLICATION_INSERT.ID) { + diffResult.push({ type: DATA_MODIFY_TYPE.DE_DUPLICATION_INSERT, data: finalData, tableName: finalTableData.tableName }) + } else if (finalData._dataImportOperationType === DATA_IMPORT_OPERATION_TYPE.UPDATE_INSERT.ID) { + diffResult.push({ type: DATA_MODIFY_TYPE.UPDATE_INSERT, data: finalData, tableName: finalTableData.tableName }) } else { diffResult.push({ type: DATA_MODIFY_TYPE.INSERT, data: finalData, tableName: finalTableData.tableName }) } diff --git a/lib/shared/data-source/data-parse/data-parser/json-parser.js b/lib/shared/data-source/data-parse/data-parser/json-parser.js index f2ac08f8d..8e19350d3 100644 --- a/lib/shared/data-source/data-parse/data-parser/json-parser.js +++ b/lib/shared/data-source/data-parse/data-parser/json-parser.js @@ -14,8 +14,8 @@ */ export class DataJsonParser { constructor (data) { - const parsedData = JSON.parse(JSON.stringify(data || [])) - this.datas = Array.isArray(parsedData) ? parsedData : [parsedData] + const list = Array.isArray(data) ? data : [data] + this.datas = list.filter(v => v) } import (that = {}) { diff --git a/lib/shared/data-source/data-parse/data-parser/sql-parser.js b/lib/shared/data-source/data-parse/data-parser/sql-parser.js index eaf84dd94..baabd7284 100644 --- a/lib/shared/data-source/data-parse/data-parser/sql-parser.js +++ b/lib/shared/data-source/data-parse/data-parser/sql-parser.js @@ -69,10 +69,20 @@ function transformJson2Sql (originDatas, finalDatas) { acc.push(`\`${cur}\`='${data[cur]}'`) return acc }, []) + const updateInsertKeys = tableKeys.reduce((acc, cur) => { + acc.push(`\`${cur}\`= VALUES(\`${cur}\`)`) + return acc + }, []) switch (type) { case DATA_MODIFY_TYPE.INSERT: sqlArr.push(`INSERT INTO \`${tableName}\`(\`${tableKeys.join('\`,\`')}\`) VALUES(${tableValues.join(',')});`) break + case DATA_MODIFY_TYPE.UPDATE_INSERT: + sqlArr.push(`INSERT INTO \`${tableName}\`(\`${tableKeys.join('\`,\`')}\`) VALUES(${tableValues.join(',')}) ON DUPLICATE KEY UPDATE ${updateInsertKeys.join(',')};`) + break + case DATA_MODIFY_TYPE.DE_DUPLICATION_INSERT: + sqlArr.push(`INSERT IGNORE INTO \`${tableName}\`(\`${tableKeys.join('\`,\`')}\`) VALUES(${tableValues.join(',')});`) + break case DATA_MODIFY_TYPE.UPDATE: sqlArr.push(`UPDATE \`${tableName}\` SET ${updateValues.join(',')} WHERE \`id\` = ${data.id};`) break diff --git a/lib/shared/data-source/data-parse/data-parser/xlsx-parser.js b/lib/shared/data-source/data-parse/data-parser/xlsx-parser.js index 8dfda7488..92d8b8b5e 100644 --- a/lib/shared/data-source/data-parse/data-parser/xlsx-parser.js +++ b/lib/shared/data-source/data-parse/data-parser/xlsx-parser.js @@ -9,6 +9,8 @@ * specific language governing permissions and limitations under the License. */ import * as XLSX from 'xlsx' +import { getTypeByValue } from '../../../util' +import { DATA_TYPES } from '../../../constant' /** * xlsx 转 json @@ -32,7 +34,11 @@ function transformJson2Xlsx (finalDatas) { const body = [] list.forEach((data) => { const dataValues = header.map((key) => { - return (Reflect.has(data, key) ? data[key] : '') + const value = Reflect.has(data, key) ? data[key] : '' + if (getTypeByValue(value) === DATA_TYPES.OBJECT.VAL) { + return JSON.stringify(value) + } + return value }) body.push(dataValues) }) diff --git a/lib/shared/data-source/data-parse/struct-parser/sql-parser.js b/lib/shared/data-source/data-parse/struct-parser/sql-parser.js index b1911b1d2..772bc644a 100644 --- a/lib/shared/data-source/data-parse/struct-parser/sql-parser.js +++ b/lib/shared/data-source/data-parse/struct-parser/sql-parser.js @@ -13,7 +13,8 @@ import { diff } from '../common' import { TABLE_MODIFY_TYPE, FIELD_MODIFY_TYPE, - INDEX_MODIFY_TYPE + INDEX_MODIFY_TYPE, + UNIQUE_MODIFY_TYPE } from '../../constant' // 解析 sql @@ -35,7 +36,10 @@ function transformSql2Json (sqls) { // 主键信息 const indexsDefinition = definitions.filter(definition => definition.resource === 'index') // 索引信息 - const primaryDefinition = definitions.find(definition => definition.resource === 'constraint') + const primaryDefinition = definitions.find(definition => definition.resource === 'constraint' && definition.constraint_type === 'primary key') + // 唯一性约束 + const uniqueDefinition = definitions.find(definition => definition.resource === 'constraint' && definition.constraint_type === 'unique') + // 构造完整字段 json const columns = columnsDefinition.map((columnDefinition) => { const name = columnDefinition.column.column @@ -46,6 +50,7 @@ function transformSql2Json (sqls) { type: columnDefinition?.definition?.dataType?.toLowerCase(), primary: !!primaryDefinition?.definition.find(primary => primary.column === name), index: !!indexsDefinition?.find(indexDefinition => indexDefinition.index === name), + unique: columnDefinition?.unique_or_primary === 'unique' || !!uniqueDefinition?.definition.find(unique => unique.column === name), nullable: columnDefinition?.nullable?.value === 'null', default: defaultVal?.value || '', comment: columnDefinition?.comment?.value?.value || '', @@ -130,7 +135,7 @@ function getTableColumnSql (column) { /** * 生成主键相关 sql - * @param {*} columns 所有的字段 + * @param {*} data 所有的字段 * @returns 主键相关 sql */ function getPrimaryKey (data) { @@ -153,17 +158,30 @@ function getIndex ({ type, data }) { } } +/** + * 生成唯一性相关 sql + */ +function getUnique ({ type, data }) { + if (type === UNIQUE_MODIFY_TYPE.DROP) { + return `INDEX \`_UNIQUE_${data.name}\`` + } + if (type === UNIQUE_MODIFY_TYPE.ADD) { + return `CONSTRAINT \`_UNIQUE_${data.name}\` UNIQUE(\`${data.name}\`)` + } +} + /** * 生成创建表的sql * @param {*} data * @returns sql字符串 */ -function createTable (data, index) { +function createTable (data, index, unique) { const { tableName, columns } = data const fields = ([ ...columns.map(getTableColumnSql), ...columns.map(getPrimaryKey), - ...index.map(getIndex) + ...index.map(getIndex), + ...unique.map(getUnique) ]).filter(v => v).map(x => ` ${x}`) return ( @@ -182,7 +200,7 @@ function createTable (data, index) { * @param {*} data * @returns sql字符串 */ -function modifyTable (data, index = []) { +function modifyTable (data, index = [], unique = []) { const { tableName, columns = [] } = data const sqlArr = [] @@ -198,6 +216,10 @@ function modifyTable (data, index = []) { const indexDetailSql = getIndex(data) sqlArr.push(`${data.type} ${indexDetailSql}`) }) + unique.forEach((data) => { + const uniqueDetailSql = getUnique(data) + sqlArr.push(`${data.type} ${uniqueDetailSql}`) + }) return ( '-- ----------------------------\n' @@ -261,14 +283,14 @@ function transformJson2Sql ({ originDatas, finalDatas }) { const diffResults = diff(originDatas, finalDatas) const sqlArray = [] - diffResults.forEach(({ type, data, originData, index }) => { + diffResults.forEach(({ type, data, originData, index, unique }) => { let sql switch (type) { case TABLE_MODIFY_TYPE.CREATE: - sql = createTable(data, index) + sql = createTable(data, index, unique) break case TABLE_MODIFY_TYPE.MODIFY: - sql = modifyTable(data, index) + sql = modifyTable(data, index, unique) break case TABLE_MODIFY_TYPE.DROP: sql = dropTable(data) diff --git a/lib/shared/data-source/helper.js b/lib/shared/data-source/helper.js index 49efb9e90..64bab4373 100644 --- a/lib/shared/data-source/helper.js +++ b/lib/shared/data-source/helper.js @@ -20,7 +20,8 @@ import { } from './index' import { uuid, - isEmpty + isEmpty, + splitSql } from '../util' import { API_METHOD, @@ -32,7 +33,8 @@ import { CONDITION_TYPE, CONNECT_TYPE_LIST, ORDER_TYPE, - ORM_KEYS + ORM_KEYS, + DATA_FILE_TYPE } from './constant' /** @@ -45,7 +47,7 @@ import { export const generateExportStruct = (tables, fileType, name) => { const dataParse = new DataParse() const structJsonParser = new StructJsonParser(tables) - if (fileType === 'sql') { + if (fileType === DATA_FILE_TYPE.SQL) { const structSqlParser = new StructSqlParser() const content = dataParse.import(structJsonParser).export(structSqlParser) return [{ content, name }] @@ -71,7 +73,7 @@ export const generateExportStruct = (tables, fileType, name) => { export const generateExportDatas = (datas, fileType, name) => { const dataParse = new DataParse() const dataJsonParser = new DataJsonParser(datas) - if (fileType === 'sql') { + if (fileType === DATA_FILE_TYPE.SQL) { const dataSqlParser = new DataSqlParser() const content = dataParse.import(dataJsonParser).export(dataSqlParser) return [{ content, name }] @@ -97,7 +99,7 @@ export const handleImportStruct = (files, fileType) => { const dataParse = new DataParse() const structJsonParser = new StructJsonParser() let tableStructs = [] - if (fileType === 'sql') { + if (fileType === DATA_FILE_TYPE.SQL) { tableStructs = dataParse .set(new StructSqlParser(files)) .export(structJsonParser) @@ -116,14 +118,21 @@ export const handleImportStruct = (files, fileType) => { * @param {*} files 导入的文件 * @param {*} fileType 文件类型 * @param {*} columns 字段信息 - * @returns 数据 json + * @returns 数据 json or sql */ export const handleImportData = (files, fileType, columns) => { const dataParse = new DataParse() const dataJsonParser = new DataJsonParser() - if (fileType === 'sql') { - const dataSqlParser = new DataSqlParser(files, columns) - return dataParse.set(dataSqlParser).export(dataJsonParser) + if (fileType === DATA_FILE_TYPE.SQL) { + // 检测 sql 内容 + files.forEach(file => { + const sqls = splitSql(file.content) + if (sqls.some((sql) => !/^(\n)?(insert|update|delete) /i.test(sql))) { + throw new Error('数据导入SQL,仅支持 INSERT、UPDATE、DELETE') + } + }) + // 数据导入功能由用户自己写 sql + return files } else { const dataXlsxParser = new DataXlsxParser(files) return dataParse.set(dataXlsxParser).export(dataJsonParser) @@ -141,6 +150,7 @@ export const getDefaultJson = () => { primary: false, index: false, nullable: false, + unique: false, default: '', comment: '', generated: false, @@ -172,6 +182,7 @@ export const normalizeJson = (item) => { normalizedItem.scale = 0 normalizedItem.length = 65535 normalizedItem.index = false + normalizedItem.unique = false break case 'date': case 'datetime': @@ -182,6 +193,8 @@ export const normalizeJson = (item) => { case 'json': normalizedItem.scale = 0 normalizedItem.length = 1024 * 1024 * 1024 + normalizedItem.index = false + normalizedItem.unique = false break default: break diff --git a/lib/shared/no-code/constant.js b/lib/shared/no-code/constant.js index 403f38b41..6e8acfe08 100644 --- a/lib/shared/no-code/constant.js +++ b/lib/shared/no-code/constant.js @@ -111,6 +111,20 @@ export const FIELDS_TYPES = [ name: '评分', default: 0, comp: 'Rate' + }, + { + type: 'COMPUTE', + name: '计算控件', + default: '--', + comp: 'Compute', + icon: 'icon bk-drag-icon bk-drag-fc-count' + }, + { + type: 'SERIAL', + name: '自动编号', + default: '--', + comp: 'Serial', + icon: 'icon bk-drag-icon bk-drag-liebiao' } ] diff --git a/lib/shared/no-code/helper.js b/lib/shared/no-code/helper.js index 69cf48fa2..f2a0d7b9a 100644 --- a/lib/shared/no-code/helper.js +++ b/lib/shared/no-code/helper.js @@ -28,7 +28,9 @@ export const transformNCJson2LCJson = (ncJson) => { TABLE: 'json', DESC: 'varchar', IMAGE: 'varchar', - RATE: 'int' + RATE: 'int', + COMPUTE: 'varchar', + SERIAL: 'varchar' } return ncJson.reduce((acc, cur) => { diff --git a/lib/shared/no-code/validate.js b/lib/shared/no-code/validate.js index be8c921c6..41cd72a23 100644 --- a/lib/shared/no-code/validate.js +++ b/lib/shared/no-code/validate.js @@ -1,5 +1,6 @@ -const dayjs = require('dayjs') +import { MoreThanOrEqual } from 'typeorm' +const dayjs = require('dayjs') // 有校验规则的组件类型 const hasValidateRulesType = ['INT', 'DATE', 'DATETIME', 'STRING', 'TEXT'] @@ -58,6 +59,7 @@ const ruleRegexMap = { 'IP': /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/g, 'QQ': /^[1-9][0-9]{4,10}$/g } +let validateDataMsg = { result: true, errorMsg: '' } const isPassValidate = (regexKey, value) => { // 重置正则得 lastIndex @@ -86,6 +88,89 @@ const returnValidataMsg = (regexKey) => { } } +function setSerialFielVlaue (serialNumber, serialRules, fieldData, data) { + fieldData.value = serialRules.map(item => { + if (item.type === 'formField') { + const formField = data.find((formValue) => { + return formValue.key === item.configValue + }) || {} + if (formField.choice?.length) { + const values = formField.value.split(',') + return formField.choice.filter((choiceItem) => { + return values.includes(choiceItem.key) + }).map((choiceItem) => { + return choiceItem.name + }).join(',') + } + return formField.value + } else if (item.type === 'serialNumber') { + return setSerialNumber(serialNumber, item.configValue) + } + return item.serialValue + }).join('-') +} +// 查询周期内的表单数据 +async function queryofrmDataList (resetCycle, formData, dataService) { + let serialNumber = 0 + // 查询数据添加时间大于等于presentCycleStartDateTime的表单数据 + const dateTime = getResetCycleDate(resetCycle) + const leng = await dataService.count(formData.tableName, { + createTime: MoreThanOrEqual(dayjs(dateTime).format()) + }) + if (leng) { + serialNumber = leng + 1 + } + return serialNumber +} +// 获取周期时间 +function getResetCycleDate (resetCycle) { +// 当前周期开始时间 + let presentCycleStartDateTime = 0 + const presentYear = new Date().getFullYear() + const presentMonth = new Date().getMonth() + 1 + let presentWeek = new Date().getDay() + const presentDay = new Date().getDate() + + if (presentWeek === 0) { + // 由于周期要求认定一周的开始是从周一开始的,所以周日时需要减7 + presentWeek = -7 + } + // 按重置周期查询当前周期内数据的条数,有数据则流水号延续,没有则使用初始值为流水号 + switch (resetCycle) { + case 'year': + presentCycleStartDateTime = new Date(`${presentYear}/01/01`).getTime() + break + case 'month': + presentCycleStartDateTime = new Date(`${presentYear}/${presentMonth}/01`).getTime() + break + case 'week': + presentCycleStartDateTime = new Date(presentYear, presentMonth, presentDay - presentWeek + 1).getTime() + break + case 'day': + presentCycleStartDateTime = new Date(`${presentYear}/${presentMonth}/${presentDay}`).getTime() + break + default: + break + } + return presentCycleStartDateTime +} +// 获取流水号 +async function getSerialNumber (resetCycle, formData, dataService) { + const num = await queryofrmDataList(resetCycle, formData, dataService) + return num +} +// 获取流水号 +function setSerialNumber (serialNumber, number) { + const serialNumberStr = `${serialNumber}` + if (number > serialNumberStr.length) { + serialNumber = serialNumberStr.padStart(number, '0') + } else if (number < serialNumberStr.length) { + validateDataMsg = { + result: false, errorMsg: '当前周期内编号位数已超过流水号所设置的位数值' + } + } + return serialNumber +} // const isValueInOption = (field, val) => { // // 无值的时候 默认为true(必填在提交的时候校验) // console.log(field.choice.map(item => item.key), val) @@ -96,13 +181,18 @@ const returnValidataMsg = (regexKey) => { * @param {[]fields} fields 字段列表 * @returns validateDataMsg {result:'' , errorMsg:''} */ -export const validateData = (fields, data) => { - let validateDataMsg = { result: true, errorMsg: '' } +export const validateData = async (fields, data, formData, dataService) => { const len = fields.length for (let i = 0; i < len; i++) { const field = fields[i] - const val = data.find(item => item.key === field.key)?.value || '' - const { type, regex } = field + const fieldData = data.find(item => item.key === field.key) + const val = fieldData?.value || '' + const { type, regex, meta } = field + if (type === 'SERIAL' && meta.serial_config_info) { + const { resetCycle, initNumber, serialRules } = meta.serial_config_info + const serialNumber = await getSerialNumber(resetCycle, formData, dataService) || initNumber + setSerialFielVlaue(serialNumber, serialRules, fieldData, data) + } if (hasValidateRulesType.includes(type) && regex !== 'EMPTY') { const value = type === 'INT' ? Number(val) : val if (!timeValidateKeys.includes(regex)) { diff --git a/lib/shared/page-code/template/page/prop.js b/lib/shared/page-code/template/page/prop.js index dee87e869..c5feee27d 100644 --- a/lib/shared/page-code/template/page/prop.js +++ b/lib/shared/page-code/template/page/prop.js @@ -204,7 +204,8 @@ function getPropsStr (code, type, compId, props, dirProps, slots) { if (valueType === 'string') vModelValue = `'${props['value'].code}'` code.dataTemplate(modelComId, vModelValue) } - propsStr += `v-model="${modelComId}"` + // 当value为变量时,v-model直接绑定相应的变量; 否则要绑定默认生成的变量 + propsStr += `v-model="${props['value'].format === 'variable' ? props['value'].code : modelComId}"` } } diff --git a/package.json b/package.json index 49355718f..56da34205 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lesscode", - "version": "1.0.8", + "version": "1.0.9", "description": "", "main": "index.js", "scripts": { @@ -82,6 +82,7 @@ "@toast-ui/vue-editor": "3.1.8", "@typescript-eslint/eslint-plugin": "^5.40.0", "@typescript-eslint/parser": "^5.40.0", + "@vant/touch-emulator": "^1.4.0", "@vue/composition-api": "^1.7.1", "@vue/eslint-config-typescript": "^7.0.0", "acorn": "~8.5.0", diff --git a/tsconfig.json b/tsconfig.json index 7b25bfb04..e1ad1ef58 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,6 @@ "noImplicitThis": true, "strictNullChecks": false, "removeComments": true, - "suppressImplicitAnyIndexErrors": true, "allowSyntheticDefaultImports": true, "allowJs": true, "resolveJsonModule": true,