更新记录
1.0.0(2026-05-11)
- 发布插件
平台兼容性
uni-app(5.08)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | Android插件版本 | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | 5.0 | 1.0.0 | - | - |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - | - | - | - |
uni-app x(5.08)
| Chrome | Safari | Android | Android插件版本 | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|---|
| - | - | 5.0 | 1.0.0 | - | - | - |
特性
- 🚀 支持 FTP连接
- 📤 文件上传/下载,实时进度监听
- 📁 目录操作(创建、删除、切换)
- 📋 文件列表获取,包含详细信息
- 🔄 支持主动/被动模式切换
- 🌐 支持多种编码格式
1. 初始化
import { CreateFtpContext } from '@/uni_modules/yuange-ftp'
// 创建 FTP 上下文
const ftpContext = CreateFtpContext()
if (ftpContext == null) {
console.error('FTP上下文初始化失败')
return
}
2. 连接服务器
ftpContext.connect({
host: 'ftp.example.com', // 服务器地址
port: 21, // 端口,默认21
username: 'your_username', // 用户名
password: 'your_password', // 密码
isPassive: true, // 是否被动模式,默认为 true
timeout: 30000, // 超时时间(毫秒)
encoding: 'UTF-8' // 编码格式,默认 UTF-8
}, {
success: (res) => {
console.log('连接成功:', res)
},
fail: (res) => {
console.error('连接失败:', res)
}
})
3. 获取文件列表
// 获取当前目录文件列表
ftpContext.list('', {
success: (res) => {
console.log('文件列表:', res.files)
},
fail: (res) => {
console.error('获取失败:', res)
}
})
// 获取指定目录文件列表
ftpContext.list('/images', {
success: (res) => {
console.log('文件列表:', res.files)
},
fail: (res) => {
console.error('获取失败:', res)
}
})
4. 上传文件
// 简单上传
ftpContext.uploadFile(
'/sdcard/xxxxxx/file.txt', // 本地文件路径
'/remote/file.txt', // 远程保存路径
{
success: (res) => {
console.log('上传成功')
},
fail: (res) => {
console.error('上传失败')
}
}
)
// 带进度上传
ftpContext.uploadFileWithProgress(
'/path/to/local/file.txt',
'/remote/file.txt',
{
progress: (progress) => {
console.log('上传进度:', {
fileName: progress.fileName, // 文件名
transferred: progress.transferred, // 已传输字节
total: progress.total, // 文件总大小
speed: progress.speed, // 传输速度
currentIndex: progress.currentIndex, // 当前文件索引
totalFiles: progress.totalFiles // 总文件数
})
}
},
{
success: (res) => {
console.log('上传成功')
},
fail: (res) => {
console.error('上传失败')
}
}
)
5. 下载文件
// 简单下载
ftpContext.downloadFile(
'/remote/file.txt', // 远程文件路径
'/path/to/local/file.txt', // 本地保存路径
{
success: (res) => {
console.log('下载成功')
},
fail: (res) => {
console.error('下载失败')
}
}
)
// 带进度下载
ftpContext.downloadFileWithProgress(
'/remote/file.txt',
'/path/to/local/file.txt',
{
progress: (progress) => {
console.log('下载进度:', {
fileName: progress.fileName,
transferred: progress.transferred,
total: progress.total,
speed: progress.speed
})
}
},
{
success: (res) => {
console.log('下载成功')
},
fail: (res) => {
console.error('下载失败')
}
}
)
6. 断开连接
ftpContext.close({
success: (res) => {
console.log('已断开连接')
},
fail: (res) => {
console.error('断开失败')
}
})
// 销毁上下文(释放资源)
ftpContext.destroy()
API 接口文档
全局函数
CreateFtpContext()
创建 FTP 上下文实例。
返回值
| 类型 | 说明 |
|---|---|
IFtpContext \| null |
FTP 上下文实例,失败返回 null |
示例
const ftpContext = CreateFtpContext()
IFtpContext 接口
所有 FTP 操作都通过 IFtpContext 接口提供。
connect(options, callback)
连接 FTP 服务器。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| options | FtpConnectOptions |
是 | 连接配置 |
| callback | FtpCallbackOptions |
是 | 回调函数 |
FtpConnectOptions
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| host | string |
- | FTP 服务器地址 |
| port | number |
21 | 端口号 |
| username | string |
- | 用户名 |
| password | string |
- | 密码 |
| isPassive | boolean |
true | 是否使用被动模式 |
| timeout | number |
30000 | 连接超时时间(毫秒) |
| encoding | string |
UTF-8 | 编码格式 |
示例
ftpContext.connect({
host: 'ftp.example.com',
port: 21,
username: 'user',
password: 'pass',
isPassive: true,
timeout: 30000,
encoding: 'UTF-8'
}, {
success: (res) => { /* 成功处理 */ },
fail: (res) => { /* 失败处理 */ }
})
close(callback)
关闭 FTP 连接。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| callback | FtpCallbackOptions |
是 | 回调函数 |
pwd(callback)
获取当前工作目录。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| callback | FtpCallbackOptions |
是 | 回调函数 |
返回数据
{
code: 200,
msg: '获取当前目录成功',
path: '/current/directory'
}
cd(path, callback)
切换工作目录。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| path | string |
是 | 目录路径 |
| callback | FtpCallbackOptions |
是 | 回调函数 |
cdup(callback)
返回上级目录。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| callback | FtpCallbackOptions |
是 | 回调函数 |
list(path, callback)
获取文件列表。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| path | string |
是 | 目录路径(空字符串表示当前目录) |
| callback | FtpCallbackOptions |
是 | 回调函数 |
返回数据
{
code: 200,
msg: '获取文件列表成功',
files: [
{
name: 'example.txt', // 文件名
path: '/example.txt', // 文件路径
size: 1024, // 文件大小(字节)
isDirectory: false, // 是否目录
lastModified: 1699999999999, // 最后修改时间(时间戳)
permissions: 'rw-r--r--', // 权限
owner: 'user', // 所有者
group: 'group' // 文件组
},
{
name: 'subdir',
path: '/subdir',
size: 4096,
isDirectory: true,
lastModified: 1699999999999,
permissions: 'rwxr-xr-x',
owner: 'user',
group: 'group'
}
]
}
mkdir(path, callback)
创建目录。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| path | string |
是 | 目录路径 |
| callback | FtpCallbackOptions |
是 | 回调函数 |
rmdir(path, callback)
删除空目录。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| path | string |
是 | 目录路径 |
| callback | FtpCallbackOptions |
是 | 回调函数 |
delete(path, callback)
删除文件。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| path | string |
是 | 文件路径 |
| callback | FtpCallbackOptions |
是 | 回调函数 |
rename(oldPath, newPath, callback)
重命名文件或目录。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| oldPath | string |
是 | 原路径 |
| newPath | string |
是 | 新路径 |
| callback | FtpCallbackOptions |
是 | 回调函数 |
uploadFile(localPath, remotePath, callback)
上传单个文件(无进度)。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| localPath | string |
是 | 本地文件路径 |
| remotePath | string |
是 | 远程保存路径 |
| callback | FtpCallbackOptions |
是 | 回调函数 |
uploadFileWithProgress(localPath, remotePath, progressCallback, callback)
上传单个文件(带进度)。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| localPath | string |
是 | 本地文件路径 |
| remotePath | string |
是 | 远程保存路径 |
| progressCallback | FtpProgressOptions |
是 | 进度回调 |
| callback | FtpCallbackOptions |
是 | 完成回调 |
progressCallback
{
progress: (progress) => {
// progress 类型为 FtpProgressInfo
}
}
uploadDirectory(localPath, remotePath, progressCallback, callback)
上传整个目录。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| localPath | string |
是 | 本地目录路径 |
| remotePath | string |
是 | 远程保存路径 |
| progressCallback | FtpProgressOptions |
是 | 进度回调 |
| callback | FtpCallbackOptions |
是 | 完成回调 |
downloadFile(remotePath, localPath, callback)
下载单个文件(无进度)。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| remotePath | string |
是 | 远程文件路径 |
| localPath | string |
是 | 本地保存路径 |
| callback | FtpCallbackOptions |
是 | 回调函数 |
downloadFileWithProgress(remotePath, localPath, progressCallback, callback)
下载单个文件(带进度)。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| remotePath | string |
是 | 远程文件路径 |
| localPath | string |
是 | 本地保存路径 |
| progressCallback | FtpProgressOptions |
是 | 进度回调 |
| callback | FtpCallbackOptions |
是 | 完成回调 |
downloadDirectory(remotePath, localPath, progressCallback, callback)
下载整个目录。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| remotePath | string |
是 | 远程目录路径 |
| localPath | string |
是 | 本地保存路径 |
| progressCallback | FtpProgressOptions |
是 | 进度回调 |
| callback | FtpCallbackOptions |
是 | 完成回调 |
setTransferMode(mode, callback)
设置传输模式。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| mode | 'BINARY' \| 'ASCII' |
是 | 传输模式 |
| callback | FtpCallbackOptions |
是 | 回调函数 |
exists(path, callback)
检查文件或目录是否存在。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| path | string |
是 | 文件或目录路径 |
| callback | FtpCallbackOptions |
是 | 回调函数 |
返回数据
{
code: 200,
msg: '文件存在',
exists: true
}
size(path, callback)
获取文件大小。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| path | string |
是 | 文件路径 |
| callback | FtpCallbackOptions |
是 | 回调函数 |
返回数据
{
code: 200,
msg: '获取文件大小成功',
size: 1024
}
modifiedTime(path, callback)
获取文件最后修改时间。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| path | string |
是 | 文件路径 |
| callback | FtpCallbackOptions |
是 | 回调函数 |
返回数据
{
code: 200,
msg: '获取修改时间成功',
time: 1699999999999
}
destroy()
销毁 FTP 上下文,释放资源。
注意:调用此方法后,上下文将不可用,需要重新调用 CreateFtpContext() 创建新实例。
类型定义
FtpProgressInfo
进度信息类型。
interface FtpProgressInfo {
fileName: string // 当前传输的文件名
transferred: number // 已传输字节数
total: number // 文件总大小(字节)
currentIndex: number // 当前传输的文件索引
totalFiles: number // 总文件数
speed: number // 传输速度(字节/秒)
}
FtpFileInfo
文件信息类型。
interface FtpFileInfo {
name: string // 文件名
path: string // 文件路径
size: number // 文件大小(字节)
isDirectory: boolean // 是否目录
lastModified: number // 最后修改时间(时间戳,毫秒)
permissions: string // 文件权限
owner: string // 所有者
group: string // 文件组
}
FtpCallbackOptions
回调选项类型。
type FtpCallbackOptions = {
success: (msg: string) => void // 成功回调
fail: (msg: string) => void // 失败回调
}
FtpProgressOptions
进度回调选项类型。
type FtpProgressOptions = {
progress: (progress: FtpProgressInfo) => void
}
完整Demo示例 ftp-demo.vue
<template>
<view class="container">
<!-- 连接设置区域 -->
<view class="section">
<text class="section-title">FTP服务器设置</text>
<view class="form-item">
<text class="label">服务器地址</text>
<input class="input" type="text" v-model="ftpConfig.host" placeholder="例如: 192.168.1.100" />
</view>
<view class="form-item">
<text class="label">端口</text>
<input class="input" type="number" v-model="ftpConfig.port" placeholder="21" />
</view>
<view class="form-item">
<text class="label">用户名</text>
<input class="input" type="text" v-model="ftpConfig.username" placeholder="anonymous" />
</view>
<view class="form-item">
<text class="label">密码</text>
<input class="input" type="text" v-model="ftpConfig.password" placeholder="密码" />
</view>
<view class="form-item">
<text class="label">被动模式</text>
<switch class="switch" :checked="ftpConfig.isPassive" @change="ftpConfig.isPassive = $event.detail.value" />
</view>
<view class="button-row">
<button class="btn btn-primary" @click="connectFtp" :disabled="isConnected">
{{ isConnected ? '已连接' : '连接' }}
</button>
<button class="btn btn-danger" @click="disconnectFtp" :disabled="!isConnected">
断开
</button>
</view>
</view>
<!-- 文件操作区域 -->
<view class="section" v-if="isConnected">
<text class="section-title">文件操作</text>
<view class="current-path">
<text class="label">当前路径:</text>
<text class="path">{{ currentPath }}</text>
</view>
<view class="form-item">
<text class="label">获取文件列表路径</text>
<input class="input" type="text" v-model="listPath" placeholder="留空则获取当前目录,例如: / 或 /images" />
</view>
<view class="button-row">
<button class="btn btn-small" @click="refreshList">获取列表</button>
<button class="btn btn-small" @click="goParent">上级目录</button>
</view>
<!-- 文件列表 -->
<view class="file-list">
<view v-if="isLoading" class="loading">
<text>加载中...</text>
</view>
<view v-else-if="fileList.length === 0" class="empty">
<text>目录为空</text>
</view>
<view v-else>
<view
v-for="(file, index) in fileList"
:key="index"
class="file-item"
@click="onFileClick(file)"
>
<view class="file-icon">
<text>{{ file.isDirectory ? '📁' : '📄' }}</text>
</view>
<view class="file-info">
<text class="file-name">{{ file.name }}</text>
<text class="file-size" v-if="!file.isDirectory">{{ formatSize(file.size) }}</text>
</view>
<view class="file-actions">
<text class="action-btn" @click.stop="deleteFile(file)">删除</text>
</view>
</view>
</view>
</view>
<!-- 新建目录 -->
<view class="form-item">
<text class="label">新建目录</text>
<view class="input-row">
<input class="input" type="text" v-model="newDirName" placeholder="目录名称" />
<button class="btn btn-small" @click="createDir">创建</button>
</view>
</view>
</view>
<!-- 上传下载区域 -->
<view class="section" v-if="isConnected">
<text class="section-title">上传下载</text>
<!-- 上传 -->
<view class="transfer-section">
<text class="label">上传文件</text>
<view class="input-row">
<input class="input" type="text" v-model="localFilePath" placeholder="本地文件路径" />
<button class="btn btn-small btn-primary" @click="uploadFile">上传</button>
</view>
<view class="input-row">
<input class="input" type="text" v-model="remoteFilePath" placeholder="远程保存路径" />
</view>
</view>
<!-- 下载 -->
<view class="transfer-section">
<text class="label">下载文件</text>
<view class="input-row">
<input class="input" type="text" v-model="downloadRemotePath" placeholder="远程文件路径" />
<button class="btn btn-small btn-primary" @click="downloadFile">下载</button>
</view>
<view class="input-row">
<input class="input" type="text" v-model="downloadLocalPath" placeholder="本地保存路径" />
</view>
</view>
<!-- 传输进度 -->
<view v-if="transferProgress" class="progress-section">
<text class="label">传输进度</text>
<view class="progress-info">
<text>文件: {{ transferProgress.fileName }}</text>
<text>进度: {{ transferProgress.transferred }} / {{ transferProgress.total }} ({{ Math.floor(transferProgress.transferred / (transferProgress.total != 0 ? transferProgress.total : 1) * 100) }}%)</text>
<text>速度: {{ formatSize(transferProgress.speed) }}/s</text>
</view>
<view class="progress-bar">
<view class="progress-fill" :style="{ width: (transferProgress.transferred / (transferProgress.total != 0 ? transferProgress.total : 1) * 100) + '%' }"></view>
</view>
</view>
</view>
<!-- 日志区域 -->
<view class="section log-section">
<text class="section-title">操作日志</text>
<view class="log-container">
<view v-for="(log, index) in logs" :key="index" :class="['log-item', 'log-' + log.type]">
<text>{{ log.time }} {{ log.message }}</text>
</view>
</view>
<view class="button-row">
<button class="btn btn-small" @click="clearLogs">清空日志</button>
</view>
</view>
</view>
</template>
<script>
// 导入 FTP 接口定义
import { CreateFtpContext } from '@/uni_modules/yuange-ftp'
export default {
data() {
return {
ftpContext: null,
ftpConfig: {
host: '192.168.1.122',
port: 21,
username: 'testftp',
password: 'Ftp123456',
isPassive: true
},
isConnected: false,
isLoading: false,
currentPath: '/',
listPath: '',
fileList: [],
newDirName: '',
localFilePath: '',
remoteFilePath: '',
downloadRemotePath: '',
downloadLocalPath: '',
transferProgress: null,
logs: []
}
},
onLoad() {
this.addLog('info', '页面加载完成')
this.requestPermissions()
this.initFtpContext()
},
methods: {
requestPermissions() {
// #ifdef APP-PLUS
const permissions = [
'android.permission.READ_EXTERNAL_STORAGE',
'android.permission.WRITE_EXTERNAL_STORAGE'
]
plus.android.requestPermissions(permissions, (result) => {
console.log('权限申请结果:', result)
if (result.granted.length > 0) {
this.addLog('success', '文件读写权限已授权')
} else {
this.addLog('warning', '部分权限未授权,可能影响文件操作')
}
}, (error) => {
console.error('权限申请失败:', error)
this.addLog('error', '权限申请失败')
})
// #endif
},
onUnload() {
if (this.ftpContext != null) {
this.ftpContext.destroy()
this.ftpContext = null
}
},
formatSize(bytes) {
if (bytes === 0) return '0 B'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
const k = 1024
const i = Math.floor(Math.log(bytes) / Math.log(k))
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + units[i]
},
addLog(type, message) {
const now = new Date()
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
const item = {
type: type,
time: time,
message: message
}
this.logs.unshift(item)
if (this.logs.length > 100) {
this.logs.pop()
}
},
clearLogs() {
this.logs = []
},
getMsg(res) {
if (res && res.msg) {
return res.msg
}
if (res && res.getString) {
return res.getString('msg') || '未知错误'
}
return '未知错误'
},
initFtpContext() {
try {
console.log('正在初始化FTP上下文...')
// 通过 import 导入的 CreateFtpContext 创建上下文
const ctx = CreateFtpContext()
if (ctx == null) {
this.addLog('error', 'FTP上下文初始化失败')
return
}
this.ftpContext = ctx
this.addLog('success', 'FTP插件已就绪')
} catch (e) {
console.error('FTP初始化异常:', e)
this.addLog('error', `FTP初始化异常: ${e.message || e}`)
}
},
connectFtp() {
if (this.ftpContext == null) {
this.addLog('error', 'FTP上下文未初始化')
return
}
this.addLog('info', `正在连接 ${this.ftpConfig.host}:${this.ftpConfig.port}...`)
this.ftpContext.connect({
host: this.ftpConfig.host,
port: parseInt(this.ftpConfig.port.toString()),
username: this.ftpConfig.username,
password: this.ftpConfig.password,
isPassive: this.ftpConfig.isPassive,
timeout: 30000,
encoding: 'UTF-8'
}, {
success: (res) => {
this.addLog('success', '连接成功')
this.isConnected = true
this.refreshList()
},
fail: (res) => {
this.addLog('error', `连接失败: ${this.getMsg(res)}`)
}
})
},
disconnectFtp() {
if (this.ftpContext == null) {
return
}
this.ftpContext.close({
success: (res) => {
this.addLog('info', '已断开连接')
this.isConnected = false
this.fileList = []
this.currentPath = '/'
},
fail: (res) => {
this.addLog('error', `断开失败: ${this.getMsg(res)}`)
}
})
},
refreshList() {
if (this.ftpContext == null) {
return
}
this.isLoading = true
const targetPath = this.listPath.trim() !== '' ? this.listPath.trim() : this.currentPath
this.addLog('info', `正在获取文件列表: ${targetPath}`)
this.ftpContext.list(targetPath, {
success: (res) => {
this.isLoading = false
try {
// res 可能是 JSON 字符串,需要解析
let data = res;
if (typeof res === 'string') {
data = JSON.parse(res);
}
let files = data.files;
if (files) {
const newList = []
for (let i = 0; i < files.length; i++) {
const f = files[i]
newList.push({
name: f.name || '',
path: f.path || '',
size: f.size || 0,
isDirectory: f.isDirectory || false,
lastModified: f.lastModified || 0,
permissions: f.permissions || '',
owner: f.owner || '',
group: f.group || ''
})
}
this.fileList = newList
}
this.addLog('info', `获取文件列表成功,共 ${this.fileList.length} 项`)
} catch (e) {
this.addLog('error', `解析文件列表失败: ${e.message || e}`)
}
},
fail: (res) => {
this.isLoading = false
this.addLog('error', `获取文件列表失败: ${this.getMsg(res)}`)
}
})
},
goParent() {
if (this.ftpContext == null) {
return
}
this.ftpContext.cdup({
success: (res) => {
this.addLog('info', '已进入上级目录')
this.refreshList()
this.ftpContext.pwd({
success: (pwdRes) => {
this.currentPath = pwdRes.path || '/'
},
fail: () => {}
})
},
fail: (res) => {
this.addLog('error', `进入上级目录失败: ${this.getMsg(res)}`)
}
})
},
onFileClick(file) {
if (file.isDirectory) {
if (this.ftpContext != null) {
this.ftpContext.cd(file.name, {
success: (res) => {
this.currentPath = file.path
this.addLog('info', `进入目录: ${file.name}`)
this.refreshList()
},
fail: (res) => {
this.addLog('error', `进入目录失败: ${this.getMsg(res)}`)
}
})
}
} else {
uni.showModal({
title: '文件操作',
content: `文件名: ${file.name}\n大小: ${this.formatSize(file.size)}`,
confirmText: '下载',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
this.downloadRemotePath = file.path
this.downloadLocalPath = `/sdcard/download/${file.name}`
this.downloadFile()
}
}
})
}
},
createDir() {
if (this.ftpContext == null || this.newDirName.trim() === '') {
return
}
this.ftpContext.mkdir(this.newDirName.trim(), {
success: (res) => {
this.addLog('success', `目录已创建: ${this.newDirName}`)
this.newDirName = ''
this.refreshList()
},
fail: (res) => {
this.addLog('error', `创建目录失败: ${this.getMsg(res)}`)
}
})
},
deleteFile(file) {
uni.showModal({
title: '确认删除',
content: `确定要删除 ${file.isDirectory ? '目录' : '文件'}: ${file.name} 吗?`,
confirmText: '删除',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
if (this.ftpContext == null) {
return
}
if (file.isDirectory) {
this.ftpContext.rmdir(file.path, {
success: (res) => {
this.addLog('success', `目录已删除: ${file.name}`)
this.refreshList()
},
fail: (res) => {
this.addLog('error', `删除目录失败: ${this.getMsg(res)}`)
}
})
} else {
this.ftpContext.delete(file.path, {
success: (res) => {
this.addLog('success', `文件已删除: ${file.name}`)
this.refreshList()
},
fail: (res) => {
this.addLog('error', `删除文件失败: ${this.getMsg(res)}`)
}
})
}
}
}
})
},
uploadFile() {
if (this.ftpContext == null || this.localFilePath.trim() === '') {
this.addLog('error', '请输入本地文件路径')
return
}
const remotePath = this.remoteFilePath.trim() !== '' ? this.remoteFilePath.trim() : this.currentPath + '/' + this.localFilePath.split('/').pop()
this.addLog('info', `开始上传: ${this.localFilePath} -> ${remotePath}`)
this.transferProgress = null
this.ftpContext.uploadFileWithProgress(this.localFilePath.trim(), remotePath, {
progress: (progress) => {
this.transferProgress = progress
}
}, {
success: (res) => {
this.addLog('success', '上传成功')
this.transferProgress = null
this.refreshList()
},
fail: (res) => {
this.addLog('error', `上传失败: ${this.getMsg(res)}`)
this.transferProgress = null
}
})
},
downloadFile() {
if (this.ftpContext == null || this.downloadRemotePath.trim() === '') {
this.addLog('error', '请输入远程文件路径')
return
}
if (this.downloadLocalPath.trim() === '') {
this.addLog('error', '请输入本地保存路径')
return
}
this.addLog('info', `开始下载: ${this.downloadRemotePath} -> ${this.downloadLocalPath}`)
this.transferProgress = null
this.ftpContext.downloadFileWithProgress(this.downloadRemotePath.trim(), this.downloadLocalPath.trim(), {
progress: (progress) => {
this.transferProgress = progress
}
}, {
success: (res) => {
// 下载成功后检查文件是否存在
this.checkFileExists(this.downloadLocalPath.trim())
this.transferProgress = null
},
fail: (res) => {
this.addLog('error', `下载失败: ${this.getMsg(res)}`)
this.transferProgress = null
}
})
},
checkFileExists(filePath) {
// #ifdef APP-PLUS
const main = plus.android.runtimeMainActivity()
const Intent = plus.android.importClass('android.content.Intent')
const Uri = plus.android.importClass('android.net.Uri')
const File = plus.android.importClass('java.io.File')
try {
const file = new File(filePath)
if (file.exists()) {
const size = file.length()
this.addLog('success', `文件下载成功!大小: ${this.formatSize(size)}, 路径: ${filePath}`)
// 尝试通过 MediaStore 刷新(Android 10+)
const context = plus.android.runtimeMainActivity()
const applicationContext = context.getApplicationContext()
// 通知系统刷新媒体库
const mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
const contentUri = Uri.fromFile(file)
mediaScanIntent.setData(contentUri)
applicationContext.sendBroadcast(mediaScanIntent)
this.addLog('info', '已通知系统刷新文件')
} else {
this.addLog('error', `文件不存在: ${filePath}`)
}
} catch (e) {
this.addLog('warning', `检查文件失败: ${e.message || e}`)
this.addLog('success', `下载回调成功,请手动检查路径: ${filePath}`)
}
// #endif
// #ifndef APP-PLUS
this.addLog('success', '下载成功(非App环境)')
// #endif
}
}
}
</script>
<style lang="scss">
.container {
padding: 20rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
.section {
background-color: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 24rpx;
display: block;
}
.form-item {
margin-bottom: 20rpx;
}
.label {
font-size: 28rpx;
color: #666666;
margin-bottom: 8rpx;
display: block;
}
.input {
width: 100%;
height: 72rpx;
border: 1px solid #e0e0e0;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
background-color: #fafafa;
}
.input-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 16rpx;
}
.input-row .input {
flex: 1;
}
.switch {
transform: scale(0.8);
}
.button-row {
display: flex;
flex-direction: row;
gap: 16rpx;
margin: 16rpx 0;
}
.btn, .btn-primary, .btn-danger, .btn-small {
margin: 0;
padding: 0;
border-radius: 8rpx;
font-size: 28rpx;
border: none;
background-color: #007aff;
color: #ffffff;
}
.btn, .btn-primary, .btn-danger {
flex: 1;
height: 80rpx;
line-height: 80rpx;
text-align: center;
}
.btn:disabled, .btn-primary:disabled, .btn-danger:disabled {
background-color: #cccccc;
color: #ffffff;
}
.btn-primary {
background-color: #007aff;
}
.btn-danger {
background-color: #ff3b30;
}
.btn-small {
flex: 0 0 auto;
min-width: 120rpx;
height: 64rpx;
line-height: 64rpx;
font-size: 26rpx;
padding: 0 24rpx;
white-space: nowrap;
}
.current-path {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 16rpx;
padding: 16rpx;
background-color: #f0f0f0;
border-radius: 8rpx;
}
.current-path .path {
color: #007aff;
font-size: 28rpx;
margin-left: 8rpx;
word-break: break-all;
}
.file-list {
margin: 16rpx 0;
max-height: 500rpx;
overflow-y: auto;
}
.loading, .empty {
text-align: center;
padding: 40rpx;
color: #999999;
}
.file-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 20rpx;
border-bottom: 1px solid #f0f0f0;
}
.file-item:active {
background-color: #f5f5f5;
}
.file-icon {
width: 60rpx;
font-size: 36rpx;
}
.file-info {
flex: 1;
margin-left: 16rpx;
}
.file-name {
font-size: 28rpx;
color: #333333;
display: block;
}
.file-size {
font-size: 24rpx;
color: #999999;
margin-top: 4rpx;
display: block;
}
.file-actions {
width: 100rpx;
text-align: right;
}
.action-btn {
color: #ff3b30;
font-size: 26rpx;
}
.transfer-section {
margin: 20rpx 0;
padding: 20rpx;
background-color: #fafafa;
border-radius: 8rpx;
}
.progress-section {
margin-top: 20rpx;
padding: 20rpx;
background-color: #f0f9ff;
border-radius: 8rpx;
}
.progress-info {
margin: 12rpx 0;
}
.progress-info text {
display: block;
font-size: 24rpx;
color: #666666;
line-height: 1.6;
}
.progress-bar {
height: 16rpx;
background-color: #e0e0e0;
border-radius: 8rpx;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #007aff;
border-radius: 8rpx;
transition: width 0.3s ease;
}
.log-section {
max-height: 400rpx;
}
.log-container {
max-height: 280rpx;
overflow-y: auto;
background-color: #1e1e1e;
border-radius: 8rpx;
padding: 16rpx;
}
.log-item {
font-size: 22rpx;
font-family: 'Courier New', monospace;
line-height: 1.6;
margin-bottom: 4rpx;
}
.log-info {
color: #4fc3f7;
}
.log-success {
color: #81c784;
}
.log-error {
color: #e57373;
}
.log-warning {
color: #fff176;
}
</style>

收藏人数:
购买普通授权版(
试用
赞赏(0)
下载 653
赞赏 0
下载 11880308
赞赏 1912
赞赏
京公网安备:11010802035340号