详情页面kd 展示,发布时间必填
This commit is contained in:
@@ -28,6 +28,10 @@ func writeListingError(c *gin.Context, err error) {
|
||||
response.BadRequest(c, "哈夫币数量不正确")
|
||||
case errors.Is(err, ErrMissingScreenshot):
|
||||
response.BadRequest(c, "请至少上传一张账号截图")
|
||||
case errors.Is(err, ErrMissingOnlineTime):
|
||||
response.BadRequest(c, "请选择在线时间")
|
||||
case errors.Is(err, ErrInvalidOnlineTime):
|
||||
response.BadRequest(c, "在线时间不正确")
|
||||
case errors.Is(err, ErrAgreementRequired):
|
||||
response.BadRequest(c, "请先阅读并同意发布协议")
|
||||
case errors.Is(err, ErrMissingUploaderName):
|
||||
|
||||
@@ -17,6 +17,8 @@ var (
|
||||
ErrDepositTooLow = errors.New("listing deposit too low")
|
||||
ErrInvalidHafCoin = errors.New("invalid haf coin amount")
|
||||
ErrMissingScreenshot = errors.New("missing screenshot")
|
||||
ErrMissingOnlineTime = errors.New("missing online time")
|
||||
ErrInvalidOnlineTime = errors.New("invalid online time")
|
||||
ErrAgreementRequired = errors.New("listing publish agreement required")
|
||||
ErrMissingUploaderName = errors.New("missing uploader name")
|
||||
ErrMissingUploadData = errors.New("missing upload data")
|
||||
|
||||
@@ -18,6 +18,9 @@ func (s *Service) Create(ctx context.Context, ownerID uint64, req CreateRequest)
|
||||
if err := validateRequest(req, rules); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateRequiredOnlineTime(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reviewRequired, err := s.reviewRequired(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -36,6 +39,9 @@ func (s *Service) Update(ctx context.Context, ownerID uint64, id uint64, req Upd
|
||||
if err := validateRequest(req, rules); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateRequiredOnlineTime(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reviewRequired, err := s.reviewRequired(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -58,6 +58,44 @@ func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRequiredOnlineTime(t *testing.T) {
|
||||
req := CreateRequest{
|
||||
AssetSummary: map[string]any{
|
||||
"online_time": map[string]any{
|
||||
"start": "09:00",
|
||||
"end": "23:00",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := validateRequiredOnlineTime(req); err != nil {
|
||||
t.Fatalf("expected valid online time, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRequiredOnlineTimeRejectsMissing(t *testing.T) {
|
||||
req := CreateRequest{AssetSummary: map[string]any{}}
|
||||
|
||||
if err := validateRequiredOnlineTime(req); err != ErrMissingOnlineTime {
|
||||
t.Fatalf("expected ErrMissingOnlineTime, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRequiredOnlineTimeRejectsInvalidRange(t *testing.T) {
|
||||
req := CreateRequest{
|
||||
AssetSummary: map[string]any{
|
||||
"online_time": map[string]any{
|
||||
"start": "23:00",
|
||||
"end": "09:00",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := validateRequiredOnlineTime(req); err != ErrInvalidOnlineTime {
|
||||
t.Fatalf("expected ErrInvalidOnlineTime, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRequiresPublishAgreements(t *testing.T) {
|
||||
service := NewService(&Repository{}, nil)
|
||||
|
||||
|
||||
@@ -41,6 +41,63 @@ func validateRequest(req CreateRequest, rules publishRules) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRequiredOnlineTime(req CreateRequest) error {
|
||||
start, end, ok := readOnlineTime(req.AssetSummary)
|
||||
if !ok {
|
||||
return ErrMissingOnlineTime
|
||||
}
|
||||
startMinute, ok := parseOnlineMinute(start)
|
||||
if !ok {
|
||||
return ErrInvalidOnlineTime
|
||||
}
|
||||
endMinute, ok := parseOnlineMinute(end)
|
||||
if !ok {
|
||||
return ErrInvalidOnlineTime
|
||||
}
|
||||
if startMinute >= endMinute {
|
||||
return ErrInvalidOnlineTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readOnlineTime(summary map[string]any) (string, string, bool) {
|
||||
if summary == nil {
|
||||
return "", "", false
|
||||
}
|
||||
raw, ok := summary["online_time"]
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
onlineTime, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
start, okStart := onlineTime["start"].(string)
|
||||
end, okEnd := onlineTime["end"].(string)
|
||||
start = strings.TrimSpace(start)
|
||||
end = strings.TrimSpace(end)
|
||||
return start, end, okStart && okEnd && start != "" && end != ""
|
||||
}
|
||||
|
||||
func parseOnlineMinute(value string) (int, bool) {
|
||||
parts := strings.Split(strings.TrimSpace(value), ":")
|
||||
if len(parts) != 2 {
|
||||
return 0, false
|
||||
}
|
||||
hour, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
minute, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
if hour < 0 || hour > 23 || minute < 0 || minute > 59 {
|
||||
return 0, false
|
||||
}
|
||||
return hour*60 + minute, true
|
||||
}
|
||||
|
||||
func consumableValue(summary map[string]any) float64 {
|
||||
if summary == nil {
|
||||
return 0
|
||||
|
||||
@@ -15,6 +15,7 @@ import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||||
import { roundMoney, formatMoney, formatCent } from '@/shared/utils/money'
|
||||
import {
|
||||
assetRegions,
|
||||
formatAssetNumber,
|
||||
formatEstimatedRentalDuration,
|
||||
formatHafCoinM,
|
||||
formatListingCode,
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
getOnlineTimeText,
|
||||
getLoginMethod,
|
||||
getServerRegion,
|
||||
readAssetNumber,
|
||||
readAssetString,
|
||||
} from '@/shared/utils/listingDisplay'
|
||||
|
||||
@@ -95,8 +97,14 @@ const coverURL = computed(() => {
|
||||
const detailMetrics = computed(() => {
|
||||
if (!listing.value) return []
|
||||
const dailyLoss = getDailyLoss(listing.value)
|
||||
const secretKD = readAssetNumber(listing.value, 'secret_kd')
|
||||
return [
|
||||
{ label: '纯币', value: formatHafCoinM(getCoinWan(listing.value)), tone: 'coin' },
|
||||
{
|
||||
label: '绝密KD',
|
||||
value: secretKD > 0 ? formatAssetNumber(secretKD) : '--',
|
||||
tone: 'coin',
|
||||
},
|
||||
{
|
||||
label: '日损耗',
|
||||
value: dailyLoss ? `${dailyLoss}/天` : '--',
|
||||
@@ -164,10 +172,12 @@ const detailSkinGroups = computed(() => {
|
||||
const accountRows = computed(() => {
|
||||
if (!listing.value) return []
|
||||
const regions = assetRegions(listing.value)
|
||||
const secretKD = readAssetNumber(listing.value, 'secret_kd')
|
||||
return [
|
||||
{ label: '所属区服', value: getServerRegion(listing.value) || '--' },
|
||||
{ label: '上号方式', value: getLoginMethod(listing.value) || '--' },
|
||||
{ label: '游戏段位', value: listing.value.rank_level || '--' },
|
||||
{ label: '绝密KD', value: secretKD > 0 ? formatAssetNumber(secretKD) : '--' },
|
||||
{ label: 'M单价', value: formatRatio(listing.value) },
|
||||
{ label: '方便上号', value: getOnlineTimeText(listing.value) || '--' },
|
||||
{ label: '预计可租', value: formatEstimatedRentalDuration(listing.value) },
|
||||
@@ -649,7 +659,7 @@ function listingPrice(item: Listing) {
|
||||
|
||||
.detail-summary-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||||
import { formatCent, formatMoney } from '@/shared/utils/money'
|
||||
import {
|
||||
assetRegions,
|
||||
formatAssetNumber,
|
||||
formatHafCoinM,
|
||||
formatListingCode,
|
||||
formatRatio,
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
getListingTitle,
|
||||
getLoginMethod,
|
||||
getServerRegion,
|
||||
readAssetNumber,
|
||||
readAssetString,
|
||||
} from '@/shared/utils/listingDisplay'
|
||||
|
||||
@@ -88,8 +90,14 @@ const orderPriceBreakdown = computed(() => {
|
||||
const detailMetrics = computed(() => {
|
||||
if (!listing.value) return []
|
||||
const dailyLoss = getDailyLoss(listing.value)
|
||||
const secretKD = readAssetNumber(listing.value, 'secret_kd')
|
||||
return [
|
||||
{ label: '纯币', value: formatHafCoinM(getCoinWan(listing.value)), tone: 'coin' },
|
||||
{
|
||||
label: '绝密KD',
|
||||
value: secretKD > 0 ? formatAssetNumber(secretKD) : '--',
|
||||
tone: 'coin',
|
||||
},
|
||||
{
|
||||
label: '日损耗',
|
||||
value: dailyLoss ? `${dailyLoss}/天` : '--',
|
||||
@@ -345,6 +353,16 @@ async function copyListingCode() {
|
||||
<span class="info-label">M单价</span>
|
||||
<span class="info-text">{{ formatRatio(listing) }}</span>
|
||||
</div>
|
||||
<div class="info-line">
|
||||
<span class="info-label">绝密KD</span>
|
||||
<span class="info-text">
|
||||
{{
|
||||
readAssetNumber(listing, 'secret_kd') > 0
|
||||
? formatAssetNumber(readAssetNumber(listing, 'secret_kd'))
|
||||
: '--'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-line">
|
||||
<span class="info-label">常用登录地</span>
|
||||
<span class="info-text">{{ assetRegions(listing).join('、') || '--' }}</span>
|
||||
@@ -642,7 +660,7 @@ async function copyListingCode() {
|
||||
/* ========== 资产指标 ========== */
|
||||
.metric-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
@@ -650,7 +668,7 @@ async function copyListingCode() {
|
||||
.metric-item {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 10px 6px;
|
||||
padding: 10px 4px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
@@ -357,6 +357,7 @@ function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
|
||||
<van-field
|
||||
v-model="form.online_start"
|
||||
label="在线开始"
|
||||
required
|
||||
type="time"
|
||||
class="publish-field time-field"
|
||||
@update:model-value="handleOnlineStartChange"
|
||||
@@ -390,6 +391,7 @@ function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
|
||||
<van-field
|
||||
v-model="form.online_end"
|
||||
label="在线结束"
|
||||
required
|
||||
type="time"
|
||||
class="publish-field time-field"
|
||||
@update:model-value="handleOnlineEndChange"
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
|
||||
.field-row label span,
|
||||
.input-block b,
|
||||
.time-preset-row strong span,
|
||||
.quantity-meta span,
|
||||
.upload-copy span {
|
||||
color: var(--color-danger);
|
||||
|
||||
@@ -322,7 +322,7 @@ function selectDailyLoss(value: string | number) {
|
||||
|
||||
<div class="time-preset-panel">
|
||||
<div class="time-preset-row">
|
||||
<strong>在线开始</strong>
|
||||
<strong>在线开始<span>*</span></strong>
|
||||
<div class="time-preset-content">
|
||||
<el-time-picker
|
||||
v-model="form.online_start"
|
||||
@@ -352,7 +352,7 @@ function selectDailyLoss(value: string | number) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="time-preset-row">
|
||||
<strong>在线结束</strong>
|
||||
<strong>在线结束<span>*</span></strong>
|
||||
<div class="time-preset-content">
|
||||
<el-time-picker
|
||||
v-model="form.online_end"
|
||||
|
||||
@@ -41,6 +41,11 @@ export function formatHafCoinM(amountWan: number) {
|
||||
return `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)}M`
|
||||
}
|
||||
|
||||
export function formatAssetNumber(value: number) {
|
||||
const rounded = Math.round(value * 10) / 10
|
||||
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)
|
||||
}
|
||||
|
||||
export function getListingDisplayPrice(item: Listing) {
|
||||
return centToYuan(item.price_cent)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user