二次编辑商品

This commit is contained in:
yml2213
2026-06-12 21:43:02 +08:00
parent c8b8922c8d
commit f1ae72cc7a
9 changed files with 251 additions and 20 deletions
@@ -128,11 +128,21 @@ export async function fetchSellerListings() {
return data.data.items
}
export async function fetchSellerListing(id: string | number) {
const { data } = await apiClient.get<ApiResponse<Listing>>(`/seller/listings/${id}`)
return data.data
}
export async function createListing(payload: ListingPayload) {
const { data } = await apiClient.post<ApiResponse<Listing>>('/listings', payload)
return data.data
}
export async function updateListing(id: string | number, payload: ListingPayload) {
const { data } = await apiClient.put<ApiResponse<Listing>>(`/listings/${id}`, payload)
return data.data
}
export async function submitListingReview(id: number) {
const { data } = await apiClient.post<ApiResponse<Listing>>(`/listings/${id}/submit-review`)
return data.data
@@ -1,6 +1,6 @@
import { readError } from '@/shared/utils/error'
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import { fetchFileBlobByURL, uploadFile } from '@/shared/api/files'
import {
@@ -16,7 +16,12 @@ import {
type PublishSalePriceConfig,
type ScreenshotKey,
} from '@/features/listings/api/listingOptions'
import { createListing } from '@/features/listings/api/listings'
import {
createListing,
fetchSellerListing,
updateListing,
type Listing,
} from '@/features/listings/api/listings'
import { usePricingCalculator } from '@/shared/composables/usePricingCalculator'
import {
buildPublishDraft,
@@ -51,6 +56,7 @@ interface UsePublishFormOptions {
export function usePublishForm(options: UsePublishFormOptions) {
const router = useRouter()
const route = useRoute()
const loading = ref(false)
const uploading = ref(false)
const suppressDraftSave = ref(false)
@@ -69,6 +75,11 @@ export function usePublishForm(options: UsePublishFormOptions) {
const screenshotPreviews = reactive<Record<string, string[]>>({})
const selectedSkins = ref<string[]>([])
let draftSaveTimer: number | undefined
const editListingID = computed(() => Number(route.params.id || 0))
const isEditMode = computed(() => Number.isFinite(editListingID.value) && editListingID.value > 0)
const pageTitle = computed(() => (isEditMode.value ? '编辑账号' : '发布账号'))
const submitButtonText = computed(() => (isEditMode.value ? '提交修改' : '立即发布'))
const submitLoadingText = computed(() => (isEditMode.value ? '提交中...' : '发布中...'))
const pricing = usePricingCalculator({
form,
@@ -93,11 +104,14 @@ export function usePublishForm(options: UsePublishFormOptions) {
commonOnlineTimes.filter(time => !canUseOnlineEnd(time))
)
onMounted(() => {
restoreDraft()
draftReady.value = true
onMounted(async () => {
if (!isEditMode.value) restoreDraft()
if (options.persistBeforeUnload) window.addEventListener('beforeunload', handleBeforeUnload)
loadPublishOptions()
await loadPublishOptions()
if (isEditMode.value) {
await loadEditingListing()
}
draftReady.value = true
})
onBeforeUnmount(() => {
@@ -164,7 +178,22 @@ export function usePublishForm(options: UsePublishFormOptions) {
}
}
async function loadEditingListing() {
if (!isEditMode.value) return
loading.value = true
try {
const listing = await fetchSellerListing(editListingID.value)
applyListingToForm(listing)
} catch (error) {
options.notifyError(readError(error, '商品信息加载失败'))
await router.push(options.submitSuccessPath)
} finally {
loading.value = false
}
}
function saveDraft(saveOptions: { force?: boolean } = {}) {
if (isEditMode.value) return
if (suppressDraftSave.value) {
clearDraftSaveTimer()
return
@@ -229,6 +258,65 @@ export function usePublishForm(options: UsePublishFormOptions) {
hydrateScreenshotPreviews()
}
function applyListingToForm(listing: Listing) {
const summary = recordValue(listing.asset_summary)
const onlineTime = recordValue(summary.online_time)
const screenshotGroups = recordValue(summary.screenshot_groups)
const skinGroups = recordValue(summary.skin_groups)
const breakdown = recordValue(summary.price_breakdown)
clearRecord(quantityValues)
clearRecord(quantityModes)
clearRecord(screenshotFiles)
revokeAllScreenshotPreviews()
Object.assign(form, {
server_region: listing.server_region || '',
face_owner: stringValue(summary.face_owner),
haf_coin_amount: listing.haf_coin_amount > 0 ? listing.haf_coin_amount / 1000000 : '',
rank_level: listing.rank_level || '',
secret_kd: stringValue(summary.secret_kd),
fire_level: numberOrEmpty(summary.fire_level),
daily_loss_m: numberOrDefault(summary.daily_loss_m, 10),
accelerated_sale_ratio: numberOrEmpty(breakdown.accelerated_sale_ratio),
season_insurance: stringValue(summary.season_insurance),
stamina_level: stringValue(summary.stamina_level),
load_level: stringValue(summary.load_level),
login_method: listing.login_platform || '',
online_start: stringValue(onlineTime.start),
online_end: stringValue(onlineTime.end),
ban_record: stringValue(summary.ban_record),
common_regions: stringArray(summary.common_regions),
depositAmountYuan: listing.deposit_amount_cent > 0 ? listing.deposit_amount_cent / 100 : '',
remark: listing.description || stringValue(summary.remark),
})
for (const item of publishOptions.value.quantity_items) {
quantityValues[item.key] = 0
}
for (const item of arrayValue(summary.resources)) {
const resource = recordValue(item)
const key = stringValue(resource.key)
if (!key) continue
quantityValues[key] = numberOrDefault(resource.quantity, 0)
const mode = stringValue(resource.mode)
quantityModes[key] = mode === '赠送' ? '赠送' : '收费'
}
for (const [key, urls] of Object.entries(screenshotGroups)) {
const cleaned = stringArray(urls)
if (cleaned.length) screenshotFiles[key] = cleaned
}
if (!Object.keys(screenshotFiles).length && listing.screenshot_urls.length) {
screenshotFiles.coin = [...listing.screenshot_urls]
}
selectedSkins.value = Object.values(skinGroups).flatMap(value => stringArray(value))
virtualAssetSaleAgreementChecked.value = true
sellerAgreementChecked.value = true
hydrateScreenshotPreviews()
}
function resetDraftState() {
Object.assign(form, defaultPublishForm())
clearRecord(quantityValues)
@@ -351,6 +439,11 @@ export function usePublishForm(options: UsePublishFormOptions) {
}
async function handleResetDraft() {
if (isEditMode.value) {
await loadEditingListing()
options.notifySuccess('已恢复原商品信息')
return
}
try {
await options.confirmReset?.()
} catch {
@@ -565,7 +658,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
}
loading.value = true
try {
const listing = await createListing({
const payload = {
title: pricing.publishTitle.value,
description: form.remark,
server_region: form.server_region,
@@ -578,18 +671,27 @@ export function usePublishForm(options: UsePublishFormOptions) {
deposit_amount_cent: yuanToCent(Number(form.depositAmountYuan)),
agreed_virtual_asset_sale: virtualAssetSaleAgreementChecked.value,
agreed_seller_agreement: sellerAgreementChecked.value,
})
removePublishDraft(options.draftKey)
}
const listing = isEditMode.value
? await updateListing(editListingID.value, payload)
: await createListing(payload)
if (!isEditMode.value) removePublishDraft(options.draftKey)
suppressDraftSave.value = true
clearDraftSaveTimer()
options.notifySuccess(
listing.status === 'published' && listing.review_status === 'approved'
? '发布成功,已上架'
: '发布成功,等待后台审核'
? isEditMode.value
? '修改成功,已上架'
: '发布成功,已上架'
: isEditMode.value
? '修改成功,等待后台审核'
: '发布成功,等待后台审核'
)
await router.push(options.submitSuccessPath)
} catch (error) {
options.notifyError(readError(error, '发布失败,请确认已登录并完成实名认证'))
options.notifyError(
readError(error, isEditMode.value ? '修改失败,请稍后重试' : '发布失败,请确认已登录并完成实名认证')
)
} finally {
loading.value = false
}
@@ -763,6 +865,37 @@ export function usePublishForm(options: UsePublishFormOptions) {
return pricing.screenshotSlots.value.find(item => item.key === key)?.label || '该截图'
}
function recordValue(value: unknown): Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: {}
}
function arrayValue(value: unknown): unknown[] {
return Array.isArray(value) ? value : []
}
function stringValue(value: unknown) {
if (value === undefined || value === null) return ''
return String(value)
}
function stringArray(value: unknown) {
return Array.isArray(value)
? value.filter((item): item is string => typeof item === 'string' && Boolean(item))
: []
}
function numberOrEmpty(value: unknown): number | '' {
const parsed = Number(value)
return Number.isFinite(parsed) && parsed > 0 ? parsed : ''
}
function numberOrDefault(value: unknown, fallback: number) {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : fallback
}
return {
...pricing,
dailyLossOptions,
@@ -785,6 +918,10 @@ export function usePublishForm(options: UsePublishFormOptions) {
selectedSkins,
uploadedScreenshotCount,
requiredScreenshotCount,
isEditMode,
pageTitle,
submitButtonText,
submitLoadingText,
disabledOnlineStartOptions,
disabledOnlineEndOptions,
loadPublishOptions,
@@ -14,6 +14,10 @@ const {
router,
loading,
uploading,
isEditMode,
pageTitle,
submitButtonText,
submitLoadingText,
publishAgreements,
virtualAssetSaleAgreementChecked,
sellerAgreementChecked,
@@ -119,7 +123,7 @@ function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
<button class="back-btn" type="button" @click="router.back()">
<van-icon name="arrow-left" :size="20" />
</button>
<h1>发布账号</h1>
<h1>{{ pageTitle }}</h1>
<span class="header-spacer"></span>
</header>
@@ -710,7 +714,7 @@ function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
<div class="publish-actions">
<van-button round class="reset-btn" :disabled="loading" @click="handleResetDraft">
重置
{{ isEditMode ? '恢复' : '重置' }}
</van-button>
<van-button
type="primary"
@@ -718,10 +722,10 @@ function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
class="submit-btn"
:loading="loading"
:disabled="!canPublishAfterAgreements"
loading-text="发布中..."
:loading-text="submitLoadingText"
@click="handleSubmit"
>
保存发布
{{ submitButtonText }}
</van-button>
</div>
</section>
@@ -33,6 +33,8 @@ const {
selectedSkins,
uploadedScreenshotCount,
requiredScreenshotCount,
isEditMode,
submitButtonText,
disabledOnlineStartOptions,
disabledOnlineEndOptions,
serverOptions,
@@ -684,13 +686,17 @@ function selectDailyLoss(value: string | number) {
:disabled="!canPublishAfterAgreements"
@click="handleSubmit"
>
立即发布
{{ submitButtonText }}
</el-button>
<el-button :icon="DocumentChecked" :disabled="loading" @click="handleSaveDraft"
<el-button
v-if="!isEditMode"
:icon="DocumentChecked"
:disabled="loading"
@click="handleSaveDraft"
>保存草稿</el-button
>
<el-button :icon="RefreshRight" :disabled="loading" @click="handleResetDraft"
>重置草稿</el-button
>{{ isEditMode ? '恢复原信息' : '重置草稿' }}</el-button
>
</div>
</div>
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { CopyDocument, Plus, Refresh } from '@element-plus/icons-vue'
import { CopyDocument, Edit, Plus, Refresh } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import {
fetchSellerListings,
@@ -18,6 +19,7 @@ const submittingID = ref<number | null>(null)
const offliningID = ref<number | null>(null)
const listings = ref<Listing[]>([])
const statusFilter = ref('all')
const route = useRoute()
const displayListings = computed(() => {
if (statusFilter.value === 'all') return listings.value
@@ -125,10 +127,20 @@ function canSubmit(row: Listing) {
return !isPendingReview(row) && row.status !== 'rented' && row.status !== 'published'
}
function canEdit(row: Listing) {
return !isPendingReview(row) && row.status !== 'rented' && row.status !== 'published'
}
function canOffline(row: Listing) {
return row.status !== 'rented' && row.status !== 'offline'
}
function editPath(row: Listing) {
return route.path.startsWith('/m/')
? `/m/seller/listings/${row.id}/edit`
: `/seller/listings/${row.id}/edit`
}
function statusTone(status: string) {
const tones: Record<string, string> = {
published: 'success',
@@ -235,6 +247,9 @@ function isPendingReview(row: Listing) {
</div>
<div class="listing-actions">
<RouterLink v-if="canEdit(item)" :to="editPath(item)">
<el-button :icon="Edit">编辑</el-button>
</RouterLink>
<el-button
:disabled="!canSubmit(item)"
:loading="submittingID === item.id"