From 3c28297dee6979db38b70df9dd73a012a3635eb0 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 5 Jun 2026 03:06:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=BC=B9=E7=AA=97=E5=92=8C?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=E8=AE=BE=E7=BD=AE=E5=AD=97=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/modules/systemconfig/dto.go | 10 + .../internal/modules/systemconfig/handler.go | 9 + .../modules/systemconfig/order_agreements.go | 56 ++++ .../modules/systemconfig/repository.go | 2 + .../internal/modules/systemconfig/service.go | 44 ++++ backend/internal/router/router.go | 3 +- .../components/OrderAgreementsDialog.vue | 237 +++++++++++++++++ .../admin/views/AdminSystemConfigsView.vue | 105 +++++++- .../listings/views/ListingDetailView.vue | 240 +++++++++++++++++- .../views/MobileListingDetailView.vue | 180 ++++++++++++- frontend/src/features/orders/api/orders.ts | 15 ++ frontend/src/styles/admin.css | 21 +- 12 files changed, 910 insertions(+), 12 deletions(-) create mode 100644 backend/internal/modules/systemconfig/order_agreements.go create mode 100644 frontend/src/features/admin/components/OrderAgreementsDialog.vue diff --git a/backend/internal/modules/systemconfig/dto.go b/backend/internal/modules/systemconfig/dto.go index 39db6ef..ba9f0ff 100644 --- a/backend/internal/modules/systemconfig/dto.go +++ b/backend/internal/modules/systemconfig/dto.go @@ -27,6 +27,16 @@ type HomeConfigDTO struct { PublishOptions PublishOptionsDTO `json:"publish_options"` } +type OrderAgreementsDTO struct { + VirtualAssetPurchase AgreementContentDTO `json:"virtual_asset_purchase"` + RenterAgreement AgreementContentDTO `json:"renter_agreement"` +} + +type AgreementContentDTO struct { + Title string `json:"title"` + Content string `json:"content"` +} + type HomeBannerItem struct { Eyebrow string `json:"eyebrow"` Title string `json:"title"` diff --git a/backend/internal/modules/systemconfig/handler.go b/backend/internal/modules/systemconfig/handler.go index ac60c06..1a451df 100644 --- a/backend/internal/modules/systemconfig/handler.go +++ b/backend/internal/modules/systemconfig/handler.go @@ -45,6 +45,15 @@ func (h *Handler) SalePriceConfig(c *gin.Context) { response.OK(c, config) } +func (h *Handler) OrderAgreements(c *gin.Context) { + agreements, err := h.service.OrderAgreements() + if err != nil { + writeConfigError(c, err) + return + } + response.OK(c, agreements) +} + func (h *Handler) HomeAnnouncements(c *gin.Context) { announcements, err := h.service.HomeAnnouncements() if err != nil { diff --git a/backend/internal/modules/systemconfig/order_agreements.go b/backend/internal/modules/systemconfig/order_agreements.go new file mode 100644 index 0000000..17a9b53 --- /dev/null +++ b/backend/internal/modules/systemconfig/order_agreements.go @@ -0,0 +1,56 @@ +package systemconfig + +import "encoding/json" + +const orderAgreementsConfigKey = "order.agreements" + +func defaultOrderAgreementsConfigValue() string { + raw, err := json.Marshal(DefaultOrderAgreements()) + if err != nil { + return "{}" + } + return string(raw) +} + +func DefaultOrderAgreements() OrderAgreementsDTO { + return OrderAgreementsDTO{ + VirtualAssetPurchase: AgreementContentDTO{ + Title: "虚拟资产购买协议", + Content: `请您在下单前仔细阅读并确认以下内容: + +一、交易性质 +1. 本平台商品涉及游戏账号租用期间的虚拟资产使用权益、租金和押金托管服务。 +2. 虚拟资产、游戏账号及相关数据均受游戏厂商规则约束,平台不承诺虚拟资产具备现实货币价值或永久可用性。 +3. 您下单即表示已理解商品页面展示的哈夫币、装备、皮肤、消耗品、租期、押金和交接规则。 + +二、风险告知 +1. 游戏厂商可能因版本更新、风控策略、账号安全策略等因素调整游戏资产状态。 +2. 因租客违规操作、使用外挂、恶意消耗、转移资产、私下交易或违反游戏规则造成的损失,可能从押金中扣除或进入争议处理。 +3. 请勿绕过平台私下付款、私下交接或私下协商退款,否则平台可能无法保障您的资金与交易权益。 + +三、费用与结算 +1. 订单租金、押金和可能产生的额外消耗品费用以平台订单和结账页面展示为准。 +2. 未实际使用完的租金或剩余押金,将按平台结账规则退回。 +3. 若发生争议,平台将依据订单记录、聊天记录、截图证据和双方说明进行处理。`, + }, + RenterAgreement: 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 a7ec406..c6bbd1f 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: orderAgreementsConfigKey, Value: defaultOrderAgreementsConfigValue(), Description: "下单前协议配置 JSON"}, {Key: "chat.default_support_admin_id", Value: "1", Description: "订单群聊回退客服 ID(仅当无可用客服时使用)"}, {Key: "chat.auto_welcome_message", Value: "欢迎加入订单群聊!如有任何问题,请随时沟通。", Description: "建群后自动发送的欢迎话术"}, {Key: homeAnnouncementsConfigKey, Value: defaultHomeAnnouncementsConfigValue(), Description: "移动端首页公告 JSON 数组"}, @@ -50,6 +51,7 @@ var adminVisibleConfigKeys = []string{ "listing.sale_price_config", "mobile.home_announcements", "mobile.home_banners", + "order.agreements", "order.pending_payment_timeout_minutes", "order.return_overdue_grace_minutes", } diff --git a/backend/internal/modules/systemconfig/service.go b/backend/internal/modules/systemconfig/service.go index 211c847..46afb4f 100644 --- a/backend/internal/modules/systemconfig/service.go +++ b/backend/internal/modules/systemconfig/service.go @@ -48,6 +48,17 @@ func (s *Service) SalePriceConfig() (*PublishSalePriceConfig, error) { return &config, nil } +func (s *Service) OrderAgreements() (*OrderAgreementsDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + agreements, err := s.orderAgreements() + if err != nil { + return nil, err + } + return &agreements, nil +} + func (s *Service) publishOptions() (PublishOptionsDTO, error) { value, err := s.repo.FindValue(publishOptionsConfigKey) if err != nil { @@ -74,6 +85,19 @@ func (s *Service) salePriceConfig() (PublishSalePriceConfig, error) { return config, nil } +func (s *Service) orderAgreements() (OrderAgreementsDTO, error) { + value, err := s.repo.FindValue(orderAgreementsConfigKey) + if err != nil { + return OrderAgreementsDTO{}, err + } + agreements := DefaultOrderAgreements() + if err := json.Unmarshal([]byte(value), &agreements); err != nil { + agreements = DefaultOrderAgreements() + } + normalizeOrderAgreements(&agreements) + return agreements, nil +} + func (s *Service) HomeAnnouncements() (*HomeAnnouncementsDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable @@ -179,6 +203,26 @@ func normalizeHomeBanners(items []HomeBannerItem) []HomeBannerItem { return normalized } +func normalizeOrderAgreements(agreements *OrderAgreementsDTO) { + defaults := DefaultOrderAgreements() + agreements.VirtualAssetPurchase.Title = strings.TrimSpace(agreements.VirtualAssetPurchase.Title) + agreements.VirtualAssetPurchase.Content = strings.TrimSpace(agreements.VirtualAssetPurchase.Content) + agreements.RenterAgreement.Title = strings.TrimSpace(agreements.RenterAgreement.Title) + agreements.RenterAgreement.Content = strings.TrimSpace(agreements.RenterAgreement.Content) + if agreements.VirtualAssetPurchase.Title == "" { + agreements.VirtualAssetPurchase.Title = defaults.VirtualAssetPurchase.Title + } + if agreements.VirtualAssetPurchase.Content == "" { + agreements.VirtualAssetPurchase.Content = defaults.VirtualAssetPurchase.Content + } + if agreements.RenterAgreement.Title == "" { + agreements.RenterAgreement.Title = defaults.RenterAgreement.Title + } + if agreements.RenterAgreement.Content == "" { + agreements.RenterAgreement.Content = defaults.RenterAgreement.Content + } +} + func normalizePublishOptions(options *PublishOptionsDTO) { defaults := DefaultPublishOptions() if len(options.ServerOptions) == 0 { diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 272f7c8..80891d5 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -186,6 +186,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-sale-price-config", systemConfigHandler.SalePriceConfig) + api.GET("/order-agreements", systemConfigHandler.OrderAgreements) api.GET("/home-announcements", systemConfigHandler.HomeAnnouncements) api.GET("/mobile-home-config", systemConfigHandler.HomeConfig) api.GET("/public/files/object", fileHandler.PublicObject) @@ -228,7 +229,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { orderRoutes := api.Group("/orders", requireAuth) { - orderRoutes.POST("", orderHandler.Create) + orderRoutes.POST("", requireRealname, orderHandler.Create) orderRoutes.GET("", orderHandler.List) orderRoutes.GET("/:id", orderHandler.Detail) orderRoutes.GET("/:id/chat", chatHandler.OrderConversation) diff --git a/frontend/src/features/admin/components/OrderAgreementsDialog.vue b/frontend/src/features/admin/components/OrderAgreementsDialog.vue new file mode 100644 index 0000000..06d31bf --- /dev/null +++ b/frontend/src/features/admin/components/OrderAgreementsDialog.vue @@ -0,0 +1,237 @@ + + + + + diff --git a/frontend/src/features/admin/views/AdminSystemConfigsView.vue b/frontend/src/features/admin/views/AdminSystemConfigsView.vue index 1156e85..0e1c564 100644 --- a/frontend/src/features/admin/views/AdminSystemConfigsView.vue +++ b/frontend/src/features/admin/views/AdminSystemConfigsView.vue @@ -15,6 +15,7 @@ import { mergeHomeConfig, type HomeBannerSlide, } from '@/features/listings/api/homeConfig' +import type { OrderAgreements } from '@/features/orders/api/orders' import { fetchSystemConfigs, type SystemConfig } from '@/features/admin/api/systemConfigs' import { formatDateTime } from '@/utils/time' import { safeParseJSON } from '@/utils/json' @@ -25,9 +26,21 @@ 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 OrderAgreementsDialog from '../components/OrderAgreementsDialog.vue' import GeneralConfigDialog from '../components/GeneralConfigDialog.vue' import AutoWelcomeConfig from '../components/AutoWelcomeConfig.vue' +const defaultOrderAgreements: OrderAgreements = { + virtual_asset_purchase: { + title: '虚拟资产购买协议', + content: '', + }, + renter_agreement: { + title: '租客协议', + content: '', + }, +} + const loading = ref(false) const configs = ref([]) const currentEditingConfig = ref(null) @@ -37,12 +50,14 @@ const publishVisible = ref(false) const salePriceVisible = ref(false) const announcementsVisible = ref(false) const bannersVisible = ref(false) +const agreementsVisible = ref(false) const generalVisible = ref(false) const publishConfig = computed(() => configs.value.find((item) => item.key === 'listing.publish_options') || null) 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 orderAgreementsConfig = computed(() => configs.value.find((item) => item.key === 'order.agreements') || null) const regularConfigs = computed(() => configs.value.filter( @@ -50,7 +65,8 @@ const regularConfigs = computed(() => item.key !== 'listing.publish_options' && item.key !== 'listing.sale_price_config' && item.key !== 'mobile.home_announcements' && - item.key !== 'mobile.home_banners', + item.key !== 'mobile.home_banners' && + item.key !== 'order.agreements', ), ) @@ -87,6 +103,16 @@ const homeStats = computed(() => { } }) +const agreementStats = computed(() => { + const agreements = parseOrderAgreements(orderAgreementsConfig.value?.value || '') + return { + virtualTitle: agreements.virtual_asset_purchase.title || '购买协议', + renterTitle: agreements.renter_agreement.title || '租客协议', + virtualWords: countContentWords(agreements.virtual_asset_purchase.content), + renterWords: countContentWords(agreements.renter_agreement.content), + } +}) + onMounted(loadConfigs) async function loadConfigs() { @@ -108,6 +134,8 @@ function openEdit(row: SystemConfig) { announcementsVisible.value = true } else if (row.key === 'mobile.home_banners') { bannersVisible.value = true + } else if (row.key === 'order.agreements') { + agreementsVisible.value = true } else { generalVisible.value = true } @@ -133,6 +161,28 @@ function parseHomeBanners(raw: string) { return cloneHomeBanners(mergeHomeConfig({ banners: Array.isArray(parsed) ? parsed : [] }).banners) } +function parseOrderAgreements(raw: string) { + const parsed = safeParseJSON(raw, defaultOrderAgreements) + return { + virtual_asset_purchase: { + title: readText(parsed.virtual_asset_purchase?.title, defaultOrderAgreements.virtual_asset_purchase.title), + content: readText(parsed.virtual_asset_purchase?.content, defaultOrderAgreements.virtual_asset_purchase.content), + }, + renter_agreement: { + title: readText(parsed.renter_agreement?.title, defaultOrderAgreements.renter_agreement.title), + content: readText(parsed.renter_agreement?.content, defaultOrderAgreements.renter_agreement.content), + }, + } +} + +function readText(value: unknown, fallback: string) { + return typeof value === 'string' && value.trim() ? value : fallback +} + +function countContentWords(value: string) { + return value.replace(/\s/g, '').length +} + function cloneOptions(options: ListingPublishOptions) { return JSON.parse(JSON.stringify(options)) as ListingPublishOptions } @@ -273,6 +323,39 @@ function formatConfigValue(row: SystemConfig) { +
+
+
+

Order Agreements

+

下单协议配置

+ 管理下单前必须阅读并勾选确认的虚拟资产购买协议和租客协议。 +
+ 编辑协议 +
+
+
+ 2 + 协议数量 +
+
+ {{ agreementStats.virtualWords }} + {{ agreementStats.virtualTitle }}字数 +
+
+ {{ agreementStats.renterWords }} + {{ agreementStats.renterTitle }}字数 +
+
+ 接口 + /api/order-agreements +
+
+ 更新 + {{ formatHomeConfigStatus(orderAgreementsConfig, '未初始化') }} +
+
+
+ @@ -324,6 +407,13 @@ function formatConfigValue(row: SystemConfig) { @saved="loadConfigs" /> + + -import { ElMessage } from "element-plus"; -import { computed, onMounted, ref } from "vue"; +import { ElMessage, ElMessageBox } from "element-plus"; +import { computed, nextTick, onMounted, ref, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; import { fetchListing, type Listing } from "@/features/listings/api/listings"; -import { createOrder } from "@/features/orders/api/orders"; +import { createOrder, fetchOrderAgreements, type OrderAgreements } from "@/features/orders/api/orders"; import { useSessionStore } from "@/stores/session"; import { roundMoney, formatMoney } from "@/shared/utils/money"; import { @@ -33,6 +33,23 @@ const router = useRouter(); const session = useSessionStore(); const loading = ref(false); const ordering = ref(false); +const agreementsLoading = ref(false); +const agreementVisible = ref(false); +const agreements = ref(null); +const virtualAgreementRead = ref(false); +const renterAgreementRead = ref(false); +const virtualAgreementChecked = ref(false); +const renterAgreementChecked = ref(false); +const virtualAgreementRef = ref(null); +const renterAgreementRef = ref(null); + +const canCreateOrderAfterAgreement = computed( + () => + virtualAgreementRead.value && + renterAgreementRead.value && + virtualAgreementChecked.value && + renterAgreementChecked.value, +); const listing = ref(null); onMounted(async () => { @@ -137,6 +154,61 @@ async function handleCreateOrder() { return; } + if (session.realnameStatus !== "verified") { + try { + await session.loadMe(); + } catch { + // 登录态失效时由全局请求拦截处理。 + } + } + + if (session.realnameStatus !== "verified") { + try { + await ElMessageBox.confirm("租号下单前需要完成实名认证。", "请先实名认证", { + confirmButtonText: "去认证", + cancelButtonText: "取消", + type: "warning", + }); + await router.push({ path: "/realname", query: { redirect: route.fullPath } }); + } catch { + // 用户取消认证时停留在当前页面。 + } + return; + } + + await openAgreementBeforeOrder(); +} + +async function openAgreementBeforeOrder() { + agreementsLoading.value = true; + try { + agreements.value = await fetchOrderAgreements(); + virtualAgreementRead.value = false; + renterAgreementRead.value = false; + virtualAgreementChecked.value = false; + renterAgreementChecked.value = false; + agreementVisible.value = true; + await nextTick(); + updateAgreementReadState("virtual"); + updateAgreementReadState("renter"); + } catch (error) { + ElMessage.error(readError(error, "协议加载失败")); + } finally { + agreementsLoading.value = false; + } +} + +async function handleConfirmAgreementAndCreateOrder() { + if (!canCreateOrderAfterAgreement.value) { + ElMessage.warning("请先阅读并勾选两份协议"); + return; + } + agreementVisible.value = false; + await submitOrder(); +} + +async function submitOrder() { + if (!listing.value) return; ordering.value = true; try { const order = await createOrder(listing.value.id); @@ -149,6 +221,21 @@ async function handleCreateOrder() { } } +function handleAgreementScroll(type: "virtual" | "renter") { + updateAgreementReadState(type); +} + +function updateAgreementReadState(type: "virtual" | "renter") { + const el = type === "virtual" ? virtualAgreementRef.value : renterAgreementRef.value; + if (!el) return; + const read = el.scrollTop + el.clientHeight >= el.scrollHeight - 8; + if (type === "virtual") { + virtualAgreementRead.value = read; + } else { + renterAgreementRead.value = read; + } +} + function readError(error: unknown, fallback: string) { if (typeof error === "object" && error && "response" in error) { const response = (error as { response?: { data?: { message?: string } } }) @@ -158,6 +245,43 @@ function readError(error: unknown, fallback: string) { return fallback; } +// 手动锁定/解锁背景滚动 +watch(agreementVisible, (visible) => { + // 只锁定 .pc-main 容器,不影响 body 和 sticky 导航栏 + const pcMain = document.querySelector('.pc-main') as HTMLElement; + if (!pcMain) return; + + if (visible) { + // 保存当前滚动位置 + const scrollTop = pcMain.scrollTop || 0; + + // 锁定主容器滚动 + pcMain.style.overflow = 'hidden'; + pcMain.style.position = 'fixed'; + pcMain.style.top = `-${scrollTop}px`; + pcMain.style.left = '0'; + pcMain.style.right = '0'; + pcMain.style.width = '100%'; + + // 保存滚动位置供恢复使用 + pcMain.dataset.scrollTop = String(scrollTop); + } else { + // 恢复主容器滚动 + const scrollTop = parseInt(pcMain.dataset.scrollTop || '0', 10); + + pcMain.style.overflow = ''; + pcMain.style.position = ''; + pcMain.style.top = ''; + pcMain.style.left = ''; + pcMain.style.right = ''; + pcMain.style.width = ''; + + // 恢复滚动位置 + pcMain.scrollTop = scrollTop; + delete pcMain.dataset.scrollTop; + } +}); + function listingPrice(item: Listing) { return formatMoney(getListingDisplayPrice(item)); } @@ -311,7 +435,7 @@ function listingPrice(item: Listing) { + + +
+

请完整阅读并勾选以下两份协议后继续下单。

+
+
+

{{ agreements.virtual_asset_purchase.title }}

+
+ {{ agreements.virtual_asset_purchase.content }} +
+ + 我已阅读并同意《{{ agreements.virtual_asset_purchase.title }}》 + + 请下拉阅读至底部 +
+
+

{{ agreements.renter_agreement.title }}

+
+ {{ agreements.renter_agreement.content }} +
+ + 我已阅读并同意《{{ agreements.renter_agreement.title }}》 + + 请下拉阅读至底部 +
+
+
+ +
@@ -713,6 +890,57 @@ function listingPrice(item: Listing) { text-align: right; } +.agreement-dialog-body { + display: grid; + gap: 14px; +} + +.agreement-tip { + margin: 0; + color: #64748b; + font-size: 14px; + font-weight: 700; +} + +.agreement-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} + +.agreement-panel { + display: grid; + gap: 10px; + min-width: 0; +} + +.agreement-panel h3 { + margin: 0; + color: #17233d; + font-size: 16px; + font-weight: 900; +} + +.agreement-content { + height: 260px; + overflow: auto; + border: 1px solid #d8dee9; + border-radius: 8px; + background: #f8fafc; + padding: 14px; + color: #1f2937; + font-size: 13px; + font-weight: 700; + line-height: 1.75; + white-space: pre-wrap; +} + +.agreement-read-hint { + color: #f97316; + font-size: 12px; + font-weight: 800; +} + @media (max-width: 1180px) { .pc-detail-layout, .detail-screenshot-grid { @@ -746,5 +974,9 @@ function listingPrice(item: Listing) { .detail-resource-grid { grid-template-columns: 1fr; } + + .agreement-grid { + grid-template-columns: 1fr; + } } diff --git a/frontend/src/features/listings/views/MobileListingDetailView.vue b/frontend/src/features/listings/views/MobileListingDetailView.vue index fdd14e3..958d18d 100644 --- a/frontend/src/features/listings/views/MobileListingDetailView.vue +++ b/frontend/src/features/listings/views/MobileListingDetailView.vue @@ -1,10 +1,10 @@