feat(personal_collect,motion_reminder): 新增回收站与云端TTS语音功能
- 新增小程序个人收藏回收站页面,支持查看已删除收藏、恢复与彻底删除操作 - 修复个人收藏文件上传时的中文名乱码问题,新增原文件名透传参数 - 接入阿里云百炼TTS服务,实现运动提醒模块的云端语音合成与本地缓存,支持系统TTS与云端MP3双降级播放 - 新增语音引擎探测与异常预警UI,覆盖web端与移动端 - 优化web端订单盒页面布局,修复卡片操作按钮换行问题 - 新增调试探针脚本、ESLint配置与相关lint脚本 - 完善后端接口与路由,更新项目依赖与配置文件
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# Windows
|
||||
[Dd]esktop.ini
|
||||
Thumbs.db
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user