Files
hfb_sys/frontend/src/views/mobile/MobileSellerListingCreateView.vue
T
2026-05-22 21:12:46 +08:00

597 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { reactive, ref } from "vue";
import { useRouter, useRoute } from "vue-router";
import { showToast } from "vant";
import { uploadFile } from "@/api/files";
import { createListing } from "@/api/listings";
const router = useRouter();
const route = useRoute();
const loading = ref(false);
const uploading = ref(false);
const screenshotUrls = ref<string[]>([]);
/** 判断当前底部导航是否激活 */
function isNavActive(path: string) {
if (path === "/m") return route.path === "/m";
return route.path.startsWith(path);
}
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,
});
/* 区服选项 */
const serverOptions = ["微信区", "QQ区", "全平台"];
const platformOptions = ["QQ", "微信", "Steam"];
/* 截图相关 */
const fileInput = ref<HTMLInputElement | null>(null);
function triggerUpload() {
fileInput.value?.click();
}
async function handleScreenshotUpload(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
uploading.value = true;
try {
const uploaded = await uploadFile(file, "listing");
screenshotUrls.value = [...screenshotUrls.value, uploaded.url];
showToast("截图已上传");
} catch (error) {
showToast(readError(error, "截图上传失败"));
} finally {
uploading.value = false;
input.value = "";
}
}
function removeScreenshot(index: number) {
screenshotUrls.value = screenshotUrls.value.filter((_, i) => i !== index);
}
/* 提交 */
async function handleSubmit() {
if (!form.title.trim()) {
showToast("请输入标题");
return;
}
loading.value = true;
try {
await createListing({ ...form, screenshot_urls: screenshotUrls.value });
showToast("发布成功");
await router.push("/m/profile");
} catch (error) {
showToast(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>
<main class="mobile-publish">
<!-- 顶部导航 -->
<header class="page-header">
<button class="back-btn" @click="router.back()">
<van-icon name="arrow-left" :size="20" />
</button>
<h1>发布账号</h1>
<span class="header-spacer"></span>
</header>
<!-- 表单主体 -->
<section class="form-body">
<!-- 基本信息 -->
<div class="form-section">
<h2 class="section-title">基本信息</h2>
<van-field
v-model="form.title"
label="标题"
placeholder="例如:高段位哈夫币充足账号"
required
class="publish-field"
/>
<van-field
v-model="form.description"
label="说明"
type="textarea"
placeholder="描述账号资产和特点"
rows="3"
autosize
class="publish-field"
/>
</div>
<!-- 游戏信息 -->
<div class="form-section">
<h2 class="section-title">游戏信息</h2>
<van-field label="区服" class="publish-field">
<template #input>
<div class="radio-group">
<button
v-for="opt in serverOptions"
:key="opt"
class="radio-btn"
:class="{ active: form.server_region === opt }"
@click="form.server_region = opt"
>
{{ opt }}
</button>
</div>
</template>
</van-field>
<van-field label="平台" class="publish-field">
<template #input>
<div class="radio-group">
<button
v-for="opt in platformOptions"
:key="opt"
class="radio-btn"
:class="{ active: form.login_platform === opt }"
@click="form.login_platform = opt"
>
{{ opt }}
</button>
</div>
</template>
</van-field>
<van-field
v-model="form.rank_level"
label="段位"
placeholder="如:钻石、战神"
class="publish-field"
/>
<van-field
v-model="form.haf_coin_amount"
label="哈夫币"
type="digit"
placeholder="0"
class="publish-field"
/>
</div>
<!-- 价格设置 -->
<div class="form-section">
<h2 class="section-title">价格设置</h2>
<div class="price-grid">
<van-field
v-model="form.price_hourly"
label="时租"
type="number"
suffix="元/小时"
class="publish-field price-field"
/>
<van-field
v-model="form.price_daily"
label="日租"
type="number"
suffix="元/天"
class="publish-field price-field"
/>
<van-field
v-model="form.price_weekly"
label="周租"
type="number"
suffix="元/周"
class="publish-field price-field"
/>
<van-field
v-model="form.deposit_amount"
label="押金"
type="number"
suffix="元"
class="publish-field price-field"
/>
</div>
</div>
<!-- 租期设置 -->
<div class="form-section">
<h2 class="section-title">租期限制</h2>
<div class="period-row">
<van-field
v-model="form.min_rent_hours"
label="最短"
type="digit"
suffix="小时"
class="publish-field period-field"
/>
<span class="period-sep">~</span>
<van-field
v-model="form.max_rent_hours"
label="最长"
type="digit"
suffix="小时"
class="publish-field period-field"
/>
</div>
</div>
<!-- 账号截图 -->
<div class="form-section">
<h2 class="section-title">账号截图</h2>
<p class="section-hint">上传账号资产截图最多12张</p>
<div class="upload-area">
<div
v-for="(url, index) in screenshotUrls"
:key="url"
class="upload-item"
>
<img :src="url" class="upload-thumb" />
<button class="remove-btn" @click="removeScreenshot(index)">
</button>
</div>
<button
v-if="screenshotUrls.length < 12"
class="upload-add"
:disabled="uploading"
@click="triggerUpload"
>
<van-icon name="photograph" :size="24" color="#ccc" />
<span>{{ uploading ? "上传中" : "添加" }}</span>
</button>
</div>
<input
ref="fileInput"
type="file"
accept="image/jpeg,image/png,image/webp"
style="display: none"
@change="handleScreenshotUpload"
/>
</div>
<!-- 提交按钮 -->
<van-button
type="primary"
block
round
class="submit-btn"
:loading="loading"
loading-text="发布中..."
@click="handleSubmit"
>
保存发布
</van-button>
</section>
<!-- 底部导航 -->
<nav class="bottom-nav">
<RouterLink
to="/m"
class="nav-item"
:class="{ active: isNavActive('/m') }"
>
<van-icon name="home-o" :size="22" />
<span>首页</span>
</RouterLink>
<RouterLink
to="/m/listings"
class="nav-item"
:class="{ active: isNavActive('/m/listings') }"
>
<van-icon name="search" :size="22" />
<span>租号</span>
</RouterLink>
<RouterLink to="/m/seller/listings/create" class="nav-item nav-publish">
<div class="publish-pill">+</div>
<span>发布</span>
</RouterLink>
<RouterLink
to="/m/orders"
class="nav-item"
:class="{ active: isNavActive('/m/orders') }"
>
<van-icon name="orders-o" :size="22" />
<span>订单</span>
</RouterLink>
<RouterLink
to="/m/profile"
class="nav-item"
:class="{ active: isNavActive('/m/profile') }"
>
<van-icon name="manager-o" :size="22" />
<span>我的</span>
</RouterLink>
</nav>
</main>
</template>
<style scoped>
.mobile-publish {
min-height: 100dvh;
background: #f5f7fa;
padding-bottom: 56px;
}
/* ========== 顶部导航 ========== */
.page-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
height: 48px;
padding: 0 12px;
background: #fff;
border-bottom: 1px solid #eee;
}
.page-header h1 {
flex: 1;
margin: 0;
font-size: 17px;
font-weight: 700;
text-align: center;
}
.back-btn {
display: grid;
width: 36px;
height: 36px;
place-items: center;
border: none;
background: none;
color: #333;
cursor: pointer;
}
.header-spacer {
width: 36px;
}
/* ========== 表单区 ========== */
.form-body {
padding: 12px 16px 24px;
}
.form-section {
background: #fff;
border-radius: 12px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.section-title {
margin: 0 0 10px;
font-size: 14px;
font-weight: 700;
color: #1a1a1a;
}
.section-hint {
margin: -6px 0 8px;
font-size: 11px;
color: #999;
}
.publish-field {
margin-bottom: 6px;
border-radius: 8px;
background: #f8f9fb;
overflow: hidden;
}
.publish-field :deep(.van-field__label) {
color: #333;
font-weight: 600;
font-size: 13px;
width: 60px;
}
.publish-field :deep(.van-field__control) {
font-size: 14px;
}
/* ========== 单选按钮组 ========== */
.radio-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.radio-btn {
padding: 5px 14px;
border-radius: 14px;
border: 1px solid #ddd;
background: #fff;
color: #666;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
.radio-btn.active {
background: #1477ff;
color: #fff;
border-color: #1477ff;
}
/* ========== 价格网格 ========== */
.price-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.price-field :deep(.van-field__label) {
width: auto;
font-size: 12px;
}
.price-field :deep(.van-field__suffix) {
font-size: 11px;
color: #999;
}
/* ========== 租期行 ========== */
.period-row {
display: flex;
align-items: center;
gap: 6px;
}
.period-field {
flex: 1;
}
.period-sep {
color: #ccc;
font-size: 16px;
padding-top: 16px;
}
/* ========== 截图上传 ========== */
.upload-area {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.upload-item {
position: relative;
width: 72px;
height: 72px;
border-radius: 8px;
overflow: hidden;
}
.upload-thumb {
width: 100%;
height: 100%;
object-fit: cover;
}
.remove-btn {
position: absolute;
top: 0;
right: 0;
width: 20px;
height: 20px;
display: grid;
place-items: center;
border: none;
border-radius: 0 0 0 8px;
background: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 10px;
cursor: pointer;
}
.upload-add {
width: 72px;
height: 72px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
border: 1px dashed #ccc;
border-radius: 8px;
background: #f8f9fb;
color: #999;
font-size: 10px;
cursor: pointer;
}
.upload-add:disabled {
opacity: 0.5;
}
/* ========== 提交按钮 ========== */
.submit-btn {
margin-top: 8px;
height: 44px;
font-size: 15px;
font-weight: 700;
background: #ff6a00 !important;
border: none !important;
}
/* ========== 底部导航 ========== */
.bottom-nav {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
display: flex;
height: 50px;
background: #fff;
border-top: 1px solid #eee;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
color: #999;
font-size: 10px;
text-decoration: none;
}
.nav-item.active {
color: #1477ff;
}
.nav-item span {
font-weight: 600;
}
.publish-pill {
width: 36px;
height: 26px;
display: grid;
place-items: center;
border-radius: 13px;
background: #ff6a00;
color: #fff;
font-size: 18px;
font-weight: 900;
}
.nav-publish span {
color: #ff6a00;
}
.nav-publish.active span {
color: #ff6a00;
}
</style>