mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-08-08 05:53:56 +08:00
refactor(submitter): 重构分类处理逻辑,完善提交校验与安装包流程
1. 重构提交器的分类处理,移除硬编码的分类映射,改用后端返回的type字段 2. 添加categoryId同步逻辑,完善表单校验和提交数据 3. 优化图标处理逻辑,支持base64格式图标 4. 完善deb包元数据处理,自动生成规范的文件名 5. 新增更全面的提交字段校验和API业务错误处理 6. 补充安装脚本的目录复制规则
This commit is contained in:
@@ -29,6 +29,9 @@ install:
|
|||||||
mkdir -p $(DESTDIR)/usr/share/icons/
|
mkdir -p $(DESTDIR)/usr/share/icons/
|
||||||
mkdir -p $(DESTDIR)/usr/lib/
|
mkdir -p $(DESTDIR)/usr/lib/
|
||||||
mkdir -p $(DESTDIR)/usr/bin/
|
mkdir -p $(DESTDIR)/usr/bin/
|
||||||
|
mkdir -p $(DESTDIR)/etc/apt/
|
||||||
|
mkdir -p $(DESTDIR)/lib/systemd/
|
||||||
|
mkdir -p $(DESTDIR)/tmp/
|
||||||
cp -rv release/*/linux*-unpacked/* $(DESTDIR)/opt/spark-store/bin/
|
cp -rv release/*/linux*-unpacked/* $(DESTDIR)/opt/spark-store/bin/
|
||||||
cp -rv release/*/linux*-unpacked/extras/* $(DESTDIR)/opt/spark-store/extras/
|
cp -rv release/*/linux*-unpacked/extras/* $(DESTDIR)/opt/spark-store/extras/
|
||||||
cp -rv tool/* $(DESTDIR)/opt/durapps/spark-store/bin/
|
cp -rv tool/* $(DESTDIR)/opt/durapps/spark-store/bin/
|
||||||
@@ -39,5 +42,12 @@ install:
|
|||||||
cp -rv pkg/usr/share/applications/ $(DESTDIR)/usr/share/
|
cp -rv pkg/usr/share/applications/ $(DESTDIR)/usr/share/
|
||||||
cp -rv pkg/usr/share/polkit-1 $(DESTDIR)/usr/share/
|
cp -rv pkg/usr/share/polkit-1 $(DESTDIR)/usr/share/
|
||||||
cp -rv pkg/usr/share/aptss $(DESTDIR)/usr/share/
|
cp -rv pkg/usr/share/aptss $(DESTDIR)/usr/share/
|
||||||
|
cp -rv pkg/usr/share/ssinstall/ $(DESTDIR)/usr/share/
|
||||||
|
cp -rv pkg/usr/share/ssinstall-local/ $(DESTDIR)/usr/share/
|
||||||
|
cp -rv pkg/usr/share/dsg/ $(DESTDIR)/usr/share/
|
||||||
|
cp -rv pkg/usr/share/bash-completion/ $(DESTDIR)/usr/share/
|
||||||
|
cp -rv pkg/etc/apt/ $(DESTDIR)/etc/
|
||||||
|
cp -rv pkg/tmp/spark-store-install/ $(DESTDIR)/tmp/
|
||||||
|
cp -rv pkg/lib/systemd/ $(DESTDIR)/lib/
|
||||||
cp -rv tool/spark-store.asc $(DESTDIR)/opt/durapps/spark-store/bin/
|
cp -rv tool/spark-store.asc $(DESTDIR)/opt/durapps/spark-store/bin/
|
||||||
ln -s ../../../spark-store/extras/spark-store $(DESTDIR)/opt/durapps/spark-store/bin/spark-store
|
ln -s ../../../spark-store/extras/spark-store $(DESTDIR)/opt/durapps/spark-store/bin/spark-store
|
||||||
|
|||||||
@@ -45,25 +45,6 @@ interface OssUploadMetadata {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const categoryNameToIdMap: Record<string, number> = {
|
|
||||||
network: 3,
|
|
||||||
chat: 9,
|
|
||||||
music: 2,
|
|
||||||
video: 12,
|
|
||||||
image_graphics: 6,
|
|
||||||
games: 1,
|
|
||||||
office: 4,
|
|
||||||
reading: 8,
|
|
||||||
development: 7,
|
|
||||||
tools: 11,
|
|
||||||
themes: 10,
|
|
||||||
others: 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
function getCategoryIdByName(categoryName: string): number {
|
|
||||||
return categoryNameToIdMap[categoryName];
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateUUID(): string {
|
function generateUUID(): string {
|
||||||
const hexChars = "0123456789abcdef";
|
const hexChars = "0123456789abcdef";
|
||||||
let uuid = "";
|
let uuid = "";
|
||||||
@@ -1083,7 +1064,33 @@ export function registerSubmitterHandlers(
|
|||||||
);
|
);
|
||||||
let iconFilePath = iconPath;
|
let iconFilePath = iconPath;
|
||||||
|
|
||||||
if (iconPath.startsWith("http://") || iconPath.startsWith("https://")) {
|
if (iconPath.startsWith("data:")) {
|
||||||
|
logger.info("[Submitter] Icon is a Base64 data URL, decoding");
|
||||||
|
const base64Data = iconPath.split(",")[1];
|
||||||
|
if (!base64Data) {
|
||||||
|
return { success: false, message: "图标数据格式错误" };
|
||||||
|
}
|
||||||
|
const tempDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), "spark-store-submitter-"),
|
||||||
|
);
|
||||||
|
iconFilePath = path.join(tempDir, "icon.png");
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(iconFilePath, Buffer.from(base64Data, "base64"));
|
||||||
|
logger.info(
|
||||||
|
{ iconFilePath },
|
||||||
|
"[Submitter] Icon decoded from data URL",
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err }, "[Submitter] Failed to decode icon data URL");
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `图标解码失败: ${(err as Error).message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
iconPath.startsWith("http://") ||
|
||||||
|
iconPath.startsWith("https://")
|
||||||
|
) {
|
||||||
logger.info(
|
logger.info(
|
||||||
{ iconPath },
|
{ iconPath },
|
||||||
"[Submitter] Icon is a remote URL, downloading first",
|
"[Submitter] Icon is a remote URL, downloading first",
|
||||||
@@ -1140,7 +1147,8 @@ export function registerSubmitterHandlers(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
iconPath.startsWith("http://") ||
|
iconPath.startsWith("http://") ||
|
||||||
iconPath.startsWith("https://")
|
iconPath.startsWith("https://") ||
|
||||||
|
iconPath.startsWith("data:")
|
||||||
) {
|
) {
|
||||||
fs.unlinkSync(iconFilePath);
|
fs.unlinkSync(iconFilePath);
|
||||||
fs.rmdirSync(path.dirname(iconFilePath));
|
fs.rmdirSync(path.dirname(iconFilePath));
|
||||||
@@ -1297,8 +1305,16 @@ export function registerSubmitterHandlers(
|
|||||||
|
|
||||||
const debFileStat = fs.statSync(debFilePath);
|
const debFileStat = fs.statSync(debFilePath);
|
||||||
|
|
||||||
|
// 参考老 Qt 投稿器:根据 deb 元数据构造 file_name: {pkgname}_{version}_{arch}.deb
|
||||||
|
// arch 由前端解析 deb 时获取并传入,无需再次调用 dpkg-deb
|
||||||
|
const debArch = String(dataObj.arch || "amd64");
|
||||||
|
const pkgVersion = String(dataObj.version || "0.0.0");
|
||||||
|
const debPkgName = String(dataObj.pkgname || "unknown");
|
||||||
|
const formFileName = `${debPkgName}_${pkgVersion}_${debArch}.deb`;
|
||||||
|
logger.info({ formFileName, debArch }, "[Submitter] Constructed file_name");
|
||||||
|
|
||||||
const categoryName = String(dataObj.category || "");
|
const categoryName = String(dataObj.category || "");
|
||||||
const categoryId = getCategoryIdByName(categoryName);
|
const categoryId = Number(dataObj.categoryId) || 0;
|
||||||
logger.info(
|
logger.info(
|
||||||
{ categoryName, categoryId },
|
{ categoryName, categoryId },
|
||||||
"[Submitter] Category name and ID",
|
"[Submitter] Category name and ID",
|
||||||
@@ -1313,55 +1329,80 @@ export function registerSubmitterHandlers(
|
|||||||
: [];
|
: [];
|
||||||
logger.info({ tagsString, tagsArray }, "[Submitter] Tags conversion");
|
logger.info({ tagsString, tagsArray }, "[Submitter] Tags conversion");
|
||||||
|
|
||||||
const submitData = {
|
const remark =
|
||||||
application_name: String(dataObj.pkgname || ""),
|
((dataObj.remark as string) || "") +
|
||||||
|
" - (来自于投稿器_v" +
|
||||||
|
getAppVersion() +
|
||||||
|
")";
|
||||||
|
|
||||||
|
const submitData: Record<string, unknown> = {
|
||||||
|
application_name: debPkgName,
|
||||||
application_name_zh: String(dataObj.name || ""),
|
application_name_zh: String(dataObj.name || ""),
|
||||||
contributor: String(dataObj.contributor || ""),
|
contributor: String(dataObj.contributor || ""),
|
||||||
icons: iconUrl,
|
icons: iconUrl,
|
||||||
size: debFileStat.size,
|
size: debFileStat.size,
|
||||||
file_name: path.basename(debFilePath).replace(/\s+/g, "_plus_"),
|
file_name: formFileName,
|
||||||
website: String(dataObj.website || ""),
|
website: String(dataObj.website || ""),
|
||||||
version: String(dataObj.version || ""),
|
version: pkgVersion,
|
||||||
more: String(dataObj.description || ""),
|
more: String(dataObj.description || ""),
|
||||||
type_id: categoryId,
|
type_id: categoryId,
|
||||||
author: String(dataObj.author || ""),
|
author: String(dataObj.author || ""),
|
||||||
remark:
|
remark,
|
||||||
((dataObj.remark as string) || "") +
|
|
||||||
" - (来自于投稿器_v" +
|
|
||||||
getAppVersion() +
|
|
||||||
")",
|
|
||||||
img_urls: screenshotUrls,
|
img_urls: screenshotUrls,
|
||||||
deb_url: debUrl,
|
deb_url: debUrl,
|
||||||
mail: String(dataObj.mail || dataObj.contributor || ""),
|
mail: String(dataObj.mail || dataObj.contributor || ""),
|
||||||
tags: tagsArray,
|
tags: tagsArray,
|
||||||
|
architecture: debArch,
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"[Submitter] ============== VALIDATING SUBMISSION DATA ==============",
|
"[Submitter] ============== VALIDATING SUBMISSION DATA ==============",
|
||||||
);
|
);
|
||||||
const requiredFields = [
|
// 对齐老 Qt 投稿器 isReadySubmit() 的完整校验
|
||||||
"application_name",
|
const checks: Array<{ field: keyof typeof submitData; label: string }> = [
|
||||||
"application_name_zh",
|
{ field: "application_name", label: "包名" },
|
||||||
"contributor",
|
{ field: "application_name_zh", label: "应用名称" },
|
||||||
"icons",
|
{ field: "contributor", label: "贡献者" },
|
||||||
"size",
|
{ field: "icons", label: "图标URL" },
|
||||||
"file_name",
|
{ field: "size", label: "文件大小" },
|
||||||
"version",
|
{ field: "file_name", label: "文件名" },
|
||||||
"type_id",
|
{ field: "website", label: "官网地址" },
|
||||||
"author",
|
{ field: "version", label: "版本号" },
|
||||||
"deb_url",
|
{ field: "more", label: "应用描述" },
|
||||||
|
{ field: "type_id", label: "分类ID" },
|
||||||
|
{ field: "author", label: "作者" },
|
||||||
|
{ field: "remark", label: "测试情况" },
|
||||||
|
{ field: "deb_url", label: "安装包URL" },
|
||||||
|
{ field: "mail", label: "联系邮箱" },
|
||||||
];
|
];
|
||||||
const missingFields = requiredFields.filter(
|
const missingFields: string[] = [];
|
||||||
(field) => !submitData[field as keyof typeof submitData],
|
for (const { field, label } of checks) {
|
||||||
);
|
const val = submitData[field];
|
||||||
|
if (val === undefined || val === null || val === "" || val === 0) {
|
||||||
|
missingFields.push(`${label}(${field})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (screenshotUrls.length === 0) {
|
||||||
|
missingFields.push("截图(img_urls)");
|
||||||
|
}
|
||||||
|
if (tagsArray.length === 0) {
|
||||||
|
missingFields.push("标签(tags)");
|
||||||
|
}
|
||||||
if (missingFields.length > 0) {
|
if (missingFields.length > 0) {
|
||||||
logger.error({ missingFields }, "[Submitter] Missing required fields");
|
logger.error({ missingFields }, "[Submitter] Missing required fields");
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `缺少必填字段: ${missingFields.join(", ")}`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"[Submitter] ============== PREPARING SUBMISSION REQUEST ==============",
|
"[Submitter] ============== PREPARING SUBMISSION REQUEST ==============",
|
||||||
);
|
);
|
||||||
logger.info({ submitData }, "[Submitter] Final submission data");
|
logger.info({ submitData }, "[Submitter] Final submission data");
|
||||||
|
logger.info(
|
||||||
|
`[Submitter] ============== FULL SUBMIT JSON ==============\n${JSON.stringify(submitData, null, 2)}`,
|
||||||
|
);
|
||||||
|
|
||||||
const submitterApiUrl =
|
const submitterApiUrl =
|
||||||
"https://upload.deepinos.org.cn/api/index/upload_application";
|
"https://upload.deepinos.org.cn/api/index/upload_application";
|
||||||
@@ -1471,6 +1512,22 @@ export function registerSubmitterHandlers(
|
|||||||
result = responseText;
|
result = responseText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查响应体中是否有业务错误码(部分 API HTTP 200 但业务失败)
|
||||||
|
if (result && typeof result === "object" && !Array.isArray(result)) {
|
||||||
|
const respObj = result as Record<string, unknown>;
|
||||||
|
if (respObj.code !== undefined && respObj.code !== 0) {
|
||||||
|
logger.error(
|
||||||
|
{ respCode: respObj.code, respMsg: respObj.msg },
|
||||||
|
"[Submitter] API returned business error",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: String(respObj.msg || respObj.message || "提交失败"),
|
||||||
|
apiResponse: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"[Submitter] ============== SUBMISSION SUCCESSFUL ==============",
|
"[Submitter] ============== SUBMISSION SUCCESSFUL ==============",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -559,7 +559,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed } from "vue";
|
import { ref, reactive, computed, watch } from "vue";
|
||||||
|
|
||||||
interface HistoryArchInfo {
|
interface HistoryArchInfo {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -595,9 +595,19 @@ const formData = reactive({
|
|||||||
description: "",
|
description: "",
|
||||||
tags: "",
|
tags: "",
|
||||||
category: "",
|
category: "",
|
||||||
|
categoryId: 0,
|
||||||
remark: "",
|
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 isSubmitting = ref(false);
|
||||||
const submitSuccess = ref(false);
|
const submitSuccess = ref(false);
|
||||||
const submitError = ref("");
|
const submitError = ref("");
|
||||||
@@ -634,23 +644,6 @@ interface Category {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务器分类 id → 英文标识符映射(与后端 submitter.ts 的 categoryNameToIdMap 反向)
|
|
||||||
// 历史记录中 category 字段存储的就是这些英文标识符
|
|
||||||
const categoryIdToValueMap: Record<number, string> = {
|
|
||||||
1: "games",
|
|
||||||
2: "music",
|
|
||||||
3: "network",
|
|
||||||
4: "office",
|
|
||||||
5: "others",
|
|
||||||
6: "image_graphics",
|
|
||||||
7: "development",
|
|
||||||
8: "reading",
|
|
||||||
9: "chat",
|
|
||||||
10: "themes",
|
|
||||||
11: "tools",
|
|
||||||
12: "video",
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Tag {
|
interface Tag {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -667,7 +660,8 @@ const isFormValid = computed(() => {
|
|||||||
formData.pkgname.trim() &&
|
formData.pkgname.trim() &&
|
||||||
formData.version.trim() &&
|
formData.version.trim() &&
|
||||||
formData.category.trim() &&
|
formData.category.trim() &&
|
||||||
formData.debFilePath
|
formData.debFilePath &&
|
||||||
|
formData.remark.trim()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -711,12 +705,10 @@ const loadCategoriesList = async (): Promise<void> => {
|
|||||||
data.data.length > 0
|
data.data.length > 0
|
||||||
) {
|
) {
|
||||||
categoriesList.value = data.data.map(
|
categoriesList.value = data.data.map(
|
||||||
(item: { id: number; name: string }, index: number) => ({
|
(item: { id: number; name: string; type?: string }, index: number) => ({
|
||||||
id: typeof item.id === "number" ? item.id : index + 1,
|
id: typeof item.id === "number" ? item.id : index + 1,
|
||||||
// API 只返回中文 name,value 通过 id 反向映射为英文标识符
|
|
||||||
// 英文标识符与历史记录中的 category 字段一致,用于匹配
|
|
||||||
name: item.name || "",
|
name: item.name || "",
|
||||||
value: categoryIdToValueMap[item.id] || item.name || "",
|
value: item.type || String(item.id),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
categoriesLoadError.value = "";
|
categoriesLoadError.value = "";
|
||||||
@@ -726,10 +718,10 @@ const loadCategoriesList = async (): Promise<void> => {
|
|||||||
);
|
);
|
||||||
} else if (data.code === 0 && Array.isArray(data)) {
|
} else if (data.code === 0 && Array.isArray(data)) {
|
||||||
categoriesList.value = data.map(
|
categoriesList.value = data.map(
|
||||||
(item: { id: number; name: string }, index: number) => ({
|
(item: { id: number; name: string; type?: string }, index: number) => ({
|
||||||
id: typeof item.id === "number" ? item.id : index + 1,
|
id: typeof item.id === "number" ? item.id : index + 1,
|
||||||
name: item.name || "",
|
name: item.name || "",
|
||||||
value: categoryIdToValueMap[item.id] || item.name || "",
|
value: item.type || String(item.id),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
categoriesLoadError.value = "";
|
categoriesLoadError.value = "";
|
||||||
@@ -1198,12 +1190,13 @@ const selectIconFile = () => {
|
|||||||
|
|
||||||
const handleIconFileSelect = (event: Event) => {
|
const handleIconFileSelect = (event: Event) => {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
const file = target.files?.[0] as File & { path?: string };
|
const file = target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
formData.iconPath = file.path || file.name;
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
iconPreview.value = e.target?.result as string;
|
const dataUrl = e.target?.result as string;
|
||||||
|
formData.iconPath = dataUrl;
|
||||||
|
iconPreview.value = dataUrl;
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
@@ -1211,17 +1204,18 @@ const handleIconFileSelect = (event: Event) => {
|
|||||||
|
|
||||||
const handleIconDrop = (event: DragEvent) => {
|
const handleIconDrop = (event: DragEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const file = event.dataTransfer?.files?.[0] as File & { path?: string };
|
const file = event.dataTransfer?.files?.[0];
|
||||||
if (
|
if (
|
||||||
file &&
|
file &&
|
||||||
(file.name.endsWith(".png") ||
|
(file.name.endsWith(".png") ||
|
||||||
file.name.endsWith(".jpg") ||
|
file.name.endsWith(".jpg") ||
|
||||||
file.name.endsWith(".jpeg"))
|
file.name.endsWith(".jpeg"))
|
||||||
) {
|
) {
|
||||||
formData.iconPath = file.path || file.name;
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
iconPreview.value = e.target?.result as string;
|
const dataUrl = e.target?.result as string;
|
||||||
|
formData.iconPath = dataUrl;
|
||||||
|
iconPreview.value = dataUrl;
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
@@ -1273,6 +1267,7 @@ const resetForm = () => {
|
|||||||
formData.description = "";
|
formData.description = "";
|
||||||
formData.tags = "";
|
formData.tags = "";
|
||||||
formData.category = "";
|
formData.category = "";
|
||||||
|
formData.categoryId = 0;
|
||||||
formData.remark = "";
|
formData.remark = "";
|
||||||
submitSuccess.value = false;
|
submitSuccess.value = false;
|
||||||
submitError.value = "";
|
submitError.value = "";
|
||||||
@@ -1304,7 +1299,9 @@ const submitForm = async () => {
|
|||||||
description: formData.description,
|
description: formData.description,
|
||||||
tags: formData.tags,
|
tags: formData.tags,
|
||||||
category: formData.category,
|
category: formData.category,
|
||||||
|
categoryId: formData.categoryId,
|
||||||
remark: formData.remark,
|
remark: formData.remark,
|
||||||
|
arch: currentDebArch.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("[Submitter] ============== SUBMIT FORM ==============");
|
console.log("[Submitter] ============== SUBMIT FORM ==============");
|
||||||
@@ -1361,6 +1358,7 @@ const packageApp = async (storeArch: string) => {
|
|||||||
description: formData.description,
|
description: formData.description,
|
||||||
tags: formData.tags,
|
tags: formData.tags,
|
||||||
category: formData.category,
|
category: formData.category,
|
||||||
|
categoryId: formData.categoryId,
|
||||||
remark: formData.remark,
|
remark: formData.remark,
|
||||||
storeArch,
|
storeArch,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user