Jelajahi Sumber

Add 前端部分组件优化

Yue 2 tahun lalu
induk
melakukan
c84acd5b29

+ 3 - 3
UI/VAP.VUE/src/components/table/partials/body/BodyTd.vue

@@ -11,14 +11,14 @@ const props = withDefaults(
 		index?: number
 		rowspan?: string
 		isTree?: boolean
-		depth?: number
+		orgh?: number
 	}>(),
 	{
 		isTree: false,
 		iconField: "name",
 		leafField: "isLeaf",
 		intervalLeft: 10,
-		depth: 0
+		orgh: 0
 	}
 )
 const propsOpts = inject(symbolKeys.bodyTd, {
@@ -87,7 +87,7 @@ onMounted(init)
 			v-if="isTree && column.field == propsOpts.iconField"
 			@click="onToggle"
 			class="me-2"
-			:style="`margin-left: ${propsOpts.intervalLeft * props.depth}px!important`">
+			:style="`margin-left: ${propsOpts.intervalLeft * props.orgh}px!important`">
 			<template v-if="propsOpts.hasChildren || (!isLeaf && propsOpts.isLazy)">
 				<VbIcon
 					v-if="propsOpts.isLazy ? false : propsOpts.isExpand"

+ 3 - 3
UI/VAP.VUE/src/components/table/partials/body/BodyTds.vue

@@ -7,7 +7,7 @@ const props = defineProps<{
 	isExpand: boolean
 	hasChildren: boolean
 	index: number
-	depth: number
+	orgh: number
 }>()
 
 const propOpts = inject(symbolKeys.bodyTds, {
@@ -39,7 +39,7 @@ provide(
 				:column="column"
 				:index="index"
 				:is-tree="isTree"
-				:depth="depth"
+				:orgh="orgh"
 				:rowspan="`${
 					row[column.field + propOpts.rowSpanSuffix]
 						? row[column.field + propOpts.rowSpanSuffix]
@@ -51,7 +51,7 @@ provide(
 			</BodyTd>
 		</template>
 		<template v-else-if="row[column.field + propOpts.rowSpanSuffix] != 0">
-			<BodyTd :row="row" :column="column" :index="index" :is-tree="isTree" :depth="depth">
+			<BodyTd :row="row" :column="column" :index="index" :is-tree="isTree" :orgh="orgh">
 				<template v-for="(_, name) in $slots" #[name]="{ row }">
 					<slot :name="name" :row="row"></slot>
 				</template>

+ 4 - 4
UI/VAP.VUE/src/components/table/partials/body/BodyTr.vue

@@ -7,7 +7,7 @@ import BodyTds from "@@@/table/partials/body/BodyTds.vue"
 const props = defineProps<{
 	row: any
 	index: number
-	depth: number
+	orgh: number
 }>()
 
 const propOpts = inject(symbolKeys.bodyTr, {
@@ -30,7 +30,7 @@ const selectedIds = toRef(propOpts.selectedIds)
 const tableBox = inject(symbolKeys.tableBox) as Ref<HTMLElement>
 
 const isExpand = computed(() => {
-	return propOpts.isLazy ? true : propOpts.expandDepth > props.depth
+	return propOpts.isLazy ? true : propOpts.expandDepth > props.orgh
 })
 
 const hasChildren = computed(() => {
@@ -42,7 +42,7 @@ const getTrClass = computed(() => {
 	if (hasChildren.value) {
 		_trClass += `hasChildren ${isExpand.value ? "tr-expand" : "tr-collapse"} `
 	}
-	_trClass += !propOpts.isLazy && props.depth > propOpts.expandDepth ? "hide" : "show"
+	_trClass += !propOpts.isLazy && props.orgh > propOpts.expandDepth ? "hide" : "show"
 	return _trClass
 })
 function getParent(row: any) {
@@ -86,7 +86,7 @@ const onDbRowClick = () => {
 			:is-tree="propOpts.isTree"
 			:hasChildren="hasChildren"
 			:isExpand="isExpand"
-			:depth="depth">
+			:orgh="orgh">
 			<template v-for="(_, name) in $slots" #[name]="{ row }">
 				<slot :name="name" :row="Object.assign({}, row)" />
 			</template>

+ 3 - 3
UI/VAP.VUE/src/components/table/partials/body/BodyTreeTr.vue

@@ -6,7 +6,7 @@ const props = defineProps<{
 	rows: any[]
 	childrenField: string
 	index: number
-	depth: number
+	orgh: number
 }>()
 const hasChildren = (row: any) => {
 	return row[props.childrenField] && row[props.childrenField].length
@@ -14,7 +14,7 @@ const hasChildren = (row: any) => {
 </script>
 <template>
 	<template v-for="(v, i) in rows" :key="i">
-		<BodyTr :row="v" :depth="depth" :index="index">
+		<BodyTr :row="v" :orgh="orgh" :index="index">
 			<template v-for="(_, name) in $slots" #[name]="{ row }">
 				<slot :name="name" :row="row" />
 			</template>
@@ -24,7 +24,7 @@ const hasChildren = (row: any) => {
 				:index="index + 1"
 				:rows="v[childrenField]"
 				:childrenField="childrenField"
-				:depth="depth + 1">
+				:orgh="orgh + 1">
 				<template v-for="(_, name) in $slots" #[name]="{ row }">
 					<slot :name="name" :row="Object.assign({}, row)" />
 				</template>

+ 3 - 3
UI/VAP.VUE/src/components/table/partials/body/TableBody.vue

@@ -9,7 +9,7 @@ const propOpts = inject(symbolKeys.body, {
 	childrenField: "children",
 	bodyClass: "fw-semibold text-gray-600"
 })
-const depth = 0
+const orgh = 0
 const rows = inject(symbolKeys.displayData, ref([]))
 const tbody = ref<HTMLElement>()
 const hasChildren = (row: any) => {
@@ -19,7 +19,7 @@ const hasChildren = (row: any) => {
 <template>
 	<tbody v-if="rows?.length > 0" ref="tbody" :class="propOpts.bodyClass">
 		<template v-for="(row, i) in rows" :key="i">
-			<BodyTr :row="row" :depth="depth" :index="i + 1">
+			<BodyTr :row="row" :orgh="orgh" :index="i + 1">
 				<template v-for="(_, name) in $slots" #[name]="{ row }">
 					<slot :name="name" :row="row" />
 				</template>
@@ -29,7 +29,7 @@ const hasChildren = (row: any) => {
 					:index="i + 1"
 					:rows="row[propOpts.childrenField]"
 					:childrenField="propOpts.childrenField"
-					:depth="depth + 1">
+					:orgh="orgh + 1">
 					<template v-for="(_, name) in $slots" #[name]="{ row }">
 						<slot :name="name" :row="row" />
 					</template>

+ 1 - 1
UI/VAP.VUE/src/components/table/partials/footer/TablePagination.vue

@@ -2,7 +2,7 @@
 import { VbUtil } from "@@/vb-dom"
 import symbolKeys from "@@@/table/symbol"
 
-const propOpts = inject(symbolKeys.pageination, {
+const propOpts = inject(symbolKeys.pagination, {
 	currentPage: ref(1),
 	pageSize: ref(15),
 	total: ref(0),

+ 1 - 1
UI/VAP.VUE/src/components/table/symbol.ts

@@ -83,7 +83,7 @@ export default {
 		tableClass: string
 		showBody: boolean
 	}>,
-	pageination: Symbol("pageination") as InjectionKey<{
+	pagination: Symbol("pagination") as InjectionKey<{
 		total: Ref<number>
 		currentPage: Ref<number>
 		pageSize: Ref<number>

+ 1 - 1
UI/VAP.Vue/src/api/system/_menu.ts

@@ -29,7 +29,7 @@ class menuApi {
 	}
 
 	// 查询权限下的子菜单
-	mennChildrenByPerms = (perms: string) => {
+	menuChildrenByPerms = (perms: string) => {
 		return Rs.get({
 			url: "/system/menu/childrenByPerms/" + perms
 		})

+ 4 - 0
UI/VAP.Vue/src/components/dict/DictSelect.vue

@@ -4,9 +4,11 @@ import appStore from "@s"
 const props = withDefaults(
 	defineProps<{
 		modelValue?: any
+		showAll?: boolean
 		dictType: string
 		placeholder?: string
 		type?: "select" | "checkbox" | "radio"
+		valueIsNumber?: boolean
 		listeners?: any
 	}>(),
 	{
@@ -38,7 +40,9 @@ function getData() {
 		:data-fun="getData"
 		:need-formate="false"
 		:placeholder="placeholder"
+		:value-is-number="valueIsNumber"
 		@change="onChange"
+		:show-all="showAll"
 		:listeners="listeners"
 		v-bind="$attrs"></vb-select>
 </template>

+ 13 - 0
UI/VAP.Vue/src/components/form/VbFormItem.vue

@@ -182,6 +182,19 @@ onMounted(() => {
 					v-bind="item.props"
 					v-on="item.listeners"></vb-select>
 			</template>
+			<template v-else-if="checkComponent('VU') || checkComponent('VbUpload')">
+				<vb-upload
+					v-model="data[item.field]"
+					:id="itemId"
+					:required="item.required"
+					:class="item.class"
+					:style="item.style"
+					:placeholder="item.placeholder ?? item.label"
+					:size="item.size"
+					:data="itemData"
+					v-bind="item.props"
+					v-on="item.listeners"></vb-upload>
+			</template>
 			<template v-else-if="checkComponent('Dict') || checkComponent('DictSelect')">
 				<dict-select
 					v-model="data[item.field]"

+ 2 - 0
UI/VAP.Vue/src/components/form/models.ts

@@ -21,6 +21,8 @@ export interface VbFormItem {
 		| "VbSelect"
 		| "VE" // VbEditor
 		| "VbEditor"
+		| "VU" // VbUpload
+		| "VbUpload"
 		| "Dict" // DictSelect
 		| "DictSelect"
 		| "VS" // VbSelect

+ 5 - 4
UI/VAP.Vue/src/components/modal/VbModal.vue

@@ -103,7 +103,7 @@ const props = withDefaults(
 const { modal, formData } = toRefs(props)
 const emits = defineEmits<{
 	(e: "update:modal", modal: Modal): boolean
-	(e: "update:formData", dara: any): boolean
+	(e: "update:formData", data: any): boolean
 	(e: "cancel", form: HTMLFormElement | undefined | null, callback?: (v: boolean) => void): boolean
 	(e: "confirm", form: HTMLFormElement | undefined | null): void
 }>()
@@ -139,9 +139,7 @@ function cancel() {
 	if (props.closeNeedConfrim) {
 		message.confirm("数据还未保存,确认关闭对话框?", "关闭对话框").then((confirm: any) => {
 			console.log("confirm", confirm)
-			if (confirm.isConfirmed) {
-				closeModal()
-			}
+			closeModal()
 		})
 	} else {
 		closeModal()
@@ -285,6 +283,9 @@ defineExpose({ show, resetForm, validateForm, changePrefixTitle, modalFormRef })
 								</template>
 							</VbForm>
 						</template>
+						<template v-if="$slots.bodyBottom">
+							<slot name="bodyBottom" />
+						</template>
 					</div>
 					<div class="modal-footer" :class="modalFooterClass" :style="modalFooterStyle">
 						<template v-if="$slots.footer">

+ 23 - 4
UI/VAP.Vue/src/components/select/VbSelect.vue

@@ -6,22 +6,24 @@ const props = withDefaults(
 		data?: any
 		dataFun?: () => Promise<any>
 		url?: string
+		valueIsNumber?: boolean
 		method?: string
 		config?: any
 		props?: {
 			label?: string
-			value?: string
+			value?: string | number
 			class?: string
 			style?: string
 			disabled?: string | ((v?: any) => boolean)
 		}
 		dataMapFun?: (v: any) => {
 			label: string
-			value: string
+			value: string | number
 			class?: string
 			style?: string
 			disabled?: boolean
 		}
+		showAll?: boolean
 		needFormate?: boolean
 		placeholder?: string
 		listeners?: any
@@ -40,16 +42,27 @@ const emits = defineEmits<{
 }>()
 const { modelValue } = toRefs(props)
 const _data = ref<any>([])
+
 const options = computed(() => {
+	const arr: any = []
 	if (props.dataFun || props.url) {
-		return _data.value
+		if (props.showAll && (_data.value.length == 0 || _data.value[0].value != "")) {
+			arr.unshift({ label: "全部", value: "" })
+		}
+		arr.push(..._data.value)
+		return arr
 	}
 	formatData(typeof props.data == "function" ? props.data() : props.data)
-	return _data.value
+	if (props.showAll && (_data.value.length == 0 || _data.value[0].value != "")) {
+		arr.unshift({ label: "全部", value: "" })
+	}
+	arr.push(..._data.value)
+	return arr
 })
 function onChange(v: any) {
 	emits("update:modelValue", v)
 	emits("change", v)
+	console.log("---1", v)
 	return v
 }
 function onChange2(v: any[]) {
@@ -59,6 +72,11 @@ function onChange2(v: any[]) {
 }
 
 function formatData(data: any[]) {
+	if (props.valueIsNumber) {
+		data.forEach((v) => {
+			v[props.props?.value ?? "value"] = Number(v[props.props?.value ?? "value"])
+		})
+	}
 	if (!props.needFormate) {
 		_data.value = data
 	} else {
@@ -103,6 +121,7 @@ function load() {
 function init() {
 	load()
 }
+
 watch(() => props.id, init)
 onMounted(() => {
 	init()

+ 25 - 19
UI/VAP.Vue/src/components/table/VbDataTable.vue

@@ -39,8 +39,8 @@ const props = withDefaults(
 		formData?: any
 		exportUrl?: string
 		exportName?: string
-		getEntiyFun?: (v: any) => Promise<any>
-		deleteEntiyFun?: (v: any) => Promise<any>
+		getEntityFun?: (v: any) => Promise<any>
+		deleteEntityFun?: (v: any) => Promise<any>
 		resetFormFun?: () => void
 		/* toolbar */
 		showToolbar?: boolean // 是否显示toolbar
@@ -82,8 +82,8 @@ const props = withDefaults(
 		fixedNumber?: number // 左边固定列数
 		fixedRightNumber?: number // 右边固定列数
 		scroll?: Scroll
-		/* rowsapn */
-		rowSpanSuffix?: string // rowsapn 的字段后缀 后台应该组成 `${字段}${后缀}`
+		/* rowspan */
+		rowSpanSuffix?: string // rowspan 的字段后缀 后台应该组成 `${字段}${后缀}`
 		/* tree */
 		isTree?: boolean
 		expandDepth?: number
@@ -267,7 +267,7 @@ const rowColumns = computed(() => {
 const keyField = ref(props.keyField ? props.keyField : rowColumns.value[0].field)
 
 const displayData = computed(() => {
-	let tableData = []
+	let tableData: any = []
 	if (!!props.remoteFun || !!props.url) {
 		tableData = remoteData.value || []
 	} else if (data?.value) {
@@ -335,8 +335,8 @@ const defaultHandleFuns = {
 		if (!row) {
 			return
 		}
-		if (props.getEntiyFun) {
-			props.getEntiyFun(row[keyField.value]).then((res) => {
+		if (props.getEntityFun) {
+			props.getEntityFun(row[keyField.value]).then((res) => {
 				emits("update:formData", res.data)
 				if (props.modal) {
 					props.modal.show()
@@ -357,7 +357,7 @@ const defaultHandleFuns = {
 		if (!rows) {
 			return
 		}
-		if (!props.deleteEntiyFun) {
+		if (!props.deleteEntityFun) {
 			message.alertError("删除接口方法未配置,请联系管理员")
 			return
 		}
@@ -365,8 +365,8 @@ const defaultHandleFuns = {
 		message
 			.confirm('是否确认删除编号为"' + ids.join(",") + '"的数据项?')
 			.then(() => {
-				props.deleteEntiyFun &&
-					props.deleteEntiyFun(ids).then(() => {
+				props.deleteEntityFun &&
+					props.deleteEntityFun(ids).then(() => {
 						customSearch()
 					})
 			})
@@ -427,9 +427,15 @@ const remote = (id?: string) => {
 
 	return new Promise((resolve, reject) => {
 		if (props.remoteFun) {
-			props.remoteFun(params).then((res: any) => {
-				processData(res.rows || res.data, res.total)
-			})
+			props
+				.remoteFun(params)
+				.then((res: any) => {
+					processData(res.rows || res.data, res.total)
+				})
+				.catch((err) => {
+					loading.value = false
+					reject(err)
+				})
 		} else if (props.url) {
 			const queryParam = props.method.toLocaleLowerCase() == "get" ? { params } : { data: params }
 
@@ -501,7 +507,7 @@ const loadHandleBtns = () => {
 	toolbarHandleBtns.value = []
 	if (!props.useCustomBtns && props.handlePerm) {
 		const _handleFuns = Object.assign({}, defaultHandleFuns, props.handleFuns ?? {})
-		apis.system.menuApi.mennChildrenByPerms(props.handlePerm).then((res) => {
+		apis.system.menuApi.menuChildrenByPerms(props.handlePerm).then((res) => {
 			res.data.forEach((v: any) => {
 				if (v.btnScript && v.btnScript != "hide") {
 					const scripts = v.btnScript.split("@")
@@ -553,12 +559,12 @@ function calcTableBoxStyle() {
 		style.height = Number(props.tableBoxHeight) ? props.tableBoxHeight + "px" : props.tableBoxHeight
 	} else {
 		nextTick(() => {
-			const tableBoxAuotHeight =
+			const tableBoxAutoHeight =
 				document.documentElement.clientHeight -
 				(document.querySelector(".app-header")?.clientHeight ?? 0) -
 				(document.querySelector(".app-footer")?.clientHeight ?? 0) -
 				5
-			style.height = tableBoxAuotHeight + "px"
+			style.height = tableBoxAutoHeight + "px"
 		})
 	}
 	if (props.tableBoxWidth) {
@@ -655,7 +661,7 @@ provide(symbolKeys.bodyTds, {
 	isLazy: props.isLazy
 })
 provide(symbolKeys.displayData, displayData)
-provide(symbolKeys.pageination, {
+provide(symbolKeys.pagination, {
 	total: dataTotalCount,
 	currentPage,
 	pageSize,
@@ -694,7 +700,7 @@ const onSort = (v: Sort) => {
 	search(false)
 }
 const onSelectAll = (isChecked: boolean) => {
-	let sRows = []
+	let sRows: any = []
 	if (isChecked) {
 		sRows = [...new Set([...selectedRows.value, ...displayData.value])]
 		selectedRows.value = sRows
@@ -712,7 +718,7 @@ const onSelectAll = (isChecked: boolean) => {
 	}
 }
 const onSelect = (v: { isChecked: boolean; row: any }) => {
-	let sRows = []
+	let sRows: any = []
 	const row = toValue(v.row)
 	if (v.isChecked) {
 		sRows = isMultipleCheck.value ? [...new Set([...selectedRows.value, row])] : [row]

+ 72 - 27
UI/VAP.Vue/src/components/upload/VbUpload.vue

@@ -2,7 +2,9 @@
 const props = withDefaults(
 	defineProps<{
 		modelValue: string | any[]
+		prefixUrl?: string
 		uploadType?: "image" | "file"
+		uploadUrl?: string
 		limit?: number // 图片数量限制
 		fileSize?: number // 大小限制(MB)
 		fileType?: string[] // 文件类型, 例如['png', 'jpg', 'jpeg']
@@ -10,27 +12,38 @@ const props = withDefaults(
 	}>(),
 	{
 		uploadType: "image",
+		prefixUrl: "/oss/preview/",
 		limit: 5,
 		fileSize: 5,
 		fileType: () => ["png", "jpg", "jpeg"],
 		isShowTip: true
 	}
 )
-const emits = defineEmits<(e: "update:modelValue", v: any) => void>()
+const emits = defineEmits<{
+	(e: "update:modelValue", v: any): void
+	(e: "uploadSuccess", v: any, v2: any): void
+	(e: "delete", v: any): void
+}>()
 const imageUploadRef = ref()
 const number = ref(0)
 const uploadList = ref<any[]>([])
-const dialogImageUrl = ref("")
-const dialogVisible = ref(false)
+const showViewer = ref(false)
+const viewerInitialIndex = ref(0)
 const baseUrl = import.meta.env.VITE_APP_BASE_API
-const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload") // 上传的图片服务器地址
+const uploadImgUrl = ref(
+	import.meta.env.VITE_APP_BASE_API + (props.uploadUrl ? props.uploadUrl : "/oss/upload/common")
+) // 上传的图片服务器地址
 const headers = ref({ Authorization: "Bearer " + getToken() })
 const fileList = ref<any[]>([])
 const isImage = computed(() => {
 	return props.uploadType == "image"
 })
 const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize))
-
+const imageUrls = computed(() => {
+	return fileList.value.map((v) => {
+		return v.url
+	})
+})
 // 上传前校检格式和大小
 function handleBeforeUpload(file: any) {
 	let flag = false
@@ -76,8 +89,16 @@ function handleUploadError() {
 
 // 上传成功回调
 function handleUploadSuccess(res: any, file: any) {
-	if (res.code === 200) {
-		uploadList.value.push({ name: res.fileName, url: res.fileName })
+	console.log("RES", res, file)
+	if (res.code === 200 && res.data.url) {
+		let url = `${baseUrl}/${props.prefixUrl ? props.prefixUrl : ""}/${res.data.url}`
+		url = url
+			.replace("https://", "$$$$")
+			.replace("http://", "@@@@")
+			.replace(/\/\//g, "/")
+			.replace("$$$$", "https://")
+			.replace("@@@@", "http://")
+		uploadList.value.push({ name: res.data.url, url })
 		uploadedSuccessfully()
 	} else {
 		number.value--
@@ -86,13 +107,14 @@ function handleUploadSuccess(res: any, file: any) {
 		imageUploadRef.value.handleRemove(file)
 		uploadedSuccessfully()
 	}
+	emits("uploadSuccess", res, file)
 }
 
 // 删除图片
 function handleDelete(file: any) {
-	const findex = fileList.value.map((f) => f.name).indexOf(file.name)
-	if (findex > -1 && uploadList.value.length === number.value) {
-		fileList.value.splice(findex, 1)
+	const fileIndex = fileList.value.map((f) => f.name).indexOf(file.name)
+	if (fileIndex > -1 && uploadList.value.length === number.value) {
+		fileList.value.splice(fileIndex, 1)
 		emits("update:modelValue", listToString(fileList.value))
 		return false
 	}
@@ -100,8 +122,9 @@ function handleDelete(file: any) {
 }
 // 删除文件
 function handleDeleteFile(index: number) {
-	fileList.value.splice(index, 1)
+	const file = fileList.value.splice(index, 1)
 	emits("update:modelValue", listToString(fileList.value))
+	emits("delete", file)
 }
 
 // 上传结束处理
@@ -117,8 +140,10 @@ function uploadedSuccessfully() {
 
 // 预览
 function handlePictureCardPreview(file: any) {
-	dialogImageUrl.value = file.url
-	dialogVisible.value = true
+	//dialogImageUrl.value = file.url
+	console.log("---", file)
+	viewerInitialIndex.value = file.index //fileList.value.map((f) => f.url).indexOf(file.url)
+	showViewer.value = true
 }
 
 // 获取文件名称
@@ -132,30 +157,41 @@ function getFileName(name: string) {
 
 // 对象转成指定字符串分隔
 function listToString(list: any[], separator?: string) {
-	let strs = ""
+	let str = ""
 	separator = separator ?? ","
 	list.forEach((v) => {
 		if (undefined !== v.url && v.url.indexOf("blob:") !== 0) {
-			strs += v.url.replace(baseUrl, "") + separator
+			str += v.name + separator
 		}
 	})
-	return strs != "" ? strs.substring(0, strs.length - 1) : ""
+	return str != "" ? str.substring(0, str.length - 1) : ""
 }
 
 watch(
 	() => props.modelValue,
 	(val) => {
 		if (val) {
-			let temp = 1
+			let temp = 0
 			// 首先将值转为数组
 			const list = Array.isArray(val) ? val : (props.modelValue as string)?.split(",")
 			// 然后将数组转为对象数组
 			fileList.value = list.map((item) => {
 				if (typeof item === "string") {
 					if (!item.includes(baseUrl)) {
-						item = { name: baseUrl + item, url: baseUrl + item }
+						let url = `${baseUrl}/${props.prefixUrl ? props.prefixUrl : ""}/${item}`
+						url = url
+							.replace("https://", "$$$$")
+							.replace("http://", "@@@@")
+							.replace(/\/\//g, "/")
+							.replace("$$$$", "https://")
+							.replace("@@@@", "http://")
+						item = {
+							name: item,
+							url,
+							index: temp
+						}
 					} else {
-						item = { name: item, url: item }
+						item = { name: item, url: item, index: temp }
 					}
 					item.uid = item.uid || new Date().getTime() + temp++
 				}
@@ -193,12 +229,19 @@ onMounted(init)
 			:headers="headers"
 			:on-preview="isImage ? handlePictureCardPreview : undefined"
 			:class="{ hide: fileList.length >= limit, 'upload-file-uploader': !isImage }">
-			<el-icon v-if="isImage" class="avatar-uploader-icon"><plus /></el-icon>
+			<el-icon v-if="isImage" class="avatar-uploader-icon">
+				<VbIcon icon-name="plus-square"></VbIcon>
+			</el-icon>
 			<el-button v-else type="primary">选取文件</el-button>
 		</el-upload>
 		<!-- 上传提示 -->
 		<div class="el-upload__tip" v-if="showTip">
-			请上传
+			<template v-if="limit">
+				最多可上传
+				<b style="color: #f56c6c">{{ limit }}</b>
+				{{ isImage ? "张" : "个" }}
+			</template>
+			<span v-else>请上传</span>
 			<template v-if="fileSize">
 				大小不超过
 				<b style="color: #f56c6c">{{ fileSize }}MB</b>
@@ -207,15 +250,17 @@ onMounted(init)
 				格式为
 				<b style="color: #f56c6c">{{ fileType.join("/") }}</b>
 			</template>
-			的文件
+			的{{ isImage ? "图片" : "文件" }}。
 		</div>
-
-		<el-dialog v-if="isImage" v-model="dialogVisible" title="预览" width="800px" append-to-body>
-			<img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
-		</el-dialog>
+		<el-image-viewer
+			v-if="isImage && showViewer"
+			:url-list="imageUrls"
+			:initial-index="viewerInitialIndex"
+			:teleported="true"
+			@close="() => (showViewer = false)" />
 		<!-- 文件列表 -->
 		<transition-group
-			v-else
+			v-if="!isImage"
 			class="upload-file-list el-upload-list el-upload-list--text"
 			name="el-fade-in-linear"
 			tag="ul">