init project
This commit is contained in:
6
miniprogram/pages/index/index.json
Normal file
6
miniprogram/pages/index/index.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"t-button": "tdesign-miniprogram/button/button",
|
||||
"t-input": "tdesign-miniprogram/input/input"
|
||||
}
|
||||
}
|
||||
60
miniprogram/pages/index/index.less
Normal file
60
miniprogram/pages/index/index.less
Normal file
@ -0,0 +1,60 @@
|
||||
/**index.wxss**/
|
||||
page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.index {
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
padding: 200rpx 0;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.logo {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
box-shadow: 2px 2px 8px rgba(0, 0, 0, .2);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin: 0 .5rem;
|
||||
display: inline-block;
|
||||
|
||||
&.gao {
|
||||
color: #FF7A9B;
|
||||
}
|
||||
|
||||
&.yeyu {
|
||||
color: #7A9BFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.password {
|
||||
width: 20rem;
|
||||
border-top: 1px solid rgba(0, 0, 0, .1);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.enter {
|
||||
width: 10rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
miniprogram/pages/index/index.ts
Normal file
46
miniprogram/pages/index/index.ts
Normal file
@ -0,0 +1,46 @@
|
||||
// index.ts
|
||||
|
||||
import config from "../../config/index"
|
||||
|
||||
interface IndexData {
|
||||
key: string;
|
||||
}
|
||||
|
||||
Page({
|
||||
data: <IndexData>{
|
||||
key: ""
|
||||
},
|
||||
onShow() {
|
||||
const key = wx.getStorageSync("key");
|
||||
if (key) {
|
||||
this.setData({
|
||||
key
|
||||
});
|
||||
}
|
||||
},
|
||||
navigateToMain() {
|
||||
wx.request({
|
||||
url: `${config.url}/journal/list`,
|
||||
method: "POST",
|
||||
header: {
|
||||
Key: this.data.key
|
||||
},
|
||||
data: {
|
||||
index: 0,
|
||||
size: 1
|
||||
},
|
||||
success: (resp) => {
|
||||
const data = resp.data as any;
|
||||
if (data.code === 20000) {
|
||||
wx.setStorageSync("key", this.data.key);
|
||||
wx.switchTab({
|
||||
url: "/pages/main/journal/index",
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: "密码错误", icon: "error" });
|
||||
}
|
||||
},
|
||||
fail: () => wx.showToast({ title: "验证失败", icon: "error" })
|
||||
});
|
||||
}
|
||||
})
|
||||
18
miniprogram/pages/index/index.wxml
Normal file
18
miniprogram/pages/index/index.wxml
Normal file
@ -0,0 +1,18 @@
|
||||
<!--index.wxml-->
|
||||
<scroll-view class="index" scroll-y type="list">
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<image class="logo" src="/assets/image/logo.png"></image>
|
||||
<text>
|
||||
<text class="name gao">小糕</text>
|
||||
<text>和</text>
|
||||
<text class="name yeyu">夜雨</text>
|
||||
<text>的美好记录</text>
|
||||
</text>
|
||||
</view>
|
||||
<view class="password">
|
||||
<t-input placeholder="请输入访问密码" model:value="{{key}}" borderless clearable />
|
||||
</view>
|
||||
<t-button class="enter" theme="primary" bind:tap="navigateToMain">进入</t-button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
9
miniprogram/pages/main/about/index.json
Normal file
9
miniprogram/pages/main/about/index.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-button": "tdesign-miniprogram/button/button",
|
||||
"t-navbar": "tdesign-miniprogram/navbar/navbar",
|
||||
"snowflake": "../../../components/background/snowflake"
|
||||
}
|
||||
}
|
||||
107
miniprogram/pages/main/about/index.less
Normal file
107
miniprogram/pages/main/about/index.less
Normal file
@ -0,0 +1,107 @@
|
||||
/* pages/info/info.less */
|
||||
page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
padding: 100rpx 0;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
|
||||
.cotainer {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
.logo {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
box-shadow: 2px 2px 8px rgba(0, 0, 0, .2);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin: 0 .5rem;
|
||||
display: inline-block;
|
||||
|
||||
&.gao {
|
||||
color: #FF7A9B;
|
||||
}
|
||||
|
||||
&.yeyu {
|
||||
color: #7A9BFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #777;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
|
||||
.love {
|
||||
color: transparent;
|
||||
font-size: 1rem;
|
||||
animation: loveGradient 1500ms linear infinite;
|
||||
text-align: center;
|
||||
background: linear-gradient(90deg, #FFB5C7, #FF7A9B, #FF3A6B, #FF7A9B, #FFB5C7);
|
||||
font-weight: bold;
|
||||
font-family: "Arial", sans-serif;
|
||||
margin-bottom: 1rem;
|
||||
background-size: 200% 100%;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
@keyframes loveGradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
.exit {
|
||||
color: #E64340;
|
||||
width: 10rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
|
||||
.label {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
&.copyright {
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
64
miniprogram/pages/main/about/index.ts
Normal file
64
miniprogram/pages/main/about/index.ts
Normal file
@ -0,0 +1,64 @@
|
||||
// pages/info/info.ts
|
||||
|
||||
import Time from "../../../utils/Time";
|
||||
import config from "../../../config/index"
|
||||
|
||||
interface IAboutData {
|
||||
timer?: number;
|
||||
total?: number;
|
||||
beginFriendText: string;
|
||||
}
|
||||
|
||||
Page({
|
||||
data: <IAboutData>{
|
||||
copyright: `Copyright © 2017 - ${new Date().getFullYear()} imyeyu.com`,
|
||||
timer: undefined,
|
||||
total: undefined,
|
||||
beginFriendText: "相识 -- 天 -- 小时 -- 分钟 -- 秒",
|
||||
beginLoveText: "相恋 -- 天 -- 小时 -- 分钟 -- 秒"
|
||||
},
|
||||
onShow() {
|
||||
const beginLove = new Date("2025/11/10 00:10:00");
|
||||
const beginFriend = new Date("2025/06/28 16:00:00");
|
||||
|
||||
const timer = setInterval(() => {
|
||||
{
|
||||
const r = Time.between(beginLove)
|
||||
this.setData({
|
||||
beginLoveText: `相恋 ${r.d} 天 ${r.h} 小时 ${r.m.toString().padStart(2, "0")} 分钟 ${r.s.toString().padStart(2, "0")} 秒`
|
||||
})
|
||||
}
|
||||
{
|
||||
const r = Time.between(beginFriend)
|
||||
this.setData({
|
||||
beginFriendText: `相识 ${r.d} 天 ${r.h} 小时 ${r.m.toString().padStart(2, "0")} 分钟 ${r.s.toString().padStart(2, "0")} 秒`
|
||||
})
|
||||
}
|
||||
}, 1000)
|
||||
this.setData({
|
||||
timer
|
||||
});
|
||||
|
||||
|
||||
wx.request({
|
||||
url: `${config.url}/journal/total`,
|
||||
method: "GET",
|
||||
header: {
|
||||
Key: wx.getStorageSync("key")
|
||||
},
|
||||
success: (resp: any) => {
|
||||
this.setData({
|
||||
total: resp.data.data
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
onHide() {
|
||||
this.data.timer && clearInterval(this.data.timer);
|
||||
},
|
||||
exit() {
|
||||
wx.redirectTo({
|
||||
"url": "/pages/index/index?from=info"
|
||||
})
|
||||
}
|
||||
})
|
||||
37
miniprogram/pages/main/about/index.wxml
Normal file
37
miniprogram/pages/main/about/index.wxml
Normal file
@ -0,0 +1,37 @@
|
||||
<!--pages/info/info.wxml-->
|
||||
<snowflake />
|
||||
<t-navbar title="关于我们" />
|
||||
<scroll-view class="info" scroll-y>
|
||||
<view class="cotainer">
|
||||
<view class="header">
|
||||
<image class="logo" src="/assets/image/logo.png"></image>
|
||||
<view>
|
||||
<text>记录</text>
|
||||
<text class="name gao">小糕</text>
|
||||
<text>和</text>
|
||||
<text class="name yeyu">夜雨</text>
|
||||
<text>的美好回忆</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="text">
|
||||
<view class="love">{{beginLoveText}}</view>
|
||||
<view>{{beginFriendText}}</view>
|
||||
<view wx:if="{{total}}">已留住 {{total}} 个我们的瞬间</view>
|
||||
</view>
|
||||
<view class="footer">
|
||||
<t-button class="exit" bind:tap="exit">退出</t-button>
|
||||
<view class="item">
|
||||
<text class="label">开发者:</text>
|
||||
<text>夜雨</text>
|
||||
</view>
|
||||
<view class="item">
|
||||
<text class="label">版本:</text>
|
||||
<text>1.2.2</text>
|
||||
</view>
|
||||
<view class="item copyright">
|
||||
<text>{{copyright}}</text>
|
||||
<text>All Rights Reserved 夜雨 版权所有</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
7
miniprogram/pages/main/journal-creater/index.json
Normal file
7
miniprogram/pages/main/journal-creater/index.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-button": "tdesign-miniprogram/button/button",
|
||||
"t-navbar": "tdesign-miniprogram/navbar/navbar"
|
||||
}
|
||||
}
|
||||
101
miniprogram/pages/main/journal-creater/index.less
Normal file
101
miniprogram/pages/main/journal-creater/index.less
Normal file
@ -0,0 +1,101 @@
|
||||
/* pages/main/journal-creater/index.wxss */
|
||||
.container {
|
||||
height: 100vh;
|
||||
|
||||
.content {
|
||||
width: calc(100% - 64px);
|
||||
padding: 0 32px 32px 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
.label {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.section {
|
||||
width: 100%;
|
||||
margin-top: 1.5rem;
|
||||
|
||||
&.time {
|
||||
display: flex;
|
||||
|
||||
.picker {
|
||||
margin-right: .25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.media {
|
||||
|
||||
.ctrl {
|
||||
display: flex;
|
||||
|
||||
.clear {
|
||||
width: 100px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.gallery {
|
||||
gap: 10rpx;
|
||||
display: grid;
|
||||
margin-top: 1rem;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
||||
.item {
|
||||
height: 200rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #FFF;
|
||||
box-shadow: 1px 1px 6px rgba(0, 0, 0, .1);
|
||||
border-radius: 2rpx;
|
||||
|
||||
.thumbnail {
|
||||
height: 200rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
position: relative;
|
||||
|
||||
|
||||
.play-icon {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.delete {
|
||||
top: 10rpx;
|
||||
right: 10rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
z-index: 3;
|
||||
padding: 5rpx;
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.submit {
|
||||
width: 10rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
306
miniprogram/pages/main/journal-creater/index.ts
Normal file
306
miniprogram/pages/main/journal-creater/index.ts
Normal file
@ -0,0 +1,306 @@
|
||||
// pages/main/journal-creater/index.ts
|
||||
import Events from "../../../utils/Events";
|
||||
import Time from "../../../utils/Time";
|
||||
import Toolkit from "../../../utils/Toolkit";
|
||||
import config from "../../../config/index";
|
||||
|
||||
enum MediaItemType {
|
||||
IMAGE,
|
||||
VIDEO
|
||||
}
|
||||
|
||||
type MediaItem = {
|
||||
type: MediaItemType;
|
||||
path: string;
|
||||
thumbPath: string;
|
||||
size: number;
|
||||
duration: number | undefined;
|
||||
raw: any;
|
||||
}
|
||||
|
||||
export type Location = {
|
||||
lat: number;
|
||||
lng: number;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface JournalEditorData {
|
||||
idea: string;
|
||||
date: string;
|
||||
time: string;
|
||||
mediaList: MediaItem[];
|
||||
location?: Location;
|
||||
qqMapSDK?: any;
|
||||
isAuthLocation: boolean;
|
||||
}
|
||||
|
||||
Page({
|
||||
data: <JournalEditorData>{
|
||||
idea: "",
|
||||
date: "2025-06-28",
|
||||
time: "16:00",
|
||||
mediaList: [],
|
||||
location: undefined,
|
||||
submitText: "提交",
|
||||
isSubmitting: false,
|
||||
submitProgress: 0,
|
||||
mediaItemTypeEnum: {
|
||||
...MediaItemType
|
||||
},
|
||||
isAuthLocation: false
|
||||
},
|
||||
async onLoad() {
|
||||
// 授权定位
|
||||
const setting = await wx.getSetting();
|
||||
wx.setStorageSync("isAuthLocation", setting.authSetting["scope.userLocation"]);
|
||||
let isAuthLocation = JSON.parse(wx.getStorageSync("isAuthLocation"));
|
||||
this.setData({ isAuthLocation });
|
||||
if (!isAuthLocation) {
|
||||
wx.authorize({
|
||||
scope: "scope.userLocation"
|
||||
}).then(() => {
|
||||
isAuthLocation = true;
|
||||
this.setData({ isAuthLocation });
|
||||
});
|
||||
}
|
||||
|
||||
const unixTime = new Date().getTime();
|
||||
this.setData({
|
||||
date: Time.toDate(unixTime),
|
||||
time: Time.toTime(unixTime)
|
||||
});
|
||||
// 获取默认定位
|
||||
wx.getLocation({
|
||||
type: "gcj02"
|
||||
}).then(resp => {
|
||||
this.setData({
|
||||
location: {
|
||||
lat: resp.latitude,
|
||||
lng: resp.longitude
|
||||
},
|
||||
});
|
||||
const argLoc = `location=${this.data.location!.lat},${this.data.location!.lng}`;
|
||||
const argKey = "key=WW5BZ-J4LCM-UIT6I-65MXY-Z5HDT-VRFFU";
|
||||
wx.request({
|
||||
url: `https://apis.map.qq.com/ws/geocoder/v1/?${argLoc}&${argKey}`,
|
||||
success: res => {
|
||||
if (res.statusCode === 200) {
|
||||
this.setData({
|
||||
location: {
|
||||
lat: this.data.location!.lat,
|
||||
lng: this.data.location!.lng,
|
||||
text: (res.data as any).result?.formatted_addresses?.recommend
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
async chooseLocation() {
|
||||
const location = await wx.chooseLocation({});
|
||||
this.setData({
|
||||
location: {
|
||||
lat: location.latitude,
|
||||
lng: location.longitude,
|
||||
text: location.name
|
||||
}
|
||||
});
|
||||
},
|
||||
addMedia() {
|
||||
const that = this;
|
||||
wx.chooseMedia({
|
||||
mediaType: ["mix"],
|
||||
sourceType: ["album", "camera"],
|
||||
camera: "back",
|
||||
success(res) {
|
||||
wx.showLoading({
|
||||
title: "加载中..",
|
||||
mask: true
|
||||
})
|
||||
const tempFiles = res.tempFiles;
|
||||
const mediaList = tempFiles.map(item => {
|
||||
return {
|
||||
type: (<any>MediaItemType)[item.fileType.toUpperCase()],
|
||||
path: item.tempFilePath,
|
||||
thumbPath: item.thumbTempFilePath,
|
||||
size: item.size,
|
||||
duration: item.duration,
|
||||
raw: item
|
||||
} as MediaItem;
|
||||
});
|
||||
that.setData({
|
||||
mediaList: [...that.data.mediaList, ...mediaList]
|
||||
});
|
||||
wx.hideLoading();
|
||||
}
|
||||
})
|
||||
},
|
||||
clearMedia() {
|
||||
wx.showModal({
|
||||
title: "提示",
|
||||
content: "确认清空已选照片或视频吗?",
|
||||
confirmText: "清空",
|
||||
confirmColor: "#E64340",
|
||||
cancelText: "取消",
|
||||
success: res => {
|
||||
if (res.confirm) {
|
||||
this.setData({
|
||||
mediaList: []
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
preview(e: WechatMiniprogram.BaseEvent) {
|
||||
wx.previewMedia({
|
||||
current: e.currentTarget.dataset.index,
|
||||
sources: this.data.mediaList.map(item => {
|
||||
return {
|
||||
url: item.path,
|
||||
type: MediaItemType[item.type].toLowerCase()
|
||||
} as WechatMiniprogram.MediaSource;
|
||||
})
|
||||
});
|
||||
},
|
||||
deleteMedia(e: WechatMiniprogram.BaseEvent) {
|
||||
const index = e.currentTarget.dataset.index;
|
||||
const mediaList = [...this.data.mediaList];
|
||||
mediaList.splice(index, 1);
|
||||
this.setData({
|
||||
mediaList
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
wx.switchTab({
|
||||
url: "/pages/main/journal/index",
|
||||
})
|
||||
},
|
||||
submit() {
|
||||
const handleFail = () => {
|
||||
wx.showToast({ title: "上传失败", icon: "error" });
|
||||
this.setData({
|
||||
submitText: "提交",
|
||||
isSubmitting: false
|
||||
})
|
||||
};
|
||||
|
||||
this.setData({
|
||||
submitText: "正在提交..",
|
||||
isSubmitting: true
|
||||
})
|
||||
|
||||
// 获取 openId
|
||||
const getOpenId = new Promise<string>((resolve, reject) => {
|
||||
wx.login({
|
||||
success: (res) => {
|
||||
if (res.code) {
|
||||
wx.request({
|
||||
url: `${config.url}/journal/openid`,
|
||||
method: "POST",
|
||||
header: {
|
||||
Key: wx.getStorageSync("key")
|
||||
},
|
||||
data: {
|
||||
code: res.code
|
||||
},
|
||||
success: (resp) => {
|
||||
const data = resp.data as any;
|
||||
if (data.code === 20000) {
|
||||
resolve(data.data);
|
||||
} else {
|
||||
reject(new Error("获取 openId 失败"));
|
||||
}
|
||||
},
|
||||
fail: () => reject(new Error("获取 openId 请求失败"))
|
||||
});
|
||||
} else {
|
||||
reject(new Error("获取登录凭证失败"));
|
||||
}
|
||||
},
|
||||
fail: () => reject(new Error("登录失败"))
|
||||
});
|
||||
});
|
||||
// 文件上传
|
||||
const uploadFiles = new Promise<string[]>((resolve, reject) => {
|
||||
const mediaList = this.data.mediaList || [];
|
||||
const total = mediaList.length;
|
||||
let completed = 0;
|
||||
|
||||
if (total === 0) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
// 更新进度初始状态
|
||||
this.setData({
|
||||
submitProgress: 0,
|
||||
});
|
||||
|
||||
const uploadPromises = mediaList.map((item) => {
|
||||
return new Promise<string>((uploadResolve, uploadReject) => {
|
||||
wx.uploadFile({
|
||||
url: `${config.url}/temp/file/upload`,
|
||||
filePath: item.path,
|
||||
name: "file",
|
||||
success: (resp) => {
|
||||
const result = JSON.parse(resp.data);
|
||||
if (result && result.code === 20000) {
|
||||
completed++;
|
||||
// 更新进度
|
||||
this.setData({
|
||||
submitProgress: (completed / total),
|
||||
});
|
||||
uploadResolve(result.data[0].id);
|
||||
} else {
|
||||
uploadReject(new Error(`文件上传失败: ${result?.message || '未知错误'}`));
|
||||
}
|
||||
},
|
||||
fail: (err) => uploadReject(new Error(`文件上传失败: ${err.errMsg}`))
|
||||
});
|
||||
});
|
||||
});
|
||||
// 并行执行所有文件上传
|
||||
Promise.all(uploadPromises).then((tempFileIds) => {
|
||||
this.setData({
|
||||
submitProgress: 1,
|
||||
});
|
||||
resolve(tempFileIds);
|
||||
}).catch(reject);
|
||||
});
|
||||
// 并行执行获取 openId 和文件上传
|
||||
Promise.all([getOpenId, uploadFiles]).then(([openId, tempFileIds]) => {
|
||||
wx.request({
|
||||
url: `${config.url}/journal/create`,
|
||||
method: "POST",
|
||||
header: {
|
||||
Key: wx.getStorageSync("key")
|
||||
},
|
||||
data: {
|
||||
type: "NORMAL",
|
||||
idea: this.data.idea,
|
||||
createdAt: Date.parse(`${this.data.date} ${this.data.time}`),
|
||||
lat: this.data.location?.lat,
|
||||
lng: this.data.location?.lng,
|
||||
location: this.data.location?.text,
|
||||
pusher: openId,
|
||||
tempFileIds
|
||||
},
|
||||
success: async (resp: any) => {
|
||||
Events.emit("JOURNAL_REFRESH");
|
||||
wx.showToast({ title: "提交成功", icon: "success" });
|
||||
this.setData({
|
||||
idea: "",
|
||||
mediaList: [],
|
||||
submitText: "提交",
|
||||
isSubmitting: false,
|
||||
});
|
||||
await Toolkit.sleep(1000);
|
||||
wx.switchTab({
|
||||
url: "/pages/main/journal/index",
|
||||
})
|
||||
},
|
||||
fail: handleFail
|
||||
});
|
||||
}).catch(handleFail);
|
||||
}
|
||||
});
|
||||
104
miniprogram/pages/main/journal-creater/index.wxml
Normal file
104
miniprogram/pages/main/journal-creater/index.wxml
Normal file
@ -0,0 +1,104 @@
|
||||
<!--pages/main/journal-creater/index.wxml-->
|
||||
<t-navbar title="新纪录">
|
||||
<text slot="left" bindtap="cancel">取消</text>
|
||||
</t-navbar>
|
||||
<scroll-view
|
||||
class="container"
|
||||
type="custom"
|
||||
scroll-y
|
||||
show-scrollbar="{{false}}"
|
||||
scroll-into-view="{{intoView}}"
|
||||
>
|
||||
<view class="content">
|
||||
<view class="section">
|
||||
<textarea
|
||||
class="idea"
|
||||
placeholder="这一刻的想法..."
|
||||
model:value="{{idea}}"
|
||||
/>
|
||||
</view>
|
||||
<view class="section time">
|
||||
<text class="label">时间:</text>
|
||||
<picker class="picker" mode="date" model:value="{{date}}">
|
||||
<view class="picker">
|
||||
{{date}}
|
||||
</view>
|
||||
</picker>
|
||||
<picker class="picker" mode="time" model:value="{{time}}">
|
||||
<view class="picker">
|
||||
{{time}}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="section location">
|
||||
<text class="label">位置:</text>
|
||||
<text wx:if="{{location}}" bind:tap="chooseLocation">{{location.text}}</text>
|
||||
<text wx:else bind:tap="chooseLocation">选择位置..</text>
|
||||
</view>
|
||||
<view class="section media">
|
||||
<view class="ctrl">
|
||||
<t-button
|
||||
class="select"
|
||||
theme="primary"
|
||||
plain="true"
|
||||
disabled="{{isSubmitting}}"
|
||||
bind:tap="addMedia"
|
||||
>选择照片/视频</t-button>
|
||||
<t-button
|
||||
class="clear"
|
||||
theme="danger"
|
||||
variant="outline"
|
||||
disabled="{{isSubmitting}}"
|
||||
bind:tap="clearMedia"
|
||||
disabled="{{mediaList.length === 0}}"
|
||||
>清空已选</t-button>
|
||||
</view>
|
||||
<view class="gallery">
|
||||
<block wx:for="{{mediaList}}" wx:key="index">
|
||||
<view class="item">
|
||||
<!-- 图片 -->
|
||||
<image
|
||||
wx:if="{{item.type === mediaItemTypeEnum.IMAGE}}"
|
||||
src="{{item.path}}"
|
||||
class="thumbnail"
|
||||
mode="aspectFill"
|
||||
bindtap="preview"
|
||||
data-index="{{index}}"
|
||||
></image>
|
||||
<!-- 视频 -->
|
||||
<view wx:if="{{item.type === mediaItemTypeEnum.VIDEO}}" class="video-container">
|
||||
<image
|
||||
src="{{item.thumbPath}}"
|
||||
class="thumbnail"
|
||||
mode="aspectFill"
|
||||
bindtap="preview"
|
||||
data-index="{{index}}"
|
||||
></image>
|
||||
<image class="play-icon" src="/assets/icon/play.png"></image>
|
||||
</view>
|
||||
<!-- 删除 -->
|
||||
<image
|
||||
src="/assets/icon/delete.png"
|
||||
class="delete"
|
||||
bindtap="deleteMedia"
|
||||
data-index="{{index}}"
|
||||
></image>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
<progress
|
||||
wx:if="{{isSubmitting}}"
|
||||
class="progress"
|
||||
percent="{{submitProgress.toFixed(2) * 100}}"
|
||||
show-info
|
||||
stroke-width="4"
|
||||
/>
|
||||
<t-button
|
||||
class="submit"
|
||||
theme="primary"
|
||||
bind:tap="submit"
|
||||
disabled="{{(!idea && mediaList.length === 0) || isSubmitting}}"
|
||||
>{{submitText}}</t-button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
3
miniprogram/pages/main/journal-list/index.json
Normal file
3
miniprogram/pages/main/journal-list/index.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
miniprogram/pages/main/journal-list/index.less
Normal file
1
miniprogram/pages/main/journal-list/index.less
Normal file
@ -0,0 +1 @@
|
||||
/* pages/main/journal-list/index.wxss */
|
||||
66
miniprogram/pages/main/journal-list/index.ts
Normal file
66
miniprogram/pages/main/journal-list/index.ts
Normal file
@ -0,0 +1,66 @@
|
||||
// pages/main/journal-list/index.ts
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
2
miniprogram/pages/main/journal-list/index.wxml
Normal file
2
miniprogram/pages/main/journal-list/index.wxml
Normal file
@ -0,0 +1,2 @@
|
||||
<!--pages/main/journal-list/index.wxml-->
|
||||
<text>pages/main/journal-list/index.wxml</text>
|
||||
13
miniprogram/pages/main/journal/index.json
Normal file
13
miniprogram/pages/main/journal/index.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-cell": "tdesign-miniprogram/cell/cell",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-navbar": "tdesign-miniprogram/navbar/navbar",
|
||||
"t-indexes": "tdesign-miniprogram/indexes/indexes",
|
||||
"t-calendar": "tdesign-miniprogram/calendar/calendar",
|
||||
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
|
||||
"t-indexes-anchor": "tdesign-miniprogram/indexes-anchor/indexes-anchor"
|
||||
},
|
||||
"styleIsolation": "shared"
|
||||
}
|
||||
162
miniprogram/pages/main/journal/index.less
Normal file
162
miniprogram/pages/main/journal/index.less
Normal file
@ -0,0 +1,162 @@
|
||||
.custom-navbar {
|
||||
|
||||
.more {
|
||||
width: 24px;
|
||||
height: 18px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
top: calc(50% - 1px);
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, .8);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: calc(100% - 4px);
|
||||
position: absolute;
|
||||
border-top: 2px solid rgba(0, 0, 0, .8);
|
||||
border-bottom: 2px solid rgba(0, 0, 0, .8);
|
||||
}
|
||||
}
|
||||
|
||||
.more-menu {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
background: rgba(0, 0, 0, .1);
|
||||
|
||||
.content {
|
||||
margin: 200rpx 0 0 12rpx;
|
||||
z-index: 1;
|
||||
position: fixed;
|
||||
background: rgba(255, 255, 255, .95);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 12px rgba(0, 0, 0, .2);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
margin: -10rpx 0 0 24rpx;
|
||||
border-top: 24rpx solid rgba(255, 255, 255, .95);
|
||||
border-left: 24rpx solid transparent;
|
||||
z-index: 1;
|
||||
position: fixed;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calendar {
|
||||
|
||||
// .t-calendar__dates-item {
|
||||
// color: var(--td-text-color-disabled);
|
||||
|
||||
// &.t-calendar__dates-item--selected {
|
||||
// color: var(--td-calendar-title-color);
|
||||
// background: transparent;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
.journal-list {
|
||||
width: 100vw;
|
||||
|
||||
.content {
|
||||
|
||||
.date {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text,
|
||||
.items {
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text {
|
||||
width: calc(100% - 32px - 2rem);
|
||||
padding: 8px 16px;
|
||||
margin: .5rem 1rem 1rem 1rem;
|
||||
position: relative;
|
||||
background: #FFF8E1;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, .2);
|
||||
border-radius: 2px;
|
||||
|
||||
// 纸张纹理效果
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
linear-gradient(90deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0.3) 50%,
|
||||
rgba(255, 255, 255, 0) 100%),
|
||||
linear-gradient(rgba(0, 0, 0, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 0, 0, 0.03) 1px, transparent 1px);
|
||||
pointer-events: none;
|
||||
background-size: 100% 100%, 10px 10px, 10px 10px;
|
||||
}
|
||||
|
||||
.location {
|
||||
color: #777;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.items {
|
||||
column-gap: .25rem;
|
||||
column-count: 3;
|
||||
padding-bottom: 2rem;
|
||||
|
||||
.item {
|
||||
overflow: hidden;
|
||||
background: #FFF;
|
||||
break-inside: avoid;
|
||||
margin-bottom: .25rem;
|
||||
|
||||
&.thumbnail {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.video {
|
||||
height: auto;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
top: 50%;
|
||||
left: 53%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
border-top: 16px solid transparent;
|
||||
border-left: 24px solid rgba(255, 255, 255, .9);
|
||||
border-bottom: 16px solid transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.start {
|
||||
color: #777;
|
||||
padding: 1rem 0;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
244
miniprogram/pages/main/journal/index.ts
Normal file
244
miniprogram/pages/main/journal/index.ts
Normal file
@ -0,0 +1,244 @@
|
||||
// pages/journal/index.ts
|
||||
|
||||
import Time from "../../../utils/Time";
|
||||
import config from "../../../config/index"
|
||||
import Events from "../../../utils/Events";
|
||||
import Toolkit from "../../../utils/Toolkit";
|
||||
|
||||
export type Journal = {
|
||||
date: string;
|
||||
location?: string;
|
||||
lat?: number;
|
||||
lng?: number;
|
||||
idea?: string;
|
||||
items: JournalItem[]
|
||||
}
|
||||
|
||||
export type JournalItem = {
|
||||
type: JournalItemType;
|
||||
mongoId: string;
|
||||
}
|
||||
|
||||
export enum JournalItemType {
|
||||
IMAGE,
|
||||
VIDEO
|
||||
}
|
||||
|
||||
interface JournalData {
|
||||
page: {
|
||||
index: number;
|
||||
size: number;
|
||||
type: string;
|
||||
orderMap?: object;
|
||||
}
|
||||
list: Journal[];
|
||||
dateFilterMin: number;
|
||||
dateFilterMax: number;
|
||||
dateFilterAllows: number[];
|
||||
dateFilterVisible: boolean;
|
||||
isFetching: boolean;
|
||||
isFinished: boolean;
|
||||
stickyOffset: number;
|
||||
isShowMoreMenu: boolean;
|
||||
}
|
||||
|
||||
Page({
|
||||
data: <JournalData>{
|
||||
page: {
|
||||
index: 0,
|
||||
size: 8,
|
||||
type: "NORMAL",
|
||||
orderMap: {
|
||||
createdAt: "DESC"
|
||||
}
|
||||
},
|
||||
list: [],
|
||||
dateFilterMin: new Date("2025/06/28 16:00:00").getTime(),
|
||||
dateFilterMax: new Date(2026, 1, 15).getTime(),
|
||||
dateFilterAllows: [
|
||||
new Date(2025, 11, 15).getTime(),
|
||||
new Date(2025, 11, 20).getTime(),
|
||||
new Date(2025, 11, 10).getTime(),
|
||||
],
|
||||
dateFilterVisible: false,
|
||||
isFetching: false,
|
||||
isFinished: false,
|
||||
stickyOffset: 0,
|
||||
isShowMoreMenu: false
|
||||
},
|
||||
onLoad() {
|
||||
Events.reset("JOURNAL_REFRESH", () => {
|
||||
this.setData({
|
||||
page: {
|
||||
index: 0,
|
||||
size: 8,
|
||||
type: "NORMAL",
|
||||
orderMap: {
|
||||
createdAt: "DESC"
|
||||
}
|
||||
},
|
||||
list: [],
|
||||
isFetching: false,
|
||||
isFinished: false
|
||||
});
|
||||
this.fetch();
|
||||
});
|
||||
this.setData({
|
||||
list: []
|
||||
})
|
||||
this.fetch();
|
||||
// 可选日期
|
||||
wx.request({
|
||||
url: `${config.url}/journal/list/date?key=${wx.getStorageSync("key")}`,
|
||||
method: "GET",
|
||||
success: async (resp: any) => {
|
||||
const dates = resp.data.data.sort((a: number, b: number) => a - b);
|
||||
this.setData({
|
||||
// dateFilterMin: dates[0],
|
||||
// dateFilterMax: dates[dates.length - 1],
|
||||
dateFilterAllows: dates,
|
||||
// dateFilterVisible: this.data.dateFilterVisible
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
onReady() {
|
||||
this.getCustomNavbarHeight();
|
||||
},
|
||||
onHide() {
|
||||
this.setData({
|
||||
isShowMoreMenu: false
|
||||
})
|
||||
},
|
||||
onReachBottom() {
|
||||
this.fetch();
|
||||
},
|
||||
getCustomNavbarHeight() {
|
||||
const query = wx.createSelectorQuery();
|
||||
query.select(".custom-navbar").boundingClientRect();
|
||||
query.exec((res) => {
|
||||
const { height = 0 } = res[0] || {};
|
||||
this.setData({ stickyOffset: height });
|
||||
});
|
||||
},
|
||||
toggleMoreMenu() {
|
||||
this.setData({
|
||||
isShowMoreMenu: !this.data.isShowMoreMenu
|
||||
})
|
||||
},
|
||||
openDateFilter() {
|
||||
this.setData({
|
||||
dateFilterVisible: true
|
||||
});
|
||||
},
|
||||
tapCalendar(e: any) {
|
||||
console.log(e);
|
||||
},
|
||||
toDateFilter(e: any) {
|
||||
console.log(e);
|
||||
// console.log(Toolkit.symmetricDiff(this.data.dateFilter.allows, e.detail.value));
|
||||
},
|
||||
fetch() {
|
||||
if (this.data.isFetching || this.data.isFinished) {
|
||||
return;
|
||||
}
|
||||
this.setData({
|
||||
isFetching: true
|
||||
});
|
||||
wx.request({
|
||||
url: `${config.url}/journal/list`,
|
||||
method: "POST",
|
||||
header: {
|
||||
Key: wx.getStorageSync("key")
|
||||
},
|
||||
data: this.data.page,
|
||||
success: async (resp: any) => {
|
||||
const list = resp.data.data.list;
|
||||
if (!list || list.length === 0) {
|
||||
this.setData({
|
||||
isFinished: true
|
||||
})
|
||||
return;
|
||||
}
|
||||
const result = list.map((journal: any) => {
|
||||
return {
|
||||
date: Time.toPassedDateTime(journal.createdAt),
|
||||
idea: journal.idea,
|
||||
lat: journal.lat,
|
||||
lng: journal.lng,
|
||||
location: journal.location,
|
||||
items: journal.items.filter((item: any) => item.attachType === "THUMB").map((item: any) => {
|
||||
const ext = JSON.parse(item.ext);
|
||||
return {
|
||||
type: ext.isVideo ? JournalItemType.VIDEO : JournalItemType.IMAGE,
|
||||
thumbUrl: `${config.url}/attachment/read/${item.mongoId}`,
|
||||
mongoId: item.mongoId,
|
||||
source: journal.items.find((source: any) => source.id === ext.sourceId)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
this.setData({
|
||||
page: {
|
||||
index: this.data.page.index + 1,
|
||||
size: 8,
|
||||
type: "NORMAL",
|
||||
orderMap: {
|
||||
createdAt: "DESC"
|
||||
}
|
||||
},
|
||||
list: this.data.list.concat(result),
|
||||
isFinished: list.length < this.data.page.size
|
||||
});
|
||||
},
|
||||
complete: () => {
|
||||
this.setData({
|
||||
isFetching: false
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
preview(e: WechatMiniprogram.BaseEvent) {
|
||||
const journalIndex = e.target.dataset.journalIndex;
|
||||
const itemIndex = e.target.dataset.itemIndex;
|
||||
const items = this.data.list[journalIndex].items;
|
||||
const total = items.length;
|
||||
|
||||
const startIndex = Math.max(0, itemIndex - 25);
|
||||
const endIndex = Math.min(total, startIndex + 50);
|
||||
const newCurrentIndex = itemIndex - startIndex;
|
||||
|
||||
const sources = items.slice(startIndex, endIndex).map((item: any) => {
|
||||
return {
|
||||
url: `${config.url}/attachment/read/${item.source.mongoId}`,
|
||||
type: item.type === 0 ? "image" : "video"
|
||||
}
|
||||
}) as any;
|
||||
wx.previewMedia({
|
||||
current: newCurrentIndex,
|
||||
sources
|
||||
})
|
||||
},
|
||||
openLocation(e: WechatMiniprogram.BaseEvent) {
|
||||
const journalIndex = e.target.dataset.journalIndex;
|
||||
const journal = this.data.list[journalIndex] as Journal;
|
||||
if (journal.lat && journal.lng) {
|
||||
wx.openLocation({
|
||||
latitude: journal.lat,
|
||||
longitude: journal.lng,
|
||||
});
|
||||
}
|
||||
},
|
||||
toCreater() {
|
||||
wx.navigateTo({
|
||||
"url": "/pages/main/journal-creater/index?from=journal"
|
||||
})
|
||||
},
|
||||
toDetail() {
|
||||
wx.showToast({
|
||||
title: "此功能暂不可用",
|
||||
icon: "none",
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
});
|
||||
56
miniprogram/pages/main/journal/index.wxml
Normal file
56
miniprogram/pages/main/journal/index.wxml
Normal file
@ -0,0 +1,56 @@
|
||||
<view class="custom-navbar">
|
||||
<t-navbar title="我们的记录">
|
||||
<view slot="left" class="more" bind:tap="toggleMoreMenu">
|
||||
<view wx:if="{{isShowMoreMenu}}" class="more-menu">
|
||||
<t-cell-group class="content" theme="card">
|
||||
<t-cell title="新纪录" leftIcon="add" bind:tap="toCreater" />
|
||||
<t-cell title="按列表查找" leftIcon="view-list" bind:tap="toDetail" />
|
||||
<t-cell title="按日期查找" leftIcon="calendar-1" bind:tap="openDateFilter" />
|
||||
<t-cell title="按地图查找" leftIcon="location" bind:tap="toDetail" />
|
||||
</t-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
</t-navbar>
|
||||
</view>
|
||||
<t-calendar
|
||||
class="calendar"
|
||||
type="multiple"
|
||||
min-date="{{dateFilterMin}}"
|
||||
max-date="{{dateFilterMax}}"
|
||||
value="{{dateFilterAllows}}"
|
||||
visible="{{dateFilterVisible}}"
|
||||
switch-mode="year-month"
|
||||
confirm-btn="{{null}}"
|
||||
bind:tap="tapCalendar"
|
||||
/>
|
||||
<t-indexes
|
||||
class="journal-list"
|
||||
bind:scroll="onScroll"
|
||||
sticky-offset="{{stickyOffset}}"
|
||||
>
|
||||
<view class="content" wx:for="{{list}}" wx:for-item="journal" wx:for-index="journalIndex" wx:key="index">
|
||||
<t-indexes-anchor class="date" index="{{journal.date}}" />
|
||||
<view wx:if="{{journal.idea || journal.location}}" class="text">
|
||||
<text class="idea">{{journal.idea}}</text>
|
||||
<view
|
||||
wx:if="{{journal.location}}"
|
||||
class="location"
|
||||
bind:tap="openLocation"
|
||||
data-journal-index="{{journalIndex}}"
|
||||
>📍 {{journal.location}}</view>
|
||||
</view>
|
||||
<view wx:if="{{journal.items}}" class="items">
|
||||
<block wx:for="{{journal.items}}" wx:for-item="item" wx:for-index="itemIndex" wx:key="date">
|
||||
<image
|
||||
class="item thumbnail {{item.type === 0 ? 'image' : 'video'}}"
|
||||
src="{{item.thumbUrl}}"
|
||||
mode="widthFix"
|
||||
bindtap="preview"
|
||||
data-item-index="{{itemIndex}}"
|
||||
data-journal-index="{{journalIndex}}"
|
||||
></image>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{isFinished}}" class="start">已回到最初的起点</view>
|
||||
</t-indexes>
|
||||
10
miniprogram/pages/main/moment/index.json
Normal file
10
miniprogram/pages/main/moment/index.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"t-popup": "tdesign-miniprogram/popup/popup",
|
||||
"t-radio": "tdesign-miniprogram/radio/radio",
|
||||
"t-navbar": "tdesign-miniprogram/navbar/navbar",
|
||||
"t-button": "tdesign-miniprogram/button/button",
|
||||
"t-checkbox": "tdesign-miniprogram/checkbox/checkbox",
|
||||
"t-radio-group": "tdesign-miniprogram/radio-group/radio-group"
|
||||
}
|
||||
}
|
||||
168
miniprogram/pages/main/moment/index.less
Normal file
168
miniprogram/pages/main/moment/index.less
Normal file
@ -0,0 +1,168 @@
|
||||
/* pages/main/moment/index.wxss */
|
||||
.moment {
|
||||
|
||||
.tips {
|
||||
padding: 4px 12px;
|
||||
font-size: .8rem;
|
||||
text-align: justify;
|
||||
text-indent: 2em;
|
||||
}
|
||||
|
||||
.action {
|
||||
padding: 8px 12px;
|
||||
|
||||
.line {
|
||||
display: flex;
|
||||
margin-bottom: .5rem;
|
||||
justify-content: space-between;
|
||||
|
||||
.btn {
|
||||
width: 50%;
|
||||
margin-right: 1rem;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.uploading {
|
||||
padding: 4px 12px;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.progress {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #777;
|
||||
display: flex;
|
||||
font-size: .8rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.items {
|
||||
column-gap: .25rem;
|
||||
column-count: 4;
|
||||
padding-bottom: 2rem;
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
|
||||
.thumbnail {
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
background: #FFF;
|
||||
break-inside: avoid;
|
||||
margin-bottom: .25rem;
|
||||
|
||||
&.video {
|
||||
height: auto;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
top: 50%;
|
||||
left: 53%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
border-top: 16px solid transparent;
|
||||
border-left: 24px solid rgba(255, 255, 255, .9);
|
||||
border-bottom: 16px solid transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 16rpx 0 16rpx 16rpx;
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.checkbox-fill {
|
||||
top: 24rpx;
|
||||
right: 26rpx;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
background: #FFF;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.archive-popup {
|
||||
|
||||
.container {
|
||||
width: 100vw;
|
||||
background: var(--td-bg-color-container);
|
||||
border-top-left-radius: 16rpx;
|
||||
border-top-right-radius: 16rpx;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 116rpx;
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
color: var(--td-text-color-primary);
|
||||
font-size: 36rpx;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin: 0 .5rem;
|
||||
|
||||
&.cancel {
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
|
||||
&.confirm {
|
||||
color: #07C160;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16rpx 32rpx;
|
||||
|
||||
.section {
|
||||
width: 100%;
|
||||
margin-top: 1.5rem;
|
||||
|
||||
.label {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
&.type {
|
||||
display: flex;
|
||||
|
||||
.radio {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
&.time {
|
||||
display: flex;
|
||||
|
||||
.picker {
|
||||
margin-right: .25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
535
miniprogram/pages/main/moment/index.ts
Normal file
535
miniprogram/pages/main/moment/index.ts
Normal file
@ -0,0 +1,535 @@
|
||||
// pages/main/moment/index.ts
|
||||
import config from "../../../config/index";
|
||||
import Events from "../../../utils/Events";
|
||||
import IOSize, { Unit } from "../../../utils/IOSize";
|
||||
import Time from "../../../utils/Time";
|
||||
import Toolkit from "../../../utils/Toolkit";
|
||||
import type { Location } from "../journal-creater/index";
|
||||
|
||||
type Item = {
|
||||
id: number;
|
||||
type: ItemType;
|
||||
mongoId: string;
|
||||
thumbUrl: string;
|
||||
sourceMongoId: string;
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
enum ItemType {
|
||||
IMAGE,
|
||||
VIDEO
|
||||
}
|
||||
|
||||
type MD5Result = {
|
||||
path: string;
|
||||
md5: string;
|
||||
}
|
||||
|
||||
interface MomentData {
|
||||
list: Item[];
|
||||
type: string;
|
||||
idea: string;
|
||||
date: string;
|
||||
time: string;
|
||||
location?: Location;
|
||||
qqMapSDK?: any;
|
||||
uploaded: string;
|
||||
hasChecked: boolean;
|
||||
isUploading: boolean;
|
||||
isArchiving: boolean;
|
||||
uploadSpeed: string;
|
||||
uploadTotal: string;
|
||||
uploadProgress: number;
|
||||
isAuthLocation: boolean;
|
||||
isVisibleArchivePopup: boolean;
|
||||
}
|
||||
|
||||
Page({
|
||||
data: <MomentData>{
|
||||
list: [],
|
||||
type: "NORMAL",
|
||||
idea: "",
|
||||
date: "2025-06-28",
|
||||
time: "16:00",
|
||||
location: undefined,
|
||||
uploaded: "0",
|
||||
hasChecked: false,
|
||||
isUploading: false,
|
||||
isArchiving: false,
|
||||
uploadSpeed: "0 MB / s",
|
||||
uploadTotal: "0 MB",
|
||||
uploadProgress: 0,
|
||||
isAuthLocation: false,
|
||||
isVisibleArchivePopup: false
|
||||
},
|
||||
async onLoad() {
|
||||
this.fetch();
|
||||
|
||||
// 授权定位
|
||||
const setting = await wx.getSetting();
|
||||
wx.setStorageSync("isAuthLocation", setting.authSetting["scope.userLocation"]);
|
||||
let isAuthLocation = JSON.parse(wx.getStorageSync("isAuthLocation"));
|
||||
this.setData({ isAuthLocation });
|
||||
if (!isAuthLocation) {
|
||||
wx.authorize({
|
||||
scope: "scope.userLocation"
|
||||
}).then(() => {
|
||||
isAuthLocation = true;
|
||||
this.setData({ isAuthLocation });
|
||||
});
|
||||
}
|
||||
const unixTime = new Date().getTime();
|
||||
this.setData({
|
||||
idea: this.data.idea,
|
||||
date: Time.toDate(unixTime),
|
||||
time: Time.toTime(unixTime)
|
||||
});
|
||||
// 获取默认定位
|
||||
wx.getLocation({
|
||||
type: "gcj02"
|
||||
}).then(resp => {
|
||||
this.setData({
|
||||
location: {
|
||||
lat: resp.latitude,
|
||||
lng: resp.longitude
|
||||
},
|
||||
});
|
||||
const argLoc = `location=${this.data.location!.lat},${this.data.location!.lng}`;
|
||||
const argKey = "key=WW5BZ-J4LCM-UIT6I-65MXY-Z5HDT-VRFFU";
|
||||
wx.request({
|
||||
url: `https://apis.map.qq.com/ws/geocoder/v1/?${argLoc}&${argKey}`,
|
||||
success: res => {
|
||||
if (res.statusCode === 200) {
|
||||
this.setData({
|
||||
location: {
|
||||
lat: this.data.location!.lat,
|
||||
lng: this.data.location!.lng,
|
||||
text: (res.data as any).result?.formatted_addresses?.recommend
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
async chooseLocation() {
|
||||
const location = await wx.chooseLocation({});
|
||||
this.setData({
|
||||
location: {
|
||||
lat: location.latitude,
|
||||
lng: location.longitude,
|
||||
text: location.name
|
||||
}
|
||||
});
|
||||
},
|
||||
fetch() {
|
||||
wx.request({
|
||||
url: `${config.url}/journal/moment/list`,
|
||||
method: "POST",
|
||||
header: {
|
||||
Key: wx.getStorageSync("key")
|
||||
},
|
||||
success: async (resp: any) => {
|
||||
const list = resp.data.data;
|
||||
if (!list || list.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.setData({
|
||||
list: list.map((item: any) => {
|
||||
const extData = JSON.parse(item.ext);
|
||||
return {
|
||||
id: item.id,
|
||||
type: extData.isImage ? ItemType.IMAGE : ItemType.VIDEO,
|
||||
mongoId: item.mongoId,
|
||||
thumbUrl: `${config.url}/attachment/read/${item.mongoId}`,
|
||||
sourceMongoId: extData.sourceMongoId,
|
||||
checked: false
|
||||
} as Item;
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
updateHasChecked() {
|
||||
this.setData({ hasChecked: this.data.list.some(item => item.checked) });
|
||||
},
|
||||
onCheckChange(e: WechatMiniprogram.BaseEvent) {
|
||||
const index = e.currentTarget.dataset.index;
|
||||
const list = [...this.data.list];
|
||||
list[index].checked = !list[index].checked;
|
||||
this.setData({ list });
|
||||
this.updateHasChecked();
|
||||
},
|
||||
preview(e: WechatMiniprogram.BaseEvent) {
|
||||
const index = e.currentTarget.dataset.index;
|
||||
const total = this.data.list.length;
|
||||
|
||||
const startIndex = Math.max(0, index - 25);
|
||||
const endIndex = Math.min(total, startIndex + 50);
|
||||
const newCurrentIndex = index - startIndex;
|
||||
|
||||
const sources = this.data.list.slice(startIndex, endIndex).map((item: Item) => {
|
||||
return {
|
||||
url: `${config.url}/attachment/read/${item.sourceMongoId}`,
|
||||
type: item.type === 0 ? "image" : "video"
|
||||
}
|
||||
}) as any;
|
||||
wx.previewMedia({
|
||||
current: newCurrentIndex,
|
||||
sources
|
||||
})
|
||||
},
|
||||
uploadMedia() {
|
||||
const handleFail = (e?: any) => {
|
||||
wx.showToast({ title: "上传失败", icon: "error" });
|
||||
wx.hideLoading();
|
||||
this.setData({
|
||||
isUploading: false,
|
||||
});
|
||||
wx.reportEvent("wxdata_perf_monitor", {
|
||||
wxdata_perf_monitor_id: "MOMENT_UPLOAD",
|
||||
wxdata_perf_monitor_level: 9,
|
||||
wxdata_perf_error_code: 1,
|
||||
wxdata_perf_error_msg: e?.message
|
||||
});
|
||||
};
|
||||
const that = this;
|
||||
wx.chooseMedia({
|
||||
mediaType: ["mix"],
|
||||
sourceType: ["album", "camera"],
|
||||
camera: "back",
|
||||
async success(res) {
|
||||
that.setData({
|
||||
isUploading: true
|
||||
});
|
||||
wx.showLoading({
|
||||
title: "正在读取..",
|
||||
mask: true
|
||||
})
|
||||
let files = res.tempFiles;
|
||||
// 计算 MD5
|
||||
const md5Results: MD5Result[] = [];
|
||||
await Promise.all(files.map(async (file) => {
|
||||
const md5 = await new Promise((resolve, reject) => {
|
||||
wx.getFileSystemManager().getFileInfo({
|
||||
filePath: file.tempFilePath,
|
||||
digestAlgorithm: "md5",
|
||||
success: res => resolve(res.digest),
|
||||
fail: () => reject(`读取失败: ${file.tempFilePath}`)
|
||||
});
|
||||
});
|
||||
md5Results.push({
|
||||
path: file.tempFilePath,
|
||||
md5: md5,
|
||||
} as MD5Result);
|
||||
}));
|
||||
// 查重
|
||||
const filterMD5Result: string[] = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: `${config.url}/journal/moment/filter`,
|
||||
method: "POST",
|
||||
header: {
|
||||
Key: wx.getStorageSync("key")
|
||||
},
|
||||
data: md5Results.map(item => item.md5),
|
||||
success: async (resp: any) => {
|
||||
resolve(resp.data.data);
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
// 过滤文件
|
||||
const filterPath = md5Results.filter(item => filterMD5Result.indexOf(item.md5) !== -1)
|
||||
.map(item => item.path);
|
||||
files = files.filter(file => filterPath.indexOf(file.tempFilePath) !== -1);
|
||||
if (files.length === 0) {
|
||||
wx.hideLoading();
|
||||
return;
|
||||
}
|
||||
wx.showLoading({
|
||||
title: "正在上传..",
|
||||
mask: true
|
||||
})
|
||||
// 计算上传大小
|
||||
const sizePromises = files.map(file => {
|
||||
return new Promise<number>((sizeResolve, sizeReject) => {
|
||||
wx.getFileSystemManager().getFileInfo({
|
||||
filePath: file.tempFilePath,
|
||||
success: (res) => sizeResolve(res.size),
|
||||
fail: (err) => sizeReject(err)
|
||||
});
|
||||
});
|
||||
});
|
||||
Promise.all(sizePromises).then(fileSizes => {
|
||||
const totalSize = fileSizes.reduce((acc, size) => acc + size, 0);
|
||||
const uploadTasks: WechatMiniprogram.UploadTask[] = [];
|
||||
let uploadedSize = 0;
|
||||
let lastUploadedSize = 0;
|
||||
|
||||
that.setData({
|
||||
uploadTotal: IOSize.format(totalSize, 2, Unit.MB)
|
||||
});
|
||||
|
||||
// 计算上传速度
|
||||
const speedUpdateInterval = setInterval(() => {
|
||||
const chunkSize = uploadedSize - lastUploadedSize;
|
||||
that.setData({
|
||||
uploadSpeed: `${IOSize.format(chunkSize)} / s`
|
||||
});
|
||||
lastUploadedSize = uploadedSize;
|
||||
}, 1000);
|
||||
// 上传文件
|
||||
const uploadPromises = files.map(file => {
|
||||
return new Promise<string>((uploadResolve, uploadReject) => {
|
||||
const task = wx.uploadFile({
|
||||
url: `${config.url}/temp/file/upload`,
|
||||
filePath: file.tempFilePath,
|
||||
name: "file",
|
||||
success: (resp) => {
|
||||
const result = JSON.parse(resp.data);
|
||||
if (result && result.code === 20000) {
|
||||
// 更新进度
|
||||
const progress = totalSize > 0 ? uploadedSize / totalSize : 1;
|
||||
that.setData({
|
||||
uploadProgress: Math.round(progress * 10000) / 100
|
||||
});
|
||||
uploadResolve(result.data[0].id);
|
||||
} else {
|
||||
uploadReject(new Error(`文件上传失败: ${result?.message || '未知错误'}`));
|
||||
}
|
||||
},
|
||||
fail: (err) => uploadReject(new Error(`文件上传失败: ${err.errMsg}`))
|
||||
});
|
||||
// 监听上传进度事件
|
||||
let prevProgress = 0;
|
||||
task.onProgressUpdate((res) => {
|
||||
const fileUploaded = (res.totalBytesExpectedToSend * res.progress) / 100;
|
||||
const delta = fileUploaded - prevProgress;
|
||||
uploadedSize += delta;
|
||||
// 保存当前进度
|
||||
prevProgress = fileUploaded;
|
||||
// 更新进度条
|
||||
that.setData({
|
||||
uploaded: IOSize.formatWithoutUnit(uploadedSize, 2, Unit.MB),
|
||||
uploadProgress: Math.round((uploadedSize / totalSize) * 10000) / 100
|
||||
});
|
||||
});
|
||||
uploadTasks.push(task);
|
||||
});
|
||||
});
|
||||
Promise.all(uploadPromises).then((tempFileIds) => {
|
||||
wx.showLoading({
|
||||
title: "正在保存..",
|
||||
mask: true
|
||||
})
|
||||
// 清除定时器
|
||||
clearInterval(speedUpdateInterval);
|
||||
uploadTasks.forEach(task => task.offProgressUpdate());
|
||||
that.setData({
|
||||
uploadProgress: 100,
|
||||
uploadSpeed: "0 MB / s"
|
||||
});
|
||||
// 上传完成转附件
|
||||
wx.request({
|
||||
url: `${config.url}/journal/moment/create`,
|
||||
method: "POST",
|
||||
header: {
|
||||
Key: wx.getStorageSync("key")
|
||||
},
|
||||
data: tempFileIds,
|
||||
success: async (resp: any) => {
|
||||
wx.showToast({ title: "上传成功", icon: "success" });
|
||||
const list = resp.data.data;
|
||||
const added = list.map((item: any) => {
|
||||
const extData = JSON.parse(item.ext);
|
||||
return {
|
||||
id: item.id,
|
||||
type: extData.isImage ? ItemType.IMAGE : ItemType.VIDEO,
|
||||
mongoId: item.mongoId,
|
||||
thumbUrl: `${config.url}/attachment/read/${item.mongoId}`,
|
||||
sourceMongoId: extData.sourceMongoId,
|
||||
checked: false
|
||||
} as Item;
|
||||
});
|
||||
// 前插列表
|
||||
that.data.list.unshift(...added);
|
||||
that.setData({
|
||||
list: that.data.list,
|
||||
isUploading: false,
|
||||
uploaded: "0",
|
||||
uploadTotal: "0 MB",
|
||||
uploadProgress: 0
|
||||
});
|
||||
that.updateHasChecked();
|
||||
wx.hideLoading();
|
||||
},
|
||||
fail: handleFail
|
||||
});
|
||||
}).catch((e: Error) => {
|
||||
// 取消所有上传任务
|
||||
uploadTasks.forEach(task => task.abort());
|
||||
that.updateHasChecked();
|
||||
handleFail(e);
|
||||
});
|
||||
}).catch(handleFail);
|
||||
}
|
||||
})
|
||||
},
|
||||
toggleArchivePopup() {
|
||||
this.setData({
|
||||
isVisibleArchivePopup: !this.data.isVisibleArchivePopup
|
||||
});
|
||||
},
|
||||
onArchivePopupVisibleChange(e: any) {
|
||||
this.setData({
|
||||
isVisibleArchivePopup: e.detail.visible
|
||||
});
|
||||
},
|
||||
onChangeArchiveType(e: any) {
|
||||
const { value } = e.detail;
|
||||
this.setData({ type: value });
|
||||
},
|
||||
async archiveChecked() {
|
||||
const handleFail = () => {
|
||||
wx.showToast({ title: "归档失败", icon: "error" });
|
||||
wx.hideLoading();
|
||||
this.setData({
|
||||
isArchiving: false
|
||||
});
|
||||
};
|
||||
this.setData({
|
||||
isArchiving: true
|
||||
});
|
||||
wx.showLoading({
|
||||
title: "正在归档..",
|
||||
mask: true
|
||||
})
|
||||
const openId = await new Promise<string>((resolve, reject) => {
|
||||
wx.login({
|
||||
success: (res) => {
|
||||
if (res.code) {
|
||||
wx.request({
|
||||
url: `${config.url}/journal/openid`,
|
||||
method: "POST",
|
||||
header: {
|
||||
Key: wx.getStorageSync("key")
|
||||
},
|
||||
data: {
|
||||
code: res.code
|
||||
},
|
||||
success: (resp) => {
|
||||
const data = resp.data as any;
|
||||
if (data.code === 20000) {
|
||||
resolve(data.data);
|
||||
} else {
|
||||
reject(new Error("获取 openId 失败"));
|
||||
}
|
||||
},
|
||||
fail: () => reject(new Error("获取 openId 请求失败"))
|
||||
});
|
||||
} else {
|
||||
reject(new Error("获取登录凭证失败"));
|
||||
}
|
||||
},
|
||||
fail: handleFail
|
||||
});
|
||||
});
|
||||
wx.request({
|
||||
url: `${config.url}/journal/moment/archive`,
|
||||
method: "POST",
|
||||
header: {
|
||||
Key: wx.getStorageSync("key")
|
||||
},
|
||||
data: {
|
||||
type: this.data.type,
|
||||
idea: this.data.idea,
|
||||
createdAt: Date.parse(`${this.data.date} ${this.data.time}`),
|
||||
lat: this.data.location?.lat,
|
||||
lng: this.data.location?.lng,
|
||||
location: this.data.location?.text,
|
||||
pusher: openId,
|
||||
thumbIds: this.data.list.filter(item => item.checked).map(item => item.id)
|
||||
},
|
||||
success: async (resp: any) => {
|
||||
if (resp.data && resp.data.code === 20000) {
|
||||
Events.emit("JOURNAL_REFRESH");
|
||||
wx.showToast({ title: "归档成功", icon: "success" });
|
||||
this.setData({
|
||||
idea: "",
|
||||
list: [],
|
||||
hasChecked: false,
|
||||
isArchiving: false
|
||||
});
|
||||
await Toolkit.sleep(1000);
|
||||
this.fetch();
|
||||
this.toggleArchivePopup();
|
||||
} else {
|
||||
wx.showToast({ title: "归档失败", icon: "error" });
|
||||
this.setData({
|
||||
isArchiving: false
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: handleFail
|
||||
});
|
||||
},
|
||||
allChecked() {
|
||||
this.data.list.forEach(item => item.checked = true);
|
||||
this.setData({
|
||||
list: this.data.list
|
||||
});
|
||||
this.updateHasChecked();
|
||||
},
|
||||
clearChecked() {
|
||||
wx.showModal({
|
||||
title: "提示",
|
||||
content: "确认清空已选照片或视频吗?",
|
||||
confirmText: "清空已选",
|
||||
confirmColor: "#E64340",
|
||||
cancelText: "取消",
|
||||
success: res => {
|
||||
if (res.confirm) {
|
||||
this.data.list.forEach(item => {
|
||||
item.checked = false;
|
||||
})
|
||||
this.setData({
|
||||
list: this.data.list
|
||||
});
|
||||
this.updateHasChecked();
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteChecked() {
|
||||
wx.showModal({
|
||||
title: "提示",
|
||||
content: "确认删除已选照片或视频吗?",
|
||||
confirmText: "删除已选",
|
||||
confirmColor: "#E64340",
|
||||
cancelText: "取消",
|
||||
success: res => {
|
||||
if (res.confirm) {
|
||||
const selected = this.data.list.filter(item => item.checked);
|
||||
wx.request({
|
||||
url: `${config.url}/journal/moment/delete`,
|
||||
method: "POST",
|
||||
header: {
|
||||
Key: wx.getStorageSync("key")
|
||||
},
|
||||
data: selected.map(item => item.id),
|
||||
success: async (resp: any) => {
|
||||
if (resp.data && resp.data.code === 20000) {
|
||||
wx.showToast({ title: "删除成功", icon: "success" });
|
||||
const list = this.data.list.filter(item => !item.checked);
|
||||
this.setData({
|
||||
list
|
||||
});
|
||||
this.updateHasChecked();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
141
miniprogram/pages/main/moment/index.wxml
Normal file
141
miniprogram/pages/main/moment/index.wxml
Normal file
@ -0,0 +1,141 @@
|
||||
<!--pages/main/moment/index.wxml-->
|
||||
<view class="custom-navbar">
|
||||
<t-navbar class="custom-navbar" title="瞬间" />
|
||||
</view>
|
||||
<view class="moment">
|
||||
<view class="tips">
|
||||
<text>由于微信限制,一次只能上传 20 张照片或视频,可以先在此页面分批次将所有照片或视频上传,</text>
|
||||
<text style="color: #F30">记得勾选原图</text>
|
||||
<text>。无需担心重复选择,已上传的文件不会再次上传。上传后有空再挑选归档,归档后此页面不再显示该照片或视频。</text>
|
||||
</view>
|
||||
<view class="action">
|
||||
<view class="line">
|
||||
<t-button
|
||||
class="btn upload"
|
||||
theme="primary"
|
||||
plain="true"
|
||||
disabled="{{isSubmitting}}"
|
||||
bind:tap="uploadMedia"
|
||||
>上传照片/视频</t-button>
|
||||
<t-button
|
||||
class="btn archive"
|
||||
theme="primary"
|
||||
plain="true"
|
||||
bind:tap="toggleArchivePopup"
|
||||
disabled="{{isSubmitting || list.length === 0 || !hasChecked}}"
|
||||
>归档已选</t-button>
|
||||
</view>
|
||||
<view class="line">
|
||||
<t-button
|
||||
class="btn clear"
|
||||
theme="primary"
|
||||
variant="outline"
|
||||
bind:tap="allChecked"
|
||||
disabled="{{isSubmitting || list.length === 0}}"
|
||||
>全选</t-button>
|
||||
<t-button
|
||||
class="btn clear"
|
||||
theme="danger"
|
||||
variant="outline"
|
||||
bind:tap="clearChecked"
|
||||
disabled="{{isSubmitting || list.length === 0 || !hasChecked}}"
|
||||
>清空已选</t-button>
|
||||
<t-button
|
||||
class="btn delete"
|
||||
theme="danger"
|
||||
variant="outline"
|
||||
bind:tap="deleteChecked"
|
||||
disabled="{{isSubmitting || list.length === 0 || !hasChecked}}"
|
||||
>删除已选</t-button>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{isUploading}}" class="uploading">
|
||||
<progress
|
||||
class="progress"
|
||||
percent="{{uploadProgress}}"
|
||||
stroke-width="6"
|
||||
/>
|
||||
<view class="text">
|
||||
<view>{{uploaded}} / {{uploadTotal}}</view>
|
||||
<view>{{uploadSpeed}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="items">
|
||||
<view class="item" wx:for="{{list}}" wx:key="mongoId">
|
||||
<image
|
||||
class="thumbnail {{item.type === 0 ? 'image' : 'video'}}"
|
||||
src="{{item.thumbUrl}}"
|
||||
mode="widthFix"
|
||||
bind:tap="preview"
|
||||
data-index="{{index}}"
|
||||
></image>
|
||||
<t-checkbox
|
||||
class="checkbox"
|
||||
block="{{true}}"
|
||||
checked="{{item.checked}}"
|
||||
bind:change="onCheckChange"
|
||||
data-index="{{index}}"
|
||||
/>
|
||||
<view wx:if="{{item.checked}}" class="checkbox-fill"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<t-popup
|
||||
class="archive-popup"
|
||||
visible="{{isVisibleArchivePopup}}"
|
||||
placement="bottom"
|
||||
usingCustomNavbar
|
||||
bind:visible-change="onArchivePopupVisibleChange"
|
||||
>
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<t-button
|
||||
class="btn cancel"
|
||||
aria-role="button"
|
||||
bind:tap="toggleArchivePopup"
|
||||
variant="text"
|
||||
>取消</t-button>
|
||||
<view class="title">归档已选项</view>
|
||||
<t-button
|
||||
class="btn confirm"
|
||||
aria-role="button"
|
||||
bind:tap="archiveChecked"
|
||||
variant="text"
|
||||
>确定</t-button>
|
||||
</view>
|
||||
<view class="content">
|
||||
<view class="section">
|
||||
<textarea
|
||||
class="idea"
|
||||
placeholder="那一刻的想法..."
|
||||
model:value="{{idea}}"
|
||||
/>
|
||||
</view>
|
||||
<view class="section type">
|
||||
<text class="label">类型:</text>
|
||||
<t-radio-group bind:change="onChangeArchiveType" default-value="NORMAL" borderless t-class="box">
|
||||
<t-radio class="radio" block="{{false}}" label="日常" value="NORMAL" />
|
||||
<t-radio class="radio" block="{{false}}" label="专拍" value="PORTFOLIO" />
|
||||
</t-radio-group>
|
||||
</view>
|
||||
<view class="section time">
|
||||
<text class="label">时间:</text>
|
||||
<picker class="picker" mode="date" model:value="{{date}}">
|
||||
<view class="picker">
|
||||
{{date}}
|
||||
</view>
|
||||
</picker>
|
||||
<picker class="picker" mode="time" model:value="{{time}}">
|
||||
<view class="picker">
|
||||
{{time}}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="section location">
|
||||
<text class="label">位置:</text>
|
||||
<text wx:if="{{location}}" bind:tap="chooseLocation">{{location.text}}</text>
|
||||
<text wx:else bind:tap="chooseLocation">选择位置..</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</t-popup>
|
||||
7
miniprogram/pages/main/portfolio/index.json
Normal file
7
miniprogram/pages/main/portfolio/index.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"t-navbar": "tdesign-miniprogram/navbar/navbar",
|
||||
"t-collapse": "tdesign-miniprogram/collapse/collapse",
|
||||
"t-collapse-panel": "tdesign-miniprogram/collapse-panel/collapse-panel"
|
||||
}
|
||||
}
|
||||
40
miniprogram/pages/main/portfolio/index.less
Normal file
40
miniprogram/pages/main/portfolio/index.less
Normal file
@ -0,0 +1,40 @@
|
||||
/* pages/main/portfolio/index.wxss */
|
||||
.portfolio-list {
|
||||
width: 100vw;
|
||||
|
||||
.items {
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
column-gap: .25rem;
|
||||
column-count: 3;
|
||||
padding-bottom: 2rem;
|
||||
|
||||
.item {
|
||||
overflow: hidden;
|
||||
background: #FFF;
|
||||
break-inside: avoid;
|
||||
margin-bottom: .25rem;
|
||||
|
||||
&.thumbnail {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.video-container {
|
||||
height: auto;
|
||||
position: relative;
|
||||
|
||||
.play-icon {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
151
miniprogram/pages/main/portfolio/index.ts
Normal file
151
miniprogram/pages/main/portfolio/index.ts
Normal file
@ -0,0 +1,151 @@
|
||||
// pages/main/portfolio/index.ts
|
||||
|
||||
import config from "../../../config/index";
|
||||
import Events from "../../../utils/Events";
|
||||
import Time from "../../../utils/Time";
|
||||
import { Journal, JournalItemType } from "../journal/index";
|
||||
|
||||
interface IPortfolioData {
|
||||
page: {
|
||||
index: number;
|
||||
size: number;
|
||||
type: string;
|
||||
orderMap?: object;
|
||||
}
|
||||
list: Journal[];
|
||||
isFetching: boolean;
|
||||
isFinished: boolean;
|
||||
stickyOffset: number;
|
||||
}
|
||||
|
||||
Page({
|
||||
data: <IPortfolioData>{
|
||||
page: {
|
||||
index: 0,
|
||||
size: 8,
|
||||
type: "PORTFOLIO",
|
||||
orderMap: {
|
||||
createdAt: "DESC"
|
||||
}
|
||||
},
|
||||
list: [],
|
||||
isFetching: false,
|
||||
isFinished: false,
|
||||
stickyOffset: 0
|
||||
},
|
||||
onLoad() {
|
||||
Events.reset("JOURNAL_PORTFOLIO_REFRESH", () => {
|
||||
this.setData({
|
||||
page: {
|
||||
index: 0,
|
||||
size: 8,
|
||||
type: "PORTFOLIO",
|
||||
orderMap: {
|
||||
createdAt: "DESC"
|
||||
}
|
||||
},
|
||||
list: [],
|
||||
isFetching: false,
|
||||
isFinished: false
|
||||
});
|
||||
this.fetch();
|
||||
});
|
||||
this.getCustomNavbarHeight();
|
||||
this.setData({
|
||||
list: []
|
||||
})
|
||||
this.fetch();
|
||||
},
|
||||
onReachBottom() {
|
||||
this.fetch();
|
||||
},
|
||||
getCustomNavbarHeight() {
|
||||
const query = wx.createSelectorQuery();
|
||||
query.select(".custom-navbar").boundingClientRect();
|
||||
query.exec((res) => {
|
||||
const { height = 0 } = res[0] || {};
|
||||
this.setData({ stickyOffset: height });
|
||||
});
|
||||
},
|
||||
fetch() {
|
||||
if (this.data.isFetching || this.data.isFinished) {
|
||||
return;
|
||||
}
|
||||
this.setData({
|
||||
isFetching: true
|
||||
});
|
||||
wx.request({
|
||||
url: `${config.url}/journal/list`,
|
||||
method: "POST",
|
||||
header: {
|
||||
Key: wx.getStorageSync("key")
|
||||
},
|
||||
data: this.data.page,
|
||||
success: async (resp: any) => {
|
||||
const list = resp.data.data.list;
|
||||
if (!list || list.length === 0) {
|
||||
this.setData({
|
||||
isFinished: true
|
||||
})
|
||||
return;
|
||||
}
|
||||
const result = list.map((journal: any) => {
|
||||
return {
|
||||
date: Time.toPassedDate(journal.createdAt),
|
||||
idea: journal.idea,
|
||||
lat: journal.lat,
|
||||
lng: journal.lng,
|
||||
location: journal.location,
|
||||
items: journal.items.filter((item: any) => item.attachType === "THUMB").map((item: any) => {
|
||||
const ext = JSON.parse(item.ext);
|
||||
return {
|
||||
type: ext.isVideo ? JournalItemType.VIDEO : JournalItemType.IMAGE,
|
||||
thumbUrl: `${config.url}/attachment/read/${item.mongoId}`,
|
||||
mongoId: item.mongoId,
|
||||
source: journal.items.find((source: any) => source.id === ext.sourceId)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
this.setData({
|
||||
page: {
|
||||
index: this.data.page.index + 1,
|
||||
size: 8,
|
||||
type: "PORTFOLIO",
|
||||
orderMap: {
|
||||
createdAt: "DESC"
|
||||
}
|
||||
},
|
||||
list: this.data.list.concat(result),
|
||||
isFinished: list.length < this.data.page.size
|
||||
});
|
||||
},
|
||||
complete: () => {
|
||||
this.setData({
|
||||
isFetching: false
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
preview(e: WechatMiniprogram.BaseEvent) {
|
||||
const journalIndex = e.target.dataset.journalIndex;
|
||||
const itemIndex = e.target.dataset.itemIndex;
|
||||
const items = this.data.list[journalIndex].items;
|
||||
const total = items.length;
|
||||
|
||||
const startIndex = Math.max(0, itemIndex - 25);
|
||||
const endIndex = Math.min(total, startIndex + 50);
|
||||
const newCurrentIndex = itemIndex - startIndex;
|
||||
|
||||
const sources = items.slice(startIndex, endIndex).map((item: any) => {
|
||||
return {
|
||||
url: `${config.url}/attachment/read/${item.source.mongoId}`,
|
||||
type: item.type === 0 ? "image" : "video"
|
||||
}
|
||||
}) as any;
|
||||
wx.previewMedia({
|
||||
current: newCurrentIndex,
|
||||
sources
|
||||
})
|
||||
}
|
||||
});
|
||||
36
miniprogram/pages/main/portfolio/index.wxml
Normal file
36
miniprogram/pages/main/portfolio/index.wxml
Normal file
@ -0,0 +1,36 @@
|
||||
<!--pages/main/portfolio/index.wxml-->
|
||||
<view class="custom-navbar">
|
||||
<t-navbar class="custom-navbar" title="专拍" />
|
||||
</view>
|
||||
<view class="portfolio-list">
|
||||
<t-collapse class="collapse" expandMutex expandIcon>
|
||||
<t-collapse-panel
|
||||
class="panel"
|
||||
wx:for="{{list}}"
|
||||
header="{{journal.idea}}"
|
||||
value="{{journalIndex}}"
|
||||
header-right-content="{{journal.date}}"
|
||||
wx:for-item="journal"
|
||||
wx:for-index="journalIndex"
|
||||
wx:key="journalIndex"
|
||||
>
|
||||
<view wx:if="{{journal.items}}" class="items">
|
||||
<block
|
||||
wx:for="{{journal.items}}"
|
||||
wx:for-item="item"
|
||||
wx:for-index="itemIndex"
|
||||
wx:key="itemIndex"
|
||||
>
|
||||
<image
|
||||
class="item thumbnail"
|
||||
src="{{item.thumbUrl}}"
|
||||
mode="widthFix"
|
||||
bindtap="preview"
|
||||
data-journal-index="{{journalIndex}}"
|
||||
data-item-index="{{itemIndex}}"
|
||||
></image>
|
||||
</block>
|
||||
</view>
|
||||
</t-collapse-panel>
|
||||
</t-collapse>
|
||||
</view>
|
||||
9
miniprogram/pages/main/travel/index.json
Normal file
9
miniprogram/pages/main/travel/index.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-cell": "tdesign-miniprogram/cell/cell",
|
||||
"t-navbar": "tdesign-miniprogram/navbar/navbar",
|
||||
"t-collapse": "tdesign-miniprogram/collapse/collapse",
|
||||
"t-collapse-panel": "tdesign-miniprogram/collapse-panel/collapse-panel"
|
||||
}
|
||||
}
|
||||
25
miniprogram/pages/main/travel/index.less
Normal file
25
miniprogram/pages/main/travel/index.less
Normal file
@ -0,0 +1,25 @@
|
||||
/* pages/main/travel/travel.wxss */
|
||||
|
||||
.travel {
|
||||
|
||||
.collapse {
|
||||
|
||||
.panel {
|
||||
|
||||
.images {
|
||||
column-gap: .25rem;
|
||||
column-count: 3;
|
||||
padding-bottom: 2rem;
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
background: #FFF;
|
||||
break-inside: avoid;
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
107
miniprogram/pages/main/travel/index.ts
Normal file
107
miniprogram/pages/main/travel/index.ts
Normal file
@ -0,0 +1,107 @@
|
||||
// pages/main/travel/travel.ts
|
||||
|
||||
import config from "../../../config/index";
|
||||
|
||||
export type Luggage = {
|
||||
gao: LuggageItem[];
|
||||
yu: LuggageItem[];
|
||||
}
|
||||
|
||||
export type LuggageItem = {
|
||||
name: string;
|
||||
isTaken: boolean;
|
||||
}
|
||||
|
||||
type Guide = {
|
||||
title: string;
|
||||
images: string[];
|
||||
}
|
||||
|
||||
interface ITravelData {
|
||||
luggage?: Luggage;
|
||||
guides: Guide[];
|
||||
guidesDB: Guide[];
|
||||
activeCollapse?: number;
|
||||
}
|
||||
|
||||
Page({
|
||||
data: <ITravelData>{
|
||||
luggage: undefined,
|
||||
guides: [],
|
||||
guidesDB: [],
|
||||
activeCollapse: undefined
|
||||
},
|
||||
onLoad() {
|
||||
wx.request({
|
||||
url: `${config.url}/journal/travel`,
|
||||
method: "GET",
|
||||
header: {
|
||||
Key: wx.getStorageSync("key")
|
||||
},
|
||||
success: async (resp: any) => {
|
||||
this.setData({
|
||||
luggage: resp.data.data.luggage,
|
||||
guides: resp.data.data.guides.map((item: any) => {
|
||||
return {
|
||||
title: item.title,
|
||||
images: [] // 留空分批加载
|
||||
}
|
||||
}),
|
||||
guidesDB: resp.data.data.guides
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
onShow() {
|
||||
wx.request({
|
||||
url: `${config.url}/journal/travel`,
|
||||
method: "GET",
|
||||
header: {
|
||||
Key: wx.getStorageSync("key")
|
||||
},
|
||||
success: async (resp: any) => {
|
||||
this.setData({
|
||||
luggage: resp.data.data.luggage
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
toLuggageList(e: WechatMiniprogram.BaseEvent) {
|
||||
const name = e.target.dataset.name;
|
||||
wx.setStorageSync("luggage", {
|
||||
name,
|
||||
luggage: this.data.luggage
|
||||
});
|
||||
wx.navigateTo({
|
||||
"url": "/pages/main/travel/luggage/index"
|
||||
})
|
||||
},
|
||||
onCollapseChange(e: any) {
|
||||
const index = e.detail.value;
|
||||
if (this.data.guides[index].images.length === 0) {
|
||||
this.data.guides[index].images = this.data.guidesDB[index].images.map((item: any) => {
|
||||
return `${config.url}/attachment/read/${item}`;
|
||||
});
|
||||
this.setData({
|
||||
guides: this.data.guides
|
||||
})
|
||||
}
|
||||
this.setData({
|
||||
activeCollapse: index
|
||||
})
|
||||
},
|
||||
preview(e: WechatMiniprogram.BaseEvent) {
|
||||
const index = e.target.dataset.index;
|
||||
const imageIndex = e.target.dataset.imageIndex;
|
||||
const images = this.data.guides[index].images;
|
||||
wx.previewMedia({
|
||||
current: imageIndex,
|
||||
sources: images.map((image: any) => {
|
||||
return {
|
||||
url: image,
|
||||
type: "image"
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
});
|
||||
30
miniprogram/pages/main/travel/index.wxml
Normal file
30
miniprogram/pages/main/travel/index.wxml
Normal file
@ -0,0 +1,30 @@
|
||||
<!--pages/main/travel/travel.wxml-->
|
||||
<view class="custom-navbar">
|
||||
<t-navbar title="北海之旅" />
|
||||
</view>
|
||||
<view class="travel">
|
||||
<t-cell title="小糕的旅行装备" arrow bindtap="toLuggageList" data-name="gao"></t-cell>
|
||||
<t-cell title="夜雨的旅行装备" arrow bindtap="toLuggageList" data-name="yu"></t-cell>
|
||||
<t-collapse class="collapse" bind:change="onCollapseChange" expandMutex expandIcon>
|
||||
<t-collapse-panel
|
||||
class="panel"
|
||||
wx:for="{{guides}}"
|
||||
header="{{item.title}}"
|
||||
value="{{index}}"
|
||||
wx:key="index"
|
||||
>
|
||||
<view wx:if="{{item.images}}" class="images">
|
||||
<block wx:for="{{item.images}}" wx:for-item="image" wx:for-index="imageIndex" wx:key="imageIndex">
|
||||
<image
|
||||
class="image"
|
||||
src="{{image}}"
|
||||
mode="widthFix"
|
||||
bindtap="preview"
|
||||
data-index="{{index}}"
|
||||
data-image-index="{{imageIndex}}"
|
||||
></image>
|
||||
</block>
|
||||
</view>
|
||||
</t-collapse-panel>
|
||||
</t-collapse>
|
||||
</view>
|
||||
10
miniprogram/pages/main/travel/luggage/index.json
Normal file
10
miniprogram/pages/main/travel/luggage/index.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-input": "tdesign-miniprogram/input/input",
|
||||
"t-navbar": "tdesign-miniprogram/navbar/navbar",
|
||||
"t-button": "tdesign-miniprogram/button/button",
|
||||
"t-checkbox": "tdesign-miniprogram/checkbox/checkbox",
|
||||
"t-checkbox-group": "tdesign-miniprogram/checkbox-group/checkbox-group"
|
||||
}
|
||||
}
|
||||
88
miniprogram/pages/main/travel/luggage/index.less
Normal file
88
miniprogram/pages/main/travel/luggage/index.less
Normal file
@ -0,0 +1,88 @@
|
||||
.luggage {
|
||||
|
||||
.tips {
|
||||
color: #777;
|
||||
margin: .5rem 0;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.items {
|
||||
gap: 8px;
|
||||
width: calc(100% - 64rpx);
|
||||
margin: 12rpx auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
||||
.item {
|
||||
--td-checkbox-vertical-padding: 12rpx 24rpx;
|
||||
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
border: 3rpx solid #CCC;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
word-break: break-all;
|
||||
border-radius: 12rpx;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--td-brand-color, #0052d9);
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-width: 24px 24px 24px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--td-brand-color);
|
||||
border-bottom-color: transparent;
|
||||
border-right-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
color: var(--td-bg-color-container, #fff);
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
height: calc(100% - 24rpx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-container {
|
||||
left: 0;
|
||||
right: 0;
|
||||
color: #333;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
padding: 20rpx;
|
||||
position: fixed;
|
||||
border-top: 1px solid rgba(0, 0, 0, .1);
|
||||
background: rgba(240, 240, 240, .8);
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
.input {
|
||||
--td-input-vertical-padding: 8rpx;
|
||||
margin-right: .5rem;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
86
miniprogram/pages/main/travel/luggage/index.ts
Normal file
86
miniprogram/pages/main/travel/luggage/index.ts
Normal file
@ -0,0 +1,86 @@
|
||||
// pages/main/travel/luggage/index.ts
|
||||
|
||||
import { LuggageItem } from "..";
|
||||
import config from "../../../../config/index"
|
||||
|
||||
interface ILuggageData {
|
||||
name: string;
|
||||
value: LuggageItem[];
|
||||
keyboardHeight: number;
|
||||
addValue: string;
|
||||
}
|
||||
|
||||
Page({
|
||||
data: <ILuggageData>{
|
||||
name: "",
|
||||
value: [],
|
||||
keyboardHeight: 0,
|
||||
addValue: ""
|
||||
},
|
||||
onLoad() {
|
||||
wx.onKeyboardHeightChange(res => {
|
||||
this.setData({
|
||||
keyboardHeight: res.height
|
||||
})
|
||||
})
|
||||
},
|
||||
onShow() {
|
||||
const store = wx.getStorageSync("luggage");
|
||||
const value = store.luggage[store.name];
|
||||
this.setData({
|
||||
value,
|
||||
name: store.name === "gao" ? "小糕" : "夜雨"
|
||||
});
|
||||
},
|
||||
doBack() {
|
||||
const store = wx.getStorageSync("luggage");
|
||||
store.luggage[store.name] = this.data.value;
|
||||
wx.request({
|
||||
url: `${config.url}/journal/travel/luggage/update`,
|
||||
method: "POST",
|
||||
header: {
|
||||
Key: wx.getStorageSync("key")
|
||||
},
|
||||
data: store.luggage,
|
||||
success: () => {
|
||||
wx.navigateBack();
|
||||
}
|
||||
});
|
||||
},
|
||||
onTapItem(e: WechatMiniprogram.BaseEvent) {
|
||||
const index = e.currentTarget.dataset.index;
|
||||
this.data.value[index].isTaken = !this.data.value[index].isTaken;
|
||||
this.setData({ value: this.data.value });
|
||||
},
|
||||
showMenu(e: WechatMiniprogram.BaseEvent) {
|
||||
const index = e.currentTarget.dataset.index;
|
||||
wx.showActionSheet({
|
||||
itemList: ["删除"],
|
||||
itemColor: "red",
|
||||
success: () => {
|
||||
this.data.value.splice(index, 1);
|
||||
this.setData({ value: this.data.value })
|
||||
}
|
||||
});
|
||||
},
|
||||
onInputFocus() {
|
||||
this.setData({
|
||||
keyboardHeight: this.data.keyboardHeight
|
||||
})
|
||||
},
|
||||
onInputBlur() {
|
||||
this.setData({
|
||||
keyboardHeight: 0
|
||||
})
|
||||
},
|
||||
add() {
|
||||
this.data.value.push({
|
||||
name: this.data.addValue,
|
||||
isTaken: false
|
||||
})
|
||||
this.setData({
|
||||
value: this.data.value,
|
||||
addValue: ""
|
||||
})
|
||||
}
|
||||
})
|
||||
39
miniprogram/pages/main/travel/luggage/index.wxml
Normal file
39
miniprogram/pages/main/travel/luggage/index.wxml
Normal file
@ -0,0 +1,39 @@
|
||||
<!--pages/main/travel/luggage/index.wxml-->
|
||||
<wxs module="_"> module.exports.contain = function(arr, key) { return arr.indexOf(key) > -1 } </wxs>
|
||||
|
||||
<view class="custom-navbar">
|
||||
<t-navbar title="{{name}}的旅行装备" bind:go-back="doBack" delta="0" left-arrow />
|
||||
</view>
|
||||
<view class="luggage">
|
||||
<view class="tips">tips: 勾选表示已携带,返回自动保存</view>
|
||||
<t-checkbox-group class="items">
|
||||
<view
|
||||
class="item {{item.isTaken ? 'active' : ''}}"
|
||||
wx:for="{{value}}"
|
||||
wx:key="index"
|
||||
data-index="{{index}}"
|
||||
bindtap="onTapItem"
|
||||
bindlongpress="showMenu"
|
||||
>
|
||||
<t-icon wx:if="{{item.isTaken}}" name="check" class="icon" ariaHidden="{{true}}" />
|
||||
<t-checkbox
|
||||
class="checkbox"
|
||||
value="{{item.isTaken}}"
|
||||
label="{{item.name}}"
|
||||
icon="none"
|
||||
borderless
|
||||
/>
|
||||
</view>
|
||||
</t-checkbox-group>
|
||||
</view>
|
||||
<view class="add-container" style="transform: translateY(-{{keyboardHeight}}px)">
|
||||
<t-input
|
||||
class="input"
|
||||
placeholder="请输入新的物品"
|
||||
cursor-spacing="20"
|
||||
adjust-position="{{false}}"
|
||||
model:value="{{addValue}}"
|
||||
borderless
|
||||
/>
|
||||
<t-button size="small" theme="primary" bindtap="add" disabled="{{!addValue}}">添加</t-button>
|
||||
</view>
|
||||
Reference in New Issue
Block a user