init project
61
.gitignore
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
# 依赖目录
|
||||
node_modules/
|
||||
miniprogram_npm/
|
||||
|
||||
# 构建输出目录
|
||||
dist/
|
||||
unpackage/
|
||||
|
||||
# 环境变量文件
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# 日志文件
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# 操作系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# 编辑器目录和设置
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# 微信开发者工具生成文件
|
||||
project.private.config.json
|
||||
sitemap.json
|
||||
typings/
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
||||
*.bak
|
||||
|
||||
# 调试文件
|
||||
debug.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# 本地配置
|
||||
config/local.js
|
||||
settings/private.json
|
||||
|
||||
# 云开发相关(如使用)
|
||||
cloudfunctions/**/node_modules/
|
||||
cloudfunctions/**/*.log
|
||||
|
||||
# 其他
|
||||
*.zip
|
||||
*.rar
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
[Tt]humbs.db
|
||||
66
miniprogram/app.json
Normal file
@ -0,0 +1,66 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/main/journal/index",
|
||||
"pages/main/journal-creater/index",
|
||||
"pages/main/portfolio/index",
|
||||
"pages/main/travel/index",
|
||||
"pages/main/about/index",
|
||||
"pages/main/travel/luggage/index",
|
||||
"pages/main/moment/index",
|
||||
"pages/main/journal-list/index"
|
||||
],
|
||||
"window": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#FFFFFF"
|
||||
},
|
||||
"lazyCodeLoading": "requiredComponents",
|
||||
"tabBar": {
|
||||
"color": "#8a8a8a",
|
||||
"selectedColor": "#07C160",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderStyle": "white",
|
||||
"list": [
|
||||
{
|
||||
"text": "归档",
|
||||
"pagePath": "pages/main/journal/index",
|
||||
"iconPath": "assets/icon/journal.png",
|
||||
"selectedIconPath": "assets/icon/journal_active.png"
|
||||
},
|
||||
{
|
||||
"text": "专拍",
|
||||
"pagePath": "pages/main/portfolio/index",
|
||||
"iconPath": "assets/icon/portfolio.png",
|
||||
"selectedIconPath": "assets/icon/portfolio_active.png"
|
||||
},
|
||||
{
|
||||
"text": "瞬间",
|
||||
"pagePath": "pages/main/moment/index",
|
||||
"iconPath": "assets/icon/moment.png",
|
||||
"selectedIconPath": "assets/icon/moment_active.png"
|
||||
},
|
||||
{
|
||||
"text": "旅行",
|
||||
"pagePath": "pages/main/travel/index",
|
||||
"iconPath": "assets/icon/travel.png",
|
||||
"selectedIconPath": "assets/icon/travel_active.png"
|
||||
},
|
||||
{
|
||||
"text": "关于",
|
||||
"pagePath": "pages/main/about/index",
|
||||
"iconPath": "assets/icon/info.png",
|
||||
"selectedIconPath": "assets/icon/info_active.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
"permission": {
|
||||
"scope.userLocation": {
|
||||
"desc": "您的位置信息将用于发布新的日记记录定位"
|
||||
}
|
||||
},
|
||||
"requiredPrivateInfos": [
|
||||
"getLocation",
|
||||
"chooseLocation"
|
||||
]
|
||||
}
|
||||
7
miniprogram/app.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// app.ts
|
||||
App<IAppOption>({
|
||||
globalData: {
|
||||
},
|
||||
onLaunch() {
|
||||
},
|
||||
})
|
||||
2
miniprogram/app.wxss
Normal file
@ -0,0 +1,2 @@
|
||||
/**app.wxss**/
|
||||
@import "./tdesign.wxss";
|
||||
BIN
miniprogram/assets/icon/delete.png
Normal file
|
After Width: | Height: | Size: 416 B |
BIN
miniprogram/assets/icon/info.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
miniprogram/assets/icon/info_active.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
miniprogram/assets/icon/journal.png
Normal file
|
After Width: | Height: | Size: 699 B |
BIN
miniprogram/assets/icon/journal_active.png
Normal file
|
After Width: | Height: | Size: 762 B |
BIN
miniprogram/assets/icon/moment.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
miniprogram/assets/icon/moment_active.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
miniprogram/assets/icon/play.png
Normal file
|
After Width: | Height: | Size: 517 B |
BIN
miniprogram/assets/icon/portfolio.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
miniprogram/assets/icon/portfolio_active.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
miniprogram/assets/icon/travel.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
miniprogram/assets/icon/travel_active.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
miniprogram/assets/image/logo.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
4
miniprogram/components/background/snowflake/index.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
52
miniprogram/components/background/snowflake/index.less
Normal file
@ -0,0 +1,52 @@
|
||||
/* components/background/snow/index.wxss */
|
||||
@keyframes fall {
|
||||
from {
|
||||
transform: translateY(-10px) rotate(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(100vh) rotate(1800deg);
|
||||
}
|
||||
}
|
||||
|
||||
page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.snowflakes {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
|
||||
.snowflake {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
display: block;
|
||||
position: absolute;
|
||||
animation: fall linear infinite;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background: rgba(255, 122, 155, .8);
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 45%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 10%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: 45%;
|
||||
width: 10%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
61
miniprogram/components/background/snowflake/index.ts
Normal file
@ -0,0 +1,61 @@
|
||||
// components/background/snow/index.ts
|
||||
import Toolkit from "../../../utils/Toolkit";
|
||||
|
||||
interface Snowflake {
|
||||
x: number;
|
||||
s: number;
|
||||
speed: number;
|
||||
}
|
||||
|
||||
Component({
|
||||
|
||||
data: {
|
||||
snowflakes: [] as Snowflake[],
|
||||
timer: undefined as number | undefined,
|
||||
docWidth: 375,
|
||||
docHeight: 667,
|
||||
},
|
||||
|
||||
methods: {
|
||||
createSnowflake() {
|
||||
const snowflake = {
|
||||
x: Toolkit.random(0, this.data.docWidth),
|
||||
s: Toolkit.random(6, 20),
|
||||
speed: Toolkit.random(14, 26)
|
||||
};
|
||||
this.setData({
|
||||
snowflakes: [...this.data.snowflakes, snowflake]
|
||||
});
|
||||
}
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {
|
||||
const systemInfo = wx.getWindowInfo();
|
||||
this.setData({
|
||||
docWidth: systemInfo.windowWidth,
|
||||
docHeight: systemInfo.windowHeight,
|
||||
timer: setInterval(() => {
|
||||
this.createSnowflake();
|
||||
console.log(this.data.snowflakes);
|
||||
|
||||
if (40 < this.data.snowflakes.length) {
|
||||
if (this.data.timer) {
|
||||
clearInterval(this.data.timer);
|
||||
}
|
||||
this.setData({
|
||||
timer: undefined
|
||||
})
|
||||
}
|
||||
}, 2000)
|
||||
});
|
||||
},
|
||||
detached() {
|
||||
if (this.data.timer) {
|
||||
clearInterval(this.data.timer);
|
||||
this.setData({
|
||||
timer: undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
9
miniprogram/components/background/snowflake/index.wxml
Normal file
@ -0,0 +1,9 @@
|
||||
<!--components/background/snow/index.wxml-->
|
||||
<view class="snowflakes" style="width: {{docWidth}}px; height: {{docHeight}}px;">
|
||||
<view
|
||||
class="snowflake"
|
||||
wx:for="{{snowflakes}}"
|
||||
wx:key="index"
|
||||
style="left: {{item.x}}px; width: {{item.s}}px; height: {{item.s}}px; animation-duration: {{item.speed}}s;"
|
||||
></view>
|
||||
</view>
|
||||
4
miniprogram/components/search-list/index.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
miniprogram/components/search-list/index.less
Normal file
@ -0,0 +1 @@
|
||||
/* components/search-list/index.wxss */
|
||||
24
miniprogram/components/search-list/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// components/search-list/index.ts
|
||||
Component({
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
|
||||
}
|
||||
})
|
||||
2
miniprogram/components/search-list/index.wxml
Normal file
@ -0,0 +1,2 @@
|
||||
<!--components/search-list/index.wxml-->
|
||||
<text>components/search-list/index.wxml</text>
|
||||
17
miniprogram/config/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
const envArgs = {
|
||||
develop: {
|
||||
url: "http://localhost:8091"
|
||||
},
|
||||
trial: {
|
||||
url: "https://api.imyeyu.com"
|
||||
},
|
||||
release: {
|
||||
url: "https://api.imyeyu.com"
|
||||
}
|
||||
}
|
||||
|
||||
const env = wx.getAccountInfoSync().miniProgram.envVersion;
|
||||
|
||||
export default {
|
||||
...envArgs[env]
|
||||
}
|
||||
30
miniprogram/package-lock.json
generated
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "miniprogram",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "miniprogram",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"tdesign-miniprogram": "^1.11.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"miniprogram-api-typings": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/miniprogram-api-typings": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/miniprogram-api-typings/-/miniprogram-api-typings-4.1.0.tgz",
|
||||
"integrity": "sha512-4RBsz27nBKyRkVGoNkRaPx24/KeJBw3zaaIlXDR8s/WBh2PbcUAc+q7wLLbp7Qsmb3bLzzUu7tqAti+B06kmjg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tdesign-miniprogram": {
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/tdesign-miniprogram/-/tdesign-miniprogram-1.11.2.tgz",
|
||||
"integrity": "sha512-lXcry3vRa9jHzjpOdIxuIAh7F85kImym82VwLbCqr/TkMhycOsOepx+r1S9fum7u2nsWiYRTV+HuvDHN3KlIuA=="
|
||||
}
|
||||
}
|
||||
}
|
||||
18
miniprogram/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "miniprogram",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"tdesign-miniprogram": "^1.11.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"miniprogram-api-typings": "^4.1.0"
|
||||
}
|
||||
}
|
||||
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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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>
|
||||
265
miniprogram/tdesign.wxss
Normal file
@ -0,0 +1,265 @@
|
||||
@media (prefers-color-scheme: light) {
|
||||
page, .page {
|
||||
--td-brand-color-1: #dffbdf;
|
||||
--td-brand-color-2: #c2f5c7;
|
||||
--td-brand-color-3: #8add9a;
|
||||
--td-brand-color-4: #07c160;
|
||||
--td-brand-color-5: #0ea753;
|
||||
--td-brand-color-6: #00893f;
|
||||
--td-brand-color-7: #006d30;
|
||||
--td-brand-color-8: #005424;
|
||||
--td-brand-color-9: #003c17;
|
||||
--td-brand-color-10: #00260c;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color: var(--td-brand-color-4);
|
||||
--td-brand-color-active: var(--td-brand-color-5);
|
||||
--td-warning-color-1: #fef3e6;
|
||||
--td-warning-color-2: #f9e0c7;
|
||||
--td-warning-color-3: #f7c797;
|
||||
--td-warning-color-4: #f2995f;
|
||||
--td-warning-color-5: #ed7b2f;
|
||||
--td-warning-color-6: #d35a21;
|
||||
--td-warning-color-7: #ba431b;
|
||||
--td-warning-color-8: #9e3610;
|
||||
--td-warning-color-9: #842b0b;
|
||||
--td-warning-color-10: #5a1907;
|
||||
--td-warning-color: var(--td-warning-color-5);
|
||||
--td-warning-color-focus: var(--td-warning-color-2);
|
||||
--td-warning-color-active: var(--td-warning-color-6);
|
||||
--td-warning-color-disabled: var(--td-warning-color-3);
|
||||
--td-warning-color-light: var(--td-warning-color-1);
|
||||
--td-error-color-1: #fff0ed;
|
||||
--td-error-color-2: #ffd8d3;
|
||||
--td-error-color-3: #ffb8b0;
|
||||
--td-error-color-4: #ff9187;
|
||||
--td-error-color-5: #fd6059;
|
||||
--td-error-color-6: #e64340;
|
||||
--td-error-color-7: #b32c2b;
|
||||
--td-error-color-8: #8d1619;
|
||||
--td-error-color-9: #6b0007;
|
||||
--td-error-color-10: #490003;
|
||||
--td-error-color: var(--td-error-color-6);
|
||||
--td-error-color-focus: var(--td-error-color-2);
|
||||
--td-error-color-active: var(--td-error-color-7);
|
||||
--td-error-color-disabled: var(--td-error-color-3);
|
||||
--td-error-color-light: var(--td-error-color-1);
|
||||
--td-success-color-1: #dffbdf;
|
||||
--td-success-color-2: #c2f5c7;
|
||||
--td-success-color-3: #8add9a;
|
||||
--td-success-color-4: #07c160;
|
||||
--td-success-color-5: #0ea753;
|
||||
--td-success-color-6: #00893f;
|
||||
--td-success-color-7: #006d30;
|
||||
--td-success-color-8: #005424;
|
||||
--td-success-color-9: #003c17;
|
||||
--td-success-color-10: #00260c;
|
||||
--td-success-color: var(--td-success-color-5);
|
||||
--td-success-color-focus: var(--td-success-color-2);
|
||||
--td-success-color-active: var(--td-success-color-6);
|
||||
--td-success-color-disabled: var(--td-success-color-3);
|
||||
--td-success-color-light: var(--td-success-color-1);
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e7e7e7;
|
||||
--td-gray-color-4: #dcdcdc;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-container: #fff;
|
||||
--td-bg-color-page: var(--td-gray-color-2);
|
||||
--td-bg-color-container-active: var(--td-gray-color-3);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
|
||||
--td-bg-color-component: var(--td-gray-color-3);
|
||||
--td-bg-color-component-active: var(--td-gray-color-6);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-2);
|
||||
--td-component-stroke: var(--td-gray-color-3);
|
||||
--td-component-border: var(--td-gray-color-4);
|
||||
--td-font-white-1: #ffffff;
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-gray-1);
|
||||
--td-text-color-secondary: var(--td-font-gray-2);
|
||||
--td-text-color-placeholder: var(--td-font-gray-3);
|
||||
--td-text-color-disabled: var(--td-font-gray-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
|
||||
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 8%);
|
||||
--td-scrollbar-color: rgba(0, 0, 0, 10%);
|
||||
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
|
||||
--td-scroll-track-color: #fff;
|
||||
--td-bg-color-specialcomponent: #fff;
|
||||
--td-border-level-1-color: var(--td-gray-color-3);
|
||||
--td-border-level-2-color: var(--td-gray-color-4);
|
||||
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
|
||||
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%), 0 5px 5px -3px rgba(0, 0, 0, 10%);
|
||||
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%), 0 8px 10px -5px rgba(0, 0, 0, 8%);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
|
||||
--td-mask-active: rgba(0, 0, 0, 0.6);
|
||||
--td-mask-disabled: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
page, .page {
|
||||
--td-brand-color-1: #0ea75320;
|
||||
--td-brand-color-2: #003c17;
|
||||
--td-brand-color-3: #005424;
|
||||
--td-brand-color-4: #006d30;
|
||||
--td-brand-color-5: #00893f;
|
||||
--td-brand-color-6: #0ea753;
|
||||
--td-brand-color-7: #07c160;
|
||||
--td-brand-color-8: #8add9a;
|
||||
--td-brand-color-9: #c2f5c7;
|
||||
--td-brand-color-10: #dffbdf;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color: var(--td-brand-color-6);
|
||||
--td-brand-color-active: var(--td-brand-color-7);
|
||||
--td-warning-color-1: #4f2a1d;
|
||||
--td-warning-color-2: #582f21;
|
||||
--td-warning-color-3: #733c23;
|
||||
--td-warning-color-4: #a75d2b;
|
||||
--td-warning-color-5: #cf6e2d;
|
||||
--td-warning-color-6: #dc7633;
|
||||
--td-warning-color-7: #e8935c;
|
||||
--td-warning-color-8: #ecbf91;
|
||||
--td-warning-color-9: #eed7bf;
|
||||
--td-warning-color-10: #f3e9dc;
|
||||
--td-error-color-1: #fff0ed;
|
||||
--td-error-color-2: #ffd8d3;
|
||||
--td-error-color-3: #ffb8b0;
|
||||
--td-error-color-4: #ff9187;
|
||||
--td-error-color-5: #fd6059;
|
||||
--td-error-color-6: #e64340;
|
||||
--td-error-color-7: #b32c2b;
|
||||
--td-error-color-8: #8d1619;
|
||||
--td-error-color-9: #6b0007;
|
||||
--td-error-color-10: #490003;
|
||||
--td-success-color-1: #dffbdf;
|
||||
--td-success-color-2: #c2f5c7;
|
||||
--td-success-color-3: #8add9a;
|
||||
--td-success-color-4: #07c160;
|
||||
--td-success-color-5: #0ea753;
|
||||
--td-success-color-6: #00893f;
|
||||
--td-success-color-7: #006d30;
|
||||
--td-success-color-8: #005424;
|
||||
--td-success-color-9: #003c17;
|
||||
--td-success-color-10: #00260c;
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e7e7e7;
|
||||
--td-gray-color-4: #dcdcdc;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-page: var(--td-gray-color-14);
|
||||
--td-bg-color-container: var(--td-gray-color-13);
|
||||
--td-bg-color-container-active: var(--td-gray-color-10);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component: var(--td-gray-color-11);
|
||||
--td-bg-color-component-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-12);
|
||||
--td-component-stroke: var(--td-gray-color-11);
|
||||
--td-component-border: var(--td-gray-color-9);
|
||||
--td-font-white-1: rgba(255, 255, 255, 0.9);
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-white-1);
|
||||
--td-text-color-secondary: var(--td-font-white-2);
|
||||
--td-text-color-placeholder: var(--td-font-white-3);
|
||||
--td-text-color-disabled: var(--td-font-white-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-shadow-1: 0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
|
||||
--td-shadow-2: 0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.10), 0 5px 5px rgba(0, 0, 0, 0.16);
|
||||
--td-shadow-3: 0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.20);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 55%);
|
||||
--td-scrollbar-color: rgba(255, 255, 255, 10%);
|
||||
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
|
||||
--td-scroll-track-color: #333;
|
||||
--td-bg-color-specialcomponent: transparent;
|
||||
--td-border-level-1-color: var(--td-gray-color-11);
|
||||
--td-border-level-2-color: var(--td-gray-color-9);
|
||||
--td-mask-active: rgba(0, 0, 0, 0.4);
|
||||
--td-mask-disabled: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
page {
|
||||
--td-font-family: pingfang sc, microsoft yahei, arial regular;
|
||||
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
|
||||
--td-font-size-link-small: 12px;
|
||||
--td-font-size-link-medium: 14px;
|
||||
--td-font-size-link-large: 16px;
|
||||
--td-font-size-mark-extra-small: 10px;
|
||||
--td-font-size-mark-small: 12px;
|
||||
--td-font-size-mark-medium: 14px;
|
||||
--td-font-size-mark-large: 16px;
|
||||
--td-font-size-body-extra-small: 10px;
|
||||
--td-font-size-body-small: 12px;
|
||||
--td-font-size-body-medium: 14px;
|
||||
--td-font-size-body-large: 16px;
|
||||
--td-font-size-title-small: 14px;
|
||||
--td-font-size-title-medium: 16px;
|
||||
--td-font-size-title-large: 18px;
|
||||
--td-font-size-title-extra-large: 20px;
|
||||
--td-font-size-headline-small: 24px;
|
||||
--td-font-size-headline-medium: 28px;
|
||||
--td-font-size-headline-large: 36px;
|
||||
--td-font-size-display-medium: 48px;
|
||||
--td-font-size-display-large: 64px;
|
||||
--td-font-size: 10px;
|
||||
--td-font-size-xs: var(--td-font-size-body-extra-small);
|
||||
--td-font-size-s: var(--td-font-size-body-small);
|
||||
--td-font-size-base: var(--td-font-size-title-small);
|
||||
--td-font-size-m: var(--td-font-size-title-medium);
|
||||
--td-font-size-l: var(--td-font-size-title-large);
|
||||
--td-font-size-xl: var(--td-font-size-title-extra-large);
|
||||
--td-font-size-xxl: var(--td-font-size-headline-large);
|
||||
--td-radius-small: 2px;
|
||||
--td-radius-default: 4px;
|
||||
--td-radius-large: 6px;
|
||||
--td-radius-extraLarge: 7px;
|
||||
--td-radius-round: 999px;
|
||||
--td-radius-circle: 50%;
|
||||
}
|
||||
31
miniprogram/utils/Cooker.ts
Normal file
@ -0,0 +1,31 @@
|
||||
export default class Cooker {
|
||||
|
||||
static set(name: string, value: string, ttlMS: number) {
|
||||
let expires = "";
|
||||
if (ttlMS) {
|
||||
let date = new Date();
|
||||
date.setTime(date.getTime() + ttlMS);
|
||||
expires = "; expires=" + date.toUTCString();
|
||||
}
|
||||
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
||||
}
|
||||
|
||||
static get(name: string) {
|
||||
let nameSplit = name + "=";
|
||||
let ca = document.cookie.split(";");
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == " ") {
|
||||
c = c.substring(1, c.length);
|
||||
}
|
||||
if (c.indexOf(nameSplit) == 0) {
|
||||
return c.substring(nameSplit.length, c.length);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static remove(name: string) {
|
||||
document.cookie = name + "=; Max-Age=-99999999;";
|
||||
}
|
||||
}
|
||||
106
miniprogram/utils/Events.ts
Normal file
@ -0,0 +1,106 @@
|
||||
/**
|
||||
* ### 全局事件管理
|
||||
*
|
||||
* ```js
|
||||
* // 注册
|
||||
* Events.register("eventName", () => {
|
||||
* // 触发执行
|
||||
* });
|
||||
*
|
||||
* // 触发
|
||||
* Events.emit("eventName", '支持参数');
|
||||
*
|
||||
* // 移除
|
||||
* Events.remove("eventName");
|
||||
* ```
|
||||
*/
|
||||
export default class Events {
|
||||
|
||||
// 监听数组
|
||||
private static listeners = new Map<any, Observer<any>[]>();
|
||||
|
||||
/**
|
||||
* 注册事件(会叠加)
|
||||
*
|
||||
* @param key 事件名称
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
public static register<T>(key: T, callback: Function) {
|
||||
const observers: Observer<T>[] | undefined = Events.listeners.get(key);
|
||||
if (!observers) {
|
||||
Events.listeners.set(key, []);
|
||||
}
|
||||
Events.listeners.get(key)?.push(new Observer<T>(key, callback));
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置并注册(不会叠加)
|
||||
*
|
||||
* @param key 事件名称
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
public static reset<T>(key: T, callback: Function) {
|
||||
Events.listeners.set(key, []);
|
||||
this.register(key, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件
|
||||
*
|
||||
* @param key 事件名称
|
||||
*/
|
||||
public static remove<T>(key: T) {
|
||||
const observers: Observer<T>[] | undefined = Events.listeners.get(key);
|
||||
if (observers) {
|
||||
for (let i = 0, l = observers.length; i < l; i++) {
|
||||
if (observers[i].equals(key)) {
|
||||
observers.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Events.listeners.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发事件
|
||||
*
|
||||
* @param key 事件名称
|
||||
* @param args 参数
|
||||
*/
|
||||
public static emit<T>(key: T, ...args: any[]) {
|
||||
const observers: Observer<T>[] | undefined = Events.listeners.get(key);
|
||||
if (observers) {
|
||||
for (const observer of observers) {
|
||||
// 通知
|
||||
observer.notify(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 观察者 */
|
||||
class Observer<T> {
|
||||
|
||||
private callback: Function = () => {}; // 回调函数
|
||||
|
||||
private readonly key: T;
|
||||
|
||||
constructor(key: T, callback: Function) {
|
||||
this.key = key;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通知
|
||||
*
|
||||
* @param args 不定参数
|
||||
*/
|
||||
notify(...args: any[]): void {
|
||||
this.callback.call(this.key, ...args);
|
||||
}
|
||||
|
||||
equals(name: any): boolean {
|
||||
return name === this.key;
|
||||
}
|
||||
}
|
||||
111
miniprogram/utils/IOSize.ts
Normal file
@ -0,0 +1,111 @@
|
||||
export enum Unit {
|
||||
|
||||
/** B */
|
||||
B = "B",
|
||||
|
||||
/** KB */
|
||||
KB = "KB",
|
||||
|
||||
/** MB */
|
||||
MB = "MB",
|
||||
|
||||
/** GB */
|
||||
GB = "GB",
|
||||
|
||||
/** TB */
|
||||
TB = "TB",
|
||||
|
||||
/** PB */
|
||||
PB = "PB",
|
||||
|
||||
/** EB */
|
||||
EB = "EB"
|
||||
}
|
||||
|
||||
/** 储存单位 */
|
||||
export default class IOSize {
|
||||
|
||||
/** 1 字节 */
|
||||
public static BYTE = 1;
|
||||
|
||||
/** 1 KB */
|
||||
public static KB = IOSize.BYTE << 10;
|
||||
|
||||
/** 1 MB */
|
||||
public static MB = IOSize.KB << 10;
|
||||
|
||||
/** 1 GB */
|
||||
public static GB = IOSize.MB << 10;
|
||||
|
||||
/** 1 TB */
|
||||
public static TB = IOSize.GB << 10;
|
||||
|
||||
/** 1 PB */
|
||||
public static PB = IOSize.TB << 10;
|
||||
|
||||
/** 1 EB */
|
||||
public static EB = IOSize.PB << 10;
|
||||
|
||||
public static Unit = Unit;
|
||||
|
||||
/**
|
||||
* <p>格式化一个储存容量,保留两位小数
|
||||
* <pre>
|
||||
* // 返回 100.01 KB
|
||||
* format(102411, 2);
|
||||
* </pre>
|
||||
*
|
||||
* @param size 字节大小
|
||||
* @param fixed 保留小数
|
||||
* @param stopUnit 停止单位
|
||||
* @return
|
||||
*/
|
||||
public static format(size: number, fixed = 2, stopUnit?: Unit): string {
|
||||
const units = Object.keys(Unit);
|
||||
if (0 < size) {
|
||||
for (let i = 0; i < units.length; i++, size /= 1024) {
|
||||
const unit = units[i];
|
||||
if (size <= 1000 || i === units.length - 1 || unit === stopUnit) {
|
||||
if (i === 0) {
|
||||
// 最小单位不需要小数
|
||||
return size + " B";
|
||||
} else {
|
||||
return `${size.toFixed(fixed)} ${unit}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "0 B";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <p>格式化一个储存容量,保留两位小数,不带单位
|
||||
* <pre>
|
||||
* // 返回 100.01
|
||||
* format(102411, 2);
|
||||
* </pre>
|
||||
*
|
||||
* @param size 字节大小
|
||||
* @param fixed 保留小数
|
||||
* @param stopUnit 停止单位
|
||||
* @return
|
||||
*/
|
||||
public static formatWithoutUnit(size: number, fixed = 2, stopUnit?: Unit): string {
|
||||
const units = Object.keys(Unit);
|
||||
if (0 < size) {
|
||||
for (let i = 0; i < units.length; i++, size /= 1024) {
|
||||
const unit = units[i];
|
||||
if (size <= 1000 || i === units.length - 1 || unit === stopUnit) {
|
||||
if (i === 0) {
|
||||
// 最小单位不需要小数
|
||||
return size.toFixed(0);
|
||||
} else {
|
||||
return size.toFixed(fixed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "0";
|
||||
}
|
||||
}
|
||||
129
miniprogram/utils/Storage.ts
Normal file
@ -0,0 +1,129 @@
|
||||
export default class Storage {
|
||||
|
||||
/**
|
||||
* 获取为布尔值
|
||||
*
|
||||
* @param key 键
|
||||
* @returns 布尔值
|
||||
*/
|
||||
public static is(key: string): boolean {
|
||||
return this.getString(key) === "true";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取为布尔值并取反
|
||||
*
|
||||
* @param key 键
|
||||
* @returns 布尔值
|
||||
*/
|
||||
public static not(key: string): boolean {
|
||||
return !this.is(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取为指定对象
|
||||
*
|
||||
* @template T 对象类型
|
||||
* @param key 键
|
||||
* @returns {T | undefined} 返回对象
|
||||
*/
|
||||
public static getObject<T>(key: string): T {
|
||||
if (this.has(key)) {
|
||||
return this.getJSON(key) as T;
|
||||
}
|
||||
throw Error(`not found ${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取值,如果没有则储存并使用默认值
|
||||
*
|
||||
* @template T 默认值类型
|
||||
* @param key 键
|
||||
* @param def 默认值
|
||||
* @return {T} 对象
|
||||
*/
|
||||
public static getDefault<T>(key: string, def: T): T {
|
||||
if (this.has(key)) {
|
||||
return this.getJSON(key) as T;
|
||||
}
|
||||
this.setObject(key, def);
|
||||
return def;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取为 JSON
|
||||
*
|
||||
* @param key 键
|
||||
* @returns JSON 对象
|
||||
*/
|
||||
public static getJSON(key: string) {
|
||||
return JSON.parse(this.getString(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取为字符串(其他获取方式一般经过这个方法,找不到配置或配置值无效时会抛错)
|
||||
*
|
||||
* @param key 键
|
||||
* @returns 字符串
|
||||
*/
|
||||
public static getString(key: string): string {
|
||||
const value = localStorage.getItem(key);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
throw new Error(`not found: ${key}, ${value}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否存在某配置
|
||||
*
|
||||
* @param key 键
|
||||
* @returns true 为存在
|
||||
*/
|
||||
public static has(key: string): boolean {
|
||||
return localStorage.getItem(key) !== undefined && localStorage.getItem(key) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置值
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
*/
|
||||
public static setObject(key: string, value: any) {
|
||||
if (value instanceof Object || value instanceof Array) {
|
||||
this.setJSON(key, value);
|
||||
} else {
|
||||
this.setString(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 JSON 值
|
||||
*
|
||||
* @param key 键
|
||||
* @param json JSON 字符串
|
||||
*/
|
||||
public static setJSON(key: string, json: any) {
|
||||
localStorage.setItem(key, JSON.stringify(json));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置值
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
*/
|
||||
public static setString(key: string, value: any) {
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除属性
|
||||
*
|
||||
* @param key 键
|
||||
*/
|
||||
public static remove(key: string) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
97
miniprogram/utils/Time.ts
Normal file
@ -0,0 +1,97 @@
|
||||
export default class Time {
|
||||
|
||||
/** 1 秒时间戳 */
|
||||
public static S = 1E3;
|
||||
/** 1 分钟时间戳 */
|
||||
public static M = Time.S * 60;
|
||||
/** 1 小时时间戳 */
|
||||
public static H = Time.M * 60;
|
||||
/** 1 天时间戳 */
|
||||
public static D = Time.H * 24;
|
||||
|
||||
public static now(): number {
|
||||
return new Date().getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unix 时间戳转日期
|
||||
*
|
||||
* @param unix 时间戳
|
||||
*/
|
||||
public static toDate(unix?: number): string {
|
||||
if (!unix) return "";
|
||||
const d = new Date(unix);
|
||||
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d.getDate().toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unix 时间戳转时间
|
||||
*
|
||||
* @param unix 时间戳
|
||||
*/
|
||||
public static toTime(unix?: number): string {
|
||||
if (!unix) return "";
|
||||
const d = new Date(unix);
|
||||
return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unix 时间戳转日期和时间
|
||||
*
|
||||
* @param unix 时间戳
|
||||
*/
|
||||
public static toDateTime(unix?: number): string {
|
||||
if (!unix) return "";
|
||||
return `${this.toDate(unix)} ${this.toTime(unix)}`;
|
||||
}
|
||||
|
||||
public static toPassedDate(unix?: number): string {
|
||||
return this.toPassedDateTime(unix, false);
|
||||
}
|
||||
|
||||
public static toPassedDateTime(unix?: number, withDetailTime = true): string {
|
||||
if (!unix) {
|
||||
return "";
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
const between = now - unix;
|
||||
|
||||
if (Time.D * 4 <= between) {
|
||||
return withDetailTime ? this.toDateTime(unix) : this.toDate(unix);
|
||||
} else if (Time.D < between) {
|
||||
return `${Math.floor(between / Time.D)} 天前`;
|
||||
} else if (Time.H < between) {
|
||||
return `${Math.floor(between / Time.H)} 小时前`;
|
||||
} else if (Time.M < between) {
|
||||
return `${Math.floor(between / Time.M)} 分钟前`;
|
||||
} else {
|
||||
return "刚刚";
|
||||
}
|
||||
}
|
||||
|
||||
public static between(begin: Date, end?: Date) : any {
|
||||
if (!end) {
|
||||
end = new Date();
|
||||
}
|
||||
const cs = 1000, cm = 6E4, ch = 36E5, cd = 864E5, cy = 31536E6;
|
||||
const l = end.getTime() - begin.getTime();
|
||||
const y = Math.floor(l / cy),
|
||||
d = Math.floor((l / cd) - y * 365),
|
||||
h = Math.floor((l - (y * 365 + d) * cd) / ch),
|
||||
m = Math.floor((l - (y * 365 + d) * cd - h * ch) / cm),
|
||||
s = Math.floor((l - (y * 365 + d) * cd - h * ch - m * cm) / cs),
|
||||
ms = Math.floor(((l - (y * 365 + d) * cd - h * ch - m * cm) / cs - s) * cs);
|
||||
return { l, y, d, h, m, s, ms };
|
||||
}
|
||||
|
||||
public static toMediaTime(seconds: number): string {
|
||||
seconds = Math.floor(seconds);
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const second = seconds % 60;
|
||||
if (0 < hours) {
|
||||
return `${hours}:${minutes.toString().padStart(2, "0")}:${second.toString().padStart(2, "0")}`;
|
||||
}
|
||||
return `${minutes.toString().padStart(2, "0")}:${second.toString().padStart(2, "0")}`;
|
||||
}
|
||||
}
|
||||
229
miniprogram/utils/Toolkit.ts
Normal file
@ -0,0 +1,229 @@
|
||||
export default class Toolkit {
|
||||
|
||||
public static isFunction = (val: any) => typeof val === "function";
|
||||
public static isArray = Array.isArray;
|
||||
public static isString = (val: any) => typeof val === "string";
|
||||
public static isSymbol = (val: any) => typeof val === "symbol";
|
||||
public static isObject = (val: any) => val !== null && typeof val === "object";
|
||||
|
||||
public static guid() {
|
||||
const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
|
||||
return `${s4() + s4()}-${s4()}-${s4()}-${s4()}-${s4() + s4() + s4()}`;
|
||||
}
|
||||
|
||||
public static className(...args: any[]) {
|
||||
const classes = [];
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const value = args[i];
|
||||
if (!value) continue;
|
||||
if (this.isString(value)) {
|
||||
classes.push(value);
|
||||
} else if (this.isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const inner: any = this.className(value[i]);
|
||||
if (inner) {
|
||||
classes.push(inner);
|
||||
}
|
||||
}
|
||||
} else if (this.isObject(value)) {
|
||||
for (const name in value) {
|
||||
if (value[name]) {
|
||||
classes.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
public static isEmpty(obj: any): boolean {
|
||||
if (this.isString(obj)) {
|
||||
return (obj as string).trim().length === 0;
|
||||
}
|
||||
if (this.isArray(obj)) {
|
||||
return (obj as []).length === 0;
|
||||
}
|
||||
if (this.isFunction(obj)) {
|
||||
return this.isEmpty(obj());
|
||||
}
|
||||
if (this.isObject(obj)) {
|
||||
return obj === undefined || obj === null;
|
||||
}
|
||||
return obj === undefined || obj === null;
|
||||
}
|
||||
|
||||
public static isNotEmpty(obj: any): boolean {
|
||||
return !this.isEmpty(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* ### 延时执行
|
||||
*
|
||||
* ```js
|
||||
* await sleep(1E3)
|
||||
* ```
|
||||
*
|
||||
* @param ms 延时毫秒
|
||||
*/
|
||||
public static async sleep(ms: number): Promise<unknown> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* 转为数字
|
||||
*
|
||||
* @param string 字符串
|
||||
* @param fallback 如果失败,返回该值
|
||||
* @returns 转换后或转换失败数据
|
||||
*/
|
||||
public static toNumber(string: string, fallback?: number): number {
|
||||
if (!string) return fallback as number;
|
||||
const number = Number(string);
|
||||
return isFinite(number) ? number : fallback as number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步执行
|
||||
*
|
||||
* @param event 函数
|
||||
*/
|
||||
public static async(event: (...args: any[]) => any) {
|
||||
setTimeout(event, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机数
|
||||
*
|
||||
* @param min 最小值
|
||||
* @param max 最大值
|
||||
*/
|
||||
public static random(min = 0, max = 100): number {
|
||||
return Math.floor(Math.random() * (max + 1 - min)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 深克隆对象
|
||||
*
|
||||
* @param origin 源对象
|
||||
* @param target 递归对象
|
||||
* @returns 克隆对象
|
||||
*/
|
||||
public static deepClone(origin: any, target = {} as any) {
|
||||
const toString = Object.prototype.toString;
|
||||
const arrType = "[object Array]";
|
||||
for (const key in origin) {
|
||||
if (Object.prototype.hasOwnProperty.call(origin, key)) {
|
||||
if (typeof origin[key] === "object" && origin[key] !== null) {
|
||||
target[key] = toString.call(origin[key]) === arrType ? [] : {};
|
||||
this.deepClone(origin[key], target[key]);
|
||||
} else {
|
||||
target[key] = origin[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
public static keyValueString(obj: object, assign: string, split: string): string {
|
||||
let result = "";
|
||||
Object.entries(obj).forEach(([k, v]) => result += k + assign + v + split);
|
||||
return result.substring(0, result.length - split.length);
|
||||
}
|
||||
|
||||
public static toURLArgs(obj?: { [key: string]: any }): string {
|
||||
if (!obj) {
|
||||
return "";
|
||||
}
|
||||
const args: string[] = [];
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
const value = obj[key];
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => {
|
||||
args.push(`${encodeURIComponent(key)}=${encodeURIComponent(item)}`);
|
||||
});
|
||||
} else {
|
||||
args.push(`${encodeURIComponent(key)}=${encodeURIComponent(value.toString())}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return args.join("&");
|
||||
}
|
||||
|
||||
public static toObject(map: Map<any, any>): object {
|
||||
return Array.from(map.entries()).reduce((acc, [key, value]) => {
|
||||
acc[key] = value ?? null;
|
||||
return acc;
|
||||
}, {} as { [key: string]: string | undefined });
|
||||
}
|
||||
|
||||
public static uuid(): string {
|
||||
const char = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
const length = [8, 4, 4, 4, 12];
|
||||
let result = "";
|
||||
for (let i = 0; i < length.length; i++) {
|
||||
for (let j = 0; j < length[i]; j++) {
|
||||
result += char[this.random(0, char.length - 1)];
|
||||
}
|
||||
result += "-";
|
||||
}
|
||||
return result.substring(0, result.length - 1);
|
||||
}
|
||||
|
||||
public static keyFromValue(e: any, value: any): string {
|
||||
return Object.keys(e)[Object.values(e).indexOf(value)];
|
||||
}
|
||||
|
||||
// 防抖
|
||||
// eslint-disable-next-line
|
||||
public static debounce<T extends (...args: any[]) => any>(callback: T, defaultImmediate = true, delay = 600): T & {
|
||||
cancel(): void
|
||||
} {
|
||||
let timerId: ReturnType<typeof setTimeout> | null = null; // 存储定时器
|
||||
let immediate = defaultImmediate;
|
||||
// 定义一个 cancel 办法,用于勾销防抖
|
||||
const cancel = (): void => {
|
||||
if (timerId) {
|
||||
clearTimeout(timerId);
|
||||
timerId = null;
|
||||
}
|
||||
};
|
||||
|
||||
const debounced = function (this: ThisParameterType<T>, ...args: Parameters<T>): void {
|
||||
const context = this;
|
||||
if (timerId) {
|
||||
cancel();
|
||||
}
|
||||
if (immediate) {
|
||||
callback.apply(context, args);
|
||||
immediate = false;
|
||||
timerId = setTimeout(() => {
|
||||
immediate = defaultImmediate;
|
||||
}, delay);
|
||||
} else {
|
||||
// 设置定时器,在延迟时间后执行指标函数
|
||||
timerId = setTimeout(() => {
|
||||
callback.apply(context, args);
|
||||
immediate = defaultImmediate;
|
||||
}, delay);
|
||||
}
|
||||
};
|
||||
// 将 cancel 方法附加到 debounced 函数上
|
||||
(debounced as any).cancel = cancel;
|
||||
return debounced as T & { cancel(): void };
|
||||
}
|
||||
|
||||
public static format(template: string, variables: { [key: string]: any }): string {
|
||||
return template.replace(/\$\{(\w+)}/g, (_, key) => variables[key]);
|
||||
}
|
||||
|
||||
public static symmetricDiff(arr1: any[], arr2: any[]) {
|
||||
const set1 = new Set(arr1);
|
||||
const set2 = new Set(arr2);
|
||||
|
||||
return [
|
||||
...arr1.filter(item => !set2.has(item)),
|
||||
...arr2.filter(item => !set1.has(item))
|
||||
];
|
||||
}
|
||||
}
|
||||
51
project.config.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"description": "项目配置文件",
|
||||
"miniprogramRoot": "miniprogram/",
|
||||
"compileType": "miniprogram",
|
||||
"setting": {
|
||||
"useCompilerPlugins": [
|
||||
"typescript",
|
||||
"less"
|
||||
],
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
},
|
||||
"coverView": false,
|
||||
"postcss": false,
|
||||
"minified": true,
|
||||
"enhance": true,
|
||||
"showShadowRootInWxmlPanel": false,
|
||||
"packNpmRelationList": [],
|
||||
"ignoreUploadUnusedFiles": true,
|
||||
"compileHotReLoad": false,
|
||||
"skylineRenderEnable": true,
|
||||
"es6": true,
|
||||
"compileWorklet": true,
|
||||
"uglifyFileName": false,
|
||||
"uploadWithSourceMap": true,
|
||||
"packNpmManually": false,
|
||||
"minifyWXSS": true,
|
||||
"minifyWXML": true,
|
||||
"localPlugins": false,
|
||||
"condition": true,
|
||||
"swc": false,
|
||||
"disableSWC": true,
|
||||
"disableUseStrict": false
|
||||
},
|
||||
"simulatorType": "wechat",
|
||||
"simulatorPluginLibVersion": {},
|
||||
"condition": {},
|
||||
"srcMiniprogramRoot": "miniprogram/",
|
||||
"editorSetting": {
|
||||
"tabIndent": "tab",
|
||||
"tabSize": 4
|
||||
},
|
||||
"libVersion": "3.10.1",
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"appid": "wx54620c1bac03ccc6"
|
||||
}
|
||||
41
tsconfig.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"module": "CommonJS",
|
||||
"target": "ES2020",
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitReturns": true,
|
||||
"alwaysStrict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"strict": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"lib": [
|
||||
"ES2020"
|
||||
],
|
||||
"typeRoots": [
|
||||
"./typings"
|
||||
],
|
||||
"types": [
|
||||
"miniprogram-api-typings"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts",
|
||||
"miniprogram/pages/main/journal/components/journal-list/index.js"
|
||||
],
|
||||
"paths": {
|
||||
"@vant/weapp/*": [
|
||||
"miniprogram/node_modules/@vant/weapp/dist/*"
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||