From 7b25f243ab55e739c8c8561b6a43cd90eb22502b Mon Sep 17 00:00:00 2001 From: kuaifan Date: Tue, 14 Jul 2026 03:49:59 +0000 Subject: [PATCH] =?UTF-8?q?feat(file):=20=E6=96=87=E4=BB=B6=E9=A1=B5?= =?UTF-8?q?=E6=8B=86=E5=88=86=E3=80=8C=E6=88=91=E7=9A=84/=E5=85=B1?= =?UTF-8?q?=E4=BA=AB=E3=80=8D=E4=B8=A4=E4=B8=AA=E6=9D=BF=E5=9D=97=E5=B9=B6?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E5=B7=A5=E5=85=B7=E6=A0=8F=E4=B8=BA=E5=8D=95?= =?UTF-8?q?=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 顶部 Tab 切换:我的文件(私有 share=0)/ 共享文件(双向,含我共享出去的 + 别人共享给我的),共享板块支持「全部/我共享的/共享给我的」二次筛选 - 后端 File::getFileList 增加 scope 参数,按板块拆分根目录查询;FileController::lists 透传并校验 - 前端用 board/sharedSrc 取代原「仅显示我的」复选框,board 持久化到 IDB - Tab、子筛选、视图切换合并到同一行(flex 自动换行,视图切换靠最右) - 根目录不再重复显示板块标题,仅进入子文件夹时显示面包屑(板块名为可点击回根入口) - 同步更新 ai-kb 文件入口说明 --- app/Http/Controllers/Api/FileController.php | 7 +- app/Models/File.php | 101 ++++++---- language/original-web.txt | 4 + resources/ai-kb/zh/menu-map/file/entry.md | 9 +- resources/assets/js/pages/manage/file.vue | 100 +++++++--- resources/assets/js/store/actions.js | 8 +- resources/assets/sass/pages/page-file.scss | 200 +++++++++++++------- 7 files changed, 289 insertions(+), 140 deletions(-) diff --git a/app/Http/Controllers/Api/FileController.php b/app/Http/Controllers/Api/FileController.php index d6a009003..40de674e0 100755 --- a/app/Http/Controllers/Api/FileController.php +++ b/app/Http/Controllers/Api/FileController.php @@ -41,6 +41,7 @@ class FileController extends AbstractController * @apiName lists * * @apiParam {Number} [pid] 父级ID + * @apiParam {String} [scope] 板块范围(根目录生效):mine=我的文件、shared=共享文件、all=全部(默认) * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -51,8 +52,12 @@ class FileController extends AbstractController $user = User::auth(); // $pid = intval(Request::input('pid')); + $scope = Request::input('scope', 'all'); + if (!in_array($scope, ['mine', 'shared', 'all'])) { + $scope = 'all'; + } // - return Base::retSuccess('success', (new File)->getFileList($user, $pid)); + return Base::retSuccess('success', (new File)->getFileList($user, $pid, 'all', true, $scope)); } /** diff --git a/app/Models/File.php b/app/Models/File.php index 9879b11f6..2c782f07d 100644 --- a/app/Models/File.php +++ b/app/Models/File.php @@ -178,28 +178,26 @@ class File extends AbstractModel * @param int $pid * @param string $type * @param bool $isGetparent + * @param string $scope 板块范围(根目录生效):mine=仅我的私有文件;shared=共享文件(别人共享给我的+我共享出去的);all=全部(默认,兼容旧调用) * @return array */ - public function getFileList($user, int $pid, $type = "all", $isGetparent = true) + public function getFileList($user, int $pid, $type = "all", $isGetparent = true, $scope = "all") { $permission = 1000; $userids = $user->isTemp() ? [$user->userid] : [0, $user->userid]; - $builder = File::wherePid($pid) - ->when($type == 'dir', function ($q) { - $q->whereType('folder'); - }); + // if ($pid > 0) { + // 目录内:按权限返回子级(不区分板块) File::permissionFind($pid, $userids, 0, $permission); - } else { - $builder->whereUserid($user->userid); - } - // - $array = $builder->take(500)->get()->toArray(); - foreach ($array as &$item) { - $item['permission'] = $permission; - } - // - if ($pid > 0) { + $array = File::wherePid($pid) + ->when($type == 'dir', function ($q) { + $q->whereType('folder'); + }) + ->take(500)->get()->toArray(); + foreach ($array as &$item) { + $item['permission'] = $permission; + } + unset($item); // 遍历获取父级 if ($isGetparent) { while ($pid > 0) { @@ -230,24 +228,61 @@ class File extends AbstractModel $array = array_values($array); } } else { - // 获取共享相关 - DB::statement("SET SQL_MODE=''"); - $pre = DB::connection()->getTablePrefix(); - $list = File::select(["files.*", DB::raw("MAX({$pre}file_users.permission) as permission")]) - ->join('file_users', 'files.id', '=', 'file_users.file_id') - ->where('files.userid', '!=', $user->userid) - ->whereIn('file_users.userid', $userids) - ->groupBy('files.id') - ->take(100) - ->when($type == 'dir', function ($q) { - $q->where('files.type', 'folder'); - }) - ->get(); - if ($list->isNotEmpty()) { - foreach ($list as $file) { - $temp = $file->toArray(); - $temp['pid'] = 0; - $array[] = $temp; + // 根目录:按板块拆分 + $array = []; + // 我的文件(mine 仅私有 share=0;all 含全部我的) + if ($scope === 'mine' || $scope === 'all') { + $mine = File::wherePid(0) + ->whereUserid($user->userid) + ->when($scope === 'mine', function ($q) { + $q->where('share', 0); + }) + ->when($type == 'dir', function ($q) { + $q->whereType('folder'); + }) + ->take(500)->get()->toArray(); + foreach ($mine as &$item) { + $item['permission'] = $permission; + } + unset($item); + $array = array_merge($array, $mine); + } + // 共享文件 + if ($scope === 'shared' || $scope === 'all') { + // 别人共享给我的 + DB::statement("SET SQL_MODE=''"); + $pre = DB::connection()->getTablePrefix(); + $list = File::select(["files.*", DB::raw("MAX({$pre}file_users.permission) as permission")]) + ->join('file_users', 'files.id', '=', 'file_users.file_id') + ->where('files.userid', '!=', $user->userid) + ->whereIn('file_users.userid', $userids) + ->groupBy('files.id') + ->take(100) + ->when($type == 'dir', function ($q) { + $q->where('files.type', 'folder'); + }) + ->get(); + if ($list->isNotEmpty()) { + foreach ($list as $file) { + $temp = $file->toArray(); + $temp['pid'] = 0; + $array[] = $temp; + } + } + // 我共享出去的(仅 shared 板块补充;all 板块已包含在“我的文件”里) + if ($scope === 'shared') { + $mineShared = File::wherePid(0) + ->whereUserid($user->userid) + ->where('share', 1) + ->when($type == 'dir', function ($q) { + $q->whereType('folder'); + }) + ->take(500)->get()->toArray(); + foreach ($mineShared as &$item) { + $item['permission'] = $permission; + } + unset($item); + $array = array_merge($array, $mineShared); } } } diff --git a/language/original-web.txt b/language/original-web.txt index 9022a4d44..7d4e8e7d6 100644 --- a/language/original-web.txt +++ b/language/original-web.txt @@ -2527,3 +2527,7 @@ AI任务分析 确定授权 当前任务所属项目已被删除 当前任务所属项目已归档 +我的文件 +共享文件 +我共享的 +共享给我的 diff --git a/resources/ai-kb/zh/menu-map/file/entry.md b/resources/ai-kb/zh/menu-map/file/entry.md index 4ecf0ce73..829a41e4d 100644 --- a/resources/ai-kb/zh/menu-map/file/entry.md +++ b/resources/ai-kb/zh/menu-map/file/entry.md @@ -17,7 +17,7 @@ prerequisites: [] negative: - 文件入口不在「应用」二级页面,是左侧栏一级菜单 - 移动端目前没有专门的「文件」Tabbar,需要从「更多」进 -last_verified: v1.7.90 +last_verified: v1.8.64 --- # 文件入口在哪 @@ -30,9 +30,12 @@ DooTask 的「文件」是一级导航,相当于个人网盘 + 团队共享盘 - 移动端:底部 Tabbar「更多」→「文件」 ## 默认视图 -- 顶部面包屑显示当前路径(根目录 / 文件夹层级) +- 顶部一行是板块 Tab:「我的文件」/「共享文件」,同一行最右是视图切换(宫格 / 列表),窄屏自动换行 +- 「我的文件」:自己创建的私有文件(未共享) +- 「共享文件」:双向共享——包含「我共享出去的」和「别人共享给我的」,可用「全部 / 我共享的 / 共享给我的」二次筛选 +- 进入子文件夹后,Tab 下方出现面包屑路径(以板块名为起点,可点击回到板块根目录);根目录不显示重复标题 - 主区域列出当前文件夹下的文件与文件夹(最多 500 条,超出滚动加载) -- 自己创建的在「我的文件」段,他人共享给我的在「共享」段 +- 板块选择会记住上次停留的 Tab ## 权限要求 - 所有登录用户可见 diff --git a/resources/assets/js/pages/manage/file.vue b/resources/assets/js/pages/manage/file.vue index 8870e2717..491235b6b 100644 --- a/resources/assets/js/pages/manage/file.vue +++ b/resources/assets/js/pages/manage/file.vue @@ -36,10 +36,27 @@ -
+
+
+
{{$L('我的文件')}}
+
{{$L('共享文件')}}
+
+
+ {{$L('全部')}} + {{$L('我共享的')}} + {{$L('共享给我的')}} +
+
+
+
+
+
+
+ +
    -
  • - {{$L('全部文件')}} +
  • + {{board === 'shared' ? $L('共享文件') : $L('我的文件')}}
  • {{$L('搜索')}} "{{searchKey}}"
  • -
    - - {{showBtnText ? $L('仅显示我的') : $L('仅我的')}} - -
    -
    -
    -
    -
import('./components/FilePreview'); const FileContent = () => import('./components/FileContent'); -const FileObject = {sort: null, mode: null, shared: null}; +const FileObject = {sort: null, mode: null, board: null}; export default { components: {Forwarder, UserAvatarTip, UserSelect, FilePreview, DrawerOverlay, FileContent}, @@ -563,7 +571,8 @@ export default { ], tableMode: "", - hideShared: false, + board: "mine", // 当前板块:mine=我的文件、shared=共享文件 + sharedSrc: "all", // 共享板块二次筛选:all=全部、byme=我共享的、tome=共享给我的 columns: [], shareShow: false, @@ -623,14 +632,14 @@ export default { async beforeRouteEnter(to, from, next) { FileObject.sort = await $A.IDBJson("cacheFileSort") FileObject.mode = await $A.IDBString("fileTableMode") - FileObject.shared = await $A.IDBBoolean("fileHideShared") + FileObject.board = await $A.IDBString("fileBoard") next() }, created() { this.tableMode = FileObject.mode - this.hideShared = FileObject.shared + this.board = FileObject.board === 'shared' ? 'shared' : 'mine' this.columns = [ { type: 'selection', @@ -879,15 +888,32 @@ export default { }, fileList() { - const {fileLists, searchKey, hideShared, pid, selectedItems, userId} = this; + const {fileLists, searchKey, board, sharedSrc, pid, selectedItems, userId} = this; const list = $A.cloneJSON(sortBy(fileLists.filter(file => { - if (hideShared && file.userid != userId && file.created_id != userId) { - return false - } if (searchKey) { return file.name.indexOf(searchKey) !== -1; } - return file.pid == pid; + if (file.pid != pid) { + return false; + } + // 根目录按板块区分:我的=我的私有文件(share=0),共享=其余(双向共享) + if (pid == 0) { + const isMinePrivate = file.userid == userId && !file.share; + if (board === 'mine') { + return isMinePrivate; + } + if (isMinePrivate) { + return false; + } + // 共享板块二次筛选:我共享的(我拥有) / 共享给我的(他人拥有) + if (sharedSrc === 'byme') { + return file.userid == userId; + } + if (sharedSrc === 'tome') { + return file.userid != userId; + } + } + return true; }), file => { return (file.type == 'folder' ? 'a' : 'b') + file.name; })); @@ -906,11 +932,6 @@ export default { }) }, - hasShareFile() { - const {fileLists, userId} = this; - return fileLists.findIndex(file => file.share && file.userid != userId) !== -1 - }, - shearFirst() { const {fileLists, shearIds} = this; if (shearIds.length === 0) { @@ -919,6 +940,11 @@ export default { return fileLists.find(item => item.id == shearIds[0]) }, + showNavigator() { + // 仅在进入子目录、搜索、选中或剪切时显示导航行;根目录空闲时隐藏(板块名已由 Tab 标示) + return this.pid > 0 || !!this.searchKey || this.selectedItems.length > 0 || !!this.shearFirst; + }, + navigator() { let {pid, fileLists} = this; let array = []; @@ -1009,8 +1035,8 @@ export default { } }, - hideShared(val) { - $A.IDBSave("fileHideShared", val) + board(val) { + $A.IDBSave("fileBoard", val) }, fileShow(val) { @@ -1147,7 +1173,7 @@ export default { return; } this.loadIng++; - this.$store.dispatch("getFiles", this.pid).then(async () => { + this.$store.dispatch("getFiles", {pid: this.pid, scope: this.board}).then(async () => { this.loadIng--; this.openFileJudge() this.shakeFile(this.$route.params.shakeId); @@ -1478,6 +1504,22 @@ export default { }) }, + switchBoard(board) { + if (this.board === board) { + return; + } + this.board = board; + this.sharedSrc = 'all'; + this.selectedItems = []; + this.clearShear(); + if (this.pid > 0) { + // 退回板块根目录,由 pid 变化触发重载 + this.browseFolder(0); + } else { + this.getFileList(); + } + }, + browseFolder(id, shakeId = null) { if (this.pid == id && this.fid == 0 && shakeId) { this.shakeFile(shakeId); diff --git a/resources/assets/js/store/actions.js b/resources/assets/js/store/actions.js index 7c1709068..716a1862d 100644 --- a/resources/assets/js/store/actions.js +++ b/resources/assets/js/store/actions.js @@ -1491,13 +1491,13 @@ export default { * @param pid * @returns {Promise} */ - getFiles({commit, state, dispatch}, pid) { + getFiles({commit, state, dispatch}, payload) { + const pid = typeof payload === 'object' && payload !== null ? payload.pid : payload; + const scope = typeof payload === 'object' && payload !== null ? payload.scope : undefined; return new Promise(function (resolve, reject) { dispatch("call", { url: 'file/lists', - data: { - pid - }, + data: scope ? {pid, scope} : {pid}, }).then((result) => { const ids = result.data.map(({id}) => id) commit("file/save", state.fileLists.filter((item) => item.pid != pid || ids.includes(item.id))); diff --git a/resources/assets/sass/pages/page-file.scss b/resources/assets/sass/pages/page-file.scss index fcc7458a1..fd9de13fd 100644 --- a/resources/assets/sass/pages/page-file.scss +++ b/resources/assets/sass/pages/page-file.scss @@ -82,6 +82,121 @@ } } } + .file-tabs { + display: flex; + align-items: center; + flex-wrap: wrap; + margin: 0 32px 4px; + gap: 8px 20px; + .file-tabs-nav { + display: flex; + align-items: flex-end; + gap: 24px; + .file-tab { + position: relative; + padding: 4px 2px 8px; + font-size: 15px; + font-weight: 500; + color: $primary-text-color; + cursor: pointer; + transition: color 0.2s; + &:hover { + color: $primary-title-color; + } + &.active { + color: $primary-title-color; + &:after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 2px; + border-radius: 2px; + background-color: $primary-color; + } + } + } + } + .file-shared-src { + display: flex; + align-items: center; + gap: 8px; + > span { + font-size: 12.5px; + color: $primary-text-color; + padding: 3px 12px; + border-radius: 14px; + border: 1px solid transparent; + background-color: rgba(0, 0, 0, 0.03); + cursor: pointer; + transition: all 0.2s; + &.on { + color: $primary-color; + border-color: rgba($primary-color, 0.5); + background-color: rgba($primary-color, 0.08); + } + } + } + .file-tabs-full { + flex: 1; + min-width: 12px; + } + .switch-button { + flex-shrink: 0; + display: flex; + align-items: center; + background-color: #ffffff; + border-radius: 6px; + position: relative; + transition: box-shadow 0.2s; + &:hover { + box-shadow: 0 0 10px #e6ecfa; + } + &:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 50%; + height: 100%; + z-index: 0; + color: $primary-color; + border-radius: 6px; + border: 1px solid $primary-color; + background-color: rgba($primary-color, 0.1); + transition: left 0.2s; + } + > div { + z-index: 1; + width: 32px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + cursor: pointer; + color: $primary-text-color; + > i { + font-size: 17px; + } + &:first-child { + color: $primary-color; + } + } + &.table { + &:before { + left: 50%; + } + > div:first-child { + color: $primary-text-color; + } + > div:last-child { + color: $primary-color; + } + } + } + } .file-navigator { display: flex; align-items: center; @@ -183,72 +298,6 @@ } .flex-full { flex: 1; - margin-right: 24px; - } - .only-checkbox { - flex-shrink: 0; - display: flex; - align-items: center; - margin-right: 14px; - opacity: 0.9; - height: 30px; - .ivu-checkbox-focus { - box-shadow: none; - } - } - .switch-button { - flex-shrink: 0; - display: flex; - align-items: center; - background-color: #ffffff; - border-radius: 6px; - position: relative; - transition: box-shadow 0.2s; - &:hover { - box-shadow: 0 0 10px #e6ecfa; - } - &:before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 50%; - height: 100%; - z-index: 0; - color: $primary-color; - border-radius: 6px; - border: 1px solid $primary-color; - background-color: rgba($primary-color, 0.1); - transition: left 0.2s; - } - > div { - z-index: 1; - width: 32px; - height: 30px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 6px; - cursor: pointer; - color: $primary-text-color; - > i { - font-size: 17px; - } - &:first-child { - color: $primary-color; - } - } - &.table { - &:before { - left: 50%; - } - > div:first-child { - color: $primary-text-color; - } - > div:last-child { - color: $primary-color; - } - } } } .file-drag { @@ -747,14 +796,25 @@ body.window-portrait { .file-head { margin: 24px 16px 16px; } + .file-tabs { + margin: 0 16px 6px; + flex-wrap: wrap; + gap: 10px 16px; + .file-tabs-nav { + gap: 20px; + } + .file-shared-src { + flex-wrap: wrap; + > span { + padding: 2px 10px; + } + } + } .file-navigator { - margin: 0 24px 0; + margin: 0 16px 0; .flex-full { margin-right: 10px; } - .only-checkbox { - margin-right: 0; - } } .file-drag { .file-list {