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
+145
View File
@@ -0,0 +1,145 @@
const app = () => getApp()
const BASE_URL = function () {
try {
const a = getApp()
if (a && a.globalData && a.globalData.apiBase) return a.globalData.apiBase
} catch (e) {}
return 'https://traesite.umersoft.com:8975'
}
const request = (options) => {
return new Promise((resolve, reject) => {
const { url, method = 'GET', data = {}, header = {}, timeout = 30000 } = options
const fullUrl = url.indexOf('http') === 0 ? url : BASE_URL() + url
wx.request({
url: fullUrl,
method,
data,
header: Object.assign({
'content-type': 'application/json',
}, header),
timeout,
success: (res) => {
const body = res.data || {}
if (res.statusCode >= 200 && res.statusCode < 300 && body && body.ok) {
resolve(body)
} else {
const msg = body && body.error ? body.error : `请求失败 (HTTP ${res.statusCode})`
reject(new Error(msg))
}
},
fail: (err) => {
reject(new Error(err && err.errMsg ? err.errMsg : '网络错误'))
},
})
})
}
const uploadFile = (options) => {
return new Promise((resolve, reject) => {
const { url, filePath, name = 'file', formData = {}, timeout = 120000 } = options
const fullUrl = url.indexOf('http') === 0 ? url : BASE_URL() + url
wx.uploadFile({
url: fullUrl,
filePath,
name,
formData,
timeout,
success: (res) => {
try {
const body = typeof res.data === 'string' ? JSON.parse(res.data) : (res.data || {})
if (res.statusCode >= 200 && res.statusCode < 300 && body && body.ok) {
resolve(body)
} else {
const msg = body && body.error ? body.error : `上传失败 (HTTP ${res.statusCode})`
reject(new Error(msg))
}
} catch (e) {
reject(new Error('上传响应解析失败'))
}
},
fail: (err) => {
reject(new Error(err && err.errMsg ? err.errMsg : '上传网络错误'))
},
})
})
}
const core = {
getMeta: () => request({ url: '/api/mp_time_assistant/meta', method: 'GET' }),
getState: () => request({ url: '/api/mp_time_assistant/state', method: 'GET' }),
setActiveKit: (kitId) => request({
url: '/api/mp_time_assistant/state/active-kit',
method: 'PUT',
data: { kitId },
}),
}
const personalCollect = {
list: (keyword = '') => request({
url: '/api/mp_time_assistant/kits/personal_collect/items',
method: 'GET',
data: { keyword: keyword || '' },
}),
get: (id) => request({
url: `/api/mp_time_assistant/kits/personal_collect/items/${encodeURIComponent(id)}`,
method: 'GET',
}),
create: (payload) => request({
url: '/api/mp_time_assistant/kits/personal_collect/items',
method: 'POST',
data: payload,
}),
update: (payload) => request({
url: '/api/mp_time_assistant/kits/personal_collect/items',
method: 'PUT',
data: payload,
}),
remove: (id) => request({
url: '/api/mp_time_assistant/kits/personal_collect/items',
method: 'DELETE',
data: { id },
}),
upload: (filePath, groupId = '') => uploadFile({
url: '/api/mp_time_assistant/kits/personal_collect/upload',
filePath,
name: 'file',
formData: groupId ? { groupId } : {},
}),
}
const chooseChatAttachments = (count = 9) => {
return new Promise((resolve, reject) => {
try {
if (!wx.chooseMessageFile) {
reject(new Error('当前微信版本不支持从聊天选择文件,请升级微信'))
return
}
wx.chooseMessageFile({
count: Math.max(1, Math.min(9, Number(count) || 9)),
type: 'all',
success: (res) => resolve(res.tempFiles || []),
fail: (err) => {
const msg = err && err.errMsg ? String(err.errMsg) : '用户取消或授权失败'
if (msg.indexOf('cancel') !== -1 || msg.indexOf('取消') !== -1) {
resolve([])
} else {
reject(new Error(msg))
}
},
})
} catch (e) {
reject(e)
}
})
}
module.exports = {
BASE_URL,
request,
uploadFile,
core,
personalCollect,
chooseChatAttachments,
}
+23
View File
@@ -0,0 +1,23 @@
App({
globalData: {
apiBase: 'https://traesite.umersoft.com:8975',
meta: null,
kits: [],
},
onLaunch() {
try {
const stored = wx.getStorageSync('mp_ta_meta')
if (stored) {
this.globalData.meta = stored.meta || null
this.globalData.kits = stored.kits || []
}
} catch (e) {}
},
setMeta(meta) {
this.globalData.meta = meta || null
this.globalData.kits = (meta && meta.kits) || []
try {
wx.setStorageSync('mp_ta_meta', { meta, kits: this.globalData.kits })
} catch (e) {}
},
})
+19
View File
@@ -0,0 +1,19 @@
{
"pages": [
"pages/hub/hub",
"kits/personal_collect/pages/list/list",
"kits/personal_collect/pages/edit/edit",
"kits/personal_collect/pages/detail/detail"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#1a1a2e",
"navigationBarTitleText": "MilesYang时光助手",
"navigationBarTextStyle": "white",
"backgroundColor": "#0f0f1a"
},
"style": "v2",
"sitemapLocation": "sitemap.json",
"__comment_requiredPrivateInfos_chooseMessageFile": "chooseMessageFile 不再需要在 requiredPrivateInfos 中静态声明,调用时微信自动弹窗授权即可。",
"lazyCodeLoading": "requiredComponents"
}
+328
View File
@@ -0,0 +1,328 @@
page {
background: #0f0f1a;
color: #e8e8f0;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 28rpx;
line-height: 1.6;
}
.container {
padding: 32rpx;
min-height: 100vh;
box-sizing: border-box;
}
.page-header {
padding: 80rpx 0 48rpx 0;
}
.page-title {
font-size: 48rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 12rpx;
}
.page-subtitle {
font-size: 26rpx;
color: #8888a8;
line-height: 1.6;
}
.card {
background: #1a1a2e;
border: 1rpx solid #2a2a44;
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-sizing: border-box;
}
.kit-card {
display: flex;
flex-direction: row;
align-items: center;
background: #1a1a2e;
border: 1rpx solid #2a2a44;
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-sizing: border-box;
}
.kit-card:active {
opacity: 0.75;
}
.kit-icon {
width: 96rpx;
height: 96rpx;
border-radius: 48rpx;
background: rgba(108, 122, 224, 0.15);
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
flex-shrink: 0;
}
.kit-icon-text {
font-size: 40rpx;
}
.kit-body {
flex: 1;
min-width: 0;
}
.kit-title {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 8rpx;
}
.kit-desc {
font-size: 24rpx;
color: #8888a8;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.chevron {
color: #555570;
font-size: 36rpx;
margin-left: 16rpx;
flex-shrink: 0;
}
.btn {
display: inline-block;
padding: 20rpx 48rpx;
border-radius: 16rpx;
font-size: 28rpx;
font-weight: 500;
text-align: center;
box-sizing: border-box;
line-height: 1.4;
}
.btn-primary {
background: #6c7ae0;
color: #ffffff;
}
.btn-primary:active {
opacity: 0.85;
}
.btn-ghost {
background: transparent;
border: 1rpx solid #3a3a5a;
color: #b8b8d0;
}
.btn-ghost:active {
background: rgba(108, 122, 224, 0.08);
}
.btn-danger {
background: #e05561;
color: #ffffff;
}
.btn-danger:active {
opacity: 0.85;
}
.btn-block {
display: block;
width: 100%;
}
.input-group {
margin-bottom: 32rpx;
}
.input-label {
font-size: 26rpx;
color: #8888a8;
margin-bottom: 16rpx;
display: block;
}
.input,
.textarea {
width: 100%;
background: #12121f;
border: 1rpx solid #2a2a44;
border-radius: 16rpx;
padding: 24rpx;
color: #ffffff !important;
font-size: 28rpx;
box-sizing: border-box;
line-height: 1.6;
}
.textarea {
min-height: 240rpx;
}
.tag {
display: inline-block;
padding: 8rpx 20rpx;
border-radius: 20rpx;
background: rgba(108, 122, 224, 0.12);
color: #8fa0ee;
font-size: 22rpx;
margin-right: 12rpx;
margin-bottom: 12rpx;
}
.empty {
padding: 120rpx 32rpx;
text-align: center;
color: #555570;
font-size: 28rpx;
}
.search-box {
display: flex;
flex-direction: row;
align-items: center;
background: #12121f;
border: 1rpx solid #2a2a44;
border-radius: 16rpx;
padding: 0 24rpx;
margin-bottom: 32rpx;
box-sizing: border-box;
}
.search-input {
flex: 1;
padding: 24rpx 0;
color: #ffffff !important;
font-size: 28rpx;
}
.search-icon {
color: #555570;
font-size: 32rpx;
margin-right: 16rpx;
}
.row {
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
}
.row-between {
justify-content: space-between;
}
.spacer {
flex: 1;
}
.divider {
height: 1rpx;
background: #2a2a44;
margin: 24rpx 0;
}
.muted {
color: #8888a8;
}
.small {
font-size: 24rpx;
}
.item-actions {
display: flex;
flex-direction: row;
gap: 16rpx;
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid #23233a;
}
.mini-btn {
padding: 12rpx 24rpx;
border-radius: 12rpx;
font-size: 24rpx;
line-height: 1.3;
}
.attachments-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 16rpx;
}
.attachment-thumb {
width: 160rpx;
height: 160rpx;
border-radius: 16rpx;
background: #12121f;
border: 1rpx solid #2a2a44;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.attachment-thumb image {
width: 100%;
height: 100%;
}
.attachment-icon {
font-size: 48rpx;
color: #8888a8;
}
.attachment-name {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 8rpx 12rpx;
background: rgba(0, 0, 0, 0.6);
color: #ffffff;
font-size: 18rpx;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fab {
position: fixed;
right: 40rpx;
bottom: 60rpx;
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
background: #6c7ae0;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 56rpx;
font-weight: 300;
box-shadow: 0 8rpx 32rpx rgba(108, 122, 224, 0.35);
z-index: 99;
}
.fab:active {
transform: scale(0.95);
}
@@ -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 全局样式 */
@@ -0,0 +1,345 @@
const api = require('../../../../api.js')
Page({
data: {
isEdit: false,
loading: true,
saving: false,
id: '',
title: '',
remark: '',
tagsText: '',
attachments: [],
uploadGroupId: '',
error: '',
},
onLoad(options) {
const id = options && options.id ? String(options.id) : ''
this.setData({ isEdit: !!id, id })
this.generateUploadGroupId()
if (id) {
this.loadItem(id)
} else {
this.applyClipPrefill()
this.setData({ loading: false })
}
},
applyClipPrefill() {
try {
const text = wx.getStorageSync('mp_ta_clip_text')
if (!text || typeof text !== 'string') return
const mode = wx.getStorageSync('mp_ta_clip_mode') || 'new'
try { wx.removeStorageSync('mp_ta_clip_text') } catch (e) {}
try { wx.removeStorageSync('mp_ta_clip_mode') } catch (e) {}
const title = ((String(text || '').split('\n').map(s => s.trim()).find(s => s.length) || '').slice(0, 28)) || '文字收藏'
this.setData({
title: title,
remark: text,
})
wx.showToast({
title: mode === 'append' ? '已追加剪贴板文字' : '已预填剪贴板文字',
icon: 'none',
duration: 1600,
})
} catch (e) {}
},
generateUploadGroupId() {
const ts = Date.now().toString(36)
const rnd = Math.random().toString(36).slice(2, 10)
this.setData({ uploadGroupId: `${ts}_${rnd}` })
},
async loadItem(id) {
this.setData({ loading: true, error: '' })
try {
const res = await api.personalCollect.get(id)
const item = res.data || {}
const tags = Array.isArray(item.tags) ? item.tags : []
const attachments = (item.attachments || []).map(a => ({
...a,
url: a.filePath ? `https://traesite.umersoft.com:8975/${a.filePath}` : '',
uploaded: true,
uploading: false,
}))
this.setData({
title: item.title || '',
remark: item.remark || '',
tagsText: tags.join(', '),
attachments,
})
} catch (e) {
this.setData({ error: e && e.message ? e.message : '加载失败' })
} finally {
this.setData({ loading: false })
}
},
onTitleInput(e) {
this.setData({ title: String(e.detail.value || '') })
},
onRemarkInput(e) {
this.setData({ remark: String(e.detail.value || '') })
},
onTagsInput(e) {
this.setData({ tagsText: String(e.detail.value || '') })
},
async onChooseAttachments() {
try {
const files = await api.chooseChatAttachments(9 - this.data.attachments.length)
if (!files || !files.length) return
for (const f of files) {
await this.uploadOne(f)
}
} catch (e) {
const msg = e && e.message ? String(e.message) : ''
if (msg.indexOf('cancel') !== -1 || msg.indexOf('取消') !== -1) return
if (msg.indexOf('auth') !== -1 || msg.indexOf('deny') !== -1 || msg.indexOf('权限') !== -1 || msg.indexOf('授权') !== -1) {
wx.showModal({
title: '需要聊天文件权限',
content: '请点击右上角「···」→ 设置,打开「聊天记录」权限后重试。',
showCancel: false,
})
return
}
wx.showModal({
title: '选择失败',
content: msg || '请重试',
showCancel: false,
})
}
},
async uploadOne(tempFile) {
const tempId = `tmp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
const isImage = tempFile.type === 'image' || /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(tempFile.name || '')
const pending = {
id: tempId,
fileName: tempFile.name || '未命名',
filePath: '',
fileSize: Number(tempFile.size || 0),
isImage,
url: tempFile.path || tempFile.tempFilePath || '',
uploaded: false,
uploading: true,
error: '',
}
const list = this.data.attachments.concat([pending])
this.setData({ attachments: list })
const idx = list.length - 1
try {
const res = await api.personalCollect.upload(tempFile.path || tempFile.tempFilePath, this.data.uploadGroupId)
const data = (res && res.data) || {}
const attachment = {
id: data.id || tempId,
fileName: data.fileName || tempFile.name || '未命名',
storedName: data.storedName || '',
filePath: data.filePath || '',
fileSize: Number(data.fileSize || tempFile.size || 0),
mimeType: data.mimeType || '',
isImage: !!data.isImage || isImage,
url: data.url || (data.filePath ? `https://traesite.umersoft.com:8975/${data.filePath}` : '') || '',
groupId: data.groupId || '',
uploaded: true,
uploading: false,
error: '',
}
const next = this.data.attachments.slice()
next[idx] = attachment
this.setData({ attachments: next })
this.autoFillFromAttachments(false)
} catch (e) {
const next = this.data.attachments.slice()
next[idx] = Object.assign({}, next[idx], {
uploading: false,
error: e && e.message ? e.message : '上传失败',
})
this.setData({ attachments: next })
}
},
autoFillFromAttachments(force) {
if (this.data.isEdit && !force) return
const atts = this.data.attachments.filter(a => a && a.uploaded)
if (!atts.length) return
const hasTitle = !!(this.data.title || '').trim()
const hasRemark = !!(this.data.remark || '').trim()
const { title, remark } = this.buildAutoText(atts)
const patch = {}
if (!hasTitle || force) patch.title = title
if (!hasRemark || force) patch.remark = remark
if (Object.keys(patch).length) this.setData(patch)
},
buildAutoText(atts) {
const images = atts.filter(a => a.isImage)
const files = atts.filter(a => !a.isImage)
const imageCount = images.length
const fileCount = files.length
const total = atts.length
const pad = n => n < 10 ? '0' + n : '' + n
const now = new Date()
const ts = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`
const humanSize = bytes => {
const n = Number(bytes || 0)
if (n < 1024) return n + ' B'
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB'
return (n / 1024 / 1024).toFixed(2) + ' MB'
}
let title = ''
if (total === 1) {
const only = atts[0]
const rawName = only.fileName || '附件'
const ext = (rawName.split('.').pop() || '').toLowerCase()
const stem = rawName.lastIndexOf('.') > 0 ? rawName.slice(0, rawName.lastIndexOf('.')) : rawName
if (only.isImage) {
title = `图片 - ${ts}`
} else {
title = stem || rawName
}
} else if (imageCount > 0 && fileCount === 0) {
title = `${imageCount} 张图片 - ${ts}`
} else if (fileCount > 0 && imageCount === 0) {
if (fileCount === 1) {
const only = files[0]
const rawName = only.fileName || '附件'
const stem = rawName.lastIndexOf('.') > 0 ? rawName.slice(0, rawName.lastIndexOf('.')) : rawName
title = stem || rawName
} else {
title = `${fileCount} 个文件 - ${ts}`
}
} else {
title = `${imageCount} 图 + ${fileCount} 文件 - ${ts}`
}
const lines = []
lines.push(`附件数量:${total}`)
if (imageCount) lines.push(`图片:${imageCount} 张`)
if (fileCount) lines.push(`文件:${fileCount} 个`)
lines.push('')
lines.push('附件清单:')
atts.slice(0, 9).forEach((a, i) => {
lines.push(` ${i + 1}. ${a.fileName || '未命名'}(${humanSize(a.fileSize)})`)
})
if (atts.length > 9) lines.push(` ... 另有 ${atts.length - 9} 个附件未列出`)
lines.push('')
lines.push(`整理时间:${ts}`)
const remark = lines.join('\n')
return { title, remark }
},
onPreviewImage(e) {
const url = e.currentTarget.dataset.url
const urls = this.data.attachments.filter(a => a && a.isImage && a.url).map(a => a.url)
if (!url || !urls.length) return
wx.previewImage({
current: url,
urls,
})
},
onRemoveAttachment(e) {
const id = e.currentTarget.dataset.id
const idx = this.data.attachments.findIndex(a => a.id === id)
if (idx < 0) return
const next = this.data.attachments.slice()
next.splice(idx, 1)
this.setData({ attachments: next })
},
onRetryUpload(e) {
const id = e.currentTarget.dataset.id
const idx = this.data.attachments.findIndex(a => a.id === id)
if (idx < 0) return
const att = this.data.attachments[idx]
if (!att || att.uploaded || !att.url) return
this.uploadOne({
name: att.fileName || '未命名',
size: att.fileSize || 0,
type: att.isImage ? 'image' : 'file',
path: att.url,
}).then(() => {
const next = this.data.attachments.slice()
next.splice(idx, 1)
this.setData({ attachments: next })
})
},
parseTags(text) {
const s = String(text || '').trim()
if (!s) return []
return s
.split(/[,,\s]+/)
.map(t => t.trim())
.filter(Boolean)
},
async onSave() {
if (this.data.saving) return
const title = this.data.title.trim()
const remark = this.data.remark.trim()
if (!title && !remark) {
wx.showToast({ title: '标题或备注至少填一个', icon: 'none' })
return
}
const errorAtt = this.data.attachments.find(a => a && a.error)
if (errorAtt) {
wx.showModal({
title: '存在上传失败的附件',
content: `「${errorAtt.fileName}」上传失败:${errorAtt.error}。请移除或重试后再保存。`,
showCancel: false,
})
return
}
const uploading = this.data.attachments.find(a => a && a.uploading)
if (uploading) {
wx.showToast({ title: '请等待附件上传完成', icon: 'none' })
return
}
const attachmentIds = this.data.attachments
.filter(a => a && a.uploaded && a.id)
.map(a => a.id)
const payload = {
title,
remark,
tags: this.parseTags(this.data.tagsText),
attachmentIds,
}
this.setData({ saving: true })
wx.showLoading({ title: this.data.isEdit ? '保存中...' : '创建中...', mask: true })
try {
if (this.data.isEdit) {
await api.personalCollect.update(Object.assign({ id: this.data.id }, payload))
} else {
await api.personalCollect.create(payload)
}
wx.hideLoading()
wx.showToast({
title: this.data.isEdit ? '已保存' : '已创建',
icon: 'success',
})
setTimeout(() => {
wx.navigateBack()
}, 800)
} catch (e) {
wx.hideLoading()
wx.showModal({
title: this.data.isEdit ? '保存失败' : '创建失败',
content: e && e.message ? e.message : '请重试',
showCancel: false,
})
} finally {
this.setData({ saving: false })
}
},
})
@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "编辑收藏",
"backgroundTextStyle": "light"
}
@@ -0,0 +1,101 @@
<view wx:if="{{loading}}" class="container">
<view class="empty">加载中...</view>
</view>
<view wx:else class="container" style="padding-bottom: 200rpx;">
<view wx:if="{{error}}" class="card" style="border-color:#55333a;">
<text style="color:#e05561; font-size: 26rpx;">⚠️ {{error}}</text>
</view>
<view class="input-group">
<text class="input-label">标题</text>
<input
class="input"
placeholder="输入标题(必填或填备注)"
placeholder-style="color:#555570"
value="{{title}}"
bindinput="onTitleInput"
maxlength="200"
style="color:#ffffff;"
/>
</view>
<view class="input-group">
<text class="input-label">备注</text>
<textarea
class="textarea"
placeholder="输入备注信息"
placeholder-style="color:#555570"
value="{{remark}}"
bindinput="onRemarkInput"
maxlength="4000"
auto-height
style="color:#ffffff;"
/>
</view>
<view class="input-group">
<text class="input-label">标签(逗号或空格分隔,如:工作, 重要)</text>
<input
class="input"
placeholder="标签1, 标签2"
placeholder-style="color:#555570"
value="{{tagsText}}"
bindinput="onTagsInput"
maxlength="500"
style="color:#ffffff;"
/>
</view>
<view class="input-group">
<view class="row row-between" style="margin-bottom: 16rpx;">
<text class="input-label" style="margin-bottom:0;">附件(从聊天选择,最多 9 个)</text>
<view class="btn btn-ghost mini-btn" bindtap="onChooseAttachments" style="margin:0;">+ 从聊天选</view>
</view>
<view wx:if="{{attachments.length === 0}}" class="card" style="text-align:center; color:#555570; border-style: dashed;">
<text>还没有附件,点右上角按钮选择</text>
</view>
<view wx:if="{{attachments.length > 0}}" class="attachments-grid">
<block wx:for="{{attachments}}" wx:key="id">
<view class="attachment-thumb">
<block wx:if="{{item.isImage && item.url}}">
<image
src="{{item.url}}"
mode="aspectFill"
data-url="{{item.url}}"
bindtap="onPreviewImage"
/>
</block>
<block wx:else>
<text class="attachment-icon">📄</text>
</block>
<view class="attachment-name">{{item.fileName}}</view>
<view wx:if="{{item.uploading}}" style="position:absolute; left:0; right:0; top:0; bottom:0; background:rgba(0,0,0,0.55); display:flex; align-items:center; justify-content:center;">
<text style="color:#fff; font-size:22rpx;">上传中...</text>
</view>
<view wx:elif="{{item.error}}" style="position:absolute; left:0; right:0; top:0; bottom:0; background:rgba(88,20,30,0.7); display:flex; align-items:center; justify-content:center; flex-direction:column; padding: 12rpx; box-sizing:border-box;">
<text style="color:#fff; font-size:20rpx; line-height:1.4; text-align:center;">上传失败</text>
<view style="margin-top:8rpx; padding:6rpx 16rpx; background:#fff; color:#1a1a2e; border-radius:8rpx; font-size:20rpx;" catchtap="onRetryUpload" data-id="{{item.id}}">重试</view>
</view>
<view
style="position:absolute; top:8rpx; right:8rpx; width:44rpx; height:44rpx; border-radius:22rpx; background:rgba(0,0,0,0.55); display:flex; align-items:center; justify-content:center; color:#fff; font-size:28rpx; line-height:1;"
catchtap="onRemoveAttachment"
data-id="{{item.id}}"
>×</view>
</view>
</block>
</view>
</view>
</view>
<view style="position:fixed; left:0; right:0; bottom:0; padding:24rpx 32rpx; background:#0f0f1a; border-top:1rpx solid #2a2a44; box-sizing:border-box; z-index:99;">
<view class="btn btn-primary btn-block {{saving ? '' : ''}}" style="opacity: {{saving ? 0.6 : 1}};" bindtap="onSave">
{{isEdit ? (saving ? '保存中...' : '保存修改') : (saving ? '创建中...' : '创建收藏')}}
</view>
</view>
@@ -0,0 +1 @@
/* 编辑页独立样式,大部分复用 app.wxss 全局样式 */
@@ -0,0 +1,110 @@
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,
keyword: '',
items: [],
error: '',
},
onShow() {
this.refresh()
},
onPullDownRefresh() {
this.refresh().finally(() => {
wx.stopPullDownRefresh()
})
},
onSearchInput(e) {
const val = e.detail && e.detail.value !== undefined ? String(e.detail.value) : ''
this.setData({ keyword: val })
},
onSearchConfirm() {
this.refresh()
},
onClearSearch() {
this.setData({ keyword: '' })
this.refresh()
},
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 })
} catch (e) {
this.setData({ error: e && e.message ? e.message : '加载失败' })
} finally {
this.setData({ loading: false })
}
},
onViewTap(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({
url: `/kits/personal_collect/pages/detail/detail?id=${encodeURIComponent(id)}`,
})
},
onEditTap(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({
url: `/kits/personal_collect/pages/edit/edit?id=${encodeURIComponent(id)}`,
})
},
onDeleteTap(e) {
const id = e.currentTarget.dataset.id
const title = e.currentTarget.dataset.title || '此收藏'
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(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,
})
}
},
})
},
onAddTap() {
wx.navigateTo({
url: '/kits/personal_collect/pages/edit/edit',
})
},
})
@@ -0,0 +1,5 @@
{
"navigationBarTitleText": "微信收藏夹",
"enablePullDownRefresh": true,
"backgroundTextStyle": "light"
}
@@ -0,0 +1,77 @@
<view class="container" style="padding-bottom: 200rpx;">
<view class="page-header">
<view class="page-title">微信收藏夹</view>
<view class="page-subtitle">保存聊天里的图片与文件,标签搜索管理。</view>
</view>
<view class="search-box">
<text class="search-icon">🔍</text>
<input
class="search-input"
placeholder="搜索标题 / 备注 / 标签"
placeholder-style="color:#555570"
value="{{keyword}}"
bindinput="onSearchInput"
bindconfirm="onSearchConfirm"
confirm-type="search"
/>
<view wx:if="{{keyword}}" bindtap="onClearSearch" style="padding: 0 8rpx;">
<text style="color:#8888a8; font-size: 36rpx;">×</text>
</view>
</view>
<view wx:if="{{error}}" class="card" style="border-color:#55333a;">
<text style="color:#e05561; font-size: 26rpx;">⚠️ {{error}}</text>
</view>
<view wx:if="{{loading}}" class="empty">加载中...</view>
<block wx:if="{{!loading && 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">
<view class="card" bindtap="onViewTap" data-id="{{item.id}}">
<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 && item.remark}}" 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 wx:if="{{item.remark && item.title}}" class="muted small" style="display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.5;">{{item.remark}}</view>
</view>
<view wx:if="{{item.previewImage}}" style="width:128rpx; height:128rpx; margin-left: 24rpx; border-radius: 16rpx; overflow: hidden; background:#12121f; flex-shrink:0;">
<image
src="https://traesite.umersoft.com:8975/{{item.previewImage.filePath}}"
mode="aspectFill"
style="width:100%; height:100%;"
/>
</view>
<view wx:elif="{{item.fileCount > 0}}" style="width:128rpx; height:128rpx; margin-left: 24rpx; border-radius: 16rpx; background:#12121f; display:flex; align-items:center; justify-content:center; flex-direction: column; flex-shrink:0;">
<text style="font-size: 40rpx;">📎</text>
<text class="muted small" style="margin-top: 8rpx;">{{item.fileCount}} 个</text>
</view>
</view>
<view wx:if="{{item.tagsText}}" style="margin-top: 20rpx;">
<block wx:for="{{item.tags}}" wx:for-item="tag" wx:key="*this">
<view class="tag">#{{tag}}</view>
</block>
</view>
<view class="row row-between" style="margin-top: 20rpx;">
<text class="muted small">更新于 {{item.updatedAtText}}</text>
</view>
<view class="item-actions">
<view class="btn btn-ghost mini-btn" catchtap="onViewTap" data-id="{{item.id}}">查看</view>
<view class="btn btn-ghost mini-btn" catchtap="onEditTap" data-id="{{item.id}}">编辑</view>
<view class="spacer"></view>
<view class="btn btn-danger mini-btn" catchtap="onDeleteTap" data-id="{{item.id}}" data-title="{{item.title || (item.remark && item.remark.slice(0, 10)) || '此收藏'}}">删除</view>
</view>
</view>
</block>
</view>
<view class="fab" bindtap="onAddTap">+</view>
@@ -0,0 +1 @@
/* 列表页独立样式,大部分复用 app.wxss 全局样式 */
@@ -0,0 +1,173 @@
const api = require('../../api.js')
Page({
data: {
loading: true,
kits: [],
appNameZh: 'MilesYang时光助手',
subtitle: '个人工具集合,记录时光点滴。',
error: '',
clipVisible: false,
clipText: '',
clipCount: 0,
clipLines: 0,
clipPreview: '',
},
onLoad() {
this.loadMeta()
},
onShow() {
this.checkClipboard()
},
onPullDownRefresh() {
this.loadMeta().finally(() => {
wx.stopPullDownRefresh()
})
},
simpleHash(str) {
const s = String(str || '')
let h1 = 0x811c9dc5
let h2 = 0xdeadbeef
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i)
h1 = Math.imul(h1 ^ c, 16777619)
h2 = Math.imul(h2 ^ c, 2246822519)
}
h1 = (h1 ^ h1 >>> 16) >>> 0
h2 = (h2 ^ h2 >>> 15) >>> 0
return h1.toString(36) + h2.toString(36)
},
checkClipboard() {
try {
const enable = wx.getStorageSync('mp_ta_clip_enable')
if (enable === false || enable === 'false') return
} catch (e) {}
wx.getClipboardData({
success: (res) => {
const text = (res.data || '').trim()
if (text.length < 6 || text.length > 30000) return
const hash = this.simpleHash(text)
try {
const last = wx.getStorageSync('mp_ta_clip_hash')
if (last === hash) return
wx.setStorageSync('mp_ta_clip_hash', hash)
} catch (e) {}
const count = text.length
const lines = text.split('\n').length
const preview = text.length > 220 ? text.slice(0, 220) + '\n\n……' : text
this.setData({
clipVisible: true,
clipText: text,
clipCount: count,
clipLines: lines,
clipPreview: preview,
})
},
fail: () => {}
})
},
onClipCollect(e) {
const mode = (e && e.currentTarget && e.currentTarget.dataset.mode) || 'new'
const text = this.data.clipText || ''
if (!text) { this.setData({ clipVisible: false }); return }
try { wx.setStorageSync('mp_ta_clip_text', text) } catch (e) {}
try { wx.setStorageSync('mp_ta_clip_mode', mode) } catch (e) {}
this.setData({ clipVisible: false })
wx.navigateTo({
url: '/kits/personal_collect/pages/edit/edit',
fail: () => { wx.showToast({ title: '打开失败', icon: 'none' }) }
})
},
onClipCancel() {
this.setData({ clipVisible: false })
},
onClipDismissForever() {
wx.showModal({
title: '不再检测剪贴板?',
content: '关闭后可在未来任意新增收藏页面时重新开启。',
confirmText: '不再提示',
cancelText: '保留',
success: (r) => {
if (r.confirm) {
try { wx.setStorageSync('mp_ta_clip_enable', false) } catch (e) {}
this.setData({ clipVisible: false })
wx.showToast({ title: '已关闭检测', icon: 'none' })
}
}
})
},
async loadMeta() {
this.setData({ loading: true, error: '' })
try {
const res = await api.core.getMeta()
if (res && res.appNameZh) {
this.setData({ appNameZh: res.appNameZh })
}
if (res && Array.isArray(res.kits) && res.kits.length) {
this.setData({ kits: this.decorateKits(res.kits) })
try { getApp().setMeta(res) } catch (e) {}
} else {
this.setData({ kits: this.fallbackKits() })
}
} catch (e) {
this.setData({
error: e && e.message ? e.message : '加载失败,请检查网络',
kits: this.fallbackKits(),
})
} finally {
this.setData({ loading: false })
}
},
fallbackKits() {
return this.decorateKits([{
id: 'personal_collect',
name: '微信收藏夹',
description: '聊天图片、文件收藏管理,标签搜索,软删除可追溯。',
}])
},
decorateKits(list) {
const icons = {
personal_collect: '📁',
}
return list.map(item => Object.assign({}, item, {
icon: icons[item.id] || '🛠️',
path: this.getKitPath(item.id),
}))
},
getKitPath(id) {
const map = {
personal_collect: '/kits/personal_collect/pages/list/list',
}
return map[id] || ''
},
async onKitTap(e) {
const id = e.currentTarget.dataset.id
const path = e.currentTarget.dataset.path
if (!path) {
wx.showToast({ title: '功能开发中', icon: 'none' })
return
}
try {
await api.core.setActiveKit(id)
} catch (e) {}
wx.navigateTo({
url: path,
fail: () => {
wx.showToast({ title: '打开失败', icon: 'none' })
},
})
},
})
@@ -0,0 +1,5 @@
{
"navigationBarTitleText": "MilesYang时光助手",
"enablePullDownRefresh": true,
"backgroundTextStyle": "light"
}
@@ -0,0 +1,35 @@
<view class="container">
<view class="page-header">
<view class="page-title">{{appNameZh}}</view>
<view class="page-subtitle">{{subtitle}}</view>
<view wx:if="{{error}}" class="muted small" style="margin-top: 16rpx; color: #e05561;">⚠️ {{error}}</view>
</view>
<view wx:if="{{clipVisible}}" class="clip-card" catchtouchmove="noop">
<view class="clip-head">
<view class="clip-title">📝 检测到剪贴板文字</view>
<view class="clip-count">{{clipCount}} 字 · {{clipLines}} 行</view>
</view>
<view class="clip-preview">{{clipPreview}}</view>
<view class="clip-actions">
<view class="clip-btn clip-btn-ghost" bindtap="onClipDismissForever">不再提示</view>
<view class="clip-btn clip-btn-ghost" bindtap="onClipCancel">取消</view>
<view class="clip-btn clip-btn-primary" bindtap="onClipCollect" data-mode="new">📌 收藏为文字</view>
</view>
</view>
<view wx:if="{{loading}}" class="empty">加载中...</view>
<block wx:for="{{kits}}" wx:key="id">
<view class="kit-card" bindtap="onKitTap" data-id="{{item.id}}" data-path="{{item.path}}">
<view class="kit-icon">
<text class="kit-icon-text">{{item.icon}}</text>
</view>
<view class="kit-body">
<view class="kit-title">{{item.name}}</view>
<view class="kit-desc">{{item.description}}</view>
</view>
<view class="chevron">›</view>
</view>
</block>
</view>
@@ -0,0 +1,72 @@
/* 首页独立样式,大部分复用 app.wxss 全局样式 */
.clip-card {
margin: 20rpx 24rpx 12rpx;
padding: 24rpx 26rpx 22rpx;
background: linear-gradient(155deg, rgba(108, 122, 224, 0.18), rgba(176, 107, 224, 0.10));
border: 1px solid rgba(140, 150, 240, 0.22);
border-radius: 24rpx;
box-shadow: 0 10rpx 30rpx rgba(0,0,0,0.28);
}
.clip-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14rpx;
}
.clip-title {
font-size: 30rpx;
font-weight: 700;
color: #ececff;
letter-spacing: 0.5rpx;
}
.clip-count {
font-size: 22rpx;
color: #a0a4d0;
background: rgba(108, 122, 224, 0.22);
padding: 4rpx 16rpx;
border-radius: 999rpx;
}
.clip-preview {
max-height: 260rpx;
overflow: hidden;
padding: 18rpx 20rpx;
background: rgba(0,0,0,0.32);
border-radius: 16rpx;
font-size: 25rpx;
line-height: 1.8;
color: #d4d4ee;
white-space: pre-wrap;
word-break: break-word;
border: 1px solid rgba(255,255,255,0.06);
margin-bottom: 18rpx;
}
.clip-actions {
display: flex;
gap: 14rpx;
justify-content: flex-end;
flex-wrap: wrap;
}
.clip-btn {
padding: 12rpx 24rpx;
font-size: 25rpx;
border-radius: 14rpx;
text-align: center;
min-width: 160rpx;
}
.clip-btn-ghost {
background: rgba(255,255,255,0.04);
color: #b8b8d8;
border: 1px solid rgba(255,255,255,0.08);
}
.clip-btn-secondary {
background: rgba(108, 122, 224, 0.16);
color: #cfd4ff;
border: 1px solid rgba(108, 122, 224, 0.32);
}
.clip-btn-primary {
background: linear-gradient(135deg, #6c7ae0, #b06be0);
color: #fff;
font-weight: 600;
box-shadow: 0 8rpx 20rpx rgba(108, 122, 224, 0.32);
}
@@ -0,0 +1,57 @@
{
"description": "MilesYang时光助手 - 微信小程序版工具箱",
"packOptions": {
"ignore": [],
"include": []
},
"setting": {
"bundle": false,
"userConfirmedBundleSwitch": false,
"urlCheck": true,
"scopeDataCheck": false,
"coverView": true,
"es6": true,
"postcss": true,
"compileHotReLoad": false,
"lazyloadPlaceholderEnable": false,
"preloadBackgroundData": false,
"minified": true,
"autoAudits": false,
"newFeature": false,
"uglifyFileName": false,
"uploadWithSourceMap": true,
"useIsolateContext": true,
"nodeModules": false,
"enhance": true,
"useMultiFrameRuntime": true,
"useApiHook": true,
"useApiHostProcess": true,
"showShadowRootInWxmlPanel": true,
"packNpmManually": false,
"enableEngineNative": false,
"packNpmRelationList": [],
"minifyWXSS": true,
"showES6CompileOption": false,
"minifyWXML": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"compileWorklet": false,
"localPlugins": false,
"disableUseStrict": false,
"useCompilerPlugins": false,
"condition": false
},
"compileType": "miniprogram",
"libVersion": "3.0.0",
"appid": "wx4fe6339f60c7b2a4",
"projectname": "mp_time_assistant",
"condition": {},
"editorSetting": {
"tabIndent": "insertSpaces",
"tabSize": 2
},
"simulatorPluginLibVersion": {}
}
@@ -0,0 +1,23 @@
{
"libVersion": "3.17.2",
"projectname": "mp_time_assistant%5B%E6%97%B6%E5%85%89%E5%8A%A9%E6%89%8B%5D",
"condition": {},
"setting": {
"urlCheck": true,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"useApiHook": true,
"showShadowRootInWxmlPanel": true,
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"compileHotReLoad": true,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": false,
"useIsolateContext": true
}
}
@@ -0,0 +1,7 @@
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/sitemap.html",
"rules": [{
"action": "disallow",
"page": "*"
}]
}