5 Commits

Author SHA1 Message Date
237899745
d0535c7208 fix: keep mobile filters compact and visible
Some checks failed
Build and tests / test (push) Has been cancelled
2026-08-03 23:21:22 +08:00
237899745
c92ad8b096 feat: prioritize mobile workspace content
Some checks failed
Build and tests / test (push) Has been cancelled
2026-08-03 23:07:07 +08:00
237899745
e809de892d fix: preserve workspace cascade in production build
Some checks failed
Build and tests / test (push) Has been cancelled
2026-08-03 22:47:15 +08:00
237899745
8095b2391e fix: unify compact navigation breakpoint
Some checks failed
Build and tests / test (push) Has been cancelled
2026-08-03 22:25:55 +08:00
237899745
c529e6c51b feat: streamline mobile file workspace 2026-08-03 22:21:09 +08:00
5 changed files with 1769 additions and 66 deletions

View File

@@ -1802,7 +1802,7 @@
</div> </div>
<!-- 导航栏 --> <!-- 导航栏 -->
<div class="navbar" v-if="isLoggedIn"> <nav class="navbar" v-if="isLoggedIn" aria-label="主导航">
<div class="navbar-brand"> <div class="navbar-brand">
<span class="brand-mark"><i class="fas fa-cloud"></i></span> <span class="brand-mark"><i class="fas fa-cloud"></i></span>
<span class="brand-copy"> <span class="brand-copy">
@@ -1812,19 +1812,19 @@
</div> </div>
<div class="navbar-menu"> <div class="navbar-menu">
<div class="nav-section-label">工作台</div> <div class="nav-section-label">工作台</div>
<div v-if="user && !user.is_admin" class="nav-item" :class="{active: currentView === 'files'}" @click="switchView('files')"> <button v-if="user && !user.is_admin" type="button" class="nav-item" :class="{active: currentView === 'files'}" :aria-current="currentView === 'files' ? 'page' : null" @click="switchView('files')">
<i class="fas fa-folder"></i> 我的文件 <i class="fas fa-folder"></i><span class="nav-item-label">文件</span>
</div> </button>
<div v-if="user && !user.is_admin" class="nav-item" :class="{active: currentView === 'shares'}" @click="switchView('shares')"> <button v-if="user && !user.is_admin" type="button" class="nav-item" :class="{active: currentView === 'shares'}" :aria-current="currentView === 'shares' ? 'page' : null" @click="switchView('shares')">
<i class="fas fa-share-alt"></i> 我的分享 <i class="fas fa-share-alt"></i><span class="nav-item-label">分享</span>
</div> </button>
<div v-if="user && user.is_admin" class="nav-item" :class="{active: currentView === 'admin'}" @click="switchView('admin')"> <button v-if="user && user.is_admin" type="button" class="nav-item" :class="{active: currentView === 'admin'}" :aria-current="currentView === 'admin' ? 'page' : null" @click="switchView('admin')">
<i class="fas fa-user-shield"></i> 管理员 <i class="fas fa-user-shield"></i><span class="nav-item-label">管理</span>
</div> </button>
<div class="nav-section-label nav-service-label">服务</div> <div class="nav-section-label nav-service-label">服务</div>
<div class="nav-item" :class="{active: currentView === 'settings'}" @click="switchView('settings')"> <button type="button" class="nav-item" :class="{active: currentView === 'settings'}" :aria-current="currentView === 'settings' ? 'page' : null" @click="switchView('settings')">
<i class="fas fa-cog"></i> 设置 <i class="fas fa-cog"></i><span class="nav-item-label">设置</span>
</div> </button>
<button class="btn btn-secondary navbar-download-btn" :disabled="desktopClientDownloading" @click="downloadDesktopClient"> <button class="btn btn-secondary navbar-download-btn" :disabled="desktopClientDownloading" @click="downloadDesktopClient">
<i :class="desktopClientDownloading ? 'fas fa-spinner fa-spin' : 'fas fa-download'"></i> <i :class="desktopClientDownloading ? 'fas fa-spinner fa-spin' : 'fas fa-download'"></i>
{{ desktopClientDownloading ? '准备中...' : '下载客户端' }} {{ desktopClientDownloading ? '准备中...' : '下载客户端' }}
@@ -1837,15 +1837,15 @@
<strong>{{ user.username }}</strong> <strong>{{ user.username }}</strong>
</span> </span>
</div> </div>
<button class="btn btn-danger navbar-logout-btn" @click="logout" title="退出登录"> <button class="btn btn-danger navbar-logout-btn" @click="logout" title="退出登录" aria-label="退出登录">
<i class="fas fa-power-off"></i> 退出 <i class="fas fa-power-off"></i><span class="nav-item-label">退出</span>
</button> </button>
</div> </div>
</div> </div>
</div> </nav>
<!-- 文件视图 --> <!-- 文件视图 -->
<div v-if="isLoggedIn && currentView === 'files'" class="main-container"> <div v-if="isLoggedIn && currentView === 'files'" class="main-container files-main-container">
<div class="card files-view-card"> <div class="card files-view-card">
<header class="workspace-page-header files-page-header"> <header class="workspace-page-header files-page-header">
@@ -1909,19 +1909,22 @@
<!-- 文件列表 --> <!-- 文件列表 -->
<div v-else class="files-content-shell"> <div v-else class="files-content-shell">
<div class="files-command-bar"> <div class="files-command-bar">
<div class="files-command-primary"> <div class="files-command-primary files-command-primary-desktop">
<button class="btn btn-primary" @click="$refs.fileUploadInput.click()"> <button class="btn btn-primary" @click="$refs.fileUploadInput.click()">
<i class="fas fa-upload"></i> 上传文件 <i class="fas fa-upload"></i><span class="btn-label">上传文件</span>
</button> </button>
<button class="btn btn-secondary" @click="showCreateFolderModal = true"> <button class="btn btn-secondary" @click="showCreateFolderModal = true">
<i class="fas fa-folder-plus"></i> 新建文件夹 <i class="fas fa-folder-plus"></i><span class="btn-label">新建文件夹</span>
</button> </button>
</div> </div>
<button type="button" class="btn btn-primary mobile-file-create-trigger" @click="openMobileCreateSheet($event)" aria-label="新建或上传">
<i class="fas fa-plus"></i><span>新建</span>
</button>
<div class="view-toggle-group files-view-toggle" aria-label="文件视图切换"> <div class="view-toggle-group files-view-toggle" aria-label="文件视图切换">
<button class="btn" :class="fileViewMode === 'grid' ? 'btn-primary' : 'btn-secondary'" @click="fileViewMode = 'grid'" title="大图标视图"> <button class="btn" :class="fileViewMode === 'grid' ? 'btn-primary' : 'btn-secondary'" @click="setFileViewMode('grid')" :aria-pressed="fileViewMode === 'grid'" aria-label="大图标视图" title="大图标视图">
<i class="fas fa-th-large"></i><span>大图标</span> <i class="fas fa-th-large"></i><span>大图标</span>
</button> </button>
<button class="btn" :class="fileViewMode === 'list' ? 'btn-primary' : 'btn-secondary'" @click="fileViewMode = 'list'" title="列表视图"> <button class="btn" :class="fileViewMode === 'list' ? 'btn-primary' : 'btn-secondary'" @click="setFileViewMode('list')" :aria-pressed="fileViewMode === 'list'" aria-label="列表视图" title="列表视图">
<i class="fas fa-list"></i><span>列表</span> <i class="fas fa-list"></i><span>列表</span>
</button> </button>
</div> </div>
@@ -1932,14 +1935,15 @@
<div class="files-search-field"> <div class="files-search-field">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
<input <input
type="text" type="search"
class="form-input" class="form-input"
v-model="globalSearchKeyword" v-model="globalSearchKeyword"
@input="triggerGlobalSearch" @input="triggerGlobalSearch"
@focus="globalSearchVisible = !!globalSearchKeyword" @focus="globalSearchVisible = !!globalSearchKeyword"
placeholder="搜索全部目录中的文件或文件夹"> aria-label="搜索文件或文件夹"
placeholder="搜索文件或文件夹">
</div> </div>
<select class="form-input files-search-type" v-model="globalSearchType" @change="runGlobalSearch"> <select class="form-input files-search-type" v-model="globalSearchType" @change="runGlobalSearch" aria-label="搜索类型">
<option value="all">全部</option> <option value="all">全部</option>
<option value="file">仅文件</option> <option value="file">仅文件</option>
<option value="directory">仅文件夹</option> <option value="directory">仅文件夹</option>
@@ -2114,6 +2118,7 @@
@touchstart.stop @touchstart.stop
@touchmove.stop @touchmove.stop
@touchend.stop @touchend.stop
aria-label="更多操作"
title="更多操作"> title="更多操作">
<i class="fas fa-ellipsis-h"></i> <i class="fas fa-ellipsis-h"></i>
</button> </button>
@@ -2510,7 +2515,7 @@
</div> </div>
<!-- 设置视图 --> <!-- 设置视图 -->
<div v-if="isLoggedIn && currentView === 'settings'" class="main-container"> <div v-if="isLoggedIn && currentView === 'settings'" class="main-container settings-main-container">
<div class="card settings-page-card"> <div class="card settings-page-card">
<div class="settings-page-head"> <div class="settings-page-head">
<h2 class="settings-page-title"> <h2 class="settings-page-title">
@@ -3120,7 +3125,7 @@
</div> </div>
<!-- 分享视图 --> <!-- 分享视图 -->
<div v-if="isLoggedIn && currentView === 'shares'" class="main-container"> <div v-if="isLoggedIn && currentView === 'shares'" class="main-container shares-main-container">
<div class="card shares-page-card"> <div class="card shares-page-card">
<header class="workspace-page-header shares-page-head"> <header class="workspace-page-header shares-page-head">
<div class="workspace-page-heading"> <div class="workspace-page-heading">
@@ -3129,21 +3134,21 @@
<p>集中查看分享状态、访问数据和文件直链。</p> <p>集中查看分享状态、访问数据和文件直链。</p>
</div> </div>
<div class="shares-page-actions"> <div class="shares-page-actions">
<button class="btn btn-secondary" @click="refreshShareResources"> <button type="button" class="btn btn-secondary share-refresh-btn" @click="refreshShareResources" aria-label="刷新分享资源">
<i class="fas fa-sync-alt"></i> 刷新 <i class="fas fa-sync-alt"></i><span class="share-action-label">刷新</span>
</button> </button>
<button class="btn btn-primary" @click="switchView('files')"> <button type="button" class="btn btn-primary share-create-btn" @click="switchView('files')" aria-label="创建分享" title="创建分享">
<i class="fas fa-plus"></i> 创建分享 <i class="fas fa-plus"></i><span class="share-action-label">创建分享</span>
</button> </button>
</div> </div>
</header> </header>
<nav class="share-resource-tabs" aria-label="分享资源分类"> <nav class="share-resource-tabs" aria-label="分享资源分类">
<button :class="{ active: shareResourceTab === 'shares' }" @click="shareResourceTab = 'shares'"> <button type="button" :class="{ active: shareResourceTab === 'shares' }" @click="shareResourceTab = 'shares'">
<i class="fas fa-share-alt"></i> 分享链接 <i class="fas fa-share-alt"></i> 分享链接
<span>{{ shares.length }}</span> <span>{{ shares.length }}</span>
</button> </button>
<button :class="{ active: shareResourceTab === 'directLinks' }" @click="shareResourceTab = 'directLinks'"> <button type="button" :class="{ active: shareResourceTab === 'directLinks' }" @click="shareResourceTab = 'directLinks'">
<i class="fas fa-link"></i> 文件直链 <i class="fas fa-link"></i> 文件直链
<span>{{ directLinks.length }}</span> <span>{{ directLinks.length }}</span>
</button> </button>
@@ -3156,12 +3161,12 @@
<span><i class="fas fa-shield-alt"></i> 状态 <strong>运行正常</strong></span> <span><i class="fas fa-shield-alt"></i> 状态 <strong>运行正常</strong></span>
</div> </div>
<div class="share-toolbar shares-toolbar"> <div class="share-toolbar shares-toolbar" :class="{ 'share-toolbar-direct': shareResourceTab === 'directLinks' }">
<div class="share-search-field"> <div class="share-search-field">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
<input type="text" v-model="shareFilters.keyword" :placeholder="shareResourceTab === 'shares' ? '搜索文件名、链接或分享码' : '搜索文件名或直链地址'"> <input type="text" v-model="shareFilters.keyword" :placeholder="shareResourceTab === 'shares' ? '搜索文件名、链接或分享码' : '搜索文件名或直链地址'">
</div> </div>
<template v-if="shareResourceTab === 'shares'"> <div v-if="shareResourceTab === 'shares'" class="share-filter-controls">
<select v-model="shareFilters.type" aria-label="分享类型"> <select v-model="shareFilters.type" aria-label="分享类型">
<option value="all">全部类型</option> <option value="all">全部类型</option>
<option value="file">文件</option> <option value="file">文件</option>
@@ -3183,7 +3188,7 @@
<option value="downloads_desc">下载最多</option> <option value="downloads_desc">下载最多</option>
<option value="expire_asc">最早到期</option> <option value="expire_asc">最早到期</option>
</select> </select>
</template> </div>
</div> </div>
<div v-if="shareResourceTab === 'shares'" class="share-resource-panel"> <div v-if="shareResourceTab === 'shares'" class="share-resource-panel">
@@ -3295,7 +3300,7 @@
</div> </div>
<!-- 管理员视图 --> <!-- 管理员视图 -->
<div v-if="isLoggedIn && currentView === 'admin' && user && user.is_admin" class="main-container"> <div v-if="isLoggedIn && currentView === 'admin' && user && user.is_admin" class="main-container admin-main-container">
<!-- 管理员标签页导航 --> <!-- 管理员标签页导航 -->
<div class="card admin-tabs-card" style="margin-bottom: 20px; padding: 0;"> <div class="card admin-tabs-card" style="margin-bottom: 20px; padding: 0;">
<div class="admin-tabs-nav"> <div class="admin-tabs-nav">
@@ -3315,9 +3320,9 @@
</div> </div>
<!-- ========== 概览标签页 ========== --> <!-- ========== 概览标签页 ========== -->
<div v-show="adminTab === 'overview'"> <div v-show="adminTab === 'overview'" class="admin-overview-page">
<!-- 调试模式开关 --> <!-- 调试模式开关 -->
<div class="card" style="margin-bottom: 30px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;"> <div class="card admin-debug-card" style="margin-bottom: 30px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<div class="admin-debug-row" style="display: flex; align-items: center; justify-content: space-between;"> <div class="admin-debug-row" style="display: flex; align-items: center; justify-content: space-between;">
<div> <div>
<h3 style="margin-bottom: 10px; color: white;"> <h3 style="margin-bottom: 10px; color: white;">
@@ -3335,7 +3340,7 @@
</div> </div>
<!-- 服务器存储统计 --> <!-- 服务器存储统计 -->
<div class="card" style="margin-bottom: 30px;"> <div class="card admin-storage-card" style="margin-bottom: 30px;">
<h3 style="margin-bottom: 20px;"> <h3 style="margin-bottom: 20px;">
<i class="fas fa-hdd"></i> 服务器存储统计 <i class="fas fa-hdd"></i> 服务器存储统计
</h3> </h3>
@@ -4030,12 +4035,23 @@
</div><!-- 监控标签页结束 --> </div><!-- 监控标签页结束 -->
<!-- ========== 用户标签页 ========== --> <!-- ========== 用户标签页 ========== -->
<div v-show="adminTab === 'users'"> <div v-show="adminTab === 'users'" class="admin-users-page">
<div class="card"> <div class="card admin-users-card">
<div class="admin-users-header"> <div class="admin-users-header">
<h3 style="margin: 0;">用户管理</h3> <h3 style="margin: 0;">用户管理</h3>
<button class="btn btn-secondary" @click="loadUsers" :disabled="adminUsersLoading" style="min-width: 92px;"> <button
<i :class="adminUsersLoading ? 'fas fa-spinner fa-spin' : 'fas fa-rotate'"></i> 刷新 type="button"
class="mobile-admin-users-reset-btn"
:disabled="activeAdminUserFilterCount === 0"
@click="resetAdminUserFilters"
aria-label="重置用户筛选"
title="重置筛选"
>
<i class="fas fa-filter-circle-xmark"></i>
</button>
<button type="button" class="btn btn-secondary admin-users-refresh-btn" @click="loadUsers" :disabled="adminUsersLoading" style="min-width: 92px;" aria-label="刷新用户列表">
<i :class="adminUsersLoading ? 'fas fa-spinner fa-spin' : 'fas fa-rotate'"></i>
<span>刷新</span>
</button> </button>
</div> </div>
@@ -4046,16 +4062,16 @@
</div> </div>
<div class="admin-users-filter"> <div class="admin-users-filter">
<label>角色</label> <label>角色</label>
<select v-model="adminUserFilters.role" @change="handleAdminUsersFilterChange"> <select v-model="adminUserFilters.role" @change="handleAdminUsersFilterChange" aria-label="用户角色">
<option value="all">全部</option> <option value="all">全部角色</option>
<option value="admin">管理员</option> <option value="admin">管理员</option>
<option value="user">普通用户</option> <option value="user">普通用户</option>
</select> </select>
</div> </div>
<div class="admin-users-filter"> <div class="admin-users-filter">
<label>状态</label> <label>状态</label>
<select v-model="adminUserFilters.status" @change="handleAdminUsersFilterChange"> <select v-model="adminUserFilters.status" @change="handleAdminUsersFilterChange" aria-label="用户状态">
<option value="all">全部</option> <option value="all">全部状态</option>
<option value="active">正常</option> <option value="active">正常</option>
<option value="banned">已封禁</option> <option value="banned">已封禁</option>
<option value="unverified">未激活</option> <option value="unverified">未激活</option>
@@ -4064,8 +4080,8 @@
</div> </div>
<div class="admin-users-filter"> <div class="admin-users-filter">
<label>存储</label> <label>存储</label>
<select v-model="adminUserFilters.storage" @change="handleAdminUsersFilterChange"> <select v-model="adminUserFilters.storage" @change="handleAdminUsersFilterChange" aria-label="用户存储类型">
<option value="all">全部</option> <option value="all">全部存储</option>
<option value="local">当前本地</option> <option value="local">当前本地</option>
<option value="oss">当前OSS</option> <option value="oss">当前OSS</option>
<option value="local_only">仅本地</option> <option value="local_only">仅本地</option>
@@ -4075,7 +4091,7 @@
</div> </div>
<div class="admin-users-filter"> <div class="admin-users-filter">
<label>排序</label> <label>排序</label>
<select v-model="adminUserFilters.sort" @change="handleAdminUsersFilterChange"> <select v-model="adminUserFilters.sort" @change="handleAdminUsersFilterChange" aria-label="用户排序方式">
<option value="created_desc">注册时间(新到旧)</option> <option value="created_desc">注册时间(新到旧)</option>
<option value="created_asc">注册时间(旧到新)</option> <option value="created_asc">注册时间(旧到新)</option>
<option value="username_asc">用户名A-Z</option> <option value="username_asc">用户名A-Z</option>
@@ -4086,10 +4102,10 @@
</div> </div>
<div class="admin-users-filter admin-users-filter-page-size"> <div class="admin-users-filter admin-users-filter-page-size">
<label>每页</label> <label>每页</label>
<select v-model.number="adminUsersPageSize" @change="handleAdminUsersPageSizeChange"> <select v-model.number="adminUsersPageSize" @change="handleAdminUsersPageSizeChange" aria-label="每页用户数量">
<option :value="20">20</option> <option :value="20">20</option>
<option :value="50">50</option> <option :value="50">50</option>
<option :value="100">100</option> <option :value="100">100</option>
</select> </select>
</div> </div>
<div class="admin-users-filter admin-users-filter-reset"> <div class="admin-users-filter admin-users-filter-reset">
@@ -4474,9 +4490,30 @@
<i class="fas fa-trash"></i> 删除 <i class="fas fa-trash"></i> 删除
</div> </div>
</div> </div>
<!-- 移动端新建面板 -->
<div v-if="showMobileCreateSheet" class="modal-overlay mobile-file-sheet-overlay mobile-create-sheet-overlay" @click="closeMobileCreateSheet">
<section class="mobile-file-sheet mobile-create-sheet" role="dialog" aria-modal="true" aria-labelledby="mobile-create-title" @click.stop>
<div class="mobile-file-sheet-handle"></div>
<div id="mobile-create-title" class="mobile-file-sheet-title mobile-create-sheet-title">新建</div>
<div class="mobile-create-sheet-actions">
<button type="button" class="mobile-create-option" @click="mobileCreateAction('upload')">
<span class="mobile-create-option-icon upload"><i class="fas fa-cloud-upload-alt"></i></span>
<span class="mobile-create-option-copy"><strong>上传文件</strong><small>从手机选择一个或多个文件</small></span>
<i class="fas fa-chevron-right mobile-create-option-arrow"></i>
</button>
<button type="button" class="mobile-create-option" @click="mobileCreateAction('folder')">
<span class="mobile-create-option-icon folder"><i class="fas fa-folder-plus"></i></span>
<span class="mobile-create-option-copy"><strong>新建文件夹</strong><small>创建目录并整理云端文件</small></span>
<i class="fas fa-chevron-right mobile-create-option-arrow"></i>
</button>
</div>
<button type="button" class="mobile-file-sheet-btn cancel mobile-create-cancel" @click="closeMobileCreateSheet">取消</button>
</section>
</div>
<!-- 移动端文件操作面板 --> <!-- 移动端文件操作面板 -->
<div v-if="showMobileFileActionSheet && mobileActionFile" class="modal-overlay mobile-file-sheet-overlay" @click="closeMobileFileActionSheet"> <div v-if="showMobileFileActionSheet && mobileActionFile" class="modal-overlay mobile-file-sheet-overlay" @click="closeMobileFileActionSheet">
<div class="mobile-file-sheet" @click.stop> <section class="mobile-file-sheet" role="dialog" aria-modal="true" aria-label="文件操作" @click.stop>
<div class="mobile-file-sheet-handle"></div> <div class="mobile-file-sheet-handle"></div>
<div class="mobile-file-sheet-title" :title="getFileDisplayName(mobileActionFile)"> <div class="mobile-file-sheet-title" :title="getFileDisplayName(mobileActionFile)">
{{ getFileDisplayName(mobileActionFile) }} {{ getFileDisplayName(mobileActionFile) }}
@@ -4508,7 +4545,7 @@
<i class="fas fa-times"></i> 取消 <i class="fas fa-times"></i> 取消
</button> </button>
</div> </div>
</div> </section>
</div> </div>
<!-- 管理员:编辑用户存储权限模态框 --> <!-- 管理员:编辑用户存储权限模态框 -->
@@ -4915,7 +4952,7 @@
color: var(--text-secondary); color: var(--text-secondary);
} }
@media (min-width: 769px) { @media (min-width: 901px) {
.mobile-file-sheet-overlay { .mobile-file-sheet-overlay {
display: none !important; display: none !important;
} }

View File

@@ -12,6 +12,19 @@ createApp({
return (saved && ['overview', 'settings', 'monitor', 'users'].includes(saved)) ? saved : 'overview'; return (saved && ['overview', 'settings', 'monitor', 'users'].includes(saved)) ? saved : 'overview';
})(); })();
const initialFileViewMode = (() => {
try {
const saved = localStorage.getItem('fileViewMode');
if (saved && ['grid', 'list'].includes(saved)) {
return saved;
}
} catch (error) {
console.warn('[视图] 无法读取文件视图偏好:', error);
}
return window.matchMedia('(max-width: 900px)').matches ? 'list' : 'grid';
})();
return { return {
// API配置 // API配置
// API配置 - 通过nginx代理访问 // API配置 - 通过nginx代理访问
@@ -29,7 +42,7 @@ createApp({
// 视图状态 // 视图状态
currentView: 'files', currentView: 'files',
isLogin: true, isLogin: true,
fileViewMode: 'grid', // 文件显示模式: grid 大图标, list 列表 fileViewMode: initialFileViewMode, // 移动端默认列表视图,并记住用户选择
shareViewMode: 'list', // 分享显示模式: grid 大图标, list 列表 shareViewMode: 'list', // 分享显示模式: grid 大图标, list 列表
shareResourceTab: 'shares', // 分享中心资源标签: shares / directLinks shareResourceTab: 'shares', // 分享中心资源标签: shares / directLinks
settingsTab: 'storage', // 设置中心标签: storage / appearance / security settingsTab: 'storage', // 设置中心标签: storage / appearance / security
@@ -141,7 +154,6 @@ createApp({
status: 'all', // all/active/expiring/expired/protected/public status: 'all', // all/active/expiring/expired/protected/public
sort: 'created_desc' // created_desc/created_asc/views_desc/downloads_desc/expire_asc sort: 'created_desc' // created_desc/created_asc/views_desc/downloads_desc/expire_asc
}, },
// 文件重命名 // 文件重命名
showRenameModal: false, showRenameModal: false,
renameForm: { renameForm: {
@@ -349,6 +361,7 @@ createApp({
contextMenuX: 0, contextMenuX: 0,
contextMenuY: 0, contextMenuY: 0,
contextMenuFile: null, contextMenuFile: null,
showMobileCreateSheet: false,
showMobileFileActionSheet: false, showMobileFileActionSheet: false,
mobileActionFile: null, mobileActionFile: null,
@@ -715,6 +728,17 @@ createApp({
return list; return list;
}, },
activeAdminUserFilterCount() {
let count = 0;
if (String(this.adminUserFilters.keyword || '').trim()) count += 1;
if (this.adminUserFilters.role !== 'all') count += 1;
if (this.adminUserFilters.status !== 'all') count += 1;
if (this.adminUserFilters.storage !== 'all') count += 1;
if (this.adminUserFilters.sort !== 'created_desc') count += 1;
if (Number(this.adminUsersPageSize) !== 20) count += 1;
return count;
},
adminUsersFilteredCount() { adminUsersFilteredCount() {
return Math.max(0, Number(this.adminUsersTotalCount) || 0); return Math.max(0, Number(this.adminUsersTotalCount) || 0);
}, },
@@ -2237,7 +2261,7 @@ handleDragLeave(e) {
}, },
isMobileViewport() { isMobileViewport() {
return window.matchMedia('(max-width: 768px)').matches; return window.matchMedia('(max-width: 900px)').matches;
}, },
setMobileSheetScrollLock(locked) { setMobileSheetScrollLock(locked) {
@@ -2245,6 +2269,46 @@ handleDragLeave(e) {
document.body.style.overflow = locked ? 'hidden' : ''; document.body.style.overflow = locked ? 'hidden' : '';
}, },
setFileViewMode(mode) {
if (!['grid', 'list'].includes(mode)) return;
this.fileViewMode = mode;
try {
localStorage.setItem('fileViewMode', mode);
} catch (error) {
console.warn('[视图] 无法保存文件视图偏好:', error);
}
},
openMobileCreateSheet(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
this.closeMobileFileActionSheet();
this.showMobileCreateSheet = true;
this.setMobileSheetScrollLock(true);
},
closeMobileCreateSheet() {
this.showMobileCreateSheet = false;
this.setMobileSheetScrollLock(false);
},
mobileCreateAction(action) {
this.closeMobileCreateSheet();
if (action === 'upload') {
this.$nextTick(() => this.$refs.fileUploadInput?.click());
return;
}
if (action === 'folder') {
this.showCreateFolderModal = true;
}
},
openMobileFileActionSheet(file, event) { openMobileFileActionSheet(file, event) {
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
@@ -5176,11 +5240,12 @@ handleDragLeave(e) {
this.isDragging = false; this.isDragging = false;
}); });
// 添加 ESC 键监听(按 ESC 关闭拖拽覆盖层) // 添加 ESC 键监听(按 ESC 关闭当前浮层)
window.addEventListener("keydown", (e) => { window.addEventListener("keydown", (e) => {
if (e.key === "Escape" && this.isDragging) { if (e.key !== "Escape") return;
this.isDragging = false; if (this.isDragging) this.isDragging = false;
} if (this.showMobileCreateSheet) this.closeMobileCreateSheet();
if (this.showMobileFileActionSheet) this.closeMobileFileActionSheet();
}); });
// 设置axios响应拦截器处理401错误token过期/失效) // 设置axios响应拦截器处理401错误token过期/失效)

View File

@@ -1,4 +1,12 @@
import { copyFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs'; import { createHash } from 'node:crypto';
import {
copyFileSync,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
writeFileSync
} from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -32,3 +40,26 @@ for (const entry of ['libs', 'favicon.ico']) {
} }
} }
} }
// Vite hoists linked stylesheets into <head>, ahead of legacy inline styles in
// app.html. Re-emit the workspace layer after those styles so production and
// development use the same cascade order.
const workspaceSourcePath = path.join(frontendRoot, 'workspace.css');
const workspaceContents = readFileSync(workspaceSourcePath);
const workspaceHash = createHash('sha256').update(workspaceContents).digest('hex').slice(0, 12);
const workspaceFileName = `workspace-${workspaceHash}.css`;
copyFileSync(workspaceSourcePath, path.join(outputDirectory, workspaceFileName));
const builtAppPath = path.join(outputDirectory, 'app.html');
const builtAppHtml = readFileSync(builtAppPath, 'utf8');
const runtimeStylesheet = ` <link rel="stylesheet" href="/${workspaceFileName}" data-workspace-runtime>\n`;
const nextBuiltAppHtml = builtAppHtml.includes('data-workspace-runtime')
? builtAppHtml.replace(/\s*<link[^>]+data-workspace-runtime[^>]*>\s*/u, `\n${runtimeStylesheet}`)
: builtAppHtml.replace('</body>', `${runtimeStylesheet}</body>`);
if (!nextBuiltAppHtml.includes('data-workspace-runtime')) {
throw new Error('Unable to inject the runtime workspace stylesheet into dist/app.html');
}
writeFileSync(builtAppPath, nextBuiltAppHtml, 'utf8');
console.log(`Injected runtime workspace stylesheet: ${workspaceFileName}`);

View File

@@ -32,4 +32,21 @@ for (const fileName of ['app.html', 'share.html']) {
assert.strictEqual(/vue\.global(?:\.prod)?\.js|axios\.min\.js/.test(html), false, `${fileName} still loads global Vue/Axios`); assert.strictEqual(/vue\.global(?:\.prod)?\.js|axios\.min\.js/.test(html), false, `${fileName} still loads global Vue/Axios`);
} }
const builtAppHtml = readFileSync(path.join(outputDirectory, 'app.html'), 'utf8');
const runtimeWorkspaceMatch = builtAppHtml.match(
/<link rel="stylesheet" href="\/(workspace-[a-f0-9]{12}\.css)" data-workspace-runtime>/
);
assert.ok(runtimeWorkspaceMatch, 'app.html is missing the hashed runtime workspace stylesheet');
const runtimeWorkspacePath = path.join(outputDirectory, runtimeWorkspaceMatch[1]);
assert.ok(existsSync(runtimeWorkspacePath), 'runtime workspace stylesheet is missing');
assert.ok(
builtAppHtml.indexOf(runtimeWorkspaceMatch[0]) > builtAppHtml.lastIndexOf('</style>'),
'runtime workspace stylesheet must load after every legacy inline style'
);
const runtimeWorkspaceCss = readFileSync(runtimeWorkspacePath, 'utf8');
assert.ok(runtimeWorkspaceCss.includes('.mobile-file-create-trigger'), 'runtime workspace stylesheet is incomplete');
assert.ok(runtimeWorkspaceCss.includes('@media (max-width: 900px)'), 'runtime workspace mobile breakpoint is missing');
console.log(`Verified ${htmlFiles.length} HTML entries and their emitted assets.`); console.log(`Verified ${htmlFiles.length} HTML entries and their emitted assets.`);

File diff suppressed because it is too large Load Diff