diff --git a/electron/main/backend/submitter.ts b/electron/main/backend/submitter.ts index f1cccc45..0bfd8e66 100644 --- a/electron/main/backend/submitter.ts +++ b/electron/main/backend/submitter.ts @@ -698,11 +698,18 @@ export function registerSubmitterHandlers( ipcMain.handle( "search-history-app", - async (_event, pkgname: string, useMirror = false) => { + async (_event, pkgname: string, baseUrl?: string) => { try { - const baseUrl = useMirror - ? "https://mirrors.sdu.edu.cn/spark-store" - : "https://spk-json.spark-app.store"; + let resolvedBaseUrl = baseUrl || "https://erotica.spark-app.store"; + // 开发模式下来自 Vite 代理的路径(如 /local_amd64-store), + // 主进程的 fetch() 无法解析相对 URL,需要转为实际地址 + if (resolvedBaseUrl.startsWith("/")) { + logger.info( + { devPath: resolvedBaseUrl }, + "[Submitter] Dev proxy path detected, resolving to production URL", + ); + resolvedBaseUrl = "https://erotica.spark-app.store"; + } const storeArchs = [ "amd64-store", "arm64-store", @@ -716,7 +723,7 @@ export function registerSubmitterHandlers( // 为每个架构目录获取各自的分类列表(不同目录可能有不同分类,如 apm-extensions 只在 *-apm 下有) const archCategoriesMap = new Map(); for (const arch of storeArchs) { - const cats = await fetchCategoriesFromCdn(baseUrl, arch); + const cats = await fetchCategoriesFromCdn(resolvedBaseUrl, arch); archCategoriesMap.set(arch, cats); } @@ -726,8 +733,7 @@ export function registerSubmitterHandlers( logger.info( { pkgname, - useMirror, - baseUrl, + baseUrl: resolvedBaseUrl, storeArchs, archCategoryCounts: Array.from(archCategoriesMap.entries()).map( ([a, c]) => ({ arch: a, categoryCount: c.length }), @@ -741,7 +747,7 @@ export function registerSubmitterHandlers( for (const arch of storeArchs) { const categories = archCategoriesMap.get(arch) || FALLBACK_CATEGORIES; for (const category of categories) { - const url = `${baseUrl}/${arch}/${category}/${pkgname}/app.json`; + const url = `${resolvedBaseUrl}/${arch}/${category}/${pkgname}/app.json`; logger.info( { arch, category, url }, "[Submitter] Starting search request", @@ -780,7 +786,7 @@ export function registerSubmitterHandlers( "[Submitter] Found matching item", ); - let iconUrl = json.icons || json.icon || ""; + const iconUrl = json.icons || json.icon || ""; let imgs = json.imgUrls || json.imgs || json.img_urls || []; if (typeof imgs === "string") { @@ -805,27 +811,6 @@ export function registerSubmitterHandlers( } } - if (iconUrl && typeof iconUrl === "string") { - if (useMirror) { - iconUrl = iconUrl.replace( - "spk-json.spark-app.store", - "mirrors.sdu.edu.cn/spark-store", - ); - } - } - - if (Array.isArray(imgs)) { - imgs = imgs.map((img: string) => { - if (useMirror && typeof img === "string") { - return img.replace( - "spk-json.spark-app.store", - "mirrors.sdu.edu.cn/spark-store", - ); - } - return img; - }); - } - results.push({ id: json.id || json.Id || 0, name: json.name || json.Name || "", @@ -847,11 +832,24 @@ export function registerSubmitterHandlers( "[Submitter] Added to results", ); } + } else { + logger.warn( + { + arch, + category, + url, + status: response.status, + statusText: response.statusText, + }, + "[Submitter] Non-ok response", + ); } }) .catch((error) => { + const errMsg = + error instanceof Error ? error.message : String(error); logger.warn( - { arch, category, error }, + { arch, category, url, error: errMsg }, "[Submitter] Request failed or exception caught", ); }); @@ -1275,6 +1273,35 @@ export function registerSubmitterHandlers( ); continue; } + } else if (screenshot.startsWith("data:")) { + logger.info( + "[Submitter] Screenshot is a Base64 data URL, decoding", + ); + const base64Data = screenshot.split(",")[1]; + if (!base64Data) { + logger.warn("[Submitter] Invalid screenshot data URL, skipping"); + continue; + } + const tempDir = fs.mkdtempSync( + path.join(os.tmpdir(), "spark-store-submitter-"), + ); + screenshotFilePath = path.join(tempDir, `screen_${i + 1}.png`); + try { + fs.writeFileSync( + screenshotFilePath, + Buffer.from(base64Data, "base64"), + ); + logger.info( + { screenshotFilePath }, + "[Submitter] Screenshot decoded from data URL", + ); + } catch (err) { + logger.error( + { err }, + "[Submitter] Failed to decode screenshot data URL", + ); + continue; + } } if (fs.existsSync(screenshotFilePath)) { @@ -1313,7 +1340,8 @@ export function registerSubmitterHandlers( if ( screenshot.startsWith("http://") || - screenshot.startsWith("https://") + screenshot.startsWith("https://") || + screenshot.startsWith("data:") ) { fs.unlinkSync(screenshotFilePath); fs.rmdirSync(path.dirname(screenshotFilePath)); diff --git a/src/components/SubmitterWindow.vue b/src/components/SubmitterWindow.vue index 5a30bc43..4ecb6ad7 100644 --- a/src/components/SubmitterWindow.vue +++ b/src/components/SubmitterWindow.vue @@ -384,6 +384,55 @@ + +
+

+ 请完善以下信息: +

+ +
+
import { ref, reactive, computed, watch } from "vue"; +import { APM_STORE_BASE_URL } from "@/global/storeConfig"; interface HistoryArchInfo { id: number; @@ -755,14 +805,55 @@ const tagsList = ref([]); const selectedTags = ref([]); const selectedTagValue = ref(""); +const isValidUrl = (url: string): boolean => { + if (!url.trim()) return false; + try { + const parsed = new URL(url); + return parsed.protocol === "http:" || parsed.protocol === "https:"; + } catch { + return false; + } +}; + const isFormValid = computed(() => { - return ( + const hasRequiredFields = formData.name.trim() && formData.pkgname.trim() && formData.version.trim() && formData.category.trim() && formData.debFilePath && - formData.remark.trim() + formData.remark.trim() && + formData.website.trim() && + formData.tags.trim(); + + if (!hasRequiredFields) return false; + + // 官网:必须为有效的 http/https URL + if (!isValidUrl(formData.website.trim())) { + return false; + } + + // 截图:至少一张,且每张都是有效的 data URL 或 http/https URL + if (formData.screenshots.length === 0) return false; + for (const screenshot of formData.screenshots) { + if ( + !screenshot.startsWith("data:") && + !screenshot.startsWith("http://") && + !screenshot.startsWith("https://") + ) { + return false; + } + } + + return true; +}); + +const hasUserStartedFilling = computed(() => { + return ( + formData.name.trim() || + formData.pkgname.trim() || + formData.debFilePath || + formData.screenshots.length > 0 ); }); @@ -952,8 +1043,6 @@ const selectDebFile = async () => { } }; -const useMirror = ref(true); - const searchHistoryApp = async () => { console.log( "[Submitter] ============== SEARCHING HISTORY INFO ==============", @@ -962,7 +1051,6 @@ const searchHistoryApp = async () => { "[Submitter] pkgname is not empty, searching history with:", formData.pkgname, ); - console.log("[Submitter] Using mirror:", useMirror.value); console.log( "[Submitter] Calling IPC: search-history-app with pkgname:", @@ -971,7 +1059,7 @@ const searchHistoryApp = async () => { const historyResult = await window.ipcRenderer.invoke( "search-history-app", formData.pkgname, - useMirror.value, + APM_STORE_BASE_URL, ); console.log( "[Submitter] Received history search response:", @@ -1079,6 +1167,9 @@ const searchHistoryApp = async () => { historyResult?.message, ); } + // 未找到历史记录时清空旧数据,避免上一个应用的残留 + availableArchs.value = []; + showArchDialog.value = false; } }; @@ -1261,9 +1352,12 @@ const selectArch = async (arch: HistoryArchInfo) => { console.log("[Submitter] Tags loaded from history:", selectedTags.value); } - const baseUrl = useMirror.value - ? `https://mirrors.sdu.edu.cn/spark-store/${arch.store}/${arch.category}/${arch.pkgname}` - : `https://spk-json.spark-app.store/${arch.store}/${arch.category}/${arch.pkgname}`; + // 开发模式下来自 Vite 代理的路径(如 /local_amd64-store), + // 后端 fetch/fs 无法处理,需要解析为实际 CDN 地址 + const resolvedBaseUrl = APM_STORE_BASE_URL.startsWith("/") + ? "https://erotica.spark-app.store" + : APM_STORE_BASE_URL; + const baseUrl = `${resolvedBaseUrl}/${arch.store}/${arch.category}/${arch.pkgname}`; console.log( "[Submitter] Building icon and screenshot URLs with baseUrl:", @@ -1391,6 +1485,12 @@ const resetForm = () => { uploadStage.value = ""; uploadStageMessage.value = ""; uploadStages.value = []; + selectedTags.value = []; + selectedTagValue.value = ""; + availableArchs.value = []; + showArchDialog.value = false; + currentDebArch.value = ""; + isSearchingHistory.value = false; }; const closeSubmitSuccessModal = () => {