Files
2026-09-24 16:25:22 +08:00

707 lines
19 KiB
Plaintext

<script lang="ts">
import { type PropType } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { xFileSystem, xFileSListType,fileListType } from "@/uni_modules/x-file-s"
export type XFILESUPLOAD_BEFORE_CALLBACK = (list : fileListType[]) => Promise<fileListType[]>
// #ifdef MP
type fileListType = any;
// #endif
/**
*
* @name 文件选择 xUploadFile
* @page /pages/index/upload-file
* @category 表单组件
* @description 使用时请注意区别:安卓端和IOS端没有区别。web端是我模拟的格式有点区别,上传文件是File对象。
* 没有提供双向绑定,因为可能的数据格式化比较麻烦,请根据change事件来保存数据,微信端请保证你的小程序隐私政策中勾选了相关权限使用.
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
xfile: null as null | xFileSystem,
fileList: [] as fileListType[],
_uploading: false,
_uploadingIndex: 0,
}
},
emits: [
/**
* 只有删除和上传完成时才会触发
* 并返回当前的文件列表。
*/
'change'
],
props: {
/**
* 提供的默认上传的文件列表
* 请确保要有以下三个字段:
* 1.name
* 2.type
* 3.request 你服务器自己构造的成功数据,例:{code,data,msg}
* 小心的是,如果你变动了此值,内部会把默认当前的列表数据清空,直接用此列表替换。这样你在表单加载数据更新编辑时
* 会很有用。
*/
defaultList: {
type: Array as PropType<UTSJSONObject[] | null>,
default: null
},
/**
* 头部数据
*/
header: {
type: Object as PropType<UTSJSONObject | null>,
default: null
},
/**
* 文件上传的字段名称
*/
name: {
type: String,
default: "file"
},
/**
* 表单数据
*/
formData: {
type: Object as PropType<UTSJSONObject | null>,
default: null
},
/**
* 服务器上传地址
*/
url: {
type: String,
default: "https://mockapi.eolink.com/LRViGGZ8e6c1e8b4a636cd82bca1eb15d2635ed8c74e774/admin/upload_pic/"
},
/**
* 最大上传数量
* -1表示不限制
*/
maxCount: {
type: Number,
default: -1
},
/**
* 是否多选
*/
multiple: {
type: Boolean,
default: true
},
/**
* 最大文件大小,单位字节
* 超过此大小的文件不会被上传。30*1024*1024
*/
maxFileSize: {
type: Number,
default: 30*1024*1024
},
/**
* 当前是否在上传中
* 请使用v-model:loading来
* 不可更改此值。
*/
loading: {
type: Boolean,
default: true
},
/**
* 选择文件后自动上传
*/
autoStart: {
type: Boolean,
default: true
},
/**
* 服务正常返回的状态码如果为此值表示失败。
*/
statusCode: {
type: Number,
default: 200
},
/**
* 禁用上传功能
*/
disabled: {
type: Boolean,
default: false
},
/**
* 已上传的文件是否禁用删除
* 这个是某些特殊的erp系统中的需求需要。
*/
uploadFileDisabledDel: {
type: Boolean,
default: false
},
/**
* 文件过滤器,不要动态修改。
* 空数组时为所有文件类型,类型请注意使用如下规范:
* 如:["jpg","docx","doc","webp"]
* 在ios需要14+支持类型过滤,14以下此属性无效,
* web是过滤指定类型.
* 在安卓端事实上系统差异,暂时无法多文件类型过滤,安卓只会取类型文件中的最后一项,如上述只会取webp
* 但在安卓是我映射的类型,因为如果比如我要jpg,gif格式,只取gif肯定不行,因此你写jpg,gif,png其实在
* 安卓端都是同一种类型文件对应image*所有图图片类型,比如你写pdf,zip,josn格式,都是统一为application*
* 因此在安卓它是对应一种文件类的类型不是指代具体的后缀.
*/
filter: {
type: Array as PropType<string[]>,
default: ():string[] => [] as string[]
},
/**
* 底部线条颜色
*/
borderBottomColor: {
type: String,
default: "#f5f5f5"
},
/**
* 上传前执行的函数勾子,如果不会使用见demo
* 类型:(lsit:fileListType[]) => Promise<fileListType[]>
* 这里可以过滤不需要上传的文件.
*/
beforeUpload: {
type: Function as PropType<XFILESUPLOAD_BEFORE_CALLBACK>,
default: (list : fileListType[]) : Promise<fileListType[]> => {
return Promise.resolve(list)
}
}
},
mounted() {
this.xfile = new xFileSystem(this.filter, this.multiple, this.maxFileSize)
this.$nextTick(function () {
this.covertList();
})
},
watch: {
defaultList() {
this.covertList();
}
},
beforeUnmount() {
this.xfile?.destory()
},
computed: {
_maxCount() : number {
return this.maxCount
},
_url() : string {
return this.url
},
_formData() : UTSJSONObject | null {
return this.formData
},
_header() : UTSJSONObject | null {
return this.header
},
_name() : string {
return this.name
},
_disabled() : boolean {
return this.disabled || (this.fileList.length > this._maxCount && this._maxCount > -1)
},
_borderBottomColor() : string {
if (xConfig.dark == 'dark') return xConfig.borderDarkColor
return this.borderBottomColor;
},
_isDark() : boolean {
return xConfig.dark == 'dark'
}
},
methods: {
covertList() {
if (this.defaultList == null) return;
let dlist = this.defaultList!.map((el : UTSJSONObject, index : number) : fileListType => {
let uid = Math.floor(Math.random() * 1 * Math.floor(Math.random() * Date.now())).toString().substring(0, 10);
let type = el.getString("type")
type = type == null ? uid : type
let name = el.getString("name")
name = name == null ? index.toString() : name;
/*
* ⚠ 本地补丁(2026-09-24):
* request 是可选字段(xFileSListType.request?: any),defaultList 里
* 不带 request(或值为 null)时,原写法 `el.getAny("request") as any`
* 会把 null 强转成 Kotlin 非空 Any,运行期直接抛:
* java.lang.NullPointerException: null cannot be cast to non-null type kotlin.Any
* 与上面 type / name 用 null 兜底是同一类问题,这里漏了。
* → 声明为可空 `any | null`,且不再做 `as any` 强转。
* request 在模板里从不读取,仅原样透传给宿主页面,置空无副作用。
*/
let requestdata : any | null = el.getAny("request")
// #ifdef APP || MP-WEIXIN
return {
name,
type,
id: uid,
request: requestdata,
realFilePath: "",
cacheFilePath: "",
status: 2,
file: "",
uri:"",
size: 0
} as fileListType
// #endif
// #ifdef WEB
return {
name,
type,
id: uid,
request: requestdata,
realFilePath: "",
cacheFilePath: "",
status: 2,
file: "",
uri:"",
size: 0
}
// #endif
})
this.fileList = dlist;
this.xfile!.clear();
},
/**
* 文件目录或者文件是否存在
*/
fileIsOnlyInSys(dir : string) : Promise<string> {
// #ifdef WEB||MP-WEIXIN
return Promise.resolve("")
// #endif
let baseNamePath = dir == null ? 'tmui4temp' : dir
baseNamePath = uni.env.CACHE_PATH + "/" + baseNamePath
const fileManager = uni.getFileSystemManager()
return new Promise((res, rej) => {
fileManager.readdir({
dirPath: baseNamePath,
success(result : ReadDirSuccessResult) {
if (result == null) {
fileManager.mkdir({
dirPath: baseNamePath,
recursive: true,
success(result : FileManagerSuccessResult) {
res(baseNamePath)
},
fail(er : IFileSystemManagerFail) {
console.log('创建目录失败', er)
rej("")
}
} as MkDirOptions)
} else {
// console.log(result)
res(baseNamePath)
}
},
fail(ere : IFileSystemManagerFail) {
fileManager.mkdir({
dirPath: baseNamePath,
recursive: true,
success(result : FileManagerSuccessResult) {
res(baseNamePath)
},
fail(er : IFileSystemManagerFail) {
console.log('创建目录失败', er)
rej("")
}
} as MkDirOptions)
}
} as ReadDirOptions)
})
},
getCacheFiles(dir : string | null) : Promise<string[]> {
// #ifdef WEB || MP-WEIXIN
return Promise.resolve([])
// #endif
let baseNamePath = dir == null ? 'tmui4temp' : dir
baseNamePath = uni.env.CACHE_PATH + "/" + baseNamePath
const fileManager = uni.getFileSystemManager()
return new Promise((res, rej) => {
fileManager.readdir({
dirPath: baseNamePath,
success(result : ReadDirSuccessResult) {
if (result == null) {
fileManager.mkdir({
dirPath: baseNamePath,
recursive: true,
success(result2 : FileManagerSuccessResult) {
res([] as string[])
},
fail(er : IFileSystemManagerFail) {
rej([] as string[])
}
} as MkDirOptions)
} else {
res(result.files)
}
},
fail(ere : IFileSystemManagerFail) {
fileManager.mkdir({
dirPath: baseNamePath,
recursive: true,
success(result : FileManagerSuccessResult) {
res([] as string[])
},
fail(er : IFileSystemManagerFail) {
console.log('创建目录失败', er)
rej([] as string[])
}
} as MkDirOptions)
}
} as ReadDirOptions)
})
},
openfile() {
let _this = this;
if (this._uploading || this._disabled || (_this.fileList.length > _this._maxCount && _this._maxCount > -1)) return;
this.xfile!.openDocument((listtest : any) => {
let list = [] as fileListType[];
// #ifdef APP-IOS
let templist = JSON.parseArray<string>(listtest)
for (let i = 0; i < templist.length; i++) {
let item = JSON.parse(templist[i])! as fileListType;
list.push(item)
}
// #endif
// #ifndef APP-IOS
list = listtest as fileListType[]
// #endif
for (let i = 0; i < list.length; i++) {
let el = list[i] as fileListType
let isSonme = _this.fileList.some((ele) : boolean => ele.type == el.type && ele.name == el.name)
if (_this.fileList.length > _this._maxCount && _this._maxCount > -1) {
break;
} else if (!isSonme) {
_this.fileList.push(el as fileListType)
}
}
if (_this.autoStart) {
_this.start();
}
})
},
copyFile(item : fileListType, call : (item : fileListType | null) => void) {
let _this = this;
let index = this.fileList.findIndex((v) : boolean => v.id == item.id)
if (index == -1) {
call(null)
return
}
// #ifdef APP-IOS
item!.realFilePath = uni.env.CACHE_PATH + '/' + item!.realFilePath
call(item)
// #endif
// #ifdef MP-WEIXIN
item!.realFilePath = item!.realFilePath
call(item)
// #endif
if ((item?.cacheFilePath != null && item?.cacheFilePath != "") || item.status != 0) {
// #ifndef APP-IOS
call(item)
// #endif
return
}
//创建临时存放的目录
this.fileIsOnlyInSys('tmui4temp')
.then((basedir : string) => {
// #ifndef APP-IOS
_this.xfile!.copyFileToPath(item, basedir)
.then((oreal : fileListType | null) => {
if (oreal != null) {
item = oreal!
_this.fileList.splice(index, 1, oreal!)
call(item)
} else {
call(null)
}
})
// #endif
})
},
remove(item : fileListType) {
if (this._uploading) {
// "上传中禁止删除"
uni.showToast({ title: this!.i18n.t("tmui4x.uploadFile.tips1"), icon: 'none' })
return;
}
if (this.uploadFileDisabledDel) {
// 已上传文件禁止删除
uni.showToast({ title: this!.i18n.t("tmui4x.uploadFile.tips2"), icon: 'none' })
return;
}
this.xfile!.remove(item.id)
let tems = this.fileList.slice(0)
this.fileList = tems.filter((v) : boolean => v.id != item.id);
/**
* 变动触发change
* @param {fileListType[]} fileList -文件列表。
*/
this.$emit("change", this.fileList.slice(0))
},
/**
* 安卓专用清缓存函数
* @description 如果你是安卓端,如果不用了离开页面了,请手动调用 下此函数
* 来清空应用缓存文件。以免造成数据占了内存卡。
* @public
*/
clearCacheFiles() {
// #ifdef APP-ANDROID
let baseNamePath = 'tmui4temp'
baseNamePath = uni.env.CACHE_PATH + "/" + baseNamePath
this.getCacheFiles(null)
.then((files : string[]) => {
const fileManager = uni.getFileSystemManager()
files.forEach((el : string) => {
fileManager.unlink({
filePath: baseNamePath + "/" + el,
success: (res : FileManagerSuccessResult) => {
console.info("缓存清除成功:", el)
},
fail: (res : IFileSystemManagerFail) => {
console.error("文件删除失败:", el)
}
} as UnLinkOptions)
})
})
// #endif
},
/**
* 上传文件列表
* @public
*/
start() {
if (this._uploading) return;
let _this = this;
this._uploadingIndex = 0;
this._uploading = true;
this.beforeUpload(this.fileList.slice(0))
.then((list : fileListType[]) => {
_this.fileList = list.slice(0)
_this.uploadFile();
})
.catch((_ : any | null) => {
console.error("参数有误,请返回正确.")
})
},
statusColor(status : number | null) : string {
if (status == 0) return "#888888"
if (status == 1 || status == 4) return "#FF0000"
if (status == 2) return "#12cb4d"
return "#1480e5"
},
getFilesize(item : fileListType) : string {
let size = item.size
if (size == null || size == 0) return ""
let fileszie = size! as number;
if (fileszie <= 1024) {
return fileszie.toString() + " B"
}
if (fileszie <= 1024 * 1024) {
let fz = (fileszie / 1024).toFixed(2)
return fz + "kb"
}
if (fileszie <= 1024 * 1024 * 1024) {
let fz = (fileszie / (1024 * 1024)).toFixed(2)
return fz + "mb"
}
let fz = (fileszie / (1024 * 1024 * 1024)).toFixed(2)
return fz + "gb"
},
uploadFile() {
let _this = this;
if (this.fileList.length == 0 || this._uploadingIndex > this.fileList.length - 1) {
this._uploading = false;
this.$emit("change", this.fileList.slice(0))
this.clearCacheFiles()
_this.xfile?.destory()
_this.xfile = new xFileSystem(_this.filter, _this.multiple, this.maxFileSize)
return;
}
let item = this.fileList[this._uploadingIndex];
if (item.status == 2 || item.status == 4) {
this._uploadingIndex += 1;
this.uploadFile();
return;
}
this.copyFile(item, (realItem : fileListType | null) => {
if (realItem != null) {
item = realItem!
item.status = 3;
_this.$forceUpdate()
let filepath = item!.realFilePath;
let files = null as null | UploadFileOptionFiles[];
// #ifdef WEB
filepath = "";
files = [
{
name: _this._name,
uri: "",
file: item.file!
}
] as UploadFileOptionFiles[]
// #endif
uni.uploadFile({
url: _this._url,
formData: _this._formData,
name: _this._name,
header: _this._header,
filePath: item!.realFilePath,
files,
success: (res : UploadFileSuccess) => {
if (res.statusCode != _this.statusCode) {
item.status = 1;
_this._uploadingIndex += 1;
_this.$forceUpdate()
_this.uploadFile();
} else {
item.status = 2;
item!.request = res.data;
_this._uploadingIndex += 1;
// #ifdef WEB
item.file = null;
// #endif
_this.$forceUpdate()
_this.uploadFile();
}
},
fail: (error : UploadFileFail) => {
console.error(error)
item.status = 1;
_this._uploadingIndex += 1;
_this.$forceUpdate()
_this.uploadFile();
}
})
}
})
},
getTextStatus(item : fileListType) : string {
let text = "";
if (item.status == 0) {
text = this!.i18n.t("tmui4x.uploadFile.uploadStatus",0);//"待上传"
} else if (item.status == 1) {
text = this!.i18n.t("tmui4x.uploadFile.uploadStatus",2);//"上传失败"
} else if (item.status == 2) {
text = this!.i18n.t("tmui4x.uploadFile.uploadStatus",3);//"上传成功"
} else if (item.status == 3) {
text = this!.i18n.t("tmui4x.uploadFile.uploadStatus",1);//"上传中..."
} else if (item.status == 4) {
text = this!.i18n.t("tmui4x.uploadFile.uploadStatus",4);//"超过大小"
}
let fsize = this.getFilesize(item as fileListType);
return text + (fsize == '' ? "" : " | " + fsize)
}
},
}
</script>
<template>
<view class="xFile">
<view @click="openfile">
<!--
@slot 触发文件选择的区域插槽
-->
<slot>
<view style="pointer-events: none;">
<!-- 选择文件 -->
<x-button :disabled="_disabled" :loading="_uploading" icon="sticky-note-add-line"
:block="true">{{i18n!.t("tmui4x.uploadFile.title")}}</x-button>
</view>
</slot>
</view>
<slot name="list" :list="fileList">
<view style="margin-top: 16px;" v-if="fileList.length>0">
<view class="xFileItem" :style="
{
borderBottomColor:index==fileList.length-1? 'transparent': _borderBottomColor,
padding:`16px 0px`
}" v-for="(item,index) in fileList"
:key="index">
<view class="xFileItemWrap">
<view style="margin-right: 20px;flex:1">
<text :style="{color:_isDark?'#ffffff':'#333333',fontSize:'15px'}" class="xFileItemTitle">{{item.name}}</text>
<text class="xFileItemSubtext"
:style="{color:statusColor(item.status),fontSize:'12px'}">{{getTextStatus(item)}}</text>
</view>
<x-icon v-if="(item.status!=3)&&!uploadFileDisabledDel" @click="remove(item)" name="close-line"
font-size="24px" color="#bdbdbd"></x-icon>
</view>
</view>
</view>
</slot>
</view>
</template>
<style scoped>
.xFileItemWrap {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.xFileItem {
/* padding: 16px 0px; */
border-bottom-width: 1px;
border-bottom-style: solid;
}
.xFileItemTitle {
/* font-size: 15px; */
/* #ifdef APP */
lines: 1;
text-overflow: ellipsis;
/* #endif */
/* #ifdef H5 */
word-break: break-all;
/* #endif */
}
.xFileItemSubtext {
/* font-size: 12px; */
margin-top: 5px;
}
</style>