Skip to content

Commit fafb11d

Browse files
authored
Merge pull request #24 from xxcheng123/feature/strm-optimize
feat(storage): 添加个人和家庭云盘支持
2 parents f713c28 + cac6daf commit fafb11d

31 files changed

Lines changed: 1998 additions & 119 deletions

configs/init.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import (
66
"crypto/md5"
77
"flag"
88
"fmt"
9-
"github.com/joho/godotenv"
109
"os"
1110
"path/filepath"
1211
"runtime"
1312
"time"
1413

14+
"github.com/joho/godotenv"
15+
1516
"github.com/glebarez/sqlite"
1617
"github.com/xxcheng123/cloudpan189-share/internal/models"
1718
logger2 "github.com/xxcheng123/cloudpan189-share/internal/pkgs/logger"

fe/src/api/storage.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export interface Storage {
1212
id: number
1313
parentId: number
1414
name: string
15-
osType: string // subscribe:订阅类型;share:分享类型
15+
osType: string // subscribe:订阅类型;share:分享类型;person:个人类型;family:家庭类型
1616
createdAt: string
1717
updatedAt: string
1818
localPath: string
@@ -41,11 +41,13 @@ export interface PreAddStorageResponse {
4141

4242
export interface AddStorageRequest {
4343
localPath: string
44-
protocol: string // 目前只允许 subscribe、share
45-
cloudToken?: number
44+
protocol: string // 目前允许 subscribe、share、person、family
45+
cloudToken?: number // person、family 时必填
4646
subscribeUser?: string // subscribe 时必填
4747
shareCode?: string // share 时必填
48-
shareAccessCode?: string // share 时必填
48+
shareAccessCode?: string // share 时可选
49+
fileId?: string // person、family 时必填
50+
familyId?: string // family 时必填
4951
}
5052

5153
export interface AddStorageResponse {

fe/src/api/storage_bridge.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import api from './index'
2+
3+
// 文件节点类型定义
4+
export interface FileNode {
5+
parentId: string
6+
id: string
7+
name: string
8+
isFolder: number // 0: 文件, 1: 文件夹
9+
}
10+
11+
// 获取个人文件节点请求参数
12+
export interface GetPersonNodesRequest {
13+
id?: string // 默认为 -11
14+
cloudToken: number
15+
currentPage?: number // 默认为 1
16+
pageSize?: number // 默认为 30
17+
}
18+
19+
// 获取个人文件节点响应
20+
export interface GetPersonNodesResponse {
21+
data: FileNode[]
22+
total: number
23+
currentPage: number
24+
pageSize: number
25+
}
26+
27+
// 获取家庭文件节点请求参数
28+
export interface GetFamilyNodesRequest {
29+
id?: string
30+
familyId?: string
31+
cloudToken: number
32+
currentPage?: number // 默认为 1
33+
pageSize?: number // 默认为 30
34+
}
35+
36+
// 获取家庭文件节点响应
37+
export interface GetFamilyNodesResponse {
38+
data: FileNode[]
39+
total: number
40+
currentPage: number
41+
pageSize: number
42+
}
43+
44+
// 家庭列表请求参数
45+
export interface FamilyListRequest {
46+
cloudToken: number
47+
}
48+
49+
// 家庭信息
50+
export interface FamilyInfo {
51+
count: number
52+
createTime: string
53+
familyId: string
54+
remarkName: string
55+
type: number
56+
useFlag: number
57+
userRole: number
58+
expireTime?: string
59+
}
60+
61+
// 家庭列表响应
62+
export type FamilyListResponse = FamilyInfo[]
63+
64+
// 存储桥接 API
65+
export const storageBridgeApi = {
66+
// 获取个人文件节点
67+
getPersonNodes: (params: GetPersonNodesRequest): Promise<GetPersonNodesResponse> => {
68+
return api.get('/storage/bridge/get_person_nodes', { params })
69+
},
70+
71+
// 获取家庭文件节点
72+
getFamilyNodes: (params: GetFamilyNodesRequest): Promise<GetFamilyNodesResponse> => {
73+
return api.get('/storage/bridge/get_family_nodes', { params })
74+
},
75+
76+
// 获取家庭列表
77+
getFamilyList: (params: FamilyListRequest): Promise<FamilyListResponse> => {
78+
return api.get('/storage/bridge/family_file', { params })
79+
}
80+
}

fe/src/components/ConfirmDialog.vue

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
</template>
2424

2525
<script setup lang="ts">
26-
import { ref } from 'vue'
26+
import { ref, watch } from 'vue'
2727
import Icons from './Icons.vue'
2828
2929
interface Props {
@@ -34,7 +34,7 @@ interface Props {
3434
isDanger?: boolean
3535
}
3636
37-
const props = withDefaults(defineProps<Props>(), {
37+
withDefaults(defineProps<Props>(), {
3838
title: '确认操作',
3939
confirmText: '确定',
4040
cancelText: '取消',
@@ -70,6 +70,17 @@ const handleOverlayClick = () => {
7070
handleCancel()
7171
}
7272
73+
// 监听弹窗显示状态,控制背景滚动
74+
watch(visible, (isVisible) => {
75+
if (isVisible) {
76+
// 禁用背景滚动
77+
document.body.style.overflow = 'hidden'
78+
} else {
79+
// 恢复背景滚动
80+
document.body.style.overflow = ''
81+
}
82+
})
83+
7384
defineExpose({
7485
show,
7586
hide
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
<template>
2+
<div class="family-file-tree-selector">
3+
<div v-if="loading" class="tree-loading">
4+
<div class="loading-spinner"></div>
5+
<span>加载文件列表中...</span>
6+
</div>
7+
<div v-else-if="files.length === 0" class="tree-empty">
8+
暂无文件
9+
</div>
10+
<div v-else class="tree-container">
11+
<FileTreeNode
12+
v-for="file in files"
13+
:key="file.id"
14+
:node="file"
15+
:selected-id="selectedId"
16+
:expanded-ids="expandedIds"
17+
:loading-ids="loadingIds"
18+
@select="handleSelect"
19+
@toggle="handleToggle"
20+
/>
21+
</div>
22+
</div>
23+
</template>
24+
25+
<script setup lang="ts">
26+
import { ref, watch } from 'vue'
27+
import FileTreeNode from './FileTreeNode.vue'
28+
import { storageBridgeApi, type FileNode } from '@/api/storage_bridge'
29+
import { toast } from '@/utils/toast'
30+
31+
interface TreeFileNode extends FileNode {
32+
children?: TreeFileNode[]
33+
level: number
34+
}
35+
36+
interface Props {
37+
cloudToken?: string | number
38+
familyId?: string
39+
modelValue?: string
40+
}
41+
42+
interface Emits {
43+
(e: 'update:modelValue', value: string): void
44+
(e: 'select', file: TreeFileNode): void
45+
}
46+
47+
const props = defineProps<Props>()
48+
const emit = defineEmits<Emits>()
49+
50+
const files = ref<TreeFileNode[]>([])
51+
const loading = ref(false)
52+
const selectedId = ref<string>('')
53+
const expandedIds = ref<Set<string>>(new Set())
54+
const loadingIds = ref<Set<string>>(new Set())
55+
56+
// 获取家庭文件列表
57+
const fetchFiles = async () => {
58+
if (!props.cloudToken || !props.familyId) return
59+
60+
try {
61+
loading.value = true
62+
const response = await storageBridgeApi.getFamilyNodes({
63+
cloudToken: Number(props.cloudToken),
64+
familyId: props.familyId,
65+
id: '' // 根目录
66+
})
67+
68+
// 只显示文件夹,过滤掉文件
69+
files.value = (response.data || [])
70+
.filter(file => file.isFolder)
71+
.map(file => ({
72+
...file,
73+
level: 0,
74+
children: undefined
75+
}))
76+
} catch (error) {
77+
console.error('获取家庭文件列表失败:', error)
78+
toast.error('获取家庭文件列表失败')
79+
files.value = []
80+
} finally {
81+
loading.value = false
82+
}
83+
}
84+
85+
// 监听 modelValue 变化
86+
watch(() => props.modelValue, (newValue) => {
87+
if (newValue !== selectedId.value) {
88+
selectedId.value = newValue || ''
89+
}
90+
}, { immediate: true })
91+
92+
// 监听 cloudToken 和 familyId 变化
93+
watch(() => [props.cloudToken, props.familyId], ([newToken, newFamilyId]) => {
94+
if (newToken && newFamilyId) {
95+
fetchFiles()
96+
} else {
97+
files.value = []
98+
selectedId.value = ''
99+
expandedIds.value.clear()
100+
loadingIds.value.clear()
101+
}
102+
}, { immediate: true })
103+
104+
// 处理选择
105+
const handleSelect = (file: TreeFileNode) => {
106+
if (!file.isFolder) return
107+
108+
selectedId.value = file.id
109+
emit('update:modelValue', file.id)
110+
emit('select', file)
111+
}
112+
113+
// 处理展开/折叠
114+
const handleToggle = async (file: TreeFileNode) => {
115+
if (!file.isFolder) return
116+
117+
const fileId = file.id
118+
119+
if (expandedIds.value.has(fileId)) {
120+
// 折叠
121+
expandedIds.value.delete(fileId)
122+
} else {
123+
// 展开
124+
expandedIds.value.add(fileId)
125+
126+
// 如果没有子文件,则加载
127+
if (!file.children) {
128+
await loadChildren(file)
129+
}
130+
}
131+
}
132+
133+
// 加载子文件
134+
const loadChildren = async (parentFile: TreeFileNode) => {
135+
if (!props.cloudToken || !props.familyId || loadingIds.value.has(parentFile.id)) return
136+
137+
try {
138+
loadingIds.value.add(parentFile.id)
139+
140+
const response = await storageBridgeApi.getFamilyNodes({
141+
cloudToken: Number(props.cloudToken),
142+
familyId: props.familyId,
143+
id: parentFile.id
144+
})
145+
146+
// 只显示文件夹,过滤掉文件
147+
const children = (response.data || [])
148+
.filter(file => file.isFolder)
149+
.map(file => ({
150+
...file,
151+
level: parentFile.level + 1,
152+
children: undefined
153+
}))
154+
155+
// 更新父文件的子文件列表
156+
parentFile.children = children
157+
158+
// 触发响应式更新
159+
files.value = [...files.value]
160+
161+
} catch (error) {
162+
console.error('加载家庭子文件失败:', error)
163+
toast.error('加载家庭子文件失败')
164+
} finally {
165+
loadingIds.value.delete(parentFile.id)
166+
}
167+
}
168+
169+
// 查找文件节点
170+
const findFileNode = (files: TreeFileNode[], id: string): TreeFileNode | null => {
171+
for (const file of files) {
172+
if (file.id === id) return file
173+
if (file.children) {
174+
const found = findFileNode(file.children, id)
175+
if (found) return found
176+
}
177+
}
178+
return null
179+
}
180+
181+
// 获取选中的文件
182+
const getSelectedFile = (): TreeFileNode | null => {
183+
if (!selectedId.value) return null
184+
return findFileNode(files.value, selectedId.value)
185+
}
186+
187+
// 暴露方法
188+
defineExpose({
189+
getSelectedFile,
190+
refresh: fetchFiles
191+
})
192+
</script>
193+
194+
<style scoped>
195+
.family-file-tree-selector {
196+
border: 1px solid #e5e7eb;
197+
border-radius: 8px;
198+
background: white;
199+
max-height: 300px;
200+
overflow-y: auto;
201+
}
202+
203+
.tree-loading {
204+
display: flex;
205+
align-items: center;
206+
justify-content: center;
207+
gap: 0.75rem;
208+
padding: 2rem;
209+
color: #6b7280;
210+
font-size: 0.875rem;
211+
}
212+
213+
.loading-spinner {
214+
width: 16px;
215+
height: 16px;
216+
border: 2px solid #e5e7eb;
217+
border-top: 2px solid #3b82f6;
218+
border-radius: 50%;
219+
animation: spin 1s linear infinite;
220+
}
221+
222+
@keyframes spin {
223+
0% { transform: rotate(0deg); }
224+
100% { transform: rotate(360deg); }
225+
}
226+
227+
.tree-empty {
228+
padding: 2rem;
229+
text-align: center;
230+
color: #9ca3af;
231+
font-size: 0.875rem;
232+
}
233+
234+
.tree-container {
235+
padding: 0.5rem 0;
236+
}
237+
</style>

0 commit comments

Comments
 (0)