This commit is contained in:
2025-10-17 10:31:13 +08:00
commit e6e86f2ce0
1043 changed files with 1031839 additions and 0 deletions

View File

@@ -0,0 +1,535 @@
<template>
<ContentWrap>
<div class="flex gap-x-10px">
<div class="flex-basis-180px flex-shrink-0">
<avue-tree
ref="treeRef"
v-model="treeForm"
:option="treeOption"
:data="treeData"
:permission="treePermission"
:before-open="treeBeforeOpen"
@node-contextmenu="treeNodeContextmenu"
@node-click="treeNodeClick"
@update="treeUpdate"
@save="treeSave"
@del="treeDel"
>
</avue-tree>
</div>
<avue-crud
class="table-content flex-1"
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
@selection-change="selectionChange"
>
<template #menu-left="{ size }">
<el-button
:size="size"
type="danger"
:disabled="!tableSelect.length"
v-hasPermi="['jeelowcode:desform:delete']"
@click="rowDel(selectIds)"
>
<Icon :size="16" icon="mi:delete" class="mr-3px" />批量删除
</el-button>
</template>
<template #menu="{ size, row }">
<div class="flex justify-center flex-items-center">
<el-button
type="primary"
text
:size="size"
@click="menuHandle({ type: 'form', row })"
v-hasPermi="['jeelowcode:desform:update']"
>
<Icon :size="14" icon="ep:edit-pen"></Icon>
<span>设计表单</span>
</el-button>
<span
v-if="
checkPermi(['jeelowcode:desform:update']) &&
checkPermi([
'jeelowcode:desform:create',
'jeelowcode:desform:update',
'jeelowcode:desform:delete'
])
"
class="ml-8px mr-8px mt-2px inline-block h-16px w-1px bg-#e8e8e8 .dark:bg-[var(--el-border-color-dark)]"
></span>
<el-dropdown
@command="menuHandle"
v-if="
checkPermi([
'jeelowcode:desform:create',
'jeelowcode:desform:update',
'jeelowcode:desform:delete'
])
"
>
<div class="mt--2px cursor-pointer">
<el-text :size="size" type="primary">
更多
<Icon :size="16" icon="iconamoon:arrow-down-2-light" />
</el-text>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-if="checkPermi(['jeelowcode:desform:update'])"
:command="{ type: 'edit', row }"
>
编辑
</el-dropdown-item>
<el-dropdown-item
:command="{ type: 'copy', row }"
v-if="checkPermi(['jeelowcode:desform:create'])"
>
复制表
</el-dropdown-item>
<el-dropdown-item :command="{ type: 'routeAddress', row }">
路由地址
</el-dropdown-item>
<el-dropdown-item
v-if="row.isOpen == 'Y'"
:command="{ type: 'externalAddress', row }"
>
外部访问链接
</el-dropdown-item>
<el-dropdown-item
v-if="checkPermi(['jeelowcode:desform:update'])"
:command="{ type: 'formUnlock', row }"
>
解除锁定
</el-dropdown-item>
<el-dropdown-item
v-if="checkPermi(['jeelowcode:desform:delete'])"
:command="{ type: 'del', row }"
>
删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</avue-crud>
</div>
<DesignPopup v-model="designPop.dialog" v-bind="designPop.params" :isBodyFull="true">
<template #default>
<LowFormDesign :formDesignData="designPop.value" :isShow="designPop.dialog"></LowFormDesign>
</template>
</DesignPopup>
</ContentWrap>
</template>
<script lang="ts" setup>
import * as FormApi from '@/api/design/form'
import { ElLoading } from 'element-plus'
import { LowFormDesign } from '@/components/LowFormDesign/index'
import { cloneDeep } from 'lodash-es'
import { useGroup } from '@/hooks/design/useGroup'
import { checkPermi } from '@/utils/permission'
import { ElButton } from 'element-plus'
import useCopyText from '@/hooks/design/useCopyText'
defineOptions({ name: 'FormDesign' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const { copyText } = useCopyText()
const loading = ref(true) // 列表的加载中
//表格配置
const tableOption = reactive({
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
selection: true,
dialogWidth: 800,
dialogFullscreen: false,
editBtn: false,
delBtn: false,
border: true,
index: false,
menuWidth: 180,
span: 12,
column: {
id: {
label: '表单id',
display: false,
search: true
},
desformName: {
label: '表单名称',
search: true,
rules: [{ required: true, message: '请输入表单名称', trigger: 'blur' }]
},
groupDesformId: {
label: '分组类型',
type: 'tree',
value: '',
dicData: [],
filterable: true,
defaultExpandAll: true,
props: { label: 'name', value: 'id' },
hide: true
},
updateTime: {
label: '修改时间',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
display: false
},
isOpen: {
label: '外部链接访问',
type: 'select',
width: 110,
search: true,
searchLabelWidth: 110,
value: 'N',
dicData: [
{ label: '禁用', value: 'N' },
{ label: '启用', value: 'Y' }
]
},
isTemplate: {
label: '是否作为模板',
type: 'select',
width: 110,
search: true,
searchLabelWidth: 110,
value: 'N',
dicData: [
{ label: '是', value: 'Y' },
{ label: '否', value: 'N' }
]
}
}
})
const tableForm = ref<any>({})
const tableData = ref([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const tableSelect = ref([])
const designPop = ref({
value: {},
dialog: false,
params: {}
})
const crudRef = ref()
const treeRef = ref()
const isUnload = ref(false)
const selectIds = computed(() => {
return tableSelect.value.map((item) => item['id'])
})
const permission = getCurrPermi(['jeelowcode:desform'])
useCrudHeight(crudRef)
const {
treeForm,
treeOption,
treeData,
groupValue,
treePermission,
treeBeforeOpen,
treeNodeContextmenu,
treeNodeClick,
getTreeData,
treeUpdate,
treeSave,
treeDel
} = useGroup(treeRef, FormApi, () => resetChange())
const menuHandle = async ({ type, row }) => {
if (type == 'edit') crudRef.value.rowEdit(row)
else if (type == 'form') openFormDesing(row)
else if (type == 'copy') copyForm(row)
else if (type.indexOf('Address') != -1) showAddress(type, row)
else if (type.indexOf('Unlock') !== -1) unlockForm(type, row)
else if (type == 'del') rowDel(row)
}
const showAddress = (type, row) => {
let title = '菜单路由地址'
let urlType = 'view'
if (type == 'externalAddress') urlType = 'external'
let htmlArr = [
{ label: `表单填写(默认值启用)`, value: `form/${urlType}/fillout/${row.id}` },
{ label: `表单填写(默认值禁用)`, value: `form/${urlType}/filloutNo/${row.id}` },
{ label: `表单详情查看`, value: `form/${urlType}/detail/${row.id}` }
]
if (type == 'externalAddress') {
htmlArr = htmlArr.map((item) => {
item.value = `${window.location.origin}/${item.value}`
return item
})
title = '外部链接访问地址'
}
let list: VNode[] = []
htmlArr.forEach((item) => {
list.push(
h('div', { style: { marginBottom: '10px', border: ' 1px solid #eee', padding: '10px' } }, [
h('div', [
h('span', { style: { fontWeight: 600, fontSize: '14px' } }, item.label + ''),
h(
ElButton,
{ size: 'small', type: 'primary', onClick: () => copyText(item.value) },
() => '复制'
)
]),
h('div', { style: { fontSize: '12px' } }, item.value)
])
)
})
message.alert('', title, {
message: () => {
return h('div', { width: '100%' }, list)
},
confirmButtonText: '关闭',
dangerouslyUseHTMLString: true,
customStyle: { width: '100%' },
customClass: 'form-design-address-box'
})
}
const openFormDesing = async (row) => {
loading.value = true
const data = await FormApi.getFormDetail({ desFormId: row.id, lock: true })
loading.value = false
if (data.timeStr) {
message
.confirm(
`<div>
<div style="font-size:16px"><span style="font-size:14px">当前操作人:</span>${data.userName}</div>
<div style="font-size:16px"><span style="font-size:14px">开始操作时间:</span>${data.timeStr}</div>
<div style="color:#E6A23C">注:如果确认没有人正在编辑,可在操作列更多里面解除锁定</div>
</div>`,
'当前表单已锁定',
{
dangerouslyUseHTMLString: true,
cancelButtonText: '关闭',
confirmButtonText: '强制解锁'
}
)
.then(async () => menuHandle({ type: `formUnlockOpen`, row }))
return
}
isUnload.value = true
designPop.value = {
value: data,
dialog: true,
params: {
title: `表单设计 【${row.desformName}】(${row.id}`,
fullscreen: true,
width: '90%',
handleClose: async (done) => {
await FormApi.unlockForm(row.id)
isUnload.value = false
done()
}
}
}
}
const copyForm = (row) => {
message
.prompt('新表单名称', '复制表单', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: '',
inputValidator: (value) => {
if (!value) return '请输入表单名称'
}
})
.then(async ({ value }) => {
loading.value = true
const data = await FormApi.getFormDetail({ desFormId: row.id })
data.desformName = value
const oldId = data.id
const loadingFun = async (id?) => {
const hideLoading = () => {
loading.value = false
}
if (id) {
const desform = JSON.parse(data.desformJson)
if (desform.scssEnhance) {
const rexp = new RegExp(oldId, 'g')
desform.scssEnhance = desform.scssEnhance.replace(rexp, id)
rowUpdate(
{ id, desformJson: JSON.stringify(desform) },
row.$index,
hideLoading,
hideLoading,
true
)
}
} else hideLoading()
}
delete data.id
rowSave(data, loadingFun, loadingFun, true)
})
}
const unlockForm = (type, row) => {
message.confirm('是否确认当前表单没有其他人在编辑', '提示').then(async () => {
loading.value = true
await FormApi.unlockForm(row.id)
loading.value = false
message.success('解锁成功')
if (type.indexOf('Open') !== -1) menuHandle({ type: `form`, row })
})
}
const selectionChange = (data) => {
tableSelect.value = data
}
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
if (groupValue.value) searchObj['groupDesformId'] = groupValue.value
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await FormApi.getFormList(searchObj)
tableData.value = data.records
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
if (Object.keys(params).length && groupValue.value) {
treeRef.value.setCurrentKey(0)
groupValue.value = 0
}
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
const groupData = cloneDeep(treeData.value)
tableOption.column.groupDesformId.dicData = groupData[0].children
if (['edit', 'view'].includes(type) && tableForm.value['id']) {
loading.value = true
const data = await FormApi.getFormDetail({ desFormId: tableForm.value['id'] })
tableForm.value = data
loading.value = false
}
if (type == 'add' && groupValue.value) {
tableForm.value.groupDesformId = groupValue.value
}
done()
}
/** 新增操作 */
const rowSave = async (form: object, done, loading, isId?) => {
const elLoading = ElLoading.service({ fullscreen: true })
let bool = await FormApi.saveFormData(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
if (isId) done(bool)
else done()
} else loading()
elLoading.close()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading, onData?) => {
let bool = await FormApi.updateFormData(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
if (!onData) getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (data) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await FormApi.deleteFormData(data instanceof Array ? data : [data.id])
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
const beforeUnload = (event) => {
if (isUnload.value) return (event.returnValue = '您确定要关闭页面吗?')
}
/** 初始化 **/
onMounted(async () => {
window.addEventListener('beforeunload', beforeUnload)
getTableData()
getTreeData()
})
onBeforeUnmount(() => {
window.removeEventListener('beforeunload', beforeUnload)
})
</script>
<style lang="scss" scoped>
.table-content {
max-width: calc(100% - 190px);
}
</style>
<style lang="scss">
.form-design-address-box {
.el-message-box__container {
display: block;
}
}
</style>

View File

@@ -0,0 +1,157 @@
<template>
<div v-loading="loading">
<div
v-if="isOpen"
class="form-external-box w-100% h-100vh py-20px box-border flex flex-col items-center"
>
<div
v-if="isInit && isAuth"
class="pos-relative w-80% b-1px b-[var(--el-border-color-light)] b-solid rd-4px bg-#fff"
>
<div
class="external-title text-20px py-10px text-center b-b-1px b-b-[var(--el-border-color-light)] b-b-solid"
>
{{ externalTitle }}
</div>
<div class="max-h-[calc(100vh-90px)] overflow-y-auto">
<div v-if="isInit" :class="formType !== 'view' ? 'pb-53px' : ''">
<LowForm
ref="formRef"
:form-id="formId"
:form-type="formType"
:table-id="tableId"
handle-type="default"
:form-option="formOption"
:default-is-open="isOpen"
></LowForm>
</div>
<div v-else style="height: 200px"> </div>
</div>
<div
v-if="formType !== 'view' && (btnObj.submit || btnObj.empty)"
class="w-100% pos-absolute left-0% bottom-0 py-10px bg-#fff text-center b-t-1px b-t-[var(--el-border-color-light)] b-t-solid rd-b-4px"
>
<el-button type="primary" v-if="btnObj.submit" @click="handleBtnClick('submit')">
<Icon icon="ep:check"></Icon>
<span>提交</span>
</el-button>
<el-button v-if="btnObj.empty" @click="handleBtnClick('close')">
<Icon icon="ep:close"></Icon>
<span>清空</span>
</el-button>
</div>
</div>
<div v-else class="text-20px mt-50px">无权限访问</div>
</div>
<div v-else>
<ContentWrap>
<LowForm
v-if="isInit"
ref="formRef"
class="py-10px!"
:form-id="formId"
:form-type="formType"
:table-id="tableId"
:form-option="formOption"
:before-close="beforeClose"
></LowForm>
</ContentWrap>
</div>
</div>
</template>
<script setup lang="ts">
import * as FormApi from '@/api/design/form'
const route = useRoute()
const message = useMessage()
const loading = ref(false)
const isInit = ref(false)
const isOpen = ref(false)
const isAuth = ref(true)
const formId = ref('')
const formType = ref<any>('')
const tableId = ref('')
const formOption = ref({})
const btnObj = { submit: false, empty: false }
const externalTitle = ref('')
const formRef = ref()
const resetForm = () => {
loading.value = true
isInit.value = false
setTimeout(() => {
loading.value = false
isInit.value = true
}, 300)
}
const handleBtnClick = async (type) => {
if (type == 'submit') {
loading.value = true
const data = await formRef.value.handleSubmit(true, () => {
loading.value = false
})
if (data && tableId.value) beforeClose(type)
} else formRef.value.handleClear()
}
const beforeClose = (type, form?, loading?) => {
if (loading) loading()
if (type == 'submit') resetForm()
}
onMounted(async () => {
loading.value = true
isOpen.value = route.path.indexOf('external') !== -1
const pathList = route.path.split('/')
const length = pathList.length - 1
if (/\d$/.test(pathList[length])) {
formId.value = pathList[length]
switch (pathList[length - 1]) {
case 'fillout':
formType.value = 'add'
break
case 'filloutNo':
formType.value = 'edit'
break
case 'detail':
formType.value = 'view'
break
}
const data = await FormApi[`${isOpen.value ? 'getOpenFormDetail' : 'getFormDetail'}`]({
desFormId: formId.value
}).catch(() => false)
if (!data || (isOpen.value && data.isOpen != 'Y')) return (loading.value = false)
const option = JSON.parse(data.desformJson || `{}`)
option.i18nData = JSON.parse(data.i18nData || `{}`)
tableId.value = option.tableDesignId || ''
if (isOpen.value) {
externalTitle.value = option.externalTitle || data.desformName
btnObj.submit = option.submitBtn
btnObj.empty = option.emptyBtn
option.submitBtn = false
option.emptyBtn = false
}
formOption.value = option
loading.value = false
isInit.value = true
}
})
</script>
<style lang="scss" scoped>
.form-external-box {
background-attachment: scroll;
background-color: rgb(236 239 244 / 100%);
background-image: url('@/assets/svgs/external/external_bg.svg');
background-position: center center;
background-repeat: no-repeat;
background-size: 103%;
background-origin: border-box;
.external-title {
font-family: '微软雅黑', sans-serif;
}
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<div class="control-word-content">
<table cellspacing="0" cellpadding="0" style="width: 100%">
<tbody v-for="(control, index) in tbodyList" :key="index">
<tr>
<td colspan="2" class="title">
<span>{{ control.title }}</span>
</td>
</tr>
<tr v-for="(tr, trIndex) in control.list" :key="trIndex">
<template v-if="props.type == 'simple'">
<td>
<div class="flex flex-wrap items-center">
<div class="value" v-if="tr.value">
<el-tooltip v-if="tr.tip" :content="tr.tip">
{{ valueStyle == 'simple' ? tr.value : ` ${tr.value} ` }}
</el-tooltip>
<template v-else>
{{ valueStyle == 'simple' ? tr.value : ` ${tr.value} ` }}
</template>
</div>
<div class="text-12px" :class="{ 'pl-5px': valueStyle == 'simple' }" v-if="tr.label">
{{ tr.label }}
</div>
</div>
</td>
</template>
<template v-else>
<td v-for="(td, tdIndex) in tr" :key="tdIndex" :colspan="td.colspan">
<div class="flex flex-wrap">
<span class="value" v-if="td.value">
<el-tooltip v-if="td.tip" :content="td.tip">
{{ valueStyle == 'simple' ? td.value : ` ${td.value} ` }}
</el-tooltip>
<template v-else>
{{ valueStyle == 'simple' ? td.value : ` ${td.value} ` }}
</template>
</span>
<span :class="{ 'pl-5px': valueStyle == 'simple' }" v-if="td.label">
{{ td.label }}
</span>
</div>
</td>
</template>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import controlParams from '@/components/LowDesign/src/utils/tipView'
defineOptions({ name: 'TipView' })
interface Props {
type?: 'default' | 'simple'
valueStyle?: 'default' | 'simple'
tipKeyList?: Array<string>
width?: string
}
const props = defineProps<Props>()
const tbodyList = computed(() => {
const list = [] as any
if (props.tipKeyList) {
props.tipKeyList.forEach((key) => {
list.push(controlParams[key])
})
}
return list
})
</script>
<style lang="scss" scoped>
.control-word-content {
padding: 5px 10px;
overflow-y: auto;
box-sizing: border-box;
table {
width: 100%;
border-collapse: collapse;
}
td {
padding: 6px 8px;
text-align: left;
border: 1px solid black;
.value {
font-size: 16px;
color: #409eff;
}
&.title {
font-size: 16px;
color: #fff;
text-align: center;
background-color: #409eff;
}
}
}
.dark .control-word-content {
td {
border-color: var(--el-border-color-dark) !important;
}
}
</style>

View File

@@ -0,0 +1,450 @@
<template>
<div class="verify-option">
<el-container class="h-100%">
<el-aside width="220px" class="left-tree">
<div class="verify-title">
<span>校验类型</span>
</div>
<div class="verify-draggable">
<draggable
class="verify-content"
tag="div"
:list="verifyTypeList"
:group="{ name: 'config', pull: 'clone', put: false }"
ghost-class="verify-ghost"
:sort="false"
item-key="value"
>
<template #item="{ element }">
<div class="verify-item" @click="setOption(element)">
<span>{{ element.label }}</span>
</div>
</template>
</draggable>
</div>
</el-aside>
<el-main class="main-option">
<div class="option-title table-item-row">
<div class="row-item">
<div class="cell">序号</div>
</div>
<div class="row-item text-center">
<div class="cell">校验类型</div>
</div>
<div class="row-item">
<div class="cell">校验表达式</div>
</div>
<div class="row-item">
<div class="cell">启用状态</div>
</div>
<div class="row-item">
<div class="cell">操作</div>
</div>
</div>
<div class="option-content">
<draggable
class="content-draggable"
:list="verifyList"
:group="{ name: 'option', put: true }"
ghost-class="option-ghost"
:animation="300"
handle=".move-box"
item-key="prop"
@add="handleAddColumn"
>
<template #item="{ element, index }">
<div class="option-item table-item-row mt--2px">
<div class="row-item move-box">
<div class="cell">{{ index + 1 }}</div>
</div>
<div class="row-item move-box">
<div class="cell">
<avue-text-ellipsis
:key="element.value"
:text="element.label"
:height="40"
:width="140"
use-tooltip
placement="top"
>
<template #more>
<small>...</small>
</template>
</avue-text-ellipsis>
</div>
</div>
<div class="row-item">
<div class="cell">
<template v-if="element.controlType == 'length'">
<div class="flex items-center">
<span>最小长度</span>
<avue-input-number
class="flex-basis-90px flex-shrink-0"
v-model="element.leng_min"
></avue-input-number>
<span class="ml-10px">最大长度</span>
<avue-input-number
class="flex-basis-90px flex-shrink-0"
v-model="element.leng_max"
></avue-input-number>
<span class="ml-10px">输入类型</span>
<avue-select
class="flex-basis-100px flex-shrink-0"
v-model="element.leng_type"
:dic="lengTypeDic"
></avue-select>
</div>
</template>
<template v-else-if="element.controlType == 'regExp'">
<div class="flex items-center">
<span>正则</span>
<avue-input
class="reg-exp-input flex-1"
v-model="element.regExp"
:placeholder="element.tip"
prepend="/"
append="/"
></avue-input>
<span class="ml-10px">失败提示语</span>
<avue-input
class="flex-basis-120px flex-shrink-0"
v-model="element.msg"
></avue-input>
</div>
</template>
<template v-else-if="element.controlType == 'MEditor'">
<avue-input
v-model="element.customStr"
readonly
@click="openEditor(index)"
></avue-input>
</template>
<template v-else-if="patternObj[element.type] || element.tip">
<div class="h-32px line-height-32px">
{{
patternObj[element.type] ||
(isSub ? element.subTip || element.tip : element.tip)
}}
</div>
</template>
</div>
</div>
<div class="row-item">
<div class="cell">
<avue-switch v-model="element.display" :dic="switchDic"></avue-switch>
</div>
</div>
<div class="row-item">
<div class="cell">
<el-button type="danger" link text @click="delRow(element)"> 删除 </el-button>
</div>
</div>
</div>
</template>
</draggable>
</div>
</el-main>
</el-container>
<DesignPopup v-model="MEDialog.value" v-bind="MEDialog.params" :isBodyFull="true">
<template #default>
<MonacoEditor v-model="MEData.value" v-bind="MEData.params"></MonacoEditor>
</template>
</DesignPopup>
</div>
</template>
<script setup lang="ts">
import draggable from 'vuedraggable'
import { cloneDeep } from 'lodash-es'
import { rulesVerify, patternObj } from '@/components/LowDesign/src/utils/verifyOption'
import useMEDialog from '@/hooks/design/useMEDialog'
defineOptions({ name: 'VerifyOption' })
interface Props {
show: boolean
isSub?: boolean
filterKey?: string[]
}
const props = defineProps<Props>()
const verifyStr = defineModel<string>({ default: '' })
const message = useMessage() // 消息弹窗
const { MEDialog, MEData, openMEDialog } = useMEDialog()
const switchDic = [
{ label: '', value: false },
{ label: '', value: true }
]
const lengTypeDic = [
{ label: '任意字符', value: 'all' },
{ label: '数字', value: 'number' },
{ label: '字母', value: 'alphabet' },
{ label: '中文', value: 'chinese' }
]
const verifyList = ref<any>([])
const verifyTypeList = computed(() => {
if (props.filterKey) {
return rulesVerify.filter((item) => !props.filterKey?.includes(item.type))
}
return rulesVerify
})
const onlyKeyList = computed(() => {
return verifyList.value
.filter((item) => !['customRegExp', 'customRule'].includes(item.type) && item.prop)
.map((item) => item.type)
})
const openEditor = (index) => {
openMEDialog(
{
prop: 'customStr',
label: '自定义校验规则',
params: {
width: '50%'
}
},
verifyList.value[index]
)
}
const initFun = () => {
verifyList.value = []
if (verifyStr.value) verifyList.value = JSON.parse(verifyStr.value)
}
watch(
() => props.show,
(val) => {
if (val) initFun()
}
)
const handleAddColumn = (e) => {
const newIndex = e.newIndex
const data = cloneDeep(verifyList.value[newIndex])
if (onlyKeyList.value.includes(data.type)) {
verifyList.value = verifyList.value.filter((item) => item.prop)
message.info('该校验类型已存在')
return
}
if (data.type == 'leng') data.leng_type = 'all'
if (data.type == 'customRule')
data.customStr = `return {
validator: (rule, value, callback) => {
if (!value) {
callback(new Error('值不能为空')) //校验失败
} else {
callback() //校验成功
}
},
trigger: 'blur'
}`
if (!data.prop) data.prop = `option_${Math.ceil(Math.random() * 9999999)}`
data.display = true
verifyList.value[newIndex] = data
}
const setOption = (row) => {
verifyList.value.push(row)
handleAddColumn({ newIndex: verifyList.value.length - 1 })
setTimeout(() => {
getOptionStr()
}, 300)
}
const delRow = (row) => {
verifyList.value = verifyList.value.filter((item) => item.prop != row.prop)
}
const getOptionStr = () => {
if (verifyList.value.length) return JSON.stringify(verifyList.value)
else ''
}
onMounted(() => {
initFun()
})
defineExpose({ getOptionStr })
</script>
<style lang="scss" scoped>
.verify-option {
width: 100%;
height: 100%;
margin-top: -1px;
.left-tree {
padding: 10px;
border: 1px solid #f1f1f1;
.verify-title {
display: flex;
align-items: center;
margin-bottom: 10px;
font-size: 14px;
font-weight: 600;
color: #333;
cursor: pointer;
.el-icon {
color: #666;
}
}
&:nth-last-of-type(1) {
margin-bottom: 0;
}
.verify-draggable {
margin-left: 10px;
}
.verify-item {
width: 160px;
padding: 4px 8px;
margin-bottom: 10px;
font-size: 14px;
cursor: move;
background-color: #f4f6fc;
border: 1px dashed #f4f6fc;
&:hover {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
}
}
}
.main-option {
padding: 0;
border: 1px solid #f1f1f1;
border-left: 0;
.option-content {
height: calc(100% - 41px);
.content-draggable {
height: 100%;
padding-bottom: 55px;
overflow-y: auto;
box-sizing: border-box;
&::-webkit-scrollbar {
width: 0;
}
}
.option-ghost {
position: relative;
width: 0 !important;
height: 32px;
min-width: 0 !important;
padding: 0 !important;
margin: 1px 2px 0;
overflow: hidden;
font-size: 0;
background: white;
border-left: 5px solid var(--el-color-primary);
content: '';
outline: none 0;
box-sizing: border-box;
}
}
.table-item-row {
display: flex;
align-items: end;
.row-item {
height: 100%;
min-height: 32px;
padding: 4px 0;
font-size: 14px;
line-height: 32px;
border-bottom: 1px solid #dcdfe6;
.cell {
padding: 0 12px;
& > div {
width: 100%;
}
.reg-exp-input {
::v-deep(.el-input) {
.el-input-group__prepend,
.el-input-group__append {
padding: 0 8px !important;
}
}
}
}
&:nth-child(1) {
flex-basis: 60px;
text-align: center;
border-right: 1px solid #dcdfe6;
}
&:nth-child(2) {
flex-basis: 170px;
border-right: 1px solid #dcdfe6;
}
&:nth-child(3) {
flex: 1;
line-height: normal;
border-right: 1px solid #dcdfe6;
}
&:nth-child(4) {
flex-basis: 110px;
text-align: center;
border-right: 1px solid #dcdfe6;
}
&:nth-child(5) {
flex-basis: 110px;
text-align: center;
}
&.move-box {
cursor: move;
}
}
}
.option-title {
background-color: #fafafa;
.row-item:nth-child(3) {
line-height: 32px;
}
}
}
.right-custom {
.custom-title {
display: flex;
align-items: center;
justify-content: center;
height: 32px;
padding: 4px 0;
font-size: 14px;
line-height: 32px;
text-align: center;
background-color: #fafafa;
border: 1px solid #dcdfe6;
}
.custom-content {
width: 100%;
height: calc(100% - 42px);
}
}
}
</style>

View File

@@ -0,0 +1,3 @@
import TipView from './TipView.vue';
import VerifyOption from './VerifyOption.vue';
export { TipView, VerifyOption }

View File

@@ -0,0 +1,259 @@
import { VxeUI } from 'vxe-table'
import { tableInfoOption } from '../../tableDesign/designData';
import { MonacoEditor } from '@/components/MonacoEditor/index'
import { useAppStoreWithOut } from '@/store/modules/app'
const appStore = useAppStoreWithOut()
//表单开发校验
const infoTableIsEdit = ({ row, column }, { verifyEdit, noStop }) => {
let bool = true
//判断如果是 需要编辑校验并且是系统字段 不可编辑
if (verifyEdit && tableInfoOption.disabledArr.includes(row.fieldCode) && row.only) bool = false
//判断是否可以绑定字典
if (!['select', 'radio', 'checkbox', 'tree', 'cascader', 'dicTableSelect'].includes(row.controlType) && ['dictType', 'dictCode', 'dictTable', 'dictText', 'dictTextFormatter', 'dictTableColumn', 'dictTableSearch'].includes(column.field)) {
if (!noStop) bool = false
}
if (row.controlType != 'dicTableSelect' && column.field == 'dictTableColumn') bool = false
//判断如果是 系统字典 不可编辑
if (row.dictType == 'dict' && ['dictTable', 'dictText', 'dictTableColumn', 'dictTableSearch'].includes(column.field)) bool = false
//判断如果没有同步数据库 接口查询、外键 不可编辑
if (row.isDb == 'N' && ['queryIsDb', 'mainTable', 'mainField'].includes(column.field)) bool = false
//如果是树表pid的接口查询 不可编辑
if (row.fieldCode == 'pid' && row.only && column.field == 'queryIsDb') bool = false
//如果是id 接口查询 不可编辑
if (row.fieldCode == 'id' && row.only && column.field == 'queryIsDb') bool = false
//如果配置了外键 接口查询 不可编辑
if (row.mainField && row.mainField && column.field == 'queryIsDb') bool = false
//如果虚拟字段 处理方式为空 处理配置不能编辑
if (!row.virtualType && ['virtual_java_str', 'virtual_sql_str', 'virtual_fun_str'].includes(column.field)) bool = false
return bool
}
const handleLowClickInput = (row, column) => {
const prop = column.fieldProp || column.field
let text = row[prop]
if (text) {
if (['summarySql', 'virtual_sql_str', 'virtual_java_str'].includes(prop)) {
const obj = JSON.parse(text)
text = obj[obj.valueType]
if (prop == 'summarySql' && obj.valueType == 'group') {
text = text.replace('#{jeelowcode_summary_table}', '当前表的数据源')
}
if (prop == 'virtual_java_str') {
if (obj.valueType == 'group') {
text = text.map(item => `${item.type == 'CALCULATE' ? '计算' : '拼接'}${item.value} `).join(' ')
} else if (obj.valueType == 'custom') {
text = `JAVA类名/Sping Key${text.javaPath}`
}
}
}
}
return text
}
watch(() => appStore.isDark, (val) => {
if (val) VxeUI.setTheme('dark')
else VxeUI.setTheme('light')
}, { immediate: true })
export const useRenderVxeColumn = (useType = 'table') => {
if (VxeUI.renderer.get('LowInput') !== null) return
const stopIcon = <icon style="position: absolute; right: 4px; top: 2px; color: #f56c6c;" size={14} icon="uiw:stop-o" />
const lowControl = {
LowInput: {
default: (renderOpts, { row, column, fieldProp }, isStop) => {
const prop = fieldProp || column.field
if (isStop) return (<div> <span>{row[prop]}</span> {stopIcon} </div>)
return <span>{row[prop]}</span>
},
edit: (renderOpts, { row, column, fieldProp }) => {
const { placeholder } = renderOpts
const prop = fieldProp || column.field
return <el-input class="my-cell" text="text" v-model={row[prop]} placeholder={placeholder ? placeholder : '请输入 ' + column.title} />
}
},
LowNumber: {
default: (renderOpts, { row, column }, isStop) => {
if (isStop) return (<div> <span>{row[column.field]}</span> {stopIcon} </div>)
return <span>{row[column.field]}</span>
},
edit: (renderOpts, { row, column }) => {
return <div class="w-100% flex"><el-input-number v-model={row[column.field]} controls={false} placeholder={'请输入 ' + column.title} {...(renderOpts.params || {})} /></div>
}
},
LowCheckbox: {
default: (renderOpts, { row, column }, isStop) => {
if (isStop) return (<div> <span>{row[column.field] == 'Y' ? '√' : ''}</span> {stopIcon} </div>)
return <span>{row[column.field] == 'Y' ? '√' : ''}</span>
},
edit: (renderOpts, { row, column }) => {
return <el-checkbox v-model={row[column.field]} true-value="Y" false-value="N" />
}
},
LowSelect: {
default: (renderOpts, { row, column }, isStop = false) => {
if (column.field == 'dictType' && !row[column.field] && infoTableIsEdit({ row, column }, renderOpts)) {
return <div class="c-#ccc text-12px"></div>
}
const { typeKey } = renderOpts
let dicObj = {}
if (typeKey && row[typeKey]) {
dicObj = renderOpts[`${row[typeKey]}DicObj`] || {}
if (row[typeKey] == 'table' && column.field != 'dictTable' && row.dictTable) dicObj = dicObj[row.dictTable] || {}
} else if (column.field == 'mainField') {
dicObj = renderOpts.dicObj || {}
if (row.mainTable) dicObj = dicObj[row.mainTable] || {}
} else if (!typeKey) dicObj = renderOpts['dicObj'] || {}
let value = row[column.field]
if (value === '' || value === undefined || value === null) value = []
if (typeof value == 'string') value = [value]
if (value.length && Object.keys(dicObj).length) {
const text: Array<string> = []
value.forEach(key => text.push(dicObj[key] || key))
value = text.join(' | ')
}
if (isStop) return (<div> <span>{value}</span> {stopIcon} </div>)
return <span>{value}</span>
},
edit: (renderOpts, { row, rowIndex, column }) => {
const { multiple, filterable, allowCreate, typeKey } = renderOpts
let dicData = [] as any
if (typeKey && row[typeKey]) {
dicData = renderOpts[`${row[typeKey]}DicData`]
if (row[typeKey] == 'table' && column.field != 'dictTable') {
if (row.dictTable) dicData = dicData[row.dictTable]
else dicData = [{ label: '请先选择 字典Table', value: '-1', disabled: true }]
}
} else if (column.field == 'mainField') {
dicData = renderOpts.dicData
if (row.mainTable) dicData = dicData[row.mainTable]
else dicData = [{ label: '请先选择 外键-主表名', value: '-1', disabled: true }]
} else if (!typeKey) dicData = renderOpts['dicData']
//树表
if (column.field == 'dictTable' && ['tree', 'cascader'].includes(row.controlType) && row.dictType) {
dicData = renderOpts['treeDicData']
}
//设置禁用选择
if (column.field == 'dictType') {
dicData = dicData.map(item => {
if (['tree', 'cascader', 'dicTableSelect'].includes(row.controlType) && item.value == 'dict') item.disabled = true
else item.disabled = false
return item
})
}
return (
<avue-select
popper-class="vxe-table--ignore-clear"
v-model={row[column.field]}
placeholder={'请选择 ' + column.title}
dic={dicData}
multiple={multiple}
filterable={filterable}
allowCreate={allowCreate}
onChange={() => renderOpts.events ? renderOpts.events.change(row, column.field, rowIndex) : ''}
/>
)
}
},
LowSummaryBottomSql: {
default: (renderOpts, { row, column }, isStop = false) => {
const { dicObj } = renderOpts
let value = row.summaryJson.sqlType
if (value === '' || value === undefined || value === null) return ''
if (value == 'custom') return <span>SQL<span style="color:#409EFF">{row.summaryJson.sqlValue}</span></span>
if (Object.keys(dicObj).length) value = dicObj[value]
if (isStop) return (<div> <span>{value}</span> {stopIcon} </div>)
return <span>{value}</span>
},
edit: (renderOpts, { row, column }) => {
const { dicData } = renderOpts
const visible = row.summaryJson.sqlType == 'custom'
return (
<el-popover popper-class="low-summary-buttom-sql__popover" popper-style={{ width: 'auto', height: 'auto' }} visible={visible} placement='bottom-start' v-slots={{
reference: () => (
<avue-select
popper-class="vxe-table--ignore-clear"
v-model={row.summaryJson.sqlType}
placeholder={'请选择 ' + column.title}
dic={dicData}
/>
),
default: () => (
<div style={{ width: '400px', height: '200px' }} class="vxe-table--ignore-clear">
<MonacoEditor v-model={row.summaryJson.sqlValue} language='mysql' editorOption={{ minimap: false }}></MonacoEditor>
</div>
),
}}>
</el-popover>
)
}
},
LowClickInput: {
default: (renderOpts, { row, column, fieldProp }, isStop) => {
const text = handleLowClickInput(row, { ...column, fieldProp })
if (isStop) return (<div> <span>{text}</span> {stopIcon} </div>)
return <span>{text}</span>
},
edit: (renderOpts, { row, rowIndex, column, fieldProp }) => {
const prop = fieldProp || column.field
const text = handleLowClickInput(row, { ...column, fieldProp })
return <el-input class="my-cell" readonly onClick={() => renderOpts.events.click(row, prop, rowIndex)} text="text" value={text} placeholder={'请输入 ' + column.title} />
}
},
virtualInput: {
default: (renderOpts, { row, column }, isStop) => {
if (!row.virtualType) isStop = true
const option = { row, column, fieldProp: `virtual_${row.virtualType}_str` }
if (row.virtualType == 'fun') return lowControl.LowInput.default(renderOpts, option, isStop)
else return lowControl.LowClickInput.default(renderOpts, option, isStop)
},
edit: (renderOpts, { row, rowIndex, column }) => {
const option = { row, column, rowIndex, fieldProp: `virtual_${row.virtualType}_str` }
if (row.virtualType == 'fun') return lowControl.LowInput.edit(renderOpts, option)
else return lowControl.LowClickInput.edit(renderOpts, option)
}
},
LowMonacoEditorInput: {
default: (renderOpts, { row, column, fieldProp }, isStop) => {
const prop = fieldProp || column.field
if (isStop) return (<div> <span>{row[prop]}</span> {stopIcon} </div>)
return <span>{row[prop]}</span>
},
edit: (renderOpts, { row, column, fieldProp }) => {
const prop = fieldProp || column.field
return (
<el-popover popper-style={{ width: 'auto', height: 'auto' }} placement='top' trigger='click' v-slots={{
reference: () => (
<el-input class="my-cell" text="text" v-model={row[prop]} clearable placeholder={'请输入 ' + column.title} onClick={() => renderOpts.events.click(row)} />
),
default: () => (
<div>
<div style={{ fontSize: '16px' }}>{row.fieldName}{row.fieldCode} {column.title}</div>
<div style={{ width: '600px', height: '200px' }} class="vxe-table--ignore-clear">
<MonacoEditor v-model={row[prop]} language='javascript' editorOption={{ minimap: false }}></MonacoEditor>
</div>
</div>
),
}}>
</el-popover>
)
}
}
}
for (const key in lowControl) {
const addObj = { renderDefault: lowControl[key].default, renderCell: lowControl[key].default }
if (lowControl[key].edit) {
addObj['renderEdit'] = (renderOpts: any, params) => {
if (useType == 'table') {
if (!infoTableIsEdit(params, renderOpts)) return lowControl[key].default(renderOpts, params, true)
}
return lowControl[key].edit(renderOpts, params)
}
}
VxeUI.renderer.add(key, addObj)
}
}

View File

@@ -0,0 +1,300 @@
<template>
<div class="import-data-box pb-50px!">
<div class="top-steps">
<el-steps :active="stepValue" align-center finish-status="finish" process-status="finish">
<el-step
v-for="(title, index) in stepList"
:key="title"
:class="{ success: stepValue > index, active: stepValue == index }"
>
<template #title>
<div class="text-14px"> {{ title }} </div>
</template>
<template #icon>
<div class="step-icon"></div>
</template>
</el-step>
</el-steps>
</div>
<div class="import-content">
<div v-if="stepValue == 0">
<div class="file-item flex" v-for="(item, key) in uploadList" :key="key">
<div class="item-icon flex-basis-100px flex-shrink-0 bg-[var(--el-fill-color-light)]">
<Icon color="#999" :icon="item.icon" :size="60"></Icon>
</div>
<div class="flex-1 pl-20px pt-10px pb-10px">
<div class="text-16px">{{ item.title }}</div>
<div class="text-14px pt-10px pb-10px">{{ item.tip }}</div>
<div v-if="key == 'upload' && !files?.length">
<el-button type="primary" :loading="item.loading" link @click="open()">
{{ item.btnText }}
</el-button>
</div>
<div v-if="key == 'upload' && files?.length" class="flex items-center">
<Icon :size="14" icon="ep:link"></Icon>
<div class="c-#409EFF pl-2px">{{ files[0].name }}</div>
<div class="c-#999">{{ fileSizeFormatter(files[0], 'size', files[0].size) }}</div>
<Icon :size="14" icon="ep:close" @click="reset" class="cursor-pointer"></Icon>
</div>
</div>
</div>
</div>
<div v-else-if="stepValue == 1" class="w-80% m-auto mt-40px mb-80px">
<el-progress
:text-inside="true"
:stroke-width="18"
:percentage="progress"
:format="() => progress + '%'"
/>
<div class="text-center text-16px mt-10px">
<span>
{{ t('Avue.crud.importStep_3.playTip') }}
{{ importInfo.handleCou }}/{{ importInfo.totalCou }}...
</span>
</div>
</div>
<div v-else-if="stepValue == 2" class="mt-20px pb-40px">
<div class="flex flex-col items-center">
<Icon color="#67C23A" icon="mdi:success-circle-outline" :size="60"></Icon>
<div class="text-20px pt-10px pb-10px">{{ t('Avue.crud.importStep_4.tip') }}</div>
</div>
</div>
</div>
<div
class="pos-absolute left-0 bottom-0 w-100% box-border px-20px py-10px flex justify-end bg-[var(--el-bg-color)] b-solid b-0px b-t-1px b-#f1f1f1 dark:b-[var(--el-border-color-dark)]"
>
<template v-if="stepValue == 0">
<el-button
type="primary"
:disabled="!files || !files?.length"
:loading="loading"
@click="importData()"
>
<Icon v-if="!loading" icon="ep:view" :size="16"> </Icon>
<span>开始导入数据</span>
</el-button>
</template>
<template v-if="stepValue == 2">
<el-button type="default" @click="closeImport">
<Icon icon="ep:close" :size="14" class="mr-3px"> </Icon>
{{ t('dialog.close') }}
</el-button>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import * as I18nApi from '@/api/design/i18n/index'
import { fileSizeFormatter } from '@/utils/index'
import { useFileDialog } from '@vueuse/core'
defineOptions({ name: 'ImportData' })
interface Props {
show: boolean
localeMap: object[]
isFull?: boolean
}
const props = defineProps<Props>()
const emit = defineEmits(['close-popup', 'reset-change'])
const { t } = useI18n()
const stepList = [
t('Avue.crud.importStep_1.title'),
t('Avue.crud.importStep_3.title'),
t('Avue.crud.importStep_4.title')
]
const uploadList = ref({
upload: {
icon: 'uiw:cloud-upload',
title: '上传已经完善国际化的信息表',
tip: t('Avue.crud.importStep_1.tip_2'),
btnText: t('Avue.crud.importStep_1.btn_2'),
loading: false
}
})
const stepValue = ref(0)
const loading = ref(false)
const importInfo = ref({ batchCode: 0, handleCou: 0, totalCou: 0 })
const timer = ref<any>(null)
const isStop = ref(false)
const viewRef = ref()
const message = useMessage() // 消息弹窗
const { files, open, reset } = useFileDialog({
multiple: false, //可选:是否可以多选文件
accept: '.xls,.xlsx', //可选:自定义上传文件类型
reset: true //可选:再次选择时是否把之前选的文件清除
})
const progress = computed(() => {
return Number(((importInfo.value.handleCou / importInfo.value.totalCou) * 100).toFixed(2))
})
const init = () => {
stepValue.value = 0
isStop.value = false
for (let key in importInfo.value) importInfo.value[key] = 0
reset()
}
const rollPoling = async (done?, type?) => {
if (timer.value) clearTimeout(timer.value)
const data = await I18nApi.getImportProgress(importInfo.value.batchCode).catch(() => false)
console.log(data)
if (data !== false) {
importInfo.value.handleCou = data.handleCou
importInfo.value.totalCou = data.totalCou
if (type == 'init') {
done()
stepValue.value = 1
}
}
if (data.handleCou < data.totalCou) {
timer.value = setTimeout(() => {
rollPoling()
}, 2000)
} else stepValue.value = 2
}
const importData = async () => {
await message.confirm(t('Avue.crud.importTips.importDataBtnTip'))
loading.value = true
const file = files.value ? files.value[0] : ''
const uploadRes = await I18nApi.uploadFile({ updateSupport: 1, file })
const res = await I18nApi.uploadExcelData({
fileUrl: uploadRes.data.fileUrl,
exportLangParam: props.localeMap.map((item) => `${item['lang']}=${item['name']}`).join(','),
file
}).catch(() => false)
const done = () => (loading.value = false)
if (res) {
importInfo.value.batchCode = res.data
rollPoling(done, 'init')
} else done()
}
const handleClose = async (done) => {
if (timer.value) clearTimeout(timer.value)
emit('reset-change')
done()
}
const closeImport = () => {
emit('reset-change')
emit('close-popup')
}
watch(
() => props.show,
(val) => {
if (val && stepValue.value !== 1) {
init()
}
}
)
watch(
() => props?.isFull,
(val) => {
viewRef.value?.setCalcHeight(val ? 80 : 215)
}
)
defineExpose({ handleClose })
</script>
<style lang="scss" scoped>
.import-data-box {
padding: 20px;
::v-deep(.el-steps) {
.el-step__icon {
width: 16px;
height: 16px;
border-radius: 50%;
}
.el-step__line {
top: 7px;
}
.step-icon {
width: 100%;
height: 100%;
border-radius: 50%;
box-sizing: border-box;
}
.el-step__head {
&.is-success {
.step-icon {
background-color: var(--el-color-success);
}
}
&.is-finish {
.step-icon {
background-color: var(--el-color-primary);
}
}
&.is-wait {
.step-icon {
background-color: var(--el-text-color-placeholder);
}
}
}
.el-step {
&.active {
.step-icon {
background-color: var(--el-bg-color);
border: 2px solid var(--el-color-primary);
}
.el-step__line::before {
position: absolute;
left: 0;
width: 50%;
height: 2px;
background-color: var(--el-color-primary);
content: '';
}
}
&.success {
.el-step__line {
background-color: var(--el-color-primary);
}
}
}
}
.import-content {
padding: 20px 40px;
.file-item {
margin-bottom: 20px;
border: var(--el-border);
.item-icon {
display: flex;
align-items: center;
justify-content: center;
min-height: 100px;
border-right: var(--el-border);
}
}
.error-list {
padding: 15px 20px;
border: 1px solid #ece9e9;
}
}
}
</style>

View File

@@ -0,0 +1,180 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu-left="{ size }">
<el-button
type="primary"
plain
:size="size"
@click="importDialog = true"
:loading="exportLoading"
v-hasPermi="['jeelowcode:i18n:import']"
>
<Icon icon="clarity:import-line"></Icon>
<span>导入国际化配置</span>
</el-button>
<el-button
type="success"
plain
:size="size"
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['jeelowcode:i18n:export']"
>
<Icon icon="clarity:export-line"></Icon>
<span>导出国际化配置</span>
</el-button>
</template>
<template #fileName="{ row }">
<div
class="cursor-pointer hover:c-#409EFF"
@click="downloadByUrl({ url: row.fileUrl, fileName: row.fileName })"
>
{{ row.fileName }}
</div>
</template>
</avue-crud>
<!-- 导入 -->
<DesignPopup
v-model="importDialog"
title="导入国际化配置"
width="900px"
:dialog-params="{ alignCenter: true }"
:handleClose="importRef?.handleClose"
>
<template #default="{ isFull }">
<ImportI18n
ref="importRef"
:show="importDialog"
:isFull="isFull"
:locale-map="localeStore.localeMap"
@close-popup="importDialog = false"
@reset-change="resetChange"
></ImportI18n>
</template>
</DesignPopup>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { downloadByUrl } from '@/utils/filt'
import * as I18nApi from '@/api/design/i18n'
import { CommonStatusEnum } from '@/utils/constants'
import ImportI18n from './ImportI18n.vue'
import { useLocaleStoreWithOut } from '@/store/modules/locale'
defineOptions({ name: 'SystemPost' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const localeStore = useLocaleStoreWithOut()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
align: 'center',
headerAlign: 'center',
addBtn: false,
menu: false,
column: {
fileName: {
label: '导入文件'
},
userName: {
label: '操作人',
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
createTime: {
label: '导入时间',
display: false,
type: 'datetime',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
}
}
}) //表格配置
const tableForm = ref<{ id?: number }>({})
const tableData = ref([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const exportLoading = ref(false) // 导出的加载中
const crudRef = ref()
const importDialog = ref(false)
const importRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
try {
const data = await I18nApi.getImportList(searchObj)
tableData.value = data.records
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
const resetChange = () => {
tablePage.value.currentPage = 1
getTableData()
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await I18nApi.exportExcelData({ param: localeStore.localeMap })
download.excel(data, '国际化配置.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,922 @@
<template>
<ContentWrap>
<div class="flex gap-x-10px">
<div class="flex-basis-180px flex-shrink-0">
<avue-tree
ref="treeRef"
v-model="treeForm"
:option="treeOption"
:data="treeData"
:permission="treePermission"
:before-open="treeBeforeOpen"
@node-contextmenu="treeNodeContextmenu"
@node-click="treeNodeClick"
@update="treeUpdate"
@save="treeSave"
@del="treeDel"
>
</avue-tree>
</div>
<avue-crud
class="table-content flex-1"
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
@selection-change="selectionChange"
>
<template #menu-left="{ size }">
<el-dropdown @command="menuLeftHandle" v-hasPermi="['jeelowcode:tab:js']">
<el-button type="primary" :size="size" class="ml-10px">
<Icon :size="14" icon="teenyicons:webpack-outline" />
<span class="ml-3px!">前端增强</span>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="js">JS增强</el-dropdown-item>
<el-dropdown-item command="scss">SCSS增强</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template #menu="{ size, row, index }">
<div class="flex justify-center flex-items-center">
<el-button
type="primary"
text
:size="size"
@click="menuHandle({ type: 'edit', row, index })"
v-hasPermi="['jeelowcode:tab:update']"
>
<Icon :size="14" icon="ep:edit-pen"></Icon>
<span>编辑</span>
</el-button>
<span
v-if="
checkPermi(['jeelowcode:tab:update']) &&
checkPermi([
'jeelowcode:tab:create',
'jeelowcode:tab:update',
'jeelowcode:tab:delete'
])
"
class="ml-8px mr-8px mt-2px inline-block h-16px w-1px bg-#e8e8e8 .dark:bg-[var(--el-border-color-dark)]"
></span>
<el-dropdown
@command="menuHandle"
v-if="
checkPermi([
'jeelowcode:tab:create',
'jeelowcode:tab:update',
'jeelowcode:tab:delete'
])
"
>
<div class="mt--2px cursor-pointer">
<el-text :size="size" type="primary">
更多
<Icon :size="16" icon="iconamoon:arrow-down-2-light" />
</el-text>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="{ type: 'test', row }"> 功能测试 </el-dropdown-item>
<el-dropdown-item :command="{ type: 'routeAddress', row }">
路由地址
</el-dropdown-item>
<el-dropdown-item
v-if="checkPermi(['jeelowcode:tab:update'])"
:command="{ type: 'jsUnlock', row }"
>
js增强解锁
</el-dropdown-item>
<el-dropdown-item
v-if="checkPermi(['jeelowcode:tab:update'])"
:command="{ type: 'scssUnlock', row }"
>
scss增强解锁
</el-dropdown-item>
<el-dropdown-item
v-if="checkPermi(['jeelowcode:tab:delete'])"
:command="{ type: 'del', row }"
>
删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<template #lowSelectRadio="{ row }">
<el-radio
class="low-select-radio"
v-model="radioValue"
:label="row.id"
@click="radioClick(row)"
/>
</template>
<template #optionList-form>
<avue-crud ref="moduleCrudRef" :option="moduleOption" :data="tableForm.optionList">
<template #menu="{ size, index }">
<el-button text :size="size" type="danger" @click="delModuleTableRow(index)">
<span>删除</span>
</el-button>
</template>
<template #darg="{ index }">
<el-dropdown trigger="hover" placement="bottom" @command="dropdownCommand">
<div>
<Icon :size="20" icon="gg:menu-grid-r" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="{ index: index, type: 'up' }" :disabled="index == 0"
>向上移</el-dropdown-item
>
<el-dropdown-item
:command="{ index: index, type: 'down' }"
:disabled="index == tableForm.optionList?.length - 1"
>向下移</el-dropdown-item
>
<el-dropdown-item :command="{ index: index, type: 'add' }"
>插入一行</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template #tabIcon="{ row }">
<Icon v-if="row.tabIcon" :size="14" :icon="row.tabIcon" />
</template>
<template #tabIcon-form="{ row, column, size, disabled }">
<IconSelectInput
v-model="row.tabIcon"
:prop="column.prop"
:column="column"
:size="size"
:disabled="disabled"
/>
</template>
<template #moduleValue-form="{ row }">
<avue-select
v-if="row.moduleType == 'table'"
v-model="row.moduleValue"
placeholder="请选择表单开发配置"
:dic="tableDicData"
:virtualize="true"
:filterable="true"
></avue-select>
<avue-select
v-else-if="row.moduleType == 'form'"
v-model="row.moduleValue"
placeholder="请选择自定义表单配置"
:dic="formDicData"
:virtualize="true"
:filterable="true"
></avue-select>
<avue-select
v-else-if="row.moduleType == 'report'"
v-model="row.moduleValue"
placeholder="请选择报表配置"
:dic="reportDicData"
:virtualize="true"
:filterable="true"
></avue-select>
<avue-input
v-else-if="row.moduleType == 'custom'"
v-model="row.moduleValue"
placeholder="模块所在文件路径"
prepend="src/"
>
</avue-input>
</template>
<template #isLazy-header="{ column }">
<span>{{ column.label }}</span>
<div class="pos-absolute right-1px top-0">
<el-tooltip
class="box-item"
effect="dark"
content="第一次切换到当前标签页之前模块内容都不会加载"
placement="top-start"
>
<Icon :size="14" icon="ep:info-filled"></Icon>
</el-tooltip>
</div>
</template>
<template #isLazy-form="{ row }">
<el-checkbox v-model="row.isLazy" true-value="Y" false-value="N" />
</template>
<template #isRefresh-header="{ column }">
<span>{{ column.label }}</span>
<div class="pos-absolute right-1px top-0">
<el-tooltip
class="box-item"
effect="dark"
content="切换到当前标签页时,会刷新当前模块内容的数据"
placement="top-start"
>
<Icon :size="14" icon="ep:info-filled"></Icon>
</el-tooltip>
</div>
</template>
<template #isRefresh-form="{ row }">
<el-checkbox v-model="row.isRefresh" true-value="Y" false-value="N" />
</template>
<template #tabI18n-form="{ row }">
<I18nInput v-model="row.tabI18n" :def-name="row.tabName"></I18nInput>
</template>
</avue-crud>
</template>
</avue-crud>
</div>
<DesignPopup v-model="MEDialog.value" v-bind="MEDialog.params" :isBodyFull="true">
<template #default>
<el-container class="h-100%">
<el-main class="p-0!">
<MonacoEditor
class="flex-1"
v-model="MEData.value"
v-bind="MEData.params"
></MonacoEditor>
</el-main>
<el-aside width="300px" v-if="MEDialog.otherParams">
<TipView v-bind="MEDialog.otherParams"></TipView>
</el-aside>
</el-container>
</template>
</DesignPopup>
<DesignPopup v-model="samplePopup" title="控件使用示例" :width="280">
<template #default>
<div class="text-center p-20px">
<div class="b b-b-0 b-solid b-#f1f1f1 .dark:b-[var(--el-border-color-dark)]">
<div
v-for="(item, key) in controlPath"
:key="key"
class="h-50px flex items-center b-b b-b-solid b-#f1f1f1 .dark:b-[var(--el-border-color-dark)] pr-10px"
>
<div class="text-14px flex-1">{{ item.name }}</div>
<el-button
class="flex-basis-80px flex-shrink-0"
size="small"
type="primary"
@click="copySampleStr(key)"
>点击复制</el-button
>
</div>
</div>
</div>
</template>
</DesignPopup>
</ContentWrap>
</template>
<script lang="ts" setup>
import { TipView } from '../general/components/index'
import * as ModuleApi from '@/api/design/module'
import * as FormApi from '@/api/design/form'
import * as TableApi from '@/api/design/table'
import * as ReportApi from '@/api/design/report'
import { ElLoading } from 'element-plus'
import { cloneDeep } from 'lodash-es'
import { useGroup } from '@/hooks/design/useGroup'
import { checkPermi } from '@/utils/permission'
import { ElButton } from 'element-plus'
import { I18nInput } from '@/components/LowDesign/src/shareControl/index'
import controlPath from '@/components/LowDesign/src/controlPath'
import useCopyText from '@/hooks/design/useCopyText'
import useMEDialog from '@/hooks/design/useMEDialog'
import {
moduleScssEnhanceExample,
controlInitExample
} from '@/components/LowDesign/src/utils/example'
import { formattingLengStr } from '@/utils/lowDesign'
defineOptions({ name: 'FormDesign' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const router = useRouter() // 路由
const { getCurrPermi } = useCrudPermi()
const { copyText } = useCopyText()
const { MEDialog, MEData, openMEDialog } = useMEDialog()
const loading = ref(true) // 列表的加载中
//表格配置
const tabsType = [
{ label: '默认风格', value: 'default' },
{ label: '卡片风格', value: 'card' },
{ label: '带边框的卡片风格', value: 'border-card' }
]
const tabsPosition = [
{ label: '上边', value: 'top' },
{ label: '左边', value: 'left' },
{ label: '右边', value: 'right' },
{ label: '下边', value: 'bottom' }
]
const moduleType = [
{ label: '表单开发', value: 'table' },
{ label: '自定义表单', value: 'form' },
{ label: '报表设计', value: 'report' },
{ label: '自定义模块', value: 'custom' }
]
const getRules = (label, type?) => {
return [
{
required: true,
message: (type == 'select' ? '请选择' : '请输入') + label,
trigger: type == 'select' ? 'change' : 'blur'
}
]
}
const tableOption = ref({
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
dialogWidth: '90%',
dialogFullscreen: true,
dialogCustomClass: 'tabs-design-form',
editBtn: false,
delBtn: false,
border: true,
index: false,
menuWidth: 180,
span: 12,
column: {
lowSelectRadio: {
label: '',
display: false,
width: 50,
overHidden: false,
fixed: true,
showColumn: false
},
id: { label: '模块id', display: false, search: true, overHidden: true, width: 170 },
moduleName: { label: '模块名称', display: false, search: true, overHidden: true },
tabsType: { label: '风格类型', display: false, type: 'select', dicData: tabsType, width: 140 },
tabsPosition: {
label: '选项卡位置',
display: false,
type: 'select',
dicData: tabsPosition,
width: 120
},
updateTime: {
label: '修改时间',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
display: false,
width: 160
}
},
group: [
{
label: '基本信息',
collapse: true,
prop: 'group_info',
column: {
moduleName: { label: '模块名称', rules: getRules('模块名称') },
groupModuleId: {
label: '分组类型',
type: 'tree',
value: '',
dicData: [],
filterable: true,
defaultExpandAll: true,
props: { label: 'name', value: 'id' }
},
tabsType: {
label: '风格类型',
type: 'select',
dicData: tabsType,
value: 'border-card',
clearable: false
},
tabsPosition: {
label: '选项卡位置',
type: 'select',
dicData: tabsPosition,
value: 'top',
clearable: false
}
}
},
{
label: '模块配置',
collapse: true,
prop: 'group_module',
labelWidth: 20,
column: {
optionList: {
hide: false,
span: 24,
dataType: 'array',
value: [
{
$cellEdit: true,
tabName: '',
tabCode: '',
moduleType: 'table',
moduleValue: '',
tabIcon: '',
isLazy: false,
isRefresh: false
}
],
rules: [
{
validator: (rule, value, callback) => {
if (value?.length) callback()
else callback(new Error('请添加模块信息'))
},
trigger: 'blur'
}
]
}
}
}
]
})
const tableForm = ref<any>({})
const tableData = ref([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const tableSelect = ref<any[]>([])
const radioValue = ref('')
const jsEnhanceData = ref({})
const designPop = ref({
value: {},
dialog: false,
params: {}
})
const moduleOption = ref({
addBtn: false,
editBtn: false,
addRowBtn: true,
refreshBtn: false,
columnBtn: false,
addBtnText: '新增模块',
menuWidth: 70,
height: undefined,
calcHeight: undefined,
maxHeight: 300,
index: true,
column: {
darg: { width: 45 },
tabName: { label: '标签名称', cell: true, minWidth: 150, rules: getRules('标签名称') },
tabCode: { label: '标签编码', cell: true, width: 150, rules: getRules('标签名称') },
moduleType: {
label: '模块类型',
type: 'select',
dicData: moduleType,
value: 'table',
cell: true,
width: 140,
clearable: false
},
moduleValue: {
label: '模块内容',
cell: true,
minWidth: 180,
rules: getRules('模块内容', 'select')
},
tabI18n: { label: '国际化配置', cell: true, width: 100 },
tabIcon: { label: '标签图标', cell: true, width: 140 },
isLazy: { label: '懒加载', cell: true, width: 70, value: 'N' },
isRefresh: { label: '刷新数据', cell: true, width: 85, value: 'N' }
}
})
const tableDicData = ref([])
const formDicData = ref([])
const reportDicData = ref([])
const samplePopup = ref(false)
const crudRef = ref()
const treeRef = ref()
const isUnload = ref(false)
const permission = getCurrPermi(['jeelowcode:tab'])
useCrudHeight(crudRef)
const {
treeForm,
treeOption,
treeData,
groupValue,
treePermission,
treeBeforeOpen,
treeNodeContextmenu,
treeNodeClick,
getTreeData,
treeUpdate,
treeSave,
treeDel
} = useGroup(treeRef, ModuleApi, () => resetChange())
const radioClick = (row) => {
if (row.id == radioValue.value) {
setTimeout(() => {
radioValue.value = ''
tableSelect.value = []
}, 30)
} else {
tableSelect.value = [row]
}
}
const delModuleTableRow = (index) => {
tableForm.value.optionList.splice(index, 1)
}
const menuLeftHandle = (type) => {
if (['js', 'scss'].includes(type) && tableSelect.value.length !== 1) {
return message.warning('请勾选一条表格数据')
}
if (['js', 'scss'].includes(type)) openModuleEnhance(type)
}
const dropdownCommand = ({ index, type }) => {
if (type == 'up' || type == 'down') {
const delItem = tableForm.value.optionList.splice(index, 1)[0]
nextTick(() =>
tableForm.value.optionList.splice(type == 'up' ? index - 1 : index + 1, 0, delItem)
)
} else if (type == 'add') {
tableForm.value.optionList.splice(index + 1, 0, { $cellEdit: true, moduleType: 'table' })
}
}
const handleWebEnhance = (moduleId, enhanceType) => {
return new Promise(async (resolve) => {
let apiType = 'updateJsData'
let apiData = {
id: jsEnhanceData.value['id'],
tabId: moduleId,
jsType: enhanceType,
jsJson: MEData.value.value
}
if (!apiData.id) {
apiType = 'saveJsData'
delete apiData.id
}
const data = await ModuleApi[apiType](apiData).catch(() => false)
if (data) message.success('保存成功')
resolve(data)
})
}
const getAllDic = () => {
TableApi.getAllDbDicData({ onlyTableName: 'Y' }).then(async (res) => {
tableDicData.value = res.map((item) => {
return { label: `${item.tableDescribe}[${item.tableName}]`, value: item.tableId }
})
})
FormApi.getFormList({}).then((res) => {
formDicData.value = res.records.map((item) => {
return { label: item.desformName, value: item.id }
})
})
ReportApi.getDbList({}).then((res) => {
reportDicData.value = res.records.map((item) => {
return { label: item.reportName, value: item.reportCode }
})
})
}
const menuHandle = async ({ type, row, index }) => {
if (type == 'edit') crudRef.value.rowEdit(row, index)
else if (type == 'test') router.push({ path: '/low/module/test/' + row.id })
else if (type == 'routeAddress') showRouteAddress(row)
else if (type.indexOf('Unlock') != -1) unlockEnhance(type, row)
else if (type == 'del') rowDel(row)
}
const showRouteAddress = (row) => {
const url = `module/view/` + row.id
message.alert(`路由地址:</br>${url}`, '菜单的路由地址', {
confirmButtonText: '复制',
dangerouslyUseHTMLString: true,
callback: (action) => {
if (action == 'confirm') copyText(url)
}
})
}
const unlockEnhance = (type, row) => {
const enhanceType = type.split('Unlock')[0]
message.confirm(`是否强制解除${enhanceType}增强锁定?`, '提示').then(async () => {
await ModuleApi.unlockJs(row.id, enhanceType)
message.success('解除锁定成功')
if (type.indexOf('Open') !== -1 && tableSelect.value.length) menuLeftHandle(enhanceType)
})
}
const openModuleEnhance = async (enhanceType) => {
loading.value = true
jsEnhanceData.value = { jsJson: '' }
const moduleId = tableSelect.value[0].id
const detailData = await ModuleApi.getJsDetail({
tabId: moduleId,
type: enhanceType,
lock: true
}).catch(() => false)
loading.value = false
if (detailData === false) return
if (detailData) {
if (detailData.timeStr) {
message
.confirm(
`<div>
<div style="font-size:16px"><span style="font-size:14px">锁定操作人:</span>${detailData.userName}</div>
<div style="font-size:16px"><span style="font-size:14px">开始锁定时间:</span>${detailData.timeStr}</div>
<div style="color:#E6A23C">注:如果确认没有人正在编辑,可点击下方强制解锁</div>
</div>`,
'当前增强已锁定',
{
dangerouslyUseHTMLString: true,
cancelButtonText: '关闭',
confirmButtonText: '强制解锁'
}
)
.then(() => {
menuHandle({
type: `${enhanceType}UnlockOpen`,
row: tableSelect.value[0],
index: tableSelect.value[0].$index
})
})
return
}
if (detailData.jsJson) {
jsEnhanceData.value = detailData
}
}
let params = {
title: `${enhanceType}增强 【${tableSelect.value[0].moduleName}】`,
fullscreen: true,
handleClose: async (done) => {
await ModuleApi.unlockJs(moduleId, enhanceType)
isUnload.value = false
done()
},
footerBtn: [
{
params: { type: 'primary' },
name: '保 存',
icon: 'mingcute:save-line',
loading: true,
clickFun: async (loading) => {
const id = await handleWebEnhance(moduleId, enhanceType)
if (id) jsEnhanceData.value['id'] = id
loading()
}
},
{
params: { type: 'success' },
name: '保存并关闭',
icon: 'mingcute:save-line',
loading: true,
clickFun: async (loading) => {
const bool = await handleWebEnhance(moduleId, enhanceType)
await ModuleApi.unlockJs(moduleId, enhanceType)
loading()
if (bool) {
MEDialog.value.value = false
isUnload.value = false
}
}
},
{
params: {},
name: '关 闭',
icon: 'material-symbols:close',
clickFun: async () => {
await message.confirm('是否确认关闭?')
await ModuleApi.unlockJs(moduleId, enhanceType)
MEDialog.value.value = false
isUnload.value = false
}
}
]
}
if (enhanceType == 'js') {
params['providerType'] = 'moduleJsEnhance'
params['otherParams'] = { type: 'simple', width: '300px', tipKeyList: ['ModuleJsEnhance'] }
params['language'] = 'javascript'
params['headerBtn'] = [
{
params: { type: 'success' },
name: '控件使用示例',
icon: 'jam:code-sample',
clickFun: async () => (samplePopup.value = true)
}
]
if (!jsEnhanceData.value['jsJson']) {
jsEnhanceData.value['jsJson'] = `return {
}`
}
} else if (enhanceType == 'scss') {
params['language'] = 'scss'
if (!jsEnhanceData.value['jsJson']) {
jsEnhanceData.value['jsJson'] = moduleScssEnhanceExample(moduleId)
}
}
openMEDialog({ params, prop: 'jsJson' }, jsEnhanceData.value)
isUnload.value = true
}
const copySampleStr = (type) => {
copyText(controlInitExample(type))
samplePopup.value = false
}
const selectionChange = (data) => {
tableSelect.value = data
}
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
if (groupValue.value) searchObj['groupModuleId'] = groupValue.value
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await ModuleApi.getModuleList(searchObj)
tableData.value = data.records
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
if (Object.keys(params).length && groupValue.value) {
treeRef.value.setCurrentKey(0)
groupValue.value = 0
}
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
const groupData = cloneDeep(treeData.value)
const groupModuleId = tableOption.value.group[0].column.groupModuleId
if (groupModuleId) groupModuleId.dicData = groupData[0].children
if (['edit', 'view'].includes(type) && tableForm.value['id']) {
loading.value = true
const data = await ModuleApi.getModuleDetail(tableForm.value['id'])
tableForm.value = {
...data.tab,
optionList: data.optionList.map((item) => {
if (type == 'edit') item.$cellEdit = true
return item
})
}
loading.value = false
}
if (type == 'add' && groupValue.value) {
tableForm.value.groupModuleId = groupValue.value
}
done()
}
const initI18nData = (list) => {
if (!list?.length) return []
return list.map((item) => {
item.tabI18n = formattingLengStr(item.tabI18n, item.tabName)
return item
})
}
/** 新增操作 */
const rowSave = async (form: object, done, loading, isId?) => {
const elLoading = ElLoading.service({ fullscreen: true })
const saveData = { tab: { ...form }, optionList: initI18nData(form['optionList']) }
delete saveData.tab['optionList']
let bool = await ModuleApi.saveModuleData(saveData).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
if (isId) done(bool)
else done()
} else loading()
elLoading.close()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading, onData?) => {
const editData = { tab: { ...form }, optionList: initI18nData(form['optionList']) }
delete editData.tab['optionList']
let bool = await ModuleApi.updateModuleData(editData).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
if (!onData) getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (data) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await ModuleApi.deleteModuleData(data instanceof Array ? data : [data.id])
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
const beforeUnload = (event) => {
if (isUnload.value) return (event.returnValue = '您确定要关闭页面吗?')
}
/** 初始化 **/
onMounted(async () => {
window.addEventListener('beforeunload', beforeUnload)
getTableData()
getTreeData()
getAllDic()
})
onBeforeUnmount(() => {
window.removeEventListener('beforeunload', beforeUnload)
})
</script>
<style lang="scss" scoped>
.table-content {
max-width: calc(100% - 190px);
}
</style>
<style lang="scss">
.form-design-address-box {
.el-message-box__container {
display: block;
}
}
</style>
<style lang="scss">
.tabs-design-form {
.el-dialog__body {
padding-top: 0 !important;
}
.avue-crud {
.avue-crud__empty {
padding: 0;
.el-empty {
padding: 0;
}
.el-empty__description {
margin: 0;
}
}
}
}
</style>

View File

@@ -0,0 +1,70 @@
<template>
<LowModule ref="lowTabsRef" v-if="moduleId" :moduleId="moduleId"></LowModule>
<div v-else>无权限访问</div>
</template>
<script setup lang="ts">
import { useTagsViewStore } from '@/store/modules/tagsView'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
const route = useRoute()
const tagsViewStore = useTagsViewStore()
const { wsCache } = useCache()
const moduleId = ref('')
const timer = ref<any>(null)
const lowTabsRef = ref()
const setTestTitle = () => {
timer.value = setInterval(() => {
const moduleName = lowTabsRef.value?.moduleInfo?.moduleName
if (moduleName) {
if (timer.value) clearInterval(timer.value)
tagsViewStore.visitedViews = tagsViewStore.visitedViews.map((tag) => {
if (tag.path == '/low/module/test/' + moduleId.value) {
if (tag.meta) tag.meta.title = '功能测试:' + moduleName
}
return tag
})
}
}, 100)
}
onMounted(() => {
if (route.params.id) {
//功能测试
const menus = wsCache.get(CACHE_KEY.USER).menus
const praentArr = route.meta.activeMenu?.split('/').filter((path) => path) || []
let isPermission = false
if (praentArr.length) {
praentArr[0] = '/' + praentArr[0]
let findIndex = 0
const findPath = (menuList) => {
for (const index in menuList) {
if (menuList[index].path == praentArr[findIndex]) {
if (findIndex == praentArr.length - 1) isPermission = true
else findIndex++
if (menuList[index].children) findPath(menuList[index].children)
if (isPermission) break
}
}
}
findPath(menus)
}
if (isPermission && typeof route.params.id == 'string') moduleId.value = route.params.id
setTestTitle()
} else {
const pathList = route.path.split('/')
const length = pathList.length - 1
if (/\d$/.test(pathList[length])) {
moduleId.value = pathList[length]
}
}
})
onUnmounted(() => {
if (timer.value) clearInterval(timer.value)
})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,246 @@
<template>
<avue-tabs ref="tabsRef" :option="tabsOption" @change="tabsHandleChange"></avue-tabs>
<InfoVxeTopBtn
v-show="tabsValue.edit"
:selectNum="tabsValue.dataKey ? infoSelect[tabsValue.dataKey].length : 0"
:tabItem="tabsValue"
:size="size"
@cell-add="cellAddData"
@del-data="delTableInfoData"
@order-data="setInfoOrder"
/>
<template v-for="tab in tabsOption.column" :key="tab.prop">
<div v-show="tabsValue.prop === tab.prop">
<InfoVxeTable
v-model="infoData[tab.dataKey]"
:ref="(el) => (tableRefObj[tab.prop] = el)"
:tabItem="tab"
:column="tableInfoOption.infoColumn[`${tab.key}Column`]"
@selection-change="infoSelectionChange"
@cell-click="cellClick"
@dropdown-command="dorpdownHandleCommand"
></InfoVxeTable>
</div>
</template>
<DesignPopup
v-model="optionsDialog.dialog.value"
v-bind="optionsDialog.dialog"
:isBodyFull="true"
>
<template #default>
<template v-for="(control, key) in optionComponents" :key="key">
<component
:ref="(el) => (optionRef[key] = el)"
v-if="key == optionsDialog.controlType"
:is="control"
v-model="optionsDialog.controlValue"
v-bind="optionsDialog.controlData"
:show="optionsDialog.dialog.value"
></component>
</template>
</template>
</DesignPopup>
</template>
<script setup lang="ts">
import { SqlOption, InfoVxeTable, InfoVxeTopBtn } from '../../tableDesign/components'
import { MonacoEditor } from '@/components/MonacoEditor/index'
import { tableInfoOption } from '../designData'
import { formattingLengStr } from '@/utils/lowDesign'
import { cloneDeep } from 'lodash-es'
import * as DictDataApi from '@/api/system/dict/dict.type'
defineOptions({ name: 'TableInfo' })
const message = useMessage()
interface Props {
formType: string
editInfoData: any
size: any
}
const props = withDefaults(defineProps<Props>(), {
formType: ''
})
const tabsOption = ref({
column: [{ label: '字段属性', prop: 'tab_field', key: 'field', dataKey: 'basics', edit: true }]
})
const tabsValue = ref(tabsOption.value.column[0])
const infoData = ref({
basics: [] as any[]
})
const infoSelect = ref({
basics: []
})
const tableInfoDefault = ref<any[]>([])
const optionsDialog = ref<any>({ dialog: {}, controlData: {} })
const tableRefObj = ref({})
const optionComponents = markRaw({
sqlOption: SqlOption,
meditor: MonacoEditor
})
const optionRef = ref({})
const tabsRef = ref()
const fieldList = computed(() => {
let dicData: Array<{ label: string; value: string; type: string }> = []
infoData.value.basics.forEach((item) => {
if (item.fieldCode && item.isDb == 'Y')
dicData.push({
label: `${item.fieldCode}${item.fieldName ? '' + item.fieldName + '' : ''}`,
value: item.fieldCode,
type: item.fieldType
})
})
return dicData
})
watch(
() => fieldList.value,
(dicData: any) => {
const dicDataObj = {}
dicData.forEach(({ value, label }) => (dicDataObj[value] = label))
}
)
const dorpdownHandleCommand = (command) => {
let { dataKey } = tabsValue.value
if (!dataKey) return
let { index, type } = command
if (type == 'up' || type == 'down') {
const delItem = infoData.value[dataKey].splice(index, 1)[0]
nextTick(() => infoData.value[dataKey].splice(type == 'up' ? index - 1 : index + 1, 0, delItem))
} else if (type == 'add') cellAddData(index + 1)
}
const setInfoOrder = () => {
let { dataKey } = tabsValue.value
if (!dataKey) return
message
.prompt('请输入序号(格式:原序号/最终序号)', '调整排序', { inputValue: '/', type: '' })
.then(({ value }) => {
let orderArr = value.split('/')
if (orderArr.length == 2) {
const delItem = infoData.value[dataKey].splice(Number(orderArr[0].trim()) - 1, 1)[0]
nextTick(() => infoData.value[dataKey].splice(Number(orderArr[1].trim()) - 1, 0, delItem))
} else message.error('请输入正确的格式2/3')
})
.catch(() => {})
}
const infoSelectionChange = (selectObj) => {
let { dataKey } = tabsValue.value
if (!dataKey) return
infoSelect.value[dataKey] = selectObj.records
}
const delTableInfoData = () => {
let { dataKey, prop } = tabsValue.value
if (!dataKey) return
const keyArr = infoSelect.value[dataKey].map((item) => item._X_ROW_KEY)
infoData.value[dataKey] = infoData.value[dataKey].filter(
(item) => !keyArr.includes(item._X_ROW_KEY)
)
tableRefObj.value[prop].vxeTableRef.clearCheckboxRow()
infoSelect.value[dataKey] = []
}
const tabsHandleChange = (column) => {
tabsValue.value = column
}
const setTabsValue = (prop) => {
tabsOption.value.column.forEach((item, index) => {
if (item.prop == prop) {
tabsRef.value.changeTabs(index)
}
})
}
const cellAddData = (addIndex) => {
let { key, dataKey } = tabsValue.value
if (!dataKey) return
let addData = cloneDeep(tableInfoOption.infoDefaultData[dataKey]) || {}
let index = infoData.value[dataKey].length
infoData.value[dataKey].splice(addIndex === undefined ? index : addIndex, 0, addData)
tableScrollIndex(key, index, addIndex)
}
const tableScrollIndex = (key, index, addIndex?) => {
setTimeout(() => {
const tableBodyDom = document.querySelector(`.${key}-vxe-table .vxe-table--body-wrapper`)
if (tableBodyDom) {
const bool = addIndex === undefined
tableBodyDom.scrollTop =
(bool ? index : addIndex) * 40 - (bool ? 0 : tableBodyDom['offsetHeight'] / 2)
}
tableRefObj.value[`tab_${key}`].vxeTableRef.setEditRow(
tableRefObj.value[`tab_${key}`].vxeTableRef.getData(addIndex ? addIndex : index)
)
}, 300)
}
const cellClick = ({ rowIndex }) => {
let { prop } = tabsValue.value
tableRefObj.value[prop].vxeTableRef.setEditRow(
tableRefObj.value[prop].vxeTableRef.getData(rowIndex)
)
}
const initEditInfoData = () => {
const data = tableInfoOption.formattingInitData(props.editInfoData)
const fieldList: any[] = []
tableInfoDefault.value = data.infoData.filter((item) => {
fieldList.push(cloneDeep(item))
return false
})
infoData.value.basics = fieldList
}
onMounted(() => {
tableInfoDefault.value = []
infoData.value.basics = []
if (props.formType != 'add') initEditInfoData()
const { fieldColumn } = tableInfoOption.infoColumn
fieldColumn.labelI18n.editRender.events = {
click: (row) => {
nextTick(() => {
tableRefObj.value['tab_field'].vxeTableRef.setRow(row, {
labelI18n: formattingLengStr(row.labelI18n, row.fieldName)
})
})
}
}
DictDataApi.getSimpleDictTypeList().then((dicData) => {
const dicObj = {}
dicData = dicData.map(({ type, name }) => {
dicObj[type] = `${type}${name}`
return { label: dicObj[type], value: type }
})
Object.assign(fieldColumn.dictCode.editRender, { dicData, dicObj })
})
})
defineExpose({
infoData,
tableInfoDefault,
fieldList,
tableRefObj,
setTabsValue,
tableScrollIndex,
initEditInfoData
})
</script>
<style lang="scss">
.low-summary-buttom-sql__popover {
.el-popper__arrow {
left: 20px !important;
}
}
</style>
<style lang="scss" scoped>
::v-deep(.virtual-hide-row) {
display: none;
}
</style>

View File

@@ -0,0 +1,5 @@
import TableInfo from './TableInfo.vue'
export {
TableInfo
}

View File

@@ -0,0 +1,222 @@
import { verifyReportCode } from '@/api/design/report';
import { getAllDbDicData } from '@/api/design/table';
import { cloneDeep } from 'lodash-es';
// 数据字典
export const dicObj = {
dic_whether: [
{ label: '是', value: 'Y' },
{ label: '否', value: 'N' },
],
tableSelect: [
{ label: '多选', value: 'multiple' },
{ label: '单选', value: 'radio' },
{ label: '禁用', value: 'disabled' },
],
dataConfig: [
{ label: '分页', value: 'page' },
],
dataConfigSelect: [
{ label: '需登录', value: 'authFalse', desc: '访问该表接口:需登录' },
{ label: '需登录、鉴权', value: 'authTrue', desc: '访问该表接口:需登录并且需要配置菜单权限' },
{ label: '不登录可查询数据', value: 'authOpen', desc: '访问该表查询接口:不需要登录' },
],
tableConfig: [
{ label: '固定表格高度', value: 'height' },
{ label: '序号列', value: 'index' },
{ label: '纵向边框', value: 'border' },
{ label: '斑马纹样式', value: 'stripe' },
{ label: '同步', value: 'sync' },
],
fieldType: [
{ label: '字符串 String', value: 'String' },
{ label: '整数 Integer', value: 'Integer' },
{ label: '大整数 BigInt', value: 'BigInt' },
{ label: '小数 BigDecimal', value: 'BigDecimal' },
{ label: '日期 Date', value: 'Date' },
{ label: '时间 Time', value: 'Time' },
{ label: '日期时间 DateTime', value: 'DateTime' },
{ label: '文本 Text', value: 'Text' },
{ label: '大文本 LongText', value: 'LongText' },
{ label: '二进制 Blob', value: 'Blob' },
],
cellWidthType: [
{ label: '固定', value: 'fixed' },
{ label: '最小', value: 'min' },
],
queryMode: [
{ label: '精确查询', value: 'EQ' },
{ label: '模糊查询', value: 'LIKE' },
{ label: '范围查询(仅适用于日期、时间、数字)', value: 'RANGE' },
{ label: '包含查询in', value: 'IN' },
{ label: '不等于(!=', value: 'NE' },
],
}
export const getDicObj = (key) => {
const obj = {}
if (dicObj[key]) dicObj[key].forEach(item => obj[item.value] = item.label)
return obj
}
const rules_required = (label, type = '') => [{ required: true, message: `${['select'].includes(type) ? '请选择' : '请输入'} ${label}`, trigger: "blur" }]
const reportCode_required = async (rule, value, callback) => {
if (value === '') callback(new Error('请输入 报表编码'));
else {
const bool = await verifyReportCode(value)
if (bool) callback(new Error('报表编码已存在,请修改'));
else callback()
}
};
const dataSourcesCode_dicFormatter = (data) => {
data = data.map(item => {
if (item.id === 0) {
item.dbCode = 'master'
item.name = `${item.name}(本地数据源)`
}
else item.name = `${item.name}${item.dbType}`
return item
})
return data.filter(item => item.dbCode)
}
const dataOriginDicObj = {}
export const getDataOriginDicData = (dbCode, isReacquire?) => {
return new Promise(async resolve => {
if (isReacquire) for (const key in dataOriginDicObj) delete dataOriginDicObj[key]
if (dataOriginDicObj[dbCode]) return resolve(dataOriginDicObj[dbCode])
let data = []
if (dbCode == 'master') data = await getAllDbDicData({ systemFlag: 'Y' })
else data = await getAllDbDicData({ dataSourcesCode: dbCode })
dataOriginDicObj[dbCode] = dataOrigin_dicFormatter(data, dbCode)
resolve(dataOriginDicObj[dbCode])
})
}
const dataOriginObj = {}
const dataOrigin_dicFormatter = (data, dbCode) => {
const sysList: any[] = []
const dbList: any[] = []
data.forEach(item => {
const row = {
label: `${item.tableName}${item.tableDescribe}`,
tableText: item.tableDescribe,
tableName: item.tableName,
value: item.tableId,
type: 'table',
fieldList: item.fieldModelList.map(child => {
let label = child.fieldCode
if (child.fieldName) label = `${label}${child.fieldName}`
return { label, value: child.fieldCode, tableName: item.tableName, type: 'field', fieldType: child.fieldType }
})
}
if (item.tableId == item.tableName) sysList.push(row)
else if (item.tableClassify !== 2) dbList.push(row)
if (!dataOriginObj[dbCode]) dataOriginObj[dbCode] = {}
dataOriginObj[dbCode][item.tableId] = row
})
const dicData: any[] = []
if (sysList.length) dicData.push({ label: '系统表', value: 'sys', disabled: true, children: sysList })
if (dbList.length) dicData.push({ label: '表单开发', value: 'dbForm', disabled: true, children: dbList })
return dicData
}
//表格配置
const tableOptionColumn = {
reportName: { label: '报表名称', display: false, search: true, minWidth: 140 },
reportCode: { label: '报表编码', display: false, search: true, minWidth: 140, overHidden: true },
javaConfig: { label: 'JAVA类路径', display: false, search: true, minWidth: 140, overHidden: true, searchLabelWidth: 100, },
createTime: { label: '创建时间', type: 'datetime', format: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'x', display: false, width: 160 },
custom_form: { label: '', labelWidth: 0, span: 24, hide: true, showColumn: false },
custom_info: { label: '', labelWidth: 0, span: 24, hide: true, showColumn: false }
}
// 表单配置
const customFormColumn = {
reportName: { label: '报表名称', rules: rules_required('报表名称') },
reportCode: { label: '报表编码', rules: [...rules_required('报表编码'), { validator: reportCode_required, trigger: 'blur', required: true }] },
groupReportId: { label: '分组类型', type: 'tree', value: '', dicData: [], filterable: true, defaultExpandAll: true, props: { label: 'name', value: 'id' } },
dataSourcesCode: { label: '数据源', type: 'select', value: 'master', span: 6, clearable: false, dicUrl: '/infra/data-source-config/list', props: { label: 'name', value: 'dbCode' }, dicFormatter: dataSourcesCode_dicFormatter },
dataOrigin: { label: '数据表', type: 'tree', value: '', span: 8, dataType: 'string', multiple: true, filterable: true, parent: false, defaultExpandAll: true, dicData: [] },
originButton: { label: '', labelWidth: 0, },
javaConfig: { label: 'JAVA类路径', span: 12 },
dataConfig: { label: '数据配置', type: 'checkbox', span: 24, dicData: dicObj.dataConfig, dataType: 'string', value: ['page', 'sync','authFalse'] },
tableConfig: { label: '表格配置', type: 'checkbox', span: 24, dicData: dicObj.tableConfig, dataType: 'string', value: ['height', 'header', 'menu', 'index', 'border'] },
}
const infoColumn = {
fieldColumn: {
fieldCode: { title: '字段编码', minWidth: 120, editRender: { name: 'LowInput', verifyEdit: true } },
fieldName: { title: '字段名称', minWidth: 120, editRender: { name: 'LowInput' } },
labelI18n: { title: '国际化配置', width: 140, editRender: { name: 'LowMonacoEditorInput', events: {} } },
fieldType: { title: '字段类型', minWidth: 100, editRender: { name: 'LowSelect', verifyEdit: true, dicData: dicObj.fieldType, dicObj: getDicObj('fieldType') } },
queryIsDb: { title: '接口查询', width: 75, align: "center", editRender: { name: 'LowCheckbox' } },
queryIsWeb: { title: '查询控件', width: 75, align: "center", editRender: { name: 'LowCheckbox' } },
queryMode: { title: '查询模式', width: 130, editRender: { name: 'LowSelect', verifyEdit: true, dicData: dicObj.queryMode, dicObj: getDicObj('queryMode') } },
dictCode: { title: '字典Code', width: 180, editRender: { name: 'LowSelect', verifyEdit: true, filterable: true, noStop: true, dicData: [] } },
isExport: { title: '是否可导出', width: 90, align: "center", editRender: { name: 'LowCheckbox' } },
isShowSort: { title: '是否排序', width: 75, align: "center", editRender: { name: 'LowCheckbox' } },
},
}
const infoApiKey = {}
const apiKey = { fieldColumn: 'fieldList' }
for (const key in infoColumn) {
if (apiKey[key]) {
const keys = Object.keys(infoColumn[key])
if (key == 'fieldColumn') keys.push('sortNum')
infoApiKey[apiKey[key]] = keys
}
}
//默认值
const infoDefaultData = {
basics: {
fieldCode: '', fieldName: '', labelI18n: '', fieldType: 'String', queryIsDb: 'N', queryIsWeb: 'N', queryMode: 'LIKE', dictCode: '', isExport: 'Y', isShowSort: 'N',
},
}
//格式化接口初始数据
const formattingInitData = (editInfoData) => {
const optionObj = {}
for (const apiKey in infoApiKey) {
const key = apiKey
optionObj[key] = {}
editInfoData[key]?.forEach(item => optionObj[key][item.fieldCode] = item)
}
const infoData = [] as any
editInfoData.fieldList.forEach(fieldItem => {
const fieldCode = fieldItem.fieldCode
const infoItem: any = {}
for (const apiKey in infoApiKey) {
const dataKey = apiKey
if (!optionObj[dataKey]) continue
const editItem = optionObj[dataKey][fieldCode] || cloneDeep(infoDefaultData)
infoItem[`${apiKey}_id`] = editItem.id
for (const i in infoApiKey[apiKey]) {
const key = infoApiKey[apiKey][i]
if (apiKey != 'fieldList' && ['fieldCode', 'fieldName'].includes(key)) continue
infoItem[key] = editItem[key]
}
}
infoData.push(infoItem)
})
return { infoData }
}
export const pageOption = {
tableOptionColumn, customFormColumn, dataOriginObj,
infoApiKey,
reportCode_required
}
export const tableInfoOption = {
infoColumn,
infoDefaultData,
formattingInitData,
}

View File

@@ -0,0 +1,857 @@
<template>
<ContentWrap>
<div class="flex gap-x-10px">
<div class="flex-basis-180px flex-shrink-0">
<avue-tree
ref="treeRef"
v-model="treeForm"
:option="treeOption"
:data="treeData"
:permission="treePermission"
:before-open="treeBeforeOpen"
@node-contextmenu="treeNodeContextmenu"
@node-click="treeNodeClick"
@update="treeUpdate"
@save="treeSave"
@del="treeDel"
>
</avue-tree>
</div>
<avue-crud
class="table-content flex-1"
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
:before-open="beforeOpen"
:before-close="beforeClose"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu="{ size, row }">
<div class="flex justify-center flex-items-center">
<el-button
type="primary"
text
:size="size"
v-hasPermi="['jeelowcode:report:update']"
@click="menuHandle({ type: 'edit', row, index: row.$index })"
>
<Icon :size="14" icon="ep:edit-pen"></Icon>
<span>编辑</span>
</el-button>
<span
v-if="checkPermi(['jeelowcode:report:update'])"
class="ml-8px mr-8px mt-2px inline-block h-16px w-1px bg-#e8e8e8 .dark:bg-[var(--el-border-color-dark)]"
></span>
<el-dropdown @command="menuHandle">
<div class="mt--2px cursor-pointer">
<el-text :size="size" type="primary">
更多
<Icon :size="16" icon="iconamoon:arrow-down-2-light" />
</el-text>
</div>
<template #dropdown>
<el-dropdown-menu>
<template v-for="item in menuMoreList" :key="item.type">
<el-dropdown-item
v-if="!item.isShow || item.isShow()"
:command="{ type: item.type, row }"
>
{{ item.label }}
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<template #menu-form-before="{ disabled, size, type }">
<el-button
v-if="type == 'edit'"
type="primary"
:size="size"
:disabled="disabled"
:loading="saveLoading"
@click="rowUpdate(tableForm)"
>
<Icon :size="14" icon="ep:circle-check"></Icon>
<span>修改</span>
</el-button>
</template>
<template #custom_form-form>
<avue-form
class="table-design-custom-form"
ref="customFormRef"
:option="customFormOption"
v-model="tableForm"
>
<template #originButton>
<div>
<el-button
type="primary"
class="ml-2px"
@click="openDataOrigin"
:disabled="!tableForm.dataOrigin"
>
<Icon :size="14" icon="lucide:text-search"></Icon> <span>数据源SQL配置</span>
</el-button>
<el-button
type="primary"
class="ml-2px"
@click="analysisDataOrigin"
:loading="analysisLoading"
:disabled="!tableForm.originButton"
>
<Icon v-if="!analysisLoading" :size="14" icon="streamline:code-analysis"></Icon>
<span>解析配置生成字段</span>
</el-button>
</div>
</template>
<template #dataConfig="scope">
<div class="flex">
<avue-checkbox
class="flex-basis-60px flex-shrink-0"
v-model="tableForm.dataConfig"
:dic="scope.column.dicData"
></avue-checkbox>
<avue-select
class="flex-basis-160px flex-shrink-0"
v-model="authValue"
:dic="dicObj.dataConfigSelect"
:clearable="false"
></avue-select>
</div>
</template>
</avue-form>
</template>
<template #custom_info-form="scope">
<TableInfo
v-if="isTableInfo"
ref="tableInfoRef"
:formType="scope.type"
:editInfoData="editInfoData"
:size="scope.size"
></TableInfo>
</template>
</avue-crud>
</div>
<DesignPopup
v-model="dataOriginPopup.show"
title="数据源SQL配置"
width="90%"
:isBodyFull="true"
:dialogParams="{ top: '5vh' }"
:handleClose="(done) => handlePopupClose(done, 'originButton')"
>
<template #default>
<DataOriginOption
:ref="(el) => (optionRef.originButton = el)"
v-model="tableForm.originButton"
:show="dataOriginPopup.show"
:tableList="dataOriginPopup.tableList"
:viewField="dataOriginPopup.viewField"
:dbCode="tableForm.dataSourcesCode"
></DataOriginOption>
</template>
</DesignPopup>
</ContentWrap>
</template>
<script lang="ts" setup>
import { TableInfo } from './components'
import { DataOriginOption } from '../tableDesign/components'
import * as TableApi from '@/api/design/table'
import * as ReportApi from '@/api/design/report'
import { pageOption, tableInfoOption, dicObj, getDataOriginDicData } from './designData'
import { formattingLengStr } from '@/utils/lowDesign'
import { ElMessage, ElButton, ElLoading } from 'element-plus'
import { cloneDeep } from 'lodash-es'
import useCopyText from '@/hooks/design/useCopyText'
import { useRenderVxeColumn } from '../general/components/useRenderVxeColumn'
import { useGroup } from '@/hooks/design/useGroup'
import { checkPermi } from '@/utils/permission'
defineOptions({ name: 'TableDesign' })
const { copyText } = useCopyText()
useRenderVxeColumn()
const message = useMessage() // 消息弹窗
const router = useRouter() // 路由
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const saveLoading = ref(false)
const isTableInfo = ref(false)
const analysisLoading = ref(false)
//表格配置
const tableOption = reactive({
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
searchIndex: 3,
searchIcon: true,
labelSuffix: ' ',
span: 8,
dialogWidth: '100%',
dialogFullscreen: true,
editBtn: false,
delBtn: false,
border: true,
index: true,
menuWidth: 150,
updateBtnText: '修改并关闭',
column: pageOption.tableOptionColumn
})
const tableForm = ref<any>({})
const tableData = ref([])
const authValue = ref('')
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const customFormOption = ref({
labelWidth: 120,
span: 8,
menuBtn: false,
column: pageOption.customFormColumn
})
const editInfoData = ref({})
const apiDetailData = ref<any>({})
const dataOriginPopup = ref({
show: false,
tableList: [] as any[],
viewField: [] as any[]
})
const crudRef = ref()
const tableInfoRef = ref()
const customFormRef = ref()
const treeRef = ref()
const optionRef = ref<any>({})
const isUnload = ref(false)
const permission = getCurrPermi(['jeelowcode:report'])
const menuMoreList = [
{ label: '功能测试', type: 'test' },
{ label: '路由地址', type: 'address' },
{ label: '接口地址', type: 'apiUrl' },
{ label: '复制报表', type: 'copy', isShow: () => checkPermi(['jeelowcode:report:create']) },
{ label: '删除', type: 'del', isShow: () => checkPermi(['jeelowcode:report:delete']) }
]
useCrudHeight(crudRef)
const {
treeForm,
treeOption,
treeData,
groupValue,
treePermission,
treeBeforeOpen,
treeNodeContextmenu,
treeNodeClick,
getTreeData,
treeUpdate,
treeSave,
treeDel
} = useGroup(treeRef, ReportApi, () => resetChange())
const openDataOrigin = () => {
dataOriginPopup.value.tableList = []
const dataOriginObj = pageOption.dataOriginObj[tableForm.value.dataSourcesCode]
tableForm.value.dataOrigin.split(',').forEach((key) => {
const tableItem = dataOriginObj[key]
if (tableItem) dataOriginPopup.value.tableList.push(tableItem)
})
const filedData = [...tableInfoRef.value.infoData.basics, ...tableInfoRef.value.tableInfoDefault]
dataOriginPopup.value.viewField = filedData.map((item) => {
return { label: item.fieldName, value: item.fieldCode }
})
dataOriginPopup.value.show = true
}
watch(
() => authValue.value,
() => {
if (tableForm.value.dataConfig) {
tableForm.value.dataConfig = tableForm.value.dataConfig.filter(
(key) => !['authFalse', 'authTrue', 'authOpen', ''].includes(key)
)
}
if (authValue.value) tableForm.value.dataConfig.push(authValue.value)
}
)
watch(
() => tableForm.value.dataSourcesCode,
async (newVal, oldVal) => {
if (newVal && oldVal) tableForm.value.dataOrigin = []
if (newVal) {
customFormRef.value?.updateDic('dataOrigin', [])
const dicData = await getDataOriginDicData(newVal, oldVal === undefined)
setTimeout(() => {
customFormRef.value.updateDic('dataOrigin', dicData)
}, 300)
}
}
)
const tableFormVerify = (type) => {
return new Promise((resolve, reject) => {
customFormRef.value.validate((bool, done, msg) => {
done()
if (!bool) return reject(msg)
let infoData = {}
let errText = ''
let fieldCodeArr: any[string] = []
const filedData = [...tableInfoRef.value.infoData.basics]
tableInfoRef.value.tableInfoDefault.forEach((item) => {
filedData.splice(item.sortNum || 999, 0, item)
})
for (const i in filedData) {
const index = Number(i)
const item = filedData[index]
item.sortNum = index + 1
let messageText = ''
let tabKey = 'mysql'
if (!item.fieldCode || !item.fieldName) {
messageText = `<div style="line-height:24px">
<div>${!item.fieldCode ? '字段编码' : '字段名称'}必须填写</div>
<div>序号:${index + 1}</div>
</div>`
}
if (fieldCodeArr.includes(item.fieldCode)) {
messageText = `<div style="line-height:24px">
<div>
<span>字段编码重复:</span>
<span style="color:red">${item.fieldCode}</span>
</div>
<div>序号:${index + 1}</div>
</div>`
}
fieldCodeArr.push(item.fieldCode)
if (!/(^[a-zA-Z]{2}(_?[a-zA-Z0-9])*_?$)/.test(item.fieldCode)) {
messageText = `<div style="line-height:24px">
<div>
<span>字段编码不符合规范</span>
<span style="color:red">${item.fieldCode}</span>
</div>
<div>命名规则:只能由字母、数字、下划线组成;必须以字母开头;不能以单个字母加下滑线开头</div>
<div>序号:${index + 1}</div>
</div>`
}
if (messageText) {
handleVerifyError(tabKey, item._X_ROW_KEY, index)
ElMessage({ dangerouslyUseHTMLString: true, message: messageText })
errText = 'message'
break
}
for (let key in pageOption.infoApiKey) {
if (!infoData[key]) infoData[key] = []
let itemObj: any = {}
pageOption.infoApiKey[key].forEach((prop) => {
itemObj[prop] = item[prop] !== undefined ? item[prop] : ''
})
if (key == 'fieldList') {
itemObj.labelI18n = formattingLengStr(itemObj.labelI18n, itemObj.fieldName)
}
if (type == 'edit' && item[`${key}_id`]) itemObj['id'] = item[`${key}_id`]
infoData[key].push(itemObj)
}
}
if (errText) return reject(errText)
if (type == 'edit') {
infoData['delIdVo'] = {}
contrastEditData(infoData)
}
resolve(infoData)
})
})
}
const contrastEditData = (infoData) => {
for (const key in infoData) {
if (!apiDetailData.value[key] || key == 'delIdVo') break
infoData.delIdVo[key] = cloneDeep(apiDetailData.value.delIdVo[key])
infoData[key] = infoData[key].map((item) => {
const contrastObj = apiDetailData.value[key][item.id]
if (item.id) {
const index = infoData.delIdVo[key].indexOf(item.id)
if (index != -1) infoData.delIdVo[key].splice(index, 1)
if (contrastObj) {
for (const k in item) {
if (item[k] != contrastObj[k]) {
item.isModify = 'Y'
// console.log(key, k, '修改了','原:'+contrastObj[k], '新:'+item[k], )
break
}
}
}
}
return item
})
}
}
const setApiDetailData = (data) => {
apiDetailData.value = { delIdVo: {} }
for (const key in data) {
let listKey = key
if (listKey != 'report') {
if (!apiDetailData.value[listKey]) apiDetailData.value[listKey] = {}
if (!apiDetailData.value.delIdVo[listKey]) apiDetailData.value.delIdVo[listKey] = []
data[key]?.forEach((item) => {
if (item.id) {
apiDetailData.value[listKey][item.id] = item
apiDetailData.value.delIdVo[listKey].push(item.id)
}
})
}
}
for (const key in apiDetailData.value) {
if (key == 'delIdVo') break
if (!Object.keys(apiDetailData.value[key]).length) delete apiDetailData.value[key]
}
}
const handleVerifyError = (key, rowId, index) => {
const tabKey = `tab_${key}`
const vxeTableRef = tableInfoRef.value.tableRefObj[tabKey]?.vxeTableRef
if (vxeTableRef) {
tableInfoRef.value.setTabsValue(tabKey)
setTimeout(() => {
vxeTableRef.setEditRow(vxeTableRef.getRowById(rowId))
tableInfoRef.value.tableScrollIndex(key, index, index)
}, 300)
}
}
const menuHandle = async ({ type, row, index }) => {
if (type == 'edit') crudRef.value.rowEdit(row, index)
else if (type == 'test') router.push({ path: '/low/report/test/' + row.reportCode })
else if (type == 'address') showAddress(row)
else if (type == 'apiUrl') showApiUrl(row)
else if (type == 'copy') copyReport(row)
else if (type == 'del') rowDel(row)
}
const showAddress = (row) => {
const url = `report/view/` + row.reportCode
message.alert(`路由地址:</br>${url}`, '菜单的路由地址', {
confirmButtonText: '复制',
dangerouslyUseHTMLString: true,
callback: (action) => {
if (action == 'confirm') copyText(url)
}
})
}
const showApiUrl = async (row) => {
loading.value = true
const detailData = await ReportApi.getDbDetail(row.id).finally(() => (loading.value = false))
const isOpen = detailData.report.dataConfig.indexOf('authOpen') != -1
const apiList = [
{
label: '获取报表数据[post]',
value: `/jeelowcode/report-data/list/${row.reportCode}`
},
{
label: '批量获取报表数据[post]',
value: `/jeelowcode/report-data/batch/list/报表code,报表code,...`
}
]
if (isOpen) {
apiList.push({
label: '未登录时获取报表数据[post]',
value: `/jeelowcode/open/report/list/${row.reportCode}`
})
}
let list: VNode[] = []
apiList.forEach((item) => {
list.push(
h('div', { style: { marginBottom: '10px', border: ' 1px solid #eee', padding: '10px' } }, [
h('div', [
h('span', { style: { fontWeight: 600, fontSize: '14px' } }, item.label + ''),
h(
ElButton,
{ size: 'small', type: 'primary', onClick: () => copyText(item.value) },
() => '复制'
)
]),
h('div', { style: { fontSize: '12px' } }, item.value)
])
)
})
message.alert('', '接口地址', {
message: () => {
return h('div', { width: '360px' }, list)
},
confirmButtonText: '关闭',
dangerouslyUseHTMLString: true,
customStyle: { width: '384px' }
})
}
const copyReport = (row) => {
message
.prompt('新报表编码', '复制报表', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: '',
inputValidator: (value) => {
if (!value) return '请输入报表编码'
let RExp = /([a-zA-Z_$][a-zA-Z\d_$]*\.)*[a-zA-Z_$][a-zA-Z\d_$]*/
if (!RExp.test(value)) return '报表编码格式错误!'
}
})
.then(async ({ value }) => {
loading.value = true
const bool = await ReportApi.verifyReportCode(value)
if (bool) {
loading.value = false
return message.info('报表编码已存在')
}
await ReportApi.copyReportData(row.reportCode, value)
message.success('复制成功')
resetChange()
})
}
const handlePopupClose = (done, prop) => {
const str = optionRef.value[prop]?.getOptionStr() || ''
done()
setTimeout(() => {
tableForm.value[prop] = str
}, 30)
}
const setInfoBasics = (infoData) => {
const basics_id: any[] = []
const basics_defaule: any[] = []
tableInfoRef.value.infoData.basics.forEach((item) => {
if (item.only) {
if (item.fieldCode == 'id') basics_id.push(item)
else basics_defaule.push(item)
}
})
tableInfoRef.value.infoData.basics = [...basics_id, ...infoData, ...basics_defaule]
}
const analysisDataOrigin = async () => {
await message.confirm('解析后将会覆盖现有的字段配置,是否确定解析?')
analysisLoading.value = true
const dataOption = JSON.parse(tableForm.value.originButton)
const apiData: any = {}
if (dataOption.typeKey == 'custom' || tableForm.value.dataSourcesCode !== 'master') {
apiData.explainSql =
dataOption.typeKey == 'custom' ? dataOption.customSql : dataOption.executeSql
apiData.dataSourcesCode = tableForm.value.dataSourcesCode
} else {
apiData.modelList = dataOption.optionObj.select
}
TableApi.viewDataOriginAnalysis(apiData)
.then((analysisData) => {
for (const key in analysisData) {
analysisData[key] = analysisData[key]?.map((item) => {
delete item.id
delete item.dbformId
return item
})
}
let { infoData } = tableInfoOption.formattingInitData(analysisData)
infoData = infoData.map((item) => {
for (const key in item) {
if (item[key] === null || item[key] === undefined || key == 'fieldList_id') {
delete item[key]
}
}
return { ...cloneDeep(tableInfoOption.infoDefaultData.basics), ...item }
})
setInfoBasics(infoData)
message.success('解析成功')
})
.finally(() => (analysisLoading.value = false))
}
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
if (groupValue.value) searchObj['groupReportId'] = groupValue.value
for (const key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await ReportApi.getDbList(searchObj)
tableData.value = data.records
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
if (Object.keys(params).length && groupValue.value) {
treeRef.value.setCurrentKey(0)
groupValue.value = 0
}
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
isTableInfo.value = false
const groupData = cloneDeep(treeData.value)
customFormOption.value.column.groupReportId.dicData = groupData[0].children
const { reportCode } = customFormOption.value.column
if (['edit', 'view'].includes(type) && tableForm.value.id) {
loading.value = true
const data = await ReportApi.getDbDetail(tableForm.value.id)
setApiDetailData(cloneDeep(data))
data.report.tableConfig = data.report.tableConfig?.split(',') || []
data.report.dataConfig = data.report.dataConfig?.split(',') || []
editInfoData.value = data
tableForm.value = { ...data.report }
reportCode['disabled'] = true
reportCode['rules'] = []
if (tableForm.value.dataSourcesConfig) {
const dataSourcesConfig = JSON.parse(tableForm.value.dataSourcesConfig)
delete tableForm.value.dataSourcesConfig
tableForm.value.dataOrigin = dataSourcesConfig.dataOrigin
tableForm.value.originButton = JSON.stringify({
...dataSourcesConfig.optionData,
executeSql: dataSourcesConfig.executeSql
})
}
const dataConfig = tableForm.value.dataConfig
dicObj.dataConfigSelect.forEach((item) => {
if (dataConfig.includes(item.value)) authValue.value = item.value
})
loading.value = false
} else {
tableForm.value = { ...tableForm.value, dataConfig: ['page', 'authFalse'] }
reportCode['disabled'] = false
reportCode['rules'] = [{ validator: pageOption.reportCode_required, trigger: 'blur' }] as any
if (groupValue.value) tableForm.value.groupReportId = groupValue.value
authValue.value = ''
}
if (!authValue.value) authValue.value = 'authFalse'
isUnload.value = ['edit', 'add'].includes(type)
done()
//延迟显示
setTimeout(() => (isTableInfo.value = true), 300)
}
const beforeClose = async (done, type) => {
isUnload.value = false
done()
}
const handleApiFormData = (formData) => {
const form = cloneDeep(formData)
form.dataConfig = form.dataConfig.join(',')
form.groupReportId = form.groupReportId || ''
if (form.dataOrigin || form.originButton) {
const dataSourcesConfig = {
dataOrigin: form.dataOrigin || '',
executeSql: '',
optionData: {}
}
if (form.originButton) {
const originButton = JSON.parse(form.originButton)
if (form.dataOrigin) dataSourcesConfig.executeSql = originButton.executeSql
delete originButton.executeSql
dataSourcesConfig.optionData = originButton
}
delete form.dataOrigin
delete form.originButton
form.dataSourcesConfig = JSON.stringify(dataSourcesConfig)
} else form.dataSourcesConfig = ''
return form
}
/** 新增操作 */
const rowSave = async (formData, done, loading) => {
const form = handleApiFormData(formData)
tableFormVerify('add')
.then(async (infoData: object) => {
const elLoading = ElLoading.service({ fullscreen: true })
let bool = await ReportApi.saveDbData({ report: { ...form }, ...infoData }).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
elLoading.close()
})
.catch((error) => {
if (typeof error == 'object') {
let key = Object.keys(error)[0]
message.info(error[key][0].message)
} else if (error !== 'message') {
message.alert(error, '请修改', { dangerouslyUseHTMLString: true })
}
loading()
})
}
/** 编辑操作 */
const rowUpdate = async (formData, index?, done?, loading?) => {
let isGetDetail = false
if (!loading || !done) {
saveLoading.value = true
loading = () => (saveLoading.value = false)
done = () => (saveLoading.value = false)
isGetDetail = true
}
const form = handleApiFormData(formData)
tableFormVerify('edit')
.then(async (infoData: object) => {
let bool = await ReportApi.updateDbData({ report: { ...form }, ...infoData }).catch(
() => false
)
if (bool) {
if (isGetDetail) {
const data = await ReportApi.getDbDetail(form.id)
setApiDetailData(cloneDeep(data))
editInfoData.value = data
setTimeout(() => {
tableInfoRef.value.initEditInfoData()
}, 30)
}
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
})
.catch((error) => {
if (typeof error == 'object') {
let key = Object.keys(error)[0]
message.info(error[key][0].message)
} else if (error !== 'message') {
message.alert(error, '请修改', { dangerouslyUseHTMLString: true })
}
loading()
})
}
/** 删除按钮操作 */
const rowDel = async (form) => {
try {
// 删除的二次确认
await message.delConfirm()
loading.value = true
// 发起删除
await ReportApi.deleteDbData([form.id])
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
const beforeUnload = (event) => {
if (isUnload.value) return (event.returnValue = '您确定要关闭页面吗?')
}
onMounted(async () => {
window.addEventListener('beforeunload', beforeUnload)
getTableData()
getTreeData()
})
onBeforeUnmount(() => {
window.removeEventListener('beforeunload', beforeUnload)
})
</script>
<style lang="scss" scoped>
.table-content {
max-width: calc(100% - 190px);
}
</style>
<style lang="scss">
.table-design-custom-form {
.el-form-item {
margin-bottom: 18px !important;
.el-form-item {
margin-bottom: 0 !important;
}
}
}
.dark {
.table-design-option {
.view-field-content {
.content-item {
color: var(--el-text-color-seconda) !important;
background-color: var(--el-fill-color-light) !important;
}
}
.integrality-content,
.left-tree,
.option-content,
.view-field,
.view-field .title,
.alias-item {
border-color: var(--el-border-color-dark) !important;
}
.integrality-content > div {
background-color: var(--el-fill-color-light) !important;
}
}
}
</style>

View File

@@ -0,0 +1,49 @@
<template>
<ContentWrap>
<LowReport v-if="reportCode" :reportCode="reportCode" :isPermi="isPermi"></LowReport>
<div v-else>无权限访问</div>
</ContentWrap>
</template>
<script setup lang="ts">
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
const route = useRoute()
const { wsCache } = useCache()
const reportCode = ref('')
const isPermi = ref(false)
onMounted(() => {
if (route.params.code) {
//功能测试
const menus = wsCache.get(CACHE_KEY.USER).menus
const praentArr = route.meta.activeMenu?.split('/').filter((path) => path) || []
let isPermission = false
if (praentArr.length) {
praentArr[0] = '/' + praentArr[0]
let findIndex = 0
const findPath = (menuList) => {
for (const index in menuList) {
if (menuList[index].path == praentArr[findIndex]) {
if (findIndex == praentArr.length - 1) isPermission = true
else findIndex++
if (menuList[index].children) findPath(menuList[index].children)
if (isPermission) break
}
}
}
findPath(menus)
}
if (isPermission && typeof route.params.code == 'string') reportCode.value = route.params.code
} else {
const pathList = route.path.split('/')
const length = pathList.length - 1
if (pathList[length]) {
reportCode.value = pathList[length]
isPermi.value = true
}
}
})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,683 @@
<template>
<DesignPopup
v-model="popupShow"
:title="`租户权限配置 【${tableDescribe}】(${tableName}`"
:fullscreen="true"
controlType="drawer"
width="900px"
>
<div class="p-20px pt-0">
<div class="flex items-center mt-10px mb-10px">
<span class="flex-basis-50px flex-shrink-0 text-14px">租户</span>
<avue-select
class="flex-basis-300px w-300px"
v-model="currTenant"
:dic="tenantDic"
:props="{ label: 'tenantName', value: 'tenantId' }"
@change="(data) => initData(data.value)"
></avue-select>
</div>
<el-tabs v-model="tabValue" type="border-card" v-loading="loading">
<el-tab-pane label="字段权限" name="field">
<avue-crud ref="fieldCrudRef" :data="fieldData" :option="fieldOption">
<template #enableState-header="{ column }">
<div class="w-100% flex items-center justify-center gap-x-5px">
<avue-switch
v-model="fieldAllObj.open"
@click="setAll('field', ['enableState'])"
></avue-switch>
<span>{{ column.label }}</span>
</div>
</template>
<template #listIsView-header="{ column }">
<div class="w-100% flex items-center justify-center gap-x-5px" v-if="column">
<el-checkbox
v-model="fieldAllObj.list"
:indeterminate="fieldAllObj.listHalf"
@click="setAll('field', ['listIsView'])"
>
</el-checkbox>
<span>{{ column.label }}</span>
</div>
</template>
<template #formConfig-header="{ column }">
<div class="w-100% flex items-center justify-center gap-x-5px" v-if="column">
<el-checkbox
v-model="fieldAllObj.form"
:indeterminate="fieldAllObj.formHalf"
@click="setAll('field', ['formIsView', 'formIsEdit'])"
>
</el-checkbox>
<span>{{ column.label }}</span>
</div>
</template>
<template #enableState-form="{ dic, column, row }">
<avue-switch
v-model="row[column.prop]"
:dic="dic"
:beforeChange="(done) => statusDeforeChange(row, 'field', done)"
></avue-switch>
</template>
<template #listIsView-form="{ column, row }">
<el-checkbox
true-value="Y"
false-value="N"
v-model="row[column.prop]"
:disabled="row.enableState == 'N'"
@change="(val) => fieldCheckBoxChange(val, row, 'listIsView')"
>
可见
</el-checkbox>
</template>
<template #formConfig-form="{ row }">
<el-checkbox
true-value="Y"
false-value="N"
:disabled="row.enableState == 'N'"
v-model="row.formIsView"
@change="(val) => fieldCheckBoxChange(val, row, 'formIsView')"
>
可见
</el-checkbox>
<el-checkbox
true-value="Y"
false-value="N"
:disabled="row.enableState == 'N'"
v-model="row.formIsEdit"
@change="(val) => fieldCheckBoxChange(val, row, 'formIsEdit')"
>
可编辑
</el-checkbox>
</template>
</avue-crud>
</el-tab-pane>
<el-tab-pane label="按钮权限" name="btn">
<avue-crud ref="btnCrudRef" :data="btnData" :option="btnOption">
<template #enableState-header="{ column }">
<div class="w-100% flex items-center justify-center gap-x-5px">
<avue-switch v-model="btnAll" @click="setAll('btn')"></avue-switch>
<span>{{ column.label }}</span>
</div>
</template>
<template #enableState-form="{ dic, column, row }">
<avue-switch
v-model="row[column.prop]"
:dic="dic"
:beforeChange="(done) => statusDeforeChange(row, 'btn', done)"
></avue-switch>
</template>
<template #authText> 可见 </template>
</avue-crud>
</el-tab-pane>
<el-tab-pane label="数据权限" name="data">
<avue-crud
ref="dataCrudRef"
v-model="dataForm"
:data="dataData"
:option="dataOption"
:before-open="dataBeforeOpen"
:table-loading="dataLoading"
@row-save="(form, done, loading) => saveRuleData(form, done, loading)"
@row-update="(form, index, done, loading) => saveRuleData(form, done, loading)"
>
<template #menu="{ row }">
<el-button text type="danger" @click="dataRowDel(row)">
<Icon :size="14" icon="ep:delete"></Icon>
<span>删除</span>
</el-button>
</template>
<template #enableState-header="{ column }">
<div class="w-100% flex items-center justify-center gap-x-5px">
<avue-switch v-model="dataAll" @click="setAll('data')"></avue-switch>
<span>{{ column.label }}</span>
</div>
</template>
<template #enableState-form="{ dic, column, row }">
<avue-switch
v-model="row[column.prop]"
:dic="dic"
:beforeChange="(done) => statusDeforeChange(row, 'data', done)"
></avue-switch>
</template>
<template #sqlRuleValue-form="{ column, size }">
<avue-input
v-model="dataForm[column.prop]"
:size="size"
readonly
:placeholder="`请输入 ${column.label}`"
@click="openMEDialog(column, dataForm)"
></avue-input>
</template>
</avue-crud>
</el-tab-pane>
</el-tabs>
</div>
</DesignPopup>
<DesignPopup v-model="MEDialog.value" v-bind="MEDialog.params" :isBodyFull="true">
<template #title>{{ MEDialog.title }}</template>
<template #default>
<MonacoEditor v-model="MEData.value" v-bind="MEData.params"></MonacoEditor>
</template>
</DesignPopup>
</template>
<script lang="ts" setup>
import useMEDialog from '@/hooks/design/useMEDialog'
import * as Auth from '@/api/design/table/auth'
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
defineOptions({ name: 'JavaEnhance' })
const { MEDialog, MEData, openMEDialog } = useMEDialog()
interface Props {
dbformId?: string
tableName?: string
tableDescribe?: string
}
const popupShow = defineModel({ default: false, type: Boolean })
const props = defineProps<Props>()
const loading = ref(false)
const tabsOption = ref({
type: 'border-card',
column: [
{ label: '字段权限', prop: 'field' },
{ label: '按钮权限', prop: 'btn' },
{ label: '数据权限', prop: 'data' }
]
})
const tabValue = ref(tabsOption.value.column[0].prop)
const currTenant = ref('1')
const tenantDic = ref<any[]>([])
//字段权限
const fieldAllObj = ref({
open: true,
list: false,
form: false,
listHalf: false,
formHalf: false
})
const fieldOption = reactive({
border: true,
header: false,
menu: false,
rowKey: 'fieldCode',
calcHeight: 185,
column: {
enableState: {
label: '启用',
type: 'switch',
cell: true,
width: 100,
dicData: [
{ label: '', value: 'N' },
{ label: '', value: 'Y' }
]
},
fieldCode: { label: '字段编码' },
fieldName: { label: '字段名称' },
listIsView: { label: '列表控制', cell: true },
formConfig: { label: '表单控制', cell: true }
}
})
const fieldData = ref<any>([])
const filedAll = computed(() => {
const obj = {
open: true,
list: true,
form: true,
listHalf: false,
formHalf: false
}
if (fieldData.value.length) {
for (let key in fieldData.value) {
const item = fieldData.value[key]
if (item.enableState == 'Y') {
if (obj.list && item.listIsView == 'N') obj.list = false
if (obj.form && (item.formIsView == 'N' || item.formIsEdit == 'N')) obj.form = false
//设置半选
if (!obj.listHalf && item.listIsView == 'Y') obj.listHalf = true
if (!obj.formHalf && (item.formIsView == 'Y' || item.formIsEdit == 'Y')) obj.formHalf = true
} else if (obj.open) obj.open = false
}
if (obj.list) obj.listHalf = false
if (obj.form) obj.formHalf = false
} else for (const key in obj) obj[key] = false
return obj
})
watch(
() => filedAll.value,
(obj) => {
fieldAllObj.value = obj
}
)
//按钮权限
const btnAll = ref(false)
const btnOption = reactive({
border: true,
header: false,
menu: false,
rowKey: 'buttonCode',
calcHeight: 185,
column: {
enableState: {
label: '启用',
type: 'switch',
cell: true,
dicData: [
{ label: '', value: 'N' },
{ label: '', value: 'Y' }
]
},
buttonName: { label: '按钮名称' },
buttonCode: { label: '按钮编码' },
authText: { label: '权限控制' }
}
})
const btnData = ref<any>([])
watch(
() => btnData.value,
(list) => {
let bool = true
if (list.length) {
list.forEach((item) => {
if (item.enableState != 'Y') bool = false
})
} else bool = false
btnAll.value = bool
},
{ deep: true }
)
//数据权限
const dataLoading = ref(false)
const dataForm = ref<any>({ ruleCondition: '' })
const isDataForm = ref(false)
const dataAll = ref(false)
const dataOption = reactive({
border: true,
menuWidth: 150,
columnBtn: false,
refreshBtn: false,
calcHeight: 185,
column: {
enableState: {
label: '启用',
type: 'switch',
cell: true,
display: false,
width: 100,
dicData: [
{ label: '', value: 'N' },
{ label: '', value: 'Y' }
]
},
ruleName: {
label: '规则名称',
width: 200,
rules: [{ required: true, message: `请输入 规则名称`, trigger: 'blur' }]
},
ruleType: {
label: '规则类型',
type: 'radio',
value: 'field',
hide: true,
dicData: [
{ label: '字段选择', value: 'field' },
{ label: '自定义SQL', value: 'sql' }
],
control: (val) => {
const bool = val == 'field'
return {
ruleField: { display: bool },
ruleCondition: { display: bool },
ruleValue: { display: bool },
sqlRuleValue: { display: !bool }
}
}
},
ruleField: {
label: '规则字段',
type: 'select',
dicData: [],
filterable: true,
hide: true,
rules: [{ required: true, message: `请选择 规则字段`, trigger: 'change' }]
},
ruleCondition: {
label: '规则条件',
type: 'select',
rules: [{ required: true, message: `请选择 规则条件`, trigger: 'change' }],
dicData: [
{ label: '等于(=', value: 'eq' },
{ label: '不等于(!=', value: 'ne' },
{ label: `模糊LIKE "%xxx%"`, value: 'like' },
{ label: `左模糊LIKE "%xxx"`, value: 'leftLike' },
{ label: `右模糊LIKE "xxx%"`, value: 'rightLike' },
{ label: `包含IN("xxx","xxx"...)`, value: 'in' },
{ label: `不包含NOT IN("xxx","xxx"...)`, value: 'notIn' },
{ label: '为空IS NULL', value: 'isNull' },
{ label: '不为空IS NOT NULL', value: 'isNotNull' }
],
hide: true,
control: (val, form) => {
let bool = !['isNull', 'isNotNull'].includes(val)
if (form.ruleType == 'sql') bool = false
return { ruleValue: { display: bool } }
}
},
ruleValue: {
label: '规则值',
hide: true,
type: 'select',
allowCreate: true,
filterable: true,
multiple: false,
dataType: 'string',
dicData: [
{ label: '其他值请直接输入(字符串请加"双引号"', value: '-', disabled: true },
{ label: '获取当前人ID', value: '#{jeelowcode_user_id}' },
{ label: '获取当前人名称', value: '#{jeelowcode_user_nickname}' },
{ label: '获取当前人部门', value: '#{jeelowcode_user_dept}' },
{ label: '获取当前人所有部门', value: '#{jeelowcode_user_all_dept}' },
{ label: '获取当前人租户', value: '#{jeelowcode_tenant_id}' },
{ label: '获取当前日期', value: '#{jeelowcode_date}' },
{ label: '获取当前时间', value: '#{jeelowcode_time}' }
],
rules: [{ required: true, message: `请选择 规则值`, trigger: 'change' }]
},
sqlRuleValue: {
label: '规则SQL',
display: false,
hide: true,
rules: [{ required: true, message: `请输入 规则SQL`, trigger: 'blur' }]
},
ruleSql: {
label: '规则描述',
display: false
}
}
})
const dataData = ref<any>()
watch(
() => dataData.value,
(list) => {
let bool = true
if (list.length) {
list.forEach((item) => {
if (item.enableState != 'Y') bool = false
})
} else bool = false
dataAll.value = bool
},
{ deep: true }
)
watch(
() => dataForm.value.ruleCondition,
(newVal, oldVal) => {
const newBool = ['in', 'notIn'].includes(newVal)
const oldBool = ['in', 'notIn'].includes(oldVal)
if ((newBool && !oldBool) || (oldBool && !newBool)) {
if (isDataForm.value) dataForm.value.ruleValue = newBool ? [] : ''
dataOption.column.ruleValue.multiple = newBool
}
}
)
const dataCrudRef = ref()
const btnCrudRef = ref()
const fieldCrudRef = ref()
const { initTableLayout } = useCrudHeight([dataCrudRef, btnCrudRef, fieldCrudRef])
watch(
() => popupShow.value,
(val: boolean) => {
tabValue.value = tabsOption.value.column[0].prop
if (val) initData()
}
)
watch(
() => tabValue.value,
() => {
initTableLayout()
}
)
//处理权限数据
const handleSaveAuthData = (data, row, type) => {
if (type == 'field') {
data = {
formIsEdit: row.formIsEdit == 'Y' ? null : 'N',
formIsView: row.formIsView == 'Y' ? null : 'N',
listIsView: row.listIsView == 'Y' ? null : 'N',
enableState: row.enableState == 'Y' ? null : 'N',
...data,
fieldCode: row.fieldCode
}
} else if (type == 'btn') data.buttonCode = row.buttonCode
else if (type == 'data') data.ruleId = row.id
data.tenantId = currTenant.value
data.dbformId = props.dbformId
return data
}
//保存权限数据
const apiType = { field: 'saveFieldAuth', btn: 'saveButtonAuth', data: 'saveDataAuth' }
const saveAuthData = (data, row, type) => {
data = handleSaveAuthData(data, row, type)
return Auth[apiType[type]]([data]).catch(() => false)
}
//修改所有的启用状态
const statusDeforeChange = async (row, type, done) => {
const value = row.enableState == 'Y' ? 'N' : 'Y'
let data: any = { enableState: value == 'Y' ? null : value }
const res = await saveAuthData(data, row, type)
done(res ? true : false)
}
//修改字段列表、表单控制
const fieldCheckBoxChange = async (value, row, dataKey) => {
let data: any = { [dataKey]: value == 'Y' ? null : value }
const res = await saveAuthData(data, row, 'field')
if (!res) {
fieldData.value = fieldData.value.map((item) => {
if (item.fieldCode == row.fieldCode) {
item[dataKey] = item[dataKey] == 'Y' ? 'N' : 'Y'
}
return item
})
}
}
//全选控制
const setAll = async (type, keyList?) => {
const editData: any[] = []
if (type == 'field') {
const obj = { enableState: 'open', listIsView: 'list', formIsView: 'form', formIsEdit: 'form' }
fieldData.value = fieldData.value.map((item) => {
const editItem: any = {}
let bool = false
keyList.forEach((prop) => {
if (prop == 'enableState' || (prop != 'enableState' && item.enableState == 'Y')) {
const value = fieldAllObj.value[obj[prop]] ? 'N' : 'Y'
if (item[prop] !== value) {
editItem[prop] = value == 'Y' ? null : 'N'
bool = true
}
item[prop] = value
}
})
if (bool) editData.push(handleSaveAuthData(editItem, item, type))
return item
})
setTimeout(() => {
fieldAllObj.value[obj[keyList[0]]] = !fieldAllObj.value[obj[keyList[0]]]
}, 30)
} else if (type == 'btn') {
const value = btnAll.value ? 'N' : 'Y'
btnData.value = btnData.value.map((item) => {
if (item.enableState !== value) {
editData.push(handleSaveAuthData({ enableState: value == 'Y' ? null : 'N' }, item, type))
}
item.enableState = value
return item
})
setTimeout(() => {
btnAll.value = !btnAll.value
}, 30)
} else if (type == 'data') {
const value = dataAll.value ? 'N' : 'Y'
dataData.value = dataData.value.map((item) => {
if (item.enableState !== value) {
editData.push(handleSaveAuthData({ enableState: value == 'Y' ? null : 'N' }, item, type))
}
item.enableState = value
return item
})
setTimeout(() => {
dataAll.value = !dataAll.value
}, 30)
}
if (editData.length) {
loading.value = true
await Auth[apiType[type]](editData).catch(() => false)
if (type == 'field') await getFieldAuthData()
else if (type == 'btn') await getButtonAuthData()
else if (type == 'data') await getDataAuthData()
loading.value = false
}
}
const initData = async (tenantId?) => {
loading.value = true
if (tenantId) currTenant.value = tenantId
else {
currTenant.value = '1'
tabValue.value = 'field'
}
const promiseArr = [getTenantList(), getFieldAuthData(), getButtonAuthData(), getDataAuthData()]
await Promise.all(promiseArr)
loading.value = false
}
//获取所有租户
const getTenantList = () => {
return Auth.getAllTenant()
.then((data) => {
tenantDic.value = data.map((item) => {
if (item.tenantId == '1') item.tenantName = '所有租户默认权限'
return item
})
})
.catch(() => false)
}
//获取字段权限列表
const getFieldAuthData = () => {
return Auth.getFieldAuth(currTenant.value, props.dbformId)
.then((data) => {
if (data) {
fieldData.value = data.map((item) => {
item.$cellEdit = true
if (item.enableState == null) item.enableState = 'Y'
if (item.formIsEdit == null) item.formIsEdit = 'Y'
if (item.formIsView == null) item.formIsView = 'Y'
if (item.listIsView == null) item.listIsView = 'Y'
return item
})
}
})
.catch(() => false)
}
//获取按钮权限列表
const getButtonAuthData = () => {
return Auth.getButtonAuth(currTenant.value, props.dbformId)
.then((data) => {
if (data) {
btnData.value = data.map((item) => {
item.$cellEdit = true
if (item.enableState == null) item.enableState = 'Y'
return item
})
}
})
.catch(() => false)
}
//获取数据权限列表
const getDataAuthData = () => {
return Auth.getDataAuth(currTenant.value, props.dbformId)
.then((data) => {
dataData.value = (data || []).map((item) => {
item.$cellEdit = true
if (item.enableState == null) item.enableState = 'Y'
if (item.ruleType == 'sql') {
item.sqlRuleValue = item.ruleValue
item.ruleValue = ''
}
return item
})
})
.catch(() => false)
}
//保存、修改数据权限
const saveRuleData = async (form, done, loading) => {
let conditionText = ''
if (form.ruleCondition == 'eq') conditionText = `= ${form.ruleValue}`
else if (form.ruleCondition == 'ne') conditionText = `!= ${form.ruleValue}`
else if (form.ruleCondition == 'like') conditionText = `LIKE "%${form.ruleValue}%"`
else if (form.ruleCondition == 'leftLike') conditionText = `LIKE "%${form.ruleValue}"`
else if (form.ruleCondition == 'rightLike') conditionText = `LIKE "${form.ruleValue}%"`
else if (form.ruleCondition == 'in') conditionText = `IN( ${form.ruleValue} )`
else if (form.ruleCondition == 'notIn') conditionText = `NOT IN( ${form.ruleValue} )`
else if (form.ruleCondition == 'isNull') conditionText = 'IS NULL'
else if (form.ruleCondition == 'isNotNull') conditionText = 'IS NOT NULL'
form.ruleSql = form.ruleType == 'sql' ? form.sqlRuleValue : `${form.ruleField} ${conditionText}`
const data = await Auth.saveRuleData({
...form,
dbformId: props.dbformId,
ruleValue: form.ruleType == 'sql' ? form.sqlRuleValue : form.ruleValue
}).catch(() => false)
if (!data) return loading()
done()
dataLoading.value = true
await getDataAuthData()
dataLoading.value = false
}
//删除数据权限
const dataRowDel = async (row) => {
await message.delConfirm()
dataLoading.value = true
await Auth.deleteRuleData(row.id).catch(() => false)
await getDataAuthData()
dataLoading.value = false
}
const dataBeforeOpen = (done) => {
isDataForm.value = false
//设置规则字段
dataOption.column.ruleField.dicData = fieldData.value.map((item) => {
return { label: `${item.fieldName}${item.fieldCode}`, value: item.fieldCode }
})
done()
setTimeout(() => {
isDataForm.value = true
}, 300)
}
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,476 @@
<template>
<div class="config-option table-design-option">
<el-container class="h-100%">
<el-aside width="220px" class="left-tree">
<template v-for="(config, index) in listData" :key="config.title">
<div
class="flex items-center mb-10px text-14px font-600 c-#333 cursor-pointer"
@click="setCollapse(index)"
>
<Icon
v-if="collapseObj[index] || collapseObj[index] === undefined"
:size="14"
icon="ep:caret-bottom"
></Icon>
<Icon v-else :size="14" icon="ep:caret-right"></Icon>
<span class="ml-2px">{{ config.title }}</span>
</div>
<el-collapse-transition>
<div
class="ml-10px"
v-show="collapseObj[index] === undefined ? true : collapseObj[index]"
>
<draggable
class="config-content"
tag="div"
:list="config.list"
:group="{ name: 'config', pull: 'clone', put: false }"
ghost-class="config-ghost"
:sort="false"
item-key="value"
>
<template #item="{ element }">
<div class="config-item" @click="setOption(element)">
<span>{{ element.label }}</span>
</div>
</template>
</draggable>
</div>
</el-collapse-transition>
</template>
<div class="text-center pb-20px">
<el-button @click="openEditor" class="w-100%">
更多配置
<span class="text-12px c-[var(--el-color-primary)]" v-if="customStr != defStr"
>已编写</span
>
</el-button>
</div>
</el-aside>
<el-main class="main-option">
<div class="option-title table-item-row">
<div class="row-item">
<div class="cell">序号</div>
</div>
<div class="row-item text-center">
<div class="cell">配置名称</div>
</div>
<div class="row-item">
<div class="cell">配置值</div>
</div>
<div class="row-item">
<div class="cell">操作</div>
</div>
</div>
<div class="option-content">
<draggable
class="content-draggable"
:list="optionList"
:group="{ name: 'option', put: true }"
ghost-class="option-ghost"
:animation="300"
handle=".move-box"
item-key="prop"
@add="handleAddColumn"
>
<template #item="{ element, index }">
<div class="option-item table-item-row">
<div class="row-item move-box">
<div class="cell">{{ index + 1 }}</div>
</div>
<div class="row-item move-box">
<div class="cell"
><avue-text-ellipsis
:key="element.value"
:text="element.label"
:height="40"
:width="196"
use-tooltip
placement="top"
>
<template #more>
<small>...</small>
</template>
</avue-text-ellipsis>
</div>
</div>
<div class="row-item">
<div class="cell">
<template v-if="element.type == 'input' || !element.type">
<avue-input
v-model="element.val"
placeholder="请输入"
v-bind="element.params || {}"
></avue-input>
</template>
<template v-else-if="element.type == 'number'">
<avue-input-number
v-model="element.val"
v-bind="element.params || {}"
placeholder="请输入"
></avue-input-number>
</template>
<template v-else-if="element.type == 'color'">
<avue-input-color
v-model="element.val"
v-bind="element.params || {}"
placeholder="请选择"
></avue-input-color>
</template>
<template v-else-if="element.type == 'select'">
<avue-select
v-model="element.val"
v-bind="element.params || {}"
placeholder="请选择"
:dic="element.dic"
></avue-select>
</template>
<template v-else-if="element.type == 'radio'">
<avue-radio
v-model="element.val"
:dic="element.dic"
v-bind="element.params || {}"
></avue-radio>
</template>
<template v-else-if="element.type == 'switch'">
<avue-switch
v-model="element.val"
:dic="element.dic"
v-bind="element.params || {}"
></avue-switch>
</template>
<div class="tip" v-if="element.valTipShow == element.val && element.valTip">
{{ element.valTip }}
</div>
</div>
</div>
<div class="row-item">
<div class="cell">
<el-button type="danger" link text @click="delRow(element)"> 删除 </el-button>
</div>
</div>
</div>
</template>
</draggable>
</div>
</el-main>
</el-container>
<DesignPopup v-model="MEDialog.value" v-bind="MEDialog.params" :isBodyFull="true">
<template #default>
<MonacoEditor v-model="MEData.value" v-bind="MEData.params"></MonacoEditor>
</template>
</DesignPopup>
</div>
</template>
<script setup lang="ts">
import draggable from 'vuedraggable'
import { cloneDeep } from 'lodash-es'
import controlOption from '@/components/LowDesign/src/utils/controlOption'
import { handleStrObj } from '@/utils/lowDesign'
import useMEDialog from '@/hooks/design/useMEDialog'
defineOptions({ name: 'ConfigOption' })
interface Props {
configKey: Array<string>
show: boolean
}
const props = defineProps<Props>()
const optionStr = defineModel<string>({ default: '' })
const message = useMessage() // 消息弹窗
const { MEDialog, MEData, openMEDialog } = useMEDialog()
const collapseObj = ref({})
const listData = ref<any>([])
const optionList = ref<any>([])
let defStr = `return {
}`
const customStr = ref(defStr)
const optionKey = computed(() => {
return optionList.value.filter((item) => item.prop).map((item) => item.value)
})
const openEditor = () => {
openMEDialog(
{
prop: 'value',
label: '更多配置编辑',
params: {
width: '50%',
headerBtn: [
{
name: `配置参考地址`,
clickFun: () => {
window.open('https://avuejs.com/crud/crud-doc.html')
},
params: { size: 'small' }
}
]
}
},
customStr
)
}
const initFun = () => {
listData.value = []
optionList.value = []
customStr.value = defStr
if (props.configKey.length) {
props.configKey.forEach((key) => {
if (controlOption[key]) listData.value = [...listData.value, ...controlOption[key]]
})
for (const index in listData.value) collapseObj.value[index] = Number(index) == 0
}
if (listData.value.length) {
if (optionStr.value && optionStr.value.indexOf('option_str')) {
const data = cloneDeep(listData.value)
const dataObj = {}
data.forEach((typeItem) => typeItem.list.forEach((item) => (dataObj[item.value] = item)))
const { custom_str, option_str } = handleStrObj(optionStr.value) as any
if (custom_str) customStr.value = custom_str
if (option_str) {
const option = handleStrObj(option_str)
for (let key in option) {
if (dataObj[key])
optionList.value.push({
...dataObj[key],
val: option[key],
prop: `option_${Math.ceil(Math.random() * 9999999)}`
})
}
}
}
}
}
watch(
() => props.show,
(val) => {
if (val) initFun()
}
)
const setCollapse = (index) => {
for (let key in collapseObj.value) {
collapseObj.value[key] = key == index ? !collapseObj.value[key] : false
}
}
const handleAddColumn = (e) => {
const newIndex = e.newIndex
const data = cloneDeep(optionList.value[newIndex])
if (optionKey.value.includes(data.value)) {
optionList.value = optionList.value.filter((item) => item.prop)
message.info('该配置已存在')
return
}
if (!data.prop) data.prop = `option_${Math.ceil(Math.random() * 9999999)}`
delete data.icon
optionList.value[newIndex] = data
}
const setOption = (row) => {
optionList.value.push(row)
handleAddColumn({ newIndex: optionList.value.length - 1 })
setTimeout(() => {
getOptionStr()
}, 300)
}
const delRow = (row) => {
optionList.value = optionList.value.filter((item) => item.prop != row.prop)
}
const getOptionStr = () => {
if (optionList.value.length === 0 && customStr.value == defStr) {
return ''
}
let optionStr = 'return {'
const setValue = (value) => {
if (typeof value == 'string') value = `'${value}'`
if (value instanceof Array) {
let text = ''
value.forEach((item, index) => {
text = `${text}${index != 0 ? ',' : ''}${setValue(item)}`
})
return `[${text}]`
}
return value
}
optionList.value.forEach((item) => {
optionStr = `${optionStr}
"${item.value}":${setValue(item.val)},`
})
optionStr = `${optionStr}
}`
return `return {
option_str:${'`'}${optionStr}${'`'},
custom_str: ${'`'}${customStr.value}${'`'},
}`
}
onMounted(() => {
initFun()
})
defineExpose({ getOptionStr })
</script>
<style lang="scss" scoped>
.config-option {
width: 100%;
height: 100%;
margin-top: -1px;
.left-tree {
padding: 10px;
border: 1px solid #f1f1f1;
.config-item {
width: 160px;
padding: 4px 8px;
margin-bottom: 10px;
font-size: 14px;
cursor: move;
background-color: #f4f6fc;
border: 1px dashed #f4f6fc;
&:hover {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
}
}
}
.main-option {
padding: 0;
border: 1px solid #f1f1f1;
border-left: 0;
.option-content {
height: calc(100% - 41px);
.content-draggable {
height: 100%;
padding-bottom: 55px;
overflow-y: auto;
box-sizing: border-box;
&::-webkit-scrollbar {
width: 0;
}
}
.option-ghost {
position: relative;
width: 0 !important;
height: 40px;
min-width: 0 !important;
padding: 0 !important;
margin: 1px 2px 0;
overflow: hidden;
font-size: 0;
background: white;
border-left: 5px solid var(--el-color-primary);
content: '';
outline: none 0;
box-sizing: border-box;
}
}
.table-item-row {
display: flex;
align-items: end;
.row-item {
height: 100%;
min-height: 32px;
padding: 4px 0;
font-size: 14px;
line-height: 32px;
border-bottom: 1px solid #dcdfe6;
.cell {
position: relative;
padding: 0 12px;
& > div {
width: 100%;
}
.tip {
position: absolute;
bottom: -5px;
left: 0;
padding-right: 10px;
font-size: 12px;
color: #e6a23c;
text-align: right;
box-sizing: border-box;
}
}
&:nth-child(1) {
flex-basis: 60px;
text-align: center;
border-right: 1px solid #dcdfe6;
}
&:nth-child(2) {
flex-basis: 260px;
border-right: 1px solid #dcdfe6;
}
&:nth-child(3) {
flex: 1;
line-height: normal;
border-right: 1px solid #dcdfe6;
}
&:nth-child(4) {
flex-basis: 110px;
text-align: center;
}
&.move-box {
cursor: move;
}
}
}
.option-title {
background-color: #fafafa;
.row-item:nth-child(3) {
line-height: 32px;
}
}
}
.right-custom {
.custom-title {
display: flex;
align-items: center;
justify-content: center;
height: 32px;
padding: 4px 0;
font-size: 14px;
line-height: 32px;
text-align: center;
background-color: #fafafa;
border: 1px solid #dcdfe6;
}
.custom-content {
width: 100%;
height: calc(100% - 42px);
}
}
}
</style>

View File

@@ -0,0 +1,364 @@
<template>
<DesignPopup
v-model="popupShow"
:title="`自定义按钮配置 【${tableDescribe}】(${tableName}`"
:fullscreen="true"
v-model:isFull="isFull"
>
<div class="p-20px">
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:data="tableData"
:option="tableOption"
:table-loading="loading"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
@selection-change="selectionChange"
>
<template #menu-left="{ size }">
<el-button
:size="size"
type="danger"
:disabled="!tableSelect.length"
@click="rowDel(selectIds)"
>
<Icon :size="16" icon="mi:delete" class="mr-3px" />批量删除
</el-button>
</template>
<template #buttonExp-form="scope">
<avue-input
v-model="tableForm[scope.column.prop]"
:size="scope.size"
readonly
:placeholder="`请输入 ${scope.column.label}`"
@click="openMEDialog(scope.column, tableForm)"
></avue-input>
</template>
<template #buttonIcon-form="{ column, size, disabled }">
<IconSelectInput
v-model="tableForm.buttonIcon"
:prop="column.prop"
:column="column"
:size="size"
:disabled="disabled"
/>
</template>
<template #buttonIcon="{ row }">
<Icon :size="14" :icon="row.buttonIcon" />
</template>
<template #buttonI18n-form>
<I18nInput v-model="tableForm.buttonI18n" :def-name="tableForm.buttonName"></I18nInput>
</template>
</avue-crud>
</div>
<DesignPopup v-model="MEDialog.value" v-bind="MEDialog.params" :isBodyFull="true">
<template #title>{{ MEDialog.title }}</template>
<template #default>
<MonacoEditor
class="bg-#1e1e1e"
v-model="MEData.value"
v-bind="MEData.params"
></MonacoEditor>
</template>
</DesignPopup>
</DesignPopup>
</template>
<script lang="ts" setup>
import * as ButtonApi from '@/api/design/table/customButton'
import useMEDialog from '@/hooks/design/useMEDialog'
import { I18nInput } from '@/components/LowDesign/src/shareControl/index'
import { customButtonExample } from '@/components/LowDesign/src/utils/example'
import useCopyText from '@/hooks/design/useCopyText'
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
defineOptions({ name: 'CustomButton' })
interface Props {
dbformId?: string
tableName?: string
tableDescribe?: string
}
const popupShow = defineModel({ default: false, type: Boolean })
const props = defineProps<Props>()
const { copyText } = useCopyText()
const { MEDialog, MEData, openMEDialog } = useMEDialog()
const loading = ref(true) // 列表的加载中消息弹窗
const defaultBtn = [
'addBtn',
'editBtn',
'viewBtn',
'delBtn',
'importBtn',
'exportBtn',
'batchDelBtn'
]
const tableOption = reactive({
border: true,
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 12,
calcHeight: 20,
labelWidth: 100,
dialogWidth: 900,
selection: true,
viewBtn: false,
column: {
buttonName: {
label: '按钮名称',
rules: [{ required: true, message: '请输入按钮名称', trigger: 'blur' }]
},
buttonCode: {
label: '按钮编码',
rules: [
{ required: true, message: '请输入按钮编码', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (defaultBtn.includes(value)) callback(`请不要以${value}作为按钮编码`)
else callback()
},
trigger: 'blur'
}
]
},
buttonLocation: {
label: '按钮位置',
type: 'select',
clearable: false,
dicData: [
{ label: '操作列按钮', value: 'menu' },
{ label: '表格头部按钮', value: 'header' }
],
value: 'menu'
},
buttonType: {
label: '按钮类型',
type: 'select',
clearable: false,
dicData: [
{ label: '默认', value: '' },
{ label: '主要', value: 'primary' },
{ label: '成功', value: 'success' },
{ label: '信息', value: 'info' },
{ label: '警告', value: 'warning' },
{ label: '危险', value: 'danger' }
],
value: ''
},
buttonIcon: {
label: '按钮图标'
},
buttonSort: {
label: '按钮排序',
value: '1'
},
buttonI18n: {
label: '国际化配置',
row: true,
hide: true
},
buttonShow: {
label: '是否启用',
type: 'radio',
dicData: [
{ label: '启用', value: 'Y' },
{ label: '停用', value: 'N' }
],
span: 12,
value: 'Y'
},
buttonAuth: {
label: '权限控制',
type: 'radio',
labelTip: '开启后可以通过菜单权限控制显隐',
dicData: [
{ label: '启用', value: 'Y' },
{ label: '停用', value: 'N' }
],
span: 12,
value: 'N'
},
buttonExp: {
label: '其他配置',
span: 24,
hide: true,
params: {
providerType: 'tableJsEnhance',
width: '50%',
headerBtn: [
{
name: `复制配置示例`,
icon: 'solar:copy-outline',
clickFun: () => {
copyText(customButtonExample || '')
},
params: {
size: 'small',
type: 'primary'
}
}
]
}
}
}
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const isFull = ref(true)
const tableSelect = ref([])
const crudRef = ref()
const { initTableLayout, windowSize } = useCrudHeight(crudRef)
const selectIds = computed(() => {
return tableSelect.value.map((item) => item['id'])
})
const calcHeight = computed(() => {
return isFull.value ? 20 : windowSize.height.value * 0.1 + 70
})
watch(
() => popupShow.value,
(val: boolean) => {
if (val) getTableData()
}
)
watch(
() => calcHeight.value,
() => {
tableOption.calcHeight = calcHeight.value
}
)
watch(
() => isFull.value,
() => {
initTableLayout()
}
)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize,
dbformId: props.dbformId
}
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await ButtonApi.getBtnList(searchObj)
tableData.value = data.records
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
const selectionChange = (data) => {
tableSelect.value = data
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
if (['edit', 'view'].includes(type) && tableForm.value.id) {
tableForm.value = await ButtonApi.getBtnDetail(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await ButtonApi.saveBtnData({ ...form, dbformId: props.dbformId }).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await ButtonApi.updateBtnData({ ...form, dbformId: props.dbformId }).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (data) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await ButtonApi.deleteBtnData(data instanceof Array ? data : [data.id])
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>
<style lang="scss">
.button-info-dialog {
.el-dialog__body {
padding: 20px;
overflow-x: hidden;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,452 @@
<template>
<div class="formatting-option table-design-option">
<el-tabs v-model="tabsValue" tab-position="top" type="border-card">
<el-tab-pane name="group">
<template #label>
<el-radio v-model="tabsValue" label="group">JAVA表达式处理</el-radio>
</template>
<el-container class="h-100%">
<el-aside width="180px" class="left-tree">
<div class="formatting-title">
<span>配置工具</span>
</div>
<div class="formatting-draggable">
<draggable
class="formatting-content"
tag="div"
:list="optionUtil"
:group="{ name: 'config', pull: 'clone', put: false }"
ghost-class="formatting-ghost"
:sort="false"
item-key="value"
>
<template #item="{ element }">
<div class="formatting-item" @click="setOption(element)">
<span>{{ element.label }}</span>
</div>
</template>
</draggable>
</div>
</el-aside>
<el-main class="main-option">
<div class="option-content">
<draggable
class="content-draggable"
:list="formattingList"
:group="{ name: 'option', put: true }"
ghost-class="option-ghost"
:animation="300"
handle=".move-icon"
item-key="prop"
@add="handleAddColumn"
>
<template #item="{ element }">
<div class="formatting-control">
<template v-if="element.value == 'fieldSelect'">
<avue-select
class="w-200px"
v-model="element.text"
:dic="fieldList"
:class="['select_' + element.prop + '_filed']"
:style="{ width: element.width_filed || '80px' }"
placeholder="请选择 字段"
@change="() => setSelectWidth(element, 5, '_filed')"
></avue-select>
</template>
<template v-if="element.value == 'modeType'">
<avue-select
class="w-100px"
v-model="element.text"
:dic="modeType"
:class="['select_' + element.prop + '_mode']"
:style="{ width: element.width_mode || '80px' }"
placeholder="计算方式"
@change="() => setSelectWidth(element, 5, '_mode')"
></avue-select>
</template>
<template v-if="element.value == 'parenthesis'">
<avue-select
class="w-100px"
v-model="element.text"
:dic="parenthesis"
:class="['select_' + element.prop + '_par']"
:style="{ width: element.width_par || '80px' }"
placeholder="括号运算符"
@change="() => setSelectWidth(element, 5, '_par')"
></avue-select>
</template>
<template v-if="element.value == 'modeNum'">
<avue-input-number
class="w-80px!"
v-model="element.text"
:controls="false"
placeholder="计算值"
></avue-input-number>
</template>
<template v-if="element.value == 'joinStr'">
<avue-input
class="w-120px"
v-model="element.text"
placeholder="拼接字符"
></avue-input>
</template>
<div class="del-icon" @click="delRow(element)">
<Icon color="#F56C6C" :size="22" icon="lets-icons:dell-fill" class="mr-3px" />
</div>
<div class="move-icon">
<Icon :size="22" icon="mingcute:move-line" class="mr-3px" />
</div>
</div>
</template>
</draggable>
</div>
<div class="formatting-result flex h-60px box-border p-10px">
<div class="flex-basis-85px flex-shrink-0"> </div>
<div class="flex-1 flex flex-wrap gap-x-20px">
<span v-for="(item, index) in valueText" :key="index">
{{ item.type == 'CALCULATE' ? '计算' : '拼接' }}<span class="c-#409EFF">{{
item.value
}}</span>
</span>
</div>
</div>
</el-main>
</el-container>
</el-tab-pane>
<el-tab-pane name="custom" class="pl-20px pr-20px pt-10px">
<template #label>
<el-radio v-model="tabsValue" label="custom">JAVA函数处理</el-radio>
</template>
<div class="custom-box">
<div class="flex items-center mt-10px">
<div class="flex-basis-140px">JAVA类名/Sping Key</div>
<avue-input
class="flex-1"
v-model="customValue.javaPath"
placeholder="请输入 JAVA类名/Sping Key"
></avue-input>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import draggable from 'vuedraggable'
import { cloneDeep } from 'lodash-es'
defineOptions({ name: 'FormattingOption' })
interface Props {
show: boolean
fieldList: Array<any>
}
interface TextList {
value: string
type: 'CALCULATE' | 'CONCAT'
}
const props = defineProps<Props>()
const formattingStr = defineModel<string>({ default: '' })
const formattingList = ref<any>([])
const tabsValue = ref('group')
const customValue = ref({
javaPath: ''
})
const modeType = [
{ label: '加法', value: ' + ' },
{ label: '减法', value: ' - ' },
{ label: '乘法', value: ' * ' },
{ label: '除法', value: ' / ' },
{ label: '取余', value: ' % ' },
{ label: '取模', value: ' mod ' }
]
const parenthesis = [
{ label: '左括号', value: ' ( ' },
{ label: '右括号', value: ' ) ' }
]
const optionUtil = [
{ label: '字段选择', value: 'fieldSelect', type: 'CALCULATE' },
{ label: '计算方式', value: 'modeType', type: 'CALCULATE' },
{ label: '括号运算符', value: 'parenthesis', type: 'CALCULATE' },
{ label: '计算值', value: 'modeNum', type: 'CALCULATE' },
{ label: '拼接字符', value: 'joinStr', tip: '拼接字符 例如:() - 等', type: 'CONCAT' }
]
const valueText = computed(() => {
let groupText = ''
let lastType: 'CALCULATE' | 'CONCAT'
let textList: TextList[] = []
formattingList.value.forEach((item) => {
let text = item.text
if (item.value == 'fieldSelect' && text) text = '#{' + text + '}'
if (item.value == 'modeNum' && text === null) text = ''
if (!lastType || lastType == item.type) {
groupText = `${groupText}${text}`
if (!textList.length) textList.push({ value: groupText, type: item.type })
else textList[textList.length - 1].value = groupText
} else {
groupText = text
textList.push({ value: groupText, type: item.type })
}
lastType = item.type
})
return textList
})
const initFun = () => {
const optionObj = {}
optionUtil.forEach((item) => (optionObj[item.value] = item))
formattingList.value = []
if (formattingStr.value) {
const { list, custom, valueType } = JSON.parse(formattingStr.value)
customValue.value = custom || { javaPath: '' }
tabsValue.value = valueType || 'group'
if (list) {
formattingList.value = list.map((item) => {
return {
...optionObj[item.value],
...item,
prop: `option_${Math.ceil(Math.random() * 9999999)}`
}
})
}
}
}
watch(
() => props.show,
(val) => {
if (val) initFun()
}
)
const setSelectWidth = (element, num = 0, addClass = '') => {
setTimeout(() => {
const el = document.querySelector(
`.main-option .select_${element.prop}${addClass} .el-select__selected-item.el-select__placeholder span`
) as HTMLSpanElement
if (!el) return
const width = el.offsetWidth
element[`width${addClass}`] = width + 40 + num + 'px'
}, 30)
}
const handleAddColumn = (e) => {
const newIndex = e.newIndex
const data = cloneDeep(formattingList.value[newIndex])
if (!data.prop) data.prop = `option_${Math.ceil(Math.random() * 9999999)}`
if (data.value == 'modeNum') data.text = null
else data.text = ''
if (data.value == 'parenthesis') {
let valueIndex: number | null = null
const groupText = valueText.value[valueText.value.length - 1].value || ''
const lastLeft = groupText.lastIndexOf(' ( ')
const lastRight = groupText.lastIndexOf(' ) ')
if ((lastLeft == -1 && lastRight == -1) || lastRight > lastLeft) {
valueIndex = 0
} else valueIndex = 1
if (valueIndex !== null) data.text = parenthesis[valueIndex].value
}
delete data.label
formattingList.value[newIndex] = data
}
const setOption = (row) => {
formattingList.value.push(row)
handleAddColumn({ newIndex: formattingList.value.length - 1 })
setTimeout(() => {
getOptionStr()
}, 300)
}
const delRow = (row) => {
formattingList.value = formattingList.value.filter((item) => item.prop != row.prop)
}
const getOptionStr = () => {
if (formattingList.value.length || customValue.value)
return JSON.stringify({
list: formattingList.value.map((item) => {
return { value: item.value, text: item.text }
}),
valueType: tabsValue.value,
group: valueText.value.filter((item) => item.value !== ''),
custom: customValue.value
})
else ''
}
onMounted(() => {
initFun()
})
defineExpose({ getOptionStr })
</script>
<style lang="scss" scoped>
.formatting-option {
width: 100%;
height: 100%;
.left-tree {
padding: 10px;
border-right: 1px solid #f1f1f1;
.formatting-title {
display: flex;
align-items: center;
margin-bottom: 10px;
font-size: 14px;
font-weight: 600;
color: #333;
cursor: pointer;
.el-icon {
color: #666;
}
}
&:nth-last-of-type(1) {
margin-bottom: 0;
}
.formatting-draggable {
margin-left: 10px;
}
.formatting-item {
width: 120px;
padding: 4px 8px;
margin-bottom: 10px;
font-size: 14px;
cursor: move;
background-color: #f4f6fc;
border: 1px dashed #f4f6fc;
&:hover {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
}
}
}
.main-option {
padding: 0;
border-left: 0;
.option-content {
height: calc(100% - 60px);
.content-draggable {
display: flex;
height: 100%;
padding: 10px;
padding-bottom: 55px;
overflow-y: auto;
box-sizing: border-box;
flex-wrap: wrap;
gap: 10px 8px;
align-content: flex-start;
.formatting-control {
position: relative;
.del-icon {
position: absolute;
top: -9px;
left: -8px;
display: none;
cursor: pointer;
}
.move-icon {
position: absolute;
right: -14px;
bottom: -9px;
display: none;
cursor: move;
}
&:hover {
.del-icon {
display: block;
}
.move-icon {
display: block;
}
}
}
}
.option-ghost {
position: relative;
width: 0 !important;
height: 32px;
min-width: 0 !important;
padding: 0 !important;
overflow: hidden;
font-size: 0;
background: white;
border-left: 5px solid var(--el-color-primary);
content: '';
outline: none 0;
box-sizing: border-box;
}
}
.formatting-result {
border-top: 1px solid #dcdfe6;
}
}
.right-custom {
.custom-title {
display: flex;
align-items: center;
justify-content: center;
height: 32px;
padding: 4px 0;
font-size: 14px;
line-height: 32px;
text-align: center;
background-color: #fafafa;
border: 1px solid #dcdfe6;
}
.custom-content {
width: 100%;
height: calc(100% - 42px);
}
}
.custom-box {
height: calc(100% - 51px);
}
::v-deep(.el-tabs) {
height: 100%;
border: none;
.el-tabs__header {
margin-right: 0;
.el-tabs__item {
justify-content: center;
}
}
.el-tabs__content {
height: calc(100% - 39px);
padding: 0;
.el-tab-pane {
height: 100%;
}
}
}
}
</style>

View File

@@ -0,0 +1,88 @@
<template>
<vxe-table
ref="vxeTableRef"
:class="tabItem.key + '-vxe-table'"
:border="true"
size="small"
show-overflow="tooltip"
height="356"
:row-config="{ isHover: true }"
:edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }"
:data="tableData"
:scroll-y="{ enabled: true }"
:checkbox-config="checkboxConfig || {}"
:row-class-name="rowClassName"
@checkbox-all="(obj) => emit('selection-change', obj)"
@checkbox-change="(obj) => emit('selection-change', obj)"
@cell-click="(obj) => emit('cell-click', obj)"
>
<vxe-column v-if="tabItem.edit" type="checkbox" width="50" align="center"></vxe-column>
<vxe-column type="seq" width="50" align="center"></vxe-column>
<vxe-column v-if="tabItem.edit" key="customDrag" field="customDrag" width="50" align="center">
<template #default="{ rowIndex }">
<el-dropdown
trigger="hover"
placement="bottom"
@command="(obj) => emit('dropdown-command', obj)"
>
<div>
<Icon :size="20" icon="gg:menu-grid-r" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="{ index: rowIndex, type: 'up' }" :disabled="rowIndex == 0"
>向上移</el-dropdown-item
>
<el-dropdown-item
:command="{ index: rowIndex, type: 'down' }"
:disabled="rowIndex == tableData.length - 1"
>向下移</el-dropdown-item
>
<el-dropdown-item :command="{ index: rowIndex, type: 'add' }"
>插入一行</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</vxe-column>
<vxe-column v-for="(item, key) in column" :key="key" :field="key" v-bind="item"></vxe-column>
</vxe-table>
</template>
<script setup lang="ts">
defineOptions({ name: 'InfoVxeTable' })
interface Props {
tabItem: any
column: object
rowClassName?: string | Function
checkboxConfig?: object
}
const props = defineProps<Props>()
const tableData = defineModel<Array<any>>({ default: [] })
const emit = defineEmits(['selection-change', 'cell-click', 'dropdown-command'])
const vxeTableRef = ref()
defineExpose({ vxeTableRef })
</script>
<style lang="scss" scoped>
.view-vxe-table,
.exp-vxe-table {
::v-deep(.vxe-table--header) {
.vxe-header--column {
position: relative;
.vxe-cell {
.vxe-cell-title-suffix-icon {
position: absolute;
top: 5px;
right: -2px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<div class="mb-10px">
<el-button :size="size" type="primary" icon="el-icon-plus" @click.stop="emit('cell-add')">
新增
</el-button>
<el-button :size="size" type="danger" @click.stop="emit('del-data')" :disabled="!selectNum">
<Icon :size="16" icon="mi:delete" class="mr-3px" />批量删除
</el-button>
<el-button @click.stop="emit('order-data')">调整排序</el-button>
<slot></slot>
</div>
</template>
<script setup lang="ts">
defineOptions({ name: 'InfoVxeTopBtn' })
interface Props {
selectNum: number
tabItem: any
size: string
}
const props = defineProps<Props>()
const emit = defineEmits(['cell-add', 'del-data', 'order-data'])
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,379 @@
<template>
<DesignPopup
v-model="popupShow"
:title="`Java增强 【${tableDescribe}】(${tableName}`"
:fullscreen="true"
v-model:isFull="isFull"
>
<div class="p-20px">
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:data="tableData"
:option="tableOption"
:table-loading="loading"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
@selection-change="selectionChange"
@sort-change="sortChange"
>
<template #menu-left="{ size }">
<el-button
:size="size"
type="danger"
:disabled="!tableSelect.length"
@click="rowDel(selectIds)"
>
<Icon :size="16" icon="mi:delete" class="mr-3px" />批量删除
</el-button>
</template>
<template #onlineScript-form="{ size, column }">
<avue-input
type="textarea"
:minRows="3"
:maxRows="5"
v-model="tableForm[column.prop]"
:size="size"
readonly
:placeholder="`请输入 ${column.label}`"
@click="openMEDialog(column, tableForm)"
></avue-input>
</template>
</avue-crud>
</div>
<DesignPopup v-model="MEDialog.value" v-bind="MEDialog.params" :isBodyFull="true">
<template #title>{{ MEDialog.title }}</template>
<template #default>
<MonacoEditor
class="bg-#1e1e1e"
v-model="MEData.value"
v-bind="MEData.params"
></MonacoEditor>
</template>
</DesignPopup>
</DesignPopup>
</template>
<script lang="ts" setup>
import * as JavaApi from '@/api/design/table/javaEnhance'
import useMEDialog from '@/hooks/design/useMEDialog'
import { JavaDataExample } from '@/components/LowDesign/src/utils/example'
import useCopyText from '@/hooks/design/useCopyText'
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
defineOptions({ name: 'JavaEnhance' })
interface Props {
dbformId?: string
tableName?: string
tableDescribe?: string
}
const popupShow = defineModel({ default: false, type: Boolean })
const props = defineProps<Props>()
const { copyText } = useCopyText()
const { MEDialog, MEData, openMEDialog } = useMEDialog()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
border: true,
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 12,
calcHeight: 20,
labelWidth: 120,
dialogWidth: 900,
selection: true,
menuWidth: 180,
column: {
buttonCode: {
label: '页面功能',
type: 'select',
rules: [{ required: true, message: '请选择 页面功能', trigger: 'blur' }],
dicData: [
{ label: '新增', value: 'add' },
{ label: '编辑', value: 'edit' },
{ label: '详情', value: 'detail' },
{ label: '删除', value: 'delete' },
{ label: '列表', value: 'list' },
{ label: '导入', value: 'import' },
{ label: '导出', value: 'export' },
{ label: '统计', value: 'summary' }
],
sortable: true,
editDisabled: true,
width: 140,
span: 12,
control: (val) => {
let bool = false
if (typeObj.value[val] !== undefined && typeObj.value[val] >= 1) bool = true
return {
listResultHandleType: { display: bool }
}
}
},
listResultHandleType: {
label: '结果处理类型',
type: 'select',
width: 120,
rules: [{ required: true, message: '请选择 结果处理类型', trigger: 'blur' }],
dicData: [
{ label: '串行', value: 0 },
{ label: '合集', value: 1 },
{ label: '差集', value: 2 },
{ label: '并集', value: 3 },
{ label: '交集', value: 4 }
],
value: 0,
span: 12,
dataType: 'number',
display: false
},
javaType: {
label: 'Java类型',
type: 'radio',
dicData: [
{ label: 'spring-key', value: 'spring' },
{ label: 'java-class', value: 'class' },
{ label: 'http-api', value: 'http' },
{ label: '在线编辑', value: 'online_edit' }
],
value: 'spring',
width: 140,
span: 24,
control: (val) => {
const bool = val == 'online_edit'
return {
javaClassUrl: { display: !bool },
onlineScript: { display: bool }
}
}
},
javaClassUrl: {
label: 'java类路径',
rules: [{ required: true, message: '请输入 java类路径', trigger: 'blur' }],
span: 24
},
onlineScript: {
label: '在线脚本',
span: 24,
display: false,
rules: [{ required: true, message: '请输入 在线脚本', trigger: 'change' }],
params: {
language: 'java',
otherParams: {
type: 'simple',
valueStyle: 'simple',
tipKeyList: ['javaEnhance']
},
headerBtn: [
{
params: { type: 'primary', size: 'small' },
name: '在线脚本模板',
icon: 'mingcute:copy-line',
clickFun: () => {
copyText(JavaDataExample)
}
}
]
},
overHidden: true
},
activeStatus: {
label: '状态',
type: 'radio',
dicData: [
{ label: '启用', value: 'Y' },
{ label: '禁用', value: 'N' }
],
value: 'Y',
width: 100,
span: 24
},
sort: {
label: '排序',
type: 'number',
value: 1,
span: 12,
width: 100
},
remark: {
label: '备注',
span: 24
}
}
}) //表格配置
const tableForm = ref<{ id?: number; button_exp?: string }>({})
const tableData = ref<any>([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const tableSelect = ref([])
const typeObj = ref({ list: 0, export: 0 })
const tableSort = ref({ column: '', order: '' })
const isFull = ref(true)
const crudRef = ref()
const { initTableLayout, windowSize } = useCrudHeight(crudRef)
const selectIds = computed(() => {
return tableSelect.value.map((item) => item['id'])
})
const calcHeight = computed(() => {
return isFull.value ? 20 : windowSize.height.value * 0.1 + 70
})
watch(
() => popupShow.value,
(val: boolean) => {
if (val) getTableData()
}
)
watch(
() => calcHeight.value,
() => {
tableOption.calcHeight = calcHeight.value
}
)
watch(
() => isFull.value,
() => {
initTableLayout()
}
)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize,
dbformId: props.dbformId
}
if (tableSort.value.column) searchObj = { ...searchObj, ...tableSort.value }
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await JavaApi.getJavaList(searchObj)
tableData.value = data.records
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
const selectionChange = (data) => {
tableSelect.value = data
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
if (['edit', 'view'].includes(type) && tableForm.value.id) {
tableForm.value = await JavaApi.getJavaDetail(tableForm.value.id)
}
typeObj.value = { list: 0, export: 0 }
tableData.value.forEach((item) => {
if (tableForm.value.id != item.id && typeObj.value[item.buttonCode] !== undefined) {
typeObj.value[item.buttonCode]++
}
})
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await JavaApi.saveJavaData({ ...form, dbformId: props.dbformId }).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await JavaApi.updateJavaData({ ...form, dbformId: props.dbformId }).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (data) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await JavaApi.deleteJavaData(data instanceof Array ? data : [data.id])
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
const sortChange = ({ order, prop }) => {
const sortKey = { ascending: 'asc', descending: 'desc' }
tableSort.value = { order: sortKey[order], column: prop }
if (tablePage.value) tablePage.value['currentPage'] = 1
getTableData()
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>
<style lang="scss">
.button-info-dialog {
.el-dialog__body {
padding: 20px;
overflow-x: hidden;
}
}
</style>

View File

@@ -0,0 +1,317 @@
<template>
<DesignPopup
v-model="popupShow"
:title="`JS增强历史版本【${tableDescribe}】(${tableName}`"
:width="800"
v-model:isFull="isFull"
>
<div class="p-20px">
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:data="tableData"
:option="tableOption"
:table-loading="loading"
@search-change="searchChange"
@search-reset="resetChange"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
@selection-change="selectionChange"
>
<template #menu="{ size, row }">
<el-button :size="size" text type="primary" @click="handleJs('view', row)">
查看
</el-button>
<el-button :size="size" text type="primary" @click="handleJs('diff', row)">
比较
</el-button>
</template>
</avue-crud>
</div>
<DesignPopup
v-model="MEDialog.value"
v-bind="MEDialog.params"
class="js-enhance-history-dialog"
:isBodyFull="true"
>
<template #title>{{ MEDialog.title }}</template>
<template #default>
<div class="relative h-100%">
<MonacoEditor
v-if="currType == 'view'"
v-model="MEData.value"
v-bind="MEData.params"
></MonacoEditor>
<DeffEditor
ref="deffRef"
v-else
v-model="MEData.value"
v-bind="MEData.params"
></DeffEditor>
</div>
</template>
</DesignPopup>
</DesignPopup>
</template>
<script lang="ts" setup>
import * as GeneralApi from '@/api/design/general'
import * as JsApi from '@/api/design/table/jsEnhance'
import useMEDialog from '@/hooks/design/useMEDialog'
import { formatDate } from '@/utils/formatTime'
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
defineOptions({ name: 'JavaEnhance' })
interface Props {
dbformId?: string
tableName?: string
tableDescribe?: string
}
const popupShow = defineModel({ default: false, type: Boolean })
const props = defineProps<Props>()
const { MEDialog, MEData, openMEDialog } = useMEDialog()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
align: 'center',
headerAlign: 'center',
searchMenuSpan: 8,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 12,
border: true,
calcHeight: 130,
labelWidth: 100,
dialogWidth: 900,
selection: false,
header: false,
editBtn: false,
delBtn: false,
column: {
createUserName: { label: '修改人', search: true, searchSpan: 8 },
createTime: {
label: '修改时间',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
search: true,
searchType: 'date',
searchSpan: 8,
searchFormat: 'YYYY-MM-DD'
}
}
}) //表格配置
const tableForm = ref<{ id?: number; button_exp?: string }>({})
const tableData = ref([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const tableSelect = ref([])
const newJsData = ref({})
const currType = ref('')
const isFull = ref(true)
const crudRef = ref()
const deffRef = ref()
const { initTableLayout, windowSize } = useCrudHeight(crudRef)
const calcHeight = computed(() => {
return isFull.value ? 20 : windowSize.height.value * 0.1 + 70
})
watch(
() => popupShow.value,
async (val: boolean) => {
if (val) getTableData('getDetail')
},
{ immediate: true }
)
watch(
() => calcHeight.value,
() => {
tableOption.calcHeight = calcHeight.value
}
)
watch(
() => isFull.value,
() => {
initTableLayout()
}
)
const handleJs = async (type, row) => {
currType.value = type
const data = await GeneralApi.getHistoryDetail({ historyId: row.id, type: 'js' })
let params = {}
if (type == 'view') {
params = {
title: `js增强历史版本 【${row.createUserName}】(${formatDate(new Date(row.createTime))}`,
fullscreen: true,
providerType: 'tableJsEnhance',
editorOption: { readOnly: true }
}
openMEDialog({ params, prop: 'jsJson' }, data)
} else {
params = {
title: `js增强历史版本 【${row.createUserName}】(${formatDate(new Date(row.createTime))}`,
fullscreen: true,
providerType: 'tableJsEnhance',
oldValue: data.jsJson || '',
footerBtn: [
{
params: { type: 'primary' },
name: '保 存',
icon: 'mingcute:save-line',
loading: true,
clickFun: async (loading) => {
const bool = await message
.confirm('是否将【右侧】内容保存为最新的JS增强内容?')
.catch(() => false)
if (!bool) return loading()
const data = await getNewJsDetail(true)
let isLock = true
if (data && data.timeStr) {
isLock = false
const confirm = await message
.confirm(
`<div>
<div>当前增强已锁定</div>
<div style="font-size:16px"><span style="font-size:14px">锁定操作人:</span>${data.userName}</div>
<div style="font-size:16px"><span style="font-size:14px">开始锁定时间:</span>${data.timeStr}</div>
<div style="color:#E6A23C">注:请确认没有其他人在编辑后再覆盖,避免增强被多次修改</div>
</div>`,
'是否强制覆盖?',
{ dangerouslyUseHTMLString: true, confirmButtonText: '确认覆盖保存' }
)
.catch(() => false)
if (confirm !== 'confirm') return loading()
}
const upadteData = await JsApi.updateJsData({
id: data.id,
dbformId: props.dbformId,
jsType: 'js',
jsJson: deffRef.value.getValue()
}).catch(() => false)
if (upadteData !== false) {
if (isLock) await JsApi.unlockJs(props.dbformId, 'js')
message.success('保存成功')
getTableData()
}
loading()
}
},
{
params: {},
name: '关 闭',
icon: 'material-symbols:close',
clickFun: async () => {
await message.confirm('是否确认关闭?')
MEDialog.value.value = false
}
}
]
}
openMEDialog({ params, prop: 'jsJson' }, newJsData.value)
}
}
const getNewJsDetail = (lock = false) => {
return new Promise(async (resolve) => {
const data = await JsApi.getJsDetail({
dbformId: props.dbformId,
type: 'js',
lock
})
resolve(data)
}) as Promise<any>
}
/** 查询列表 */
const getTableData = async (type?) => {
if (type == 'getDetail') newJsData.value = await getNewJsDetail()
if (!newJsData.value) {
tableData.value = []
tablePage.value.total = 0
loading.value = false
return
}
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize,
type: 'js',
id: newJsData.value['id']
}
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await GeneralApi.getHistoryList(searchObj)
tableData.value = data.records
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
const selectionChange = (data) => {
tableSelect.value = data
}
</script>
<style lang="scss">
.js-enhance-history-dialog {
.monaco-scrollable-element::before {
position: absolute;
top: 5px;
right: 20px;
z-index: 999;
padding: 3px 10px;
color: #f56c6c;
border: 1px solid #f56c6c;
border-radius: 20px;
}
.editor.original .monaco-scrollable-element::before {
content: '旧版增强';
}
.editor.modified .monaco-scrollable-element::before {
content: '最新增强(可修改)';
}
}
</style>

View File

@@ -0,0 +1,502 @@
<template>
<div class="search-option table-design-option">
<el-container class="search-content">
<el-aside width="240px" class="left-tree">
<div class="search-title">
<span>字段名称</span>
</div>
<div class="search-draggable">
<draggable
class="search-content"
tag="div"
:list="filedList"
:group="{ name: 'config', pull: 'clone', put: false }"
ghost-class="search-ghost"
:search="false"
item-key="value"
>
<template #item="{ element }">
<div class="search-item" @click="setOption(element)">
<span>{{ element.label }}</span>
</div>
</template>
</draggable>
</div>
</el-aside>
<el-main class="main-option">
<div class="option-title table-item-row">
<div class="row-item">
<div class="cell">序号</div>
</div>
<div class="row-item text-center">
<div class="cell">字段名称</div>
</div>
<div class="row-item">
<div class="cell">类型</div>
</div>
<div class="row-item">
<div class="cell">where条件</div>
</div>
<div class="row-item">
<div class="cell">操作</div>
</div>
</div>
<div class="option-content">
<draggable
class="content-draggable"
:list="searchList"
:group="{ name: 'option', put: true }"
ghost-class="option-ghost"
:animation="300"
handle=".move-box"
item-key="prop"
@add="handleAddColumn"
>
<template #item="{ element, index }">
<div class="option-item table-item-row mt--2px" v-if="element.textList">
<div class="row-item move-box">
<div class="cell">{{ index + 1 }}</div>
</div>
<div class="row-item">
<div class="cell">
<avue-input
v-if="element.controlType != 'custom'"
class="flex-1"
v-model="element.textList[0].value"
placeholder="字段名称"
></avue-input>
</div>
</div>
<div class="row-item">
<div class="cell">
<avue-select
v-model="element.condition"
:dic="conditionDic"
@change="(data) => typeChange(data, index)"
></avue-select>
</div>
</div>
<div class="row-item">
<div class="cell flex items-center gap-x-6px">
<template v-if="['default', 'text'].includes(element.controlType)">
<avue-input
class="flex-1"
v-if="element.controlType != 'text'"
v-model="element.textList[2].value"
placeholder="值"
></avue-input>
</template>
<template v-else-if="element.controlType == 'between'">
<avue-input
class="flex-1"
v-if="element.controlType != 'text'"
v-model="element.textList[2].value"
placeholder="值"
></avue-input>
<span class="flex-shrink-0">{{ element.textList[3].value }}</span>
<avue-input
class="flex-1"
v-if="element.controlType != 'text'"
v-model="element.textList[4].value"
placeholder="值"
></avue-input>
</template>
<template v-else-if="element.controlType == 'in'">
<span class="flex-shrink-0">{{ element.textList[2].value }}</span>
<avue-select
v-model="element.textList[3].value"
multiple
filterable
allowCreate
:dic="[{ label: '请直接输入值', value: 'tip', disabled: true }]"
></avue-select>
<span class="flex-shrink-0">{{ element.textList[4].value }}</span>
</template>
<template v-else-if="element.controlType == 'custom'">
<el-popover
placement="bottom-start"
:popperStyle="{ width: 'auto', height: 'auto' }"
trigger="click"
>
<template #reference>
<avue-input
class="flex-1"
v-model="element.textList[0].value"
placeholder="自定义where"
readonly
></avue-input>
</template>
<div class="w-40vw h-200px">
<MonacoEditor
v-model="element.textList[0].value"
language="mysql"
:editorOption="{ minimap: false }"
/>
</div>
</el-popover>
</template>
</div>
</div>
<div class="row-item">
<div class="cell">
<el-button type="danger" link text @click="delRow(element)"> 删除 </el-button>
</div>
</div>
</div>
</template>
</draggable>
</div>
</el-main>
</el-container>
<div class="flex items-start pt-4px pb-4px pl-10px pr-10px">
<div class="flex-shrink-0">输出的where条件</div>
<div class="flex-1 flex flex-wrap gap-x-6px">
<template v-for="(item, index) in whereList" :key="index">
<span>AND</span>
<span class="c-#409EFF">{{ item }}</span>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import draggable from 'vuedraggable'
import { cloneDeep } from 'lodash-es'
defineOptions({ name: 'SearchOption' })
interface Props {
show: boolean
filedList: Array<any>
}
const props = defineProps<Props>()
const searchStr = defineModel<string>({ default: '' })
const searchList = ref<any>([])
const conditionDic = [
{ label: '>(大于)', value: 'GT' },
{ label: '>=(大于等于)', value: 'GE' },
{ label: '<(小于)', value: 'LT' },
{ label: '<=(小于等于)', value: 'LE' },
{ label: '=(等于)', value: 'EQ' },
{ label: '!=(不等于)', value: 'NE' },
{ label: 'BETWEEN范围内', value: 'BETWEEN' },
{ label: 'NOT BETWEEN不在范围内', value: 'NOT_BETWEEN' },
{ label: 'IN 指定数组内)', value: 'IN' },
{ label: 'NOT IN不在指定数组内', value: 'NOT_IN' },
{ label: 'LIKE模糊', value: 'LIKE' },
{ label: 'NOT LIKE模糊取反', value: 'NOT_LIKE' },
{ label: 'IS NULL为空', value: 'IS_NULL', type: 'text' },
{ label: 'IS NOT NULL不为空', value: 'IS_NOT_NULL', type: 'text' },
{ label: '自定义', value: 'custom', type: 'custom' }
]
const whereList = computed(() => {
if (searchList.value.length) {
const textList = getOptionStr('whereMap')
return textList
}
return ''
})
const initFun = () => {
searchList.value = []
if (searchStr.value) searchList.value = JSON.parse(searchStr.value).searchList
}
const typeChange = ({ value, item }, index) => {
const row = searchList.value[index]
const text = item.label.split('')[0]
const textList = [
{ type: 'text', value: searchList.value[index].textList[0].value },
{ type: 'text', value: text }
]
let type = ''
if (['GT', 'GE', 'LT', 'LE', 'EQ', 'NE', 'LIKE', 'NOT_LIKE'].includes(value)) {
textList.push({ type: 'input', value: '' })
type = 'default'
} else if (['IS_NULL', 'IS_NOT_NULL'].includes(value)) {
type = 'text'
} else if (['BETWEEN', 'NOT_BETWEEN'].includes(value)) {
textList.push(
{ type: 'input', value: '' },
{ type: 'text', value: 'AND' },
{ type: 'input', value: '' }
)
type = 'between'
} else if (['IN', 'NOT_IN'].includes(value)) {
textList.push(
{ type: 'text', value: '(' },
{ type: 'input', value: [] },
{ type: 'text', value: ')' }
)
type = 'in'
} else if (value == 'custom') {
row.textList = [{ type: 'text', value: '' }]
row.controlType = 'custom'
return
}
row.textList = textList
row.controlType = type
}
watch(
() => props.show,
(val) => {
if (val) initFun()
}
)
const handleAddColumn = (e) => {
const newIndex = e.newIndex
const data = cloneDeep(searchList.value[newIndex])
if (!data.prop) data.prop = `option_${Math.ceil(Math.random() * 9999999)}`
data.textList = [{ type: 'text', value: data.value }]
delete data.label
delete data.value
searchList.value[newIndex] = data
}
const setOption = (row) => {
searchList.value.push(row)
handleAddColumn({ newIndex: searchList.value.length - 1 })
}
const delRow = (row) => {
searchList.value = searchList.value.filter((item) => item.prop != row.prop)
}
const getOptionStr = (getType = 'str') => {
if (searchList.value.length) {
if (getType == 'str') {
return JSON.stringify({
searchList: searchList.value,
whereList: getWhereList()
})
} else if (getType == 'whereMap') return getWhereList()
} else ''
}
const getWhereList = () => {
const list: string[] = []
searchList.value.forEach((item) => {
let text = ''
if (item.controlType == 'custom') text = item.textList[0].value
else if (item.textList && item.textList.length) {
item.textList.forEach((child) => {
text = `${text}${text ? ' ' : ''}${
child.type == 'text' ? child.value : handleValue(item.type, child.value)
}`
})
}
list.push(text)
})
return list
}
const handleValue = (filedType, value) => {
let whereType = 'string'
const valueType = value instanceof Array ? 'array' : 'string'
const setValueFormat = (val) => {
let text = val
if (whereType == 'string' && val.indexOf(`'`) == -1) {
text = `'${val}'`
}
return text
}
if (['BigInt', 'BigDecimal', 'Integer'].includes(filedType)) whereType = 'number'
if (valueType == 'string') value = [value]
value = value.map((item) => setValueFormat(item)).join(',')
return value
}
onMounted(() => {
initFun()
})
defineExpose({ getOptionStr })
</script>
<style lang="scss" scoped>
.search-option {
width: 100%;
height: 100%;
margin-top: -1px;
.search-content {
height: calc(100% - 60px);
}
.left-tree {
padding: 10px;
border: 1px solid #f1f1f1;
.search-title {
display: flex;
align-items: center;
margin-bottom: 10px;
font-size: 14px;
font-weight: 600;
color: #333;
cursor: pointer;
.el-icon {
color: #666;
}
}
&:nth-last-of-type(1) {
margin-bottom: 0;
}
.search-draggable {
margin-left: 10px;
}
.search-item {
width: 180px;
padding: 4px 8px;
margin-bottom: 10px;
font-size: 14px;
cursor: move;
background-color: #f4f6fc;
border: 1px dashed #f4f6fc;
&:hover {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
}
}
}
.main-option {
padding: 0;
border: 1px solid #f1f1f1;
border-left: 0;
.option-content {
height: calc(100% - 41px);
.content-draggable {
height: 100%;
padding-bottom: 55px;
overflow-y: auto;
box-sizing: border-box;
&::-webkit-scrollbar {
width: 0;
}
}
.option-ghost {
position: relative;
width: 0 !important;
height: 40px;
min-width: 0 !important;
padding: 0 !important;
margin: 1px 2px 0;
overflow: hidden;
font-size: 0;
background: white;
border-left: 5px solid var(--el-color-primary);
content: '';
outline: none 0;
box-sizing: border-box;
}
}
.table-item-row {
display: flex;
.row-item {
display: flex;
align-items: center;
height: auto;
min-height: 32px;
padding: 4px 0;
font-size: 14px;
line-height: 32px;
border-bottom: 1px solid #dcdfe6;
.cell {
flex: 1;
padding: 0 12px;
& > div {
width: 100%;
}
.reg-exp-input {
::v-deep(.el-input) {
.el-input-group__prepend,
.el-input-group__append {
padding: 0 8px !important;
}
}
}
}
&:nth-child(1) {
flex-basis: 60px;
text-align: center;
border-right: 1px solid #dcdfe6;
}
&:nth-child(2) {
flex-basis: 230px;
border-right: 1px solid #dcdfe6;
}
&:nth-child(3) {
flex-basis: 180px;
line-height: normal;
text-align: center;
border-right: 1px solid #dcdfe6;
}
&:nth-child(4) {
flex: 1;
border-right: 1px solid #dcdfe6;
}
&:nth-child(5) {
flex-basis: 110px;
text-align: center;
}
&.move-box {
cursor: move;
}
}
}
.option-title {
background-color: #fafafa;
.row-item:nth-child(4) {
line-height: 32px;
text-align: center;
}
}
}
.right-custom {
.custom-title {
display: flex;
align-items: center;
justify-content: center;
height: 32px;
padding: 4px 0;
font-size: 14px;
line-height: 32px;
text-align: center;
background-color: #fafafa;
border: 1px solid #dcdfe6;
}
.custom-content {
width: 100%;
height: calc(100% - 42px);
}
}
}
</style>

View File

@@ -0,0 +1,339 @@
<template>
<div class="sort-option table-design-option">
<el-container class="h-100%">
<el-aside width="240px" class="left-tree">
<div class="sort-title">
<span>字段名称</span>
</div>
<div class="sort-draggable">
<draggable
class="sort-content"
tag="div"
:list="filedList"
:group="{ name: 'config', pull: 'clone', put: false }"
ghost-class="sort-ghost"
:sort="false"
item-key="value"
>
<template #item="{ element }">
<div class="sort-item" @click="setOption(element)">
<span>{{ element.label }}</span>
</div>
</template>
</draggable>
</div>
</el-aside>
<el-main class="main-option">
<div class="option-title table-item-row">
<div class="row-item">
<div class="cell">序号</div>
</div>
<div class="row-item text-center">
<div class="cell">字段名称</div>
</div>
<div class="row-item">
<div class="cell">排序方式</div>
</div>
<div class="row-item">
<div class="cell">操作</div>
</div>
</div>
<div class="option-content">
<draggable
class="content-draggable"
:list="sortList"
:group="{ name: 'option', put: true }"
ghost-class="option-ghost"
:animation="300"
handle=".move-box"
item-key="column"
@add="handleAddColumn"
>
<template #item="{ element, index }">
<div class="option-item table-item-row mt--2px">
<div class="row-item move-box">
<div class="cell">{{ index + 1 }}</div>
</div>
<div class="row-item move-box">
<div class="cell">
<avue-text-ellipsis
:key="element.value"
:text="filedObj[element.column]"
:height="40"
:width="220"
use-tooltip
placement="top"
>
<template #more>
<small>...</small>
</template>
</avue-text-ellipsis>
</div>
</div>
<div class="row-item">
<div class="cell">
<avue-select v-model="element.order" :dic="dicObj.orderByType"></avue-select>
</div>
</div>
<div class="row-item">
<div class="cell">
<el-button type="danger" link text @click="delRow(element)"> 删除 </el-button>
</div>
</div>
</div>
</template>
</draggable>
</div>
</el-main>
</el-container>
</div>
</template>
<script setup lang="ts">
import draggable from 'vuedraggable'
import { cloneDeep } from 'lodash-es'
import { dicObj } from '@/views/lowdesign/tableDesign/designData'
defineOptions({ name: 'SortOption' })
interface Props {
show: boolean
filedList: Array<any>
}
const props = defineProps<Props>()
const sortStr = defineModel<string>({ default: '' })
const message = useMessage() // 消息弹窗
const sortList = ref<any>([])
const sortPropList = computed(() => {
return sortList.value.filter((item) => item.column).map((item) => item.column)
})
const filedObj = computed(() => {
const obj = {}
props.filedList.forEach((item) => (obj[item.value] = item.label))
return obj
})
const initFun = () => {
sortList.value = []
if (sortStr.value) sortList.value = JSON.parse(sortStr.value)
}
watch(
() => props.show,
(val) => {
if (val) initFun()
}
)
const handleAddColumn = (e) => {
const newIndex = e.newIndex
const data = cloneDeep(sortList.value[newIndex])
if (sortPropList.value.includes(data.value)) {
sortList.value = sortList.value.filter((item) => item.column)
message.info('该字段已存在')
return
}
data.order = 'asc'
data.column = data.value
delete data.value
delete data.label
sortList.value[newIndex] = data
}
const setOption = (row) => {
sortList.value.push(row)
handleAddColumn({ newIndex: sortList.value.length - 1 })
setTimeout(() => {
getOptionStr()
}, 300)
}
const delRow = (row) => {
sortList.value = sortList.value.filter((item) => item.column != row.column)
}
const getOptionStr = () => {
if (sortList.value.length) return JSON.stringify(sortList.value)
else ''
}
onMounted(() => {
initFun()
})
defineExpose({ getOptionStr })
</script>
<style lang="scss" scoped>
.sort-option {
width: 100%;
height: 100%;
margin-top: -1px;
.left-tree {
padding: 10px;
border: 1px solid #f1f1f1;
.sort-title {
display: flex;
align-items: center;
margin-bottom: 10px;
font-size: 14px;
font-weight: 600;
color: #333;
cursor: pointer;
.el-icon {
color: #666;
}
}
&:nth-last-of-type(1) {
margin-bottom: 0;
}
.sort-draggable {
margin-left: 10px;
}
.sort-item {
width: 180px;
padding: 4px 8px;
margin-bottom: 10px;
font-size: 14px;
cursor: move;
background-color: #f4f6fc;
border: 1px dashed #f4f6fc;
&:hover {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
}
}
}
.main-option {
padding: 0;
border: 1px solid #f1f1f1;
border-left: 0;
.option-content {
height: calc(100% - 41px);
.content-draggable {
height: 100%;
padding-bottom: 55px;
overflow-y: auto;
box-sizing: border-box;
&::-webkit-scrollbar {
width: 0;
}
}
.option-ghost {
position: relative;
width: 0 !important;
height: 40px;
min-width: 0 !important;
padding: 0 !important;
margin: 1px 2px 0;
overflow: hidden;
font-size: 0;
background: white;
border-left: 5px solid var(--el-color-primary);
content: '';
outline: none 0;
box-sizing: border-box;
}
}
.table-item-row {
display: flex;
align-items: end;
.row-item {
height: 100%;
min-height: 32px;
padding: 4px 0;
font-size: 14px;
line-height: 32px;
border-bottom: 1px solid #dcdfe6;
.cell {
padding: 0 12px;
& > div {
width: 100%;
}
.reg-exp-input {
::v-deep(.el-input) {
.el-input-group__prepend,
.el-input-group__append {
padding: 0 8px !important;
}
}
}
}
&:nth-child(1) {
flex-basis: 60px;
text-align: center;
border-right: 1px solid #dcdfe6;
}
&:nth-child(2) {
flex-basis: 250px;
border-right: 1px solid #dcdfe6;
}
&:nth-child(3) {
flex: 1;
line-height: normal;
border-right: 1px solid #dcdfe6;
}
&:nth-child(4) {
flex-basis: 110px;
text-align: center;
}
&.move-box {
cursor: move;
}
}
}
.option-title {
background-color: #fafafa;
.row-item:nth-child(3) {
line-height: 32px;
}
}
}
.right-custom {
.custom-title {
display: flex;
align-items: center;
justify-content: center;
height: 32px;
padding: 4px 0;
font-size: 14px;
line-height: 32px;
text-align: center;
background-color: #fafafa;
border: 1px solid #dcdfe6;
}
.custom-content {
width: 100%;
height: calc(100% - 42px);
}
}
}
</style>

View File

@@ -0,0 +1,366 @@
<template>
<DesignPopup
v-model="popupShow"
:title="`SQL增强 【${tableDescribe}】(${tableName}`"
:fullscreen="true"
v-model:isFull="isFull"
>
<div class="p-20px">
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:data="tableData"
:option="tableOption"
:table-loading="loading"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
@selection-change="selectionChange"
@sort-change="sortChange"
>
<template #menu-left="{ size }">
<el-button
:size="size"
type="danger"
:disabled="!tableSelect.length"
@click="rowDel(selectIds)"
>
<Icon :size="16" icon="mi:delete" class="mr-3px" />批量删除
</el-button>
</template>
<template #buttonCode-form="{ size, column }">
<div class="flex">
<avue-select
v-model="tableForm[column.prop]"
:size="size"
:placeholder="`请选择 ${column.label}`"
:dic="column.dicData"
></avue-select>
<div
class="absolute left-110% whitespace-nowrap c-#be123c"
v-if="tableForm[column.prop]"
>
<span v-if="['add', 'edit', 'delete', 'import'].includes(tableForm[column.prop])">
支持新增删除修改sql语句
</span>
<span v-else>支持查询sql语句</span>
</div>
</div>
</template>
<template #executeSql-form="{ size, column }">
<avue-input
type="textarea"
:minRows="3"
:maxRows="5"
v-model="tableForm[column.prop]"
:size="size"
readonly
:placeholder="`请输入 ${column.label}`"
@click="openMEDialog(column, tableForm)"
></avue-input>
</template>
</avue-crud>
</div>
<DesignPopup v-model="MEDialog.value" v-bind="MEDialog.params" :isBodyFull="true">
<template #title>{{ MEDialog.title }}</template>
<template #default>
<el-container class="h-100%">
<el-main class="p-0!">
<MonacoEditor
class="bg-#1e1e1e"
style="flex: 1"
v-model="MEData.value"
v-bind="MEData.params"
></MonacoEditor>
</el-main>
<el-aside width="430px">
<TipView v-bind="MEDialog.otherParams"></TipView>
</el-aside>
</el-container>
</template>
</DesignPopup>
</DesignPopup>
</template>
<script lang="ts" setup>
import { TipView } from '../../general/components/index'
import * as SqlApi from '@/api/design/table/qslEnhance'
import useMEDialog from '@/hooks/design/useMEDialog'
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
defineOptions({ name: 'SqlEnhance' })
interface Props {
dbformId?: string
tableName?: string
tableDescribe?: string
}
const popupShow = defineModel({ default: false, type: Boolean })
const props = defineProps<Props>()
const { MEDialog, MEData, openMEDialog } = useMEDialog()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
border: true,
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 12,
calcHeight: 20,
labelWidth: 120,
dialogWidth: 900,
selection: true,
column: {
buttonCode: {
label: '页面功能',
type: 'select',
rules: [{ required: true, message: '请选择 页面功能', trigger: 'blur' }],
dicData: [
{ label: '新增', value: 'add' },
{ label: '编辑', value: 'edit' },
{ label: '详情', value: 'detail' },
{ label: '删除', value: 'delete' },
{ label: '列表', value: 'list' },
{ label: '导入', value: 'import' },
{ label: '导出', value: 'export' }
],
sortable: true,
editDisabled: true,
width: 140,
span: 12,
row: true,
control: (val) => {
let bool = false
if (typeObj.value[val] !== undefined && typeObj.value[val] >= 1) bool = true
return {
listResultHandleType: { display: bool }
}
}
},
listResultHandleType: {
label: '结果处理类型',
type: 'select',
width: 120,
rules: [{ required: true, message: '请选择 结果处理类型', trigger: 'blur' }],
dicData: [
{ label: '串行', value: 0 },
{ label: '合集', value: 1 },
{ label: '差集', value: 2 },
{ label: '并集', value: 3 },
{ label: '交集', value: 4 }
],
value: 0,
span: 12,
dataType: 'number',
display: false
},
executeSql: {
label: 'SQL增强',
span: 24,
rules: [{ required: true, message: '请输入 SQL增强', trigger: 'change' }],
params: {
language: 'mysql',
otherParams: {
type: 'simple',
valueStyle: 'simple',
tipKeyList: ['SqlEnhance']
}
},
minWidth: 140,
overHidden: true
},
activeStatus: {
label: '状态',
type: 'radio',
dicData: [
{ label: '启用', value: 'Y' },
{ label: '禁用', value: 'N' }
],
value: 'Y',
width: 100,
span: 24
},
sort: {
label: '排序',
type: 'number',
value: 1,
span: 12,
width: 100
},
remark: {
label: '备注',
span: 24,
minWidth: 120
}
}
}) //表格配置
const tableForm = ref<{ id?: number; button_exp?: string }>({})
const tableData = ref<any>([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const tableSelect = ref([])
const typeObj = ref({ list: 0, export: 0 })
const tableSort = ref({ column: '', order: '' })
const isFull = ref(true)
const crudRef = ref()
const { initTableLayout, windowSize } = useCrudHeight(crudRef)
const selectIds = computed(() => {
return tableSelect.value.map((item) => item['id'])
})
const calcHeight = computed(() => {
return isFull.value ? 20 : windowSize.height.value * 0.1 + 70
})
watch(
() => popupShow.value,
(val: boolean) => {
if (val) getTableData()
}
)
watch(
() => calcHeight.value,
() => {
tableOption.calcHeight = calcHeight.value
}
)
watch(
() => isFull.value,
() => {
initTableLayout()
}
)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize,
dbformId: props.dbformId
}
if (tableSort.value.column) searchObj = { ...searchObj, ...tableSort.value }
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await SqlApi.getSqlList(searchObj)
tableData.value = data.records
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
const selectionChange = (data) => {
tableSelect.value = data
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
if (['edit', 'view'].includes(type) && tableForm.value.id) {
tableForm.value = await SqlApi.getSqlDetail(tableForm.value.id)
}
typeObj.value = { list: 0, export: 0 }
tableData.value.forEach((item) => {
if (tableForm.value.id != item.id && typeObj.value[item.buttonCode] !== undefined) {
typeObj.value[item.buttonCode]++
}
})
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await SqlApi.saveSqlData({ ...form, dbformId: props.dbformId }).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await SqlApi.updateSqlData({ ...form, dbformId: props.dbformId }).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (data) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await SqlApi.deleteSqlData(data instanceof Array ? data : [data.id])
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
const sortChange = ({ order, prop }) => {
const sortKey = { ascending: 'asc', descending: 'desc' }
tableSort.value = { order: sortKey[order], column: prop }
if (tablePage.value) tablePage.value['currentPage'] = 1
getTableData()
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>
<style lang="scss">
.button-info-dialog {
.el-dialog__body {
padding: 20px;
overflow-x: hidden;
}
}
</style>

View File

@@ -0,0 +1,886 @@
<template>
<div class="summary-top-option table-design-option" :class="{ full: type == 'virtualSql' }">
<div class="formatting-content flex items-center" v-if="type == 'summaryTop'">
<div>统计控件</div>
<div class="flex-basis-90px mr-10px">
<avue-select
v-model="summaryControl"
:dic="dicObj.summaryControl"
:clearable="false"
></avue-select>
</div>
<div class="flex-1 flex items-center" v-if="summaryControl == 'text'">
<div class="flex-basis-145px">返回值格式化配置</div>
<el-popover placement="bottom-start" :width="400">
<template #reference>
<el-input
@keydown.space="(e) => e.stopPropagation()"
v-model="formattingObj.text"
></el-input>
</template>
<div>
<div class="text-16px">示例 </div>
<div class="line-height-32px">
<div>
SQL返回值为
<span class="c-#409EFF">[{sex: "男", cou: 2}, {sex: "女", cou: 3}]</span>
</div>
<div>
格式化配置为
<span class="c-#409EFF">#{sex}#{cou}</span>
</div>
<div>
返回值会被格式化为
<span class="c-#409EFF">["男2人","女3人"]</span>
</div>
</div>
</div>
</el-popover>
</div>
<div class="flex-1 flex items-center" v-else-if="summaryControl == 'card'">
<div class="flex-basis-70px flex-shrink-0">名称配置</div>
<el-popover placement="bottom-start" :width="400">
<template #reference>
<el-input
@keydown.space="(e) => e.stopPropagation()"
v-model="formattingObj.card_name"
></el-input>
</template>
<div>
<div class="text-16px">示例 </div>
<div class="line-height-32px">
<div>
SQL返回值为
<span class="c-#409EFF">[{sex: "男", cou: 2}, {sex: "女", cou: 3}]</span>
</div>
<div>
名称配置为
<span class="c-#409EFF">#{sex}</span>
</div>
<div>
名称显示为
<span class="c-#409EFF">男生 女生</span>
</div>
</div>
</div>
</el-popover>
<div class="flex-basis-70px flex-shrink-0 ml-10px">值配置</div>
<el-popover placement="bottom-start" :width="400">
<template #reference>
<el-input
@keydown.space="(e) => e.stopPropagation()"
v-model="formattingObj.card_value"
></el-input>
</template>
<div>
<div class="text-16px">示例 </div>
<div class="line-height-32px">
<div>
SQL返回值为
<span class="c-#409EFF">[{sex: "男", cou: 2}, {sex: "女", cou: 3}]</span>
</div>
<div>
值配置为
<span class="c-#409EFF">#{cou}</span>
</div>
<div>
值显示为
<span class="c-#409EFF">3 2</span>
</div>
</div>
</div>
</el-popover>
</div>
<div class="flex-1 flex items-center" v-else-if="['line', 'bar'].includes(summaryControl)">
<div class="flex-basis-70px flex-shrink-0">X轴配置</div>
<el-popover placement="bottom-start" :width="400">
<template #reference>
<el-input
@keydown.space="(e) => e.stopPropagation()"
v-model="formattingObj[`${summaryControl}_x`]"
></el-input>
</template>
<div>
<div class="text-16px">示例 </div>
<div class="line-height-32px">
<div>
SQL返回值为
<span class="c-#409EFF">[{sex: "男", cou: 2}, {sex: "女", cou: 3}]</span>
</div>
<div>
X轴配置为
<span class="c-#409EFF">#{sex}</span>
</div>
<div>
X轴显示为
<span class="c-#409EFF">男生 女生</span>
</div>
</div>
</div>
</el-popover>
<div class="flex-basis-70px flex-shrink-0 ml-10px">Y轴配置</div>
<el-popover placement="bottom-start" :width="400">
<template #reference>
<el-input
@keydown.space="(e) => e.stopPropagation()"
v-model="formattingObj[`${summaryControl}_y`]"
></el-input>
</template>
<div>
<div class="text-16px">示例 </div>
<div class="line-height-32px">
<div>
SQL返回值为
<span class="c-#409EFF">[{sex: "男", cou: 2}, {sex: "女", cou: 3}]</span>
</div>
<div>
Y轴配置为
<span class="c-#409EFF">#{cou}</span>
</div>
<div>
Y轴显示为
<span class="c-#409EFF">3 2</span>
</div>
</div>
</div>
</el-popover>
<div class="flex-basis-95px flex-shrink-0 ml-10px">Y轴单位名称</div>
<el-input
class="flex-basis-80px flex-shrink-0"
@keydown.space="(e) => e.stopPropagation()"
v-model="formattingObj[`${summaryControl}_y_unit`]"
></el-input>
</div>
<div class="flex-1 flex items-center" v-else-if="summaryControl == 'pie'">
<div class="flex-basis-70px flex-shrink-0">名称配置</div>
<el-popover placement="bottom-start" :width="400">
<template #reference>
<el-input
@keydown.space="(e) => e.stopPropagation()"
v-model="formattingObj.pie_name"
></el-input>
</template>
<div>
<div class="text-16px">示例 </div>
<div class="line-height-32px">
<div>
SQL返回值为
<span class="c-#409EFF">[{sex: "男", cou: 2}, {sex: "女", cou: 3}]</span>
</div>
<div>
名称配置为
<span class="c-#409EFF">#{sex}</span>
</div>
<div>
名称显示为
<span class="c-#409EFF">男生 女生</span>
</div>
</div>
</div>
</el-popover>
<div class="flex-basis-70px flex-shrink-0 ml-10px">值配置</div>
<el-popover placement="bottom-start" :width="400">
<template #reference>
<el-input
@keydown.space="(e) => e.stopPropagation()"
v-model="formattingObj.pie_value"
></el-input>
</template>
<div>
<div class="text-16px">示例 </div>
<div class="line-height-32px">
<div>
SQL返回值为
<span class="c-#409EFF">[{sex: "男", cou: 2}, {sex: "女", cou: 3}]</span>
</div>
<div>
值配置为
<span class="c-#409EFF">#{cou}</span>
</div>
<div>
值显示为
<span class="c-#409EFF">3 2</span>
</div>
</div>
</div>
</el-popover>
<div class="flex-basis-100px flex-shrink-0 ml-10px">值的单位名称</div>
<el-input
class="flex-basis-80px flex-shrink-0"
@keydown.space="(e) => e.stopPropagation()"
v-model="formattingObj.pie_unit"
></el-input>
</div>
</div>
<el-tabs v-model="tabsValue" tab-position="top" type="border-card">
<el-tab-pane name="group">
<template #label>
<el-radio v-model="tabsValue" label="group">{{ typeLabel }}SQL配置</el-radio>
</template>
<el-container class="h-100%">
<el-aside width="190px" class="left-tree">
<div class="summary-title">
<span>配置工具</span>
</div>
<div class="summary-draggable">
<draggable
class="summary-content"
tag="div"
:list="optionUtil"
:group="{ name: 'config', pull: 'clone', put: false }"
ghost-class="summary-ghost"
:sort="false"
item-key="value"
>
<template #item="{ element }">
<div class="summary-item">
<span>{{ element.label }}</span>
</div>
</template>
</draggable>
</div>
</el-aside>
<el-main class="main-option">
<div
v-for="(item, index) in optionsList"
:key="index"
:class="{ 'option-box': item.type != 'text' }"
>
<div v-if="item.type == 'text'" class="default-text">{{ item.value }}</div>
<div v-else class="h-100%">
<div class="option-content" :class="{ full: !sqlObj[item.type].length }">
<draggable
:class="`content-draggable-${item.type}`"
:list="sqlObj[item.type]"
:group="{ name: 'option', put: true }"
ghost-class="option-ghost"
:animation="300"
handle=".move-icon"
item-key="prop"
@add="(e) => handleAddColumn(item.type, e)"
>
<template #item="{ element }">
<div class="summary-control">
<template v-if="element.value == 'fieldSelect'">
<div class="summary-fun-content flex items-center">
<avue-select
:class="['select_' + element.prop]"
:style="{ width: element.width || '120px' }"
v-model="element.text"
:dic="fieldList"
placeholder="字段"
@change="() => setSelectWidth(element, 5)"
></avue-select>
<div class="w-20px text-14px text-center" v-show="element.alias"
>AS</div
>
<avue-input
class="alias w-80px"
v-model="element.alias"
placeholder="别名"
></avue-input>
</div>
</template>
<template v-if="element.value == 'summaryFun'">
<div class="summary-fun-content flex items-center">
<avue-select
:class="['select_' + element.prop + '_summary']"
:style="{ width: element.width_summary || '100px' }"
v-model="element.text"
:dic="summaryType"
placeholder="统计函数"
filterable
allowCreate
@change="() => setSelectWidth(element, 3, '_summary')"
></avue-select>
<div
class="flex items-center"
v-show="element.text && summaryTypeValueList.includes(element.text)"
>
<div class="w-20px text-16px text-center mt--5px">(</div>
<avue-select
:class="['select_' + element.prop + '_filed']"
:style="{ width: element.width_filed || '80px' }"
class="summary_filed-select"
v-model="element.params"
:dic="fieldList"
placeholder="字段"
@change="() => setSelectWidth(element, -19, '_filed')"
></avue-select>
<div class="w-20px text-16px text-center mt--5px">)</div>
<div class="w-20px text-14px text-center" v-show="element.alias"
>AS</div
>
<avue-input
class="alias w-80px"
v-model="element.alias"
placeholder="别名"
></avue-input>
</div>
</div>
</template>
<template v-for="control in controlList" :key="control.type">
<template v-if="control.type == element.value">
<div class="border-box flex items-center">
<div class="pr-10px">{{ control.value }}</div>
<el-popover
placement="bottom-start"
:popperStyle="{ width: 'auto', height: 'auto' }"
trigger="click"
>
<template #reference>
<div v-if="element.text" class="text-14px pr-10px cursor-pointer">
{{ element.text }}
</div>
<div v-else class="text-12px c-#999">
{{ control.tip }}
</div>
</template>
<div class="w-40vw h-200px">
<MonacoEditor
v-model="element.text"
language="mysql"
:editorOption="{ minimap: false }"
/>
</div>
</el-popover>
</div>
</template>
</template>
<template v-if="element.value == 'custom'">
<div class="border-box">
<el-popover
placement="bottom-start"
:popperStyle="{ width: 'auto', height: 'auto' }"
trigger="click"
>
<template #reference>
<div v-if="element.text" class="text-14px pr-10px cursor-pointer">
{{ element.text }}
</div>
<div v-else class="text-12px c-#999"> 点击输入自定义SQL</div>
</template>
<div class="w-40vw h-200px">
<MonacoEditor
v-model="element.text"
language="mysql"
:editorOption="{ minimap: false }"
/>
</div>
</el-popover>
</div>
</template>
<div class="del-icon" @click="delRow(item.type, element)">
<Icon
color="#F56C6C"
:size="22"
icon="lets-icons:dell-fill"
class="mr-3px"
/>
</div>
<div class="move-icon" @click="delRow(item.type, element)">
<Icon :size="22" icon="mingcute:move-line" class="mr-3px" />
</div>
</div>
</template>
</draggable>
</div>
</div>
</div>
<div class="summary-result flex h-60px box-border p-10px mt-20px">
<div class="flex-basis-85px flex-shrink-0"> </div>
<div class="flex-1 flex flex-wrap gap-x-10px">
<div v-if="type == 'virtualSql'" class="c-#67C23A">SELECT (</div>
<span
v-for="item in valueText"
:key="item.text"
:class="[
item.color ? `c-${item.color}` : `c-#333 .dark:c-[var(--el-text-color-regular)]`
]"
>{{ item.text }}</span
>
<div v-if="type == 'virtualSql'" class="c-#67C23A">
) FROM {{ tableName }} tbl
</div>
</div>
</div>
</el-main>
</el-container>
</el-tab-pane>
<el-tab-pane name="custom" class="pl-20px pr-20px pt-10px">
<template #label>
<el-radio v-model="tabsValue" label="custom">自定义{{ typeLabel }}SQL</el-radio>
</template>
<div class="pb-10px text-16px">
<span>自定义{{ typeLabel }}SQL语句</span>
<span class="text-14px c-#E6A23C ml-10px" v-if="type == 'summaryTop'">
当前表的数据源#{jeelowcode_summary_table}
</span>
</div>
<div class="custom-box">
<MonacoEditor v-model="customValue" language="mysql"></MonacoEditor>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import draggable from 'vuedraggable'
import { cloneDeep } from 'lodash-es'
import { dicObj } from '../designData'
defineOptions({ name: 'SqlOption' })
interface Props {
type: 'summaryTop' | 'virtualSql'
show: boolean
fieldList: Array<any>
tableName?: string
}
const props = defineProps<Props>()
const sqlStr = defineModel<string>({ default: '' })
const summaryControl = ref('text')
const formattingObj = ref<any>({})
const formattingKey = [
//文本
'text',
//卡片
'card_name',
'card_value',
//折线图
'line_x',
'line_y',
'line_y_unit',
//柱状图
'bar_x',
'bar_y',
'bar_y_unit',
//饼图
'pie_value',
'pie_name',
'pie_unit'
]
formattingKey.forEach((key) => (formattingObj.value[key] = ''))
const sqlObj = ref({
top: [],
buttom: []
})
const typeLabel = ref('')
const optionUtil = [
{ label: '字段选择', value: 'fieldSelect' },
{ label: '过滤WHERE', value: 'where' },
{ label: '统计函数', value: 'summaryFun' },
{ label: '分组GROUP BY', value: 'groupBy' },
{ label: '筛选HAVING', value: 'having' },
{ label: '排序ORDER BY', value: 'orderBy' },
{ label: '自定义SQL', value: 'custom' }
]
const summaryType = [
{ label: 'COUNT', value: 'COUNT' },
{ label: 'SUM', value: 'SUM' },
{ label: 'AVG', value: 'AVG' },
{ label: 'MAX', value: 'MAX' },
{ label: 'MIN', value: 'MIN' },
{ label: '其他函数请直接输入', value: '-1', disabled: true }
]
const controlList = [
{ type: 'where', value: 'WHERE', tip: '点击输入WHERE过滤条件' },
{ type: 'groupBy', value: 'GROUP BY', tip: '点击输入分组SQL' },
{ type: 'having', value: 'HAVING', tip: '点击输入having条件' },
{ type: 'orderBy', value: 'ORDER BY', tip: '点击输入排序SQL 例如id DESC' }
]
const optionsList = ref([
{ type: 'text', value: 'SELECT' },
{ type: 'top' },
{ type: 'text', value: 'FROM (当前表的数据源) tbl' },
{ type: 'buttom' }
])
const summaryTypeValueList = summaryType.map((item) => item.value)
const tabsValue = ref('group')
const customValue = ref('')
const valueText = computed(() => {
const sqlList: any[] = [{ text: 'SELECT ' }]
for (const key in sqlObj.value) {
if (key == 'buttom') {
if (props.type == 'summaryTop') {
sqlList.push({
text: ' FROM (当前表的数据源) tbl ',
practical: ' FROM (#{jeelowcode_summary_table}) tbl '
})
} else if (props.type == 'virtualSql') sqlList.push({ text: ' FROM ' })
}
let lastType = ''
let sqlText = ''
sqlObj.value[key].forEach((item) => {
const value = item.value
const text = item.text === undefined ? '' : item.text
const isComma = ['fieldSelect', 'summaryFun'].includes(lastType)
if (isComma) sqlText = sqlText + ', '
if (['fieldSelect', 'custom'].includes(value)) {
sqlText = `${sqlText}${text}${item.alias ? ' AS ' + item.alias : ''}`
} else if (value == 'summaryFun') {
if (summaryTypeValueList.includes(text)) {
sqlText =
sqlText + `${text}(${item.params || ''})${item.alias ? ' AS ' + item.alias : ''}`
} else sqlText = sqlText + text
} else if (value == 'where') sqlText = sqlText + `WHERE ${item.text || ''} `
else if (value == 'groupBy') sqlText = sqlText + `GROUP BY ${item.text || ''} `
else if (value == 'orderBy') sqlText = sqlText + `ORDER BY ${item.text || ''} `
else if (value == 'having') sqlText = sqlText + `HAVING ${item.text || ''} `
lastType = item.value
})
sqlList.push({ text: sqlText, color: '#409EFF' })
}
return sqlList
})
const initFun = () => {
if (props.type == 'summaryTop') {
typeLabel.value = '统计'
summaryControl.value = 'text'
formattingObj.value = {}
formattingKey.forEach((key) => (formattingObj.value[key] = ''))
} else if (props.type == 'virtualSql') {
optionsList.value[2].value = 'FROM'
typeLabel.value = '格式化'
}
sqlObj.value = { top: [], buttom: [] }
if (sqlStr.value && sqlStr.value.indexOf('sqlObj') !== -1) {
const data = JSON.parse(sqlStr.value)
customValue.value = data.custom
tabsValue.value = data.valueType
if (props.type == 'summaryTop') {
formattingObj.value = data.formattingObj
summaryControl.value = data.summaryControl || 'text'
}
for (let key in data.sqlObj) {
data.sqlObj[key].forEach((item) => {
sqlObj.value[key].push({
...item,
prop: `option_${Math.ceil(Math.random() * 9999999)}`
})
})
}
} else {
customValue.value = ''
tabsValue.value = 'group'
}
}
watch(
() => props.show,
(val) => {
if (val) initFun()
}
)
const setSelectWidth = (element, num = 0, addClass = '') => {
setTimeout(() => {
const el = document.querySelector(
`.main-option .select_${element.prop}${addClass} .el-select__selected-item.el-select__placeholder span`
) as HTMLSpanElement
if (!el) return
const width = el.offsetWidth
element[`width${addClass}`] = width + 40 + num + 'px'
}, 30)
}
const handleAddColumn = (dataKey, e) => {
const newIndex = e.newIndex
const data = cloneDeep(sqlObj.value[dataKey][newIndex])
if (!data.prop) data.prop = `option_${Math.ceil(Math.random() * 9999999)}`
if (data.value == 'groupBy') {
const fieldArr = []
sqlObj.value.top.forEach((item) => {
if (item['value'] == 'fieldSelect' && item['text']) fieldArr.push(item['text'])
})
if (fieldArr.length) data.text = fieldArr.join(',')
}
delete data.label
sqlObj.value[dataKey][newIndex] = data
}
const delRow = (dataKey, row) => {
sqlObj.value[dataKey] = sqlObj.value[dataKey].filter((item) => item.prop != row.prop)
}
const getOptionStr = () => {
if (sqlObj.value.top.length || sqlObj.value.buttom.length || customValue.value) {
const copySqlObj = cloneDeep(sqlObj.value)
for (let key in copySqlObj) {
copySqlObj[key] = copySqlObj[key].map((item) => {
delete item.prop
return item
})
}
const obj: any = {
sqlObj: copySqlObj,
valueType: tabsValue.value,
custom: customValue.value,
group: valueText.value.map((item) => item.practical || item.text).join('')
}
if (props.type == 'summaryTop') {
obj.formattingObj = formattingObj.value
obj.summaryControl = summaryControl.value
}
return JSON.stringify(obj)
} else ''
}
onMounted(() => {
initFun()
})
defineExpose({ getOptionStr })
</script>
<style lang="scss" scoped>
.summary-top-option {
width: 100%;
height: 100%;
.formatting-content {
padding: 5px 10px;
background-color: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color-light);
}
.left-tree {
padding: 10px;
border-right: 1px solid #f1f1f1;
.summary-title {
display: flex;
align-items: center;
margin-bottom: 10px;
font-size: 14px;
font-weight: 600;
color: #333;
cursor: pointer;
.el-icon {
color: #666;
}
}
&:nth-last-of-type(1) {
margin-bottom: 0;
}
.summary-draggable {
margin-left: 10px;
}
.summary-item {
width: 130px;
padding: 4px 8px;
margin-bottom: 10px;
font-size: 14px;
cursor: move;
background-color: #f4f6fc;
border: 1px dashed #f4f6fc;
&:hover {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
}
}
}
.main-option {
padding: 0;
border-left: 0;
.default-text {
padding: 10px;
font-size: 16px;
color: #409eff;
}
.option-box {
height: calc((100% - 60px) / 2 - 60px);
min-height: 100px;
}
.option-content {
position: relative;
width: calc(100% - 20px);
height: 100%;
margin-left: 10px;
border: 1px solid #f1f1f1;
&.full {
&::before {
position: absolute;
top: 10px;
left: 10px;
font-size: 16px;
color: #999;
content: '拖拽左侧配置工具配置统计SQL';
}
}
.content-draggable-top,
.content-draggable-buttom {
display: flex;
height: 100%;
padding: 10px;
padding-bottom: 40px;
overflow-y: auto;
box-sizing: border-box;
flex-wrap: wrap;
gap: 10px 8px;
align-content: flex-start;
.summary-control {
position: relative;
.del-icon {
position: absolute;
top: -9px;
left: -8px;
display: none;
cursor: pointer;
}
.move-icon {
position: absolute;
right: -14px;
bottom: -9px;
display: none;
cursor: move;
}
&:hover {
.del-icon {
display: block;
}
.move-icon {
display: block;
}
}
}
}
.option-ghost {
position: relative;
width: 0 !important;
height: 32px;
min-width: 0 !important;
padding: 0 !important;
overflow: hidden;
font-size: 0;
background: white;
border-left: 5px solid var(--el-color-primary);
content: '';
outline: none 0;
box-sizing: border-box;
}
}
.summary-result {
border-top: 1px solid #dcdfe6;
}
}
.right-custom {
.custom-title {
display: flex;
align-items: center;
justify-content: center;
height: 32px;
padding: 4px 0;
font-size: 14px;
line-height: 32px;
text-align: center;
background-color: #fafafa;
border: 1px solid #dcdfe6;
}
.custom-content {
width: 100%;
height: calc(100% - 42px);
}
}
.custom-box {
height: calc(100% - 51px);
}
::v-deep(.el-tabs) {
height: calc(100% - 43px);
border: none;
.el-tabs__header {
margin-right: 0;
.el-tabs__nav {
.el-tabs__item {
justify-content: center;
}
}
}
.el-tabs__content {
height: calc(100% - 39px);
padding: 0;
.el-tab-pane {
height: 100%;
}
}
}
}
.border-box {
min-height: 30px;
padding: 1px 11px;
line-height: 30px;
border-radius: 5px;
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
}
.summary-fun-content {
padding: 1px;
border-radius: 5px;
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
::v-deep(.el-input__wrapper),
::v-deep(.el-select__wrapper) {
height: 30px;
padding: 0 11px;
box-shadow: none;
.el-input__inner {
text-align: center;
}
}
.summary_filed-select {
::v-deep(.el-select__wrapper) {
padding-right: 0;
padding-left: 0;
}
}
.alias {
::v-deep(.el-input) {
.el-input__inner {
text-align: left;
}
}
}
}
.summary-top-option.full ::v-deep(.el-tabs) .el-tabs__content {
height: 100%;
}
</style>

View File

@@ -0,0 +1,736 @@
<template>
<avue-tabs ref="tabsRef" :option="tabsOption" @change="tabsHandleChange"></avue-tabs>
<InfoVxeTopBtn
v-show="tabsValue.edit"
:selectNum="tabsValue.dataKey ? infoSelect[tabsValue.dataKey].length : 0"
:tabItem="tabsValue"
:size="size"
@cell-add="cellAddData"
@del-data="delTableInfoData"
@order-data="setInfoOrder"
>
<template #default>
<el-button @click.stop="setDefaultField" v-show="tabsValue.key == 'mysql'">
<span>{{ viewDefaultField == 'Y' ? '隐藏' : '显示' }}系统字段</span>
<el-tooltip effect="dark" content="仅影响当前界面显示,不影响系统字段的创建">
<Icon :size="14" icon="ep:info-filled"></Icon>
</el-tooltip>
</el-button>
</template>
</InfoVxeTopBtn>
<template v-for="tab in tabsOption.column" :key="tab.prop">
<div v-show="tabsValue.prop === tab.prop">
<div v-if="tab.prop == 'tab_exp'" class="flex items-center mb-8px ml-20px">
<div class="flex-basis-160px flex-shrink-0">导入数据重复处理类型</div>
<avue-select class="w-120px!" v-model="tableFormData.importDuplicateType" :dic="dicObj.importDuplicateType"></avue-select>
</div>
<template v-if="tab.children">
<el-tabs
:class="{ 'h-430px': childTabsValue.edit, 'h-386px': !childTabsValue.edit }"
v-model="childTabsValue.prop"
tabPosition="left"
type="border-card"
@tab-change="childTabChange"
>
<InfoVxeTopBtn
v-show="childTabsValue.edit"
:selectNum="childTabsValue.dataKey ? infoSelect[childTabsValue.dataKey].length : 0"
:tabItem="childTabsValue"
:size="size"
@cell-add="cellAddData"
@del-data="delTableInfoData"
@order-data="setInfoOrder"
/>
<el-tab-pane
v-for="child in tab.children"
:key="child.prop"
:name="child.prop"
:label="child.label"
>
<InfoVxeTable
v-model="infoData[child.dataKey]"
:ref="(el) => (tableRefObj[child.prop] = el)"
:tabItem="child"
:column="tableInfoOption.infoColumn[`${child.key}Column`]"
@selection-change="infoSelectionChange"
@cell-click="cellClick"
@dropdown-command="dorpdownHandleCommand"
></InfoVxeTable>
</el-tab-pane>
</el-tabs>
</template>
<template v-else>
<InfoVxeTable
v-model="infoData[tab.dataKey]"
:ref="(el) => (tableRefObj[tab.prop] = el)"
:tabItem="tab"
:column="tableInfoOption.infoColumn[`${tab.key}Column`]"
:checkboxConfig="tab.key == 'mysql' ? { checkMethod: mysqlCheckMethod } : {}"
:row-class-name="tab.key == 'virtual' ? virtualRowClassName : ''"
@selection-change="infoSelectionChange"
@cell-click="cellClick"
@dropdown-command="dorpdownHandleCommand"
></InfoVxeTable>
</template>
</div>
</template>
<DesignPopup
v-model="optionsDialog.dialog.value"
v-bind="optionsDialog.dialog"
:isBodyFull="true"
>
<template #default>
<template v-for="(control, key) in optionComponents" :key="key">
<component
:ref="(el) => (optionRef[key] = el)"
v-if="key == optionsDialog.controlType"
:is="control"
v-model="optionsDialog.controlValue"
v-bind="optionsDialog.controlData"
:show="optionsDialog.dialog.value"
></component>
</template>
</template>
</DesignPopup>
</template>
<script setup lang="ts">
import ConfigOption from './ConfigOption.vue'
import FormattingOption from './FormattingOption.vue'
import SqlOption from './SqlOption.vue'
import InfoVxeTable from './InfoVxeTable.vue'
import InfoVxeTopBtn from './InfoVxeTopBtn.vue'
import { MonacoEditor } from '@/components/MonacoEditor/index'
import { VerifyOption } from '../../general/components/index'
import { tableInfoOption, dicObj } from '../designData'
import { formattingLengStr } from '@/utils/lowDesign'
import { cloneDeep } from 'lodash-es'
import * as DictDataApi from '@/api/system/dict/dict.type'
import * as TableApi from '@/api/design/table/index'
defineOptions({ name: 'TableInfo' })
const message = useMessage()
interface Props {
formType: string
editInfoData: any
showDef?: string
size: any
tableName: string
}
const props = withDefaults(defineProps<Props>(), {
formType: ''
})
const emit = defineEmits(['set-tree-label'])
const tableFormData = defineModel({ type: Object })
const tabsOption = ref({
column: [
{ label: '数据库属性', prop: 'tab_mysql', key: 'mysql', dataKey: 'basics', edit: true },
{ label: '页面属性', prop: 'tab_view', key: 'view', dataKey: 'basics' },
{ label: '查询属性', prop: 'tab_query', key: 'query', dataKey: 'basics' },
{ label: '字典配置', prop: 'tab_dic', key: 'dic', dataKey: 'basics' },
{ label: '导入/导出配置', prop: 'tab_exp', key: 'exp', dataKey: 'basics' },
{
label: '统计配置',
prop: 'tab_summary',
children: [
{
label: '表格底部统计',
prop: 'tab_summary_bottom',
key: 'summaryBottom',
dataKey: 'basics'
},
{
label: '表格顶部统计',
prop: 'tab_summary_top',
key: 'summaryTop',
dataKey: 'summaryTop',
edit: true
}
]
},
{ label: '虚拟字段格式化', prop: 'tab_virtual', key: 'virtual', dataKey: 'basics' },
{ label: '外键', prop: 'tab_key', key: 'key', dataKey: 'basics' },
{ label: '索引', prop: 'tab_index', key: 'index', dataKey: 'index', edit: true }
]
})
const tabsValue = ref(tabsOption.value.column[0])
const childTabsValue = ref<any>({})
const viewDefaultField = ref('N')
const infoData = ref({
basics: [] as any[],
index: [],
summaryTop: []
})
const infoSelect = ref({
basics: [],
index: [],
summaryTop: []
})
const tableInfoDefault = ref<any[]>([])
const optionsDialog = ref<any>({ dialog: {}, controlData: {} })
const tableRefObj = ref({})
const optionComponents = markRaw({
config: ConfigOption,
verify: VerifyOption,
formatting: FormattingOption,
sqlOption: SqlOption,
meditor: MonacoEditor
})
const optionRef = ref({})
const tabsRef = ref()
const fieldList = computed(() => {
let dicData: Array<{ label: string; value: string; type: string }> = []
infoData.value.basics.forEach((item) => {
if (item.fieldCode && item.isDb == 'Y')
dicData.push({
label: `${item.fieldCode}${item.fieldName ? '' + item.fieldName + '' : ''}`,
value: item.fieldCode,
type: item.fieldType
})
})
if (viewDefaultField.value == 'N') {
dicData = [...dicData, ...dicObj.indexFieldDefault]
}
return dicData
})
const treeLabel = computed(() => {
let dicData: Array<{ label: string; value: string }> = []
infoData.value.basics.forEach((item) => {
if (item.fieldCode && !item.only)
dicData.push({
label: `${item.fieldCode}${item.fieldName ? '' + item.fieldName + '' : ''}`,
value: item.fieldCode
})
})
return dicData
})
watch(
() => fieldList.value,
(dicData: any) => {
const dicDataObj = {}
dicData.forEach(({ value, label }) => (dicDataObj[value] = label))
Object.assign(tableInfoOption.infoColumn.indexColumn.indexFieldCodeList.editRender, {
dicData: dicData,
dicObj: dicDataObj
})
}
)
watch(
() => treeLabel.value,
(val: any) => {
emit('set-tree-label', val)
}
)
const dorpdownHandleCommand = (command) => {
let { dataKey, children } = tabsValue.value
if (children) dataKey = childTabsValue.value.dataKey
if (!dataKey) return
let { index, type } = command
if (type == 'up' || type == 'down') {
const delItem = infoData.value[dataKey].splice(index, 1)[0]
nextTick(() => infoData.value[dataKey].splice(type == 'up' ? index - 1 : index + 1, 0, delItem))
} else if (type == 'add') cellAddData(index + 1)
}
const setInfoOrder = () => {
let { dataKey, children } = tabsValue.value
if (children) dataKey = childTabsValue.value.dataKey
if (!dataKey) return
message
.prompt('请输入序号(格式:原序号/最终序号)', '调整排序', { inputValue: '/', type: '' })
.then(({ value }) => {
let orderArr = value.split('/')
if (orderArr.length == 2) {
const delItem = infoData.value[dataKey].splice(Number(orderArr[0].trim()) - 1, 1)[0]
nextTick(() => infoData.value[dataKey].splice(Number(orderArr[1].trim()) - 1, 0, delItem))
} else message.error('请输入正确的格式2/3')
})
.catch(() => {})
}
const setDefaultField = () => {
let defaultData = [] as any
let basicsData = [] as any
if (viewDefaultField.value == 'Y') {
//隐藏
infoData.value.basics.forEach((item, index) => {
item.sortNum = index + 1
if (
tableInfoOption.disabledArr.includes(item.fieldCode) &&
!['id', 'pid'].includes(item.fieldCode)
) {
defaultData.push(item)
} else {
basicsData.push(item)
}
})
viewDefaultField.value = 'N'
} else {
basicsData = [...infoData.value.basics]
tableInfoDefault.value.forEach((item) => {
basicsData.splice(item.sortNum - 1, 0, item)
})
viewDefaultField.value = 'Y'
}
tableInfoDefault.value = defaultData
infoData.value.basics = basicsData
}
const mysqlCheckMethod = ({ row }) => {
return !row.only
}
const virtualRowClassName = ({ row }) => {
if (row.isDb == 'Y') return 'virtual-hide-row'
}
const infoSelectionChange = (selectObj) => {
let { dataKey, children } = tabsValue.value
if (children) dataKey = childTabsValue.value.dataKey
if (!dataKey) return
infoSelect.value[dataKey] = selectObj.records
}
const delTableInfoData = () => {
let { dataKey, prop, children } = tabsValue.value
if (children) {
dataKey = childTabsValue.value.dataKey
prop = childTabsValue.value.prop
}
if (!dataKey) return
const keyArr = infoSelect.value[dataKey].map((item) => item._X_ROW_KEY)
infoData.value[dataKey] = infoData.value[dataKey].filter(
(item) => !keyArr.includes(item._X_ROW_KEY)
)
tableRefObj.value[prop].vxeTableRef.clearCheckboxRow()
infoSelect.value[dataKey] = []
}
const setTableInfoTree = (tableType) => {
let isPid = false
infoData.value.basics = infoData.value.basics.filter((item) => {
if (item.fieldCode == 'pid') {
if (item.only || isPid) return false
isPid = true
}
return true
})
if (tableType == 2) {
if (isPid) {
infoData.value.basics = infoData.value.basics.map((item) => {
if (item.fieldCode == 'pid') item.only = true
return item
})
} else {
infoData.value.basics.splice(1, 0, ...tableInfoOption.getDefaultMysqlField('tree'))
}
}
}
const tabsHandleChange = (column) => {
tabsValue.value = column
if (column.children?.length) childTabsValue.value = cloneDeep(column.children[0])
}
const childTabChange = (prop) => {
if (tabsValue.value.children) {
const index = tabsValue.value.children.findIndex((item) => item.prop == prop)
childTabsValue.value = cloneDeep(tabsValue.value.children[index])
} else childTabsValue.value = []
}
const setTabsValue = (prop) => {
tabsOption.value.column.forEach((item, index) => {
if (item.prop == prop) {
tabsRef.value.changeTabs(index)
}
})
}
const cellAddData = (addIndex) => {
let { prop, key, dataKey, children } = tabsValue.value
if (children) {
prop = childTabsValue.value.prop
key = childTabsValue.value.key
dataKey = childTabsValue.value.dataKey
}
if (!dataKey) return
let addData = cloneDeep(tableInfoOption.infoDefaultData[dataKey]) || {}
let index = infoData.value[dataKey].length
if (key == 'mysql' && addIndex === undefined) {
const indexList = [] as any
infoData.value[dataKey].forEach((item, index) => {
if (
tableInfoOption.disabledArr.includes(item.fieldCode) &&
!['id', 'pid'].includes(item.fieldCode)
)
indexList.push({ label: item.fieldCode, value: index })
})
indexList.sort((a, b) => a.value - b.value)
if (indexList.length) {
for (const i in indexList) {
const currIndex = Number(i)
if (indexList[currIndex + 1] === undefined) break
if (indexList[currIndex].value != indexList[currIndex + 1].value - 1) {
index = indexList[currIndex + 1].value
}
}
}
}
infoData.value[dataKey].splice(addIndex === undefined ? index : addIndex, 0, addData)
tableScrollIndex(key, index, addIndex)
}
const tableScrollIndex = (key, index, addIndex?) => {
setTimeout(() => {
const tableBodyDom = document.querySelector(`.${key}-vxe-table .vxe-table--body-wrapper`)
if (tableBodyDom) {
const bool = addIndex === undefined
tableBodyDom.scrollTop =
(bool ? index : addIndex) * 40 - (bool ? 0 : tableBodyDom['offsetHeight'] / 2)
}
if (key == 'summaryTop') key = 'summary_top'
tableRefObj.value[`tab_${key}`].vxeTableRef.setEditRow(
tableRefObj.value[`tab_${key}`].vxeTableRef.getData(addIndex ? addIndex : index)
)
}, 300)
}
const openOptions = (type, { row, dataKey, index, typeText, infoKey }, otherData = {}) => {
const { viewColumn } = tableInfoOption.infoColumn
const controlText = viewColumn.controlType.editRender.dicObj[row.controlType]
let title = ''
if (['virtual_sql_str', 'virtual_java_str'].includes(dataKey))
title = `${row.fieldCode} ${typeText}`
else if (!row.fieldCode) title = typeText
else if (row.fieldCode) title = `${row.fieldCode} ${row.fieldName} ${typeText}${controlText}`
optionsDialog.value = {
controlValue: row[dataKey],
controlType: type,
controlData: { ...otherData },
dialog: {
value: true,
title,
handleClose: (done) => {
let optionStr = ''
const controlRef = optionRef.value[type]
if (controlRef?.getOptionStr) optionStr = controlRef.getOptionStr()
else if (controlRef?.getValue) optionStr = controlRef.getValue()
done()
setTimeout(() => {
infoData.value[infoKey][index][dataKey] = optionStr
if (infoKey == 'summaryTop' && dataKey == 'summarySql') {
const data = JSON.parse(optionStr || '{}')
infoData.value[infoKey][index].summaryControl = data.summaryControl || 'text'
}
}, 30)
},
dialogParams: { closeOnPressEscape: true, closeOnClickModal: true }
}
}
}
const lowClickInputClick = (row, dataKey, index, type) => {
let typeText = ''
let infoKey = 'basics'
const otherData: any = {}
if (['controlsConfig', 'queryConfig'].includes(dataKey)) {
typeText = dataKey == 'controlsConfig' ? '控件配置' : '查询配置'
otherData.configKey =
dataKey == 'controlsConfig'
? [row.controlType + 'Config', 'cellConfig', 'formConfig']
: ['queryConfig']
} else if (dataKey == 'verifyConfig') {
typeText = '校验配置'
} else if (dataKey == 'summarySql') {
typeText = '统计SQL配置'
infoKey = 'summaryTop'
otherData.type = 'summaryTop'
} else if (dataKey == 'virtual_sql_str') {
typeText = '格式化配置处理'
otherData.type = 'virtualSql'
otherData.tableName = props.tableName
}
if (['summarySql', 'virtual_sql_str', 'virtual_java_str'].includes(dataKey)) {
otherData.fieldList = fieldList.value
}
openOptions(type, { row, dataKey, index, typeText, infoKey }, otherData)
}
const cellClick = ({ rowIndex }) => {
let { prop, children } = tabsValue.value
if (children) prop = childTabsValue.value.prop
tableRefObj.value[prop].vxeTableRef.setEditRow(
tableRefObj.value[prop].vxeTableRef.getData(rowIndex)
)
}
const initEditInfoData = () => {
const data = tableInfoOption.formattingInitData(props.editInfoData)
if (viewDefaultField.value == 'Y') infoData.value.basics = data.infoData
else {
const fieldList: any[] = []
tableInfoDefault.value = data.infoData.filter((item) => {
if (
tableInfoOption.disabledArr.includes(item.fieldCode) &&
!['id', 'pid'].includes(item.fieldCode)
) {
return true
}
fieldList.push(cloneDeep(item))
return false
})
infoData.value.basics = fieldList
}
infoData.value.index = data.indexData
infoData.value.summaryTop = data.summaryTopData
}
onMounted(() => {
tableInfoDefault.value = []
infoData.value.basics = []
infoData.value.index = []
if (props.formType == 'add') {
viewDefaultField.value = 'N'
tableInfoDefault.value = tableInfoOption.getDefaultMysqlField().filter((item) => {
if (item.fieldCode == 'id') {
infoData.value.basics.push(item)
return false
}
return true
})
} else {
viewDefaultField.value = props.showDef || 'N'
initEditInfoData()
}
const { viewColumn, dicColumn, keyColumn } = tableInfoOption.infoColumn
const clickOption = {
viewColumn: ['controlsConfig', 'verifyConfig'],
queryColumn: ['queryConfig'],
summaryTopColumn: ['summarySql'],
virtualColumn: ['virtualValue']
}
for (let key in clickOption) {
clickOption[key].forEach((prop) => {
let type = ''
if (
(key == 'viewColumn' && prop == 'controlsConfig') ||
(key == 'queryColumn' && prop == 'queryConfig')
)
type = 'config'
else if (key == 'viewColumn' && prop == 'verifyConfig') type = 'verify'
else if (key == 'summaryTopColumn' && prop == 'summarySql') type = 'sqlOption'
else if (key == 'virtualColumn' && prop == 'virtualValue') type = 'virtual'
tableInfoOption.infoColumn[key][prop].editRender.events = {
click: (row, dataKey, index) => {
if (type == 'virtual') {
let controlType = ''
if (row.virtualType == 'sql') controlType = 'sqlOption'
else if (row.virtualType == 'java') controlType = 'formatting'
if (controlType) return lowClickInputClick(row, dataKey, index, controlType)
} else {
return lowClickInputClick(row, dataKey, index, type)
}
}
}
})
}
viewColumn.controlType.editRender.events = {
change: (row) => {
nextTick(() => {
if (
!['select', 'radio', 'checkbox', 'tree', 'cascader', 'dicTableSelect'].includes(
row.controlType
)
) {
tableRefObj.value['tab_dic'].vxeTableRef.setRow(row, {
dictType: '',
dictCode: '',
dictTable: '',
dictText: '',
dictTableColumn: []
})
}
})
}
}
viewColumn.labelI18n.editRender.events = {
click: (row) => {
nextTick(() => {
tableRefObj.value['tab_view'].vxeTableRef.setRow(row, {
labelI18n: formattingLengStr(row.labelI18n, row.fieldName)
})
})
}
}
dicColumn.dictType.editRender.events = {
change: (row) => {
nextTick(() => {
if (row.dictType == 'table' && row.dictTable) return
if (row.dictType == 'dict' && !row.dictTable) return
tableRefObj.value['tab_dic'].vxeTableRef.setRow(row, {
dictCode: '',
dictTable: '',
dictText: '',
dictTableColumn: []
})
})
}
}
keyColumn.mainTable.editRender.events = {
change: (row) => {
nextTick(() => {
if (!row.mainTable) tableRefObj.value['tab_key'].vxeTableRef.setRow(row, { mainField: '' })
if (row.mainTable && row.mainField) {
tableRefObj.value['tab_query'].vxeTableRef.setRow(row, { queryIsDb: 'Y' })
}
})
}
}
keyColumn.mainField.editRender.events = {
change: (row) => {
nextTick(() => {
if (row.mainField) {
tableRefObj.value['tab_mysql'].vxeTableRef.setRow(row, { fieldType: 'BigInt' })
}
if (row.mainTable && row.mainField) {
tableRefObj.value['tab_query'].vxeTableRef.setRow(row, { queryIsDb: 'Y' })
}
})
}
}
dicColumn.dictTableSearch.editRender.events = {
click: (row) => {
nextTick(() => {
tableRefObj.value['tab_dic'].vxeTableRef.setRow(row, {
dictTableSearch: row.dictTableSearch || `return {\n\t\n}`
})
})
}
}
DictDataApi.getSimpleDictTypeList().then((dicData) => {
const dicObj = {}
dicData = dicData.map(({ type, name }) => {
dicObj[type] = `${type}${name}`
return { label: dicObj[type], value: type }
})
Object.assign(dicColumn.dictCode.editRender, { dictDicData: dicData, dictDicObj: dicObj })
})
TableApi.getAllDbDicData().then((dbData) => {
const dicObj = {}
const tableDicData = {}
const tableDicObj = {}
const codeDicData = {}
const keyTableDicData: Array<object> = []
const keyTreeTableDicData: Array<object> = []
const keyTableDicObj = {}
const keyFieldDicData = {}
const keyFieldDicObj = {}
if (props.formType == 'edit') {
dbData = dbData.filter((item) => item.tableName != props.editInfoData.dbForm.tableName)
}
const dicData = dbData.map(
({ tableName, tableDescribe, tableId, tableType, fieldModelList }) => {
tableDicData[tableId] = []
codeDicData[tableId] = []
tableDicObj[tableId] = {}
keyFieldDicData[tableName] = []
keyFieldDicObj[tableName] = {}
tableDicData[tableId] = fieldModelList.map(({ fieldCode, fieldName }) => {
tableDicObj[tableId][fieldCode] = `${fieldCode}${fieldName}`
keyFieldDicObj[tableName][fieldCode] = `${fieldCode}${fieldName}`
const currItem = { label: tableDicObj[tableId][fieldCode], value: fieldCode }
codeDicData[tableId].push(currItem)
if (fieldCode == 'id') {
keyFieldDicData[tableName].push(currItem)
}
return currItem
})
dicObj[tableId] = `${tableName}${tableDescribe}`
const returnData = { label: dicObj[tableId], value: tableId }
if (tableType == 3) {
keyTableDicData.push({ label: dicObj[tableId], value: tableName })
keyTableDicObj[tableName] = `${tableName}${tableDescribe}`
}
if (tableType == 2) keyTreeTableDicData.push(returnData)
if (codeDicData[tableId].length) {
codeDicData[tableId] = [
{
label: '请确保绑定的字段在表内值是唯一的,否会导致数据异常一般绑定id',
value: 'jeelowcode-tiop',
disabled: true
},
...codeDicData[tableId]
]
}
return returnData
}
)
Object.assign(dicColumn.dictTable.editRender, {
tableDicData: dicData,
tableDicObj: dicObj,
treeDicData: keyTreeTableDicData
})
Object.assign(dicColumn.dictCode.editRender, { tableDicData: codeDicData, tableDicObj })
Object.assign(dicColumn.dictText.editRender, {
dictDicData: [],
dictDicObj: {},
tableDicData,
tableDicObj
})
Object.assign(dicColumn.dictTableColumn.editRender, {
dictDicData: [],
dictDicObj: {},
tableDicData,
tableDicObj
})
Object.assign(keyColumn.mainTable.editRender, {
dicData: keyTableDicData,
dicObj: keyTableDicObj
})
Object.assign(keyColumn.mainField.editRender, {
dicData: keyFieldDicData,
dicObj: keyFieldDicObj
})
})
})
defineExpose({
infoData,
tableInfoDefault,
viewDefaultField,
fieldList,
tableRefObj,
setTableInfoTree,
setTabsValue,
tableScrollIndex,
initEditInfoData
})
</script>
<style lang="scss">
.low-summary-buttom-sql__popover {
.el-popper__arrow {
left: 20px !important;
}
}
</style>
<style lang="scss" scoped>
::v-deep(.virtual-hide-row) {
display: none;
}
</style>

View File

@@ -0,0 +1,19 @@
import TableInfo from './TableInfo.vue'
import InfoVxeTable from './InfoVxeTable.vue'
import InfoVxeTopBtn from './InfoVxeTopBtn.vue'
import CustomButton from './CustomButton.vue'
import SqlEnhance from './SqlEnhance.vue'
import JavaEnhance from './JavaEnhance.vue'
import JsEnhanceHistory from './JsEnhanceHistory.vue';
import AuthConfig from './AuthConfig.vue'
import ConfigOption from './ConfigOption.vue';
import SortOption from './SortOption.vue';
import SearchOption from './SearchOption.vue';
import FormattingOption from './FormattingOption.vue';
import SqlOption from './SqlOption.vue';
import DataOriginOption from './DataOriginOption.vue'
export {
TableInfo, InfoVxeTable, InfoVxeTopBtn,
CustomButton, SqlEnhance, JavaEnhance, JsEnhanceHistory, AuthConfig,
ConfigOption, SortOption, SearchOption, FormattingOption, SqlOption, DataOriginOption
}

View File

@@ -0,0 +1,587 @@
import { verifyDbName } from '@/api/design/table';
import { cloneDeep } from 'lodash-es';
// 数据字典
export const dicObj = {
dic_whether: [
{ label: '是', value: 'Y' },
{ label: '否', value: 'N' },
],
tableType: [
{ label: '单表', value: 1 },
{ label: '树表', value: 2 },
{ label: '主表', value: 3 },
{ label: '附表', value: 4 },
],
tableClassify: [
{ label: '业务表', value: 1 },
{ label: '表视图', value: 2 },
],
is_db_sync: [
{ label: '已同步', value: 'Y' },
{ label: '未同步', value: 'N' },
],
tableIdType: [
{ label: 'ID_WORKER(分布式自增)', value: 'NATIVE' },
],
themeTemplate: [
{ label: '默认主题', value: 'normal' },
{ label: 'ERP主题', value: 'erp' },
{ label: '内嵌子表主题', value: 'innerTable' },
],
searchStyle: [
{ label: '表格顶部', value: 'default' },
{ label: '表格内嵌', value: 'inline' },
],
treeStyle: [
{ label: '默认样式', value: 'default' },
{ label: '左树右表', value: 'around' },
],
treeMode: [
{ label: '默认', value: 'default' },
{ label: '懒加载', value: 'treeLazy' },
],
tableSelect: [
{ label: '多选', value: 'multiple' },
{ label: '单选', value: 'radio' },
{ label: '禁用', value: 'disabled' },
],
subTableMapping: [
{ label: '一对多', value: 'many' },
{ label: '一对一', value: 'one' },
],
operateMenuStyle: [
{ label: '横向排列', value: 'normal' },
{ label: '更多下拉', value: 'more' },
],
singleStyle: [
{ label: '默认表格', value: 'default' },
{ label: '可展开表格', value: 'expand' },
{ label: '卡片表格', value: 'card' },
],
singleCardSpan: [
{ label: '一列', value: 1 },
{ label: '二列', value: 2 },
{ label: '三列', value: 3 },
{ label: '四列', value: 4 },
{ label: '六列', value: 6 },
{ label: '八列', value: 8 },
{ label: '十二列', value: 12 },
],
expandMode: [
{ label: '默认', value: 'default' },
{ label: '手风琴', value: 'accordion' }
],
formStyle: [
{ label: '一列', value: 1 },
{ label: '二列', value: 2 },
{ label: '三列', value: 3 },
{ label: '四列', value: 4 },
],
dataConfig: [
{ label: '分页', value: 'page' },
{ label: '初始数据请求', value: 'initDataReq' },
{ label: '数据权限过滤', value: 'dataAuth' },
],
dataConfigSelect: [
{ label: '需登录', value: 'authFalse', desc: '访问该表接口:需登录' },
{ label: '需登录、鉴权', value: 'authTrue', desc: '访问该表接口:需登录并且需要配置菜单权限' },
{ label: '不登录可查询、新增数据', value: 'authOpen', desc: '访问该表查询、新增接口:不需要登录' },
],
tableConfig: [
{ label: '固定表格高度', value: 'height' },
{ label: '表格头部操作', value: 'header' },
{ label: '操作列', value: 'menu' },
{ label: '序号列', value: 'index' },
{ label: '纵向边框', value: 'border' },
{ label: '斑马纹样式', value: 'stripe' },
{ label: '表格滚动条固定在底部', value: 'rollBottom' },
],
basicFunction: [
{ label: '新增', value: 'addBtn' },
{ label: '编辑', value: 'editBtn' },
{ label: '查看', value: 'viewBtn' },
{ label: '删除', value: 'delBtn' },
{ label: '批量删除', value: 'batchDelBtn' },
{ label: '导入', value: 'importBtn' },
{ label: '导出', value: 'exportBtn' },
],
fieldType: [
{ label: '字符串 String', value: 'String' },
{ label: '整数 Integer', value: 'Integer' },
{ label: '大整数 BigInt', value: 'BigInt' },
{ label: '小数 BigDecimal', value: 'BigDecimal' },
{ label: '日期 Date', value: 'Date' },
{ label: '时间 Time', value: 'Time' },
{ label: '日期时间 DateTime', value: 'DateTime' },
{ label: '文本 Text', value: 'Text' },
{ label: '大文本 LongText', value: 'LongText' },
{ label: '二进制 Blob', value: 'Blob' },
],
cellWidthType: [
{ label: '固定', value: 'fixed' },
{ label: '最小', value: 'min' },
],
controlType: [
{ label: '文本输入框', value: 'input' },
{ label: '数字输入框', value: 'number' },
{ label: '下拉选择框', value: 'select' },
{ label: '单选框', value: 'radio' },
{ label: '多选框', value: 'checkbox' },
{ label: '日期', value: 'date' },
{ label: '时间', value: 'time' },
{ label: '开关', value: 'switch' },
{ label: '文件', value: 'file' },
{ label: '图片', value: 'image' },
{ label: '树型选择框', value: 'tree' },
{ label: 'Cascader级联选择框', value: 'cascader' },
{ label: '表格选择框', value: 'dicTableSelect' },
{ label: '用户选择框', value: 'userSelect' },
{ label: '部门选择框', value: 'deptSelect' },
{ label: '地区选择框', value: 'regionSelect' },
{ label: '代码编辑器', value: 'monacoEditor' },
{ label: '富文本', value: 'ueditor' },
{ label: 'MarkDown编辑器', value: 'markDown' },
{ label: '自定义控件', value: 'customControl' },
],
dictType: [
{ label: '系统字典', value: 'dict' },
{ label: '表格数据', value: 'table' },
],
dictTextFormatter: [
{ label: '字典Code-字典Text', value: '{dicCode}-{dicText}' },
{ label: '字典Text-字典Code', value: '{dicText}-{dicCode}' },
{ label: '字典Text(字典Code)', value: '{dicText}({dicCode})' },
{ label: '字典Text[字典Code]', value: '{dicText}[{dicCode}]' },
{ label: '其他格式请自行输入', value: 'custom-tip-1', disabled: true },
{ label: '例如code{dicCode}text{dicText}', value: 'custom-tip-2', disabled: true },
{ label: '效果code1001text小明', value: 'custom-tip-3', disabled: true },
],
queryMode: [
{ label: '精确查询(=', value: 'EQ' },
{ label: '模糊查询like', value: 'LIKE' },
{ label: '范围查询(仅适用于控件类型为:日期、时间、数字)', value: 'RANGE' },
{ label: '包含查询in', value: 'IN' },
{ label: '不等于(!=', value: 'NE' },
],
importDuplicateType: [
{ label: '更新数据', value: 'update' },
{ label: '先删后增', value: 'delete_update' },
],
indexType: [
{ label: 'normal', value: 'normal' },
{ label: 'unique', value: 'unique' },
],
summaryType: [
{ label: '合计', value: 'sum' },
{ label: '平均', value: 'avg' },
{ label: '最大值', value: 'max' },
{ label: '最小值', value: 'min' },
{ label: '自定义SQL', value: 'custom' },
],
orderByType: [
{ label: '升序', value: 'asc' },
{ label: '降序', value: 'desc' },
],
summaryControl: [
{ label: '文本', value: 'text' },
{ label: '卡片', value: 'card' },
{ label: '折线图', value: 'line' },
{ label: '柱状图', value: 'bar' },
{ label: '饼图', value: 'pie' },
],
virtualType: [
{ label: 'SQL函数处理', value: 'fun' },
{ label: 'SQL处理', value: 'sql' },
{ label: 'JAVA处理', value: 'java' },
],
indexFieldDefault: [] as any[]
}
export const getDicObj = (key) => {
const obj = {}
if (dicObj[key]) dicObj[key].forEach(item => obj[item.value] = item.label)
return obj
}
const control_tableType = (val, form) => {
dicObj.dataConfig = dicObj.dataConfig.map(item => {
if (item.value == 'page') {
item['disabled'] = val == 2
}
return item
})
dicObj.tableSelect = dicObj.tableSelect.map(item => {
if (item.value == 'radio') {
item['disabled'] = val == 2
}
return item
})
dicObj.basicFunction = dicObj.basicFunction.filter(item => {
if (item.value == 'addChild') return false
return true
})
if (val == 2) {
dicObj.basicFunction.splice(1, 0, { label: '新增子级', value: 'addChild' })
dicObj.basicFunction = [...dicObj.basicFunction]
}
if (val != 3 && form.themeTemplate != 'normal') form.themeTemplate = 'normal'
return {
themeTemplate: { disabled: val != 3 },
treeLabelField: { display: val == 2 },
subTableTitle: { display: val == 4 },
subTableListStr: { display: val == 3 && form.subTableListStr },
isDesForm: { display: val != 4 },
dataConfig: { dicData: dicObj.dataConfig },
tableSelect: { dicData: dicObj.tableSelect },
basicFunction: { dicData: dicObj.basicFunction },
}
}
const control_tableClassify = (val) => {
return {
dataOrigin: { display: val == 2 },
originButton: { display: val == 2 },
}
}
const control_isDesForm = (val) => {
return {
desformWebId: { display: val == 'Y' }
}
}
const rules_required = (label, type = '') => [{ required: true, message: `${['select'].includes(type) ? '请选择' : '请输入'} ${label}`, trigger: "blur" }]
const tableName_required = async (rule, value, callback) => {
const regExp = /^[a-z][a-z0-9_]*$/
if (value === '') callback(new Error('请输入 表名'));
else if (!regExp.test(value)) callback(new Error('表名只能使用小写字母、数字、下划线,并以字母开头'));
else {
const bool = await verifyDbName(value)
if (bool) callback(new Error('表名已存在,请修改'));
else callback()
}
};
const dataOriginObj = {}
const dataOrigin_dicFormatter = (data) => {
const sysList: any[] = []
const dbList: any[] = []
data.forEach(item => {
const row = {
label: `${item.tableName}${item.tableDescribe}`,
tableText: item.tableDescribe,
tableName: item.tableName,
value: item.tableId,
type: 'table',
fieldList: item.fieldModelList.map(child => {
let label = child.fieldCode
if (child.fieldName) label = `${label}${child.fieldName}`
return { label, value: child.fieldCode, tableName: item.tableName, type: 'field', fieldType: child.fieldType }
})
}
if (item.tableId == item.tableName) sysList.push(row)
else if (item.tableClassify !== 2) dbList.push(row)
dataOriginObj[item.tableId] = row
})
return [
{ label: '系统表', value: 'sys', children: sysList },
{ label: '表单开发', value: 'dbForm', children: dbList },
]
}
//表格配置
const tableOptionColumn = {
lowSelectRadio: { label: '', display: false, width: 50, overHidden: false, fixed: true, showColumn: false },
id: { label: '表ID', display: false, search: true, width: 100, overHidden: true, type: 'number' },
tableName: { label: '表名', display: false, search: true, minWidth: 140 },
tableDescribe: { label: '表描述', display: false, search: true, minWidth: 180, overHidden: true },
tableType: { label: '表类型', display: false, value: 1, search: true, type: 'select', dicData: dicObj.tableType, width: 70 },
tableClassify: { label: '表分类', display: false, value: 1, search: true, type: 'select', dicData: dicObj.tableClassify, width: 70 },
isDbSync: { label: '同步状态', display: false, type: 'select', search: true, dicData: dicObj.is_db_sync, width: 85 },
optionCondition: { label: '配置情况', display: false, width: 180, className: 'option-condition-td' },
custom_form: { label: '', labelWidth: 0, span: 24, hide: true, showColumn: false },
custom_info: { label: '', labelWidth: 0, span: 24, hide: true, showColumn: false }
}
// 表单配置
const customFormColumn = {
tableName: { label: '表名', rules: [{ validator: tableName_required, trigger: 'blur', required: true }] },
tableDescribe: { label: '表描述', rules: rules_required('表描述') },
// tableIdType: { label: '主键类型', type: 'select', value: 'NATIVE', dicData: dicObj.tableIdType, clearable: false },
groupDbformId: { label: '分组类型', type: 'tree', value: '', dicData: [], filterable: true, defaultExpandAll: true, props: { label: 'name', value: 'id' } },
tableType: { label: '表类型', type: 'select', value: 1, dicData: dicObj.tableType, control: control_tableType, clearable: false },
subTableTitle: { label: '附表-Tab标题', display: false },
tableClassify: { label: '表分类', type: 'select', value: 1, dicData: dicObj.tableClassify, control: control_tableClassify, clearable: false },
dataOrigin: { label: '数据来源', type: 'tree', value: '', display: false, span: 12, dataType: 'string', multiple: true, filterable: true, parent: false, defaultExpandAll: true, dicUrl: '/jeelowcode/dbform/get/all-table', dicQuery: { systemFlag: 'Y' }, dicFormatter: dataOrigin_dicFormatter },
originButton: { label: '', labelWidth: 0, display: false },
themeTemplate: { label: '主题模板', type: 'select', disabled: true, value: 'normal', dicData: dicObj.themeTemplate, clearable: false },
treeLabelField: { label: '树表回显字段', display: false, value: '', type: 'select', dicData: [], clearable: false, rules: rules_required('树表回显字段', 'select'), },
treeStyle: { label: '树表样式', display: false, value: 'default', type: 'select', dicData: dicObj.treeStyle, clearable: false },
treeMode: { label: '树表模式', display: false, value: 'default', type: 'select', dicData: dicObj.treeMode, clearable: false },
tableSelect: { label: '表格选择', type: 'select', value: 'multiple', display: false, dicData: dicObj.tableSelect },
operateMenuStyle: { label: '操作栏样式', type: 'select', value: 'more', display: false, dicData: dicObj.operateMenuStyle, clearable: false },
maxMenuNum: { label: '操作栏最大横排数', type: 'number', value: 1, display: false },
tableStyle: { label: '单表样式', display: false },
formStyle: { label: '表单风格', type: 'select', value: 2, dicData: dicObj.formStyle, clearable: false },
isDesForm: { label: '默认表单', type: 'select', value: 'N', control: control_isDesForm, clearable: false, dicData: [{ label: '是', value: 'N' }, { label: '否', value: 'Y' }] },
desformWebId: { label: '自定义表单', display: false, type: 'select', filterable: true, rules: rules_required('自定义表单', 'select'), dicUrl: '/jeelowcode/desform/page', dicMethod: 'post', virtualize: true, props: { label: 'desformName', value: 'id' }, dicFormatter: (data) => data.records },
subTableListStr: { label: '关联的附表', display: false, span: 24, disabled: true, placeholder: '关联的附表需通过[附表的外键配置]绑定当前主表,当前字段仅作显示用途。' },
dataConfig: { label: '数据配置', type: 'checkbox', span: 24, dicData: dicObj.dataConfig, dataType: 'string', value: ['page', 'initDataReq', 'authFalse'] },
tableConfig: { label: '表格配置', type: 'checkbox', span: 24, dicData: dicObj.tableConfig, dataType: 'string', value: ['height', 'header', 'menu', 'index', 'border', 'rollBottom'] },
basicFunction: { label: '基础功能', type: 'checkbox', span: 18, dicData: dicObj.basicFunction, dataType: 'string', value: 'addBtn,editBtn,viewBtn,delBtn,batchDelBtn,importBtn,exportBtn' },
basicConfig: { label: '', labelWidth: 0, span: 6, params: { otherParams: { type: 'default', width: '620px', tipKeyList: ['tableOption'] }, dialogParams: { closeOnClickModal: true, closeOnPressEscape: true } } },
importDuplicateType: { label: '导入数据重复处理类型', display: false, dicData: dicObj.importDuplicateType, value: 'update' }
}
const infoColumn = {
mysqlColumn: {
fieldCode: { title: '字段编码', minWidth: 120, editRender: { name: 'LowInput', verifyEdit: true } },
fieldName: { title: '字段名称', minWidth: 120, editRender: { name: 'LowInput' } },
fieldType: { title: '字段类型', minWidth: 100, editRender: { name: 'LowSelect', verifyEdit: true, dicData: dicObj.fieldType, dicObj: getDicObj('fieldType') } },
fieldDefaultVal: { title: '默认值', minWidth: 120, editRender: { name: 'LowInput', verifyEdit: true } },
fieldLen: { title: '字段长度', minWidth: 80, editRender: { name: 'LowNumber', verifyEdit: true } },
fieldPointLen: { title: '小数位数', minWidth: 80, editRender: { name: 'LowNumber', verifyEdit: true } },
fieldRemark: { title: '备注', minWidth: 120, editRender: { name: 'LowInput' } },
isPrimaryKey: { title: '是否主键', width: 54, align: "center", editRender: { name: 'LowCheckbox', verifyEdit: true } },
isNull: { title: '是否为空', width: 54, align: "center", editRender: { name: 'LowCheckbox', verifyEdit: true } },
isDb: { title: '同步数据库', width: 64, align: "center", editRender: { name: 'LowCheckbox', verifyEdit: true } },
},
viewColumn: {
fieldCode: { title: '字段编码', width: 140 },
fieldName: { title: '字段名称', width: 140 },
labelI18n: { title: '国际化配置', width: 140, editRender: { name: 'LowMonacoEditorInput', events: {} } },
isShowList: { title: '列表显示', width: 54, align: "center", editRender: { name: 'LowCheckbox' }, titleSuffix: { content: '控制表格列是否显示' } },
isDbSelect: { title: '字段显示', width: 54, align: "center", editRender: { name: 'LowCheckbox', verifyEdit: true, }, titleSuffix: { content: '控制接口是否返回该字段' } },
isShowForm: { title: '表单显示', width: 54, align: "center", editRender: { name: 'LowCheckbox', verifyEdit: true, } },
isRequired: { title: '是否必填', width: 54, align: "center", editRender: { name: 'LowCheckbox', verifyEdit: true, } },
cellWidthType: { title: '列宽类型', width: 100, align: "center", editRender: { name: 'LowSelect', dicData: dicObj.cellWidthType, dicObj: getDicObj('cellWidthType') } },
cellWidth: { title: '列宽', width: 80, align: "center", editRender: { name: 'LowInput', placeholder: '120' } },
controlType: { title: '控件类型', width: 140, editRender: { name: 'LowSelect', verifyEdit: true, filterable: true, dicData: dicObj.controlType, dicObj: getDicObj('controlType'), events: {} } },
controlsConfig: { title: '控件配置', minWidth: 100, editRender: { name: 'LowClickInput', events: {} } },
verifyConfig: { title: '校验配置', minWidth: 100, editRender: { name: 'LowClickInput', events: {} } },
isShowSort: { title: '是否排序', width: 54, align: "center", editRender: { name: 'LowCheckbox' } },
isShowColumn: { title: '是否可控', width: 54, align: "center", editRender: { name: 'LowCheckbox' } },
},
queryColumn: {
fieldCode: { title: '字段编码', width: 140 },
fieldName: { title: '字段名称', width: 140 },
isShowList: { title: '列表显示', width: 54, align: "center", cellRender: { name: 'LowCheckbox' } },
queryIsDb: { title: '接口查询', width: 54, align: "center", editRender: { name: 'LowCheckbox' } },
filterAuth: { title: '权限过滤', width: 54, align: "center", editRender: { name: 'LowCheckbox' } },
queryIsWeb: { title: '查询控件', width: 54, align: "center", editRender: { name: 'LowCheckbox' } },
queryDefaultVal: { title: '查询控件默认值', width: 160, editRender: { name: 'LowInput' } },
queryMode: { title: '查询模式', width: 130, editRender: { name: 'LowSelect', dicData: dicObj.queryMode, dicObj: getDicObj('queryMode') } },
queryConfig: { title: '查询配置', minWidth: 100, editRender: { name: 'LowClickInput', events: {} } },
},
dicColumn: {
fieldCode: { title: '字段编码', width: 140 },
fieldName: { title: '字段名称', width: 140 },
dictType: { title: '字典类型', width: 140, editRender: { name: 'LowSelect', verifyEdit: true, events: {}, dicData: dicObj.dictType, dicObj: getDicObj('dictType') } },
dictCode: { title: '字典Code', width: 180, editRender: { name: 'LowSelect', verifyEdit: true, filterable: true, typeKey: 'dictType', dicData: [] } },
dictTable: { title: '字典Table', width: 230, editRender: { name: 'LowSelect', verifyEdit: true, filterable: true, typeKey: 'dictType', dicData: [] } },
dictText: { title: '字典Text', width: 180, editRender: { name: 'LowSelect', verifyEdit: true, filterable: true, typeKey: 'dictType', dicData: [] } },
dictTextFormatter: { title: '字典Text格式化', width: 180, editRender: { name: 'LowSelect', verifyEdit: true, filterable: true, allowCreate: true, dicData: dicObj.dictTextFormatter, dicObj: getDicObj('dictTextFormatter') } },
dictTableColumn: { title: '字典显示列', editRender: { name: 'LowSelect', verifyEdit: true, multiple: true, filterable: true, typeKey: 'dictType', dicData: [] } },
dictTableSearch: { title: '额外查询条件', editRender: { name: 'LowMonacoEditorInput', verifyEdit: true, events: {} } },
},
expColumn: {
fieldCode: { title: '字段编码', width: 140 },
fieldName: { title: '字段名称', width: 140 },
isShowList: { title: '列表显示', width: 74, align: "center", cellRender: { name: 'LowCheckbox' } },
isExport: { title: '可导出', width: 70, align: "center", editRender: { name: 'LowCheckbox' } },
isImport: { title: '可导入', width: 70, align: "center", editRender: { name: 'LowCheckbox' } },
isImportDuplicate: { title: '导入去重', width: 80, align: "center", editRender: { name: 'LowCheckbox' }, titleSuffix: { content: '开启后导入将校验表内数据中对应的字段是否重复,勾选多个字段以(AND)判断是否重复' } },
importExampleTxt: { title: '导入模板示例文本', minWidth: 120, editRender: { name: 'LowInput' } },
},
keyColumn: {
fieldCode: { title: '字段编码', width: 140 },
fieldName: { title: '字段名称', width: 140 },
mainTable: { title: '外键-主表名', width: 230, editRender: { name: 'LowSelect', verifyEdit: true, filterable: true, dicData: [], events: {} } },
mainField: { title: '外键-主键字段', editRender: { name: 'LowSelect', verifyEdit: true, dicData: [], events: {} } },
},
summaryBottomColumn: {
fieldCode: { title: '字段编码', width: 140 },
fieldName: { title: '字段名称', width: 140 },
summaryShow: { title: '显示统计', width: 80, align: "center", editRender: { name: 'LowCheckbox' } },
summaryLabel: { title: '统计名称', width: 160, editRender: { name: 'LowInput' } },
summarySql: { title: '统计执行sql', minWidth: 120, editRender: { name: 'LowSummaryBottomSql', dicData: dicObj.summaryType, dicObj: getDicObj('summaryType') } },
},
summaryTopColumn: {
summaryShow: { title: '显示统计', width: 80, align: "center", editRender: { name: 'LowCheckbox' } },
summarySpan: { title: '统计栅格', width: 80, align: 'center', editRender: { name: 'LowNumber', params: { min: 1, max: 24 } } },
summaryLabel: { title: '统计名称', width: 160, editRender: { name: 'LowInput' } },
summaryControl: { title: '统计控件类型', width: 120, align: 'center', cellRender: { name: 'LowSelect', dicData: dicObj.summaryControl, dicObj: getDicObj('summaryControl') } },
summarySql: { title: '统计SQL配置', minWidth: 100, editRender: { name: 'LowClickInput', events: {} } },
},
virtualColumn: {
fieldCode: { title: '字段编码', width: 140 },
fieldName: { title: '字段名称', width: 140 },
virtualType: { title: '处理方式', width: 140, align: 'center', editRender: { name: 'LowSelect', dicData: dicObj.virtualType, dicObj: getDicObj('virtualType') } },
virtualValue: { title: '处理配置', minWidth: 140, editRender: { name: 'virtualInput', events: {} } },
},
indexColumn: {
indexName: { title: '索引名称', width: 180, editRender: { name: 'LowInput' } },
indexType: { title: '索引类型', width: 180, editRender: { name: 'LowSelect', dicData: dicObj.indexType } },
indexFieldCodeList: { title: '索引字段列表', editRender: { name: 'LowSelect', multiple: true, filterable: true, dicData: [] } }
},
}
const infoApiKey = {}
const indexApiKey = Object.keys(infoColumn.indexColumn)
const apiKey = { mysqlColumn: 'fieldList', viewColumn: 'webList', queryColumn: 'queryList', dicColumn: 'dictList', expColumn: 'exportList', keyColumn: 'foreignkeyList', summaryBottomColumn: 'summaryList' }
for (const key in infoColumn) {
if (apiKey[key]) {
const keys = Object.keys(infoColumn[key])
if (key == 'mysqlColumn') keys.push('sortNum')
infoApiKey[apiKey[key]] = keys
}
}
const disabledArr = ['id', 'tenant_id', 'create_user', 'create_time', 'create_dept', 'update_user', 'update_time', 'pid', 'is_deleted']
//默认值
const infoDefaultData = {
basics: {
fieldCode: '', fieldName: '', fieldLen: 128, fieldPointLen: 0, fieldDefaultVal: '', fieldType: 'String', fieldRemark: '', isPrimaryKey: 'N', isNull: 'Y', isDb: 'Y',
labelI18n: '', isShowList: 'Y', isDbSelect: 'Y', isShowForm: 'Y', isRequired: 'N', cellWidthType: 'min', controlType: 'input', isShowSort: 'N', isShowColumn: 'Y',
queryIsDb: 'N', queryIsWeb: 'N', queryMode: 'EQ', queryConfig: '',
dictType: '', dictCode: '', dictTable: '', dictText: '', dictTextFormatter: '', dictTableColumn: [], dictTableSearch: '',
isImport: 'Y', isExport: 'Y', importRemoveRepet: 'N', importExampleTxt: '',
mainTable: '', mainField: '',
summaryShow: 'N', summaryLabel: '', summarySql: '', summaryJson: { sqlType: '', sqlValue: '' },
virtualType: '', virtual_java_str: '', virtual_sql_str: '', virtual_fun_str: '', virtualValue: ''
},
index: { indexName: '', indexFieldCodeList: [], indexType: 'normal' },
summaryTop: { summaryShow: 'Y', summarySpan: 8, summaryControl: 'text', summaryLabel: '', summarySql: '', }
}
//获取默认字段配置
const getDefaultMysqlField = (type?: string) => {
let fieldObj = {}
if (type == 'tree') {
fieldObj = {
pid: { fieldName: '父级节点', fieldType: 'BigInt', fieldDefaultVal: 0, isShowList: 'N', isShowForm: 'Y', only: true, is_tree_field: true, queryIsDb: 'Y' },
}
} else {
fieldObj = {
id: { fieldName: '主键', fieldType: 'BigInt', isPrimaryKey: 'Y', isNull: 'N', isShowList: 'N', isShowForm: 'N', isImport: 'N', isExport: 'N', queryIsDb: 'Y', isShowColumn: 'N', dictType: '', only: true },
tenant_id: { fieldName: '租户编号', fieldType: 'BigInt', isShowList: 'N', isShowForm: 'N', isImport: 'N', isExport: 'N', isShowColumn: 'N', dictType: '', only: true },
create_user: { fieldName: '创建人', fieldType: 'BigInt', isShowList: 'N', isShowForm: 'N', controlType: 'userSelect', isImport: 'N', isExport: 'N', queryIsDb: 'Y', isShowColumn: 'N', dictType: '', only: true },
create_time: { fieldName: '创建时间', fieldType: 'DateTime', isShowList: 'N', isShowForm: 'N', controlType: 'date', isImport: 'N', isExport: 'N', queryIsDb: 'Y', isShowColumn: 'N', dictType: '', only: true },
create_dept: { fieldName: '创建部门id', fieldType: 'BigInt', isShowList: 'N', isShowForm: 'N', controlType: 'deptSelect', isImport: 'N', isExport: 'N', queryIsDb: 'Y', isShowColumn: 'N', dictType: '', only: true },
update_user: { fieldName: '更新人', fieldType: 'BigInt', isShowList: 'N', isShowForm: 'N', controlType: 'userSelect', isImport: 'N', isExport: 'N', queryIsDb: 'Y', isShowColumn: 'N', dictType: '', only: true },
update_time: { fieldName: '更新时间', fieldType: 'DateTime', isShowList: 'N', isShowForm: 'N', controlType: 'date', isImport: 'N', isExport: 'N', queryIsDb: 'Y', isShowColumn: 'N', dictType: '', only: true },
is_deleted: { fieldName: '是否删除', fieldType: 'Integer', fieldLen: 2, fieldDefaultVal: 0, isShowList: 'N', isShowForm: 'N', isShowColumn: 'N', isImport: 'N', isExport: 'N', queryIsDb: 'Y', dictType: '', only: true },
}
}
const defaultFieldData: any[] = []
for (const key in fieldObj) defaultFieldData.push({ ...cloneDeep(infoDefaultData.basics), ...fieldObj[key], fieldCode: key, })
return defaultFieldData
}
getDefaultMysqlField().forEach(item => {
if (item.fieldCode == 'id') return
dicObj.indexFieldDefault.push({ label: `${item.fieldCode}${item.fieldName}`, value: item.fieldCode, type: item.fieldType })
})
//格式化接口初始数据
const formattingInitData = (editInfoData) => {
const optionObj = {}
const infoData = [] as any
const indexData = [] as any
const summaryTopData = [] as any
for (const apiKey in infoApiKey) {
let key = apiKey
if (apiKey == 'summaryList') key = 'summaryBottomList'
optionObj[key] = {}
editInfoData[key]?.forEach(item => optionObj[key][item.fieldCode] = item)
}
editInfoData.fieldList.forEach(fieldItem => {
const fieldCode = fieldItem.fieldCode
const infoItem: any = {}
for (const apiKey in infoApiKey) {
let dataKey = apiKey
if (apiKey == 'summaryList') dataKey = 'summaryBottomList'
if (!optionObj[dataKey]) continue
const editItem = optionObj[dataKey][fieldCode] || cloneDeep(infoDefaultData)
infoItem[`${apiKey}_id`] = editItem.id
if (apiKey == 'summaryList') infoItem.summaryJson = editItem.summaryJson ? JSON.parse(editItem.summaryJson) : cloneDeep(infoDefaultData.basics.summaryJson)
if (apiKey == 'webList') {
if (editItem.formatConfig) {
const formatConfig = JSON.parse(editItem.formatConfig)
infoItem.virtualType = formatConfig.formatType
infoItem.virtual_sql_str = JSON.stringify(formatConfig.formatJson.sql || {})
infoItem.virtual_java_str = JSON.stringify(formatConfig.formatJson.java || {})
infoItem.virtual_fun_str = formatConfig.formatJson.fun || ''
infoItem.virtualValue = ''
} else {
infoItem.virtualType = ''
infoItem.virtual_sql_str = ''
infoItem.virtual_java_str = ''
infoItem.virtual_fun_str = ''
infoItem.virtualValue = ''
}
}
for (const i in infoApiKey[apiKey]) {
const key = infoApiKey[apiKey][i]
if (apiKey != 'fieldList' && ['fieldCode', 'fieldName'].includes(key)) continue
if (['queryList', 'exportList'].includes(apiKey) && key == 'isShowList') continue
infoItem[key] = editItem[key]
if (key == 'dictTableColumn' && !(infoItem[key] instanceof Array)) {
infoItem[key] = infoItem[key] ? infoItem[key].split(',') : []
}
if (key == 'fieldCode' && disabledArr.includes(infoItem[key])) {
infoItem.only = true
if (infoItem[key] === 'pid' && editInfoData.dbForm.tableType !== 2) {
delete infoItem.only
}
}
}
}
infoData.push(infoItem)
})
if (editInfoData.indexList) {
const indexKeyList = Object.keys(infoColumn.indexColumn)
editInfoData.indexList.forEach(item => {
const indexItem = { id: item.id }
indexKeyList.forEach(prop => {
if (prop == 'indexFieldCodeList') indexItem[prop] = item[prop] ? item[prop].split(',') : []
else indexItem[prop] = item[prop]
})
indexData.push(indexItem)
})
}
if (editInfoData.summaryTopList) {
editInfoData.summaryTopList.forEach(item => {
const topObj = JSON.parse(item.summaryJson)
summaryTopData.push({
id: item.id,
summaryLabel: item.summaryLabel,
summaryShow: item.summaryShow,
summarySpan: topObj.summarySpan,
summaryControl: topObj.summaryControl,
summarySql: JSON.stringify(topObj.summarySql),
})
})
}
return { infoData, indexData, summaryTopData }
}
export const pageOption = {
tableOptionColumn, customFormColumn, dataOriginObj,
infoApiKey, indexApiKey,
tableName_required
}
export const tableInfoOption = {
infoColumn,
infoDefaultData,
disabledArr,
getDefaultMysqlField,
formattingInitData,
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,74 @@
<template>
<ContentWrap>
<LowTable ref="lowTableRef" v-if="tableId" :tableId="tableId" :isPermi="isPermi"></LowTable>
<div v-else>无权限访问</div>
</ContentWrap>
</template>
<script setup lang="ts">
import { useTagsViewStore } from '@/store/modules/tagsView'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
const route = useRoute()
const tagsViewStore = useTagsViewStore()
const { wsCache } = useCache()
const tableId = ref('')
const isPermi = ref(false)
const timer = ref<any>(null)
const lowTableRef = ref()
const setTestTitle = () => {
timer.value = setInterval(() => {
const tableDescribe = lowTableRef.value?.tableInfo?.tableDescribe
if (tableDescribe) {
if (timer.value) clearInterval(timer.value)
tagsViewStore.visitedViews = tagsViewStore.visitedViews.map((tag) => {
if (tag.path == '/low/table/test/' + tableId.value) {
if (tag.meta) tag.meta.title = '功能测试:' + tableDescribe
}
return tag
})
}
}, 100)
}
onMounted(() => {
if (route.params.id) {
//功能测试
const menus = wsCache.get(CACHE_KEY.USER).menus
const praentArr = route.meta.activeMenu?.split('/').filter((path) => path) || []
let isPermission = false
if (praentArr.length) {
praentArr[0] = '/' + praentArr[0]
let findIndex = 0
const findPath = (menuList) => {
for (const index in menuList) {
if (menuList[index].path == praentArr[findIndex]) {
if (findIndex == praentArr.length - 1) isPermission = true
else findIndex++
if (menuList[index].children) findPath(menuList[index].children)
if (isPermission) break
}
}
}
findPath(menus)
}
if (isPermission && typeof route.params.id == 'string') tableId.value = route.params.id
setTestTitle()
} else {
const pathList = route.path.split('/')
const length = pathList.length - 1
if (/\d$/.test(pathList[length])) {
tableId.value = pathList[length]
isPermi.value = true
}
}
})
onUnmounted(() => {
if (timer.value) clearInterval(timer.value)
})
</script>
<style lang="scss" scoped></style>