完善测试建表并统一前端格式

This commit is contained in:
yml2213
2026-07-14 01:32:14 +08:00
parent 2efcede800
commit 2ca6fd9306
36 changed files with 714 additions and 478 deletions
+12 -4
View File
@@ -42,7 +42,8 @@ export interface UpdateRulePayload {
// ── 渠道 ──
export async function fetchPushChannels() {
const { data } = await apiClient.get<ApiResponse<{ items: PushChannel[] }>>('/admin/push-channels')
const { data } =
await apiClient.get<ApiResponse<{ items: PushChannel[] }>>('/admin/push-channels')
return data.data.items
}
@@ -52,17 +53,24 @@ export async function createPushChannel(payload: CreateChannelPayload) {
}
export async function updatePushChannel(id: number, payload: UpdateChannelPayload) {
const { data } = await apiClient.put<ApiResponse<PushChannel>>(`/admin/push-channels/${id}`, payload)
const { data } = await apiClient.put<ApiResponse<PushChannel>>(
`/admin/push-channels/${id}`,
payload
)
return data.data
}
export async function deletePushChannel(id: number) {
const { data } = await apiClient.delete<ApiResponse<{ deleted: boolean }>>(`/admin/push-channels/${id}`)
const { data } = await apiClient.delete<ApiResponse<{ deleted: boolean }>>(
`/admin/push-channels/${id}`
)
return data.data
}
export async function testPushChannel(id: number) {
const { data } = await apiClient.post<ApiResponse<{ sent: boolean }>>(`/admin/push-channels/${id}/test`)
const { data } = await apiClient.post<ApiResponse<{ sent: boolean }>>(
`/admin/push-channels/${id}/test`
)
return data.data
}
@@ -2,7 +2,14 @@
import { computed, ref, watch } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { ElInput, ElMessage } from 'element-plus'
import { Bell, Document, Operation, Picture, QuestionFilled, Warning } from '@element-plus/icons-vue'
import {
Bell,
Document,
Operation,
Picture,
QuestionFilled,
Warning,
} from '@element-plus/icons-vue'
import type { Announcement } from '@/features/announcement'
import type {
CreateAnnouncementRequest,
@@ -134,7 +141,9 @@ async function handleImageChange(event: Event) {
uploadingImage.value = true
try {
const uploaded = await uploadAdminFile(file, 'announcement')
insertContentAtCursor(`![${imageAltText(uploaded.filename)}|w=720](${uploaded.medium_url || uploaded.url})`)
insertContentAtCursor(
`![${imageAltText(uploaded.filename)}|w=720](${uploaded.medium_url || uploaded.url})`
)
ElMessage.success('图片已插入')
} catch (error) {
ElMessage.error(readError(error, '图片上传失败'))
@@ -205,7 +214,9 @@ function applyImageSettings() {
return
}
const widthMark = imageSettingsForm.value.useCustomWidth ? `|w=${imageSettingsForm.value.width}` : ''
const widthMark = imageSettingsForm.value.useCustomWidth
? `|w=${imageSettingsForm.value.width}`
: ''
const nextMarkdown = `![${escapeMarkdownImageText(alt)}${widthMark}](${url})`
const content = formData.value.content
formData.value.content = `${content.slice(0, range.start)}${nextMarkdown}${content.slice(range.end)}`
@@ -358,7 +369,11 @@ function escapeMarkdownImageText(text: string) {
</el-form-item>
<el-form-item label="显示宽度">
<div class="image-width-control">
<el-switch v-model="imageSettingsForm.useCustomWidth" active-text="自定义" inactive-text="原宽" />
<el-switch
v-model="imageSettingsForm.useCustomWidth"
active-text="自定义"
inactive-text="原宽"
/>
<el-slider
v-model="imageSettingsForm.width"
:min="120"
@@ -208,7 +208,12 @@ function accountTypeLabel(type: string) {
:admin="true"
:preview-src-list="withdrawal.certificate_urls"
fit="cover"
:image-style="{ width: '100px', height: '100px', borderRadius: '4px', cursor: 'pointer' }"
:image-style="{
width: '100px',
height: '100px',
borderRadius: '4px',
cursor: 'pointer',
}"
/>
</div>
</el-descriptions-item>
@@ -142,7 +142,9 @@ function shortDate(date: string) {
<h2 class="section-title">
<el-icon><Shop /></el-icon>
账号上下架
<small class="section-subtitle">按北京时间自然日 · {{ dashboard.listing_daily.days }} </small>
<small class="section-subtitle"
>按北京时间自然日 · {{ dashboard.listing_daily.days }} </small
>
</h2>
<div class="listing-daily-grid">
@@ -167,7 +169,10 @@ function shortDate(date: string) {
<div
class="mini-bar publish"
:style="{
height: barHeight(row.published_count, trendMax(r => r.published_count)),
height: barHeight(
row.published_count,
trendMax(r => r.published_count)
),
}"
/>
<em>{{ shortDate(row.date) }}</em>
@@ -230,7 +235,10 @@ function shortDate(date: string) {
<div
class="mini-bar trade"
:style="{
height: barHeight(row.trade_leave_count, trendMax(r => r.trade_leave_count)),
height: barHeight(
row.trade_leave_count,
trendMax(r => r.trade_leave_count)
),
}"
/>
<em>{{ shortDate(row.date) }}</em>
@@ -9,11 +9,7 @@ import { fetchAdminUsers, type AdminUserItem } from '@/features/admin/api/adminU
import { fetchAdminFileBlob } from '@/shared/api/files'
import AuthImage from '@/shared/components/business/AuthImage.vue'
import { adminPath } from '@/shared/utils/adminPath'
import {
formatCentWithSymbol,
formatMoneyWithSymbol,
roundMoney,
} from '@/shared/utils/money'
import { formatCentWithSymbol, formatMoneyWithSymbol, roundMoney } from '@/shared/utils/money'
import {
adminMarkListingAbnormal,
adminOfflineListing,
@@ -1,7 +1,16 @@
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, onMounted, ref } from 'vue'
import { Plus, Refresh, View, Edit, Delete, Download, Upload, StarFilled } from '@element-plus/icons-vue'
import {
Plus,
Refresh,
View,
Edit,
Delete,
Download,
Upload,
StarFilled,
} from '@element-plus/icons-vue'
import {
fetchPaymentConfigs,
@@ -432,7 +441,12 @@ function deleteDisabledReason(row: PaymentConfig) {
</div>
</template>
</el-table-column>
<el-table-column prop="last_used_at" label="最后成功支付" min-width="180" show-overflow-tooltip>
<el-table-column
prop="last_used_at"
label="最后成功支付"
min-width="180"
show-overflow-tooltip
>
<template #default="{ row }">
{{ row.last_used_at ? formatDateTime(row.last_used_at, '-') : '-' }}
</template>
@@ -216,9 +216,15 @@ async function saveRule() {
</el-table-column>
<el-table-column label="操作" width="260" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" :icon="Edit" @click="openEditChannel(row)">编辑</el-button>
<el-button size="small" type="primary" :icon="Edit" @click="openEditChannel(row)"
>编辑</el-button
>
<el-button size="small" type="primary" @click="handleTestChannel(row)">测试</el-button>
<el-button size="small" :type="row.enabled ? 'warning' : 'success'" @click="toggleChannel(row)">
<el-button
size="small"
:type="row.enabled ? 'warning' : 'success'"
@click="toggleChannel(row)"
>
{{ row.enabled ? '禁用' : '启用' }}
</el-button>
<el-button size="small" type="danger" :icon="Delete" @click="handleDeleteChannel(row)" />
@@ -226,7 +232,10 @@ async function saveRule() {
</el-table-column>
</el-table>
<el-empty v-if="!channelsLoading && channels.length === 0" description="暂无推送渠道,点击上方按钮新增" />
<el-empty
v-if="!channelsLoading && channels.length === 0"
description="暂无推送渠道,点击上方按钮新增"
/>
<!-- 规则管理 -->
<div class="section-header" style="margin-top: 32px">
@@ -251,7 +260,9 @@ async function saveRule() {
<el-table-column label="消息模板" min-width="250" prop="message_template" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" :icon="Edit" @click="openEditRule(row)">编辑</el-button>
<el-button size="small" type="primary" :icon="Edit" @click="openEditRule(row)"
>编辑</el-button
>
</template>
</el-table-column>
</el-table>
@@ -276,11 +287,12 @@ async function saveRule() {
/>
</el-select>
</el-form-item>
<el-form-item v-for="field in channelConfigFields(channelForm.type)" :key="field.key" :label="field.label">
<el-input
v-model="channelForm.config[field.key]"
:placeholder="field.placeholder"
/>
<el-form-item
v-for="field in channelConfigFields(channelForm.type)"
:key="field.key"
:label="field.label"
>
<el-input v-model="channelForm.config[field.key]" :placeholder="field.placeholder" />
</el-form-item>
</el-form>
<template #footer>
@@ -300,7 +312,12 @@ async function saveRule() {
<span class="form-hint">库存低于此值时触发预警</span>
</el-form-item>
<el-form-item label="消息模板">
<el-input v-model="ruleForm.message_template" type="textarea" :rows="3" placeholder="支持 {{.Count}} 占位符" />
<el-input
v-model="ruleForm.message_template"
type="textarea"
:rows="3"
placeholder="支持 {{.Count}} 占位符"
/>
</el-form-item>
</el-form>
<template #footer>
@@ -37,7 +37,12 @@ import { useAdminSessionStore } from '@/stores/adminSession'
const adminSession = useAdminSessionStore()
const loading = ref(false)
const qrcodes = ref<ChatQrCode[]>([])
const stats = ref<QrCodeStats>({ unused_count: 0, used_count: 0, disabled_count: 0, total_count: 0 })
const stats = ref<QrCodeStats>({
unused_count: 0,
used_count: 0,
disabled_count: 0,
total_count: 0,
})
const canManageQrCodes = computed(() => adminSession.hasPermission('qrcode:manage'))
const currentPage = ref(1)
@@ -136,7 +141,9 @@ const editStatusOptions = computed(() =>
const maxBatchUploadCount = 20
const uploading = computed(() => uploadPendingCount.value > 0)
const uploadBusy = computed(() => uploading.value || uploadSaving.value)
const uploadedImageCountText = computed(() => `${uploadedImages.value.length}/${maxBatchUploadCount}`)
const uploadedImageCountText = computed(
() => `${uploadedImages.value.length}/${maxBatchUploadCount}`
)
const renameLoadingIds = ref(new Set<number>())
const selectedRows = ref<ChatQrCode[]>([])
const selectedCount = computed(() => selectedRows.value.length)
@@ -228,7 +235,12 @@ function parseGroupNameFromOcrText(text: string) {
return !excludedPatterns.some(pattern => pattern.test(line))
})
return candidates.find(line => line.includes('群')) || candidates.find(line => /[-]/.test(line)) || candidates[0] || ''
return (
candidates.find(line => line.includes('群')) ||
candidates.find(line => /[-]/.test(line)) ||
candidates[0] ||
''
)
}
const ocrConcurrencyLimit = 4
@@ -333,10 +345,6 @@ async function handleFileUpload(options: { file: File }) {
}
}
function imagePreviewSources() {
return uploadedImages.value.map(item => item.url).filter(Boolean)
}
function removeImage(index: number) {
const [removed] = uploadedImages.value.splice(index, 1)
if (removed?.previewUrl) {
@@ -377,9 +385,7 @@ async function submitUpload() {
}
uploadSaving.value = true
try {
const expiresAt = uploadForm.expires_at
? new Date(uploadForm.expires_at).toISOString()
: null
const expiresAt = uploadForm.expires_at ? new Date(uploadForm.expires_at).toISOString() : null
const items: CreateQrCodePayload[] = uploadedImages.value.map(img => ({
image_url: img.url,
group_name: img.groupName,
@@ -832,12 +838,7 @@ onMounted(reloadAll)
:image-style="{ width: '52px', height: '52px', borderRadius: '6px' }"
:preview-src-list="[img.url]"
/>
<img
v-else
class="uploaded-local-preview"
:src="img.previewUrl"
alt=""
/>
<img v-else class="uploaded-local-preview" :src="img.previewUrl" alt="" />
<button
class="uploaded-remove"
type="button"
@@ -872,7 +873,11 @@ onMounted(reloadAll)
</div>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="uploadForm.note" placeholder="7月企微群A统一备注适用于同一批次" maxlength="50" />
<el-input
v-model="uploadForm.note"
placeholder="7月企微群A统一备注适用于同一批次"
maxlength="50"
/>
</el-form-item>
<el-form-item label="过期时间">
<el-date-picker
@@ -932,10 +937,20 @@ onMounted(reloadAll)
</el-select>
</el-form-item>
<el-form-item label="群名">
<el-input v-model="editForm.group_name" placeholder="企业微信群名" maxlength="64" clearable />
<el-input
v-model="editForm.group_name"
placeholder="企业微信群名"
maxlength="64"
clearable
/>
</el-form-item>
<el-form-item label="企微改名">
<el-switch v-model="editForm.wecom_renamed" inline-prompt active-text="已改" inactive-text="未改" />
<el-switch
v-model="editForm.wecom_renamed"
inline-prompt
active-text="已改"
inactive-text="未改"
/>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="editForm.note" placeholder="7月企微群A" maxlength="50" />
@@ -130,11 +130,7 @@ onMounted(loadOrders)
</div>
<div class="review-cards">
<div
v-for="order in orders"
:key="order.id"
class="review-card"
>
<div v-for="order in orders" :key="order.id" class="review-card">
<div class="card-header">
<div class="order-info">
<span class="order-no" @click="openDetail(order)">{{ order.order_no }}</span>
@@ -167,26 +163,23 @@ onMounted(loadOrders)
<div class="info-row">
<span class="info-label">订单总额</span>
<span class="info-value"
>{{ formatCentWithSymbol((order.rent_amount_cent || 0) + order.deposit_amount_cent) }}租金{{
formatCentWithSymbol(order.rent_amount_cent || 0)
}}
/ 押金{{ formatCentWithSymbol(order.deposit_amount_cent) }}</span
>{{
formatCentWithSymbol((order.rent_amount_cent || 0) + order.deposit_amount_cent)
}}租金{{ formatCentWithSymbol(order.rent_amount_cent || 0) }} / 押金{{
formatCentWithSymbol(order.deposit_amount_cent)
}}</span
>
</div>
<div class="info-row">
<span class="info-label">退款金额</span>
<span class="info-value refund-amount">{{ formatCentWithSymbol(order.refund_amount_cent || 0) }}</span>
<span class="info-value refund-amount">{{
formatCentWithSymbol(order.refund_amount_cent || 0)
}}</span>
</div>
</div>
<div class="card-actions">
<el-button
:icon="View"
size="small"
@click="openDetail(order)"
>
查看详情
</el-button>
<el-button :icon="View" size="small" @click="openDetail(order)"> 查看详情 </el-button>
<el-button
:icon="Check"
type="primary"
@@ -211,28 +204,16 @@ onMounted(loadOrders)
</div>
<!-- 驳回弹窗 -->
<el-dialog
v-model="rejectVisible"
title="驳回退款"
width="420px"
>
<el-dialog v-model="rejectVisible" title="驳回退款" width="420px">
<div v-if="rejectOrder" class="reject-dialog">
<p class="reject-tip">请选择驳回方式</p>
<div class="reject-options">
<el-button
type="primary"
:loading="submitting"
@click="handleRejectRestore"
>
<el-button type="primary" :loading="submitting" @click="handleRejectRestore">
驳回退款恢复订单
</el-button>
<p class="option-desc">仅驳回退款请求订单恢复至待交接状态交接流程可继续</p>
<el-button
type="danger"
:loading="submitting"
@click="handleRejectClose"
>
<el-button type="danger" :loading="submitting" @click="handleRejectClose">
驳回退款并关闭订单
</el-button>
<p class="option-desc">驳回退款且关闭订单商品下架归档订单不可再操作</p>
@@ -106,7 +106,8 @@ const listingGroupWelcomeConfig = computed(
() => configs.value.find(item => item.key === 'chat.listing_group_welcome') || null
)
const renterRetentionConfig = computed(
() => configs.value.find(item => item.key === 'chat.renter_retention_days_after_order_end') || null
() =>
configs.value.find(item => item.key === 'chat.renter_retention_days_after_order_end') || null
)
const paddleOcrTokenConfig = computed(() =>
systemConfigByKey(
@@ -40,8 +40,7 @@ export async function fetchUnreadNotificationCount() {
}
export async function markAllNotificationsRead() {
const { data } = await apiClient.put<ApiResponse<{ read_count: number }>>(
'/notifications/read-all'
)
const { data } =
await apiClient.put<ApiResponse<{ read_count: number }>>('/notifications/read-all')
return data.data
}
+172 -36
View File
@@ -167,7 +167,9 @@ async function handleRegister() {
}
const modeEyebrow = computed(() => (topTab.value === 'register' ? 'Register' : 'Login'))
const modeTitle = computed(() => (topTab.value === 'register' ? '创建账号' : '欢迎回来'))
const modeSubtitle = computed(() => (topTab.value === 'register' ? '注册后即可发布或租赁账号' : '登录后即可发布或租赁账号'))
const modeSubtitle = computed(() =>
topTab.value === 'register' ? '注册后即可发布或租赁账号' : '登录后即可发布或租赁账号'
)
</script>
<template>
@@ -198,14 +200,10 @@ const modeSubtitle = computed(() => (topTab.value === 'register' ? '注册后即
</div>
<div class="login-tabs">
<button
:class="{ active: topTab === 'login' }"
@click="switchTab('login')"
>登录</button>
<button
:class="{ active: topTab === 'register' }"
@click="switchTab('register')"
>注册</button>
<button :class="{ active: topTab === 'login' }" @click="switchTab('login')">登录</button>
<button :class="{ active: topTab === 'register' }" @click="switchTab('register')">
注册
</button>
</div>
<!-- 登录模式下的方式切换 -->
@@ -213,23 +211,48 @@ const modeSubtitle = computed(() => (topTab.value === 'register' ? '注册后即
<button
:class="{ active: loginMode === 'password' }"
@click="switchLoginMode('password')"
>密码登录</button>
>
密码登录
</button>
<span class="sub-divider">|</span>
<button
:class="{ active: loginMode === 'sms' }"
@click="switchLoginMode('sms')"
>短信登录</button>
<button :class="{ active: loginMode === 'sms' }" @click="switchLoginMode('sms')">
短信登录
</button>
</div>
<!-- 短信登录 -->
<el-form v-if="topTab === 'login' && loginMode === 'sms'" class="user-form" label-position="top" @submit.prevent>
<el-form
v-if="topTab === 'login' && loginMode === 'sms'"
class="user-form"
label-position="top"
@submit.prevent
>
<el-form-item label="手机号">
<el-input v-model="form.phone" maxlength="11" placeholder="请输入手机号" size="large" :prefix-icon="Iphone" />
<el-input
v-model="form.phone"
maxlength="11"
placeholder="请输入手机号"
size="large"
:prefix-icon="Iphone"
/>
</el-form-item>
<el-form-item label="图形验证码">
<div class="user-captcha-row">
<el-input v-model="form.captchaCode" maxlength="4" placeholder="请输入图形验证码" size="large" :prefix-icon="Key" @keyup.enter="handleSendCode" />
<button class="user-captcha-image" type="button" :disabled="captchaLoading" aria-label="刷新图形验证码" @click="refreshCaptcha">
<el-input
v-model="form.captchaCode"
maxlength="4"
placeholder="请输入图形验证码"
size="large"
:prefix-icon="Key"
@keyup.enter="handleSendCode"
/>
<button
class="user-captcha-image"
type="button"
:disabled="captchaLoading"
aria-label="刷新图形验证码"
@click="refreshCaptcha"
>
<img v-if="captcha" :src="captcha.image" alt="图形验证码" />
<span v-else>刷新</span>
</button>
@@ -237,37 +260,107 @@ const modeSubtitle = computed(() => (topTab.value === 'register' ? '注册后即
</el-form-item>
<el-form-item label="验证码">
<div class="user-code-row">
<el-input v-model="form.code" maxlength="6" placeholder="6 位验证码" size="large" :prefix-icon="ChatLineRound" @keyup.enter="handleLogin" />
<el-button size="large" :disabled="countDown > 0 || sending" :loading="sending" @click="handleSendCode">
<el-input
v-model="form.code"
maxlength="6"
placeholder="6 位验证码"
size="large"
:prefix-icon="ChatLineRound"
@keyup.enter="handleLogin"
/>
<el-button
size="large"
:disabled="countDown > 0 || sending"
:loading="sending"
@click="handleSendCode"
>
{{ countDown > 0 ? `${countDown}s` : sending ? '发送中' : '发送验证码' }}
</el-button>
</div>
</el-form-item>
<el-button class="user-login-btn" type="primary" size="large" :loading="loading" @click="handleLogin">登录</el-button>
<el-button
class="user-login-btn"
type="primary"
size="large"
:loading="loading"
@click="handleLogin"
>登录</el-button
>
<p class="login-notice">未收到验证码时请稍后重试或联系客服处理</p>
</el-form>
<!-- 密码登录 -->
<el-form v-if="topTab === 'login' && loginMode === 'password'" class="user-form" label-position="top" @submit.prevent>
<el-form
v-if="topTab === 'login' && loginMode === 'password'"
class="user-form"
label-position="top"
@submit.prevent
>
<el-form-item label="手机号">
<el-input v-model="form.phone" maxlength="11" placeholder="请输入手机号" size="large" :prefix-icon="Iphone" />
<el-input
v-model="form.phone"
maxlength="11"
placeholder="请输入手机号"
size="large"
:prefix-icon="Iphone"
/>
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" maxlength="20" placeholder="请输入密码" size="large" :prefix-icon="Lock" show-password @keyup.enter="handlePasswordLogin" />
<el-input
v-model="form.password"
type="password"
maxlength="20"
placeholder="请输入密码"
size="large"
:prefix-icon="Lock"
show-password
@keyup.enter="handlePasswordLogin"
/>
</el-form-item>
<el-button class="user-login-btn" type="primary" size="large" :loading="loading" @click="handlePasswordLogin">登录</el-button>
<el-button
class="user-login-btn"
type="primary"
size="large"
:loading="loading"
@click="handlePasswordLogin"
>登录</el-button
>
<p class="login-notice">未设置密码请使用短信登录后前往个人中心设置密码</p>
</el-form>
<!-- 注册 -->
<el-form v-if="topTab === 'register'" class="user-form" label-position="top" @submit.prevent>
<el-form
v-if="topTab === 'register'"
class="user-form"
label-position="top"
@submit.prevent
>
<el-form-item label="手机号">
<el-input v-model="form.phone" maxlength="11" placeholder="请输入手机号" size="large" :prefix-icon="Iphone" />
<el-input
v-model="form.phone"
maxlength="11"
placeholder="请输入手机号"
size="large"
:prefix-icon="Iphone"
/>
</el-form-item>
<el-form-item label="图形验证码">
<div class="user-captcha-row">
<el-input v-model="form.captchaCode" maxlength="4" placeholder="请输入图形验证码" size="large" :prefix-icon="Key" @keyup.enter="handleSendCode" />
<button class="user-captcha-image" type="button" :disabled="captchaLoading" aria-label="刷新图形验证码" @click="refreshCaptcha">
<el-input
v-model="form.captchaCode"
maxlength="4"
placeholder="请输入图形验证码"
size="large"
:prefix-icon="Key"
@keyup.enter="handleSendCode"
/>
<button
class="user-captcha-image"
type="button"
:disabled="captchaLoading"
aria-label="刷新图形验证码"
@click="refreshCaptcha"
>
<img v-if="captcha" :src="captcha.image" alt="图形验证码" />
<span v-else>刷新</span>
</button>
@@ -275,19 +368,62 @@ const modeSubtitle = computed(() => (topTab.value === 'register' ? '注册后即
</el-form-item>
<el-form-item label="验证码">
<div class="user-code-row">
<el-input v-model="form.code" maxlength="6" placeholder="6 位验证码" size="large" :prefix-icon="ChatLineRound" @keyup.enter="handleRegister" />
<el-button size="large" :disabled="countDown > 0 || sending" :loading="sending" @click="handleSendCode">
<el-input
v-model="form.code"
maxlength="6"
placeholder="6 位验证码"
size="large"
:prefix-icon="ChatLineRound"
@keyup.enter="handleRegister"
/>
<el-button
size="large"
:disabled="countDown > 0 || sending"
:loading="sending"
@click="handleSendCode"
>
{{ countDown > 0 ? `${countDown}s` : sending ? '发送中' : '发送验证码' }}
</el-button>
</div>
</el-form-item>
<el-form-item label="设置密码" :error="form.password && !passwordValid ? '密码长度 8-20 位' : ''">
<el-input v-model="form.password" type="password" maxlength="20" placeholder="请输入密码(8-20 位)" size="large" :prefix-icon="Lock" show-password :class="{ 'is-error': form.password && !passwordValid }" />
<el-form-item
label="设置密码"
:error="form.password && !passwordValid ? '密码长度 8-20 位' : ''"
>
<el-input
v-model="form.password"
type="password"
maxlength="20"
placeholder="请输入密码(8-20 位)"
size="large"
:prefix-icon="Lock"
show-password
:class="{ 'is-error': form.password && !passwordValid }"
/>
</el-form-item>
<el-form-item label="确认密码" :error="form.confirmPassword && !confirmValid ? '两次密码不一致' : ''">
<el-input v-model="form.confirmPassword" type="password" maxlength="20" placeholder="请再次输入密码" size="large" :prefix-icon="Lock" show-password :class="{ 'is-error': form.confirmPassword && !confirmValid }" />
<el-form-item
label="确认密码"
:error="form.confirmPassword && !confirmValid ? '两次密码不一致' : ''"
>
<el-input
v-model="form.confirmPassword"
type="password"
maxlength="20"
placeholder="请再次输入密码"
size="large"
:prefix-icon="Lock"
show-password
:class="{ 'is-error': form.confirmPassword && !confirmValid }"
/>
</el-form-item>
<el-button class="user-login-btn" type="primary" size="large" :loading="loading" @click="handleRegister">注册</el-button>
<el-button
class="user-login-btn"
type="primary"
size="large"
:loading="loading"
@click="handleRegister"
>注册</el-button
>
<p class="login-notice">注册即表示同意用户协议隐私政策</p>
</el-form>
</div>
@@ -174,21 +174,25 @@ async function handlePasswordLogin() {
<!-- 密码登录 -->
<template v-if="loginMode === 'password'">
<label class="auth-input-row">
<input
v-model="form.password"
type="password"
maxlength="20"
placeholder="密码"
/>
<input v-model="form.password" type="password" maxlength="20" placeholder="密码" />
</label>
</template>
<div class="assist-row">
<span v-if="loginMode === 'sms'">验证码登录</span>
<span v-else>密码登录</span>
<button v-if="loginMode === 'sms'" class="link-btn" type="button" @click="switchToPassword">密码登录</button>
<button
v-if="loginMode === 'sms'"
class="link-btn"
type="button"
@click="switchToPassword"
>
密码登录
</button>
<button v-else class="link-btn" type="button" @click="switchToSms">短信验证码登录</button>
<RouterLink v-if="loginMode === 'sms'" to="/m/register">没有账号<b>立即注册</b></RouterLink>
<RouterLink v-if="loginMode === 'sms'" to="/m/register"
>没有账号<b>立即注册</b></RouterLink
>
</div>
<div class="agreement-row">
@@ -129,7 +129,13 @@ function typeLabel(type: string) {
>
查看详情
</van-button>
<van-button v-if="!item.read_at" size="small" round type="primary" @click="markRead(item)">
<van-button
v-if="!item.read_at"
size="small"
round
type="primary"
@click="markRead(item)"
>
已读
</van-button>
</div>
@@ -309,7 +309,9 @@ async function savePassword() {
</el-form>
<template #footer>
<el-button @click="showPasswordDialog = false">取消</el-button>
<el-button type="primary" :loading="savingPassword" @click="savePassword">保存密码</el-button>
<el-button type="primary" :loading="savingPassword" @click="savePassword"
>保存密码</el-button
>
</template>
</el-dialog>
</section>
@@ -206,11 +206,17 @@ function handleTabChange(tab: string) {
<span class="tab-label">
<el-icon><ChatDotRound /></el-icon>
聊天
<em v-if="unreadTotal > 0" class="tab-badge">{{ unreadTotal > 99 ? '99+' : unreadTotal }}</em>
<em v-if="unreadTotal > 0" class="tab-badge">{{
unreadTotal > 99 ? '99+' : unreadTotal
}}</em>
</span>
</template>
<div v-if="loading && conversations.length === 0" class="message-loading" v-loading="loading" />
<div
v-if="loading && conversations.length === 0"
class="message-loading"
v-loading="loading"
/>
<div v-else-if="!loading && conversations.length === 0" class="empty-panel">
<el-empty description="暂无会话">
@@ -265,7 +271,9 @@ function handleTabChange(tab: string) {
<span class="tab-label">
<el-icon><Bell /></el-icon>
通知
<em v-if="notificationUnreadCount > 0" class="tab-badge">{{ notificationUnreadCount > 99 ? '99+' : notificationUnreadCount }}</em>
<em v-if="notificationUnreadCount > 0" class="tab-badge">{{
notificationUnreadCount > 99 ? '99+' : notificationUnreadCount
}}</em>
</span>
</template>
@@ -280,10 +288,15 @@ function handleTabChange(tab: string) {
:loading="notiSubmitting"
:disabled="notificationUnreadCount === 0"
@click="markAllNotiRead"
>全部已读</el-button>
>全部已读</el-button
>
</div>
<div v-if="notiLoading && notifications.length === 0" class="message-loading" v-loading="notiLoading" />
<div
v-if="notiLoading && notifications.length === 0"
class="message-loading"
v-loading="notiLoading"
/>
<div v-else-if="!notiLoading && notifications.length === 0" class="empty-panel">
<el-empty description="暂无站内信" />
@@ -307,13 +320,15 @@ function handleTabChange(tab: string) {
v-if="item.biz_type === 'order' && item.biz_id"
size="small"
@click="router.push(`/orders/${item.biz_id}`)"
>查看订单</el-button>
>查看订单</el-button
>
<el-button
v-if="!item.read_at"
size="small"
type="primary"
@click="markNotiRead(item)"
>已读</el-button>
>已读</el-button
>
</div>
</div>
@@ -6,7 +6,7 @@ import { computed } from 'vue'
interface Props {
label: string
placeholder: string
modelValue: string[]
modelValue?: string[]
options: string[]
popoverProps: Record<string, any>
wide?: boolean
@@ -72,7 +72,11 @@ function toggleOption(value: string) {
:class="{ active: isSelected(item) }"
@click="toggleOption(item)"
>
<span class="filter-option-check" :class="{ checked: isSelected(item) }" aria-hidden="true" />
<span
class="filter-option-check"
:class="{ checked: isSelected(item) }"
aria-hidden="true"
/>
{{ item }}
</button>
</div>
@@ -52,7 +52,14 @@ export function readQueryNumber(query: LocationQuery | LocationQueryRaw, key: st
export function readQueryCSV(query: LocationQuery | LocationQueryRaw, key: string): string[] {
const raw = readQueryString(query, key)
if (!raw) return []
return [...new Set(raw.split(',').map(item => item.trim()).filter(Boolean))]
return [
...new Set(
raw
.split(',')
.map(item => item.trim())
.filter(Boolean)
),
]
}
/** 将多选数组序列化为 CSV;空数组返回 undefined 以便从 query 中省略。 */
@@ -134,11 +134,7 @@ export function useHomeFilters(
}
function setStringFilter(key: StringFilterKey, value: string | string[]) {
filters[key] = Array.isArray(value)
? uniqueOptions(value)
: value.trim()
? [value.trim()]
: []
filters[key] = Array.isArray(value) ? uniqueOptions(value) : value.trim() ? [value.trim()] : []
closeFilterPopover()
}
@@ -49,10 +49,7 @@ import {
hasAcceleratedSaleRatio,
hasGiftResources,
} from '@/shared/utils/listingDisplay'
import {
buildMobileHomeListSignature,
useMobileHomeCacheStore,
} from '@/stores/mobileHomeCache'
import { buildMobileHomeListSignature, useMobileHomeCacheStore } from '@/stores/mobileHomeCache'
import { useSessionStore } from '@/stores/session'
const router = useRouter()
@@ -85,7 +82,6 @@ const mobilePageSize = 10
let listingRequestSeq = 0
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
const sortOptions = [
{ key: 'recommended', label: '综合推荐' },
{ key: 'coinDesc', label: '哈夫币' },
@@ -734,254 +730,259 @@ syncMobileHomeQuery()
<template>
<main class="mobile-shell">
<div ref="scrollEl" class="mobile-scroll" @scroll.passive="handleListScroll">
<!-- ========== Hero 区域顶部搜索与公告 ========== -->
<section class="mobile-hero">
<div class="mobile-topbar">
<div class="mobile-brand">
<span class="mobile-logo"></span>
<div>
<strong>大锤商行</strong>
<small>哈夫币租号</small>
<!-- ========== Hero 区域顶部搜索与公告 ========== -->
<section class="mobile-hero">
<div class="mobile-topbar">
<div class="mobile-brand">
<span class="mobile-logo"></span>
<div>
<strong>大锤商行</strong>
<small>哈夫币租号</small>
</div>
</div>
</div>
<button
class="mobile-service"
type="button"
:disabled="supportLoading"
@click="handleSupportClick"
>
{{ supportLoading ? '接入中' : '客服' }}
</button>
</div>
<RouterLink class="add-home-strip" to="/m/add-home-guide" aria-label="查看添加到手机桌面教程">
<span class="add-home-mark">
<van-icon name="home-o" :size="18" />
</span>
<span class="add-home-copy">
<strong>添加到手机桌面</strong>
<small>下次像 App 一样快速打开</small>
</span>
<span class="add-home-cta">去添加</span>
</RouterLink>
<van-search
v-model="searchValue"
shape="round"
placeholder="搜编号 / 区服 / 段位"
class="home-search"
/>
<!-- 防骗提示卡片不用 van-notice-bar -->
<RouterLink class="fraud-tip" to="/m/announcements">
<van-icon name="warning-o" :size="16" color="#b8860b" />
<span class="fraud-dot"></span>
<van-swipe
class="announcement-swipe"
vertical
:autoplay="3200"
:show-indicators="false"
touchable
>
<van-swipe-item v-for="item in announcements" :key="item">
<span>{{ item }}</span>
</van-swipe-item>
</van-swipe>
<van-icon class="fraud-more" name="arrow" :size="14" />
</RouterLink>
</section>
<!-- ========== Content 区域 ========== -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<section class="mobile-content">
<!-- Banner 轮播 -->
<van-swipe class="banner-swipe" :autoplay="3600" lazy-render>
<van-swipe-item v-for="slide in bannerSlides" :key="slide.title || slide.image_url">
<div
class="mobile-banner"
:class="[`tone-${slide.tone}`, { 'has-image': slide.image_url }]"
>
<img
v-if="slide.image_url"
class="banner-image"
:src="slide.image_url"
:alt="slide.title || slide.eyebrow || '首页轮播图'"
/>
<div class="banner-copy">
<p v-if="slide.eyebrow">{{ slide.eyebrow }}</p>
<h1 v-if="slide.title">{{ slide.title }}</h1>
<span v-if="slide.pill" class="banner-pill">{{ slide.pill }}</span>
</div>
<div v-if="slide.badge" class="banner-badge">{{ slide.badge }}</div>
</div>
</van-swipe-item>
</van-swipe>
<div class="zone-strip" aria-label="账号专区">
<button
v-for="zone in visibleZoneOptions"
:key="zone.key"
class="mobile-service"
type="button"
class="zone-pill"
:class="{ active: activeZone === zone.key }"
@click="selectZone(zone.key)"
:disabled="supportLoading"
@click="handleSupportClick"
>
<strong>{{ zone.label }}</strong>
<span>{{ zone.count }}</span>
<small>{{ zone.hint }}</small>
{{ supportLoading ? '接入中' : '客服' }}
</button>
</div>
<div class="list-toolbar">
<button type="button" class="sort-entry" @click="toggleSortPanel">
<span>{{ activeSortLabel }}</span>
<van-icon :name="sortOpen ? 'arrow-up' : 'arrow-down'" :size="14" />
</button>
<button type="button" class="filter-entry" @click="openFilters">
<van-icon name="filter-o" :size="16" />
<span>筛选</span>
<em v-if="activeFilterCount">{{ activeFilterCount }}</em>
</button>
</div>
<div v-if="sortOpen" class="sort-panel">
<button
v-for="option in sortOptions"
:key="option.key"
type="button"
:class="{ active: activeSort === option.key }"
@click="selectSort(option.key)"
>
<span>{{ option.label }}</span>
<van-icon v-if="activeSort === option.key" name="success" :size="18" />
</button>
</div>
<div class="result-count">
<strong>{{ totalListings }}</strong>
<span>个可租账号</span>
</div>
<!-- 加载/错误状态 -->
<van-loading v-if="loading" class="state-loading" size="24px" vertical>
正在加载优质账号...
</van-loading>
<van-notice-bar
v-else-if="loadFailed"
left-icon="info-o"
color="#6b7a90"
background="transparent"
text="接口暂不可用,请稍后刷新。"
<RouterLink
class="add-home-strip"
to="/m/add-home-guide"
aria-label="查看添加到手机桌面教程"
>
<span class="add-home-mark">
<van-icon name="home-o" :size="18" />
</span>
<span class="add-home-copy">
<strong>添加到手机桌面</strong>
<small>下次像 App 一样快速打开</small>
</span>
<span class="add-home-cta">去添加</span>
</RouterLink>
<van-search
v-model="searchValue"
shape="round"
placeholder="搜编号 / 区服 / 段位"
class="home-search"
/>
<van-empty
v-else-if="displayListings.length === 0"
class="mobile-empty"
image="search"
description="没有符合条件的账号"
>
<van-button size="small" type="primary" @click="clearFilters"> 重置条件 </van-button>
</van-empty>
<!-- 列表卡片全宽上下布局 -->
<div class="mobile-list">
<RouterLink
v-for="item in displayListings"
:key="item.id"
class="mobile-card"
:to="`/m/listings/${item.id}`"
<!-- 防骗提示卡片不用 van-notice-bar -->
<RouterLink class="fraud-tip" to="/m/announcements">
<van-icon name="warning-o" :size="16" color="#b8860b" />
<span class="fraud-dot"></span>
<van-swipe
class="announcement-swipe"
vertical
:autoplay="3200"
:show-indicators="false"
touchable
>
<div class="card-cover">
<img
v-if="item.cover_url"
:src="item.cover_url"
:alt="getListingTitle(item)"
loading="lazy"
decoding="async"
/>
<span v-else></span>
<div
v-if="hasGiftResources(item) || hasAcceleratedSaleRatio(item)"
class="card-cover-labels"
>
<em v-if="hasGiftResources(item)">有赠送</em>
<em v-if="hasAcceleratedSaleRatio(item)">特惠</em>
</div>
</div>
<div class="card-main">
<div class="card-title-row">
<h2>{{ getListingTitle(item) }}</h2>
</div>
<div class="card-meta-row">
<div class="mobile-listing-no">{{ formatListingCode(item) }}</div>
<div
v-if="getDailyLoss(item) || formatEstimatedRentalDuration(item) !== '--'"
class="rental-meta-badge"
>
<span v-if="getDailyLoss(item)">消耗 {{ getDailyLoss(item) }}/</span>
<span
v-if="getDailyLoss(item) && formatEstimatedRentalDuration(item) !== '--'"
class="meta-separator"
>
·
</span>
<span v-if="formatEstimatedRentalDuration(item) !== '--'">
租期 {{ formatEstimatedRentalDuration(item) }}
</span>
</div>
</div>
<div class="card-action-row">
<p v-if="getMobileRatioText(item)" class="card-subtitle">
{{ getMobileRatioText(item) }}
</p>
<div class="card-badges-row">
<span
class="server-badge"
:class="`tone-${getAccessBadgeMeta(getServerRegion(item)).tone}`"
>
<van-icon :name="getAccessBadgeMeta(getServerRegion(item)).icon" :size="13" />
{{ getServerRegion(item) }}
</span>
<span
v-if="getLoginMethod(item)"
class="server-badge"
:class="`tone-${getAccessBadgeMeta(getLoginMethod(item)).tone}`"
>
<van-icon :name="getAccessBadgeMeta(getLoginMethod(item)).icon" :size="13" />
{{ getLoginMethod(item) }}
</span>
</div>
</div>
<div class="card-footer">
<div class="price-col">
<strong>¥{{ formatMoney(getListingDisplayPrice(item)) }}</strong>
<span class="rent-sub">押金 ¥{{ formatCent(item.deposit_amount_cent) }}</span>
</div>
</div>
</div>
<div class="card-chip-row">
<span
v-for="chip in getListingChips(item)"
:key="`${item.id}-${chip.label}`"
:class="`tone-${chipTone(chip.label)}`"
>
{{ chip.label }}:{{ chip.value }}
</span>
</div>
</RouterLink>
</div>
<div v-if="!loading && displayListings.length" class="mobile-load-state">
<span v-if="loadingMore">正在加载更多账号...</span>
<span v-else-if="!hasMoreListings">已经到底了</span>
</div>
<van-swipe-item v-for="item in announcements" :key="item">
<span>{{ item }}</span>
</van-swipe-item>
</van-swipe>
<van-icon class="fraud-more" name="arrow" :size="14" />
</RouterLink>
</section>
</van-pull-refresh>
<footer class="home-footer">
<a
href="https://beian.miit.gov.cn/"
target="_blank"
rel="noopener noreferrer"
class="home-footer-icp"
>湘ICP备2025146668号</a>
<span class="home-footer-divider">|</span>
<span class="home-footer-copyright">版权所有 ©2026 大锤网络游戏有限公司</span>
</footer>
<!-- ========== Content 区域 ========== -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<section class="mobile-content">
<!-- Banner 轮播 -->
<van-swipe class="banner-swipe" :autoplay="3600" lazy-render>
<van-swipe-item v-for="slide in bannerSlides" :key="slide.title || slide.image_url">
<div
class="mobile-banner"
:class="[`tone-${slide.tone}`, { 'has-image': slide.image_url }]"
>
<img
v-if="slide.image_url"
class="banner-image"
:src="slide.image_url"
:alt="slide.title || slide.eyebrow || '首页轮播图'"
/>
<div class="banner-copy">
<p v-if="slide.eyebrow">{{ slide.eyebrow }}</p>
<h1 v-if="slide.title">{{ slide.title }}</h1>
<span v-if="slide.pill" class="banner-pill">{{ slide.pill }}</span>
</div>
<div v-if="slide.badge" class="banner-badge">{{ slide.badge }}</div>
</div>
</van-swipe-item>
</van-swipe>
<div class="zone-strip" aria-label="账号专区">
<button
v-for="zone in visibleZoneOptions"
:key="zone.key"
type="button"
class="zone-pill"
:class="{ active: activeZone === zone.key }"
@click="selectZone(zone.key)"
>
<strong>{{ zone.label }}</strong>
<span>{{ zone.count }}</span>
<small>{{ zone.hint }}</small>
</button>
</div>
<div class="list-toolbar">
<button type="button" class="sort-entry" @click="toggleSortPanel">
<span>{{ activeSortLabel }}</span>
<van-icon :name="sortOpen ? 'arrow-up' : 'arrow-down'" :size="14" />
</button>
<button type="button" class="filter-entry" @click="openFilters">
<van-icon name="filter-o" :size="16" />
<span>筛选</span>
<em v-if="activeFilterCount">{{ activeFilterCount }}</em>
</button>
</div>
<div v-if="sortOpen" class="sort-panel">
<button
v-for="option in sortOptions"
:key="option.key"
type="button"
:class="{ active: activeSort === option.key }"
@click="selectSort(option.key)"
>
<span>{{ option.label }}</span>
<van-icon v-if="activeSort === option.key" name="success" :size="18" />
</button>
</div>
<div class="result-count">
<strong>{{ totalListings }}</strong>
<span>个可租账号</span>
</div>
<!-- 加载/错误状态 -->
<van-loading v-if="loading" class="state-loading" size="24px" vertical>
正在加载优质账号...
</van-loading>
<van-notice-bar
v-else-if="loadFailed"
left-icon="info-o"
color="#6b7a90"
background="transparent"
text="接口暂不可用,请稍后刷新。"
/>
<van-empty
v-else-if="displayListings.length === 0"
class="mobile-empty"
image="search"
description="没有符合条件的账号"
>
<van-button size="small" type="primary" @click="clearFilters"> 重置条件 </van-button>
</van-empty>
<!-- 列表卡片全宽上下布局 -->
<div class="mobile-list">
<RouterLink
v-for="item in displayListings"
:key="item.id"
class="mobile-card"
:to="`/m/listings/${item.id}`"
>
<div class="card-cover">
<img
v-if="item.cover_url"
:src="item.cover_url"
:alt="getListingTitle(item)"
loading="lazy"
decoding="async"
/>
<span v-else></span>
<div
v-if="hasGiftResources(item) || hasAcceleratedSaleRatio(item)"
class="card-cover-labels"
>
<em v-if="hasGiftResources(item)">有赠送</em>
<em v-if="hasAcceleratedSaleRatio(item)">特惠</em>
</div>
</div>
<div class="card-main">
<div class="card-title-row">
<h2>{{ getListingTitle(item) }}</h2>
</div>
<div class="card-meta-row">
<div class="mobile-listing-no">{{ formatListingCode(item) }}</div>
<div
v-if="getDailyLoss(item) || formatEstimatedRentalDuration(item) !== '--'"
class="rental-meta-badge"
>
<span v-if="getDailyLoss(item)">消耗 {{ getDailyLoss(item) }}/</span>
<span
v-if="getDailyLoss(item) && formatEstimatedRentalDuration(item) !== '--'"
class="meta-separator"
>
·
</span>
<span v-if="formatEstimatedRentalDuration(item) !== '--'">
租期 {{ formatEstimatedRentalDuration(item) }}
</span>
</div>
</div>
<div class="card-action-row">
<p v-if="getMobileRatioText(item)" class="card-subtitle">
{{ getMobileRatioText(item) }}
</p>
<div class="card-badges-row">
<span
class="server-badge"
:class="`tone-${getAccessBadgeMeta(getServerRegion(item)).tone}`"
>
<van-icon :name="getAccessBadgeMeta(getServerRegion(item)).icon" :size="13" />
{{ getServerRegion(item) }}
</span>
<span
v-if="getLoginMethod(item)"
class="server-badge"
:class="`tone-${getAccessBadgeMeta(getLoginMethod(item)).tone}`"
>
<van-icon :name="getAccessBadgeMeta(getLoginMethod(item)).icon" :size="13" />
{{ getLoginMethod(item) }}
</span>
</div>
</div>
<div class="card-footer">
<div class="price-col">
<strong>¥{{ formatMoney(getListingDisplayPrice(item)) }}</strong>
<span class="rent-sub">押金 ¥{{ formatCent(item.deposit_amount_cent) }}</span>
</div>
</div>
</div>
<div class="card-chip-row">
<span
v-for="chip in getListingChips(item)"
:key="`${item.id}-${chip.label}`"
:class="`tone-${chipTone(chip.label)}`"
>
{{ chip.label }}:{{ chip.value }}
</span>
</div>
</RouterLink>
</div>
<div v-if="!loading && displayListings.length" class="mobile-load-state">
<span v-if="loadingMore">正在加载更多账号...</span>
<span v-else-if="!hasMoreListings">已经到底了</span>
</div>
</section>
</van-pull-refresh>
<footer class="home-footer">
<a
href="https://beian.miit.gov.cn/"
target="_blank"
rel="noopener noreferrer"
class="home-footer-icp"
>湘ICP备2025146668号</a
>
<span class="home-footer-divider">|</span>
<span class="home-footer-copyright">版权所有 ©2026 大锤网络游戏有限公司</span>
</footer>
</div>
<MobileHomeFilterSheet
@@ -5,11 +5,7 @@ import { useRouter, useRoute } from 'vue-router'
import { showToast } from 'vant'
import MobileBottomNav from '@/components/MobileBottomNav.vue'
import {
fetchOrders,
startOrderPayment,
type Order,
} from '@/features/orders/api/orders'
import { fetchOrders, startOrderPayment, type Order } from '@/features/orders/api/orders'
import MobilePaymentCashierPopup from '@/features/orders/components/MobilePaymentCashierPopup.vue'
import MobilePayWaySelectPopup from '@/features/orders/components/MobilePayWaySelectPopup.vue'
import { useMobilePaymentCashier } from '@/features/orders/composables/useMobilePaymentCashier'
@@ -456,7 +456,11 @@ watch([() => route.query.focus, order, loading], () => {
</div>
<div class="handoff-workspace">
<OrderHandoffTimeline v-if="order" :records="handoffRecords" :listing-code="listingCode" />
<OrderHandoffTimeline
v-if="order"
:records="handoffRecords"
:listing-code="listingCode"
/>
</div>
<OrderCheckoutSummary
@@ -608,7 +612,6 @@ watch([() => route.query.focus, order, loading], () => {
</el-button>
<p class="sidebar-hint">不同意修正时需填写原因,将进入争议处理</p>
</div>
</div>
</div>
@@ -625,7 +628,11 @@ watch([() => route.query.focus, order, loading], () => {
<el-icon><Service /></el-icon>
</span>
<p>
{{ isCheckoutDisputeStage ? '提交后客服会核查结账金额和双方证据。' : '请尽量写清楚时间点、问题经过和证据,便于客服快速处理。' }}
{{
isCheckoutDisputeStage
? '提交后客服会核查结账金额和双方证据。'
: '请尽量写清楚时间点、问题经过和证据,便于客服快速处理。'
}}
</p>
</div>
<el-select
@@ -867,8 +867,6 @@
background: #ff6a00 !important;
}
@media (max-width: 370px) {
.level-options,
.region-grid {
@@ -230,7 +230,9 @@ function isPendingReview(row: Listing) {
<!-- 商品列表 -->
<section class="listing-list">
<van-loading v-if="loading" class="center-loading" size="24px" vertical>加载中...</van-loading>
<van-loading v-if="loading" class="center-loading" size="24px" vertical
>加载中...</van-loading
>
<div v-else-if="displayListings.length === 0" class="empty-state-wrap">
<van-empty description="暂无相关商品" image="search" />
@@ -562,9 +562,7 @@ function statusType(status: string): 'primary' | 'success' | 'danger' | 'warning
<van-radio name="wechat" :disabled="!!editingAccount">微信</van-radio>
<van-radio name="bank" :disabled="!!editingAccount">银行卡</van-radio>
</van-radio-group>
<p v-if="!editingAccount" class="type-tip">
推荐优先添加支付宝微信和银行卡可按需选择
</p>
<p v-if="!editingAccount" class="type-tip">推荐优先添加支付宝微信和银行卡可按需选择</p>
<van-field
v-model.trim="accountForm.account_name"