新增功能文件保险箱
*目前只是一个lite版文件管理器
This commit is contained in:
parent
a3fa8c87e0
commit
9eb9d9d60f
@ -346,6 +346,10 @@ class UserController extends Controller
|
|||||||
if (!is_dir($userFolder)) {
|
if (!is_dir($userFolder)) {
|
||||||
mkdir($userFolder);
|
mkdir($userFolder);
|
||||||
}
|
}
|
||||||
|
$secretFolder = Yii::getAlias(Yii::$app->params['dataDirectory']) . '/' . $model->id.'.secret';
|
||||||
|
if (!is_dir($secretFolder)) {
|
||||||
|
mkdir($secretFolder);
|
||||||
|
}
|
||||||
Yii::$app->session->setFlash('success', 'Registration successful. You can now log in.');
|
Yii::$app->session->setFlash('success', 'Registration successful. You can now log in.');
|
||||||
return $this->redirect(['login']);
|
return $this->redirect(['login']);
|
||||||
} else {
|
} else {
|
||||||
@ -373,8 +377,8 @@ class UserController extends Controller
|
|||||||
public function actionInfo(string $focus = null): Response|string
|
public function actionInfo(string $focus = null): Response|string
|
||||||
{
|
{
|
||||||
$model = Yii::$app->user->identity;
|
$model = Yii::$app->user->identity;
|
||||||
$usedSpace = FileSizeHelper::getDirectorySize(Yii::getAlias(Yii::$app->params['dataDirectory']) . '/' . Yii::$app->user->id);
|
$usedSpace = FileSizeHelper::getUserHomeDirSize();
|
||||||
$vaultUsedSpace = 0; // 保险箱已用空间,暂时为0
|
$vaultUsedSpace = FileSizeHelper::getUserVaultDirSize();
|
||||||
$storageLimit = $model->storage_limit;
|
$storageLimit = $model->storage_limit;
|
||||||
$totp_secret = null;
|
$totp_secret = null;
|
||||||
$totp_url = null;
|
$totp_url = null;
|
||||||
|
243
controllers/VaultController.php
Normal file
243
controllers/VaultController.php
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* 保险箱设计: 仅文件上传下载及删除,不支持文件夹
|
||||||
|
* 有些文件管理的代码迁移过来没删干净,这个忽略一下
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace app\controllers;
|
||||||
|
|
||||||
|
use app\models\UploadForm;
|
||||||
|
use app\utils\FileSizeHelper;
|
||||||
|
use app\utils\FileTypeDetector;
|
||||||
|
use Yii;
|
||||||
|
use yii\filters\AccessControl;
|
||||||
|
use yii\filters\VerbFilter;
|
||||||
|
use yii\web\Controller;
|
||||||
|
use yii\web\NotFoundHttpException;
|
||||||
|
use yii\web\Response;
|
||||||
|
use yii\web\UploadedFile;
|
||||||
|
|
||||||
|
class VaultController extends Controller
|
||||||
|
{
|
||||||
|
protected string $pattern = '/^[^\p{C}:*?"<>|\\\\]+$/u';
|
||||||
|
|
||||||
|
public function behaviors(): array
|
||||||
|
{
|
||||||
|
return array_merge(
|
||||||
|
parent::behaviors(),
|
||||||
|
[
|
||||||
|
'access' => [
|
||||||
|
'class' => AccessControl::class,
|
||||||
|
'rules' => [
|
||||||
|
[
|
||||||
|
'allow' => true,
|
||||||
|
'actions' => ['index', 'download', 'delete', 'upload'],
|
||||||
|
'roles' => ['user'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'verbs' => [
|
||||||
|
'class' => VerbFilter::class,
|
||||||
|
'actions' => [
|
||||||
|
'index' => ['GET'],
|
||||||
|
'download' => ['GET'],
|
||||||
|
'delete' => ['POST'],
|
||||||
|
'upload' => ['POST'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param null $directory
|
||||||
|
* @return string|Response
|
||||||
|
* @throws NotFoundHttpException
|
||||||
|
*/
|
||||||
|
public function actionIndex($directory = null): Response|string
|
||||||
|
{
|
||||||
|
$model = Yii::$app->user->identity;
|
||||||
|
|
||||||
|
$rootDataDirectory = Yii::getAlias(Yii::$app->params['dataDirectory']) . '/' . Yii::$app->user->id . '.secret';
|
||||||
|
|
||||||
|
if ($directory === '.' || $directory == null) {
|
||||||
|
$directory = null;
|
||||||
|
$parentDirectory = null;
|
||||||
|
} elseif (str_contains($directory, '..')) {
|
||||||
|
throw new NotFoundHttpException('Invalid directory.');
|
||||||
|
} else {
|
||||||
|
$parentDirectory = dirname($directory);
|
||||||
|
}
|
||||||
|
$directoryContents = $this->getDirectoryContents(join(DIRECTORY_SEPARATOR, [$rootDataDirectory, $directory ?: '.']));
|
||||||
|
foreach ($directoryContents as $key => $item) {
|
||||||
|
$relativePath = $directory ? $directory . '/' . $item : $item;
|
||||||
|
$absolutePath = Yii::getAlias('@app') . '/data/' . Yii::$app->user->id . '.secret/' . $relativePath;
|
||||||
|
$type = FileTypeDetector::detect($absolutePath);
|
||||||
|
$lastModified = filemtime($absolutePath);
|
||||||
|
$size = is_file($absolutePath) ? filesize($absolutePath) : null;
|
||||||
|
$rawType = is_file($absolutePath) ? mime_content_type($absolutePath) : null;
|
||||||
|
$directoryContents[$key] = ['name' => $item, 'type' => $type, 'lastModified' => $lastModified, 'size' => $size, 'rawType' => $rawType];
|
||||||
|
}
|
||||||
|
$usedSpace = FileSizeHelper::getUserHomeDirSize();
|
||||||
|
$vaultUsedSpace = FileSizeHelper::getUserVaultDirSize(); // 保险箱已用空间,暂时为0
|
||||||
|
$storageLimit = $model->storage_limit;
|
||||||
|
return $this->render('index', [
|
||||||
|
'directoryContents' => $directoryContents,
|
||||||
|
'parentDirectory' => $parentDirectory,
|
||||||
|
'directory' => $directory, // 将$directory传递给视图
|
||||||
|
'usedSpace' => $usedSpace, // B
|
||||||
|
'vaultUsedSpace' => $vaultUsedSpace, // B
|
||||||
|
'storageLimit' => $storageLimit, // MB
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定路径下的文件和文件夹内容
|
||||||
|
* @param string $path 路径
|
||||||
|
* @return array 文件和文件夹内容数组
|
||||||
|
* @throws NotFoundHttpException 如果路径不存在
|
||||||
|
*/
|
||||||
|
protected function getDirectoryContents(string $path): array
|
||||||
|
{
|
||||||
|
// 确定路径是否存在
|
||||||
|
if (!is_dir($path)) {
|
||||||
|
throw new NotFoundHttpException('Directory not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取路径下的所有文件和文件夹
|
||||||
|
$directoryContents = scandir($path);
|
||||||
|
|
||||||
|
// 移除 '.' 和 '..'
|
||||||
|
$directoryContents = array_diff($directoryContents, ['.', '..']);
|
||||||
|
|
||||||
|
// 使用 usort 对目录内容进行排序,使文件夹始终在文件之前
|
||||||
|
usort($directoryContents, function ($a, $b) use ($path) {
|
||||||
|
$aIsDir = is_dir($path . '/' . $a);
|
||||||
|
$bIsDir = is_dir($path . '/' . $b);
|
||||||
|
if ($aIsDir === $bIsDir) {
|
||||||
|
return strnatcasecmp($a, $b); // 如果两者都是文件夹或都是文件,按名称排序
|
||||||
|
}
|
||||||
|
return $aIsDir ? -1 : 1; // 文件夹始终在文件之前
|
||||||
|
});
|
||||||
|
|
||||||
|
return $directoryContents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载指定路径下的文件
|
||||||
|
*
|
||||||
|
* @param string $relativePath 文件的相对路径
|
||||||
|
* @throws NotFoundHttpException 如果文件不存在
|
||||||
|
*/
|
||||||
|
public function actionDownload(string $relativePath): void
|
||||||
|
{
|
||||||
|
// 对相对路径进行解码
|
||||||
|
$relativePath = rawurldecode($relativePath);
|
||||||
|
|
||||||
|
// 检查相对路径是否只包含允许的字符
|
||||||
|
if (!preg_match($this->pattern, $relativePath) || str_contains($relativePath, '..')) {
|
||||||
|
throw new NotFoundHttpException('Invalid file path.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定文件的绝对路径
|
||||||
|
$absolutePath = Yii::getAlias(Yii::$app->params['dataDirectory']) . '/' . Yii::$app->user->id . '.secret/' . $relativePath;
|
||||||
|
|
||||||
|
// 使用realpath函数解析路径,并检查解析后的路径是否在预期的目录中
|
||||||
|
$realPath = realpath($absolutePath);
|
||||||
|
$dataDirectory = str_replace('/', '\\', Yii::getAlias(Yii::$app->params['dataDirectory']));
|
||||||
|
if (!$realPath || !str_starts_with($realPath, $dataDirectory)) {
|
||||||
|
throw new NotFoundHttpException('File not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
if (!file_exists($realPath)) {
|
||||||
|
throw new NotFoundHttpException('File not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将文件发送给用户进行下载
|
||||||
|
Yii::$app->response->sendFile($realPath)->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件
|
||||||
|
* @throws NotFoundHttpException 如果文件不存在
|
||||||
|
*/
|
||||||
|
public function actionDelete(): Response
|
||||||
|
{
|
||||||
|
$relativePaths = Yii::$app->request->post('relativePath');
|
||||||
|
if (!is_array($relativePaths)) {
|
||||||
|
$relativePaths = [$relativePaths];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($relativePaths as $relativePath) {
|
||||||
|
$relativePath = rawurldecode($relativePath);
|
||||||
|
if (!preg_match($this->pattern, $relativePath) || str_contains($relativePath, '..')) {
|
||||||
|
throw new NotFoundHttpException('Invalid file path.');
|
||||||
|
}
|
||||||
|
$absolutePath = Yii::getAlias(Yii::$app->params['dataDirectory']) . '/' . Yii::$app->user->id . '.secret/' . $relativePath;
|
||||||
|
if (!file_exists($absolutePath)) {
|
||||||
|
throw new NotFoundHttpException('File or directory not found.');
|
||||||
|
} else {
|
||||||
|
$realPath = realpath($absolutePath);
|
||||||
|
$expectedPathPrefix = realpath(Yii::getAlias(Yii::$app->params['dataDirectory']) . '/' . Yii::$app->user->id . '.secret');
|
||||||
|
if (!str_starts_with($realPath, $expectedPathPrefix)) {
|
||||||
|
throw new NotFoundHttpException('File or directory not found.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!unlink($absolutePath)) {
|
||||||
|
Yii::$app->session->setFlash('error', 'Failed to delete file.');
|
||||||
|
} else {
|
||||||
|
Yii::$app->session->setFlash('success', 'File deleted successfully.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this->redirect(['index', 'directory' => dirname($relativePaths[0])]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传
|
||||||
|
* 注意,已存在的同名文件会被覆盖
|
||||||
|
*
|
||||||
|
* @return int|Response
|
||||||
|
*/
|
||||||
|
public function actionUpload(): Response|int
|
||||||
|
{
|
||||||
|
$model = new UploadForm();
|
||||||
|
$model->targetDir = Yii::$app->request->post('targetDir', '.');
|
||||||
|
$uploadedFiles = UploadedFile::getInstancesByName('files');
|
||||||
|
$successCount = 0;
|
||||||
|
$totalCount = count($uploadedFiles);
|
||||||
|
$sp = Yii::$app->request->post('sp', null);
|
||||||
|
|
||||||
|
foreach ($uploadedFiles as $uploadedFile) {
|
||||||
|
$model->uploadFile = $uploadedFile;
|
||||||
|
if (!preg_match($this->pattern, $model->uploadFile->fullPath) || str_contains($model->uploadFile->fullPath, '..')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!FileSizeHelper::hasEnoughSpace($model->uploadFile->size)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($model->upload(1)) {
|
||||||
|
$successCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($sp === 'editSaving') {
|
||||||
|
if ($successCount === $totalCount) {
|
||||||
|
Yii::$app->response->statusCode = 200;
|
||||||
|
return $this->asJson(['status' => 200, 'message' => '文件上传成功']);
|
||||||
|
} else {
|
||||||
|
Yii::$app->response->statusCode = 500;
|
||||||
|
return $this->asJson(['status' => 500, 'message' => '文件上传失败']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($successCount === $totalCount) {
|
||||||
|
Yii::$app->session->setFlash('success', '文件上传成功');
|
||||||
|
} elseif ($successCount > 0) {
|
||||||
|
Yii::$app->session->setFlash('warning', '部分文件上传失败,这可能是用户剩余空间不足导致');
|
||||||
|
} else {
|
||||||
|
Yii::$app->session->setFlash('error', '文件上传失败,这可能是用户剩余空间不足导致');
|
||||||
|
}
|
||||||
|
//返回状态码200
|
||||||
|
return Yii::$app->response->statusCode = 200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,7 +22,7 @@ class UploadForm extends Model
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function upload(): bool
|
public function upload(int $is_vault_file = 0): bool
|
||||||
{
|
{
|
||||||
if ($this->validate()) {
|
if ($this->validate()) {
|
||||||
if ($this->targetDir === null) {
|
if ($this->targetDir === null) {
|
||||||
@ -32,6 +32,9 @@ class UploadForm extends Model
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$userHomeDir = Yii::getAlias(Yii::$app->params['dataDirectory']) . '/' . Yii::$app->user->id;
|
$userHomeDir = Yii::getAlias(Yii::$app->params['dataDirectory']) . '/' . Yii::$app->user->id;
|
||||||
|
if ($is_vault_file == 1) {
|
||||||
|
$userHomeDir .= '.secret';
|
||||||
|
}
|
||||||
$absolutePath = $userHomeDir . '/' . $this->targetDir;
|
$absolutePath = $userHomeDir . '/' . $this->targetDir;
|
||||||
if (!is_dir($absolutePath)) {
|
if (!is_dir($absolutePath)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -44,10 +44,10 @@ $darkMode = Yii::$app->user->isGuest ? 0 : Yii::$app->user->identity->dark_mode;
|
|||||||
'items' => [
|
'items' => [
|
||||||
['label' => '首页', 'url' => ['/site/index']],
|
['label' => '首页', 'url' => ['/site/index']],
|
||||||
['label' => '我的文件', 'url' => ['/home/index']],
|
['label' => '我的文件', 'url' => ['/home/index']],
|
||||||
|
['label' => '文件保险箱', 'url' => ['/vault/index']],
|
||||||
['label' => '分享管理', 'url' => ['/share/index']],
|
['label' => '分享管理', 'url' => ['/share/index']],
|
||||||
['label' => '文件收集', 'url' => ['/collection/index']],
|
['label' => '文件收集', 'url' => ['/collection/index']],
|
||||||
['label' => '个人设置', 'url' => ['/user/info']],
|
['label' => '个人设置', 'url' => ['/user/info']],
|
||||||
// ['label' => '系统设置', 'url' => ['/site/contact']],
|
|
||||||
// ['label' => '应用下载', 'url' => ['/site/contact']],
|
// ['label' => '应用下载', 'url' => ['/site/contact']],
|
||||||
Yii::$app->user->isGuest
|
Yii::$app->user->isGuest
|
||||||
? ['label' => '登录', 'url' => ['/user/login']]
|
? ['label' => '登录', 'url' => ['/user/login']]
|
||||||
|
164
views/vault/index.php
Normal file
164
views/vault/index.php
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
<?php
|
||||||
|
// 文件管理精简版是吗?
|
||||||
|
|
||||||
|
/* @var $this yii\web\View */
|
||||||
|
/* @var $directoryContents array 文件和文件夹内容数组 */
|
||||||
|
/* @var $parentDirectory string 父目录 */
|
||||||
|
/* @var $usedSpace int */
|
||||||
|
/* @var $vaultUsedSpace int */
|
||||||
|
/* @var $storageLimit int */
|
||||||
|
|
||||||
|
/* @var $directory string 当前路径 */
|
||||||
|
|
||||||
|
use app\utils\FileSizeHelper;
|
||||||
|
use yii\bootstrap5\Html;
|
||||||
|
use app\assets\FontAwesomeAsset;
|
||||||
|
use yii\bootstrap5\Modal;
|
||||||
|
use yii\bootstrap5\Progress;
|
||||||
|
use yii\helpers\Url;
|
||||||
|
use yii\web\JqueryAsset;
|
||||||
|
use yii\web\View;
|
||||||
|
|
||||||
|
$this->title = '文件保险箱';
|
||||||
|
$this->params['breadcrumbs'][] = $this->title;
|
||||||
|
$totalUsed_F = FileSizeHelper::formatBytes($usedSpace + $vaultUsedSpace); //总已用空间 格式化文本
|
||||||
|
$storageLimit_F = FileSizeHelper::formatMegaBytes($storageLimit); //存储限制 格式化文本
|
||||||
|
$is_unlimited = ($storageLimit === -1); //检查是否为无限制容量
|
||||||
|
$usedPercent = $is_unlimited ? 0 : round($usedSpace / ($storageLimit * 1024 * 1024) * 100); //网盘已用百分比
|
||||||
|
$vaultUsedPercent = $is_unlimited ? 0 : round($vaultUsedSpace / ($storageLimit * 1024 * 1024) * 100); //保险箱已用百分比
|
||||||
|
$totalUsedPercent = min(($usedPercent + $vaultUsedPercent), 100); //总已用百分比
|
||||||
|
$freeSpace = $is_unlimited ? 'unlimited' : ($storageLimit * 1024 * 1024 - $usedSpace - $vaultUsedSpace); //剩余空间
|
||||||
|
FontAwesomeAsset::register($this);
|
||||||
|
JqueryAsset::register($this);
|
||||||
|
$this->registerCssFile('@web/css/home_style.css');
|
||||||
|
?>
|
||||||
|
<div class="home-directory">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h1><?= Html::encode($this->title) ?></h1>
|
||||||
|
<div>
|
||||||
|
<?= Html::button('下载', ['class' => 'btn btn-outline-primary single-download-btn']) ?>
|
||||||
|
<?= Html::button('删除', ['class' => 'btn btn-outline-danger batch-delete-btn']) ?>
|
||||||
|
<?= Html::button('刷新', ['class' => 'btn btn-outline-primary refresh-btn']) ?>
|
||||||
|
<div class="dropdown d-inline-block">
|
||||||
|
<button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenuButton"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-arrow-up-from-bracket"></i> 上传文件
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton" style="z-index: 1080;">
|
||||||
|
<li hidden>
|
||||||
|
<input type="file" id="file-input" name="uploadFile" multiple>
|
||||||
|
<input type="file" id="folder-input" name="uploadFile" multiple webkitdirectory>
|
||||||
|
<input type="hidden" name="targetDir" value="<?= $directory ?>" id="target-dir">
|
||||||
|
</li>
|
||||||
|
<li><?= Html::button('上传文件', ['class' => 'dropdown-item file-upload-btn']) ?></li>
|
||||||
|
<!-- 上传文件功能将会覆盖已存在的同名文件,这点请注意-->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-toggle="popover" data-bs-title="容量使用情况"
|
||||||
|
data-bs-placement="bottom"
|
||||||
|
data-bs-content="已用:<?= $totalUsed_F ?>/ <?= $storageLimit_F ?><?= $freeSpace == 'unlimited' ? '' : ($freeSpace <= 0 ? ' 容量超限,功能受限' : '') ?>">
|
||||||
|
<i
|
||||||
|
class="fa-solid fa-info"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--上传进度条-->
|
||||||
|
<?php
|
||||||
|
echo Progress::widget([
|
||||||
|
'percent' => 0,
|
||||||
|
'barOptions' => ['class' => ['bg-success', 'progress-bar-animated', 'progress-bar-striped']],
|
||||||
|
'label' => '123', //NMD 不是说可选吗
|
||||||
|
'options' => ['style' => 'display: none;margin-top: 10px;', 'id' => 'progress-bar']
|
||||||
|
]);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<?= Html::a('<i class="fa-solid fa-house"></i> HOME', ['vault/index'], ['class' => 'breadcrumb-item']) ?>
|
||||||
|
<?php if ($directory !== null): ?>
|
||||||
|
<?php
|
||||||
|
$parts = explode('/', $directory);
|
||||||
|
$path = '';
|
||||||
|
$lastIndex = count($parts) - 1;
|
||||||
|
foreach ($parts as $index => $part):
|
||||||
|
$path .= $part;
|
||||||
|
$class = $index === $lastIndex ? 'breadcrumb-item active' : 'breadcrumb-item';
|
||||||
|
echo Html::a($part, ['vault/index', 'directory' => $path], ['class' => $class]);
|
||||||
|
$path .= '/';
|
||||||
|
endforeach;
|
||||||
|
?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div class="dropdown" id="contextMenu" style="display: none;position: absolute">
|
||||||
|
<ul class="dropdown-menu shadow" aria-labelledby="dropdownMenuButton" id="contextMenu-content">
|
||||||
|
<li><a class="dropdown-item" id="option-download" href="#">下载</a></li>
|
||||||
|
<li><a class="dropdown-item" id="option-batch-delete" href="#">删除</a></li>
|
||||||
|
<li><a class="dropdown-item" id="option-refresh" href="#">刷新</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<table class="table table-hover" id="drop-area">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="selector-col"><label for="select-all" hidden></label><input type="checkbox"
|
||||||
|
id="select-all"></th>
|
||||||
|
<th scope="col" class="name-col">名称</th>
|
||||||
|
<th scope="col" class="modified-col">最近修改时间</th>
|
||||||
|
<th scope="col" class="size-col">大小</th>
|
||||||
|
<th scope="col" class="action-col">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($directoryContents as $item): ?>
|
||||||
|
<?php $relativePath = $directory ? $directory . '/' . $item['name'] : $item['name']; ?>
|
||||||
|
<?php $absolutePath = Yii::getAlias('@app') . '/data/' . Yii::$app->user->id . '.secret/' . $relativePath; ?>
|
||||||
|
<tr>
|
||||||
|
<td><label for="selector1" hidden></label><input id="selector1" type="checkbox" class="select-item"
|
||||||
|
data-relative-path="<?= Html::encode($relativePath) ?>"
|
||||||
|
data-is-directory="<?= Html::encode(is_dir($absolutePath)) ?>">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?= Html::tag('i', '', ['class' => $item['type'] . ' file_icon']) ?>
|
||||||
|
<?= Html::a($item['name'], ['vault/download', 'relativePath' => $relativePath], ['class' => 'file_name']) ?>
|
||||||
|
</td>
|
||||||
|
<td class="file_info">
|
||||||
|
<?= date('Y-m-d H:i:s', $item['lastModified']) ?>
|
||||||
|
</td>
|
||||||
|
<td class="file_info">
|
||||||
|
<?= $item['size'] !== null ? Yii::$app->formatter->asShortSize($item['size'], 2) : '' ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?= Html::button(Html::tag('i', '', ['class' => 'fa-regular fa-circle-down']), [
|
||||||
|
'value' => Url::to(['vault/download', 'relativePath' => $relativePath]),
|
||||||
|
'class' => 'btn btn-outline-primary download-btn',
|
||||||
|
'data-bs-toggle' => 'tooltip',
|
||||||
|
'data-bs-placement' => 'top',
|
||||||
|
'data-bs-title' => '下载'
|
||||||
|
]) ?>
|
||||||
|
<?= Html::button(Html::tag('i', '', ['class' => 'fa-regular fa-trash-can']), ['value' => $relativePath, 'class' => 'btn btn-outline-danger delete-btn', 'data-bs-toggle' => 'tooltip', 'data-bs-placement' => 'top', 'data-bs-title' => '删除']) ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
Modal::begin([
|
||||||
|
'title' => '<h4>确认删除</h4>',
|
||||||
|
'id' => 'deleteModal',
|
||||||
|
'size' => 'modal-sm',
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo Html::tag('div', '你确定要删除这个文件吗?', ['class' => 'modal-body']);
|
||||||
|
|
||||||
|
echo Html::beginForm(['vault/delete'], 'post', ['id' => 'delete-form']);
|
||||||
|
echo Html::hiddenInput('relativePath', '', ['id' => 'deleteRelativePath']);
|
||||||
|
echo Html::submitButton('确认', ['class' => 'btn btn-danger']);
|
||||||
|
echo Html::endForm();
|
||||||
|
|
||||||
|
Modal::end();
|
||||||
|
$this->registerJsFile('@web/js/vault_script.js', ['depends' => [JqueryAsset::class], 'position' => View::POS_END]);
|
||||||
|
?>
|
||||||
|
|
201
web/js/vault_script.js
Normal file
201
web/js/vault_script.js
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||||
|
});
|
||||||
|
$(document).on('click', '.download-btn', function () {
|
||||||
|
window.location.href = $(this).attr('value');
|
||||||
|
});
|
||||||
|
$(document).on('click', '.delete-btn', function () {
|
||||||
|
var relativePath = $(this).attr('value');
|
||||||
|
$('#deleteRelativePath').val(relativePath);
|
||||||
|
$('#deleteModal').modal('show');
|
||||||
|
});
|
||||||
|
$(document).on('click', '.file-upload-btn', function () {
|
||||||
|
$('#file-input').click();
|
||||||
|
});
|
||||||
|
$('#file-input').on('change', function () {
|
||||||
|
uploadFiles(this.files);
|
||||||
|
});
|
||||||
|
$('#folder-input').on('change', function () {
|
||||||
|
uploadFiles(this.files);
|
||||||
|
});
|
||||||
|
$(document).on('click', '.refresh-btn', function () {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
$(document).on('click', '.single-download-btn', function () {
|
||||||
|
var relativePath = $('.select-item:checked').first().data('relativePath');
|
||||||
|
window.location.href = 'index.php?r=vault%2Fdownload&relativePath=' + encodeURIComponent(relativePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
function uploadFiles(files) {
|
||||||
|
$('#progress-bar').show();
|
||||||
|
var formData = new FormData();
|
||||||
|
for (var i = 0; i < files.length; i++) {
|
||||||
|
formData.append('files[]', files[i]);
|
||||||
|
}
|
||||||
|
formData.append('targetDir', $('#target-dir').val());
|
||||||
|
formData.append('_csrf', $('meta[name="csrf-token"]').attr('content'));
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.upload.onprogress = function (event) {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
var percentComplete = event.loaded / event.total * 100;
|
||||||
|
$('#progress-bar .progress-bar').css('width', percentComplete + '%').text(Math.round(percentComplete) + '%');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.status !== 200) {
|
||||||
|
alert('An error occurred during the upload.');
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
xhr.open('POST', 'index.php?r=vault%2Fupload');
|
||||||
|
xhr.send(formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dropArea = document.getElementById('drop-area');
|
||||||
|
dropArea.addEventListener('dragover', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
dropArea.addEventListener('drop', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var items = event.dataTransfer.items;
|
||||||
|
var files = [];
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
var item = items[i];
|
||||||
|
files.push(item.getAsFile());
|
||||||
|
}
|
||||||
|
uploadFiles(files);
|
||||||
|
dropArea.classList.remove('dragging');
|
||||||
|
});
|
||||||
|
dropArea.addEventListener('dragenter', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
dropArea.classList.add('dragging');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropArea.addEventListener('dragleave', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!dropArea.contains(event.relatedTarget)) {
|
||||||
|
dropArea.classList.remove('dragging');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('select-all').addEventListener('change', function () {
|
||||||
|
var checkboxes = document.querySelectorAll('.select-item');
|
||||||
|
for (var i = 0; i < checkboxes.length; i++) {
|
||||||
|
checkboxes[i].checked = this.checked;
|
||||||
|
checkboxes[i].closest('tr').classList.toggle('selected', this.checked);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var itemCheckboxes = document.querySelectorAll('.select-item');
|
||||||
|
for (var i = 0; i < itemCheckboxes.length; i++) {
|
||||||
|
itemCheckboxes[i].addEventListener('change', function () {
|
||||||
|
if (!this.checked) {
|
||||||
|
document.getElementById('select-all').checked = false;
|
||||||
|
} else {
|
||||||
|
var allChecked = true;
|
||||||
|
for (var j = 0; j < itemCheckboxes.length; j++) {
|
||||||
|
if (!itemCheckboxes[j].checked) {
|
||||||
|
allChecked = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('select-all').checked = allChecked;
|
||||||
|
}
|
||||||
|
this.closest('tr').classList.toggle('selected', this.checked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', function (event) {
|
||||||
|
if (event.ctrlKey && event.key === 'a') {
|
||||||
|
event.preventDefault();
|
||||||
|
var checkboxes = document.querySelectorAll('.select-item');
|
||||||
|
var allChecked = Array.from(checkboxes).every(checkbox => checkbox.checked);
|
||||||
|
for (var i = 0; i < checkboxes.length; i++) {
|
||||||
|
checkboxes[i].checked = !allChecked;
|
||||||
|
checkboxes[i].closest('tr').classList.toggle('selected', !allChecked);
|
||||||
|
}
|
||||||
|
updateButtons();
|
||||||
|
}
|
||||||
|
if (event.ctrlKey && event.key === 'd') {
|
||||||
|
event.preventDefault();
|
||||||
|
$('tr.selected').removeClass('selected').find('input[type="checkbox"]').prop('checked', false);
|
||||||
|
$('#select-all').prop('checked', false);
|
||||||
|
updateButtons();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$(document).on('click', 'tr', function (event) {
|
||||||
|
if ($(event.target).is('input[type="checkbox"]') || $(event.target).is('a')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var checkboxes = document.querySelectorAll('.select-item');
|
||||||
|
var checkedCount = Array.from(checkboxes).filter(checkbox => checkbox.checked).length;
|
||||||
|
var allChecked = checkedCount >= 2;
|
||||||
|
if (!event.ctrlKey) {
|
||||||
|
$('tr.selected').not(this).removeClass('selected').find('input[type="checkbox"]').prop('checked', false);
|
||||||
|
}
|
||||||
|
if ((!allChecked) || (allChecked && event.ctrlKey)) {
|
||||||
|
$(this).toggleClass('selected');
|
||||||
|
var checkbox = $(this).children(':first-child').find('input[type="checkbox"]');
|
||||||
|
checkbox.prop('checked', !checkbox.prop('checked'));
|
||||||
|
}
|
||||||
|
updateButtons();
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateButtons() {
|
||||||
|
var checkboxes = $('.select-item:checked');
|
||||||
|
var count = checkboxes.length;
|
||||||
|
var isSingleFile = count === 1 && !checkboxes.first().data('isDirectory');
|
||||||
|
$('.single-download-btn').toggle(isSingleFile);
|
||||||
|
$('.batch-delete-btn').toggle(count >= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).on('change', '.select-item', updateButtons);
|
||||||
|
$(document).ready(function () {
|
||||||
|
updateButtons();
|
||||||
|
$('tr').contextmenu(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if ($(e.target).is('button') || $(e.target).is('i') || $(e.target).is('a')) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var clickedElement = $(this);
|
||||||
|
if (!clickedElement.hasClass('selected')) {
|
||||||
|
$('tr.selected').removeClass('selected').find('input[type="checkbox"]').prop('checked', false);
|
||||||
|
clickedElement.addClass('selected');
|
||||||
|
var checkbox = clickedElement.children(':first-child').find('input[type="checkbox"]');
|
||||||
|
checkbox.prop('checked', true);
|
||||||
|
updateButtons();
|
||||||
|
}
|
||||||
|
$('#option-download').toggle($('.single-download-btn').css('display') !== 'none');
|
||||||
|
$('#option-batch-delete').toggle($('.batch-delete-btn').css('display') !== 'none');
|
||||||
|
$('#option-refresh').toggle($('.refresh-btn').css('display') !== 'none');
|
||||||
|
$('#contextMenu').css({
|
||||||
|
display: "block",
|
||||||
|
left: e.pageX,
|
||||||
|
top: e.pageY
|
||||||
|
}).addClass('show');
|
||||||
|
$('#contextMenu .dropdown-menu').addClass('show');
|
||||||
|
});
|
||||||
|
$('#contextMenu a').off('click').on('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var clickedMenuItem = $(this).attr('id');
|
||||||
|
switch (clickedMenuItem) {
|
||||||
|
case 'option-download':
|
||||||
|
$('.single-download-btn').click();
|
||||||
|
break;
|
||||||
|
case 'option-batch-delete':
|
||||||
|
$('.batch-delete-btn').click();
|
||||||
|
break;
|
||||||
|
case 'option-refresh':
|
||||||
|
$('.refresh-btn').click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$('#contextMenu').hide().removeClass('show');
|
||||||
|
});
|
||||||
|
$(document).click(function () {
|
||||||
|
$('#contextMenu').hide().removeClass('show');
|
||||||
|
$('#contextMenu .dropdown-menu').removeClass('show');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]')
|
||||||
|
const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl))
|
Loading…
Reference in New Issue
Block a user