csgo-market/
├── pom.xml (or build.gradle)
└── src/
└── main/
├── java/
│ └── com/
│ └── yourcompany/
│ └── csgomarket/
│ ├── CsgomarketApplication.java # Spring Boot 启动类
│ ├── config/ # 配置类 (SecurityConfig, JacksonConfig...)
│ ├── controller/ # RESTful API 控制器
│ │ ├── UserController.java
│ │ ├── ListingController.java
│ │ └── ...
│ ├── dto/ # 数据传输对象
│ │ ├── UserDTO.java
│ │ ├── ListingDTO.java
│ │ ├── CreateListingDTO.java
│ │ └── ...
│ ├── entity/ # JPA 实体类 (对应数据库表)
│ │ ├── User.java
│ │ ├── SkinDefinition.java
│ │ ├── UserInventoryItem.java
│ │ ├── Listing.java
│ │ ├── Transaction.java
│ │ └── ...
│ ├── enums/ # 枚举类型 (ListingStatus, TransactionType...)
│ │ ├── ListingStatus.java
│ │ └── ...
│ ├── exception/ # 自定义异常类 & 全局异常处理
│ │ ├── GlobalExceptionHandler.java
│ │ └── ResourceNotFoundException.java
│ ├── repository/ # Spring Data JPA Repositories
│ │ ├── UserRepository.java
│ │ ├── ListingRepository.java
│ │ └── ...
│ ├── service/ # 业务逻辑服务接口
│ │ ├── UserService.java
│ │ ├── ListingService.java
│ │ ├── SteamService.java # (非常重要且复杂)
│ │ └── ...
│ ├── service/impl/ # 业务逻辑服务实现
│ │ ├── UserServiceImpl.java
│ │ ├── ListingServiceImpl.java
│ │ └── ...
│ └── util/ # 工具类
└── resources/
├── application.properties (or application.yml) # 配置文件
├── db/migration/ # 数据库迁移脚本 (Flyway/Liquibase)
└── static/ # 静态资源 (如果前后端不分离)
└── templates/ # 服务端模板 (如果使用 Thymeleaf 等)
1. 实体(列表.java)
package com.yourcompany.csgomarket.entity;
import com.yourcompany.csgomarket.enums.ListingStatus;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "listings")
@Data
@NoArgsConstructor
public class Listing {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Unique=true 应由业务逻辑或数据库约束保证一个 item 只有一个 active listing
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "inventory_item_id", referencedColumnName = "id", nullable = false, unique = true)
private UserInventoryItem inventoryItem;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "seller_id", nullable = false)
private User seller;
@Column(nullable = false, precision = 15, scale = 2)
private BigDecimal price;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private ListingStatus status = ListingStatus.ACTIVE;
@CreationTimestamp
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(nullable = false)
private LocalDateTime updatedAt;
@Column
private LocalDateTime soldAt;
// Constructors, Getters, Setters (Lombok handles this)
}
2. 存储库(ListingRepository.java)
package com.yourcompany.csgomarket.repository;
import com.yourcompany.csgomarket.entity.Listing;
import com.yourcompany.csgomarket.entity.User;
import com.yourcompany.csgomarket.enums.ListingStatus;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; // For dynamic queries
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface ListingRepository extends JpaRepository<Listing, Long>, JpaSpecificationExecutor<Listing> {
// Find active listings by seller
Page<Listing> findBySellerAndStatus(User seller, ListingStatus status, Pageable pageable);
// Find listing by inventory item ID (useful for checking duplicates)
Optional<Listing> findByInventoryItemId(Long inventoryItemId);
// Example using Specification for dynamic filtering/searching (needs implementation elsewhere)
// Page<Listing> findAll(Specification<Listing> spec, Pageable pageable);
// Find multiple listings by their IDs
List<Listing> findByIdIn(List<Long> ids);
}
3. DTO(创建列表DTO.java)
package com.yourcompany.csgomarket.dto;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class CreateListingDTO {
@NotNull(message = "Inventory item ID cannot be null")
private Long inventoryItemId;
@NotNull(message = "Price cannot be null")
@DecimalMin(value = "0.01", message = "Price must be greater than 0")
private BigDecimal price;
// Seller ID will usually be inferred from the authenticated user context
}
4. DTO(ListingDTO.java- 用于 API 响应)
package com.yourcompany.csgomarket.dto;
import com.yourcompany.csgomarket.enums.ListingStatus;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class ListingDTO {
private Long id;
private UserInventoryItemDTO inventoryItem; // Another DTO for item details
private UserSummaryDTO seller; // Simplified user DTO
private BigDecimal price;
private ListingStatus status;
private LocalDateTime createdAt;
private LocalDateTime soldAt;
}
5. 服务接口(列表服务.java)
package com.yourcompany.csgomarket.service;
import com.yourcompany.csgomarket.dto.CreateListingDTO;
import com.yourcompany.csgomarket.dto.ListingDTO;
import com.yourcompany.csgomarket.entity.User; // Assuming User entity represents authenticated user
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface ListingService {
/**
* Creates a new listing for the authenticated user.
* Requires complex logic involving inventory check and potentially Steam interaction.
*/
ListingDTO createListing(CreateListingDTO createListingDTO, User seller);
/**
* Retrieves a listing by its ID.
*/
ListingDTO getListingById(Long id);
/**
* Cancels an active listing owned by the user.
*/
void cancelListing(Long listingId, User owner);
/**
* Retrieves listings based on filter criteria (complex).
* This would involve dynamic query building.
*/
Page<ListingDTO> findListings(/* Filter criteria DTO */ Object filter, Pageable pageable);
/**
* Retrieves listings created by a specific user.
*/
Page<ListingDTO> findMyListings(User seller, Pageable pageable);
// ... other methods like handling purchase, updating status etc.
}
6. 服务实施(ListingServiceImpl.java- 简化示例)
package com.yourcompany.csgomarket.service.impl;
import com.yourcompany.csgomarket.dto.CreateListingDTO;
import com.yourcompany.csgomarket.dto.ListingDTO;
import com.yourcompany.csgomarket.entity.Listing;
import com.yourcompany.csgomarket.entity.User;
import com.yourcompany.csgomarket.entity.UserInventoryItem;
import com.yourcompany.csgomarket.enums.ListingStatus;
import com.yourcompany.csgomarket.enums.InventoryItemStatus; // Assuming enum exists
import com.yourcompany.csgomarket.exception.ResourceNotFoundException;
import com.yourcompany.csgomarket.exception.InvalidOperationException;
import com.yourcompany.csgomarket.repository.ListingRepository;
import com.yourcompany.csgomarket.repository.UserInventoryItemRepository;
import com.yourcompany.csgomarket.service.ListingService;
import com.yourcompany.csgomarket.service.SteamService; // Crucial dependency
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper; // Or MapStruct
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor // Lombok for constructor injection
public class ListingServiceImpl implements ListingService {
private final ListingRepository listingRepository;
private final UserInventoryItemRepository inventoryItemRepository;
private final SteamService steamService; // For inventory sync and potentially bot interaction
private final ModelMapper modelMapper; // For DTO mapping
@Override
@Transactional
public ListingDTO createListing(CreateListingDTO createListingDTO, User seller) {
// 1. Validate inventory item exists and belongs to the seller
UserInventoryItem item = inventoryItemRepository.findByIdAndUserId(createListingDTO.getInventoryItemId(), seller.getId())
.orElseThrow(() -> new ResourceNotFoundException("Inventory item not found or does not belong to user"));
// 2. Check if item is already listed or in an invalid state
if (item.getStatus() != InventoryItemStatus.ON_PLATFORM) { // Assuming ON_PLATFORM means ready to be listed
throw new InvalidOperationException("Item is not in a listable state (status: " + item.getStatus() + ")");
}
listingRepository.findByInventoryItemId(item.getId())
.filter(l -> l.getStatus() == ListingStatus.ACTIVE)
.ifPresent(l -> { throw new InvalidOperationException("Item is already listed actively."); });
// --- !! Placeholder for complex logic !! ---
// 3. [Optional, depending on model] Interact with Steam Bot?
// Maybe move item to a trade bot if using a centralized model.
// This is highly complex and error-prone.
// boolean botTransferSuccess = steamService.requestItemTransferToBot(item.getAssetId(), seller.getTradeOfferUrl());
// if (!botTransferSuccess) { throw new RuntimeException("Failed to transfer item to bot"); }
// --- End Placeholder ---
// 4. Create and save the listing entity
Listing newListing = new Listing();
newListing.setInventoryItem(item);
newListing.setSeller(seller);
newListing.setPrice(createListingDTO.getPrice());
newListing.setStatus(ListingStatus.ACTIVE); // Set initial status
Listing savedListing = listingRepository.save(newListing);
// 5. Update inventory item status
item.setStatus(InventoryItemStatus.LISTED);
inventoryItemRepository.save(item);
// 6. Map to DTO and return
return convertToListingDTO(savedListing);
}
@Override
public ListingDTO getListingById(Long id) {
Listing listing = listingRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Listing not found with id: " + id));
return convertToListingDTO(listing);
}
@Override
@Transactional
public void cancelListing(Long listingId, User owner) {
Listing listing = listingRepository.findById(listingId)
.orElseThrow(() -> new ResourceNotFoundException("Listing not found with id: " + listingId));
if (!listing.getSeller().getId().equals(owner.getId())) {
throw new InvalidOperationException("User is not the owner of this listing.");
}
if (listing.getStatus() != ListingStatus.ACTIVE) {
throw new InvalidOperationException("Only active listings can be cancelled.");
}
// --- !! Placeholder for complex logic !! ---
// [Optional, depending on model] Interact with Steam Bot?
// If item was transferred to a bot, initiate return transfer.
// boolean returnSuccess = steamService.requestItemReturnFromBot(listing.getInventoryItem().getAssetId(), owner.getTradeOfferUrl());
// if (!returnSuccess) { throw new RuntimeException("Failed to return item from bot"); }
// --- End Placeholder ---
listing.setStatus(ListingStatus.CANCELLED);
listingRepository.save(listing);
// Update inventory item status back
UserInventoryItem item = listing.getInventoryItem();
item.setStatus(InventoryItemStatus.ON_PLATFORM); // Or back to IN_STEAM if returned
inventoryItemRepository.save(item);
}
@Override
public Page<ListingDTO> findMyListings(User seller, Pageable pageable) {
Page<Listing> listingsPage = listingRepository.findBySellerAndStatus(seller, ListingStatus.ACTIVE, pageable); // Example: find only active
return listingsPage.map(this::convertToListingDTO);
}
// Implement findListings with SpecificationExecutor for filtering
// --- Helper Method for DTO Conversion ---
private ListingDTO convertToListingDTO(Listing listing) {
ListingDTO dto = modelMapper.map(listing, ListingDTO.class);
// Manual mapping or configuration for nested DTOs might be needed
// dto.setInventoryItem(modelMapper.map(listing.getInventoryItem(), UserInventoryItemDTO.class));
// dto.setSeller(modelMapper.map(listing.getSeller(), UserSummaryDTO.class));
// Handle potential lazy loading issues if necessary
dto.setInventoryItem(mapInventoryItem(listing.getInventoryItem())); // Example manual map
dto.setSeller(mapUserSummary(listing.getSeller())); // Example manual map
return dto;
}
// Example manual mapping helpers (replace with ModelMapper/MapStruct config)
private UserInventoryItemDTO mapInventoryItem(UserInventoryItem item) {
if (item == null) return null;
UserInventoryItemDTO dto = new UserInventoryItemDTO();
// ... map fields ...
dto.setId(item.getId());
// Make sure SkinDefinition is loaded or handle proxy
if (item.getSkinDefinition() != null) {
dto.setMarketHashName(item.getSkinDefinition().getMarketHashName());
dto.setName(item.getSkinDefinition().getName());
dto.setIconUrl(item.getSkinDefinition().getIconUrl());
// ... other definition fields
}
dto.setWearFloat(item.getWearFloat());
// ... etc. ...
return dto;
}
private UserSummaryDTO mapUserSummary(User user) {
if (user == null) return null;
UserSummaryDTO dto = new UserSummaryDTO();
dto.setSteamId(user.getSteamId());
dto.setPlatformUsername(user.getPlatformUsername());
dto.setAvatarUrl(user.getAvatarUrl());
return dto;
}
}
7. 控制器(列表控制器.java)
package com.yourcompany.csgomarket.controller;
import com.yourcompany.csgomarket.dto.CreateListingDTO;
import com.yourcompany.csgomarket.dto.ListingDTO;
import com.yourcompany.csgomarket.entity.User; // Assume this comes from Security Context
import com.yourcompany.csgomarket.service.ListingService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
// import org.springframework.security.core.annotation.AuthenticationPrincipal; // For getting User
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/listings")
@RequiredArgsConstructor
public class ListingController {
private final ListingService listingService;
// --- Public Endpoints ---
@GetMapping("/{id}")
public ResponseEntity<ListingDTO> getListingById(@PathVariable Long id) {
ListingDTO listing = listingService.getListingById(id);
return ResponseEntity.ok(listing);
}
@GetMapping
public ResponseEntity<Page<ListingDTO>> searchListings(
/* @RequestParam Map<String, String> filters, // Or specific DTO for filters */
@PageableDefault(size = 20, sort = "createdAt") Pageable pageable) {
// Page<ListingDTO> listings = listingService.findListings(filters, pageable);
// return ResponseEntity.ok(listings);
// Simplified placeholder:
return ResponseEntity.ok(Page.empty(pageable)); // Replace with actual implementation
}
// --- Authenticated Endpoints ---
@PostMapping
public ResponseEntity<ListingDTO> createListing(
@Valid @RequestBody CreateListingDTO createListingDTO //,
/* @AuthenticationPrincipal User currentUser */) { // Inject authenticated user
// !! Replace null with actual authenticated user !!
User currentUser = getCurrentAuthenticatedUserPlaceholder();
if (currentUser == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
ListingDTO createdListing = listingService.createListing(createListingDTO, currentUser);
return ResponseEntity.status(HttpStatus.CREATED).body(createdListing);
}
@GetMapping("/my")
public ResponseEntity<Page<ListingDTO>> getMyListings(
/* @AuthenticationPrincipal User currentUser, */
@PageableDefault(size = 10) Pageable pageable) {
// !! Replace null with actual authenticated user !!
User currentUser = getCurrentAuthenticatedUserPlaceholder();
if (currentUser == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
Page<ListingDTO> myListings = listingService.findMyListings(currentUser, pageable);
return ResponseEntity.ok(myListings);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> cancelMyListing(
@PathVariable Long id //,
/* @AuthenticationPrincipal User currentUser */) {
// !! Replace null with actual authenticated user !!
User currentUser = getCurrentAuthenticatedUserPlaceholder();
if (currentUser == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
listingService.cancelListing(id, currentUser);
return ResponseEntity.noContent().build();
}
// --- Placeholder for getting authenticated user ---
// Replace this with actual Spring Security integration (@AuthenticationPrincipal)
private User getCurrentAuthenticatedUserPlaceholder() {
// In real app, get this from SecurityContextHolder or @AuthenticationPrincipal
User user = new User();
user.setId(1L); // Example ID
user.setSteamId("76561198000000001"); // Example Steam ID
return user;
}
}
前端 (Vue 3 + Pinia + Axios + Element Plus) 代码结构与示例
csgo-market-ui/
├── public/
├── src/
│ ├── assets/ # 静态资源 (CSS, images)
│ ├── components/ # 可复用 UI 组件
│ │ ├── SkinCard.vue
│ │ ├── ListingForm.vue
│ │ └── ...
│ ├── layouts/ # 页面布局 (DefaultLayout.vue)
│ ├── pages/ (or views/) # 页面级组件
│ │ ├── HomePage.vue
│ │ ├── MarketplacePage.vue
│ │ ├── ItemDetailPage.vue
│ │ ├── UserProfilePage.vue
│ │ ├── MyListingsPage.vue
│ │ └── LoginPage.vue
│ ├── plugins/ # Vue 插件 (axios, element-plus)
│ ├── router/ # Vue Router 配置
│ │ └── index.js
│ ├── services/ (or api/) # API 请求封装
│ │ ├── axiosInstance.js
│ │ ├── listingService.js
│ │ ├── userService.js
│ │ └── authService.js
│ ├── stores/ # Pinia 状态管理
│ │ ├── authStore.js
│ │ └── userStore.js
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── index.html
├── vite.config.js (or vue.config.js)
└── package.json
1. API 服务(服务/listingService.js)
import axiosInstance from './axiosInstance'; // Your configured axios instance
const API_URL = '/listings'; // Base URL relative to backend
export const listingService = {
getListingById(id) {
return axiosInstance.get(`${API_URL}/${id}`);
},
searchListings(params) {
// params could include page, size, sort, filters
return axiosInstance.get(API_URL, { params });
},
createListing(createListingDTO) {
return axiosInstance.post(API_URL, createListingDTO);
},
getMyListings(params) {
// params could include page, size
return axiosInstance.get(`${API_URL}/my`, { params });
},
cancelMyListing(id) {
return axiosInstance.delete(`${API_URL}/${id}`);
}
};
2. Pinia 商店 (商店/authStore.js)
import { defineStore } from 'pinia';
import { ref } from 'vue';
// import { authService } from '@/services/authService'; // Your auth service
export const useAuthStore = defineStore('auth', () => {
const isAuthenticated = ref(false);
const user = ref(null); // Store basic user info like ID, steamId, username
const token = ref(localStorage.getItem('authToken') || null); // Example token storage
// Setup axios interceptor to add token to headers
// ...
async function loginWithSteam() {
// 1. Redirect user to backend Steam login endpoint
// window.location.href = '/api/auth/steam'; // Example backend endpoint
// 2. After successful redirect back from Steam & backend:
// Backend should provide a token (e.g., JWT)
// This function might be called on the redirect callback page
// await fetchUserAndTokenAfterRedirect();
console.warn("Steam Login logic needs implementation!");
// Placeholder:
isAuthenticated.value = true;
user.value = { id: 1, steamId: '7656...', platformUsername: 'DemoUser' };
token.value = 'fake-jwt-token';
localStorage.setItem('authToken', token.value);
// Setup axios header with token.value
}
function logout() {
isAuthenticated.value = false;
user.value = null;
token.value = null;
localStorage.removeItem('authToken');
// Remove token from axios headers
// Redirect to login page or home page
}
// async function fetchUserAndTokenAfterRedirect() { /* ... */ }
return { isAuthenticated, user, token, loginWithSteam, logout };
});
3. 页面组件(页面/MyListingsPage.vue)
<template>
<div class="my-listings-page">
<h1>My Active Listings</h1>
<el-button @click="fetchListings" :loading="loading" type="primary" plain>
Refresh
</el-button>
<el-table :data="listings" style="width: 100%" v-loading="loading" empty-text="No active listings found">
<el-table-column label="Item">
<template #default="scope">
<div style="display: flex; align-items: center;">
<el-image
style="width: 50px; height: 50px; margin-right: 10px;"
:src="scope.row.inventoryItem?.iconUrl || defaultImage"
fit="contain"
/>
<span>{{ scope.row.inventoryItem?.name || 'N/A' }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="inventoryItem.wearFloat" label="Wear" :formatter="formatWear" />
<el-table-column prop="price" label="Price">
<template #default="scope">
¥{{ scope.row.price?.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="createdAt" label="Listed At">
<template #default="scope">
{{ formatDate(scope.row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="Actions">
<template #default="scope">
<el-button
size="small"
type="danger"
@click="handleCancel(scope.row.id)"
:loading="cancellingId === scope.row.id"
>
Cancel
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="total > 0"
background
layout="prev, pager, next, sizes, total"
:total="total"
:page-sizes="[10, 20, 50]"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
style="margin-top: 20px; justify-content: flex-end;"
/>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { listingService } from '@/services/listingService';
import { ElMessage, ElMessageBox } from 'element-plus';
import defaultImage from '@/assets/placeholder.png'; // Placeholder image
const listings = ref([]);
const loading = ref(false);
const cancellingId = ref(null);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(10);
async function fetchListings() {
loading.value = true;
try {
const params = {
page: currentPage.value - 1, // Spring Pageable is 0-indexed
size: pageSize.value,
// sort: 'createdAt,desc' // Example sort
};
const response = await listingService.getMyListings(params);
listings.value = response.data.content; // Assuming Spring Page<> structure
total.value = response.data.totalElements;
} catch (error) {
console.error("Error fetching listings:", error);
ElMessage.error('Failed to load listings.');
listings.value = []; // Clear on error
total.value = 0;
} finally {
loading.value = false;
}
}
async function handleCancel(id) {
await ElMessageBox.confirm(
'Are you sure you want to cancel this listing?',
'Warning',
{
confirmButtonText: 'Yes, Cancel',
cancelButtonText: 'No',
type: 'warning',
}
).then(async () => {
cancellingId.value = id;
try {
await listingService.cancelMyListing(id);
ElMessage.success('Listing cancelled successfully.');
// Refresh the list after cancelling
fetchListings();
} catch (error) {
console.error("Error cancelling listing:", error);
ElMessage.error(error.response?.data?.message || 'Failed to cancel listing.');
} finally {
cancellingId.value = null;
}
}).catch(() => {
// User cancelled the confirmation dialog
ElMessage.info('Cancellation aborted');
});
}
function handleSizeChange(val) {
pageSize.value = val;
currentPage.value = 1; // Reset to first page when size changes
fetchListings();
}
function handleCurrentChange(val) {
currentPage.value = val;
fetchListings();
}
// Formatters
function formatWear(row, column, cellValue) {
return cellValue ? cellValue.toFixed(6) : 'N/A'; // Example formatting
}
function formatDate(dateString) {
if (!dateString) return 'N/A';
try {
return new Date(dateString).toLocaleString();
} catch (e) {
return dateString; // Fallback
}
}
onMounted(() => {
fetchListings();
});
// Optional: Watch for page/size changes if needed elsewhere
// watch([currentPage, pageSize], fetchListings);
</script>
<style scoped>
.my-listings-page {
padding: 20px;
}
/* Add more specific styles */
</style>