fix:修改bug
This commit is contained in:
parent
365f783ea0
commit
18aff3b682
@ -56,19 +56,28 @@ type NotificationService struct {
|
||||
statsRepo *repository.NotificationStatsRepository
|
||||
device *UserDeviceService // 用于推送时拉取用户活跃 cid;若 nil 则跳过推送
|
||||
pusher push.Pusher // 推送客户端;若 nil 则跳过推送
|
||||
rateLimiter *push.RateLimiter // P1-3:推送节流器,按 (user, star, type) 60s 滑窗,>1 改 summary 标题
|
||||
}
|
||||
|
||||
// NewNotificationService 创建 NotificationService。
|
||||
//
|
||||
// 参数 device 与 pusher 用于在 CreateNotification 成功后触发手机通知栏推送;
|
||||
// 若任一为 nil,则不会触发推送(便于测试 / 关闭推送功能)。
|
||||
//
|
||||
// P1-3 修复:rateLimiter 在 NewNotificationService 中初始化,pusher 为 nil 时不创建
|
||||
// (避免占用内存)。triggerPush 内部仍需 nil 检查,避免 panic。
|
||||
func NewNotificationService(db *gorm.DB, device *UserDeviceService, pusher push.Pusher) *NotificationService {
|
||||
var rl *push.RateLimiter
|
||||
if pusher != nil {
|
||||
rl = push.NewRateLimiter(60000)
|
||||
}
|
||||
return &NotificationService{
|
||||
db: db,
|
||||
notifRepo: repository.NewNotificationRepository(db),
|
||||
statsRepo: repository.NewNotificationStatsRepository(db),
|
||||
device: device,
|
||||
pusher: pusher,
|
||||
rateLimiter: rl,
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,9 +269,22 @@ func (s *NotificationService) triggerPush(n *model.Notification) {
|
||||
go func() {
|
||||
cctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// P1-3 fix:节流后再发送——60s 滑窗内同一 (user, star, type) 多条推送,
|
||||
// 首条走原 title,后续合并为 "您有 N 条新<type中文>" summary。
|
||||
// 注意只改 Title,不覆盖 Content(Content 是具体业务文案,客户端要展示)。
|
||||
title := n.Title
|
||||
if s.rateLimiter != nil {
|
||||
key := fmt.Sprintf("%d:%d:%s", n.UserID, n.StarID, n.Type)
|
||||
mode, count := s.rateLimiter.Allow(key)
|
||||
if mode == "summary" && count > 1 {
|
||||
title = fmt.Sprintf("您有 %d 条新%s", count, push.TypeChinese(n.Type))
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.pusher.Send(cctx, push.Payload{
|
||||
CIDs: cids,
|
||||
Title: n.Title,
|
||||
Title: title,
|
||||
Content: n.Content,
|
||||
Data: data,
|
||||
}); err != nil {
|
||||
|
||||
@ -13,7 +13,9 @@ import { clearAllSandboxTmpFiles } from '@/utils/ioPath.js'
|
||||
// 记录上次隐藏时间的 storage key
|
||||
const HIDE_TIME_KEY = "app_last_hide_time";
|
||||
|
||||
// 推送事件去抖:同 type 在 PUSH_DEBOUNCE_MS 毫秒内只处理一次(避免系统重投/重复抵达)
|
||||
// 推送事件去抖:同 (type, notification_id) 在 PUSH_DEBOUNCE_MS 毫秒内只处理一次
|
||||
// (避免系统重投/重复抵达)—— P1-4 修复:之前 keyed-by-type 导致
|
||||
// "不同 nid 的同 type 推送"被丢弃,现在按 (type, nid) 区分。
|
||||
const PUSH_DEBOUNCE_MS = 10000;
|
||||
const recentByType = {};
|
||||
|
||||
@ -320,13 +322,16 @@ export default {
|
||||
const envelope = payloadStr ? JSON.parse(payloadStr) : {};
|
||||
const payload = envelope?.data || data || {};
|
||||
const t = payload.type;
|
||||
const nid = payload.notification_id || "";
|
||||
const debounceKey = `${t}:${nid}`;
|
||||
const now = Date.now();
|
||||
// V1.2.7 10s 同 type 去抖,避免系统重投导致重复 dispatch
|
||||
if (t && recentByType[t] && now - recentByType[t] < PUSH_DEBOUNCE_MS) {
|
||||
console.log("[push] debounce skip", t);
|
||||
// 10s 同 (type, notification_id) 去抖,避免系统重投导致重复 dispatch
|
||||
// (P1-4:原 key=type 会丢不同的 nid,改为组合 key)
|
||||
if (debounceKey && recentByType[debounceKey] && now - recentByType[debounceKey] < PUSH_DEBOUNCE_MS) {
|
||||
console.log("[push] debounce skip", debounceKey);
|
||||
return;
|
||||
}
|
||||
if (t) recentByType[t] = now;
|
||||
if (debounceKey) recentByType[debounceKey] = now;
|
||||
// 派发到 mailbox store:更新未读计数 + (若在站内信页)PREPEND_ITEM
|
||||
this.$store.dispatch("mailbox/applyPushPayload", {
|
||||
data: payload,
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
|
||||
<!-- 背景图片 -->
|
||||
<image class="background-image" src="/static/background/starbook.jpg" mode="aspectFill"></image>
|
||||
<image
|
||||
class="background-image"
|
||||
src="/static/background/starbook.jpg"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<scroll-view
|
||||
@ -15,16 +18,38 @@
|
||||
<view v-if="isLenticularCraft" class="upload-section">
|
||||
<view class="lenticular-upload-row">
|
||||
<view class="upload-box upload-box--half" @click="chooseLenticularBg">
|
||||
<image v-if="uploadedBg" class="uploaded-image" :src="uploadedBg" mode="aspectFit"></image>
|
||||
<image
|
||||
v-if="uploadedBg"
|
||||
class="uploaded-image"
|
||||
:src="uploadedBg"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<view v-else class="upload-placeholder">
|
||||
<image class="upload-icon" src="/static/icon/add.png" mode="aspectFit"></image>
|
||||
<image
|
||||
class="upload-icon"
|
||||
src="/static/icon/add.png"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<text class="upload-text">背景图</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="upload-box upload-box--half" @click="chooseLenticularSubject">
|
||||
<image v-if="uploadedSubject" class="uploaded-image" :src="uploadedSubject" mode="aspectFit"></image>
|
||||
<view
|
||||
class="upload-box upload-box--half"
|
||||
@click="chooseLenticularSubject"
|
||||
>
|
||||
<image
|
||||
v-if="uploadedSubject"
|
||||
class="uploaded-image"
|
||||
:src="uploadedSubject"
|
||||
mode="aspectFit"
|
||||
>
|
||||
</image>
|
||||
<view v-else class="upload-placeholder">
|
||||
<image class="upload-icon" src="/static/icon/add.png" mode="aspectFit"></image>
|
||||
<image
|
||||
class="upload-icon"
|
||||
src="/static/icon/add.png"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<text class="upload-text">主体图</text>
|
||||
</view>
|
||||
</view>
|
||||
@ -39,15 +64,29 @@
|
||||
<text class="form-label">素材类型</text>
|
||||
<view class="custom-picker">
|
||||
<view class="picker-display" @click="toggleMaterialTypePicker">
|
||||
<text class="picker-text">{{ materialTypes[materialTypeIndex] }}</text>
|
||||
<text class="picker-arrow" :class="{ 'picker-arrow-up': showMaterialTypePicker }">›</text>
|
||||
<text class="picker-text">{{
|
||||
materialTypes[materialTypeIndex]
|
||||
}}</text>
|
||||
<text
|
||||
class="picker-arrow"
|
||||
:class="{ 'picker-arrow-up': showMaterialTypePicker }"
|
||||
>›</text
|
||||
>
|
||||
</view>
|
||||
<view v-if="showMaterialTypePicker" class="picker-options">
|
||||
<view v-for="(type, index) in materialTypes" :key="index" class="picker-option"
|
||||
<view
|
||||
v-for="(type, index) in materialTypes"
|
||||
:key="index"
|
||||
class="picker-option"
|
||||
:class="{ 'picker-option-active': materialTypeIndex === index }"
|
||||
@click.stop="selectMaterialType(index)">
|
||||
@click.stop="selectMaterialType(index)"
|
||||
>
|
||||
<text class="picker-option-text">{{ type }}</text>
|
||||
<text v-if="materialTypeIndex === index" class="picker-option-check">✓</text>
|
||||
<text
|
||||
v-if="materialTypeIndex === index"
|
||||
class="picker-option-check"
|
||||
>✓</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -56,8 +95,15 @@
|
||||
<!-- 藏品信息 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">藏品信息</text>
|
||||
<textarea class="form-textarea" v-model="nftInfo" placeholder="请输入藏品相关信息"
|
||||
placeholder-class="textarea-placeholder" maxlength="500" auto-height :show-confirm-bar="false" />
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
v-model="nftInfo"
|
||||
placeholder="请输入藏品相关信息"
|
||||
placeholder-class="textarea-placeholder"
|
||||
maxlength="500"
|
||||
auto-height
|
||||
:show-confirm-bar="false"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -85,7 +131,6 @@
|
||||
<button class="btn-skip" @click="handleLenticularGenerate">生成</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 通用确认弹窗 -->
|
||||
@ -103,24 +148,24 @@
|
||||
|
||||
<script setup>
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { onLoad,onUnload } from '@dcloudio/uni-app';
|
||||
import { getOssSignatureApi, createMintOrderApi } from '@/utils/api.js';
|
||||
import { resolveH5OssPostUrl } from '@/utils/h5OssPostUrl.js';
|
||||
import { getSandboxFileUri, clearSandboxSubdir } from '@/utils/ioPath.js';
|
||||
import { useAliveGuard } from '@/composables/useAliveGuard.js';
|
||||
import ConfirmModal from '@/components/ConfirmModal.vue';
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import { getOssSignatureApi, createMintOrderApi } from "@/utils/api.js";
|
||||
import { resolveH5OssPostUrl } from "@/utils/h5OssPostUrl.js";
|
||||
import { getSandboxFileUri, clearSandboxSubdir } from "@/utils/ioPath.js";
|
||||
import { useAliveGuard } from "@/composables/useAliveGuard.js";
|
||||
import ConfirmModal from "@/components/ConfirmModal.vue";
|
||||
import {
|
||||
buildCastloveFormSnapshot,
|
||||
CRAFT_LENTICULAR_CN,
|
||||
} from '@/utils/castloveMintForm.js';
|
||||
} from "@/utils/castloveMintForm.js";
|
||||
import {
|
||||
startCraftGenerationFlow,
|
||||
STUDIO_LENTICULAR,
|
||||
} from '@/utils/castloveGenerationFlow.js';
|
||||
} from "@/utils/castloveGenerationFlow.js";
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
const scrollEnhanced = true;
|
||||
@ -129,10 +174,12 @@ const scrollEnhanced = true;
|
||||
const scrollEnhanced = false;
|
||||
// #endif
|
||||
|
||||
const pageType = ref('');
|
||||
const pageName = ref('');
|
||||
const pageType = ref("");
|
||||
const pageName = ref("");
|
||||
|
||||
const isLenticularCraft = computed(() => (pageName.value || '').trim() === CRAFT_LENTICULAR_CN);
|
||||
const isLenticularCraft = computed(
|
||||
() => (pageName.value || "").trim() === CRAFT_LENTICULAR_CN,
|
||||
);
|
||||
|
||||
onUnload(() => {
|
||||
try {
|
||||
@ -151,33 +198,33 @@ onUnload(() => {
|
||||
const { guard } = useAliveGuard();
|
||||
|
||||
function safeDecodeParam(v) {
|
||||
if (v == null || v === '') return '';
|
||||
const s = typeof v === 'string' ? v : String(v);
|
||||
if (v == null || v === "") return "";
|
||||
const s = typeof v === "string" ? v : String(v);
|
||||
try {
|
||||
return decodeURIComponent(s.replace(/\+/g, ' '));
|
||||
return decodeURIComponent(s.replace(/\+/g, " "));
|
||||
} catch {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
function applyCreateRouteOptions(options) {
|
||||
if (!options || typeof options !== 'object') return;
|
||||
if (!options || typeof options !== "object") return;
|
||||
if (options.type) pageType.value = safeDecodeParam(options.type);
|
||||
if (options.name != null && String(options.name) !== '') {
|
||||
if (options.name != null && String(options.name) !== "") {
|
||||
pageName.value = safeDecodeParam(options.name);
|
||||
console.log('[LenticularCreate] 工艺名称:', pageName.value);
|
||||
console.log("[LenticularCreate] 工艺名称:", pageName.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 通用确认弹窗状态
|
||||
const confirmModal = ref({
|
||||
visible: false,
|
||||
title: '',
|
||||
content: '',
|
||||
confirmText: '确认',
|
||||
cancelText: '取消',
|
||||
title: "",
|
||||
content: "",
|
||||
confirmText: "确认",
|
||||
cancelText: "取消",
|
||||
showCancel: true,
|
||||
confirmCallback: null
|
||||
confirmCallback: null,
|
||||
});
|
||||
|
||||
// 弹窗确认回调
|
||||
@ -200,70 +247,70 @@ const onCancelModal = () => {
|
||||
const showConfirmModal = (options) => {
|
||||
confirmModal.value = {
|
||||
visible: true,
|
||||
title: options.title || '',
|
||||
content: options.content || '',
|
||||
confirmText: options.confirmText || '确认',
|
||||
cancelText: options.cancelText || '取消',
|
||||
title: options.title || "",
|
||||
content: options.content || "",
|
||||
confirmText: options.confirmText || "确认",
|
||||
cancelText: options.cancelText || "取消",
|
||||
showCancel: options.showCancel !== false,
|
||||
confirmCallback: options.success || null
|
||||
confirmCallback: options.success || null,
|
||||
};
|
||||
};
|
||||
|
||||
// 表单数据
|
||||
const uploadedImage = ref('');
|
||||
const uploadedImageBase64 = ref('');
|
||||
const uploadedImage = ref("");
|
||||
const uploadedImageBase64 = ref("");
|
||||
/** 光栅:背景 / 主体双槽位;空字符串表示普通单图上传 */
|
||||
const pendingLenticularSlot = ref('');
|
||||
const uploadedBg = ref('');
|
||||
const uploadedBgBase64 = ref('');
|
||||
const uploadedSubject = ref('');
|
||||
const uploadedSubjectBase64 = ref('');
|
||||
const originalFileName = ref('');
|
||||
const pendingLenticularSlot = ref("");
|
||||
const uploadedBg = ref("");
|
||||
const uploadedBgBase64 = ref("");
|
||||
const uploadedSubject = ref("");
|
||||
const uploadedSubjectBase64 = ref("");
|
||||
const originalFileName = ref("");
|
||||
const isUploading = ref(false);
|
||||
const materialTypes = ['粉丝自制', '热爱痕迹', '其他'];
|
||||
const materialTypes = ["粉丝自制", "热爱痕迹", "其他"];
|
||||
const materialTypeIndex = ref(0);
|
||||
const showMaterialTypePicker = ref(false);
|
||||
const nftInfo = ref('');
|
||||
const nftInfo = ref("");
|
||||
|
||||
// AI描述相关
|
||||
const aiDescription = ref('');
|
||||
const aiDescription = ref("");
|
||||
|
||||
function applyUploadResult(filePath, dataUrl) {
|
||||
const slot = pendingLenticularSlot.value;
|
||||
if (slot === 'bg') {
|
||||
if (slot === "bg") {
|
||||
uploadedBg.value = filePath;
|
||||
uploadedBgBase64.value = dataUrl;
|
||||
} else if (slot === 'subject') {
|
||||
} else if (slot === "subject") {
|
||||
uploadedSubject.value = filePath;
|
||||
uploadedSubjectBase64.value = dataUrl;
|
||||
} else {
|
||||
uploadedImage.value = filePath;
|
||||
uploadedImageBase64.value = dataUrl;
|
||||
}
|
||||
pendingLenticularSlot.value = '';
|
||||
pendingLenticularSlot.value = "";
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '图片加载成功', icon: 'success', duration: 1500 });
|
||||
uni.showToast({ title: "图片加载成功", icon: "success", duration: 1500 });
|
||||
isUploading.value = false;
|
||||
}
|
||||
|
||||
const chooseLenticularBg = () => {
|
||||
openImagePicker('bg');
|
||||
openImagePicker("bg");
|
||||
};
|
||||
|
||||
const chooseLenticularSubject = () => {
|
||||
openImagePicker('subject');
|
||||
openImagePicker("subject");
|
||||
};
|
||||
|
||||
const openImagePicker = (lenticularSlot) => {
|
||||
if (isUploading.value) {
|
||||
uni.showToast({ title: '图片上传中,请稍候', icon: 'none' });
|
||||
uni.showToast({ title: "图片上传中,请稍候", icon: "none" });
|
||||
return;
|
||||
}
|
||||
pendingLenticularSlot.value = lenticularSlot;
|
||||
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sourceType: ['album', 'camera'],
|
||||
sourceType: ["album", "camera"],
|
||||
success: guard((res) => {
|
||||
const filePath = res.tempFilePaths[0];
|
||||
const tempFile = res.tempFiles && res.tempFiles[0];
|
||||
@ -272,56 +319,64 @@ const openImagePicker = (lenticularSlot) => {
|
||||
filePath: filePath,
|
||||
success: guard((fileInfo) => {
|
||||
const maxSize = 5 * 1024 * 1024;
|
||||
const slotLabel = lenticularSlot === 'bg' ? '背景图' : '主体图';
|
||||
const slotLabel = lenticularSlot === "bg" ? "背景图" : "主体图";
|
||||
if (fileInfo.size > maxSize) {
|
||||
pendingLenticularSlot.value = '';
|
||||
pendingLenticularSlot.value = "";
|
||||
showConfirmModal({
|
||||
title: '图片不符合要求',
|
||||
title: "图片不符合要求",
|
||||
content: `${slotLabel}大小不能超过5MB,当前图片大小为${(fileInfo.size / 1024 / 1024).toFixed(2)}MB,请重新选择`,
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
confirmText: "知道了",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const mimeType = (tempFile && tempFile.type) ? tempFile.type.toLowerCase() : '';
|
||||
const mimeType =
|
||||
tempFile && tempFile.type ? tempFile.type.toLowerCase() : "";
|
||||
const pathLower = filePath.toLowerCase();
|
||||
const validByMime = mimeType === 'image/jpeg' || mimeType === 'image/png';
|
||||
const validByExt = pathLower.endsWith('.jpg') || pathLower.endsWith('.jpeg') || pathLower.endsWith('.png');
|
||||
const validByMime =
|
||||
mimeType === "image/jpeg" || mimeType === "image/png";
|
||||
const validByExt =
|
||||
pathLower.endsWith(".jpg") ||
|
||||
pathLower.endsWith(".jpeg") ||
|
||||
pathLower.endsWith(".png");
|
||||
if (!validByMime && !validByExt) {
|
||||
pendingLenticularSlot.value = '';
|
||||
pendingLenticularSlot.value = "";
|
||||
showConfirmModal({
|
||||
title: '图片格式不符合要求',
|
||||
title: "图片格式不符合要求",
|
||||
content: `${slotLabel}只支持JPG和PNG格式的图片,请重新选择`,
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
confirmText: "知道了",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const rawName = (tempFile && tempFile.name) ? tempFile.name : filePath.split('/').pop();
|
||||
const rawName =
|
||||
tempFile && tempFile.name
|
||||
? tempFile.name
|
||||
: filePath.split("/").pop();
|
||||
originalFileName.value = rawName;
|
||||
convertImageToBase64(filePath, originalFileName.value);
|
||||
}),
|
||||
fail: guard((error) => {
|
||||
pendingLenticularSlot.value = '';
|
||||
console.error('获取文件信息失败:', error);
|
||||
uni.showToast({ title: '获取文件信息失败', icon: 'none' });
|
||||
})
|
||||
pendingLenticularSlot.value = "";
|
||||
console.error("获取文件信息失败:", error);
|
||||
uni.showToast({ title: "获取文件信息失败", icon: "none" });
|
||||
}),
|
||||
});
|
||||
}),
|
||||
fail: guard((err) => {
|
||||
pendingLenticularSlot.value = '';
|
||||
console.error('选择图片失败:', err);
|
||||
uni.showToast({ title: '选择图片失败', icon: 'none' });
|
||||
})
|
||||
pendingLenticularSlot.value = "";
|
||||
console.error("选择图片失败:", err);
|
||||
uni.showToast({ title: "选择图片失败", icon: "none" });
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
// 将图片转换为 base64
|
||||
const convertImageToBase64 = (filePath, fileName) => {
|
||||
isUploading.value = true;
|
||||
uni.showLoading({ title: '处理中...', mask: true });
|
||||
uni.showLoading({ title: "处理中...", mask: true });
|
||||
|
||||
// #ifdef H5
|
||||
convertImageToBase64H5(filePath, fileName);
|
||||
@ -334,28 +389,31 @@ const convertImageToBase64 = (filePath, fileName) => {
|
||||
|
||||
const convertImageToBase64H5 = (filePath, fileName) => {
|
||||
fetch(filePath)
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
applyUploadResult(filePath, e.target.result);
|
||||
console.log('[LenticularCreate] Base64转换成功 (H5)');
|
||||
console.log('[LenticularCreate] Base64长度:', (e.target.result && e.target.result.length) || 0);
|
||||
console.log("[LenticularCreate] Base64转换成功 (H5)");
|
||||
console.log(
|
||||
"[LenticularCreate] Base64长度:",
|
||||
(e.target.result && e.target.result.length) || 0,
|
||||
);
|
||||
};
|
||||
reader.onerror = (error) => {
|
||||
pendingLenticularSlot.value = '';
|
||||
console.error('[LenticularCreate] Base64转换失败 (H5):', error);
|
||||
pendingLenticularSlot.value = "";
|
||||
console.error("[LenticularCreate] Base64转换失败 (H5):", error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
|
||||
uni.showToast({ title: "图片处理失败", icon: "none", duration: 2000 });
|
||||
isUploading.value = false;
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
})
|
||||
.catch(error => {
|
||||
pendingLenticularSlot.value = '';
|
||||
console.error('[LenticularCreate] 获取图片失败 (H5):', error);
|
||||
.catch((error) => {
|
||||
pendingLenticularSlot.value = "";
|
||||
console.error("[LenticularCreate] 获取图片失败 (H5):", error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
|
||||
uni.showToast({ title: "图片处理失败", icon: "none", duration: 2000 });
|
||||
isUploading.value = false;
|
||||
});
|
||||
};
|
||||
@ -365,51 +423,64 @@ const convertImageToBase64Native = (filePath, fileName) => {
|
||||
const fs = uni.getFileSystemManager();
|
||||
fs.readFile({
|
||||
filePath: filePath,
|
||||
encoding: 'base64',
|
||||
encoding: "base64",
|
||||
success: guard((res) => {
|
||||
const ext = fileName.toLowerCase().split('.').pop();
|
||||
let mimeType = 'image/jpeg';
|
||||
if (ext === 'png') mimeType = 'image/png';
|
||||
const ext = fileName.toLowerCase().split(".").pop();
|
||||
let mimeType = "image/jpeg";
|
||||
if (ext === "png") mimeType = "image/png";
|
||||
const dataUrl = `data:${mimeType};base64,${res.data}`;
|
||||
applyUploadResult(filePath, dataUrl);
|
||||
console.log('[LenticularCreate] Base64转换成功 (小程序)');
|
||||
console.log('[LenticularCreate] Base64长度:', dataUrl.length);
|
||||
console.log("[LenticularCreate] Base64转换成功 (小程序)");
|
||||
console.log("[LenticularCreate] Base64长度:", dataUrl.length);
|
||||
}),
|
||||
fail: guard((error) => {
|
||||
pendingLenticularSlot.value = '';
|
||||
console.error('[LenticularCreate] Base64转换失败 (小程序):', error);
|
||||
pendingLenticularSlot.value = "";
|
||||
console.error("[LenticularCreate] Base64转换失败 (小程序):", error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
|
||||
uni.showToast({ title: "图片处理失败", icon: "none", duration: 2000 });
|
||||
isUploading.value = false;
|
||||
})
|
||||
}),
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
plus.io.resolveLocalFileSystemURL(filePath, guard((entry) => {
|
||||
entry.file(guard((file) => {
|
||||
plus.io.resolveLocalFileSystemURL(
|
||||
filePath,
|
||||
guard((entry) => {
|
||||
entry.file(
|
||||
guard((file) => {
|
||||
const reader = new plus.io.FileReader();
|
||||
reader.onloadend = guard((e) => {
|
||||
applyUploadResult(filePath, e.target.result);
|
||||
console.log('[LenticularCreate] Base64转换成功 (App)');
|
||||
console.log('[LenticularCreate] Base64长度:', (e.target.result && e.target.result.length) || 0);
|
||||
console.log("[LenticularCreate] Base64转换成功 (App)");
|
||||
console.log(
|
||||
"[LenticularCreate] Base64长度:",
|
||||
(e.target.result && e.target.result.length) || 0,
|
||||
);
|
||||
});
|
||||
reader.onerror = guard((error) => {
|
||||
pendingLenticularSlot.value = '';
|
||||
console.error('[LenticularCreate] Base64转换失败 (App):', error);
|
||||
pendingLenticularSlot.value = "";
|
||||
console.error("[LenticularCreate] Base64转换失败 (App):", error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
|
||||
uni.showToast({
|
||||
title: "图片处理失败",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
isUploading.value = false;
|
||||
});
|
||||
reader.readAsDataURL(file);
|
||||
}));
|
||||
}), guard((error) => {
|
||||
pendingLenticularSlot.value = '';
|
||||
console.error('[LenticularCreate] 读取文件失败 (App):', error);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
guard((error) => {
|
||||
pendingLenticularSlot.value = "";
|
||||
console.error("[LenticularCreate] 读取文件失败 (App):", error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 });
|
||||
uni.showToast({ title: "图片处理失败", icon: "none", duration: 2000 });
|
||||
isUploading.value = false;
|
||||
}));
|
||||
}),
|
||||
);
|
||||
// #endif
|
||||
};
|
||||
|
||||
@ -433,18 +504,21 @@ const uploadImageToOss = async (base64Data, ossData) => {
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
const formData = new FormData();
|
||||
formData.append('key', ossData.dir + fileName);
|
||||
formData.append('policy', ossData.policy);
|
||||
formData.append('success_action_status', '200');
|
||||
formData.append('x-oss-credential', ossData.x_oss_credential);
|
||||
formData.append('x-oss-date', ossData.x_oss_date);
|
||||
formData.append('x-oss-security-token', ossData.security_token);
|
||||
formData.append('x-oss-signature', ossData.signature);
|
||||
formData.append('x-oss-signature-version', ossData.x_oss_signature_version);
|
||||
formData.append('file', blob, fileName);
|
||||
formData.append("key", ossData.dir + fileName);
|
||||
formData.append("policy", ossData.policy);
|
||||
formData.append("success_action_status", "200");
|
||||
formData.append("x-oss-credential", ossData.x_oss_credential);
|
||||
formData.append("x-oss-date", ossData.x_oss_date);
|
||||
formData.append("x-oss-security-token", ossData.security_token);
|
||||
formData.append("x-oss-signature", ossData.signature);
|
||||
formData.append(
|
||||
"x-oss-signature-version",
|
||||
ossData.x_oss_signature_version,
|
||||
);
|
||||
formData.append("file", blob, fileName);
|
||||
return fetch(resolveH5OssPostUrl(ossData.host), {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
})
|
||||
.then((response) => {
|
||||
@ -452,7 +526,7 @@ const uploadImageToOss = async (base64Data, ossData) => {
|
||||
const imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
|
||||
resolve(imageUrl);
|
||||
} else {
|
||||
reject(new Error('上传失败'));
|
||||
reject(new Error("上传失败"));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -461,28 +535,28 @@ const uploadImageToOss = async (base64Data, ossData) => {
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ
|
||||
const base64Content = base64Data.split(',')[1];
|
||||
const base64Content = base64Data.split(",")[1];
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`;
|
||||
|
||||
const fs = uni.getFileSystemManager();
|
||||
fs.writeFile({
|
||||
filePath: filePath,
|
||||
data: base64Content,
|
||||
encoding: 'base64',
|
||||
encoding: "base64",
|
||||
success: guard(() => {
|
||||
uni.uploadFile({
|
||||
url: ossData.host,
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
name: "file",
|
||||
formData: {
|
||||
key: ossData.dir + fileName,
|
||||
policy: ossData.policy,
|
||||
success_action_status: '200',
|
||||
'x-oss-credential': ossData.x_oss_credential,
|
||||
'x-oss-date': ossData.x_oss_date,
|
||||
'x-oss-security-token': ossData.security_token,
|
||||
'x-oss-signature': ossData.signature,
|
||||
'x-oss-signature-version': ossData.x_oss_signature_version
|
||||
success_action_status: "200",
|
||||
"x-oss-credential": ossData.x_oss_credential,
|
||||
"x-oss-date": ossData.x_oss_date,
|
||||
"x-oss-security-token": ossData.security_token,
|
||||
"x-oss-signature": ossData.signature,
|
||||
"x-oss-signature-version": ossData.x_oss_signature_version,
|
||||
},
|
||||
success: guard((uploadRes) => {
|
||||
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
|
||||
@ -494,74 +568,112 @@ const uploadImageToOss = async (base64Data, ossData) => {
|
||||
}),
|
||||
fail: guard((error) => {
|
||||
reject(error);
|
||||
})
|
||||
}),
|
||||
});
|
||||
}),
|
||||
fail: guard((error) => {
|
||||
reject(error);
|
||||
})
|
||||
}),
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
// Android 10+ 适配:bitmap.save 接受沙盒 file:// 绝对路径(避免 '_doc/' 字符串写失败)
|
||||
console.log('[LenticularCreate] App环境上传');
|
||||
const base64ContentApp = base64Data.split(',')[1];
|
||||
const bitmap = new plus.nativeObj.Bitmap('temp');
|
||||
console.log("[LenticularCreate] App环境上传");
|
||||
const base64ContentApp = base64Data.split(",")[1];
|
||||
const bitmap = new plus.nativeObj.Bitmap("temp");
|
||||
|
||||
bitmap.loadBase64Data(base64ContentApp, guard(() => {
|
||||
getSandboxFileUri(['castlove-lenticular', 'tmp'], fileName).then((tempFilePath) => {
|
||||
bitmap.save(tempFilePath, { overwrite: true }, guard(() => {
|
||||
console.log('[LenticularCreate] App临时文件保存成功:', tempFilePath);
|
||||
bitmap.loadBase64Data(
|
||||
base64ContentApp,
|
||||
guard(
|
||||
() => {
|
||||
getSandboxFileUri(["castlove-lenticular", "tmp"], fileName)
|
||||
.then((tempFilePath) => {
|
||||
bitmap.save(
|
||||
tempFilePath,
|
||||
{ overwrite: true },
|
||||
guard(
|
||||
() => {
|
||||
console.log(
|
||||
"[LenticularCreate] App临时文件保存成功:",
|
||||
tempFilePath,
|
||||
);
|
||||
bitmap.clear();
|
||||
|
||||
uni.uploadFile({
|
||||
url: ossData.host,
|
||||
filePath: tempFilePath,
|
||||
name: 'file',
|
||||
name: "file",
|
||||
formData: {
|
||||
key: ossData.dir + fileName,
|
||||
policy: ossData.policy,
|
||||
success_action_status: '200',
|
||||
'x-oss-credential': ossData.x_oss_credential,
|
||||
'x-oss-date': ossData.x_oss_date,
|
||||
'x-oss-security-token': ossData.security_token,
|
||||
'x-oss-signature': ossData.signature,
|
||||
'x-oss-signature-version': ossData.x_oss_signature_version
|
||||
success_action_status: "200",
|
||||
"x-oss-credential": ossData.x_oss_credential,
|
||||
"x-oss-date": ossData.x_oss_date,
|
||||
"x-oss-security-token": ossData.security_token,
|
||||
"x-oss-signature": ossData.signature,
|
||||
"x-oss-signature-version":
|
||||
ossData.x_oss_signature_version,
|
||||
},
|
||||
success: guard((uploadRes) => {
|
||||
console.log('[LenticularCreate] App上传响应:', uploadRes);
|
||||
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
|
||||
console.log(
|
||||
"[LenticularCreate] App上传响应:",
|
||||
uploadRes,
|
||||
);
|
||||
if (
|
||||
uploadRes.statusCode === 200 ||
|
||||
uploadRes.statusCode === 204
|
||||
) {
|
||||
const imageUrl = `${ossData.host}/${ossData.dir}${fileName}`;
|
||||
// 上传成功后清理临时文件(doc/castlove-lenticular/tmp/),避免 doc 残留垃圾
|
||||
clearSandboxSubdir(['castlove-lenticular', 'tmp']).catch((e) => {
|
||||
console.warn('[LenticularCreate] clear tmp dir failed:', e?.message)
|
||||
})
|
||||
clearSandboxSubdir([
|
||||
"castlove-lenticular",
|
||||
"tmp",
|
||||
]).catch((e) => {
|
||||
console.warn(
|
||||
"[LenticularCreate] clear tmp dir failed:",
|
||||
e?.message,
|
||||
);
|
||||
});
|
||||
resolve(imageUrl);
|
||||
} else {
|
||||
reject(new Error(`上传失败,状态码: ${uploadRes.statusCode}`));
|
||||
reject(
|
||||
new Error(
|
||||
`上传失败,状态码: ${uploadRes.statusCode}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
fail: guard((error) => {
|
||||
console.error('[LenticularCreate] App上传失败:', error);
|
||||
console.error("[LenticularCreate] App上传失败:", error);
|
||||
reject(error);
|
||||
})
|
||||
}),
|
||||
});
|
||||
}, guard((error) => {
|
||||
console.error('[LenticularCreate] App保存临时文件失败:', error);
|
||||
},
|
||||
guard((error) => {
|
||||
console.error(
|
||||
"[LenticularCreate] App保存临时文件失败:",
|
||||
error,
|
||||
);
|
||||
bitmap.clear();
|
||||
reject(new Error('保存临时文件失败'));
|
||||
})));
|
||||
}).catch((err) => {
|
||||
console.error('[LenticularCreate] getSandboxFileUri 失败:', err);
|
||||
reject(new Error("保存临时文件失败"));
|
||||
}),
|
||||
),
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("[LenticularCreate] getSandboxFileUri 失败:", err);
|
||||
bitmap.clear();
|
||||
reject(err);
|
||||
});
|
||||
}, (error) => {
|
||||
console.error('[LenticularCreate] App加载base64失败:', error);
|
||||
},
|
||||
guard((error) => {
|
||||
console.error("[LenticularCreate] App加载base64失败:", error);
|
||||
bitmap.clear();
|
||||
reject(new Error('加载图片数据失败'));
|
||||
});
|
||||
reject(new Error("加载图片数据失败"));
|
||||
}),
|
||||
),
|
||||
);
|
||||
// #endif
|
||||
});
|
||||
};
|
||||
@ -570,7 +682,10 @@ const uploadImageToOss = async (base64Data, ossData) => {
|
||||
const handleBack = async () => {
|
||||
const hasLenticular =
|
||||
isLenticularCraft.value &&
|
||||
(uploadedBg.value || uploadedSubject.value || uploadedBgBase64.value || uploadedSubjectBase64.value);
|
||||
(uploadedBg.value ||
|
||||
uploadedSubject.value ||
|
||||
uploadedBgBase64.value ||
|
||||
uploadedSubjectBase64.value);
|
||||
const hasAny =
|
||||
uploadedImage.value ||
|
||||
hasLenticular ||
|
||||
@ -578,8 +693,8 @@ const handleBack = async () => {
|
||||
aiDescription.value;
|
||||
if (hasAny) {
|
||||
showConfirmModal({
|
||||
title: '提示',
|
||||
content: '确定要返回吗?未保存的数据将会丢失',
|
||||
title: "提示",
|
||||
content: "确定要返回吗?未保存的数据将会丢失",
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
resetForm();
|
||||
@ -590,7 +705,7 @@ const handleBack = async () => {
|
||||
}
|
||||
uni.navigateBack();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
@ -615,8 +730,12 @@ const buildCraftFormData = () => {
|
||||
|
||||
return {
|
||||
...snap,
|
||||
image: isLenticularCraft.value ? uploadedSubject.value : uploadedImage.value,
|
||||
imageBase64: isLenticularCraft.value ? uploadedSubjectBase64.value : uploadedImageBase64.value,
|
||||
image: isLenticularCraft.value
|
||||
? uploadedSubject.value
|
||||
: uploadedImage.value,
|
||||
imageBase64: isLenticularCraft.value
|
||||
? uploadedSubjectBase64.value
|
||||
: uploadedImageBase64.value,
|
||||
type: pageType.value,
|
||||
typeName: pageName.value,
|
||||
materialType: snap.material_type,
|
||||
@ -646,25 +765,25 @@ const buildCraftPrompt = () => {
|
||||
const startCraftStudioPipeline = (studioKind) => {
|
||||
if (isLenticularCraft.value) {
|
||||
if (!uploadedBg.value || !uploadedSubject.value) {
|
||||
uni.showToast({ title: '请上传背景图与主体图', icon: 'none' });
|
||||
uni.showToast({ title: "请上传背景图与主体图", icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (!uploadedBgBase64.value || !uploadedSubjectBase64.value) {
|
||||
uni.showToast({ title: '图片尚未处理完成', icon: 'none' });
|
||||
uni.showToast({ title: "图片尚未处理完成", icon: "none" });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!uploadedImage.value) {
|
||||
uni.showToast({ title: '请上传藏品图片', icon: 'none' });
|
||||
uni.showToast({ title: "请上传藏品图片", icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (!uploadedImageBase64.value) {
|
||||
uni.showToast({ title: '图片尚未处理完成', icon: 'none' });
|
||||
uni.showToast({ title: "图片尚未处理完成", icon: "none" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!nftInfo.value.trim()) {
|
||||
uni.showToast({ title: '请输入藏品信息', icon: 'none' });
|
||||
uni.showToast({ title: "请输入藏品信息", icon: "none" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@ -675,13 +794,13 @@ const startCraftStudioPipeline = (studioKind) => {
|
||||
title: result.error.title,
|
||||
content: result.error.content,
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
confirmText: "知道了",
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[LenticularCreate] craft studio pipeline', e);
|
||||
uni.showToast({ title: '启动失败,请重试', icon: 'none' });
|
||||
console.error("[LenticularCreate] craft studio pipeline", e);
|
||||
uni.showToast({ title: "启动失败,请重试", icon: "none" });
|
||||
}
|
||||
};
|
||||
|
||||
@ -691,18 +810,18 @@ const handleLenticularGenerate = () => {
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
pendingLenticularSlot.value = '';
|
||||
uploadedBg.value = '';
|
||||
uploadedBgBase64.value = '';
|
||||
uploadedSubject.value = '';
|
||||
uploadedSubjectBase64.value = '';
|
||||
uploadedImage.value = '';
|
||||
uploadedImageBase64.value = '';
|
||||
originalFileName.value = '';
|
||||
pendingLenticularSlot.value = "";
|
||||
uploadedBg.value = "";
|
||||
uploadedBgBase64.value = "";
|
||||
uploadedSubject.value = "";
|
||||
uploadedSubjectBase64.value = "";
|
||||
uploadedImage.value = "";
|
||||
uploadedImageBase64.value = "";
|
||||
originalFileName.value = "";
|
||||
isUploading.value = false;
|
||||
materialTypeIndex.value = 0;
|
||||
nftInfo.value = '';
|
||||
aiDescription.value = '';
|
||||
nftInfo.value = "";
|
||||
aiDescription.value = "";
|
||||
};
|
||||
|
||||
onLoad((options) => {
|
||||
@ -854,9 +973,15 @@ onMounted(() => {
|
||||
font-size: 36rpx;
|
||||
color: #e6e6e6;
|
||||
font-weight: 500;
|
||||
font-family: 'yt', sans-serif;
|
||||
font-family: "yt", sans-serif;
|
||||
padding: 18rpx 18rpx;
|
||||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||||
background: linear-gradient(
|
||||
165deg,
|
||||
#f0e4b1 0%,
|
||||
#f08399 50%,
|
||||
#b94e73 90%,
|
||||
#834b9e 100%
|
||||
);
|
||||
border-radius: 44rpx;
|
||||
display: inline-block;
|
||||
align-self: flex-start;
|
||||
@ -929,6 +1054,7 @@ onMounted(() => {
|
||||
opacity: 0;
|
||||
transform: translateY(-10rpx);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
@ -1004,9 +1130,15 @@ onMounted(() => {
|
||||
font-size: 36rpx;
|
||||
color: #e6e6e6;
|
||||
font-weight: 500;
|
||||
font-family: 'yt', sans-serif;
|
||||
font-family: "yt", sans-serif;
|
||||
padding: 18rpx 18rpx;
|
||||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||||
background: linear-gradient(
|
||||
165deg,
|
||||
#f0e4b1 0%,
|
||||
#f08399 50%,
|
||||
#b94e73 90%,
|
||||
#834b9e 100%
|
||||
);
|
||||
border-radius: 44rpx;
|
||||
display: inline-block;
|
||||
align-self: flex-start;
|
||||
@ -1065,7 +1197,7 @@ onMounted(() => {
|
||||
line-height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
font-size: 36rpx;
|
||||
font-family: 'yt', sans-serif;
|
||||
font-family: "yt", sans-serif;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
display: flex;
|
||||
@ -1087,7 +1219,13 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.btn-skip {
|
||||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||||
background: linear-gradient(
|
||||
165deg,
|
||||
#f0e4b1 0%,
|
||||
#f08399 50%,
|
||||
#b94e73 90%,
|
||||
#834b9e 100%
|
||||
);
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -44,13 +44,22 @@ const typeMap = {
|
||||
const typeLabel = computed(() => typeMap[props.notification.type] || props.notification.type || '通知')
|
||||
|
||||
const businessMeta = computed(() => {
|
||||
// data 是 JSON 字符串,反序列化展示业务字段
|
||||
// data 可能是 string(JSON) 或 object(proto structpb.AsMap() 的产物)。
|
||||
// P0-1 fix:网关 convertNotification 走 AsMap() 直接发对象,旧写法 JSON.parse(obj) 会 SyntaxError → {}。
|
||||
let data = {}
|
||||
try { data = JSON.parse(props.notification.data || '{}') } catch (e) { data = {} }
|
||||
const raw = props.notification.data
|
||||
if (raw) {
|
||||
if (typeof raw === 'string') {
|
||||
try { data = JSON.parse(raw) } catch (e) { data = {} }
|
||||
} else if (typeof raw === 'object') {
|
||||
data = raw
|
||||
}
|
||||
}
|
||||
const list = []
|
||||
if (data.feedback_id) list.push({ k: '反馈 ID', v: data.feedback_id })
|
||||
if (data.report_id) list.push({ k: '举报 ID', v: data.report_id })
|
||||
if (data.resolved_action) list.push({ k: '处理动作', v: actionLabel(data.resolved_action) })
|
||||
// P1-1 fix:admin backend moderation_admin.py 用 action(规范名),不是 resolved_action
|
||||
if (data.action) list.push({ k: '处理动作', v: actionLabel(data.action) })
|
||||
if (data.category_code) list.push({ k: '分类', v: data.category_code })
|
||||
if (data.original_title) list.push({ k: '原标题', v: data.original_title })
|
||||
if (data.activity_id) list.push({ k: '活动 ID', v: data.activity_id })
|
||||
|
||||
@ -70,8 +70,10 @@ const notification = computed(() => {
|
||||
|
||||
async function loadOne() {
|
||||
try {
|
||||
// P0-2 fix:utils/api.js 统一返回 { code, message, data: { items, ... } },
|
||||
// 旧写法读 resp.items 永远是 undefined → find 永远 null → detailCache 不写。
|
||||
const resp = await getNotificationsApi({ type: type.value, page: 1, page_size: 50 })
|
||||
const item = (resp.items || []).find(n => n.id === nid.value)
|
||||
const item = (resp.data?.items || []).find(n => n.id === nid.value)
|
||||
if (item) store.commit('mailbox/SET_DETAIL_CACHE', item)
|
||||
} catch (e) {
|
||||
console.warn('[mailbox/detail] loadOne failed:', e)
|
||||
|
||||
@ -23,7 +23,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { onLoad, onShow, onHide } from '@dcloudio/uni-app'
|
||||
import { useStore } from 'vuex'
|
||||
// import Header from '../components/Header.vue'
|
||||
import MailboxGroup from './components/MailboxGroup.vue'
|
||||
import { useMailboxCenter } from './composables/useMailboxCenter.js'
|
||||
@ -35,8 +36,11 @@ const {
|
||||
onMarkAllRead, onClearAll,
|
||||
onSelectItem, onLongPress,
|
||||
isCollapsed, toggleCollapse,
|
||||
onMarkRead,
|
||||
} = useMailboxCenter()
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const goBack = () => {
|
||||
// 获取页面栈
|
||||
const pages = getCurrentPages();
|
||||
@ -51,7 +55,34 @@ const goBack = () => {
|
||||
}
|
||||
};
|
||||
|
||||
onShow(() => { loadAll() })
|
||||
// P1-5 fix:App.vue 推送 click 带 ?focus=mailbox&nid=N&type=T 跳进来,
|
||||
// 这里读 query → 标记已读 → 跳到详情。让"点推送"直达具体消息,不再只是"进收件箱"。
|
||||
// 用 composable 已有的 onMarkRead(nid, type),不走 store 的同名 action(不存在)。
|
||||
let focusHandled = false
|
||||
onLoad((options) => {
|
||||
if (!options || focusHandled) return
|
||||
const nid = Number(options.nid)
|
||||
const type = options.type || ''
|
||||
if (nid && type) {
|
||||
focusHandled = true
|
||||
// fire-and-forget:失败仅 warn,不影响用户进站
|
||||
onMarkRead(nid, type).catch((e) => {
|
||||
console.warn('[mailbox/index] markAsRead from push failed:', e)
|
||||
})
|
||||
uni.navigateTo({ url: `/pages/mailbox/detail?nid=${nid}&type=${encodeURIComponent(type)}` })
|
||||
}
|
||||
})
|
||||
|
||||
// P1-2 fix:用户停留在收件箱页时,推送来要 PREPEND_ITEM(否则只剩角标更新、列表无新条目)。
|
||||
// onShow 置 SET_IS_IN_PAGE=true,onHide=false。App.vue 走 mailbox/applyPushPayload 时,
|
||||
// mailbox store 的 applyPushPayload 会读 rootState.mailbox.isInMailboxPage 决定是否 PREPEND_ITEM。
|
||||
onShow(() => {
|
||||
store.commit('mailbox/SET_IS_IN_PAGE', true)
|
||||
loadAll()
|
||||
})
|
||||
onHide(() => {
|
||||
store.commit('mailbox/SET_IS_IN_PAGE', false)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user