第 3 阶段:租号发布与审核
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { apiClient } from './client'
|
||||
|
||||
export interface Listing {
|
||||
id: number
|
||||
account_id: number
|
||||
owner_id: number
|
||||
title: string
|
||||
description: string
|
||||
game_name: string
|
||||
server_region: string
|
||||
login_platform: string
|
||||
rank_level: string
|
||||
haf_coin_amount: number
|
||||
price_hourly: number
|
||||
price_daily: number
|
||||
price_weekly: number
|
||||
deposit_amount: number
|
||||
min_rent_hours: number
|
||||
max_rent_hours: number
|
||||
status: string
|
||||
review_status: string
|
||||
review_reason: string
|
||||
published_at?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface ListingPayload {
|
||||
title: string
|
||||
description: string
|
||||
server_region: string
|
||||
login_platform: string
|
||||
rank_level: string
|
||||
haf_coin_amount: number
|
||||
price_hourly: number
|
||||
price_daily: number
|
||||
price_weekly: number
|
||||
deposit_amount: number
|
||||
min_rent_hours: number
|
||||
max_rent_hours: number
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export async function fetchListings() {
|
||||
const { data } = await apiClient.get<ApiResponse<{ items: Listing[] }>>('/listings')
|
||||
return data.data.items
|
||||
}
|
||||
|
||||
export async function fetchListing(id: string | number) {
|
||||
const { data } = await apiClient.get<ApiResponse<Listing>>(`/listings/${id}`)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchSellerListings() {
|
||||
const { data } = await apiClient.get<ApiResponse<{ items: Listing[] }>>('/seller/listings')
|
||||
return data.data.items
|
||||
}
|
||||
|
||||
export async function createListing(payload: ListingPayload) {
|
||||
const { data } = await apiClient.post<ApiResponse<Listing>>('/listings', payload)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function submitListingReview(id: number) {
|
||||
const { data } = await apiClient.post<ApiResponse<Listing>>(`/listings/${id}/submit-review`)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function offlineListing(id: number) {
|
||||
const { data } = await apiClient.delete<ApiResponse<{ offline: boolean }>>(`/listings/${id}`)
|
||||
return data.data
|
||||
}
|
||||
@@ -14,6 +14,7 @@ const router = createRouter({
|
||||
{ path: '/notifications', name: 'notifications', component: () => import('@/views/account/NotificationsView.vue') },
|
||||
{ path: '/realname', name: 'realname', component: () => import('@/views/account/RealnameView.vue') },
|
||||
{ path: '/seller/listings', name: 'seller-listings', component: () => import('@/views/seller/SellerListingsView.vue') },
|
||||
{ path: '/seller/listings/create', name: 'seller-listing-create', component: () => import('@/views/seller/SellerListingCreateView.vue') },
|
||||
{ path: '/seller/handoffs', name: 'seller-handoffs', component: () => import('@/views/seller/SellerHandoffsView.vue') },
|
||||
{ path: '/seller/earnings', name: 'seller-earnings', component: () => import('@/views/seller/SellerEarningsView.vue') },
|
||||
{ path: '/admin/dashboard', name: 'admin-dashboard', component: () => import('@/views/admin/AdminDashboardView.vue') },
|
||||
|
||||
@@ -182,6 +182,90 @@ h1 {
|
||||
color: #52616f;
|
||||
}
|
||||
|
||||
.listing-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.listing-card {
|
||||
display: block;
|
||||
min-height: 172px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.listing-card span {
|
||||
color: #0f766e;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.listing-card strong {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.listing-card p {
|
||||
color: #52616f;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.listing-price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.listing-price b {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.listing-price em {
|
||||
color: #6b7785;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.page-header-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.table-panel,
|
||||
.listing-form {
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.listing-form {
|
||||
max-width: 860px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
column-gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -208,4 +292,14 @@ h1 {
|
||||
.metric-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.listing-grid,
|
||||
.detail-grid,
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.page-header-row {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { fetchListing, type Listing } from '@/api/listings'
|
||||
|
||||
const route = useRoute()
|
||||
const loading = ref(false)
|
||||
const listing = ref<Listing | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
listing.value = await fetchListing(String(route.params.id))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Listing Detail</p>
|
||||
<h1>租号详情</h1>
|
||||
<p>展示账号资产、哈夫币快照、租金押金、号主信用和风险提示。</p>
|
||||
<section class="page" v-loading="loading">
|
||||
<div v-if="listing" class="page-header">
|
||||
<p class="eyebrow">{{ listing.server_region }} / {{ listing.login_platform }}</p>
|
||||
<h1>{{ listing.title }}</h1>
|
||||
<p>{{ listing.description || '号主暂未填写详细说明。' }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="listing" class="detail-grid">
|
||||
<div class="metric-card">
|
||||
<span>哈夫币</span>
|
||||
<strong>{{ listing.haf_coin_amount }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>时租</span>
|
||||
<strong>¥{{ listing.price_hourly }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>押金</span>
|
||||
<strong>¥{{ listing.deposit_amount }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>租期</span>
|
||||
<strong>{{ listing.min_rent_hours }}-{{ listing.max_rent_hours }} 小时</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { fetchListings, type Listing } from '@/api/listings'
|
||||
|
||||
const loading = ref(false)
|
||||
const listings = ref<Listing[]>([])
|
||||
|
||||
onMounted(loadListings)
|
||||
|
||||
async function loadListings() {
|
||||
loading.value = true
|
||||
try {
|
||||
listings.value = await fetchListings()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Listings</p>
|
||||
<h1>租号列表</h1>
|
||||
<p>后续支持区服、平台、价格、押金、段位和哈夫币数量筛选。</p>
|
||||
<p>支持区服、平台、价格、押金、段位和哈夫币数量展示。</p>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!loading && listings.length === 0" description="暂无已上架租号" />
|
||||
<div v-else v-loading="loading" class="listing-grid">
|
||||
<RouterLink v-for="item in listings" :key="item.id" class="listing-card" :to="`/listings/${item.id}`">
|
||||
<span>{{ item.server_region }} / {{ item.login_platform }}</span>
|
||||
<strong>{{ item.title }}</strong>
|
||||
<p>{{ item.rank_level || '未填写段位' }} · 哈夫币 {{ item.haf_coin_amount }}</p>
|
||||
<div class="listing-price">
|
||||
<b>¥{{ item.price_hourly }}/小时</b>
|
||||
<em>押金 ¥{{ item.deposit_amount }}</em>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { createListing } from '@/api/listings'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const form = reactive({
|
||||
title: '',
|
||||
description: '',
|
||||
server_region: '微信区',
|
||||
login_platform: 'QQ',
|
||||
rank_level: '',
|
||||
haf_coin_amount: 0,
|
||||
price_hourly: 5,
|
||||
price_daily: 50,
|
||||
price_weekly: 300,
|
||||
deposit_amount: 100,
|
||||
min_rent_hours: 1,
|
||||
max_rent_hours: 24,
|
||||
})
|
||||
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
try {
|
||||
await createListing({ ...form })
|
||||
ElMessage.success('发布已创建')
|
||||
await router.push('/seller/listings')
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '发布失败,请确认已登录并完成实名认证'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function readError(error: unknown, fallback: string) {
|
||||
if (typeof error === 'object' && error && 'response' in error) {
|
||||
const response = (error as { response?: { data?: { message?: string } } }).response
|
||||
return response?.data?.message || fallback
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Create Listing</p>
|
||||
<h1>发布账号</h1>
|
||||
<p>一期先记录账号资产说明和哈夫币填报值,下单时会生成订单资产快照。</p>
|
||||
</div>
|
||||
|
||||
<el-form class="listing-form" label-position="top">
|
||||
<el-form-item label="标题">
|
||||
<el-input v-model="form.title" placeholder="例如:高段位哈夫币充足账号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="账号说明">
|
||||
<el-input v-model="form.description" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="区服">
|
||||
<el-input v-model="form.server_region" />
|
||||
</el-form-item>
|
||||
<el-form-item label="平台">
|
||||
<el-input v-model="form.login_platform" />
|
||||
</el-form-item>
|
||||
<el-form-item label="段位">
|
||||
<el-input v-model="form.rank_level" />
|
||||
</el-form-item>
|
||||
<el-form-item label="哈夫币数量">
|
||||
<el-input-number v-model="form.haf_coin_amount" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="时租">
|
||||
<el-input-number v-model="form.price_hourly" :min="1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="日租">
|
||||
<el-input-number v-model="form.price_daily" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="周租">
|
||||
<el-input-number v-model="form.price_weekly" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="押金">
|
||||
<el-input-number v-model="form.deposit_amount" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最短租期(小时)">
|
||||
<el-input-number v-model="form.min_rent_hours" :min="1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最长租期(小时)">
|
||||
<el-input-number v-model="form.max_rent_hours" :min="form.min_rent_hours" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-button type="primary" :loading="loading" @click="handleSubmit">保存发布</el-button>
|
||||
</el-form>
|
||||
</section>
|
||||
</template>
|
||||
@@ -1,9 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { fetchSellerListings, offlineListing, submitListingReview, type Listing } from '@/api/listings'
|
||||
|
||||
const loading = ref(false)
|
||||
const listings = ref<Listing[]>([])
|
||||
|
||||
onMounted(loadListings)
|
||||
|
||||
async function loadListings() {
|
||||
loading.value = true
|
||||
try {
|
||||
listings.value = await fetchSellerListings()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitReview(id: number) {
|
||||
await submitListingReview(id)
|
||||
ElMessage.success('已提交审核并自动上架')
|
||||
await loadListings()
|
||||
}
|
||||
|
||||
async function offline(id: number) {
|
||||
await offlineListing(id)
|
||||
ElMessage.success('已下架')
|
||||
await loadListings()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Seller</p>
|
||||
<h1>我的发布</h1>
|
||||
<p>管理账号发布、审核状态、上架和下架。</p>
|
||||
<div class="page-header page-header-row">
|
||||
<div>
|
||||
<p class="eyebrow">Seller</p>
|
||||
<h1>我的发布</h1>
|
||||
<p>管理账号发布、审核状态、上架和下架。</p>
|
||||
</div>
|
||||
<RouterLink to="/seller/listings/create">
|
||||
<el-button type="primary">发布账号</el-button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" class="table-panel" :data="listings">
|
||||
<el-table-column prop="title" label="标题" min-width="180" />
|
||||
<el-table-column prop="server_region" label="区服" width="120" />
|
||||
<el-table-column prop="haf_coin_amount" label="哈夫币" width="120" />
|
||||
<el-table-column prop="price_hourly" label="时租" width="100" />
|
||||
<el-table-column prop="deposit_amount" label="押金" width="100" />
|
||||
<el-table-column prop="status" label="状态" width="110" />
|
||||
<el-table-column prop="review_status" label="审核" width="110" />
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="submitReview(row.id)">上架</el-button>
|
||||
<el-button size="small" type="danger" @click="offline(row.id)">下架</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user