【升级】重构xnUpload组件、xnEditor组件,代码生成支持图片、文件上传、富文本生成

pull/217/head
俞宝山 2024-05-30 15:18:27 +08:00
parent 49e0c9e443
commit c15978ae60
8 changed files with 551 additions and 85 deletions

View File

@ -0,0 +1,15 @@
## 富文本
### 说明
这个组件将在3.2版本移除请及时更新使用xn-editor
> 更新提示
>
> 1、将<editor /> 改为 <xn-editor />
>
> 2、v-model绑定改为v-model:value
>
> 3、整体写法为<xn-editor v-model:value="formData.字段名" />
>
> 4、移除业务内跟此组件无关的其他代码即可

View File

@ -0,0 +1,125 @@
<template>
<Editor v-model="contentValue" :init="init" :disabled="disabled" :placeholder="placeholder" @onClick="onClick" />
</template>
<script setup name="Editor">
import fileApi from '@/api/dev/fileApi'
import Editor from '@tinymce/tinymce-vue'
import tinymce from 'tinymce/tinymce'
import 'tinymce/themes/silver'
import 'tinymce/icons/default'
import 'tinymce/models/dom'
//
import 'tinymce/plugins/code' //
import 'tinymce/plugins/image' //
import 'tinymce/plugins/link' //
import 'tinymce/plugins/preview' //
import 'tinymce/plugins/table' //
import 'tinymce/plugins/lists' //
import 'tinymce/plugins/advlist' //
const emit = defineEmits(['update:value', 'onClick', 'onChange'])
const props = defineProps({
value: {
type: [String, Array],
default: '',
required: false
},
placeholder: {
type: String,
default: ''
},
height: {
type: Number,
default: 300
},
disabled: {
type: Boolean,
default: false
},
plugins: {
type: [String, Array],
default: 'code image link preview table lists advlist'
},
toolbar: {
type: [String, Array],
default:
'undo redo | forecolor backcolor bold italic underline strikethrough link | blocks fontfamily fontsize | \
alignleft aligncenter alignright alignjustify outdent indent lineheight | bullist numlist | \
image table preview | code selectall'
},
fileUploadFunction: {
type: Function,
default: undefined
}
})
const contentValue = ref()
const init = ref({
language_url: '/tinymce/langs/zh_CN.js',
language: 'zh_CN',
skin_url: '/tinymce/skins/ui/oxide',
content_css: '/tinymce/skins/content/default/content.css',
menubar: false,
statusbar: true,
plugins: props.plugins,
toolbar: props.toolbar,
fontsize_formats: '12px 14px 16px 18px 20px 22px 24px 28px 32px 36px 48px 56px 72px',
height: props.height,
placeholder: props.placeholder,
branding: false,
resize: true,
elementpath: true,
content_style: '',
selector: '#textarea1',
skin: 'oxide-dark',
images_upload_handler(blobInfo, progress) {
return new Promise((resolve, reject) => {
const param = new FormData()
param.append('file', blobInfo.blob(), blobInfo.filename())
//
if (props.fileUploadFunction) {
props
.fileUploadFunction(param)
.then((data) => {
return resolve(data)
})
.catch((err) => {
return reject('err:' + err)
})
} else {
fileApi.fileUploadDynamicReturnUrl(param).then((data) => {
return resolve(data)
})
}
})
},
setup: (editor) => {
editor.on('init', () => {
// getBody().style.fontSize = '14px'
})
}
})
//
watch(
() => props.value,
(newVal) => {
contentValue.value = newVal
},
{ immediate: true, deep: true }
)
//
watch(contentValue, (newValue) => {
emit('update:value', newValue)
})
const onClick = (e) => {
emit('onClick', e, tinymce)
}
onMounted(() => {
tinymce.init({})
})
</script>
<style lang="less">
.tox-toolbar__primary {
border-bottom: 1px solid rgb(5 5 5 / 0%) !important;
}
</style>

View File

