mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-08-06 11:23:58 +08:00
1927 lines
64 KiB
Vue
1927 lines
64 KiB
Vue
<template>
|
||
<div
|
||
class="h-screen overflow-y-auto bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100"
|
||
>
|
||
<div
|
||
class="submitter-titlebar sticky top-0 z-30 border-b border-slate-200/70 bg-white px-4 py-3 dark:border-slate-800/70 dark:bg-slate-900"
|
||
>
|
||
<div class="flex items-center justify-between">
|
||
<div class="flex items-center gap-3">
|
||
<div
|
||
class="h-8 w-8 rounded-lg bg-blue-500 flex items-center justify-center"
|
||
>
|
||
<i class="fas fa-upload text-white"></i>
|
||
</div>
|
||
<h1 class="text-lg font-semibold">投稿应用</h1>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<button
|
||
type="button"
|
||
class="submitter-close-button inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-slate-800"
|
||
@click="openSubmissionQueue"
|
||
>
|
||
<i class="fas fa-list"></i>
|
||
查看当前投稿队列
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="submitter-close-button inline-flex h-8 w-8 items-center justify-center rounded-lg text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||
@click="closeWindow"
|
||
>
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="p-6 max-w-2xl mx-auto">
|
||
<div class="space-y-6">
|
||
<div>
|
||
<label
|
||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||
>安装包 (deb)</label
|
||
>
|
||
<div
|
||
class="border-2 border-dashed border-slate-200 rounded-lg p-8 text-center hover:border-blue-500 transition-colors cursor-pointer dark:border-slate-700"
|
||
@click="selectDebFile"
|
||
@drop="handleDrop"
|
||
@dragover="handleDragOver"
|
||
@dragenter="handleDragEnter"
|
||
@dragleave="handleDragLeave"
|
||
>
|
||
<input
|
||
ref="debFileInput"
|
||
type="file"
|
||
accept=".deb"
|
||
class="hidden"
|
||
@change="handleDebFileSelect"
|
||
/>
|
||
<div
|
||
v-if="isParsingDeb || isSearchingHistory"
|
||
class="flex flex-col items-center"
|
||
>
|
||
<div
|
||
class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4"
|
||
></div>
|
||
<p class="text-slate-600 dark:text-slate-400">
|
||
{{
|
||
isParsingDeb
|
||
? "正在解析 deb 文件..."
|
||
: "正在从服务器查询已上架信息..."
|
||
}}
|
||
</p>
|
||
</div>
|
||
<div v-else>
|
||
<i class="fas fa-cloud-upload text-4xl text-slate-400 mb-4"></i>
|
||
<p class="text-slate-600 dark:text-slate-400">点击浏览</p>
|
||
<p v-if="formData.debFilePath" class="mt-2 text-sm text-blue-500">
|
||
{{ formData.debFilePath.split("/").pop() }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-if="debParseError"
|
||
class="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg dark:bg-yellow-900/20 dark:border-yellow-800"
|
||
>
|
||
<p class="text-yellow-700 dark:text-yellow-400 text-sm">
|
||
{{ debParseError }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label
|
||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||
>应用名称</label
|
||
>
|
||
<input
|
||
v-model="formData.name"
|
||
type="text"
|
||
placeholder="显示在商店中的名称"
|
||
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label
|
||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||
>包名 (pkgname)</label
|
||
>
|
||
<input
|
||
v-model="formData.pkgname"
|
||
type="text"
|
||
disabled
|
||
title="包名由 deb 文件自动解析,不可手动修改"
|
||
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-slate-100 dark:bg-slate-700 dark:border-slate-600 text-slate-500 dark:text-slate-400 cursor-not-allowed"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label
|
||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||
>版本号</label
|
||
>
|
||
<input
|
||
v-model="formData.version"
|
||
type="text"
|
||
disabled
|
||
title="版本号由 deb 文件自动解析,不可手动修改"
|
||
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-slate-100 dark:bg-slate-700 dark:border-slate-600 text-slate-500 dark:text-slate-400 cursor-not-allowed"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label
|
||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||
>作者</label
|
||
>
|
||
<input
|
||
v-model="formData.author"
|
||
type="text"
|
||
placeholder="原作者信息"
|
||
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label
|
||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||
>官网地址</label
|
||
>
|
||
<input
|
||
v-model="formData.website"
|
||
type="url"
|
||
placeholder="https://example.com"
|
||
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label
|
||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||
>贡献者</label
|
||
>
|
||
<input
|
||
v-model="formData.contributor"
|
||
type="text"
|
||
placeholder="你的名字"
|
||
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label
|
||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||
>联系邮箱</label
|
||
>
|
||
<input
|
||
v-model="formData.mail"
|
||
type="email"
|
||
placeholder="your@email.com"
|
||
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label
|
||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||
>应用图标</label
|
||
>
|
||
<div
|
||
class="border-2 border-dashed border-slate-200 rounded-lg p-8 text-center hover:border-blue-500 transition-colors cursor-pointer dark:border-slate-700"
|
||
@click="selectIconFile"
|
||
@drop.prevent="handleIconDrop"
|
||
@dragover.prevent
|
||
>
|
||
<input
|
||
ref="iconFileInput"
|
||
type="file"
|
||
accept=".png"
|
||
class="hidden"
|
||
@change="handleIconFileSelect"
|
||
/>
|
||
<div v-if="iconPreview" class="mb-4">
|
||
<img
|
||
:src="iconPreview"
|
||
alt="图标预览"
|
||
class="w-24 h-24 mx-auto rounded-lg object-contain border border-slate-300 dark:border-slate-600"
|
||
/>
|
||
</div>
|
||
<i v-else class="fas fa-image text-4xl text-slate-400 mb-4"></i>
|
||
<p class="text-slate-600 dark:text-slate-400">
|
||
{{ iconPreview ? "点击浏览更换图标" : "点击浏览" }}
|
||
</p>
|
||
<p v-if="iconFileName" class="mt-2 text-sm text-blue-500">
|
||
{{ iconFileName }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label
|
||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||
>截图(最多5张)</label
|
||
>
|
||
<p class="mb-2 text-xs text-slate-500 dark:text-slate-400">
|
||
可点击添加、拖放或直接 Ctrl+V 粘贴 PNG 截图
|
||
</p>
|
||
<div
|
||
tabindex="0"
|
||
class="grid grid-cols-5 gap-3 rounded-lg border-2 border-dashed border-transparent p-2 transition-colors hover:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
@paste="handleScreenshotPaste"
|
||
@drop="handleScreenshotDrop"
|
||
@dragover.prevent
|
||
@dragenter.prevent
|
||
@dragleave.prevent
|
||
>
|
||
<div
|
||
v-for="(screenshot, index) in formData.screenshots"
|
||
:key="index"
|
||
class="relative aspect-square rounded-lg overflow-hidden border-2 border-slate-200 dark:border-slate-700"
|
||
>
|
||
<img :src="screenshot" class="w-full h-full object-cover" />
|
||
<button
|
||
type="button"
|
||
class="absolute top-1 right-1 w-6 h-6 bg-black/50 rounded-full flex items-center justify-center text-white hover:bg-black/70"
|
||
@click="removeScreenshot(index)"
|
||
>
|
||
<i class="fas fa-times text-xs"></i>
|
||
</button>
|
||
</div>
|
||
<div
|
||
v-if="formData.screenshots.length < 5"
|
||
class="aspect-square rounded-lg border-2 border-dashed border-slate-200 dark:border-slate-700 flex items-center justify-center cursor-pointer hover:border-blue-500 transition-colors"
|
||
@click="addScreenshot"
|
||
>
|
||
<i class="fas fa-plus text-slate-400"></i>
|
||
</div>
|
||
</div>
|
||
<input
|
||
ref="screenshotInput"
|
||
type="file"
|
||
accept=".png"
|
||
multiple
|
||
class="hidden"
|
||
@change="handleScreenshotSelect"
|
||
/>
|
||
<p
|
||
v-if="mediaError"
|
||
class="mt-2 text-sm text-red-600 dark:text-red-400"
|
||
>
|
||
{{ mediaError }}
|
||
</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label
|
||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||
>应用描述</label
|
||
>
|
||
<textarea
|
||
v-model="formData.description"
|
||
rows="4"
|
||
placeholder="介绍你的应用功能和特点..."
|
||
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||
></textarea>
|
||
</div>
|
||
|
||
<div>
|
||
<label
|
||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||
>标签</label
|
||
>
|
||
<div class="flex flex-wrap gap-2 mb-2">
|
||
<span
|
||
v-for="tag in selectedTags"
|
||
:key="tag.value"
|
||
class="inline-flex items-center px-2.5 py-1 rounded-full text-sm bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
|
||
>
|
||
{{ tag.name }}
|
||
<button
|
||
type="button"
|
||
class="ml-1 hover:text-blue-600 dark:hover:text-blue-300"
|
||
@click="removeTag(tag.value)"
|
||
>
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</span>
|
||
</div>
|
||
<select
|
||
v-model="selectedTagValue"
|
||
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
@change="addTag"
|
||
>
|
||
<option value="">请选择标签</option>
|
||
<option v-for="tag in tagsList" :key="tag.value" :value="tag.value">
|
||
{{ tag.name }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label
|
||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||
>测试情况</label
|
||
>
|
||
<input
|
||
v-model="formData.remark"
|
||
type="text"
|
||
placeholder="写明在何种平台的测试情况"
|
||
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label
|
||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||
>分类</label
|
||
>
|
||
<div v-if="isLoadingCategories" class="text-sm text-slate-400 py-2">
|
||
正在加载分类列表...
|
||
</div>
|
||
<div
|
||
v-else-if="categoriesLoadError"
|
||
class="p-3 bg-yellow-50 border border-yellow-200 rounded-lg dark:bg-yellow-900/20 dark:border-yellow-800"
|
||
>
|
||
<p class="text-yellow-700 dark:text-yellow-400 text-sm">
|
||
分类加载失败: {{ categoriesLoadError }}
|
||
</p>
|
||
</div>
|
||
<select
|
||
v-else
|
||
v-model="formData.category"
|
||
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
>
|
||
<option value="" disabled>请选择分类</option>
|
||
<option
|
||
v-for="category in categoriesList"
|
||
:key="category.id"
|
||
:value="category.value"
|
||
>
|
||
{{ category.name }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="flex gap-3 pt-4">
|
||
<button
|
||
type="button"
|
||
class="px-5 py-3 rounded-lg border border-slate-200 bg-white text-slate-700 font-medium hover:bg-slate-50 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-700 transition-colors"
|
||
@click="resetForm"
|
||
>
|
||
重置
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="flex-1 px-5 py-3 rounded-lg border-2 border-emerald-500 bg-emerald-50 text-emerald-700 font-medium hover:bg-emerald-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors dark:bg-emerald-900/20 dark:border-emerald-600 dark:text-emerald-400 dark:hover:bg-emerald-900/30"
|
||
:disabled="isPackaging || !isFormValid"
|
||
@click="showArchPackDialog = true"
|
||
>
|
||
<span
|
||
v-if="isPackaging"
|
||
class="flex items-center justify-center gap-2"
|
||
>
|
||
<i class="fas fa-spinner fa-spin"></i>
|
||
打包中...
|
||
</span>
|
||
<span v-else class="flex items-center justify-center gap-2">
|
||
<i class="fas fa-box-archive"></i>
|
||
打包 tar.gz
|
||
</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="flex-1 px-5 py-3 rounded-lg bg-blue-500 text-white font-medium hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||
:disabled="isSubmitting || !isFormValid"
|
||
@click="submitForm"
|
||
>
|
||
<span
|
||
v-if="isSubmitting"
|
||
class="flex items-center justify-center gap-2"
|
||
>
|
||
<i class="fas fa-spinner fa-spin"></i>
|
||
提交中...
|
||
</span>
|
||
<span v-else class="flex items-center justify-center gap-2">
|
||
<i class="fas fa-paper-plane"></i>
|
||
提交投稿
|
||
</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 表单校验提示 -->
|
||
<div
|
||
v-if="!isFormValid && hasUserStartedFilling"
|
||
class="mt-3 p-3 bg-amber-50 border border-amber-200 rounded-lg dark:bg-amber-900/20 dark:border-amber-800"
|
||
>
|
||
<p
|
||
class="text-sm font-medium text-amber-700 dark:text-amber-400 mb-1.5"
|
||
>
|
||
请完善以下信息:
|
||
</p>
|
||
<ul class="text-xs text-amber-600 dark:text-amber-500 space-y-0.5">
|
||
<li v-if="!formData.name.trim()">
|
||
<i class="fas fa-times-circle mr-1"></i> 应用名称
|
||
</li>
|
||
<li v-if="!formData.pkgname.trim()">
|
||
<i class="fas fa-times-circle mr-1"></i> 包名
|
||
</li>
|
||
<li v-if="!formData.version.trim()">
|
||
<i class="fas fa-times-circle mr-1"></i> 版本号
|
||
</li>
|
||
<li v-if="!formData.category.trim()">
|
||
<i class="fas fa-times-circle mr-1"></i> 分类
|
||
</li>
|
||
<li v-if="!formData.debFilePath">
|
||
<i class="fas fa-times-circle mr-1"></i> 安装包
|
||
</li>
|
||
<li v-if="!formData.remark.trim()">
|
||
<i class="fas fa-times-circle mr-1"></i> 测试情况
|
||
</li>
|
||
<li v-if="!formData.website.trim()">
|
||
<i class="fas fa-times-circle mr-1"></i> 官网地址
|
||
</li>
|
||
<li
|
||
v-if="
|
||
formData.website.trim() && !isValidUrl(formData.website.trim())
|
||
"
|
||
>
|
||
<i class="fas fa-times-circle mr-1"></i> 官网地址格式不正确(需以
|
||
http:// 或 https:// 开头)
|
||
</li>
|
||
<li v-if="!formData.tags.trim()">
|
||
<i class="fas fa-times-circle mr-1"></i> 标签
|
||
</li>
|
||
<li v-if="formData.screenshots.length === 0">
|
||
<i class="fas fa-times-circle mr-1"></i> 至少上传一张截图
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div
|
||
v-if="isSubmitting || isPackaging"
|
||
class="p-4 bg-blue-50 border border-blue-200 rounded-lg dark:bg-blue-900/20 dark:border-blue-800"
|
||
>
|
||
<div class="flex items-center gap-3 mb-3">
|
||
<i
|
||
class="fas fa-spinner fa-spin text-blue-500 dark:text-blue-400"
|
||
></i>
|
||
<span class="text-blue-700 dark:text-blue-400 font-medium text-sm">
|
||
{{ isPackaging ? "正在打包..." : uploadStage || "正在提交..." }}
|
||
</span>
|
||
</div>
|
||
<div class="space-y-2">
|
||
<div
|
||
class="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-2.5"
|
||
>
|
||
<div
|
||
class="h-2.5 rounded-full transition-all duration-300"
|
||
:class="isPackaging ? 'bg-emerald-500' : 'bg-blue-500'"
|
||
:style="{ width: uploadProgress + '%' }"
|
||
></div>
|
||
</div>
|
||
<p class="text-xs text-slate-500 dark:text-slate-400">
|
||
{{ uploadStageMessage || "准备中..." }}
|
||
</p>
|
||
<div
|
||
v-if="uploadStages.length > 0 && !isPackaging"
|
||
class="mt-3 space-y-1.5"
|
||
>
|
||
<div
|
||
v-for="stage in uploadStages"
|
||
:key="stage.stage"
|
||
class="flex items-center gap-2 text-xs"
|
||
>
|
||
<i
|
||
v-if="stage.progress >= 100"
|
||
class="fas fa-check-circle text-green-500"
|
||
></i>
|
||
<i
|
||
v-else-if="stage.progress > 0"
|
||
class="fas fa-spinner fa-spin text-blue-500"
|
||
></i>
|
||
<i
|
||
v-else
|
||
class="far fa-circle text-slate-300 dark:text-slate-600"
|
||
></i>
|
||
<span
|
||
:class="
|
||
stage.progress >= 100
|
||
? 'text-green-600 dark:text-green-400'
|
||
: stage.progress > 0
|
||
? 'text-blue-600 dark:text-blue-400'
|
||
: 'text-slate-400 dark:text-slate-500'
|
||
"
|
||
>{{ stage.label }}</span
|
||
>
|
||
<span
|
||
v-if="stage.progress > 0 && stage.progress < 100"
|
||
class="text-slate-400"
|
||
>{{ Math.floor(stage.progress) }}%</span
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="showSubmitSuccessModal" class="relative">
|
||
<div
|
||
class="p-6 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-700 shadow-lg"
|
||
>
|
||
<div class="flex items-center gap-3 mb-4">
|
||
<div
|
||
class="w-10 h-10 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center"
|
||
>
|
||
<i class="fas fa-check-circle text-green-500 text-xl"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="font-semibold text-slate-900 dark:text-slate-100">
|
||
投稿提交成功!
|
||
</h3>
|
||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||
我们会尽快审核你的应用
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="flex gap-3">
|
||
<button
|
||
type="button"
|
||
class="flex-1 px-4 py-2 rounded-lg bg-blue-500 text-white font-medium hover:bg-blue-600 transition-colors"
|
||
@click="closeSubmitSuccessModal"
|
||
>
|
||
关闭
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="flex-1 px-4 py-2 rounded-lg border border-slate-200 bg-white text-slate-700 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
|
||
@click="continueSubmission"
|
||
>
|
||
继续投递
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="submitError"
|
||
class="p-4 bg-red-50 border border-red-200 rounded-lg dark:bg-red-900/20 dark:border-red-800"
|
||
>
|
||
<div class="flex items-center gap-2 text-red-700 dark:text-red-400">
|
||
<i class="fas fa-exclamation-circle"></i>
|
||
<span>{{ submitError }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="packageSuccess"
|
||
class="p-4 bg-emerald-50 border border-emerald-200 rounded-lg dark:bg-emerald-900/20 dark:border-emerald-800"
|
||
>
|
||
<div
|
||
class="flex items-center gap-2 text-emerald-700 dark:text-emerald-400"
|
||
>
|
||
<i class="fas fa-check-circle"></i>
|
||
<span
|
||
>打包完成!文件已保存到: {{ packageResult?.tarFileName }}</span
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="packageError"
|
||
class="p-4 bg-red-50 border border-red-200 rounded-lg dark:bg-red-900/20 dark:border-red-800"
|
||
>
|
||
<div class="flex items-center gap-2 text-red-700 dark:text-red-400">
|
||
<i class="fas fa-exclamation-circle"></i>
|
||
<span>{{ packageError }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<Teleport to="body">
|
||
<div
|
||
v-if="showArchPackDialog"
|
||
data-submitter-arch-pack-dialog
|
||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||
>
|
||
<div
|
||
class="absolute inset-0 bg-black/50"
|
||
@click="showArchPackDialog = false"
|
||
></div>
|
||
<div
|
||
class="relative bg-white dark:bg-slate-900 rounded-xl shadow-2xl w-full max-w-md p-6"
|
||
>
|
||
<div class="flex items-center justify-between mb-6">
|
||
<h2
|
||
class="text-lg font-semibold text-slate-900 dark:text-slate-100"
|
||
>
|
||
选择打包架构
|
||
</h2>
|
||
<button
|
||
type="button"
|
||
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
|
||
@click="showArchPackDialog = false"
|
||
>
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<p class="text-slate-600 dark:text-slate-400 mb-4">
|
||
请选择目标架构以生成对应的 tar.gz 包:
|
||
</p>
|
||
|
||
<div class="space-y-3 mb-6">
|
||
<button
|
||
v-for="arch in packArchOptions"
|
||
:key="arch.store"
|
||
type="button"
|
||
class="w-full p-4 rounded-lg border-2 border-slate-200 dark:border-slate-700 hover:border-emerald-500 transition-colors text-left"
|
||
@click="selectPackArch(arch)"
|
||
>
|
||
<div class="flex items-center gap-2">
|
||
<span
|
||
class="rounded-full px-2 py-0.5 text-xs font-medium"
|
||
:class="
|
||
isSparkHistoryStore(arch.store)
|
||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
|
||
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
||
"
|
||
>
|
||
{{ isSparkHistoryStore(arch.store) ? "Spark" : "APM" }}
|
||
</span>
|
||
<span class="font-medium text-slate-900 dark:text-slate-100">
|
||
{{ getArchDisplayName(arch.store) }}
|
||
</span>
|
||
</div>
|
||
<div class="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
||
输出: {{ formData.pkgname.toLowerCase() }}-{{
|
||
arch.store
|
||
}}.tar.gz
|
||
</div>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="flex gap-3">
|
||
<button
|
||
type="button"
|
||
class="flex-1 px-4 py-2 rounded-lg border border-slate-200 bg-white text-slate-700 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700"
|
||
@click="showArchPackDialog = false"
|
||
>
|
||
取消
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
|
||
<Teleport to="body">
|
||
<div
|
||
v-if="showArchDialog"
|
||
data-submitter-arch-dialog
|
||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||
>
|
||
<div
|
||
class="absolute inset-0 bg-black/50"
|
||
@click="showArchDialog = false"
|
||
></div>
|
||
<div
|
||
class="relative bg-white dark:bg-slate-900 rounded-xl shadow-2xl w-full max-w-md p-6"
|
||
>
|
||
<div class="flex items-center justify-between mb-6">
|
||
<h2
|
||
class="text-lg font-semibold text-slate-900 dark:text-slate-100"
|
||
>
|
||
选择历史架构信息
|
||
</h2>
|
||
<button
|
||
type="button"
|
||
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
|
||
@click="showArchDialog = false"
|
||
>
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<p class="text-slate-600 dark:text-slate-400 mb-4">
|
||
检测到该包名已有历史投稿记录,请选择架构以获取历史信息:
|
||
</p>
|
||
|
||
<div class="space-y-3 mb-6">
|
||
<button
|
||
v-for="arch in sortedArchs"
|
||
:key="arch.store"
|
||
type="button"
|
||
class="w-full p-4 rounded-lg border-2 transition-colors text-left"
|
||
:class="archOriginBorderClass(arch.store)"
|
||
@click="selectArch(arch)"
|
||
>
|
||
<div class="flex items-center gap-2">
|
||
<span class="font-medium text-slate-900 dark:text-slate-100">
|
||
{{ getArchDisplayName(arch.store) }}
|
||
</span>
|
||
<span
|
||
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
|
||
:class="getOriginBadgeClass(arch.store)"
|
||
>
|
||
{{ getOriginLabel(arch.store) }}
|
||
</span>
|
||
</div>
|
||
<div class="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
||
版本: {{ arch.version }} | 分类: {{ arch.category }}
|
||
</div>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="flex gap-3">
|
||
<button
|
||
type="button"
|
||
class="flex-1 px-4 py-2 rounded-lg border border-slate-200 bg-white text-slate-700 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700"
|
||
@click="showArchDialog = false"
|
||
>
|
||
跳过
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, watch } from "vue";
|
||
import { APM_STORE_BASE_URL } from "@/global/storeConfig";
|
||
|
||
interface HistoryArchInfo {
|
||
id: number;
|
||
name: string;
|
||
pkgname: string;
|
||
version: string;
|
||
store: string;
|
||
author: string;
|
||
contributor: string;
|
||
website: string;
|
||
category: string;
|
||
tags: string;
|
||
more: string;
|
||
icon: string;
|
||
imgs: string[];
|
||
}
|
||
|
||
const debFileInput = ref<HTMLInputElement | null>(null);
|
||
const iconFileInput = ref<HTMLInputElement | null>(null);
|
||
const screenshotInput = ref<HTMLInputElement | null>(null);
|
||
|
||
const formData = reactive({
|
||
name: "",
|
||
pkgname: "",
|
||
version: "",
|
||
author: "",
|
||
contributor: "",
|
||
mail: "",
|
||
website: "",
|
||
debFilePath: "",
|
||
iconPath: "",
|
||
screenshots: [] as string[],
|
||
description: "",
|
||
tags: "",
|
||
category: "",
|
||
categoryId: 0,
|
||
remark: "",
|
||
});
|
||
|
||
// 当 category 变化时,从 categoriesList 中同步 categoryId
|
||
watch(
|
||
() => formData.category,
|
||
(val) => {
|
||
const found = categoriesList.value.find((c) => c.value === val);
|
||
formData.categoryId = found?.id ?? 0;
|
||
},
|
||
);
|
||
|
||
const isSubmitting = ref(false);
|
||
const submitSuccess = ref(false);
|
||
const submitError = ref("");
|
||
const showSubmitSuccessModal = ref(false);
|
||
const isParsingDeb = ref(false);
|
||
const debParseError = ref("");
|
||
const isSearchingHistory = ref(false);
|
||
const showArchDialog = ref(false);
|
||
const availableArchs = ref<HistoryArchInfo[]>([]);
|
||
const currentDebArch = ref("");
|
||
const mediaError = ref("");
|
||
const iconPreview = ref("");
|
||
const iconFileName = ref("");
|
||
|
||
const uploadProgress = ref(0);
|
||
const uploadStage = ref("");
|
||
const uploadStageMessage = ref("");
|
||
const uploadStages = ref<{ stage: string; label: string; progress: number }[]>(
|
||
[],
|
||
);
|
||
|
||
const isLoadingCategories = ref(false);
|
||
const categoriesLoadError = ref("");
|
||
|
||
const isPackaging = ref(false);
|
||
const packageSuccess = ref(false);
|
||
const packageError = ref("");
|
||
const showArchPackDialog = ref(false);
|
||
const packageResult = ref<{
|
||
tarPath: string;
|
||
tempDir: string;
|
||
tarFileName: string;
|
||
} | null>(null);
|
||
|
||
const packArchOptions = [
|
||
{ store: "store", label: "AMD64 (x86_64)" },
|
||
{ store: "aarch64-store", label: "ARM64 (aarch64)" },
|
||
{ store: "loong64-store", label: "LoongArch64" },
|
||
];
|
||
|
||
interface Category {
|
||
id: number;
|
||
name: string;
|
||
value: string;
|
||
}
|
||
|
||
interface Tag {
|
||
name: string;
|
||
value: string;
|
||
}
|
||
|
||
const categoriesList = ref<Category[]>([]);
|
||
const tagsList = ref<Tag[]>([]);
|
||
const selectedTags = ref<Tag[]>([]);
|
||
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(() => {
|
||
const hasRequiredFields =
|
||
formData.name.trim() &&
|
||
formData.pkgname.trim() &&
|
||
formData.version.trim() &&
|
||
formData.category.trim() &&
|
||
formData.debFilePath &&
|
||
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
|
||
);
|
||
});
|
||
|
||
const getHistoryArch = (
|
||
store: string,
|
||
): "amd64" | "arm64" | "loong64" | "other" => {
|
||
if (store === "store" || store.startsWith("amd64-")) return "amd64";
|
||
if (store === "aarch64-store" || store.startsWith("arm64-")) return "arm64";
|
||
if (store === "loong64-store" || store.startsWith("loong64-"))
|
||
return "loong64";
|
||
return "other";
|
||
};
|
||
|
||
const isSparkHistoryStore = (store: string): boolean => {
|
||
// 兼容新格式(amd64-store / arm64-store / loong64-store / amd64-apm ...)
|
||
// 与旧格式(store / aarch64-store / loong64-store)
|
||
return store === "store" || store.endsWith("-store");
|
||
};
|
||
|
||
const sortHistoryArchs = (archs: HistoryArchInfo[]): HistoryArchInfo[] => {
|
||
const archOrder: Record<ReturnType<typeof getHistoryArch>, number> = {
|
||
amd64: 0,
|
||
arm64: 1,
|
||
loong64: 2,
|
||
other: 3,
|
||
};
|
||
|
||
return [...archs].sort((a, b) => {
|
||
const sourceOrder =
|
||
Number(isSparkHistoryStore(b.store)) -
|
||
Number(isSparkHistoryStore(a.store));
|
||
if (sourceOrder !== 0) return sourceOrder;
|
||
return (
|
||
archOrder[getHistoryArch(a.store)] - archOrder[getHistoryArch(b.store)]
|
||
);
|
||
});
|
||
};
|
||
|
||
const getArchDisplayName = (store: string): string => {
|
||
const arch = getHistoryArch(store);
|
||
const archMap: Record<typeof arch, string> = {
|
||
amd64: "AMD64 (x86_64)",
|
||
arm64: "ARM64 (aarch64)",
|
||
loong64: "LoongArch64",
|
||
other: store,
|
||
};
|
||
return archMap[arch];
|
||
};
|
||
|
||
const sortedArchs = computed(() => sortHistoryArchs(availableArchs.value));
|
||
|
||
const getOriginLabel = (store: string): string => {
|
||
return isSparkHistoryStore(store) ? "Spark" : "APM";
|
||
};
|
||
|
||
const getOriginBadgeClass = (store: string): string => {
|
||
if (isSparkHistoryStore(store)) {
|
||
return "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400";
|
||
}
|
||
return "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400";
|
||
};
|
||
|
||
const archOriginBorderClass = (store: string): string => {
|
||
if (isSparkHistoryStore(store)) {
|
||
return "border-slate-200 dark:border-slate-700 hover:border-blue-500";
|
||
}
|
||
return "border-slate-200 dark:border-slate-700 hover:border-amber-400";
|
||
};
|
||
|
||
const loadCategoriesPromise = ref<Promise<void> | null>(null);
|
||
|
||
const loadCategoriesList = async (): Promise<void> => {
|
||
// 防止并发调用
|
||
if (isLoadingCategories.value) {
|
||
if (loadCategoriesPromise.value) {
|
||
return loadCategoriesPromise.value;
|
||
}
|
||
return;
|
||
}
|
||
|
||
isLoadingCategories.value = true;
|
||
categoriesLoadError.value = "";
|
||
|
||
const promise = (async () => {
|
||
console.log(
|
||
"[Submitter] ============== LOAD CATEGORIES START ==============",
|
||
);
|
||
|
||
try {
|
||
const result = await window.ipcRenderer.invoke("get-category-list");
|
||
|
||
if (result?.success && result.data) {
|
||
const data = result.data;
|
||
|
||
if (
|
||
data.code === 0 &&
|
||
Array.isArray(data.data) &&
|
||
data.data.length > 0
|
||
) {
|
||
categoriesList.value = data.data.map(
|
||
(
|
||
item: { id: number; name: string; type?: string },
|
||
index: number,
|
||
) => ({
|
||
id: typeof item.id === "number" ? item.id : index + 1,
|
||
name: item.name || "",
|
||
value: item.type || String(item.id),
|
||
}),
|
||
);
|
||
categoriesLoadError.value = "";
|
||
console.log(
|
||
"[Submitter] Categories loaded from API:",
|
||
categoriesList.value,
|
||
);
|
||
} else if (data.code === 0 && Array.isArray(data)) {
|
||
categoriesList.value = data.map(
|
||
(
|
||
item: { id: number; name: string; type?: string },
|
||
index: number,
|
||
) => ({
|
||
id: typeof item.id === "number" ? item.id : index + 1,
|
||
name: item.name || "",
|
||
value: item.type || String(item.id),
|
||
}),
|
||
);
|
||
categoriesLoadError.value = "";
|
||
console.log(
|
||
"[Submitter] Categories loaded from API (direct array):",
|
||
categoriesList.value,
|
||
);
|
||
} else {
|
||
const errMsg = `服务器返回异常: code=${data.code}, msg=${data.msg || "未知"}`;
|
||
categoriesLoadError.value = errMsg;
|
||
console.error("[Submitter]", errMsg);
|
||
}
|
||
} else {
|
||
const errMsg = result?.message || "获取分类列表失败";
|
||
categoriesLoadError.value = errMsg;
|
||
console.error("[Submitter] IPC failed:", errMsg);
|
||
}
|
||
} catch (error) {
|
||
const errMsg = (error as Error)?.message || "获取分类列表异常";
|
||
categoriesLoadError.value = errMsg;
|
||
console.error("[Submitter] Exception:", errMsg);
|
||
} finally {
|
||
isLoadingCategories.value = false;
|
||
loadCategoriesPromise.value = null;
|
||
}
|
||
})();
|
||
|
||
loadCategoriesPromise.value = promise;
|
||
return promise;
|
||
};
|
||
|
||
const loadTagsList = async () => {
|
||
console.log("[Submitter] ============== LOAD TAGS START ==============");
|
||
console.log("[Submitter] Calling IPC: get-tags-list");
|
||
|
||
try {
|
||
const startTime = Date.now();
|
||
const result = await window.ipcRenderer.invoke("get-tags-list");
|
||
const endTime = Date.now();
|
||
|
||
console.log(
|
||
"[Submitter] ============== IPC RESPONSE RECEIVED ==============",
|
||
);
|
||
console.log("[Submitter] Request duration:", endTime - startTime, "ms");
|
||
console.log("[Submitter] Result success:", result?.success);
|
||
console.log("[Submitter] Result message:", result?.message);
|
||
console.log("[Submitter] Full result:", JSON.stringify(result, null, 2));
|
||
|
||
if (result?.success && result.data) {
|
||
const data = result.data;
|
||
console.log(
|
||
"[Submitter] ============== PROCESSING RESPONSE ==============",
|
||
);
|
||
console.log("[Submitter] Response code:", data.code);
|
||
console.log("[Submitter] Response message:", data.msg);
|
||
console.log("[Submitter] Data type:", typeof data.data);
|
||
console.log("[Submitter] Data length:", data.data?.length);
|
||
console.log("[Submitter] Raw data:", JSON.stringify(data.data, null, 2));
|
||
|
||
if (data.code === 0 && data.data) {
|
||
tagsList.value = data.data.map(
|
||
(item: { name: string; value: string }) => ({
|
||
name: item.name,
|
||
value: item.value,
|
||
}),
|
||
);
|
||
console.log("[Submitter] ============== TAGS LOADED ==============");
|
||
console.log("[Submitter] Tags list:", tagsList.value);
|
||
console.log("[Submitter] Tags count:", tagsList.value.length);
|
||
} else {
|
||
console.error(
|
||
"[Submitter] ============== INVALID RESPONSE CODE ==============",
|
||
);
|
||
console.error("[Submitter] Expected code 0, got:", data.code);
|
||
console.error("[Submitter] Response message:", data.msg);
|
||
}
|
||
} else {
|
||
console.error(
|
||
"[Submitter] ============== IPC CALL FAILED ==============",
|
||
);
|
||
console.error("[Submitter] Success:", result?.success);
|
||
console.error("[Submitter] Message:", result?.message);
|
||
console.error("[Submitter] Data:", result?.data);
|
||
}
|
||
} catch (error) {
|
||
console.error("[Submitter] ============== EXCEPTION CAUGHT ==============");
|
||
console.error("[Submitter] Error type:", (error as Error)?.name);
|
||
console.error("[Submitter] Error message:", (error as Error)?.message);
|
||
console.error("[Submitter] Error stack:", (error as Error)?.stack);
|
||
}
|
||
};
|
||
|
||
const addTag = () => {
|
||
if (selectedTagValue.value) {
|
||
const tag = tagsList.value.find((t) => t.value === selectedTagValue.value);
|
||
if (tag && !selectedTags.value.some((t) => t.value === tag.value)) {
|
||
selectedTags.value.push(tag);
|
||
updateFormTags();
|
||
}
|
||
selectedTagValue.value = "";
|
||
}
|
||
};
|
||
|
||
const removeTag = (tagValue: string) => {
|
||
const index = selectedTags.value.findIndex((t) => t.value === tagValue);
|
||
if (index !== -1) {
|
||
selectedTags.value.splice(index, 1);
|
||
updateFormTags();
|
||
}
|
||
};
|
||
|
||
const updateFormTags = () => {
|
||
formData.tags = selectedTags.value.map((t) => t.value).join(";");
|
||
};
|
||
|
||
const openSubmissionQueue = (): void => {
|
||
window.open(
|
||
"https://upload.spark-app.store/",
|
||
"_blank",
|
||
"noopener,noreferrer",
|
||
);
|
||
};
|
||
|
||
const selectDebFile = async () => {
|
||
const result = await window.ipcRenderer.invoke("select-deb-file");
|
||
if (result?.success && result.filePath) {
|
||
formData.debFilePath = result.filePath;
|
||
await parseDebFileAndSearchHistory(result.filePath);
|
||
}
|
||
};
|
||
|
||
const searchHistoryApp = async () => {
|
||
console.log(
|
||
"[Submitter] ============== SEARCHING HISTORY INFO ==============",
|
||
);
|
||
console.log(
|
||
"[Submitter] pkgname is not empty, searching history with:",
|
||
formData.pkgname,
|
||
);
|
||
|
||
console.log(
|
||
"[Submitter] Calling IPC: search-history-app with pkgname:",
|
||
formData.pkgname,
|
||
);
|
||
const historyResult = await window.ipcRenderer.invoke(
|
||
"search-history-app",
|
||
formData.pkgname,
|
||
APM_STORE_BASE_URL,
|
||
);
|
||
console.log(
|
||
"[Submitter] Received history search response:",
|
||
JSON.stringify(historyResult, null, 2),
|
||
);
|
||
|
||
if (
|
||
historyResult?.success &&
|
||
historyResult.data &&
|
||
historyResult.data.length > 0
|
||
) {
|
||
console.log("[Submitter] ============== HISTORY INFO FOUND ==============");
|
||
console.log("[Submitter] History info count:", historyResult.data.length);
|
||
console.log(
|
||
"[Submitter] Available archs data:",
|
||
JSON.stringify(historyResult.data, null, 2),
|
||
);
|
||
|
||
console.log(
|
||
"[Submitter] ============== BEFORE SETTING STATE ==============",
|
||
);
|
||
console.log("[Submitter] availableArchs before:", availableArchs.value);
|
||
console.log("[Submitter] showArchDialog before:", showArchDialog.value);
|
||
|
||
availableArchs.value = sortHistoryArchs(
|
||
historyResult.data as HistoryArchInfo[],
|
||
);
|
||
console.log("[Submitter] availableArchs after:", availableArchs.value);
|
||
console.log(
|
||
"[Submitter] availableArchs length:",
|
||
availableArchs.value.length,
|
||
);
|
||
|
||
// 确保分类列表已加载,避免 select 无法回显
|
||
if (categoriesList.value.length === 0) {
|
||
await loadCategoriesList();
|
||
}
|
||
|
||
// 从第一条历史记录预填名称和分类
|
||
const firstArch = availableArchs.value[0];
|
||
if (firstArch) {
|
||
formData.name = firstArch.name || formData.name;
|
||
formData.category = firstArch.category || formData.category;
|
||
}
|
||
|
||
showArchDialog.value = true;
|
||
console.log("[Submitter] showArchDialog after:", showArchDialog.value);
|
||
|
||
console.log(
|
||
"[Submitter] ============== DIALOG SHOULD BE SHOWING ==============",
|
||
);
|
||
console.log("[Submitter] Dialog visibility:", showArchDialog.value);
|
||
console.log(
|
||
"[Submitter] Available architectures to display:",
|
||
availableArchs.value.map((a) => a.store),
|
||
);
|
||
|
||
nextTick(() => {
|
||
console.log("[Submitter] ============== AFTER NEXT TICK ==============");
|
||
console.log(
|
||
"[Submitter] showArchDialog in nextTick:",
|
||
showArchDialog.value,
|
||
);
|
||
console.log(
|
||
"[Submitter] availableArchs in nextTick:",
|
||
availableArchs.value,
|
||
);
|
||
|
||
const dialogElement = document.querySelector(
|
||
"[data-submitter-arch-dialog]",
|
||
);
|
||
console.log("[Submitter] Dialog element found:", !!dialogElement);
|
||
if (dialogElement) {
|
||
console.log("[Submitter] Dialog element:", dialogElement);
|
||
console.log(
|
||
"[Submitter] Dialog element style:",
|
||
window.getComputedStyle(dialogElement),
|
||
);
|
||
}
|
||
});
|
||
} else {
|
||
console.log(
|
||
"[Submitter] ============== NO HISTORY INFO FOUND ==============",
|
||
);
|
||
console.log("[Submitter] historyResult.success:", historyResult?.success);
|
||
console.log("[Submitter] historyResult.data:", historyResult?.data);
|
||
console.log(
|
||
"[Submitter] historyResult.data.length:",
|
||
historyResult?.data?.length,
|
||
);
|
||
|
||
if (historyResult?.success === true && historyResult.data) {
|
||
console.log("[Submitter] Success is true but no data found");
|
||
console.log("[Submitter] Data is:", historyResult.data);
|
||
console.log("[Submitter] Data type:", typeof historyResult.data);
|
||
} else if (!historyResult?.success) {
|
||
console.log(
|
||
"[Submitter] Search failed with message:",
|
||
historyResult?.message,
|
||
);
|
||
}
|
||
// 未找到历史记录时清空旧数据,避免上一个应用的残留
|
||
availableArchs.value = [];
|
||
showArchDialog.value = false;
|
||
}
|
||
};
|
||
|
||
const parseDebFileAndSearchHistory = async (debPath: string) => {
|
||
isParsingDeb.value = true;
|
||
debParseError.value = "";
|
||
availableArchs.value = [];
|
||
showArchDialog.value = false;
|
||
formData.pkgname = "";
|
||
formData.version = "";
|
||
formData.author = "";
|
||
formData.contributor = "";
|
||
formData.website = "";
|
||
formData.description = "";
|
||
currentDebArch.value = "";
|
||
|
||
try {
|
||
console.log(
|
||
"[Submitter] ============== STARTING DEB FILE PARSING ==============",
|
||
);
|
||
console.log("[Submitter] Input debPath:", debPath);
|
||
console.log("[Submitter] debPath type:", typeof debPath);
|
||
console.log("[Submitter] debPath length:", debPath.length);
|
||
|
||
console.log("[Submitter] Calling IPC: parse-deb-file with path:", debPath);
|
||
const parseResult = await window.ipcRenderer.invoke(
|
||
"parse-deb-file",
|
||
debPath,
|
||
);
|
||
console.log(
|
||
"[Submitter] Received IPC response:",
|
||
JSON.stringify(parseResult, null, 2),
|
||
);
|
||
|
||
if (parseResult?.success && parseResult.data) {
|
||
const debInfo = parseResult.data;
|
||
console.log(
|
||
"[Submitter] Parsed debInfo successfully:",
|
||
JSON.stringify(debInfo, null, 2),
|
||
);
|
||
|
||
console.log("[Submitter] Setting form data from debInfo:");
|
||
console.log("[Submitter] pkgname:", debInfo.pkgname);
|
||
console.log("[Submitter] version:", debInfo.version);
|
||
console.log("[Submitter] author:", debInfo.author);
|
||
console.log("[Submitter] maintainer:", debInfo.maintainer);
|
||
console.log("[Submitter] homepage:", debInfo.homepage);
|
||
console.log("[Submitter] description:", debInfo.description);
|
||
console.log("[Submitter] architecture:", debInfo.architecture);
|
||
|
||
formData.pkgname = debInfo.pkgname || "";
|
||
formData.version = debInfo.version || "";
|
||
formData.author = debInfo.author || debInfo.maintainer || "";
|
||
formData.contributor = debInfo.maintainer || "";
|
||
formData.website = debInfo.homepage || "";
|
||
formData.description = debInfo.description || "";
|
||
currentDebArch.value = debInfo.architecture || "";
|
||
|
||
console.log(
|
||
"[Submitter] Form data after setting:",
|
||
JSON.stringify(formData, null, 2),
|
||
);
|
||
|
||
if (formData.pkgname) {
|
||
isSearchingHistory.value = true;
|
||
try {
|
||
await searchHistoryApp();
|
||
} finally {
|
||
isSearchingHistory.value = false;
|
||
}
|
||
}
|
||
} else {
|
||
console.error("[Submitter] Failed to parse deb file");
|
||
console.error("[Submitter] parseResult.success:", parseResult?.success);
|
||
console.error("[Submitter] parseResult.message:", parseResult?.message);
|
||
debParseError.value = parseResult?.message || "解析deb文件失败";
|
||
}
|
||
} catch (error) {
|
||
console.error("[Submitter] Exception caught during deb parsing:", error);
|
||
console.error("[Submitter] Error stack:", (error as Error)?.stack);
|
||
debParseError.value = (error as Error)?.message || "解析deb文件失败";
|
||
} finally {
|
||
isParsingDeb.value = false;
|
||
console.log(
|
||
"[Submitter] ============== DEB FILE PARSING COMPLETED ==============",
|
||
);
|
||
}
|
||
};
|
||
|
||
const handleDebFileSelect = async (_event: Event) => {};
|
||
|
||
const handleDragOver = (event: DragEvent) => {
|
||
event.preventDefault();
|
||
console.log(
|
||
"[Submitter] Drag over detected, types available:",
|
||
event.dataTransfer?.types,
|
||
);
|
||
};
|
||
|
||
const handleDragEnter = (event: DragEvent) => {
|
||
event.preventDefault();
|
||
console.log("[Submitter] Drag enter detected");
|
||
};
|
||
|
||
const handleDragLeave = (event: DragEvent) => {
|
||
console.log("[Submitter] Drag leave detected");
|
||
};
|
||
|
||
const handleDrop = async (event: DragEvent) => {
|
||
event.preventDefault();
|
||
|
||
console.log("[Submitter] Drop event triggered");
|
||
console.log("[Submitter] DataTransfer types:", event.dataTransfer?.types);
|
||
|
||
const files = event.dataTransfer?.files;
|
||
console.log("[Submitter] Files count:", files?.length);
|
||
|
||
if (files && files.length > 0) {
|
||
const file = files[0] as File & { path?: string };
|
||
console.log("[Submitter] File name:", file.name);
|
||
console.log("[Submitter] File type:", file.type);
|
||
console.log("[Submitter] File path (from File object):", file.path);
|
||
|
||
if (file.name.endsWith(".deb")) {
|
||
console.log("[Submitter] File is a deb package");
|
||
|
||
// 在 contextIsolation 环境下,File.path 不可用
|
||
// 使用 Electron 的 webUtils.getPathForFile() 获取真实文件系统路径
|
||
let filePath: string;
|
||
try {
|
||
filePath = window.electronUtils.getPathForFile(file);
|
||
console.log("[Submitter] File path from electronUtils:", filePath);
|
||
} catch {
|
||
console.warn(
|
||
"[Submitter] electronUtils.getPathForFile failed, trying fallback",
|
||
);
|
||
const textUriList = event.dataTransfer?.getData("text/uri-list");
|
||
const textPlain = event.dataTransfer?.getData("text/plain");
|
||
filePath = textUriList || textPlain || "";
|
||
}
|
||
console.log("[Submitter] Final filePath:", filePath);
|
||
|
||
if (filePath) {
|
||
let path = filePath;
|
||
if (path.startsWith("file://")) {
|
||
path = path.replace("file://", "");
|
||
}
|
||
console.log("[Submitter] Cleaned path:", path);
|
||
|
||
formData.debFilePath = path;
|
||
console.log("[Submitter] Calling parseDebFileAndSearchHistory...");
|
||
await parseDebFileAndSearchHistory(path);
|
||
console.log("[Submitter] parseDebFileAndSearchHistory completed");
|
||
} else {
|
||
console.error("[Submitter] No valid file path found");
|
||
debParseError.value = "无法获取文件路径,请使用点击方式选择文件";
|
||
}
|
||
} else {
|
||
console.warn("[Submitter] File is not a deb package:", file.name);
|
||
debParseError.value = "请选择 .deb 格式的安装包";
|
||
}
|
||
} else {
|
||
console.error("[Submitter] No files found in drop event");
|
||
debParseError.value = "拖放的文件无效";
|
||
}
|
||
};
|
||
|
||
const selectArch = async (arch: HistoryArchInfo) => {
|
||
showArchDialog.value = false;
|
||
console.log("[Submitter] selectArch called with:", arch);
|
||
|
||
// 确保分类列表已加载,避免 select 无法回显
|
||
if (categoriesList.value.length === 0) {
|
||
await loadCategoriesList();
|
||
}
|
||
|
||
formData.name = arch.name || formData.name;
|
||
formData.author = arch.author || formData.author;
|
||
formData.contributor = arch.contributor || formData.contributor;
|
||
formData.website = arch.website || formData.website;
|
||
formData.category = arch.category || formData.category;
|
||
formData.description = arch.more || formData.description;
|
||
|
||
if (arch.tags) {
|
||
const tagValues = arch.tags.split(";").filter((t) => t.trim());
|
||
selectedTags.value = tagValues
|
||
.map((value) => {
|
||
const existingTag = tagsList.value.find((t) => t.value === value);
|
||
if (existingTag) {
|
||
return existingTag;
|
||
}
|
||
return { name: value, value };
|
||
})
|
||
.filter((t) => t);
|
||
updateFormTags();
|
||
console.log("[Submitter] Tags loaded from history:", selectedTags.value);
|
||
}
|
||
|
||
// 开发模式下来自 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:",
|
||
baseUrl,
|
||
);
|
||
|
||
if (arch.icon) {
|
||
formData.iconPath = `${baseUrl}/icon.png`;
|
||
iconPreview.value = formData.iconPath;
|
||
iconFileName.value = "icon.png";
|
||
console.log("[Submitter] Icon URL:", formData.iconPath);
|
||
console.log("[Submitter] Icon preview set:", iconPreview.value);
|
||
}
|
||
|
||
if (arch.imgs && arch.imgs.length > 0) {
|
||
formData.screenshots = arch.imgs.slice(0, 5);
|
||
console.log("[Submitter] Screenshots from history:", formData.screenshots);
|
||
} else {
|
||
formData.screenshots = [];
|
||
for (let i = 1; i <= 5; i++) {
|
||
formData.screenshots.push(`${baseUrl}/screen_${i}.png`);
|
||
}
|
||
console.log("[Submitter] Screenshots generated:", formData.screenshots);
|
||
}
|
||
|
||
showArchDialog.value = false;
|
||
};
|
||
|
||
const selectIconFile = () => {
|
||
iconFileInput.value?.click();
|
||
};
|
||
|
||
const isPngFile = (file: File): boolean => {
|
||
return (
|
||
file.type === "image/png" || (file.type === "" && /\.png$/i.test(file.name))
|
||
);
|
||
};
|
||
|
||
const readIconFile = (file: File): void => {
|
||
iconFileName.value = file.name;
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
const dataUrl = e.target?.result as string;
|
||
formData.iconPath = dataUrl;
|
||
iconPreview.value = dataUrl;
|
||
};
|
||
reader.readAsDataURL(file);
|
||
};
|
||
|
||
const handleIconFileSelect = (event: Event) => {
|
||
const target = event.target as HTMLInputElement;
|
||
const file = target.files?.[0];
|
||
if (file) {
|
||
if (isPngFile(file)) {
|
||
mediaError.value = "";
|
||
readIconFile(file);
|
||
} else {
|
||
mediaError.value = "应用图标仅支持 PNG 格式。";
|
||
}
|
||
}
|
||
target.value = "";
|
||
};
|
||
|
||
const handleIconDrop = (event: DragEvent) => {
|
||
event.preventDefault();
|
||
const file = event.dataTransfer?.files?.[0];
|
||
if (!file) return;
|
||
if (isPngFile(file)) {
|
||
mediaError.value = "";
|
||
readIconFile(file);
|
||
} else {
|
||
mediaError.value = "应用图标仅支持 PNG 格式。";
|
||
}
|
||
};
|
||
|
||
const addScreenshot = () => {
|
||
screenshotInput.value?.click();
|
||
};
|
||
|
||
const importScreenshots = (files: File[]) => {
|
||
const pngFiles = files.filter(isPngFile);
|
||
if (pngFiles.length !== files.length) {
|
||
mediaError.value = "截图仅支持 PNG 格式。";
|
||
} else if (pngFiles.length > 0) {
|
||
mediaError.value = "";
|
||
}
|
||
|
||
for (const file of pngFiles) {
|
||
if (formData.screenshots.length >= 5) break;
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = () => {
|
||
if (
|
||
formData.screenshots.length < 5 &&
|
||
typeof reader.result === "string"
|
||
) {
|
||
formData.screenshots.push(reader.result);
|
||
}
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
};
|
||
|
||
const handleScreenshotSelect = (event: Event) => {
|
||
const target = event.target as HTMLInputElement;
|
||
importScreenshots(Array.from(target.files ?? []));
|
||
target.value = "";
|
||
};
|
||
|
||
const handleScreenshotDrop = (event: DragEvent) => {
|
||
event.preventDefault();
|
||
importScreenshots(Array.from(event.dataTransfer?.files ?? []));
|
||
};
|
||
|
||
const handleScreenshotPaste = (event: ClipboardEvent) => {
|
||
const files = Array.from(event.clipboardData?.items ?? [])
|
||
.filter((item) => item.kind === "file")
|
||
.map((item) => item.getAsFile())
|
||
.filter((file): file is File => file !== null);
|
||
|
||
if (files.length > 0) {
|
||
event.preventDefault();
|
||
importScreenshots(files);
|
||
}
|
||
};
|
||
|
||
const removeScreenshot = (index: number) => {
|
||
formData.screenshots.splice(index, 1);
|
||
};
|
||
|
||
const resetForm = () => {
|
||
formData.name = "";
|
||
formData.pkgname = "";
|
||
formData.version = "";
|
||
formData.author = "";
|
||
formData.contributor = "";
|
||
formData.mail = "";
|
||
formData.website = "";
|
||
formData.debFilePath = "";
|
||
formData.iconPath = "";
|
||
formData.screenshots = [];
|
||
formData.description = "";
|
||
formData.tags = "";
|
||
formData.category = "";
|
||
formData.categoryId = 0;
|
||
formData.remark = "";
|
||
submitSuccess.value = false;
|
||
submitError.value = "";
|
||
debParseError.value = "";
|
||
mediaError.value = "";
|
||
packageSuccess.value = false;
|
||
packageError.value = "";
|
||
packageResult.value = null;
|
||
iconFileName.value = "";
|
||
iconPreview.value = "";
|
||
uploadProgress.value = 0;
|
||
uploadStage.value = "";
|
||
uploadStageMessage.value = "";
|
||
uploadStages.value = [];
|
||
selectedTags.value = [];
|
||
selectedTagValue.value = "";
|
||
availableArchs.value = [];
|
||
showArchDialog.value = false;
|
||
currentDebArch.value = "";
|
||
isSearchingHistory.value = false;
|
||
};
|
||
|
||
const clearSubmitTransientState = (): void => {
|
||
showSubmitSuccessModal.value = false;
|
||
submitSuccess.value = false;
|
||
isSubmitting.value = false;
|
||
uploadProgress.value = 0;
|
||
uploadStage.value = "";
|
||
uploadStageMessage.value = "";
|
||
uploadStages.value = [];
|
||
};
|
||
|
||
const closeSubmitSuccessModal = () => {
|
||
clearSubmitTransientState();
|
||
};
|
||
|
||
const continueSubmission = () => {
|
||
const shouldClear = window.confirm(
|
||
"是否清空当前表单内容后继续投递?选择“取消”将保留已填写内容。",
|
||
);
|
||
if (shouldClear) {
|
||
resetForm();
|
||
} else {
|
||
clearSubmitTransientState();
|
||
}
|
||
};
|
||
|
||
const submitForm = async () => {
|
||
if (!isFormValid.value) return;
|
||
|
||
isSubmitting.value = true;
|
||
submitSuccess.value = false;
|
||
submitError.value = "";
|
||
|
||
uploadProgress.value = 0;
|
||
uploadStage.value = "准备提交...";
|
||
uploadStageMessage.value = "正在准备...";
|
||
uploadStages.value = [
|
||
{ stage: "icon", label: "上传图标", progress: 0 },
|
||
{ stage: "screenshots", label: "上传截图", progress: 0 },
|
||
{ stage: "deb", label: "上传安装包", progress: 0 },
|
||
{ stage: "submit", label: "提交信息", progress: 0 },
|
||
];
|
||
|
||
const progressListener = (
|
||
_event: unknown,
|
||
data: { step: string; progress: number; message: string },
|
||
) => {
|
||
let stageIndex: number;
|
||
if (data.step.startsWith("screenshot")) {
|
||
stageIndex = uploadStages.value.findIndex(
|
||
(s) => s.stage === "screenshots",
|
||
);
|
||
} else {
|
||
stageIndex = uploadStages.value.findIndex((s) => s.stage === data.step);
|
||
}
|
||
if (stageIndex !== -1) {
|
||
uploadStages.value[stageIndex].progress = Math.max(
|
||
uploadStages.value[stageIndex].progress,
|
||
data.progress,
|
||
);
|
||
}
|
||
if (data.progress >= 100 && !data.step.startsWith("screenshot")) {
|
||
uploadProgress.value = data.progress;
|
||
} else {
|
||
uploadProgress.value = Math.max(uploadProgress.value, data.progress);
|
||
}
|
||
uploadStage.value = data.message;
|
||
uploadStageMessage.value = data.message;
|
||
};
|
||
|
||
window.ipcRenderer.on("submit-upload-progress", progressListener);
|
||
|
||
try {
|
||
const submitData = {
|
||
name: formData.name,
|
||
pkgname: formData.pkgname,
|
||
version: formData.version,
|
||
author: formData.author,
|
||
contributor: formData.contributor,
|
||
mail: formData.mail,
|
||
website: formData.website,
|
||
debFilePath: formData.debFilePath,
|
||
iconPath: formData.iconPath,
|
||
screenshots: [...formData.screenshots],
|
||
description: formData.description,
|
||
tags: formData.tags,
|
||
category: formData.category,
|
||
categoryId: formData.categoryId,
|
||
remark: formData.remark,
|
||
arch: currentDebArch.value,
|
||
};
|
||
|
||
console.log("[Submitter] ============== SUBMIT FORM ==============");
|
||
console.log(
|
||
"[Submitter] Submit data:",
|
||
JSON.stringify(submitData, null, 2),
|
||
);
|
||
console.log(
|
||
"[Submitter] Screenshots count:",
|
||
submitData.screenshots.length,
|
||
);
|
||
console.log("[Submitter] Icon path:", submitData.iconPath);
|
||
|
||
const result = await window.ipcRenderer.invoke("submit-app", submitData);
|
||
|
||
if (result?.success) {
|
||
uploadStages.value.forEach((s) => (s.progress = 100));
|
||
uploadProgress.value = 100;
|
||
uploadStage.value = "提交完成";
|
||
uploadStageMessage.value = "投稿提交成功!";
|
||
submitSuccess.value = true;
|
||
showSubmitSuccessModal.value = true;
|
||
} else {
|
||
submitError.value = result?.message || "提交失败";
|
||
}
|
||
} catch (error) {
|
||
submitError.value = (error as Error)?.message || "提交失败";
|
||
} finally {
|
||
window.ipcRenderer.off("submit-upload-progress", progressListener);
|
||
isSubmitting.value = false;
|
||
}
|
||
};
|
||
|
||
const selectPackArch = async (arch: { store: string; label: string }) => {
|
||
showArchPackDialog.value = false;
|
||
await packageApp(arch.store);
|
||
};
|
||
|
||
const packageApp = async (storeArch: string) => {
|
||
if (!isFormValid.value) return;
|
||
|
||
isPackaging.value = true;
|
||
packageSuccess.value = false;
|
||
packageError.value = "";
|
||
packageResult.value = null;
|
||
|
||
uploadProgress.value = 0;
|
||
uploadStage.value = "准备打包...";
|
||
uploadStageMessage.value = "正在准备...";
|
||
uploadStages.value = [
|
||
{ stage: "init", label: "创建临时目录", progress: 0 },
|
||
{ stage: "icon", label: "处理图标", progress: 0 },
|
||
{ stage: "screenshots", label: "处理截图", progress: 0 },
|
||
{ stage: "deb", label: "复制安装包", progress: 0 },
|
||
{ stage: "json", label: "生成配置文件", progress: 0 },
|
||
{ stage: "tar", label: "打包压缩", progress: 0 },
|
||
];
|
||
|
||
const progressListener = (
|
||
_event: unknown,
|
||
data: { step: string; progress: number; message: string },
|
||
) => {
|
||
const stageIndex = uploadStages.value.findIndex(
|
||
(s) => s.stage === data.step,
|
||
);
|
||
if (stageIndex !== -1) {
|
||
uploadStages.value[stageIndex].progress = data.progress;
|
||
}
|
||
uploadProgress.value = data.progress;
|
||
uploadStage.value = data.step;
|
||
uploadStageMessage.value = data.message;
|
||
};
|
||
|
||
window.ipcRenderer.on("package-progress", progressListener);
|
||
|
||
try {
|
||
const packageData = {
|
||
name: formData.name,
|
||
pkgname: formData.pkgname,
|
||
version: formData.version,
|
||
author: formData.author,
|
||
contributor: formData.contributor,
|
||
mail: formData.mail,
|
||
website: formData.website,
|
||
debFilePath: formData.debFilePath,
|
||
iconPath: formData.iconPath,
|
||
screenshots: [...formData.screenshots],
|
||
description: formData.description,
|
||
tags: formData.tags,
|
||
category: formData.category,
|
||
categoryId: formData.categoryId,
|
||
remark: formData.remark,
|
||
storeArch,
|
||
};
|
||
|
||
console.log("[Submitter] ============== PACKAGE APP ==============");
|
||
console.log(
|
||
"[Submitter] Package data:",
|
||
JSON.stringify(packageData, null, 2),
|
||
);
|
||
|
||
const result = await window.ipcRenderer.invoke("package-app", packageData);
|
||
|
||
if (result?.success) {
|
||
uploadStages.value.forEach((s) => (s.progress = 100));
|
||
uploadProgress.value = 100;
|
||
uploadStageMessage.value = "打包完成!";
|
||
packageSuccess.value = true;
|
||
packageResult.value = result.data;
|
||
} else {
|
||
packageError.value = result?.message || "打包失败";
|
||
}
|
||
} catch (error) {
|
||
packageError.value = (error as Error)?.message || "打包失败";
|
||
} finally {
|
||
window.ipcRenderer.off("package-progress", progressListener);
|
||
isPackaging.value = false;
|
||
}
|
||
};
|
||
|
||
const closeWindow = () => {
|
||
window.ipcRenderer.send("close-submitter-window");
|
||
};
|
||
|
||
import { onMounted, nextTick } from "vue";
|
||
|
||
const getGitInfo = async () => {
|
||
try {
|
||
const [nameResult, emailResult] = await Promise.all([
|
||
window.ipcRenderer.invoke("get-git-name"),
|
||
window.ipcRenderer.invoke("get-git-email"),
|
||
]);
|
||
|
||
let gitName = "";
|
||
let gitEmail = "";
|
||
|
||
if (nameResult?.success && nameResult.data) {
|
||
gitName = nameResult.data;
|
||
console.log("[Submitter] Git name auto-filled:", gitName);
|
||
}
|
||
|
||
if (emailResult?.success && emailResult.data) {
|
||
gitEmail = emailResult.data;
|
||
console.log("[Submitter] Git email auto-filled:", gitEmail);
|
||
}
|
||
|
||
// 将 git name 和 email 合并填入 contributor 字段,格式: Name <email>
|
||
if (gitName || gitEmail) {
|
||
if (gitName && gitEmail) {
|
||
formData.contributor = `${gitName} <${gitEmail}>`;
|
||
} else if (gitName) {
|
||
formData.contributor = gitName;
|
||
} else if (gitEmail) {
|
||
formData.contributor = gitEmail;
|
||
}
|
||
}
|
||
|
||
// 单独填入邮箱
|
||
if (gitEmail) {
|
||
formData.mail = gitEmail;
|
||
}
|
||
} catch (err) {
|
||
console.warn("[Submitter] Failed to get git info:", err);
|
||
}
|
||
};
|
||
|
||
onMounted(async () => {
|
||
console.log("[Submitter] Component mounted, loading categories and tags");
|
||
// 先等待分类列表加载完成,避免后续竞态
|
||
await Promise.all([loadCategoriesList(), loadTagsList()]);
|
||
// 尝试从 git 配置读取 name 和 email,填入 contributor 和 mail
|
||
await getGitInfo();
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.submitter-titlebar {
|
||
-webkit-app-region: drag;
|
||
}
|
||
|
||
.submitter-close-button {
|
||
-webkit-app-region: no-drag;
|
||
}
|
||
</style>
|