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