Download dashboard/src/components/shared/FileConfigItem.vue from qa1145/astrbbbb: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/spaces/qa1145/astrbbbb/resolve/main/dashboard/src/components/shared/FileConfigItem.vue
- Command line
-
hf download hf://spaces/qa1145/astrbbbb/dashboard/src/components/shared/FileConfigItem.vue
-
curl -L -o FileConfigItem.vue https://huggingface.co/spaces/qa1145/astrbbbb/resolve/main/dashboard/src/components/shared/FileConfigItem.vue
11 kB
| <template> | |
| <div class="file-config-item"> | |
| <div class="d-flex align-center gap-2"> | |
| <v-btn size="small" color="primary" variant="tonal" @click="dialog = true"> | |
| {{ tm('fileUpload.button') }} | |
| </v-btn> | |
| <span class="text-caption text-medium-emphasis ml-2"> | |
| {{ fileCountText }} | |
| </span> | |
| </div> | |
| <v-dialog v-model="dialog" max-width="700"> | |
| <v-card class="file-dialog-card" variant="flat"> | |
| <v-card-title class="d-flex align-center"> | |
| <span class="text-h3">{{ tm('fileUpload.dialogTitle') }}</span> | |
| <v-spacer /> | |
| <v-btn icon="mdi-close" variant="text" @click="dialog = false" /> | |
| </v-card-title> | |
| <v-card-text class="file-dialog-body"> | |
| <div v-if="mergedFileItems.length === 0" class="empty-text"> | |
| {{ tm('fileUpload.empty') }} | |
| </div> | |
| <v-list density="compact" lines="one"> | |
| <v-list-item v-for="item in mergedFileItems" :key="item.path"> | |
| <template #prepend> | |
| <v-icon size="18">mdi-file</v-icon> | |
| </template> | |
| <v-list-item-title class="file-name"> | |
| {{ getDisplayName(item.path) }} | |
| </v-list-item-title> | |
| <template #append> | |
| <div class="d-flex align-center gap-1"> | |
| <v-chip v-if="item.status !== 'ok'" size="x-small" :color="getStatusColor(item.status)" | |
| variant="tonal"> | |
| {{ getStatusText(item.status) }} | |
| </v-chip> | |
| <v-btn v-if="item.status === 'unconfigured'" icon="mdi-plus" size="x-small" variant="text" | |
| @click="addToConfig(item.path)" /> | |
| <v-btn icon="mdi-delete" size="x-small" variant="text" | |
| @click="item.status === 'unconfigured' ? deletePhysicalFile(item.path) : deleteFile(item.path)" /> | |
| </div> | |
| </template> | |
| </v-list-item> | |
| <v-divider v-if="mergedFileItems.length > 0" class="my-2" /> | |
| <v-list-item class="upload-item" :class="{ dragover: isDragging }" @drop.prevent="handleDrop" | |
| @dragover.prevent="isDragging = true" @dragleave="isDragging = false" @click="openFilePicker"> | |
| <template #prepend> | |
| <v-icon size="18" color="primary">mdi-plus</v-icon> | |
| </template> | |
| <v-list-item-title>{{ tm('fileUpload.dropzone') }}</v-list-item-title> | |
| <v-list-item-subtitle v-if="allowedTypesText" class="upload-hint"> | |
| {{ tm('fileUpload.allowedTypes', { types: allowedTypesText }) }} | |
| </v-list-item-subtitle> | |
| </v-list-item> | |
| </v-list> | |
| <input ref="fileInput" type="file" multiple hidden :accept="acceptAttr" @change="handleFileSelect" /> | |
| </v-card-text> | |
| <v-card-actions class="file-dialog-actions"> | |
| <v-spacer /> | |
| <v-btn color="primary" variant="elevated" @click="dialog = false"> | |
| {{ tm('fileUpload.done') }} | |
| </v-btn> | |
| </v-card-actions> | |
| </v-card> | |
| </v-dialog> | |
| </div> | |
| </template> | |
| <script setup> | |
| import { computed, ref, watch } from 'vue' | |
| import axios from 'axios' | |
| import { useToast } from '@/utils/toast' | |
| import { useModuleI18n } from '@/i18n/composables' | |
| const props = defineProps({ | |
| modelValue: { | |
| type: Array, | |
| default: () => [] | |
| }, | |
| itemMeta: { | |
| type: Object, | |
| default: null | |
| }, | |
| pluginName: { | |
| type: String, | |
| default: '' | |
| }, | |
| configKey: { | |
| type: String, | |
| default: '' | |
| } | |
| }) | |
| const emit = defineEmits(['update:modelValue']) | |
| const { tm } = useModuleI18n('features/config') | |
| const toast = useToast() | |
| const dialog = ref(false) | |
| const isDragging = ref(false) | |
| const fileInput = ref(null) | |
| const uploading = ref(false) | |
| const loadingFiles = ref(false) | |
| const MAX_FILE_BYTES = 500 * 1024 * 1024 | |
| const MAX_FILE_MB = 500 | |
| const directoryFiles = ref([]) | |
| const fileList = computed({ | |
| get: () => (Array.isArray(props.modelValue) ? props.modelValue : []), | |
| set: (val) => emit('update:modelValue', val) | |
| }) | |
| const mergedFileItems = computed(() => { | |
| const configured = new Set(fileList.value) | |
| const existing = new Set(directoryFiles.value) | |
| const items = [] | |
| for (const path of fileList.value) { | |
| items.push({ | |
| path, | |
| status: existing.has(path) ? 'ok' : 'missing' | |
| }) | |
| } | |
| for (const path of directoryFiles.value) { | |
| if (!configured.has(path)) { | |
| items.push({ | |
| path, | |
| status: 'unconfigured' | |
| }) | |
| } | |
| } | |
| return items | |
| }) | |
| const acceptAttr = computed(() => { | |
| const types = props.itemMeta?.file_types | |
| if (!Array.isArray(types) || types.length === 0) { | |
| return undefined | |
| } | |
| return types | |
| .map((ext) => `.${String(ext).replace(/^\\./, '')}`) | |
| .join(',') | |
| }) | |
| const allowedTypesText = computed(() => { | |
| const types = props.itemMeta?.file_types | |
| if (!Array.isArray(types) || types.length === 0) { | |
| return '' | |
| } | |
| return types.map((ext) => String(ext).replace(/^\\./, '')).join(', ') | |
| }) | |
| const fileCountText = computed(() => { | |
| return tm('fileUpload.fileCount', { count: fileList.value.length }) | |
| }) | |
| const getStatusText = (status) => { | |
| if (status === 'missing') { | |
| return tm('fileUpload.statusMissing') | |
| } | |
| if (status === 'unconfigured') { | |
| return tm('fileUpload.statusUnconfigured') | |
| } | |
| return '' | |
| } | |
| const getStatusColor = (status) => { | |
| if (status === 'missing') { | |
| return 'error' | |
| } | |
| if (status === 'unconfigured') { | |
| return 'warning' | |
| } | |
| return 'primary' | |
| } | |
| const openFilePicker = () => { | |
| fileInput.value?.click() | |
| } | |
| const loadDirectoryFiles = async () => { | |
| if (!props.pluginName || !props.configKey || loadingFiles.value) { | |
| return | |
| } | |
| loadingFiles.value = true | |
| try { | |
| const response = await axios.get( | |
| `/api/config/file/get?scope=plugin&name=${encodeURIComponent( | |
| props.pluginName | |
| )}&key=${encodeURIComponent(props.configKey)}` | |
| ) | |
| if (response.data.status === 'ok') { | |
| const files = response.data.data?.files || [] | |
| directoryFiles.value = Array.from(new Set(files)) | |
| } else { | |
| toast.warning(response.data.message || tm('fileUpload.loadFailed')) | |
| } | |
| } catch (error) { | |
| console.error('Load file list failed:', error) | |
| toast.warning(tm('fileUpload.loadFailed')) | |
| } finally { | |
| loadingFiles.value = false | |
| } | |
| } | |
| const handleFileSelect = (event) => { | |
| const target = event.target | |
| if (target?.files && target.files.length > 0) { | |
| uploadFiles(Array.from(target.files)) | |
| } | |
| if (target) { | |
| target.value = '' | |
| } | |
| } | |
| const handleDrop = (event) => { | |
| isDragging.value = false | |
| if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) { | |
| uploadFiles(Array.from(event.dataTransfer.files)) | |
| } | |
| } | |
| const uploadFiles = async (files) => { | |
| if (!props.pluginName || !props.configKey) { | |
| toast.warning('Missing plugin config info') | |
| return | |
| } | |
| if (uploading.value) { | |
| return | |
| } | |
| const oversized = files.filter((file) => file.size > MAX_FILE_BYTES) | |
| if (oversized.length > 0) { | |
| oversized.forEach((file) => { | |
| toast.warning( | |
| tm('fileUpload.fileTooLarge', { name: file.name, max: MAX_FILE_MB }) | |
| ) | |
| }) | |
| } | |
| const validFiles = files.filter((file) => file.size <= MAX_FILE_BYTES) | |
| if (validFiles.length === 0) { | |
| return | |
| } | |
| uploading.value = true | |
| try { | |
| const formData = new FormData() | |
| validFiles.forEach((file, index) => { | |
| formData.append(`file${index}`, file) | |
| }) | |
| const response = await axios.post( | |
| `/api/config/file/upload?scope=plugin&name=${encodeURIComponent( | |
| props.pluginName | |
| )}&key=${encodeURIComponent(props.configKey)}`, | |
| formData, | |
| { headers: { 'Content-Type': 'multipart/form-data' } } | |
| ) | |
| if (response.data.status === 'ok') { | |
| const uploaded = response.data.data?.uploaded || [] | |
| const errors = response.data.data?.errors || [] | |
| if (uploaded.length > 0) { | |
| const merged = [...fileList.value] | |
| for (const path of uploaded) { | |
| if (!merged.includes(path)) { | |
| merged.push(path) | |
| } | |
| } | |
| fileList.value = merged | |
| const updatedDirectory = new Set(directoryFiles.value) | |
| uploaded.forEach((path) => updatedDirectory.add(path)) | |
| directoryFiles.value = Array.from(updatedDirectory) | |
| toast.success(tm('fileUpload.uploadSuccess', { count: uploaded.length })) | |
| } | |
| if (errors.length > 0) { | |
| toast.warning(errors.join('\\n')) | |
| } | |
| } else { | |
| toast.error(response.data.message || tm('fileUpload.uploadFailed')) | |
| } | |
| } catch (error) { | |
| console.error('File upload failed:', error) | |
| toast.error(tm('fileUpload.uploadFailed')) | |
| } finally { | |
| uploading.value = false | |
| } | |
| } | |
| const addToConfig = (filePath) => { | |
| if (!fileList.value.includes(filePath)) { | |
| fileList.value = [...fileList.value, filePath] | |
| toast.success(tm('fileUpload.addToConfig')) | |
| } | |
| } | |
| const deleteFile = (filePath) => { | |
| fileList.value = fileList.value.filter((item) => item !== filePath) | |
| directoryFiles.value = directoryFiles.value.filter((item) => item !== filePath) | |
| if (props.pluginName) { | |
| axios | |
| .post( | |
| `/api/config/file/delete?scope=plugin&name=${encodeURIComponent( | |
| props.pluginName | |
| )}`, | |
| { path: filePath } | |
| ) | |
| .catch((error) => { | |
| console.warn('Staged file delete failed:', error) | |
| toast.warning(tm('fileUpload.deleteFailed')) | |
| }) | |
| } | |
| toast.success(tm('fileUpload.deleteSuccess')) | |
| } | |
| const deletePhysicalFile = (filePath) => { | |
| directoryFiles.value = directoryFiles.value.filter((item) => item !== filePath) | |
| if (props.pluginName) { | |
| axios | |
| .post( | |
| `/api/config/file/delete?scope=plugin&name=${encodeURIComponent( | |
| props.pluginName | |
| )}`, | |
| { path: filePath } | |
| ) | |
| .catch((error) => { | |
| console.warn('File delete failed:', error) | |
| toast.warning(tm('fileUpload.deleteFailed')) | |
| }) | |
| } | |
| toast.success(tm('fileUpload.deleteSuccess')) | |
| } | |
| const getDisplayName = (path) => { | |
| if (!path) return '' | |
| const parts = String(path).split('/') | |
| return parts[parts.length - 1] || path | |
| } | |
| watch( | |
| () => dialog.value, | |
| (value) => { | |
| if (value) { | |
| loadDirectoryFiles() | |
| } | |
| } | |
| ) | |
| </script> | |
| <style scoped> | |
| .file-config-item { | |
| width: 100%; | |
| } | |
| .file-dialog-card { | |
| height: 70vh; | |
| box-shadow: none; | |
| } | |
| .file-dialog-body { | |
| overflow-y: auto; | |
| max-height: calc(70vh - 120px); | |
| } | |
| .file-dialog-actions { | |
| padding: 16px 24px 20px; | |
| } | |
| .upload-hint { | |
| font-size: 12px; | |
| color: rgba(var(--v-theme-on-surface), 0.5); | |
| } | |
| .empty-text { | |
| font-size: 12px; | |
| color: rgba(var(--v-theme-on-surface), 0.5); | |
| } | |
| .file-name { | |
| font-weight: 600; | |
| word-break: break-word; | |
| } | |
| .upload-item { | |
| cursor: pointer; | |
| transition: background 0.2s ease; | |
| } | |
| .upload-item:hover, | |
| .upload-item.dragover { | |
| background: rgba(var(--v-theme-on-surface), 0.04); | |
| } | |
| </style> | |