增加发布协议确认配置

This commit is contained in:
yml2213
2026-06-07 21:14:49 +08:00
parent 954c3d6be3
commit eebd2d3b04
19 changed files with 760 additions and 13 deletions
@@ -3,11 +3,14 @@ import { useRouter } from 'vue-router'
import { fetchFileBlobByURL, uploadFile } from '@/shared/api/files'
import {
emptyListingPublishAgreements,
emptyListingPublishOptions,
emptyListingSalePriceConfig,
fetchListingPublishAgreements,
fetchListingPublishOptions,
fetchListingSalePriceConfig,
type ChargeMode,
type ListingPublishAgreements,
type ListingPublishOptions,
type PublishSalePriceConfig,
type ScreenshotKey,
@@ -45,6 +48,9 @@ export function usePublishForm(options: UsePublishFormOptions) {
const draftReady = ref(false)
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions)
const salePriceConfig = ref<PublishSalePriceConfig>(emptyListingSalePriceConfig)
const publishAgreements = ref<ListingPublishAgreements>(emptyListingPublishAgreements)
const virtualAssetSaleAgreementChecked = ref(false)
const sellerAgreementChecked = ref(false)
const fileInput = ref<HTMLInputElement | null>(null)
const activeUploadKey = ref<ScreenshotKey>('coin')
const form = reactive<PublishForm>(defaultPublishForm())
@@ -66,6 +72,9 @@ export function usePublishForm(options: UsePublishFormOptions) {
})
const uploadedScreenshotCount = computed(() => pricing.screenshotUrls.value.length)
const requiredScreenshotCount = ref(0)
const canPublishAfterAgreements = computed(
() => virtualAssetSaleAgreementChecked.value && sellerAgreementChecked.value,
)
onMounted(() => {
restoreDraft()
@@ -114,12 +123,14 @@ export function usePublishForm(options: UsePublishFormOptions) {
async function loadPublishOptions() {
try {
const [nextPublishOptions, nextSalePriceConfig] = await Promise.all([
const [nextPublishOptions, nextSalePriceConfig, nextPublishAgreements] = await Promise.all([
fetchListingPublishOptions(),
fetchListingSalePriceConfig(),
fetchListingPublishAgreements(),
])
publishOptions.value = nextPublishOptions
salePriceConfig.value = nextSalePriceConfig
publishAgreements.value = nextPublishAgreements
// 默认数字都是0
for (const item of nextPublishOptions.quantity_items) {
@@ -130,6 +141,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
} catch {
publishOptions.value = emptyListingPublishOptions
salePriceConfig.value = emptyListingSalePriceConfig
publishAgreements.value = emptyListingPublishAgreements
}
}
@@ -205,6 +217,8 @@ export function usePublishForm(options: UsePublishFormOptions) {
clearRecord(screenshotFiles)
revokeAllScreenshotPreviews()
selectedSkins.value = []
virtualAssetSaleAgreementChecked.value = false
sellerAgreementChecked.value = false
activeUploadKey.value = 'coin'
// Also re-initialize quantityValues to 0
@@ -397,6 +411,8 @@ export function usePublishForm(options: UsePublishFormOptions) {
screenshot_urls: pricing.screenshotUrls.value,
price: pricing.calculatedFinalPrice.value,
deposit_amount: Number(form.deposit_amount),
agreed_virtual_asset_sale: virtualAssetSaleAgreementChecked.value,
agreed_seller_agreement: sellerAgreementChecked.value,
})
removePublishDraft(options.draftKey)
suppressDraftSave.value = true
@@ -445,6 +461,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
}
if (!pricing.calculatedFinalPrice.value) return '请完善币数、保险、体力和负重后再发布'
if (!Number.isFinite(pricing.calculatedFinalPrice.value)) return '发布价格计算异常,请检查填写内容'
if (!canPublishAfterAgreements.value) return '请先阅读并勾选两份发布协议'
for (const item of pricing.screenshotSlots.value) {
if (isScreenshotRequired(item) && !screenshotFiles[item.key]) return `请上传${item.label}`
}
@@ -530,6 +547,10 @@ export function usePublishForm(options: UsePublishFormOptions) {
router,
loading,
uploading,
publishAgreements,
virtualAssetSaleAgreementChecked,
sellerAgreementChecked,
canPublishAfterAgreements,
fileInput,
activeUploadKey,
form,
@@ -644,6 +644,64 @@
line-height: 1.5;
}
.publish-agreement-card {
display: grid;
gap: 10px;
margin-bottom: 12px;
padding: 12px;
border: 1px solid #edf0f4;
border-radius: 8px;
background: #fff;
}
.agreement-check-label {
color: #30343a;
font-size: 13px;
font-weight: 700;
line-height: 1.45;
}
.agreement-link {
padding: 0;
border: none;
background: transparent;
color: #1477ff;
font: inherit;
}
.publish-agreement-popup {
max-height: 86vh;
}
.agreement-popup-body {
display: grid;
gap: 12px;
max-height: 86vh;
overflow: auto;
padding: 20px 16px calc(16px + env(safe-area-inset-bottom));
}
.agreement-popup-body h3 {
margin: 0;
color: #17233d;
font-size: 18px;
font-weight: 900;
}
.agreement-popup-content {
max-height: 62vh;
overflow: auto;
padding: 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #f8fafc;
color: #1f2937;
font-size: 12px;
font-weight: 700;
line-height: 1.7;
white-space: pre-wrap;
}
.publish-actions {
display: grid;
grid-template-columns: 92px minmax(0, 1fr);
@@ -1,8 +1,10 @@
<script setup lang="ts">
import { showDialog, showToast } from 'vant'
import { computed, ref } from 'vue'
import MobileBottomNav from '@/components/MobileBottomNav.vue'
import { usePublishForm } from '@/features/seller/composables/usePublishForm'
import type { AgreementContent } from '@/features/listings/api/listingOptions'
const {
commonOnlineTimes,
@@ -11,6 +13,10 @@ const {
router,
loading,
uploading,
publishAgreements,
virtualAssetSaleAgreementChecked,
sellerAgreementChecked,
canPublishAfterAgreements,
fileInput,
activeUploadKey,
form,
@@ -78,7 +84,15 @@ const {
notifyError: (message) => showToast({ message, icon: 'cross' }),
})
const agreementPopupVisible = ref(false)
const activeAgreement = ref<AgreementContent | null>(null)
const activeAgreementTitle = computed(() => activeAgreement.value?.title || '协议内容')
const activeAgreementContent = computed(() => activeAgreement.value?.content || '')
function openPublishAgreement(agreement: AgreementContent) {
activeAgreement.value = agreement
agreementPopupVisible.value = true
}
function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
setter(value)
@@ -637,6 +651,33 @@ function selectOnlineEnd(value: string) {
/>
</div>
<div class="publish-agreement-card">
<van-checkbox v-model="virtualAssetSaleAgreementChecked" icon-size="18px">
<span class="agreement-check-label">
我已阅读并同意
<button
type="button"
class="agreement-link"
@click.stop.prevent="openPublishAgreement(publishAgreements.virtual_asset_sale)"
>
{{ publishAgreements.virtual_asset_sale.title }}
</button>
</span>
</van-checkbox>
<van-checkbox v-model="sellerAgreementChecked" icon-size="18px">
<span class="agreement-check-label">
我已阅读并同意
<button
type="button"
class="agreement-link"
@click.stop.prevent="openPublishAgreement(publishAgreements.seller_agreement)"
>
{{ publishAgreements.seller_agreement.title }}
</button>
</span>
</van-checkbox>
</div>
<div class="publish-actions">
<van-button
round
@@ -651,6 +692,7 @@ function selectOnlineEnd(value: string) {
round
class="submit-btn"
:loading="loading"
:disabled="!canPublishAfterAgreements"
loading-text="发布中..."
@click="handleSubmit"
>
@@ -659,6 +701,13 @@ function selectOnlineEnd(value: string) {
</div>
</section>
<van-popup v-model:show="agreementPopupVisible" round closeable position="bottom" lock-scroll class="publish-agreement-popup">
<div class="agreement-popup-body">
<h3>{{ activeAgreementTitle }}</h3>
<div class="agreement-popup-content">{{ activeAgreementContent }}</div>
</div>
</van-popup>
<MobileBottomNav />
</main>
</template>
@@ -679,14 +679,11 @@
position: sticky;
top: var(--summary-sticky-top);
align-self: start;
max-height: calc(100vh - var(--summary-sticky-top) - 24px);
}
.summary-panel {
padding: 16px;
max-height: inherit;
overflow-y: auto;
overscroll-behavior: contain;
padding: 14px;
overflow: visible;
border: 1px solid #e6ebf2;
border-radius: var(--radius-8);
background: #fff;
@@ -710,9 +707,9 @@
.summary-breakdown {
display: grid;
gap: 10px;
margin-top: 14px;
padding: 12px;
gap: 8px;
margin-top: 12px;
padding: 10px;
border: 1px solid #ffe1c7;
border-radius: var(--radius-8);
background: #fffaf6;
@@ -745,15 +742,15 @@
.summary-list {
display: grid;
gap: 10px;
margin: 14px 0;
gap: 8px;
margin: 12px 0;
}
.summary-list div {
display: flex;
justify-content: space-between;
gap: 12px;
padding-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #eef2f6;
}
@@ -764,6 +761,76 @@
text-align: right;
}
.publish-agreement-panel {
display: grid;
gap: 6px;
margin-bottom: 12px;
padding: 8px 10px;
border: 1px solid #eef2f6;
border-radius: var(--radius-8);
background: #f8fafc;
}
.publish-agreement-panel :deep(.el-checkbox) {
align-items: center;
height: 24px;
margin-right: 0;
white-space: nowrap;
}
.publish-agreement-panel :deep(.el-checkbox__label) {
min-width: 0;
line-height: 1.2;
}
.agreement-check-label {
display: inline-flex;
align-items: center;
gap: 2px;
width: 100%;
max-width: 100%;
min-width: 0;
color: var(--color-text-main);
font-size: 12px;
font-weight: 800;
white-space: nowrap;
}
.agreement-link {
display: inline-block;
min-width: 0;
max-width: 100%;
overflow: hidden;
padding: 0;
border: none;
background: transparent;
color: #1477ff;
font: inherit;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
.agreement-link:hover {
color: #0f5fd0;
text-decoration: underline;
}
.agreement-dialog-content {
max-height: 62vh;
overflow: auto;
padding: 14px;
border: 1px solid #d8dee9;
border-radius: var(--radius-8);
background: #f8fafc;
color: #1f2937;
font-size: 13px;
font-weight: 700;
line-height: 1.75;
white-space: pre-wrap;
}
.summary-actions {
display: grid;
gap: 10px;
@@ -1,8 +1,10 @@
<script setup lang="ts">
import { Delete, DocumentChecked, Picture, RefreshRight, UploadFilled } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, ref } from 'vue'
import { usePublishForm } from '@/features/seller/composables/usePublishForm'
import type { AgreementContent } from '@/features/listings/api/listingOptions'
import OptionChips from './components/OptionChips.vue'
import PublishSection from './components/PublishSection.vue'
@@ -12,6 +14,10 @@ const {
formatNumber,
loading,
uploading,
publishAgreements,
virtualAssetSaleAgreementChecked,
sellerAgreementChecked,
canPublishAfterAgreements,
fileInput,
activeUploadKey,
form,
@@ -83,10 +89,20 @@ const {
notifyError: (message) => ElMessage.error(message),
})
const agreementDialogVisible = ref(false)
const activeAgreement = ref<AgreementContent | null>(null)
const activeAgreementTitle = computed(() => activeAgreement.value?.title || '协议内容')
const activeAgreementContent = computed(() => activeAgreement.value?.content || '')
function toggleSkinOption(value: string | number) {
toggleSkin(String(value))
}
function openPublishAgreement(agreement: AgreementContent) {
activeAgreement.value = agreement
agreementDialogVisible.value = true
}
function selectDailyLoss(value: string | number) {
form.daily_loss_m = Number(value)
}
@@ -510,14 +526,55 @@ function selectOnlineEnd(value: string | number) {
<strong>{{ uploadedScreenshotCount }}/{{ requiredScreenshotCount }}</strong>
</div>
</div>
<div class="publish-agreement-panel">
<el-checkbox v-model="virtualAssetSaleAgreementChecked">
<span class="agreement-check-label">
我已阅读并同意
<button
type="button"
class="agreement-link"
:title="publishAgreements.virtual_asset_sale.title"
@click.prevent.stop="openPublishAgreement(publishAgreements.virtual_asset_sale)"
>
{{ publishAgreements.virtual_asset_sale.title }}
</button>
</span>
</el-checkbox>
<el-checkbox v-model="sellerAgreementChecked">
<span class="agreement-check-label">
我已阅读并同意
<button
type="button"
class="agreement-link"
:title="publishAgreements.seller_agreement.title"
@click.prevent.stop="openPublishAgreement(publishAgreements.seller_agreement)"
>
{{ publishAgreements.seller_agreement.title }}
</button>
</span>
</el-checkbox>
</div>
<div class="summary-actions">
<el-button :icon="UploadFilled" class="btn-publish" type="primary" :loading="loading" @click="handleSubmit">立即发布</el-button>
<el-button
:icon="UploadFilled"
class="btn-publish"
type="primary"
:loading="loading"
:disabled="!canPublishAfterAgreements"
@click="handleSubmit"
>
立即发布
</el-button>
<el-button :icon="DocumentChecked" :disabled="loading" @click="handleSaveDraft">保存草稿</el-button>
<el-button :icon="RefreshRight" :disabled="loading" @click="handleResetDraft">重置草稿</el-button>
</div>
</div>
</aside>
</div>
<el-dialog v-model="agreementDialogVisible" :title="activeAgreementTitle" width="720px" class="publish-agreement-dialog">
<div class="agreement-dialog-content">{{ activeAgreementContent }}</div>
</el-dialog>
</main>
</template>