add 增加代码生成所需组件
This commit is contained in:
parent
9726df3966
commit
5260bf7f2e
@ -57,3 +57,43 @@ export interface TenantInfo {
|
||||
tenantEnabled: boolean;
|
||||
voList: TenantVO[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格列
|
||||
*/
|
||||
export interface Column {
|
||||
label: string;
|
||||
prop: string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
slot?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格上方的按钮
|
||||
*/
|
||||
export interface Button {
|
||||
text: string;
|
||||
type: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text';
|
||||
icon: string;
|
||||
handler: () => void;
|
||||
permission: string[];
|
||||
disabledCondition?: (ids: (string | number)[]) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格的操作栏
|
||||
*/
|
||||
export interface Operation {
|
||||
text: string;
|
||||
type: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text';
|
||||
icon: string;
|
||||
handler: (row: any) => void;
|
||||
permission: string[];
|
||||
needConfirm?: boolean;
|
||||
}
|
||||
|
||||
export interface QueryParams {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
[key: string]: any;
|
||||
}
|
72
src/components/RexCheckbox/index.vue
Normal file
72
src/components/RexCheckbox/index.vue
Normal file
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<el-col :span="colSpan">
|
||||
<el-form-item :label="label" :prop="prop">
|
||||
<el-checkbox-group v-bind="$attrs">
|
||||
<el-checkbox
|
||||
v-for="item in computedItems"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
></el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const _props = defineProps({
|
||||
label: String,
|
||||
prop: String,
|
||||
|
||||
/**
|
||||
* 每行显示几个, 默认24
|
||||
*/
|
||||
colSpan: {
|
||||
type: Number,
|
||||
default: 24
|
||||
},
|
||||
|
||||
/**
|
||||
* 传入的标准数据项
|
||||
*/
|
||||
items: {
|
||||
type: Array as () => any[],
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 传入的字典数据
|
||||
*/
|
||||
dicts: {
|
||||
type: Array as () => any[],
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 自定义配置项,仅当 dicts 为空时使用
|
||||
* 默认配置: { label: 'label', value: 'value' }
|
||||
*/
|
||||
props: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
label: 'label',
|
||||
value: 'value'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 使用计算属性统一管理显示数据
|
||||
const computedItems = computed(() => {
|
||||
if (_props.dicts.length > 0) {
|
||||
return _props.dicts.map((dict) => ({
|
||||
label: dict.label,
|
||||
value: dict.value
|
||||
}))
|
||||
} else {
|
||||
return _props.items.map((item) => ({
|
||||
label: item[_props.props.label],
|
||||
value: item[_props.props.value]
|
||||
}))
|
||||
}
|
||||
})
|
||||
</script>
|
23
src/components/RexDatePicker/index.vue
Normal file
23
src/components/RexDatePicker/index.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<el-col :span="colSpan">
|
||||
<el-form-item :label="label" :prop="prop">
|
||||
<el-date-picker v-bind="$attrs">
|
||||
<template v-for="(_, name) in $slots" #[name]="slotProps">
|
||||
<slot :name="name" v-bind="slotProps"></slot>
|
||||
</template>
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
label?: string
|
||||
prop?: string
|
||||
colSpan?: number
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
colSpan: 24
|
||||
})
|
||||
</script>
|
26
src/components/RexDrawer/index.vue
Normal file
26
src/components/RexDrawer/index.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<el-drawer v-bind="$attrs" :size="drawerSize" :show-close="false" append-to-body>
|
||||
<template #header="{ close, titleId, titleClass }">
|
||||
<h4 :id="titleId" :class="titleClass">{{ title }}</h4>
|
||||
<el-button @click="toggleDrawerSize" icon="FullScreen" link></el-button>
|
||||
<el-button @click="close" link icon="Close"></el-button>
|
||||
</template>
|
||||
<template v-for="(_value, name) in $slots" #[name]="scopeData">
|
||||
<slot :name="name" v-bind="scopeData || {}"></slot>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useDrawerSize } from './useDrawerSize';
|
||||
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
initialSize: {
|
||||
type: Number,
|
||||
default: 800
|
||||
}
|
||||
});
|
||||
|
||||
const { drawerSize, toggleDrawerSize } = useDrawerSize(props.initialSize);
|
||||
</script>
|
18
src/components/RexDrawer/useDrawerSize.ts
Normal file
18
src/components/RexDrawer/useDrawerSize.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
export function useDrawerSize(initialSize = 800) {
|
||||
const drawerSize = ref(initialSize);
|
||||
const maxDrawerSize = document.body.clientWidth;
|
||||
|
||||
const isFullSize = computed(() => drawerSize.value === maxDrawerSize);
|
||||
|
||||
const toggleDrawerSize = () => {
|
||||
drawerSize.value = isFullSize.value ? initialSize : maxDrawerSize;
|
||||
};
|
||||
|
||||
return {
|
||||
drawerSize: computed(() => `${drawerSize.value}px`),
|
||||
isFullSize,
|
||||
toggleDrawerSize
|
||||
};
|
||||
}
|
163
src/components/RexEditor/index.vue
Normal file
163
src/components/RexEditor/index.vue
Normal file
@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<el-col :span="colSpan">
|
||||
<el-form-item :label="label" :prop="prop">
|
||||
<div>
|
||||
<el-upload
|
||||
v-if="type === 'url'"
|
||||
:action="upload.url"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-error="handleUploadError"
|
||||
class="editor-img-uploader"
|
||||
name="file"
|
||||
:show-file-list="false"
|
||||
:headers="upload.headers"
|
||||
ref="uploadRef"
|
||||
/>
|
||||
<div class="editor">
|
||||
<quill-editor
|
||||
ref="quillEditorRef"
|
||||
v-model:content="content"
|
||||
contentType="html"
|
||||
@textChange="handleTextChange"
|
||||
:options="options"
|
||||
:style="styles"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { QuillEditor, Quill } from '@vueup/vue-quill'
|
||||
import '@vueup/vue-quill/dist/vue-quill.snow.css'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { globalHeaders } from '@/utils/request'
|
||||
import { computed, ref, watch, reactive, toRaw } from 'vue'
|
||||
import { getCurrentInstance } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
label: String,
|
||||
prop: String,
|
||||
colSpan: {
|
||||
type: Number,
|
||||
default: 24
|
||||
},
|
||||
modelValue: propTypes.string,
|
||||
height: propTypes.number.def(400),
|
||||
minHeight: propTypes.number.def(400),
|
||||
readOnly: propTypes.bool.def(false),
|
||||
fileSize: propTypes.number.def(5),
|
||||
type: propTypes.string.def('url')
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const upload = reactive({
|
||||
headers: globalHeaders(),
|
||||
url: import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload'
|
||||
})
|
||||
|
||||
const quillEditorRef = ref()
|
||||
const content = ref('')
|
||||
|
||||
const options = computed(() => ({
|
||||
theme: 'snow',
|
||||
bounds: document.body,
|
||||
debug: 'warn',
|
||||
modules: {
|
||||
toolbar: {
|
||||
container: [
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
['blockquote', 'code-block'],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ indent: '-1' }, { indent: '+1' }],
|
||||
[{ size: ['small', false, 'large', 'huge'] }],
|
||||
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||||
[{ color: [] }, { background: [] }],
|
||||
[{ align: [] }],
|
||||
['clean'],
|
||||
['link', 'image', 'video']
|
||||
],
|
||||
handlers: {
|
||||
image: () => {
|
||||
const uploadElement = document.querySelector('.editor-img-uploader>.el-upload') as HTMLElement
|
||||
uploadElement?.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
placeholder: '请输入内容',
|
||||
readOnly: props.readOnly
|
||||
}))
|
||||
|
||||
const styles = computed(() => ({
|
||||
minHeight: props.minHeight ? `${props.minHeight}px` : undefined,
|
||||
height: props.height ? `${props.height}px` : undefined
|
||||
}))
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (newValue !== content.value) {
|
||||
content.value = newValue ?? '<p></p>'
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const handleTextChange = () => emit('update:modelValue', content.value)
|
||||
|
||||
const handleUploadSuccess = (res: any) => {
|
||||
if (res.code === 200) {
|
||||
const quill = toRaw(quillEditorRef.value).getQuill()
|
||||
const length = quill.selection.savedRange.index
|
||||
quill.insertEmbed(length, 'image', res.data.url)
|
||||
quill.setSelection(length + 1)
|
||||
proxy?.$modal.closeLoading()
|
||||
} else {
|
||||
proxy?.$modal.msgError(res.msg)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBeforeUpload = (file: File) => {
|
||||
const isValidType = ['image/jpeg', 'image/jpg', 'image/png', 'image/svg'].includes(file.type)
|
||||
if (!isValidType) {
|
||||
proxy?.$modal.msgError(`图片格式错误!`)
|
||||
return false
|
||||
}
|
||||
|
||||
const isValidSize = file.size / 1024 / 1024 < props.fileSize
|
||||
if (!isValidSize) {
|
||||
proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
|
||||
return false
|
||||
}
|
||||
|
||||
proxy?.$modal.loading('正在上传文件,请稍候...')
|
||||
return true
|
||||
}
|
||||
|
||||
const handleUploadError = (err: any) => {
|
||||
console.error(err)
|
||||
proxy?.$modal.msgError('上传文件失败')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.editor-img-uploader {
|
||||
display: none;
|
||||
}
|
||||
.editor,
|
||||
.ql-toolbar {
|
||||
white-space: pre-wrap !important;
|
||||
line-height: normal !important;
|
||||
}
|
||||
.quill-img {
|
||||
display: none;
|
||||
}
|
||||
/* 其他样式保持不变 */
|
||||
</style>
|
231
src/components/RexFileUpload/index.vue
Normal file
231
src/components/RexFileUpload/index.vue
Normal file
@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<el-col :span="colSpan">
|
||||
<el-form-item :label="label" :prop="prop">
|
||||
<div class="upload-file">
|
||||
<el-upload
|
||||
multiple
|
||||
:action="uploadFileUrl"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:file-list="fileList"
|
||||
:limit="limit"
|
||||
:on-error="handleUploadError"
|
||||
:on-exceed="handleExceed"
|
||||
:on-success="handleUploadSuccess"
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
class="upload-file-uploader"
|
||||
ref="fileUploadRef"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<!-- 上传按钮 -->
|
||||
<el-button type="primary">选取文件</el-button>
|
||||
</el-upload>
|
||||
<!-- 上传提示 -->
|
||||
<div class="el-upload__tip" v-if="showTip">
|
||||
请上传
|
||||
<template v-if="fileSize">
|
||||
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
|
||||
</template>
|
||||
<template v-if="fileType">
|
||||
格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
|
||||
</template>
|
||||
的文件
|
||||
</div>
|
||||
<!-- 文件列表 -->
|
||||
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
|
||||
<li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
|
||||
<el-link :href="`${file.url}`" :underline="false" target="_blank">
|
||||
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
|
||||
</el-link>
|
||||
<div class="ele-upload-list__item-content-action">
|
||||
<el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
|
||||
</div>
|
||||
</li>
|
||||
</transition-group>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { listByIds, delOss } from "@/api/system/oss";
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
import { globalHeaders } from "@/utils/request";
|
||||
|
||||
const props = defineProps({
|
||||
label: String,
|
||||
prop: String,
|
||||
/**
|
||||
* 每行显示几个, 默认24
|
||||
*/
|
||||
colSpan: {
|
||||
type: Number,
|
||||
default: 24
|
||||
},
|
||||
modelValue: [String, Object, Array],
|
||||
// 数量限制
|
||||
limit: propTypes.number.def(5),
|
||||
// 大小限制(MB)
|
||||
fileSize: propTypes.number.def(5),
|
||||
// 文件类型, 例如['png', 'jpg', 'jpeg']
|
||||
fileType: propTypes.array.def(["doc", "xls", "ppt", "txt", "pdf"]),
|
||||
// 是否显示提示
|
||||
isShowTip: propTypes.bool.def(true),
|
||||
})
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const number = ref(0);
|
||||
const uploadList = ref<any[]>([]);
|
||||
|
||||
const baseUrl = import.meta.env.VITE_APP_BASE_API;
|
||||
const uploadFileUrl = ref(baseUrl + "/resource/oss/upload"); // 上传文件服务器地址
|
||||
const headers = ref(globalHeaders());
|
||||
|
||||
const fileList = ref<any[]>([]);
|
||||
const showTip = computed(
|
||||
() => props.isShowTip && (props.fileType || props.fileSize)
|
||||
);
|
||||
|
||||
const fileUploadRef = ref<ElUploadInstance>();
|
||||
|
||||
watch(() => props.modelValue, async val => {
|
||||
if (val) {
|
||||
let temp = 1;
|
||||
// 首先将值转为数组
|
||||
let list = [];
|
||||
if (Array.isArray(val)) {
|
||||
list = val;
|
||||
} else {
|
||||
const res = await listByIds(val as string)
|
||||
list = res.data.map((oss) => {
|
||||
const data = { name: oss.originalName, url: oss.url, ossId: oss.ossId };
|
||||
return data;
|
||||
});
|
||||
}
|
||||
// 然后将数组转为对象数组
|
||||
fileList.value = list.map(item => {
|
||||
item = { name: item.name, url: item.url, ossId: item.ossId };
|
||||
item.uid = item.uid || new Date().getTime() + temp++;
|
||||
return item;
|
||||
});
|
||||
} else {
|
||||
fileList.value = [];
|
||||
return [];
|
||||
}
|
||||
}, { deep: true, immediate: true });
|
||||
|
||||
// 上传前校检格式和大小
|
||||
const handleBeforeUpload = (file: any) => {
|
||||
// 校检文件类型
|
||||
if (props.fileType.length) {
|
||||
const fileName = file.name.split('.');
|
||||
const fileExt = fileName[fileName.length - 1];
|
||||
const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
|
||||
if (!isTypeOk) {
|
||||
proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join("/")}格式文件!`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 校检文件大小
|
||||
if (props.fileSize) {
|
||||
const isLt = file.size / 1024 / 1024 < props.fileSize;
|
||||
if (!isLt) {
|
||||
proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
proxy?.$modal.loading("正在上传文件,请稍候...");
|
||||
number.value++;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 文件个数超出
|
||||
const handleExceed = () => {
|
||||
proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
|
||||
}
|
||||
|
||||
// 上传失败
|
||||
const handleUploadError = () => {
|
||||
proxy?.$modal.msgError("上传文件失败");
|
||||
}
|
||||
|
||||
// 上传成功回调
|
||||
const handleUploadSuccess = (res: any, file: UploadFile) => {
|
||||
if (res.code === 200) {
|
||||
uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
|
||||
uploadedSuccessfully();
|
||||
} else {
|
||||
number.value--;
|
||||
proxy?.$modal.closeLoading();
|
||||
proxy?.$modal.msgError(res.msg);
|
||||
fileUploadRef.value?.handleRemove(file);
|
||||
uploadedSuccessfully();
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
const handleDelete = (index: number) => {
|
||||
let ossId = fileList.value[index].ossId;
|
||||
delOss(ossId);
|
||||
fileList.value.splice(index, 1);
|
||||
emit("update:modelValue", listToString(fileList.value));
|
||||
}
|
||||
|
||||
// 上传结束处理
|
||||
const uploadedSuccessfully = () => {
|
||||
if (number.value > 0 && uploadList.value.length === number.value) {
|
||||
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
|
||||
uploadList.value = [];
|
||||
number.value = 0;
|
||||
emit("update:modelValue", listToString(fileList.value));
|
||||
proxy?.$modal.closeLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 获取文件名称
|
||||
const getFileName = (name: string) => {
|
||||
// 如果是url那么取最后的名字 如果不是直接返回
|
||||
if (name.lastIndexOf("/") > -1) {
|
||||
return name.slice(name.lastIndexOf("/") + 1);
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
// 对象转成指定字符串分隔
|
||||
const listToString = (list: any[], separator?: string) => {
|
||||
let strs = "";
|
||||
separator = separator || ",";
|
||||
list.forEach(item => {
|
||||
if (item.ossId) {
|
||||
strs += item.ossId + separator;
|
||||
}
|
||||
})
|
||||
return strs != "" ? strs.substring(0, strs.length - 1) : "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.upload-file-uploader {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.upload-file-list .el-upload-list__item {
|
||||
border: 1px solid #e4e7ed;
|
||||
line-height: 2;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.upload-file-list .ele-upload-list__item-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.ele-upload-list__item-content-action .el-link {
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
230
src/components/RexImageUpload/index.vue
Normal file
230
src/components/RexImageUpload/index.vue
Normal file
@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<el-col :span="colSpan">
|
||||
<el-form-item :label="label" :prop="prop">
|
||||
<div class="component-upload-image">
|
||||
<el-upload
|
||||
multiple
|
||||
:action="uploadImgUrl"
|
||||
list-type="picture-card"
|
||||
:on-success="handleUploadSuccess"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:limit="limit"
|
||||
:on-error="handleUploadError"
|
||||
:on-exceed="handleExceed"
|
||||
ref="imageUpload"
|
||||
:before-remove="handleDelete"
|
||||
:show-file-list="true"
|
||||
:headers="headers"
|
||||
:file-list="fileList"
|
||||
:on-preview="handlePictureCardPreview"
|
||||
:class="{ hide: fileList.length >= limit }"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<el-icon class="avatar-uploader-icon">
|
||||
<plus />
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
<!-- 上传提示 -->
|
||||
<div class="el-upload__tip" v-if="showTip">
|
||||
请上传
|
||||
<template v-if="fileSize">
|
||||
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
|
||||
</template>
|
||||
<template v-if="fileType">
|
||||
格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
|
||||
</template>
|
||||
的文件
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
|
||||
<img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { listByIds, delOss } from "@/api/system/oss";
|
||||
import { ComponentInternalInstance } from "vue";
|
||||
import { OssVO } from "@/api/system/oss/types";
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
import {globalHeaders} from "@/utils/request";
|
||||
|
||||
const props = defineProps({
|
||||
label: String,
|
||||
prop: String,
|
||||
/**
|
||||
* 每行显示几个, 默认24
|
||||
*/
|
||||
colSpan: {
|
||||
type: Number,
|
||||
default: 24
|
||||
},
|
||||
modelValue: [String, Object, Array],
|
||||
// 图片数量限制
|
||||
limit: propTypes.number.def(5),
|
||||
// 大小限制(MB)
|
||||
fileSize: propTypes.number.def(5),
|
||||
// 文件类型, 例如['png', 'jpg', 'jpeg']
|
||||
fileType: propTypes.array.def(["png", "jpg", "jpeg"]),
|
||||
// 是否显示提示
|
||||
isShowTip: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
})
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const number = ref(0);
|
||||
const uploadList = ref<any[]>([]);
|
||||
const dialogImageUrl = ref("");
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
const baseUrl = import.meta.env.VITE_APP_BASE_API;
|
||||
const uploadImgUrl = ref(baseUrl + "/resource/oss/upload"); // 上传的图片服务器地址
|
||||
const headers = ref(globalHeaders());
|
||||
|
||||
const fileList = ref<any[]>([]);
|
||||
const showTip = computed(
|
||||
() => props.isShowTip && (props.fileType || props.fileSize)
|
||||
);
|
||||
|
||||
const imageUploadRef = ref<ElUploadInstance>();
|
||||
|
||||
watch(() => props.modelValue, async val => {
|
||||
if (val) {
|
||||
// 首先将值转为数组
|
||||
let list: OssVO[] = [];
|
||||
if (Array.isArray(val)) {
|
||||
list = val as OssVO[];
|
||||
} else {
|
||||
const res = await listByIds(val as string)
|
||||
list = res.data
|
||||
}
|
||||
// 然后将数组转为对象数组
|
||||
fileList.value = list.map(item => {
|
||||
// 字符串回显处理 如果此处存的是url可直接回显 如果存的是id需要调用接口查出来
|
||||
let itemData;
|
||||
if (typeof item === "string") {
|
||||
itemData = { name: item, url: item };
|
||||
} else {
|
||||
// 此处name使用ossId 防止删除出现重名
|
||||
itemData = { name: item.ossId, url: item.url, ossId: item.ossId };
|
||||
}
|
||||
return itemData;
|
||||
});
|
||||
} else {
|
||||
fileList.value = [];
|
||||
return [];
|
||||
}
|
||||
}, { deep: true, immediate: true });
|
||||
|
||||
/** 上传前loading加载 */
|
||||
const handleBeforeUpload = (file: any) => {
|
||||
let isImg = false;
|
||||
if (props.fileType.length) {
|
||||
let fileExtension = "";
|
||||
if (file.name.lastIndexOf(".") > -1) {
|
||||
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
|
||||
}
|
||||
isImg = props.fileType.some((type: any) => {
|
||||
if (file.type.indexOf(type) > -1) return true;
|
||||
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
isImg = file.type.indexOf("image") > -1;
|
||||
}
|
||||
if (!isImg) {
|
||||
proxy?.$modal.msgError(
|
||||
`文件格式不正确, 请上传${props.fileType.join("/")}图片格式文件!`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (props.fileSize) {
|
||||
const isLt = file.size / 1024 / 1024 < props.fileSize;
|
||||
if (!isLt) {
|
||||
proxy?.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
proxy?.$modal.loading("正在上传图片,请稍候...");
|
||||
number.value++;
|
||||
}
|
||||
|
||||
// 文件个数超出
|
||||
const handleExceed = () => {
|
||||
proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
|
||||
}
|
||||
|
||||
// 上传成功回调
|
||||
const handleUploadSuccess = (res: any, file: UploadFile) => {
|
||||
if (res.code === 200) {
|
||||
uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
|
||||
uploadedSuccessfully();
|
||||
} else {
|
||||
number.value--;
|
||||
proxy?.$modal.closeLoading();
|
||||
proxy?.$modal.msgError(res.msg);
|
||||
imageUploadRef.value?.handleRemove(file);
|
||||
uploadedSuccessfully();
|
||||
}
|
||||
}
|
||||
|
||||
// 删除图片
|
||||
const handleDelete = (file: UploadFile): boolean => {
|
||||
const findex = fileList.value.map(f => f.name).indexOf(file.name);
|
||||
if (findex > -1 && uploadList.value.length === number.value) {
|
||||
let ossId = fileList.value[findex].ossId;
|
||||
delOss(ossId);
|
||||
fileList.value.splice(findex, 1);
|
||||
emit("update:modelValue", listToString(fileList.value));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 上传结束处理
|
||||
const uploadedSuccessfully = () => {
|
||||
if (number.value > 0 && uploadList.value.length === number.value) {
|
||||
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
|
||||
uploadList.value = [];
|
||||
number.value = 0;
|
||||
emit("update:modelValue", listToString(fileList.value));
|
||||
proxy?.$modal.closeLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 上传失败
|
||||
const handleUploadError = () => {
|
||||
proxy?.$modal.msgError("上传图片失败");
|
||||
proxy?.$modal.closeLoading();
|
||||
}
|
||||
|
||||
// 预览
|
||||
const handlePictureCardPreview = (file: any) => {
|
||||
dialogImageUrl.value = file.url;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 对象转成指定字符串分隔
|
||||
const listToString = (list: any[], separator?: string) => {
|
||||
let strs = "";
|
||||
separator = separator || ",";
|
||||
for (let i in list) {
|
||||
if (undefined !== list[i].ossId && list[i].url.indexOf("blob:") !== 0) {
|
||||
strs += list[i].ossId + separator;
|
||||
}
|
||||
}
|
||||
return strs != "" ? strs.substring(0, strs.length - 1) : "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// .el-upload--picture-card 控制加号部分
|
||||
:deep(.hide .el-upload--picture-card) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
31
src/components/RexInput/index.vue
Normal file
31
src/components/RexInput/index.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<el-col :span="colSpan">
|
||||
<el-form-item :label="label" :prop="prop">
|
||||
<el-input v-bind="$attrs" :placeholder="computedPlaceholder">
|
||||
<template v-for="(_, name) in $slots" #[name]="slotProps">
|
||||
<slot :name="name" v-bind="slotProps"></slot>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
label?: string
|
||||
prop?: string
|
||||
colSpan?: number
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
colSpan: 24,
|
||||
placeholder: ''
|
||||
})
|
||||
|
||||
const computedPlaceholder = computed(() => {
|
||||
return props.placeholder || (props.label ? `请输入${props.label}` : '')
|
||||
})
|
||||
</script>
|
65
src/components/RexRadio/index.vue
Normal file
65
src/components/RexRadio/index.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<el-col :span="colSpan">
|
||||
<el-form-item :label="label" :prop="prop">
|
||||
<el-radio-group v-bind="$attrs">
|
||||
<el-radio v-for="item in computedItems" :key="item.value" :label="item.label" :value="item.value"></el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const _props = defineProps({
|
||||
label: String,
|
||||
prop: String,
|
||||
/**
|
||||
* 每行显示几个, 默认24
|
||||
*/
|
||||
colSpan: {
|
||||
type: Number,
|
||||
default: 24
|
||||
},
|
||||
/**
|
||||
* 传入的标准数据项
|
||||
*/
|
||||
items: {
|
||||
type: Array as () => any[],
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 传入的字典数据
|
||||
*/
|
||||
dicts: {
|
||||
type: Array as () => any[],
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 自定义配置项,仅当 dicts 为空时使用
|
||||
* 默认配置: { label: 'label', value: 'value' }
|
||||
*/
|
||||
props: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
label: 'label',
|
||||
value: 'value'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 使用计算属性统一管理显示数据
|
||||
const computedItems = computed(() => {
|
||||
if (_props.dicts.length > 0) {
|
||||
return _props.dicts.map((dict) => ({
|
||||
label: dict.label,
|
||||
value: dict.value
|
||||
}))
|
||||
} else {
|
||||
return _props.items.map((item) => ({
|
||||
label: item[_props.props.label],
|
||||
value: item[_props.props.value]
|
||||
}))
|
||||
}
|
||||
})
|
||||
</script>
|
69
src/components/RexSelect/index.vue
Normal file
69
src/components/RexSelect/index.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<el-col :span="colSpan">
|
||||
<el-form-item :label="label" :prop="prop">
|
||||
<el-select v-bind="$attrs">
|
||||
<el-option v-for="item in computedItems" :value="item[props.value]" :label="item[props.label]"></el-option>
|
||||
<template v-for="(_value, name) in $slots" #[name]="scopeData">
|
||||
<slot :name="name" v-bind="scopeData"></slot>
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const _props = defineProps({
|
||||
label: String,
|
||||
prop: String,
|
||||
|
||||
/**
|
||||
* 每行显示几个, 默认24
|
||||
*/
|
||||
colSpan: {
|
||||
type: Number,
|
||||
default: 24
|
||||
},
|
||||
/**
|
||||
* 传入的标准数据项
|
||||
*/
|
||||
items: {
|
||||
type: Array as () => any[],
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 传入的字典数据
|
||||
*/
|
||||
dicts: {
|
||||
type: Array as () => any[],
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 自定义配置项,仅当 dicts 为空时使用
|
||||
* 默认配置: { label: 'label', value: 'value' }
|
||||
*/
|
||||
props: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
label: 'label',
|
||||
value: 'value'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 使用计算属性统一管理显示数据
|
||||
const computedItems = computed(() => {
|
||||
if (_props.dicts.length > 0) {
|
||||
return _props.dicts.map((dict) => ({
|
||||
label: dict.label,
|
||||
value: dict.value
|
||||
}))
|
||||
} else {
|
||||
return _props.items.map((item) => ({
|
||||
label: item[_props.props.label],
|
||||
value: item[_props.props.value]
|
||||
}))
|
||||
}
|
||||
})
|
||||
</script>
|
132
src/components/RexTable/index.vue
Normal file
132
src/components/RexTable/index.vue
Normal file
@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5" v-for="button in buttons" :key="button.text">
|
||||
<el-button
|
||||
:type="button.type"
|
||||
plain
|
||||
:icon="button.icon"
|
||||
:disabled="button.disabledCondition ? button.disabledCondition(selectedIds) : false"
|
||||
@click="() => handleButtonClick(button.handler)"
|
||||
v-if="button.permission && button.permission.length > 0"
|
||||
v-hasPermi="button.permission"
|
||||
>
|
||||
{{ button.text }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
:type="button.type"
|
||||
plain
|
||||
:icon="button.icon"
|
||||
:disabled="button.disabledCondition ? button.disabledCondition(selectedIds) : false"
|
||||
@click="() => handleButtonClick(button.handler)"
|
||||
>
|
||||
{{ button.text }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="$emit('query')"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table ref="tableRef" v-loading="loading" :data="data" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="50" align="center" />
|
||||
<el-table-column v-for="column in columns" :key="column.prop" v-bind="column">
|
||||
<template #default="scope" v-if="column.slot">
|
||||
<slot :name="column.slot" :row="scope.row"></slot>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" v-if="operations.length">
|
||||
<template #default="scope">
|
||||
<el-tooltip v-for="op in operations" :key="op.text" :content="op.text" placement="top">
|
||||
<el-button
|
||||
link
|
||||
:type="op.type"
|
||||
:icon="op.icon"
|
||||
@click="() => handleOperation(op, scope.row)"
|
||||
v-if="op.permission && op.permission.length > 0"
|
||||
v-hasPermi="op.permission"
|
||||
></el-button>
|
||||
<el-button
|
||||
v-else
|
||||
link
|
||||
:type="op.type"
|
||||
:icon="op.icon"
|
||||
@click="() => handleOperation(op, scope.row)"
|
||||
></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="$emit('query')"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Button, Column, Operation, QueryParams } from '@/api/types'
|
||||
import { ref } from 'vue'
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance
|
||||
|
||||
const props = defineProps<{
|
||||
loading: boolean
|
||||
data: any[]
|
||||
total: number
|
||||
queryParams: QueryParams
|
||||
columns: Column[]
|
||||
buttons: Button[]
|
||||
operations: Operation[]
|
||||
clearSelection: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:selectedIds', ids: (string | number)[]): void
|
||||
(e: 'query'): void
|
||||
(e: 'update:clearSelection', clearSelection: boolean): void
|
||||
}>()
|
||||
|
||||
const showSearch = ref(true)
|
||||
const selectedIds = ref<(string | number)[]>([])
|
||||
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedIds.value = selection.map((item) => item.id)
|
||||
emit('update:selectedIds', selectedIds.value)
|
||||
}
|
||||
|
||||
const handleOperation = async (op: Operation, row: any) => {
|
||||
if (op.needConfirm) {
|
||||
try {
|
||||
await proxy?.$modal.confirm(`确定要${op.text}吗?`)
|
||||
op.handler(row)
|
||||
} catch (error) {
|
||||
console.log('Operation cancelled')
|
||||
}
|
||||
} else {
|
||||
op.handler(row)
|
||||
}
|
||||
}
|
||||
|
||||
const handleButtonClick = (handler: () => void) => {
|
||||
handler()
|
||||
}
|
||||
|
||||
const tableRef = ref()
|
||||
watch(
|
||||
() => props.clearSelection,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
tableRef.value?.clearSelection()
|
||||
selectedIds.value = []
|
||||
emit('update:selectedIds', [])
|
||||
emit('update:clearSelection', false)
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
21
src/components/RexTreeSelect/index.vue
Normal file
21
src/components/RexTreeSelect/index.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<el-col :span="colSpan">
|
||||
<el-form-item :label="label" :prop="prop">
|
||||
<el-tree-select v-bind="$attrs"></el-tree-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
label: String,
|
||||
prop: String,
|
||||
/**
|
||||
* 每行显示几个, 默认24
|
||||
*/
|
||||
colSpan: {
|
||||
type: Number,
|
||||
default: 24
|
||||
}
|
||||
});
|
||||
</script>
|
Loading…
x
Reference in New Issue
Block a user