@ -0,0 +1,39 @@
## 小诺文件上传
### 说明
改组件为文件上传、支持单个、多个文件返回id、返回数组、返回所有
@author yubaoshan
@data 2024年5月27日09:15:17
### props定义
| 序号 | 编码 | 类型 | 说明 | 默认 |
|-----|---------------------------|---------|---------------------------------------------------------------------------------------------|----------------------------------|
| 1 | uploadReturnIdApi | String | 上传返回id接口地址 | /dev/file/uploadLocalReturnId |
| 2 | uploadDynamicReturnUrlApi | String | 上传返回url接口地址 | /dev/file/uploadDynamicReturnUrl |
| 3 | uploadIdDownloadUrl | String | 当上传接口为id的情况下配置下载接口 | /dev/file/download?id= |
| 4 | uploadMode | String | 上传样式或图片方式 file、drag、image | file |
| 5 | uploadNumber | Number | 上传数量 | 1 |
| 6 | uploadText | String | 上传文字 | 上传 |
| 7 | uploadResultType | String | 上传返回分类 字符串逗号隔离或数组 interval、array | interval |
| 8 | showUploadList | Boolean | 跟antdv官方一样是否显示文件列表 | true |
| 9 | accept | String | 跟antdv官方一样接受上传的文件类型如果uploadMode配置了image类型上传的必须是图片该参数也只能配置图片的某一项或多项具体百度查看文件上传accept类型配置 | - |
| 10 | completeResult | Boolean | 是否是完整的结果就是文件上传返回什么该组件返回什么uploadResultCategory必须为array | false |
| 11 | value | String, Array | 父组件传来的参数,通过v-model:value绑定 | - |
### emits定义
| 序号 | 方法名 | 参数类型 | 说明 |
|----|--------|---------------------------------------|-----------------------------|
| 1 | value | 根据uploadResultType、completeResult 而定 | 当选择用户后通过v-model:value绑定到组件上 |
| 2 | onChange | 根据uploadResultType、completeResult 而定 | 通过@onChange 方法返回上传的数据 |
### slot定义
| 序号 | 插槽名 | 用途 |
|----|--------|-------------|
| 1 | explain | 主要用于一些提示性文字 |

View File

