From f585be0dcdb7be2c4bdaf5e509a5856e30c256da Mon Sep 17 00:00:00 2001 From: shenmo Date: Thu, 16 Jul 2026 15:56:13 +0800 Subject: [PATCH] feat(submitter): add form validation tips and improve submission checks 1. add real-time form validation prompt component for submission page 2. add URL validation for official website and screenshot format checks 3. refactor form validity calculation and user input status judgment 4. replace mirror switch with configurable base URL, support dev proxy path handling 5. add processing logic for base64 format screenshots 6. optimize history search and form reset logic --- electron/main/backend/submitter.ts | 92 ++++++++++++++-------- src/components/SubmitterWindow.vue | 118 ++++++++++++++++++++++++++--- 2 files changed, 169 insertions(+), 41 deletions(-) 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 @@ + +
+

+ 请完善以下信息: +

+
    +
  • + 应用名称 +
  • +
  • + 包名 +
  • +
  • + 版本号 +
  • +
  • + 分类 +
  • +
  • + 安装包 +
  • +
  • + 测试情况 +
  • +
  • + 官网地址 +
  • +
  • + 官网地址格式不正确(需以 + http:// 或 https:// 开头) +
  • +
  • + 标签 +
  • +
  • + 至少上传一张截图 +
  • +
+
+
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 = () => {