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