feat: add independent direct-link sharing flow

This commit is contained in:
2026-02-17 21:57:38 +08:00
parent d236a790a1
commit 6242622f1a
4 changed files with 842 additions and 6 deletions

View File

@@ -2198,6 +2198,69 @@
</div>
</div>
<!-- 生成直链模态框 -->
<div v-if="showDirectLinkModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showDirectLinkModal', $event)">
<div class="modal-content" @click.stop>
<h3 style="margin-bottom: 20px;">生成文件直链</h3>
<p style="color: var(--text-secondary); margin-bottom: 15px;">文件: <strong>{{ directLinkForm.fileName }}</strong></p>
<p style="color: var(--text-secondary); margin-bottom: 15px; word-break: break-all;">路径: {{ directLinkForm.filePath }}</p>
<div class="form-group">
<label class="form-label">有效期</label>
<select class="form-input" v-model="directLinkForm.expiryType">
<option value="never">永久</option>
<option value="7">7天</option>
<option value="30">30天</option>
<option value="custom">自定义</option>
</select>
</div>
<div v-if="directLinkForm.expiryType === 'custom'" class="form-group">
<label class="form-label">自定义天数</label>
<input type="number" class="form-input" v-model.number="directLinkForm.customDays" min="1" max="365">
</div>
<div v-if="directLinkResult" class="share-success-panel" style="margin-top: 15px;">
<div class="share-success-head">
<i class="fas fa-circle-check"></i>
<div>
<div class="share-success-title">直链创建成功</div>
<div class="share-success-subtitle">{{ directLinkResult.target_name || directLinkForm.fileName }} · 直链下载</div>
</div>
</div>
<div class="share-success-link" :title="directLinkResult.direct_url">{{ directLinkResult.direct_url }}</div>
<div class="share-success-actions">
<button class="btn btn-primary" @click="copyDirectLink(directLinkResult.direct_url)">
<i class="fas fa-copy"></i> 复制直链
</button>
<button class="btn btn-secondary" @click="openShare(directLinkResult.direct_url)">
<i class="fas fa-up-right-from-square"></i> 打开直链
</button>
</div>
<div class="share-success-meta">
<span class="share-chip info" v-if="directLinkResult.link_code">
<i class="fas fa-hashtag"></i> {{ directLinkResult.link_code }}
</span>
<span class="share-chip" :class="directLinkResult.expires_at ? (isExpiringSoon(directLinkResult.expires_at) ? 'warn' : 'success') : 'info'">
<i class="fas fa-clock"></i>
{{ directLinkResult.expires_at ? formatExpireTime(directLinkResult.expires_at) : '永久有效' }}
</span>
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="btn btn-primary" @click="createDirectLink()" :disabled="creatingDirectLink" style="flex: 1;">
<i class="fas" :class="creatingDirectLink ? 'fa-spinner fa-spin' : 'fa-link'"></i> {{ creatingDirectLink ? '生成中...' : '生成直链' }}
</button>
<button class="btn btn-secondary" @click="showDirectLinkModal = false; directLinkResult = null" style="flex: 1;">
<i class="fas fa-times"></i> 关闭
</button>
</div>
</div>
</div>
<!-- OSS 配置引导弹窗 -->
<div v-if="showOssGuideModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showOssGuideModal', $event)">
<div class="modal-content" @click.stop style="max-width: 520px; border-radius: 16px; overflow: hidden;">
@@ -2814,7 +2877,7 @@
<button class="btn" :class="shareViewMode === 'list' ? 'btn-primary' : 'btn-secondary'" @click="shareViewMode = 'list'">
<i class="fas fa-list"></i> 列表
</button>
<button class="btn btn-secondary" @click="loadShares">
<button class="btn btn-secondary" @click="refreshShareResources">
<i class="fas fa-sync-alt"></i> 刷新
</button>
</div>
@@ -2934,6 +2997,60 @@
</tr>
</tbody>
</table>
<!-- 直链管理(独立于普通分享) -->
<div style="margin-top: 24px;">
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 10px;">
<h4 style="margin: 0; display: flex; align-items: center; gap: 8px;">
<i class="fas fa-link"></i> 我的直链
</h4>
<small style="color: var(--text-secondary);">提示:可在文件右键菜单中生成直链</small>
</div>
<div v-if="directLinksLoading" class="alert alert-info">
正在加载直链列表...
</div>
<div v-else-if="directLinks.length === 0" class="alert alert-info">
还没有创建直链
</div>
<div v-else-if="filteredDirectLinks.length === 0" class="alert alert-warning">
没有符合搜索条件的直链
</div>
<table v-else class="share-list-table">
<thead>
<tr style="border-bottom: 2px solid #ddd;">
<th style="padding: 10px; text-align: left; width: 26%;">文件路径</th>
<th style="padding: 10px; text-align: left; width: 36%;">直链地址</th>
<th style="padding: 10px; text-align: center; width: 8%;">下载</th>
<th style="padding: 10px; text-align: center; width: 15%;">到期时间</th>
<th style="padding: 10px; text-align: center; width: 15%;">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="link in filteredDirectLinks" :key="`direct-${link.id}`" style="border-bottom: 1px solid #eee;">
<td style="padding: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="link.file_path">{{ link.file_path }}</td>
<td style="padding: 10px; overflow: hidden;">
<a :href="link.direct_url" target="_blank" style="color: #667eea; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="link.direct_url">{{ link.direct_url }}</a>
</td>
<td style="padding: 10px; text-align: center;">{{ link.download_count || 0 }}</td>
<td style="padding: 10px; text-align: center;">
<span v-if="!link.expires_at" style="color: #22c55e;"><i class="fas fa-infinity"></i> 永久有效</span>
<span v-else :style="{color: isExpiringSoon(link.expires_at) ? '#ffc107' : isExpired(link.expires_at) ? '#dc3545' : '#667eea'}" :title="link.expires_at"><i class="fas fa-clock"></i> {{ formatExpireTime(link.expires_at) }}</span>
</td>
<td style="padding: 10px; text-align: center;">
<div style="display: inline-flex; gap: 6px;">
<button class="btn btn-secondary" @click="copyDirectLink(link.direct_url)">
<i class="fas fa-copy"></i> 复制
</button>
<button class="btn" style="background: #ef4444; color: white;" @click="deleteDirectLink(link.id)">
<i class="fas fa-trash"></i> 删除
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
@@ -3949,6 +4066,9 @@
<div class="context-menu-item" @click="contextMenuAction('share')">
<i class="fas fa-share"></i> 分享
</div>
<div v-if="!contextMenuFile.isDirectory" class="context-menu-item" @click="contextMenuAction('direct_link')">
<i class="fas fa-link"></i> 生成直链
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item context-menu-item-danger" @click="contextMenuAction('delete')">
<i class="fas fa-trash"></i> 删除
@@ -3978,6 +4098,9 @@
<button class="mobile-file-sheet-btn" @click="mobileFileAction('share')">
<i class="fas fa-share"></i> 分享
</button>
<button v-if="!mobileActionFile.isDirectory" class="mobile-file-sheet-btn" @click="mobileFileAction('direct_link')">
<i class="fas fa-link"></i> 生成直链
</button>
<button class="mobile-file-sheet-btn danger" @click="mobileFileAction('delete')">
<i class="fas fa-trash"></i> 删除
</button>

