feat(personal_collect,motion_reminder): 新增回收站与云端TTS语音功能

- 新增小程序个人收藏回收站页面,支持查看已删除收藏、恢复与彻底删除操作
- 修复个人收藏文件上传时的中文名乱码问题,新增原文件名透传参数
- 接入阿里云百炼TTS服务,实现运动提醒模块的云端语音合成与本地缓存,支持系统TTS与云端MP3双降级播放
- 新增语音引擎探测与异常预警UI,覆盖web端与移动端
- 优化web端订单盒页面布局,修复卡片操作按钮换行问题
- 新增调试探针脚本、ESLint配置与相关lint脚本
- 完善后端接口与路由,更新项目依赖与配置文件
This commit is contained in:
yangxiangyuan
2026-09-17 16:26:27 +08:00
parent d4bf3627fd
commit bbc7ca6523
29 changed files with 3228 additions and 163 deletions
+14
View File
@@ -0,0 +1,14 @@
# Windows
[Dd]esktop.ini
Thumbs.db
$RECYCLE.BIN/
# macOS
.DS_Store
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
# Node.js
node_modules/
+21 -2
View File
@@ -112,11 +112,30 @@ const personalCollect = {
method: 'DELETE',
data: { id },
}),
upload: (filePath, groupId = '') => uploadFile({
trash: () => request({
url: '/api/mp_personal_collect/kits/personal_collect/trash',
method: 'GET',
}),
restore: (id) => request({
url: '/api/mp_personal_collect/kits/personal_collect/trash/restore',
method: 'PUT',
data: { id },
}),
purge: (id) => request({
url: '/api/mp_personal_collect/kits/personal_collect/trash',
method: 'DELETE',
data: { id },
}),
upload: (filePath, groupId = '', originalName = '') => uploadFile({
url: '/api/mp_personal_collect/kits/personal_collect/upload',
filePath,
name: 'file',
formData: groupId ? { groupId } : {},
// 原始文件名由小程序随 multipart 透传(wx.chooseMessageFile 的 name 是 UTF-8 完好),
// 避免服务端只能拿到桌面微信传输中已被损坏的多字节文件名
formData: Object.assign(
groupId ? { groupId } : {},
originalName ? { originalName } : {}
),
}),
}
@@ -134,7 +134,7 @@ Page({
const idx = list.length - 1
try {
const res = await api.personalCollect.upload(tempFile.path || tempFile.tempFilePath, this.data.uploadGroupId)
const res = await api.personalCollect.upload(tempFile.path || tempFile.tempFilePath, this.data.uploadGroupId, tempFile.name || '')
const data = (res && res.data) || {}
const attachment = {
id: data.id || tempId,
@@ -13,6 +13,8 @@ Page({
loading: true,
keyword: '',
items: [],
trashItems: [],
trashMode: false,
error: '',
},
@@ -43,16 +45,28 @@ Page({
async refresh() {
this.setData({ loading: true, error: '' })
try {
const res = await api.personalCollect.list(this.data.keyword || '')
const raw = Array.isArray(res.data) ? res.data : []
const items = raw.map(item => ({
...item,
updatedAtText: formatDate(item.updatedAt || item.createdAt || ''),
tagsText: (item.tags || []).join(' · '),
previewImage: (item.attachments || []).find(a => a && a.isImage && a.filePath) || null,
fileCount: Array.isArray(item.attachments) ? item.attachments.length : 0,
}))
this.setData({ items })
if (this.data.trashMode) {
const res = await api.personalCollect.trash()
const raw = Array.isArray(res.data) ? res.data : []
const trashItems = raw.map(item => ({
...item,
deletedAtText: formatDate(item.updatedAt || item.createdAt || ''),
tagsText: (item.tags || []).join(' · '),
fileCount: Array.isArray(item.attachments) ? item.attachments.length : 0,
}))
this.setData({ trashItems })
} else {
const res = await api.personalCollect.list(this.data.keyword || '')
const raw = Array.isArray(res.data) ? res.data : []
const items = raw.map(item => ({
...item,
updatedAtText: formatDate(item.updatedAt || item.createdAt || ''),
tagsText: (item.tags || []).join(' · '),
previewImage: (item.attachments || []).find(a => a && a.isImage && a.filePath) || null,
fileCount: Array.isArray(item.attachments) ? item.attachments.length : 0,
}))
this.setData({ items })
}
} catch (e) {
this.setData({ error: e && e.message ? e.message : '加载失败' })
} finally {
@@ -107,4 +121,65 @@ Page({
url: '/kits/personal_collect/pages/edit/edit',
})
},
onSwitchToTrash() {
if (this.data.trashMode) return
this.setData({ trashMode: true, keyword: '', error: '' })
this.refresh()
},
onBackToList() {
if (!this.data.trashMode) return
this.setData({ trashMode: false, keyword: '', error: '' })
this.refresh()
},
onRestoreTap(e) {
const id = e.currentTarget.dataset.id
const title = e.currentTarget.dataset.title || '此收藏'
wx.showLoading({ title: '恢复中...', mask: true })
api.personalCollect.restore(id)
.then(() => {
wx.hideLoading()
wx.showToast({ title: '已恢复', icon: 'success' })
this.refresh()
})
.catch((err) => {
wx.hideLoading()
wx.showModal({
title: '恢复失败',
content: err && err.message ? err.message : '请重试',
showCancel: false,
})
})
},
onPurgeTap(e) {
const id = e.currentTarget.dataset.id
const title = e.currentTarget.dataset.title || '此收藏'
const count = Number(e.currentTarget.dataset.count || 0)
wx.showModal({
title: '彻底删除',
content: `「${title}」将从回收站永久删除${count > 0 ? ',同时清除 ' + count + ' 个附件文件' : ''},不可恢复!`,
confirmText: '彻底删除',
confirmColor: '#e05561',
success: async (res) => {
if (!res.confirm) return
try {
wx.showLoading({ title: '彻底删除中...', mask: true })
await api.personalCollect.purge(id)
wx.hideLoading()
wx.showToast({ title: '已彻底删除', icon: 'success' })
this.refresh()
} catch (err) {
wx.hideLoading()
wx.showModal({
title: '删除失败',
content: err && err.message ? err.message : '请重试',
showCancel: false,
})
}
},
})
},
})
@@ -4,7 +4,12 @@
<view class="page-subtitle">保存聊天里的图片与文件,标签搜索管理。</view>
</view>
<view class="search-box">
<view class="trash-tabs">
<view class="trash-tab {{!trashMode ? 'trash-tab-active' : ''}}" bindtap="onBackToList">收藏</view>
<view class="trash-tab {{trashMode ? 'trash-tab-active' : ''}}" bindtap="onSwitchToTrash">回收站</view>
</view>
<view wx:if="{{!trashMode}}" class="search-box">
<text class="search-icon">🔍</text>
<input
class="search-input"
@@ -26,14 +31,21 @@
<view wx:if="{{loading}}" class="empty">加载中...</view>
<block wx:if="{{!loading && items.length === 0}}">
<block wx:if="{{!loading && !trashMode && items.length === 0}}">
<view class="empty">
<view style="font-size: 80rpx; margin-bottom: 24rpx;">📁</view>
<view>{{keyword ? '没有匹配的收藏' : '还没有收藏,点右下角 + 新建'}}</view>
</view>
</block>
<block wx:for="{{items}}" wx:key="id">
<block wx:if="{{!loading && trashMode && trashItems.length === 0}}">
<view class="empty">
<view style="font-size: 80rpx; margin-bottom: 24rpx;">🗑️</view>
<view>回收站是空的</view>
</view>
</block>
<block wx:for="{{items}}" wx:key="id" wx:if="{{!trashMode}}">
<view class="card" bindtap="onViewTap" data-id="{{item.id}}">
<view class="row row-between">
<view style="flex:1; min-width:0;">
@@ -72,6 +84,22 @@
</view>
</view>
</block>
<block wx:for="{{trashItems}}" wx:key="id" wx:if="{{trashMode}}">
<view class="card">
<view class="row row-between">
<view style="flex:1; min-width:0;">
<view wx:if="{{item.title}}" style="font-size: 32rpx; font-weight: 600; color:#ffffff; margin-bottom: 12rpx;">{{item.title}}</view>
<view wx:if="{{!item.title}}" style="font-size: 32rpx; font-weight: 600; color:#ffffff; margin-bottom: 12rpx; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden;">{{item.remark || '未命名收藏'}}</view>
<view class="muted small" style="margin-top: 8rpx;">删除于 {{item.deletedAtText}} · {{item.fileCount}} 个附件</view>
</view>
</view>
<view class="item-actions" style="margin-top: 20rpx;">
<view class="btn btn-ghost mini-btn" catchtap="onRestoreTap" data-id="{{item.id}}" data-title="{{item.title || item.remark || '此收藏'}}">恢复</view>
<view class="spacer"></view>
<view class="btn btn-danger mini-btn" catchtap="onPurgeTap" data-id="{{item.id}}" data-title="{{item.title || item.remark || '此收藏'}}" data-count="{{item.fileCount}}">彻底删除</view>
</view>
</view>
</block>
</view>
<view class="fab" bindtap="onAddTap">+</view>
<view class="fab" wx:if="{{!trashMode}}" catchtap="onAddTap">+</view>
@@ -1 +1,25 @@
/* 列表页独立样式,大部分复用 app.wxss 全局样式 */
/* 收藏 / 回收站 切换 Tab */
.trash-tabs {
display: flex;
margin: 24rpx 32rpx 0;
background: rgba(255,255,255,0.06);
border-radius: 999rpx;
padding: 6rpx;
overflow: hidden;
}
.trash-tab {
flex: 1;
text-align: center;
padding: 14rpx 0;
font-size: 28rpx;
color: #9a9ab8;
border-radius: 999rpx;
transition: all 0.2s ease;
}
.trash-tab-active {
background: rgba(255,255,255,0.92);
color: #1a1a2e;
font-weight: 600;
}