diff --git a/backend/internal/modules/listing/dto.go b/backend/internal/modules/listing/dto.go index d1a7f47..1e4d62a 100644 --- a/backend/internal/modules/listing/dto.go +++ b/backend/internal/modules/listing/dto.go @@ -47,6 +47,9 @@ type CreateRequest struct { ScreenshotURLS []string `json:"screenshot_urls"` Price float64 `json:"price"` DepositAmount float64 `json:"deposit_amount"` + + AgreedVirtualAssetSale bool `json:"agreed_virtual_asset_sale"` + AgreedSellerAgreement bool `json:"agreed_seller_agreement"` } type UpdateRequest = CreateRequest diff --git a/backend/internal/modules/listing/handler.go b/backend/internal/modules/listing/handler.go index 410359c..9918a74 100644 --- a/backend/internal/modules/listing/handler.go +++ b/backend/internal/modules/listing/handler.go @@ -577,6 +577,8 @@ func writeListingError(c *gin.Context, err error) { response.BadRequest(c, "哈夫币数量不正确") case errors.Is(err, ErrMissingScreenshot): response.BadRequest(c, "请至少上传一张账号截图") + case errors.Is(err, ErrAgreementRequired): + response.BadRequest(c, "请先阅读并同意发布协议") case errors.Is(err, ErrMissingUploaderName): response.BadRequest(c, "上传人名称不能为空") case errors.Is(err, ErrMissingUploadData): diff --git a/backend/internal/modules/listing/service.go b/backend/internal/modules/listing/service.go index 9dc664c..7227abe 100644 --- a/backend/internal/modules/listing/service.go +++ b/backend/internal/modules/listing/service.go @@ -22,6 +22,7 @@ var ( ErrDepositTooLow = errors.New("listing deposit too low") ErrInvalidHafCoin = errors.New("invalid haf coin amount") ErrMissingScreenshot = errors.New("missing screenshot") + ErrAgreementRequired = errors.New("listing publish agreement required") ErrMissingUploaderName = errors.New("missing uploader name") ErrMissingUploadData = errors.New("missing upload data") ErrUploaderNotFound = errors.New("uploader not found") @@ -80,6 +81,9 @@ func (s *Service) Create(ownerID uint64, req CreateRequest) (*ListingDTO, error) if s.repo == nil { return nil, ErrDependencyUnavailable } + if !req.AgreedVirtualAssetSale || !req.AgreedSellerAgreement { + return nil, ErrAgreementRequired + } rules, err := s.publishRules() if err != nil { return nil, err diff --git a/backend/internal/modules/listing/service_test.go b/backend/internal/modules/listing/service_test.go index d3ffc19..f7c28cf 100644 --- a/backend/internal/modules/listing/service_test.go +++ b/backend/internal/modules/listing/service_test.go @@ -58,6 +58,15 @@ func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) { } } +func TestCreateRequiresPublishAgreements(t *testing.T) { + service := NewService(&Repository{}, nil) + + _, err := service.Create(1, CreateRequest{}) + if err != ErrAgreementRequired { + t.Fatalf("expected ErrAgreementRequired, got %v", err) + } +} + func TestApplySellerListingPriceUsesSellerTotalPrice(t *testing.T) { item := &ListingDTO{ Price: 238, diff --git a/backend/internal/modules/systemconfig/dto.go b/backend/internal/modules/systemconfig/dto.go index c72b8f1..442216c 100644 --- a/backend/internal/modules/systemconfig/dto.go +++ b/backend/internal/modules/systemconfig/dto.go @@ -32,6 +32,11 @@ type OrderAgreementsDTO struct { RenterAgreement AgreementContentDTO `json:"renter_agreement"` } +type ListingPublishAgreementsDTO struct { + VirtualAssetSale AgreementContentDTO `json:"virtual_asset_sale"` + SellerAgreement AgreementContentDTO `json:"seller_agreement"` +} + type PostRentalNoticeDTO struct { Title string `json:"title"` Content string `json:"content"` diff --git a/backend/internal/modules/systemconfig/handler.go b/backend/internal/modules/systemconfig/handler.go index 2952258..849884d 100644 --- a/backend/internal/modules/systemconfig/handler.go +++ b/backend/internal/modules/systemconfig/handler.go @@ -54,6 +54,15 @@ func (h *Handler) OrderAgreements(c *gin.Context) { response.OK(c, agreements) } +func (h *Handler) ListingPublishAgreements(c *gin.Context) { + agreements, err := h.service.ListingPublishAgreements() + if err != nil { + writeConfigError(c, err) + return + } + response.OK(c, agreements) +} + func (h *Handler) PostRentalNotice(c *gin.Context) { notice, err := h.service.PostRentalNotice() if err != nil { diff --git a/backend/internal/modules/systemconfig/listing_publish_agreements.go b/backend/internal/modules/systemconfig/listing_publish_agreements.go new file mode 100644 index 0000000..53256a6 --- /dev/null +++ b/backend/internal/modules/systemconfig/listing_publish_agreements.go @@ -0,0 +1,56 @@ +package systemconfig + +import "encoding/json" + +const listingPublishAgreementsConfigKey = "listing.publish_agreements" + +func defaultListingPublishAgreementsConfigValue() string { + raw, err := json.Marshal(DefaultListingPublishAgreements()) + if err != nil { + return "{}" + } + return string(raw) +} + +func DefaultListingPublishAgreements() ListingPublishAgreementsDTO { + return ListingPublishAgreementsDTO{ + VirtualAssetSale: AgreementContentDTO{ + Title: "虚拟资产出售协议", + Content: `请您在发布账号前仔细阅读并确认以下内容: + +一、发布性质 +1. 您发布的账号、哈夫币、皮肤、装备、消耗品等均属于游戏内虚拟资产或使用权益展示。 +2. 虚拟资产受游戏厂商规则、版本更新、风控策略和账号状态影响,平台不承诺其具备现实货币价值或永久可用性。 +3. 您应确保发布信息真实、完整、可交接,且与截图材料和账号实际资产一致。 + +二、风险告知 +1. 发布账号后,买家下单前后可能因游戏环境变化、资产变动、封禁记录、登录限制等产生交易风险。 +2. 若您隐瞒账号异常、资产缺失、封禁记录、不可用物资或其他影响交易的信息,平台可依据证据处理赔付、退款或下架。 +3. 请勿绕过平台私下收款、私下交接或诱导用户脱离平台沟通,否则平台可能限制账号发布权限。 + +三、费用与结算 +1. 发布价格、押金、额外消耗品价值以平台发布页和订单页面展示为准。 +2. 订单完成后,平台将按订单记录、结账结果和相关规则进行结算。 +3. 若发生争议,平台将依据订单记录、资产快照、聊天记录、截图证据和双方说明进行处理。`, + }, + SellerAgreement: AgreementContentDTO{ + Title: "号主协议", + Content: `请您作为号主在发布账号前确认并遵守以下约定: + +一、账号发布义务 +1. 您应如实填写区服、段位、哈夫币数量、保险、体力、负重、登录方式、封禁记录、常用地区和备注信息。 +2. 您应上传真实、清晰、可核验的账号截图材料,不得使用伪造、过期或与账号不一致的截图。 +3. 您不得发布盗号、黑号、纠纷账号、已被限制使用账号或无法完成正常交接的账号。 + +二、交接与协作 +1. 买家下单后,您应在平台要求时间内完成交接,及时响应扫码、人脸冻结、登录验证等流程。 +2. 您应在租用期间保持可联系状态,不得无故拒绝交接、恶意拖延或私下更改交易条件。 +3. 租用结束后,请按平台流程确认归还和结账,配合核验资产状态。 + +三、责任承担 +1. 因您发布信息不实、交接不及时、账号异常隐瞒或私下交易造成的损失,平台可依据规则处理退款、赔付、下架或限制发布。 +2. 因租客违规使用造成的损失,您应通过平台流程提交证据并配合争议处理。 +3. 您同意平台依据订单记录、资产快照、聊天记录、截图证据和相关规则处理结算、赔付及争议。`, + }, + } +} diff --git a/backend/internal/modules/systemconfig/repository.go b/backend/internal/modules/systemconfig/repository.go index 751146f..945f0dd 100644 --- a/backend/internal/modules/systemconfig/repository.go +++ b/backend/internal/modules/systemconfig/repository.go @@ -31,6 +31,7 @@ var defaultConfigs = []defaultConfig{ {Key: "order.pending_payment_timeout_minutes", Value: "15", Description: "订单待支付超时取消分钟数"}, {Key: "order.return_overdue_grace_minutes", Value: "10", Description: "预计截止后结账宽限分钟数"}, {Key: "listing.review_required", Value: "false", Description: "发布账号是否需要后台人工审核"}, + {Key: listingPublishAgreementsConfigKey, Value: defaultListingPublishAgreementsConfigValue(), Description: "发布账号前协议配置 JSON"}, {Key: orderAgreementsConfigKey, Value: defaultOrderAgreementsConfigValue(), Description: "下单前协议配置 JSON"}, {Key: postRentalNoticeConfigKey, Value: defaultPostRentalNoticeConfigValue(), Description: "租后须知配置 JSON"}, {Key: "chat.default_support_admin_id", Value: "1", Description: "订单群聊回退客服 ID(仅当无可用客服时使用)"}, @@ -47,6 +48,7 @@ var adminVisibleConfigKeys = []string{ "handoff.owner_return_confirm_timeout_minutes", "handoff.owner_submit_timeout_minutes", "handoff.renter_confirm_timeout_minutes", + "listing.publish_agreements", "listing.publish_options", "listing.review_required", "listing.sale_price_config", diff --git a/backend/internal/modules/systemconfig/service.go b/backend/internal/modules/systemconfig/service.go index 1df39a6..c05fa40 100644 --- a/backend/internal/modules/systemconfig/service.go +++ b/backend/internal/modules/systemconfig/service.go @@ -59,6 +59,17 @@ func (s *Service) OrderAgreements() (*OrderAgreementsDTO, error) { return &agreements, nil } +func (s *Service) ListingPublishAgreements() (*ListingPublishAgreementsDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + agreements, err := s.listingPublishAgreements() + if err != nil { + return nil, err + } + return &agreements, nil +} + func (s *Service) PostRentalNotice() (*PostRentalNoticeDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable @@ -109,6 +120,19 @@ func (s *Service) orderAgreements() (OrderAgreementsDTO, error) { return agreements, nil } +func (s *Service) listingPublishAgreements() (ListingPublishAgreementsDTO, error) { + value, err := s.repo.FindValue(listingPublishAgreementsConfigKey) + if err != nil { + return ListingPublishAgreementsDTO{}, err + } + agreements := DefaultListingPublishAgreements() + if err := json.Unmarshal([]byte(value), &agreements); err != nil { + agreements = DefaultListingPublishAgreements() + } + normalizeListingPublishAgreements(&agreements) + return agreements, nil +} + func (s *Service) postRentalNotice() (PostRentalNoticeDTO, error) { value, err := s.repo.FindValue(postRentalNoticeConfigKey) if err != nil { @@ -247,6 +271,26 @@ func normalizeOrderAgreements(agreements *OrderAgreementsDTO) { } } +func normalizeListingPublishAgreements(agreements *ListingPublishAgreementsDTO) { + defaults := DefaultListingPublishAgreements() + agreements.VirtualAssetSale.Title = strings.TrimSpace(agreements.VirtualAssetSale.Title) + agreements.VirtualAssetSale.Content = strings.TrimSpace(agreements.VirtualAssetSale.Content) + agreements.SellerAgreement.Title = strings.TrimSpace(agreements.SellerAgreement.Title) + agreements.SellerAgreement.Content = strings.TrimSpace(agreements.SellerAgreement.Content) + if agreements.VirtualAssetSale.Title == "" { + agreements.VirtualAssetSale.Title = defaults.VirtualAssetSale.Title + } + if agreements.VirtualAssetSale.Content == "" { + agreements.VirtualAssetSale.Content = defaults.VirtualAssetSale.Content + } + if agreements.SellerAgreement.Title == "" { + agreements.SellerAgreement.Title = defaults.SellerAgreement.Title + } + if agreements.SellerAgreement.Content == "" { + agreements.SellerAgreement.Content = defaults.SellerAgreement.Content + } +} + func normalizePostRentalNotice(notice *PostRentalNoticeDTO) { defaults := DefaultPostRentalNotice() notice.Title = strings.TrimSpace(notice.Title) diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 117030f..379e47f 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -240,6 +240,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { { api.GET("/health", health.Check) api.GET("/listing-publish-options", systemConfigHandler.PublishOptions) + api.GET("/listing-publish-agreements", systemConfigHandler.ListingPublishAgreements) api.GET("/listing-sale-price-config", systemConfigHandler.SalePriceConfig) api.GET("/order-agreements", systemConfigHandler.OrderAgreements) api.GET("/post-rental-notice", systemConfigHandler.PostRentalNotice) diff --git a/frontend/src/features/admin/components/ListingPublishAgreementsDialog.vue b/frontend/src/features/admin/components/ListingPublishAgreementsDialog.vue new file mode 100644 index 0000000..42e7ec9 --- /dev/null +++ b/frontend/src/features/admin/components/ListingPublishAgreementsDialog.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/frontend/src/features/admin/views/AdminSystemConfigsView.vue b/frontend/src/features/admin/views/AdminSystemConfigsView.vue index a25348c..5ff3a3f 100644 --- a/frontend/src/features/admin/views/AdminSystemConfigsView.vue +++ b/frontend/src/features/admin/views/AdminSystemConfigsView.vue @@ -7,6 +7,7 @@ import { mergeListingSalePriceConfig, mergeListingPublishOptions, type ListingPublishOptions, + type ListingPublishAgreements, type PublishSalePriceConfig, } from '@/features/listings/api/listingOptions' import { @@ -26,6 +27,7 @@ import PublishOptionsDialog from '../components/PublishOptionsDialog.vue' import SalePriceDialog from '../components/SalePriceDialog.vue' import HomeAnnouncementsDialog from '../components/HomeAnnouncementsDialog.vue' import HomeBannersDialog from '../components/HomeBannersDialog.vue' +import ListingPublishAgreementsDialog from '../components/ListingPublishAgreementsDialog.vue' import OrderAgreementsDialog from '../components/OrderAgreementsDialog.vue' import PostRentalNoticeDialog from '../components/PostRentalNoticeDialog.vue' import GeneralConfigDialog from '../components/GeneralConfigDialog.vue' @@ -42,6 +44,17 @@ const defaultOrderAgreements: OrderAgreements = { }, } +const defaultListingPublishAgreements: ListingPublishAgreements = { + virtual_asset_sale: { + title: '虚拟资产出售协议', + content: '', + }, + seller_agreement: { + title: '号主协议', + content: '', + }, +} + const loading = ref(false) const configs = ref([]) const currentEditingConfig = ref(null) @@ -51,6 +64,7 @@ const publishVisible = ref(false) const salePriceVisible = ref(false) const announcementsVisible = ref(false) const bannersVisible = ref(false) +const listingPublishAgreementsVisible = ref(false) const agreementsVisible = ref(false) const postRentalNoticeVisible = ref(false) const generalVisible = ref(false) @@ -59,6 +73,7 @@ const publishConfig = computed(() => configs.value.find((item) => item.key === ' const salePriceConfig = computed(() => configs.value.find((item) => item.key === 'listing.sale_price_config') || null) const homeAnnouncementsConfig = computed(() => configs.value.find((item) => item.key === 'mobile.home_announcements') || null) const homeBannersConfig = computed(() => configs.value.find((item) => item.key === 'mobile.home_banners') || null) +const listingPublishAgreementsConfig = computed(() => configs.value.find((item) => item.key === 'listing.publish_agreements') || null) const orderAgreementsConfig = computed(() => configs.value.find((item) => item.key === 'order.agreements') || null) const postRentalNoticeConfig = computed(() => configs.value.find((item) => item.key === 'profile.post_rental_notice') || null) @@ -69,6 +84,7 @@ const regularConfigs = computed(() => item.key !== 'listing.sale_price_config' && item.key !== 'mobile.home_announcements' && item.key !== 'mobile.home_banners' && + item.key !== 'listing.publish_agreements' && item.key !== 'order.agreements' && item.key !== 'profile.post_rental_notice', ), @@ -117,6 +133,16 @@ const agreementStats = computed(() => { } }) +const listingPublishAgreementStats = computed(() => { + const agreements = parseListingPublishAgreements(listingPublishAgreementsConfig.value?.value || '') + return { + saleTitle: agreements.virtual_asset_sale.title || '出售协议', + sellerTitle: agreements.seller_agreement.title || '号主协议', + saleWords: countContentWords(agreements.virtual_asset_sale.content), + sellerWords: countContentWords(agreements.seller_agreement.content), + } +}) + onMounted(loadConfigs) async function loadConfigs() { @@ -138,6 +164,8 @@ function openEdit(row: SystemConfig) { announcementsVisible.value = true } else if (row.key === 'mobile.home_banners') { bannersVisible.value = true + } else if (row.key === 'listing.publish_agreements') { + listingPublishAgreementsVisible.value = true } else if (row.key === 'order.agreements') { agreementsVisible.value = true } else if (row.key === 'profile.post_rental_notice') { @@ -181,6 +209,20 @@ function parseOrderAgreements(raw: string) { } } +function parseListingPublishAgreements(raw: string) { + const parsed = safeParseJSON(raw, defaultListingPublishAgreements) + return { + virtual_asset_sale: { + title: readText(parsed.virtual_asset_sale?.title, defaultListingPublishAgreements.virtual_asset_sale.title), + content: readText(parsed.virtual_asset_sale?.content, defaultListingPublishAgreements.virtual_asset_sale.content), + }, + seller_agreement: { + title: readText(parsed.seller_agreement?.title, defaultListingPublishAgreements.seller_agreement.title), + content: readText(parsed.seller_agreement?.content, defaultListingPublishAgreements.seller_agreement.content), + }, + } +} + function readText(value: unknown, fallback: string) { return typeof value === 'string' && value.trim() ? value : fallback } @@ -329,6 +371,39 @@ function formatConfigValue(row: SystemConfig) { +
+
+
+

Publish Agreements

+

发布协议配置

+ 管理发布账号前必须勾选确认的虚拟资产出售协议和号主协议。 +
+ 编辑发布协议 +
+
+
+ 2 + 协议数量 +
+
+ {{ listingPublishAgreementStats.saleWords }} + {{ listingPublishAgreementStats.saleTitle }}字数 +
+
+ {{ listingPublishAgreementStats.sellerWords }} + {{ listingPublishAgreementStats.sellerTitle }}字数 +
+
+ 接口 + /api/listing-publish-agreements +
+
+ 更新 + {{ formatHomeConfigStatus(listingPublishAgreementsConfig, '未初始化') }} +
+
+
+
@@ -446,6 +521,13 @@ function formatConfigValue(row: SystemConfig) { @saved="loadConfigs" /> + + >('/listing-publish-options') return mergeListingPublishOptions(data.data) } +export async function fetchListingPublishAgreements() { + const { data } = await apiClient.get>('/listing-publish-agreements') + return mergeListingPublishAgreements(data.data) +} + export async function fetchListingSalePriceConfig() { const { data } = await apiClient.get>('/listing-sale-price-config') return mergeListingSalePriceConfig(data.data) @@ -193,6 +219,20 @@ export function mergeListingSalePriceConfig(options?: Partial): ListingPublishAgreements { + return { + virtual_asset_sale: normalizeAgreementContent(options?.virtual_asset_sale, emptyListingPublishAgreements.virtual_asset_sale), + seller_agreement: normalizeAgreementContent(options?.seller_agreement, emptyListingPublishAgreements.seller_agreement), + } +} + +function normalizeAgreementContent(value: unknown, fallback: AgreementContent): AgreementContent { + const row = isRecord(value) ? value : {} + const title = typeof row.title === 'string' && row.title.trim() ? row.title.trim() : fallback.title + const content = typeof row.content === 'string' && row.content.trim() ? row.content.trim() : fallback.content + return { title, content } +} + function normalizeStringList(values?: unknown[]) { return Array.isArray(values) ? values.map((item) => (typeof item === 'string' ? item.trim() : '')).filter(Boolean) diff --git a/frontend/src/features/listings/api/listings.ts b/frontend/src/features/listings/api/listings.ts index 2ca1d4f..1f48e52 100644 --- a/frontend/src/features/listings/api/listings.ts +++ b/frontend/src/features/listings/api/listings.ts @@ -41,6 +41,8 @@ export interface ListingPayload { screenshot_urls: string[] price: number deposit_amount: number + agreed_virtual_asset_sale: boolean + agreed_seller_agreement: boolean } export interface PublicListingQuery { diff --git a/frontend/src/features/seller/composables/usePublishForm.ts b/frontend/src/features/seller/composables/usePublishForm.ts index f8f1c19..6630926 100644 --- a/frontend/src/features/seller/composables/usePublishForm.ts +++ b/frontend/src/features/seller/composables/usePublishForm.ts @@ -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(emptyListingPublishOptions) const salePriceConfig = ref(emptyListingSalePriceConfig) + const publishAgreements = ref(emptyListingPublishAgreements) + const virtualAssetSaleAgreementChecked = ref(false) + const sellerAgreementChecked = ref(false) const fileInput = ref(null) const activeUploadKey = ref('coin') const form = reactive(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, diff --git a/frontend/src/features/seller/views/MobileSellerListingCreateView.css b/frontend/src/features/seller/views/MobileSellerListingCreateView.css index c4bf80c..4d8fc82 100644 --- a/frontend/src/features/seller/views/MobileSellerListingCreateView.css +++ b/frontend/src/features/seller/views/MobileSellerListingCreateView.css @@ -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); diff --git a/frontend/src/features/seller/views/MobileSellerListingCreateView.vue b/frontend/src/features/seller/views/MobileSellerListingCreateView.vue index 8e60e8a..40a1c2a 100644 --- a/frontend/src/features/seller/views/MobileSellerListingCreateView.vue +++ b/frontend/src/features/seller/views/MobileSellerListingCreateView.vue @@ -1,8 +1,10 @@