feat(mp, web, calendar): 新增微信收藏夹与日历批量生成功能

- 新增微信小程序版个人收藏夹全套功能,包含聚合首页、收藏列表、编辑、详情页面与配套后端API
- 新增web版时光助手门户与个人收藏夹工具,支持与小程序共享后端数据
- 优化日历提醒工具:替换本地bootstrap资源为公共CDN版本,新增批量生成循环提醒的前端界面与后端逻辑,新增数据库batch_id字段,更新鉴权配置文件
- 补充项目配置、站点地图、调试诊断脚本等配套文件
- 完善各模块的页面配置与样式文件
This commit is contained in:
yangxiangyuan
2026-09-02 22:20:37 +08:00
parent 132d132473
commit db7648eb4d
75 changed files with 8618 additions and 70 deletions
@@ -0,0 +1,165 @@
const api = require('../../../../api.js')
const formatDate = iso => {
if (!iso) return ''
const d = new Date(iso)
if (isNaN(d.getTime())) return iso
const pad = n => n < 10 ? '0' + n : '' + n
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
Page({
data: {
loading: true,
id: '',
item: null,
createdAtText: '',
updatedAtText: '',
error: '',
imageUrls: [],
hasNonImageAttachment: false,
},
onLoad(options) {
const id = options && options.id ? String(options.id) : ''
if (!id) {
this.setData({ loading: false, error: '缺少 ID 参数' })
return
}
this.setData({ id })
this.loadItem(id)
},
onShow() {
if (this.data.id && !this.data.loading) {
this.loadItem(this.data.id)
}
},
async loadItem(id) {
this.setData({ loading: true, error: '' })
try {
const res = await api.personalCollect.get(id)
const item = res.data || null
const images = (item && item.attachments || []).filter(a => a && a.isImage && a.filePath)
const imageUrls = images.map(a => `https://traesite.umersoft.com:8975/${a.filePath}`)
const attachments = (item && item.attachments || []).map(a => ({
...a,
url: a.filePath ? `https://traesite.umersoft.com:8975/${a.filePath}` : '',
}))
const hasNonImage = (item.attachments || []).some(a => !a.isImage)
this.setData({
item: Object.assign({}, item, { attachments }),
createdAtText: formatDate(item.createdAt || ''),
updatedAtText: formatDate(item.updatedAt || ''),
imageUrls,
hasNonImageAttachment: !!hasNonImage,
})
} catch (e) {
this.setData({ error: e && e.message ? e.message : '加载失败' })
} finally {
this.setData({ loading: false })
}
},
onPreviewImage(e) {
const url = e.currentTarget.dataset.url
if (!url || !this.data.imageUrls.length) return
wx.previewImage({
current: url,
urls: this.data.imageUrls,
})
},
onOpenFile(e) {
const url = e.currentTarget.dataset.url
const name = e.currentTarget.dataset.name || 'file'
if (!url) return
wx.showLoading({ title: '打开中...', mask: true })
wx.downloadFile({
url,
timeout: 120000,
success: (res) => {
wx.hideLoading()
if (res.statusCode >= 200 && res.statusCode < 300 && res.tempFilePath) {
wx.openDocument({
filePath: res.tempFilePath,
showMenu: true,
fileType: this.detectFileType(name),
success: () => {},
fail: (err) => {
wx.showModal({
title: '打开失败',
content: err && err.errMsg ? err.errMsg : '无法预览此类型文件',
showCancel: false,
})
},
})
} else {
wx.showModal({
title: '下载失败',
content: `HTTP ${res.statusCode}`,
showCancel: false,
})
}
},
fail: (err) => {
wx.hideLoading()
wx.showModal({
title: '下载失败',
content: err && err.errMsg ? err.errMsg : '网络错误',
showCancel: false,
})
},
})
},
detectFileType(name) {
const ext = (name || '').split('.').pop().toLowerCase()
const map = {
pdf: 'pdf',
doc: 'doc', docx: 'doc',
xls: 'xls', xlsx: 'xls',
ppt: 'ppt', pptx: 'ppt',
}
return map[ext] || ''
},
onEditTap() {
if (!this.data.id) return
wx.navigateTo({
url: `/kits/personal_collect/pages/edit/edit?id=${encodeURIComponent(this.data.id)}`,
})
},
onDeleteTap() {
const title = this.data.item && this.data.item.title
? this.data.item.title
: (this.data.item && this.data.item.remark && this.data.item.remark.slice(0, 10)) || '此收藏'
wx.showModal({
title: '确认删除',
content: `确定删除「${title}」吗?删除后不再显示。`,
confirmText: '删除',
confirmColor: '#e05561',
success: async (res) => {
if (!res.confirm) return
try {
wx.showLoading({ title: '删除中...', mask: true })
await api.personalCollect.remove(this.data.id)
wx.hideLoading()
wx.showToast({ title: '已删除', icon: 'success' })
setTimeout(() => {
wx.navigateBack()
}, 800)
} catch (err) {
wx.hideLoading()
wx.showModal({
title: '删除失败',
content: err && err.message ? err.message : '请重试',
showCancel: false,
})
}
},
})
},
})
@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "收藏详情",
"backgroundTextStyle": "light"
}
@@ -0,0 +1,69 @@
<view wx:if="{{loading}}" class="container">
<view class="empty">加载中...</view>
</view>
<view wx:elif="{{error}}" class="container">
<view class="card" style="border-color:#55333a;">
<text style="color:#e05561;">⚠️ {{error}}</text>
</view>
</view>
<view wx:elif="{{!item}}" class="container">
<view class="empty">未找到该收藏</view>
</view>
<view wx:else class="container" style="padding-bottom: 200rpx;">
<view class="card">
<view wx:if="{{item.title}}" style="font-size: 40rpx; font-weight:600; color:#ffffff; line-height:1.4;">{{item.title}}</view>
<view class="divider"></view>
<view wx:if="{{item.remark}}" style="white-space: pre-wrap; font-size: 28rpx; color:#e8e8f0; line-height:1.8;">{{item.remark}}</view>
<view wx:if="{{item.tags && item.tags.length}}" style="margin-top: 32rpx;">
<block wx:for="{{item.tags}}" wx:for-item="tag" wx:key="*this">
<view class="tag">#{{tag}}</view>
</block>
</view>
<view class="divider"></view>
<view class="row" style="margin-bottom: 12rpx;">
<text class="muted small" style="width: 180rpx;">创建时间</text>
<text class="small">{{createdAtText || '-'}}</text>
</view>
<view class="row">
<text class="muted small" style="width: 180rpx;">更新时间</text>
<text class="small">{{updatedAtText || '-'}}</text>
</view>
</view>
<view wx:if="{{item.attachments && item.attachments.length}}" class="card">
<view style="font-size: 30rpx; font-weight:600; color:#ffffff; margin-bottom: 24rpx;">附件 ({{item.attachments.length}})</view>
<view class="attachments-grid">
<block wx:for="{{item.attachments}}" wx:for-item="att" wx:key="id">
<view class="attachment-thumb">
<block wx:if="{{att.isImage}}">
<image
src="{{att.url}}"
mode="aspectFill"
data-url="{{att.url}}"
bindtap="onPreviewImage"
/>
</block>
<block wx:else>
<text class="attachment-icon" data-url="{{att.url}}" data-name="{{att.fileName}}" catchtap="onOpenFile" style="padding: 20rpx;">📄</text>
</block>
<view class="attachment-name" data-url="{{att.url}}" data-name="{{att.fileName}}" catchtap="{{att.isImage ? 'onPreviewImage' : 'onOpenFile'}}">{{att.fileName}}</view>
</view>
</block>
</view>
<view wx:if="{{hasNonImageAttachment}}" class="muted small" style="margin-top: 24rpx;">
💡 非图片附件点击名称即可下载并使用微信自带文档查看器打开。
</view>
</view>
</view>
<view wx:if="{{item}}" style="position:fixed; left:0; right:0; bottom:0; padding:24rpx 32rpx; background:#0f0f1a; border-top:1rpx solid #2a2a44; box-sizing:border-box; display:flex; gap:20rpx; z-index:99;">
<view class="btn btn-ghost" style="flex:1;" bindtap="onDeleteTap">删除</view>
<view class="btn btn-primary" style="flex:1;" bindtap="onEditTap">编辑</view>
</view>
@@ -0,0 +1 @@
/* 详情页独立样式,大部分复用 app.wxss 全局样式 */