View File

@@ -92,6 +92,8 @@ createApp({
// 分享管理
shares: [],
directLinks: [],
directLinksLoading: false,
showShareFileModal: false,
creatingShare: false, // 创建分享中状态
shareFileForm: {
@@ -104,6 +106,15 @@ createApp({
customDays: 7
},
shareResult: null,
showDirectLinkModal: false,
creatingDirectLink: false,
directLinkForm: {
fileName: '',
filePath: '',
expiryType: 'never',
customDays: 7
},
directLinkResult: null,
shareFilters: {
keyword: '',
type: 'all', // all/file/directory/all_files
@@ -580,6 +591,28 @@ createApp({
return list;
},
filteredDirectLinks() {
let list = [...this.directLinks];
const keyword = this.shareFilters.keyword.trim().toLowerCase();
if (keyword) {
list = list.filter((link) =>
(link.file_path || '').toLowerCase().includes(keyword)
|| (link.file_name || '').toLowerCase().includes(keyword)
|| (link.link_code || '').toLowerCase().includes(keyword)
|| (link.direct_url || '').toLowerCase().includes(keyword)
);
}
list.sort((a, b) => {
const ta = a.created_at ? new Date(a.created_at).getTime() : 0;
const tb = b.created_at ? new Date(b.created_at).getTime() : 0;
return tb - ta;
});
return list;
},
adminUsersFilteredCount() {
return Math.max(0, Number(this.adminUsersTotalCount) || 0);
},
@@ -1940,6 +1973,9 @@ handleDragLeave(e) {
case 'share':
this.openShareFileModal(this.contextMenuFile);
break;
case 'direct_link':
this.openDirectLinkModal(this.contextMenuFile);
break;
case 'delete':
this.confirmDeleteFile(this.contextMenuFile);
break;
@@ -2293,6 +2329,55 @@ handleDragLeave(e) {
}
},
openDirectLinkModal(file) {
if (!file || file.isDirectory) {
this.showToast('warning', '提示', '目录不支持生成直链,请选择文件');
return;
}
this.directLinkForm.fileName = file.name;
this.directLinkForm.filePath = this.currentPath === '/'
? `/${file.name}`
: `${this.currentPath}/${file.name}`;
this.directLinkForm.expiryType = 'never';
this.directLinkForm.customDays = 7;
this.directLinkResult = null;
this.showDirectLinkModal = true;
},
async createDirectLink() {
if (this.creatingDirectLink) return;
this.creatingDirectLink = true;
try {
const expiryCheck = this.resolveShareExpiry(this.directLinkForm.expiryType, this.directLinkForm.customDays);
if (!expiryCheck.valid) {
this.showToast('warning', '提示', expiryCheck.message);
return;
}
const response = await axios.post(`${this.apiBase}/api/direct-link/create`, {
file_path: this.directLinkForm.filePath,
file_name: this.directLinkForm.fileName,
expiry_days: expiryCheck.value
});
if (response.data?.success) {
this.directLinkResult = {
...response.data,
target_name: this.directLinkForm.fileName
};
this.showToast('success', '成功', '直链已创建');
this.loadDirectLinks();
}
} catch (error) {
console.error('创建直链失败:', error);
this.showToast('error', '错误', error.response?.data?.message || '创建直链失败');
} finally {
this.creatingDirectLink = false;
}
},
// ===== 文件上传 =====
@@ -2485,6 +2570,40 @@ handleDragLeave(e) {
}
},
async loadDirectLinks() {
this.directLinksLoading = true;
try {
const response = await axios.get(`${this.apiBase}/api/direct-link/my`);
if (response.data?.success) {
this.directLinks = response.data.links || [];
}
} catch (error) {
console.error('加载直链列表失败:', error);
this.showToast('error', '加载失败', error.response?.data?.message || '加载直链列表失败');
} finally {
this.directLinksLoading = false;
}
},
async deleteDirectLink(id) {
if (!confirm('确定要删除这个直链吗?')) return;
try {
const response = await axios.delete(`${this.apiBase}/api/direct-link/${id}`);
if (response.data?.success) {
this.showToast('success', '成功', '直链已删除');
this.loadDirectLinks();
}
} catch (error) {
console.error('删除直链失败:', error);
this.showToast('error', '删除失败', error.response?.data?.message || '删除直链失败');
}
},
async refreshShareResources() {
await Promise.all([this.loadShares(), this.loadDirectLinks()]);
},
async createShare() {
this.shareForm.path = this.currentPath;
@@ -2728,6 +2847,10 @@ handleDragLeave(e) {
this.copyTextToClipboard(url, '分享链接已复制到剪贴板');
},
copyDirectLink(url) {
this.copyTextToClipboard(url, '直链已复制到剪贴板');
},
copySharePassword(password) {
this.copyTextToClipboard(password, '访问密码已复制到剪贴板');
},
@@ -3286,7 +3409,7 @@ handleDragLeave(e) {
break;
case 'shares':
// 切换到分享视图时,重新加载分享列表
this.loadShares();
this.refreshShareResources();
break;
case 'admin':
// 切换到管理后台时,重新加载用户列表、健康检测和系统日志
@@ -4110,7 +4233,7 @@ handleDragLeave(e) {
watch: {
currentView(newView) {
if (newView === 'shares') {
this.loadShares();
this.refreshShareResources();
} else if (newView === 'admin' && this.user?.is_admin) {
this.loadUsers();
this.loadSystemSettings();