fix:修改bug

This commit is contained in:
zerosaturation 2026-07-15 13:12:27 +08:00
parent 365f783ea0
commit 18aff3b682
8 changed files with 2129 additions and 1781 deletions

View File

@ -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 {

View File

@ -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,

View File

@ -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

View File

@ -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 })

View File

@ -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)

View File

@ -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=falseApp.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>