重新设计分享管理页面

- 添加搜索/筛选功能(关键字、类型、状态、排序)
- 重新设计卡片视图,展示更多信息(状态、类型、加密、存储来源等)
- 添加filteredShares计算属性实现筛选和排序
- 添加辅助方法:getShareTypeLabel、getShareStatus、getShareProtection等
- 优化分享卡片样式,支持深色/浅色主题

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-30 12:57:35 +08:00
parent 7d5a230007
commit 2b25f18137
2 changed files with 284 additions and 20 deletions

View File

@@ -1028,6 +1028,109 @@
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.15);
} }
/* 分享卡片布局 */
.share-card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 14px;
}
.share-card {
background: var(--bg-card);
border: 1px solid var(--glass-border);
border-radius: 12px;
padding: 14px;
box-shadow: 0 10px 30px rgba(0,0,0,0.12);
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.share-card:hover {
transform: translateY(-2px);
border-color: var(--glass-border-hover);
box-shadow: 0 14px 36px rgba(0,0,0,0.18);
}
.share-card__title {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
font-size: 15px;
color: var(--text-primary);
margin-bottom: 8px;
word-break: break-all;
}
.share-card__chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 10px;
}
.share-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
border: 1px solid var(--glass-border);
background: rgba(255,255,255,0.04);
color: var(--text-secondary);
}
.share-chip.success { color: #22c55e; background: rgba(34,197,94,0.12); border-color: rgba(34,197,94,0.25); }
.share-chip.warn { color: #f59e0b; background: rgba(245,158,11,0.14); border-color: rgba(245,158,11,0.25); }
.share-chip.danger { color: #ef4444; background: rgba(239,68,68,0.14); border-color: rgba(239,68,68,0.25); }
.share-chip.info { color: var(--accent-1); background: rgba(102,126,234,0.14); border-color: rgba(102,126,234,0.25); }
.share-card__meta {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 12px;
}
.share-card__meta span {
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.share-card__actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.share-card__actions .btn {
padding: 8px 12px;
font-size: 13px;
}
.share-toolbar {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.share-toolbar input,
.share-toolbar select {
background: rgba(255,255,255,0.05);
border: 1px solid var(--glass-border);
border-radius: 8px;
padding: 8px 12px;
color: var(--text-primary);
}
.share-toolbar input::placeholder {
color: var(--text-secondary);
}
.share-toolbar select {
min-width: 140px;
cursor: pointer;
}
body.light-theme .share-card {
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
}
body.light-theme .share-card:hover {
box-shadow: 0 14px 36px rgba(0,0,0,0.12);
}
</style> </style>
</head> </head>
<body> <body>
@@ -1919,46 +2022,108 @@
</div> </div>
</div> </div>
<!-- 分享视图 --> <!-- 分享视图 -->
<div v-if="isLoggedIn && currentView === 'shares'" class="main-container"> <div v-if="isLoggedIn && currentView === 'shares'" class="main-container">
<div class="card"> <div class="card">
<!-- 标题和工具栏 --> <!-- 标题和工具栏 -->
<div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;"> <div style="margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap;">
<h3 style="margin: 0;">我的分享</h3> <h3 style="margin: 0; display: flex; align-items: center; gap: 8px;">
<div style="display: flex; gap: 10px;"> <i class="fas fa-share-alt"></i> 我的分享
</h3>
<div style="display: flex; gap: 8px;">
<button class="btn" :class="shareViewMode === 'grid' ? 'btn-primary' : 'btn-secondary'" @click="shareViewMode = 'grid'"> <button class="btn" :class="shareViewMode === 'grid' ? 'btn-primary' : 'btn-secondary'" @click="shareViewMode = 'grid'">
<i class="fas fa-th-large"></i> 大图标 <i class="fas fa-th-large"></i> 卡片
</button> </button>
<button class="btn" :class="shareViewMode === 'list' ? 'btn-primary' : 'btn-secondary'" @click="shareViewMode = 'list'"> <button class="btn" :class="shareViewMode === 'list' ? 'btn-primary' : 'btn-secondary'" @click="shareViewMode = 'list'">
<i class="fas fa-list"></i> 列表 <i class="fas fa-list"></i> 列表
</button> </button>
<button class="btn btn-secondary" @click="loadShares">
<i class="fas fa-sync-alt"></i> 刷新
</button>
</div> </div>
</div> </div>
<!-- 筛选/搜索 -->
<div class="share-toolbar" style="margin-bottom: 14px;">
<input type="text" v-model="shareFilters.keyword" placeholder="搜索路径 / 链接 / 分享码" style="flex: 1; min-width: 180px;">
<select v-model="shareFilters.type">
<option value="all">全部类型</option>
<option value="file">文件</option>
<option value="directory">文件夹</option>
<option value="all_files">全部文件</option>
</select>
<select v-model="shareFilters.status">
<option value="all">全部状态</option>
<option value="active">有效</option>
<option value="expiring">即将到期</option>
<option value="expired">已过期</option>
<option value="protected">已加密</option>
<option value="public">公开</option>
</select>
<select v-model="shareFilters.sort">
<option value="created_desc">最新创建</option>
<option value="created_asc">最早创建</option>
<option value="views_desc">访问最多</option>
<option value="downloads_desc">下载最多</option>
<option value="expire_asc">最早到期</option>
</select>
</div>
<!-- 空状态 --> <!-- 空状态 -->
<div v-if="shares.length === 0" class="alert alert-info"> <div v-if="shares.length === 0" class="alert alert-info">
还没有创建任何分享 还没有创建任何分享
</div> </div>
<div v-else-if="filteredShares.length === 0" class="alert alert-warning">
没有符合筛选条件的分享,试试清空搜索/筛选。
</div>
<!-- 大图标视图 --> <!-- 大图标视图 -->
<div v-else-if="shareViewMode === 'grid'" class="file-grid"> <div v-else-if="shareViewMode === 'grid'" class="share-card-grid">
<div v-for="share in shares" :key="share.id" class="file-grid-item"> <div v-for="share in filteredShares" :key="share.id" class="share-card">
<div class="file-icon"> <div class="share-card__title">
<i class="fas fa-share-alt" style="font-size: 64px; color: #667eea;"></i> <i class="fas" :class="{
'fa-file-alt': share.share_type === 'file',
'fa-folder': share.share_type === 'directory',
'fa-layer-group': share.share_type === 'all'
}" style="color: var(--accent-1);"></i>
<span :title="share.share_path">{{ share.share_path }}</span>
</div> </div>
<div class="file-name" :title="share.share_path">{{ share.share_path }}</div> <div class="share-card__chips">
<div class="file-size" style="font-size: 12px; color: var(--text-secondary);"> <span :class="['share-chip', getShareStatus(share).class]">
访问: {{ share.view_count }} | 下载: {{ share.download_count }} <i class="fas" :class="getShareStatus(share).icon"></i> {{ getShareStatus(share).text }}
</span>
<span class="share-chip info">
<i class="fas fa-tag"></i> {{ getShareTypeLabel(share.share_type) }}
</span>
<span class="share-chip info">
<i class="fas" :class="getShareProtection(share).icon"></i> {{ getShareProtection(share).text }}
</span>
<span class="share-chip info" v-if="share.storage_type">
<i class="fas fa-hdd"></i> {{ getStorageLabel(share.storage_type) }}
</span>
<span class="share-chip info">
<i class="fas fa-barcode"></i> {{ share.share_code }}
</span>
</div> </div>
<div class="file-actions"> <div style="font-size: 13px; color: var(--text-secondary); margin-bottom: 10px; word-break: break-all;">
<button class="btn-icon" @click.stop="window.open(share.share_url, '_blank')" title="打开分享"> <i class="fas fa-link"></i>
<i class="fas fa-external-link-alt"></i> <a :href="share.share_url" target="_blank" style="color: var(--accent-1); word-break: break-all;">{{ share.share_url }}</a>
</div>
<div class="share-card__meta">
<span><i class="fas fa-eye"></i> 访问 {{ share.view_count }}</span>
<span><i class="fas fa-download"></i> 下载 {{ share.download_count }}</span>
<span><i class="fas fa-clock"></i> {{ share.expires_at ? formatExpireTime(share.expires_at) : '永久有效' }}</span>
<span><i class="fas fa-calendar-alt"></i> 创建 {{ formatDateTime(share.created_at) }}</span>
</div>
<div class="share-card__actions">
<button class="btn btn-secondary" @click.stop="window.open(share.share_url, '_blank')">
<i class="fas fa-external-link-alt"></i> 打开
</button> </button>
<button class="btn-icon" @click.stop="copyShareLink(share.share_url)" title="复制链接"> <button class="btn btn-secondary" @click.stop="copyShareLink(share.share_url)">
<i class="fas fa-copy"></i> <i class="fas fa-copy"></i> 复制链接
</button> </button>
<button class="btn-icon" style="color: #ef4444;" @click.stop="deleteShare(share.id)" title="删除"> <button class="btn" style="background: #ef4444; color: white;" @click.stop="deleteShare(share.id)">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i> 删除
</button> </button>
</div> </div>
</div> </div>
@@ -1977,7 +2142,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="share in shares" :key="share.id" style="border-bottom: 1px solid #eee;"> <tr v-for="share in filteredShares" :key="share.id" style="border-bottom: 1px solid #eee;">
<td style="padding: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="share.share_path">{{ share.share_path }}</td> <td style="padding: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="share.share_path">{{ share.share_path }}</td>
<td style="padding: 10px; overflow: hidden;"> <td style="padding: 10px; overflow: hidden;">
<a :href="share.share_url" target="_blank" style="color: #667eea; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="share.share_url">{{ share.share_url }}</a> <a :href="share.share_url" target="_blank" style="color: #667eea; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="share.share_url">{{ share.share_url }}</a>

View File

@@ -89,6 +89,12 @@ createApp({
customDays: 7 customDays: 7
}, },
shareResult: null, shareResult: null,
shareFilters: {
keyword: '',
type: 'all', // all/file/directory/all_files
status: 'all', // all/active/expiring/expired/protected/public
sort: 'created_desc' // created_desc/created_asc/views_desc/downloads_desc/expire_asc
},
// 文件重命名 // 文件重命名
showRenameModal: false, showRenameModal: false,
@@ -304,6 +310,56 @@ createApp({
// 存储类型显示文本 // 存储类型显示文本
storageTypeText() { storageTypeText() {
return this.storageType === 'local' ? '本地存储' : 'SFTP存储'; return this.storageType === 'local' ? '本地存储' : 'SFTP存储';
},
// 分享筛选+排序后的列表
filteredShares() {
let list = [...this.shares];
const keyword = this.shareFilters.keyword.trim().toLowerCase();
if (keyword) {
list = list.filter(s =>
(s.share_path || '').toLowerCase().includes(keyword) ||
(s.share_code || '').toLowerCase().includes(keyword) ||
(s.share_url || '').toLowerCase().includes(keyword)
);
}
if (this.shareFilters.type !== 'all') {
const targetType = this.shareFilters.type === 'all_files' ? 'all' : this.shareFilters.type;
list = list.filter(s => (s.share_type || 'file') === targetType);
}
if (this.shareFilters.status !== 'all') {
list = list.filter(s => {
if (this.shareFilters.status === 'expired') return this.isExpired(s.expires_at);
if (this.shareFilters.status === 'expiring') return this.isExpiringSoon(s.expires_at) && !this.isExpired(s.expires_at);
if (this.shareFilters.status === 'active') return !this.isExpired(s.expires_at);
if (this.shareFilters.status === 'protected') return !!s.share_password;
if (this.shareFilters.status === 'public') return !s.share_password;
return true;
});
}
list.sort((a, b) => {
const getTime = s => s.created_at ? new Date(s.created_at).getTime() : 0;
const getExpire = s => s.expires_at ? new Date(s.expires_at).getTime() : Number.MAX_SAFE_INTEGER;
switch (this.shareFilters.sort) {
case 'created_asc':
return getTime(a) - getTime(b);
case 'views_desc':
return (b.view_count || 0) - (a.view_count || 0);
case 'downloads_desc':
return (b.download_count || 0) - (a.download_count || 0);
case 'expire_asc':
return getExpire(a) - getExpire(b);
default:
return getTime(b) - getTime(a); // created_desc
}
});
return list;
} }
}, },
@@ -1916,6 +1972,49 @@ handleDragLeave(e) {
return expireDate <= now; return expireDate <= now;
}, },
// 分享类型标签
getShareTypeLabel(type) {
switch (type) {
case 'directory': return '文件夹';
case 'all': return '全部文件';
case 'file':
default: return '文件';
}
},
// 分享状态标签
getShareStatus(share) {
if (this.isExpired(share.expires_at)) {
return { text: '已过期', class: 'danger', icon: 'fa-clock' };
}
if (this.isExpiringSoon(share.expires_at)) {
return { text: '即将到期', class: 'warn', icon: 'fa-hourglass-half' };
}
return { text: '有效', class: 'success', icon: 'fa-check-circle' };
},
// 分享保护标签
getShareProtection(share) {
if (share.share_password) {
return { text: '已加密', class: 'info', icon: 'fa-lock' };
}
return { text: '公开', class: 'info', icon: 'fa-unlock' };
},
// 存储来源
getStorageLabel(storageType) {
if (!storageType) return '默认';
return storageType === 'local' ? '本地存储' : storageType.toUpperCase();
},
// 格式化时间
formatDateTime(value) {
if (!value) return '--';
const d = new Date(value);
if (Number.isNaN(d.getTime())) return value;
return d.toLocaleString();
},
copyShareLink(url) { copyShareLink(url) {
// 复制分享链接到剪贴板 // 复制分享链接到剪贴板
if (navigator.clipboard && navigator.clipboard.writeText) { if (navigator.clipboard && navigator.clipboard.writeText) {