feat:访问他人展览页面修改

This commit is contained in:
zerosaturation 2026-06-23 22:29:39 +08:00
parent 94457ffa8a
commit 37a3f292df
12 changed files with 111 additions and 44 deletions

Binary file not shown.

View File

@ -91,11 +91,13 @@ func ConvertAssetInfo(pbAsset *pbGallery.AssetInfo) *AssetInfoDTO {
}
return &AssetInfoDTO{
AssetID: pbAsset.AssetId,
Name: pbAsset.Name,
CoverURL: pbAsset.CoverUrl,
LikeCount: pbAsset.LikeCount,
RemainTime: pbAsset.RemainTime,
AssetID: pbAsset.AssetId,
Name: pbAsset.Name,
CoverURL: pbAsset.CoverUrl,
LikeCount: pbAsset.LikeCount,
RemainTime: pbAsset.RemainTime,
Earnings: pbAsset.Earnings,
HourlyEarnings: pbAsset.HourlyEarnings,
}
}

View File

@ -45,11 +45,13 @@ type SlotInfoDTO struct {
// AssetInfoDTO 资产信息(展馆展示用)
type AssetInfoDTO struct {
AssetID int64 `json:"asset_id"` // 资产ID
Name string `json:"name"` // 资产名称
CoverURL string `json:"cover_url"` // 封面图URL
LikeCount int32 `json:"like_count"` // 点赞数
RemainTime int64 `json:"remain_time"` // 剩余时间(秒)
AssetID int64 `json:"asset_id"` // 资产ID
Name string `json:"name"` // 资产名称
CoverURL string `json:"cover_url"` // 封面图URL
LikeCount int32 `json:"like_count"` // 点赞数
RemainTime int64 `json:"remain_time"` // 剩余时间(秒)
Earnings int64 `json:"earnings"` // 当前可领取收益(与 ExhibitedAssetItemDTO 命名对齐)
HourlyEarnings float64 `json:"hourly_earnings"` // 每小时收益
}
// UnlockConditionDTO 解锁条件

View File

@ -610,14 +610,16 @@ func (x *SlotInfo) GetOperation() string {
}
type AssetInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
AssetId int64 `protobuf:"varint,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
CoverUrl string `protobuf:"bytes,3,opt,name=cover_url,json=coverUrl,proto3" json:"cover_url,omitempty"`
LikeCount int32 `protobuf:"varint,4,opt,name=like_count,json=likeCount,proto3" json:"like_count,omitempty"`
RemainTime int64 `protobuf:"varint,5,opt,name=remain_time,json=remainTime,proto3" json:"remain_time,omitempty"` // 剩余时间(秒)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
AssetId int64 `protobuf:"varint,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
CoverUrl string `protobuf:"bytes,3,opt,name=cover_url,json=coverUrl,proto3" json:"cover_url,omitempty"`
LikeCount int32 `protobuf:"varint,4,opt,name=like_count,json=likeCount,proto3" json:"like_count,omitempty"`
RemainTime int64 `protobuf:"varint,5,opt,name=remain_time,json=remainTime,proto3" json:"remain_time,omitempty"` // 剩余时间(秒)
Earnings int64 `protobuf:"varint,6,opt,name=earnings,proto3" json:"earnings,omitempty"` // 当前可领取收益(按 calculateRealtimeEarnings 实时计算)
HourlyEarnings float64 `protobuf:"fixed64,7,opt,name=hourly_earnings,json=hourlyEarnings,proto3" json:"hourly_earnings,omitempty"` // 每小时收益(按 calculateHourlyEarnings 计算)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AssetInfo) Reset() {
@ -685,6 +687,20 @@ func (x *AssetInfo) GetRemainTime() int64 {
return 0
}
func (x *AssetInfo) GetEarnings() int64 {
if x != nil {
return x.Earnings
}
return 0
}
func (x *AssetInfo) GetHourlyEarnings() float64 {
if x != nil {
return x.HourlyEarnings
}
return 0
}
type UnlockCondition struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // level, crystal
@ -1810,7 +1826,7 @@ const file_gallery_proto_rawDesc = "" +
"visibility\x12\x1f\n" +
"\vcan_operate\x18\v \x01(\bR\n" +
"canOperate\x12\x1c\n" +
"\toperation\x18\f \x01(\tR\toperation\"\x97\x01\n" +
"\toperation\x18\f \x01(\tR\toperation\"\xdc\x01\n" +
"\tAssetInfo\x12\x19\n" +
"\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x1b\n" +
@ -1818,7 +1834,9 @@ const file_gallery_proto_rawDesc = "" +
"\n" +
"like_count\x18\x04 \x01(\x05R\tlikeCount\x12\x1f\n" +
"\vremain_time\x18\x05 \x01(\x03R\n" +
"remainTime\";\n" +
"remainTime\x12\x1a\n" +
"\bearnings\x18\x06 \x01(\x03R\bearnings\x12'\n" +
"\x0fhourly_earnings\x18\a \x01(\x01R\x0ehourlyEarnings\";\n" +
"\x0fUnlockCondition\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x14\n" +
"\x05value\x18\x02 \x01(\x05R\x05value\"r\n" +

View File

@ -137,6 +137,8 @@ message AssetInfo {
string cover_url = 3;
int32 like_count = 4;
int64 remain_time = 5; //
int64 earnings = 6; // calculateRealtimeEarnings
double hourly_earnings = 7; // calculateHourlyEarnings
}
message UnlockCondition {

View File

@ -510,8 +510,8 @@ func (r *galleryRepository) GetMyExhibitedAssets(userID, starID int64, page, pag
// R0 = 5 水晶/小时
now := time.Now().UnixMilli()
for _, item := range items {
item.HourlyEarnings = calculateHourlyEarnings(item.LikeCount)
item.Earnings = calculateRealtimeEarnings(item.LikeCount, item.ExhibitedAt, now, item.ExpireAt)
item.HourlyEarnings = CalculateHourlyEarnings(item.LikeCount)
item.Earnings = CalculateRealtimeEarnings(item.LikeCount, item.ExhibitedAt, now, item.ExpireAt)
}
return items, total, nil
@ -554,8 +554,8 @@ func (r *galleryRepository) GetUserExhibitedAssets(userID, starID int64, page, p
// 实时计算每个资产的收益
for _, item := range items {
item.HourlyEarnings = calculateHourlyEarnings(item.LikeCount)
item.Earnings = calculateRealtimeEarnings(item.LikeCount, item.ExhibitedAt, now, item.ExpireAt)
item.HourlyEarnings = CalculateHourlyEarnings(item.LikeCount)
item.Earnings = CalculateRealtimeEarnings(item.LikeCount, item.ExhibitedAt, now, item.ExpireAt)
}
return items, total, nil
@ -740,10 +740,11 @@ func generateHostProfileID(userID, starID int64) int64 {
return userID*1000000 + starID
}
// calculateHourlyEarnings 计算每小时收益
// CalculateHourlyEarnings 计算每小时收益
// 公式R0 × [100% + Buff(n)]
// R0 = 5 水晶/小时Buff(n) 根据点赞数计算
func calculateHourlyEarnings(likeCount int32) float64 {
// 导出供 service 层(如 gallery_service在构造 AssetInfo 时复用,避免公式漂移
func CalculateHourlyEarnings(likeCount int32) float64 {
R0 := float64(5) // 水晶/小时
// 计算Buff
@ -763,11 +764,12 @@ func calculateHourlyEarnings(likeCount int32) float64 {
return R0 * (100 + float64(buff)) / 100
}
// calculateRealtimeEarnings 实时计算展示收益
// CalculateRealtimeEarnings 实时计算展示收益
// 公式R1 = R0 × T × [100% + Buff(n)]
// R0 = 5 水晶/小时T = 上架时长小时Buff(n) 根据点赞数计算
// 注意:使用 min(now, expireAt) 确保过期后收益不再增长
func calculateRealtimeEarnings(likeCount int32, startTime, now, expireAt int64) int64 {
// 导出供 service 层(如 gallery_service在构造 AssetInfo 时复用,避免公式漂移
func CalculateRealtimeEarnings(likeCount int32, startTime, now, expireAt int64) int64 {
// 计算有效截止时间(展览结束时间 vs 当前时间,取较小值)
endTime := now
if expireAt > 0 && expireAt < now {
@ -781,5 +783,5 @@ func calculateRealtimeEarnings(likeCount int32, startTime, now, expireAt int64)
}
// 总收益 = 每小时收益 × 时长转int64取整
return int64(calculateHourlyEarnings(likeCount) * float64(T))
return int64(CalculateHourlyEarnings(likeCount) * float64(T))
}

View File

@ -205,6 +205,9 @@ func (s *galleryService) buildSlotInfos(slots []*models.BoothSlot, viewerUID, vi
remainTime = 0
}
slotInfo.Asset.RemainTime = remainTime
// 计算收益(复用 repository 中的公式,与 GetMyExhibitedAssets 保持一致)
slotInfo.Asset.HourlyEarnings = repository.CalculateHourlyEarnings(assetInfo.LikeCount)
slotInfo.Asset.Earnings = repository.CalculateRealtimeEarnings(assetInfo.LikeCount, exhibition.StartTime, now, exhibition.ExpireAt)
}
}
}

View File

@ -602,7 +602,7 @@ defineExpose({
position: relative;
/* 左侧留出足够空间给钻石图标,防止文字被压在图标正下方 */
padding-left: 50rpx;
top: -24rpx;
/* top: -24rpx; */
}
/* --- 左侧钻石图标 --- */

View File

@ -557,9 +557,14 @@ const loadExhibitedAssets = async () => {
cover_url: slot.asset.cover_url,
like_count: slot.asset.like_count,
earnings: slot.asset.earnings || 0,
hourly_earnings: slot.asset.hourly_earnings || 0,
name: slot.asset.name,
slot_index: slot.slot_index ?? 0,
is_lenticular: slot.asset.is_lenticular ?? false,
// 00:00:00 bug remainSeconds / expire_at
remainSeconds: slot.asset.remain_time || 0,
expire_at: slot.expire_at,
occupied_at: slot.occupied_at,
}));
// cover_url 访 URLOSS / URL /
for (const item of mapped) {

View File

@ -18,7 +18,7 @@
class="message-row__input"
:value="inputValue"
:placeholder="displayText"
placeholder-class="message-row__input-placeholder"
:placeholder-style="placeholderStyle"
confirm-type="send"
:maxlength="200"
@input="onInput"
@ -143,6 +143,15 @@ const displayText = computed(() => {
// 2) emoji , placeholder
const inputValue = ref("");
// placeholder :app placeholder-class + <style scoped> widget
// (data-v 穿 EditText/ UITextField), placeholder-style
// text-shadow placeholder px( rpx + Android WebView
// ); .message-row__input-placeholder
const placeholderStyle =
'color: rgba(255, 255, 255, 0.9); font-weight: bold; ' +
'font-family: "Abhaya Libre ExtraBold", "PingFang SC", sans-serif; ' +
'text-shadow: -2rpx 2rpx 8rpx rgba(206, 9, 9, 0.45);';
function onInput(e) {
// uniapp / h5: e.detail.value;;
const val = typeof e === "string" ? e : e?.detail?.value ?? "";
@ -194,6 +203,18 @@ function handleSend() {
z-index: 50;
}
/* placeholder template :placeholder-style ,
原因见 script placeholderStyle 的注释
.message-row__input-placeholder 类已废弃,保留以下注释方便回滚对照 */
/*
.message-row__input-placeholder {
color: rgba(255, 255, 255, 0.9) !important;
font-weight: bold;
font-family: "Abhaya Libre ExtraBold", "PingFang SC", sans-serif;
text-shadow: -2rpx 2rpx 8rpx rgba(206, 9, 9, 0.45);
}
*/
/* 外层 pill */
.message-row__pill {
position: absolute;
@ -239,14 +260,6 @@ function handleSend() {
transform-origin: center;
}
.message-row__input-placeholder {
color: rgba(255, 255, 255, 0.9);
font-weight: bold;
font-family: "Abhaya Libre ExtraBold", "PingFang SC", sans-serif;
text-shadow: -2rpx 2rpx 8rpx rgba(206, 9, 9, 0.45);
}
/* 左侧头像 (突出 pill 左缘) */
.message-row__avatar {
position: absolute;
left: 12rpx; /* 6px */

View File

@ -18,7 +18,7 @@
</view>
<!-- 我的排名条对应 Figma 110-662 -->
<view v-if="myInfo.rank" class="my-rank-card">
<view class="my-rank-card">
<view class="my-rank-row">
<image
class="my-avatar"
@ -27,7 +27,7 @@
@error="handleAvatarError"
/>
<text class="my-rank-label">当前排名</text>
<text class="my-rank-number">{{ myInfo.rank }}</text>
<text class="my-rank-number">{{ myInfo.rank || '暂无排名' }}</text>
<image
class="my-rank-icon"
src="/static/rank/lsph.png"
@ -74,6 +74,20 @@ const myInfo = ref({
gapToPrev: 0,
});
// /
function getFallbackAvatar() {
try {
const userStr = uni.getStorageSync("user");
if (userStr) {
const u = typeof userStr === "string" ? JSON.parse(userStr) : userStr;
return u?.avatar_url || u?.avatar || "";
}
} catch (e) {
//
}
return "";
}
//
function handleAvatarError(e) {
e.target.src = "/static/avatar/1.jpeg";
@ -123,7 +137,12 @@ async function loadRanking() {
gapToPrev: calcGapToPrev(my, items),
};
} else {
myInfo.value = { rank: null, avatar: "", gapToPrev: 0 };
// "" 0
myInfo.value = {
rank: null,
avatar: getFallbackAvatar(),
gapToPrev: 0,
};
}
}
} catch (err) {
@ -231,6 +250,7 @@ defineExpose({
font-family: "yt", "Baloo Bhai", sans-serif;
text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.45);
margin: 0 8rpx;
white-space: nowrap;
}
.my-rank-icon {

View File

@ -697,7 +697,7 @@ onUnload(() => {
/* background: rgba(255,255,255,0.5);
border-radius: 50%; */
position: fixed;
top: 88rpx;
top: 96rpx;
left: 32rpx;
}
@ -716,7 +716,7 @@ onUnload(() => {
position: fixed;
/* 左侧留出足够空间给钻石图标,防止文字被压在图标正下方 */
padding-left: 50rpx;
top: 96rpx;
top: 104rpx;
left: 112rpx;
}