Compare commits
113 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
5ea0848759 | 4 weeks ago |
|
|
09904980a2 | 4 weeks ago |
|
|
5020a8c59e | 4 weeks ago |
|
|
1a8c18092c | 4 weeks ago |
|
|
fd9e25d41a | 4 weeks ago |
|
|
c1b591d205 | 4 weeks ago |
|
|
3fd3fde8e6 | 4 weeks ago |
|
|
a804917469 | 4 weeks ago |
|
|
45d50826b9 | 4 weeks ago |
|
|
ad1a308d47 | 4 weeks ago |
|
|
09af5062f8 | 4 weeks ago |
|
|
138b3bc47d | 4 weeks ago |
|
|
72e6338cbe | 4 weeks ago |
|
|
1951e9da29 | 4 weeks ago |
|
|
9e4a60001e | 4 weeks ago |
|
|
03fefae930 | 4 weeks ago |
|
|
460743dd48 | 4 weeks ago |
|
|
a665d4e9ea | 4 weeks ago |
|
|
b9d1d0100a | 4 weeks ago |
|
|
12a4244135 | 4 weeks ago |
|
|
d6b67b3ef8 | 4 weeks ago |
|
|
bcad6e02b6 | 4 weeks ago |
|
|
cfd0d1432b | 4 weeks ago |
|
|
d765a35a34 | 4 weeks ago |
|
|
11cee468e7 | 4 weeks ago |
|
|
7da46b930a | 4 weeks ago |
|
|
0bfede96e3 | 4 weeks ago |
|
|
28e279af9e | 4 weeks ago |
|
|
d855284c2c | 1 month ago |
|
|
973f48eb2e | 1 month ago |
|
|
8e5eea7ada | 1 month ago |
|
|
1a5b3f5042 | 1 month ago |
|
|
53264cb6e4 | 1 month ago |
|
|
a7fdf12ff1 | 1 month ago |
|
|
7dfdd2b122 | 1 month ago |
|
|
ce787c5f92 | 1 month ago |
|
|
a5173c37a5 | 1 month ago |
|
|
a09e24ddcd | 1 month ago |
|
|
09290d8d06 | 1 month ago |
|
|
e68764777f | 1 month ago |
|
|
42149b1d87 | 1 month ago |
|
|
a5f6bf336e | 1 month ago |
|
|
9a49278cde | 1 month ago |
|
|
9b45b1f6c5 | 1 month ago |
|
|
1c97afb5c0 | 1 month ago |
|
|
5a8a05811b | 1 month ago |
|
|
407b3114d0 | 1 month ago |
|
|
e24a43ffc2 | 1 month ago |
|
|
95bff6ce59 | 1 month ago |
|
|
3cc7deb10d | 1 month ago |
|
|
e8bf953b6b | 1 month ago |
|
|
ff20f2b935 | 1 month ago |
|
|
78d226bcc6 | 1 month ago |
|
|
d66e080a60 | 1 month ago |
|
|
b068045b87 | 1 month ago |
|
|
9514e33cca | 1 month ago |
|
|
8cd5aedce3 | 1 month ago |
|
|
40b2f2bd55 | 1 month ago |
|
|
5706972ee0 | 1 month ago |
|
|
e062173183 | 1 month ago |
|
|
6add555b45 | 1 month ago |
|
|
908fce5bf3 | 2 months ago |
|
|
3b8d309468 | 2 months ago |
|
|
2416e88962 | 2 months ago |
|
|
e65ac2b6a0 | 2 months ago |
|
|
c3ebf719d3 | 2 months ago |
|
|
3e370b580f | 2 months ago |
|
|
d418613a1c | 2 months ago |
|
|
12dc7404cd | 2 months ago |
|
|
ca86d667d5 | 2 months ago |
|
|
fda0c0e377 | 2 months ago |
|
|
340708bbdd | 2 months ago |
|
|
6cf9d9acdf | 2 months ago |
|
|
5de45a14db | 2 months ago |
|
|
4bcf2dd472 | 2 months ago |
|
|
707486b390 | 2 months ago |
|
|
c35152900a | 2 months ago |
|
|
f889055866 | 2 months ago |
|
|
d3249c5098 | 2 months ago |
|
|
940af4ba24 | 2 months ago |
|
|
28e63d21de | 2 months ago |
|
|
90766478be | 2 months ago |
|
|
3d984d15af | 2 months ago |
|
|
94e34bd3e8 | 2 months ago |
|
|
0d170b8380 | 2 months ago |
|
|
a9748db0c6 | 2 months ago |
|
|
68677fa7e5 | 2 months ago |
|
|
b0e4b1b602 | 2 months ago |
|
|
8e8ab6802e | 2 months ago |
|
|
c9d860d375 | 2 months ago |
|
|
56b77a075a | 2 months ago |
|
|
a148b283c0 | 2 months ago |
|
|
96c4555066 | 2 months ago |
|
|
fcb33a7163 | 2 months ago |
|
|
6bb401b366 | 2 months ago |
|
|
4426281ca6 | 2 months ago |
|
|
2767b35236 | 2 months ago |
|
|
d4cae0e13c | 2 months ago |
|
|
79849ceeb8 | 2 months ago |
|
|
2dbbda432f | 2 months ago |
|
|
23fd3a9a8c | 2 months ago |
|
|
9d661fa8ca | 2 months ago |
|
|
7f3c089597 | 3 months ago |
|
|
6544b2537f | 3 months ago |
|
|
5a5380710e | 3 months ago |
|
|
6d233d6a8d | 3 months ago |
|
|
0098af14d3 | 3 months ago |
|
|
4489fc5874 | 3 months ago |
|
|
f5cf6a1eea | 3 months ago |
|
|
7c5e7c19df | 3 months ago |
|
|
0863897eca | 3 months ago |
|
|
402de12fb7 | 3 months ago |
|
|
1b67138962 | 3 months ago |
217 changed files with 77699 additions and 8198 deletions
@ -0,0 +1 @@ |
|||||
|
{"projectName":"trae_l237eu51"} |
||||
@ -1,208 +1,416 @@ |
|||||
<script> |
<script> |
||||
export default { |
import store from "./store"; |
||||
globalData: { |
export default { |
||||
mainSliderIndex: 0, |
globalData: { |
||||
randomImages: [], |
mainSliderIndex: 0, |
||||
bgMusic: null, |
randomImages: [], |
||||
isMusicPlaying: false, |
bgMusic: null, |
||||
musicSrc: 'https://static.ticket.sz-trip.com/epicSoul/EpicSouls.mp3', |
isMusicPlaying: false, |
||||
currentAudio: null // 全局音频实例 |
musicSrc: "https://des.dayunyuanjian.cn/epicSoul/EpicSouls.mp3", |
||||
}, |
initMusicSrc: "https://des.dayunyuanjian.cn/epicSoul/EpicSouls.mp3", |
||||
onLaunch: function() { |
// 用户使用统计相关 |
||||
console.warn('当前组件仅支持 uni_modules 目录结构 ,请升级 HBuilderX 到 3.1.0 版本以上!') |
userSessionId: null, |
||||
console.log('App Launch') |
networkStartTime: null, // 网络时间开始时间 |
||||
// 移除初始化背景音乐的调用 |
networkEndTime: null, // 网络时间结束时间 |
||||
// this.initBackgroundMusic(); |
currentAudio: null, // 全局音频实例 |
||||
// 审核 |
}, |
||||
this.Post({id: 10217},'/api/article/getArticleById').then(res => { |
onLaunch: function () { |
||||
try { |
// 初始化用户使用统计 |
||||
let SHFlag = res.data.title |
this.initUserUsageStats(); |
||||
// let SHFlag = res.data.subtitle |
|
||||
uni.setStorageSync('SHFlag', SHFlag) |
// 重试上报本地存储的使用统计数据 |
||||
} catch(e) {} |
this.retryReportLocalStats(); |
||||
}); |
|
||||
}, |
// 移除初始化背景音乐的调用 |
||||
onShow: function() { |
// this.initBackgroundMusic(); |
||||
console.log('App Show') |
// 审核 |
||||
}, |
this.Post({ id: 10217 }, "/api/article/getArticleById").then((res) => { |
||||
onHide: function() { |
try { |
||||
console.log('App Hide') |
let SHFlag = res.data.title; |
||||
}, |
// let SHFlag = res.data.subtitle |
||||
methods: { |
uni.setStorageSync("SHFlag", SHFlag); |
||||
initBackgroundMusic() { |
} catch (e) {} |
||||
try { |
}); |
||||
console.log('bgMusic',this.globalData.bgMusic) |
}, |
||||
// 销毁旧的音频实例(关键!) |
onShow: function () { |
||||
if (this.globalData.bgMusic) { |
// 记录应用显示时间(重新进入小程序) |
||||
this.globalData.bgMusic.stop(); |
this.recordAppShow(); |
||||
this.globalData.bgMusic.destroy() |
this.getUserInfo(); |
||||
this.globalData.bgMusic = null; |
}, |
||||
} |
onHide: function () { |
||||
|
// 记录应用隐藏时间(退出小程序) |
||||
let bgMusic; |
this.recordAppHide(); |
||||
|
}, |
||||
// 区分平台 - 小程序环境使用背景音频,H5等环境使用内部音频 |
methods: { |
||||
// #ifdef MP-WEIXIN |
getUserInfo() { |
||||
// try { |
if (!this.getUserId()) return; |
||||
// bgMusic = uni.getBackgroundAudioManager(); |
this.Post({}, "/framework/user/getInfo", "DES").then((res) => { |
||||
// // 小程序环境需要设置title |
let token = JSON.parse(uni.getStorageSync("userInfo")).token; |
||||
// bgMusic.title = '背景音乐'; |
res.data.token = token; |
||||
// // 设置音频组件的 ID |
uni.setStorageSync("userInfo", JSON.stringify(res.data)); |
||||
// bgMusic.id = 'mp-audio'; |
}); |
||||
// } catch (e) { |
}, |
||||
// console.error('获取背景音频管理器失败,改用内部音频上下文', e); |
// 初始化用户使用统计 |
||||
// bgMusic = uni.createInnerAudioContext(); |
initUserUsageStats() { |
||||
// } |
// 生成会话ID |
||||
bgMusic = uni.createInnerAudioContext(); |
this.globalData.userSessionId = this.generateSessionId(); |
||||
// #endif |
}, |
||||
|
|
||||
// #ifndef MP-WEIXIN |
// 生成会话ID |
||||
bgMusic = uni.createInnerAudioContext(); |
generateSessionId() { |
||||
// #endif |
const timestamp = Date.now(); |
||||
|
const random = Math.random().toString(36).substring(2, 15); |
||||
// 配置音频 |
return `session_${timestamp}_${random}`; |
||||
bgMusic.src = this.globalData.musicSrc; |
}, |
||||
console.log(bgMusic.src) |
|
||||
bgMusic.loop = true; // 循环播放 |
// 记录应用显示时间 - 获取网络时间作为开始时间 |
||||
|
recordAppShow() { |
||||
// 使用不同的事件监听方式,兼容两种音频上下文 |
// 获取网络时间作为开始时间 |
||||
if (bgMusic.onPlay) { |
this.getNetworkTime() |
||||
// BackgroundAudioManager 方式 |
.then((networkTime) => { |
||||
bgMusic.onPlay(() => { |
this.globalData.networkStartTime = networkTime; |
||||
this.globalData.isMusicPlaying = true; |
}) |
||||
}); |
.catch((err) => { |
||||
|
// 获取网络时间失败,静默处理 |
||||
bgMusic.onPause(() => { |
}); |
||||
this.globalData.isMusicPlaying = false; |
}, |
||||
}); |
|
||||
|
// 记录应用隐藏时间 - 获取网络时间作为结束时间 |
||||
bgMusic.onStop(() => { |
recordAppHide() { |
||||
this.globalData.isMusicPlaying = false; |
// 获取网络时间作为结束时间 |
||||
}); |
this.getNetworkTime() |
||||
|
.then((networkTime) => { |
||||
bgMusic.onEnded(() => { |
this.globalData.networkEndTime = networkTime; |
||||
// 循环播放 (BackgroundAudioManager需要重新设置src) |
// 如果使用时长超过1秒,则上报统计数据 |
||||
bgMusic.src = this.globalData.musicSrc; |
this.reportUserUsageStats(); |
||||
bgMusic.play(); |
}) |
||||
}); |
.catch((err) => { |
||||
} else { |
// 获取网络时间失败,静默处理 |
||||
// InnerAudioContext 方式 |
}); |
||||
bgMusic.onPlay(() => { |
}, |
||||
this.globalData.isMusicPlaying = true; |
getUserId() { |
||||
}); |
const userInfoFromStorage = uni.getStorageSync("userInfo"); |
||||
|
if (userInfoFromStorage) { |
||||
bgMusic.onPause(() => { |
const userInfo = JSON.parse(userInfoFromStorage); |
||||
this.globalData.isMusicPlaying = false; |
if (userInfo.id) { |
||||
}); |
return userInfo.id; |
||||
|
} |
||||
bgMusic.onStop(() => { |
} |
||||
this.globalData.isMusicPlaying = false; |
return store.state.user.userInfo.id; |
||||
}); |
}, |
||||
|
|
||||
bgMusic.onEnded(() => { |
// 上报用户使用统计数据 |
||||
// InnerAudioContext设置了loop不需要手动重新播放 |
reportUserUsageStats() { |
||||
this.globalData.isMusicPlaying = false; |
if ( |
||||
}); |
!this.globalData.networkStartTime || |
||||
} |
!this.globalData.networkEndTime |
||||
|
) { |
||||
// 保存到全局 |
return; |
||||
this.globalData.bgMusic = bgMusic; |
} |
||||
|
let userId = this.getUserId(); |
||||
// 创建全局方法供其他页面调用 |
const usageData = { |
||||
uni.$bgMusic = { |
sessionId: this.globalData.userSessionId, |
||||
play: () => { |
startTime: this.globalData.networkStartTime.toString(), |
||||
if (bgMusic && bgMusic.play) { |
endTime: this.globalData.networkEndTime.toString(), |
||||
bgMusic.play(); |
userId: userId, |
||||
} |
method: "POST", |
||||
return this.globalData.isMusicPlaying; |
}; |
||||
}, |
if (!userId) { |
||||
pause: () => { |
this.saveUsageStatsToLocal(usageData); |
||||
if (bgMusic && bgMusic.pause) { |
return; |
||||
bgMusic.pause(); |
} |
||||
} |
|
||||
return this.globalData.isMusicPlaying; |
// 调用接口上报数据 |
||||
}, |
this.Post(usageData, "/api/visit/end", "DES") |
||||
toggle: () => { |
.then((res) => { |
||||
if (!bgMusic) return false; |
// 上报成功后清理数据 |
||||
|
this.clearUsageStats(); |
||||
if (this.globalData.isMusicPlaying) { |
}) |
||||
if (bgMusic.pause) bgMusic.pause(); |
.catch((err) => { |
||||
} else { |
// 上报失败时,将数据存储到本地,下次启动时重试 |
||||
if (bgMusic.play) bgMusic.play(); |
this.saveUsageStatsToLocal(usageData); |
||||
} |
}); |
||||
return this.globalData.isMusicPlaying; |
}, |
||||
}, |
|
||||
isPlaying: () => this.globalData.isMusicPlaying |
// 获取平台信息 |
||||
}; |
getPlatform() { |
||||
} catch (err) { |
// #ifdef MP-WEIXIN |
||||
console.error('初始化背景音乐失败:', err); |
return "weixin"; |
||||
} |
// #endif |
||||
}, |
|
||||
updateMusicSrc(newSrc) { |
// #ifdef H5 |
||||
this.globalData.musicSrc = newSrc; |
return "h5"; |
||||
if (this.globalData.bgMusic) { |
// #endif |
||||
this.globalData.bgMusic.src = newSrc; |
|
||||
} |
// #ifdef APP-PLUS |
||||
} |
return "app"; |
||||
} |
// #endif |
||||
} |
|
||||
|
return "unknown"; |
||||
|
}, |
||||
|
|
||||
|
// 获取网络时间 |
||||
|
getNetworkTime() { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
// 调用服务器接口获取网络时间 |
||||
|
this.Post({}, "/api/visit/currentTime", "DES") |
||||
|
.then((res) => { |
||||
|
if (res.code == 1 || res.code == 200) { |
||||
|
// 假设接口返回的时间戳字段为 serverTime |
||||
|
const networkTime = res.data; |
||||
|
resolve(networkTime); |
||||
|
} else { |
||||
|
reject(new Error(res.msg || "获取网络时间失败")); |
||||
|
} |
||||
|
}) |
||||
|
.catch((err) => { |
||||
|
reject(err); |
||||
|
}); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 获取设备信息 |
||||
|
getDeviceInfo() { |
||||
|
try { |
||||
|
const systemInfo = uni.getSystemInfoSync(); |
||||
|
return { |
||||
|
model: systemInfo.model || "", |
||||
|
system: systemInfo.system || "", |
||||
|
platform: systemInfo.platform || "", |
||||
|
version: systemInfo.version || "", |
||||
|
}; |
||||
|
} catch (e) { |
||||
|
return {}; |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 清理使用统计数据 |
||||
|
clearUsageStats() { |
||||
|
this.globalData.networkStartTime = null; |
||||
|
this.globalData.networkEndTime = null; |
||||
|
this.globalData.userSessionId = null; |
||||
|
}, |
||||
|
|
||||
|
// 保存使用统计数据到本地 |
||||
|
saveUsageStatsToLocal(usageData) { |
||||
|
try { |
||||
|
const localStats = uni.getStorageSync("pendingUsageStats") || []; |
||||
|
localStats.push(usageData); |
||||
|
uni.setStorageSync("pendingUsageStats", localStats); |
||||
|
} catch (e) { |
||||
|
// 保存失败,静默处理 |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 重试上报本地存储的使用统计数据 |
||||
|
retryReportLocalStats() { |
||||
|
try { |
||||
|
const localStats = uni.getStorageSync("pendingUsageStats") || []; |
||||
|
if (localStats.length === 0) { |
||||
|
return; |
||||
|
} |
||||
|
let userId = this.getUserId(); |
||||
|
if (!userId) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 逐个上报 |
||||
|
localStats.forEach((stats, index) => { |
||||
|
stats.userId = userId; |
||||
|
this.Post(stats, "/api/visit/end", "DES") |
||||
|
.then((res) => { |
||||
|
// 上报成功后从本地移除 |
||||
|
localStats.splice(index, 1); |
||||
|
uni.setStorageSync("pendingUsageStats", localStats); |
||||
|
}) |
||||
|
.catch((err) => { |
||||
|
// 上报失败,静默处理 |
||||
|
}); |
||||
|
}); |
||||
|
} catch (e) { |
||||
|
// 重试失败,静默处理 |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
initBackgroundMusic() { |
||||
|
try { |
||||
|
console.log("bgMusic", this.globalData.bgMusic); |
||||
|
// 销毁旧的音频实例(关键!) |
||||
|
if (this.globalData.bgMusic) { |
||||
|
console.log("销毁bgMusic"); |
||||
|
this.globalData.bgMusic.stop(); |
||||
|
this.globalData.bgMusic.destroy(); |
||||
|
this.globalData.bgMusic = null; |
||||
|
} |
||||
|
|
||||
|
let bgMusic; |
||||
|
|
||||
|
// 区分平台 - 小程序环境使用背景音频,H5等环境使用内部音频 |
||||
|
// #ifdef MP-WEIXIN |
||||
|
// try { |
||||
|
// bgMusic = uni.getBackgroundAudioManager(); |
||||
|
// // 小程序环境需要设置title |
||||
|
// bgMusic.title = '背景音乐'; |
||||
|
// // 设置音频组件的 ID |
||||
|
// bgMusic.id = 'mp-audio'; |
||||
|
// } catch (e) { |
||||
|
// console.error('获取背景音频管理器失败,改用内部音频上下文', e); |
||||
|
// bgMusic = uni.createInnerAudioContext(); |
||||
|
// } |
||||
|
bgMusic = uni.createInnerAudioContext(); |
||||
|
// #endif |
||||
|
|
||||
|
// #ifndef MP-WEIXIN |
||||
|
bgMusic = uni.createInnerAudioContext(); |
||||
|
// #endif |
||||
|
|
||||
|
// 配置音频 |
||||
|
bgMusic.src = this.globalData.musicSrc; |
||||
|
console.log(bgMusic.src); |
||||
|
bgMusic.loop = true; // 循环播放 |
||||
|
|
||||
|
// 使用不同的事件监听方式,兼容两种音频上下文 |
||||
|
if (bgMusic.onPlay) { |
||||
|
// BackgroundAudioManager 方式 |
||||
|
bgMusic.onPlay(() => { |
||||
|
this.globalData.isMusicPlaying = true; |
||||
|
}); |
||||
|
|
||||
|
bgMusic.onPause(() => { |
||||
|
this.globalData.isMusicPlaying = false; |
||||
|
}); |
||||
|
|
||||
|
bgMusic.onStop(() => { |
||||
|
this.globalData.isMusicPlaying = false; |
||||
|
}); |
||||
|
|
||||
|
bgMusic.onEnded(() => { |
||||
|
// 循环播放 (BackgroundAudioManager需要重新设置src) |
||||
|
bgMusic.src = this.globalData.musicSrc; |
||||
|
bgMusic.play(); |
||||
|
}); |
||||
|
} else { |
||||
|
// InnerAudioContext 方式 |
||||
|
bgMusic.onPlay(() => { |
||||
|
this.globalData.isMusicPlaying = true; |
||||
|
}); |
||||
|
|
||||
|
bgMusic.onPause(() => { |
||||
|
this.globalData.isMusicPlaying = false; |
||||
|
}); |
||||
|
|
||||
|
bgMusic.onStop(() => { |
||||
|
this.globalData.isMusicPlaying = false; |
||||
|
}); |
||||
|
|
||||
|
bgMusic.onEnded(() => { |
||||
|
// InnerAudioContext设置了loop不需要手动重新播放 |
||||
|
this.globalData.isMusicPlaying = false; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 保存到全局 |
||||
|
this.globalData.bgMusic = bgMusic; |
||||
|
|
||||
|
// 创建全局方法供其他页面调用 |
||||
|
uni.$bgMusic = { |
||||
|
play: () => { |
||||
|
if (bgMusic && bgMusic.play) { |
||||
|
bgMusic.play(); |
||||
|
} |
||||
|
return this.globalData.isMusicPlaying; |
||||
|
}, |
||||
|
pause: () => { |
||||
|
if (bgMusic && bgMusic.pause) { |
||||
|
bgMusic.pause(); |
||||
|
} |
||||
|
return this.globalData.isMusicPlaying; |
||||
|
}, |
||||
|
toggle: () => { |
||||
|
if (!bgMusic) return false; |
||||
|
|
||||
|
if (this.globalData.isMusicPlaying) { |
||||
|
if (bgMusic.pause) bgMusic.pause(); |
||||
|
} else { |
||||
|
if (bgMusic.play) bgMusic.play(); |
||||
|
} |
||||
|
return this.globalData.isMusicPlaying; |
||||
|
}, |
||||
|
isPlaying: () => this.globalData.isMusicPlaying, |
||||
|
}; |
||||
|
} catch (err) { |
||||
|
console.error("初始化背景音乐失败:", err); |
||||
|
} |
||||
|
}, |
||||
|
updateMusicSrc(newSrc) { |
||||
|
this.globalData.musicSrc = newSrc; |
||||
|
if (this.globalData.bgMusic) { |
||||
|
this.globalData.bgMusic.src = newSrc; |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
</script> |
</script> |
||||
|
|
||||
<style lang="scss"> |
<style lang="scss"> |
||||
/*每个页面公共css */ |
@import "@/static/css/icon.scss"; |
||||
@import '@/uni_modules/uni-scss/index.scss'; |
@font-face { |
||||
@import "@/static/css/base.css"; |
font-family: "Futura"; |
||||
|
src: url(https://des.dayunyuanjian.cn/epicSoul/taozi/fonts/Futura.ttc); |
||||
/* #ifndef APP-NVUE */ |
} |
||||
// 设置整个项目的背景色 |
/*每个页面公共css */ |
||||
page { |
@import "@/uni_modules/uni-scss/index.scss"; |
||||
background-color: #f5f5f5; |
@import "@/static/css/base.css"; |
||||
} |
|
||||
|
/* #ifndef APP-NVUE */ |
||||
/* #endif */ |
// 设置整个项目的背景色 |
||||
.example-info { |
page { |
||||
font-size: 14px; |
background-color: #f5f5f5; |
||||
color: #333; |
} |
||||
padding: 10px; |
|
||||
} |
/* #endif */ |
||||
|
.example-info { |
||||
/* 清除按钮默认样式 */ |
font-size: 14px; |
||||
button::after { |
color: #333; |
||||
border: none; |
padding: 10px; |
||||
} |
} |
||||
|
|
||||
@keyframes bounce { |
/* 清除按钮默认样式 */ |
||||
|
button::after { |
||||
0%, |
border: none; |
||||
20%, |
} |
||||
50%, |
|
||||
80%, |
@keyframes bounce { |
||||
100% { |
0%, |
||||
transform: translateY(0); |
20%, |
||||
} |
50%, |
||||
|
80%, |
||||
40% { |
100% { |
||||
transform: translateY(-20rpx); |
transform: translateY(0); |
||||
} |
} |
||||
|
|
||||
60% { |
40% { |
||||
transform: translateY(-10rpx); |
transform: translateY(-20rpx); |
||||
} |
} |
||||
} |
|
||||
|
60% { |
||||
/* 音乐控制按钮动画 */ |
transform: translateY(-10rpx); |
||||
@keyframes rotate { |
} |
||||
from { |
} |
||||
transform: rotate(0deg); |
|
||||
} |
/* 音乐控制按钮动画 */ |
||||
|
@keyframes rotate { |
||||
to { |
from { |
||||
transform: rotate(360deg); |
transform: rotate(0deg); |
||||
} |
} |
||||
} |
|
||||
|
to { |
||||
/* 隐藏微信小程序默认音频组件 */ |
transform: rotate(360deg); |
||||
#mp-audio { |
} |
||||
display: none; |
} |
||||
} |
|
||||
</style> |
/* 隐藏微信小程序默认音频组件 */ |
||||
|
#mp-audio { |
||||
|
display: none; |
||||
|
} |
||||
|
</style> |
||||
|
|||||
@ -0,0 +1,253 @@ |
|||||
|
# 省市区选择组件使用说明 |
||||
|
|
||||
|
## 概述 |
||||
|
|
||||
|
`AreaPicker.vue` 是一个功能完整的省市区三级联动选择组件,支持自定义样式插槽,可以灵活地集成到各种页面中。 |
||||
|
|
||||
|
## 功能特性 |
||||
|
|
||||
|
- ✅ 省市区三级联动选择 |
||||
|
- ✅ 支持默认值设置 |
||||
|
- ✅ 支持插槽自定义显示样式 |
||||
|
- ✅ 完整的事件回调 |
||||
|
- ✅ 灵活的数据请求方式 |
||||
|
- ✅ 错误处理和容错机制 |
||||
|
|
||||
|
## Props |
||||
|
|
||||
|
| 参数 | 类型 | 默认值 | 说明 | |
||||
|
|------|------|--------|------| |
||||
|
| placeholder | String | '请选择' | 占位符文本 | |
||||
|
| defaultValue | Object | null | 默认选中值,格式:`{ provinceId, cityId, areaId }` | |
||||
|
| disabled | Boolean | false | 是否禁用 | |
||||
|
|
||||
|
## Events |
||||
|
|
||||
|
| 事件名 | 参数 | 说明 | |
||||
|
|--------|------|------| |
||||
|
| change | data | 选择改变时触发,返回选中的省市区信息 | |
||||
|
|
||||
|
### change 事件返回数据格式 |
||||
|
|
||||
|
```javascript |
||||
|
{ |
||||
|
provinceId: '110000', // 省份ID |
||||
|
cityId: '110100', // 城市ID |
||||
|
areaId: '110101', // 区域ID |
||||
|
fullText: '北京市北京市东城区' // 完整地址文本 |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Methods |
||||
|
|
||||
|
| 方法名 | 参数 | 返回值 | 说明 | |
||||
|
|--------|------|--------|------| |
||||
|
| getValue | - | Object | 获取当前选中的值 | |
||||
|
| reset | - | - | 重置选择 | |
||||
|
|
||||
|
## 插槽使用 |
||||
|
|
||||
|
组件提供了默认插槽,允许完全自定义显示样式。插槽提供以下数据: |
||||
|
|
||||
|
| 插槽参数 | 类型 | 说明 | |
||||
|
|----------|------|------| |
||||
|
| selectedText | String | 当前选中的完整文本 | |
||||
|
| placeholder | String | 占位符文本 | |
||||
|
| provinceData | Array | 省份数据列表 | |
||||
|
| cityData | Array | 城市数据列表 | |
||||
|
| areaData | Array | 区域数据列表 | |
||||
|
| multiIndex | Array | 当前选中的索引数组 | |
||||
|
| currentSelection | Object | 当前选中的详细信息 | |
||||
|
|
||||
|
### currentSelection 对象结构 |
||||
|
|
||||
|
```javascript |
||||
|
{ |
||||
|
province: { id: '110000', name: '北京市' }, |
||||
|
city: { id: '110100', name: '北京市' }, |
||||
|
area: { id: '110101', name: '东城区' }, |
||||
|
fullText: '北京市北京市东城区' |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## 使用示例 |
||||
|
|
||||
|
### 基本使用 |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<view> |
||||
|
<!-- 使用默认样式 --> |
||||
|
<AreaPicker |
||||
|
ref="areaPicker" |
||||
|
placeholder="请选择省市区" |
||||
|
:defaultValue="{ provinceId: '110000', cityId: '110100', areaId: '110101' }" |
||||
|
@change="onAreaChange" |
||||
|
/> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
methods: { |
||||
|
onAreaChange(data) { |
||||
|
console.log('选中的地区:', data) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
``` |
||||
|
|
||||
|
### 自定义样式(插槽) |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<view> |
||||
|
<!-- 自定义样式1:简单自定义 --> |
||||
|
<AreaPicker |
||||
|
placeholder="请选择省市区" |
||||
|
@change="onAreaChange" |
||||
|
> |
||||
|
<template v-slot="{ selectedText, placeholder }"> |
||||
|
<view class="custom-display"> |
||||
|
<text class="icon">📍</text> |
||||
|
<text class="text">{{ selectedText || placeholder }}</text> |
||||
|
<text class="arrow">▼</text> |
||||
|
</view> |
||||
|
</template> |
||||
|
</AreaPicker> |
||||
|
|
||||
|
<!-- 自定义样式2:显示详细信息 --> |
||||
|
<AreaPicker |
||||
|
placeholder="请选择省市区" |
||||
|
@change="onAreaChange" |
||||
|
> |
||||
|
<template v-slot="{ selectedText, placeholder, currentSelection }"> |
||||
|
<view class="detail-display"> |
||||
|
<view v-if="currentSelection.province" class="detail-info"> |
||||
|
<text>省:{{ currentSelection.province.name }}</text> |
||||
|
<text>市:{{ currentSelection.city.name }}</text> |
||||
|
<text>区:{{ currentSelection.area.name }}</text> |
||||
|
</view> |
||||
|
<text v-else class="placeholder">{{ placeholder }}</text> |
||||
|
</view> |
||||
|
</template> |
||||
|
</AreaPicker> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
methods: { |
||||
|
onAreaChange(data) { |
||||
|
console.log('选中的地区:', data) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
.custom-display { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
padding: 12px 16px; |
||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
|
border-radius: 25px; |
||||
|
color: white; |
||||
|
} |
||||
|
|
||||
|
.icon { |
||||
|
margin-right: 10px; |
||||
|
} |
||||
|
|
||||
|
.text { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.arrow { |
||||
|
margin-left: 10px; |
||||
|
} |
||||
|
|
||||
|
.detail-display { |
||||
|
padding: 15px; |
||||
|
background-color: #f8f9fa; |
||||
|
border: 2px dashed #dee2e6; |
||||
|
border-radius: 8px; |
||||
|
} |
||||
|
|
||||
|
.detail-info { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 5px; |
||||
|
} |
||||
|
|
||||
|
.placeholder { |
||||
|
color: #adb5bd; |
||||
|
font-style: italic; |
||||
|
} |
||||
|
</style> |
||||
|
``` |
||||
|
|
||||
|
### 在 header 组件中使用 |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<view> |
||||
|
<header |
||||
|
:isSearch="true" |
||||
|
:isAreaPicker="true" |
||||
|
areaPlaceholder="请选择省市区" |
||||
|
:defaultAreaValue="{ provinceId: '110000', cityId: '110100', areaId: '110101' }" |
||||
|
@areaChange="onAreaChange" |
||||
|
/> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
methods: { |
||||
|
onAreaChange(data) { |
||||
|
console.log('选中的地区:', data) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
``` |
||||
|
|
||||
|
## 数据接口要求 |
||||
|
|
||||
|
组件需要后端提供省市区数据接口,接口返回格式如下: |
||||
|
|
||||
|
```javascript |
||||
|
// 省份数据 |
||||
|
[ |
||||
|
{ id: '110000', name: '北京市' }, |
||||
|
{ id: '120000', name: '天津市' }, |
||||
|
// ... |
||||
|
] |
||||
|
|
||||
|
// 城市数据(根据省份ID获取) |
||||
|
[ |
||||
|
{ id: '110100', name: '北京市', pid: '110000' }, |
||||
|
// ... |
||||
|
] |
||||
|
|
||||
|
// 区域数据(根据城市ID获取) |
||||
|
[ |
||||
|
{ id: '110101', name: '东城区', pid: '110100' }, |
||||
|
{ id: '110102', name: '西城区', pid: '110100' }, |
||||
|
// ... |
||||
|
] |
||||
|
``` |
||||
|
|
||||
|
## 注意事项 |
||||
|
|
||||
|
1. 组件会自动处理数据请求的多种方式(父组件 Post 方法、全局 Post 方法、uni.request) |
||||
|
2. 建议在使用前确保数据接口正常可用 |
||||
|
3. 插槽内容会完全替换默认的显示样式 |
||||
|
4. 可以通过 ref 调用组件的 getValue() 和 reset() 方法 |
||||
|
5. 组件支持设置默认值,会自动回显对应的省市区信息 |
||||
|
|
||||
|
## 完整示例 |
||||
|
|
||||
|
查看 `AreaPickerSlotExample.vue` 文件获取完整的使用示例,包含多种自定义样式的演示。 |
||||
@ -0,0 +1,107 @@ |
|||||
|
<template> |
||||
|
<div></div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: 'AudioPlayer', |
||||
|
props: ['src'], |
||||
|
data() { |
||||
|
return { |
||||
|
backgroundAudioManager: null, |
||||
|
currentTime: 0, |
||||
|
duration: 0, |
||||
|
timeUpdateTimer: null |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.backgroundAudioManager = wx.getBackgroundAudioManager() |
||||
|
|
||||
|
// 设置音频信息 |
||||
|
this.backgroundAudioManager.title = '有声电子书' |
||||
|
this.backgroundAudioManager.epname = '章节播放' |
||||
|
this.backgroundAudioManager.singer = 'EpicSoul' |
||||
|
this.backgroundAudioManager.coverImgUrl = '' |
||||
|
|
||||
|
// 监听播放事件 |
||||
|
this.backgroundAudioManager.onPlay(() => { |
||||
|
this.startTimeUpdate() |
||||
|
this.$emit('play') |
||||
|
}) |
||||
|
|
||||
|
// 监听暂停事件 |
||||
|
this.backgroundAudioManager.onPause(() => { |
||||
|
this.stopTimeUpdate() |
||||
|
this.$emit('pause') |
||||
|
}) |
||||
|
|
||||
|
// 监听停止事件 |
||||
|
this.backgroundAudioManager.onStop(() => { |
||||
|
this.stopTimeUpdate() |
||||
|
this.$emit('pause') |
||||
|
}) |
||||
|
|
||||
|
// 监听播放结束事件 |
||||
|
this.backgroundAudioManager.onEnded(() => { |
||||
|
this.stopTimeUpdate() |
||||
|
this.$emit('ended') |
||||
|
}) |
||||
|
|
||||
|
// 监听错误事件 |
||||
|
this.backgroundAudioManager.onError((err) => { |
||||
|
console.error('背景音频播放错误:', err) |
||||
|
this.stopTimeUpdate() |
||||
|
}) |
||||
|
|
||||
|
// 设置音频源 |
||||
|
if (this.src) { |
||||
|
this.backgroundAudioManager.src = this.src |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
play() { |
||||
|
if (this.backgroundAudioManager.src) { |
||||
|
this.backgroundAudioManager.play() |
||||
|
} |
||||
|
}, |
||||
|
pause() { |
||||
|
this.backgroundAudioManager.pause() |
||||
|
}, |
||||
|
seek(time) { |
||||
|
this.backgroundAudioManager.seek(time) |
||||
|
}, |
||||
|
startTimeUpdate() { |
||||
|
this.stopTimeUpdate() |
||||
|
this.timeUpdateTimer = setInterval(() => { |
||||
|
this.currentTime = this.backgroundAudioManager.currentTime || 0 |
||||
|
this.duration = this.backgroundAudioManager.duration || 0 |
||||
|
this.$emit('timeupdate', { |
||||
|
currentTime: this.currentTime, |
||||
|
duration: this.duration |
||||
|
}) |
||||
|
}, 100) |
||||
|
}, |
||||
|
stopTimeUpdate() { |
||||
|
if (this.timeUpdateTimer) { |
||||
|
clearInterval(this.timeUpdateTimer) |
||||
|
this.timeUpdateTimer = null |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
watch: { |
||||
|
src(newSrc) { |
||||
|
if (newSrc) { |
||||
|
this.backgroundAudioManager.src = newSrc |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
beforeDestroy() { |
||||
|
this.stopTimeUpdate() |
||||
|
// 注意:不要销毁backgroundAudioManager,因为它是全局的 |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* AudioPlayer组件样式 */ |
||||
|
</style> |
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,739 @@ |
|||||
|
<template> |
||||
|
<view class="container"> |
||||
|
<!-- 顶部标题栏 --> |
||||
|
<!-- 主要内容区域 --> |
||||
|
<!-- 文本显示区域 --> |
||||
|
<scroll-view |
||||
|
class="content" |
||||
|
:show-scrollbar="false" |
||||
|
enhanced |
||||
|
scroll-y |
||||
|
:scroll-top="scrollTop" |
||||
|
scroll-with-animation |
||||
|
:style="{ height: scrollViewHeight + 'px' }" |
||||
|
> |
||||
|
<view class="content-wrapper"> |
||||
|
<view class="article-content"> |
||||
|
<text |
||||
|
v-for="(char, index) in currentChapter.characters" |
||||
|
:key="index" |
||||
|
class="character" |
||||
|
:class="{ active: index === activeCharIndex }" |
||||
|
> |
||||
|
{{ char.Text }} |
||||
|
</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</scroll-view> |
||||
|
<!-- 底部控制栏 --> |
||||
|
<view class="controls"> |
||||
|
<!-- 章节信息栏 --> |
||||
|
<view class="chapter-info-bar"> |
||||
|
<text class="chapter-title">{{ currentChapter.title }}</text> |
||||
|
<text class="chapter-count"> |
||||
|
{{ currentChapterIndex + 1 }}/{{ chapters.length }} |
||||
|
</text> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 进度条区域 --> |
||||
|
<view class="progress-section"> |
||||
|
<text class="current-time">{{ formatTime(currentTime) }}</text> |
||||
|
<view class="progress-container"> |
||||
|
<view class="progress-bar" @click="seek"> |
||||
|
<view class="progress" :style="{ width: progress + '%' }"></view> |
||||
|
<view |
||||
|
class="progress-thumb" |
||||
|
:style="{ left: progress + '%' }" |
||||
|
></view> |
||||
|
</view> |
||||
|
</view> |
||||
|
<text class="duration">{{ formatTime(duration) }}</text> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 控制按钮区域 --> |
||||
|
<view class="control-buttons"> |
||||
|
<!-- 上一章按钮 --> |
||||
|
<button |
||||
|
class="control-btn prev-btn" |
||||
|
@click="prevChapter" |
||||
|
:disabled="currentChapterIndex === 0" |
||||
|
> |
||||
|
<text class="control-icon">⏮</text> |
||||
|
</button> |
||||
|
|
||||
|
<!-- 播放/暂停按钮 --> |
||||
|
<button class="play-btn" @click="togglePlay"> |
||||
|
<text class="play-icon">{{ playing ? "⏸️" : "▶️" }}</text> |
||||
|
</button> |
||||
|
|
||||
|
<!-- 下一章按钮 --> |
||||
|
<button |
||||
|
class="control-btn next-btn" |
||||
|
@click="nextChapter" |
||||
|
:disabled="currentChapterIndex === chapters.length - 1" |
||||
|
> |
||||
|
<text class="control-icon">⏭</text> |
||||
|
</button> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<AudioPlayer |
||||
|
ref="audioPlayer" |
||||
|
:src="currentChapter.audioUrl" |
||||
|
@timeupdate="onTimeUpdate" |
||||
|
@ended="onEnded" |
||||
|
></AudioPlayer> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
// 由于uniapp不支持直接使用audio元素,我们使用innerAudioContext |
||||
|
import AudioPlayer from "../components/AudioPlayer.vue"; |
||||
|
|
||||
|
export default { |
||||
|
components: { |
||||
|
AudioPlayer, |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
chapters: [ |
||||
|
{ |
||||
|
title: "第一章:人工智能的崛起", |
||||
|
audioUrl: "https://des.dayunyuanjian.cn/epicSoul/EpicSouls.mp3", |
||||
|
// 字符级别的时间同步数据 |
||||
|
characters: [ |
||||
|
{"BeginIndex": 0, "BeginTime": 0, "EndIndex": 1, "EndTime": 500, "Phoneme": "ren2", "Text": "人"}, |
||||
|
{"BeginIndex": 1, "BeginTime": 500, "EndIndex": 2, "EndTime": 1000, "Phoneme": "gong1", "Text": "工"}, |
||||
|
{"BeginIndex": 2, "BeginTime": 1000, "EndIndex": 3, "EndTime": 1500, "Phoneme": "zhi4", "Text": "智"}, |
||||
|
{"BeginIndex": 3, "BeginTime": 1500, "EndIndex": 4, "EndTime": 2000, "Phoneme": "neng2", "Text": "能"}, |
||||
|
{"BeginIndex": 4, "BeginTime": 2000, "EndIndex": 5, "EndTime": 2200, "Phoneme": "", "Text": "的"}, |
||||
|
{"BeginIndex": 5, "BeginTime": 2200, "EndIndex": 6, "EndTime": 2700, "Phoneme": "fa1", "Text": "发"}, |
||||
|
{"BeginIndex": 6, "BeginTime": 2700, "EndIndex": 7, "EndTime": 3200, "Phoneme": "zhan3", "Text": "展"}, |
||||
|
{"BeginIndex": 7, "BeginTime": 3200, "EndIndex": 8, "EndTime": 3700, "Phoneme": "li4", "Text": "历"}, |
||||
|
{"BeginIndex": 8, "BeginTime": 3700, "EndIndex": 9, "EndTime": 4200, "Phoneme": "cheng2", "Text": "程"}, |
||||
|
{"BeginIndex": 9, "BeginTime": 4200, "EndIndex": 10, "EndTime": 4400, "Phoneme": "", "Text": "。"}, |
||||
|
{"BeginIndex": 10, "BeginTime": 5500, "EndIndex": 11, "EndTime": 6000, "Phoneme": "cong2", "Text": "从"}, |
||||
|
{"BeginIndex": 11, "BeginTime": 6000, "EndIndex": 12, "EndTime": 6500, "Phoneme": "yi1", "Text": "1"}, |
||||
|
{"BeginIndex": 12, "BeginTime": 6500, "EndIndex": 13, "EndTime": 7000, "Phoneme": "jiu3", "Text": "9"}, |
||||
|
{"BeginIndex": 13, "BeginTime": 7000, "EndIndex": 14, "EndTime": 7500, "Phoneme": "wu3", "Text": "5"}, |
||||
|
{"BeginIndex": 14, "BeginTime": 7500, "EndIndex": 15, "EndTime": 8000, "Phoneme": "ling2", "Text": "0"}, |
||||
|
{"BeginIndex": 15, "BeginTime": 8000, "EndIndex": 16, "EndTime": 8500, "Phoneme": "nian2", "Text": "年"}, |
||||
|
{"BeginIndex": 16, "BeginTime": 8500, "EndIndex": 17, "EndTime": 9000, "Phoneme": "dai4", "Text": "代"}, |
||||
|
{"BeginIndex": 17, "BeginTime": 9000, "EndIndex": 18, "EndTime": 9500, "Phoneme": "kai1", "Text": "开"}, |
||||
|
{"BeginIndex": 18, "BeginTime": 9500, "EndIndex": 19, "EndTime": 10000, "Phoneme": "shi3", "Text": "始"}, |
||||
|
{"BeginIndex": 19, "BeginTime": 10000, "EndIndex": 20, "EndTime": 10200, "Phoneme": "", "Text": ","}, |
||||
|
{"BeginIndex": 20, "BeginTime": 10200, "EndIndex": 21, "EndTime": 10700, "Phoneme": "ren2", "Text": "人"}, |
||||
|
{"BeginIndex": 21, "BeginTime": 10700, "EndIndex": 22, "EndTime": 11200, "Phoneme": "gong1", "Text": "工"}, |
||||
|
{"BeginIndex": 22, "BeginTime": 11200, "EndIndex": 23, "EndTime": 11700, "Phoneme": "zhi4", "Text": "智"}, |
||||
|
{"BeginIndex": 23, "BeginTime": 11700, "EndIndex": 24, "EndTime": 12200, "Phoneme": "neng2", "Text": "能"}, |
||||
|
{"BeginIndex": 24, "BeginTime": 12200, "EndIndex": 25, "EndTime": 12700, "Phoneme": "zuo4", "Text": "作"}, |
||||
|
{"BeginIndex": 25, "BeginTime": 12700, "EndIndex": 26, "EndTime": 13200, "Phoneme": "wei2", "Text": "为"}, |
||||
|
{"BeginIndex": 26, "BeginTime": 13200, "EndIndex": 27, "EndTime": 13700, "Phoneme": "yi1", "Text": "一"}, |
||||
|
{"BeginIndex": 27, "BeginTime": 13700, "EndIndex": 28, "EndTime": 14200, "Phoneme": "men2", "Text": "门"}, |
||||
|
{"BeginIndex": 28, "BeginTime": 14200, "EndIndex": 29, "EndTime": 14700, "Phoneme": "xue2", "Text": "学"}, |
||||
|
{"BeginIndex": 29, "BeginTime": 14700, "EndIndex": 30, "EndTime": 15200, "Phoneme": "ke1", "Text": "科"}, |
||||
|
{"BeginIndex": 30, "BeginTime": 15200, "EndIndex": 31, "EndTime": 15700, "Phoneme": "zheng4", "Text": "正"}, |
||||
|
{"BeginIndex": 31, "BeginTime": 15700, "EndIndex": 32, "EndTime": 16200, "Phoneme": "shi4", "Text": "式"}, |
||||
|
{"BeginIndex": 32, "BeginTime": 16200, "EndIndex": 33, "EndTime": 16700, "Phoneme": "dan4", "Text": "诞"}, |
||||
|
{"BeginIndex": 33, "BeginTime": 16700, "EndIndex": 34, "EndTime": 17200, "Phoneme": "sheng1", "Text": "生"}, |
||||
|
{"BeginIndex": 34, "BeginTime": 17200, "EndIndex": 35, "EndTime": 17400, "Phoneme": "", "Text": "。"} |
||||
|
], |
||||
|
content: `人工智能的发展历程。从1950年代开始,人工智能作为一门学科正式诞生。`, |
||||
|
}, |
||||
|
{ |
||||
|
title: "第二章:科技与人文的融合", |
||||
|
audioUrl: "https://des.dayunyuanjian.cn/epicSoul/EpicSouls.mp3", |
||||
|
// 字符级别的时间同步数据 |
||||
|
characters: [ |
||||
|
{"BeginIndex": 0, "BeginTime": 0, "EndIndex": 1, "EndTime": 500, "Phoneme": "ke1", "Text": "科"}, |
||||
|
{"BeginIndex": 1, "BeginTime": 500, "EndIndex": 2, "EndTime": 1000, "Phoneme": "ji4", "Text": "技"}, |
||||
|
{"BeginIndex": 2, "BeginTime": 1000, "EndIndex": 3, "EndTime": 1500, "Phoneme": "fa1", "Text": "发"}, |
||||
|
{"BeginIndex": 3, "BeginTime": 1500, "EndIndex": 4, "EndTime": 2000, "Phoneme": "zhan3", "Text": "展"}, |
||||
|
{"BeginIndex": 4, "BeginTime": 2000, "EndIndex": 5, "EndTime": 2500, "Phoneme": "yu3", "Text": "与"}, |
||||
|
{"BeginIndex": 5, "BeginTime": 2500, "EndIndex": 6, "EndTime": 3000, "Phoneme": "ren2", "Text": "人"}, |
||||
|
{"BeginIndex": 6, "BeginTime": 3000, "EndIndex": 7, "EndTime": 3500, "Phoneme": "wen2", "Text": "文"}, |
||||
|
{"BeginIndex": 7, "BeginTime": 3500, "EndIndex": 8, "EndTime": 4000, "Phoneme": "guan1", "Text": "关"}, |
||||
|
{"BeginIndex": 8, "BeginTime": 4000, "EndIndex": 9, "EndTime": 4500, "Phoneme": "huai2", "Text": "怀"}, |
||||
|
{"BeginIndex": 9, "BeginTime": 4500, "EndIndex": 10, "EndTime": 5000, "Phoneme": "de5", "Text": "的"}, |
||||
|
{"BeginIndex": 10, "BeginTime": 5000, "EndIndex": 11, "EndTime": 5500, "Phoneme": "ping2", "Text": "平"}, |
||||
|
{"BeginIndex": 11, "BeginTime": 5500, "EndIndex": 12, "EndTime": 6000, "Phoneme": "heng2", "Text": "衡"}, |
||||
|
{"BeginIndex": 12, "BeginTime": 6000, "EndIndex": 13, "EndTime": 6200, "Phoneme": "", "Text": "。"} |
||||
|
], |
||||
|
content: `科技发展与人文关怀的平衡。`, |
||||
|
}, |
||||
|
{ |
||||
|
title: "第三章:未来社会的展望", |
||||
|
audioUrl: "https://des.dayunyuanjian.cn/epicSoul/EpicSouls.mp3", |
||||
|
// 字符级别的时间同步数据 |
||||
|
characters: [ |
||||
|
{"BeginIndex": 0, "BeginTime": 0, "EndIndex": 1, "EndTime": 500, "Phoneme": "zhan3", "Text": "展"}, |
||||
|
{"BeginIndex": 1, "BeginTime": 500, "EndIndex": 2, "EndTime": 1000, "Phoneme": "wang4", "Text": "望"}, |
||||
|
{"BeginIndex": 2, "BeginTime": 1000, "EndIndex": 3, "EndTime": 1500, "Phoneme": "wei4", "Text": "未"}, |
||||
|
{"BeginIndex": 3, "BeginTime": 1500, "EndIndex": 4, "EndTime": 2000, "Phoneme": "lai2", "Text": "来"}, |
||||
|
{"BeginIndex": 4, "BeginTime": 2000, "EndIndex": 5, "EndTime": 2500, "Phoneme": "she4", "Text": "社"}, |
||||
|
{"BeginIndex": 5, "BeginTime": 2500, "EndIndex": 6, "EndTime": 3000, "Phoneme": "hui4", "Text": "会"}, |
||||
|
{"BeginIndex": 6, "BeginTime": 3000, "EndIndex": 7, "EndTime": 3500, "Phoneme": "de5", "Text": "的"}, |
||||
|
{"BeginIndex": 7, "BeginTime": 3500, "EndIndex": 8, "EndTime": 4000, "Phoneme": "fa1", "Text": "发"}, |
||||
|
{"BeginIndex": 8, "BeginTime": 4000, "EndIndex": 9, "EndTime": 4500, "Phoneme": "zhan3", "Text": "展"}, |
||||
|
{"BeginIndex": 9, "BeginTime": 4500, "EndIndex": 10, "EndTime": 5000, "Phoneme": "qu1", "Text": "趋"}, |
||||
|
{"BeginIndex": 10, "BeginTime": 5000, "EndIndex": 11, "EndTime": 5500, "Phoneme": "shi4", "Text": "势"}, |
||||
|
{"BeginIndex": 11, "BeginTime": 5500, "EndIndex": 12, "EndTime": 5700, "Phoneme": "", "Text": "。"} |
||||
|
], |
||||
|
content: `展望未来社会的发展趋势。`, |
||||
|
}, |
||||
|
], |
||||
|
currentChapterIndex: 0, |
||||
|
playing: false, |
||||
|
currentTime: 0, |
||||
|
duration: 0, |
||||
|
activeCharIndex: -1, |
||||
|
scrollTop: 0, |
||||
|
scrollViewHeight: 0, |
||||
|
}; |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.syncAudioState(); |
||||
|
this.calculateScrollViewHeight(); |
||||
|
}, |
||||
|
onShow() { |
||||
|
// 页面显示时同步音频状态 |
||||
|
this.syncAudioState(); |
||||
|
}, |
||||
|
onHide() { |
||||
|
// 页面隐藏时保持音频播放状态 |
||||
|
// 背景音频会自动继续播放 |
||||
|
}, |
||||
|
computed: { |
||||
|
currentChapter() { |
||||
|
const chapter = this.chapters[this.currentChapterIndex]; |
||||
|
// 使用字符级别的时间同步数据 |
||||
|
const characters = chapter.characters.map((char) => { |
||||
|
return { |
||||
|
...char, |
||||
|
timeInSeconds: char.BeginTime / 1000, // 将毫秒转换为秒 |
||||
|
}; |
||||
|
}); |
||||
|
return { |
||||
|
...chapter, |
||||
|
characters: characters, |
||||
|
}; |
||||
|
}, |
||||
|
progress() { |
||||
|
return (this.currentTime / this.duration) * 100 || 0; |
||||
|
}, |
||||
|
currentTimeFormatted() { |
||||
|
return this.formatTime(this.currentTime); |
||||
|
}, |
||||
|
durationFormatted() { |
||||
|
return this.formatTime(this.duration); |
||||
|
}, |
||||
|
}, |
||||
|
methods: { |
||||
|
togglePlay() { |
||||
|
if (this.playing) { |
||||
|
this.$refs.audioPlayer.pause(); |
||||
|
this.playing = false; |
||||
|
} else { |
||||
|
this.$refs.audioPlayer.play(); |
||||
|
this.playing = true; |
||||
|
} |
||||
|
}, |
||||
|
prevChapter() { |
||||
|
if (this.currentChapterIndex > 0) { |
||||
|
this.currentChapterIndex--; |
||||
|
this.resetPlayback(); |
||||
|
} |
||||
|
}, |
||||
|
nextChapter() { |
||||
|
if (this.currentChapterIndex < this.chapters.length - 1) { |
||||
|
this.currentChapterIndex++; |
||||
|
this.resetPlayback(); |
||||
|
} |
||||
|
}, |
||||
|
resetPlayback() { |
||||
|
this.playing = false; |
||||
|
this.currentTime = 0; |
||||
|
this.duration = 0; |
||||
|
this.activeCharIndex = -1; |
||||
|
this.scrollTop = 0; |
||||
|
|
||||
|
// 更新背景音频信息 |
||||
|
const currentChapter = this.chapters[this.currentChapterIndex]; |
||||
|
if ( |
||||
|
this.$refs.audioPlayer && |
||||
|
this.$refs.audioPlayer.backgroundAudioManager |
||||
|
) { |
||||
|
const bgAudio = this.$refs.audioPlayer.backgroundAudioManager; |
||||
|
bgAudio.title = currentChapter.title; |
||||
|
bgAudio.epname = `第${this.currentChapterIndex + 1}章`; |
||||
|
bgAudio.src = currentChapter.audioUrl; |
||||
|
// 确保音频从头开始播放 |
||||
|
bgAudio.currentTime = 0; |
||||
|
// 如果音频播放器有seek方法,也调用它来确保重置 |
||||
|
if (this.$refs.audioPlayer.seek) { |
||||
|
this.$refs.audioPlayer.seek(0); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
onTimeUpdate(e) { |
||||
|
this.currentTime = e.currentTime; |
||||
|
this.duration = e.duration; |
||||
|
|
||||
|
// 同步播放状态 |
||||
|
this.playing = !e.paused; |
||||
|
|
||||
|
// 找到当前应该高亮的字符 |
||||
|
const characters = this.currentChapter.characters; |
||||
|
const currentTimeMs = this.currentTime * 1000; // 转换为毫秒 |
||||
|
for (let i = characters.length - 1; i >= 0; i--) { |
||||
|
if (currentTimeMs >= characters[i].BeginTime && currentTimeMs <= characters[i].EndTime) { |
||||
|
if (this.activeCharIndex !== i) { |
||||
|
this.activeCharIndex = i; |
||||
|
// 滚动到当前字符,这里简单实现:每个字符高度约30rpx,滚动到当前字符 |
||||
|
this.scrollTop = Math.floor(i / 20) * 30; // 假设每行约20个字符 |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
onEnded() { |
||||
|
this.playing = false; |
||||
|
// 自动播放下一章节 |
||||
|
if (this.currentChapterIndex < this.chapters.length - 1) { |
||||
|
this.nextChapter(); |
||||
|
setTimeout(() => { |
||||
|
this.$refs.audioPlayer.play(); |
||||
|
this.playing = true; |
||||
|
}, 500); |
||||
|
} |
||||
|
}, |
||||
|
seek(e) { |
||||
|
// 使用uni.createSelectorQuery获取进度条信息 |
||||
|
const query = uni.createSelectorQuery().in(this); |
||||
|
query |
||||
|
.select(".progress-bar") |
||||
|
.boundingClientRect((rect) => { |
||||
|
if (rect) { |
||||
|
const clickX = e.detail.x - rect.left; |
||||
|
const progressPercent = clickX / rect.width; |
||||
|
const seekTime = progressPercent * this.duration; |
||||
|
|
||||
|
if (seekTime >= 0 && seekTime <= this.duration) { |
||||
|
this.$refs.audioPlayer.seek(seekTime); |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
.exec(); |
||||
|
}, |
||||
|
formatTime(seconds) { |
||||
|
const mins = Math.floor(seconds / 60); |
||||
|
const secs = Math.floor(seconds % 60); |
||||
|
return `${mins.toString().padStart(2, "0")}:${secs |
||||
|
.toString() |
||||
|
.padStart(2, "0")}`; |
||||
|
}, |
||||
|
syncAudioState() { |
||||
|
// 同步背景音频状态到界面 |
||||
|
if ( |
||||
|
this.$refs.audioPlayer && |
||||
|
this.$refs.audioPlayer.backgroundAudioManager |
||||
|
) { |
||||
|
const bgAudio = this.$refs.audioPlayer.backgroundAudioManager; |
||||
|
|
||||
|
// 检查当前是否有音频在播放 |
||||
|
if (bgAudio.src) { |
||||
|
// 同步播放状态 - 注意:paused为true表示暂停,false表示播放 |
||||
|
this.playing = !bgAudio.paused; |
||||
|
|
||||
|
// 同步时间信息 |
||||
|
this.currentTime = bgAudio.currentTime || 0; |
||||
|
this.duration = bgAudio.duration || 0; |
||||
|
|
||||
|
// 如果有音频在播放,启动时间更新 |
||||
|
if (this.playing) { |
||||
|
this.$refs.audioPlayer.startTimeUpdate(); |
||||
|
} |
||||
|
} else { |
||||
|
// 没有音频源时重置状态 |
||||
|
this.playing = false; |
||||
|
this.currentTime = 0; |
||||
|
this.duration = 0; |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
calculateScrollViewHeight() { |
||||
|
// 获取系统信息 |
||||
|
const systemInfo = uni.getSystemInfoSync(); |
||||
|
const windowHeight = systemInfo.windowHeight; |
||||
|
|
||||
|
// 计算控制栏高度(大约300rpx转换为px) |
||||
|
const controlsHeight = 300 * (systemInfo.windowWidth / 750); |
||||
|
|
||||
|
// 计算内容区域可用高度 |
||||
|
this.scrollViewHeight = windowHeight - controlsHeight - 60; // 60px为额外边距 |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
.container { |
||||
|
height: 100vh; |
||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; |
||||
|
padding: 30rpx; |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
/* 顶部标题栏样式 */ |
||||
|
.header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
padding: 40rpx 48rpx; |
||||
|
background: rgba(255, 255, 255, 0.95); |
||||
|
backdrop-filter: blur(20rpx); |
||||
|
border-bottom: 2rpx solid rgba(255, 255, 255, 0.2); |
||||
|
box-shadow: 0 4rpx 40rpx rgba(0, 0, 0, 0.1); |
||||
|
} |
||||
|
|
||||
|
.header-content { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.title { |
||||
|
font-size: 48rpx; |
||||
|
font-weight: 700; |
||||
|
color: #2d3748; |
||||
|
margin-bottom: 8rpx; |
||||
|
} |
||||
|
|
||||
|
.subtitle { |
||||
|
font-size: 32rpx; |
||||
|
color: #718096; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.chapter-info { |
||||
|
background: linear-gradient(135deg, #667eea, #764ba2); |
||||
|
padding: 16rpx 32rpx; |
||||
|
border-radius: 40rpx; |
||||
|
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3); |
||||
|
} |
||||
|
|
||||
|
.chapter-count { |
||||
|
color: white; |
||||
|
font-size: 28rpx; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.content { |
||||
|
width: 690rpx; |
||||
|
background: rgba(255, 255, 255, 0.9); |
||||
|
backdrop-filter: blur(20rpx); |
||||
|
border-radius: 32rpx; |
||||
|
margin: 0 auto; |
||||
|
margin-bottom: 20rpx; |
||||
|
} |
||||
|
|
||||
|
.content-wrapper { |
||||
|
padding: 48rpx; |
||||
|
} |
||||
|
|
||||
|
.article-content { |
||||
|
line-height: 1.8; |
||||
|
font-size: 32rpx; |
||||
|
color: #2d3748; |
||||
|
text-align: justify; |
||||
|
} |
||||
|
|
||||
|
.character { |
||||
|
transition: all 0.3s ease; |
||||
|
padding: 2rpx 4rpx; |
||||
|
border-radius: 6rpx; |
||||
|
display: inline; |
||||
|
margin: 0 1rpx; |
||||
|
} |
||||
|
|
||||
|
.character.active { |
||||
|
background: linear-gradient( |
||||
|
135deg, |
||||
|
rgba(102, 126, 234, 0.25), |
||||
|
rgba(118, 75, 162, 0.25) |
||||
|
); |
||||
|
color: #667eea; |
||||
|
font-weight: 700; |
||||
|
box-shadow: 0 2rpx 8rpx rgba(102, 126, 234, 0.3); |
||||
|
transform: scale(1.1); |
||||
|
} |
||||
|
|
||||
|
/* 底部控制栏样式 */ |
||||
|
.controls { |
||||
|
background: rgba(255, 255, 255, 0.98); |
||||
|
backdrop-filter: blur(40rpx); |
||||
|
border-top: 2rpx solid rgba(255, 255, 255, 0.3); |
||||
|
box-shadow: 0 -16rpx 64rpx rgba(0, 0, 0, 0.12), |
||||
|
0 -4rpx 16rpx rgba(0, 0, 0, 0.08); |
||||
|
border-radius: 48rpx 48rpx 0 0; |
||||
|
padding: 32rpx 40rpx 40rpx; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 24rpx; |
||||
|
position: fixed; |
||||
|
bottom: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
z-index: 1000; |
||||
|
} |
||||
|
|
||||
|
/* 章节信息栏 */ |
||||
|
.chapter-info-bar { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
padding: 0 8rpx; |
||||
|
} |
||||
|
|
||||
|
.chapter-title { |
||||
|
font-size: 28rpx; |
||||
|
font-weight: 600; |
||||
|
color: #2d3748; |
||||
|
flex: 1; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
margin-right: 16rpx; |
||||
|
} |
||||
|
|
||||
|
.chapter-count { |
||||
|
font-size: 24rpx; |
||||
|
color: #718096; |
||||
|
font-weight: 500; |
||||
|
background: rgba(139, 92, 246, 0.1); |
||||
|
padding: 8rpx 16rpx; |
||||
|
border-radius: 20rpx; |
||||
|
backdrop-filter: blur(10rpx); |
||||
|
} |
||||
|
|
||||
|
/* 进度条区域 */ |
||||
|
.progress-section { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 20rpx; |
||||
|
} |
||||
|
|
||||
|
.current-time, |
||||
|
.duration { |
||||
|
font-size: 24rpx; |
||||
|
color: #718096; |
||||
|
font-weight: 500; |
||||
|
font-family: "Courier New", monospace; |
||||
|
min-width: 80rpx; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
/* 控制按钮区域 */ |
||||
|
.control-buttons { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
gap: 32rpx; |
||||
|
} |
||||
|
|
||||
|
/* 播放按钮 */ |
||||
|
.play-btn { |
||||
|
width: 96rpx; |
||||
|
height: 96rpx; |
||||
|
border-radius: 50%; |
||||
|
border: none; |
||||
|
background: linear-gradient(135deg, #8b5cf6, #a855f7); |
||||
|
color: white; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
||||
|
box-shadow: 0 12rpx 40rpx rgba(139, 92, 246, 0.4), |
||||
|
0 6rpx 20rpx rgba(0, 0, 0, 0.15); |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
z-index: 2; |
||||
|
} |
||||
|
|
||||
|
/* 控制按钮(上一章/下一章) */ |
||||
|
.control-btn { |
||||
|
width: 88rpx; |
||||
|
height: 88rpx; |
||||
|
border-radius: 50%; |
||||
|
border: none; |
||||
|
background: rgba(139, 92, 246, 0.08); |
||||
|
color: #8b5cf6; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
||||
|
box-shadow: 0 4rpx 16rpx rgba(139, 92, 246, 0.15); |
||||
|
backdrop-filter: blur(20rpx); |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.play-btn::before { |
||||
|
content: ""; |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
background: linear-gradient( |
||||
|
135deg, |
||||
|
rgba(255, 255, 255, 0.2), |
||||
|
rgba(255, 255, 255, 0.05) |
||||
|
); |
||||
|
border-radius: 50%; |
||||
|
opacity: 0; |
||||
|
transition: opacity 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.play-btn:hover { |
||||
|
transform: scale(1.08); |
||||
|
box-shadow: 0 18rpx 56rpx rgba(139, 92, 246, 0.5), |
||||
|
0 9rpx 28rpx rgba(0, 0, 0, 0.2); |
||||
|
} |
||||
|
|
||||
|
.play-btn:hover::before { |
||||
|
opacity: 1; |
||||
|
} |
||||
|
|
||||
|
.play-btn:active { |
||||
|
transform: scale(0.96); |
||||
|
transition: transform 0.1s ease; |
||||
|
} |
||||
|
|
||||
|
/* 进度条容器 */ |
||||
|
.progress-container { |
||||
|
flex: 1; |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
.progress-bar { |
||||
|
height: 12rpx; |
||||
|
background: rgba(139, 92, 246, 0.12); |
||||
|
border-radius: 6rpx; |
||||
|
position: relative; |
||||
|
cursor: pointer; |
||||
|
overflow: hidden; |
||||
|
backdrop-filter: blur(20rpx); |
||||
|
box-shadow: inset 0 2rpx 6rpx rgba(0, 0, 0, 0.1); |
||||
|
} |
||||
|
|
||||
|
.progress { |
||||
|
height: 100%; |
||||
|
background: linear-gradient(90deg, #8b5cf6 0%, #a855f7 50%, #c084fc 100%); |
||||
|
border-radius: 6rpx; |
||||
|
transition: width 0.2s cubic-bezier(0.4, 0, 0.2, 1); |
||||
|
box-shadow: 0 4rpx 16rpx rgba(139, 92, 246, 0.3); |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
.progress::after { |
||||
|
content: ""; |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
background: linear-gradient( |
||||
|
90deg, |
||||
|
rgba(255, 255, 255, 0.3) 0%, |
||||
|
rgba(255, 255, 255, 0.1) 100% |
||||
|
); |
||||
|
border-radius: 6rpx; |
||||
|
} |
||||
|
|
||||
|
.progress-thumb { |
||||
|
position: absolute; |
||||
|
top: 50%; |
||||
|
width: 28rpx; |
||||
|
height: 28rpx; |
||||
|
background: white; |
||||
|
border: 4rpx solid #8b5cf6; |
||||
|
border-radius: 50%; |
||||
|
transform: translate(-50%, -50%); |
||||
|
box-shadow: 0 6rpx 20rpx rgba(139, 92, 246, 0.4), |
||||
|
0 3rpx 6rpx rgba(0, 0, 0, 0.1); |
||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.progress-thumb:hover { |
||||
|
transform: translate(-50%, -50%) scale(1.2); |
||||
|
box-shadow: 0 8rpx 24rpx rgba(139, 92, 246, 0.5), |
||||
|
0 4rpx 8rpx rgba(0, 0, 0, 0.15); |
||||
|
} |
||||
|
|
||||
|
/* 控制按钮交互效果 */ |
||||
|
.control-btn::before { |
||||
|
content: ""; |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
background: linear-gradient( |
||||
|
135deg, |
||||
|
rgba(139, 92, 246, 0.1), |
||||
|
rgba(168, 85, 247, 0.05) |
||||
|
); |
||||
|
border-radius: 50%; |
||||
|
opacity: 0; |
||||
|
transition: opacity 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.control-btn:hover { |
||||
|
background: rgba(139, 92, 246, 0.15); |
||||
|
transform: scale(1.1); |
||||
|
box-shadow: 0 8rpx 32rpx rgba(139, 92, 246, 0.25); |
||||
|
} |
||||
|
|
||||
|
.control-btn:hover::before { |
||||
|
opacity: 1; |
||||
|
} |
||||
|
|
||||
|
.control-btn:active { |
||||
|
transform: scale(0.95); |
||||
|
transition: transform 0.1s ease; |
||||
|
} |
||||
|
|
||||
|
.control-btn:disabled { |
||||
|
opacity: 0.3; |
||||
|
cursor: not-allowed; |
||||
|
transform: none; |
||||
|
box-shadow: 0 2rpx 6rpx rgba(139, 92, 246, 0.1); |
||||
|
} |
||||
|
|
||||
|
.control-btn:disabled:hover { |
||||
|
background: rgba(139, 92, 246, 0.08); |
||||
|
transform: none; |
||||
|
box-shadow: 0 2rpx 6rpx rgba(139, 92, 246, 0.1); |
||||
|
} |
||||
|
|
||||
|
.control-btn:disabled::before { |
||||
|
opacity: 0; |
||||
|
} |
||||
|
|
||||
|
/* 控制图标样式 */ |
||||
|
.control-icon { |
||||
|
font-size: 32rpx; |
||||
|
line-height: 1; |
||||
|
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.1)); |
||||
|
} |
||||
|
|
||||
|
.play-icon { |
||||
|
font-size: 42rpx; |
||||
|
line-height: 1; |
||||
|
margin-left: 3rpx; |
||||
|
filter: drop-shadow(0 3rpx 6rpx rgba(0, 0, 0, 0.2)); |
||||
|
} |
||||
|
</style> |
||||
File diff suppressed because it is too large
@ -1,180 +1,196 @@ |
|||||
<template> |
<template> |
||||
<view> |
<view> |
||||
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange"> |
<BackButton /> |
||||
<swiper-item v-for="(image, index) in swiperImages" :key="index"> |
<swiper |
||||
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }"> |
class="swiper" |
||||
<!-- 仅在第四张图片添加子模块 --> |
:current="currentIndex" |
||||
<template v-if="index === 3"> |
:vertical="true" |
||||
<image |
@change="handleSwiperChange" |
||||
:src="`https://static.ticket.sz-trip.com/epicSoul/bmzm/home/home4-${i + 1}.png`" |
> |
||||
v-for="i in 4" |
<swiper-item v-for="(image, index) in swiperImages" :key="index"> |
||||
:key="i" |
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }"> |
||||
:class="'module'+(i+1)" |
<!-- 仅在第四张图片添加子模块 --> |
||||
:style="{ animationDelay: `${i * animationConfig.delay}s` }" |
<template v-if="index === 3"> |
||||
@click="setStorage(i);gotoPath(`/bmzm/chapter1/index?index=${i + 1}`)" |
<image |
||||
></image> |
:src="`https://des.dayunyuanjian.cn/epicSoul/bmzm/home/home4-${ |
||||
</template> |
i + 1 |
||||
</view> |
}.png`" |
||||
</swiper-item> |
v-for="i in 4" |
||||
</swiper> |
:key="i" |
||||
<MusicControl /> |
:class="'module' + (i + 1)" |
||||
<NavMenu :nav-index="0" @jump-to-page="handleJumpToPage" /> |
:style="{ animationDelay: `${i * animationConfig.delay}s` }" |
||||
<AudioControl audioSrc="https://des.js-dyyj.com/data/2025/09/05/9875a62d-14ef-481e-b88f-19c061478ce6.MP3" /> |
@click=" |
||||
</view> |
setStorage(i); |
||||
|
gotoPath(`/bmzm/chapter1/index?index=${i + 1}`); |
||||
|
" |
||||
|
></image> |
||||
|
</template> |
||||
|
</view> |
||||
|
</swiper-item> |
||||
|
</swiper> |
||||
|
<MusicControl /> |
||||
|
<NavMenu :nav-index="0" @jump-to-page="handleJumpToPage" /> |
||||
|
<AudioControl |
||||
|
audioSrc="https://des.dayunyuanjian.cn/data/2025/09/05/9875a62d-14ef-481e-b88f-19c061478ce6.MP3" |
||||
|
/> |
||||
|
</view> |
||||
</template> |
</template> |
||||
|
|
||||
<script> |
<script> |
||||
import AudioControl from '@/components/AudioControl.vue'; |
import AudioControl from "@/components/AudioControl.vue"; |
||||
import MusicControl from '@/components/MusicControl.vue'; |
import MusicControl from "@/components/MusicControl.vue"; |
||||
import NavMenu from '../components/NavMenu.vue'; |
import NavMenu from "../components/NavMenu.vue"; |
||||
|
import BackButton from "@/components/BackButton.vue"; |
||||
export default { |
export default { |
||||
components: { |
components: { |
||||
MusicControl, |
MusicControl, |
||||
NavMenu, |
NavMenu, |
||||
AudioControl |
AudioControl, |
||||
}, |
BackButton |
||||
data() { |
}, |
||||
return { |
data() { |
||||
currentIndex: 0, |
return { |
||||
swiperImages: [ |
currentIndex: 0, |
||||
'https://static.ticket.sz-trip.com/epicSoul/bmzm/home/home1s.gif', |
swiperImages: [ |
||||
'https://static.ticket.sz-trip.com/epicSoul/bmzm/home/home2.png', |
"https://des.dayunyuanjian.cn/epicSoul/bmzm/home/home1s.gif", |
||||
'https://static.ticket.sz-trip.com/epicSoul/bmzm/home/home3.png', |
"https://des.dayunyuanjian.cn/epicSoul/bmzm/home/home2.png", |
||||
'https://static.ticket.sz-trip.com/epicSoul/bmzm/home/home4.png' |
"https://des.dayunyuanjian.cn/epicSoul/bmzm/home/home3.png", |
||||
], |
"https://des.dayunyuanjian.cn/epicSoul/bmzm/home/home4.png", |
||||
animationConfig: { |
], |
||||
delay: 0.5, |
animationConfig: { |
||||
duration: 3, |
delay: 0.5, |
||||
keyframes: { |
duration: 3, |
||||
start: 1, |
keyframes: { |
||||
first: 0.8, |
start: 1, |
||||
second: 1.2, |
first: 0.8, |
||||
third: 0.9, |
second: 1.2, |
||||
end: 1.1 |
third: 0.9, |
||||
} |
end: 1.1, |
||||
} |
|
||||
}; |
|
||||
}, |
|
||||
onShow() { |
|
||||
uni.removeStorageSync('answerObj'); |
|
||||
const app = getApp(); |
|
||||
app.updateMusicSrc('https://static.ticket.sz-trip.com/epicSoul/bmzm.mp3'); |
|
||||
app.initBackgroundMusic(); // 初始化背景音乐 |
|
||||
uni.$bgMusic.play(); // 播放音乐 |
|
||||
}, |
|
||||
methods: { |
|
||||
handleSwiperChange(e) { |
|
||||
this.currentIndex = e.detail.current; |
|
||||
}, |
}, |
||||
// 存储答案,供后面使用 |
}, |
||||
setStorage(i) { |
}; |
||||
let text = '' |
}, |
||||
switch (i){ |
|
||||
case 0: |
onShow() { |
||||
text = '月光白' |
uni.removeStorageSync("answerObj"); |
||||
break; |
const app = getApp(); |
||||
case 1: |
app.updateMusicSrc("https://des.dayunyuanjian.cn/epicSoul/bmzm.mp3"); |
||||
text = '黎明青' |
app.initBackgroundMusic(); // 初始化背景音乐 |
||||
break; |
uni.$bgMusic.play(); // 播放音乐 |
||||
case 2: |
}, |
||||
text = '玄天黑' |
methods: { |
||||
break; |
handleSwiperChange(e) { |
||||
case 3: |
this.currentIndex = e.detail.current; |
||||
text = '胭脂红' |
}, |
||||
break; |
// 存储答案,供后面使用 |
||||
default: |
setStorage(i) { |
||||
break; |
let text = ""; |
||||
} |
switch (i) { |
||||
this.appendToStorage('answerObj', { answer1: text }); |
case 0: |
||||
} |
text = "月光白"; |
||||
|
break; |
||||
|
case 1: |
||||
|
text = "黎明青"; |
||||
|
break; |
||||
|
case 2: |
||||
|
text = "玄天黑"; |
||||
|
break; |
||||
|
case 3: |
||||
|
text = "胭脂红"; |
||||
|
break; |
||||
|
default: |
||||
|
break; |
||||
|
} |
||||
|
this.appendToStorage("answerObj", { answer1: text }); |
||||
}, |
}, |
||||
// 微信分享配置 |
}, |
||||
// #ifdef MP-WEIXIN |
// 微信分享配置 |
||||
onShareAppMessage() { |
// #ifdef MP-WEIXIN |
||||
return { |
onShareAppMessage() { |
||||
title: '不眠之夜·Endless Dream|「Epic Soul」阅读体 issue03', |
return { |
||||
mpId: 'wx8954209bb3ad489e', |
title: "不眠之夜·Endless Dream|「Epic Soul」阅读体 issue03", |
||||
path: '/bmzm/home/home', |
mpId: "wx9660f8c5776663e0", |
||||
imageUrl: 'https://static.ticket.sz-trip.com/epicSoul/bmzm/share.jpg' |
path: "/bmzm/home/home", |
||||
}; |
imageUrl: "https://des.dayunyuanjian.cn/epicSoul/bmzm/share.jpg", |
||||
}, |
}; |
||||
onShareTimeline() { |
}, |
||||
return { |
onShareTimeline() { |
||||
title: '不眠之夜·Endless Dream|「Epic Soul」阅读体 issue03', |
return { |
||||
query: '', |
title: "不眠之夜·Endless Dream|「Epic Soul」阅读体 issue03", |
||||
imageUrl: 'https://static.ticket.sz-trip.com/epicSoul/bmzm/share.jpg' |
query: "", |
||||
}; |
imageUrl: "https://des.dayunyuanjian.cn/epicSoul/bmzm/share.jpg", |
||||
} |
}; |
||||
// #endif |
}, |
||||
|
// #endif |
||||
}; |
}; |
||||
</script> |
</script> |
||||
|
|
||||
<style lang="scss" scoped> |
<style lang="scss" scoped> |
||||
.swiper { |
.swiper { |
||||
width: 100vw; |
width: 100vw; |
||||
height: 100vh; |
height: 100vh; |
||||
} |
} |
||||
|
|
||||
.swiper-item { |
.swiper-item { |
||||
width: 100vw; |
width: 100vw; |
||||
height: 100vh; |
height: 100vh; |
||||
background-size: 100% 100%; |
background-size: 100% 100%; |
||||
position: relative; |
position: relative; |
||||
|
|
||||
.module1 { |
.module1 { |
||||
position: absolute; |
position: absolute; |
||||
width: 345.97rpx; |
width: 345.97rpx; |
||||
height: 323.42rpx; |
height: 323.42rpx; |
||||
top: 154rpx; |
top: 154rpx; |
||||
left: 48rpx; |
left: 48rpx; |
||||
animation: randomSize 3s infinite alternate; |
animation: randomSize 3s infinite alternate; |
||||
} |
} |
||||
.module2 { |
.module2 { |
||||
position: absolute; |
position: absolute; |
||||
width: 271.11rpx; |
width: 271.11rpx; |
||||
height: 293.67rpx; |
height: 293.67rpx; |
||||
top: 276rpx; |
top: 276rpx; |
||||
right: 36rpx; |
right: 36rpx; |
||||
animation: randomSize 3s infinite alternate; |
animation: randomSize 3s infinite alternate; |
||||
} |
} |
||||
.module3 { |
.module3 { |
||||
position: absolute; |
position: absolute; |
||||
width: 245.2rpx; |
width: 245.2rpx; |
||||
height: 232.25rpx; |
height: 232.25rpx; |
||||
top: 746rpx; |
top: 746rpx; |
||||
left: 71rpx; |
left: 71rpx; |
||||
animation: randomSize 3s infinite alternate; |
animation: randomSize 3s infinite alternate; |
||||
} |
} |
||||
.module4 { |
.module4 { |
||||
position: absolute; |
position: absolute; |
||||
width: 293.19rpx; |
width: 293.19rpx; |
||||
height: 270.15rpx; |
height: 270.15rpx; |
||||
top: 605rpx; |
top: 605rpx; |
||||
right: 115rpx; |
right: 115rpx; |
||||
animation: randomSize 3s infinite alternate; |
animation: randomSize 3s infinite alternate; |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
.swiper-img { |
.swiper-img { |
||||
width: 100vw; |
width: 100vw; |
||||
height: 100vh; |
height: 100vh; |
||||
} |
} |
||||
|
|
||||
@keyframes randomSize { |
@keyframes randomSize { |
||||
0% { |
0% { |
||||
transform: scale(1); |
transform: scale(1); |
||||
} |
} |
||||
25% { |
25% { |
||||
transform: scale(0.8); |
transform: scale(0.8); |
||||
} |
} |
||||
50% { |
50% { |
||||
transform: scale(1.2); |
transform: scale(1.2); |
||||
} |
} |
||||
75% { |
75% { |
||||
transform: scale(0.9); |
transform: scale(0.9); |
||||
} |
} |
||||
100% { |
100% { |
||||
transform: scale(1.1); |
transform: scale(1.1); |
||||
} |
} |
||||
} |
} |
||||
</style> |
</style> |
||||
|
|||||
@ -0,0 +1,55 @@ |
|||||
|
import QQMapWX from '@/libs/qqmap-wx-jssdk1.2/qqmap-wx-jssdk.js'; |
||||
|
//获取位置信息
|
||||
|
async function getLocationInfo() { |
||||
|
return new Promise((resolve) => { |
||||
|
//位置信息默认数据
|
||||
|
let location = { |
||||
|
longitude: 0, |
||||
|
latitude: 0, |
||||
|
province: '', |
||||
|
city: '', |
||||
|
area: '', |
||||
|
street: '', |
||||
|
address: '', |
||||
|
}; |
||||
|
uni.getLocation({ |
||||
|
type: 'gcj02', |
||||
|
success(res) { |
||||
|
location.longitude = res.longitude; |
||||
|
location.latitude = res.latitude; |
||||
|
// 腾讯地图Api
|
||||
|
const qqmapsdk = new QQMapWX({ |
||||
|
key: 'X5YBZ-ES6K3-Q6E3P-RUVXH-2R5ZQ-ERBFG', //这里填写自己申请的key
|
||||
|
}); |
||||
|
qqmapsdk.reverseGeocoder({ |
||||
|
location, |
||||
|
success(response) { |
||||
|
let info = response.result; |
||||
|
let _c = info.ad_info.adcode.slice(0,3) |
||||
|
location.cityId = info.ad_info.city_code.slice(3); |
||||
|
location.provinceId = _c.padEnd(6,'0'); |
||||
|
location.areaId = info.ad_info.adcode; |
||||
|
|
||||
|
location.province = info.address_component.province; |
||||
|
location.city = info.address_component.city; |
||||
|
location.area = info.address_component.district; |
||||
|
location.street = info.address_component.street; |
||||
|
location.address = info.address; |
||||
|
resolve(location); |
||||
|
}, |
||||
|
fail(e){ |
||||
|
console.log(e,'地址信息报错') |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
fail(err) { |
||||
|
console.log(err); |
||||
|
resolve(location); |
||||
|
}, |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
//导出
|
||||
|
module.exports = { |
||||
|
getLocationInfo, |
||||
|
}; |
||||
@ -0,0 +1,222 @@ |
|||||
|
<template> |
||||
|
<uni-popup ref="popup" type="bottom" :mask-click="false" :z-index="999"> |
||||
|
<view class="activate-agent-popup"> |
||||
|
<!-- 弹窗头部 --> |
||||
|
<view class="popup-header"> |
||||
|
<text class="popup-title">激活AGENT</text> |
||||
|
<text class="popup-close" @click="closePopup">×</text> |
||||
|
</view> |
||||
|
|
||||
|
<!-- AGENT列表 --> |
||||
|
<view class="agent-list"> |
||||
|
<view |
||||
|
class="agent-item" |
||||
|
v-for="(agent, index) in agentList" |
||||
|
:key="index" |
||||
|
@click="handleAgentClick(agent)" |
||||
|
> |
||||
|
<!-- 头像区域 --> |
||||
|
<view class="agent-avatar"> |
||||
|
<view class="avatar-bubble"> |
||||
|
<image |
||||
|
:src="agent.headImage" |
||||
|
mode="aspectFill" |
||||
|
class="avatar-img" |
||||
|
></image> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 信息区域 --> |
||||
|
<view class="agent-info"> |
||||
|
<view class="agent-name"> |
||||
|
<text class="name-chinese">{{ agent.name }}</text> |
||||
|
<!-- <text class="name-pinyin">{{ agent.namePinyin }}</text> --> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 状态按钮 --> |
||||
|
<view class="agent-status"> |
||||
|
<view |
||||
|
class="status-btn" |
||||
|
:class=" |
||||
|
agent.status == '0' ? 'status-inactive' : 'status-active' |
||||
|
" |
||||
|
> |
||||
|
<text class="status-text">{{ agent.status==1?'去使用':'去激活' }}</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</uni-popup> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: "ActivateAgentPopup", |
||||
|
props:["agentList"], |
||||
|
data() { |
||||
|
return { |
||||
|
|
||||
|
}; |
||||
|
}, |
||||
|
methods: { |
||||
|
// 打开弹窗 |
||||
|
openPopup() { |
||||
|
this.$refs.popup.open(); |
||||
|
}, |
||||
|
|
||||
|
// 关闭弹窗 |
||||
|
closePopup() { |
||||
|
this.$refs.popup.close(); |
||||
|
}, |
||||
|
|
||||
|
// 处理AGENT点击 |
||||
|
handleAgentClick(agent) { |
||||
|
if (agent.status ==1) { |
||||
|
// 激活逻辑 |
||||
|
uni.navigateTo({ |
||||
|
url: "/subPackages/other/evita?id="+agent.agentId |
||||
|
}); |
||||
|
} else { |
||||
|
uni.navigateTo({ |
||||
|
url:"/subPackages/equityGoods/detail?id="+agent.vos[0].benefitPackageId |
||||
|
}) |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
|
||||
|
.activate-agent-popup { |
||||
|
background: #ffffff; |
||||
|
border-radius: 24rpx 24rpx 0 0; |
||||
|
padding: 40rpx 30rpx; |
||||
|
max-height: 80vh; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
|
||||
|
// 弹窗头部 |
||||
|
.popup-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-bottom: 10rpx; |
||||
|
padding-bottom: 20rpx; |
||||
|
} |
||||
|
|
||||
|
.popup-title { |
||||
|
font-size: 36rpx; |
||||
|
font-weight: bold; |
||||
|
color: #000000; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
|
||||
|
.popup-close { |
||||
|
font-size: 48rpx; |
||||
|
color: #999999; |
||||
|
padding: 10rpx; |
||||
|
line-height: 1; |
||||
|
} |
||||
|
|
||||
|
// AGENT列表 |
||||
|
.agent-list { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 30rpx; |
||||
|
} |
||||
|
|
||||
|
.agent-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
padding: 20rpx; |
||||
|
background: #F1F1F1; |
||||
|
border-radius: 20rpx; |
||||
|
transition: all 0.3s ease; |
||||
|
|
||||
|
&:active { |
||||
|
transform: scale(0.98); |
||||
|
background: #f5f5f5; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 头像区域 |
||||
|
.agent-avatar { |
||||
|
margin-right: 24rpx; |
||||
|
} |
||||
|
|
||||
|
.avatar-bubble { |
||||
|
width: 100rpx; |
||||
|
height: 100rpx; |
||||
|
border-radius: 50%; |
||||
|
|
||||
|
backdrop-filter: blur(10rpx); |
||||
|
border: 2rpx solid rgba(255, 255, 255, 0.2); |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
overflow: hidden; |
||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1); |
||||
|
} |
||||
|
|
||||
|
.avatar-img { |
||||
|
width: 80rpx; |
||||
|
height: 80rpx; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
|
||||
|
// 信息区域 |
||||
|
.agent-info { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.agent-name { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 4rpx; |
||||
|
} |
||||
|
|
||||
|
.name-chinese { |
||||
|
font-size: 32rpx; |
||||
|
font-weight: bold; |
||||
|
color: #333333; |
||||
|
line-height: 1.2; |
||||
|
} |
||||
|
|
||||
|
.name-pinyin { |
||||
|
font-size: 24rpx; |
||||
|
color: #77f3f9; |
||||
|
font-weight: 500; |
||||
|
line-height: 1.2; |
||||
|
} |
||||
|
|
||||
|
// 状态按钮 |
||||
|
.agent-status { |
||||
|
margin-left: 20rpx; |
||||
|
} |
||||
|
|
||||
|
.status-btn { |
||||
|
padding: 12rpx 24rpx; |
||||
|
border-radius: 20rpx; |
||||
|
min-width: 120rpx; |
||||
|
text-align: center; |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.status-inactive { |
||||
|
background: #354242; |
||||
|
color: #ffffff; |
||||
|
} |
||||
|
|
||||
|
.status-active { |
||||
|
background: linear-gradient(135deg, #fffdb7 0%, #97fffa 100%); |
||||
|
color: #000000; |
||||
|
} |
||||
|
|
||||
|
.status-text { |
||||
|
font-size: 26rpx; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,429 @@ |
|||||
|
<template> |
||||
|
<view class="area-picker"> |
||||
|
<picker |
||||
|
mode="multiSelector" |
||||
|
:range="newProvinceDataList" |
||||
|
range-key="name" |
||||
|
@change="changeArea" |
||||
|
@columnchange="pickerColumnchange" |
||||
|
:value="multiIndex" |
||||
|
> |
||||
|
<!-- 使用插槽,允许父组件自定义显示样式 --> |
||||
|
<slot |
||||
|
:selectedText="selectedText" |
||||
|
:placeholder="placeholder" |
||||
|
:provinceData="newProvinceDataList[0]" |
||||
|
:cityData="newProvinceDataList[1]" |
||||
|
:areaData="newProvinceDataList[2]" |
||||
|
:multiIndex="multiIndex" |
||||
|
:currentSelection="getCurrentSelection()" |
||||
|
> |
||||
|
<!-- 默认显示样式 --> |
||||
|
<view class="picker-display"> |
||||
|
<text class="picker-text" :class="{ 'placeholder': !selectedText }">{{ selectedText || placeholder }}</text> |
||||
|
<image |
||||
|
class="dropdown-icon" |
||||
|
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOCIgdmlld0JveD0iMCAwIDEyIDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xIDFMNiA2TDExIDEiIHN0cm9rZT0iIzk5OTk5OSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+" |
||||
|
mode="heightFix" |
||||
|
></image> |
||||
|
</view> |
||||
|
</slot> |
||||
|
</picker> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: 'AreaPicker', |
||||
|
props: { |
||||
|
// 占位符文本 |
||||
|
placeholder: { |
||||
|
type: String, |
||||
|
default: '请选择地区' |
||||
|
}, |
||||
|
// 默认选中的省市区ID |
||||
|
defaultValue: { |
||||
|
type: Object, |
||||
|
default: () => ({ |
||||
|
provinceId: null, |
||||
|
cityId: null, |
||||
|
areaId: null |
||||
|
}) |
||||
|
}, |
||||
|
// 是否禁用 |
||||
|
disabled: { |
||||
|
type: Boolean, |
||||
|
default: false |
||||
|
}, |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
// 省市区数据 |
||||
|
columns: [], |
||||
|
newProvinceDataList: [[], [], []], |
||||
|
multiIndex: [0, 0, 0], |
||||
|
provinceId: null, |
||||
|
cityId: null, |
||||
|
areaId: null, |
||||
|
ready: false, |
||||
|
selectedText: '', |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.getSeldCityList() |
||||
|
}, |
||||
|
watch: { |
||||
|
defaultValue: { |
||||
|
handler(newVal) { |
||||
|
console.log('----有值回显') |
||||
|
if (newVal && newVal.provinceId && this.ready) { |
||||
|
this.setDefaultValue(newVal) |
||||
|
} |
||||
|
}, |
||||
|
deep: true, |
||||
|
immediate: true |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
// 获取省市区数据 |
||||
|
getSeldCityList() { |
||||
|
// 尝试多种方式获取数据 |
||||
|
let requestMethod = null; |
||||
|
|
||||
|
// 1. 尝试使用父组件的 Post 方法 |
||||
|
if (this.$parent && this.$parent.Post) { |
||||
|
requestMethod = this.$parent.Post; |
||||
|
} |
||||
|
// 2. 尝试使用全局的 Post 方法(如果通过 mixin 引入) |
||||
|
else if (this.Post) { |
||||
|
requestMethod = this.Post; |
||||
|
} |
||||
|
// 3. 尝试使用 uni.request |
||||
|
else { |
||||
|
this.requestWithUni(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
requestMethod({}, '/api/areas/getAll').then(res => { |
||||
|
if (res) { |
||||
|
this.processAreaData(res.data) |
||||
|
} |
||||
|
}).catch(err => { |
||||
|
console.warn('省市区数据获取失败:', err) |
||||
|
// 可以在这里设置一些默认数据或提示用户 |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
// 使用 uni.request 获取数据的备用方法 |
||||
|
requestWithUni() { |
||||
|
uni.request({ |
||||
|
url: '/api/areas/getAll', // 这里需要根据实际的完整URL调整 |
||||
|
method: 'POST', |
||||
|
data: {}, |
||||
|
success: (res) => { |
||||
|
if (res.data) { |
||||
|
this.processAreaData(res.data.data || res.data) |
||||
|
} |
||||
|
}, |
||||
|
fail: (err) => { |
||||
|
console.warn('省市区数据获取失败:', err) |
||||
|
// 可以提供一些默认的省市区数据或提示用户 |
||||
|
uni.showToast({ |
||||
|
title: '地区数据加载失败', |
||||
|
icon: 'none' |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
// 处理省市区数据 |
||||
|
processAreaData(data) { |
||||
|
var result = {} |
||||
|
for (var i = 0; i < data.length; i++) { |
||||
|
var item = data[i] |
||||
|
if (item.parent_id == 0) { |
||||
|
continue |
||||
|
} |
||||
|
// 获取省 |
||||
|
if (item.parent_id == "1") { |
||||
|
result[item.id.toString()] = {} |
||||
|
result[item.id.toString()].children = [] |
||||
|
result[item.id.toString()].name = item.name |
||||
|
result[item.id.toString()].id = item.id |
||||
|
} else if (result[item.parent_id.toString()]) { |
||||
|
// 填充市 |
||||
|
var t = { |
||||
|
id: item.id, |
||||
|
name: item.name, |
||||
|
children: [] |
||||
|
} |
||||
|
result[item.parent_id.toString()].children.push(t) |
||||
|
} else { |
||||
|
// 填充区 |
||||
|
var k = { |
||||
|
id: item.id, |
||||
|
name: item.name |
||||
|
} |
||||
|
for (var j = 0; j < result[item.parent_id.toString().substr(0, 2) + "0000"].children.length; j++) { |
||||
|
if (result[item.parent_id.toString().substr(0, 2) + "0000"].children[j].id == item.parent_id) { |
||||
|
result[item.parent_id.toString().substr(0, 2) + "0000"].children[j].children.push(k) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var r = [] |
||||
|
// 将Object转为Array |
||||
|
for (var i in result) { |
||||
|
r.push(result[i]) |
||||
|
} |
||||
|
|
||||
|
this.columns = r |
||||
|
this.initPickerData() |
||||
|
this.ready = true |
||||
|
|
||||
|
// 如果有默认值,设置默认选中 |
||||
|
if (this.defaultValue && this.defaultValue.provinceId) { |
||||
|
this.setDefaultValue(this.defaultValue) |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 初始化picker数据 |
||||
|
initPickerData() { |
||||
|
if (this.columns.length === 0) return |
||||
|
|
||||
|
// 初始化省份数据 |
||||
|
this.newProvinceDataList[0] = this.columns.map(item => ({ |
||||
|
name: item.name, |
||||
|
id: item.id |
||||
|
})) |
||||
|
|
||||
|
// 初始化城市数据(默认第一个省份的城市) |
||||
|
if (this.columns[0] && this.columns[0].children) { |
||||
|
this.newProvinceDataList[1] = this.columns[0].children.map(item => ({ |
||||
|
name: item.name, |
||||
|
id: item.id |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
// 初始化区县数据(默认第一个城市的区县) |
||||
|
if (this.columns[0] && this.columns[0].children[0] && this.columns[0].children[0].children) { |
||||
|
this.newProvinceDataList[2] = this.columns[0].children[0].children.map(item => ({ |
||||
|
name: item.name, |
||||
|
id: item.id |
||||
|
})) |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 设置默认值 |
||||
|
setDefaultValue(defaultValue) { |
||||
|
if (!this.ready || !defaultValue) return |
||||
|
|
||||
|
// 查找省份索引 |
||||
|
const provinceIndex = this.newProvinceDataList[0].findIndex(item => item.id == defaultValue.provinceId) |
||||
|
if (provinceIndex === -1) return |
||||
|
|
||||
|
this.multiIndex[0] = provinceIndex |
||||
|
|
||||
|
// 更新城市数据 |
||||
|
this.newProvinceDataList[1] = this.columns[provinceIndex].children.map(item => ({ |
||||
|
name: item.name, |
||||
|
id: item.id |
||||
|
})) |
||||
|
|
||||
|
// 查找城市索引 |
||||
|
const cityIndex = this.newProvinceDataList[1].findIndex(item => item.id == defaultValue.cityId) |
||||
|
if (cityIndex !== -1) { |
||||
|
this.multiIndex[1] = cityIndex |
||||
|
|
||||
|
// 更新区县数据 |
||||
|
this.newProvinceDataList[2] = this.columns[provinceIndex].children[cityIndex].children.map(item => ({ |
||||
|
name: item.name, |
||||
|
id: item.id |
||||
|
})) |
||||
|
|
||||
|
// 查找区县索引 |
||||
|
const areaIndex = this.newProvinceDataList[2].findIndex(item => item.id == defaultValue.areaId) |
||||
|
if (areaIndex !== -1) { |
||||
|
this.multiIndex[2] = areaIndex |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.updateSelectedText() |
||||
|
}, |
||||
|
|
||||
|
// 选择完成事件 |
||||
|
changeArea(e) { |
||||
|
this.multiIndex = e.detail.value |
||||
|
this.updateSelectedText() |
||||
|
this.emitChange() |
||||
|
}, |
||||
|
|
||||
|
// 列滑动事件 |
||||
|
pickerColumnchange(e) { |
||||
|
const column = e.detail.column |
||||
|
const value = e.detail.value |
||||
|
|
||||
|
if (column === 0) { |
||||
|
// 第一列滑动(省份) |
||||
|
this.multiIndex[0] = value |
||||
|
|
||||
|
// 更新城市数据 |
||||
|
this.newProvinceDataList[1] = this.columns[this.multiIndex[0]].children.map(item => ({ |
||||
|
name: item.name, |
||||
|
id: item.id |
||||
|
})) |
||||
|
|
||||
|
// 更新区县数据 |
||||
|
if (this.columns[this.multiIndex[0]].children.length === 1) { |
||||
|
this.newProvinceDataList[2] = this.columns[this.multiIndex[0]].children[0].children.map(item => ({ |
||||
|
name: item.name, |
||||
|
id: item.id |
||||
|
})) |
||||
|
} else { |
||||
|
this.newProvinceDataList[2] = this.columns[this.multiIndex[0]].children[this.multiIndex[1]].children.map(item => ({ |
||||
|
name: item.name, |
||||
|
id: item.id |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
// 重置城市和区县索引 |
||||
|
this.multiIndex.splice(1, 1, 0) |
||||
|
this.multiIndex.splice(2, 1, 0) |
||||
|
} else if (column === 1) { |
||||
|
// 第二列滑动(城市) |
||||
|
this.multiIndex[1] = value |
||||
|
|
||||
|
// 更新区县数据 |
||||
|
this.newProvinceDataList[2] = this.columns[this.multiIndex[0]].children[this.multiIndex[1]].children.map(item => ({ |
||||
|
name: item.name, |
||||
|
id: item.id |
||||
|
})) |
||||
|
|
||||
|
// 重置区县索引 |
||||
|
this.multiIndex.splice(2, 1, 0) |
||||
|
} else if (column === 2) { |
||||
|
// 第三列滑动(区县) |
||||
|
this.multiIndex[2] = value |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 更新选中的省市区ID和显示文本 |
||||
|
updateSelectedText() { |
||||
|
if (this.newProvinceDataList[0][this.multiIndex[0]] && |
||||
|
this.newProvinceDataList[1][this.multiIndex[1]] && |
||||
|
this.newProvinceDataList[2][this.multiIndex[2]]) { |
||||
|
|
||||
|
this.selectedText = |
||||
|
this.newProvinceDataList[1][this.multiIndex[1]].name |
||||
|
|
||||
|
this.provinceId = this.newProvinceDataList[0][this.multiIndex[0]].id |
||||
|
this.cityId = this.newProvinceDataList[1][this.multiIndex[1]].id |
||||
|
this.areaId = this.newProvinceDataList[2][this.multiIndex[2]].id |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 触发change事件 |
||||
|
emitChange() { |
||||
|
const selectedText = this.newProvinceDataList[0][this.multiIndex[0]].name + |
||||
|
this.newProvinceDataList[1][this.multiIndex[1]].name + |
||||
|
this.newProvinceDataList[2][this.multiIndex[2]].name |
||||
|
|
||||
|
const result = { |
||||
|
provinceId: this.provinceId, |
||||
|
cityId: this.cityId, |
||||
|
areaId: this.areaId, |
||||
|
province: this.newProvinceDataList[0][this.multiIndex[0]]?.name || '', |
||||
|
city: this.newProvinceDataList[1][this.multiIndex[1]]?.name || '', |
||||
|
area: this.newProvinceDataList[2][this.multiIndex[2]]?.name || '', |
||||
|
fullText: selectedText |
||||
|
} |
||||
|
|
||||
|
this.$emit('change', result) |
||||
|
}, |
||||
|
|
||||
|
// 获取当前选中值 |
||||
|
getValue() { |
||||
|
const selectedText = this.newProvinceDataList[0][this.multiIndex[0]]?.name + |
||||
|
this.newProvinceDataList[1][this.multiIndex[1]]?.name + |
||||
|
this.newProvinceDataList[2][this.multiIndex[2]]?.name |
||||
|
|
||||
|
return { |
||||
|
provinceId: this.provinceId, |
||||
|
cityId: this.cityId, |
||||
|
areaId: this.areaId, |
||||
|
provinceName: this.newProvinceDataList[0][this.multiIndex[0]]?.name || '', |
||||
|
cityName: this.newProvinceDataList[1][this.multiIndex[1]]?.name || '', |
||||
|
areaName: this.newProvinceDataList[2][this.multiIndex[2]]?.name || '', |
||||
|
fullText: selectedText |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 重置选择 |
||||
|
reset() { |
||||
|
this.multiIndex = [0, 0, 0] |
||||
|
this.selectedText = '' |
||||
|
this.provinceId = null |
||||
|
this.cityId = null |
||||
|
this.areaId = null |
||||
|
this.initPickerData() |
||||
|
}, |
||||
|
|
||||
|
// 获取当前选中的详细信息(供插槽使用) |
||||
|
getCurrentSelection() { |
||||
|
if (!this.newProvinceDataList[0][this.multiIndex[0]] || |
||||
|
!this.newProvinceDataList[1][this.multiIndex[1]] || |
||||
|
!this.newProvinceDataList[2][this.multiIndex[2]]) { |
||||
|
return { |
||||
|
province: null, |
||||
|
city: null, |
||||
|
area: null, |
||||
|
fullText: '' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
province: this.newProvinceDataList[0][this.multiIndex[0]], |
||||
|
city: this.newProvinceDataList[1][this.multiIndex[1]], |
||||
|
area: this.newProvinceDataList[2][this.multiIndex[2]], |
||||
|
fullText: this.newProvinceDataList[0][this.multiIndex[0]].name + |
||||
|
this.newProvinceDataList[1][this.multiIndex[1]].name + |
||||
|
this.newProvinceDataList[2][this.multiIndex[2]].name |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped lang="scss"> |
||||
|
.area-picker { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.picker-display { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
padding: 8rpx 16rpx; |
||||
|
border: 2rpx solid #e0e0e0; |
||||
|
border-radius: 8rpx; |
||||
|
background-color: #fff; |
||||
|
min-height: 60rpx; |
||||
|
} |
||||
|
|
||||
|
.picker-text { |
||||
|
font-size: 30rpx; |
||||
|
color: #333; |
||||
|
flex: 1; |
||||
|
|
||||
|
&.placeholder { |
||||
|
color: #999; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.dropdown-icon { |
||||
|
width: 24rpx; |
||||
|
height: 16rpx; |
||||
|
margin-left: 8rpx; |
||||
|
} |
||||
|
</style> |
||||
@ -1,155 +0,0 @@ |
|||||
# AudioControl 音频控制组件使用文档 |
|
||||
|
|
||||
## 组件功能 |
|
||||
- 在父组件右上角显示音频控制图标 |
|
||||
- 点击图标播放指定音频,同时暂停背景音乐 |
|
||||
- 再次点击暂停音频,恢复背景音乐 |
|
||||
- 音频播放结束后自动恢复背景音乐 |
|
||||
|
|
||||
## 组件属性 (Props) |
|
||||
|
|
||||
| 属性名 | 类型 | 必填 | 默认值 | 说明 | |
|
||||
|--------|------|------|--------|------| |
|
||||
| audioSrc | String | 是 | - | 音频文件路径 | |
|
||||
| visible | Boolean | 否 | true | 是否显示组件 | |
|
||||
|
|
||||
## 使用方法 |
|
||||
|
|
||||
### 1. 在父组件中引入和注册组件 |
|
||||
|
|
||||
```vue |
|
||||
<template> |
|
||||
<view class="parent-container"> |
|
||||
<!-- 父组件内容 --> |
|
||||
<view class="content"> |
|
||||
<!-- 你的页面内容 --> |
|
||||
</view> |
|
||||
|
|
||||
<!-- 音频控制组件 --> |
|
||||
<AudioControl |
|
||||
:audioSrc="audioUrl" |
|
||||
:visible="showAudio" |
|
||||
/> |
|
||||
</view> |
|
||||
</template> |
|
||||
|
|
||||
<script> |
|
||||
import AudioControl from '@/components/AudioControl.vue'; |
|
||||
|
|
||||
export default { |
|
||||
components: { |
|
||||
AudioControl |
|
||||
}, |
|
||||
data() { |
|
||||
return { |
|
||||
audioUrl: 'https://your-domain.com/audio/sample.mp3', // 替换为你的音频URL |
|
||||
showAudio: true |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<style> |
|
||||
.parent-container { |
|
||||
position: relative; /* 重要:确保AudioControl能正确定位 */ |
|
||||
width: 100vw; |
|
||||
height: 100vh; |
|
||||
} |
|
||||
</style> |
|
||||
``` |
|
||||
|
|
||||
### 2. 使用项目中的showImg方法(推荐) |
|
||||
|
|
||||
如果你的音频文件也存储在项目服务器上,可以使用项目的showImg方法: |
|
||||
|
|
||||
```vue |
|
||||
<script> |
|
||||
export default { |
|
||||
data() { |
|
||||
return { |
|
||||
audioUrl: this.showImg('/uploads/audio/your-audio-file.mp3'), |
|
||||
showAudio: true |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
``` |
|
||||
|
|
||||
### 3. 动态控制音频源 |
|
||||
|
|
||||
你可以根据不同的页面或条件播放不同的音频: |
|
||||
|
|
||||
```vue |
|
||||
<script> |
|
||||
export default { |
|
||||
data() { |
|
||||
return { |
|
||||
audioUrl: '', |
|
||||
showAudio: true |
|
||||
} |
|
||||
}, |
|
||||
mounted() { |
|
||||
// 根据页面设置不同的音频 |
|
||||
this.setAudioForCurrentPage(); |
|
||||
}, |
|
||||
methods: { |
|
||||
setAudioForCurrentPage() { |
|
||||
const currentRoute = this.$route.path; // 假设使用vue-router |
|
||||
|
|
||||
switch(currentRoute) { |
|
||||
case '/chapter1': |
|
||||
this.audioUrl = this.showImg('/uploads/audio/chapter1.mp3'); |
|
||||
break; |
|
||||
case '/chapter2': |
|
||||
this.audioUrl = this.showImg('/uploads/audio/chapter2.mp3'); |
|
||||
break; |
|
||||
default: |
|
||||
this.audioUrl = this.showImg('/uploads/audio/default.mp3'); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
``` |
|
||||
|
|
||||
## 样式说明 |
|
||||
|
|
||||
组件默认定位在父组件的右上角(top: 30rpx, right: 30rpx),如果需要调整位置,可以在父组件中覆盖样式: |
|
||||
|
|
||||
```vue |
|
||||
<style> |
|
||||
/* 调整音频控制组件位置 */ |
|
||||
.parent-container ::v-deep .audio-control { |
|
||||
top: 50rpx !important; |
|
||||
right: 50rpx !important; |
|
||||
} |
|
||||
</style> |
|
||||
``` |
|
||||
|
|
||||
## 注意事项 |
|
||||
|
|
||||
1. **父组件样式**:确保父组件设置了 `position: relative`,这样AudioControl组件才能正确定位 |
|
||||
2. **音频格式**:建议使用 mp3 格式的音频文件,兼容性最好 |
|
||||
3. **音频路径**:确保音频文件路径正确且可访问 |
|
||||
4. **背景音乐**:组件会自动处理与MusicControl组件的交互,无需额外配置 |
|
||||
|
|
||||
## 图标说明 |
|
||||
|
|
||||
- 🔊:音频未播放状态 |
|
||||
- 🎧:音频播放中状态,带有脉动动画效果 |
|
||||
|
|
||||
## 事件处理 |
|
||||
|
|
||||
组件内部已处理所有音频播放逻辑,包括: |
|
||||
- 播放音频时自动暂停背景音乐 |
|
||||
- 暂停音频时自动恢复背景音乐 |
|
||||
- 音频播放结束时自动恢复背景音乐 |
|
||||
- 组件销毁时自动清理资源 |
|
||||
|
|
||||
## 示例场景 |
|
||||
|
|
||||
适用于以下场景: |
|
||||
- 章节页面播放对应的音频解说 |
|
||||
- 展示页面播放介绍音频 |
|
||||
- 互动页面播放提示音频 |
|
||||
- 任何需要临时播放音频并暂停背景音乐的场景 |
|
||||
@ -0,0 +1,97 @@ |
|||||
|
<template> |
||||
|
<view |
||||
|
class="back-button-container" |
||||
|
:style="{ paddingTop: statusBarHeight + 'px' }" |
||||
|
> |
||||
|
<view @click="handleBack" class="back-btn"> |
||||
|
<image class="back-icon" :src="iconSrc" mode="aspectFill"></image> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: "BackButton", |
||||
|
props: { |
||||
|
// 自定义图标路径 |
||||
|
iconSrc: { |
||||
|
type: String, |
||||
|
default: "https://epic.js-dyyj.com/uploads/20250825/f7e4825867dbd90e2cd0721a49fad6eb.png", |
||||
|
}, |
||||
|
// 自定义返回逻辑 |
||||
|
customBack: { |
||||
|
type: Function, |
||||
|
default: null, |
||||
|
}, |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
statusBarHeight: 0, |
||||
|
}; |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.setStatusBarHeight(); |
||||
|
}, |
||||
|
methods: { |
||||
|
setStatusBarHeight() { |
||||
|
try { |
||||
|
const systemInfo = uni.getSystemInfoSync(); |
||||
|
this.statusBarHeight = systemInfo.statusBarHeight || 0; |
||||
|
} catch (e) { |
||||
|
// 开发工具或获取失败时使用默认值 |
||||
|
this.statusBarHeight = 0; |
||||
|
} |
||||
|
}, |
||||
|
handleBack() { |
||||
|
if (this.customBack) { |
||||
|
// 使用自定义返回逻辑 |
||||
|
this.customBack(); |
||||
|
} else { |
||||
|
// 默认返回逻辑 |
||||
|
uni.navigateBack({ |
||||
|
delta: 1, |
||||
|
fail: () => { |
||||
|
// 如果返回失败,跳转到首页 |
||||
|
uni.switchTab({ |
||||
|
url: "/pages/index/index", |
||||
|
}); |
||||
|
}, |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.back-button-container { |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
z-index: 999; |
||||
|
padding-left: 20rpx; |
||||
|
padding-bottom: 20rpx; |
||||
|
} |
||||
|
|
||||
|
.back-btn { |
||||
|
width: 80rpx; |
||||
|
height: 80rpx; |
||||
|
background-color: rgba(0, 0, 0, 0.3); |
||||
|
border-radius: 50%; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
backdrop-filter: blur(10rpx); |
||||
|
transition: all 0.3s ease; |
||||
|
|
||||
|
&:active { |
||||
|
transform: scale(0.95); |
||||
|
background-color: rgba(0, 0, 0, 0.5); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.back-icon { |
||||
|
width: 40rpx; |
||||
|
height: 40rpx; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,320 @@ |
|||||
|
<template> |
||||
|
<view class="product-section"> |
||||
|
<view class="title-section"> |
||||
|
<div |
||||
|
style=" |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
" |
||||
|
@click="handleMoreClick" |
||||
|
> |
||||
|
<text class="title">EPIC SOUL阅读体</text> |
||||
|
<view class="more-btn"><image src="https://des.dayunyuanjian.cn/data/2025/08/31/affb21f0-fcc1-4746-9543-0d54369aa315.png" style="width:124.6rpx;height: 36rpx;"/></view> |
||||
|
</div> |
||||
|
</view> |
||||
|
<!-- 轮播容器 --> |
||||
|
<view class="carousel-container"> |
||||
|
<!-- 左箭头 --> |
||||
|
<view |
||||
|
class="nav-arrow left-arrow" |
||||
|
@click="prevSlide" |
||||
|
v-if="list.length > 1 && currentIndex > 0" |
||||
|
> |
||||
|
<image style="width: 50rpx;height: 50rpx;" :src="showImg('/uploads/20250908/29beeddf1e45571d2c5a4187f2f1ae05.png')"></image> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 轮播内容 --> |
||||
|
<scroll-view |
||||
|
class="carousel-scroll" |
||||
|
scroll-x="true" |
||||
|
:show-scrollbar="false" |
||||
|
:enhanced="true" |
||||
|
:scroll-with-animation="true" |
||||
|
:scroll-left="scrollLeft" |
||||
|
@scroll="onScroll" |
||||
|
@scrollend="onScrollEnd" |
||||
|
> |
||||
|
<view class="carousel-content"> |
||||
|
<view |
||||
|
class="carousel-item" |
||||
|
v-for="(item, index) in list" |
||||
|
:key="index" |
||||
|
@click="gotoUrlNew(item)" |
||||
|
v-if="item && item.image" |
||||
|
> |
||||
|
<view class="issue-card"> |
||||
|
<!-- 背景图片 --> |
||||
|
<image |
||||
|
class="card-bg" |
||||
|
:src="showImg(item.image)" |
||||
|
mode="aspectFill" |
||||
|
></image> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 无数据时的提示 --> |
||||
|
<view v-if="!list || list.length === 0" class="no-data"> |
||||
|
<text>暂无数据</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</scroll-view> |
||||
|
|
||||
|
<!-- 右箭头 --> |
||||
|
<view |
||||
|
class="nav-arrow right-arrow" |
||||
|
@click="nextSlide" |
||||
|
v-if="list.length > 1 && currentIndex < list.length - 1" |
||||
|
> |
||||
|
<image style="width: 50rpx;height: 50rpx;" :src="showImg('/uploads/20250908/6622b3699518d6b559e1241d7addb7af.png')"></image> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
data() { |
||||
|
return { |
||||
|
list: [], |
||||
|
currentIndex: 0, |
||||
|
scrollLeft: 0, |
||||
|
itemWidth: 224, // 207rpx + 30rpx margin |
||||
|
}; |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.getList(); |
||||
|
}, |
||||
|
methods: { |
||||
|
handleMoreClick(){ |
||||
|
uni.switchTab({ |
||||
|
url:'/pages/index/readingBody' |
||||
|
}) |
||||
|
}, |
||||
|
getList() { |
||||
|
this.Post( |
||||
|
{ |
||||
|
type_id: 3, |
||||
|
offset: 0, |
||||
|
limit: 10, // 增加获取数量,确保有足够数据 |
||||
|
}, |
||||
|
"/api/article/getArticleByType" |
||||
|
).then((res) => { |
||||
|
if (res.data && res.data.length > 0) { |
||||
|
this.list = res.data; |
||||
|
// 重置索引,确保从第一项开始 |
||||
|
this.currentIndex = 0; |
||||
|
this.scrollLeft = 0; |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
// 上一张 |
||||
|
prevSlide() { |
||||
|
if (!this.validateData()) return; |
||||
|
|
||||
|
if (this.list.length > 1) { |
||||
|
this.currentIndex = |
||||
|
this.currentIndex > 0 ? this.currentIndex - 1 : this.list.length - 1; |
||||
|
this.$nextTick(() => { |
||||
|
this.scrollToCurrentItem(); |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 下一张 |
||||
|
nextSlide() { |
||||
|
if (!this.validateData()) return; |
||||
|
|
||||
|
if (this.list.length > 1) { |
||||
|
this.currentIndex = |
||||
|
this.currentIndex < this.list.length - 1 ? this.currentIndex + 1 : 0; |
||||
|
this.$nextTick(() => { |
||||
|
this.scrollToCurrentItem(); |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 滚动到当前项目 |
||||
|
scrollToCurrentItem() { |
||||
|
// 确保索引在有效范围内 |
||||
|
if (this.currentIndex < 0) { |
||||
|
this.currentIndex = 0; |
||||
|
} |
||||
|
if (this.currentIndex >= this.list.length) { |
||||
|
this.currentIndex = this.list.length - 1; |
||||
|
} |
||||
|
|
||||
|
const scrollPosition = this.currentIndex * this.itemWidth; |
||||
|
this.scrollLeft = scrollPosition; |
||||
|
}, |
||||
|
|
||||
|
// 滚动事件 |
||||
|
onScroll(e) { |
||||
|
const scrollLeft = e.detail.scrollLeft; |
||||
|
const index = Math.round(scrollLeft / this.itemWidth); |
||||
|
if ( |
||||
|
index !== this.currentIndex && |
||||
|
index >= 0 && |
||||
|
index < this.list.length |
||||
|
) { |
||||
|
this.currentIndex = index; |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 滚动结束事件 |
||||
|
onScrollEnd(e) { |
||||
|
const scrollLeft = e.detail.scrollLeft; |
||||
|
const index = Math.round(scrollLeft / this.itemWidth); |
||||
|
// 确保索引在有效范围内 |
||||
|
this.currentIndex = Math.max(0, Math.min(index, this.list.length - 1)); |
||||
|
this.$nextTick(() => { |
||||
|
this.scrollToCurrentItem(); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 点击卡片 |
||||
|
handleItemClick(item) { |
||||
|
console.log("点击了卡片:", item); |
||||
|
// 这里可以添加跳转逻辑 |
||||
|
}, |
||||
|
|
||||
|
// 验证数据有效性 |
||||
|
validateData() { |
||||
|
if (!this.list || this.list.length === 0) { |
||||
|
this.currentIndex = 0; |
||||
|
this.scrollLeft = 0; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// 确保当前索引在有效范围内 |
||||
|
if (this.currentIndex < 0) { |
||||
|
this.currentIndex = 0; |
||||
|
} |
||||
|
if (this.currentIndex >= this.list.length) { |
||||
|
this.currentIndex = this.list.length - 1; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.product-section { |
||||
|
width: 100%; |
||||
|
background-color: #fffdd6; |
||||
|
padding: 40rpx 25rpx; |
||||
|
margin: 30rpx 0; |
||||
|
border-radius: 40rpx; |
||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1), |
||||
|
0 0 0 1rpx rgba(255, 255, 255, 0.2) inset; |
||||
|
} |
||||
|
|
||||
|
// 轮播容器 |
||||
|
.carousel-container { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
height: 368rpx; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
// 轮播滚动视图 |
||||
|
.carousel-scroll { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
// 轮播内容 |
||||
|
.carousel-content { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
height: 100%; |
||||
|
min-width: max-content; |
||||
|
} |
||||
|
|
||||
|
// 轮播项 |
||||
|
.carousel-item { |
||||
|
flex-shrink: 0; |
||||
|
width: 210rpx; |
||||
|
height: 368rpx; |
||||
|
margin: 0 7rpx; |
||||
|
will-change: transform; |
||||
|
} |
||||
|
|
||||
|
// 卡片样式 |
||||
|
.issue-card { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
// 背景图片 |
||||
|
.card-bg { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
// 导航箭头 |
||||
|
.nav-arrow { |
||||
|
position: absolute; |
||||
|
top: 50%; |
||||
|
transform: translateY(-50%); |
||||
|
width: 60rpx; |
||||
|
height: 60rpx; |
||||
|
border-radius: 50%; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
cursor: pointer; |
||||
|
z-index: 3; |
||||
|
transition: all 0.3s ease; |
||||
|
&.left-arrow { |
||||
|
left: 0rpx; |
||||
|
} |
||||
|
|
||||
|
&.right-arrow { |
||||
|
right: 0rpx; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.arrow-icon { |
||||
|
font-size: 32rpx; |
||||
|
color: #ffffff; |
||||
|
font-weight: bold; |
||||
|
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
// 无数据提示 |
||||
|
.no-data { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
width: 100%; |
||||
|
height: 368rpx; |
||||
|
color: #999; |
||||
|
font-size: 28rpx; |
||||
|
} |
||||
|
|
||||
|
// 标题区域 |
||||
|
.title-section { |
||||
|
display: inline-block; |
||||
|
padding: 0rpx 0 30rpx; |
||||
|
width: 100%; |
||||
|
.title { |
||||
|
font-size: 34rpx; |
||||
|
font-weight: bold; |
||||
|
color: #000000; |
||||
|
} |
||||
|
|
||||
|
.more-btn { |
||||
|
font-size: 26rpx; |
||||
|
color: #000000; |
||||
|
margin-left: 35rpx; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -1,104 +1,149 @@ |
|||||
<template> |
<template> |
||||
<view class="custom-tab-bar"> |
<view class=""> |
||||
<view class="tab-item" v-for="(item,i) in tabBarList" :key="i" v-if="tabBarShowList[i]" @click="switchTab(i)"> |
<view class="custom-tab-bar-placeholder"></view> |
||||
<text :style="{ 'color': currentTab === i?item.selectColor:'#fff' }">{{ item.text }}</text> |
<view class="custom-tab-bar"> |
||||
</view> |
<view |
||||
</view> |
class="tab-item" |
||||
|
v-for="(item, i) in tabBarList" |
||||
|
:key="i" |
||||
|
v-if="tabBarShowList[i]" |
||||
|
@click="switchTab(i)" |
||||
|
> |
||||
|
<image |
||||
|
:src="currentTab === i ? item.select_img : item.img" |
||||
|
style="height: 80rpx;" |
||||
|
:style="{'width':item.width}" |
||||
|
></image> |
||||
|
<!-- <text :style="{ color: currentTab === i ? item.selectColor : '#fff' }">{{ |
||||
|
item.text |
||||
|
}}</text> --> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
</template> |
</template> |
||||
|
|
||||
<script> |
<script> |
||||
export default { |
export default { |
||||
props: { |
props: { |
||||
currentTab: { |
currentTab: { |
||||
type: Number, |
type: Number, |
||||
default: 0 |
default: 0, |
||||
} |
}, |
||||
}, |
}, |
||||
data() { |
data() { |
||||
return { |
return { |
||||
tabBarList: [{ |
tabBarList: [ |
||||
"pagePath": "pages/index/index", |
{ |
||||
"selectColor": "#00FF00", |
pagePath: "pages/index/index", |
||||
"text": "首页" |
selectColor: "#00FF00", |
||||
}, |
img: require("@/static/image/tabbar/home.png"), |
||||
{ |
select_img: require("@/static/image/tabbar/home_select.png"), |
||||
"pagePath": "pages/index/readingBody", |
text: "首页", |
||||
"selectColor": "#00FF00", |
width:'53rpx' |
||||
"text": "阅读体" |
}, |
||||
}, |
{ |
||||
{ |
pagePath: "pages/index/readingBody", |
||||
"pagePath": "pages/index/sensoryStore", |
selectColor: "#00FF00", |
||||
"selectColor": "#00FF00", |
text: "阅读体", |
||||
"text": "有感商店" |
img: require("@/static/image/tabbar/book.png"), |
||||
}, |
select_img: require("@/static/image/tabbar/book_select.png"), |
||||
{ |
width:'58rpx' |
||||
"pagePath": "pages/index/intelligentAgent", |
}, |
||||
"selectColor": "#00FFFF", |
// { |
||||
"text": "智能体" |
// pagePath: "pages/index/sensoryStore", |
||||
}, |
// selectColor: "#00FF00", |
||||
{ |
// text: "有感商店", |
||||
"pagePath": "pages/index/iSoul", |
// }, |
||||
"selectColor": "#00FF00", |
{ |
||||
"text": "iSoul" |
pagePath: "pages/index/timeShopBank", |
||||
} |
selectColor: "#00FF00", |
||||
], |
text: "时间银行", |
||||
tabBarShowList: [] |
img: require("@/static/image/tabbar/time.png"), |
||||
}; |
select_img: require("@/static/image/tabbar/time_select.png"), |
||||
}, |
width:'58rpx' |
||||
onLoad() { |
}, |
||||
this.getCurrentTab(); |
{ |
||||
}, |
pagePath: "pages/index/intelligentAgent", |
||||
mounted() { |
selectColor: "#00FFFF", |
||||
this.tabBarShowList = uni.getStorageSync('SHFlag').split(',').map(item => { |
text: "智能体", |
||||
return item.trim().toLowerCase() === 'true'; |
img: require("@/static/image/tabbar/agent.png"), |
||||
}); |
select_img: require("@/static/image/tabbar/agent_select.png"), |
||||
}, |
width:'58rpx' |
||||
methods: { |
}, |
||||
getCurrentTab() { |
{ |
||||
const pages = getCurrentPages(); |
pagePath: "pages/index/iSoul", |
||||
const currentPage = pages[pages.length - 1]; |
selectColor: "#00FF00", |
||||
const currentPath = currentPage.route; |
text: "iSoul", |
||||
this.tabBarList.forEach((item, index) => { |
img: require("@/static/image/tabbar/isoul.png"), |
||||
if (item.pagePath === currentPath) { |
select_img: require("@/static/image/tabbar/isoul_select.png"), |
||||
this.currentTab = index; |
width:'47rpx' |
||||
} |
}, |
||||
}); |
], |
||||
}, |
tabBarShowList: [], |
||||
switchTab(index) { |
}; |
||||
if (this.currentTab === index) return; |
}, |
||||
uni.switchTab({ |
onLoad() { |
||||
url: '/' + this.tabBarList[index].pagePath |
this.getCurrentTab(); |
||||
}); |
}, |
||||
} |
mounted() { |
||||
} |
this.tabBarShowList = uni |
||||
}; |
.getStorageSync("SHFlag") |
||||
|
.split(",") |
||||
|
.map((item) => { |
||||
|
return item.trim().toLowerCase() === "true"; |
||||
|
}); |
||||
|
}, |
||||
|
methods: { |
||||
|
getCurrentTab() { |
||||
|
const pages = getCurrentPages(); |
||||
|
const currentPage = pages[pages.length - 1]; |
||||
|
const currentPath = currentPage.route; |
||||
|
this.tabBarList.forEach((item, index) => { |
||||
|
if (item.pagePath === currentPath) { |
||||
|
this.currentTab = index; |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
switchTab(index) { |
||||
|
if (this.currentTab === index) return; |
||||
|
uni.switchTab({ |
||||
|
url: "/" + this.tabBarList[index].pagePath, |
||||
|
}); |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
</script> |
</script> |
||||
|
|
||||
<style scoped> |
<style scoped> |
||||
.custom-tab-bar { |
.custom-tab-bar { |
||||
position: fixed; |
position: fixed; |
||||
bottom: 0; |
bottom: 0; |
||||
left: 0; |
left: 0; |
||||
right: 0; |
right: 0; |
||||
display: flex; |
display: flex; |
||||
justify-content: space-around; |
justify-content: space-around; |
||||
align-items: center; |
align-items: center; |
||||
height: 123rpx; |
z-index: 30; |
||||
z-index: 30; |
padding: 20rpx 0; |
||||
background: #989898; |
background: white; |
||||
} |
padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx); |
||||
|
} |
||||
|
|
||||
.tab-item { |
.custom-tab-bar-placeholder { |
||||
display: flex; |
height: calc(env(safe-area-inset-bottom) + 120rpx); |
||||
flex-direction: column; |
width: 100%; |
||||
align-items: center; |
} |
||||
justify-content: center; |
|
||||
flex-shrink: 0; |
|
||||
height: 100%; |
|
||||
} |
|
||||
|
|
||||
|
.tab-item { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
flex-shrink: 0; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
.tab-item text { |
.tab-item text { |
||||
font-size: 31rpx; |
font-size: 31rpx; |
||||
} |
} |
||||
</style> |
</style> |
||||
|
|||||
@ -0,0 +1,931 @@ |
|||||
|
<template> |
||||
|
<!-- 灵动岛占位区域 - 始终存在但控制可见性 --> |
||||
|
<view |
||||
|
class="dynamic-island-placeholder" |
||||
|
:class="{ visible: isScrolled }" |
||||
|
:style="{ height: 216 + 'rpx' }" |
||||
|
> |
||||
|
<view |
||||
|
class="dynamic-island" |
||||
|
:class="{ |
||||
|
compact: actualCompactState, |
||||
|
fixed: isFixed, |
||||
|
}" |
||||
|
:style="{ top: isFixed ? fixedTopPosition + 'px' : 0 }" |
||||
|
@click="handleToggle" |
||||
|
> |
||||
|
<!-- 展开状态 --> |
||||
|
<view v-if="!actualCompactState" class="expanded-content"> |
||||
|
<template> |
||||
|
<template v-if="styleType != 'timeShop'"> |
||||
|
<!-- 三栏布局 --> |
||||
|
<view class="three-column-layout"> |
||||
|
<!-- 右侧:头像和链接 --> |
||||
|
<view class="right-section"> |
||||
|
<view class="avatar-container" @click="toWebView"> |
||||
|
<image |
||||
|
class="avatar" |
||||
|
src="https://epic.js-dyyj.com/uploads/20250826/92b0a21e9125fc21ca294a408bf3508f.png" |
||||
|
mode="aspectFill" |
||||
|
></image> |
||||
|
<view class="ai-label">智能体</view> |
||||
|
</view> |
||||
|
<view |
||||
|
class="" |
||||
|
style=" |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: space-between; |
||||
|
height: 100%; |
||||
|
" |
||||
|
> |
||||
|
<view class="profile-info"> |
||||
|
<text class="profile-title">数字领航员</text> |
||||
|
<text class="profile-name">EVITA</text> |
||||
|
</view> |
||||
|
<view class="platform-link"> |
||||
|
<view class="link-text" @click="toDesInfo" |
||||
|
>交响介绍 >> |
||||
|
</view> |
||||
|
<!-- <view class="link-text">DES广播 >></view> --> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
<!-- 左侧分隔线 --> |
||||
|
<view class="column-divider"></view> |
||||
|
<!-- 左侧:欢迎信息 --> |
||||
|
<view class="left-section"> |
||||
|
<view class="welcome-message"> |
||||
|
<view class="welcome-text" |
||||
|
>Hi! |
||||
|
{{ |
||||
|
userInfo && userInfo.token ? userInfo.nickname : "用户" |
||||
|
}}欢迎回来~</view |
||||
|
> |
||||
|
</view> |
||||
|
<view |
||||
|
class="" |
||||
|
style="font-size: 24rpx; font-weight: bold; color: #000000" |
||||
|
> |
||||
|
查看您的交响数据文化资产行 |
||||
|
</view> |
||||
|
<view |
||||
|
class="" |
||||
|
style=" |
||||
|
display: flex; |
||||
|
align-items: flex-end; |
||||
|
justify-content: space-between; |
||||
|
" |
||||
|
> |
||||
|
<view class="" @click="toOrder"> |
||||
|
<view class="stats-info"> |
||||
|
<text class="stats-number">{{ |
||||
|
(userInfo && userInfo.unUseOrderQuantity) || 0 |
||||
|
}}</text> |
||||
|
<text class="stats-unit">个</text> |
||||
|
</view> |
||||
|
<view class="stats-label">交响权益行</view> |
||||
|
</view> |
||||
|
<div @click="toTime"> |
||||
|
<view class="stats-info"> |
||||
|
<text class="stats-number">{{ |
||||
|
(userInfo && userInfo.ipQuantity) || 0 |
||||
|
}}</text> |
||||
|
<text class="stats-unit">个</text> |
||||
|
</view> |
||||
|
<view class="stats-label">交响资产行</view> |
||||
|
</div> |
||||
|
<view class="middle-section"> |
||||
|
<view class="time-reward-container" @click="toPoint"> |
||||
|
<text |
||||
|
class="time-reward-title" |
||||
|
style="margin-bottom: 5rpx" |
||||
|
>时间奖励</text |
||||
|
> |
||||
|
<view class="time-reward-stats"> |
||||
|
<text class="time-reward-number">{{ |
||||
|
userInfo && userInfo.token |
||||
|
? userInfo.hourValue || 0 |
||||
|
: 0 |
||||
|
}}</text> |
||||
|
<text class="time-reward-unit">点</text> |
||||
|
</view> |
||||
|
<text class="time-reward-label" style="font-weight: bold" |
||||
|
>交响时间行</text |
||||
|
> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
<template v-if="styleType == 'timeShop'"> |
||||
|
<view class="bottom-section"> |
||||
|
<view class="" style="flex: 1"> |
||||
|
<view class="" style="display: flex"> |
||||
|
<view |
||||
|
class="time-reward-container" |
||||
|
style="width: 200rpx" |
||||
|
@click="toPoint" |
||||
|
> |
||||
|
<text class="time-reward-title">时间奖励</text> |
||||
|
<view class="time-reward-stats"> |
||||
|
<text class="time-reward-number">{{ |
||||
|
userInfo && userInfo.token ? userInfo.hourValue || 0 : 0 |
||||
|
}}</text> |
||||
|
<text class="time-reward-unit">点</text> |
||||
|
</view> |
||||
|
<text |
||||
|
class="time-reward-label" |
||||
|
style="font-size: 24rpx; font-weight: bold" |
||||
|
>交响时间行</text |
||||
|
> |
||||
|
</view> |
||||
|
<view class=""> |
||||
|
<view class="time-reward-number" style="font-size: 34rpx"> |
||||
|
{{ |
||||
|
userInfo && userInfo.token ? userInfo.nickname : "用户" |
||||
|
}} |
||||
|
</view> |
||||
|
<view |
||||
|
class="time-reward-label" |
||||
|
@click="toPoint" |
||||
|
style=" |
||||
|
margin-top: 15rpx; |
||||
|
font-weight: bold; |
||||
|
font-size: 26rpx; |
||||
|
" |
||||
|
> |
||||
|
积分:{{ totalPoints || 0 |
||||
|
}}<text |
||||
|
@click.stop="pointDetail" |
||||
|
style=" |
||||
|
color: #999999; |
||||
|
font-size: 22rpx; |
||||
|
margin-left: 10rpx; |
||||
|
" |
||||
|
>积分获取规则</text |
||||
|
> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
<view |
||||
|
class="" |
||||
|
style=" |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
font-size: 26rpx; |
||||
|
font-weight: bold; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
margin-top: 20rpx; |
||||
|
" |
||||
|
> |
||||
|
<view class="" style="width: 200rpx"> |
||||
|
时长:{{ |
||||
|
userInfo && userInfo.token ? userInfo.hour || 0 : 0 |
||||
|
}}h |
||||
|
</view> |
||||
|
<view class=""> |
||||
|
<image |
||||
|
style="width: 22rpx; height: 22rpx; margin-right: 15rpx" |
||||
|
:src=" |
||||
|
showImg( |
||||
|
'/uploads/20250822/c8ee7615823a1ffaba400a4d5746de9a.png' |
||||
|
) |
||||
|
" |
||||
|
> |
||||
|
</image> |
||||
|
点赞:{{ (userInfo && userInfo.likeCount) || 0 }} |
||||
|
</view> |
||||
|
<view class=""> |
||||
|
<image |
||||
|
style="width: 22rpx; height: 22rpx; margin: 0 15rpx" |
||||
|
:src=" |
||||
|
showImg( |
||||
|
'/uploads/20250822/84c49f78f1c86b7340aaaa391bd4b7cf.png' |
||||
|
) |
||||
|
" |
||||
|
> |
||||
|
</image> |
||||
|
留言:0 |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
<image |
||||
|
class="avatar" |
||||
|
@click="toWebView" |
||||
|
src="https://epic.js-dyyj.com/uploads/20250826/92b0a21e9125fc21ca294a408bf3508f.png" |
||||
|
mode="aspectFill" |
||||
|
></image> |
||||
|
</view> |
||||
|
</template> |
||||
|
</template> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 紧凑状态 --> |
||||
|
<view v-else class="compact-content"> |
||||
|
<text class="compact-name">{{ getCompactName() }}</text> |
||||
|
<image |
||||
|
class="compact-avatar" |
||||
|
src="https://epic.js-dyyj.com/uploads/20250826/92b0a21e9125fc21ca294a408bf3508f.png" |
||||
|
mode="aspectFill" |
||||
|
></image> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: "DynamicIsland", |
||||
|
props: { |
||||
|
isCompact: { |
||||
|
type: Boolean, |
||||
|
default: false, |
||||
|
}, |
||||
|
styleType: { |
||||
|
type: String, |
||||
|
default: "", |
||||
|
}, |
||||
|
|
||||
|
title: { |
||||
|
type: String, |
||||
|
default: "用户", |
||||
|
}, |
||||
|
subtitle: { |
||||
|
type: String, |
||||
|
default: "周杰伦 - 青花瓷", |
||||
|
}, |
||||
|
avatarUrl: { |
||||
|
type: String, |
||||
|
default: "https://picsum.photos/80/80", |
||||
|
}, |
||||
|
actionText: { |
||||
|
type: String, |
||||
|
default: "暂停", |
||||
|
}, |
||||
|
// 新增页面标识符,用于区分不同页面的灵动岛实例 |
||||
|
pageId: { |
||||
|
type: String, |
||||
|
default: "default_page", |
||||
|
}, |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
isExpanded: false, |
||||
|
statusBarHeight: 0, |
||||
|
isScrolled: false, |
||||
|
scrollThreshold: 160, // 灵动岛大卡片高度(160rpx) |
||||
|
// 内部数据,减少对外部props的依赖 |
||||
|
currentTitle: "Hi!用户,欢迎回来~", |
||||
|
currentSubtitle: "2个权益 | 120时间银行", |
||||
|
currentAvatar: "https://picsum.photos/80/80", |
||||
|
currentAction: "激活你的Agent", |
||||
|
userInfo: {}, |
||||
|
totalPoints: 0, |
||||
|
}; |
||||
|
}, |
||||
|
computed: { |
||||
|
// 计算实际显示状态:只有在固定模式下才使用内部状态 |
||||
|
actualCompactState() { |
||||
|
if (this.isScrolled) { |
||||
|
return !this.isExpanded; |
||||
|
} |
||||
|
return false; // 非滚动状态下始终展开 |
||||
|
}, |
||||
|
// 计算固定定位的top值 |
||||
|
fixedTopPosition() { |
||||
|
// 状态栏高度 + 导航栏高度(40px) + 间距(20px) |
||||
|
return this.statusBarHeight + 40 + 20; |
||||
|
}, |
||||
|
// 计算占位区域高度 - 始终保持固定高度,等于灵动岛大卡片高度 |
||||
|
placeholderHeight() { |
||||
|
// 占位区域高度应该等于灵动岛展开状态的高度 |
||||
|
// 包括上下边距:32rpx(上) + 160rpx(灵动岛) + 24rpx(下) = 216rpx |
||||
|
const islandHeightRpx = 160; // 灵动岛展开高度 |
||||
|
const topMarginRpx = 32; // 上边距 |
||||
|
const bottomMarginRpx = 24; // 下边距 |
||||
|
return topMarginRpx + islandHeightRpx + bottomMarginRpx; |
||||
|
}, |
||||
|
// 使用内部数据或props数据 |
||||
|
title() { |
||||
|
return this.currentTitle; |
||||
|
}, |
||||
|
subtitle() { |
||||
|
return this.currentSubtitle; |
||||
|
}, |
||||
|
avatarUrl() { |
||||
|
return this.currentAvatar; |
||||
|
}, |
||||
|
actionText() { |
||||
|
return this.currentAction; |
||||
|
}, |
||||
|
// 计算是否固定 |
||||
|
isFixed() { |
||||
|
return this.isScrolled; |
||||
|
}, |
||||
|
}, |
||||
|
mounted() { |
||||
|
// uni-app中通过父组件传递点击事件 |
||||
|
this.setStatusBarHeight(); |
||||
|
// 监听页面滚动 |
||||
|
this.addScrollListener(); |
||||
|
// 获取用户信息 |
||||
|
this.getUserInfo(); |
||||
|
}, |
||||
|
beforeDestroy() { |
||||
|
// 清理滚动监听 |
||||
|
this.removeScrollListener(); |
||||
|
}, |
||||
|
methods: { |
||||
|
toLogin() { |
||||
|
uni.navigateTo({ |
||||
|
url: "/pages/login/login", |
||||
|
}); |
||||
|
}, |
||||
|
handleToggle() { |
||||
|
if (this.isScrolled) { |
||||
|
// 固定模式下切换内部状态 |
||||
|
this.isExpanded = !this.isExpanded; |
||||
|
this.$emit("toggle", this.isExpanded); |
||||
|
} else { |
||||
|
// 非固定模式下触发外部事件 |
||||
|
this.$emit("toggle"); |
||||
|
} |
||||
|
}, |
||||
|
handleAction() { |
||||
|
this.$emit("action"); |
||||
|
}, |
||||
|
// 外部调用收缩方法 |
||||
|
collapseIsland() { |
||||
|
if (this.isScrolled && this.isExpanded) { |
||||
|
this.isExpanded = false; |
||||
|
this.$emit("toggle", this.isExpanded); |
||||
|
} |
||||
|
}, |
||||
|
// 添加滚动监听 |
||||
|
addScrollListener() { |
||||
|
// 只监听带页面ID的滚动事件,避免不同页面间的状态冲突 |
||||
|
const eventName = `pageScroll_${this.pageId}`; |
||||
|
console.log("DynamicIsland 添加滚动监听:", eventName); |
||||
|
uni.$on(eventName, this.handlePageScroll); |
||||
|
}, |
||||
|
// 移除滚动监听 |
||||
|
removeScrollListener() { |
||||
|
// 移除带页面ID的滚动监听 |
||||
|
const eventName = `pageScroll_${this.pageId}`; |
||||
|
uni.$off(eventName, this.handlePageScroll); |
||||
|
}, |
||||
|
// 处理页面滚动 |
||||
|
handlePageScroll(e) { |
||||
|
const scrollTop = e.scrollTop || e; |
||||
|
const shouldScroll = scrollTop > this.scrollThreshold; |
||||
|
|
||||
|
if (this.isScrolled !== shouldScroll) { |
||||
|
this.isScrolled = shouldScroll; |
||||
|
// 添加触觉反馈 |
||||
|
} |
||||
|
|
||||
|
// 滚动时自动收缩展开的灵动岛 |
||||
|
if (this.isScrolled) { |
||||
|
this.collapseIsland(); |
||||
|
} |
||||
|
}, |
||||
|
toOrder() { |
||||
|
uni.switchTab({ |
||||
|
url: "/pages/index/iSoul", |
||||
|
}); |
||||
|
}, |
||||
|
toTime() { |
||||
|
uni.switchTab({ |
||||
|
url: "/pages/index/timeShopBank", |
||||
|
}); |
||||
|
}, |
||||
|
getCompactName() { |
||||
|
// 从用户信息中获取昵称 |
||||
|
if (this.userInfo && this.userInfo.nickname) { |
||||
|
return this.userInfo.nickname; |
||||
|
} |
||||
|
return "用户"; |
||||
|
}, |
||||
|
toPoint() { |
||||
|
uni.navigateTo({ |
||||
|
url: "/subPackages/points/index", |
||||
|
}); |
||||
|
}, |
||||
|
getStatNumber(type) { |
||||
|
// 从副标题中解析统计数据 |
||||
|
if (this.subtitle) { |
||||
|
if (type === "权益") { |
||||
|
const match = this.subtitle.match(/(\d+)个权益/); |
||||
|
return match ? match[1] : "0"; |
||||
|
} else if (type === "时间银行") { |
||||
|
const match = this.subtitle.match(/(\d+)时间银行/); |
||||
|
return match ? match[1] : "0"; |
||||
|
} |
||||
|
} |
||||
|
return "0"; |
||||
|
}, |
||||
|
// 设置状态栏高度 |
||||
|
setStatusBarHeight() { |
||||
|
try { |
||||
|
const systemInfo = uni.getSystemInfoSync(); |
||||
|
this.statusBarHeight = systemInfo.statusBarHeight || 0; |
||||
|
} catch (e) { |
||||
|
console.warn("获取系统信息失败:", e); |
||||
|
this.statusBarHeight = 0; |
||||
|
} |
||||
|
}, |
||||
|
// 获取用户信息 |
||||
|
getUserInfo() { |
||||
|
try { |
||||
|
this.userInfo = |
||||
|
(uni.getStorageSync("userInfo") && |
||||
|
JSON.parse(uni.getStorageSync("userInfo"))) || |
||||
|
this.$store.state.user.userInfo || |
||||
|
{}; |
||||
|
console.log(this.userInfo, "this.userInfo"); |
||||
|
// 更新标题显示用户昵称 |
||||
|
if (this.userInfo && this.userInfo.nickname) { |
||||
|
this.currentTitle = `Hi!${this.userInfo.nickname},欢迎回来~`; |
||||
|
this.Post({}, "/framework/points/getLastBalance", "DES").then( |
||||
|
(res) => { |
||||
|
if (res.code === 200) { |
||||
|
this.totalPoints = res.data.balance || 0; |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
console.warn("获取用户信息失败:", e); |
||||
|
this.userInfo = {}; |
||||
|
} |
||||
|
}, |
||||
|
toWebView() { |
||||
|
// uni.navigateTo({ |
||||
|
// url: "/subPackages/webPage/webPage?url=" + |
||||
|
// "https://des.dayunyuanjian.cn/dist/#/", |
||||
|
// }); |
||||
|
uni.navigateTo({ |
||||
|
url: "/subPackages/other/evita?id=0", |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
toDesInfo() { |
||||
|
uni.navigateTo({ |
||||
|
url: "/subPackages/other/introduction", |
||||
|
}); |
||||
|
}, |
||||
|
pointDetail() { |
||||
|
uni.navigateTo({ |
||||
|
url: "/subPackages/user/privacyInfo?id=10222", |
||||
|
}); |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style scoped lang="scss"> |
||||
|
/* 灵动岛占位区域样式 - 始终存在但控制可见性 */ |
||||
|
.dynamic-island-placeholder { |
||||
|
width: 100%; |
||||
|
background: transparent; |
||||
|
position: relative; |
||||
|
opacity: 1; |
||||
|
transition: opacity 0.3s ease; |
||||
|
// padding: 24rpx 0; |
||||
|
margin-top: 24rpx; |
||||
|
} |
||||
|
|
||||
|
.dynamic-island-placeholder.visible { |
||||
|
opacity: 1; |
||||
|
} |
||||
|
|
||||
|
/* 当灵动岛不是固定状态时,确保它在占位符内正常显示 */ |
||||
|
.dynamic-island-placeholder .dynamic-island:not(.fixed) { |
||||
|
position: relative; |
||||
|
z-index: 100; |
||||
|
} |
||||
|
|
||||
|
.dynamic-island { |
||||
|
// margin: 24rpx auto 24rpx; |
||||
|
margin: 0 auto; |
||||
|
z-index: 100; |
||||
|
|
||||
|
background: linear-gradient(180deg, #fffdb7 0%, #97fffa 100%); |
||||
|
backdrop-filter: blur(20rpx); |
||||
|
-webkit-backdrop-filter: blur(20rpx); |
||||
|
border-radius: 40rpx; |
||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1), |
||||
|
0 0 0 1rpx rgba(255, 255, 255, 0.2) inset; |
||||
|
|
||||
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); |
||||
|
overflow: hidden; |
||||
|
|
||||
|
// 展开状态 |
||||
|
width: 710rpx; |
||||
|
height: 216rpx; |
||||
|
|
||||
|
// 紧凑状态 |
||||
|
&.compact { |
||||
|
width: 300rpx; |
||||
|
height: 80rpx; |
||||
|
border-radius: 40rpx; |
||||
|
|
||||
|
.expanded-content { |
||||
|
opacity: 0; |
||||
|
visibility: hidden; |
||||
|
transition: opacity 0.15s ease-out, visibility 0s linear 0.15s; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&:not(.compact) { |
||||
|
.compact-content { |
||||
|
opacity: 0; |
||||
|
visibility: hidden; |
||||
|
transition: opacity 0.15s ease-out, visibility 0s linear 0.15s; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 固定定位状态 |
||||
|
&.fixed { |
||||
|
position: fixed; |
||||
|
left: 50%; |
||||
|
transform: translateX(-50%); |
||||
|
z-index: 998; |
||||
|
margin: 0; |
||||
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); |
||||
|
animation: slideInFromTop 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.expanded-content { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
height: 100%; |
||||
|
padding: 24rpx 25rpx; |
||||
|
opacity: 1; |
||||
|
visibility: visible; |
||||
|
transition: opacity 0.2s ease-in 0.1s, visibility 0s linear 0s; |
||||
|
} |
||||
|
|
||||
|
/* 三栏布局样式 */ |
||||
|
.three-column-layout { |
||||
|
display: flex; |
||||
|
align-items: flex-end; |
||||
|
height: 100%; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
/* 列分隔线 */ |
||||
|
.column-divider { |
||||
|
width: 2rpx; |
||||
|
height: 160rpx; |
||||
|
background: rgba(0, 0, 0, 0.1); |
||||
|
margin: 0rpx 25rpx; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
/* 左侧区域 */ |
||||
|
.left-section { |
||||
|
color: #333; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: space-between; |
||||
|
flex: 1; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.welcome-message { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
.welcome-text { |
||||
|
font-size: 22rpx; |
||||
|
color: #000000; |
||||
|
line-height: 1.2; |
||||
|
text-overflow: ellipsis; |
||||
|
overflow: hidden; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.welcome-subtitle { |
||||
|
font-size: 24rpx; |
||||
|
color: #000000; |
||||
|
margin-top: 4rpx; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.stats-info { |
||||
|
display: flex; |
||||
|
align-items: baseline; |
||||
|
margin-bottom: 8rpx; |
||||
|
} |
||||
|
|
||||
|
.stats-number { |
||||
|
font-size: 40rpx; |
||||
|
color: #333; |
||||
|
font-weight: bold; |
||||
|
line-height: 1; |
||||
|
} |
||||
|
|
||||
|
.stats-unit { |
||||
|
font-size: 24rpx; |
||||
|
color: #000000; |
||||
|
margin-left: 4rpx; |
||||
|
} |
||||
|
|
||||
|
.stats-label { |
||||
|
font-size: 22rpx; |
||||
|
color: #000000; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
/* 中间区域 */ |
||||
|
.middle-section { |
||||
|
display: flex; |
||||
|
justify-content: flex-start; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.time-reward-container { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: flex-end; |
||||
|
align-items: flex-start; |
||||
|
} |
||||
|
|
||||
|
.time-reward-title { |
||||
|
font-size: 20rpx; |
||||
|
color: #000000; |
||||
|
font-weight: 500; |
||||
|
margin-bottom: 12rpx; |
||||
|
} |
||||
|
|
||||
|
.time-reward-stats { |
||||
|
display: flex; |
||||
|
align-items: baseline; |
||||
|
margin-bottom: 8rpx; |
||||
|
} |
||||
|
|
||||
|
.time-reward-number { |
||||
|
font-size: 40rpx; |
||||
|
color: #000000; |
||||
|
font-weight: bold; |
||||
|
line-height: 1; |
||||
|
} |
||||
|
|
||||
|
.time-reward-unit { |
||||
|
font-size: 24rpx; |
||||
|
color: #000000; |
||||
|
margin-left: 4rpx; |
||||
|
} |
||||
|
|
||||
|
.time-reward-label { |
||||
|
font-size: 22rpx; |
||||
|
color: #000000; |
||||
|
} |
||||
|
|
||||
|
/* 右侧区域 */ |
||||
|
.right-section { |
||||
|
display: flex; |
||||
|
align-items: flex-end; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.avatar-container { |
||||
|
position: relative; |
||||
|
margin-right: 10rpx; |
||||
|
width: 140rpx; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.avatar { |
||||
|
width: 130rpx; |
||||
|
height: 130rpx; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
|
||||
|
.profile-info { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: flex-start; |
||||
|
justify-content: flex-start; |
||||
|
} |
||||
|
|
||||
|
.profile-title { |
||||
|
font-size: 26rpx; |
||||
|
color: #000000; |
||||
|
margin-bottom: 4rpx; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.profile-name { |
||||
|
font-size: 26rpx; |
||||
|
color: #000000; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.platform-link { |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.link-text { |
||||
|
font-size: 22rpx; |
||||
|
color: #000000; |
||||
|
text-decoration: underline; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
/* 保留原有的底部区域样式用于timeShop模式 */ |
||||
|
.bottom-section { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
flex: 1; |
||||
|
color: #000000; |
||||
|
} |
||||
|
|
||||
|
.stats-section { |
||||
|
display: flex; |
||||
|
gap: 32rpx; |
||||
|
} |
||||
|
|
||||
|
.stat-item { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
font-size: 28rpx; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.stat-number { |
||||
|
font-size: 32rpx; |
||||
|
color: #000000; |
||||
|
font-weight: bold; |
||||
|
line-height: 1; |
||||
|
margin-bottom: 4rpx; |
||||
|
} |
||||
|
|
||||
|
.stat-label { |
||||
|
font-size: 28rpx; |
||||
|
color: #000000; |
||||
|
line-height: 1; |
||||
|
font-weight: bold; |
||||
|
margin-top: 20rpx; |
||||
|
} |
||||
|
|
||||
|
.divider { |
||||
|
width: 2rpx; |
||||
|
height: 60rpx; |
||||
|
background: rgba(255, 255, 255, 0.3); |
||||
|
margin: 0 24rpx; |
||||
|
} |
||||
|
|
||||
|
.action-section { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
flex: 1; |
||||
|
justify-content: space-between; |
||||
|
} |
||||
|
|
||||
|
.action-text { |
||||
|
font-size: 26rpx; |
||||
|
color: #ffffff; |
||||
|
font-weight: bold; |
||||
|
white-space: nowrap; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
max-width: 200rpx; |
||||
|
} |
||||
|
|
||||
|
// 添加点击反馈动画 |
||||
|
.dynamic-island:active { |
||||
|
transform: scale(0.98); |
||||
|
|
||||
|
&.fixed { |
||||
|
transform: translateX(-50%) scale(0.98); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 从顶部滑入动画 |
||||
|
@keyframes slideInFromTop { |
||||
|
0% { |
||||
|
transform: translateX(-50%) translateY(-100%); |
||||
|
opacity: 0; |
||||
|
} |
||||
|
|
||||
|
100% { |
||||
|
transform: translateX(-50%) translateY(0); |
||||
|
opacity: 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.compact-content { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
height: 100%; |
||||
|
padding: 0 24rpx; |
||||
|
opacity: 1; |
||||
|
visibility: visible; |
||||
|
transition: opacity 0.2s ease-in 0.1s, visibility 0s linear 0s; |
||||
|
} |
||||
|
|
||||
|
.compact-name { |
||||
|
font-size: 27rpx; |
||||
|
color: #333; |
||||
|
font-weight: bold; |
||||
|
flex: 1; |
||||
|
text-align: left; |
||||
|
white-space: nowrap; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
max-width: 200rpx; |
||||
|
} |
||||
|
|
||||
|
.compact-avatar { |
||||
|
width: 48rpx; |
||||
|
height: 48rpx; |
||||
|
border-radius: 50%; |
||||
|
border: 2rpx solid rgba(0, 0, 0, 0.2); |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
// 动画定义 |
||||
|
@keyframes pulse { |
||||
|
0% { |
||||
|
opacity: 1; |
||||
|
transform: scale(1); |
||||
|
} |
||||
|
|
||||
|
50% { |
||||
|
opacity: 0.6; |
||||
|
transform: scale(1.1); |
||||
|
} |
||||
|
|
||||
|
100% { |
||||
|
opacity: 1; |
||||
|
transform: scale(1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 响应式适配 |
||||
|
// @media (max-width: 750rpx) { |
||||
|
// .dynamic-island { |
||||
|
// &.is-expanded { |
||||
|
// width: 100vw; |
||||
|
// max-width: 750rpx; |
||||
|
// } |
||||
|
|
||||
|
// &.is-compact { |
||||
|
// width: 280rpx; |
||||
|
// } |
||||
|
// } |
||||
|
// } |
||||
|
.action-text-box { |
||||
|
color: #000000; |
||||
|
|
||||
|
.action-text-box-des { |
||||
|
font-size: 20rpx; |
||||
|
} |
||||
|
|
||||
|
.action-text-box-msg { |
||||
|
font-size: 24rpx; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
margin-top: 20rpx; |
||||
|
} |
||||
|
|
||||
|
.action-text-box-img { |
||||
|
width: 57rpx; |
||||
|
height: 46rpx; |
||||
|
margin-right: 10rpx; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.ai-label { |
||||
|
border: 1rpx solid; |
||||
|
padding: 0rpx 15rpx; |
||||
|
height: 40rpx; |
||||
|
line-height: 38rpx; |
||||
|
font-weight: bold; |
||||
|
font-size: 20rpx; |
||||
|
border-radius: 4rpx; |
||||
|
border-color: #333333; |
||||
|
color: #333333; |
||||
|
display: inline; |
||||
|
margin-top: 10rpx; |
||||
|
} |
||||
|
|
||||
|
.ai-name { |
||||
|
font-size: 27rpx; |
||||
|
font-weight: bold; |
||||
|
color: #ffffff; |
||||
|
margin-left: 10rpx; |
||||
|
} |
||||
|
</style> |
||||
Binary file not shown.
@ -0,0 +1,414 @@ |
|||||
|
<template> |
||||
|
<view class="client-chat" style="height: 100%"> |
||||
|
<view ref="first" clazz="pop-demo" :use-arrow="true"> |
||||
|
<view class="pop-demo-list" v-for="pop in popperList" :key="pop.id"> |
||||
|
<v-button type="link" kind="primary" @click="openSearchUrl(pop, pop.id)"> |
||||
|
{{ pop.id }}.{{ pop.name }} |
||||
|
<v-icon remote name="arrow_right_line" size="12" valign="-1"></v-icon> |
||||
|
</v-button> |
||||
|
</view> |
||||
|
</view> |
||||
|
<view class="qa-item" v-for="(item, index) in msgList" :key="index"> |
||||
|
<!-- 时间戳 --> |
||||
|
<view class="timestamp" |
||||
|
v-if="index === 0 || (index !== 0 && item.timestamp && (Number(item.timestamp) - Number(msgList[index - 1].timestamp)) > timestampGap)"> |
||||
|
{{ moment(new Date(String(item.timestamp).length === 10 ? item.timestamp * 1000 : |
||||
|
Number(item.timestamp))).format('MM-DD HH:mm') }} |
||||
|
</view> |
||||
|
|
||||
|
<!-- 问题 --> |
||||
|
<view class="question-item" v-if="item.is_from_self"> |
||||
|
<v-spinner status="default" class="qs-loading" v-if="item.is_loading"></v-spinner> |
||||
|
<!-- <VueMarkdown class="question-text" style="max-width: 352px" :source="item.content" |
||||
|
:anchorAttributes="{ target: '_blank' }" :linkify="false" /> --> |
||||
|
</view> |
||||
|
<!-- 答案 --> |
||||
|
<view class="answer-item" v-if="!item.is_from_self"> |
||||
|
<!-- 头像 --> |
||||
|
<view class="answer-avatar"> |
||||
|
<img class="robot-avatar" :src="item.from_avatar" /> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 答案信息 --> |
||||
|
<view class="answer-info" :ref="item.record_id"> |
||||
|
<view v-if="item.agent_thought && item.agent_thought.procedures && item.agent_thought.procedures.length > 0"> |
||||
|
<!-- 思考部分 --> |
||||
|
<MsgThought |
||||
|
v-for="(thought, i) in item.agent_thought.procedures" |
||||
|
:key="i" |
||||
|
:content="thought.debugging.content" |
||||
|
:title="thought.title" |
||||
|
:titleIcon="thought.icon" |
||||
|
:nodeName="thought.name" |
||||
|
:status="thought.status" |
||||
|
:elapsed="thought.elapsed" |
||||
|
:detailVisible="thought.detailVisible" |
||||
|
/> |
||||
|
</view> |
||||
|
<view class="loading" v-if="item.loading_message">正在思考中</view> |
||||
|
<!-- 回复主体 --> |
||||
|
<MsgContent :showTags="true" |
||||
|
:recordId="item.record_id" |
||||
|
:isReplaceLinks="true" |
||||
|
:loadingMessage="item.loading_message" |
||||
|
:content="item.content" |
||||
|
:isFinal="item.is_final" |
||||
|
:isMdExpand="item.isMdExpand" |
||||
|
@littleTagClick="littleTagClick" |
||||
|
/> |
||||
|
<!-- 参考来源 --> |
||||
|
<Reference v-if="item.references && item.references.length>0" :references-list="item.references"/> |
||||
|
<!-- 运行状态 --> |
||||
|
<TokensBoardBfr class="tokens-board-class" :showDtl="true" :tokensData="item.tokens_msg"></TokensBoardBfr> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import clone from 'clone'; |
||||
|
// import VueMarkdown from 'vue-markdown'; |
||||
|
import elementResizeDetectorMaker from 'element-resize-detector'; |
||||
|
import { scrollToBottom } from './../utils/util'; |
||||
|
import { MESSAGE_TYPE, ACCESS_TYPE } from './../constants'; |
||||
|
import TokensBoardBfr from './tokens-board-brif.vue'; |
||||
|
import Reference from './reference-component.vue'; |
||||
|
|
||||
|
export default { |
||||
|
name: 'ClientChat', |
||||
|
components: { |
||||
|
// VueMarkdown, |
||||
|
Reference, |
||||
|
TokensBoardBfr |
||||
|
}, |
||||
|
data () { |
||||
|
return { |
||||
|
popperList: [], |
||||
|
oldPopDemo: null, |
||||
|
loading: false, |
||||
|
historyLoading: false, |
||||
|
timestampGap: 5 * 60, // 两次问题间隔大于5min,则展示时间戳(接口侧返回的时间戳是秒级) |
||||
|
msgList: [], // 对话消息列表 |
||||
|
robotName: '', // 机器人名称 |
||||
|
chatBoxHeight: document.body.clientHeight, |
||||
|
jsScrolling: false, |
||||
|
userScrolling: false |
||||
|
}; |
||||
|
}, |
||||
|
created () { |
||||
|
// 监听用户端/管理端体验侧的ws事件 |
||||
|
this.listenClientAndManageEvent(); |
||||
|
// 监听公共的ws事件 |
||||
|
this.listenCommonEvent(); |
||||
|
}, |
||||
|
mounted () { |
||||
|
const erd = elementResizeDetectorMaker(); |
||||
|
const bodyDom = document.body; |
||||
|
|
||||
|
erd.listenTo(bodyDom, (element) => { |
||||
|
this.chatBoxHeight = element.clientHeight - 113; // 57+56 头部的高度 |
||||
|
}); |
||||
|
|
||||
|
document.addEventListener('click', this.handleOutsideClick); |
||||
|
|
||||
|
const sDom = document.querySelector('.client-chat'); |
||||
|
sDom.addEventListener('scroll', () => { |
||||
|
if (this.msgList[this.msgList.length - 1].is_final === false && !this.jsScrolling) { |
||||
|
this.userScrolling = true; |
||||
|
} else { |
||||
|
this.jsScrolling = false; |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
beforeDestroy () { |
||||
|
// 移除全局事件监听器 |
||||
|
document.removeEventListener('click', this.handleOutsideClick); |
||||
|
}, |
||||
|
methods: { |
||||
|
openSearchUrl (refer, index) { |
||||
|
window.open(refer.url); |
||||
|
}, |
||||
|
// 监听用户端/管理端体验侧的ws事件 |
||||
|
listenClientAndManageEvent () { |
||||
|
// 从缓存获取机器人信息 |
||||
|
let cachedConfig = null; |
||||
|
if (ACCESS_TYPE === 'ws') { |
||||
|
cachedConfig = this.$clientData.getConfigInfo(); |
||||
|
} else { |
||||
|
cachedConfig = this.$SseCls.sseQueryConfigInfo(); |
||||
|
} |
||||
|
if (cachedConfig) { |
||||
|
this.robotName = cachedConfig.name; |
||||
|
} |
||||
|
|
||||
|
// 监听答案消息队列变更事件 |
||||
|
this.$eventHub.$on('client_msgContentChange', (res) => { |
||||
|
const { chatsContent, type } = res; |
||||
|
|
||||
|
// PS:若新消息不属于当前机器人,则在 $clientData 中监听到ws消息后判断并屏蔽。不在此处判断和屏蔽 |
||||
|
this.renderMsgList(chatsContent, type); |
||||
|
}); |
||||
|
}, |
||||
|
// 监听公共的ws事件 |
||||
|
listenCommonEvent () { |
||||
|
this.$eventHub.$on('data_history', () => { |
||||
|
this.historyLoading = false; |
||||
|
}); |
||||
|
|
||||
|
this.$eventHub.$on('data_historyError', () => { |
||||
|
this.historyLoading = false; |
||||
|
}); |
||||
|
}, |
||||
|
// 渲染消息会话页面 |
||||
|
renderMsgList (data, type) { |
||||
|
// 无需滚动至底部的ws事件:用户端拉取历史记录、用户端停止生成、坐席端取历史记录、点赞点踩 |
||||
|
const noScrollEvt = [MESSAGE_TYPE.HISTORY, MESSAGE_TYPE.STOP, MESSAGE_TYPE.WORKBENCH_HISTORY, MESSAGE_TYPE.FEEDBACK]; |
||||
|
const list = data.map(el => { |
||||
|
return { ...el, showPop: true }; |
||||
|
}); |
||||
|
this.msgList = clone(list); |
||||
|
// console.log('=======更新消list========', clone(list)); |
||||
|
|
||||
|
// 对话框滚动至底部(部分ws事件类型无需执行滚动) |
||||
|
this.$nextTick(() => { |
||||
|
const sDom = document.querySelector('.client-chat'); |
||||
|
|
||||
|
if (!sDom) return; |
||||
|
|
||||
|
if (!this.userScrolling && (!noScrollEvt.includes(type))) { |
||||
|
this.jsScrolling = true; |
||||
|
scrollToBottom(sDom, sDom.scrollHeight); |
||||
|
} |
||||
|
if (this.msgList.length > 0 && this.msgList[this.msgList.length - 1].is_final === true) { |
||||
|
this.userScrolling = false; |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
handleOutsideClick (event) { |
||||
|
if (!this.oldPopDemo) { return; }; |
||||
|
const firstElement = document.getElementsByClassName('pop-demo')[0]; |
||||
|
if (this.oldPopDemo.contains(event.target) || firstElement.contains(event.target)) { |
||||
|
} else { |
||||
|
if (this.oldPopDemo) { |
||||
|
this.$refs['first'] && this.$refs['first'].unbindTrigger(this.oldPopDemo); |
||||
|
} |
||||
|
// 调用你想要执行的方法 |
||||
|
this.$refs['first'] && this.$refs['first'].hide(); |
||||
|
this.oldPopDemo = null; |
||||
|
} |
||||
|
}, |
||||
|
littleTagClick (e, r) { |
||||
|
const findMsg = this.$clientData.getMsgById(r); |
||||
|
let innerDome = e.querySelectorAll('.little-tags'); |
||||
|
let outerTextArr = []; |
||||
|
if (innerDome && innerDome.length > 0) { |
||||
|
innerDome.forEach(dom => { |
||||
|
outerTextArr.push(dom.outerText); |
||||
|
}); |
||||
|
} |
||||
|
this.popperList = findMsg.references.filter(e => outerTextArr.includes(e.id)); |
||||
|
if (e) { |
||||
|
this.$refs['first'] && this.$refs['first'].bindTrigger(e, 'manual'); |
||||
|
this.$refs['first'] && this.$refs['first'].update(); |
||||
|
this.$refs['first'] && this.$refs['first'].show(); |
||||
|
this.oldPopDemo = e; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss"> |
||||
|
.client-chat::-webkit-scrollbar { |
||||
|
display: none; |
||||
|
} |
||||
|
.pop-demo{ |
||||
|
// background-color: pink; |
||||
|
padding: 10px; |
||||
|
display: flex; |
||||
|
min-width: var(--size-l); |
||||
|
padding: var(--spacing-base); |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
// align-items: center; |
||||
|
gap: var(--spacing-tight); |
||||
|
|
||||
|
border-radius: var(--radius-normal); |
||||
|
border: 0.5px solid var(--color-border-normal); |
||||
|
|
||||
|
background: var(--color-bg-2); |
||||
|
/* shadow/--shadow-medium */ |
||||
|
box-shadow: var(--shadow-medium-x-1) var(--shadow-medium-y-2) var(--shadow-medium-blur-1) var(--shadow-medium-spread-1) var(--shadow-medium-color-1), var(--shadow-medium-x-2) var(--shadow-medium-y-2) var(--shadow-medium-blur-2) var(--shadow-medium-spread-2) var(--shadow-medium-color-2), var(--shadow-medium-x-3) var(--shadow-medium-y-3) var(--shadow-medium-blur-3) var(--shadow-medium-spread-3) var(--shadow-medium-color-3); |
||||
|
|
||||
|
.v-popper__arrow{ |
||||
|
display: block; |
||||
|
} |
||||
|
.pop-demo-list{ |
||||
|
color: var(--color-link-normal); |
||||
|
/* caption/--caption-regular */ |
||||
|
font-family: var(--font-family-normal); |
||||
|
font-size: 12px; |
||||
|
font-style: normal; |
||||
|
font-weight: 400; |
||||
|
line-height: 16px; /* 133.333% */ |
||||
|
.v-button { |
||||
|
text-decoration: none; |
||||
|
text-align: left; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.client-chat { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
flex: 1; |
||||
|
overflow-y: overlay; |
||||
|
padding: 0 12px; |
||||
|
|
||||
|
.loading { |
||||
|
margin: 1em 0; |
||||
|
width: 150px; |
||||
|
|
||||
|
&:after { |
||||
|
content: "."; |
||||
|
animation: ellipsis 1.5s steps(1, end) infinite; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@keyframes ellipsis { |
||||
|
0% { |
||||
|
content: "."; |
||||
|
} |
||||
|
|
||||
|
33% { |
||||
|
content: ".."; |
||||
|
} |
||||
|
|
||||
|
66% { |
||||
|
content: "..."; |
||||
|
} |
||||
|
|
||||
|
100% { |
||||
|
content: "."; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.qa-item { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
margin-bottom: 16px; |
||||
|
font-weight: 400; |
||||
|
font-size: 14px; |
||||
|
color: var(--color-text-primary); |
||||
|
|
||||
|
.timestamp { |
||||
|
font-weight: 400; |
||||
|
font-size: 12px; |
||||
|
line-height: 16px; |
||||
|
text-align: center; |
||||
|
color: var(--color-text-caption); |
||||
|
margin: 16px 0; |
||||
|
} |
||||
|
|
||||
|
.question-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
width: fit-content; |
||||
|
text-align: center; |
||||
|
align-self: flex-end; |
||||
|
padding-left: 44px; |
||||
|
|
||||
|
.qs-error { |
||||
|
min-width: 16px; |
||||
|
margin-right: 10px; |
||||
|
color: var(--color-error-normal); |
||||
|
} |
||||
|
|
||||
|
.qs-loading { |
||||
|
margin-right: 10px; |
||||
|
} |
||||
|
|
||||
|
.question-text { |
||||
|
background: #DBE8FF; |
||||
|
border-radius: 6px; |
||||
|
padding: 0 12px; |
||||
|
text-align: left; |
||||
|
word-break: break-all; |
||||
|
word-wrap: break-word; |
||||
|
|
||||
|
code { |
||||
|
white-space: break-spaces; |
||||
|
} |
||||
|
|
||||
|
img { |
||||
|
max-width: 80%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.summary-item { |
||||
|
align-self: center; |
||||
|
margin: 12px 0; |
||||
|
} |
||||
|
|
||||
|
.answer-item { |
||||
|
display: flex; |
||||
|
|
||||
|
.contacter-avatar { |
||||
|
width: 32px; |
||||
|
height: 32px; |
||||
|
border-radius: 50%; |
||||
|
margin-right: 12px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.answer-info { |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
padding: 12px; |
||||
|
background: #F4F5F7; |
||||
|
border-radius: 6px; |
||||
|
width: calc(100% - 67px); |
||||
|
|
||||
|
.answer-expand { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
cursor: pointer; |
||||
|
width: 44px; |
||||
|
height: 24px; |
||||
|
margin-bottom: 12px; |
||||
|
background: var(--color-bg-2); |
||||
|
box-shadow: var(--shadow-small-light); |
||||
|
border-radius: 16px; |
||||
|
align-self: center; |
||||
|
} |
||||
|
|
||||
|
.stop-ws { |
||||
|
color: var(--color-text-caption); |
||||
|
margin-left: 5px; |
||||
|
} |
||||
|
|
||||
|
.answer-source { |
||||
|
margin: 12px 0; |
||||
|
font-size: 14px; |
||||
|
color: var(--color-text-caption); |
||||
|
text-align: left; |
||||
|
|
||||
|
.v-button { |
||||
|
text-decoration: none; |
||||
|
text-align: left; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.qa-item:last-child { |
||||
|
padding-bottom: 120px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,232 @@ |
|||||
|
<template> |
||||
|
<view class="pending"> |
||||
|
{{title}} |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: 'Pending', |
||||
|
data() { |
||||
|
return { |
||||
|
timeObj: null, |
||||
|
title: '正在思考中...' |
||||
|
}; |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.init() |
||||
|
}, |
||||
|
beforeDestroy() { |
||||
|
this.timeObj && clearInterval(this.timeObj); |
||||
|
}, |
||||
|
methods: { |
||||
|
init() { |
||||
|
this.timeObj = setInterval(() => { |
||||
|
this.title = this.title == '正在思考中...' ? this.title === '正在思考中..' ? '正在思考中.' : '正在思考中..' : '正在思考中...' |
||||
|
}, 500) |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss"> |
||||
|
.client-chat::-webkit-scrollbar { |
||||
|
display: none; |
||||
|
} |
||||
|
.pop-demo { |
||||
|
// background-color: pink; |
||||
|
padding: 10px; |
||||
|
display: flex; |
||||
|
min-width: var(--size-l); |
||||
|
padding: var(--spacing-base); |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
// align-items: center; |
||||
|
gap: var(--spacing-tight); |
||||
|
|
||||
|
border-radius: var(--radius-normal); |
||||
|
border: 0.5px solid var(--color-border-normal); |
||||
|
|
||||
|
background: var(--color-bg-2); |
||||
|
/* shadow/--shadow-medium */ |
||||
|
box-shadow: var(--shadow-medium-x-1) var(--shadow-medium-y-2) |
||||
|
var(--shadow-medium-blur-1) var(--shadow-medium-spread-1) |
||||
|
var(--shadow-medium-color-1), |
||||
|
var(--shadow-medium-x-2) var(--shadow-medium-y-2) |
||||
|
var(--shadow-medium-blur-2) var(--shadow-medium-spread-2) |
||||
|
var(--shadow-medium-color-2), |
||||
|
var(--shadow-medium-x-3) var(--shadow-medium-y-3) |
||||
|
var(--shadow-medium-blur-3) var(--shadow-medium-spread-3) |
||||
|
var(--shadow-medium-color-3); |
||||
|
|
||||
|
.v-popper__arrow { |
||||
|
display: block; |
||||
|
} |
||||
|
.pop-demo-list { |
||||
|
color: var(--color-link-normal); |
||||
|
/* caption/--caption-regular */ |
||||
|
font-family: var(--font-family-normal); |
||||
|
font-size: 12px; |
||||
|
font-style: normal; |
||||
|
font-weight: 400; |
||||
|
line-height: 16px; /* 133.333% */ |
||||
|
.v-button { |
||||
|
text-decoration: none; |
||||
|
text-align: left; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.client-chat { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
flex: 1; |
||||
|
overflow-y: overlay; |
||||
|
padding: 0 12px; |
||||
|
|
||||
|
.loading { |
||||
|
margin: 1em 0; |
||||
|
width: 150px; |
||||
|
|
||||
|
&:after { |
||||
|
content: "."; |
||||
|
animation: ellipsis 1.5s steps(1, end) infinite; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@keyframes ellipsis { |
||||
|
0% { |
||||
|
content: "."; |
||||
|
} |
||||
|
|
||||
|
33% { |
||||
|
content: ".."; |
||||
|
} |
||||
|
|
||||
|
66% { |
||||
|
content: "..."; |
||||
|
} |
||||
|
|
||||
|
100% { |
||||
|
content: "."; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.qa-item { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
margin-bottom: 16px; |
||||
|
font-weight: 400; |
||||
|
font-size: 14px; |
||||
|
color: var(--color-text-primary); |
||||
|
|
||||
|
.timestamp { |
||||
|
font-weight: 400; |
||||
|
font-size: 12px; |
||||
|
line-height: 16px; |
||||
|
text-align: center; |
||||
|
color: var(--color-text-caption); |
||||
|
margin: 16px 0; |
||||
|
} |
||||
|
|
||||
|
.question-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
width: fit-content; |
||||
|
text-align: center; |
||||
|
align-self: flex-end; |
||||
|
padding-left: 44px; |
||||
|
|
||||
|
.qs-error { |
||||
|
min-width: 16px; |
||||
|
margin-right: 10px; |
||||
|
color: var(--color-error-normal); |
||||
|
} |
||||
|
|
||||
|
.qs-loading { |
||||
|
margin-right: 10px; |
||||
|
} |
||||
|
|
||||
|
.question-text { |
||||
|
background: #dbe8ff; |
||||
|
border-radius: 6px; |
||||
|
padding: 0 12px; |
||||
|
text-align: left; |
||||
|
word-break: break-all; |
||||
|
word-wrap: break-word; |
||||
|
|
||||
|
code { |
||||
|
white-space: break-spaces; |
||||
|
} |
||||
|
|
||||
|
img { |
||||
|
max-width: 80%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.summary-item { |
||||
|
align-self: center; |
||||
|
margin: 12px 0; |
||||
|
} |
||||
|
|
||||
|
.answer-item { |
||||
|
display: flex; |
||||
|
|
||||
|
.contacter-avatar { |
||||
|
width: 32px; |
||||
|
height: 32px; |
||||
|
border-radius: 50%; |
||||
|
margin-right: 12px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.answer-info { |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
padding: 12px; |
||||
|
background: #f4f5f7; |
||||
|
border-radius: 6px; |
||||
|
width: calc(100% - 67px); |
||||
|
|
||||
|
.answer-expand { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
cursor: pointer; |
||||
|
width: 44px; |
||||
|
height: 24px; |
||||
|
margin-bottom: 12px; |
||||
|
background: var(--color-bg-2); |
||||
|
box-shadow: var(--shadow-small-light); |
||||
|
border-radius: 16px; |
||||
|
align-self: center; |
||||
|
} |
||||
|
|
||||
|
.stop-ws { |
||||
|
color: var(--color-text-caption); |
||||
|
margin-left: 5px; |
||||
|
} |
||||
|
|
||||
|
.answer-source { |
||||
|
margin: 12px 0; |
||||
|
font-size: 14px; |
||||
|
color: var(--color-text-caption); |
||||
|
text-align: left; |
||||
|
|
||||
|
.v-button { |
||||
|
text-decoration: none; |
||||
|
text-align: left; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.qa-item:last-child { |
||||
|
padding-bottom: 120px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
File diff suppressed because it is too large
Binary file not shown.
@ -0,0 +1,347 @@ |
|||||
|
import Vue from "vue"; |
||||
|
import { MESSAGE_TYPE } from "./chat_constant"; |
||||
|
import { getQueryVariable, generateRequestId, arrayUnique } from "./util"; |
||||
|
import { v4 as uuidv4 } from "uuid"; |
||||
|
import GLOBAL from "./global"; |
||||
|
import Observer from "./observer"; |
||||
|
const $e = Observer; |
||||
|
// const $s = this.$s;
|
||||
|
|
||||
|
let cache = null; // 缓存
|
||||
|
let timeoutTasks = {}; // 超时任务管理
|
||||
|
const msgSendTimeout = 2 * 60 * 1000; // 发送消息超时ms,此处超时默认为2min
|
||||
|
|
||||
|
class ClientData { |
||||
|
constructor(option) { |
||||
|
cache = { |
||||
|
session_id: "", // 会话ID
|
||||
|
configInfo: null, // 配置信息
|
||||
|
chatsContent: [], // 会话聊天内容
|
||||
|
systemEvents: [], // 系统事件栈
|
||||
|
transferInfo: { |
||||
|
transferStatus: false, |
||||
|
transferAvatar: "", |
||||
|
}, // 当前转人工状态
|
||||
|
}; |
||||
|
|
||||
|
this.$s = null |
||||
|
|
||||
|
} |
||||
|
init() { |
||||
|
// 获取基础配置
|
||||
|
this.queryConfigInfo(); |
||||
|
} |
||||
|
setAttr(options) { |
||||
|
this.$s =options.socketObj |
||||
|
} |
||||
|
// 获取基础配置
|
||||
|
async queryConfigInfo() { |
||||
|
try { |
||||
|
const seatBizId = ''// getQueryVariable("seat_biz_id");
|
||||
|
console.log("seatBizId", seatBizId); |
||||
|
const sessionInfo = await this.createSession(); |
||||
|
console.log("createsession, res", sessionInfo); |
||||
|
if (sessionInfo.code === 0) { |
||||
|
cache.seat_biz_id = seatBizId; |
||||
|
cache.session_id = sessionInfo.data.session_id; |
||||
|
} else { |
||||
|
uni.showModal({ |
||||
|
title: "获取会话ID失败,请重试", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
} |
||||
|
// 接着获取机器人基础信息
|
||||
|
const botInfo = { |
||||
|
code: 0, |
||||
|
data: { |
||||
|
name: "测试机器人", |
||||
|
avatar: |
||||
|
"https://qbot-1251316161.cos.ap-nanjing.myqcloud.com/avatar.png", |
||||
|
is_available: true, |
||||
|
bot_biz_id: "1664519736704069632", |
||||
|
}, |
||||
|
}; |
||||
|
if (botInfo.data) { |
||||
|
cache.configInfo = botInfo.data; |
||||
|
cache.configInfo.session_id = sessionInfo.data.session_id; |
||||
|
$e.$emit("client_configChange", cache.configInfo); |
||||
|
} else { |
||||
|
uni.showModal({ |
||||
|
title: "获取机器人信息失败", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
console.log("获取机器人信息失败", e); |
||||
|
uni.showModal({ |
||||
|
title: "获取会话信息失败,请刷新页面重试", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
async createSession() { |
||||
|
const session_id = uuidv4(); |
||||
|
return { code: 0, data: { session_id: session_id } }; |
||||
|
} |
||||
|
// 消息上行事件(用户端)
|
||||
|
triggerSendMsg =async (msg, type='text', status= true) => { |
||||
|
console.log("[triggerSendMsg]", msg, wx); |
||||
|
if (!cache.configInfo || !cache.configInfo.session_id) { |
||||
|
await this.queryConfigInfo(); |
||||
|
} |
||||
|
const requestId = generateRequestId(); |
||||
|
const params = { |
||||
|
request_id: requestId, |
||||
|
session_id: cache.configInfo ? cache.configInfo.session_id : 0, |
||||
|
is_msg_status: status |
||||
|
}; |
||||
|
if (type == "text") { |
||||
|
params.content = msg; |
||||
|
} else if (type == "img") { |
||||
|
params.content = ``; |
||||
|
params.realContent = msg; |
||||
|
} |
||||
|
|
||||
|
this.$s.emit("send", params, type); |
||||
|
} |
||||
|
|
||||
|
// 监听token用量和详情事件
|
||||
|
listenTokenStat() { |
||||
|
this.$s.on("token_stat", (data) => { |
||||
|
$e.$emit("token_state_change", data); |
||||
|
if (data.session_id !== cache.session_id) return; // 若新消息不属于当前机器人时,则不做处理
|
||||
|
let loadingMsg = cache.chatsContent.find((el) => el.loading_message); |
||||
|
let loadingText = "思考中"; |
||||
|
if (loadingMsg) { |
||||
|
if (data.procedures && data.procedures.length > 0) { |
||||
|
loadingText = |
||||
|
data.procedures[data.procedures.length - 1].title || "思考中"; |
||||
|
} |
||||
|
let currentList = cache.chatsContent; |
||||
|
currentList.forEach((el) => { |
||||
|
if (el.loading_message) { |
||||
|
el.text = loadingText; |
||||
|
el.record_id = data.record_id; |
||||
|
el.tokens_msg = data; |
||||
|
// 只有标准模式加这个
|
||||
|
if (GLOBAL.webimToken[0].pattern === "standard") { |
||||
|
el.is_final = false; |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
$e.$emit("client_msgContentChange", { |
||||
|
chatsContent: cache.chatsContent, |
||||
|
type: MESSAGE_TYPE.ANSWER, |
||||
|
}); |
||||
|
} else { |
||||
|
let findedMsg = cache.chatsContent.find( |
||||
|
(el) => el.record_id === data.record_id |
||||
|
); |
||||
|
if (!findedMsg) return; |
||||
|
findedMsg.tokens_msg = data; |
||||
|
|
||||
|
$e.$emit("client_msgContentChange", { |
||||
|
chatsContent: cache.chatsContent, |
||||
|
type: MESSAGE_TYPE.ANSWER, |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 组装消息队列数据
|
||||
|
// 问题确认消息:根据request_id关联覆盖(服务端收到问题后的确认消息)
|
||||
|
// 答案消息:倒序遍历插入(服务端答案消息)
|
||||
|
assembleMsgContent(msgList, type) { |
||||
|
console.log("assembleMsgContent", msgList, type); |
||||
|
let newMsg = msgList; |
||||
|
|
||||
|
if (type === MESSAGE_TYPE.QUESTION) { |
||||
|
// 发送的问题消息由前端临时插入消息队列
|
||||
|
cache.chatsContent.push(newMsg); |
||||
|
} else if (type === MESSAGE_TYPE.ANSWER) { |
||||
|
if (cache.chatsContent.length < 1) { |
||||
|
cache.chatsContent.push(newMsg); |
||||
|
} else { |
||||
|
let currentList = cache.chatsContent; |
||||
|
|
||||
|
timeoutTasks[newMsg.request_id] && |
||||
|
clearTimeout(timeoutTasks[newMsg.request_id]); |
||||
|
|
||||
|
if (currentList.length === 2 && newMsg.can_rating) { |
||||
|
currentList[0].transferRobot = true; |
||||
|
} |
||||
|
if (newMsg.transfer && newMsg.loading_message) { |
||||
|
currentList.pop(); |
||||
|
currentList[currentList.length - 1].loading_message = false; |
||||
|
currentList[currentList.length - 1] = { |
||||
|
...newMsg, |
||||
|
...currentList[currentList.length - 1], |
||||
|
transfer: true, |
||||
|
transferRobot: false, |
||||
|
}; |
||||
|
} else { |
||||
|
for (let i = currentList.length - 1; i >= 0; i--) { |
||||
|
const { transfer, quit, transferRobot } = currentList[i]; |
||||
|
let tmp = { |
||||
|
...newMsg, |
||||
|
transfer, |
||||
|
quit, |
||||
|
transferRobot, |
||||
|
}; |
||||
|
// 保留tokens_msg,防止覆盖
|
||||
|
if (currentList[i].tokens_msg) { |
||||
|
tmp = { ...tmp, tokens_msg: currentList[i].tokens_msg }; |
||||
|
} |
||||
|
// 保留thought 放置被覆盖
|
||||
|
if (currentList[i].agent_thought) { |
||||
|
tmp = { ...tmp, agent_thought: currentList[i].agent_thought }; |
||||
|
} |
||||
|
// 保留reference
|
||||
|
if (currentList[i].references) { |
||||
|
tmp = { ...tmp, references: currentList[i].references }; |
||||
|
} |
||||
|
// 答案消息流式输出覆盖(record_id)
|
||||
|
if (newMsg.record_id === currentList[i].record_id) { |
||||
|
currentList[i] = tmp; |
||||
|
break; |
||||
|
} |
||||
|
// 服务端问题消息确认数据,覆盖前端插入的临时问题消息数据(request_id匹配 & 自己发出的问题消息)
|
||||
|
if ( |
||||
|
newMsg.request_id && |
||||
|
newMsg.request_id === currentList[i].request_id && |
||||
|
newMsg.is_from_self |
||||
|
) { |
||||
|
newMsg.is_loading = false; // 服务端确认收到问题消息,则去除”发送中“状态
|
||||
|
currentList[i] = tmp; |
||||
|
// 非人工状态时, 并且用户发送的不是敏感消息。插入临时[正在思考中...]消息
|
||||
|
if (!newMsg.is_evil && !cache.transferInfo.transferStatus) { |
||||
|
currentList.push({ |
||||
|
loading_message: true, |
||||
|
is_from_self: false, |
||||
|
content: "", |
||||
|
from_avatar: cache.configInfo.avatar, |
||||
|
timestamp: Number(currentList[i].timestamp), // 精确到秒
|
||||
|
}); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
// 插入最新答案消息
|
||||
|
if (Number(newMsg.timestamp) >= Number(currentList[i].timestamp)) { |
||||
|
if (currentList[i].loading_message) { |
||||
|
// 删除原来的[正在思考中...]消息
|
||||
|
currentList[currentList.length - 1] = newMsg; |
||||
|
} else { |
||||
|
currentList.splice(i + 1, 0, newMsg); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
if ( |
||||
|
i === 0 && |
||||
|
Number(newMsg.timestamp) < Number(currentList[i].timestamp) |
||||
|
) { |
||||
|
currentList.splice(0, 0, newMsg); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} else if (type === MESSAGE_TYPE.HISTORY) { |
||||
|
let currentList = cache.chatsContent; |
||||
|
// 历史数据打上标签,无需展示”重新生成“和”停止生成“操作
|
||||
|
msgList = msgList.map((r) => { |
||||
|
return { |
||||
|
...r, |
||||
|
is_history: true, |
||||
|
is_final: true, |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
if (currentList.length === 0) { |
||||
|
// 若消息队列为空(用户端,初始拉取历史记录,用做判断欢迎页展示场景)
|
||||
|
cache.chatsContent = [].concat(msgList); |
||||
|
} else { |
||||
|
// 若消息队列不为空
|
||||
|
let oldMsgCurrent = currentList[0]; |
||||
|
let newMsgHistory = msgList[msgList.length - 1]; |
||||
|
|
||||
|
// 将历史数据拼装到消息队列中(按照时间戳重排数据)
|
||||
|
if (Number(newMsgHistory.timestamp) < Number(oldMsgCurrent.timestamp)) { |
||||
|
cache.chatsContent = [].concat(msgList).concat(cache.chatsContent); |
||||
|
} else { |
||||
|
msgList.reverse().forEach((msg) => { |
||||
|
for (let i = 0; i < cache.chatsContent.length; i++) { |
||||
|
if (msg.record_id === cache.chatsContent[i].record_id) { |
||||
|
// 重复覆盖
|
||||
|
cache.chatsContent[i] = msg; |
||||
|
break; |
||||
|
} else if ( |
||||
|
Number(msg.timestamp) <= Number(cache.chatsContent[i].timestamp) |
||||
|
) { |
||||
|
cache.chatsContent.splice(i, 0, msg); |
||||
|
break; |
||||
|
} else if ( |
||||
|
i === cache.chatsContent.length - 1 && |
||||
|
Number(msg.timestamp) > Number(cache.chatsContent[i].timestamp) |
||||
|
) { |
||||
|
cache.chatsContent.splice(i + 1, 0, msg); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 消息去重。同一record_id取最新,同时保留消息最早的时间戳
|
||||
|
cache.chatsContent = arrayUnique( |
||||
|
cache.chatsContent, |
||||
|
"record_id", |
||||
|
"timestamp" |
||||
|
); |
||||
|
|
||||
|
// 消息队列变更通知事件
|
||||
|
$e.$emit("client_msgContentChange", { |
||||
|
chatsContent: cache.chatsContent, |
||||
|
type, |
||||
|
}); |
||||
|
} |
||||
|
// 修改指定msgId的消息内容
|
||||
|
modifyMsgContent(msgId) { |
||||
|
const findedMsg = this.getMsgById(msgId); |
||||
|
|
||||
|
if (findedMsg) { |
||||
|
findedMsg.is_final = true; |
||||
|
findedMsg.content = findedMsg.content.concat( |
||||
|
`<span class="stop-ws">| 已停止生成</span>` |
||||
|
); |
||||
|
|
||||
|
$e.$emit("client_msgContentChange", { |
||||
|
chatsContent: cache.chatsContent, |
||||
|
type: MESSAGE_TYPE.STOP, // ”停止生成“事件
|
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
// 根据msgId获取消息
|
||||
|
getMsgById(msgId) { |
||||
|
const findedMsg = cache.chatsContent.find((r) => r.record_id === msgId); |
||||
|
return findedMsg; |
||||
|
} |
||||
|
// 根据msgId获取其关联问题消息
|
||||
|
getQmsgById(msgId) { |
||||
|
let findedQmsg = null; |
||||
|
const findedMsg = this.getMsgById(msgId); |
||||
|
|
||||
|
if (findedMsg) { |
||||
|
findedQmsg = cache.chatsContent.find( |
||||
|
(r) => r.record_id === findedMsg.related_record_id |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return findedQmsg; |
||||
|
} |
||||
|
releaseCache() {} |
||||
|
destroy() { |
||||
|
// be careful to clear the cache to avoid errors
|
||||
|
this.releaseCache(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default ClientData; |
||||
@ -0,0 +1,50 @@ |
|||||
|
import Vue from 'vue'; |
||||
|
|
||||
|
class EventHub { |
||||
|
constructor (vm) { |
||||
|
this.vm = vm; |
||||
|
this.curVm = null; |
||||
|
this.events = {}; |
||||
|
this.eventMapUid = {}; |
||||
|
} |
||||
|
$register (vm) { |
||||
|
this.curVm = vm; |
||||
|
} |
||||
|
setEventMapUid (uid, type) { |
||||
|
if (!this.eventMapUid[uid]) { |
||||
|
this.eventMapUid[uid] = []; |
||||
|
} |
||||
|
this.eventMapUid[uid].push(type); |
||||
|
} |
||||
|
$on (type, fn) { |
||||
|
if (!this.events[type]) { |
||||
|
this.events[type] = []; |
||||
|
} |
||||
|
this.events[type].push(fn); |
||||
|
if (this.curVm instanceof this.vm) { |
||||
|
this.setEventMapUid(this.curVm._uid, type); |
||||
|
} |
||||
|
} |
||||
|
$emit (type, ...args) { |
||||
|
if (this.events[type]) { |
||||
|
this.events[type].forEach(fn => fn(...args)); |
||||
|
} |
||||
|
} |
||||
|
$off (type, fn) { |
||||
|
if (fn && this.events[type]) { |
||||
|
const index = this.events[type].findIndex(f => f === fn); |
||||
|
if (index !== -1) { |
||||
|
this.events[type].splice(index, 1); |
||||
|
} |
||||
|
return; |
||||
|
} |
||||
|
delete this.events[type]; |
||||
|
} |
||||
|
$offAll (uid) { |
||||
|
const curAllTypes = this.eventMapUid[uid] || []; |
||||
|
curAllTypes.forEach(type => this.$off(type)); |
||||
|
delete this.eventMapUid[uid]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default new EventHub(Vue);; |
||||
@ -0,0 +1,225 @@ |
|||||
|
// 心跳间隔
|
||||
|
const HEART_BEAT_TIME = 15000; |
||||
|
// 心跳最大失败次数(超过此次数重连)
|
||||
|
const HEART_BEAT_FAIL_NUM = 1; |
||||
|
// 重连间隔
|
||||
|
const RECONNECT_TIME = 3000; |
||||
|
|
||||
|
import { generateRequestId } from "./util"; |
||||
|
|
||||
|
export default class Audio { |
||||
|
constructor(option) { |
||||
|
this._options = option; |
||||
|
this.socket = null; |
||||
|
this.session_id = null; |
||||
|
this.selfCloseStatus = false |
||||
|
this.connectSocketTimeOut = null |
||||
|
} |
||||
|
getToken() { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
console.log("-----开始请求token-----"); |
||||
|
uni.request({ |
||||
|
method: "GET", |
||||
|
dataType: "json", |
||||
|
// url: `http://192.168.124.118:8083/xcx/framework/agent/${this._options.agentId}`,
|
||||
|
// url: `https://des.js-dyyj.com/getDemoToken?id=${this._options.agentId}`,
|
||||
|
url: `https://des.dayunyuanjian.cn/getDemoToken?id=${this._options.agentId}`, |
||||
|
success: (res) => { |
||||
|
console.log("请求token成功", res); |
||||
|
resolve(res); |
||||
|
}, |
||||
|
fail: (err) => { |
||||
|
wx.showToast({ |
||||
|
title: "创建失败", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
console.log("请求token失败", err); |
||||
|
reject(); |
||||
|
}, |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
async init() { |
||||
|
return this.createSocket(); |
||||
|
} |
||||
|
async createSocket(options) { |
||||
|
console.log("开始创建socket", options); |
||||
|
return new Promise(async (resolve, reject) => { |
||||
|
if (this.selfCloseStatus) {return} |
||||
|
// const origin = "wss://des.js-dyyj.com/xcx/tts-websocket";
|
||||
|
const origin = "wss://des.dayunyuanjian.cn/xcx/tts-websocket"; |
||||
|
// 建立连接
|
||||
|
const socket = wx.connectSocket({ |
||||
|
url: `${origin}`, |
||||
|
success: (e) => { |
||||
|
console.log("创建语音长链接成功", e); |
||||
|
}, |
||||
|
complete: (e) => { |
||||
|
console.log("socket - complete", e); |
||||
|
}, |
||||
|
}); |
||||
|
this.socket = socket; |
||||
|
|
||||
|
socket.onOpen((e) => { |
||||
|
console.log("socket.onOpen", e); |
||||
|
// 监听发送
|
||||
|
options && options.complete && options.complete(); |
||||
|
if (this.selfCloseStatus) { |
||||
|
this.destroy() |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
socket.onMessage((e) => { |
||||
|
const { data } = e; |
||||
|
|
||||
|
if (data instanceof ArrayBuffer) { |
||||
|
this._options.onMessage && this._options.onMessage(data); |
||||
|
} else { |
||||
|
if ( Object.prototype.toString.call(data) === "[object String]" ) { |
||||
|
const tmp = JSON.parse(data); |
||||
|
if (Object.prototype.toString.call(tmp.type) === "[object Number]") { |
||||
|
this._options.onMessage && this._options.onMessage(tmp); |
||||
|
} else { |
||||
|
if (!this.session_id) { |
||||
|
this.session_id = tmp.data |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
// this.on(type, e);
|
||||
|
this.createInter(); |
||||
|
resolve(); |
||||
|
}); |
||||
|
// 失败
|
||||
|
socket.onError((e) => { |
||||
|
console.log("websocket error 创建语音长链接报错", e); |
||||
|
//
|
||||
|
}); |
||||
|
// 关闭
|
||||
|
socket.onClose((e) => { |
||||
|
console.log("websocket close 语音长链接关闭", e); |
||||
|
this.connectSocketTimeOut && clearTimeout(this.connectSocketTimeOut); |
||||
|
//非自动关闭重连
|
||||
|
if (e.code != 1000) { |
||||
|
this.doConnectTimeout(); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
doConnectTimeout() { |
||||
|
if (this.selfCloseStatus){ |
||||
|
// 主动退出 不在重连
|
||||
|
return |
||||
|
} |
||||
|
// 重连一次
|
||||
|
console.log("websocket 异常关闭 开始重连"); |
||||
|
this.connectSocketTimeOut = setTimeout(() => { |
||||
|
this.createSocket({ |
||||
|
complete: function (res) { |
||||
|
// this.reconnectLock = false;
|
||||
|
}, |
||||
|
}); |
||||
|
}, RECONNECT_TIME); |
||||
|
} |
||||
|
|
||||
|
send(e, t) { |
||||
|
console.log("开始请求 websocket send", e); |
||||
|
this.socket && this.socket.send(e); |
||||
|
} |
||||
|
|
||||
|
getMsgData(e) { |
||||
|
if (e && typeof e == "string") { |
||||
|
const status = e.indexOf("42"); |
||||
|
const index = e.indexOf("["); |
||||
|
if (status > -1 && index > -1) { |
||||
|
const txt = e.substring(index); |
||||
|
const item = JSON.parse(txt); |
||||
|
const [type, obj] = item; |
||||
|
const payload = obj.payload ? obj.payload : {}; |
||||
|
const params = { |
||||
|
chatId: this._options.agentId, |
||||
|
type, |
||||
|
contentType: "text", //默认文字
|
||||
|
timestamp: new Date().getTime(), |
||||
|
...payload, |
||||
|
}; |
||||
|
// 缓存聊天记录
|
||||
|
return params; |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
emit(type, text, contentType) { |
||||
|
console.log("emit", type, text); |
||||
|
switch (type) { |
||||
|
case "send": |
||||
|
// 发送消息
|
||||
|
// 发送消息
|
||||
|
// const socketParams = {
|
||||
|
// request_id: generateRequestId()
|
||||
|
// };
|
||||
|
const socketParams = { |
||||
|
session_id: this.session_id, |
||||
|
message_id: generateRequestId(), |
||||
|
action: "ACTION_SYNTHESIS", |
||||
|
data: text, |
||||
|
}; |
||||
|
this.send({ data: JSON.stringify(socketParams) }, contentType); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
// 监听
|
||||
|
on(type, params) { |
||||
|
switch (type) { |
||||
|
case "reply": |
||||
|
// 回复结束
|
||||
|
const tmpParams = { |
||||
|
...params, |
||||
|
name: this.robotObj.name, |
||||
|
headImage: this.robotObj.headImage, |
||||
|
}; |
||||
|
if ( |
||||
|
params && |
||||
|
params.type === "reply" && |
||||
|
!params.is_from_self && |
||||
|
params.can_rating |
||||
|
) { |
||||
|
// 回复消息
|
||||
|
console.log("reply回复内容", tmpParams.content, tmpParams); |
||||
|
this._options.onMessage && this._options.onMessage(tmpParams); |
||||
|
} |
||||
|
|
||||
|
if (!params.is_from_self && params.is_final) { |
||||
|
if (this.failMsg[tmpParams.request_id]) { |
||||
|
this.failMsg[tmpParams.request_id].status = true; |
||||
|
// 已回复 状态为true
|
||||
|
} |
||||
|
|
||||
|
// 回复结束 写入缓存
|
||||
|
// setMsgData(tmpParams);
|
||||
|
} |
||||
|
// 回复
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
createInter() { |
||||
|
if (this.timeoutObj) { |
||||
|
clearTimeout(this.timeoutObj); |
||||
|
} |
||||
|
this.timeoutObj = setTimeout(() => { |
||||
|
console.log(111111, this.socket) |
||||
|
this.socket && this.socket.send && this.socket.send({ data: 3 }); |
||||
|
}, HEART_BEAT_TIME); |
||||
|
} |
||||
|
|
||||
|
// 关闭socket
|
||||
|
destroy() { |
||||
|
if (this.socket && this.socket.readyState == 1) { |
||||
|
this.socket && this.socket.close(); |
||||
|
this.socket = null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,247 @@ |
|||||
|
// 心跳间隔
|
||||
|
const HEART_BEAT_TIME = 15000; |
||||
|
// 心跳最大失败次数(超过此次数重连)
|
||||
|
const HEART_BEAT_FAIL_NUM = 1; |
||||
|
// 重连间隔
|
||||
|
const RECONNECT_TIME = 3000; |
||||
|
|
||||
|
import { generateRequestId1 } from "./util"; |
||||
|
|
||||
|
export default class Audio { |
||||
|
constructor(option) { |
||||
|
this._options = option; |
||||
|
this.socket = null; |
||||
|
this.session_id = null; |
||||
|
this.selfCloseStatus = false |
||||
|
this.connectSocketTimeOut = null |
||||
|
|
||||
|
this.appkey = 'hi93syrOZPxi7AZr'; |
||||
|
this.token = '66df6ddcb4b643f08450cdf77d3de768'; |
||||
|
} |
||||
|
getToken() { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
console.log("-----开始请求token-----"); |
||||
|
uni.request({ |
||||
|
method: "GET", |
||||
|
dataType: "json", |
||||
|
// url: `http://192.168.124.118:8083/xcx/framework/agent/${this._options.agentId}`,
|
||||
|
url: `https://des.js-dyyj.com/getDemoToken?id=${this._options.agentId}`, |
||||
|
success: (res) => { |
||||
|
console.log("请求token成功", res); |
||||
|
resolve(res); |
||||
|
}, |
||||
|
fail: (err) => { |
||||
|
wx.showToast({ |
||||
|
title: "创建失败", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
console.log("请求token失败", err); |
||||
|
reject(); |
||||
|
}, |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
async init() { |
||||
|
return this.createSocket(); |
||||
|
} |
||||
|
async createSocket(options) { |
||||
|
console.log("开始创建socket", options); |
||||
|
return new Promise(async (resolve, reject) => { |
||||
|
const origin = `wss://nls-gateway-cn-beijing.aliyuncs.com/ws/v1?token=${this.token}`; |
||||
|
// 建立连接
|
||||
|
const socket = wx.connectSocket({ |
||||
|
url: `${origin}`, |
||||
|
binaryType: 'arraybuffer', |
||||
|
success: (e) => { |
||||
|
console.log("创建语音长链接成功", e); |
||||
|
}, |
||||
|
complete: (e) => { |
||||
|
console.log("socket - complete", e); |
||||
|
}, |
||||
|
}); |
||||
|
this.socket = socket; |
||||
|
|
||||
|
socket.onOpen((e) => { |
||||
|
console.log("socket.onOpen", e); |
||||
|
// 监听发送
|
||||
|
options && options.complete && options.complete(); |
||||
|
|
||||
|
const socketParams = { |
||||
|
header: { |
||||
|
name: 'StartSynthesis', |
||||
|
appkey: this.appkey, |
||||
|
message_id: '1c17d4f8e7894a20aecb9f774b54ba06', //generateRequestId1(),
|
||||
|
task_id: '1c17d4f8e7894a20aecb9f774b54ba06',//generateRequestId1(),
|
||||
|
namespace: 'FlowingSpeechSynthesizer' |
||||
|
}, |
||||
|
payload: { |
||||
|
voice: 'xiaoyun', |
||||
|
format: 'pcm', |
||||
|
sample_rate: 16000 |
||||
|
} |
||||
|
}; |
||||
|
this.send({ data: JSON.stringify(socketParams) }, 'txt'); |
||||
|
}); |
||||
|
|
||||
|
socket.onMessage((e) => { |
||||
|
const { data } = e; |
||||
|
|
||||
|
if (data instanceof ArrayBuffer) { |
||||
|
this._options.onMessage && this._options.onMessage(data); |
||||
|
} else { |
||||
|
if ( Object.prototype.toString.call(data) === "[object String]" ) { |
||||
|
const tmp = JSON.parse(data); |
||||
|
if (Object.prototype.toString.call(tmp.type) === "[object Number]") { |
||||
|
this._options.onMessage && this._options.onMessage(tmp); |
||||
|
} else { |
||||
|
if (!this.session_id) { |
||||
|
this.session_id = tmp.data |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
// this.on(type, e);
|
||||
|
this.createInter(); |
||||
|
resolve(); |
||||
|
}); |
||||
|
// 失败
|
||||
|
socket.onError((e) => { |
||||
|
console.log("websocket error 创建语音长链接报错", e); |
||||
|
//
|
||||
|
}); |
||||
|
// 关闭
|
||||
|
socket.onClose((e) => { |
||||
|
console.log("websocket close 语音长链接关闭", e); |
||||
|
this.connectSocketTimeOut && clearTimeout(this.connectSocketTimeOut); |
||||
|
//非自动关闭重连
|
||||
|
if (e.code != 1000) { |
||||
|
this.doConnectTimeout(); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
doConnectTimeout() { |
||||
|
if (this.selfCloseStatus){ |
||||
|
// 主动退出 不在重连
|
||||
|
return |
||||
|
} |
||||
|
// 重连一次
|
||||
|
console.log("websocket 异常关闭 开始重连"); |
||||
|
this.connectSocketTimeOut = setTimeout(() => { |
||||
|
this.createSocket({ |
||||
|
complete: function (res) { |
||||
|
// this.reconnectLock = false;
|
||||
|
}, |
||||
|
}); |
||||
|
}, RECONNECT_TIME); |
||||
|
} |
||||
|
|
||||
|
send(e, t) { |
||||
|
console.log("开始请求 websocket send", e); |
||||
|
this.socket && this.socket.send(e); |
||||
|
} |
||||
|
|
||||
|
getMsgData(e) { |
||||
|
if (e && typeof e == "string") { |
||||
|
const status = e.indexOf("42"); |
||||
|
const index = e.indexOf("["); |
||||
|
if (status > -1 && index > -1) { |
||||
|
const txt = e.substring(index); |
||||
|
const item = JSON.parse(txt); |
||||
|
const [type, obj] = item; |
||||
|
const payload = obj.payload ? obj.payload : {}; |
||||
|
const params = { |
||||
|
chatId: this._options.agentId, |
||||
|
type, |
||||
|
contentType: "text", //默认文字
|
||||
|
timestamp: new Date().getTime(), |
||||
|
...payload, |
||||
|
}; |
||||
|
// 缓存聊天记录
|
||||
|
return params; |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
emit(type, text, contentType) { |
||||
|
console.log("emit", type, text, generateRequestId1()); |
||||
|
switch (type) { |
||||
|
case "send": |
||||
|
// 发送消息
|
||||
|
// 发送消息
|
||||
|
// const socketParams = {
|
||||
|
// request_id: generateRequestId1()
|
||||
|
// };
|
||||
|
const socketParams = { |
||||
|
header: { |
||||
|
name: 'RunSynthesis', |
||||
|
appkey: this.appkey, |
||||
|
namespace: 'FlowingSpeechSynthesizer', |
||||
|
message_id: '1c17d4f8e7894a20aecb9f774b54ba06', |
||||
|
task_id: '1c17d4f8e7894a20aecb9f774b54ba06', // generateRequestId1(),
|
||||
|
}, |
||||
|
payload: { |
||||
|
text: text |
||||
|
} |
||||
|
// action: "ACTION_SYNTHESIS",
|
||||
|
// data: text,
|
||||
|
}; |
||||
|
this.send({ data: JSON.stringify(socketParams) }, contentType); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
// 监听
|
||||
|
on(type, params) { |
||||
|
switch (type) { |
||||
|
case "reply": |
||||
|
// 回复结束
|
||||
|
const tmpParams = { |
||||
|
...params, |
||||
|
name: this.robotObj.name, |
||||
|
headImage: this.robotObj.headImage, |
||||
|
}; |
||||
|
if ( |
||||
|
params && |
||||
|
params.type === "reply" && |
||||
|
!params.is_from_self && |
||||
|
params.can_rating |
||||
|
) { |
||||
|
// 回复消息
|
||||
|
console.log("reply回复内容", tmpParams.content, tmpParams); |
||||
|
this._options.onMessage && this._options.onMessage(tmpParams); |
||||
|
} |
||||
|
|
||||
|
if (!params.is_from_self && params.is_final) { |
||||
|
if (this.failMsg[tmpParams.request_id]) { |
||||
|
this.failMsg[tmpParams.request_id].status = true; |
||||
|
// 已回复 状态为true
|
||||
|
} |
||||
|
|
||||
|
// 回复结束 写入缓存
|
||||
|
// setMsgData(tmpParams);
|
||||
|
} |
||||
|
// 回复
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
createInter() { |
||||
|
if (this.timeoutObj) { |
||||
|
clearTimeout(this.timeoutObj); |
||||
|
} |
||||
|
this.timeoutObj = setTimeout(() => { |
||||
|
console.log(111111, this.socket) |
||||
|
this.socket && this.socket.send && this.socket.send({ data: 3 }); |
||||
|
}, HEART_BEAT_TIME); |
||||
|
} |
||||
|
|
||||
|
// 关闭socket
|
||||
|
destroy() { |
||||
|
if (this.socket && this.socket.readyState == 1) { |
||||
|
this.socket && this.socket.close(); |
||||
|
this.socket = null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
// type: Q-问题,A-答案,H-历史消息,S-停止生成,C-结束会话,T-转接会话,R-参考来源,F-点赞/点踩回执
|
||||
|
export const MESSAGE_TYPE = { |
||||
|
QUESTION: 'Q', |
||||
|
ANSWER: 'A', |
||||
|
HISTORY: 'H', |
||||
|
STOP: 'S', |
||||
|
CLOSE: 'C', |
||||
|
TRANSFER: 'T', |
||||
|
REFERENCE: 'R', |
||||
|
FEEDBACK: 'F', |
||||
|
WORKBENCH_HISTORY: 'WH' |
||||
|
}; |
||||
@ -0,0 +1,8 @@ |
|||||
|
wx.GLOBE = { |
||||
|
webimToken: [], |
||||
|
SOCKET: null, |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
export default wx.GLOBE |
||||
@ -0,0 +1,56 @@ |
|||||
|
/** |
||||
|
* !插入数据 |
||||
|
* @param roomId |
||||
|
* @param sourceId |
||||
|
* @param orderId |
||||
|
* @param type |
||||
|
* @param fileId |
||||
|
* @param txt |
||||
|
* @param time |
||||
|
* @param userId |
||||
|
* @param nickName |
||||
|
* @param sendOrReceive |
||||
|
* @param address |
||||
|
*/ |
||||
|
export const setMsgData = data => { |
||||
|
console.log('setMsgData', data) |
||||
|
const resData = data |
||||
|
const session_id = resData.chatId // 群id
|
||||
|
|
||||
|
let msgData = wx.getStorageSync('imMsgData') || {} |
||||
|
// * 插入群数据
|
||||
|
if (msgData[session_id]) { |
||||
|
if (resData.timestamp > msgData[session_id].timestamp) { |
||||
|
msgData[session_id].timestamp = resData.timestamp |
||||
|
} |
||||
|
msgData[session_id].listMsg.push(resData) |
||||
|
} else { |
||||
|
msgData[session_id] = { |
||||
|
listMsg: [resData], |
||||
|
timestamp: resData.timestamp, |
||||
|
session_id: resData.session_id, |
||||
|
} |
||||
|
} |
||||
|
wx.setStorageSync('imMsgData', msgData) |
||||
|
return resData |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// ! 获取群消息
|
||||
|
export const getHistroyMsg = id => { |
||||
|
let msgData = wx.getStorageSync('imMsgData') || {} |
||||
|
let msgList = [] |
||||
|
if (msgData[id]) { |
||||
|
const data = msgData[id] |
||||
|
// *处理历史消息并按时间排序
|
||||
|
const compare = property => { |
||||
|
return function(a, b) { |
||||
|
var value1 = a[property] |
||||
|
var value2 = b[property] |
||||
|
return value1 - value2 |
||||
|
} |
||||
|
} |
||||
|
msgList = data.listMsg.sort(compare('time')) |
||||
|
} |
||||
|
return { msgList } |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
import GLOBAL from './global'; |
||||
|
class Observer { |
||||
|
constructor() { |
||||
|
this._event = {} |
||||
|
} |
||||
|
$on(eventName, handler) { |
||||
|
if (this._event[eventName]) { |
||||
|
this._event[eventName].push(handler) |
||||
|
} else { |
||||
|
this._event[eventName] = [handler] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$emit(eventName) { |
||||
|
let events = this._event[eventName] |
||||
|
let otherArgs = Array.prototype.slice.call(arguments, 1) |
||||
|
let that = this |
||||
|
if (events) { |
||||
|
events.forEach(event => { |
||||
|
event.apply(that, otherArgs) |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
$off(eventName, handler) { |
||||
|
let events = this._event[eventName] |
||||
|
if (events) { |
||||
|
this._event[eventName] = events.filter(event => { |
||||
|
return event !== handler |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
$once(eventName, handler) { |
||||
|
let that = this |
||||
|
function func() { |
||||
|
let args = Array.prototype.slice.call(arguments, 0) |
||||
|
handler.apply(that, args) |
||||
|
this.off(eventName, func) |
||||
|
} |
||||
|
this.on(eventName, func) |
||||
|
} |
||||
|
} |
||||
|
if (!GLOBAL.observer) { |
||||
|
GLOBAL.observer = new Observer() |
||||
|
} |
||||
|
export default GLOBAL.observer |
||||
@ -0,0 +1,168 @@ |
|||||
|
export default class PcmPlayer { |
||||
|
constructor(options) { |
||||
|
this._options = options; |
||||
|
this.audioCtx = null; |
||||
|
this.rate = this._options.rate ?? 16000; |
||||
|
this.ch = this._options.ch ?? 1; |
||||
|
this.fadeMs = this._options.fadeMs ?? 20; |
||||
|
this.fadeSamples = (this.rate * this.fadeMs) / 1000; |
||||
|
this.queue = []; |
||||
|
this.isPlaying = false; |
||||
|
this.startTime = 0; |
||||
|
this._t = null; |
||||
|
this.AllTexts = []; |
||||
|
this.totalLen = 0 |
||||
|
} |
||||
|
|
||||
|
feed(data) { |
||||
|
if (!this.audioCtx) { |
||||
|
this.audioCtx = wx.createWebAudioContext(); |
||||
|
} |
||||
|
|
||||
|
this.queue.push(new Int16Array(data)); |
||||
|
if (!this.isPlaying) this._play(); |
||||
|
} |
||||
|
|
||||
|
loadAudio = (url) => { |
||||
|
return new Promise((resolve) => { |
||||
|
wx.request({ |
||||
|
url, |
||||
|
responseType: "arraybuffer", |
||||
|
success: (res) => { |
||||
|
console.log("res.data", res.data); |
||||
|
audioCtx.decodeAudioData( |
||||
|
res.data, |
||||
|
(buffer) => { |
||||
|
resolve(buffer); |
||||
|
}, |
||||
|
(err) => { |
||||
|
console.error("decodeAudioData fail", err); |
||||
|
reject(); |
||||
|
} |
||||
|
); |
||||
|
}, |
||||
|
fail: (res) => { |
||||
|
console.error("request fail", res); |
||||
|
reject(); |
||||
|
}, |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
async _play() { |
||||
|
this.isPlaying = true; |
||||
|
if (!this.audioCtx) { |
||||
|
return; |
||||
|
} |
||||
|
// 开始播放
|
||||
|
this.startTime = this.audioCtx.currentTime ?? 0; |
||||
|
setTimeout(() => { |
||||
|
this.syncLoop(); |
||||
|
}); |
||||
|
while (this.queue.length) { |
||||
|
const pcm = this.queue.shift(); |
||||
|
const len = pcm.length / this.ch; |
||||
|
const buf = this.audioCtx.createBuffer(this.ch, len, this.rate); |
||||
|
|
||||
|
// 1. 归一化(防溢出)
|
||||
|
for (let c = 0; c < this.ch; c++) { |
||||
|
const data = buf.getChannelData(c); |
||||
|
for (let i = 0; i < len; i++) { |
||||
|
const val = pcm[i * this.ch + c]; |
||||
|
data[i] = val > 0 ? val / 0x7fff : val / 0x8000; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 2. 帧边界淡入淡出(防不连续滋滋)
|
||||
|
const fadeSamples = Math.min(this.fadeSamples, len); // 5ms@16kHz
|
||||
|
const data = buf.getChannelData(0); |
||||
|
for (let j = 0; j < fadeSamples; j++) { |
||||
|
const gain = j / fadeSamples; |
||||
|
data[j] *= gain; // fade-in
|
||||
|
data[len - 1 - j] *= gain; // fade-out
|
||||
|
} |
||||
|
|
||||
|
// 3. 播放并等待结束
|
||||
|
await new Promise((resolve, reject) => { |
||||
|
if (!this.isPlaying) {return reject()} |
||||
|
const src = this.audioCtx.createBufferSource(); |
||||
|
src.buffer = buf; |
||||
|
src.connect(this.audioCtx.destination); |
||||
|
src.onended = resolve; |
||||
|
src.start(); |
||||
|
}); |
||||
|
} |
||||
|
this.isPlaying = false; |
||||
|
} |
||||
|
|
||||
|
// 监听音频播放进度
|
||||
|
syncLoop() { |
||||
|
const loop = () => { |
||||
|
if (!this.audioCtx) { |
||||
|
return; |
||||
|
} |
||||
|
const now = this.audioCtx.currentTime - this.startTime; |
||||
|
// 找当前字幕
|
||||
|
const index = this.AllTexts.findIndex( |
||||
|
(s) => now >= s.beginTime && now < s.endTime |
||||
|
); |
||||
|
if (index > -1) { |
||||
|
const obj = this.AllTexts[index]; |
||||
|
if (this.AllTexts.length == 1) { |
||||
|
// 最后一条数据
|
||||
|
obj.is_final = true; |
||||
|
} |
||||
|
const tmp = this.AllTexts.shift(); |
||||
|
this._options.onPlay && this._options.onPlay(tmp); |
||||
|
if (!this.AllTexts.length) { |
||||
|
clearTimeout(this._t); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
if (!this.AllTexts.length) { |
||||
|
this._t = setTimeout(loop, 16); |
||||
|
} else { |
||||
|
if (now < this.AllTexts[this.AllTexts.length - 1].endTime) { |
||||
|
this._t = setTimeout(loop, 16); |
||||
|
} else { |
||||
|
// 结束
|
||||
|
clearTimeout(this._t); |
||||
|
console.log("语音播放结束"); |
||||
|
this.destroy(); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
loop(); |
||||
|
} |
||||
|
|
||||
|
// 增加文字
|
||||
|
addText(res) { |
||||
|
if (res.result && res.result.subtitles && res.result.subtitles.length) { |
||||
|
const tmpText = res.result.subtitles.map((it) => { |
||||
|
return { |
||||
|
...it, |
||||
|
content: it.text, |
||||
|
beginTime: it.beginTime / 1000, |
||||
|
endTime: it.endTime / 1000, |
||||
|
request_id: res.requestId, |
||||
|
sessionId: res.sessionId, |
||||
|
}; |
||||
|
}); |
||||
|
this.AllTexts = [...this.AllTexts, ...tmpText]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
close() { |
||||
|
this.isPlaying = false; |
||||
|
this.audioCtx.close(); |
||||
|
} |
||||
|
destroy() { |
||||
|
this.queue = []; |
||||
|
this.isPlaying = false; |
||||
|
this.audioCtx && this.audioCtx.close(); |
||||
|
this.audioCtx = null; |
||||
|
this.AllTexts = []; |
||||
|
this.startTime = 0; |
||||
|
this._options.onAudioEnd && this._options.onAudioEnd(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,322 @@ |
|||||
|
import Vue from "vue"; |
||||
|
import GLOBAL_OBJ from "./global"; |
||||
|
import "./EventHub"; |
||||
|
import { setMsgData } from "./message"; |
||||
|
// 心跳间隔
|
||||
|
const HEART_BEAT_TIME = 15000; |
||||
|
// 心跳最大失败次数(超过此次数重连)
|
||||
|
const HEART_BEAT_FAIL_NUM = 1; |
||||
|
// 重连间隔
|
||||
|
const RECONNECT_TIME = 3000; |
||||
|
|
||||
|
export default class Socket { |
||||
|
constructor(option) { |
||||
|
this.socket = null; |
||||
|
this._options = option; |
||||
|
this.timeoutObj = null; |
||||
|
this.robotObj = {}; |
||||
|
this.selfCloseStatus = false |
||||
|
this.reconnectLock = false |
||||
|
console.log("Socket init", GLOBAL_OBJ); |
||||
|
// 失败回答
|
||||
|
this.failMsg = {} |
||||
|
} |
||||
|
getToken() { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
console.log('-----开始请求token-----') |
||||
|
uni.request({ |
||||
|
method: "GET", |
||||
|
dataType: "json", |
||||
|
// url: `http://192.168.124.118:8083/xcx/framework/agent/${this._options.agentId}`,
|
||||
|
// url: `https://des.js-dyyj.com/getDemoToken?id=${this._options.agentId}`,
|
||||
|
url: `https://des.dayunyuanjian.cn/getDemoToken?id=${this._options.agentId}`, |
||||
|
success: (res) => { |
||||
|
console.log("请求token成功", res); |
||||
|
resolve(res); |
||||
|
}, |
||||
|
fail: (err) => { |
||||
|
wx.showToast({ |
||||
|
title: "创建失败", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
console.log("请求token失败", err); |
||||
|
reject() |
||||
|
}, |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
async init() { |
||||
|
return this.createSocket(); |
||||
|
} |
||||
|
async createSocket(options) { |
||||
|
return new Promise(async (resolve, reject) => { |
||||
|
const origin = "wss://wss.lke.cloud.tencent.com"; |
||||
|
// const origin = "wss://des.dayunyuanjian.cn";
|
||||
|
let path = "/v1/qbot/chat/conn/"; |
||||
|
let initSocket = 1; |
||||
|
let mainToken |
||||
|
const res = await this.getToken(); |
||||
|
if ( |
||||
|
res && |
||||
|
res.data && |
||||
|
res.data.apiResponse && |
||||
|
res.data.apiResponse.Token |
||||
|
) { |
||||
|
mainToken = res.data.apiResponse.Token; |
||||
|
} |
||||
|
// 机器人信息
|
||||
|
this.robotObj = res.data.requestInfo; |
||||
|
console.log("获取token:", mainToken); |
||||
|
|
||||
|
if (this.selfCloseStatus) {return} |
||||
|
|
||||
|
// 建立连接
|
||||
|
const socket = wx.connectSocket({ |
||||
|
url: `${origin}${path}?EIO=4&transport=websocket`, |
||||
|
success: (e) => { |
||||
|
console.log("创建长链接成功", e); |
||||
|
}, |
||||
|
complete: (e) => { |
||||
|
console.log("socket - complete", e); |
||||
|
|
||||
|
}, |
||||
|
}); |
||||
|
this.socket = socket; |
||||
|
GLOBAL_OBJ.SOCKET = this; |
||||
|
|
||||
|
socket.onOpen((e) => { |
||||
|
// 监听发送
|
||||
|
if (initSocket === 1) { |
||||
|
const token = mainToken || ""; |
||||
|
if (token) { |
||||
|
// cb({ token: token });
|
||||
|
this.send({ |
||||
|
data: |
||||
|
"40" + |
||||
|
JSON.stringify({ |
||||
|
token: token, |
||||
|
}), |
||||
|
}); |
||||
|
} else { |
||||
|
// cb({ token: '' });
|
||||
|
this.send({ |
||||
|
data: JSON.stringify({ |
||||
|
token: "", |
||||
|
}), |
||||
|
}); |
||||
|
} |
||||
|
initSocket++; |
||||
|
} else { |
||||
|
const token = mainToken || ""; |
||||
|
// cb({ token: token });
|
||||
|
this.send({ |
||||
|
data: JSON.stringify({ |
||||
|
token: token, |
||||
|
}), |
||||
|
}); |
||||
|
initSocket++; |
||||
|
} |
||||
|
|
||||
|
options && options.complete && options.complete() |
||||
|
|
||||
|
|
||||
|
if (this.selfCloseStatus) { |
||||
|
this.destroy() |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
socket.onMessage((e) => { |
||||
|
console.log("socket.onMessage", e); |
||||
|
const { data } = e; |
||||
|
if (data == 2) { |
||||
|
// 触发浪涌
|
||||
|
this.send({ |
||||
|
data: '3', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
const params = this.getMsgData(data); |
||||
|
const { type } = params ?? {}; |
||||
|
this.on(type, params); |
||||
|
const num = "" + data.substring(0, 2); |
||||
|
if (num == "40") { |
||||
|
console.log("创建对话成功", e); |
||||
|
// 发送失败信息
|
||||
|
this.sendFailMsg() |
||||
|
resolve(); |
||||
|
// 链接成功
|
||||
|
} |
||||
|
|
||||
|
this.createInter(); |
||||
|
}); |
||||
|
// 失败
|
||||
|
socket.onError((e) => { |
||||
|
console.log("websocket error 长链接报错", e); |
||||
|
// 失败重建
|
||||
|
this.doConnectTimeout(); |
||||
|
//
|
||||
|
}); |
||||
|
// 关闭
|
||||
|
socket.onClose((e) => { |
||||
|
console.log("websocket close 长链接关闭", e); |
||||
|
this.connectSocketTimeOut && clearTimeout(this.connectSocketTimeOut); |
||||
|
//非自动关闭重连
|
||||
|
if (e.code != 1000) { |
||||
|
this.doConnectTimeout(); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
doConnectTimeout() { |
||||
|
if (this.selfCloseStatus){ |
||||
|
// 主动退出 不在重连
|
||||
|
return |
||||
|
} |
||||
|
// 重连一次
|
||||
|
console.log("websocket 异常关闭 开始重连"); |
||||
|
this.connectSocketTimeOut = setTimeout(() => { |
||||
|
this.createSocket({ |
||||
|
complete: function (res) { |
||||
|
// this.reconnectLock = false;
|
||||
|
}, |
||||
|
}); |
||||
|
}, RECONNECT_TIME); |
||||
|
} |
||||
|
|
||||
|
onConnect(e) { |
||||
|
console.log("websocket connect", e); |
||||
|
} |
||||
|
|
||||
|
send(e, t) { |
||||
|
console.log("开始请求 websocket send", e); |
||||
|
this.socket && this.socket.send(e); |
||||
|
this.createInter(); |
||||
|
} |
||||
|
|
||||
|
sendFailMsg() { |
||||
|
const content = Object.values(this.failMsg) |
||||
|
const lastFailMsgIndex = content.findLastIndex(it => !it.status) |
||||
|
if (lastFailMsgIndex > -1) { |
||||
|
const msg = content[lastFailMsgIndex] |
||||
|
console.log('开始重新发送失败信息', msg) |
||||
|
// 重新发送失败的信息
|
||||
|
this.send(msg.data.socketParams,msg.data.contentType) |
||||
|
} |
||||
|
} |
||||
|
getMsgData(e) { |
||||
|
if (e && typeof e == "string") { |
||||
|
const status = e.indexOf("42"); |
||||
|
const index = e.indexOf("["); |
||||
|
if (status > -1 && index > -1) { |
||||
|
const txt = e.substring(index); |
||||
|
const item = JSON.parse(txt); |
||||
|
const [type, obj] = item; |
||||
|
const payload = obj.payload ? obj.payload : {}; |
||||
|
const params = { |
||||
|
chatId: this._options.agentId, |
||||
|
type, |
||||
|
contentType: "text", //默认文字
|
||||
|
timestamp: new Date().getTime(), |
||||
|
...payload, |
||||
|
}; |
||||
|
// 缓存聊天记录
|
||||
|
return params; |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
emit(type, params, contentType) { |
||||
|
console.log("emit", type, params); |
||||
|
const tmpParams = params |
||||
|
tmpParams.incremental = true |
||||
|
const data = { |
||||
|
//incremental:true,
|
||||
|
payload: tmpParams |
||||
|
|
||||
|
}; |
||||
|
switch (type) { |
||||
|
case "send": |
||||
|
// 发送消息
|
||||
|
const socketParams = { data: "42" + JSON.stringify(["send", data]) }; |
||||
|
this.send(socketParams, contentType); |
||||
|
|
||||
|
console.log(params.request_id + '发送内容', data) |
||||
|
if (!params.is_msg_status) { |
||||
|
// 不写入缓存中
|
||||
|
return; |
||||
|
} |
||||
|
const msgParams = { |
||||
|
chatId: this._options.agentId, |
||||
|
contentType, |
||||
|
type, |
||||
|
timestamp: new Date().getTime(), |
||||
|
...params, |
||||
|
content: params.realContent ? params.realContent : params.content, |
||||
|
}; |
||||
|
msgParams && setMsgData(msgParams); |
||||
|
this.failMsg[params.request_id] = { |
||||
|
data: { |
||||
|
socketParams, |
||||
|
contentType |
||||
|
}, |
||||
|
statue: false |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
// 监听
|
||||
|
on(type, params) { |
||||
|
switch (type) { |
||||
|
case "reply": |
||||
|
// 回复结束
|
||||
|
const tmpParams = { |
||||
|
...params, |
||||
|
name: this.robotObj.name, |
||||
|
headImage: this.robotObj.headImage, |
||||
|
}; |
||||
|
if ( |
||||
|
params && |
||||
|
params.type === "reply" && |
||||
|
!params.is_from_self && |
||||
|
params.can_rating |
||||
|
) { |
||||
|
// 回复消息
|
||||
|
console.log('reply回复内容', tmpParams.content, tmpParams) |
||||
|
this._options.onMessage && this._options.onMessage(tmpParams); |
||||
|
} |
||||
|
|
||||
|
if (!params.is_from_self && params.is_final) { |
||||
|
if (this.failMsg[tmpParams.request_id]) { |
||||
|
this.failMsg[tmpParams.request_id].status = true |
||||
|
// 已回复 状态为true
|
||||
|
} |
||||
|
|
||||
|
// 回复结束 写入缓存
|
||||
|
// setMsgData(tmpParams);
|
||||
|
} |
||||
|
// 回复
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 关闭socket
|
||||
|
destroy() { |
||||
|
if (this.socket && this.socket.readyState == 1) { |
||||
|
this.socket && this.socket.close(); |
||||
|
this.socket = null; |
||||
|
} |
||||
|
this.failMsg={} |
||||
|
} |
||||
|
|
||||
|
createInter() { |
||||
|
if (this.timeoutObj) { |
||||
|
clearTimeout(this.timeoutObj); |
||||
|
} |
||||
|
return |
||||
|
this.timeoutObj = setTimeout(() => { |
||||
|
this.socket && this.socket.send && this.socket.send({ data: 3 }); |
||||
|
}, HEART_BEAT_TIME); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,132 @@ |
|||||
|
/** |
||||
|
* 获取 location hash 参数 |
||||
|
* @param {string} variable 参数名 |
||||
|
* @returns {string} 参数值 |
||||
|
*/ |
||||
|
export const getQueryVariable = (variable) => { |
||||
|
const query = window.location.hash.split('?'); |
||||
|
|
||||
|
if (query.length < 2) { |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
const vars = query[1].split('&'); |
||||
|
|
||||
|
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
||||
|
for (let i = 0; i < vars.length; i++) { |
||||
|
const pair = vars[i].split('='); |
||||
|
if (pair[0] === variable) { |
||||
|
return decodeURI(pair[1]); |
||||
|
} |
||||
|
} |
||||
|
return ''; |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* 滚动至指定dom底部 |
||||
|
*/ |
||||
|
export const scrollToBottom = (sDom, sTop) => { |
||||
|
if (!sDom) return; |
||||
|
sDom.scrollTo({ |
||||
|
top: sTop |
||||
|
// behavior: 'smooth'
|
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* 数组去重 |
||||
|
*/ |
||||
|
export const arrayUnique = (arr, replaceKey, holdKey) => { |
||||
|
let temp = {}; |
||||
|
|
||||
|
return arr.reduce((prev, cur) => { |
||||
|
if (!temp[cur[replaceKey]]) { |
||||
|
temp[cur[replaceKey]] = {index: prev.length}; |
||||
|
prev.push(cur); |
||||
|
} else { |
||||
|
const oldItem = temp[cur[replaceKey]]; |
||||
|
cur[holdKey] = oldItem[holdKey]; |
||||
|
prev.splice(oldItem['index'], 1, cur); |
||||
|
} |
||||
|
|
||||
|
return prev; |
||||
|
}, []); |
||||
|
}; |
||||
|
|
||||
|
export const generateRequestId1 = (length = 32) => { |
||||
|
const data = |
||||
|
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', |
||||
|
'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', |
||||
|
'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', |
||||
|
's', 't', 'u', 'v', 'w', 'x', 'y', 'z']; |
||||
|
let nums = ''; |
||||
|
for (let i = 0; i < length; i++) { |
||||
|
const r = parseInt(Math.random() * 61, 10); |
||||
|
nums += data[r]; |
||||
|
} |
||||
|
return nums //+ '-' + parseInt(Math.random() * 10000000000, 10);
|
||||
|
}; |
||||
|
export const generateRequestId = (length = 10) => { |
||||
|
const data = |
||||
|
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', |
||||
|
'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', |
||||
|
'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', |
||||
|
's', 't', 'u', 'v', 'w', 'x', 'y', 'z']; |
||||
|
let nums = ''; |
||||
|
for (let i = 0; i < length; i++) { |
||||
|
const r = parseInt(Math.random() * 61, 10); |
||||
|
nums += data[r]; |
||||
|
} |
||||
|
return nums + '-' + parseInt(Math.random() * 10000000000, 10); |
||||
|
}; |
||||
|
|
||||
|
function escapeHtml (str) { |
||||
|
return str.replace(/[&<>"'/]/g, function (match) { |
||||
|
return { |
||||
|
'&': '&', |
||||
|
'<': '<', |
||||
|
'>': '>', |
||||
|
'"': '"', |
||||
|
"'": ''', |
||||
|
'/': '/' |
||||
|
}[match]; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
export const splitTextForTTS = (text, maxLength = 25) => { |
||||
|
// 定义优先分割的标点符号
|
||||
|
const punctuation = ['。', ',', ';', '?', '!', ',', ';', '?', '!', '、']; |
||||
|
let segments = []; |
||||
|
let segment = ''; |
||||
|
let tempSegment = ''; |
||||
|
|
||||
|
for (let i = 0; i < text.length; i++) { |
||||
|
tempSegment += text[i]; // 如果超过最大长度,则尝试在上一个标点符号处分割
|
||||
|
|
||||
|
if (tempSegment.length > maxLength) { |
||||
|
let lastPunctuationIndex = -1; |
||||
|
for (let j = tempSegment.length - 1; j >= 0; j--) { |
||||
|
if (punctuation.includes(tempSegment[j])) { |
||||
|
lastPunctuationIndex = j; |
||||
|
break; |
||||
|
} |
||||
|
} // 如果找到标点符号,则在标点符号后分割
|
||||
|
|
||||
|
if (lastPunctuationIndex !== -1) { |
||||
|
segments.push(tempSegment.slice(0, lastPunctuationIndex + 1).trim()); |
||||
|
tempSegment = tempSegment.slice(lastPunctuationIndex + 1).trim(); |
||||
|
} else { |
||||
|
// 如果没有找到标点符号,则在最大长度处分割
|
||||
|
segments.push(tempSegment.slice(0, maxLength).trim()); |
||||
|
tempSegment = tempSegment.slice(maxLength).trim(); |
||||
|
} |
||||
|
} |
||||
|
} // 添加最后一个段落
|
||||
|
|
||||
|
if (tempSegment.length > 0) { |
||||
|
segments.push(tempSegment.trim()); |
||||
|
} |
||||
|
|
||||
|
return segments; |
||||
|
} |
||||
@ -0,0 +1,364 @@ |
|||||
|
<template> |
||||
|
<view class="product-section"> |
||||
|
<view class="title-section"> |
||||
|
<div style=" |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
"> |
||||
|
<text class="title">文化IP合作体</text> |
||||
|
</div> |
||||
|
</view> |
||||
|
<!-- 轮播容器 --> |
||||
|
<view class="carousel-container"> |
||||
|
<!-- 左箭头 --> |
||||
|
<view class="nav-arrow left-arrow" @click="prevSlide" v-if="list.length > 1 && currentIndex > 0"> |
||||
|
<image style="width: 50rpx;height: 50rpx;" |
||||
|
:src="showImg('/uploads/20250908/29beeddf1e45571d2c5a4187f2f1ae05.png')"></image> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 轮播内容 --> |
||||
|
<scroll-view class="carousel-scroll" scroll-x="true" :show-scrollbar="false" :enhanced="true" |
||||
|
:scroll-with-animation="true" :scroll-left="scrollLeft" @scroll="onScroll" @scrollend="onScrollEnd"> |
||||
|
<view class="carousel-content"> |
||||
|
<view class="carousel-item" v-for="(item, index) in list" :key="index" @click="gotoDetail(item,index)" |
||||
|
v-if="item && item.image"> |
||||
|
<view class="issue-card"> |
||||
|
<!-- 背景图片 --> |
||||
|
<image class="card-bg" :src="showImg(item.image)" mode="aspectFill"></image> |
||||
|
<view class="title-info"> |
||||
|
<view class="title-item"> |
||||
|
{{item.title}} |
||||
|
</view> |
||||
|
<view class="title-des"> |
||||
|
{{item.des}} |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 无数据时的提示 --> |
||||
|
<view v-if="!list || list.length === 0" class="no-data"> |
||||
|
<text>暂无数据</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</scroll-view> |
||||
|
|
||||
|
<!-- 右箭头 --> |
||||
|
<view class="nav-arrow right-arrow" @click="nextSlide" |
||||
|
v-if="list.length > 1 && currentIndex < list.length - 1"> |
||||
|
|
||||
|
<image style="width: 50rpx;height: 50rpx;" |
||||
|
:src="showImg('/uploads/20250908/6622b3699518d6b559e1241d7addb7af.png')"></image> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
data() { |
||||
|
return { |
||||
|
list: [{ |
||||
|
image: '/uploads/20250908/bb069c36e88de7cde4e8dd24ac3b33b5.png', |
||||
|
title: '太湖NAINO', |
||||
|
des: 'NAINO的异次元发现', |
||||
|
}, |
||||
|
{ |
||||
|
image: '/uploads/20250908/ab1ffc93875de79fc87ee2c5e70cad3f.png', |
||||
|
title: 'SUNRISE日出东方', |
||||
|
des: '扬州瓜洲古渡', |
||||
|
}, |
||||
|
{ |
||||
|
image: '/uploads/20250908/4d38fcd2e403a13b85a96188de12a2a8.png', |
||||
|
title: '中国昆曲博物馆', |
||||
|
des: '姹紫婿红里', |
||||
|
}, |
||||
|
{ |
||||
|
image: '/uploads/20250908/d4115d6c907f8b3ed5d30a11f9912460.png', |
||||
|
title: '退思园', |
||||
|
des: '苏州古典园林生活', |
||||
|
mini: { |
||||
|
appID: 'wxf3af8e268906fd6d', |
||||
|
path: 'pages/index/index' |
||||
|
} |
||||
|
} |
||||
|
], |
||||
|
currentIndex: 0, |
||||
|
scrollLeft: 0, |
||||
|
itemWidth: 224, // 207rpx + 30rpx margin |
||||
|
}; |
||||
|
}, |
||||
|
mounted() { |
||||
|
// this.getList(); |
||||
|
}, |
||||
|
methods: { |
||||
|
handleMoreClick() { |
||||
|
uni.switchTab({ |
||||
|
url: '/pages/index/readingBody' |
||||
|
}) |
||||
|
}, |
||||
|
gotoDetail(item,index) { |
||||
|
|
||||
|
uni.navigateTo({ |
||||
|
url:`/subPackages/other/ipPoster?index=${index}&item=${encodeURIComponent(JSON.stringify(item))}` |
||||
|
}) |
||||
|
|
||||
|
}, |
||||
|
getList() { |
||||
|
// this.Post( |
||||
|
// { |
||||
|
// type_id: 3, |
||||
|
// offset: 0, |
||||
|
// limit: 10, // 增加获取数量,确保有足够数据 |
||||
|
// }, |
||||
|
// "/api/article/getArticleByType" |
||||
|
// ).then((res) => { |
||||
|
// if (res.data && res.data.length > 0) { |
||||
|
// this.list = res.data; |
||||
|
// // 重置索引,确保从第一项开始 |
||||
|
// this.currentIndex = 0; |
||||
|
// this.scrollLeft = 0; |
||||
|
// } |
||||
|
// }); |
||||
|
}, |
||||
|
|
||||
|
// 上一张 |
||||
|
prevSlide() { |
||||
|
if (!this.validateData()) return; |
||||
|
|
||||
|
if (this.list.length > 1) { |
||||
|
this.currentIndex = |
||||
|
this.currentIndex > 0 ? this.currentIndex - 1 : this.list.length - 1; |
||||
|
this.$nextTick(() => { |
||||
|
this.scrollToCurrentItem(); |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 下一张 |
||||
|
nextSlide() { |
||||
|
if (!this.validateData()) return; |
||||
|
|
||||
|
if (this.list.length > 1) { |
||||
|
this.currentIndex = |
||||
|
this.currentIndex < this.list.length - 1 ? this.currentIndex + 1 : 0; |
||||
|
this.$nextTick(() => { |
||||
|
this.scrollToCurrentItem(); |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 滚动到当前项目 |
||||
|
scrollToCurrentItem() { |
||||
|
// 确保索引在有效范围内 |
||||
|
if (this.currentIndex < 0) { |
||||
|
this.currentIndex = 0; |
||||
|
} |
||||
|
if (this.currentIndex >= this.list.length) { |
||||
|
this.currentIndex = this.list.length - 1; |
||||
|
} |
||||
|
|
||||
|
const scrollPosition = this.currentIndex * this.itemWidth; |
||||
|
this.scrollLeft = scrollPosition; |
||||
|
}, |
||||
|
|
||||
|
// 滚动事件 |
||||
|
onScroll(e) { |
||||
|
const scrollLeft = e.detail.scrollLeft; |
||||
|
const index = Math.round(scrollLeft / this.itemWidth); |
||||
|
if ( |
||||
|
index !== this.currentIndex && |
||||
|
index >= 0 && |
||||
|
index < this.list.length |
||||
|
) { |
||||
|
this.currentIndex = index; |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 滚动结束事件 |
||||
|
onScrollEnd(e) { |
||||
|
const scrollLeft = e.detail.scrollLeft; |
||||
|
const index = Math.round(scrollLeft / this.itemWidth); |
||||
|
// 确保索引在有效范围内 |
||||
|
this.currentIndex = Math.max(0, Math.min(index, this.list.length - 1)); |
||||
|
this.$nextTick(() => { |
||||
|
this.scrollToCurrentItem(); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 点击卡片 |
||||
|
handleItemClick(item) { |
||||
|
console.log("点击了卡片:", item); |
||||
|
// 这里可以添加跳转逻辑 |
||||
|
}, |
||||
|
|
||||
|
// 验证数据有效性 |
||||
|
validateData() { |
||||
|
if (!this.list || this.list.length === 0) { |
||||
|
this.currentIndex = 0; |
||||
|
this.scrollLeft = 0; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// 确保当前索引在有效范围内 |
||||
|
if (this.currentIndex < 0) { |
||||
|
this.currentIndex = 0; |
||||
|
} |
||||
|
if (this.currentIndex >= this.list.length) { |
||||
|
this.currentIndex = this.list.length - 1; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.product-section { |
||||
|
width: 100%; |
||||
|
background-color: #fffdd6; |
||||
|
padding: 40rpx 25rpx; |
||||
|
margin: 30rpx 0; |
||||
|
border-radius: 40rpx; |
||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1), |
||||
|
0 0 0 1rpx rgba(255, 255, 255, 0.2) inset; |
||||
|
} |
||||
|
|
||||
|
// 轮播容器 |
||||
|
.carousel-container { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
height: 191rpx; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
// 轮播滚动视图 |
||||
|
.carousel-scroll { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
// 轮播内容 |
||||
|
.carousel-content { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
height: 100%; |
||||
|
min-width: max-content; |
||||
|
} |
||||
|
|
||||
|
// 轮播项 |
||||
|
.carousel-item { |
||||
|
flex-shrink: 0; |
||||
|
width: 154rpx; |
||||
|
height: 191rpx; |
||||
|
margin: 0 7rpx; |
||||
|
will-change: transform; |
||||
|
} |
||||
|
|
||||
|
// 卡片样式 |
||||
|
.issue-card { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
// 背景图片 |
||||
|
.card-bg { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
// 导航箭头 |
||||
|
.nav-arrow { |
||||
|
position: absolute; |
||||
|
top: 50%; |
||||
|
transform: translateY(-50%); |
||||
|
width: 60rpx; |
||||
|
height: 60rpx; |
||||
|
border-radius: 50%; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
cursor: pointer; |
||||
|
z-index: 3; |
||||
|
transition: all 0.3s ease; |
||||
|
|
||||
|
&.left-arrow { |
||||
|
left: 0rpx; |
||||
|
} |
||||
|
|
||||
|
&.right-arrow { |
||||
|
right: 0rpx; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.arrow-icon { |
||||
|
font-size: 32rpx; |
||||
|
color: #ffffff; |
||||
|
font-weight: bold; |
||||
|
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
// 无数据提示 |
||||
|
.no-data { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
width: 100%; |
||||
|
height: 368rpx; |
||||
|
color: #999; |
||||
|
font-size: 28rpx; |
||||
|
} |
||||
|
|
||||
|
// 标题区域 |
||||
|
.title-section { |
||||
|
display: inline-block; |
||||
|
padding: 0rpx 0 30rpx; |
||||
|
width: 100%; |
||||
|
|
||||
|
.title { |
||||
|
font-size: 34rpx; |
||||
|
font-weight: bold; |
||||
|
color: #000000; |
||||
|
} |
||||
|
|
||||
|
.more-btn { |
||||
|
font-size: 26rpx; |
||||
|
color: #000000; |
||||
|
margin-left: 35rpx; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.title-info { |
||||
|
position: absolute; |
||||
|
bottom: 10rpx; |
||||
|
right: 0; |
||||
|
width: 100%; |
||||
|
color: white; |
||||
|
padding: 0 5rpx; |
||||
|
|
||||
|
.title-item { |
||||
|
text-align: center; |
||||
|
font-size: 16rpx; |
||||
|
font-weight: bold; |
||||
|
overflow: hidden; |
||||
|
word-wrap: normal; |
||||
|
text-overflow: ellipsis; |
||||
|
} |
||||
|
|
||||
|
.title-des { |
||||
|
margin-top: 5rpx; |
||||
|
text-align: center; |
||||
|
font-size: 14rpx; |
||||
|
overflow: hidden; |
||||
|
word-wrap: normal; |
||||
|
text-overflow: ellipsis; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,407 @@ |
|||||
|
<template> |
||||
|
<view class="product-section" :style="{'background-color':color}"> |
||||
|
<!-- 商品标题区域 --> |
||||
|
<view class="title-section" > |
||||
|
<div style="display: flex; align-items: center;justify-content: space-between;" @click="handleMoreClick"> |
||||
|
<text class="title">{{ title }}</text> |
||||
|
<view class="more-btn" v-if="type!=3"> |
||||
|
<image src="https://des.dayunyuanjian.cn/data/2025/08/31/affb21f0-fcc1-4746-9543-0d54369aa315.png" style="width:124.6rpx;height: 36rpx;"/> |
||||
|
</view> |
||||
|
</div> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 商品列表 --> |
||||
|
<view class="product-list"> |
||||
|
<view class="" v-if="type==3" @click="toCGCwx"> |
||||
|
<image |
||||
|
style="height: 357rpx;width: 657rpx;border-radius: 20rpx;" |
||||
|
:src="showImg('/uploads/20250829/66732062c2d566802a7842525e9b92e6.png')" |
||||
|
mode="aspectFill" |
||||
|
></image> |
||||
|
</view> |
||||
|
<template v-else> |
||||
|
<view |
||||
|
class="product-card" |
||||
|
v-for="(item, index) in productList" |
||||
|
:key="index" |
||||
|
@click="handleProductClick(item)" |
||||
|
> |
||||
|
<view class="card-image-container"> |
||||
|
<image |
||||
|
class="card-image" |
||||
|
:src="showImg(item.image)" |
||||
|
mode="aspectFill" |
||||
|
></image> |
||||
|
<!-- 图片蒙层 --> |
||||
|
<view class="image-overlay" v-if="type==1"></view> |
||||
|
<!-- 智能体标签 --> |
||||
|
<view class="content-box-info" v-if="item.agent" @click.stop="toAgent(item)"> |
||||
|
<!-- 头像 --> |
||||
|
<image |
||||
|
class="avatar" |
||||
|
:src="showImg(item.agent.headImage)" |
||||
|
mode="aspectFill" |
||||
|
></image> |
||||
|
<view class="ai-tag"> |
||||
|
<view |
||||
|
class="ai-label" |
||||
|
:style="{ |
||||
|
borderColor: aiTagBorderColor, |
||||
|
color: aiTagTextColor, |
||||
|
}" |
||||
|
>智能体</view |
||||
|
> |
||||
|
</view> |
||||
|
<view class="ai-name">{{ item.agent.name }}</view> |
||||
|
|
||||
|
</view> |
||||
|
</view> |
||||
|
<view class="card-content"> |
||||
|
<view class="title-price-heart"> |
||||
|
<view class="card-title">{{ item.title }}</view> |
||||
|
<view class="card-price">¥{{ item.price }} <text style="font-size:24rpx ;margin-left: 5rpx;" v-if="type==2">起</text></view> |
||||
|
<template v-if="type==1"> |
||||
|
<image |
||||
|
v-if="!item.type" |
||||
|
class="heart-icon" |
||||
|
src="https://epic.js-dyyj.com/uploads/20250728/2f3ae212c01fa3b67be81abc5723cf5c.png" |
||||
|
@click.stop="handleLikeClick(item, index)" |
||||
|
></image> |
||||
|
<image |
||||
|
v-else |
||||
|
class="heart-icon" |
||||
|
src="https://epic.js-dyyj.com/uploads/20250728/dd7ed269b24e84a2dd141da6ab980fd6.png" |
||||
|
@click.stop="handleLikeClick(item, index)" |
||||
|
></image> |
||||
|
</template> |
||||
|
<!-- <template v-if="isFeel"> |
||||
|
<image |
||||
|
v-if="!item.isShop" |
||||
|
class="shop-icon" |
||||
|
src="https://epic.js-dyyj.com/uploads/20250728/195bfc195a54b93c13595a01a5d8bb3b.png" |
||||
|
@click.stop="handleLikeClick(item, index)" |
||||
|
></image> |
||||
|
<image |
||||
|
v-else |
||||
|
class="shop-icon" |
||||
|
src="https://epic.js-dyyj.com/uploads/20250728/77c4546ac6415f9db69bb10888d2a975.png" |
||||
|
@click.isShop="handleLikeClick(item, index)" |
||||
|
></image> |
||||
|
</template> --> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: "ProductSection", |
||||
|
props: { |
||||
|
// 标题 |
||||
|
title: { |
||||
|
type: String, |
||||
|
default: "商品列表", |
||||
|
}, |
||||
|
// 商品列表 |
||||
|
productList: { |
||||
|
type: Array, |
||||
|
default: () => [], |
||||
|
}, |
||||
|
// 更多按钮跳转路径 |
||||
|
moreUrl: { |
||||
|
type: String, |
||||
|
default: "", |
||||
|
}, |
||||
|
// 商品详情跳转路径前缀 |
||||
|
detailUrlPrefix: { |
||||
|
type: String, |
||||
|
default: "/subPackages/techan/detail", |
||||
|
}, |
||||
|
// 标题区域背景颜色 |
||||
|
titleBgColor: { |
||||
|
type: String, |
||||
|
default: "#9efffa", |
||||
|
}, |
||||
|
// AI标签边框颜色 |
||||
|
aiTagBorderColor: { |
||||
|
type: String, |
||||
|
default: "#01f1f196", |
||||
|
}, |
||||
|
// AI标签字体颜色 |
||||
|
aiTagTextColor: { |
||||
|
type: String, |
||||
|
default: "#02fcfc", |
||||
|
}, |
||||
|
type: { |
||||
|
type: Number, |
||||
|
default: 1, |
||||
|
}, |
||||
|
|
||||
|
color: { |
||||
|
type: String, |
||||
|
default: "#D3FCFF", |
||||
|
}, |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
// 处理更多按钮点击 |
||||
|
handleMoreClick() { |
||||
|
if (this.moreUrl) { |
||||
|
uni.navigateTo({ |
||||
|
url: this.moreUrl, |
||||
|
}); |
||||
|
} |
||||
|
this.$emit("more-click"); |
||||
|
}, |
||||
|
|
||||
|
// 处理商品点击 |
||||
|
handleProductClick(item) { |
||||
|
if (this.detailUrlPrefix) { |
||||
|
uni.navigateTo({ |
||||
|
url: `${this.detailUrlPrefix}?id=${item.id}`, |
||||
|
}); |
||||
|
} |
||||
|
this.$emit("product-click", item); |
||||
|
}, |
||||
|
|
||||
|
// 处理收藏点击 |
||||
|
handleLikeClick(item, index) { |
||||
|
// 更新本地状态 |
||||
|
this.Post({ |
||||
|
packageId: item.id, |
||||
|
type:!item.type |
||||
|
}, |
||||
|
"/framework/benefitPackage/collect",'DES' |
||||
|
).then((res) => { |
||||
|
// 显示提示 |
||||
|
const updatedItem = { ...item, type: !item.type }; |
||||
|
this.productList[index].type = !item.type |
||||
|
uni.showToast({ |
||||
|
title: updatedItem.type ? "已收藏" : "取消收藏", |
||||
|
icon: "none", |
||||
|
duration: 1500, |
||||
|
}); |
||||
|
this.$forceUpdate() |
||||
|
// 向父组件发送事件 |
||||
|
this.$emit("like-toggle", { item: updatedItem, index }); |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
}, |
||||
|
toCGCwx(){ |
||||
|
wx.navigateToMiniProgram({ |
||||
|
appId: 'wx9d68934300b1fe90', |
||||
|
path: 'pages/index/index', |
||||
|
envVersion: 'release', |
||||
|
success(res) { |
||||
|
// 打开成功 |
||||
|
}, |
||||
|
fail(e){ |
||||
|
console.log(e) |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
toAgent(e){ |
||||
|
uni.navigateTo({ |
||||
|
url: `/subPackages/other/evita?id=${e.agentId}&product=${e.benefitPackageId}` |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 显示图片 |
||||
|
showImg(img) { |
||||
|
if (!img) return; |
||||
|
|
||||
|
if (img.indexOf("https://") != -1 || img.indexOf("http://") != -1) { |
||||
|
return img; |
||||
|
} else { |
||||
|
return this.$options._base.prototype.NEWAPIURLIMG + img; |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
@font-face { |
||||
|
font-family: "Futura"; |
||||
|
src: url(https://des.dayunyuanjian.cn/epicSoul/taozi/fonts/Futura.ttc); |
||||
|
} |
||||
|
.product-section { |
||||
|
width: 100%; |
||||
|
background-color: #D3FCFF; |
||||
|
padding:40rpx 25rpx; |
||||
|
margin: 30rpx 0; |
||||
|
border-radius: 40rpx; |
||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1), |
||||
|
0 0 0 1rpx rgba(255, 255, 255, 0.2) inset; |
||||
|
} |
||||
|
|
||||
|
// 标题区域 |
||||
|
.title-section { |
||||
|
display: inline-block; |
||||
|
padding: 0rpx 0 30rpx; |
||||
|
width: 100%; |
||||
|
.title { |
||||
|
font-size: 34rpx; |
||||
|
font-weight: bold; |
||||
|
color: #000000; |
||||
|
} |
||||
|
|
||||
|
.more-btn { |
||||
|
font-size: 26rpx; |
||||
|
color: #000000; |
||||
|
margin-left: 35rpx; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 商品列表 |
||||
|
.product-list { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 24rpx; |
||||
|
} |
||||
|
|
||||
|
// 商品卡片 |
||||
|
.product-card { |
||||
|
background: #ffffff; |
||||
|
border-radius: 24rpx; |
||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1), |
||||
|
0 0 0 1rpx rgba(255, 255, 255, 0.2) inset; |
||||
|
overflow: hidden; |
||||
|
transition: transform 0.2s ease; |
||||
|
|
||||
|
&:active { |
||||
|
transform: scale(0.98); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 图片容器 |
||||
|
.card-image-container { |
||||
|
position: relative; |
||||
|
height: 320rpx; |
||||
|
overflow: hidden; |
||||
|
border-radius: 20rpx 20rpx 0 0 ; |
||||
|
} |
||||
|
|
||||
|
.card-image { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
object-fit: cover; |
||||
|
border-radius: 20rpx 20rpx 0 0 ; |
||||
|
} |
||||
|
|
||||
|
// 图片蒙层 |
||||
|
.image-overlay { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
background: #00000047; |
||||
|
pointer-events: none; |
||||
|
border-radius: 20rpx; |
||||
|
} |
||||
|
|
||||
|
// 智能体标签 |
||||
|
.content-box-info { |
||||
|
position: absolute; |
||||
|
top: 30rpx; |
||||
|
right: 20rpx; |
||||
|
padding: 0rpx 20rpx; |
||||
|
text-align: center; |
||||
|
} |
||||
|
.ai-tag { |
||||
|
padding: 8rpx 0rpx; |
||||
|
gap: 8rpx; |
||||
|
|
||||
|
} |
||||
|
.ai-label { |
||||
|
border: 1rpx solid; |
||||
|
padding: 0rpx 15rpx; |
||||
|
height: 40rpx; |
||||
|
line-height: 38rpx; |
||||
|
font-weight: bold; |
||||
|
font-size: 20rpx; |
||||
|
border-radius: 4rpx; |
||||
|
} |
||||
|
|
||||
|
.ai-name { |
||||
|
font-size: 27rpx; |
||||
|
font-weight: bold; |
||||
|
color: #ffffff; |
||||
|
margin-left: 10rpx; |
||||
|
} |
||||
|
// 头像 |
||||
|
.avatar { |
||||
|
width: 119rpx; |
||||
|
height: 119rpx; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
|
||||
|
// 卡片内容 |
||||
|
.card-content { |
||||
|
padding: 18rpx; |
||||
|
} |
||||
|
|
||||
|
.title-price-heart { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.card-title { |
||||
|
font-size: 30rpx; |
||||
|
color: #000000; |
||||
|
line-height: 1.4; |
||||
|
flex: 1; |
||||
|
margin-right: 20rpx; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.card-price { |
||||
|
font-size: 30rpx; |
||||
|
color: #000000; |
||||
|
margin-right: 24rpx; |
||||
|
flex-shrink: 0; |
||||
|
min-width: 120rpx; |
||||
|
font-family: "Futura"; |
||||
|
} |
||||
|
|
||||
|
.heart-icon { |
||||
|
width: 35rpx; |
||||
|
height: 29rpx; |
||||
|
transition: all 0.3s ease; |
||||
|
flex-shrink: 0; |
||||
|
|
||||
|
&.liked { |
||||
|
opacity: 1; |
||||
|
filter: hue-rotate(320deg) saturate(2); |
||||
|
} |
||||
|
|
||||
|
&:active { |
||||
|
transform: scale(1.2); |
||||
|
} |
||||
|
} |
||||
|
.shop-icon { |
||||
|
width: 39rpx; |
||||
|
height: 36rpx; |
||||
|
transition: all 0.3s ease; |
||||
|
flex-shrink: 0; |
||||
|
margin-left: 10rpx; |
||||
|
|
||||
|
&.liked { |
||||
|
opacity: 1; |
||||
|
filter: hue-rotate(320deg) saturate(2); |
||||
|
} |
||||
|
|
||||
|
&:active { |
||||
|
transform: scale(1.2); |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -1,293 +0,0 @@ |
|||||
# SwipeToNext 组件使用文档 |
|
||||
|
|
||||
## 组件介绍 |
|
||||
|
|
||||
`SwipeToNext` 是一个通用的触底跳转组件,封装了手势检测、延迟防抖、提示文字等功能,可以在任何需要滑动跳转的页面中使用。 |
|
||||
|
|
||||
## 组件特性 |
|
||||
|
|
||||
- ✅ 手势滑动检测 |
|
||||
- ✅ 防误触发机制(延迟允许跳转) |
|
||||
- ✅ 可配置的滑动阈值 |
|
||||
- ✅ 自定义提示文字 |
|
||||
- ✅ 支持事件监听 |
|
||||
- ✅ 完全可配置的参数 |
|
||||
|
|
||||
## 使用方法 |
|
||||
|
|
||||
### 1. 引入组件 |
|
||||
|
|
||||
```vue |
|
||||
<script> |
|
||||
import SwipeToNext from '@/components/SwipeToNext.vue'; |
|
||||
|
|
||||
export default { |
|
||||
components: { |
|
||||
SwipeToNext |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
``` |
|
||||
|
|
||||
### 2. 基础使用 |
|
||||
|
|
||||
```vue |
|
||||
<template> |
|
||||
<SwipeToNext |
|
||||
:is-last-slide="isLastSlide" |
|
||||
:target-path="'/next/page'" |
|
||||
@swipe-to-next="handleSwipeToNext" |
|
||||
> |
|
||||
<!-- 你的页面内容 --> |
|
||||
<view class="content"> |
|
||||
<!-- 轮播图或其他内容 --> |
|
||||
</view> |
|
||||
</SwipeToNext> |
|
||||
</template> |
|
||||
|
|
||||
<script> |
|
||||
export default { |
|
||||
data() { |
|
||||
return { |
|
||||
isLastSlide: false |
|
||||
} |
|
||||
}, |
|
||||
methods: { |
|
||||
// 处理swiper切换或其他逻辑 |
|
||||
handlePageChange() { |
|
||||
// 根据你的逻辑设置 isLastSlide |
|
||||
this.isLastSlide = true; // 当到达最后一页时 |
|
||||
}, |
|
||||
handleSwipeToNext(targetPath) { |
|
||||
console.log('即将跳转到:', targetPath); |
|
||||
// 可以在这里添加额外的逻辑,如数据统计 |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
``` |
|
||||
|
|
||||
### 3. 完整配置使用 |
|
||||
|
|
||||
```vue |
|
||||
<template> |
|
||||
<SwipeToNext |
|
||||
:is-last-slide="isLastSlide" |
|
||||
:target-path="'/next/page'" |
|
||||
:show-tip="true" |
|
||||
tip-text="向上滑动查看更多内容" |
|
||||
:swipe-threshold="100" |
|
||||
:delay-time="800" |
|
||||
:enable-delay="true" |
|
||||
@swipe-to-next="handleSwipeToNext" |
|
||||
> |
|
||||
<!-- 你的页面内容 --> |
|
||||
<view class="content"> |
|
||||
<!-- 内容区域 --> |
|
||||
</view> |
|
||||
</SwipeToNext> |
|
||||
</template> |
|
||||
``` |
|
||||
|
|
||||
## Props 参数 |
|
||||
|
|
||||
| 参数名 | 类型 | 默认值 | 必填 | 说明 | |
|
||||
|--------|------|--------|------|------| |
|
||||
| `isLastSlide` | Boolean | `false` | ✅ | 是否在最后一页/最后一个状态 | |
|
||||
| `targetPath` | String | - | ✅ | 跳转的目标路径 | |
|
||||
| `showTip` | Boolean | `true` | ❌ | 是否显示提示文字 | |
|
||||
| `tipText` | String | `'继续向上滑动进入下一章节'` | ❌ | 提示文字内容 | |
|
||||
| `swipeThreshold` | Number | `80` | ❌ | 滑动阈值(像素) | |
|
||||
| `delayTime` | Number | `500` | ❌ | 延迟允许跳转的时间(毫秒) | |
|
||||
| `enableDelay` | Boolean | `true` | ❌ | 是否启用延迟机制 | |
|
||||
| `alwaysEnable` | Boolean | `false` | ❌ | 是否总是启用跳转(忽略isLastSlide状态) | |
|
||||
|
|
||||
## Events 事件 |
|
||||
|
|
||||
| 事件名 | 参数 | 说明 | |
|
||||
|--------|------|------| |
|
||||
| `swipe-to-next` | `targetPath` | 触发跳转时的回调事件 | |
|
||||
|
|
||||
## 使用场景示例 |
|
||||
|
|
||||
### 场景1:图片轮播页面 |
|
||||
|
|
||||
```vue |
|
||||
<template> |
|
||||
<SwipeToNext |
|
||||
:is-last-slide="currentIndex === images.length - 1" |
|
||||
:target-path="'/gallery/next'" |
|
||||
> |
|
||||
<swiper @change="handleSwiperChange"> |
|
||||
<swiper-item v-for="(img, index) in images" :key="index"> |
|
||||
<image :src="img" mode="aspectFit" /> |
|
||||
</swiper-item> |
|
||||
</swiper> |
|
||||
</SwipeToNext> |
|
||||
</template> |
|
||||
|
|
||||
<script> |
|
||||
export default { |
|
||||
data() { |
|
||||
return { |
|
||||
currentIndex: 0, |
|
||||
images: ['img1.jpg', 'img2.jpg', 'img3.jpg'] |
|
||||
} |
|
||||
}, |
|
||||
methods: { |
|
||||
handleSwiperChange(e) { |
|
||||
this.currentIndex = e.detail.current; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
``` |
|
||||
|
|
||||
### 场景2:文章阅读页面 |
|
||||
|
|
||||
```vue |
|
||||
<template> |
|
||||
<SwipeToNext |
|
||||
:is-last-slide="isReadComplete" |
|
||||
:target-path="'/article/next'" |
|
||||
tip-text="继续滑动阅读下一篇文章" |
|
||||
:swipe-threshold="60" |
|
||||
> |
|
||||
<scroll-view @scrolltolower="handleScrollToBottom"> |
|
||||
<view class="article-content"> |
|
||||
<!-- 文章内容 --> |
|
||||
</view> |
|
||||
</scroll-view> |
|
||||
</SwipeToNext> |
|
||||
</template> |
|
||||
|
|
||||
<script> |
|
||||
export default { |
|
||||
data() { |
|
||||
return { |
|
||||
isReadComplete: false |
|
||||
} |
|
||||
}, |
|
||||
methods: { |
|
||||
handleScrollToBottom() { |
|
||||
// 滚动到底部时认为阅读完成 |
|
||||
this.isReadComplete = true; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
``` |
|
||||
|
|
||||
### 场景4:单张图片或总是启用触底跳转 |
|
||||
|
|
||||
```vue |
|
||||
<template> |
|
||||
<SwipeToNext |
|
||||
:always-enable="true" |
|
||||
:target-path="'/next/chapter'" |
|
||||
tip-text="向上滑动查看下一内容" |
|
||||
:enable-delay="false" |
|
||||
> |
|
||||
<view class="single-image"> |
|
||||
<image :src="imageUrl" mode="aspectFit" /> |
|
||||
</view> |
|
||||
</SwipeToNext> |
|
||||
</template> |
|
||||
|
|
||||
<script> |
|
||||
export default { |
|
||||
data() { |
|
||||
return { |
|
||||
imageUrl: 'single-image.jpg' |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
``` |
|
||||
|
|
||||
```vue |
|
||||
<template> |
|
||||
<SwipeToNext |
|
||||
:is-last-slide="currentStep === totalSteps - 1" |
|
||||
:target-path="'/guide/complete'" |
|
||||
tip-text="向上滑动完成引导" |
|
||||
> |
|
||||
<view class="guide-step"> |
|
||||
<view class="step-content"> |
|
||||
步骤 {{ currentStep + 1 }} / {{ totalSteps }} |
|
||||
</view> |
|
||||
<button @click="nextStep">下一步</button> |
|
||||
</view> |
|
||||
</SwipeToNext> |
|
||||
</template> |
|
||||
|
|
||||
<script> |
|
||||
export default { |
|
||||
data() { |
|
||||
return { |
|
||||
currentStep: 0, |
|
||||
totalSteps: 5 |
|
||||
} |
|
||||
}, |
|
||||
methods: { |
|
||||
nextStep() { |
|
||||
if (this.currentStep < this.totalSteps - 1) { |
|
||||
this.currentStep++; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
``` |
|
||||
|
|
||||
## 特殊情况处理 |
|
||||
|
|
||||
### 单张图片问题 |
|
||||
|
|
||||
当只有一张图片时,传统的 `isLastSlide` 逻辑不适用。这时可以使用 `alwaysEnable` 参数: |
|
||||
|
|
||||
```vue |
|
||||
<!-- 单张图片的解决方案 --> |
|
||||
<SwipeToNext |
|
||||
:always-enable="true" |
|
||||
:target-path="'/next/page'" |
|
||||
:enable-delay="false" |
|
||||
> |
|
||||
<image src="single-image.jpg" /> |
|
||||
</SwipeToNext> |
|
||||
``` |
|
||||
|
|
||||
### 参数优先级 |
|
||||
|
|
||||
当 `alwaysEnable="true"` 时: |
|
||||
- 忽略 `isLastSlide` 的值 |
|
||||
- 总是显示提示文字 |
|
||||
- 总是允许触底跳转 |
|
||||
- 建议设置 `enableDelay="false"` 以获得更好的响应速度 |
|
||||
|
|
||||
|
|
||||
|
|
||||
1. **确保正确设置 `isLastSlide`**:这是控制是否允许跳转的关键属性 |
|
||||
2. **路径格式**:`targetPath` 需要是有效的 uni-app 路由路径 |
|
||||
3. **性能考虑**:如果不需要延迟机制,可以设置 `enableDelay: false` 来提高响应速度 |
|
||||
4. **样式覆盖**:组件内的提示文字样式可以通过全局样式覆盖 |
|
||||
5. **事件监听**:建议监听 `swipe-to-next` 事件进行数据统计或其他操作 |
|
||||
|
|
||||
## 自定义样式 |
|
||||
|
|
||||
如果需要自定义提示文字的样式,可以在页面中添加: |
|
||||
|
|
||||
```scss |
|
||||
// 覆盖组件样式 |
|
||||
.swipe-to-next .bottom-tip { |
|
||||
bottom: 200rpx !important; // 调整位置 |
|
||||
background: rgba(255, 255, 255, 0.9) !important; // 改变背景色 |
|
||||
|
|
||||
text { |
|
||||
color: #333 !important; // 改变文字颜色 |
|
||||
font-size: 32rpx !important; // 改变字体大小 |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
这个组件极大地简化了触底跳转功能的实现,让你可以专注于业务逻辑而不用重复编写相同的手势检测代码。 |
|
||||
@ -0,0 +1,508 @@ |
|||||
|
<template> |
||||
|
<view class="waterfall-layout"> |
||||
|
<!-- 空状态 --> |
||||
|
<view v-if="!leftItems.length && !rightItems.length" class="empty-state"> |
||||
|
<text class="empty-title">暂无内容</text> |
||||
|
<text class="empty-desc">快来发布第一篇笔记吧~</text> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 瀑布流内容 --> |
||||
|
<view v-else class="waterfall-container"> |
||||
|
<!-- 左列 --> |
||||
|
<view class="column"> |
||||
|
<view |
||||
|
v-for="(item, index) in leftItems" |
||||
|
class="waterfall-item" |
||||
|
@click="handleItemClick(index, leftItems)" |
||||
|
> |
||||
|
<view class="image-container"> |
||||
|
<image |
||||
|
v-if="item.coverImage" |
||||
|
:src="item.coverImage && item.coverImage.split(',')[0]" |
||||
|
class="item-image" |
||||
|
mode="aspectFill" |
||||
|
/> |
||||
|
<!-- 状态蒙层 --> |
||||
|
<view |
||||
|
v-if="item.status === 0 || item.status === -1" |
||||
|
class="status-overlay" |
||||
|
> |
||||
|
<text class="status-text">{{ |
||||
|
item.status === 0 ? "待审核" : "审核不通过" |
||||
|
}}</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
<view class="item-content"> |
||||
|
<text v-if="item.title" class="item-title">{{ item.title }}</text> |
||||
|
<view class="item-footer"> |
||||
|
<view class="user-info"> |
||||
|
<image |
||||
|
:src="item.headImg" |
||||
|
class="user-avatar" |
||||
|
mode="aspectFill" |
||||
|
/> |
||||
|
<text class="username">{{ item.nickname }}</text> |
||||
|
</view> |
||||
|
<view |
||||
|
v-if="item.status !== 0 && item.status !== -1" |
||||
|
class="like-info" |
||||
|
@click.stop="handleLikeClick(item)" |
||||
|
> |
||||
|
<image |
||||
|
v-if="!item.userLiked" |
||||
|
src="https://epic.js-dyyj.com/uploads/20250728/2f3ae212c01fa3b67be81abc5723cf5c.png" |
||||
|
style="height: 30rpx; width: 35rpx" |
||||
|
></image> |
||||
|
<image |
||||
|
v-else |
||||
|
src="https://epic.js-dyyj.com/uploads/20250728/dd7ed269b24e84a2dd141da6ab980fd6.png" |
||||
|
style="height: 30rpx; width: 35rpx" |
||||
|
></image> |
||||
|
<text class="like-count">{{ item.likeCount || 0 }}</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 右列 --> |
||||
|
<view class="column"> |
||||
|
<view |
||||
|
v-for="(item, index) in rightItems" |
||||
|
class="waterfall-item" |
||||
|
@click="handleItemClick(index, rightItems)" |
||||
|
> |
||||
|
<view class="image-container"> |
||||
|
<image |
||||
|
v-if="item.coverImage" |
||||
|
:src="item.coverImage && item.coverImage.split(',')[0]" |
||||
|
class="item-image" |
||||
|
mode="aspectFill" |
||||
|
/> |
||||
|
<!-- 状态蒙层 --> |
||||
|
<view |
||||
|
v-if="item.status === 0 || item.status === -1" |
||||
|
class="status-overlay" |
||||
|
> |
||||
|
<text class="status-text">{{ |
||||
|
item.status === 0 ? "待审核" : "审核不通过" |
||||
|
}}</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
<view class="item-content"> |
||||
|
<text v-if="item.title" class="item-title">{{ item.title }}</text> |
||||
|
<view class="item-footer"> |
||||
|
<view class="user-info"> |
||||
|
<image |
||||
|
:src="item.headImg" |
||||
|
class="user-avatar" |
||||
|
mode="aspectFill" |
||||
|
/> |
||||
|
<text class="username" |
||||
|
>{{ item.nickname }}{{ item.nickname |
||||
|
}}{{ item.nickname }}</text |
||||
|
> |
||||
|
</view> |
||||
|
<view |
||||
|
v-if="item.status !== 0 && item.status !== -1" |
||||
|
class="like-info" |
||||
|
@click.stop="handleLikeClick(item)" |
||||
|
> |
||||
|
<image |
||||
|
v-if="!item.userLiked" |
||||
|
src="https://epic.js-dyyj.com/uploads/20250728/2f3ae212c01fa3b67be81abc5723cf5c.png" |
||||
|
style="height: 30rpx; width: 35rpx" |
||||
|
></image> |
||||
|
<image |
||||
|
v-else |
||||
|
src="https://epic.js-dyyj.com/uploads/20250728/dd7ed269b24e84a2dd141da6ab980fd6.png" |
||||
|
style="height: 30rpx; width: 35rpx" |
||||
|
></image> |
||||
|
<text class="like-count">{{ item.likeCount || 0 }}</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: "WaterfallLayout", |
||||
|
props: { |
||||
|
// 数据源 |
||||
|
items: { |
||||
|
type: Array, |
||||
|
default: () => [], |
||||
|
}, |
||||
|
// 列数(固定为2列) |
||||
|
columnCount: { |
||||
|
type: Number, |
||||
|
default: 2, |
||||
|
}, |
||||
|
// 列间距(rpx) |
||||
|
columnGap: { |
||||
|
type: Number, |
||||
|
default: 16, |
||||
|
}, |
||||
|
// 项目间距(rpx) |
||||
|
itemGap: { |
||||
|
type: Number, |
||||
|
default: 16, |
||||
|
}, |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
leftItems: [], |
||||
|
rightItems: [], |
||||
|
}; |
||||
|
}, |
||||
|
watch: { |
||||
|
items: { |
||||
|
handler(newItems) { |
||||
|
this.calculateLayout(newItems); |
||||
|
}, |
||||
|
immediate: true, |
||||
|
deep: true, |
||||
|
}, |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.calculateLayout(this.items); |
||||
|
}, |
||||
|
methods: { |
||||
|
// 处理点赞/取消点赞 |
||||
|
handleLikeClick(item) { |
||||
|
if (!item || !item.id) { |
||||
|
uni.showToast({ |
||||
|
title: "操作失败,笔记ID不存在", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 乐观更新UI |
||||
|
const isLiked = !item.userLiked; |
||||
|
item.userLiked = isLiked; |
||||
|
item.likeCount = isLiked |
||||
|
? (item.likeCount || 0) + 1 |
||||
|
: Math.max((item.likeCount || 0) - 1, 0); |
||||
|
|
||||
|
// 调用API |
||||
|
const url = isLiked |
||||
|
? `/framework/noteLike/add/${item.id}` |
||||
|
: `/framework/noteLike/cancel/${item.id}`; |
||||
|
|
||||
|
this.Post({}, url, "DES") |
||||
|
.then((res) => { |
||||
|
if (res.code !== 200) { |
||||
|
// 如果请求失败,回滚UI更新 |
||||
|
item.userLiked = !isLiked; |
||||
|
item.likeCount = !isLiked |
||||
|
? (item.likeCount || 0) + 1 |
||||
|
: Math.max((item.likeCount || 0) - 1, 0); |
||||
|
|
||||
|
uni.showToast({ |
||||
|
title: res.msg || "操作失败,请稍后重试", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
} else { |
||||
|
// 请求成功,发出事件通知父组件 |
||||
|
this.$emit("like-change", { |
||||
|
noteId: item.id, |
||||
|
isLiked: isLiked, |
||||
|
likeCount: item.likeCount, |
||||
|
}); |
||||
|
|
||||
|
// 同时发送全局事件,通知其他页面更新 |
||||
|
uni.$emit("note-like-change", { |
||||
|
noteId: item.id, |
||||
|
isLiked: isLiked, |
||||
|
likeCount: item.likeCount, |
||||
|
}); |
||||
|
} |
||||
|
}) |
||||
|
.catch((err) => { |
||||
|
// 请求失败,回滚UI更新 |
||||
|
item.userLiked = !isLiked; |
||||
|
item.likeCount = !isLiked |
||||
|
? (item.likeCount || 0) + 1 |
||||
|
: Math.max((item.likeCount || 0) - 1, 0); |
||||
|
|
||||
|
uni.showToast({ |
||||
|
title: err.msg || "网络异常,请稍后重试", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
}); |
||||
|
}, |
||||
|
// 获取列的实际高度(通过DOM查询) |
||||
|
getColumnHeight(columnRef) { |
||||
|
if (!columnRef) return 0; |
||||
|
const query = uni.createSelectorQuery().in(this); |
||||
|
return new Promise((resolve) => { |
||||
|
query |
||||
|
.select(columnRef) |
||||
|
.boundingClientRect((data) => { |
||||
|
resolve(data ? data.height : 0); |
||||
|
}) |
||||
|
.exec(); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 计算布局 |
||||
|
calculateLayout(items) { |
||||
|
if (!items || !items.length) { |
||||
|
this.leftItems = []; |
||||
|
this.rightItems = []; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 清空现有数据 |
||||
|
this.leftItems = []; |
||||
|
this.rightItems = []; |
||||
|
|
||||
|
// 逐个添加项目 |
||||
|
for (let i = 0; i < items.length; i++) { |
||||
|
this.addItem(items[i]); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 添加单个项目到合适的列 |
||||
|
addItem(item) { |
||||
|
// 简单的交替分配逻辑:比较两列的项目数量 |
||||
|
if (this.leftItems.length <= this.rightItems.length) { |
||||
|
this.leftItems.push(item); |
||||
|
} else { |
||||
|
this.rightItems.push(item); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 清空所有项目 |
||||
|
clearItems() { |
||||
|
this.leftItems = []; |
||||
|
this.rightItems = []; |
||||
|
this.$emit("items-cleared"); |
||||
|
}, |
||||
|
|
||||
|
// 处理项目点击 |
||||
|
handleItemClick(index, list) { |
||||
|
this.$emit("item-click", list[index]); |
||||
|
}, |
||||
|
|
||||
|
// 获取所有项目 |
||||
|
getAllItems() { |
||||
|
return [...this.leftItems, ...this.rightItems]; |
||||
|
}, |
||||
|
|
||||
|
// 移除项目 |
||||
|
removeItem(itemId) { |
||||
|
// 从左列移除 |
||||
|
let index = this.leftItems.findIndex((item) => item.id === itemId); |
||||
|
if (index !== -1) { |
||||
|
this.leftItems.splice(index, 1); |
||||
|
this.$emit("item-removed", itemId); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 从右列移除 |
||||
|
index = this.rightItems.findIndex((item) => item.id === itemId); |
||||
|
if (index !== -1) { |
||||
|
this.rightItems.splice(index, 1); |
||||
|
this.$emit("item-removed", itemId); |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.waterfall-layout { |
||||
|
width: 100%; |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
/* 空状态样式 */ |
||||
|
.empty-state { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 160rpx 40rpx; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.empty-icon { |
||||
|
width: 240rpx; |
||||
|
height: 240rpx; |
||||
|
margin-bottom: 40rpx; |
||||
|
opacity: 0.6; |
||||
|
} |
||||
|
|
||||
|
.empty-title { |
||||
|
font-size: 32rpx; |
||||
|
color: #666; |
||||
|
margin-bottom: 16rpx; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.empty-desc { |
||||
|
font-size: 28rpx; |
||||
|
color: #999; |
||||
|
line-height: 1.4; |
||||
|
} |
||||
|
|
||||
|
.waterfall-container { |
||||
|
display: flex; |
||||
|
gap: 16rpx; |
||||
|
padding: 0 20rpx; |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
.column { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 16rpx; |
||||
|
} |
||||
|
|
||||
|
.waterfall-item { |
||||
|
box-sizing: border-box; |
||||
|
border-radius: 12rpx; |
||||
|
background: #fff; |
||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); |
||||
|
overflow: hidden; |
||||
|
transition: transform 0.2s ease; |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
.waterfall-item:active { |
||||
|
transform: scale(0.98); |
||||
|
} |
||||
|
|
||||
|
.item-image { |
||||
|
width: 100%; |
||||
|
height: 476rpx; |
||||
|
object-fit: cover; |
||||
|
} |
||||
|
|
||||
|
.item-content { |
||||
|
padding: 16rpx; |
||||
|
} |
||||
|
|
||||
|
.item-title { |
||||
|
font-size: 28rpx; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
line-height: 1.3; |
||||
|
margin-bottom: 12rpx; |
||||
|
display: -webkit-box; |
||||
|
-webkit-box-orient: vertical; |
||||
|
-webkit-line-clamp: 2; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
} |
||||
|
|
||||
|
.item-desc { |
||||
|
font-size: 24rpx; |
||||
|
color: #666; |
||||
|
line-height: 1.4; |
||||
|
margin-bottom: 16rpx; |
||||
|
display: -webkit-box; |
||||
|
-webkit-box-orient: vertical; |
||||
|
-webkit-line-clamp: 2; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
} |
||||
|
|
||||
|
.item-tags { |
||||
|
display: flex; |
||||
|
flex-wrap: wrap; |
||||
|
gap: 8rpx; |
||||
|
margin-bottom: 16rpx; |
||||
|
} |
||||
|
|
||||
|
.tag { |
||||
|
padding: 4rpx 12rpx; |
||||
|
background: #f5f5f5; |
||||
|
color: #666; |
||||
|
font-size: 20rpx; |
||||
|
border-radius: 12rpx; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.item-footer { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-top: 16rpx; |
||||
|
} |
||||
|
|
||||
|
.user-info { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 12rpx; |
||||
|
} |
||||
|
|
||||
|
.user-avatar { |
||||
|
width: 32rpx; |
||||
|
height: 32rpx; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
|
||||
|
.username { |
||||
|
font-size: 22rpx; |
||||
|
color: #666; |
||||
|
width: 160rpx; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.like-info { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 6rpx; |
||||
|
padding: 4rpx 8rpx; |
||||
|
} |
||||
|
|
||||
|
.like-icon { |
||||
|
font-size: 24rpx; |
||||
|
color: #ff6b6b; |
||||
|
} |
||||
|
|
||||
|
.like-count { |
||||
|
font-size: 22rpx; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.image-container { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
overflow: hidden; |
||||
|
height: 476rpx; |
||||
|
} |
||||
|
|
||||
|
/* 状态蒙层样式 */ |
||||
|
.status-overlay { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
background-color: rgba(0, 0, 0, 0.6); |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
z-index: 10; |
||||
|
} |
||||
|
|
||||
|
.status-text { |
||||
|
color: #ffffff; |
||||
|
font-size: 28rpx; |
||||
|
font-weight: 500; |
||||
|
padding: 10rpx 20rpx; |
||||
|
background-color: rgba(0, 0, 0, 0.5); |
||||
|
border-radius: 8rpx; |
||||
|
} |
||||
|
</style> |
||||
@ -1,56 +1,306 @@ |
|||||
<template> |
<template> |
||||
<view class="header" :style="{'height': height+'px','padding-top':statusBarHeight+'px'}"> |
<view> |
||||
<image src="https://static.ticket.sz-trip.com/epicSoul/readingBody/search.png" mode="heightFix" |
<!-- 占位区域,防止内容塌陷 --> |
||||
@click="gotoPath('/subPackages/search/search?type='+type)" v-if="isSearch"></image> |
<view v-if="fixed" class="header-placeholder" :style="{ height: height + 'px' }"></view> |
||||
<view style="width: 50rpx;" v-else></view> |
|
||||
<image src="https://static.ticket.sz-trip.com/uploads/20250625/9bb05097e07570a934235983e1681a9f.png" mode="heightFix"></image> |
<view |
||||
<view style="width: 50rpx;"></view> |
class="header" |
||||
</view> |
:class="{ 'header-fixed': fixed }" |
||||
|
:style="{ height: height + 'px', 'padding-top': statusBarHeight + 'px' }" |
||||
|
> |
||||
|
|
||||
|
<!-- 左侧:地区筛选和搜索 --> |
||||
|
<view class="left-section" v-if="isSearch"> |
||||
|
<!-- 使用省市区选择组件 --> |
||||
|
<!-- 原有的简单地区选择 --> |
||||
|
<view v-if="isLocation&&address.cityId "> |
||||
|
<AreaPicker |
||||
|
:defaultValue="{ |
||||
|
provinceId: address.provinceId, |
||||
|
cityId: address.cityId, |
||||
|
areaId:address.areaId |
||||
|
}" |
||||
|
ref="areaPicker3" |
||||
|
placeholder="请选择省市区" |
||||
|
:selectedText="selectedText" |
||||
|
@change="changeAddress" |
||||
|
> |
||||
|
<template v-slot="{ selectedText, placeholder, currentSelection }"> |
||||
|
<view class="location-selector"> |
||||
|
|
||||
|
<text class="location-text">{{ selectedText }}</text> |
||||
|
<image |
||||
|
class="dropdown-icon" |
||||
|
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOCIgdmlld0JveD0iMCAwIDEyIDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xIDFMNiA2TDExIDEiIHN0cm9rZT0iIzk5OTk5OSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+" |
||||
|
mode="heightFix" |
||||
|
></image> |
||||
|
</view> |
||||
|
</template> |
||||
|
</AreaPicker> |
||||
|
|
||||
|
</view> |
||||
|
|
||||
|
<!-- <image |
||||
|
class="search-icon" |
||||
|
src="https://des.dayunyuanjian.cn/epicSoul/readingBody/search.png" |
||||
|
mode="heightFix" |
||||
|
@click="gotoPath('/subPackages/search/search?type=' + type)" |
||||
|
></image> --> |
||||
|
<uni-icons @click="handleBack" v-if="isBack" type="left" size="28"></uni-icons> |
||||
|
</view> |
||||
|
<view class="left-section" v-else></view> |
||||
|
|
||||
|
<!-- 中间:Logo --> |
||||
|
<view class="logo"> |
||||
|
<image |
||||
|
class="" |
||||
|
:src="showImg('/uploads/20250910/fff8aa38c1338a9bfceaf33763e9f4c6.png')" |
||||
|
mode="heightFix" |
||||
|
></image> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 右侧:占位 --> |
||||
|
<view class="right-section"></view> |
||||
|
</view> |
||||
|
</view> |
||||
</template> |
</template> |
||||
|
|
||||
<script> |
<script> |
||||
export default { |
import AreaPicker from './AreaPicker.vue' |
||||
props: { |
|
||||
isSearch: { |
export default { |
||||
type: Boolean, |
components: { |
||||
default: true |
AreaPicker |
||||
}, |
}, |
||||
type: { |
props: { |
||||
type: String, |
isSearch: { |
||||
default: '' |
type: Boolean, |
||||
} |
default: true, |
||||
}, |
}, |
||||
name:"header", |
isBack: { |
||||
data() { |
type: Boolean, |
||||
return { |
default: true, |
||||
// 导航栏参数 |
}, |
||||
height: 0, |
address: { |
||||
statusBarHeight: 0, |
type: Object, |
||||
}; |
default: () =>{}, |
||||
}, |
}, |
||||
mounted() { |
type: { |
||||
this.initRectInfo() |
type: String, |
||||
}, |
default: "", |
||||
methods:{ |
}, |
||||
initRectInfo () { |
fixed: { |
||||
const sysInfo = uni.getSystemInfoSync() |
type: Boolean, |
||||
this.statusBarHeight = sysInfo.statusBarHeight |
default: false, |
||||
// 默认高度 |
}, |
||||
this.height = sysInfo.statusBarHeight + 40 |
isLocation: { |
||||
console.log("sysInfo" ,sysInfo) |
type: Boolean, |
||||
}, |
default: false, |
||||
} |
}, |
||||
} |
selectedText: { |
||||
|
type: String, |
||||
|
default: '' |
||||
|
}, |
||||
|
// 新增:是否使用省市区选择组件 |
||||
|
isAreaPicker: { |
||||
|
type: Boolean, |
||||
|
default: true, |
||||
|
}, |
||||
|
// 省市区选择器占位符 |
||||
|
areaPlaceholder: { |
||||
|
type: String, |
||||
|
default: '请选择地区' |
||||
|
}, |
||||
|
// 省市区默认值 |
||||
|
defaultAreaValue: { |
||||
|
type: Object, |
||||
|
default: () => ({ |
||||
|
provinceId: null, |
||||
|
cityId: null, |
||||
|
areaId: null |
||||
|
}) |
||||
|
} |
||||
|
}, |
||||
|
name: "header", |
||||
|
data() { |
||||
|
return { |
||||
|
// 导航栏参数 |
||||
|
height: 0, |
||||
|
statusBarHeight: 0, |
||||
|
// 地区选择 |
||||
|
areaPickerVisible: false, |
||||
|
selectedAreaText: '' |
||||
|
}; |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.initRectInfo(); |
||||
|
}, |
||||
|
methods: { |
||||
|
handleBack(){ |
||||
|
uni.switchTab({ |
||||
|
url:'/pages/index/index' |
||||
|
}) |
||||
|
}, |
||||
|
initRectInfo() { |
||||
|
const sysInfo = uni.getSystemInfoSync(); |
||||
|
this.statusBarHeight = sysInfo.statusBarHeight; |
||||
|
// 默认高度 |
||||
|
this.height = sysInfo.statusBarHeight + 40; |
||||
|
console.log("sysInfo", sysInfo); |
||||
|
}, |
||||
|
showLocationPicker() { |
||||
|
// 显示地区选择器 |
||||
|
uni.showActionSheet({ |
||||
|
itemList: ["苏州", "上海", "杭州", "南京", "无锡"], |
||||
|
success: (res) => { |
||||
|
const locations = ["苏州", "上海", "杭州", "南京", "无锡"]; |
||||
|
this.selectedText = locations[res.tapIndex]; |
||||
|
// 触发地区变更事件 |
||||
|
this.$emit("locationChange", this.selectedText); |
||||
|
}, |
||||
|
}); |
||||
|
}, |
||||
|
changeAddress(e){ |
||||
|
this.$emit('change',e) |
||||
|
}, |
||||
|
// 显示省市区选择弹窗 |
||||
|
showAreaPicker() { |
||||
|
this.areaPickerVisible = true; |
||||
|
}, |
||||
|
// 省市区选择变化事件 |
||||
|
onAreaChange(areaData) { |
||||
|
this.selectedAreaText = areaData.fullText; |
||||
|
// 触发省市区变更事件,传递完整的地区数据 |
||||
|
this.$emit('areaChange', areaData); |
||||
|
}, |
||||
|
// 获取当前选中的省市区数据 |
||||
|
getAreaValue() { |
||||
|
if (this.$refs.areaPicker) { |
||||
|
return this.$refs.areaPicker.getValue(); |
||||
|
} |
||||
|
return null; |
||||
|
}, |
||||
|
// 重置省市区选择 |
||||
|
resetArea() { |
||||
|
this.selectedAreaText = ''; |
||||
|
if (this.$refs.areaPicker) { |
||||
|
this.$refs.areaPicker.reset(); |
||||
|
} |
||||
|
}, |
||||
|
// 为了兼容 AreaPicker 组件中的数据请求,添加 Post 方法 |
||||
|
Post(data, url) { |
||||
|
// 这里需要根据项目的实际请求方法来实现 |
||||
|
// 可以调用全局的请求方法或者通过 mixin 引入 |
||||
|
return this.$parent.Post ? this.$parent.Post(data, url) : Promise.reject('Post method not found'); |
||||
|
} |
||||
|
}, |
||||
|
}; |
||||
</script> |
</script> |
||||
|
|
||||
<style scoped lang="scss"> |
<style scoped lang="scss"> |
||||
.header{ |
.header { |
||||
flex-shrink: 0; |
flex-shrink: 0; |
||||
display: flex; |
display: flex; |
||||
align-items: center; |
align-items: center; |
||||
justify-content: space-around; |
justify-content: space-between; |
||||
image{ |
padding: 0 20rpx; |
||||
height: 46.16rpx; |
background-color: #fff; |
||||
} |
|
||||
} |
&.header-fixed { |
||||
</style> |
position: fixed; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
z-index: 999; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.header-placeholder { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.left-section { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
flex: 1; |
||||
|
|
||||
|
.area-picker-wrapper { |
||||
|
margin-right: 20rpx; |
||||
|
min-width: 200rpx; |
||||
|
max-width: 300rpx; |
||||
|
} |
||||
|
|
||||
|
.area-display { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
padding: 8rpx 16rpx; |
||||
|
border: 2rpx solid #e0e0e0; |
||||
|
border-radius: 8rpx; |
||||
|
background-color: #fff; |
||||
|
min-height: 60rpx; |
||||
|
} |
||||
|
|
||||
|
.area-text { |
||||
|
font-size: 30rpx; |
||||
|
color: #333; |
||||
|
flex: 1; |
||||
|
|
||||
|
&.placeholder { |
||||
|
color: #999; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.dropdown-icon { |
||||
|
width: 24rpx; |
||||
|
height: 16rpx; |
||||
|
margin-left: 8rpx; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
.search-icon { |
||||
|
height: 36.16rpx; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.logo { |
||||
|
position: absolute; |
||||
|
left: 50%; |
||||
|
transform: translateX(-50%); |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
color: #000000; |
||||
|
font-weight: bold; |
||||
|
font-size: 35rpx; |
||||
|
image{ |
||||
|
height: 85rpx; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.right-section { |
||||
|
flex: 1; |
||||
|
} |
||||
|
.location-selector { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
padding: 8rpx 0rpx; |
||||
|
// border: 2rpx solid #e0e0e0; |
||||
|
border-radius: 20rpx; |
||||
|
background-color: #fff; |
||||
|
min-width: 100rpx; |
||||
|
margin-right: 20rpx; |
||||
|
|
||||
|
.location-text { |
||||
|
font-size: 30rpx; |
||||
|
color: #333; |
||||
|
margin-right: 8rpx; |
||||
|
} |
||||
|
|
||||
|
.dropdown-icon { |
||||
|
width: 24rpx; |
||||
|
height: 16rpx; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
|
|||||
@ -0,0 +1,52 @@ |
|||||
|
<template> |
||||
|
<text :class="[type,'iconfont-' + name,{'nmr-linear':islinear}]" :style="{'background-image':islinear?linearStyle:'',color: color,'line-height':size + 'rpx', 'font-size': size + 'rpx' }" |
||||
|
@click="_onClick" /> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
// 组件使用方法 |
||||
|
// <nmr-icon name="renwu" size="30" color="#ff0000"></nmr-icon> |
||||
|
export default { |
||||
|
name: 'UniIcon', |
||||
|
props: { |
||||
|
type: { |
||||
|
type: String, |
||||
|
default: 'des' |
||||
|
}, |
||||
|
islinear: {//是否渐变图标 |
||||
|
type: Boolean, |
||||
|
default: false, |
||||
|
}, |
||||
|
linearStyle: {//渐变样式 |
||||
|
type: String, |
||||
|
default: 'linear-gradient(180deg, #64BDE7 0%, #A7D9F3 100%)' |
||||
|
}, |
||||
|
name: { |
||||
|
type: String, |
||||
|
default: '' |
||||
|
}, |
||||
|
color: { |
||||
|
type: String, |
||||
|
default: '#333333' |
||||
|
}, |
||||
|
|
||||
|
size: { |
||||
|
type: [Number, String], |
||||
|
default: 32 |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
_onClick() { |
||||
|
this.$emit('click') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
<style> |
||||
|
.nmr-linear{ |
||||
|
-webkit-background-clip: text; |
||||
|
background-clip: text; |
||||
|
color: transparent !important; |
||||
|
background-image: linear-gradient(180deg, #64BDE7 0%, #A7D9F3 100%); |
||||
|
} |
||||
|
</style> |
||||
@ -1,225 +0,0 @@ |
|||||
# 跨页面音频控制解决方案 |
|
||||
|
|
||||
## 进一步优化:解决跨页面状态同步问题 |
|
||||
|
|
||||
### 问题描述 |
|
||||
在跨页面场景下,当音频正在播放时跳转到新页面,新页面的MusicControl组件不知道有音频在播放,点击背景音乐按钮时会直接播放背景音乐,导致音频和背景音乐同时播放。 |
|
||||
|
|
||||
### 解决方案 |
|
||||
|
|
||||
#### 1. MusicControl组件增强检测 |
|
||||
```javascript |
|
||||
// 在mounted生命周期中添加全局音频状态检测 |
|
||||
mounted() { |
|
||||
this.syncMusicState(); |
|
||||
this.checkGlobalAudioState(); // 新增:检查全局音频状态 |
|
||||
|
|
||||
// 定时器也要检查全局音频状态 |
|
||||
this.timer = setInterval(() => { |
|
||||
this.syncMusicState(); |
|
||||
this.checkGlobalAudioState(); |
|
||||
}, 1000); |
|
||||
} |
|
||||
|
|
||||
// 新增方法:检查全局音频状态 |
|
||||
checkGlobalAudioState() { |
|
||||
const app = getApp(); |
|
||||
if (app && app.globalData && app.globalData.currentAudio) { |
|
||||
const globalAudio = app.globalData.currentAudio; |
|
||||
this.isAudioPlaying = !globalAudio.paused; |
|
||||
} else { |
|
||||
this.isAudioPlaying = false; |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
#### 2. 全局音频管理工具优化 |
|
||||
```javascript |
|
||||
// 在音频状态变化时发送全局事件 |
|
||||
pauseCurrentAudio() { |
|
||||
const audio = this.getCurrentAudio(); |
|
||||
if (audio && !audio.paused) { |
|
||||
audio.pause(); |
|
||||
this.notifyAudioStateChange(false); // 通知状态变化 |
|
||||
return true; |
|
||||
} |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
// 新增:通知音频状态变化 |
|
||||
notifyAudioStateChange(isPlaying) { |
|
||||
if (typeof uni !== 'undefined') { |
|
||||
uni.$emit('audioPlaying', isPlaying); |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
### 修复后的交互流程 |
|
||||
```mermaid |
|
||||
graph TD |
|
||||
A[跨页面跳转] --> B[MusicControl组件加载] |
|
||||
B --> C[检查全局音频状态] |
|
||||
C --> D{有音频在播放?} |
|
||||
D -->|是| E[设置isAudioPlaying=true] |
|
||||
D -->|否| F[设置isAudioPlaying=false] |
|
||||
E --> G[点击背景音乐按钮] |
|
||||
F --> G |
|
||||
G --> H{检查isAudioPlaying} |
|
||||
H -->|有音频| I[先暂停音频再播放背景音乐] |
|
||||
H -->|无音频| J[直接播放背景音乐] |
|
||||
``` |
|
||||
|
|
||||
## 问题描述 |
|
||||
|
|
||||
AudioControl组件在页面跳转时会出现以下问题: |
|
||||
1. 组件状态重置,图标显示不正确 |
|
||||
2. 音频实例丢失连接,但音频可能仍在播放 |
|
||||
3. 无法在其他页面控制正在播放的音频 |
|
||||
|
|
||||
## 解决方案 |
|
||||
|
|
||||
### 1. 全局音频实例管理 |
|
||||
|
|
||||
在`App.vue`的`globalData`中添加`currentAudio`属性,用于保存当前的音频实例: |
|
||||
|
|
||||
```javascript |
|
||||
globalData: { |
|
||||
// ... 其他属性 |
|
||||
currentAudio: null // 全局音频实例 |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
### 2. AudioControl组件优化 |
|
||||
|
|
||||
#### 状态同步机制 |
|
||||
- 组件挂载时检查全局音频状态 |
|
||||
- 复用已存在的音频实例(如果URL匹配) |
|
||||
- 组件销毁时不销毁全局音频实例 |
|
||||
|
|
||||
#### 核心方法改进 |
|
||||
```javascript |
|
||||
// 检查全局音频状态 |
|
||||
checkGlobalAudioState() { |
|
||||
const app = getApp(); |
|
||||
if (app && app.globalData && app.globalData.currentAudio) { |
|
||||
const globalAudio = app.globalData.currentAudio; |
|
||||
if (globalAudio.src === this.audioSrc) { |
|
||||
this.isAudioPlaying = !globalAudio.paused; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 初始化音频时复用全局实例 |
|
||||
initAudio() { |
|
||||
const app = getApp(); |
|
||||
if (app && app.globalData && app.globalData.currentAudio) { |
|
||||
if (app.globalData.currentAudio.src === this.audioSrc) { |
|
||||
// 复用现有实例 |
|
||||
this.audioContext = app.globalData.currentAudio; |
|
||||
this.isAudioPlaying = !this.audioContext.paused; |
|
||||
return; |
|
||||
} |
|
||||
} |
|
||||
// 创建新实例... |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
### 3. 全局音频管理工具 |
|
||||
|
|
||||
创建了`utils/globalAudioManager.js`工具类,提供统一的音频控制接口: |
|
||||
|
|
||||
```javascript |
|
||||
// 在任何页面或组件中使用 |
|
||||
uni.$globalAudio.pauseCurrentAudio(); // 暂停当前音频 |
|
||||
uni.$globalAudio.playCurrentAudio(); // 播放当前音频 |
|
||||
uni.$globalAudio.isAudioPlaying(); // 检查播放状态 |
|
||||
uni.$globalAudio.getCurrentAudioSrc(); // 获取当前音频源 |
|
||||
``` |
|
||||
|
|
||||
## 使用示例 |
|
||||
|
|
||||
### 在页面中控制音频 |
|
||||
|
|
||||
```vue |
|
||||
<template> |
|
||||
<view> |
|
||||
<!-- 音频控制组件 --> |
|
||||
<AudioControl :audioSrc="audioUrl" /> |
|
||||
|
|
||||
<!-- 手动控制按钮 --> |
|
||||
<button @click="toggleAudio">切换音频</button> |
|
||||
<button @click="stopAudio">停止音频</button> |
|
||||
</view> |
|
||||
</template> |
|
||||
|
|
||||
<script> |
|
||||
import AudioControl from '@/components/AudioControl.vue'; |
|
||||
|
|
||||
export default { |
|
||||
components: { AudioControl }, |
|
||||
data() { |
|
||||
return { |
|
||||
audioUrl: this.showImg('/uploads/audio/chapter1.mp3') |
|
||||
} |
|
||||
}, |
|
||||
methods: { |
|
||||
toggleAudio() { |
|
||||
if (uni.$globalAudio.isAudioPlaying()) { |
|
||||
uni.$globalAudio.pauseCurrentAudio(); |
|
||||
} else { |
|
||||
uni.$globalAudio.playCurrentAudio(); |
|
||||
} |
|
||||
}, |
|
||||
|
|
||||
stopAudio() { |
|
||||
uni.$globalAudio.stopCurrentAudio(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
``` |
|
||||
|
|
||||
### 在其他页面检查音频状态 |
|
||||
|
|
||||
```javascript |
|
||||
// 在任何页面的onShow生命周期中 |
|
||||
onShow() { |
|
||||
// 检查是否有音频在播放 |
|
||||
if (uni.$globalAudio.isAudioPlaying()) { |
|
||||
console.log('有音频正在播放:', uni.$globalAudio.getCurrentAudioSrc()); |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
## 技术优势 |
|
||||
|
|
||||
### ✅ **状态持久化** |
|
||||
- 音频实例在页面跳转时不会丢失 |
|
||||
- 组件状态能够正确同步全局音频状态 |
|
||||
|
|
||||
### ✅ **跨页面控制** |
|
||||
- 在任何页面都可以控制当前播放的音频 |
|
||||
- 提供统一的音频管理接口 |
|
||||
|
|
||||
### ✅ **资源优化** |
|
||||
- 避免创建多个音频实例 |
|
||||
- 自动清理无用的音频资源 |
|
||||
|
|
||||
### ✅ **用户体验** |
|
||||
- 页面跳转时音频播放不中断 |
|
||||
- 图标状态显示正确 |
|
||||
- 音频控制逻辑一致 |
|
||||
|
|
||||
## 注意事项 |
|
||||
|
|
||||
1. **页面生命周期**:音频实例与页面生命周期解耦,需要手动管理 |
|
||||
2. **内存管理**:确保在应用退出时正确清理音频资源 |
|
||||
3. **状态同步**:多个AudioControl组件需要监听相同的全局状态 |
|
||||
4. **错误处理**:增强错误处理机制,确保音频异常时的状态恢复 |
|
||||
|
|
||||
## 兼容性 |
|
||||
|
|
||||
- ✅ uni-app |
|
||||
- ✅ 小程序环境 |
|
||||
- ✅ H5环境 |
|
||||
- ✅ APP环境 |
|
||||
@ -1,144 +0,0 @@ |
|||||
# 音频与背景音乐交互功能说明 |
|
||||
|
|
||||
## Bug修复记录 |
|
||||
|
|
||||
### 问题描述 |
|
||||
当音频播放时点击背景音乐按钮,背景音乐暂停音频并开始播放。此时再点击关闭背景音乐,图标显示关闭状态但背景音乐仍在播放。 |
|
||||
|
|
||||
### 问题原因 |
|
||||
1. 点击背景音乐按钮时会发送事件暂停音频 |
|
||||
2. 音频暂停时会调用restoreBackgroundMusic恢复背景音乐 |
|
||||
3. 然后MusicControl再执行自己的切换逻辑 |
|
||||
4. 导致背景音乐被恢复后又被操作,状态混乱 |
|
||||
|
|
||||
### 解决方案 |
|
||||
1. **MusicControl组件优化**: |
|
||||
- 检测是否有音频在播放 |
|
||||
- 如有音频,先暂停音频,延迟执行背景音乐切换 |
|
||||
- 如无音频,直接切换背景音乐状态 |
|
||||
|
|
||||
2. **AudioControl组件优化**: |
|
||||
- 在handleBackgroundMusicToggle中只暂停音频 |
|
||||
- 不自动恢复背景音乐,让MusicControl自己控制 |
|
||||
|
|
||||
### 修复后的交互流程 |
|
||||
```mermaid |
|
||||
graph TD |
|
||||
A[点击背景音乐按钮] --> B{是否有音频播放?} |
|
||||
B -->|是| C[发送暂停音频事件] |
|
||||
C --> D[AudioControl暂停音频\n不恢复背景音乐] |
|
||||
D --> E[延迟100ms后切换背景音乐] |
|
||||
B -->|否| F[直接切换背景音乐状态] |
|
||||
``` |
|
||||
|
|
||||
## 实现的功能 |
|
||||
|
|
||||
### 🎵 **背景音乐控制音频** |
|
||||
当点击背景音乐控制按钮时: |
|
||||
- 如果有音频正在播放,会自动暂停音频 |
|
||||
- 然后正常切换背景音乐的播放/暂停状态 |
|
||||
|
|
||||
### 🎧 **音频控制背景音乐** |
|
||||
当点击音频控制按钮时: |
|
||||
- 播放音频时自动暂停背景音乐 |
|
||||
- 暂停音频时自动恢复背景音乐 |
|
||||
- 音频播放结束时自动恢复背景音乐 |
|
||||
|
|
||||
## 技术实现 |
|
||||
|
|
||||
### 事件通信机制 |
|
||||
使用uni-app的全局事件机制实现组件间通信: |
|
||||
|
|
||||
```javascript |
|
||||
// AudioControl组件发送事件 |
|
||||
uni.$emit('audioPlaying', true/false); |
|
||||
|
|
||||
// MusicControl组件发送事件 |
|
||||
uni.$emit('backgroundMusicToggle'); |
|
||||
|
|
||||
// 组件监听事件 |
|
||||
uni.$on('eventName', this.handlerFunction); |
|
||||
``` |
|
||||
|
|
||||
### 交互流程 |
|
||||
|
|
||||
#### 点击背景音乐按钮: |
|
||||
```mermaid |
|
||||
graph TD |
|
||||
A[点击背景音乐按钮] --> B[发送backgroundMusicToggle事件] |
|
||||
B --> C[AudioControl收到事件] |
|
||||
C --> D{音频是否在播放?} |
|
||||
D -->|是| E[暂停音频] |
|
||||
D -->|否| F[继续背景音乐操作] |
|
||||
E --> G[恢复背景音乐] |
|
||||
F --> H[切换背景音乐状态] |
|
||||
``` |
|
||||
|
|
||||
#### 点击音频按钮: |
|
||||
```mermaid |
|
||||
graph TD |
|
||||
A[点击音频按钮] --> B{当前音频状态?} |
|
||||
B -->|未播放| C[暂停背景音乐] |
|
||||
C --> D[播放音频] |
|
||||
D --> E[发送audioPlaying:true事件] |
|
||||
B -->|正在播放| F[暂停音频] |
|
||||
F --> G[恢复背景音乐] |
|
||||
G --> H[发送audioPlaying:false事件] |
|
||||
``` |
|
||||
|
|
||||
## 使用方法 |
|
||||
|
|
||||
在页面中同时使用两个组件: |
|
||||
|
|
||||
```vue |
|
||||
<template> |
|
||||
<view class="page-container"> |
|
||||
<!-- 页面内容 --> |
|
||||
|
|
||||
<!-- 音频控制组件 --> |
|
||||
<AudioControl :audioSrc="audioUrl" /> |
|
||||
|
|
||||
<!-- 背景音乐控制组件 --> |
|
||||
<MusicControl /> |
|
||||
</view> |
|
||||
</template> |
|
||||
|
|
||||
<script> |
|
||||
import AudioControl from '@/components/AudioControl.vue'; |
|
||||
import MusicControl from '@/components/MusicControl.vue'; |
|
||||
|
|
||||
export default { |
|
||||
components: { |
|
||||
AudioControl, |
|
||||
MusicControl |
|
||||
}, |
|
||||
data() { |
|
||||
return { |
|
||||
audioUrl: this.showImg('/uploads/audio/your-audio.mp3') |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
``` |
|
||||
|
|
||||
## 优势特点 |
|
||||
|
|
||||
### ✅ **智能交互** |
|
||||
- 两个组件能够智能感知对方的状态 |
|
||||
- 避免同时播放音频和背景音乐造成的冲突 |
|
||||
- 提供良好的用户体验 |
|
||||
|
|
||||
### ✅ **解耦设计** |
|
||||
- 组件间通过事件通信,保持松耦合 |
|
||||
- 每个组件都能独立工作 |
|
||||
- 易于维护和扩展 |
|
||||
|
|
||||
### ✅ **状态同步** |
|
||||
- 实时同步音频和背景音乐的播放状态 |
|
||||
- 确保状态的一致性和准确性 |
|
||||
|
|
||||
## 注意事项 |
|
||||
|
|
||||
1. **事件监听清理**:组件销毁时会自动清理事件监听,避免内存泄漏 |
|
||||
2. **状态管理**:两个组件都维护各自的状态,通过事件保持同步 |
|
||||
3. **错误处理**:包含完善的错误处理机制,确保功能稳定运行 |
|
||||
File diff suppressed because it is too large
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
@ -0,0 +1,47 @@ |
|||||
|
<template> |
||||
|
<view class="content"> |
||||
|
<GPT :agentId="agentId"></GPT> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import GPT from '@/components/GPT/index.vue' |
||||
|
export default { |
||||
|
components: { |
||||
|
GPT, |
||||
|
}, |
||||
|
|
||||
|
data() { |
||||
|
return { |
||||
|
agentId:'' |
||||
|
} |
||||
|
}, |
||||
|
onLoad(option) { |
||||
|
if(option){ |
||||
|
this.agentId = option.id |
||||
|
} |
||||
|
wx.hideShareMenu() |
||||
|
const app = getApp(); |
||||
|
const bgMusic = app.globalData.bgMusic; |
||||
|
bgMusic.pause(); |
||||
|
|
||||
|
}, |
||||
|
onLaunch: options => { |
||||
|
this.options = options |
||||
|
}, |
||||
|
onShow() { |
||||
|
|
||||
|
console.log('【init message connect type------>】', ); |
||||
|
}, |
||||
|
methods: { |
||||
|
}, |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped lang="scss"> |
||||
|
.content { |
||||
|
height: 100vh; |
||||
|
} |
||||
|
|
||||
|
</style> |
||||
|
|
||||
@ -0,0 +1,748 @@ |
|||||
|
<template> |
||||
|
<view class="follow-tab-container"> |
||||
|
<!-- 搜索栏 --> |
||||
|
<view class="search-section"> |
||||
|
<view class="search-bar"> |
||||
|
<image |
||||
|
class="search-icon" |
||||
|
:src=" |
||||
|
showImg('/uploads/20250826/a4d605e82622223c270df0af4e378ab3.png') |
||||
|
" |
||||
|
></image> |
||||
|
<input |
||||
|
class="search-input" |
||||
|
placeholder="搜索已关注的人" |
||||
|
v-model="searchText" |
||||
|
@confirm="handleSearch" |
||||
|
/> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 我的关注标题和排序 --> |
||||
|
<view class="follows-header"> |
||||
|
<text class="follows-title">我的关注 ({{ totalCount || 0 }})</text> |
||||
|
<!-- <view class="sort-option" @click="toggleSort"> |
||||
|
<text class="sort-text">综合排序</text> |
||||
|
<image class="sort-arrow" :src="showImg('/uploads/20250826/8e40deaa0bc67da3a9b104ff0e6b3e7c.png')"></image> |
||||
|
</view> --> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 分类导航 --> |
||||
|
<!-- <view class="category-tabs"> |
||||
|
<view |
||||
|
class="tab-item" |
||||
|
:class="{ active: activeCategory === 'all' }" |
||||
|
@click="switchCategory('all')" |
||||
|
> |
||||
|
全部 |
||||
|
</view> |
||||
|
<view |
||||
|
class="tab-item" |
||||
|
:class="{ active: activeCategory === 'merchant' }" |
||||
|
@click="switchCategory('merchant')" |
||||
|
> |
||||
|
商家 |
||||
|
</view> |
||||
|
</view> --> |
||||
|
|
||||
|
<!-- 关注用户列表 --> |
||||
|
<view class="follows-list"> |
||||
|
<!-- 加载状态 --> |
||||
|
<view class="loading-state" v-if="loading"> |
||||
|
<view class="loading-spinner"></view> |
||||
|
<text class="loading-text">加载中...</text> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 空状态显示 --> |
||||
|
<view class="empty-state" v-else-if="filteredFollowsList.length === 0"> |
||||
|
<image |
||||
|
class="empty-icon" |
||||
|
:src=" |
||||
|
showImg('/uploads/20250826/a4d605e82622223c270df0af4e378ab3.png') |
||||
|
" |
||||
|
mode="aspectFit" |
||||
|
></image> |
||||
|
<text class="empty-text">{{ |
||||
|
searchText ? "未找到相关用户" : "暂无关注" |
||||
|
}}</text> |
||||
|
<text class="empty-desc">{{ |
||||
|
searchText ? "换个关键词试试吧" : "关注你感兴趣的人,获取更多精彩内容" |
||||
|
}}</text> |
||||
|
</view> |
||||
|
<view |
||||
|
class="follow-item" |
||||
|
v-for="(item, index) in filteredFollowsList" |
||||
|
:key="item.id" |
||||
|
> |
||||
|
<image |
||||
|
class="user-avatar" |
||||
|
:src=" |
||||
|
item.followUserHeadimg || |
||||
|
'https://epic.js-dyyj.com/uploads/20250728/7d9ba1fe109643681396cb03f60f3218.png' |
||||
|
" |
||||
|
mode="aspectFill" |
||||
|
></image> |
||||
|
<view class="user-info"> |
||||
|
<text class="user-name">{{ item.followUserNickname }}</text> |
||||
|
<view class="update-time"> |
||||
|
{{ item.formatTime }} |
||||
|
</view> |
||||
|
</view> |
||||
|
<view class="action-buttons"> |
||||
|
<view class="follow-status-btn"> 已关注 </view> |
||||
|
<view class="more-options" @click="showOptions(item)"> |
||||
|
<text class="more-dots">•••</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 推荐用户分隔线 --> |
||||
|
<view class="divider-line" v-if="false"></view> |
||||
|
|
||||
|
<!-- 推荐用户区域 --> |
||||
|
<view class="recommend-section" v-if="false"> |
||||
|
<view class="recommend-header"> |
||||
|
<view |
||||
|
class="recommend-title" |
||||
|
style="display: flex; align-items: center" |
||||
|
> |
||||
|
你可能感兴趣的人 |
||||
|
<image |
||||
|
style="width: 30rpx; height: 30rpx; margin-left: 10rpx" |
||||
|
:src=" |
||||
|
showImg('/uploads/20250826/f1422cbef4c33e8c21d9e7e805c8bad9.png') |
||||
|
" |
||||
|
></image> |
||||
|
</view> |
||||
|
<view class="close-btn" @click="closeRecommend"> |
||||
|
<text class="close-text">关闭</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<view class="recommend-list"> |
||||
|
<view |
||||
|
class="recommend-item" |
||||
|
v-for="(item, index) in recommendList" |
||||
|
:key="item.id" |
||||
|
> |
||||
|
<image |
||||
|
class="user-avatar" |
||||
|
src="https://epic.js-dyyj.com/uploads/20250728/7d9ba1fe109643681396cb03f60f3218.png" |
||||
|
mode="aspectFill" |
||||
|
></image> |
||||
|
<view class="user-info"> |
||||
|
<text class="user-name">{{ item.name }}</text> |
||||
|
<text class="user-desc">{{ item.description }}</text> |
||||
|
</view> |
||||
|
<view class="action-buttons"> |
||||
|
<view class="follow-btn" @click="followUser(item)"> 关注 </view> |
||||
|
<view class="dismiss-btn" @click="dismissUser(item)"> |
||||
|
<text class="dismiss-text">×</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: "FollowTab", |
||||
|
data() { |
||||
|
return { |
||||
|
searchText: "", |
||||
|
activeCategory: "all", |
||||
|
followsList: [], |
||||
|
recommendList: [ |
||||
|
{ |
||||
|
id: 101, |
||||
|
name: "颜真卿", |
||||
|
avatar: "/uploads/20250826/avatar1.png", |
||||
|
description: "介绍介绍介绍", |
||||
|
}, |
||||
|
{ |
||||
|
id: 102, |
||||
|
name: "颜真卿", |
||||
|
avatar: "/uploads/20250826/avatar1.png", |
||||
|
description: "介绍介绍介绍", |
||||
|
}, |
||||
|
], |
||||
|
// 分页相关数据 |
||||
|
pageNum: 1, |
||||
|
pageSize: 10, |
||||
|
loading: false, |
||||
|
hasMore: true, |
||||
|
totalCount: 0, |
||||
|
}; |
||||
|
}, |
||||
|
computed: { |
||||
|
filteredFollowsList() { |
||||
|
// 直接返回关注列表,搜索过滤已由后端处理 |
||||
|
return this.followsList; |
||||
|
}, |
||||
|
}, |
||||
|
mounted() { |
||||
|
// 组件挂载时获取关注列表 |
||||
|
this.getFollowList(); |
||||
|
}, |
||||
|
// 监听页面滚动到底部事件 |
||||
|
onReachBottom() { |
||||
|
this.loadMoreFollows(); |
||||
|
}, |
||||
|
// 下拉刷新 |
||||
|
onPullDownRefresh() { |
||||
|
this.getFollowList(); |
||||
|
}, |
||||
|
methods: { |
||||
|
handleSearch() { |
||||
|
// 调用接口进行搜索 |
||||
|
this.getFollowList(); |
||||
|
}, |
||||
|
|
||||
|
toggleSort() { |
||||
|
// 切换排序方式 |
||||
|
uni.showToast({ |
||||
|
title: "排序功能开发中", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
switchCategory(category) { |
||||
|
this.activeCategory = category; |
||||
|
}, |
||||
|
|
||||
|
// 获取关注列表数据 |
||||
|
getFollowList(refresh = true) { |
||||
|
let token = uni.getStorageSync("token1"); |
||||
|
if (!token) { |
||||
|
uni.showToast({ |
||||
|
title: "请先登录", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
uni.navigateTo({ |
||||
|
url: "/pages/login/login", |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
if (this.loading) return; |
||||
|
|
||||
|
if (refresh) { |
||||
|
this.pageNum = 1; |
||||
|
this.hasMore = true; |
||||
|
} |
||||
|
|
||||
|
this.loading = true; |
||||
|
const params = { |
||||
|
pageNum: this.pageNum, |
||||
|
pageSize: this.pageSize, |
||||
|
}; |
||||
|
|
||||
|
// 如果有搜索文本,添加到请求参数中 |
||||
|
if (this.searchText.trim()) { |
||||
|
params.followUserNickname = this.searchText.trim(); |
||||
|
} |
||||
|
|
||||
|
this.Post(params, "/framework/follow/followList", "DES") |
||||
|
.then((res) => { |
||||
|
if (res.code === 200 && res.rows) { |
||||
|
const newItems = res.rows || []; |
||||
|
|
||||
|
if (this.pageNum === 1) { |
||||
|
// 首次加载或刷新 |
||||
|
this.followsList = newItems; |
||||
|
this.totalCount = res.total || 0; |
||||
|
} else { |
||||
|
// 加载更多 |
||||
|
this.followsList.push(...newItems); |
||||
|
} |
||||
|
|
||||
|
// 判断是否还有更多数据 |
||||
|
this.hasMore = newItems.length === this.pageSize; |
||||
|
} else { |
||||
|
uni.showToast({ |
||||
|
title: res.msg || "获取关注列表失败", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
} |
||||
|
}) |
||||
|
.catch((error) => { |
||||
|
console.error("获取关注列表失败:", error); |
||||
|
uni.showToast({ |
||||
|
title: "加载失败,请重试", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
}) |
||||
|
.finally(() => { |
||||
|
this.loading = false; |
||||
|
uni.stopPullDownRefresh(); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 加载更多关注数据 |
||||
|
loadMoreFollows() { |
||||
|
if (!this.loading && this.hasMore) { |
||||
|
this.pageNum++; |
||||
|
this.getFollowList(false); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
showOptions(item) { |
||||
|
uni.showActionSheet({ |
||||
|
itemList: ["取消关注"], |
||||
|
success: (res) => { |
||||
|
switch (res.tapIndex) { |
||||
|
case 0: |
||||
|
this.unfollowUser(item); |
||||
|
break; |
||||
|
case 1: |
||||
|
this.reportUser(item); |
||||
|
break; |
||||
|
case 2: |
||||
|
this.blockUser(item); |
||||
|
break; |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
unfollowUser(item) { |
||||
|
uni.showModal({ |
||||
|
title: "提示", |
||||
|
content: "确定要取消关注该用户吗?", |
||||
|
success: (res) => { |
||||
|
if (res.confirm) { |
||||
|
// 调用取消关注接口 |
||||
|
this.Post( |
||||
|
{ |
||||
|
followUserId: item.followUserId, |
||||
|
type: 2, // 2表示取消关注 |
||||
|
}, |
||||
|
"/framework/follow/followUser", |
||||
|
"DES" |
||||
|
) |
||||
|
.then((res) => { |
||||
|
if (res.code === 200) { |
||||
|
// 从列表中移除 |
||||
|
const index = this.followsList.findIndex( |
||||
|
(user) => user.id === item.id |
||||
|
); |
||||
|
if (index > -1) { |
||||
|
this.followsList.splice(index, 1); |
||||
|
this.totalCount--; |
||||
|
} |
||||
|
|
||||
|
uni.showToast({ |
||||
|
title: "已取消关注", |
||||
|
icon: "success", |
||||
|
}); |
||||
|
} else { |
||||
|
uni.showToast({ |
||||
|
title: res.msg || "取消关注失败", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
} |
||||
|
}) |
||||
|
.catch((error) => { |
||||
|
console.error("取消关注失败:", error); |
||||
|
uni.showToast({ |
||||
|
title: "操作失败,请重试", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
reportUser(item) { |
||||
|
uni.showToast({ |
||||
|
title: "举报功能开发中", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
blockUser(item) { |
||||
|
uni.showToast({ |
||||
|
title: "拉黑功能开发中", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
followUser(item) { |
||||
|
// 添加到关注列表 |
||||
|
this.followsList.unshift({ |
||||
|
id: item.id, |
||||
|
name: item.name, |
||||
|
avatar: item.avatar, |
||||
|
newItems: 0, |
||||
|
category: "user", |
||||
|
}); |
||||
|
|
||||
|
// 从推荐列表移除 |
||||
|
const index = this.recommendList.findIndex((user) => user.id === item.id); |
||||
|
if (index > -1) { |
||||
|
this.recommendList.splice(index, 1); |
||||
|
} |
||||
|
|
||||
|
uni.showToast({ |
||||
|
title: "关注成功", |
||||
|
icon: "success", |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
dismissUser(item) { |
||||
|
const index = this.recommendList.findIndex((user) => user.id === item.id); |
||||
|
if (index > -1) { |
||||
|
this.recommendList.splice(index, 1); |
||||
|
uni.showToast({ |
||||
|
title: "已移除推荐", |
||||
|
icon: "success", |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
closeRecommend() { |
||||
|
this.recommendList = []; |
||||
|
uni.showToast({ |
||||
|
title: "已关闭推荐", |
||||
|
icon: "success", |
||||
|
}); |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.follow-tab-container { |
||||
|
background: #fff; |
||||
|
} |
||||
|
|
||||
|
/* 搜索栏 */ |
||||
|
.search-section { |
||||
|
padding: 32rpx; |
||||
|
background: #fff; |
||||
|
} |
||||
|
|
||||
|
.search-bar { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
background: #f8f9fa; |
||||
|
border-radius: 40rpx; |
||||
|
padding: 0 32rpx; |
||||
|
height: 80rpx; |
||||
|
|
||||
|
.search-icon { |
||||
|
width: 32rpx; |
||||
|
height: 32rpx; |
||||
|
margin-right: 16rpx; |
||||
|
color: #999; |
||||
|
} |
||||
|
|
||||
|
.search-input { |
||||
|
flex: 1; |
||||
|
font-size: 28rpx; |
||||
|
color: #333; |
||||
|
|
||||
|
&::placeholder { |
||||
|
color: #999; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 关注标题和排序 */ |
||||
|
.follows-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-top: 20rpx; |
||||
|
padding: 0 32rpx 24rpx; |
||||
|
margin-bottom: 20rpx; |
||||
|
|
||||
|
.follows-title { |
||||
|
font-size: 32rpx; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.sort-option { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
|
||||
|
.sort-text { |
||||
|
font-size: 28rpx; |
||||
|
color: #000000; |
||||
|
margin-right: 8rpx; |
||||
|
} |
||||
|
|
||||
|
.sort-arrow { |
||||
|
width: 9rpx; |
||||
|
height: 24rpx; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 分类导航 */ |
||||
|
.category-tabs { |
||||
|
display: flex; |
||||
|
padding: 0 32rpx 32rpx; |
||||
|
gap: 16rpx; |
||||
|
|
||||
|
.tab-item { |
||||
|
border-radius: 32rpx; |
||||
|
transition: all 0.3s ease; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 15rpx 30rpx; |
||||
|
font-size: 28rpx; |
||||
|
color: #000000; |
||||
|
font-weight: bold; |
||||
|
|
||||
|
&.active { |
||||
|
background: #00ffff; |
||||
|
color: #000000; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 关注用户列表 */ |
||||
|
.follows-list { |
||||
|
padding: 0 32rpx; |
||||
|
|
||||
|
/* 加载状态样式 */ |
||||
|
.loading-state { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 60rpx 0; |
||||
|
|
||||
|
.loading-spinner { |
||||
|
width: 60rpx; |
||||
|
height: 60rpx; |
||||
|
border: 4rpx solid #f3f3f3; |
||||
|
border-top: 4rpx solid #00ffff; |
||||
|
border-radius: 50%; |
||||
|
margin-bottom: 20rpx; |
||||
|
animation: spin 1s linear infinite; |
||||
|
} |
||||
|
|
||||
|
.loading-text { |
||||
|
font-size: 28rpx; |
||||
|
color: #999; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@keyframes spin { |
||||
|
0% { |
||||
|
transform: rotate(0deg); |
||||
|
} |
||||
|
100% { |
||||
|
transform: rotate(360deg); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 空状态样式 */ |
||||
|
.empty-state { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 100rpx 0; |
||||
|
|
||||
|
.empty-icon { |
||||
|
width: 120rpx; |
||||
|
height: 120rpx; |
||||
|
margin-bottom: 30rpx; |
||||
|
opacity: 0.5; |
||||
|
} |
||||
|
|
||||
|
.empty-text { |
||||
|
font-size: 32rpx; |
||||
|
font-weight: 500; |
||||
|
color: #333; |
||||
|
margin-bottom: 16rpx; |
||||
|
} |
||||
|
|
||||
|
.empty-desc { |
||||
|
font-size: 26rpx; |
||||
|
color: #999; |
||||
|
text-align: center; |
||||
|
max-width: 80%; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.follow-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
padding: 32rpx 0; |
||||
|
border-bottom: 1rpx solid #f0f0f0; |
||||
|
|
||||
|
&:last-child { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
|
||||
|
.user-avatar { |
||||
|
width: 100rpx; |
||||
|
height: 100rpx; |
||||
|
border-radius: 50%; |
||||
|
margin-right: 24rpx; |
||||
|
} |
||||
|
|
||||
|
.user-info { |
||||
|
flex: 1; |
||||
|
|
||||
|
.user-name { |
||||
|
display: block; |
||||
|
font-size: 30rpx; |
||||
|
font-weight: 500; |
||||
|
color: #000000; |
||||
|
margin-bottom: 8rpx; |
||||
|
} |
||||
|
|
||||
|
.update-tag { |
||||
|
display: inline-block; |
||||
|
background: #f8f9fa; |
||||
|
border-radius: 16rpx; |
||||
|
padding: 10rpx 12rpx; |
||||
|
font-size: 24rpx; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.update-time { |
||||
|
font-size: 24rpx; |
||||
|
color: #999; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.action-buttons { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 16rpx; |
||||
|
|
||||
|
.follow-status-btn { |
||||
|
border-radius: 24rpx; |
||||
|
padding: 12rpx 24rpx; |
||||
|
border: 2rpx solid #e5e5e5; |
||||
|
font-size: 26rpx; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.more-options { |
||||
|
padding: 8rpx; |
||||
|
|
||||
|
.more-dots { |
||||
|
font-size: 24rpx; |
||||
|
color: #000; |
||||
|
letter-spacing: 2rpx; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 分隔线 */ |
||||
|
.divider-line { |
||||
|
height: 1rpx; |
||||
|
background: #f0f0f0; |
||||
|
margin: 32rpx 0; |
||||
|
} |
||||
|
|
||||
|
/* 推荐用户区域 */ |
||||
|
.recommend-section { |
||||
|
padding: 0 32rpx; |
||||
|
|
||||
|
.recommend-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-bottom: 32rpx; |
||||
|
|
||||
|
.recommend-title { |
||||
|
font-size: 28rpx; |
||||
|
font-weight: 500; |
||||
|
color: #666666; |
||||
|
margin-right: 10rpx; |
||||
|
} |
||||
|
|
||||
|
.close-btn { |
||||
|
padding: 8rpx 16rpx; |
||||
|
|
||||
|
.close-text { |
||||
|
font-size: 28rpx; |
||||
|
color: #666; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.recommend-list { |
||||
|
.recommend-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
padding: 32rpx 0; |
||||
|
border-bottom: 1rpx solid #f0f0f0; |
||||
|
|
||||
|
&:last-child { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
|
||||
|
.user-avatar { |
||||
|
width: 100rpx; |
||||
|
height: 100rpx; |
||||
|
border-radius: 50%; |
||||
|
margin-right: 24rpx; |
||||
|
} |
||||
|
|
||||
|
.user-info { |
||||
|
flex: 1; |
||||
|
|
||||
|
.user-name { |
||||
|
display: block; |
||||
|
font-size: 30rpx; |
||||
|
font-weight: 500; |
||||
|
color: #333; |
||||
|
margin-bottom: 8rpx; |
||||
|
} |
||||
|
|
||||
|
.user-desc { |
||||
|
font-size: 26rpx; |
||||
|
color: #999; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.action-buttons { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 16rpx; |
||||
|
|
||||
|
.follow-btn { |
||||
|
border: 2rpx solid #00ffff; |
||||
|
border-radius: 24rpx; |
||||
|
padding: 12rpx 24rpx; |
||||
|
font-size: 26rpx; |
||||
|
color: #000; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.dismiss-btn { |
||||
|
width: 48rpx; |
||||
|
height: 48rpx; |
||||
|
border-radius: 50%; |
||||
|
background: #f8f9fa; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
|
||||
|
.dismiss-text { |
||||
|
font-size: 32rpx; |
||||
|
color: #999; |
||||
|
font-weight: 300; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,662 @@ |
|||||
|
<template> |
||||
|
<view class="bg"> |
||||
|
<view class="title">我的</view> |
||||
|
|
||||
|
<view class="topBox" @click="gotoProfile"> |
||||
|
<view class="avatar-box flex-center"> |
||||
|
<image :src="showImg(userInfo.avatar)" mode="aspectFill" class="headImg" v-if="userInfo.avatar"></image> |
||||
|
<image src="https://changshu.js-dyyj.com/uploads/20250326/516242619f0772bee371a60684618c01.png" mode="aspectFill" |
||||
|
class="headImg" v-else></image> |
||||
|
</view> |
||||
|
<view class="username" v-if="userInfo.nickname">{{userInfo.nickname}}</view> |
||||
|
<view class="username" v-else>请登录/注册 ></view> |
||||
|
<view class="personalCenter flex-center" v-if="userInfo.nickname"> |
||||
|
个人中心 |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<view class="orderBox"> |
||||
|
<navigator :url="'/subPackages/order/trades'" class="moreBox flex-between"> |
||||
|
我的订单 |
||||
|
<span class="flex-between">查看更多 <img |
||||
|
src="https://static.ticket.sz-trip.com/cgc/images/user/rightIcon.png" alt=""></span> |
||||
|
</navigator> |
||||
|
|
||||
|
<view class="flex-around" style="margin-top: 20rpx;"> |
||||
|
<view class="orderItem" v-for="(item,index) in orderList" :key="index" @click="goTrades(item)"> |
||||
|
<image :src="item.src" mode="heightFix"></image> |
||||
|
<view style="margin-top: 10rpx;">{{item.title}}</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
<!-- 待付款轮播 --> |
||||
|
<!-- <swiper class="my-swipe" :autoplay="3000" indicator-color="white" v-if="dfkList && dfkList.length>0" circular> |
||||
|
<swiper-item v-for="(item,index) in dfkList" :key="item.id"> |
||||
|
<div class="dfkBox" @click="goToOrderDetail(item)"> |
||||
|
<image :src="showImg(item.order_child[0].specifications_image)" mode="aspectFill"></image> |
||||
|
<div class="contentBox"> |
||||
|
<div style="width:300rpx;"> |
||||
|
<div style="font-size: 27rpx;margin-bottom: 10rpx;">等待付款 </div> |
||||
|
<div style="display: flex;color: #8A8A8A;font-size: 27rpx;">剩余时间:<uni-countdown class="countdown" @timeup="timeup(index)" :show-day="false" :hour="differTimeList[index].slice(0,2)" :minute="differTimeList[index].slice(3,5)" :second="differTimeList[index].slice(6,8)"/></div> |
||||
|
</div> |
||||
|
<div class="orderBtn" @click.stop="setOrderId(item.order_id)">去支付</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</swiper-item> |
||||
|
</swiper> --> |
||||
|
</view> |
||||
|
|
||||
|
<view class="cygj"> |
||||
|
<view class="cyItem" style="font-weight: bold;font-size: 31rpx;color: #333333;"> |
||||
|
常用工具 |
||||
|
</view> |
||||
|
<view class="cyItem flex-between" v-for="(item,index) in cyList" :key="index" |
||||
|
@click="gotoUrl(item,index)" v-if="item.isShow"> |
||||
|
<view class="flex-center"> |
||||
|
<img :src="item.src" class="headIcon"> |
||||
|
{{item.title}} |
||||
|
</view> |
||||
|
<img src="https://static.ticket.sz-trip.com/yandu/images/user/rightIcon-gray.png" class="rightIcon"> |
||||
|
</view> |
||||
|
<button id="contact" class="cyItem flex-between" open-type="contact" bindcontact="handleContact" session-from="sessionFrom"> |
||||
|
<view class="flex-center"> |
||||
|
<img src="https://static.ticket.sz-trip.com/cgc/images/user/kfdh.png" class="headIcon"> |
||||
|
在线客服 |
||||
|
</view> |
||||
|
<img src="https://static.ticket.sz-trip.com/dongtai/images/user/rightIcon-gray.png" class="rightIcon"> |
||||
|
</button> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 旅游咨询弹框 --> |
||||
|
<uni-popup ref="popup" type="center"> |
||||
|
<view class="consult-popup"> |
||||
|
即将拨打客服电话 |
||||
|
<view class="consult-subtitle">服务时间:周一至周五<br>9:00-12:00,13:00-18:00</view> |
||||
|
<view class="consult-btns"> |
||||
|
<view @click="$refs.popup.close()">取消</view> |
||||
|
<view @click="clickPhone('0515-69186109')">确定</view> |
||||
|
<!-- <view> |
||||
|
<button class="confirm" open-type="contact">确定</button> |
||||
|
</view> --> |
||||
|
</view> |
||||
|
</view> |
||||
|
</uni-popup> |
||||
|
|
||||
|
<CustomTabBar :currentTab="4" /> |
||||
|
<MusicControl /> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import moment from "moment"; |
||||
|
import CustomTabBar from '@/components/CustomTabBar.vue'; |
||||
|
import MusicControl from '@/components/MusicControl.vue'; |
||||
|
export default { |
||||
|
components: {CustomTabBar,MusicControl}, |
||||
|
data() { |
||||
|
return { |
||||
|
dfkList: [], |
||||
|
differTimeList: [], |
||||
|
nowDateTime: '', //当前时间秒数 |
||||
|
userInfo: {}, |
||||
|
orderList: [{ |
||||
|
src: 'https://des.dayunyuanjian.cn/epicSoul/image/user/dfk.png', |
||||
|
title: '待付款', |
||||
|
status: 'WAIT_PAYMENT' |
||||
|
}, |
||||
|
{ |
||||
|
src: 'https://des.dayunyuanjian.cn/epicSoul/image/user/dfh.png', |
||||
|
title: '待使用/发货', |
||||
|
status: 'PAYMENT_SUCCESSFULLY' |
||||
|
}, |
||||
|
{ |
||||
|
src: 'https://des.dayunyuanjian.cn/epicSoul/image/user/dsh.png', |
||||
|
title: '待收货', |
||||
|
status: 'POST' |
||||
|
}, |
||||
|
// { |
||||
|
// src: 'https://changshu.js-dyyj.com/uploads/20250326/3f13d3a10dd0f88e764e3ddf1157c108.png', |
||||
|
// title: '待评价', |
||||
|
// status: 'WAIT_COMMENT' |
||||
|
// }, |
||||
|
{ |
||||
|
src: 'https://des.dayunyuanjian.cn/epicSoul/image/user/sh.png', |
||||
|
title: '退款/售后', |
||||
|
status: 'WAIT_REFUND,REFUND_SUCCESS,REFUND_REFUSAL,REFUND_ERROR,REFUND_PART' |
||||
|
}, |
||||
|
], |
||||
|
cyList: [ |
||||
|
{ |
||||
|
src: 'https://static.ticket.sz-trip.com/cgc/images/user/gwc.png', |
||||
|
title: '购物车', |
||||
|
path: '/subPackages/user/gwc', |
||||
|
isShow: true |
||||
|
}, |
||||
|
// { |
||||
|
// src: 'https://static.ticket.sz-trip.com/cgc/images/user/kfdh.png', |
||||
|
// title: '客服电话', |
||||
|
// path: '', |
||||
|
// isShow: true |
||||
|
// }, |
||||
|
{ |
||||
|
src: 'https://static.ticket.sz-trip.com/cgc/images/user/shdz.png', |
||||
|
title: '收货地址', |
||||
|
path: '/subPackages/user/travelerList', |
||||
|
isShow: true |
||||
|
}, |
||||
|
{ |
||||
|
src: 'https://static.ticket.sz-trip.com/cgc/images/user/ysgl.png', |
||||
|
title: '隐私管理', |
||||
|
path: '/subPackages/user/privacy', |
||||
|
isShow: true |
||||
|
}, |
||||
|
// { |
||||
|
// src: 'https://changshu.js-dyyj.com/uploads/20250326/3e977f62b6cbfeec5a17d945b96b8c8c.png', |
||||
|
// title: '投诉建议', |
||||
|
// path: '/subPackages/service/service', |
||||
|
// isShow: true |
||||
|
// }, |
||||
|
], |
||||
|
} |
||||
|
}, |
||||
|
onShow() { |
||||
|
this.userInfo = (uni.getStorageSync('userInfo') && JSON.parse(uni.getStorageSync('userInfo'))) || this.$store.state.user.userInfo || {} |
||||
|
console.log(this.userInfo) |
||||
|
// this.dfkList = [] |
||||
|
// this.nowDateTime = parseInt(new Date().getTime() / 1000) |
||||
|
// this.Post({}, "/api/user/userInfo").then((res) => { |
||||
|
// if (res.data) { |
||||
|
// this.userInfo = res.data; |
||||
|
// // this.getDfk() |
||||
|
|
||||
|
// // 是否展示商户核销 |
||||
|
// this.Post({},'/api/merchants/is_merchant').then(res => { |
||||
|
// this.cyList[6].isShow = res.data |
||||
|
// }) |
||||
|
// } |
||||
|
// }); |
||||
|
}, |
||||
|
methods: { |
||||
|
// 个人信息或登录 |
||||
|
gotoProfile() { |
||||
|
// 有token去个人信息,没有去登录 |
||||
|
if(this.userInfo.token) { |
||||
|
uni.navigateTo({ |
||||
|
url: '/subPackages/user/profile' |
||||
|
}) |
||||
|
}else { |
||||
|
uni.navigateTo({ |
||||
|
url: '/pages/login/login' |
||||
|
}) |
||||
|
} |
||||
|
}, |
||||
|
timeup(index) { |
||||
|
// return this.dfkList.splice(index,1) |
||||
|
}, |
||||
|
setOrderId(id) { |
||||
|
let that = this; |
||||
|
that.orderId = id; |
||||
|
that.Post({ |
||||
|
order_id: id, |
||||
|
type: "miniprogram", |
||||
|
platform: 'miniprogram' |
||||
|
}, |
||||
|
'/api/pay/unify' |
||||
|
).then(res => { |
||||
|
if (res.data) { |
||||
|
uni.requestPayment({ |
||||
|
nonceStr: res.data.nonceStr, |
||||
|
package: res.data.package, |
||||
|
paySign: res.data.paySign, |
||||
|
signType: res.data.signType, |
||||
|
timeStamp: res.data.timeStamp |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
// 待付款 |
||||
|
getDfk() { |
||||
|
this.differTimeList = [] |
||||
|
let params = { |
||||
|
offset: this.dfkList.length, |
||||
|
limit: 10, |
||||
|
status: 'WAIT_PAYMENT', |
||||
|
} |
||||
|
this.Post(params, '/api/order/orderList').then(res => { |
||||
|
this.isLoading = false |
||||
|
if (res) { |
||||
|
this.dfkList = [...this.dfkList, ...res.data] |
||||
|
this.dfkList.forEach(item => { |
||||
|
// 获取时间差,订单关闭时间-当前时间,若存在即展示倒计时differTimeList |
||||
|
let del; |
||||
|
if (moment(item.close_time).diff(moment()) > 0) { |
||||
|
del = moment.utc(moment(item.close_time).diff(moment())).format('HH:mm:ss') |
||||
|
} else { |
||||
|
del = '00:00:00' |
||||
|
} |
||||
|
this.differTimeList.push(del) |
||||
|
}) |
||||
|
console.log(this.differTimeList); |
||||
|
console.log('this.differTimeList:' + this.differTimeList[0].slice(0, 2)) |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
goToOrderDetail(item) { |
||||
|
uni.navigateTo({ |
||||
|
url: '/subPackages/order/detail?id=' + item.order_id |
||||
|
}); |
||||
|
}, |
||||
|
getChild(list) { |
||||
|
let arr = [] |
||||
|
for (let i = 0; i < list.length; i++) { |
||||
|
if (list[i].product_model == "ticket") { |
||||
|
console.log(list[i]); |
||||
|
arr.push(list[i]) |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
console.log(arr); |
||||
|
if (arr.length > 0) { |
||||
|
return arr[0] |
||||
|
} else { |
||||
|
return list[0] |
||||
|
} |
||||
|
|
||||
|
}, |
||||
|
goCoupon() { |
||||
|
uni.navigateTo({ |
||||
|
url: "/subPackages/user/coupon", |
||||
|
}); |
||||
|
}, |
||||
|
goKeFu() { |
||||
|
uni.navigateTo({ |
||||
|
url: "/subPackages/publicservices/ServiceOnline", |
||||
|
}); |
||||
|
}, |
||||
|
// open(){ |
||||
|
// this.$refs.popup.open('center') |
||||
|
// }, |
||||
|
gotoUrl(item, index) { |
||||
|
if (item.title == "客服电话") { |
||||
|
this.$refs.popup.open() |
||||
|
return; |
||||
|
} |
||||
|
uni.navigateTo({ |
||||
|
url: item.path |
||||
|
}) |
||||
|
}, |
||||
|
qidai() { |
||||
|
uni.showToast({ |
||||
|
title: '功能建设中...', |
||||
|
icon: 'none' |
||||
|
}) |
||||
|
}, |
||||
|
goTrades(item) { |
||||
|
if (item) { |
||||
|
uni.navigateTo({ |
||||
|
url: "/subPackages/order/trades?type=" + item.title, |
||||
|
}); |
||||
|
} else { |
||||
|
uni.navigateTo({ |
||||
|
url: "/subPackages/order/trades", |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
//判断是否关注公众号 |
||||
|
isGz(item) { |
||||
|
this.$refs.pop.openPop( |
||||
|
'https://yjks.oss-cn-shanghai.aliyuncs.com/uploads/20230517/db9eb60e0abfea8be1075b406fefe551.jpg'); |
||||
|
// this.Post({}, '/api/wechat/getSubcribeInfo').then(res => { |
||||
|
// if (res.data) { |
||||
|
// uni.navigateTo({ |
||||
|
// url:'/subPackages/webPage/webPage?url='+'https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=MzU2NjQwNTYxNg==#wechat_redirect' |
||||
|
// }) |
||||
|
// } else { |
||||
|
// console.log(this.$refs.pop); |
||||
|
// } |
||||
|
// }); |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped lang="scss"> |
||||
|
view{ |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
::v-deep .uni-countdown { |
||||
|
font-size: 20px !important; |
||||
|
|
||||
|
::v-deep .uni-countdown__splitor { |
||||
|
font-size: 20px !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.bg { |
||||
|
min-height: 100vh; |
||||
|
overflow-x: hidden; |
||||
|
background: url('https://des.dayunyuanjian.cn/epicSoul/image/user/bg.png') no-repeat; |
||||
|
background-size: 100% auto; |
||||
|
background-color: #F7F7F7; |
||||
|
padding-bottom: 100rpx; |
||||
|
} |
||||
|
|
||||
|
.title { |
||||
|
font-weight: bold; |
||||
|
font-size: 36rpx; |
||||
|
color: #333333; |
||||
|
position: absolute; |
||||
|
top: 110rpx; |
||||
|
left: 50%; |
||||
|
transform: translate(-50%, 0); |
||||
|
} |
||||
|
|
||||
|
.topBox { |
||||
|
width: 750rpx; |
||||
|
height: 373rpx; |
||||
|
padding: 100rpx 0 0 26rpx; |
||||
|
box-sizing: border-box; |
||||
|
display: flex; |
||||
|
margin-top: 90rpx; |
||||
|
|
||||
|
.avatar-box { |
||||
|
width: 120rpx; |
||||
|
height: 120rpx; |
||||
|
background: #FFFFFF; |
||||
|
border-radius: 50%; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.headImg { |
||||
|
width: 120rpx; |
||||
|
height: 120rpx; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
|
||||
|
.username { |
||||
|
margin: 40rpx 0 0 28rpx; |
||||
|
font-weight: 500; |
||||
|
font-size: 40rpx; |
||||
|
color: #000000; |
||||
|
} |
||||
|
|
||||
|
.personalCenter { |
||||
|
width: 165rpx; |
||||
|
height: 59rpx; |
||||
|
background: rgba(24, 135, 145, 1); |
||||
|
border-radius: 29rpx 0rpx 0rpx 29rpx; |
||||
|
margin: 40rpx 0 0 auto; |
||||
|
font-weight: bold; |
||||
|
font-size: 28rpx; |
||||
|
color: #FFFFFF; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.orderBox { |
||||
|
width: 697rpx; |
||||
|
background: #FFFFFF; |
||||
|
box-shadow: 0rpx 0rpx 23rpx 0rpx rgba(80, 80, 80, 0.12); |
||||
|
border-radius: 20rpx; |
||||
|
margin: -48rpx auto 0; |
||||
|
padding-bottom: 30.6rpx; |
||||
|
|
||||
|
.moreBox { |
||||
|
height: 84rpx; |
||||
|
margin: auto; |
||||
|
font-size: 31rpx; |
||||
|
font-family: PingFang SC; |
||||
|
font-weight: bold; |
||||
|
color: #000000; |
||||
|
padding-left: 26rpx; |
||||
|
|
||||
|
span { |
||||
|
padding-right: 18rpx; |
||||
|
font-size: 27rpx; |
||||
|
font-family: PingFang SC; |
||||
|
font-weight: 500; |
||||
|
box-sizing: border-box; |
||||
|
|
||||
|
img { |
||||
|
width: 12rpx; |
||||
|
height: 21rpx; |
||||
|
margin-left: 12rpx; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.orderItem { |
||||
|
font-size: 27rpx; |
||||
|
font-family: PingFang SC; |
||||
|
font-weight: 500; |
||||
|
color: #000000; |
||||
|
width: 25%; |
||||
|
text-align: center; |
||||
|
|
||||
|
image { |
||||
|
height: 60rpx; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.lxwm { |
||||
|
width: 696rpx; |
||||
|
height: 447rpx; |
||||
|
background: #FFFFFF; |
||||
|
box-shadow: 0px 4rpx 12rpx 0px rgba(150, 149, 149, 0.3); |
||||
|
border-radius: 20rpx; |
||||
|
margin: auto; |
||||
|
padding: 27rpx 19rpx 0 19rpx; |
||||
|
font-size: 31rpx; |
||||
|
font-family: PingFang SC; |
||||
|
font-weight: bold; |
||||
|
color: #000000; |
||||
|
|
||||
|
.midBox { |
||||
|
padding: 26rpx 43rpx 21rpx 44rpx; |
||||
|
box-sizing: border-box; |
||||
|
|
||||
|
img { |
||||
|
width: 265rpx; |
||||
|
height: 252rpx; |
||||
|
border-radius: 15rpx; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.botBox { |
||||
|
padding: 0 30rpx 0 51rpx; |
||||
|
font-size: 27rpx; |
||||
|
font-family: PingFang SC; |
||||
|
font-weight: bold; |
||||
|
color: #000000; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.cygj { |
||||
|
width: 697rpx; |
||||
|
background: #FFFFFF; |
||||
|
border-radius: 20rpx; |
||||
|
margin: 30rpx auto 0; |
||||
|
padding: 0 27rpx; |
||||
|
font-size: 28rpx; |
||||
|
font-family: PingFang SC; |
||||
|
font-weight: 500; |
||||
|
color: #000000; |
||||
|
box-sizing: border-box; |
||||
|
|
||||
|
.cyItem { |
||||
|
height: 106rpx; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
border-bottom: 1rpx solid #D8D8D8; |
||||
|
|
||||
|
.headIcon { |
||||
|
width: 42rpx; |
||||
|
height: 42rpx; |
||||
|
margin-right: 15rpx; |
||||
|
} |
||||
|
|
||||
|
.rightIcon { |
||||
|
width: 20rpx; |
||||
|
height: 20rpx; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.cyItem:last-child { |
||||
|
border: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.my-swipe { |
||||
|
// width: 100%; |
||||
|
margin: 0 30rpx; |
||||
|
margin-top: 37.3rpx; |
||||
|
} |
||||
|
|
||||
|
swiper { |
||||
|
height: 111rpx !important; |
||||
|
} |
||||
|
|
||||
|
.dfkBox { |
||||
|
width: 100%; |
||||
|
height: 111rpx; |
||||
|
background: #F7F7F7; |
||||
|
margin: 0 auto 30.64rpx; |
||||
|
// padding: 0.25rem; |
||||
|
display: flex; |
||||
|
} |
||||
|
|
||||
|
.dfkBox image { |
||||
|
width: 137rpx; |
||||
|
height: 111rpx; |
||||
|
border-radius: 13rpx; |
||||
|
flex-shrink: 0; |
||||
|
// margin-right: 16.7rpx; |
||||
|
} |
||||
|
|
||||
|
.dfkBox .contentBox { |
||||
|
padding-left: 5rpx; |
||||
|
padding-right: 5rpx; |
||||
|
height: 111rpx; |
||||
|
width: 100%; |
||||
|
display: flex; |
||||
|
justify-content: space-around; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
// .van-count-down{ |
||||
|
// font-size: 0.39rem; |
||||
|
// font-family: PingFang SC; |
||||
|
// font-weight: 400; |
||||
|
// color: #FB6E4D; |
||||
|
// } |
||||
|
|
||||
|
.orderBtn { |
||||
|
width: 152rpx; |
||||
|
height: 56rpx; |
||||
|
background: linear-gradient(270deg, #FC5109, #FDC43A); |
||||
|
; |
||||
|
border-radius: 28rpx; |
||||
|
text-align: center; |
||||
|
line-height: 56rpx; |
||||
|
font-size: 27rpx; |
||||
|
font-family: PingFang SC; |
||||
|
font-weight: 400; |
||||
|
color: #FFFFFF; |
||||
|
// margin-left: 50rpx; |
||||
|
// margin-right: 20rpx; |
||||
|
} |
||||
|
|
||||
|
#contact { |
||||
|
-webkit-tap-highlight-color: transparent; |
||||
|
background-color: rgba(0, 0, 0, 0); |
||||
|
border-radius: 0; |
||||
|
box-sizing: border-box; |
||||
|
color: transparent; |
||||
|
cursor: pointer; |
||||
|
overflow: hidden; |
||||
|
padding: 0; |
||||
|
position: relative; |
||||
|
text-align: center; |
||||
|
text-decoration: none; |
||||
|
border: transparent 0px solid; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
font-size: 28rpx; |
||||
|
font-family: PingFang SC; |
||||
|
font-weight: 500; |
||||
|
color: #000000; |
||||
|
} |
||||
|
|
||||
|
button::after { |
||||
|
border: none; |
||||
|
background-color: rgba(0, 0, 0, 0); |
||||
|
} |
||||
|
|
||||
|
.more { |
||||
|
width: 100vw; |
||||
|
height: 100vh; |
||||
|
background-color: rgba(0, 0, 0, .5); |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: space-around; |
||||
|
align-items: center; |
||||
|
|
||||
|
image { |
||||
|
width: 646rpx; |
||||
|
height: 959rpx; |
||||
|
} |
||||
|
|
||||
|
img { |
||||
|
width: 80rpx; |
||||
|
height: 80rpx; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.consult-popup { |
||||
|
width: 487rpx; |
||||
|
height: 367rpx; |
||||
|
background: #F0F0F0; |
||||
|
border-radius: 20rpx; |
||||
|
padding: 65rpx 0 23rpx; |
||||
|
font-weight: bold; |
||||
|
font-size: 32rpx; |
||||
|
color: #000000; |
||||
|
text-align: center; |
||||
|
|
||||
|
.consult-subtitle { |
||||
|
font-weight: 500; |
||||
|
font-size: 27rpx; |
||||
|
color: #333333; |
||||
|
margin-top: 43rpx; |
||||
|
} |
||||
|
|
||||
|
.consult-btns { |
||||
|
display: flex; |
||||
|
margin-top: 75rpx; |
||||
|
|
||||
|
view { |
||||
|
width: 50%; |
||||
|
font-weight: bold; |
||||
|
font-size: 32rpx; |
||||
|
color: #00AEA0; |
||||
|
line-height: 54rpx; |
||||
|
} |
||||
|
view:first-child { |
||||
|
border-right: 1rpx solid #D8D8D8; |
||||
|
color: #000000; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.confirm{ |
||||
|
height: 58rpx; |
||||
|
line-height: 58rpx; |
||||
|
background: none; |
||||
|
font-weight: bold; |
||||
|
font-size: 32rpx; |
||||
|
color: #00AEA0; |
||||
|
} |
||||
|
</style> |
||||
File diff suppressed because it is too large
@ -1,99 +1,321 @@ |
|||||
<template> |
<template> |
||||
<view class="bg"> |
<view class="bg"> |
||||
<headerVue :isSearch="false"></headerVue> |
<headerVue |
||||
<view class="content"> |
@change="changeAddress" |
||||
<swiper class="top-banner" :indicator-dots="false" :autoplay="false" v-if="topBanner && topBanner.length > 0"> |
:address="addressInfo" |
||||
<swiper-item v-for="(item, index) in topBanner" :key="index" @click.stop="gotoUrlNew(item,index)"> |
fixed |
||||
<image class="top-banner" :src="showImg(item.head_img)" mode="aspectFill" lazy-load="true"></image> |
isLocation |
||||
</swiper-item> |
:isBack="false" |
||||
</swiper> |
></headerVue> |
||||
</view> |
|
||||
<CustomTabBar :currentTab="0" /> |
<!-- 灵动岛组件 --> |
||||
<MusicControl /> |
<!-- 灵动岛组件 - 自包含,无需传递参数 --> |
||||
</view> |
<DynamicIsland |
||||
|
ref="dynamicIsland" |
||||
|
:page-id="'index_page'" |
||||
|
@toggle="handleIslandToggle" |
||||
|
@action="handleIslandAction" |
||||
|
/> |
||||
|
|
||||
|
<view class="content" @click="handleContentClick"> |
||||
|
<IPComponents></IPComponents> |
||||
|
<Book></Book> |
||||
|
<!-- 权益商品区域 --> |
||||
|
<ProductSection |
||||
|
v-show="productList.length" |
||||
|
title="EPIC SOUL文旅权益商品" |
||||
|
:productList="productList" |
||||
|
moreUrl="/subPackages/equityGoods/list" |
||||
|
detailUrlPrefix="/subPackages/equityGoods/detail" |
||||
|
@like-toggle="handleLikeToggle" |
||||
|
color="#D2FFDE" |
||||
|
:type="1" |
||||
|
/> |
||||
|
|
||||
|
<ProductSection |
||||
|
titleBgColor="#92FF8F" |
||||
|
aiTagTextColor="#08FB05" |
||||
|
aiTagBorderColor="#6EAA3D" |
||||
|
title="EPIC SOUL文化有感商品" |
||||
|
:productList="productListFeeling" |
||||
|
moreUrl="/pages/index/sensoryStore" |
||||
|
detailUrlPrefix="/subPackages/techan/detail" |
||||
|
@like-toggle="handleLikeToggle" |
||||
|
:type="2" |
||||
|
color="#D3FCFF" |
||||
|
/> |
||||
|
<ProductSection |
||||
|
titleBgColor="#92FF8F" |
||||
|
aiTagTextColor="#08FB05" |
||||
|
aiTagBorderColor="#6EAA3D" |
||||
|
title="CGC-ICH 大运河非物质文化遗产" |
||||
|
@like-toggle="handleLikeToggle" |
||||
|
:type="3" |
||||
|
color="#FFEAD2" |
||||
|
/> |
||||
|
</view> |
||||
|
|
||||
|
<CustomTabBar :currentTab="0" /> |
||||
|
<MusicControl /> |
||||
|
</view> |
||||
</template> |
</template> |
||||
|
|
||||
<script> |
<script> |
||||
import MusicControl from '@/components/MusicControl.vue'; |
import MusicControl from "@/components/MusicControl.vue"; |
||||
import headerVue from "@/components/header.vue" |
import headerVue from "@/components/header.vue"; |
||||
import CustomTabBar from '@/components/CustomTabBar.vue'; |
import CustomTabBar from "@/components/CustomTabBar.vue"; |
||||
export default { |
import DynamicIsland from "@/components/DynamicIsland.vue"; |
||||
components: {CustomTabBar,headerVue,MusicControl}, |
import ProductSection from "@/components/ProductSection.vue"; |
||||
data() { |
import Book from "@/components/Book.vue"; |
||||
return { |
import IPComponents from "@/components/IPComponents.vue"; |
||||
topBanner: [] |
|
||||
} |
export default { |
||||
}, |
components: { |
||||
onLoad() { |
CustomTabBar, |
||||
|
headerVue, |
||||
}, |
MusicControl, |
||||
onReady() { |
DynamicIsland, |
||||
this.getList() |
ProductSection, |
||||
}, |
Book, |
||||
onShow() { |
IPComponents |
||||
this.browse_record({type: 'page',title: '首页'}); |
}, |
||||
}, |
computed: { |
||||
onReachBottom() { |
// 其他计算属性可以在这里添加 |
||||
|
}, |
||||
}, |
data() { |
||||
methods: { |
return { |
||||
gotoUrlNew(item,index) { |
topBanner: [], |
||||
if(index == 0) { |
productList: [ |
||||
uni.switchTab({ |
// { |
||||
url:"/pages/index/readingBody" |
// id: 1, |
||||
}) |
// image: |
||||
}else if(index == 1) { |
// "https://epic.js-dyyj.com/uploads/20250728/58aed304917c9d60761f833c4f8dceb8.png", |
||||
uni.switchTab({ |
// avatar: |
||||
url:"/pages/index/intelligentAgent" |
// "https://epic.js-dyyj.com/uploads/20250728/d27ef6e6c26877da7775664fed376c6f.png", |
||||
}) |
// aiName: "文徵明", |
||||
} |
// title: "世界花园 | 研学之旅", |
||||
}, |
// price: "588.00", |
||||
viewDetail(item) { |
// isLiked: true, |
||||
if (item.url) { |
// }, |
||||
uni.navigateTo({ |
// { |
||||
url:"/subPackages/webPage/webPage?url="+encodeURIComponent(item.url) |
// id: 2, |
||||
}) |
// image: |
||||
return |
// "https://epic.js-dyyj.com/uploads/20250728/00e8704b23a0c9fd57023527146211b9.png", |
||||
} |
// avatar: |
||||
uni.navigateTo({ |
// "https://epic.js-dyyj.com/uploads/20250728/d7bf0dd2f3f272afba687b525a7c575c.png", |
||||
url:'/subPackages/letter/detail?id='+item.id |
// aiName: "苏青壳", |
||||
}) |
// title: "生命的扶持 | 风景之旅", |
||||
}, |
// price: "398.00", |
||||
getList() { |
// isLiked: false, |
||||
// 大轮播 |
// }, |
||||
this.Post({ |
], |
||||
type_id: 3, |
productListFeeling: [ |
||||
position: 17, |
// { |
||||
}, '/api/adv/getAdv').then(res => { |
// id: 37, |
||||
if(res.data) { |
// image: |
||||
this.topBanner = res.data; |
// "https://epic.js-dyyj.com/uploads/20250822/f0ade3dd98a5a5e24ed0b60a023979e4.png", |
||||
} |
// title: "中国神仙IP系列 丝织品", |
||||
}); |
// price: "198.00", |
||||
}, |
// isLiked: true, |
||||
gotoVideo(item) { |
// isShop: true, |
||||
uni.navigateTo({ |
// }, |
||||
url: '/subPackages/video/video?item=' + encodeURIComponent(JSON.stringify(item)) |
// { |
||||
}) |
// id: 39, |
||||
} |
// image: |
||||
} |
// "https://epic.js-dyyj.com/uploads/20250822/19b1fb3e07fd459d347e727274af445c.png", |
||||
} |
// title: "仙人乘槎 马克杯", |
||||
</script> |
// price: "35.00", |
||||
|
// isLiked: false, |
||||
|
// }, |
||||
|
], |
||||
|
selectedText: "", |
||||
|
addressInfo: null, |
||||
|
isLoading: true, // 添加加载状态 |
||||
|
}; |
||||
|
}, |
||||
|
onLoad() {}, |
||||
|
async onReady() { |
||||
|
let res = await this.$main.getLocationInfo(); |
||||
|
console.log(res); |
||||
|
if (!res.cityId) { |
||||
|
res = { |
||||
|
address: "江苏省苏州市吴中区太湖东路288号", |
||||
|
area: "吴中区", |
||||
|
areaId: "320506", |
||||
|
city: "苏州市", |
||||
|
cityId: "320500", |
||||
|
latitude: 31.26249, |
||||
|
longitude: 120.63212, |
||||
|
province: "江苏省", |
||||
|
provinceId: "320000", |
||||
|
street: "太湖东路", |
||||
|
}; |
||||
|
} |
||||
|
this.addressInfo = res; |
||||
|
this.selectedText = res && res.city; |
||||
|
uni.setStorageSync("SYS_ADDRESS_INFO", JSON.stringify(res)); |
||||
|
this.getList(); |
||||
|
}, |
||||
|
onShow() { |
||||
|
this.geBenefitPackaget(); |
||||
|
this.browse_record({ |
||||
|
type: "page", |
||||
|
title: "首页", |
||||
|
}); |
||||
|
this.$nextTick(() => { |
||||
|
this.$refs.dynamicIsland.getUserInfo(); |
||||
|
}); |
||||
|
}, |
||||
|
onPageScroll(e) { |
||||
|
// 只触发带页面ID的事件,避免不同页面间的状态冲突 |
||||
|
uni.$emit("pageScroll_index_page", e.scrollTop); |
||||
|
}, |
||||
|
onReachBottom() {}, |
||||
|
methods: { |
||||
|
changeAddress(res) { |
||||
|
this.addressInfo = res; |
||||
|
uni.setStorageSync("SYS_ADDRESS_INFO", JSON.stringify(res)); |
||||
|
this.geBenefitPackaget(); |
||||
|
}, |
||||
|
gotoUrlNew(item, index) { |
||||
|
if (index == 0) { |
||||
|
uni.switchTab({ |
||||
|
url: "/pages/index/readingBody", |
||||
|
}); |
||||
|
} else if (index == 1) { |
||||
|
uni.switchTab({ |
||||
|
url: "/pages/index/intelligentAgent", |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
viewDetail(item) { |
||||
|
if (item.url) { |
||||
|
uni.navigateTo({ |
||||
|
url: |
||||
|
"/subPackages/webPage/webPage?url=" + encodeURIComponent(item.url), |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
uni.navigateTo({ |
||||
|
url: "/subPackages/letter/detail?id=" + item.id, |
||||
|
}); |
||||
|
}, |
||||
|
getList() { |
||||
|
// 大轮播 |
||||
|
this.Post( |
||||
|
{ |
||||
|
type_id: 3, |
||||
|
position: 17, |
||||
|
}, |
||||
|
"/api/adv/getAdv" |
||||
|
).then((res) => { |
||||
|
if (res.data) { |
||||
|
this.topBanner = res.data; |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
geBenefitPackaget() { |
||||
|
this.Post( |
||||
|
{ |
||||
|
cityId: this.addressInfo&&this.addressInfo.cityId||320500, |
||||
|
}, |
||||
|
"/framework/index/benefitPackage/list", |
||||
|
"DES" |
||||
|
).then((res) => { |
||||
|
if (res.data) { |
||||
|
this.productList = res.data.map((item) => { |
||||
|
return { |
||||
|
...item, |
||||
|
image: item.mainUrl, |
||||
|
id: item.benefitPackageId, |
||||
|
avatar: |
||||
|
"https://epic.js-dyyj.com/uploads/20250728/d27ef6e6c26877da7775664fed376c6f.png", |
||||
|
aiName: "文徵明", |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
this.Post( |
||||
|
{ |
||||
|
}, |
||||
|
"/framework/index/goods/indexList", |
||||
|
"DES" |
||||
|
).then((res) => { |
||||
|
if (res.data) { |
||||
|
this.productListFeeling = res.data.map(item =>{ |
||||
|
return { |
||||
|
title:item.goodsName, |
||||
|
price:item.salePrice, |
||||
|
image:item.posterUrl, |
||||
|
id:item.goodsId, |
||||
|
...item |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
}, |
||||
|
gotoVideo(item) { |
||||
|
uni.navigateTo({ |
||||
|
url: |
||||
|
"/subPackages/video/video?item=" + |
||||
|
encodeURIComponent(JSON.stringify(item)), |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 处理收藏状态切换 |
||||
|
handleLikeToggle({ item, index }) { |
||||
|
this.productList[index] = item; |
||||
|
}, |
||||
|
|
||||
|
// 处理灵动岛点击切换 |
||||
|
handleIslandToggle(isExpanded) { |
||||
|
console.log("灵动岛状态切换:", isExpanded ? "展开模式" : "紧凑模式"); |
||||
|
}, |
||||
|
|
||||
|
// 处理灵动岛操作按钮 |
||||
|
handleIslandAction() { |
||||
|
uni.showToast({ |
||||
|
title: "执行操作", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
console.log("执行操作"); |
||||
|
}, |
||||
|
|
||||
|
// 处理内容区域点击 |
||||
|
handleContentClick() { |
||||
|
// 点击内容区域时收缩灵动岛 |
||||
|
if (this.$refs.dynamicIsland) { |
||||
|
this.$refs.dynamicIsland.collapseIsland(); |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
||||
|
<style> |
||||
|
page { |
||||
|
} |
||||
|
</style> |
||||
<style lang="scss" scoped> |
<style lang="scss" scoped> |
||||
|
.bg { |
||||
|
min-height: 100vh; |
||||
|
background: #f5f5f5; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
/* 页面样式 */ |
||||
|
|
||||
|
.content { |
||||
|
height: 100%; |
||||
|
width: 100%; |
||||
|
padding: 0 20rpx; |
||||
|
box-sizing: border-box; |
||||
|
margin-top: 20rpx; |
||||
|
} |
||||
|
|
||||
.bg { |
.tab-bar-placeholder { |
||||
min-height: 100vh; |
height: 143rpx; |
||||
background: #FFFFFF; |
width: 100%; |
||||
display: flex; |
} |
||||
flex-direction: column; |
|
||||
} |
|
||||
.content{ |
|
||||
height: calc(100vh - 123rpx); |
|
||||
width: 100%; |
|
||||
} |
|
||||
.top-banner { |
|
||||
width: 100%; |
|
||||
height: calc(100vh - 123rpx); |
|
||||
} |
|
||||
|
|
||||
</style> |
</style> |
||||
|
|||||
@ -0,0 +1,262 @@ |
|||||
|
<template> |
||||
|
<view class="bg"> |
||||
|
<BackButton /> |
||||
|
<!-- <headerVue :type="'goods'"></headerVue> --> |
||||
|
<view class="banner-content"> |
||||
|
|
||||
|
<swiper class="top-banner" :circular="true" :interval="6000" :duration="800" |
||||
|
:indicator-dots="false" :autoplay="true" @change="swiperChange" > |
||||
|
<swiper-item v-for="(item, index) in topBanner" :key="index" @click.stop="gotoUrlNew(item)"> |
||||
|
<image class="top-banner" :src="showImg(item.head_img)" mode="aspectFill"></image> |
||||
|
</swiper-item> |
||||
|
</swiper> |
||||
|
|
||||
|
<view class="dot-container"> |
||||
|
<view :class="['dot-line',index==swiperIndex?'active':'']" v-for="(item, index) in topBanner" :key="index"></view> |
||||
|
</view> |
||||
|
|
||||
|
</view> |
||||
|
<view class="desc-box"> |
||||
|
<view class="title-sec" style="color: black;"> |
||||
|
关于有感商品 |
||||
|
</view> |
||||
|
<view class=""> |
||||
|
寻常商品,满足你的日常所需;而「有感商品」,则回应你的精神所向。 |
||||
|
</view> |
||||
|
<view class=""> |
||||
|
我们坚信“意义大于产品”。这里的每一件商品都诞生于EPIC SOUL「交响」阅读体的史诗,是精神漫游的实体回响。它的存在,是为了让你在消费中完成一次次深刻的情感连接与自我认同。 |
||||
|
</view> |
||||
|
<view class=""> |
||||
|
在这里,消费的终点不是拥有,而是更深刻的连接与共鸣。欢迎探索,一件件写满故事与想象力的生活信物。 |
||||
|
</view> |
||||
|
</view> |
||||
|
<view class="product-content"> |
||||
|
<!-- <image class="head-img" src="https://static.ticket.sz-trip.com/uploads/20250625/e3112c280ef9761af741907a737ef221.png"></image> --> |
||||
|
<view class="title-sec"> |
||||
|
有感商品上新 |
||||
|
</view> |
||||
|
|
||||
|
<scroll-view style="width: 100%;" scroll-x> |
||||
|
<view class="product"> |
||||
|
<view class="item" v-for="(item,i) in list" :key="item.goods.id" @click="goDetailByType(item)"> |
||||
|
<image class="item-img" :src="showImg(item.goods.image)"></image> |
||||
|
<view class="content"> |
||||
|
<view class="title text-overflow">{{item.goods.title}}</view> |
||||
|
<view class="bottom"> |
||||
|
<view class="price">¥{{item.goods.money/100}}</view> |
||||
|
<image src="https://des.dayunyuanjian.cn/epicSoul/readingBody/gwc.png" class="buy-cart"></image> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
</view> |
||||
|
</scroll-view> |
||||
|
|
||||
|
<!-- <image style="margin: 53rpx 0 35rpx;" class="head-img" src="https://static.ticket.sz-trip.com/uploads/20250627/73153098ff5c115e02afb0328ade1e29.png"></image> --> |
||||
|
<view class="title-sec"> |
||||
|
有感商品精选 |
||||
|
</view> |
||||
|
<view class="img-container"> |
||||
|
<image v-for="(type,i) in typeList" :key="i" :src="showImg(type.img)" |
||||
|
@click="gotoPath(`/subPackages/haveFeeling/detailXiang?id=${type.id}`)"></image> |
||||
|
</view> |
||||
|
</view> |
||||
|
<!-- <CustomTabBar :currentTab="2" /> --> |
||||
|
<MusicControl /> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import headerVue from "@/components/header.vue" |
||||
|
import CustomTabBar from '@/components/CustomTabBar.vue'; |
||||
|
import MusicControl from '@/components/MusicControl.vue'; |
||||
|
import BackButton from "@/components/BackButton.vue"; |
||||
|
export default { |
||||
|
components: {CustomTabBar,headerVue,MusicControl,BackButton}, |
||||
|
data() { |
||||
|
return { |
||||
|
topBanner: [], |
||||
|
list: [], |
||||
|
swiperIndex: 0, |
||||
|
typeList: [], |
||||
|
} |
||||
|
}, |
||||
|
onLoad() { |
||||
|
|
||||
|
}, |
||||
|
onReady() { |
||||
|
this.getProduct() |
||||
|
this.getTypes() |
||||
|
this.getList() |
||||
|
}, |
||||
|
onReachBottom() { |
||||
|
|
||||
|
}, |
||||
|
methods: { |
||||
|
swiperChange(e) { |
||||
|
this.swiperIndex = e.detail.current |
||||
|
}, |
||||
|
|
||||
|
viewDetail(item) { |
||||
|
if (item.url) { |
||||
|
uni.navigateTo({ |
||||
|
url:"/subPackages/webPage/webPage?url="+encodeURIComponent(item.url) |
||||
|
}) |
||||
|
return |
||||
|
} |
||||
|
uni.navigateTo({ |
||||
|
url:'/subPackages/letter/detail?id='+item.id |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
getProduct () { |
||||
|
this.Post({ |
||||
|
tag_id: 40, |
||||
|
offset: 0, |
||||
|
},'/api/tag/getGoodsByTagId').then(res => { |
||||
|
this.list = res.data; |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
|
||||
|
getList() { |
||||
|
// 大轮播 |
||||
|
this.Post({ |
||||
|
type_id: 3, |
||||
|
position: 18, |
||||
|
}, '/api/adv/getAdv').then(res => { |
||||
|
if(res.data) { |
||||
|
this.topBanner = res.data; |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 获取分类图片 |
||||
|
getTypes () { |
||||
|
this.Post({ |
||||
|
parent_id: 0, |
||||
|
}, '/api/goods/type').then(res => { |
||||
|
if(res.data) { |
||||
|
this.typeList = res.data; |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
|
||||
|
.bg { |
||||
|
min-height: 100vh; |
||||
|
background: #FFFFFF; |
||||
|
padding-bottom: 200rpx; |
||||
|
} |
||||
|
.banner-content{ |
||||
|
width: 100%; |
||||
|
height: 496.4rpx; |
||||
|
position: relative; |
||||
|
.top-banner { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
||||
|
.dot-container{ |
||||
|
position: absolute; |
||||
|
bottom: 43rpx; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
width: 100%; |
||||
|
left: 0; |
||||
|
.dot-line{ |
||||
|
width: 52rpx; |
||||
|
height: 4rpx; |
||||
|
margin: 0 8rpx; |
||||
|
background: RGBA(189, 170, 173, 0.8); |
||||
|
&.active{ |
||||
|
background: RGBA(255, 255, 255, 0.8); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.head-img{ |
||||
|
width: 697.24rpx; |
||||
|
height: 42.57rpx; |
||||
|
margin: 0 auto; |
||||
|
display: block; |
||||
|
} |
||||
|
.product-content{ |
||||
|
padding: 63rpx 26rpx 0; |
||||
|
padding-top: 0; |
||||
|
.product{ |
||||
|
padding: 36rpx 0 0; |
||||
|
padding-top: 0; |
||||
|
display: flex; |
||||
|
flex-wrap: nowrap; |
||||
|
// justify-content: space-between; |
||||
|
.item{ |
||||
|
width: 214.69rpx; |
||||
|
margin-right: 20rpx; |
||||
|
} |
||||
|
.item-img{ |
||||
|
width: 214.69rpx; |
||||
|
height: 286.33rpx; |
||||
|
} |
||||
|
.content{ |
||||
|
height: 80rpx; |
||||
|
padding: 0rpx 4rpx; |
||||
|
font-weight: 400; |
||||
|
font-size: 24rpx; |
||||
|
color: #000000; |
||||
|
} |
||||
|
.bottom{ |
||||
|
display: flex; |
||||
|
padding-top: 13rpx; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
} |
||||
|
.buy-cart{ |
||||
|
width: 28rpx; |
||||
|
height: 24rpx; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.img-container{ |
||||
|
width: 100%; |
||||
|
image{ |
||||
|
display: block; |
||||
|
width: 100%; |
||||
|
height: 314rpx; |
||||
|
border-radius: 40rpx; |
||||
|
margin-bottom: 20rpx; |
||||
|
} |
||||
|
} |
||||
|
.line{ |
||||
|
width: 220rpx; |
||||
|
height: 1rpx; |
||||
|
background: #E4E4E4; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
.head-img-yougan{ |
||||
|
width: 183.45rpx; |
||||
|
height: 42.57rpx; |
||||
|
} |
||||
|
.title-sec{ |
||||
|
font-size: 34rpx; |
||||
|
font-weight: 500; |
||||
|
margin: 30rpx 0; |
||||
|
} |
||||
|
.desc-box{ |
||||
|
padding: 0 20rpx; |
||||
|
color: #616161; |
||||
|
margin: 30rpx 0; |
||||
|
font-size: 24rpx; |
||||
|
padding: 0 30rpx; |
||||
|
view{ |
||||
|
margin-bottom: 20rpx; |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,599 @@ |
|||||
|
<template> |
||||
|
<view class="page-container"> |
||||
|
<!-- 导航栏组件 --> |
||||
|
<headerVue fixed></headerVue> |
||||
|
|
||||
|
<!-- 灵动岛组件 --> |
||||
|
<DynamicIsland |
||||
|
ref="dynamicIsland" |
||||
|
:page-id="'timeShopBank_page'" |
||||
|
:style-type="'timeShop'" |
||||
|
/> |
||||
|
<view class="desc-box"> |
||||
|
<view class=""> 欢迎来到「旅行时间行」,你的精神财富储蓄所。 </view> |
||||
|
<view class=""> |
||||
|
在这里,你的每一次人文漫游、每一次灵感闪现,都值得被郑重记录。我们鼓励你分享高质量的图文笔记,将旅途中的美与感动,化为这座精神星球上的璀璨星辰。 |
||||
|
</view> |
||||
|
<view class=""> |
||||
|
为他人的美好驻足、点赞、留言,每一次真诚的互动,都是在为你的时间银行存入一笔宝贵的「精神货币」。这些资产不仅可以兑换独家福利与实体好物,更能为你解锁专属的荣誉身份,让你成为这座星球上最闪耀的共创者。 |
||||
|
</view> |
||||
|
<view class=""> 即刻发布你的第一篇笔记,开启你的财富积累之旅吧! </view> |
||||
|
</view> |
||||
|
|
||||
|
<image |
||||
|
style="width: 700rpx; height: 14rpx; margin: 20rpx auto; display: block" |
||||
|
:src="showImg('/uploads/20250829/f7214bc2a4f4e236561de893ca7b9113.png')" |
||||
|
></image> |
||||
|
|
||||
|
<!-- Tab切换组件 --> |
||||
|
<view class="tab-container"> |
||||
|
<view class="tab-wrapper"> |
||||
|
<view |
||||
|
v-for="(tab, index) in tabs" |
||||
|
:key="index" |
||||
|
class="tab-item" |
||||
|
:class="{ active: currentTab === index }" |
||||
|
@click="switchTab(index)" |
||||
|
> |
||||
|
<text class="tab-text">{{ tab.name }}</text> |
||||
|
<view v-if="currentTab === index" class="tab-indicator"></view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 内容区域 --> |
||||
|
<view class="content-area"> |
||||
|
<!-- <view v-if="currentTab == 0" class="notes-content"> |
||||
|
<WaterfallLayout |
||||
|
:items="waterfallItems" |
||||
|
:column-count="2" |
||||
|
:column-gap="16" |
||||
|
:item-gap="16" |
||||
|
@item-click="handleItemClick" |
||||
|
@like-change="handleNoteLikeChange" |
||||
|
style="margin-top: 20rpx" |
||||
|
/> |
||||
|
</view> --> |
||||
|
<view v-if="currentTab === 0" class="follow-content recommend-content"> |
||||
|
<text class="coming-soon">笔记功能开发中...</text> |
||||
|
</view> |
||||
|
<!-- 关注tab内容 --> |
||||
|
<view v-if="currentTab == 1" class="follow-content recommend-content"> |
||||
|
<!-- <FollowTab ref="followTab" /> --> |
||||
|
<text class="coming-soon">关注功能开发中...</text> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 推荐tab内容 --> |
||||
|
<!-- <view v-if="currentTab == 2" class="notes-content"> |
||||
|
<WaterfallLayout |
||||
|
:items="waterfallItems" |
||||
|
:column-count="2" |
||||
|
:column-gap="16" |
||||
|
:item-gap="16" |
||||
|
@item-click="handleItemClick" |
||||
|
@like-change="handleNoteLikeChange" |
||||
|
style="margin-top: 20rpx" |
||||
|
/> |
||||
|
</view> --> |
||||
|
<view v-if="currentTab ==2" class="notes-content recommend-content"> |
||||
|
<text class="coming-soon">笔记功能开发中...</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
<!-- <view class="fab-container" v-if="canPublish"> --> |
||||
|
<!-- <image |
||||
|
@click="goToPublish" |
||||
|
:src="showImg('/uploads/20250825/7ea7864b8abb89c3dd7834f025e49b3f.png')" |
||||
|
style="width: 91rpx; height: 91rpx" |
||||
|
></image> |
||||
|
</view> --> |
||||
|
<!-- 控制按钮 --> |
||||
|
<!-- <view class="controls"> |
||||
|
<button @click="addRandomItem" class="control-btn primary"> |
||||
|
添加项目 |
||||
|
</button> |
||||
|
<button @click="clearAllItems" class="control-btn danger">清空</button> |
||||
|
</view> --> |
||||
|
|
||||
|
<!-- 底部占位区域,防止内容被TabBar遮挡 --> |
||||
|
<view class="tab-bar-placeholder"></view> |
||||
|
|
||||
|
<!-- 底部TabBar组件 --> |
||||
|
<CustomTabBar :currentTab="2" /> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import WaterfallLayout from "@/components/WaterfallLayout.vue"; |
||||
|
import headerVue from "@/components/header.vue"; |
||||
|
import CustomTabBar from "@/components/CustomTabBar.vue"; |
||||
|
import DynamicIsland from "@/components/DynamicIsland.vue"; |
||||
|
import FollowTab from "./components/FollowTab.vue"; |
||||
|
|
||||
|
export default { |
||||
|
name: "TimeShopBank", |
||||
|
components: { |
||||
|
WaterfallLayout, |
||||
|
headerVue, |
||||
|
CustomTabBar, |
||||
|
DynamicIsland, |
||||
|
FollowTab, |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
currentTab: 2, // 默认选中"笔记" |
||||
|
tabs: [ |
||||
|
{ name: "笔记", id: "notes" }, |
||||
|
{ name: "关注", id: "follow" }, |
||||
|
{ name: "推荐", id: "recommend" }, |
||||
|
], |
||||
|
waterfallItems: [], |
||||
|
// 分页相关数据 |
||||
|
pageNum: 1, |
||||
|
pageSize: 10, |
||||
|
loading: false, |
||||
|
hasMore: true, |
||||
|
autoAddEnabled: false, |
||||
|
whiteListUsers: [], // 发布功能白名单用户ID列表 |
||||
|
}; |
||||
|
}, |
||||
|
computed: { |
||||
|
// 判断用户是否有发布权限 |
||||
|
canPublish() { |
||||
|
// 如果白名单为空,则所有用户都可以发布 |
||||
|
if (!this.whiteListUsers || this.whiteListUsers.length === 0) { |
||||
|
return true; |
||||
|
} |
||||
|
// 如果用户未登录,则不显示发布按钮 |
||||
|
if (!this.userInfo || !this.userInfo.id) { |
||||
|
return false; |
||||
|
} |
||||
|
// 检查用户ID是否在白名单中 |
||||
|
return this.whiteListUsers.includes(this.userInfo.id.toString()); |
||||
|
}, |
||||
|
}, |
||||
|
onLoad() { |
||||
|
this.userInfo = |
||||
|
(uni.getStorageSync("userInfo") && |
||||
|
JSON.parse(uni.getStorageSync("userInfo"))) || |
||||
|
this.$store.state.user.userInfo || |
||||
|
{}; |
||||
|
// 加载推荐列表数据 |
||||
|
this.getRecommendList(1); |
||||
|
|
||||
|
// 监听笔记点赞变更事件 |
||||
|
uni.$on("note-like-change", this.handleNoteLikeChange); |
||||
|
}, |
||||
|
onShow() { |
||||
|
if (this.userInfo && this.userInfo.id) { |
||||
|
this.getUserInfo(); |
||||
|
this.getWhiteListConfig(); // 获取白名单配置 |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 页面卸载时移除事件监听 |
||||
|
onUnload() { |
||||
|
uni.$off("note-like-change", this.handleNoteLikeChange); |
||||
|
}, |
||||
|
|
||||
|
// 页面滚动到底部时触发 |
||||
|
onReachBottom() { |
||||
|
this.loadMoreItems(); |
||||
|
}, |
||||
|
|
||||
|
// 页面滚动时触发 - 用于灵动岛样式切换 |
||||
|
onPageScroll(e) { |
||||
|
// 只触发带页面ID的事件,避免不同页面间的状态冲突 |
||||
|
uni.$emit("pageScroll_timeShopBank_page", e.scrollTop); |
||||
|
}, |
||||
|
methods: { |
||||
|
getUserInfo() { |
||||
|
this.Post({}, "/framework/user/getInfo", "DES").then((res) => { |
||||
|
res.data.token = this.userInfo.token; |
||||
|
uni.setStorageSync("userInfo", JSON.stringify(res.data)); |
||||
|
this.userInfo = res.data; |
||||
|
this.$nextTick(() => { |
||||
|
this.$refs.dynamicIsland.getUserInfo(); |
||||
|
}); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 获取白名单配置 |
||||
|
getWhiteListConfig() { |
||||
|
this.Post({}, `/system/config/configKey/note_white_user`, "DES") |
||||
|
.then((res) => { |
||||
|
if (res.code === 200) { |
||||
|
// 将逗号分隔的字符串转换为数组 |
||||
|
this.whiteListUsers = res.msg.split(",").map((item) => item.trim()); |
||||
|
console.log("白名单用户列表:", this.whiteListUsers); |
||||
|
} |
||||
|
}) |
||||
|
.catch((err) => { |
||||
|
console.error("获取白名单配置失败:", err); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 切换到关注标签时刷新关注列表 |
||||
|
refreshFollowList() { |
||||
|
// 获取FollowTab组件实例并调用其刷新方法 |
||||
|
if (this.$refs.followTab) { |
||||
|
this.$refs.followTab.getFollowList(); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 获取推荐列表数据 |
||||
|
getRecommendList(type = 1) { |
||||
|
if (type == 0) { |
||||
|
let token = uni.getStorageSync("token1"); |
||||
|
if (!token) { |
||||
|
uni.showToast({ |
||||
|
title: "请先登录", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
uni.navigateTo({ |
||||
|
url: "/pages/login/login", |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
if (this.loading) return; |
||||
|
|
||||
|
this.loading = true; |
||||
|
const params = { |
||||
|
pageNum: this.pageNum, |
||||
|
pageSize: this.pageSize, |
||||
|
type: type, // 0: 笔记tab, 1: 推荐tab |
||||
|
}; |
||||
|
|
||||
|
this.Post(params, "/framework/note/list", "DES") |
||||
|
.then((res) => { |
||||
|
if (res.code === 200 && res.rows) { |
||||
|
const newItems = res.rows || []; |
||||
|
|
||||
|
if (this.pageNum === 1) { |
||||
|
// 首次加载或刷新 |
||||
|
this.waterfallItems = newItems; |
||||
|
} else { |
||||
|
// 加载更多 |
||||
|
this.waterfallItems.push(...newItems); |
||||
|
} |
||||
|
|
||||
|
// 判断是否还有更多数据 |
||||
|
this.hasMore = newItems.length === this.pageSize; |
||||
|
} |
||||
|
}) |
||||
|
.catch((error) => { |
||||
|
console.error("获取推荐列表失败:", error); |
||||
|
uni.showToast({ |
||||
|
title: "加载失败,请重试", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
}) |
||||
|
.finally(() => { |
||||
|
this.loading = false; |
||||
|
}); |
||||
|
}, |
||||
|
// 清空所有项目 |
||||
|
clearAllItems() { |
||||
|
uni.showModal({ |
||||
|
title: "确认清空", |
||||
|
content: "确定要清空所有项目吗?", |
||||
|
success: (res) => { |
||||
|
if (res.confirm) { |
||||
|
this.waterfallItems = []; |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 处理项目点击 |
||||
|
handleItemClick(item) { |
||||
|
console.log(item); |
||||
|
// 跳转到笔记详情页面 |
||||
|
uni.navigateTo({ |
||||
|
url: `/pages/notes/detail?id=${item.id}`, |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 跳转到发布页面 |
||||
|
goToPublish() { |
||||
|
console.log(this.userInfo, "userInfo"); |
||||
|
if (!this.userInfo.id) { |
||||
|
uni.showToast({ |
||||
|
title: "请先登录", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
uni.navigateTo({ |
||||
|
url: "/pages/login/login", |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 检查当前用户是否在白名单中 |
||||
|
const userId = this.userInfo.id.toString(); |
||||
|
if ( |
||||
|
this.whiteListUsers.length > 0 && |
||||
|
!this.whiteListUsers.includes(userId) |
||||
|
) { |
||||
|
uni.showToast({ |
||||
|
title: "暂无发布权限", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
uni.navigateTo({ |
||||
|
url: "/pages/notes/publish", |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 处理项目添加 |
||||
|
handleItemAdded(item) {}, |
||||
|
|
||||
|
// 处理自动添加请求 |
||||
|
handleAutoAddRequest() { |
||||
|
this.loadMoreItems(); |
||||
|
}, |
||||
|
|
||||
|
// 加载更多项目(到达底部时调用) |
||||
|
loadMoreItems() { |
||||
|
if (!this.loading && this.hasMore) { |
||||
|
this.pageNum++; |
||||
|
// 根据当前tab传递不同的type: 0-笔记, 1-推荐 |
||||
|
const type = this.currentTab === 0 ? 0 : 1; |
||||
|
this.getRecommendList(type); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// Tab切换方法 |
||||
|
switchTab(index) { |
||||
|
this.currentTab = index; |
||||
|
// 根据不同tab加载不同内容 |
||||
|
this.loadTabContent(this.tabs[index].id); |
||||
|
|
||||
|
// 如果切换到关注标签,刷新关注列表 |
||||
|
if (this.tabs[index].id === "follow") { |
||||
|
this.refreshFollowList(); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 加载tab对应的内容 |
||||
|
loadTabContent(tabId) { |
||||
|
// 这里可以根据不同的tabId加载不同的数据 |
||||
|
if (tabId === "notes") { |
||||
|
// 笔记tab: 重置分页数据并加载笔记列表 |
||||
|
this.pageNum = 1; |
||||
|
this.waterfallItems = []; |
||||
|
this.hasMore = true; |
||||
|
this.getRecommendList(0); |
||||
|
} else if (tabId === "recommend") { |
||||
|
// 推荐tab: 无论是首次加载还是再次点击都重置分页并重新加载数据 |
||||
|
this.pageNum = 1; |
||||
|
this.waterfallItems = []; |
||||
|
this.hasMore = true; |
||||
|
this.getRecommendList(1); |
||||
|
} |
||||
|
// 关注tab不需要重新加载数据,保持现有数据 |
||||
|
}, |
||||
|
|
||||
|
// 处理笔记点赞变更事件 |
||||
|
handleNoteLikeChange(data) { |
||||
|
console.log(data, "data"); |
||||
|
if (!data || !data.noteId) return; |
||||
|
|
||||
|
// 在瀑布流中查找并更新对应的笔记 |
||||
|
const updateItemInList = (items) => { |
||||
|
for (let i = 0; i < items.length; i++) { |
||||
|
if (items[i].id == data.noteId) { |
||||
|
// 更新点赞状态和数量 |
||||
|
items[i].userLiked = data.isLiked; |
||||
|
items[i].likeCount = data.likeCount; |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
return false; |
||||
|
}; |
||||
|
|
||||
|
// 尝试在当前显示的笔记列表中更新 |
||||
|
const updated = updateItemInList(this.waterfallItems); |
||||
|
console.log(updated, "updated"); |
||||
|
// 如果找到并更新了笔记,强制视图更新 |
||||
|
if (updated) { |
||||
|
// 使用Vue的响应式更新机制 |
||||
|
this.$forceUpdate(); |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
||||
|
<style> |
||||
|
page { |
||||
|
background-color: white; |
||||
|
} |
||||
|
</style> |
||||
|
<style lang="scss" scoped> |
||||
|
.page-container { |
||||
|
min-height: 100vh; |
||||
|
background: white; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.controls { |
||||
|
display: flex; |
||||
|
flex-wrap: wrap; |
||||
|
gap: 20rpx; |
||||
|
justify-content: center; |
||||
|
margin: 40rpx 0; |
||||
|
padding: 0 20rpx; |
||||
|
} |
||||
|
|
||||
|
.control-btn { |
||||
|
padding: 24rpx 48rpx; |
||||
|
border-radius: 48rpx; |
||||
|
font-size: 28rpx; |
||||
|
border: none; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
background: white; |
||||
|
color: #333; |
||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.control-btn.primary { |
||||
|
background: #ff4757; |
||||
|
color: white; |
||||
|
} |
||||
|
|
||||
|
.control-btn.danger { |
||||
|
background: #ff6b6b; |
||||
|
color: white; |
||||
|
} |
||||
|
|
||||
|
.control-btn:active { |
||||
|
transform: scale(0.95); |
||||
|
} |
||||
|
|
||||
|
.tab-bar-placeholder { |
||||
|
height: 120rpx; |
||||
|
} |
||||
|
|
||||
|
/* Tab切换样式 */ |
||||
|
.tab-container { |
||||
|
background: #ffffff; |
||||
|
padding: 0 32rpx; |
||||
|
margin-top: 20rpx; |
||||
|
margin-bottom: 20rpx; |
||||
|
} |
||||
|
|
||||
|
.tab-wrapper { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-around; |
||||
|
height: 88rpx; |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
.tab-item { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
height: 100%; |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
.tab-text { |
||||
|
font-size: 32rpx; |
||||
|
color: #999999; |
||||
|
font-weight: 400; |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.tab-item.active .tab-text { |
||||
|
color: #333333; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.tab-indicator { |
||||
|
position: absolute; |
||||
|
bottom: 0; |
||||
|
left: 50%; |
||||
|
transform: translateX(-50%); |
||||
|
width: 80rpx; |
||||
|
height: 4rpx; |
||||
|
background: #33fefe; |
||||
|
border-radius: 3rpx; |
||||
|
animation: slideIn 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
@keyframes slideIn { |
||||
|
from { |
||||
|
width: 0; |
||||
|
opacity: 0; |
||||
|
} |
||||
|
to { |
||||
|
width: 80rpx; |
||||
|
opacity: 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 悬浮发布按钮 */ |
||||
|
.fab-container { |
||||
|
position: fixed; |
||||
|
bottom: 250rpx; |
||||
|
right: 40rpx; |
||||
|
z-index: 999; |
||||
|
} |
||||
|
|
||||
|
.fab-btn { |
||||
|
width: 80rpx; |
||||
|
height: 80rpx; |
||||
|
border-radius: 40rpx; |
||||
|
background: linear-gradient(135deg, #ff4757, #ff6b7a); |
||||
|
color: #fff; |
||||
|
border: none; |
||||
|
box-shadow: 0 8rpx 24rpx rgba(255, 71, 87, 0.4); |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
transition: all 0.3s ease; |
||||
|
|
||||
|
&:active { |
||||
|
transform: scale(0.95); |
||||
|
box-shadow: 0 4rpx 12rpx rgba(255, 71, 87, 0.3); |
||||
|
} |
||||
|
|
||||
|
.fab-icon { |
||||
|
font-size: 48rpx; |
||||
|
font-weight: 300; |
||||
|
line-height: 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 内容区域样式 */ |
||||
|
.content-area { |
||||
|
flex: 1; |
||||
|
overflow: hidden; |
||||
|
padding: 10rpx 0; |
||||
|
} |
||||
|
|
||||
|
.notes-content { |
||||
|
position: relative; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.follow-content { |
||||
|
height: 100%; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
|
||||
|
.recommend-content { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
height: 400rpx; |
||||
|
|
||||
|
.coming-soon { |
||||
|
font-size: 28rpx; |
||||
|
color: #999; |
||||
|
} |
||||
|
} |
||||
|
.desc-box { |
||||
|
padding: 0 20rpx; |
||||
|
color: #616161; |
||||
|
margin: 30rpx 0; |
||||
|
font-size: 24rpx; |
||||
|
padding: 0 40rpx; |
||||
|
view { |
||||
|
margin-bottom: 20rpx; |
||||
|
&:nth-child(1) { |
||||
|
font-size: 24rpx; |
||||
|
font-weight: bold; |
||||
|
margin-bottom: 20rpx; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 自定义样式已移至WaterfallLayout组件内部 */ |
||||
|
</style> |
||||
File diff suppressed because it is too large
@ -0,0 +1,623 @@ |
|||||
|
<template> |
||||
|
<view class="publish-container"> |
||||
|
<!-- 内容区域 --> |
||||
|
<view class="content-scroll"> |
||||
|
<!-- 图片区域 --> |
||||
|
<view class="image-section"> |
||||
|
<uni-file-picker |
||||
|
v-model="selectedImages" |
||||
|
mode="grid" |
||||
|
file-mediatype="image" |
||||
|
file-extname="png,jpg,jpeg" |
||||
|
:auto-upload="false" |
||||
|
@select="onImageSelect" |
||||
|
@delete="onImageDelete" |
||||
|
></uni-file-picker> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 标题区域 --> |
||||
|
<view class="title-section"> |
||||
|
<input |
||||
|
class="title-input" |
||||
|
v-model="noteForm.title" |
||||
|
placeholder="请输入标题..." |
||||
|
maxlength="100" |
||||
|
auto-height |
||||
|
/> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 详情区域 --> |
||||
|
<view class="content-section"> |
||||
|
<textarea |
||||
|
class="content-input" |
||||
|
v-model="noteForm.content" |
||||
|
placeholder="分享你的想法..." |
||||
|
maxlength="2000" |
||||
|
auto-height |
||||
|
/> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 快速标签区域 --> |
||||
|
<view class="quick-tags-section"> |
||||
|
<view class="section-title">快速标签</view> |
||||
|
<scroll-view |
||||
|
class="quick-tags-scroll" |
||||
|
scroll-x="true" |
||||
|
show-scrollbar="false" |
||||
|
> |
||||
|
<view class="quick-tags-list"> |
||||
|
<view |
||||
|
class="quick-tag-item" |
||||
|
v-for="tag in quickTags" |
||||
|
:key="tag" |
||||
|
@click="insertQuickTag(tag)" |
||||
|
> |
||||
|
#{{ tag.name }} |
||||
|
</view> |
||||
|
</view> |
||||
|
</scroll-view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 已添加标签 --> |
||||
|
<view class="selected-tags-section" v-if="noteForm.tags.length"> |
||||
|
<scroll-view |
||||
|
class="selected-tags-scroll" |
||||
|
scroll-x |
||||
|
:show-scrollbar="false" |
||||
|
> |
||||
|
<view class="selected-tags-list"> |
||||
|
<view |
||||
|
class="selected-tag-item" |
||||
|
v-for="(tag, index) in noteForm.tags" |
||||
|
:key="index" |
||||
|
@click="removeTag(index)" |
||||
|
> |
||||
|
<view class="tag-text">#{{ tag.name }}</view> |
||||
|
<view class="tag-close">×</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</scroll-view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 底部占位 --> |
||||
|
<view class="bottom-placeholder"></view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 底部发布按钮 --> |
||||
|
<view class="bottom-publish"> |
||||
|
<button class="publish-btn" @click="publishNote"> |
||||
|
{{ isEditMode ? "保存修改" : "发布笔记" }} |
||||
|
</button> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: "PublishNote", |
||||
|
data() { |
||||
|
return { |
||||
|
selectedImages: [], |
||||
|
noteForm: { |
||||
|
id: "", // 笔记ID,编辑模式下有值 |
||||
|
title: "", |
||||
|
content: "", |
||||
|
tags: [], // 存储选中的标签对象 {id, name} |
||||
|
images: [], |
||||
|
}, |
||||
|
quickTags: [], // 从接口获取的标签列表 |
||||
|
imageStyles: { |
||||
|
height: "200rpx", // 边框高度 |
||||
|
width: "200rpx", // 边框宽度 |
||||
|
}, |
||||
|
isEditMode: false, // 是否是编辑模式 |
||||
|
}; |
||||
|
}, |
||||
|
computed: { |
||||
|
canPublish() { |
||||
|
return ( |
||||
|
(this.noteForm.title.trim() || this.noteForm.content.trim()) && |
||||
|
this.selectedImages && |
||||
|
this.selectedImages.length > 0 |
||||
|
); |
||||
|
}, |
||||
|
}, |
||||
|
onLoad(options) { |
||||
|
// 先获取标签列表 |
||||
|
this.getTagList().then(() => { |
||||
|
// 检查是否有noteId参数,如果有则是编辑模式 |
||||
|
if (options && options.id) { |
||||
|
this.noteForm.id = options.id; |
||||
|
this.isEditMode = true; |
||||
|
this.loadNoteDetail(); |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
methods: { |
||||
|
// 获取标签列表 |
||||
|
async getTagList() { |
||||
|
return new Promise(async (resolve) => { |
||||
|
try { |
||||
|
const res = await this.Post({}, "/framework/tag/list", "DES"); |
||||
|
if (res && res.data) { |
||||
|
this.quickTags = res.data; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error("获取标签列表失败:", error); |
||||
|
// 如果接口失败,可以设置默认标签 |
||||
|
this.quickTags = [ |
||||
|
{ id: "1", name: "DES" }, |
||||
|
{ id: "2", name: "AGENT" }, |
||||
|
{ id: "3", name: "时间银行" }, |
||||
|
{ id: "4", name: "阅读体验" }, |
||||
|
{ id: "5", name: "时间力" }, |
||||
|
]; |
||||
|
} |
||||
|
resolve(); |
||||
|
}); |
||||
|
}, |
||||
|
// 返回上一页 |
||||
|
goBack() { |
||||
|
if (this.canPublish) { |
||||
|
uni.showModal({ |
||||
|
title: "确认退出", |
||||
|
content: "退出后内容将不会保存,确定要退出吗?", |
||||
|
success: (res) => { |
||||
|
if (res.confirm) { |
||||
|
uni.navigateBack(); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
} else { |
||||
|
uni.navigateBack(); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 图片选择事件 |
||||
|
onImageSelect(e) { |
||||
|
console.log("选择图片·:", e); |
||||
|
// selectedImages已经通过v-model自动更新,立即上传新选择的图片 |
||||
|
this.uploadNewImages(e.tempFiles || []); |
||||
|
}, |
||||
|
|
||||
|
// 图片删除事件 |
||||
|
onImageDelete(e) { |
||||
|
let index = e.index; |
||||
|
// selectedImages已经通过v-model自动更新,需要同步更新noteForm.images |
||||
|
// 根据当前selectedImages的数量来调整noteForm.images |
||||
|
if (this.noteForm.images && this.noteForm.images.length > index) { |
||||
|
this.noteForm.images.splice(index, 1); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 插入快速标签 |
||||
|
insertQuickTag(tag) { |
||||
|
// 检查是否已经添加了该标签 |
||||
|
const existingTag = this.noteForm.tags.find((t) => t.id == tag.id); |
||||
|
if (!existingTag) { |
||||
|
this.noteForm.tags.unshift(tag); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 移除标签 |
||||
|
removeTag(index) { |
||||
|
this.noteForm.tags.splice(index, 1); |
||||
|
}, |
||||
|
|
||||
|
// 上传单个图片文件 |
||||
|
async uploadSingleImage(file) { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
const token = uni.getStorageSync("userInfo") |
||||
|
? JSON.parse(uni.getStorageSync("userInfo")).token |
||||
|
: ""; |
||||
|
uni.uploadFile({ |
||||
|
url: this.NEWAPIURL_DES + "/system/oss/upload", // 替换为你的上传接口 |
||||
|
filePath: file.url || file.path, |
||||
|
name: "file", |
||||
|
header: { |
||||
|
token: token || "", |
||||
|
// 添加必要的请求头,如token等 |
||||
|
}, |
||||
|
success: (res) => { |
||||
|
console.log(res); |
||||
|
try { |
||||
|
const data = JSON.parse(res.data); |
||||
|
if (data.code === 200) { |
||||
|
resolve(data.data.url); // 返回服务器返回的图片URL |
||||
|
} else { |
||||
|
reject(new Error(data.message || "上传失败")); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
reject(new Error("解析响应失败")); |
||||
|
} |
||||
|
}, |
||||
|
fail: (err) => { |
||||
|
reject(err); |
||||
|
}, |
||||
|
}); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
// 上传新选择的图片 |
||||
|
async uploadNewImages(newFiles) { |
||||
|
if (!newFiles || newFiles.length === 0) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
uni.showLoading({ title: "上传图片中..." }); |
||||
|
|
||||
|
const uploadPromises = newFiles.map((file) => |
||||
|
this.uploadSingleImage(file) |
||||
|
); |
||||
|
const uploadedUrls = await Promise.all(uploadPromises); |
||||
|
|
||||
|
// 将上传成功的URL添加到noteForm.images中 |
||||
|
this.noteForm.images = [ |
||||
|
...(this.noteForm.images || []), |
||||
|
...uploadedUrls, |
||||
|
]; |
||||
|
uni.hideLoading(); |
||||
|
} catch (error) { |
||||
|
uni.hideLoading(); |
||||
|
uni.showToast({ |
||||
|
title: error.message || "图片上传失败", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 批量上传图片(保留原方法,用于其他场景) |
||||
|
async uploadImages() { |
||||
|
if (!this.selectedImages || this.selectedImages.length === 0) { |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
const uploadPromises = this.selectedImages.map((file) => |
||||
|
this.uploadSingleImage(file) |
||||
|
); |
||||
|
|
||||
|
try { |
||||
|
const uploadedUrls = await Promise.all(uploadPromises); |
||||
|
return uploadedUrls; |
||||
|
} catch (error) { |
||||
|
throw new Error("图片上传失败: " + error.message); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 发布或更新笔记 |
||||
|
async publishNote() { |
||||
|
console.log(this.noteForm, "0000"); |
||||
|
if (!this.noteForm.images || this.noteForm.images.length === 0) { |
||||
|
uni.showToast({ |
||||
|
title: "请至少选择一张图片", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
if (!this.noteForm.title.trim() && !this.noteForm.content.trim()) { |
||||
|
uni.showToast({ |
||||
|
title: "请添加标题或内容", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
// 根据模式显示不同的加载提示 |
||||
|
uni.showLoading({ title: this.isEditMode ? "保存中..." : "发布中..." }); |
||||
|
// 图片已经在选择时上传,直接提交表单 |
||||
|
await this.submitNote(this.noteForm); |
||||
|
|
||||
|
uni.hideLoading(); |
||||
|
uni.showToast({ |
||||
|
title: this.isEditMode ? "修改成功" : "发布成功", |
||||
|
icon: "none", |
||||
|
duration: 2000, |
||||
|
}); |
||||
|
|
||||
|
// 延迟跳转,让用户看到成功提示 |
||||
|
setTimeout(() => { |
||||
|
uni.navigateBack(); |
||||
|
}, 800); |
||||
|
} catch (error) { |
||||
|
uni.hideLoading(); |
||||
|
uni.showToast({ |
||||
|
title: |
||||
|
error.data.msg || |
||||
|
(this.isEditMode ? "修改失败,请重试" : "发布失败,请重试"), |
||||
|
icon: "none", |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 加载笔记详情 |
||||
|
async loadNoteDetail() { |
||||
|
try { |
||||
|
uni.showLoading({ title: "加载中..." }); |
||||
|
const res = await this.Post( |
||||
|
{ noteId: this.noteForm.id }, |
||||
|
"/framework/note/getInfo/" + this.noteForm.id, |
||||
|
"DES" |
||||
|
); |
||||
|
|
||||
|
if (res.code === 200 && res.data) { |
||||
|
const noteData = res.data; |
||||
|
|
||||
|
// 填充表单数据 |
||||
|
this.noteForm.title = noteData.title || ""; |
||||
|
this.noteForm.content = noteData.content || ""; |
||||
|
|
||||
|
// 处理图片 |
||||
|
if (noteData.coverImage) { |
||||
|
const imageUrls = noteData.coverImage.split(","); |
||||
|
this.noteForm.images = imageUrls; |
||||
|
|
||||
|
// 为uni-file-picker准备数据 |
||||
|
this.selectedImages = imageUrls.map((url) => ({ |
||||
|
url: url, |
||||
|
extname: url.split(".").pop(), |
||||
|
name: url.split("/").pop(), |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
// 处理标签 |
||||
|
if (noteData.tagNames && noteData.tagIds) { |
||||
|
// 处理同时有tagNames和tagIds的情况 |
||||
|
const tagNames = noteData.tagNames.split(","); |
||||
|
const tagIds = noteData.tagIds.split(","); |
||||
|
|
||||
|
this.noteForm.tags = tagIds.map((id, index) => ({ |
||||
|
id: id, |
||||
|
name: tagNames[index] || "", |
||||
|
})); |
||||
|
} else if (noteData.tags) { |
||||
|
// 处理只有tags字段的情况(格式为"3,4"这样的ids集合) |
||||
|
const tagIds = noteData.tags.split(","); |
||||
|
|
||||
|
// 从quickTags中查找匹配的标签名称 |
||||
|
this.noteForm.tags = tagIds.map((id) => { |
||||
|
const matchedTag = this.quickTags.find((tag) => tag.id == id); |
||||
|
if (matchedTag) { |
||||
|
return { |
||||
|
id: id, |
||||
|
name: matchedTag.name, |
||||
|
}; |
||||
|
} else { |
||||
|
console.warn(`未找到ID为${id}的标签,请确保标签列表已正确加载`); |
||||
|
return { |
||||
|
id: id, |
||||
|
name: `标签${id}`, // 提供更友好的默认名称 |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// 如果没有找到任何匹配的标签,记录日志 |
||||
|
if ( |
||||
|
this.noteForm.tags.some( |
||||
|
(tag) => !this.quickTags.find((qt) => qt.id === tag.id) |
||||
|
) |
||||
|
) { |
||||
|
console.warn("部分标签未在标签列表中找到,可能需要刷新标签列表"); |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
uni.showToast({ |
||||
|
title: res.msg || "获取笔记详情失败", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error("加载笔记详情失败:", error); |
||||
|
uni.showToast({ |
||||
|
title: "获取笔记详情失败", |
||||
|
icon: "none", |
||||
|
}); |
||||
|
} finally { |
||||
|
uni.hideLoading(); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 提交笔记 |
||||
|
async submitNote(noteData) { |
||||
|
// 格式化数据以符合API要求 |
||||
|
const formData = { |
||||
|
title: noteData.title, |
||||
|
content: noteData.content, |
||||
|
tags: noteData.tags.map((tag) => tag.id).join(","), // 标签ID逗号分隔 |
||||
|
coverImage: noteData.images.join(","), // 图片URL逗号分隔 |
||||
|
method: "POST", |
||||
|
}; |
||||
|
|
||||
|
// 如果是编辑模式,添加笔记ID |
||||
|
if (this.isEditMode && this.noteForm.id) { |
||||
|
formData.id = this.noteForm.id; |
||||
|
formData.method = "PUT"; |
||||
|
|
||||
|
return this.Post(formData, "/framework/note/editNote", "DES"); |
||||
|
} else { |
||||
|
return this.Post(formData, "/framework/note/addNote", "DES"); |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.publish-container { |
||||
|
min-height: 100vh; |
||||
|
background: #fff; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
// 内容区域 |
||||
|
.content-scroll { |
||||
|
flex: 1; |
||||
|
padding: 0 30rpx; |
||||
|
} |
||||
|
|
||||
|
// 图片区域 |
||||
|
.image-section { |
||||
|
margin: 32rpx 0; |
||||
|
padding-bottom: 32rpx; |
||||
|
border-bottom: 1rpx solid #f0f0f0; |
||||
|
} |
||||
|
|
||||
|
// 标题区域 |
||||
|
.title-section { |
||||
|
margin: 32rpx 0; |
||||
|
padding-bottom: 32rpx; |
||||
|
border-bottom: 1rpx solid #f0f0f0; |
||||
|
|
||||
|
.title-input { |
||||
|
width: 100%; |
||||
|
min-height: 60rpx; |
||||
|
font-size: 36rpx; |
||||
|
font-weight: 600; |
||||
|
line-height: 1.4; |
||||
|
color: #333; |
||||
|
border: none; |
||||
|
padding: 0; |
||||
|
resize: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 详情区域 |
||||
|
.content-section { |
||||
|
margin: 32rpx 0; |
||||
|
padding-bottom: 32rpx; |
||||
|
border-bottom: 1rpx solid #f0f0f0; |
||||
|
|
||||
|
.content-input { |
||||
|
width: 100%; |
||||
|
min-height: 300rpx; |
||||
|
font-size: 28rpx; |
||||
|
line-height: 1.6; |
||||
|
color: #333; |
||||
|
border: none; |
||||
|
padding: 0; |
||||
|
resize: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 快速标签区域 |
||||
|
.quick-tags-section { |
||||
|
margin: 24rpx 0; |
||||
|
|
||||
|
.section-title { |
||||
|
font-size: 28rpx; |
||||
|
color: #666; |
||||
|
margin-bottom: 25rpx; |
||||
|
} |
||||
|
|
||||
|
.quick-tags-scroll { |
||||
|
width: 100%; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.quick-tags-list { |
||||
|
display: inline-flex; |
||||
|
gap: 16rpx; |
||||
|
padding-right: 32rpx; |
||||
|
width: max-content; |
||||
|
|
||||
|
.quick-tag-item { |
||||
|
flex-shrink: 0; |
||||
|
padding: 16rpx 24rpx; |
||||
|
background: #f8f9fa; |
||||
|
border-radius: 32rpx; |
||||
|
font-size: 28rpx; |
||||
|
color: #666; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
white-space: nowrap; |
||||
|
|
||||
|
&:active { |
||||
|
background: #e9ecef; |
||||
|
transform: scale(0.96); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 已选标签区域 |
||||
|
.selected-tags-section { |
||||
|
margin: 24rpx 0; |
||||
|
padding-top: 24rpx; |
||||
|
border-top: 1rpx solid #f0f0f0; |
||||
|
|
||||
|
.selected-tags-scroll { |
||||
|
width: 100%; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.selected-tags-list { |
||||
|
display: inline-flex; |
||||
|
gap: 16rpx; |
||||
|
padding-right: 32rpx; |
||||
|
width: max-content; |
||||
|
|
||||
|
.selected-tag-item { |
||||
|
flex-shrink: 0; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8rpx; |
||||
|
padding: 12rpx 20rpx; |
||||
|
background: linear-gradient(135deg, #fffdb7e6 0%, #97fffab5 100%); |
||||
|
border-radius: 32rpx; |
||||
|
cursor: pointer; |
||||
|
white-space: nowrap; |
||||
|
|
||||
|
.tag-text { |
||||
|
font-size: 26rpx; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.tag-close { |
||||
|
font-size: 24rpx; |
||||
|
color: #333; |
||||
|
opacity: 0.8; |
||||
|
margin-left: 8rpx; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 底部发布按钮 |
||||
|
.bottom-publish { |
||||
|
padding: 24rpx 32rpx; |
||||
|
background: #fff; |
||||
|
border-top: 1rpx solid #f0f0f0; |
||||
|
padding-bottom: max(24rpx, env(safe-area-inset-bottom)); |
||||
|
position: fixed; |
||||
|
bottom: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
z-index: 100; |
||||
|
.publish-btn { |
||||
|
width: 100%; |
||||
|
height: 88rpx; |
||||
|
color: #333333; |
||||
|
border-radius: 44rpx; |
||||
|
font-size: 32rpx; |
||||
|
font-weight: 600; |
||||
|
border: none; |
||||
|
transition: all 0.3s ease; |
||||
|
background: linear-gradient(135deg, #fffdb7e6 0%, #97fffab5 100%); |
||||
|
&:disabled { |
||||
|
background: linear-gradient(135deg, #ccc 0%, #ccc 100%); |
||||
|
} |
||||
|
|
||||
|
&:not(:disabled):active { |
||||
|
transform: scale(0.98); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.bottom-placeholder { |
||||
|
height: 180rpx; |
||||
|
padding-bottom: calc(24rpx + constant(safe-area-inset-bottom)); |
||||
|
padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); |
||||
|
} |
||||
|
</style> |
||||
File diff suppressed because it is too large
@ -0,0 +1,22 @@ |
|||||
|
@font-face { |
||||
|
font-family: "des"; /* Project id 5022607 */ |
||||
|
src: url('//at.alicdn.com/t/c/font_5022607_r74iwur9ql.woff2?t=1757989865177') format('woff2'), |
||||
|
url('//at.alicdn.com/t/c/font_5022607_r74iwur9ql.woff?t=1757989865177') format('woff'), |
||||
|
url('//at.alicdn.com/t/c/font_5022607_r74iwur9ql.ttf?t=1757989865177') format('truetype'); |
||||
|
} |
||||
|
|
||||
|
.des { |
||||
|
font-family: "des" !important; |
||||
|
font-size: 16px; |
||||
|
font-style: normal; |
||||
|
-webkit-font-smoothing: antialiased; |
||||
|
-moz-osx-font-smoothing: grayscale; |
||||
|
} |
||||
|
|
||||
|
.iconfont-jifen:before { |
||||
|
content: "\e614"; |
||||
|
} |
||||
|
|
||||
|
.iconfont-jifenduihuan:before { |
||||
|
content: "\e60b"; |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
# 这是一个空购物车图标的占位文件 |
||||
|
# 在实际项目中,请替换为真实的空购物车图标图片文件 |
||||
|
# 建议使用简洁的购物车图标,颜色为灰色或浅色 |
||||
|
After Width: | Height: | Size: 319 B |
@ -0,0 +1,2 @@ |
|||||
|
# 这是一个搜索图标的占位文件 |
||||
|
# 在实际项目中,请替换为真实的搜索图标图片文件 |
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue