feat(file): 文件页拆分「我的/共享」两个板块并合并工具栏为单行

- 顶部 Tab 切换:我的文件(私有 share=0)/ 共享文件(双向,含我共享出去的 + 别人共享给我的),共享板块支持「全部/我共享的/共享给我的」二次筛选
- 后端 File::getFileList 增加 scope 参数,按板块拆分根目录查询;FileController::lists 透传并校验
- 前端用 board/sharedSrc 取代原「仅显示我的」复选框,board 持久化到 IDB
- Tab、子筛选、视图切换合并到同一行(flex 自动换行,视图切换靠最右)
- 根目录不再重复显示板块标题,仅进入子文件夹时显示面包屑(板块名为可点击回根入口)
- 同步更新 ai-kb 文件入口说明
This commit is contained in:
kuaifan 2026-07-14 03:49:59 +00:00
parent 9b14120055
commit 7b25f243ab
7 changed files with 289 additions and 140 deletions

View File

@ -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));
}
/**

View File

@ -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=0all 含全部我的)
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);
}
}
}

View File

@ -2527,3 +2527,7 @@ AI任务分析
确定授权
当前任务所属项目已被删除
当前任务所属项目已归档
我的文件
共享文件
我共享的
共享给我的

View File

@ -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
## 权限要求
- 所有登录用户可见

View File

@ -36,10 +36,27 @@
</div>
</div>
<div class="file-navigator">
<div class="file-tabs">
<div class="file-tabs-nav">
<div :class="['file-tab', {active: board === 'mine'}]" @click="switchBoard('mine')">{{$L('我的文件')}}</div>
<div :class="['file-tab', {active: board === 'shared'}]" @click="switchBoard('shared')">{{$L('共享文件')}}</div>
</div>
<div v-if="board === 'shared' && pid == 0 && !searchKey" class="file-shared-src">
<span :class="{on: sharedSrc === 'all'}" @click="sharedSrc = 'all'">{{$L('全部')}}</span>
<span :class="{on: sharedSrc === 'byme'}" @click="sharedSrc = 'byme'">{{$L('我共享的')}}</span>
<span :class="{on: sharedSrc === 'tome'}" @click="sharedSrc = 'tome'">{{$L('共享给我的')}}</span>
</div>
<div class="file-tabs-full"></div>
<div :class="['switch-button', tableMode]">
<div @click="tableMode='block'"><i class="taskfont">&#xe60c;</i></div>
<div @click="tableMode='table'"><i class="taskfont">&#xe66a;</i></div>
</div>
</div>
<div v-show="showNavigator" class="file-navigator">
<ul class="scrollbar-hidden" v-show="showBtnText || (!selectedItems.length && !shearFirst)">
<li @click="browseFolder(0)">
<span>{{$L('全部文件')}}</span>
<li v-if="pid > 0 || searchKey" @click="browseFolder(0)">
<span>{{board === 'shared' ? $L('共享文件') : $L('我的文件')}}</span>
</li>
<li v-if="searchKey">{{$L('搜索')}} "{{searchKey}}"</li>
<template v-else>
@ -85,15 +102,6 @@
</template>
<div v-if="loadIng > 0" class="nav-load"><Loading/></div>
<div class="flex-full"></div>
<div v-if="hasShareFile" class="only-checkbox">
<Checkbox v-model="hideShared">
{{showBtnText ? $L('仅显示我的') : $L('仅我的')}}
</Checkbox>
</div>
<div :class="['switch-button', tableMode]">
<div @click="tableMode='block'"><i class="taskfont">&#xe60c;</i></div>
<div @click="tableMode='table'"><i class="taskfont">&#xe66a;</i></div>
</div>
</div>
<div
@ -496,7 +504,7 @@ import {chunkedUpload, CHUNK_THRESHOLD} from "../../store/chunkedUpload";
const FilePreview = () => 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);

View File

@ -1491,13 +1491,13 @@ export default {
* @param pid
* @returns {Promise<unknown>}
*/
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)));

View File

@ -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 {