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

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
+38
View File
@@ -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)
} }
+12 -4
View File
@@ -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(`![${imageAltText(uploaded.filename)}|w=720](${uploaded.medium_url || uploaded.url})`) insertContentAtCursor(
`![${imageAltText(uploaded.filename)}|w=720](${uploaded.medium_url || uploaded.url})`
)
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 = `![${escapeMarkdownImageText(alt)}${widthMark}](${url})` const nextMarkdown = `![${escapeMarkdownImageText(alt)}${widthMark}](${url})`
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
} }
+172 -36
View File
@@ -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"
+2 -4
View File
@@ -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: '在线',
+5 -2
View File
@@ -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>
+15 -4
View File
@@ -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 {
+3 -1
View File
@@ -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) {
+3 -1
View File
@@ -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 {