二次编辑商品
This commit is contained in:
@@ -3,6 +3,9 @@ package listing
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"hfb_sys/backend/internal/database"
|
||||||
|
"hfb_sys/backend/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConsumableValueOnlyCountsChargedResources(t *testing.T) {
|
func TestConsumableValueOnlyCountsChargedResources(t *testing.T) {
|
||||||
@@ -105,6 +108,46 @@ func TestCreateRequiresPublishAgreements(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRepositoryUpdateAllowsOfflineListingResubmit(t *testing.T) {
|
||||||
|
db := database.NewTestDB()
|
||||||
|
if err := db.AutoMigrate(&model.GameAccount{}, &model.RentalListing{}); err != nil {
|
||||||
|
t.Fatalf("failed to migrate test db: %v", err)
|
||||||
|
}
|
||||||
|
repo := NewRepository(db)
|
||||||
|
req := CreateRequest{
|
||||||
|
Title: "测试账号",
|
||||||
|
ServerRegion: "QQ",
|
||||||
|
LoginPlatform: "QQ账号密码",
|
||||||
|
RankLevel: "黑鹰",
|
||||||
|
HafCoinAmount: 100000000,
|
||||||
|
PriceCent: 10000,
|
||||||
|
DepositAmountCent: 50000,
|
||||||
|
ScreenshotURLS: []string{"https://example.com/a.png"},
|
||||||
|
AssetSummary: map[string]any{
|
||||||
|
"online_time": map[string]any{"start": "09:00", "end": "23:00"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
created, err := repo.Create(t.Context(), 1, req, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create listing: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := repo.Offline(t.Context(), 1, created.ID); err != nil {
|
||||||
|
t.Fatalf("failed to offline listing: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Title = "二次编辑账号"
|
||||||
|
updated, err := repo.Update(t.Context(), 1, created.ID, req, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected offline listing can be updated, got %v", err)
|
||||||
|
}
|
||||||
|
if updated.Title != "二次编辑账号" {
|
||||||
|
t.Fatalf("expected title updated, got %q", updated.Title)
|
||||||
|
}
|
||||||
|
if updated.Status != "draft" || updated.ReviewStatus != "pending" {
|
||||||
|
t.Fatalf("expected draft/pending after resubmit, got %s/%s", updated.Status, updated.ReviewStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestApplySellerListingPriceUsesSellerTotalPrice(t *testing.T) {
|
func TestApplySellerListingPriceUsesSellerTotalPrice(t *testing.T) {
|
||||||
item := &ListingDTO{
|
item := &ListingDTO{
|
||||||
PriceCent: 23800,
|
PriceCent: 23800,
|
||||||
|
|||||||
@@ -128,11 +128,21 @@ export async function fetchSellerListings() {
|
|||||||
return data.data.items
|
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) {
|
export async function createListing(payload: ListingPayload) {
|
||||||
const { data } = await apiClient.post<ApiResponse<Listing>>('/listings', payload)
|
const { data } = await apiClient.post<ApiResponse<Listing>>('/listings', payload)
|
||||||
return data.data
|
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) {
|
export async function submitListingReview(id: number) {
|
||||||
const { data } = await apiClient.post<ApiResponse<Listing>>(`/listings/${id}/submit-review`)
|
const { data } = await apiClient.post<ApiResponse<Listing>>(`/listings/${id}/submit-review`)
|
||||||
return data.data
|
return data.data
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { readError } from '@/shared/utils/error'
|
import { readError } from '@/shared/utils/error'
|
||||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
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 { fetchFileBlobByURL, uploadFile } from '@/shared/api/files'
|
||||||
import {
|
import {
|
||||||
@@ -16,7 +16,12 @@ import {
|
|||||||
type PublishSalePriceConfig,
|
type PublishSalePriceConfig,
|
||||||
type ScreenshotKey,
|
type ScreenshotKey,
|
||||||
} from '@/features/listings/api/listingOptions'
|
} 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 { usePricingCalculator } from '@/shared/composables/usePricingCalculator'
|
||||||
import {
|
import {
|
||||||
buildPublishDraft,
|
buildPublishDraft,
|
||||||
@@ -51,6 +56,7 @@ interface UsePublishFormOptions {
|
|||||||
|
|
||||||
export function usePublishForm(options: UsePublishFormOptions) {
|
export function usePublishForm(options: UsePublishFormOptions) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const uploading = ref(false)
|
const uploading = ref(false)
|
||||||
const suppressDraftSave = ref(false)
|
const suppressDraftSave = ref(false)
|
||||||
@@ -69,6 +75,11 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
const screenshotPreviews = reactive<Record<string, string[]>>({})
|
const screenshotPreviews = reactive<Record<string, string[]>>({})
|
||||||
const selectedSkins = ref<string[]>([])
|
const selectedSkins = ref<string[]>([])
|
||||||
let draftSaveTimer: number | undefined
|
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({
|
const pricing = usePricingCalculator({
|
||||||
form,
|
form,
|
||||||
@@ -93,11 +104,14 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
commonOnlineTimes.filter(time => !canUseOnlineEnd(time))
|
commonOnlineTimes.filter(time => !canUseOnlineEnd(time))
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
restoreDraft()
|
if (!isEditMode.value) restoreDraft()
|
||||||
draftReady.value = true
|
|
||||||
if (options.persistBeforeUnload) window.addEventListener('beforeunload', handleBeforeUnload)
|
if (options.persistBeforeUnload) window.addEventListener('beforeunload', handleBeforeUnload)
|
||||||
loadPublishOptions()
|
await loadPublishOptions()
|
||||||
|
if (isEditMode.value) {
|
||||||
|
await loadEditingListing()
|
||||||
|
}
|
||||||
|
draftReady.value = true
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
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 } = {}) {
|
function saveDraft(saveOptions: { force?: boolean } = {}) {
|
||||||
|
if (isEditMode.value) return
|
||||||
if (suppressDraftSave.value) {
|
if (suppressDraftSave.value) {
|
||||||
clearDraftSaveTimer()
|
clearDraftSaveTimer()
|
||||||
return
|
return
|
||||||
@@ -229,6 +258,65 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
hydrateScreenshotPreviews()
|
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() {
|
function resetDraftState() {
|
||||||
Object.assign(form, defaultPublishForm())
|
Object.assign(form, defaultPublishForm())
|
||||||
clearRecord(quantityValues)
|
clearRecord(quantityValues)
|
||||||
@@ -351,6 +439,11 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleResetDraft() {
|
async function handleResetDraft() {
|
||||||
|
if (isEditMode.value) {
|
||||||
|
await loadEditingListing()
|
||||||
|
options.notifySuccess('已恢复原商品信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await options.confirmReset?.()
|
await options.confirmReset?.()
|
||||||
} catch {
|
} catch {
|
||||||
@@ -565,7 +658,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
}
|
}
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const listing = await createListing({
|
const payload = {
|
||||||
title: pricing.publishTitle.value,
|
title: pricing.publishTitle.value,
|
||||||
description: form.remark,
|
description: form.remark,
|
||||||
server_region: form.server_region,
|
server_region: form.server_region,
|
||||||
@@ -578,18 +671,27 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
deposit_amount_cent: yuanToCent(Number(form.depositAmountYuan)),
|
deposit_amount_cent: yuanToCent(Number(form.depositAmountYuan)),
|
||||||
agreed_virtual_asset_sale: virtualAssetSaleAgreementChecked.value,
|
agreed_virtual_asset_sale: virtualAssetSaleAgreementChecked.value,
|
||||||
agreed_seller_agreement: sellerAgreementChecked.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
|
suppressDraftSave.value = true
|
||||||
clearDraftSaveTimer()
|
clearDraftSaveTimer()
|
||||||
options.notifySuccess(
|
options.notifySuccess(
|
||||||
listing.status === 'published' && listing.review_status === 'approved'
|
listing.status === 'published' && listing.review_status === 'approved'
|
||||||
? '发布成功,已上架'
|
? isEditMode.value
|
||||||
: '发布成功,等待后台审核'
|
? '修改成功,已上架'
|
||||||
|
: '发布成功,已上架'
|
||||||
|
: isEditMode.value
|
||||||
|
? '修改成功,等待后台审核'
|
||||||
|
: '发布成功,等待后台审核'
|
||||||
)
|
)
|
||||||
await router.push(options.submitSuccessPath)
|
await router.push(options.submitSuccessPath)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
options.notifyError(readError(error, '发布失败,请确认已登录并完成实名认证'))
|
options.notifyError(
|
||||||
|
readError(error, isEditMode.value ? '修改失败,请稍后重试' : '发布失败,请确认已登录并完成实名认证')
|
||||||
|
)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -763,6 +865,37 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
return pricing.screenshotSlots.value.find(item => item.key === key)?.label || '该截图'
|
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 {
|
return {
|
||||||
...pricing,
|
...pricing,
|
||||||
dailyLossOptions,
|
dailyLossOptions,
|
||||||
@@ -785,6 +918,10 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
selectedSkins,
|
selectedSkins,
|
||||||
uploadedScreenshotCount,
|
uploadedScreenshotCount,
|
||||||
requiredScreenshotCount,
|
requiredScreenshotCount,
|
||||||
|
isEditMode,
|
||||||
|
pageTitle,
|
||||||
|
submitButtonText,
|
||||||
|
submitLoadingText,
|
||||||
disabledOnlineStartOptions,
|
disabledOnlineStartOptions,
|
||||||
disabledOnlineEndOptions,
|
disabledOnlineEndOptions,
|
||||||
loadPublishOptions,
|
loadPublishOptions,
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ const {
|
|||||||
router,
|
router,
|
||||||
loading,
|
loading,
|
||||||
uploading,
|
uploading,
|
||||||
|
isEditMode,
|
||||||
|
pageTitle,
|
||||||
|
submitButtonText,
|
||||||
|
submitLoadingText,
|
||||||
publishAgreements,
|
publishAgreements,
|
||||||
virtualAssetSaleAgreementChecked,
|
virtualAssetSaleAgreementChecked,
|
||||||
sellerAgreementChecked,
|
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()">
|
<button class="back-btn" type="button" @click="router.back()">
|
||||||
<van-icon name="arrow-left" :size="20" />
|
<van-icon name="arrow-left" :size="20" />
|
||||||
</button>
|
</button>
|
||||||
<h1>发布账号</h1>
|
<h1>{{ pageTitle }}</h1>
|
||||||
<span class="header-spacer"></span>
|
<span class="header-spacer"></span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -710,7 +714,7 @@ function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
|
|||||||
|
|
||||||
<div class="publish-actions">
|
<div class="publish-actions">
|
||||||
<van-button round class="reset-btn" :disabled="loading" @click="handleResetDraft">
|
<van-button round class="reset-btn" :disabled="loading" @click="handleResetDraft">
|
||||||
重置
|
{{ isEditMode ? '恢复' : '重置' }}
|
||||||
</van-button>
|
</van-button>
|
||||||
<van-button
|
<van-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -718,10 +722,10 @@ function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
|
|||||||
class="submit-btn"
|
class="submit-btn"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:disabled="!canPublishAfterAgreements"
|
:disabled="!canPublishAfterAgreements"
|
||||||
loading-text="发布中..."
|
:loading-text="submitLoadingText"
|
||||||
@click="handleSubmit"
|
@click="handleSubmit"
|
||||||
>
|
>
|
||||||
保存发布
|
{{ submitButtonText }}
|
||||||
</van-button>
|
</van-button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ const {
|
|||||||
selectedSkins,
|
selectedSkins,
|
||||||
uploadedScreenshotCount,
|
uploadedScreenshotCount,
|
||||||
requiredScreenshotCount,
|
requiredScreenshotCount,
|
||||||
|
isEditMode,
|
||||||
|
submitButtonText,
|
||||||
disabledOnlineStartOptions,
|
disabledOnlineStartOptions,
|
||||||
disabledOnlineEndOptions,
|
disabledOnlineEndOptions,
|
||||||
serverOptions,
|
serverOptions,
|
||||||
@@ -684,13 +686,17 @@ function selectDailyLoss(value: string | number) {
|
|||||||
:disabled="!canPublishAfterAgreements"
|
:disabled="!canPublishAfterAgreements"
|
||||||
@click="handleSubmit"
|
@click="handleSubmit"
|
||||||
>
|
>
|
||||||
立即发布
|
{{ submitButtonText }}
|
||||||
</el-button>
|
</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
|
||||||
>
|
>
|
||||||
<el-button :icon="RefreshRight" :disabled="loading" @click="handleResetDraft"
|
<el-button :icon="RefreshRight" :disabled="loading" @click="handleResetDraft"
|
||||||
>重置草稿</el-button
|
>{{ isEditMode ? '恢复原信息' : '重置草稿' }}</el-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<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 { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fetchSellerListings,
|
fetchSellerListings,
|
||||||
@@ -18,6 +19,7 @@ const submittingID = ref<number | null>(null)
|
|||||||
const offliningID = ref<number | null>(null)
|
const offliningID = ref<number | null>(null)
|
||||||
const listings = ref<Listing[]>([])
|
const listings = ref<Listing[]>([])
|
||||||
const statusFilter = ref('all')
|
const statusFilter = ref('all')
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const displayListings = computed(() => {
|
const displayListings = computed(() => {
|
||||||
if (statusFilter.value === 'all') return listings.value
|
if (statusFilter.value === 'all') return listings.value
|
||||||
@@ -125,10 +127,20 @@ function canSubmit(row: Listing) {
|
|||||||
return !isPendingReview(row) && row.status !== 'rented' && row.status !== 'published'
|
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) {
|
function canOffline(row: Listing) {
|
||||||
return row.status !== 'rented' && row.status !== 'offline'
|
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) {
|
function statusTone(status: string) {
|
||||||
const tones: Record<string, string> = {
|
const tones: Record<string, string> = {
|
||||||
published: 'success',
|
published: 'success',
|
||||||
@@ -235,6 +247,9 @@ function isPendingReview(row: Listing) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="listing-actions">
|
<div class="listing-actions">
|
||||||
|
<RouterLink v-if="canEdit(item)" :to="editPath(item)">
|
||||||
|
<el-button :icon="Edit">编辑</el-button>
|
||||||
|
</RouterLink>
|
||||||
<el-button
|
<el-button
|
||||||
:disabled="!canSubmit(item)"
|
:disabled="!canSubmit(item)"
|
||||||
:loading="submittingID === item.id"
|
:loading="submittingID === item.id"
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ export function getMobilePath(path: string): string {
|
|||||||
if (path === '/seller/listings/create') {
|
if (path === '/seller/listings/create') {
|
||||||
return '/m/seller/listings/create'
|
return '/m/seller/listings/create'
|
||||||
}
|
}
|
||||||
|
if (path.startsWith('/seller/listings/') && path.endsWith('/edit')) {
|
||||||
|
return `/m${path}`
|
||||||
|
}
|
||||||
if (path === '/messages' || path.startsWith('/messages/')) {
|
if (path === '/messages' || path.startsWith('/messages/')) {
|
||||||
return '/m/messages'
|
return '/m/messages'
|
||||||
}
|
}
|
||||||
@@ -66,6 +69,7 @@ export function getPcPath(path: string): string {
|
|||||||
if (subPath === '/register') return '/login' // PC uses /login for both auth actions
|
if (subPath === '/register') return '/login' // PC uses /login for both auth actions
|
||||||
if (subPath === '/realname') return '/realname'
|
if (subPath === '/realname') return '/realname'
|
||||||
if (subPath === '/seller/listings/create') return '/seller/listings/create'
|
if (subPath === '/seller/listings/create') return '/seller/listings/create'
|
||||||
|
if (subPath.startsWith('/seller/listings/') && subPath.endsWith('/edit')) return subPath
|
||||||
|
|
||||||
if (subPath === '/messages') return '/messages'
|
if (subPath === '/messages') return '/messages'
|
||||||
if (subPath.startsWith('/chats/')) return '/messages/' + subPath.substring(7)
|
if (subPath.startsWith('/chats/')) return '/messages/' + subPath.substring(7)
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ export const mobileRoutes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/features/seller/views/MobileSellerListingCreateView.vue'),
|
component: () => import('@/features/seller/views/MobileSellerListingCreateView.vue'),
|
||||||
meta: { layout: 'blank', requiresAuth: true, requiresRealname: true },
|
meta: { layout: 'blank', requiresAuth: true, requiresRealname: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/m/seller/listings/:id/edit',
|
||||||
|
name: 'mobile-seller-listing-edit',
|
||||||
|
component: () => import('@/features/seller/views/MobileSellerListingCreateView.vue'),
|
||||||
|
meta: { layout: 'blank', requiresAuth: true, requiresRealname: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/m/seller/listings',
|
path: '/m/seller/listings',
|
||||||
name: 'mobile-seller-listings',
|
name: 'mobile-seller-listings',
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ export const sellerRoutes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/features/seller/views/SellerListingCreateView.vue'),
|
component: () => import('@/features/seller/views/SellerListingCreateView.vue'),
|
||||||
meta: { requiresAuth: true, requiresRealname: true },
|
meta: { requiresAuth: true, requiresRealname: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/seller/listings/:id/edit',
|
||||||
|
name: 'seller-listing-edit',
|
||||||
|
component: () => import('@/features/seller/views/SellerListingCreateView.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresRealname: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/seller/handoffs',
|
path: '/seller/handoffs',
|
||||||
name: 'seller-handoffs',
|
name: 'seller-handoffs',
|
||||||
|
|||||||
Reference in New Issue
Block a user