实现文件上传
This commit is contained in:
parent
3c2365660b
commit
fc65bfedcb
|
|
@ -62,10 +62,14 @@
|
|||
class="file-item"
|
||||
v-for="(file, index) in formData.files"
|
||||
:key="index"
|
||||
@click="previewFile(file)"
|
||||
>
|
||||
<text class="file-icon">📄</text>
|
||||
<text class="file-name">{{ file.name }}</text>
|
||||
<view class="remove-btn" @click="removeFile(index)">✕</view>
|
||||
<text class="file-icon">{{ getFileIcon(file.name) }}</text>
|
||||
<view class="file-info">
|
||||
<text class="file-name">{{ file.name }}</text>
|
||||
<text class="file-size" v-if="file.size > 0">{{ formatFileSize(file.size) }}</text>
|
||||
</view>
|
||||
<view class="remove-btn" @click.stop="removeFile(index)">✕</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -121,7 +125,7 @@
|
|||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { chooseAndUploadImages } from '@/utils/qiniu.js';
|
||||
import { chooseAndUploadImages, uploadFileToQiniu, batchUploadFilesToQiniu } from '@/utils/qiniu.js';
|
||||
import { submitTask } from '@/common/api.js';
|
||||
|
||||
// 表单数据
|
||||
|
|
@ -273,8 +277,8 @@ const removeImage = (index) => {
|
|||
formData.value.images.splice(index, 1);
|
||||
};
|
||||
|
||||
// 选择文件(安卓平台)
|
||||
const chooseFiles = () => {
|
||||
// 选择文件(支持多平台)
|
||||
const chooseFiles = async () => {
|
||||
const remainingCount = 5 - formData.value.files.length;
|
||||
if (remainingCount <= 0) {
|
||||
uni.showToast({
|
||||
|
|
@ -284,6 +288,72 @@ const chooseFiles = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
// 优先使用 uni.chooseFile(H5和部分平台支持)
|
||||
// #ifdef H5 || MP-WEIXIN || APP-PLUS
|
||||
try {
|
||||
uni.chooseFile({
|
||||
count: remainingCount,
|
||||
extension: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.zip', '.rar'],
|
||||
success: async (res) => {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: '上传中...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
// 批量上传文件到七牛云
|
||||
const uploadResults = await batchUploadFilesToQiniu(
|
||||
res.tempFiles.map(file => ({
|
||||
path: file.path,
|
||||
name: file.name
|
||||
}))
|
||||
);
|
||||
|
||||
// 将上传结果添加到文件列表
|
||||
const newFiles = uploadResults.map(result => ({
|
||||
name: result.name,
|
||||
path: result.url, // 保存七牛云URL
|
||||
size: result.size
|
||||
}));
|
||||
|
||||
formData.value.files = [...formData.value.files, ...newFiles];
|
||||
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: `成功添加${newFiles.length}个文件`,
|
||||
icon: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error('上传文件失败:', error);
|
||||
uni.showToast({
|
||||
title: error.message || '上传文件失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('选择文件失败:', err);
|
||||
// 如果uni.chooseFile不支持,尝试使用原生方法
|
||||
chooseFilesNative();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
// 如果不支持uni.chooseFile,使用原生方法
|
||||
chooseFilesNative();
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef H5 || MP-WEIXIN || APP-PLUS
|
||||
// 其他平台使用原生方法
|
||||
chooseFilesNative();
|
||||
// #endif
|
||||
};
|
||||
|
||||
// 原生文件选择方法(安卓平台)
|
||||
const chooseFilesNative = async () => {
|
||||
const remainingCount = 5 - formData.value.files.length;
|
||||
|
||||
// 安卓平台使用 plus API 调用原生文件选择器
|
||||
if (typeof plus !== 'undefined') {
|
||||
try {
|
||||
|
|
@ -301,7 +371,7 @@ const chooseFiles = () => {
|
|||
|
||||
// 监听文件选择结果
|
||||
const originalOnActivityResult = main.onActivityResult;
|
||||
main.onActivityResult = (requestCode, resultCode, data) => {
|
||||
main.onActivityResult = async (requestCode, resultCode, data) => {
|
||||
if (requestCode === 1001) {
|
||||
if (resultCode === -1 && data) { // RESULT_OK = -1
|
||||
try {
|
||||
|
|
@ -360,7 +430,38 @@ const chooseFiles = () => {
|
|||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
formData.value.files = [...formData.value.files, ...files];
|
||||
// 显示上传中提示
|
||||
uni.showLoading({
|
||||
title: '上传中...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
try {
|
||||
// 批量上传文件到七牛云
|
||||
const uploadResults = await batchUploadFilesToQiniu(files);
|
||||
|
||||
// 将上传结果添加到文件列表
|
||||
const newFiles = uploadResults.map(result => ({
|
||||
name: result.name,
|
||||
path: result.url, // 保存七牛云URL
|
||||
size: result.size
|
||||
}));
|
||||
|
||||
formData.value.files = [...formData.value.files, ...newFiles];
|
||||
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: `成功添加${newFiles.length}个文件`,
|
||||
icon: 'success'
|
||||
});
|
||||
} catch (uploadError) {
|
||||
uni.hideLoading();
|
||||
console.error('上传文件失败:', uploadError);
|
||||
uni.showToast({
|
||||
title: uploadError.message || '上传文件失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复原始的 onActivityResult
|
||||
|
|
@ -368,6 +469,7 @@ const chooseFiles = () => {
|
|||
main.onActivityResult = originalOnActivityResult;
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error('处理文件选择结果失败:', error);
|
||||
uni.showToast({
|
||||
title: '处理文件失败',
|
||||
|
|
@ -402,6 +504,104 @@ const removeFile = (index) => {
|
|||
formData.value.files.splice(index, 1);
|
||||
};
|
||||
|
||||
// 获取文件图标
|
||||
const getFileIcon = (fileName) => {
|
||||
if (!fileName) return '📄';
|
||||
const ext = fileName.split('.').pop().toLowerCase();
|
||||
const iconMap = {
|
||||
'pdf': '📕',
|
||||
'doc': '📘',
|
||||
'docx': '📘',
|
||||
'xls': '📗',
|
||||
'xlsx': '📗',
|
||||
'ppt': '📙',
|
||||
'pptx': '📙',
|
||||
'txt': '📄',
|
||||
'zip': '📦',
|
||||
'rar': '📦',
|
||||
'jpg': '🖼️',
|
||||
'jpeg': '🖼️',
|
||||
'png': '🖼️',
|
||||
'gif': '🖼️'
|
||||
};
|
||||
return iconMap[ext] || '📄';
|
||||
};
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes) => {
|
||||
if (!bytes || bytes === 0) return '';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// 预览/下载文件
|
||||
const previewFile = (file) => {
|
||||
if (!file.path) {
|
||||
uni.showToast({
|
||||
title: '文件路径不存在',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是图片,使用预览图片功能
|
||||
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
const ext = file.name.split('.').pop().toLowerCase();
|
||||
|
||||
if (imageExts.includes(ext)) {
|
||||
uni.previewImage({
|
||||
urls: [file.path],
|
||||
current: file.path
|
||||
});
|
||||
} else {
|
||||
// 其他文件类型,尝试打开或下载
|
||||
// #ifdef H5
|
||||
window.open(file.path, '_blank');
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.openURL(file.path);
|
||||
// #endif
|
||||
|
||||
// #ifndef H5 || APP-PLUS
|
||||
uni.showToast({
|
||||
title: '点击下载文件',
|
||||
icon: 'none'
|
||||
});
|
||||
// 可以调用下载API
|
||||
uni.downloadFile({
|
||||
url: file.path,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
uni.openDocument({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
console.log('打开文档成功');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('打开文档失败:', err);
|
||||
uni.showToast({
|
||||
title: '无法打开此文件',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('下载文件失败:', err);
|
||||
uni.showToast({
|
||||
title: '下载文件失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间为中文格式:年月日星期几时分秒
|
||||
const formatTimeToChinese = (date) => {
|
||||
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
||||
|
|
@ -440,9 +640,10 @@ const handleSubmit = async () => {
|
|||
|
||||
try {
|
||||
// 合并所有附件URL(图片和文件)
|
||||
// 图片和文件都已经上传到七牛云,直接使用URL
|
||||
const allAttaches = [
|
||||
...formData.value.images, // 图片已经是七牛云URL
|
||||
...formData.value.files.map(file => file.path) // 文件路径(如果是URL则直接使用,如果是本地路径可能需要先上传)
|
||||
...formData.value.files.map(file => file.path) // 文件已经是七牛云URL
|
||||
].filter(url => url && url.trim() !== ''); // 过滤空值
|
||||
|
||||
// 将附件数组转换为逗号分隔的字符串
|
||||
|
|
@ -656,9 +857,8 @@ const handleSubmit = async () => {
|
|||
}
|
||||
|
||||
.remove-btn {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
position: relative;
|
||||
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
|
|
@ -685,20 +885,38 @@ const handleSubmit = async () => {
|
|||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 20px;
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
.file-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 进度选择弹窗 */
|
||||
|
|
|
|||
|
|
@ -154,9 +154,13 @@
|
|||
class="file-attachment-item"
|
||||
v-for="(file, fileIndex) in record.fileAttachments"
|
||||
:key="fileIndex"
|
||||
@click="previewRecordFile(file)"
|
||||
>
|
||||
<text class="file-icon">📄</text>
|
||||
<text class="file-name">{{ file.name }}</text>
|
||||
<text class="file-icon">{{ getFileIcon(file.name) }}</text>
|
||||
<view class="file-info">
|
||||
<text class="file-name">{{ file.name }}</text>
|
||||
<text class="file-size" v-if="file.size > 0">{{ formatFileSize(file.size) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="delay-btn-wrapper" v-if="record.showDelayBtn">
|
||||
|
|
@ -396,7 +400,7 @@ const transformSubmitRecords = (submitList) => {
|
|||
userName: item.userName || '',
|
||||
userAvatar: item.userAvatar || '',
|
||||
time: formatTimeToChinese(item.createTime) || '',
|
||||
content: item.remark || item.description || item.taskDescription || '', // 如果没有提交内容,可能显示任务描述
|
||||
content: item.remark || '', // 如果没有提交内容,可能显示任务描述
|
||||
progress: null, // API 返回的数据中没有进度字段
|
||||
imageAttachments: imageAttachments,
|
||||
fileAttachments: fileAttachments,
|
||||
|
|
@ -492,6 +496,105 @@ const previewRecordImages = (imageUrls, index) => {
|
|||
}
|
||||
};
|
||||
|
||||
// 获取文件图标
|
||||
const getFileIcon = (fileName) => {
|
||||
if (!fileName) return '📄';
|
||||
const ext = fileName.split('.').pop().toLowerCase();
|
||||
const iconMap = {
|
||||
'pdf': '📕',
|
||||
'doc': '📘',
|
||||
'docx': '📘',
|
||||
'xls': '📗',
|
||||
'xlsx': '📗',
|
||||
'ppt': '📙',
|
||||
'pptx': '📙',
|
||||
'txt': '📄',
|
||||
'zip': '📦',
|
||||
'rar': '📦',
|
||||
'jpg': '🖼️',
|
||||
'jpeg': '🖼️',
|
||||
'png': '🖼️',
|
||||
'gif': '🖼️'
|
||||
};
|
||||
return iconMap[ext] || '📄';
|
||||
};
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes) => {
|
||||
if (!bytes || bytes === 0) return '';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// 预览/下载提交记录中的文件
|
||||
const previewRecordFile = (file) => {
|
||||
if (!file.path) {
|
||||
uni.showToast({
|
||||
title: '文件路径不存在',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是图片,使用预览图片功能
|
||||
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
const ext = file.name ? file.name.split('.').pop().toLowerCase() : '';
|
||||
|
||||
if (imageExts.includes(ext)) {
|
||||
uni.previewImage({
|
||||
urls: [file.path],
|
||||
current: file.path
|
||||
});
|
||||
} else {
|
||||
// 其他文件类型,尝试打开或下载
|
||||
// #ifdef H5
|
||||
window.open(file.path, '_blank');
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.openURL(file.path);
|
||||
// #endif
|
||||
|
||||
// #ifndef H5 || APP-PLUS
|
||||
uni.showToast({
|
||||
title: '正在下载文件...',
|
||||
icon: 'loading',
|
||||
duration: 2000
|
||||
});
|
||||
// 下载并打开文档
|
||||
uni.downloadFile({
|
||||
url: file.path,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
uni.openDocument({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
console.log('打开文档成功');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('打开文档失败:', err);
|
||||
uni.showToast({
|
||||
title: '无法打开此文件',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('下载文件失败:', err);
|
||||
uni.showToast({
|
||||
title: '下载文件失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
};
|
||||
|
||||
// 获取标签类型(用于uv-tags组件)
|
||||
const getTagType = (tagText) => {
|
||||
const status = getStatusFromTagText(tagText);
|
||||
|
|
@ -1113,23 +1216,42 @@ onShow(() => {
|
|||
.file-attachment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 16px;
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.no-record {
|
||||
|
|
|
|||
115
utils/qiniu.js
115
utils/qiniu.js
|
|
@ -184,6 +184,121 @@ export const chooseAndUploadImages = (options = {}) => {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传单个文件到七牛云(支持图片和文档)
|
||||
* @param {string} filePath - 文件临时路径或URI
|
||||
* @param {string} fileName - 文件名(可选,用于文档文件)
|
||||
* @param {object} [options] - 配置选项
|
||||
* @param {number} [options.maxSize=50] - 最大文件大小(MB),文档文件默认50MB
|
||||
* @param {string} [options.prefix='uploads/'] - 文件前缀路径
|
||||
* @param {boolean} [options.showToast=true] - 是否显示提示
|
||||
* @param {string} [options.domain='https://api.ccttiot.com'] - 七牛云域名
|
||||
* @returns {Promise<object>} 返回 { url: string, name: string, size: number }
|
||||
*/
|
||||
export const uploadFileToQiniu = async (filePath, fileName = '', options = {}) => {
|
||||
const {
|
||||
maxSize = 50,
|
||||
prefix = 'uploads/',
|
||||
showToast = true,
|
||||
domain = 'https://api.ccttiot.com',
|
||||
} = options
|
||||
|
||||
try {
|
||||
// 1. 如果是URI格式(content://),需要先转换为临时路径
|
||||
let tempFilePath = filePath
|
||||
if (filePath.startsWith('content://')) {
|
||||
// 在uni-app中,content:// URI需要特殊处理
|
||||
// 对于文档文件,uni.uploadFile可以直接使用URI
|
||||
tempFilePath = filePath
|
||||
}
|
||||
|
||||
// 2. 获取文件信息(如果不是URI格式)
|
||||
let fileInfo = { size: 0 }
|
||||
if (!filePath.startsWith('content://')) {
|
||||
try {
|
||||
fileInfo = await getFileInfo(tempFilePath)
|
||||
} catch (error) {
|
||||
console.warn('获取文件信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const sizeInMB = fileInfo.size / (1024 * 1024)
|
||||
if (sizeInMB > maxSize) {
|
||||
throw new Error(`文件大小不能超过 ${maxSize}MB`)
|
||||
}
|
||||
|
||||
// 3. 获取上传凭证
|
||||
const token = await getQiniuToken()
|
||||
if (!token) {
|
||||
throw new Error('获取上传凭证失败')
|
||||
}
|
||||
|
||||
// 4. 生成唯一文件名
|
||||
let fileExt = 'file'
|
||||
if (fileName) {
|
||||
const parts = fileName.split('.')
|
||||
if (parts.length > 1) {
|
||||
fileExt = parts.pop().toLowerCase()
|
||||
}
|
||||
} else {
|
||||
fileExt = getFileExtension(tempFilePath)
|
||||
}
|
||||
|
||||
const key = generateUniqueKey(prefix, fileExt)
|
||||
|
||||
// 5. 上传到七牛云
|
||||
const uploadResult = await uploadToQiniu(tempFilePath, token, key)
|
||||
|
||||
// 验证上传结果
|
||||
if (!uploadResult || typeof uploadResult !== 'object') {
|
||||
throw new Error('上传失败:返回数据格式错误')
|
||||
}
|
||||
|
||||
if (!uploadResult.key) {
|
||||
console.error('上传响应缺少key字段:', uploadResult)
|
||||
throw new Error('上传失败:未返回文件key')
|
||||
}
|
||||
|
||||
// 6. 构建完整URL
|
||||
const qiniuUrl = `${domain}/${uploadResult.key}`
|
||||
console.log('文件上传成功:', { key: uploadResult.key, url: qiniuUrl })
|
||||
|
||||
// 7. 生成文件名(如果没有提供)
|
||||
const finalFileName = fileName || `文件.${fileExt}`
|
||||
|
||||
if (showToast) {
|
||||
// 上传成功不显示提示,避免打扰用户
|
||||
}
|
||||
|
||||
return {
|
||||
url: qiniuUrl,
|
||||
name: finalFileName,
|
||||
size: fileInfo.size || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error)
|
||||
if (showToast) {
|
||||
uni.showToast({
|
||||
title: error.message || '上传失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上传文件到七牛云(支持图片和文档)
|
||||
* @param {Array<{path: string, name?: string}>} files - 文件数组,每个文件包含path和可选的name
|
||||
* @param {object} [options] - 配置选项
|
||||
* @returns {Promise<Array<{url: string, name: string, size: number}>>} 上传结果数组
|
||||
*/
|
||||
export const batchUploadFilesToQiniu = async (files, options = {}) => {
|
||||
return Promise.all(
|
||||
files.map(file => uploadFileToQiniu(file.path, file.name || '', { ...options, showToast: false }))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件信息
|
||||
* @private
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user