@ -1,55 +1,115 @@
<template>
<a-upload
v-if="props.uploadMode === 'defaults'"
v-model:file-list="fileList"
name="file"
:action="action"
:headers="headers"
:maxCount="props.uploadNumber"
@change="handleChange"
>
<a-button>
<upload-outlined />
上传
</a-button>
</a-upload>
<div>
<a-upload
v-if="props.uploadMode === 'file'"
v-model:file-list="fileList"
name="file"
:action="action"
:headers="headers"
:maxCount="props.uploadNumber"
:progress="progress"
@change="handleChange"
:showUploadList="props.showUploadList"
:accept="accept"
>
<a-button>
<upload-outlined />
{{ props.uploadText }}
</a-button>
</a-upload>
<a-upload-dragger
v-if="props.uploadMode === 'drag'"
v-model:fileList="fileList"
name="file"
:multiple="true"
:action="action"
:headers="headers"
:maxCount="props.uploadNumber"
@change="handleChange"
>
<p class="ant-upload-drag-icon">
<inbox-outlined />
</p>
<p class="ant-upload-text">单击或拖动文件到此区域上传</p>
<p class="ant-upload-hint"></p>
</a-upload-dragger>
<a-upload
v-if="props.uploadMode === 'image'"
v-model:file-list="fileList"
name="file"
:action="action"
:headers="headers"
:maxCount="props.uploadNumber"
:before-upload="beforeUpload"
list-type="picture-card"
@change="handleChange"
@preview="handlePreview"
:progress="progress"
:showUploadList="props.showUploadList"
:accept="accept"
>
<div class="clearfix" v-if="fileList.length < props.uploadNumber">
<plus-outlined />
<div style="margin-top: 8px">{{ props.uploadText }}</div>
</div>
</a-upload>
<a-modal
v-if="props.uploadMode === 'image'"
:open="previewVisible"
:title="previewTitle"
:footer="null"
@cancel="handleCancel"
>
<img alt="example" style="width: 100%" :src="previewImage" />
</a-modal>
<a-upload-dragger
v-if="props.uploadMode === 'drag'"
v-model:fileList="fileList"
name="file"
:multiple="true"
:action="action"
:headers="headers"
:maxCount="props.uploadNumber"
@change="handleChange"
:progress="progress"
:showUploadList="props.showUploadList"
:accept="accept"
>
<p class="ant-upload-drag-icon">
<inbox-outlined />
</p>
<p class="ant-upload-text">
{{ props.uploadText }}
</p>
<p class="ant-upload-hint"></p>
</a-upload-dragger>
<slot name="explain"></slot>
</div>
</template>
<script setup name="uploadIndex">
import tool from '@/utils/tool'
import sysConfig from '@/config/index'
import { message, Upload } from 'ant-design-vue'
import { cloneDeep } from 'lodash-es'
const fileList = ref([])
const emit = defineEmits({ uploadDone: null })
const emit = defineEmits(['update:value', 'onChange'])
const previewVisible = ref(false)
const previewTitle = ref('')
const previewImage = ref('')
const headers = ref({
token: tool.data.get('TOKEN')
})
const accept = ref('')
const props = defineProps({
action: {
// id
uploadReturnIdApi: {
type: String,
default: '/dev/file/uploadLocalReturnId',
required: false
},
// url
uploadDynamicReturnUrlApi: {
type: String,
default: '/dev/file/uploadDynamicReturnUrl',
required: false
},
// defaults || drag
// id
uploadIdDownloadUrl: {
type: String,
default: '/dev/file/download?id=',
required: false
},
// file || drag || image
uploadMode: {
type: String,
default: 'defaults',
default: 'file',
required: false
},
//
@ -57,49 +117,220 @@
type: Number,
default: 1,
required: false
},
//
uploadText: {
type: String,
default: '上传',
required: false
},
// idurl
uploadResultType: {
type: String,
default: 'url',
required: false
},
// interval | array
uploadResultCategory: {
type: String,
default: 'interval',
required: false
},
// antdv
showUploadList: {
type: Boolean,
default: true,
required: false
},
// antdv
accept: {
type: String,
default: '',
required: false
},
// uploadResultCategoryarray
completeResult: {
type: Boolean,
default: false,
required: false
},
//
value: {
type: [String, Array],
default: undefined,
required: false
}
})
const action = sysConfig.API_URL + props.action
const action =
props.uploadResultType === 'id'
? sysConfig.API_URL + props.uploadReturnIdApi
: sysConfig.API_URL + props.uploadDynamicReturnUrlApi
// 使
const handleChange = () => {
// 使
let result = []
for (let a = 0; a < props.uploadNumber; a++) {
const file = fileList.value[a]
if (file && file.status === 'done' && file.response && file.response.code === 200) {
const resultObj = {
name: file.name,
url: file.response.data
}
result.push(resultObj)
//
const buildFileObject = (url, id) => {
return {
data: url ? url : sysConfig.API_URL + props.uploadIdDownloadUrl + id,
name: url ? url : id,
url: url ? url : sysConfig.API_URL + props.uploadIdDownloadUrl + id,
status: 'done',
response: {
data: url ? url : id,
code: 200
}
}
if (result.length > 0) {
emit('uploadDone', result)
}
}
// DOM
const uploadFileList = () => {
if (fileList.value) {
const result = []
//
fileList.value.forEach((item) => {
const obj = {
name: item.name,
type: item.type,
size: item.size,
url: item.response.data
//
const echo = (newVal) => {
//
if (props.uploadResultCategory === 'interval') {
// id
if (props.uploadResultType === 'id') {
newVal.split(',').forEach((id) => {
const file = buildFileObject(undefined, id)
fileList.value.push(file)
fileList.value.reverse()
})
}
// url
if (props.uploadResultType === 'url') {
newVal.split(',').forEach((url) => {
const file = buildFileObject(url)
fileList.value.push(file)
fileList.value.reverse()
})
}
}
//
if (props.uploadResultCategory === 'array') {
if (props.completeResult) {
// thumbUrlbase64
let newResult = cloneDeep(newVal)
newResult.map((e) => {
if (e.thumbUrl) {
delete e.thumbUrl
}
if (props.uploadResultType === 'id') {
e.url = sysConfig.API_URL + props.uploadIdDownloadUrl + e.response.data
}
if (props.uploadResultType === 'url') {
e.url = e.response.data
}
})
fileList.value = newResult
} else {
// id
if (props.uploadResultType === 'id') {
newVal.forEach((id) => {
fileList.value.push(buildFileObject(undefined, id))
})
}
result.push(obj)
})
return result
} else {
return []
// url
if (props.uploadResultType === 'url') {
newVal.forEach((url) => {
fileList.value.push(buildFileObject(url))
})
}
}
}
}
//
defineExpose({
uploadFileList
})
//
watch(
() => props.value,
(newVal) => {
if (props.value && newVal) {
fileList.value = []
echo(newVal)
}
},
{ immediate: true, deep: true }
)
//
watch(
() => props.uploadMode,
(newVal) => {
if (newVal && newVal === 'image') {
if (props.accept) {
accept.value = props.accept
} else {
accept.value = 'image/*'
}
} else {
accept.value = props.accept
}
},
{ immediate: true, deep: true }
)
//
const progress = {
strokeWidth: 5,
format: (percent) => parseFloat(percent.toFixed(2)) + '%'
}
// image
const beforeUpload = (file) => {
const isPNG = file.type.startsWith('image/')
if (!isPNG) {
message.warning('只能上传图片类型文件')
}
return isPNG || Upload.LIST_IGNORE
}
//
const handlePreview = async (file) => {
previewVisible.value = true
previewTitle.value = file.name
// id
if (props.uploadResultType === 'id') {
previewImage.value = sysConfig.API_URL + props.uploadIdDownloadUrl + file.response.data
} else {
previewImage.value = file.response.data
}
}
//
const handleCancel = () => {
previewVisible.value = false
previewTitle.value = ''
previewImage.value = ''
}
//
const handleChange = (uploads) => {
let result = []
const file = uploads.file
if (file && (file.status === 'done' || file.status === 'removed') && file.response && file.response.code === 200) {
uploads.fileList.forEach((f) => {
result.push(f)
})
}
if (result.length > 0) {
if (props.uploadResultCategory === 'interval') {
const resultIntervalValue = ref('')
result.forEach((data) => {
resultIntervalValue.value =
data.response.data + (resultIntervalValue.value ? ',' + resultIntervalValue.value : '')
})
emit('update:value', resultIntervalValue)
emit('onChange', resultIntervalValue)
} else if (props.uploadResultCategory === 'array') {
if (props.completeResult) {
// thumbUrlbase64
let newResult = cloneDeep(result)
newResult.map((e) => {
if (e.thumbUrl) {
delete e.thumbUrl
}
})
emit('update:value', newResult)
emit('onChange', newResult)
} else {
const resultArrayValue = ref([])
result.forEach((data) => {
resultArrayValue.value.push(data.response.data)
})
emit('update:value', resultArrayValue)
emit('onChange', resultArrayValue)
}
}
return
}
emit('update:value', undefined)
emit('onChange', undefined)
}
</script>

View File

@ -10,8 +10,7 @@
class="xn-wd"
placeholder="请选择主表"
@select="selectTableColumnsData(formData.dbTable, false)"
>
</a-select>
/>
</a-form-item>
</a-col>
<a-col :span="8">
@ -21,8 +20,7 @@
:options="tableColumns"
class="xn-wd"
placeholder="选择主键"
>
</a-select>
/>
</a-form-item>
</a-col>
<a-col :span="8">
@ -31,8 +29,7 @@
v-model:value="formData.tablePrefix"
:options="tablePrefixOptions"
@change="tablePrefixChange"
>
</a-radio-group>
/>
</a-form-item>
</a-col>
<a-col :span="8">
@ -44,7 +41,7 @@
生成方式
</a-tooltip>
</template>
<a-radio-group v-model:value="formData.generateType" :options="generateTypeOptions"> </a-radio-group>
<a-radio-group v-model:value="formData.generateType" :options="generateTypeOptions" />
</a-form-item>
</a-col>
<a-col :span="8">
@ -55,8 +52,7 @@
class="xn-wd"
placeholder="请选择所属模块"
@change="moduleChange(formData.module, false)"
>
</a-select>
/>
</a-form-item>
</a-col>
<a-col :span="8">
@ -76,7 +72,7 @@
}"
selectable="false"
tree-line
></a-tree-select>
/>
</a-form-item>
</a-col>
<a-col :span="8">
@ -87,8 +83,7 @@
class="xn-wd"
placeholder="请选择移动端所属模块"
allow-clear
>
</a-select>
/>
</a-form-item>
</a-col>
<a-col :span="8">

View File

@ -29,6 +29,7 @@
:options="effectTypeOptions"
placeholder="请选择"
:disabled="toCommonFieldEstimate(record) || toFieldSelectEstimate(record)"
@change="effectTypeChange(record)"
/>
</template>
<template v-if="column.dataIndex === 'dictTypeCode'">
@ -42,7 +43,7 @@
<span v-else></span>
</template>
<template v-if="column.dataIndex === 'whetherTable'">
<a-checkbox v-model:checked="record.whetherTable" />
<a-checkbox v-model:checked="record.whetherTable" @change="whetherTableChange(record)"/>
</template>
<template v-if="column.dataIndex === 'whetherRetract'">
<a-checkbox v-model:checked="record.whetherRetract" :disabled="!record.whetherTable" />
@ -57,7 +58,7 @@
/>
</template>
<template v-if="column.dataIndex === 'queryWhether'">
<a-switch v-model:checked="record.queryWhether" :disabled="!record.whetherTable" />
<a-switch v-model:checked="record.queryWhether" :disabled="toQueryWhetherDisabled(record)" />
</template>
<template v-if="column.dataIndex === 'queryType'">
<a-select
@ -278,6 +279,18 @@
{
label: '滑动数字条',
value: 'slider'
},
{
label: '图片上传',
value: 'imageUpload'
},
{
label: '文件上传',
value: 'fileUpload'
},
{
label: '富文本',
value: 'editor'
}
])
//
@ -353,6 +366,34 @@
}
return false
}
// -
const effectTypeChange = (record) => {
//
if (record.effectType === 'imageUpload' || record.effectType === 'fileUpload') {
record.queryWhether = 'N'
record.queryType = null
}
//
if (record.effectType === 'editor') {
record.whetherTable = false
record.whetherRetract = false
record.queryWhether = 'N'
record.queryType = null
}
}
//
const whetherTableChange = (record) => {
//
if (!record.whetherTable) {
record.queryWhether = 'N'
record.queryType = null
}
}
//
const toQueryWhetherDisabled = (record) => {
//
return !record.whetherTable || record.effectType === 'imageUpload' || record.effectType === 'fileUpload'
}
//
const fieldJavaTypeChange = (record) => {
if (record.fieldJavaType === 'Date') {

View File

@ -35,6 +35,12 @@
<a-input-number v-model:value="formData.${configList[i].fieldNameCamelCase}" :min="1" :max="10000" style="width: 100%" />
<% } else if (configList[i].effectType == 'slider') {%>
<a-slider v-model:value="formData.${configList[i].fieldNameCamelCase}" :max="1000" style="width: 100%" />
<% } else if (configList[i].effectType == 'fileUpload') {%>
<xn-upload v-model:value="formData.${configList[i].fieldNameCamelCase}" />
<% } else if (configList[i].effectType == 'imageUpload') {%>
<xn-upload v-model:value="formData.${configList[i].fieldNameCamelCase}" uploadMode="image" />
<% } else if (configList[i].effectType == 'editor') {%>
<xn-editor v-model:value="formData.${configList[i].fieldNameCamelCase}" placeholder="请输入${configList[i].fieldRemark}" />
<% } %>
</a-form-item>
</a-col>
@ -63,6 +69,12 @@
<a-input-number v-model:value="formData.${configList[i].fieldNameCamelCase}" placeholder="请输入${configList[i].fieldRemark}" :min="1" :max="10000" style="width: 100%" />
<% } else if (configList[i].effectType == 'slider') {%>
<a-slider v-model:value="formData.${configList[i].fieldNameCamelCase}" placeholder="请滑动${configList[i].fieldRemark}" :max="1000" style="width: 100%" />
<% } else if (configList[i].effectType == 'fileUpload') {%>
<xn-upload v-model:value="formData.${configList[i].fieldNameCamelCase}" />
<% } else if (configList[i].effectType == 'imageUpload') {%>
<xn-upload v-model:value="formData.${configList[i].fieldNameCamelCase}" uploadMode="image" />
<% } else if (configList[i].effectType == 'editor') {%>
<xn-editor v-model:value="formData.${configList[i].fieldNameCamelCase}" placeholder="请输入${configList[i].fieldRemark}" />
<% } %>
</a-form-item>
<% } %>
@ -167,7 +179,7 @@
emit('successful')
})
.finally(() => {
submitLoading.value = false
submitLoading.value = false
})
})
.catch(() => {})

View File

@ -114,6 +114,14 @@
<template v-if="column.dataIndex === '${configList[i].fieldNameCamelCase}'">
<a-tag v-for="textValue in JSON.parse(record.${configList[i].fieldNameCamelCase})" :key="textValue" color="green">{{ $TOOL.dictTypeData('${configList[i].dictTypeCode}', textValue) }}</a-tag>
</template>
<% } else if (configList[i].effectType == 'imageUpload') { %>
<template v-if="column.dataIndex === '${configList[i].fieldNameCamelCase}'">
<a-image :src="record.${configList[i].fieldNameCamelCase}" style="width: 30px; height: 30px;" />
</template>
<% } else if (configList[i].effectType == 'fileUpload') { %>
<template v-if="column.dataIndex === '${configList[i].fieldNameCamelCase}'">
<a :href="record.${configList[i].fieldNameCamelCase}" :target="record.${configList[i].fieldNameCamelCase}">{{ record.${configList[i].fieldNameCamelCase} }}</a>
</template>
<% } %>
<% } %>
<% } %>