diff --git a/cmd/scout-client/update_commands.go b/cmd/scout-client/update_commands.go index 94a756b..438b4a9 100644 --- a/cmd/scout-client/update_commands.go +++ b/cmd/scout-client/update_commands.go @@ -101,6 +101,20 @@ func uploadCommand(args []string) { if !isScout && (*name == "" || *version == "" || *target.platform == "" || *target.architecture == "") { fatal("\u666e\u901a\u5305\u5fc5\u987b\u6307\u5b9a --package\u3001--version\u3001--platform \u548c --architecture") } + packageTypeValue := strings.TrimSpace(*packageType) + channelValue := strings.TrimSpace(*channel) + statusValue := strings.TrimSpace(*status) + if !isScout { + if packageTypeValue == "" { + packageTypeValue = "full" + } + if channelValue == "" { + channelValue = "stable" + } + if statusValue == "" { + statusValue = "published" + } + } file, err := os.Open(*filePath) if err != nil { fatal("\u6253\u5f00\u5305\u6587\u4ef6\u5931\u8d25: %v", err) @@ -113,9 +127,9 @@ func uploadCommand(args []string) { Description: *description, Platform: *target.platform, Architecture: *target.architecture, - PackageType: *packageType, - Channel: *channel, - Status: *status, + PackageType: packageTypeValue, + Channel: channelValue, + Status: statusValue, UpgradeFrom: splitPackages(*upgradeFrom), FileName: filepath.Base(*filePath), File: file, diff --git a/frontend/src/views/Home/Home.css b/frontend/src/views/Home/Home.css index 188d037..1832131 100644 --- a/frontend/src/views/Home/Home.css +++ b/frontend/src/views/Home/Home.css @@ -89,8 +89,9 @@ h3 { } .toolbar { - display: flex; - align-items: flex-end; + display: grid; + grid-template-columns: minmax(180px, 1.4fr) repeat(4, minmax(120px, 1fr)) auto; + align-items: end; gap: 14px; margin-bottom: 18px; padding: 18px; @@ -125,6 +126,29 @@ input:focus { box-shadow: 0 0 0 3px rgba(101, 230, 194, 0.12); } +select { + width: 100%; + box-sizing: border-box; + border: 1px solid #2a3751; + border-radius: 10px; + background: #10192b; + color: #eaf0fa; + font: inherit; + padding: 12px 13px; + outline: none; +} + +select:focus { + border-color: #65e6c2; + box-shadow: 0 0 0 3px rgba(101, 230, 194, 0.12); +} + +input:disabled, +select:disabled { + cursor: not-allowed; + opacity: 0.55; +} + button, a { font: inherit; @@ -271,6 +295,12 @@ button:disabled { font-weight: 800; } +.target-count, +.target-summary { + color: #8c9ab4; + font-size: 11px; +} + .empty-state { min-height: 160px; display: grid; @@ -361,6 +391,10 @@ button:disabled { flex-direction: column; } + .toolbar { + display: flex; + } + .content-grid, .upload-layout { grid-template-columns: 1fr; diff --git a/frontend/src/views/Home/Home.js b/frontend/src/views/Home/Home.js index 07f98ed..a1c3102 100644 --- a/frontend/src/views/Home/Home.js +++ b/frontend/src/views/Home/Home.js @@ -1,4 +1,4 @@ -import { computed, ref } from 'vue' +import { computed, ref } from 'vue' const defaultApi = import.meta.env.VITE_API_BASE || (['5173', '5174'].includes(window.location.port) ? 'http://127.0.0.1:8080' : window.location.origin) const sessionKey = 'scout-session' @@ -13,6 +13,7 @@ export function useHome() { const resetForm = ref({ email: '', token: '', password: '' }) const adminUsers = ref([]) const search = ref('') + const filters = ref({ platform: '', architecture: '', channel: '', currentVersion: '' }) const packages = ref([]) const selectedPackage = ref(null) const versions = ref([]) @@ -22,6 +23,10 @@ export function useHome() { description: '', platform: '', architecture: '', + packageType: '', + channel: '', + status: '', + upgradeFrom: '', file: null, }) const loading = ref(false) @@ -34,6 +39,19 @@ export function useHome() { const isLoggedIn = computed(() => Boolean(user.value && session.value)) const canUpload = computed(() => user.value?.role === 'developer' || user.value?.role === 'admin') const isAdmin = computed(() => user.value?.role === 'admin') + const isScoutPackage = computed(() => uploadForm.value.file?.name.toLowerCase().endsWith('.scout')) + const platformOptions = ['windows', 'macos', 'linux', 'android', 'any'] + const architectureOptions = computed(() => { + const platform = filters.value.platform || uploadForm.value.platform + const options = { + windows: ['amd64', 'arm64', '386'], + macos: ['amd64', 'arm64'], + linux: ['amd64', 'arm64', '386', 'armv7'], + android: ['arm64', 'armv7', '386'], + } + return options[platform] || ['amd64', 'arm64', '386', 'armv7', 'universal'] + }) + const channelOptions = ['stable', 'beta', 'dev'] function roleText(role) { return { user: '普通用户', developer: '开发者', admin: '管理员' }[role] || role @@ -68,7 +86,13 @@ export function useHome() { loading.value = true error.value = '' try { - const query = search.value.trim() ? `?query=${encodeURIComponent(search.value.trim())}` : '' + const params = new URLSearchParams() + if (search.value.trim()) params.set('query', search.value.trim()) + if (filters.value.platform) params.set('platform', filters.value.platform) + if (filters.value.architecture) params.set('architecture', filters.value.architecture) + if (filters.value.channel) params.set('channel', filters.value.channel) + if (filters.value.currentVersion) params.set('current_version', filters.value.currentVersion) + const query = params.toString() ? `?${params.toString()}` : '' const payload = await request(`/api/v1/packages${query}`) packages.value = payload.packages || [] if (selectedPackage.value) { @@ -90,7 +114,13 @@ export function useHome() { selectedPackage.value = item error.value = '' try { - const payload = await request(`/api/v1/packages/${encodeURIComponent(item.name)}/versions`) + const params = new URLSearchParams() + if (filters.value.platform) params.set('platform', filters.value.platform) + if (filters.value.architecture) params.set('architecture', filters.value.architecture) + if (filters.value.channel) params.set('channel', filters.value.channel) + if (filters.value.currentVersion) params.set('current_version', filters.value.currentVersion) + const query = params.toString() ? `?${params.toString()}` : '' + const payload = await request(`/api/v1/packages/${encodeURIComponent(item.name)}/versions${query}`) versions.value = payload.versions || [] } catch (reason) { error.value = reason.message @@ -99,6 +129,12 @@ export function useHome() { function onFileChange(event) { uploadForm.value.file = event.target.files?.[0] || null + if (isScoutPackage.value) { + uploadForm.value.name = '' + uploadForm.value.version = '' + uploadForm.value.platform = '' + uploadForm.value.architecture = '' + } } async function uploadPackage() { @@ -106,16 +142,17 @@ export function useHome() { error.value = '请先登录开发者账户,或填写兼容开发者令牌' return } - if (!uploadForm.value.name || !uploadForm.value.version || !uploadForm.value.file) { - error.value = '请填写包名、版本并选择包文件' + if (!uploadForm.value.file || (!isScoutPackage.value && (!uploadForm.value.name || !uploadForm.value.version || !uploadForm.value.platform || !uploadForm.value.architecture))) { + error.value = isScoutPackage.value ? '请选择 Scout 包文件,包名、版本和目标将从清单读取' : '请填写包名、版本、平台、架构并选择包文件' return } uploading.value = true error.value = '' message.value = '' const formData = new FormData() + const fieldNames = { packageType: 'package_type', upgradeFrom: 'upgrade_from' } Object.entries(uploadForm.value).forEach(([key, value]) => { - if (value) formData.append(key, value) + if (value) formData.append(fieldNames[key] || key, value) }) try { const headers = session.value @@ -251,12 +288,26 @@ export function useHome() { } function downloadUrl(item) { - return endpoint(`/api/v1/packages/${encodeURIComponent(item.name)}/${encodeURIComponent(item.version)}/download`) + const params = new URLSearchParams() + if (filters.value.platform) params.set('platform', filters.value.platform) + if (filters.value.architecture) params.set('architecture', filters.value.architecture) + if (filters.value.currentVersion) params.set('current_version', filters.value.currentVersion) + const query = params.toString() ? `?${params.toString()}` : '' + return endpoint(`/api/v1/packages/${encodeURIComponent(item.name)}/${encodeURIComponent(item.version)}/download${query}`) } function startNotifications() { stopNotifications() - eventSource = new EventSource(endpoint('/api/v1/updates/stream')) + const params = new URLSearchParams() + if (packages.value.length) params.set('packages', packages.value.map((item) => item.name).join(',')) + if (filters.value.platform) params.set('platform', filters.value.platform) + if (filters.value.architecture) params.set('architecture', filters.value.architecture) + if (filters.value.channel) params.set('channel', filters.value.channel) + if (selectedPackage.value && filters.value.currentVersion) { + params.set('versions', `${selectedPackage.value.name}:${filters.value.currentVersion}`) + } + const query = params.toString() ? `?${params.toString()}` : '' + eventSource = new EventSource(endpoint(`/api/v1/updates/stream${query}`)) eventSource.addEventListener('package_updated', async (event) => { const payload = JSON.parse(event.data) message.value = `收到更新通知:${payload.package.name} ${payload.package.version}` @@ -282,6 +333,7 @@ export function useHome() { resetForm, adminUsers, search, + filters, packages, selectedPackage, versions, @@ -294,6 +346,10 @@ export function useHome() { isLoggedIn, canUpload, isAdmin, + isScoutPackage, + platformOptions, + architectureOptions, + channelOptions, roleText, loadPackages, selectPackage, diff --git a/frontend/src/views/Home/Home.vue b/frontend/src/views/Home/Home.vue index 3b6aecc..ea97436 100644 --- a/frontend/src/views/Home/Home.vue +++ b/frontend/src/views/Home/Home.vue @@ -1,4 +1,4 @@ -