完善测试建表并统一前端格式
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"hfb_sys/backend/internal/model"
|
||||||
|
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
@@ -31,3 +33,39 @@ func NewTestDBWithName(name string) *gorm.DB {
|
|||||||
}
|
}
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MigrateListingLifecycleTestSchema 创建商品生命周期测试所需的共享表结构。
|
||||||
|
func MigrateListingLifecycleTestSchema(db *gorm.DB) error {
|
||||||
|
return db.AutoMigrate(
|
||||||
|
&model.User{},
|
||||||
|
&model.GameAccount{},
|
||||||
|
&model.RentalListing{},
|
||||||
|
&model.ListingStatusEvent{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MigrateRentalTransactionTestSchema 在商品生命周期基础上创建订单、支付和交接测试所需的共享表结构。
|
||||||
|
func MigrateRentalTransactionTestSchema(db *gorm.DB) error {
|
||||||
|
if err := MigrateListingLifecycleTestSchema(db); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return db.AutoMigrate(
|
||||||
|
&model.RentalOrder{},
|
||||||
|
&model.PaymentOrder{},
|
||||||
|
&model.Notification{},
|
||||||
|
&model.HandoffRecord{},
|
||||||
|
&model.OrderCheckout{},
|
||||||
|
&model.AuditLog{},
|
||||||
|
&model.ChatConversation{},
|
||||||
|
&model.ChatParticipant{},
|
||||||
|
&model.ChatMessage{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MigrateRentalDisputeTestSchema 在交易链路基础上创建申诉测试所需的表结构。
|
||||||
|
func MigrateRentalDisputeTestSchema(db *gorm.DB) error {
|
||||||
|
if err := MigrateRentalTransactionTestSchema(db); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return db.AutoMigrate(&model.Dispute{})
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,31 +5,16 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"hfb_sys/backend/internal/database"
|
||||||
"hfb_sys/backend/internal/model"
|
"hfb_sys/backend/internal/model"
|
||||||
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupDisputeTestDB(t *testing.T) *gorm.DB {
|
func setupDisputeTestDB(t *testing.T) *gorm.DB {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
db := database.NewTestDB()
|
||||||
Logger: logger.Default.LogMode(logger.Silent),
|
if err := database.MigrateRentalDisputeTestSchema(db); err != nil {
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("无法创建测试数据库: %v", err)
|
|
||||||
}
|
|
||||||
if err := db.AutoMigrate(
|
|
||||||
&model.User{},
|
|
||||||
&model.GameAccount{},
|
|
||||||
&model.RentalListing{},
|
|
||||||
&model.RentalOrder{},
|
|
||||||
&model.OrderCheckout{},
|
|
||||||
&model.HandoffRecord{},
|
|
||||||
&model.Dispute{},
|
|
||||||
&model.Notification{},
|
|
||||||
); err != nil {
|
|
||||||
t.Fatalf("数据库迁移失败: %v", err)
|
t.Fatalf("数据库迁移失败: %v", err)
|
||||||
}
|
}
|
||||||
return db
|
return db
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ func TestPublishCooldownCanBeDisabled(t *testing.T) {
|
|||||||
|
|
||||||
func TestRepositoryCreateRejectsRecentOwnerPublish(t *testing.T) {
|
func TestRepositoryCreateRejectsRecentOwnerPublish(t *testing.T) {
|
||||||
db := database.NewTestDB()
|
db := database.NewTestDB()
|
||||||
if err := db.AutoMigrate(&model.User{}, &model.GameAccount{}, &model.RentalListing{}); err != nil {
|
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
|
||||||
t.Fatalf("failed to migrate test db: %v", err)
|
t.Fatalf("failed to migrate test db: %v", err)
|
||||||
}
|
}
|
||||||
user := model.User{Phone: "18800000000", RealnameStatus: "verified", Status: "active"}
|
user := model.User{Phone: "18800000000", RealnameStatus: "verified", Status: "active"}
|
||||||
@@ -175,7 +175,7 @@ func TestRepositoryCreateRejectsRecentOwnerPublish(t *testing.T) {
|
|||||||
|
|
||||||
func TestRepositoryUpdateAllowsOfflineListingResubmit(t *testing.T) {
|
func TestRepositoryUpdateAllowsOfflineListingResubmit(t *testing.T) {
|
||||||
db := database.NewTestDB()
|
db := database.NewTestDB()
|
||||||
if err := db.AutoMigrate(&model.GameAccount{}, &model.RentalListing{}); err != nil {
|
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
|
||||||
t.Fatalf("failed to migrate test db: %v", err)
|
t.Fatalf("failed to migrate test db: %v", err)
|
||||||
}
|
}
|
||||||
repo := NewRepository(db, nil)
|
repo := NewRepository(db, nil)
|
||||||
@@ -215,7 +215,7 @@ func TestRepositoryUpdateAllowsOfflineListingResubmit(t *testing.T) {
|
|||||||
|
|
||||||
func TestRepositoryRejectsCompletedListingOwnerMutations(t *testing.T) {
|
func TestRepositoryRejectsCompletedListingOwnerMutations(t *testing.T) {
|
||||||
db := database.NewTestDB()
|
db := database.NewTestDB()
|
||||||
if err := db.AutoMigrate(&model.GameAccount{}, &model.RentalListing{}); err != nil {
|
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
|
||||||
t.Fatalf("failed to migrate test db: %v", err)
|
t.Fatalf("failed to migrate test db: %v", err)
|
||||||
}
|
}
|
||||||
repo := NewRepository(db, nil)
|
repo := NewRepository(db, nil)
|
||||||
|
|||||||
@@ -4,34 +4,18 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"hfb_sys/backend/internal/database"
|
||||||
"hfb_sys/backend/internal/model"
|
"hfb_sys/backend/internal/model"
|
||||||
|
|
||||||
"gorm.io/datatypes"
|
"gorm.io/datatypes"
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// setupTestDB 创建测试数据库
|
// setupOrderTestDB 创建订单测试数据库。
|
||||||
func setupOrderTestDB(t *testing.T) *gorm.DB {
|
func setupOrderTestDB(t *testing.T) *gorm.DB {
|
||||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
t.Helper()
|
||||||
Logger: logger.Default.LogMode(logger.Silent),
|
db := database.NewTestDB()
|
||||||
})
|
if err := database.MigrateRentalTransactionTestSchema(db); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("无法创建测试数据库: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.AutoMigrate(
|
|
||||||
&model.User{},
|
|
||||||
&model.GameAccount{},
|
|
||||||
&model.RentalListing{},
|
|
||||||
&model.RentalOrder{},
|
|
||||||
&model.PaymentOrder{},
|
|
||||||
&model.Notification{},
|
|
||||||
&model.HandoffRecord{},
|
|
||||||
&model.OrderCheckout{},
|
|
||||||
&model.AuditLog{},
|
|
||||||
); err != nil {
|
|
||||||
t.Fatalf("数据库迁移失败: %v", err)
|
t.Fatalf("数据库迁移失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,34 +6,18 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"hfb_sys/backend/internal/database"
|
||||||
"hfb_sys/backend/internal/model"
|
"hfb_sys/backend/internal/model"
|
||||||
ordermodule "hfb_sys/backend/internal/modules/order"
|
ordermodule "hfb_sys/backend/internal/modules/order"
|
||||||
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// setupPaymentTestDB 创建测试数据库
|
// setupPaymentTestDB 创建测试数据库
|
||||||
func setupPaymentTestDB(t *testing.T) *gorm.DB {
|
func setupPaymentTestDB(t *testing.T) *gorm.DB {
|
||||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
t.Helper()
|
||||||
Logger: logger.Default.LogMode(logger.Silent),
|
db := database.NewTestDB()
|
||||||
})
|
if err := database.MigrateRentalTransactionTestSchema(db); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("无法创建测试数据库: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.AutoMigrate(
|
|
||||||
&model.PaymentOrder{},
|
|
||||||
&model.RentalOrder{},
|
|
||||||
&model.RentalListing{},
|
|
||||||
&model.GameAccount{},
|
|
||||||
&model.User{},
|
|
||||||
&model.ChatConversation{},
|
|
||||||
&model.ChatParticipant{},
|
|
||||||
&model.ChatMessage{},
|
|
||||||
&model.Notification{},
|
|
||||||
); err != nil {
|
|
||||||
t.Fatalf("数据库迁移失败: %v", err)
|
t.Fatalf("数据库迁移失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ export interface UpdateRulePayload {
|
|||||||
// ── 渠道 ──
|
// ── 渠道 ──
|
||||||
|
|
||||||
export async function fetchPushChannels() {
|
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
|
return data.data.items
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,17 +53,24 @@ export async function createPushChannel(payload: CreateChannelPayload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function updatePushChannel(id: number, payload: UpdateChannelPayload) {
|
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
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deletePushChannel(id: number) {
|
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
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function testPushChannel(id: number) {
|
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
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,14 @@
|
|||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import { ElInput, ElMessage } 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 { Announcement } from '@/features/announcement'
|
||||||
import type {
|
import type {
|
||||||
CreateAnnouncementRequest,
|
CreateAnnouncementRequest,
|
||||||
@@ -134,7 +141,9 @@ async function handleImageChange(event: Event) {
|
|||||||
uploadingImage.value = true
|
uploadingImage.value = true
|
||||||
try {
|
try {
|
||||||
const uploaded = await uploadAdminFile(file, 'announcement')
|
const uploaded = await uploadAdminFile(file, 'announcement')
|
||||||
insertContentAtCursor(``)
|
insertContentAtCursor(
|
||||||
|
``
|
||||||
|
)
|
||||||
ElMessage.success('图片已插入')
|
ElMessage.success('图片已插入')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(readError(error, '图片上传失败'))
|
ElMessage.error(readError(error, '图片上传失败'))
|
||||||
@@ -205,7 +214,9 @@ function applyImageSettings() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const widthMark = imageSettingsForm.value.useCustomWidth ? `|w=${imageSettingsForm.value.width}` : ''
|
const widthMark = imageSettingsForm.value.useCustomWidth
|
||||||
|
? `|w=${imageSettingsForm.value.width}`
|
||||||
|
: ''
|
||||||
const nextMarkdown = ``
|
const nextMarkdown = ``
|
||||||
const content = formData.value.content
|
const content = formData.value.content
|
||||||
formData.value.content = `${content.slice(0, range.start)}${nextMarkdown}${content.slice(range.end)}`
|
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>
|
||||||
<el-form-item label="显示宽度">
|
<el-form-item label="显示宽度">
|
||||||
<div class="image-width-control">
|
<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
|
<el-slider
|
||||||
v-model="imageSettingsForm.width"
|
v-model="imageSettingsForm.width"
|
||||||
:min="120"
|
:min="120"
|
||||||
|
|||||||
@@ -208,7 +208,12 @@ function accountTypeLabel(type: string) {
|
|||||||
:admin="true"
|
:admin="true"
|
||||||
:preview-src-list="withdrawal.certificate_urls"
|
:preview-src-list="withdrawal.certificate_urls"
|
||||||
fit="cover"
|
fit="cover"
|
||||||
:image-style="{ width: '100px', height: '100px', borderRadius: '4px', cursor: 'pointer' }"
|
:image-style="{
|
||||||
|
width: '100px',
|
||||||
|
height: '100px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|||||||
@@ -142,7 +142,9 @@ function shortDate(date: string) {
|
|||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
<el-icon><Shop /></el-icon>
|
<el-icon><Shop /></el-icon>
|
||||||
账号上下架
|
账号上下架
|
||||||
<small class="section-subtitle">按北京时间自然日 · 近 {{ dashboard.listing_daily.days }} 日</small>
|
<small class="section-subtitle"
|
||||||
|
>按北京时间自然日 · 近 {{ dashboard.listing_daily.days }} 日</small
|
||||||
|
>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="listing-daily-grid">
|
<div class="listing-daily-grid">
|
||||||
@@ -167,7 +169,10 @@ function shortDate(date: string) {
|
|||||||
<div
|
<div
|
||||||
class="mini-bar publish"
|
class="mini-bar publish"
|
||||||
:style="{
|
: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>
|
<em>{{ shortDate(row.date) }}</em>
|
||||||
@@ -230,7 +235,10 @@ function shortDate(date: string) {
|
|||||||
<div
|
<div
|
||||||
class="mini-bar trade"
|
class="mini-bar trade"
|
||||||
:style="{
|
: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>
|
<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 { fetchAdminFileBlob } from '@/shared/api/files'
|
||||||
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||||||
import { adminPath } from '@/shared/utils/adminPath'
|
import { adminPath } from '@/shared/utils/adminPath'
|
||||||
import {
|
import { formatCentWithSymbol, formatMoneyWithSymbol, roundMoney } from '@/shared/utils/money'
|
||||||
formatCentWithSymbol,
|
|
||||||
formatMoneyWithSymbol,
|
|
||||||
roundMoney,
|
|
||||||
} from '@/shared/utils/money'
|
|
||||||
import {
|
import {
|
||||||
adminMarkListingAbnormal,
|
adminMarkListingAbnormal,
|
||||||
adminOfflineListing,
|
adminOfflineListing,
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
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 {
|
import {
|
||||||
fetchPaymentConfigs,
|
fetchPaymentConfigs,
|
||||||
@@ -432,7 +441,12 @@ function deleteDisabledReason(row: PaymentConfig) {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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 }">
|
<template #default="{ row }">
|
||||||
{{ row.last_used_at ? formatDateTime(row.last_used_at, '-') : '-' }}
|
{{ row.last_used_at ? formatDateTime(row.last_used_at, '-') : '-' }}
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -216,9 +216,15 @@ async function saveRule() {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="260" fixed="right">
|
<el-table-column label="操作" width="260" fixed="right">
|
||||||
<template #default="{ row }">
|
<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="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 ? '禁用' : '启用' }}
|
{{ row.enabled ? '禁用' : '启用' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="small" type="danger" :icon="Delete" @click="handleDeleteChannel(row)" />
|
<el-button size="small" type="danger" :icon="Delete" @click="handleDeleteChannel(row)" />
|
||||||
@@ -226,7 +232,10 @@ async function saveRule() {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</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">
|
<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="消息模板" min-width="250" prop="message_template" />
|
||||||
<el-table-column label="操作" width="100" fixed="right">
|
<el-table-column label="操作" width="100" fixed="right">
|
||||||
<template #default="{ row }">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -276,11 +287,12 @@ async function saveRule() {
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-for="field in channelConfigFields(channelForm.type)" :key="field.key" :label="field.label">
|
<el-form-item
|
||||||
<el-input
|
v-for="field in channelConfigFields(channelForm.type)"
|
||||||
v-model="channelForm.config[field.key]"
|
:key="field.key"
|
||||||
:placeholder="field.placeholder"
|
:label="field.label"
|
||||||
/>
|
>
|
||||||
|
<el-input v-model="channelForm.config[field.key]" :placeholder="field.placeholder" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -300,7 +312,12 @@ async function saveRule() {
|
|||||||
<span class="form-hint">库存低于此值时触发预警</span>
|
<span class="form-hint">库存低于此值时触发预警</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="消息模板">
|
<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-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
|||||||
@@ -37,7 +37,12 @@ import { useAdminSessionStore } from '@/stores/adminSession'
|
|||||||
const adminSession = useAdminSessionStore()
|
const adminSession = useAdminSessionStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const qrcodes = ref<ChatQrCode[]>([])
|
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 canManageQrCodes = computed(() => adminSession.hasPermission('qrcode:manage'))
|
||||||
|
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
@@ -136,7 +141,9 @@ const editStatusOptions = computed(() =>
|
|||||||
const maxBatchUploadCount = 20
|
const maxBatchUploadCount = 20
|
||||||
const uploading = computed(() => uploadPendingCount.value > 0)
|
const uploading = computed(() => uploadPendingCount.value > 0)
|
||||||
const uploadBusy = computed(() => uploading.value || uploadSaving.value)
|
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 renameLoadingIds = ref(new Set<number>())
|
||||||
const selectedRows = ref<ChatQrCode[]>([])
|
const selectedRows = ref<ChatQrCode[]>([])
|
||||||
const selectedCount = computed(() => selectedRows.value.length)
|
const selectedCount = computed(() => selectedRows.value.length)
|
||||||
@@ -228,7 +235,12 @@ function parseGroupNameFromOcrText(text: string) {
|
|||||||
return !excludedPatterns.some(pattern => pattern.test(line))
|
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
|
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) {
|
function removeImage(index: number) {
|
||||||
const [removed] = uploadedImages.value.splice(index, 1)
|
const [removed] = uploadedImages.value.splice(index, 1)
|
||||||
if (removed?.previewUrl) {
|
if (removed?.previewUrl) {
|
||||||
@@ -377,9 +385,7 @@ async function submitUpload() {
|
|||||||
}
|
}
|
||||||
uploadSaving.value = true
|
uploadSaving.value = true
|
||||||
try {
|
try {
|
||||||
const expiresAt = uploadForm.expires_at
|
const expiresAt = uploadForm.expires_at ? new Date(uploadForm.expires_at).toISOString() : null
|
||||||
? new Date(uploadForm.expires_at).toISOString()
|
|
||||||
: null
|
|
||||||
const items: CreateQrCodePayload[] = uploadedImages.value.map(img => ({
|
const items: CreateQrCodePayload[] = uploadedImages.value.map(img => ({
|
||||||
image_url: img.url,
|
image_url: img.url,
|
||||||
group_name: img.groupName,
|
group_name: img.groupName,
|
||||||
@@ -832,12 +838,7 @@ onMounted(reloadAll)
|
|||||||
:image-style="{ width: '52px', height: '52px', borderRadius: '6px' }"
|
:image-style="{ width: '52px', height: '52px', borderRadius: '6px' }"
|
||||||
:preview-src-list="[img.url]"
|
:preview-src-list="[img.url]"
|
||||||
/>
|
/>
|
||||||
<img
|
<img v-else class="uploaded-local-preview" :src="img.previewUrl" alt="" />
|
||||||
v-else
|
|
||||||
class="uploaded-local-preview"
|
|
||||||
:src="img.previewUrl"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
class="uploaded-remove"
|
class="uploaded-remove"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -872,7 +873,11 @@ onMounted(reloadAll)
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="备注">
|
<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>
|
||||||
<el-form-item label="过期时间">
|
<el-form-item label="过期时间">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
@@ -932,10 +937,20 @@ onMounted(reloadAll)
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="群名">
|
<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>
|
||||||
<el-form-item label="企微改名">
|
<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>
|
||||||
<el-form-item label="备注">
|
<el-form-item label="备注">
|
||||||
<el-input v-model="editForm.note" placeholder="如:7月企微群A" maxlength="50" />
|
<el-input v-model="editForm.note" placeholder="如:7月企微群A" maxlength="50" />
|
||||||
|
|||||||
@@ -130,11 +130,7 @@ onMounted(loadOrders)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="review-cards">
|
<div class="review-cards">
|
||||||
<div
|
<div v-for="order in orders" :key="order.id" class="review-card">
|
||||||
v-for="order in orders"
|
|
||||||
:key="order.id"
|
|
||||||
class="review-card"
|
|
||||||
>
|
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="order-info">
|
<div class="order-info">
|
||||||
<span class="order-no" @click="openDetail(order)">{{ order.order_no }}</span>
|
<span class="order-no" @click="openDetail(order)">{{ order.order_no }}</span>
|
||||||
@@ -167,26 +163,23 @@ onMounted(loadOrders)
|
|||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">订单总额</span>
|
<span class="info-label">订单总额</span>
|
||||||
<span class="info-value"
|
<span class="info-value"
|
||||||
>{{ formatCentWithSymbol((order.rent_amount_cent || 0) + order.deposit_amount_cent) }}(租金{{
|
>{{
|
||||||
formatCentWithSymbol(order.rent_amount_cent || 0)
|
formatCentWithSymbol((order.rent_amount_cent || 0) + order.deposit_amount_cent)
|
||||||
}}
|
}}(租金{{ formatCentWithSymbol(order.rent_amount_cent || 0) }} / 押金{{
|
||||||
/ 押金{{ formatCentWithSymbol(order.deposit_amount_cent) }})</span
|
formatCentWithSymbol(order.deposit_amount_cent)
|
||||||
|
}})</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">退款金额</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<el-button
|
<el-button :icon="View" size="small" @click="openDetail(order)"> 查看详情 </el-button>
|
||||||
:icon="View"
|
|
||||||
size="small"
|
|
||||||
@click="openDetail(order)"
|
|
||||||
>
|
|
||||||
查看详情
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
<el-button
|
||||||
:icon="Check"
|
:icon="Check"
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -211,28 +204,16 @@ onMounted(loadOrders)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 驳回弹窗 -->
|
<!-- 驳回弹窗 -->
|
||||||
<el-dialog
|
<el-dialog v-model="rejectVisible" title="驳回退款" width="420px">
|
||||||
v-model="rejectVisible"
|
|
||||||
title="驳回退款"
|
|
||||||
width="420px"
|
|
||||||
>
|
|
||||||
<div v-if="rejectOrder" class="reject-dialog">
|
<div v-if="rejectOrder" class="reject-dialog">
|
||||||
<p class="reject-tip">请选择驳回方式:</p>
|
<p class="reject-tip">请选择驳回方式:</p>
|
||||||
<div class="reject-options">
|
<div class="reject-options">
|
||||||
<el-button
|
<el-button type="primary" :loading="submitting" @click="handleRejectRestore">
|
||||||
type="primary"
|
|
||||||
:loading="submitting"
|
|
||||||
@click="handleRejectRestore"
|
|
||||||
>
|
|
||||||
驳回退款(恢复订单)
|
驳回退款(恢复订单)
|
||||||
</el-button>
|
</el-button>
|
||||||
<p class="option-desc">仅驳回退款请求,订单恢复至待交接状态,交接流程可继续</p>
|
<p class="option-desc">仅驳回退款请求,订单恢复至待交接状态,交接流程可继续</p>
|
||||||
|
|
||||||
<el-button
|
<el-button type="danger" :loading="submitting" @click="handleRejectClose">
|
||||||
type="danger"
|
|
||||||
:loading="submitting"
|
|
||||||
@click="handleRejectClose"
|
|
||||||
>
|
|
||||||
驳回退款并关闭订单
|
驳回退款并关闭订单
|
||||||
</el-button>
|
</el-button>
|
||||||
<p class="option-desc">驳回退款且关闭订单,商品下架归档,订单不可再操作</p>
|
<p class="option-desc">驳回退款且关闭订单,商品下架归档,订单不可再操作</p>
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ const listingGroupWelcomeConfig = computed(
|
|||||||
() => configs.value.find(item => item.key === 'chat.listing_group_welcome') || null
|
() => configs.value.find(item => item.key === 'chat.listing_group_welcome') || null
|
||||||
)
|
)
|
||||||
const renterRetentionConfig = computed(
|
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(() =>
|
const paddleOcrTokenConfig = computed(() =>
|
||||||
systemConfigByKey(
|
systemConfigByKey(
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ export async function fetchUnreadNotificationCount() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function markAllNotificationsRead() {
|
export async function markAllNotificationsRead() {
|
||||||
const { data } = await apiClient.put<ApiResponse<{ read_count: number }>>(
|
const { data } =
|
||||||
'/notifications/read-all'
|
await apiClient.put<ApiResponse<{ read_count: number }>>('/notifications/read-all')
|
||||||
)
|
|
||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,7 +167,9 @@ async function handleRegister() {
|
|||||||
}
|
}
|
||||||
const modeEyebrow = computed(() => (topTab.value === 'register' ? 'Register' : 'Login'))
|
const modeEyebrow = computed(() => (topTab.value === 'register' ? 'Register' : 'Login'))
|
||||||
const modeTitle = computed(() => (topTab.value === 'register' ? '创建账号' : '欢迎回来'))
|
const modeTitle = computed(() => (topTab.value === 'register' ? '创建账号' : '欢迎回来'))
|
||||||
const modeSubtitle = computed(() => (topTab.value === 'register' ? '注册后即可发布或租赁账号' : '登录后即可发布或租赁账号'))
|
const modeSubtitle = computed(() =>
|
||||||
|
topTab.value === 'register' ? '注册后即可发布或租赁账号' : '登录后即可发布或租赁账号'
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -198,14 +200,10 @@ const modeSubtitle = computed(() => (topTab.value === 'register' ? '注册后即
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="login-tabs">
|
<div class="login-tabs">
|
||||||
<button
|
<button :class="{ active: topTab === 'login' }" @click="switchTab('login')">登录</button>
|
||||||
:class="{ active: topTab === 'login' }"
|
<button :class="{ active: topTab === 'register' }" @click="switchTab('register')">
|
||||||
@click="switchTab('login')"
|
注册
|
||||||
>登录</button>
|
</button>
|
||||||
<button
|
|
||||||
:class="{ active: topTab === 'register' }"
|
|
||||||
@click="switchTab('register')"
|
|
||||||
>注册</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 登录模式下的方式切换 -->
|
<!-- 登录模式下的方式切换 -->
|
||||||
@@ -213,23 +211,48 @@ const modeSubtitle = computed(() => (topTab.value === 'register' ? '注册后即
|
|||||||
<button
|
<button
|
||||||
:class="{ active: loginMode === 'password' }"
|
:class="{ active: loginMode === 'password' }"
|
||||||
@click="switchLoginMode('password')"
|
@click="switchLoginMode('password')"
|
||||||
>密码登录</button>
|
>
|
||||||
|
密码登录
|
||||||
|
</button>
|
||||||
<span class="sub-divider">|</span>
|
<span class="sub-divider">|</span>
|
||||||
<button
|
<button :class="{ active: loginMode === 'sms' }" @click="switchLoginMode('sms')">
|
||||||
:class="{ active: loginMode === 'sms' }"
|
短信登录
|
||||||
@click="switchLoginMode('sms')"
|
</button>
|
||||||
>短信登录</button>
|
|
||||||
</div>
|
</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-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>
|
||||||
<el-form-item label="图形验证码">
|
<el-form-item label="图形验证码">
|
||||||
<div class="user-captcha-row">
|
<div class="user-captcha-row">
|
||||||
<el-input v-model="form.captchaCode" maxlength="4" placeholder="请输入图形验证码" size="large" :prefix-icon="Key" @keyup.enter="handleSendCode" />
|
<el-input
|
||||||
<button class="user-captcha-image" type="button" :disabled="captchaLoading" aria-label="刷新图形验证码" @click="refreshCaptcha">
|
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="图形验证码" />
|
<img v-if="captcha" :src="captcha.image" alt="图形验证码" />
|
||||||
<span v-else>刷新</span>
|
<span v-else>刷新</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -237,37 +260,107 @@ const modeSubtitle = computed(() => (topTab.value === 'register' ? '注册后即
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="验证码">
|
<el-form-item label="验证码">
|
||||||
<div class="user-code-row">
|
<div class="user-code-row">
|
||||||
<el-input v-model="form.code" maxlength="6" placeholder="6 位验证码" size="large" :prefix-icon="ChatLineRound" @keyup.enter="handleLogin" />
|
<el-input
|
||||||
<el-button size="large" :disabled="countDown > 0 || sending" :loading="sending" @click="handleSendCode">
|
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 ? '发送中' : '发送验证码' }}
|
{{ countDown > 0 ? `${countDown}s` : sending ? '发送中' : '发送验证码' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</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>
|
<p class="login-notice">未收到验证码时,请稍后重试或联系客服处理</p>
|
||||||
</el-form>
|
</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-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>
|
||||||
<el-form-item label="密码">
|
<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-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>
|
<p class="login-notice">未设置密码?请使用短信登录后前往个人中心设置密码</p>
|
||||||
</el-form>
|
</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-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>
|
||||||
<el-form-item label="图形验证码">
|
<el-form-item label="图形验证码">
|
||||||
<div class="user-captcha-row">
|
<div class="user-captcha-row">
|
||||||
<el-input v-model="form.captchaCode" maxlength="4" placeholder="请输入图形验证码" size="large" :prefix-icon="Key" @keyup.enter="handleSendCode" />
|
<el-input
|
||||||
<button class="user-captcha-image" type="button" :disabled="captchaLoading" aria-label="刷新图形验证码" @click="refreshCaptcha">
|
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="图形验证码" />
|
<img v-if="captcha" :src="captcha.image" alt="图形验证码" />
|
||||||
<span v-else>刷新</span>
|
<span v-else>刷新</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -275,19 +368,62 @@ const modeSubtitle = computed(() => (topTab.value === 'register' ? '注册后即
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="验证码">
|
<el-form-item label="验证码">
|
||||||
<div class="user-code-row">
|
<div class="user-code-row">
|
||||||
<el-input v-model="form.code" maxlength="6" placeholder="6 位验证码" size="large" :prefix-icon="ChatLineRound" @keyup.enter="handleRegister" />
|
<el-input
|
||||||
<el-button size="large" :disabled="countDown > 0 || sending" :loading="sending" @click="handleSendCode">
|
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 ? '发送中' : '发送验证码' }}
|
{{ countDown > 0 ? `${countDown}s` : sending ? '发送中' : '发送验证码' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="设置密码" :error="form.password && !passwordValid ? '密码长度 8-20 位' : ''">
|
<el-form-item
|
||||||
<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 }" />
|
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>
|
||||||
<el-form-item label="确认密码" :error="form.confirmPassword && !confirmValid ? '两次密码不一致' : ''">
|
<el-form-item
|
||||||
<el-input v-model="form.confirmPassword" type="password" maxlength="20" placeholder="请再次输入密码" size="large" :prefix-icon="Lock" show-password :class="{ 'is-error': form.confirmPassword && !confirmValid }" />
|
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-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>
|
<p class="login-notice">注册即表示同意《用户协议》和《隐私政策》</p>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -174,21 +174,25 @@ async function handlePasswordLogin() {
|
|||||||
<!-- 密码登录 -->
|
<!-- 密码登录 -->
|
||||||
<template v-if="loginMode === 'password'">
|
<template v-if="loginMode === 'password'">
|
||||||
<label class="auth-input-row">
|
<label class="auth-input-row">
|
||||||
<input
|
<input v-model="form.password" type="password" maxlength="20" placeholder="密码" />
|
||||||
v-model="form.password"
|
|
||||||
type="password"
|
|
||||||
maxlength="20"
|
|
||||||
placeholder="密码"
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="assist-row">
|
<div class="assist-row">
|
||||||
<span v-if="loginMode === 'sms'">验证码登录</span>
|
<span v-if="loginMode === 'sms'">验证码登录</span>
|
||||||
<span v-else>密码登录</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>
|
<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>
|
||||||
|
|
||||||
<div class="agreement-row">
|
<div class="agreement-row">
|
||||||
|
|||||||
@@ -129,7 +129,13 @@ function typeLabel(type: string) {
|
|||||||
>
|
>
|
||||||
查看详情
|
查看详情
|
||||||
</van-button>
|
</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>
|
</van-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -309,7 +309,9 @@ async function savePassword() {
|
|||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="showPasswordDialog = false">取消</el-button>
|
<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>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -206,11 +206,17 @@ function handleTabChange(tab: string) {
|
|||||||
<span class="tab-label">
|
<span class="tab-label">
|
||||||
<el-icon><ChatDotRound /></el-icon>
|
<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>
|
</span>
|
||||||
</template>
|
</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">
|
<div v-else-if="!loading && conversations.length === 0" class="empty-panel">
|
||||||
<el-empty description="暂无会话">
|
<el-empty description="暂无会话">
|
||||||
@@ -265,7 +271,9 @@ function handleTabChange(tab: string) {
|
|||||||
<span class="tab-label">
|
<span class="tab-label">
|
||||||
<el-icon><Bell /></el-icon>
|
<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>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -280,10 +288,15 @@ function handleTabChange(tab: string) {
|
|||||||
:loading="notiSubmitting"
|
:loading="notiSubmitting"
|
||||||
:disabled="notificationUnreadCount === 0"
|
:disabled="notificationUnreadCount === 0"
|
||||||
@click="markAllNotiRead"
|
@click="markAllNotiRead"
|
||||||
>全部已读</el-button>
|
>全部已读</el-button
|
||||||
|
>
|
||||||
</div>
|
</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">
|
<div v-else-if="!notiLoading && notifications.length === 0" class="empty-panel">
|
||||||
<el-empty description="暂无站内信" />
|
<el-empty description="暂无站内信" />
|
||||||
@@ -307,13 +320,15 @@ function handleTabChange(tab: string) {
|
|||||||
v-if="item.biz_type === 'order' && item.biz_id"
|
v-if="item.biz_type === 'order' && item.biz_id"
|
||||||
size="small"
|
size="small"
|
||||||
@click="router.push(`/orders/${item.biz_id}`)"
|
@click="router.push(`/orders/${item.biz_id}`)"
|
||||||
>查看订单</el-button>
|
>查看订单</el-button
|
||||||
|
>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="!item.read_at"
|
v-if="!item.read_at"
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="markNotiRead(item)"
|
@click="markNotiRead(item)"
|
||||||
>已读</el-button>
|
>已读</el-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { computed } from 'vue'
|
|||||||
interface Props {
|
interface Props {
|
||||||
label: string
|
label: string
|
||||||
placeholder: string
|
placeholder: string
|
||||||
modelValue: string[]
|
modelValue?: string[]
|
||||||
options: string[]
|
options: string[]
|
||||||
popoverProps: Record<string, any>
|
popoverProps: Record<string, any>
|
||||||
wide?: boolean
|
wide?: boolean
|
||||||
@@ -72,7 +72,11 @@ function toggleOption(value: string) {
|
|||||||
:class="{ active: isSelected(item) }"
|
:class="{ active: isSelected(item) }"
|
||||||
@click="toggleOption(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 }}
|
{{ item }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,7 +52,14 @@ export function readQueryNumber(query: LocationQuery | LocationQueryRaw, key: st
|
|||||||
export function readQueryCSV(query: LocationQuery | LocationQueryRaw, key: string): string[] {
|
export function readQueryCSV(query: LocationQuery | LocationQueryRaw, key: string): string[] {
|
||||||
const raw = readQueryString(query, key)
|
const raw = readQueryString(query, key)
|
||||||
if (!raw) return []
|
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 中省略。 */
|
/** 将多选数组序列化为 CSV;空数组返回 undefined 以便从 query 中省略。 */
|
||||||
|
|||||||
@@ -134,11 +134,7 @@ export function useHomeFilters(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setStringFilter(key: StringFilterKey, value: string | string[]) {
|
function setStringFilter(key: StringFilterKey, value: string | string[]) {
|
||||||
filters[key] = Array.isArray(value)
|
filters[key] = Array.isArray(value) ? uniqueOptions(value) : value.trim() ? [value.trim()] : []
|
||||||
? uniqueOptions(value)
|
|
||||||
: value.trim()
|
|
||||||
? [value.trim()]
|
|
||||||
: []
|
|
||||||
closeFilterPopover()
|
closeFilterPopover()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,10 +49,7 @@ import {
|
|||||||
hasAcceleratedSaleRatio,
|
hasAcceleratedSaleRatio,
|
||||||
hasGiftResources,
|
hasGiftResources,
|
||||||
} from '@/shared/utils/listingDisplay'
|
} from '@/shared/utils/listingDisplay'
|
||||||
import {
|
import { buildMobileHomeListSignature, useMobileHomeCacheStore } from '@/stores/mobileHomeCache'
|
||||||
buildMobileHomeListSignature,
|
|
||||||
useMobileHomeCacheStore,
|
|
||||||
} from '@/stores/mobileHomeCache'
|
|
||||||
import { useSessionStore } from '@/stores/session'
|
import { useSessionStore } from '@/stores/session'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -85,7 +82,6 @@ const mobilePageSize = 10
|
|||||||
let listingRequestSeq = 0
|
let listingRequestSeq = 0
|
||||||
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
|
||||||
const sortOptions = [
|
const sortOptions = [
|
||||||
{ key: 'recommended', label: '综合推荐' },
|
{ key: 'recommended', label: '综合推荐' },
|
||||||
{ key: 'coinDesc', label: '哈夫币' },
|
{ key: 'coinDesc', label: '哈夫币' },
|
||||||
@@ -734,254 +730,259 @@ syncMobileHomeQuery()
|
|||||||
<template>
|
<template>
|
||||||
<main class="mobile-shell">
|
<main class="mobile-shell">
|
||||||
<div ref="scrollEl" class="mobile-scroll" @scroll.passive="handleListScroll">
|
<div ref="scrollEl" class="mobile-scroll" @scroll.passive="handleListScroll">
|
||||||
<!-- ========== Hero 区域:顶部搜索与公告 ========== -->
|
<!-- ========== Hero 区域:顶部搜索与公告 ========== -->
|
||||||
<section class="mobile-hero">
|
<section class="mobile-hero">
|
||||||
<div class="mobile-topbar">
|
<div class="mobile-topbar">
|
||||||
<div class="mobile-brand">
|
<div class="mobile-brand">
|
||||||
<span class="mobile-logo">锤</span>
|
<span class="mobile-logo">锤</span>
|
||||||
<div>
|
<div>
|
||||||
<strong>大锤商行</strong>
|
<strong>大锤商行</strong>
|
||||||
<small>哈夫币租号</small>
|
<small>哈夫币租号</small>
|
||||||
|
</div>
|
||||||
</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
|
<button
|
||||||
v-for="zone in visibleZoneOptions"
|
class="mobile-service"
|
||||||
:key="zone.key"
|
|
||||||
type="button"
|
type="button"
|
||||||
class="zone-pill"
|
:disabled="supportLoading"
|
||||||
:class="{ active: activeZone === zone.key }"
|
@click="handleSupportClick"
|
||||||
@click="selectZone(zone.key)"
|
|
||||||
>
|
>
|
||||||
<strong>{{ zone.label }}</strong>
|
{{ supportLoading ? '接入中' : '客服' }}
|
||||||
<span>{{ zone.count }}</span>
|
|
||||||
<small>{{ zone.hint }}</small>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<RouterLink
|
||||||
<div class="list-toolbar">
|
class="add-home-strip"
|
||||||
<button type="button" class="sort-entry" @click="toggleSortPanel">
|
to="/m/add-home-guide"
|
||||||
<span>{{ activeSortLabel }}</span>
|
aria-label="查看添加到手机桌面教程"
|
||||||
<van-icon :name="sortOpen ? 'arrow-up' : 'arrow-down'" :size="14" />
|
>
|
||||||
</button>
|
<span class="add-home-mark">
|
||||||
<button type="button" class="filter-entry" @click="openFilters">
|
<van-icon name="home-o" :size="18" />
|
||||||
<van-icon name="filter-o" :size="16" />
|
</span>
|
||||||
<span>筛选</span>
|
<span class="add-home-copy">
|
||||||
<em v-if="activeFilterCount">{{ activeFilterCount }}</em>
|
<strong>添加到手机桌面</strong>
|
||||||
</button>
|
<small>下次像 App 一样快速打开</small>
|
||||||
</div>
|
</span>
|
||||||
<div v-if="sortOpen" class="sort-panel">
|
<span class="add-home-cta">去添加</span>
|
||||||
<button
|
</RouterLink>
|
||||||
v-for="option in sortOptions"
|
<van-search
|
||||||
:key="option.key"
|
v-model="searchValue"
|
||||||
type="button"
|
shape="round"
|
||||||
:class="{ active: activeSort === option.key }"
|
placeholder="搜编号 / 区服 / 段位"
|
||||||
@click="selectSort(option.key)"
|
class="home-search"
|
||||||
>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 列表卡片:全宽上下布局 -->
|
<!-- 防骗提示卡片(不用 van-notice-bar) -->
|
||||||
<div class="mobile-list">
|
<RouterLink class="fraud-tip" to="/m/announcements">
|
||||||
<RouterLink
|
<van-icon name="warning-o" :size="16" color="#b8860b" />
|
||||||
v-for="item in displayListings"
|
<span class="fraud-dot"></span>
|
||||||
:key="item.id"
|
<van-swipe
|
||||||
class="mobile-card"
|
class="announcement-swipe"
|
||||||
:to="`/m/listings/${item.id}`"
|
vertical
|
||||||
|
:autoplay="3200"
|
||||||
|
:show-indicators="false"
|
||||||
|
touchable
|
||||||
>
|
>
|
||||||
<div class="card-cover">
|
<van-swipe-item v-for="item in announcements" :key="item">
|
||||||
<img
|
<span>{{ item }}</span>
|
||||||
v-if="item.cover_url"
|
</van-swipe-item>
|
||||||
:src="item.cover_url"
|
</van-swipe>
|
||||||
:alt="getListingTitle(item)"
|
<van-icon class="fraud-more" name="arrow" :size="14" />
|
||||||
loading="lazy"
|
</RouterLink>
|
||||||
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>
|
</section>
|
||||||
</van-pull-refresh>
|
|
||||||
|
|
||||||
<footer class="home-footer">
|
<!-- ========== Content 区域 ========== -->
|
||||||
<a
|
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||||
href="https://beian.miit.gov.cn/"
|
<section class="mobile-content">
|
||||||
target="_blank"
|
<!-- Banner 轮播 -->
|
||||||
rel="noopener noreferrer"
|
<van-swipe class="banner-swipe" :autoplay="3600" lazy-render>
|
||||||
class="home-footer-icp"
|
<van-swipe-item v-for="slide in bannerSlides" :key="slide.title || slide.image_url">
|
||||||
>湘ICP备2025146668号</a>
|
<div
|
||||||
<span class="home-footer-divider">|</span>
|
class="mobile-banner"
|
||||||
<span class="home-footer-copyright">版权所有 ©2026 大锤网络游戏有限公司</span>
|
:class="[`tone-${slide.tone}`, { 'has-image': slide.image_url }]"
|
||||||
</footer>
|
>
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<MobileHomeFilterSheet
|
<MobileHomeFilterSheet
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ import { useRouter, useRoute } from 'vue-router'
|
|||||||
import { showToast } from 'vant'
|
import { showToast } from 'vant'
|
||||||
import MobileBottomNav from '@/components/MobileBottomNav.vue'
|
import MobileBottomNav from '@/components/MobileBottomNav.vue'
|
||||||
|
|
||||||
import {
|
import { fetchOrders, startOrderPayment, type Order } from '@/features/orders/api/orders'
|
||||||
fetchOrders,
|
|
||||||
startOrderPayment,
|
|
||||||
type Order,
|
|
||||||
} from '@/features/orders/api/orders'
|
|
||||||
import MobilePaymentCashierPopup from '@/features/orders/components/MobilePaymentCashierPopup.vue'
|
import MobilePaymentCashierPopup from '@/features/orders/components/MobilePaymentCashierPopup.vue'
|
||||||
import MobilePayWaySelectPopup from '@/features/orders/components/MobilePayWaySelectPopup.vue'
|
import MobilePayWaySelectPopup from '@/features/orders/components/MobilePayWaySelectPopup.vue'
|
||||||
import { useMobilePaymentCashier } from '@/features/orders/composables/useMobilePaymentCashier'
|
import { useMobilePaymentCashier } from '@/features/orders/composables/useMobilePaymentCashier'
|
||||||
|
|||||||
@@ -456,7 +456,11 @@ watch([() => route.query.focus, order, loading], () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="handoff-workspace">
|
<div class="handoff-workspace">
|
||||||
<OrderHandoffTimeline v-if="order" :records="handoffRecords" :listing-code="listingCode" />
|
<OrderHandoffTimeline
|
||||||
|
v-if="order"
|
||||||
|
:records="handoffRecords"
|
||||||
|
:listing-code="listingCode"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OrderCheckoutSummary
|
<OrderCheckoutSummary
|
||||||
@@ -608,7 +612,6 @@ watch([() => route.query.focus, order, loading], () => {
|
|||||||
</el-button>
|
</el-button>
|
||||||
<p class="sidebar-hint">不同意修正时需填写原因,将进入争议处理</p>
|
<p class="sidebar-hint">不同意修正时需填写原因,将进入争议处理</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -625,7 +628,11 @@ watch([() => route.query.focus, order, loading], () => {
|
|||||||
<el-icon><Service /></el-icon>
|
<el-icon><Service /></el-icon>
|
||||||
</span>
|
</span>
|
||||||
<p>
|
<p>
|
||||||
{{ isCheckoutDisputeStage ? '提交后客服会核查结账金额和双方证据。' : '请尽量写清楚时间点、问题经过和证据,便于客服快速处理。' }}
|
{{
|
||||||
|
isCheckoutDisputeStage
|
||||||
|
? '提交后客服会核查结账金额和双方证据。'
|
||||||
|
: '请尽量写清楚时间点、问题经过和证据,便于客服快速处理。'
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<el-select
|
<el-select
|
||||||
|
|||||||
@@ -867,8 +867,6 @@
|
|||||||
background: #ff6a00 !important;
|
background: #ff6a00 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 370px) {
|
@media (max-width: 370px) {
|
||||||
.level-options,
|
.level-options,
|
||||||
.region-grid {
|
.region-grid {
|
||||||
|
|||||||
@@ -230,7 +230,9 @@ function isPendingReview(row: Listing) {
|
|||||||
|
|
||||||
<!-- 商品列表 -->
|
<!-- 商品列表 -->
|
||||||
<section class="listing-list">
|
<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">
|
<div v-else-if="displayListings.length === 0" class="empty-state-wrap">
|
||||||
<van-empty description="暂无相关商品" image="search" />
|
<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="wechat" :disabled="!!editingAccount">微信</van-radio>
|
||||||
<van-radio name="bank" :disabled="!!editingAccount">银行卡</van-radio>
|
<van-radio name="bank" :disabled="!!editingAccount">银行卡</van-radio>
|
||||||
</van-radio-group>
|
</van-radio-group>
|
||||||
<p v-if="!editingAccount" class="type-tip">
|
<p v-if="!editingAccount" class="type-tip">推荐优先添加支付宝,微信和银行卡可按需选择。</p>
|
||||||
推荐优先添加支付宝,微信和银行卡可按需选择。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<van-field
|
<van-field
|
||||||
v-model.trim="accountForm.account_name"
|
v-model.trim="accountForm.account_name"
|
||||||
|
|||||||
@@ -258,10 +258,8 @@ const hasChatPermission = computed(() => adminSession.hasPermission('chat:view')
|
|||||||
const hasNotificationPermission = computed(
|
const hasNotificationPermission = computed(
|
||||||
() => adminSession.isSuperAdmin || adminSession.hasPermission('notification:view')
|
() => adminSession.isSuperAdmin || adminSession.hasPermission('notification:view')
|
||||||
)
|
)
|
||||||
const {
|
const { unreadCount: adminNotificationUnreadCount, unreadLabel: adminNotificationUnreadLabel } =
|
||||||
unreadCount: adminNotificationUnreadCount,
|
useAdminNotificationUnreadCount(route)
|
||||||
unreadLabel: adminNotificationUnreadLabel,
|
|
||||||
} = useAdminNotificationUnreadCount(route)
|
|
||||||
|
|
||||||
const statusLabels: Record<string, string> = {
|
const statusLabels: Record<string, string> = {
|
||||||
online: '在线',
|
online: '在线',
|
||||||
|
|||||||
@@ -136,7 +136,9 @@ async function handleSupportClick() {
|
|||||||
<RouterLink v-for="item in navItems" :key="item.to" class="pc-nav-link" :to="item.to">
|
<RouterLink v-for="item in navItems" :key="item.to" class="pc-nav-link" :to="item.to">
|
||||||
<el-icon><component :is="item.icon" /></el-icon>
|
<el-icon><component :is="item.icon" /></el-icon>
|
||||||
<span>{{ item.label }}</span>
|
<span>{{ item.label }}</span>
|
||||||
<em v-if="item.to === '/messages' && unreadCount > 0" class="pc-nav-badge">{{ unreadLabel }}</em>
|
<em v-if="item.to === '/messages' && unreadCount > 0" class="pc-nav-badge">{{
|
||||||
|
unreadLabel
|
||||||
|
}}</em>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -320,7 +322,8 @@ async function handleSupportClick() {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="pc-footer-icp"
|
class="pc-footer-icp"
|
||||||
>湘ICP备2025146668号</a>
|
>湘ICP备2025146668号</a
|
||||||
|
>
|
||||||
<span class="pc-footer-divider">|</span>
|
<span class="pc-footer-divider">|</span>
|
||||||
<span class="pc-footer-copyright">版权所有 ©2026 大锤网络游戏有限公司</span>
|
<span class="pc-footer-copyright">版权所有 ©2026 大锤网络游戏有限公司</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ export const AUDIT_ACTION_OPTIONS: AuditOption[] = [
|
|||||||
// 用户
|
// 用户
|
||||||
{ value: 'admin_user.freeze', label: '冻结用户', group: '用户', highRisk: true },
|
{ value: 'admin_user.freeze', label: '冻结用户', group: '用户', highRisk: true },
|
||||||
{ value: 'admin_user.unfreeze', label: '解冻用户', group: '用户' },
|
{ value: 'admin_user.unfreeze', label: '解冻用户', group: '用户' },
|
||||||
{ value: 'admin_user.set_deposit_free_quota', label: '设置免押额度', group: '用户', highRisk: true },
|
{
|
||||||
|
value: 'admin_user.set_deposit_free_quota',
|
||||||
|
label: '设置免押额度',
|
||||||
|
group: '用户',
|
||||||
|
highRisk: true,
|
||||||
|
},
|
||||||
{ value: 'admin_user.manual_realname', label: '人工实名', group: '用户', highRisk: true },
|
{ value: 'admin_user.manual_realname', label: '人工实名', group: '用户', highRisk: true },
|
||||||
{ value: 'admin_user.revoke_realname', label: '撤销实名', group: '用户', highRisk: true },
|
{ value: 'admin_user.revoke_realname', label: '撤销实名', group: '用户', highRisk: true },
|
||||||
{ value: 'admin_user.wallet_adjust', label: '调整用户余额', group: '用户', highRisk: true },
|
{ value: 'admin_user.wallet_adjust', label: '调整用户余额', group: '用户', highRisk: true },
|
||||||
@@ -67,9 +72,15 @@ export const AUDIT_BIZ_TYPE_OPTIONS: AuditOption[] = [
|
|||||||
{ value: 'admin_pickup', label: '提号' },
|
{ value: 'admin_pickup', label: '提号' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const actionLabelMap = Object.fromEntries(AUDIT_ACTION_OPTIONS.map(item => [item.value, item.label]))
|
const actionLabelMap = Object.fromEntries(
|
||||||
const bizTypeLabelMap = Object.fromEntries(AUDIT_BIZ_TYPE_OPTIONS.map(item => [item.value, item.label]))
|
AUDIT_ACTION_OPTIONS.map(item => [item.value, item.label])
|
||||||
const highRiskActions = new Set(AUDIT_ACTION_OPTIONS.filter(item => item.highRisk).map(item => item.value))
|
)
|
||||||
|
const bizTypeLabelMap = Object.fromEntries(
|
||||||
|
AUDIT_BIZ_TYPE_OPTIONS.map(item => [item.value, item.label])
|
||||||
|
)
|
||||||
|
const highRiskActions = new Set(
|
||||||
|
AUDIT_ACTION_OPTIONS.filter(item => item.highRisk).map(item => item.value)
|
||||||
|
)
|
||||||
|
|
||||||
/** 动作 code → 中文;未知 code 原样返回 */
|
/** 动作 code → 中文;未知 code 原样返回 */
|
||||||
export function auditActionLabel(action: string | undefined | null): string {
|
export function auditActionLabel(action: string | undefined | null): string {
|
||||||
|
|||||||
@@ -161,7 +161,9 @@ export function readFinalSaleRatio(
|
|||||||
|
|
||||||
export function hasAcceleratedSaleRatioInput(value: number | '' | null | undefined) {
|
export function hasAcceleratedSaleRatioInput(value: number | '' | null | undefined) {
|
||||||
const ratio = Number(value)
|
const ratio = Number(value)
|
||||||
return value !== '' && value !== null && value !== undefined && Number.isFinite(ratio) && ratio > 0
|
return (
|
||||||
|
value !== '' && value !== null && value !== undefined && Number.isFinite(ratio) && ratio > 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculatePlatformPricing(options: {
|
export function calculatePlatformPricing(options: {
|
||||||
|
|||||||
@@ -58,9 +58,7 @@ export const systemConfigSelectOptions: Record<string, SystemConfigOption[]> = {
|
|||||||
{ label: '5 天', value: '5' },
|
{ label: '5 天', value: '5' },
|
||||||
{ label: '7 天', value: '7' },
|
{ label: '7 天', value: '7' },
|
||||||
],
|
],
|
||||||
'integration.paddle_ocr_model': [
|
'integration.paddle_ocr_model': [{ label: 'PaddleOCR-VL-1.6', value: 'PaddleOCR-VL-1.6' }],
|
||||||
{ label: 'PaddleOCR-VL-1.6', value: 'PaddleOCR-VL-1.6' },
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSystemConfigSelectOptions(key: string) {
|
export function getSystemConfigSelectOptions(key: string) {
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ a {
|
|||||||
* 组件内若需更小视觉字号,请用 transform: scale() 等方式,不要直接用 <16px 的 font-size。
|
* 组件内若需更小视觉字号,请用 transform: scale() 等方式,不要直接用 <16px 的 font-size。
|
||||||
*/
|
*/
|
||||||
@media (hover: none) and (pointer: coarse) {
|
@media (hover: none) and (pointer: coarse) {
|
||||||
input:not([type='checkbox']):not([type='radio']):not([type='range']):not([type='file']):not([type='button']):not([type='submit']):not([type='reset']):not([type='image']),
|
input:not([type='checkbox']):not([type='radio']):not([type='range']):not([type='file']):not(
|
||||||
|
[type='button']
|
||||||
|
):not([type='submit']):not([type='reset']):not([type='image']),
|
||||||
textarea,
|
textarea,
|
||||||
select,
|
select,
|
||||||
.van-field__control {
|
.van-field__control {
|
||||||
|
|||||||
Reference in New Issue
Block a user