合并 main 分支并融合订单列表最小化与 SQL 治理
- Auth/权限中间件取 main 演进版(错误细分、token 来源),保留 dev 的用户 cookie 与登出撤销语义 - 后台订单列表:融合 main 的首页免 COUNT 优化与关键词索引路由,保留 dev 的最小化 AdminOrderListItemDTO(结账明细仍由详情接口返回,同步调整对应集成测试) - 保留 dev 的 token_version 冻结/解冻递增与 main 的状态变化判断及通知 - 前端:保留 dev 登出接口与 main 的 fetchMe silent 参数、SSE 登录跳转;后台订单表按 dev 最小化 DTO 移除号主到手列
This commit is contained in:
@@ -17,8 +17,10 @@ export function defaultPublishForm(): PublishForm {
|
||||
login_method: '',
|
||||
online_start: '',
|
||||
online_end: '',
|
||||
early_morning_response: '否',
|
||||
can_change_game_name: '',
|
||||
all_hero: '',
|
||||
next_season_pass: '',
|
||||
ban_record: '',
|
||||
common_regions: [],
|
||||
depositAmountYuan: '',
|
||||
|
||||
@@ -79,6 +79,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
const submitButtonText = computed(() => (isEditMode.value ? '提交修改' : '立即发布'))
|
||||
const submitLoadingText = computed(() => (isEditMode.value ? '提交中...' : '发布中...'))
|
||||
const showBanRecordRiskHint = false
|
||||
const nextSeasonPassEnabled = computed(() => publishOptions.value.next_season_pass_enabled)
|
||||
|
||||
const pricing = usePricingCalculator({
|
||||
form,
|
||||
@@ -308,8 +309,10 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
login_method: listing.login_platform || '',
|
||||
online_start: stringValue(onlineTime.start),
|
||||
online_end: stringValue(onlineTime.end),
|
||||
early_morning_response: stringValue(summary.early_morning_response) || '否',
|
||||
can_change_game_name: stringValue(summary.can_change_game_name),
|
||||
all_hero: stringValue(summary.all_hero),
|
||||
next_season_pass: stringValue(summary.next_season_pass),
|
||||
ban_record: stringValue(summary.ban_record),
|
||||
common_regions: stringArray(summary.common_regions),
|
||||
depositAmountYuan: listing.deposit_amount_cent > 0 ? listing.deposit_amount_cent / 100 : '',
|
||||
@@ -700,6 +703,9 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
if (!dailyLossOptions.includes(pricing.dailyLossMAmount.value)) return '请选择每日损耗'
|
||||
const onlineTimeError = validateOnlineTime()
|
||||
if (onlineTimeError) return onlineTimeError
|
||||
if (form.early_morning_response !== '是' && form.early_morning_response !== '否') {
|
||||
return '请选择凌晨0-2点是否可响应上号'
|
||||
}
|
||||
if (pricing.hasAcceleratedSaleRatioInput()) {
|
||||
const ratio = Number(form.accelerated_sale_ratio)
|
||||
if (!Number.isFinite(ratio) || ratio <= 0) return '加速出售比例格式不正确'
|
||||
@@ -719,6 +725,9 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
if (pricing.banRecordOptions.value.length && !form.ban_record) return '请选择封禁记录'
|
||||
if (!form.can_change_game_name) return '请选择是否可以修改游戏名'
|
||||
if (!form.all_hero) return '请选择是否有全英雄'
|
||||
if (nextSeasonPassEnabled.value && !form.next_season_pass) {
|
||||
return '请选择下赛季是否购买通行证'
|
||||
}
|
||||
if (form.depositAmountYuan === '' || Number(form.depositAmountYuan) < 0) return '请填写押金'
|
||||
if (!Number.isFinite(Number(form.depositAmountYuan))) return '押金格式不正确'
|
||||
if (
|
||||
@@ -818,8 +827,10 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
start: form.online_start,
|
||||
end: form.online_end,
|
||||
},
|
||||
early_morning_response: form.early_morning_response,
|
||||
can_change_game_name: form.can_change_game_name,
|
||||
all_hero: form.all_hero,
|
||||
next_season_pass: form.next_season_pass,
|
||||
ban_record: form.ban_record,
|
||||
common_regions: form.common_regions,
|
||||
remark: form.remark,
|
||||
@@ -911,6 +922,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
passwordAndDeviceConfirmed,
|
||||
canPublishAfterAgreements,
|
||||
showBanRecordRiskHint,
|
||||
nextSeasonPassEnabled,
|
||||
isScanOnlyServer,
|
||||
effectiveLoginMethodOptions,
|
||||
fileInput,
|
||||
|
||||
@@ -88,6 +88,10 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.early-morning-response-field :deep(.van-field__label) {
|
||||
width: 128px;
|
||||
}
|
||||
|
||||
.hint-label,
|
||||
.backend-hint-target.has-hint,
|
||||
.upload-title.has-hint {
|
||||
|
||||
@@ -27,6 +27,7 @@ const {
|
||||
passwordAndDeviceConfirmed,
|
||||
canPublishAfterAgreements,
|
||||
showBanRecordRiskHint,
|
||||
nextSeasonPassEnabled,
|
||||
fileInput,
|
||||
activeUploadKey,
|
||||
form,
|
||||
@@ -121,6 +122,8 @@ function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
|
||||
|
||||
const canChangeGameNameOptions = ['是', '否']
|
||||
const allHeroOptions = ['是', '否']
|
||||
const nextSeasonPassOptions = ['购买', '不购买']
|
||||
const earlyMorningResponseOptions = ['是', '否']
|
||||
|
||||
// 在线时间:通栏自定义块 + 底部时间选择,避免 van-field 窄列截断原生 time
|
||||
const onlineTimePickerVisible = ref(false)
|
||||
@@ -439,6 +442,28 @@ const screenshotGuides: Record<string, string> = {
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<van-field
|
||||
v-if="nextSeasonPassEnabled"
|
||||
label="下赛季是否购买通行证"
|
||||
required
|
||||
class="publish-field"
|
||||
>
|
||||
<template #input>
|
||||
<div class="radio-group">
|
||||
<button
|
||||
v-for="opt in nextSeasonPassOptions"
|
||||
:key="opt"
|
||||
type="button"
|
||||
class="radio-btn"
|
||||
:class="{ active: form.next_season_pass === opt }"
|
||||
@click="selectRadio(opt, value => (form.next_season_pass = value))"
|
||||
>
|
||||
{{ opt }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<div class="online-time-block">
|
||||
<div class="online-time-head">
|
||||
<div class="online-time-title">
|
||||
@@ -481,6 +506,27 @@ const screenshotGuides: Record<string, string> = {
|
||||
请填写能百分百联系上您的时间。该时段联系不上可能影响上架或扣款,请预留扫码与冻结人脸时间。
|
||||
</p>
|
||||
|
||||
<van-field
|
||||
label="凌晨0-2点可响应上号"
|
||||
required
|
||||
class="publish-field early-morning-response-field"
|
||||
>
|
||||
<template #input>
|
||||
<div class="radio-group">
|
||||
<button
|
||||
v-for="opt in earlyMorningResponseOptions"
|
||||
:key="opt"
|
||||
type="button"
|
||||
class="radio-btn"
|
||||
:class="{ active: form.early_morning_response === opt }"
|
||||
@click="selectRadio(opt, value => (form.early_morning_response = value))"
|
||||
>
|
||||
{{ opt }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<van-popup v-model:show="onlineTimePickerVisible" position="bottom" round teleport="body">
|
||||
<van-time-picker
|
||||
v-model="onlineTimePickerValue"
|
||||
|
||||
@@ -263,7 +263,9 @@ async function handleOffline() {
|
||||
<h2>{{ getListingTitle(listing) }}</h2>
|
||||
<p class="sub">{{ getListingSubtitle(listing) }}</p>
|
||||
<div class="tag-row">
|
||||
<van-tag plain type="primary" size="medium">{{ getServerRegion(listing) || '—' }}</van-tag>
|
||||
<van-tag plain type="primary" size="medium">{{
|
||||
getServerRegion(listing) || '—'
|
||||
}}</van-tag>
|
||||
<van-tag v-if="getLoginMethod(listing)" plain type="primary" size="medium">
|
||||
{{ getLoginMethod(listing) }}
|
||||
</van-tag>
|
||||
@@ -360,7 +362,9 @@ async function handleOffline() {
|
||||
<div class="bottom-spacer" />
|
||||
|
||||
<footer class="action-bar">
|
||||
<van-button v-if="canEdit(listing)" plain round class="btn" @click="goEdit">编辑</van-button>
|
||||
<van-button v-if="canEdit(listing)" plain round class="btn" @click="goEdit"
|
||||
>编辑</van-button
|
||||
>
|
||||
<van-button
|
||||
v-if="canSubmit(listing)"
|
||||
type="primary"
|
||||
|
||||
@@ -298,13 +298,7 @@ function isPendingReview(row: Listing) {
|
||||
|
||||
<!-- 卡片底:操作 -->
|
||||
<div class="card-footer">
|
||||
<van-button
|
||||
size="small"
|
||||
plain
|
||||
round
|
||||
class="action-btn"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<van-button size="small" plain round class="action-btn" @click="goDetail(item)">
|
||||
详情
|
||||
</van-button>
|
||||
<van-button
|
||||
|
||||
@@ -376,6 +376,10 @@
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.early-morning-response-row {
|
||||
grid-template-columns: 150px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.online-time-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -28,6 +28,7 @@ const {
|
||||
passwordAndDeviceConfirmed,
|
||||
canPublishAfterAgreements,
|
||||
showBanRecordRiskHint,
|
||||
nextSeasonPassEnabled,
|
||||
fileInput,
|
||||
activeUploadKey,
|
||||
form,
|
||||
@@ -120,6 +121,8 @@ const activeAgreementContent = computed(() => activeAgreement.value?.content ||
|
||||
|
||||
const canChangeGameNameOptions = ['是', '否']
|
||||
const allHeroOptions = ['是', '否']
|
||||
const nextSeasonPassOptions = ['购买', '不购买']
|
||||
const earlyMorningResponseOptions = ['是', '否']
|
||||
|
||||
// 截图槽位的指导示例图(仅展示、点击放大,不占用上传数量)
|
||||
const screenshotGuides: Record<string, string> = {
|
||||
@@ -355,6 +358,15 @@ function selectDailyLoss(value: string | number) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="nextSeasonPassEnabled" class="field-row panel-field-row">
|
||||
<label>下赛季是否购买通行证<span>*</span></label>
|
||||
<OptionChips
|
||||
:options="nextSeasonPassOptions"
|
||||
:model-value="form.next_season_pass"
|
||||
@select="form.next_season_pass = String($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field-row panel-field-row online-time-row">
|
||||
<label>在线时间<span>*</span></label>
|
||||
<div class="online-time-body">
|
||||
@@ -396,6 +408,15 @@ function selectDailyLoss(value: string | number) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row panel-field-row early-morning-response-row">
|
||||
<label>凌晨0-2点可响应上号<span>*</span></label>
|
||||
<OptionChips
|
||||
:options="earlyMorningResponseOptions"
|
||||
:model-value="form.early_morning_response"
|
||||
@select="form.early_morning_response = String($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="banRecordOptions.length" class="field-row panel-field-row">
|
||||
<label>封禁记录<span>*</span></label>
|
||||
<OptionChips
|
||||
|
||||
@@ -127,7 +127,10 @@ const accountRows = computed(() => {
|
||||
{ label: '负重等级', value: readAssetString(listing.value, 'load_level') || '--' },
|
||||
{ label: '绝密KD', value: secretKD > 0 ? formatAssetNumber(secretKD) : '--' },
|
||||
{ label: '纯币', value: formatHafCoinM(getCoinWan(listing.value)) },
|
||||
{ label: '日损耗', value: getDailyLoss(listing.value) ? `${getDailyLoss(listing.value)}/天` : '--' },
|
||||
{
|
||||
label: '日损耗',
|
||||
value: getDailyLoss(listing.value) ? `${getDailyLoss(listing.value)}/天` : '--',
|
||||
},
|
||||
{ label: 'M单价', value: formatRatio(listing.value) },
|
||||
{ label: '方便上号', value: getOnlineTimeText(listing.value) || '--' },
|
||||
{ label: '预计可租', value: formatEstimatedRentalDuration(listing.value) },
|
||||
@@ -207,19 +210,37 @@ async function handleOffline() {
|
||||
<section v-loading="loading" class="seller-detail-page">
|
||||
<header class="page-head">
|
||||
<div>
|
||||
<el-button text :icon="ArrowLeft" @click="router.push('/seller/listings')">返回列表</el-button>
|
||||
<el-button text :icon="ArrowLeft" @click="router.push('/seller/listings')"
|
||||
>返回列表</el-button
|
||||
>
|
||||
<h1>我的商品详情</h1>
|
||||
<p>仅本人可查看完整发布信息;他人无法通过此入口访问。</p>
|
||||
</div>
|
||||
<div v-if="listing" class="head-actions">
|
||||
<el-button :icon="CopyDocument" @click="copyCode">复制编号</el-button>
|
||||
<el-button v-if="canEdit(listing)" :icon="Edit" @click="router.push(`/seller/listings/${listing.id}/edit`)">
|
||||
<el-button
|
||||
v-if="canEdit(listing)"
|
||||
:icon="Edit"
|
||||
@click="router.push(`/seller/listings/${listing.id}/edit`)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button v-if="canSubmit(listing)" type="primary" plain :loading="acting" @click="handleSubmit">
|
||||
<el-button
|
||||
v-if="canSubmit(listing)"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="acting"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
提审
|
||||
</el-button>
|
||||
<el-button v-if="canOffline(listing)" type="danger" plain :loading="acting" @click="handleOffline">
|
||||
<el-button
|
||||
v-if="canOffline(listing)"
|
||||
type="danger"
|
||||
plain
|
||||
:loading="acting"
|
||||
@click="handleOffline"
|
||||
>
|
||||
下架
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user