diff --git a/frontend/pages.json b/frontend/pages.json
index 430c843..2982b4b 100644
--- a/frontend/pages.json
+++ b/frontend/pages.json
@@ -203,6 +203,15 @@
}
}
},
+ {
+ "path": "pages/castlove/create",
+ "style": {
+ "navigationStyle": "custom",
+ "app-plus": {
+ "bounce": "none"
+ }
+ }
+ },
{
"path": "pages/castlove/craft-select",
"style": {
diff --git a/frontend/pages/ai-dazi/index.vue b/frontend/pages/ai-dazi/index.vue
index 3d20e09..eedb990 100644
--- a/frontend/pages/ai-dazi/index.vue
+++ b/frontend/pages/ai-dazi/index.vue
@@ -112,11 +112,19 @@ isErrorProcessing: false // 防止错误重复触发
},
onLoad() {
this.initSession()
- this.initWebSocket()
},
onReady() {
console.log('[AI Chat] onReady called')
},
+ onShow() {
+ // 页面可见时连接 WebSocket(包括首次进入和从其他页面返回)
+ this.initWebSocket()
+ },
+ onHide() {
+ // 页面不可见时断开,不再占用连接
+ this.clearStreamTimeout()
+ this.closeWebSocket()
+ },
onUnload() {
this.clearStreamTimeout()
this.closeWebSocket()
@@ -196,11 +204,10 @@ isErrorProcessing: false // 防止错误重复触发
})
// 设置断开回调 - 在对话框提示用户
- socket.off('disconnect', this._disconnectHandler)
+ const prevDisconnect = this._disconnectHandler
this._disconnectHandler = () => {
console.log('[AI Chat] WebSocket disconnected')
if (this.isTyping) {
- // 先保存已收到的部分回复
if (this.currentAssistantMessage) {
this.messages.push({
role: 'assistant',
@@ -217,6 +224,7 @@ isErrorProcessing: false // 防止错误重复触发
this.scrollToBottom()
}
}
+ socket.off('disconnect', prevDisconnect)
socket.on('disconnect', this._disconnectHandler)
// 设置连接成功回调
diff --git a/frontend/pages/castlove/craft-select.vue b/frontend/pages/castlove/craft-select.vue
index a39705f..30097a7 100644
--- a/frontend/pages/castlove/craft-select.vue
+++ b/frontend/pages/castlove/craft-select.vue
@@ -410,7 +410,7 @@ export default {
fail: (err) => {
// 路径写错 / pages.json 未注册 → 兜底 toast,不闪退
console.error("[castlove] navigateTo fail", card.route_path, err);
- uni.showToast({ title: "页面不存在", icon: "none" });
+ uni.showToast({ title: "功能待开发", icon: "none" });
},
});
},
diff --git a/frontend/pages/castlove/create-laser-upload.css b/frontend/pages/castlove/create-laser-upload.css
new file mode 100644
index 0000000..a26a70f
--- /dev/null
+++ b/frontend/pages/castlove/create-laser-upload.css
@@ -0,0 +1,117 @@
+/**
+ * 铸爱 create 页 — 镭射卡上传区样式(独立 CSS,由 create.vue import,确保各端构建必进包)
+ * 根类 castlove-laser-upload-root 全局唯一,避免 scoped / 第二 style 块在部分端未生效
+ */
+.castlove-laser-upload-root {
+ position: relative;
+ width: 100%;
+ /* 略偏竖版小卡比例,接近设计稿「大卡 + 内虚线」观感 */
+ height: 520rpx;
+ box-sizing: border-box;
+ border-radius: 32rpx;
+ border: 1rpx solid rgba(255, 255, 255, 0.38);
+ background: rgba(255, 255, 255, 0.18);
+ backdrop-filter: blur(22rpx);
+ -webkit-backdrop-filter: blur(22rpx);
+ overflow: hidden;
+ box-shadow:
+ 0 22rpx 56rpx rgba(40, 22, 80, 0.36),
+ inset 0 0 0 1rpx rgba(255, 255, 255, 0.22);
+}
+
+.castlove-laser-upload-root .uploaded-image {
+ width: 100%;
+ height: 100%;
+ border-radius: 24rpx;
+ box-shadow: inset 0 0 0 1rpx rgba(255, 255, 255, 0.22);
+}
+
+.castlove-laser-upload-root .upload-laser-stack {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ box-sizing: border-box;
+}
+
+/* 与设计稿一致:卡内淡色「卡面 / 人像」底图(与工艺选择同款素材) */
+.castlove-laser-upload-root .upload-laser-photo-bg {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ opacity: 0.22;
+ pointer-events: none;
+ filter: saturate(1.15) contrast(0.92);
+}
+
+.castlove-laser-upload-root .upload-laser-watermark {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 1;
+ opacity: 1;
+ pointer-events: none;
+ /* 叠在底图上:压暗边缘 + 轻微幻彩,避免只剩「灰蒙一块」 */
+ background:
+ linear-gradient(
+ 118deg,
+ rgba(255, 170, 220, 0.12) 0%,
+ transparent 40%,
+ rgba(170, 210, 255, 0.1) 62%,
+ transparent 82%,
+ rgba(255, 210, 190, 0.08) 100%
+ ),
+ radial-gradient(ellipse 55% 75% at 50% 100%, rgba(35, 22, 70, 0.45), transparent 58%),
+ radial-gradient(ellipse 40% 48% at 50% 32%, rgba(100, 75, 150, 0.28), transparent 55%),
+ linear-gradient(185deg, rgba(255, 220, 245, 0.08) 0%, rgba(55, 35, 95, 0.25) 100%);
+}
+
+.castlove-laser-upload-root .upload-laser-inset {
+ position: absolute;
+ /* 收紧留白:虚线框更贴外框,接近设计稿「整卡即相框」 */
+ left: 14rpx;
+ right: 14rpx;
+ top: 14rpx;
+ bottom: 14rpx;
+ z-index: 2;
+ box-sizing: border-box;
+}
+
+.castlove-laser-upload-root .upload-laser-dashed-inner {
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+ border: 2rpx dashed rgba(255, 255, 255, 0.95);
+ border-radius: 26rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 20rpx;
+ padding: 24rpx 16rpx;
+ background: rgba(255, 255, 255, 0.02);
+}
+
+.castlove-laser-upload-root .upload-plus--laser {
+ font-size: 96rpx;
+ line-height: 0.88;
+ color: #ffffff;
+ font-weight: 100;
+ text-shadow: 0 2rpx 18rpx rgba(60, 30, 100, 0.35);
+}
+
+.castlove-laser-upload-root .upload-text--laser {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.96);
+ font-weight: 500;
+ letter-spacing: 1rpx;
+ text-shadow: 0 2rpx 12rpx rgba(40, 20, 80, 0.4);
+}
diff --git a/frontend/pages/castlove/create.vue b/frontend/pages/castlove/create.vue
new file mode 100644
index 0000000..3070683
--- /dev/null
+++ b/frontend/pages/castlove/create.vue
@@ -0,0 +1,1560 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 背景图
+
+
+
+
+
+
+
+ 主体图
+
+
+
+ 请各上传一张:JPG / PNG,单张不超过 5MB
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ 点击上传图片
+
+
+
+
+
+ 点击上传图片
+
+
+
+
+ {{ isLaserCardCraft ? '支持JPG.PNG格式,大小不超过5M' : '支持JPG、PNG格式,大小不超过5MB' }}
+
+
+
+
+
+
+ 素材类型
+
+
+ {{ materialTypes[materialTypeIndex] }}
+ {{ isLaserCardCraft ? '▼' : '›' }}
+
+
+
+ {{ type }}
+ ✓
+
+
+
+
+
+
+
+ 藏品信息
+
+
+
+
+
+
+ {{ isLaserCardCraft ?
+ 'AI生成描述' : 'AI 生成描述' }}
+
+
+
+
+
+
+
+
+ 发送
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/pages/castlove/laser/laser-result.vue b/frontend/pages/castlove/laser/laser-result.vue
index 0f0e2d3..ff51786 100644
--- a/frontend/pages/castlove/laser/laser-result.vue
+++ b/frontend/pages/castlove/laser/laser-result.vue
@@ -106,7 +106,8 @@ const handleSelect = (idx) => {
}
const handleBack = () => {
- uni.navigateBack({ delta: 1 })
+ // uni.navigateBack({ delta: 1 })
+ uni.redirectTo({ url: `/pages/castlove/create?name=${encodeURIComponent('镭射卡')}` })
}
// 礼盒打开后:白光扫光 → 选中默认卡(克制,无彩虹过曝)
diff --git a/frontend/pages/profile/profile.vue b/frontend/pages/profile/profile.vue
index fd9618e..90c739d 100644
--- a/frontend/pages/profile/profile.vue
+++ b/frontend/pages/profile/profile.vue
@@ -821,10 +821,10 @@ const fetchUserInfo = async (forceRefresh = false) => {
}
}
- uni.showToast({
- title: error.message || "获取用户信息失败",
- icon: "none",
- });
+ // uni.showToast({
+ // title: error.message || "获取用户信息失败",
+ // icon: "none",
+ // });
} finally {
loading.value = false;
}
diff --git a/frontend/pages/support-activity/composables/useContributionRealtime.js b/frontend/pages/support-activity/composables/useContributionRealtime.js
index 6dec82b..e335836 100644
--- a/frontend/pages/support-activity/composables/useContributionRealtime.js
+++ b/frontend/pages/support-activity/composables/useContributionRealtime.js
@@ -106,7 +106,8 @@ export function useContributionRealtime(activityId, isPageActive) {
// contribution channel 也能用新 token 重新订阅,而不是复用上一个用户的 WS。
const token = uni.getStorageSync('access_token')
if (token) {
- socket.connect(token).catch(err => console.warn('[useContributionRealtime] connect error:', err))
+ // 连接统一由页面 onShow 管理
+ // socket.connect(token).catch(err => console.warn('[useContributionRealtime] connect error:', err))
}
// 同步分支:如果 WS 已连接(单例复用导致 'connect' 事件不会再次触发)且 pubsub 可用,
// 必须直接调 onWsConnect 停轮询,否则 polling 会一直跑。
diff --git a/frontend/pages/support-activity/composables/useMessageRealtime.js b/frontend/pages/support-activity/composables/useMessageRealtime.js
index e9a0d42..86bbf04 100644
--- a/frontend/pages/support-activity/composables/useMessageRealtime.js
+++ b/frontend/pages/support-activity/composables/useMessageRealtime.js
@@ -92,7 +92,8 @@ export function useMessageRealtime(activityId) {
// 解决"另一个账户没有接收到实时推送"的问题。
const token = uni.getStorageSync('access_token')
if (token) {
- socket.connect(token).catch(err => console.warn('[useMessageRealtime] connect error:', err))
+ // 连接统一由页面 onShow 管理
+ // socket.connect(token).catch(err => console.warn('[useMessageRealtime] connect error:', err))
}
// 同步分支:如果 WS 已连接(单例复用,另一个 composable 可能先连上了),
// subscribe 会直接发送;否则缓存到 _topics,等待 connect 事件触发 resubscribeAll
diff --git a/frontend/pages/support-activity/index.vue b/frontend/pages/support-activity/index.vue
index 77c2ac0..6d3d06d 100644
--- a/frontend/pages/support-activity/index.vue
+++ b/frontend/pages/support-activity/index.vue
@@ -208,7 +208,7 @@ import TopRanking from "./components/TopRanking.vue";
import MessageBoard from "./components/MessageBoard.vue";
import MessageInput from "./components/MessageInput.vue";
import { getEarningsSummaryApi } from "@/utils/api.js";
-import { closeActivitySocket } from "@/utils/socket/ActivitySocket.js";
+import { getActivitySocket, closeActivitySocket } from "@/utils/socket/ActivitySocket.js";
import { useMessageRealtime } from "./composables/useMessageRealtime.js";
import { useContributionRealtime } from "./composables/useContributionRealtime.js";
@@ -551,7 +551,6 @@ async function preloadCriticalAssets() {
return new Promise((resolve) => {
const startTime = Date.now();
let isResolved = false;
- const img = new Image();
// 超时定时器
const timeoutId = setTimeout(() => {
@@ -562,27 +561,28 @@ async function preloadCriticalAssets() {
}
}, 3000);
- img.onload = () => {
- if (!isResolved) {
- isResolved = true;
- clearTimeout(timeoutId);
- const loadTime = Date.now() - startTime;
- performanceMonitor.recordImageLoad(true, loadTime);
- resolve();
- }
- };
-
- img.onerror = () => {
- if (!isResolved) {
- isResolved = true;
- clearTimeout(timeoutId);
- const loadTime = Date.now() - startTime;
- performanceMonitor.recordImageLoad(false, loadTime);
- resolve(); // 即使失败也继续
- }
- };
-
- img.src = src;
+ // uni-app 环境使用 uni.getImageInfo 代替 new Image(),兼容 App/iOS/Android
+ uni.getImageInfo({
+ src,
+ success: () => {
+ if (!isResolved) {
+ isResolved = true;
+ clearTimeout(timeoutId);
+ const loadTime = Date.now() - startTime;
+ performanceMonitor.recordImageLoad(true, loadTime);
+ resolve();
+ }
+ },
+ fail: () => {
+ if (!isResolved) {
+ isResolved = true;
+ clearTimeout(timeoutId);
+ const loadTime = Date.now() - startTime;
+ performanceMonitor.recordImageLoad(false, loadTime);
+ resolve(); // 即使失败也继续
+ }
+ },
+ });
});
});
@@ -609,19 +609,18 @@ function preloadNonCriticalAssets() {
nonCriticalImages.forEach((src) => {
if (src) {
const startTime = Date.now();
- const img = new Image();
-
- img.onload = () => {
- const loadTime = Date.now() - startTime;
- performanceMonitor.recordImageLoad(true, loadTime);
- };
-
- img.onerror = () => {
- const loadTime = Date.now() - startTime;
- performanceMonitor.recordImageLoad(false, loadTime);
- };
-
- img.src = src;
+ // uni-app 环境使用 uni.getImageInfo 代替 new Image(),兼容 App/iOS/Android
+ uni.getImageInfo({
+ src,
+ success: () => {
+ const loadTime = Date.now() - startTime;
+ performanceMonitor.recordImageLoad(true, loadTime);
+ },
+ fail: () => {
+ const loadTime = Date.now() - startTime;
+ performanceMonitor.recordImageLoad(false, loadTime);
+ },
+ });
}
});
}, 1000); // 延迟1秒后开始加载
@@ -652,8 +651,13 @@ onLoad(async (options) => {
onShow(() => {
isPageActive.value = true;
+ // 进入活动页面时连接 Activity socket
+ const token = uni.getStorageSync("access_token");
+ if (token) {
+ getActivitySocket().connect(token);
+ }
+
if (!isLoading.value && !errorMessage.value && progressManager) {
- // 使用 resumePolling 而不是 startPolling,避免重复启动
progressManager.resumePolling();
}
@@ -661,6 +665,9 @@ onShow(() => {
});
onHide(() => {
+ // 离开活动页面时断开 Activity socket
+ closeActivitySocket();
+
isPageActive.value = false;
// 使用 pausePolling 暂停轮询,而不是完全停止
diff --git a/frontend/utils/laser-card/laserPreviewWebgl.js b/frontend/utils/laser-card/laserPreviewWebgl.js
index 90dc609..439f89c 100644
--- a/frontend/utils/laser-card/laserPreviewWebgl.js
+++ b/frontend/utils/laser-card/laserPreviewWebgl.js
@@ -486,6 +486,33 @@ export function loadTextureImage(src, canvasForCreateImage) {
/* fall through */
}
}
+ // uni-app 环境使用 uni.getImageInfo 代替 new Image(),兼容 App/iOS/Android
+ if (typeof uni !== 'undefined' && typeof uni.getImageInfo === 'function') {
+ try {
+ uni.getImageInfo({
+ src,
+ success: (res) => {
+ const localPath = res.path || src
+ const offC = typeof uni.createOffscreenCanvas === 'function'
+ ? uni.createOffscreenCanvas({ type: '2d', width: 1, height: 1 })
+ : null
+ if (offC && typeof offC.createImage === 'function') {
+ const img = offC.createImage()
+ img.onload = () => resolve(img)
+ img.onerror = () => reject(new Error('createImage load'))
+ img.src = localPath
+ } else {
+ reject(new Error('createOffscreenCanvas failed'))
+ }
+ },
+ fail: (err) => reject(new Error('getImageInfo: ' + JSON.stringify(err)))
+ })
+ return
+ } catch (e) {
+ reject(e)
+ return
+ }
+ }
if (typeof Image !== 'undefined') {
try {
const img = new Image()