├── miniprogram
├── pages
│ ├── test
│ │ ├── test.scss
│ │ ├── test.wxml
│ │ ├── test.json
│ │ └── test.ts
│ ├── album
│ │ ├── edit
│ │ │ ├── edit.scss
│ │ │ ├── edit.json
│ │ │ ├── edit.wxml
│ │ │ └── edit.ts
│ │ ├── list
│ │ │ ├── list.json
│ │ │ ├── list.scss
│ │ │ ├── list.wxml
│ │ │ └── list.ts
│ │ └── detail
│ │ │ ├── detail.json
│ │ │ ├── detail.scss
│ │ │ ├── detail.wxml
│ │ │ └── detail.ts
│ ├── great-day
│ │ ├── edit
│ │ │ ├── edit.json
│ │ │ ├── edit.scss
│ │ │ ├── edit.wxml
│ │ │ └── edit.ts
│ │ ├── detail
│ │ │ ├── detail.json
│ │ │ ├── detail.wxml
│ │ │ ├── detail.scss
│ │ │ └── detail.ts
│ │ └── list
│ │ │ ├── list.json
│ │ │ ├── list.ts
│ │ │ ├── list.wxml
│ │ │ └── list.scss
│ ├── weather
│ │ ├── daily
│ │ │ ├── daily.json
│ │ │ ├── daily.ts
│ │ │ └── daily.scss
│ │ ├── rain
│ │ │ ├── rain.json
│ │ │ ├── rain.wxml
│ │ │ ├── rain.ts
│ │ │ └── rain.scss
│ │ ├── warning
│ │ │ ├── warning.json
│ │ │ ├── warning.ts
│ │ │ ├── warning.wxml
│ │ │ └── warning.scss
│ │ ├── living
│ │ │ ├── living.json
│ │ │ ├── living.wxml
│ │ │ ├── living.ts
│ │ │ └── living.scss
│ │ └── place
│ │ │ ├── place.json
│ │ │ ├── place.scss
│ │ │ ├── place.wxml
│ │ │ └── place.ts
│ ├── system
│ │ └── error
│ │ │ ├── error.scss
│ │ │ ├── error.json
│ │ │ ├── error.wxml
│ │ │ └── error.ts
│ ├── user
│ │ ├── avatar
│ │ │ ├── avatar.json
│ │ │ ├── avatar.scss
│ │ │ ├── avatar.wxml
│ │ │ └── avatar.ts
│ │ ├── nick-name
│ │ │ ├── nick-name.json
│ │ │ ├── nick-name.scss
│ │ │ ├── nick-name.wxml
│ │ │ └── nick-name.ts
│ │ ├── account-id
│ │ │ ├── account-id.json
│ │ │ ├── account-id.wxml
│ │ │ ├── account-id.scss
│ │ │ └── account-id.ts
│ │ ├── register-time
│ │ │ ├── register-time.json
│ │ │ ├── register-time.wxml
│ │ │ ├── register-time.ts
│ │ │ └── register-time.scss
│ │ └── user-info
│ │ │ ├── user-info.scss
│ │ │ ├── user-info.json
│ │ │ ├── user-info.wxml
│ │ │ └── user-info.ts
│ ├── scan
│ │ ├── login.json
│ │ ├── login.scss
│ │ ├── login.wxml
│ │ └── login.ts
│ ├── about
│ │ └── main
│ │ │ ├── main.json
│ │ │ ├── main.scss
│ │ │ ├── main.wxml
│ │ │ └── main.ts
│ ├── ai
│ │ └── text
│ │ │ ├── text.json
│ │ │ ├── text.wxml
│ │ │ ├── text.scss
│ │ │ └── text.ts
│ ├── me
│ │ ├── me.json
│ │ ├── me.scss
│ │ ├── me.wxml
│ │ └── me.ts
│ └── index
│ │ └── index.json
├── app
│ ├── core
│ │ ├── version.ts
│ │ ├── logger.ts
│ │ ├── http.ts
│ │ ├── system.ts
│ │ ├── constant.ts
│ │ ├── config.ts
│ │ ├── auth.ts
│ │ └── storage.ts
│ ├── services
│ │ ├── ip.interface.ts
│ │ ├── report.interface.ts
│ │ ├── ip.ts
│ │ ├── system.ts
│ │ ├── scan-login.ts
│ │ ├── ai.ts
│ │ ├── system.interface.ts
│ │ ├── authorization.ts
│ │ ├── oss.ts
│ │ ├── weather-canvas.ts
│ │ ├── weather-place.ts
│ │ ├── great-day.ts
│ │ ├── userinfo.ts
│ │ └── album.ts
│ ├── utils
│ │ ├── wx.ts
│ │ ├── types.ts
│ │ ├── params-utils.ts
│ │ ├── url-utils.ts
│ │ ├── time.ts
│ │ ├── canvas.ts
│ │ └── wx-typings.ts
│ └── http
│ │ ├── invalid-token-interceptor.ts
│ │ ├── types.ts
│ │ ├── request-log-interceptor.ts
│ │ ├── auth-interceptor.ts
│ │ ├── security-token-interceptor.ts
│ │ ├── request.ts
│ │ ├── loading-interceptor.ts
│ │ ├── miniprogram-adatper.ts
│ │ └── aliyun-apigw-signature-interceptor.ts
├── components
│ ├── top-space
│ │ ├── top-space.json
│ │ ├── top-space.scss
│ │ ├── top-space.wxml
│ │ └── top-space.ts
│ └── authorize-element
│ │ ├── authorize-element.scss
│ │ ├── authorize-element.json
│ │ ├── authorize-element.wxml
│ │ └── authorize-element.ts
├── assets
│ ├── images
│ │ ├── common
│ │ │ └── logo.jpg
│ │ └── tabbar
│ │ │ ├── me-filled.png
│ │ │ ├── album-filled.png
│ │ │ ├── home-filled.png
│ │ │ ├── robot-filled.png
│ │ │ ├── calendar-filled.png
│ │ │ ├── home-outline-dark.png
│ │ │ ├── me-outline-dark.png
│ │ │ ├── me-outline-light.png
│ │ │ ├── album-outline-dark.png
│ │ │ ├── album-outline-light.png
│ │ │ ├── home-outline-light.png
│ │ │ ├── robot-outline-dark.png
│ │ │ ├── robot-outline-light.png
│ │ │ ├── calendar-outline-dark.png
│ │ │ └── calendar-outline-light.png
│ └── styles
│ │ ├── weather.scss
│ │ └── iconfont
│ │ └── iconfont.wxss
├── sitemap.json
├── behaviors
│ ├── mixed-bahavior.ts
│ ├── share-app-behavior.ts
│ ├── theme-behavior.ts
│ └── page-lifetimes-behavior.ts
├── app.ts
├── app.scss
├── theme.json
└── app.json
├── .vscode
└── settings.json
├── CHANGELOG.md
├── .gitignore
├── .editorconfig
├── .eslintrc.json
├── docs
└── 使用 iconfont 流程.md
├── .prettierrc
├── tsconfig.json
├── LICENSE
├── package.json
├── project.private.config.json
├── project.config.json
├── README.md
└── cssconfig.json
/miniprogram/pages/test/test.scss:
--------------------------------------------------------------------------------
1 | /* pages/test/test.scss */
2 |
--------------------------------------------------------------------------------
/miniprogram/app/core/version.ts:
--------------------------------------------------------------------------------
1 | export const version = '2.1.0'
2 |
--------------------------------------------------------------------------------
/miniprogram/pages/album/edit/edit.scss:
--------------------------------------------------------------------------------
1 | /* pages/album/edit/edit.wxss */
2 |
--------------------------------------------------------------------------------
/miniprogram/components/top-space/top-space.json:
--------------------------------------------------------------------------------
1 | {
2 | "component": true
3 | }
4 |
--------------------------------------------------------------------------------
/miniprogram/pages/test/test.wxml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/miniprogram/pages/test/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "测试页",
3 | "enablePullDownRefresh": false
4 | }
5 |
--------------------------------------------------------------------------------
/miniprogram/components/authorize-element/authorize-element.scss:
--------------------------------------------------------------------------------
1 | /* components/authorize-element/authorize-element.scss */
2 |
--------------------------------------------------------------------------------
/miniprogram/pages/great-day/edit/edit.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "",
3 | "enablePullDownRefresh": false
4 | }
5 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/daily/daily.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "",
3 | "enablePullDownRefresh": false
4 | }
5 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/rain/rain.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "",
3 | "enablePullDownRefresh": false
4 | }
5 |
--------------------------------------------------------------------------------
/miniprogram/pages/great-day/detail/detail.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "",
3 | "enablePullDownRefresh": false
4 | }
5 |
--------------------------------------------------------------------------------
/miniprogram/pages/system/error/error.scss:
--------------------------------------------------------------------------------
1 | /* pages/system/error/error.wxss */
2 |
3 | .handle {
4 | padding-top: 300rpx;
5 | }
6 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/avatar/avatar.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "我的头像",
3 | "enablePullDownRefresh": false
4 | }
5 |
--------------------------------------------------------------------------------
/miniprogram/components/top-space/top-space.scss:
--------------------------------------------------------------------------------
1 | /* components/top-space/top-space.scss */
2 | .top-space {
3 | opacity: 100%;
4 | }
5 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/nick-name/nick-name.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "设置昵称",
3 | "enablePullDownRefresh": false
4 | }
5 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/warning/warning.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "天气预警",
3 | "enablePullDownRefresh": false
4 | }
5 |
--------------------------------------------------------------------------------
/miniprogram/app/core/logger.ts:
--------------------------------------------------------------------------------
1 | import {Logger} from 'miniprogram-logger-plus'
2 |
3 | export const logger = new Logger({level: 'DEBUG'})
4 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/account-id/account-id.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "我的账户 ID",
3 | "enablePullDownRefresh": false
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "html.validate.styles": false,
3 | "csscomb.formatOnSave": true,
4 | "csscomb.preset": "cssconfig.json"
5 | }
6 |
--------------------------------------------------------------------------------
/miniprogram/assets/images/common/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inlym/life-helper-miniprogram/HEAD/miniprogram/assets/images/common/logo.jpg
--------------------------------------------------------------------------------
/miniprogram/pages/user/register-time/register-time.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "我与小鸣助手",
3 | "enablePullDownRefresh": false
4 | }
5 |
--------------------------------------------------------------------------------
/miniprogram/assets/images/tabbar/me-filled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inlym/life-helper-miniprogram/HEAD/miniprogram/assets/images/tabbar/me-filled.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/tabbar/album-filled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inlym/life-helper-miniprogram/HEAD/miniprogram/assets/images/tabbar/album-filled.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/tabbar/home-filled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inlym/life-helper-miniprogram/HEAD/miniprogram/assets/images/tabbar/home-filled.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/tabbar/robot-filled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inlym/life-helper-miniprogram/HEAD/miniprogram/assets/images/tabbar/robot-filled.png
--------------------------------------------------------------------------------
/miniprogram/pages/weather/living/living.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {},
3 | "navigationBarTitleText": "生活指数",
4 | "enablePullDownRefresh": false
5 | }
6 |
--------------------------------------------------------------------------------
/miniprogram/app/services/ip.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IpInfo {
2 | /** IP 地址 */
3 | ip: string
4 |
5 | /** 所在地区,格式示例:“浙江省杭州市” */
6 | region: string
7 | }
8 |
--------------------------------------------------------------------------------
/miniprogram/assets/images/tabbar/calendar-filled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inlym/life-helper-miniprogram/HEAD/miniprogram/assets/images/tabbar/calendar-filled.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/tabbar/home-outline-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inlym/life-helper-miniprogram/HEAD/miniprogram/assets/images/tabbar/home-outline-dark.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/tabbar/me-outline-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inlym/life-helper-miniprogram/HEAD/miniprogram/assets/images/tabbar/me-outline-dark.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/tabbar/me-outline-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inlym/life-helper-miniprogram/HEAD/miniprogram/assets/images/tabbar/me-outline-light.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/tabbar/album-outline-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inlym/life-helper-miniprogram/HEAD/miniprogram/assets/images/tabbar/album-outline-dark.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/tabbar/album-outline-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inlym/life-helper-miniprogram/HEAD/miniprogram/assets/images/tabbar/album-outline-light.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/tabbar/home-outline-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inlym/life-helper-miniprogram/HEAD/miniprogram/assets/images/tabbar/home-outline-light.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/tabbar/robot-outline-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inlym/life-helper-miniprogram/HEAD/miniprogram/assets/images/tabbar/robot-outline-dark.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/tabbar/robot-outline-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inlym/life-helper-miniprogram/HEAD/miniprogram/assets/images/tabbar/robot-outline-light.png
--------------------------------------------------------------------------------
/miniprogram/components/top-space/top-space.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/miniprogram/assets/images/tabbar/calendar-outline-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inlym/life-helper-miniprogram/HEAD/miniprogram/assets/images/tabbar/calendar-outline-dark.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/tabbar/calendar-outline-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inlym/life-helper-miniprogram/HEAD/miniprogram/assets/images/tabbar/calendar-outline-light.png
--------------------------------------------------------------------------------
/miniprogram/components/authorize-element/authorize-element.json:
--------------------------------------------------------------------------------
1 | {
2 | "component": true,
3 | "usingComponents": {
4 | "mp-dialog": "weui-miniprogram/dialog/dialog"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 更新日志
2 |
3 | ## 1.0.2 / 2022-04-11
4 |
5 | - 引入暗黑模式
6 |
7 | ## 1.0.1 / 2022-04-10
8 |
9 | - fix: 修复天气查询时数据混淆的问题
10 |
11 | ## 1.0.0 / 2022-04-08
12 |
13 | - 重构版本发布。
14 |
--------------------------------------------------------------------------------
/miniprogram/pages/scan/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {
3 | "mp-msg": "weui-miniprogram/msg/msg"
4 | },
5 | "navigationBarTitleText": "登录确认",
6 | "enablePullDownRefresh": false
7 | }
8 |
--------------------------------------------------------------------------------
/miniprogram/pages/about/main/main.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "关于小鸣助手",
3 | "enablePullDownRefresh": false,
4 | "usingComponents": {
5 | "mp-msg": "weui-miniprogram/msg/msg"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/miniprogram/pages/great-day/list/list.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "纪念日",
3 | "enablePullDownRefresh": false,
4 | "usingComponents": {
5 | "mp-msg": "weui-miniprogram/msg/msg"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/miniprogram/pages/system/error/error.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {
3 | "mp-msg": "weui-miniprogram/msg/msg"
4 | },
5 | "navigationBarTitleText": "啊,出问题了",
6 | "enablePullDownRefresh": false
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### 依赖文件 ###
2 | node_modules
3 | miniprogram_npm
4 | package-lock.json
5 |
6 | ### 构建后的 Javascript 文件 ###
7 | *.js
8 |
9 | ### IntelliJ IDEA ###
10 | .idea
11 | *.iws
12 | *.iml
13 | *.ipr
14 |
--------------------------------------------------------------------------------
/miniprogram/pages/ai/text/text.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "智能会话",
3 | "enablePullDownRefresh": false,
4 | "usingComponents": {
5 | "mp-loading": "weui-miniprogram/loading/loading"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/miniprogram/pages/about/main/main.scss:
--------------------------------------------------------------------------------
1 | /* pages/about/main/main.wxss */
2 |
3 | .ext {
4 | .weui-msg__icon-img {
5 | width: 140rpx;
6 | height: 140rpx;
7 |
8 | border-radius: 20rpx;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/miniprogram/pages/album/list/list.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {
3 | "mp-actionSheet": "weui-miniprogram/actionsheet/actionsheet"
4 | },
5 | "navigationBarTitleText": "我的相册",
6 | "enablePullDownRefresh": false
7 | }
8 |
--------------------------------------------------------------------------------
/miniprogram/app/services/report.interface.ts:
--------------------------------------------------------------------------------
1 | import {MiniProgramInfo} from './system.interface'
2 |
3 | /** 小程序启动时需要上报的信息 */
4 | export interface LaunchInfo extends MiniProgramInfo {
5 | /** 上报时间(时间戳) */
6 | reportTime: number
7 | }
8 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/user-info/user-info.scss:
--------------------------------------------------------------------------------
1 | /* pages/user/user-info/user-info.wxss */
2 | .page-top {
3 | height: 1rpx;
4 | }
5 |
6 | .avatar {
7 | width: 100rpx;
8 | height: 100rpx;
9 |
10 | border-radius: 8rpx;
11 | }
12 |
--------------------------------------------------------------------------------
/miniprogram/sitemap.json:
--------------------------------------------------------------------------------
1 | {
2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
3 | "rules": [
4 | {
5 | "action": "allow",
6 | "page": "*"
7 | }
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/miniprogram/pages/about/main/main.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/avatar/avatar.scss:
--------------------------------------------------------------------------------
1 | /* pages/user/avatar/avatar.wxss */
2 |
3 | .space {
4 | height: 200rpx;
5 | }
6 |
7 | .avatar {
8 | width: 750rpx;
9 | height: 750rpx;
10 | }
11 |
12 | .button {
13 | margin-top: 100rpx;
14 | }
15 |
--------------------------------------------------------------------------------
/miniprogram/behaviors/mixed-bahavior.ts:
--------------------------------------------------------------------------------
1 | import {pageLifetimesBehavior} from './page-lifetimes-behavior'
2 | import {themeBehavior} from './theme-behavior'
3 |
4 | export const mixedBehavior = Behavior({
5 | behaviors: [pageLifetimesBehavior, themeBehavior],
6 | })
7 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/user-info/user-info.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {
3 | "mp-cells": "weui-miniprogram/cells/cells",
4 | "mp-cell": "weui-miniprogram/cell/cell"
5 | },
6 | "navigationBarTitleText": "个人信息",
7 | "enablePullDownRefresh": false
8 | }
9 |
--------------------------------------------------------------------------------
/miniprogram/app/services/ip.ts:
--------------------------------------------------------------------------------
1 | import {requestForData} from '../core/http'
2 | import {IpInfo} from './ip.interface'
3 |
4 | export function getIpInfo(): Promise {
5 | return requestForData({
6 | method: 'GET',
7 | url: '/ip',
8 | auth: false,
9 | })
10 | }
11 |
--------------------------------------------------------------------------------
/miniprogram/pages/album/detail/detail.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "",
3 | "usingComponents": {
4 | "mp-icon": "weui-miniprogram/icon/icon",
5 | "mp-actionSheet": "weui-miniprogram/actionsheet/actionsheet",
6 | "mp-toptips": "weui-miniprogram/toptips/toptips"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/place/place.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "我的关注",
3 | "enablePullDownRefresh": false,
4 | "usingComponents": {
5 | "mp-icon": "weui-miniprogram/icon/icon",
6 | "authorize-element": "../../../components/authorize-element/authorize-element"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/miniprogram/pages/test/test.ts:
--------------------------------------------------------------------------------
1 | import {themeBehavior} from '../../behaviors/theme-behavior'
2 |
3 | Page({
4 | data: {},
5 |
6 | behaviors: [themeBehavior],
7 |
8 | /** 页面初始化方法 */
9 | async init() {
10 | // ...
11 | },
12 |
13 | onLoad() {
14 | this.init()
15 | },
16 | })
17 |
--------------------------------------------------------------------------------
/miniprogram/pages/me/me.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationStyle": "custom",
3 | "usingComponents": {
4 | "top-space": "/components/top-space/top-space",
5 | "mp-cells": "weui-miniprogram/cells/cells",
6 | "mp-cell": "weui-miniprogram/cell/cell"
7 | },
8 | "enablePullDownRefresh": false
9 | }
10 |
--------------------------------------------------------------------------------
/miniprogram/pages/index/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationStyle": "custom",
3 | "usingComponents": {
4 | "mp-icon": "weui-miniprogram/icon/icon",
5 | "mp-actionSheet": "weui-miniprogram/actionsheet/actionsheet",
6 | "authorize-element": "../../components/authorize-element/authorize-element"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/nick-name/nick-name.scss:
--------------------------------------------------------------------------------
1 | /* pages/user/nick-name/nick-name.wxss */
2 |
3 | .nickname-input {
4 | font-size: 32rpx;
5 |
6 | height: 100rpx;
7 | padding: 0 28rpx;
8 |
9 | background-color: var(--weui-BG-2);
10 | }
11 |
12 | // 提交按钮
13 | .submit {
14 | margin-top: 300rpx;
15 | }
16 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/register-time/register-time.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 我在这一刻加入小鸣助手
5 | {{ registerTime }}
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig
2 |
3 | root = true
4 |
5 | [*]
6 | end_of_line = crlf
7 | insert_final_newline = true
8 | charset = utf-8
9 | indent_style = space
10 | indent_size = 2
11 | trim_trailing_whitespace = true
12 |
13 | [*.wxml]
14 | indent_size = 4
15 |
16 | [*.xml]
17 | indent_size = 4
18 |
19 | [*.json]
20 | indent_size = 2
21 |
--------------------------------------------------------------------------------
/miniprogram/pages/album/edit/edit.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {
3 | "mp-form-page": "weui-miniprogram/form-page/form-page",
4 | "mp-form": "weui-miniprogram/form/form",
5 | "mp-cells": "weui-miniprogram/cells/cells",
6 | "mp-cell": "weui-miniprogram/cell/cell",
7 | "mp-toptips": "weui-miniprogram/toptips/toptips"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/miniprogram/app.ts:
--------------------------------------------------------------------------------
1 | // app.ts
2 |
3 | import {logger} from './app/core/logger'
4 | import {checkUpdate} from './app/core/system'
5 |
6 | App({
7 | // 监听未处理的 Promise 拒绝事件
8 | onUnhandledRejection(res) {
9 | logger.debug(`[UnhandledRejection] ${res.reason}`)
10 | },
11 |
12 | onShow() {
13 | // 检查版本更新
14 | checkUpdate()
15 | },
16 | })
17 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/avatar/avatar.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/account-id/account-id.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 我的账户 ID
5 | {{ accountId }}
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/miniprogram/app/utils/wx.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 封装微信原生方法
3 | */
4 |
5 | /**
6 | * 展示单按钮的模态框
7 | *
8 | * @param content 文本内容
9 | */
10 | export async function showSingleButtonModel(content: string) {
11 | const res = await wx.showModal({
12 | title: '提示',
13 | content,
14 | showCancel: false,
15 | confirmText: '我知道了',
16 | })
17 |
18 | return res.confirm
19 | }
20 |
--------------------------------------------------------------------------------
/miniprogram/pages/system/error/error.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/miniprogram/app/core/http.ts:
--------------------------------------------------------------------------------
1 | import {AxiosRequestConfig} from 'axios'
2 | import {request} from '../http/request'
3 | import {RequestOptionsInternal} from '../http/types'
4 |
5 | /**
6 | * 获取请求数据
7 | */
8 | export async function requestForData(options: RequestOptionsInternal): Promise {
9 | const response = await request(options as unknown as AxiosRequestConfig)
10 | return response.data
11 | }
12 |
--------------------------------------------------------------------------------
/miniprogram/app/http/invalid-token-interceptor.ts:
--------------------------------------------------------------------------------
1 | import {AxiosResponse} from 'axios'
2 |
3 | /**
4 | * 登录状态失效拦截器
5 | *
6 | * ## 主要用途
7 | * 响应状态码 401 表示未授权,可能是登录凭证失效了,本地清除该登录凭证。
8 | */
9 | export async function invalidTokenInterceptor(response: AxiosResponse): Promise {
10 | if (response.status === 401) {
11 | wx.reLaunch({
12 | url: '/pages/system/error/error',
13 | })
14 | }
15 |
16 | return response
17 | }
18 |
--------------------------------------------------------------------------------
/miniprogram/components/authorize-element/authorize-element.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
15 | {{ dialog.content }}
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "eslint:recommended",
5 | "plugin:@typescript-eslint/eslint-recommended",
6 | "plugin:@typescript-eslint/recommended",
7 | "prettier"
8 | ],
9 | "plugins": ["@typescript-eslint", "prettier"],
10 | "rules": {
11 | "@typescript-eslint/no-non-null-assertion": 0,
12 | "@typescript-eslint/no-explicit-any": 0,
13 | "@typescript-eslint/no-this-alias": 0
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/docs/使用 iconfont 流程.md:
--------------------------------------------------------------------------------
1 | # 使用 iconfont 流程
2 |
3 | 在 iconfont 添加新图标后,想要在小程序中使用,需要进行资源替换,以下是操作流程:
4 |
5 | 第 1 步:将 iconfont 项目设置中,将字体格式勾选 `WOFF2` 和 `Base64`。(这一步已完成,后续无需操作)
6 |
7 | 第 2 步:从 iconfont 官网下载资源(压缩包)并解压至本地。
8 |
9 | 第 3 步:将资源目录中的 `iconfont.css` 复制至小程序的 `miniprogram/assets/styles/iconfont` 目录下。
10 |
11 | 第 4 步:将复制后的 `iconfont.css` 文件改名为 `iconfont.wxss`(文件内容无需做任何修改)。
12 |
13 | 第 5 步:在 `app.scss` 中引入(该步已完成,后续迭代中无需操作)。
14 |
15 | ```scss
16 | @import '/assets/styles/iconfont/iconfont.wxss';
17 | ```
18 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": false,
6 | "singleQuote": true,
7 | "quoteProps": "as-needed",
8 | "trailingComma": "es5",
9 | "bracketSpacing": false,
10 | "jsxBracketSameLine": true,
11 | "arrowParens": "always",
12 | "endOfLine": "auto",
13 | "embeddedLanguageFormatting": "auto",
14 | "overrides": [
15 | {
16 | "files": "*.wxml",
17 | "options": {
18 | "tabWidth": 4,
19 | "printWidth": 120
20 | }
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/register-time/register-time.ts:
--------------------------------------------------------------------------------
1 | // pages/user/register-time/register-time.ts
2 | import {getUserInfo} from '../../../app/services/userinfo'
3 | import {themeBehavior} from '../../../behaviors/theme-behavior'
4 |
5 | Page({
6 | data: {
7 | /** 注册时间 */
8 | registerTime: '',
9 | },
10 |
11 | behaviors: [themeBehavior],
12 |
13 | /** 页面初始化方法 */
14 | async init() {
15 | const userInfo = await getUserInfo()
16 | this.setData({registerTime: userInfo.registerTime})
17 | },
18 |
19 | onLoad() {
20 | this.init()
21 | },
22 | })
23 |
--------------------------------------------------------------------------------
/miniprogram/behaviors/share-app-behavior.ts:
--------------------------------------------------------------------------------
1 | import {StaticUrl} from '../app/core/constant'
2 |
3 | /**
4 | * 分享操作混合器
5 | */
6 | export const shareAppBehavior = Behavior({
7 | methods: {
8 | /** 生命周期事件 —— 用户点击转发按钮 */
9 | onShareAppMessage() {
10 | return {
11 | title: '好友推荐:这个小程序真的好用哭了 ~',
12 | path: 'pages/index/index',
13 | imageUrl: StaticUrl.SHARE_APP_LOGO,
14 | }
15 | },
16 |
17 | /** 分享到朋友圈 */
18 | onShareTimeline() {
19 | return {
20 | title: '这是什么神仙小程序,也太好用了吧 ~',
21 | }
22 | },
23 | },
24 | })
25 |
--------------------------------------------------------------------------------
/miniprogram/app/utils/types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 点击事件类型
3 | *
4 | * ## 备注
5 | * 该事件类型经常用到,且原生的类型不好用,因此自定义了一个便于内部调用。
6 | */
7 | export interface TapEvent {
8 | currentTarget: {
9 | dataset: T
10 | }
11 | }
12 |
13 | /** 只包含一个 id 属性的类型,一般用在页面入参 */
14 | export interface Id {
15 | id: string
16 | }
17 |
18 | /** 通用列表响应数据 */
19 | export interface CommonListResponse {
20 | list: T[]
21 | }
22 |
23 | /** 通用响应数据结构(仅报错时返回),所有响应数据都需要继承该接口 */
24 | export interface CommonResponse {
25 | /** 错误码 */
26 | errorCode: number
27 | /** 错误消息 */
28 | errorMessage: string
29 | }
30 |
--------------------------------------------------------------------------------
/miniprogram/app/services/system.ts:
--------------------------------------------------------------------------------
1 | import {MiniProgramInfo} from './system.interface'
2 |
3 | /**
4 | * 获取小程序的相关信息
5 | */
6 | export async function getMiniprogramInfo(): Promise {
7 | const [window, system, device, appBase, account, battery, network] = await Promise.all([
8 | wx.getWindowInfo(),
9 | wx.getSystemInfoSync(),
10 | wx.getDeviceInfo(),
11 | wx.getAppBaseInfo(),
12 | wx.getAccountInfoSync(),
13 | wx.getBatteryInfo(),
14 | wx.getNetworkType(),
15 | ])
16 |
17 | return {window, system, device, appBase, account, battery, network}
18 | }
19 |
--------------------------------------------------------------------------------
/miniprogram/app/utils/params-utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 查询字符串工具
3 | */
4 | export class ParamsUtils {
5 | public static encode(obj?: Record): string {
6 | if (obj === null || obj === undefined) {
7 | return ''
8 | }
9 |
10 | const parts: string[] = []
11 | Object.keys(obj).forEach((key) => {
12 | const value = obj[key]
13 | if (typeof value !== 'undefined' && value !== null && value !== '') {
14 | parts.push(`${key}=${String(value)}`)
15 | }
16 | })
17 |
18 | parts.sort()
19 | return parts.join('&')
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/nick-name/nick-name.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
21 |
22 |
--------------------------------------------------------------------------------
/miniprogram/app/utils/url-utils.ts:
--------------------------------------------------------------------------------
1 | import { ParamsUtils } from './params-utils'
2 |
3 | /**
4 | * URL 工具
5 | */
6 | export class UrlUtils {
7 | /**
8 | * 拼接 URL,主要用于 Axios 中
9 | * @param {string} baseURL URL 地址前缀
10 | * @param {string} url URL 链接或路径
11 | * @param {Record} params 请求参数
12 | */
13 | public static combineUrl(baseURL: string, url: string, params?: Record): string {
14 | const querystring = ParamsUtils.encode(params)
15 | return (baseURL || '') + (url || '') + (querystring ? '?' + querystring : '')
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/miniprogram/app/http/types.ts:
--------------------------------------------------------------------------------
1 | /** 内部调用使用的请求参数 */
2 | export interface RequestOptionsInternal {
3 | /** 内部调用使用的请求参数 */
4 | method: 'GET' | 'POST' | 'PUT' | 'DELETE'
5 |
6 | /** 请求地址 */
7 | url: string
8 |
9 | /** 请求参数 */
10 | params?: Record
11 |
12 | /** 请求数据 */
13 | data?: Record
14 |
15 | /** 是否需要登录 */
16 | auth?: boolean
17 |
18 | /** 是否需要展示 Loading 提示 */
19 | loading?: boolean
20 | }
21 |
22 | /** 基本响应数据结构 */
23 | export interface BaseResponse {
24 | /** 错误码 */
25 | errorCode: number
26 |
27 | /** 错误消息 */
28 | errorMessage: string
29 | }
30 |
--------------------------------------------------------------------------------
/miniprogram/app/utils/time.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs'
2 |
3 | /**
4 | * 计算星期文案,示例:昨天、今天、明天、周一、…、周日
5 | *
6 | * @param date 标准格式日期
7 | */
8 | export function calcWeekdayText(date: string) {
9 | const now = dayjs()
10 | const target = dayjs(date, 'YYYY-MM-DD')
11 |
12 | if (target.add(1, 'day').isSame(now, 'date')) {
13 | return '昨天'
14 | } else if (target.isSame(now, 'date')) {
15 | return '今天'
16 | } else if (target.subtract(1, 'day').isSame(now, 'date')) {
17 | return '明天'
18 | } else {
19 | const week = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
20 | return week[target.day()]
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/warning/warning.ts:
--------------------------------------------------------------------------------
1 | import {themeBehavior} from '../../../behaviors/theme-behavior'
2 | import {WarningNow} from '../../../app/services/weather-data'
3 | import {PageChannelEvent} from '../../../app/core/constant'
4 |
5 | // pages/weather/warning/warning.ts
6 | Page({
7 | data: {
8 | warnings: [] as WarningNow[],
9 | },
10 |
11 | behaviors: [themeBehavior],
12 |
13 | onLoad() {
14 | const eventChannel = this.getOpenerEventChannel()
15 | if (eventChannel) {
16 | eventChannel.on(PageChannelEvent.DATA_TRANSFER, (data) => {
17 | this.setData(data)
18 | })
19 | }
20 | },
21 | })
22 |
--------------------------------------------------------------------------------
/miniprogram/behaviors/theme-behavior.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 主题切换跟踪器
3 | *
4 | * ## 备注
5 | * 1. 按照正常流程,应该在 `detached` 生命周期时进行取消主题切换监听(`wx.offThemeChange`),如果不取消监听会导致监听器不断增多。
6 | * 2. 加上取消监听代码后,会导致主题切换监听异常,暂无解决办法。
7 | * 3. 综上考虑,不取消监听。
8 | */
9 | export const themeBehavior = Behavior({
10 | data: {
11 | /** 主题:'light' | 'dark' */
12 | theme: wx.getSystemInfoSync().theme,
13 | },
14 |
15 | lifetimes: {
16 | attached() {
17 | wx.onThemeChange((res) => {
18 | this.setData({theme: res.theme})
19 | })
20 | },
21 |
22 | ready() {
23 | this.setData({theme: wx.getSystemInfoSync().theme})
24 | },
25 | },
26 | })
27 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/warning/warning.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ item.title }}
8 | 发布时间:{{ item.pubTime }}
9 |
10 |
11 | {{ item.text }}
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/miniprogram/pages/about/main/main.ts:
--------------------------------------------------------------------------------
1 | // pages/about/main/main.ts
2 |
3 | import {StaticUrl} from '../../../app/core/constant'
4 | import {getVersion} from '../../../app/core/system'
5 | import {themeBehavior} from '../../../behaviors/theme-behavior'
6 |
7 | Page({
8 | data: {
9 | /** 当前的版本号 */
10 | version: '',
11 |
12 | /** logo 图片的 URL 地址 */
13 | logoUrl: '',
14 | },
15 |
16 | behaviors: [themeBehavior],
17 |
18 | /** 页面初始化方法 */
19 | async init() {
20 | const version = getVersion()
21 | const logoUrl = StaticUrl.LOGO
22 | this.setData({version, logoUrl})
23 | },
24 |
25 | onLoad() {
26 | this.init()
27 | },
28 | })
29 |
--------------------------------------------------------------------------------
/miniprogram/app/http/request-log-interceptor.ts:
--------------------------------------------------------------------------------
1 | import { AxiosResponse } from 'axios'
2 | import { logger } from '../core/logger'
3 | import { UrlUtils } from '../utils/url-utils'
4 |
5 | /**
6 | * 请求日志拦截器
7 | */
8 | export function requestLogInterceptor(response: AxiosResponse): AxiosResponse {
9 | const method = response.config.method
10 | const baseURL = response.config.baseURL
11 | const url = response.config.url
12 | const params = response.config.params
13 | const status = response.status
14 |
15 | const wholeUrl = UrlUtils.combineUrl(baseURL!, url!, params)
16 |
17 | logger.info(`[HTTP] ${status} ${method} ${wholeUrl}`)
18 |
19 | return response
20 | }
21 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/register-time/register-time.scss:
--------------------------------------------------------------------------------
1 | /* pages/user/register-time/register-time.wxss */
2 |
3 | .container {
4 | display: flex;
5 | align-items: center;
6 | flex-direction: column;
7 |
8 | .title {
9 | font-size: 38rpx;
10 | font-weight: bold;
11 |
12 | margin-top: 200rpx;
13 |
14 | text-align: center;
15 | }
16 |
17 | .content {
18 | font-size: 50rpx;
19 | font-weight: bold;
20 |
21 | margin-top: 40rpx;
22 | padding: 30rpx 50rpx;
23 |
24 | text-align: center;
25 | letter-spacing: 4rpx;
26 |
27 | color: var(--weui-BRAND);
28 | border-radius: 20rpx;
29 | background-color: var(--weui-BG-2);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/miniprogram/app/http/auth-interceptor.ts:
--------------------------------------------------------------------------------
1 | import {AxiosRequestConfig} from 'axios'
2 | import {login} from '../core/auth'
3 | import {StorageField} from '../core/constant'
4 | import {storage} from '../core/storage'
5 | import {RequestOptionsInternal} from './types'
6 |
7 | /**
8 | * 登录鉴权拦截器
9 | *
10 | * ### 主要用途
11 | * 对于需要鉴权的 API,判断是否获取登录凭证,若无则先登录再请求。
12 | */
13 | export async function authInterceptor(config: AxiosRequestConfig): Promise {
14 | const auth = (config as unknown as RequestOptionsInternal).auth
15 | const securityToken = storage.get(StorageField.TOKEN)
16 |
17 | if (auth && !securityToken) {
18 | await login()
19 | }
20 |
21 | return config
22 | }
23 |
--------------------------------------------------------------------------------
/miniprogram/app/http/security-token-interceptor.ts:
--------------------------------------------------------------------------------
1 | import {AxiosRequestConfig} from 'axios'
2 | import {SecurityToken} from '../core/auth'
3 | import {StorageField} from '../core/constant'
4 | import {storage} from '../core/storage'
5 |
6 | /**
7 | * 登录凭证拦截器
8 | *
9 | * ## 主要用途
10 | * 本地存在登录凭证且未过期,则携带在请求头中,否则不进行任何操作。
11 | */
12 | export function securityTokenInterceptor(config: AxiosRequestConfig): AxiosRequestConfig {
13 | const securityToken = storage.get(StorageField.TOKEN)
14 |
15 | if (securityToken) {
16 | const headers = config.headers || {}
17 | headers[securityToken.headerName] = securityToken.token
18 | config.headers = headers
19 | }
20 |
21 | return config
22 | }
23 |
--------------------------------------------------------------------------------
/miniprogram/pages/system/error/error.ts:
--------------------------------------------------------------------------------
1 | // pages/system/error/error.ts
2 |
3 | import {login} from '../../../app/core/auth'
4 | import {themeBehavior} from '../../../behaviors/theme-behavior'
5 |
6 | // 页面用途
7 | // 当出现一些异常难以处理时,直接跳转到这个页面,然后让用户重试操作。
8 |
9 | Page({
10 | data: {},
11 |
12 | behaviors: [themeBehavior],
13 |
14 | /**
15 | * 页面初始化方法
16 | */
17 | init() {
18 | // 清除所有缓存
19 | wx.clearStorageSync()
20 |
21 | // 重新登录一次
22 | login()
23 | },
24 |
25 | /**
26 | * 生命周期函数--监听页面加载
27 | */
28 | onLoad() {
29 | this.init()
30 | },
31 |
32 | /**
33 | * 跳转到首页
34 | */
35 | goToIndexPage() {
36 | wx.switchTab({url: '/pages/index/index'})
37 | },
38 | })
39 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/account-id/account-id.scss:
--------------------------------------------------------------------------------
1 | /* pages/user/account-id/account-id.wxss */
2 |
3 | .container {
4 | display: flex;
5 | align-items: center;
6 | flex-direction: column;
7 |
8 | .title {
9 | font-size: 38rpx;
10 | font-weight: bold;
11 |
12 | margin-top: 200rpx;
13 |
14 | text-align: center;
15 | }
16 |
17 | .content {
18 | font-size: 80rpx;
19 | font-weight: bold;
20 |
21 | margin-top: 40rpx;
22 | padding: 30rpx 50rpx;
23 |
24 | text-align: center;
25 | letter-spacing: 22rpx;
26 |
27 | color: var(--weui-TEXTGREEN);
28 | border-radius: 20rpx;
29 | background-color: var(--weui-BG-2);
30 | }
31 |
32 | .copy {
33 | margin-top: 400rpx;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/account-id/account-id.ts:
--------------------------------------------------------------------------------
1 | // pages/user/account-id/account-id.ts
2 |
3 | import {getUserInfo} from '../../../app/services/userinfo'
4 | import {themeBehavior} from '../../../behaviors/theme-behavior'
5 |
6 | Page({
7 | data: {
8 | /** 账户 ID */
9 | accountId: 0,
10 | },
11 |
12 | behaviors: [themeBehavior],
13 |
14 | /** 页面初始化方法 */
15 | async init() {
16 | const userInfo = await getUserInfo()
17 | this.setData({accountId: userInfo.accountId})
18 | },
19 |
20 | onLoad() {
21 | this.init()
22 | },
23 |
24 | /** 复制账户 ID */
25 | copy() {
26 | const accountId = this.data.accountId
27 |
28 | wx.vibrateShort({type: 'medium'})
29 | wx.setClipboardData({data: accountId + ''})
30 | },
31 | })
32 |
--------------------------------------------------------------------------------
/miniprogram/app/core/system.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 检查小程序版本更新
3 | *
4 | * @since 1.6.0
5 | */
6 | import {logger} from './logger'
7 | import {version} from './version'
8 |
9 | export function checkUpdate() {
10 | const updateManager = wx.getUpdateManager()
11 |
12 | updateManager.onUpdateReady(() => {
13 | wx.showModal({
14 | title: '更新提示',
15 | content: '新版本已经准备好,是否重启应用?',
16 | showCancel: false,
17 | confirmText: '立即重启',
18 | }).then((res) => {
19 | if (res.confirm) {
20 | updateManager.applyUpdate()
21 | }
22 | })
23 | })
24 |
25 | updateManager.onUpdateFailed(() => {
26 | logger.debug('版本更新失败')
27 | })
28 | }
29 |
30 | /**
31 | * 获取当前的小程序版本号
32 | */
33 | export function getVersion() {
34 | return version
35 | }
36 |
--------------------------------------------------------------------------------
/miniprogram/app.scss:
--------------------------------------------------------------------------------
1 | @import "/assets/styles/iconfont/iconfont.wxss";
2 |
3 | page, .page {
4 | font-family: -apple-system-font, Helvetica Neue, Helvetica, sans-serif;
5 | font-size: 32rpx;
6 | line-height: 1.4;
7 |
8 | min-height: 100%;
9 |
10 | color: var(--weui-FG-0);
11 | background-color: var(--weui-BG-0);
12 | }
13 |
14 | .page {
15 | min-height: 100vh;
16 | }
17 |
18 | image {
19 | max-width: 100%;
20 | max-height: 100%;
21 | }
22 |
23 | view, image, text, navigator {
24 | box-sizing: border-box;
25 | margin: 0;
26 | padding: 0;
27 | }
28 |
29 | /* 单行文本缩略 */
30 | .text-ellipsis-l1 {
31 | overflow: hidden;
32 |
33 | white-space: nowrap;
34 | text-overflow: ellipsis;
35 | }
36 |
37 | /* 通用 view 组件 按下去的样式 */
38 | .hover {
39 | opacity: .3;
40 | }
41 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/rain/rain.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ rain.summary }}
6 |
7 |
8 |
9 |
10 |
11 |
12 | 现在
13 | 1小时
14 | 2小时
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/avatar/avatar.ts:
--------------------------------------------------------------------------------
1 | // pages/user/avatar/avatar.ts
2 | // 头像预览页
3 |
4 | import {getUserInfo, updateAvatar} from '../../../app/services/userinfo'
5 | import {themeBehavior} from '../../../behaviors/theme-behavior'
6 |
7 | Page({
8 | data: {
9 | /** 头像图片的 URL 地址 */
10 | avatarUrl: '',
11 | },
12 |
13 | behaviors: [themeBehavior],
14 |
15 | /** 页面初始化方法 */
16 | async init() {
17 | const userInfo = await getUserInfo()
18 | this.setData({avatarUrl: userInfo.avatarUrl})
19 | },
20 |
21 | onShow() {
22 | this.init()
23 | },
24 |
25 | /** 处理选择头像事件 */
26 | async handleChooseAvatar(e: WechatMiniprogram.CustomEvent<{avatarUrl: string}>) {
27 | const avatarUrl = e.detail.avatarUrl
28 | await updateAvatar(avatarUrl)
29 |
30 | this.init()
31 | },
32 | })
33 |
--------------------------------------------------------------------------------
/miniprogram/app/services/scan-login.ts:
--------------------------------------------------------------------------------
1 | import {requestForData} from '../core/http'
2 | import {CommonResponse} from '../utils/types'
3 |
4 | /** 扫码登录凭据 */
5 | export interface QrCodeTicket extends CommonResponse {
6 | /** 票据 ID */
7 | id: string
8 | /** 小程序码图片的 URL 地址 */
9 | url: string
10 | /** IP 地址 */
11 | ip: string
12 | /** 地区信息 */
13 | region: string
14 | }
15 |
16 | /**
17 | * 进行 "扫码" 操作
18 | */
19 | export function scanQrcode(id: string): Promise {
20 | return requestForData({
21 | method: 'PUT',
22 | url: `/login/qrcode/scan/${id}`,
23 | auth: true,
24 | })
25 | }
26 |
27 | /**
28 | * 进行 "确认登录" 操作
29 | */
30 | export function confirmQrcode(id: string): Promise {
31 | return requestForData({
32 | method: 'PUT',
33 | url: `/login/qrcode/confirm/${id}`,
34 | auth: true,
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/warning/warning.scss:
--------------------------------------------------------------------------------
1 | /* pages/weather/warning/warning.wxss */
2 |
3 | .warning {
4 | padding: 20rpx;
5 |
6 | .item {
7 | margin-bottom: 20rpx;
8 | padding: 20rpx;
9 |
10 | border-radius: 20rpx;
11 | background-color: var(--weui-BG-1);
12 |
13 | .head {
14 | display: flex;
15 |
16 | .image {
17 | width: 200rpx;
18 | height: 150rpx;
19 | }
20 |
21 | .right {
22 | display: flex;
23 | flex-direction: column;
24 | justify-content: space-between;
25 | margin-left: 20rpx;
26 |
27 | .title {
28 | font-size: 30rpx;
29 | }
30 |
31 | .time {
32 | font-size: 24rpx;
33 | }
34 | }
35 | }
36 |
37 | .content {
38 | font-size: 28rpx;
39 | margin-top: 18rpx;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/rain/rain.ts:
--------------------------------------------------------------------------------
1 | import {themeBehavior} from '../../../behaviors/theme-behavior'
2 | import {MinutelyRain} from '../../../app/services/weather-data'
3 | import {PageChannelEvent} from '../../../app/core/constant'
4 |
5 | Page({
6 | data: {
7 | // ----------------------------- 从上个页面带来的数据 -----------------------------
8 |
9 | /** 分钟级降水数据 */
10 | rain: {} as MinutelyRain,
11 |
12 | /** 地点名称 */
13 | locationName: '',
14 | },
15 |
16 | behaviors: [themeBehavior],
17 |
18 | onLoad() {
19 | const eventChannel = this.getOpenerEventChannel()
20 | if (eventChannel) {
21 | eventChannel.on(PageChannelEvent.DATA_TRANSFER, (data) => {
22 | this.setData(data)
23 | })
24 | }
25 |
26 | // 将页面标题设为地点名称
27 | const title = this.data.locationName
28 | wx.setNavigationBarTitle({title})
29 | },
30 | })
31 |
--------------------------------------------------------------------------------
/miniprogram/pages/me/me.scss:
--------------------------------------------------------------------------------
1 | /* pages/me/me.scss */
2 | .top {
3 | padding-bottom: 40rpx;
4 |
5 | background-color: var(--weui-BG-2);
6 | }
7 |
8 | .userinfo {
9 | display: flex;
10 | justify-content: flex-start;
11 |
12 | padding-left: 50rpx;
13 |
14 | .avatar image {
15 | width: 120rpx;
16 | height: 120rpx;
17 |
18 | border-radius: 10rpx;
19 | }
20 |
21 | .info {
22 | display: flex;
23 | flex: 1;
24 | flex-direction: column;
25 | justify-content: space-between;
26 |
27 | margin-left: 30rpx;
28 | padding: 4rpx 0 10rpx;
29 | padding-right: 28rpx;
30 |
31 | .nickname {
32 | font-size: 38rpx;
33 | font-weight: bold;
34 | }
35 |
36 | .desc {
37 | font-size: 30rpx;
38 |
39 | display: flex;
40 | justify-content: space-between;
41 |
42 | color: var(--weui-FG-1);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/miniprogram/components/top-space/top-space.ts:
--------------------------------------------------------------------------------
1 | // components/top-space/top-space.ts
2 |
3 | /**
4 | * ## 注意事项
5 | * 1. 使用本组件时,需要开启 **自定义导航栏** (` "navigationStyle": "custom" `)
6 | */
7 | Component({
8 | /** 组件的属性列表 */
9 | properties: {
10 | /** 相对于胶囊按钮底部往下的偏移量,单位:px */
11 | offset: {
12 | type: Number,
13 | value: 0,
14 | },
15 | },
16 |
17 | /** 组件的初始数据 */
18 | data: {
19 | /** 顶部保留高度,为预估的胶囊按钮底部相对于屏幕顶部的高度 */
20 | reservedHeight: 80,
21 | },
22 |
23 | /** 组件的生命周期 */
24 | lifetimes: {
25 | attached() {
26 | this.resetReservedHeight()
27 | },
28 | },
29 |
30 | /**
31 | * 组件的方法列表
32 | */
33 | methods: {
34 | /** 根据胶囊按钮重置保留高度 */
35 | resetReservedHeight() {
36 | /** 胶囊按钮下边界坐标 */
37 | const { bottom } = wx.getMenuButtonBoundingClientRect()
38 | this.setData({
39 | reservedHeight: bottom,
40 | })
41 | },
42 | },
43 | })
44 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strictNullChecks": true,
4 | "noImplicitAny": true,
5 | "module": "CommonJS",
6 | "target": "ES5",
7 | "allowJs": false,
8 | "experimentalDecorators": true,
9 | "noImplicitThis": true,
10 | "noImplicitReturns": true,
11 | "alwaysStrict": true,
12 | "inlineSourceMap": true,
13 | "inlineSources": true,
14 | "noFallthroughCasesInSwitch": true,
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "strict": true,
18 | "removeComments": true,
19 | "pretty": true,
20 | "strictPropertyInitialization": true,
21 | "lib": [
22 | "ES6",
23 | "ES2016.Array.Include",
24 | "DOM"
25 | ],
26 | "types": [
27 | "miniprogram-api-typings"
28 | ],
29 | "allowSyntheticDefaultImports": true
30 | },
31 | "include": [
32 | "./**/*.ts"
33 | ],
34 | "exclude": [
35 | "node_modules",
36 | "old-files"
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/miniprogram/pages/scan/login.scss:
--------------------------------------------------------------------------------
1 | /* pages/scan/login.scss */
2 | page, .page {
3 | background-color: var(--weui-BG-2);
4 | }
5 |
6 | .scanned {
7 | padding: 20rpx;
8 |
9 | .container {
10 | display: flex;
11 | align-items: center;
12 | flex-direction: column;
13 | justify-content: space-between;
14 | height: 100%;
15 | padding: 240rpx 0;
16 | border-radius: 24rpx;
17 | background-color: var(--weui-BG-2);
18 |
19 | .pc {
20 | width: 280rpx;
21 | }
22 |
23 | .title {
24 | font-size: 36rpx;
25 | margin-top: 40rpx;
26 | }
27 |
28 | .ip {
29 | font-size: 26rpx;
30 | margin-top: 30rpx;
31 | color: var(--weui-FG-1);
32 | }
33 |
34 | .confirm {
35 | margin-top: 200rpx;
36 | }
37 |
38 | .cancel {
39 | font-size: 28rpx;
40 | margin-top: 40rpx;
41 | color: var(--weui-LINK);
42 | }
43 | }
44 | }
45 |
46 | .success {
47 | position: relative;
48 | height: 100%;
49 | }
50 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/rain/rain.scss:
--------------------------------------------------------------------------------
1 | /* pages/weather/rain/rain.wxss */
2 |
3 | @import "../../../assets/styles/weather.scss";
4 |
5 | .empty {
6 | background-color: var(--weui-BG-0);
7 | height: 32rpx;
8 | }
9 |
10 | .rain-container {
11 | margin: 0 28rpx;
12 | border-radius: 20rpx;
13 | padding: 20rpx;
14 |
15 | .head {
16 | height: 160rpx;
17 | display: flex;
18 | justify-content: center;
19 | align-items: center;
20 |
21 | .summary {
22 | font-size: 30rpx;
23 | }
24 | }
25 |
26 | .body {
27 | .chart {
28 | display: flex;
29 | justify-content: space-between;
30 | align-items: flex-end;
31 | min-height: 200rpx;
32 |
33 | .item {
34 | width: 10rpx;
35 | }
36 | }
37 |
38 | .time {
39 | margin-top: 20rpx;
40 | display: flex;
41 | justify-content: space-between;
42 | align-items: center;
43 |
44 | .desc {
45 | font-size: 26rpx;
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/miniprogram/pages/album/edit/edit.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 inlym
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "life-helper-miniprogram",
3 | "version": "2.0.0",
4 | "description": "「小鸣助手」项目 - 小程序端源码",
5 | "scripts": {
6 | "tsc": "node ./node_modules/typescript/lib/tsc.js"
7 | },
8 | "keywords": [
9 | "life-helper",
10 | "inlym",
11 | "miniprogram"
12 | ],
13 | "author": "inlym",
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/inlym/life-helper-miniprogram/issues"
17 | },
18 | "homepage": "https://github.com/inlym/life-helper-miniprogram#readme",
19 | "devDependencies": {
20 | "@types/crypto-js": "^4.1.1",
21 | "@typescript-eslint/eslint-plugin": "^5.59.7",
22 | "@typescript-eslint/parser": "^5.59.7",
23 | "eslint": "^8.41.0",
24 | "eslint-config-prettier": "^8.8.0",
25 | "eslint-plugin-prettier": "^4.2.1",
26 | "miniprogram-api-typings": "^3.9.1",
27 | "prettier": "^2.8.8",
28 | "typescript": "^5.0.4"
29 | },
30 | "dependencies": {
31 | "axios": "0.21.4",
32 | "crypto-js": "^4.1.1",
33 | "dayjs": "^1.11.7",
34 | "life-helper-miniprogram-secret": "^2.0.0",
35 | "miniprogram-logger-plus": "^1.2.1"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/miniprogram/pages/great-day/edit/edit.scss:
--------------------------------------------------------------------------------
1 | /* pages/great-day/edit/edit.wxss */
2 |
3 | .icon-area {
4 | display: flex;
5 | align-items: center;
6 | flex-direction: column;
7 | justify-content: center;
8 |
9 | height: 400rpx;
10 |
11 | // border: 2rpx solid red;
12 |
13 | .emoji {
14 | font-size: 120rpx;
15 | }
16 |
17 | .button {
18 | margin-top: 20rpx;
19 | }
20 | }
21 |
22 | .input-area {
23 | margin-top: 30rpx;
24 | padding: 0 28rpx;
25 |
26 | .name {
27 | height: 100rpx;
28 | padding: 0 28rpx;
29 |
30 | border-radius: 24rpx;
31 | background-color: var(--weui-BG-1);
32 | }
33 |
34 | .date-picker {
35 | margin-top: 40rpx;
36 |
37 | .picker-inner {
38 | display: flex;
39 | align-items: center;
40 | justify-content: space-between;
41 |
42 | height: 100rpx;
43 | padding: 0 28rpx;
44 |
45 | border-radius: 24rpx;
46 | background-color: var(--weui-BG-1);
47 |
48 | .space {
49 | flex: 1;
50 | }
51 |
52 | .iconfont {
53 | margin-left: 20rpx;
54 | }
55 | }
56 | }
57 | }
58 |
59 | .submit {
60 | margin-top: 200rpx;
61 | }
62 |
--------------------------------------------------------------------------------
/miniprogram/app/core/constant.ts:
--------------------------------------------------------------------------------
1 | /** 在 `Storage` 中存储的字段 */
2 | export const StorageField = {
3 | /** 上一次获取的 `code` 信息 */
4 | CODE: 'CODE',
5 |
6 | /** 登录凭证 `token` */
7 | TOKEN: 'TOKEN',
8 |
9 | /** 当前选中的天气地点 ID */
10 | SELECTED_WEATHER_PLACE_ID: 'SELECTED_WEATHER_PLACE_ID',
11 |
12 | /** 用户信息 */
13 | USER_INFO: 'USER_INFO',
14 | }
15 |
16 | /** 静态资源地址 */
17 | export const StaticUrl = {
18 | /** 主标志 */
19 | LOGO: 'https://static.lifehelper.com.cn/static/project/logo.png',
20 |
21 | /** 用于分享时 logo */
22 | SHARE_APP_LOGO: 'https://static.lifehelper.com.cn/static/project/share.jpeg',
23 | }
24 |
25 | /** 页面通道事件 */
26 | export const PageChannelEvent = {
27 | /** 页面传递数据 */
28 | DATA_TRANSFER: 'DATA_TRANSFER',
29 |
30 | /** 刷新页面数据 */
31 | REFRESH_DATA: 'REFRESH_DATA',
32 | }
33 |
34 | /** 通用颜色 */
35 | export const CommonColor = {
36 | RED: '#fa5151',
37 | ORANGE: '#fa9d3b',
38 | YELLOW: '#ffc300',
39 | GREEN: '#91d300',
40 | LIGHTGREEN: '#95ec69',
41 | BRAND: '#07c160',
42 | BLUE: '#10aeff',
43 | INDIGO: '#1485ee',
44 | PURPLE: '#6467f0',
45 | WHITE: '#fff',
46 | BLACK: '#000',
47 | LINK: '#576b95',
48 | TEXTGREEN: '#06ae56',
49 | }
50 |
--------------------------------------------------------------------------------
/miniprogram/pages/ai/text/text.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 | 我
13 | {{ item.content }}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/miniprogram/pages/scan/login.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 确认登录“小鸣助手”网页版
9 | 登录 IP:{{ ip }}({{ region }})
10 |
11 |
12 | 取消登录
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/miniprogram/app/utils/canvas.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 创建并初始化 Canvas
3 | *
4 | * ## 备忘
5 | * 官方的示例方案,基于 canvas 的宽高使用 px,而本项目会使用 rpx,因此略作调整。
6 | *
7 | * ## 使用说明
8 | *
9 | * ```typescript
10 | * // 获取 ctx
11 | * const ctx = await createCanvasContext('#my-canvas')
12 | *
13 | * // 获取 canvas 宽高
14 | * const width = ctx.canvas.width
15 | * const height = ctx.canvas.height
16 | *
17 | * // 后续在使用 w 和 好 参数时,不要当作 px,而是作为 canvas 宽高的百分比
18 | * // 例如要画 1 个面积占比 1/4 的矩形
19 | * ctx.fillRect(0, 0, ctx.canvas.width / 2, ctx.canvas.height / 2)
20 | * ```
21 | *
22 | * @param selector Canvas 的选择器名称
23 | */
24 | export function createCanvasContext(selector: string): Promise {
25 | return new Promise((resolve) => {
26 | const query = wx.createSelectorQuery()
27 | query
28 | .select(selector)
29 | .fields({ node: true, size: true })
30 | .exec((res) => {
31 | const canvas = res[0].node
32 | const ctx = canvas.getContext('2d')
33 | const dpr = wx.getSystemInfoSync().pixelRatio
34 | canvas.width = res[0].width * dpr
35 | canvas.height = res[0].height * dpr
36 |
37 | // 备注:官方示例这里还有一句下面的语句,这里去掉了,原因见上方备忘
38 | // ctx.scale(dpr, dpr)
39 |
40 | resolve(ctx)
41 | })
42 | })
43 | }
44 |
--------------------------------------------------------------------------------
/miniprogram/app/services/ai.ts:
--------------------------------------------------------------------------------
1 | import {BaseResponse} from './../http/types'
2 | import {requestForData} from '../core/http'
3 |
4 | export interface CreateCompletionResponse extends BaseResponse {
5 | /** 回复的内容 */
6 | text: string
7 | }
8 |
9 | export function createCompletion(prompt: string): Promise {
10 | return requestForData({
11 | method: 'POST',
12 | url: '/ai/text',
13 | auth: false,
14 | data: {prompt},
15 | })
16 | }
17 |
18 | /** 智能会话消息 */
19 | export interface AiChatMessage {
20 | /** 消息 ID */
21 | id: string
22 | /** 角色 */
23 | role: string
24 | /** 文本内容 */
25 | content: string
26 | }
27 |
28 | /** 智能会话对象 */
29 | export interface AiChat {
30 | /** 会话 ID */
31 | id: string
32 | /** 消息列表 */
33 | messages: AiChatMessage[]
34 | }
35 |
36 | /**
37 | * 创建一个新的智能会话
38 | */
39 | export function createAiChat(): Promise {
40 | return requestForData({
41 | method: 'POST',
42 | url: '/ai/chat',
43 | auth: false,
44 | })
45 | }
46 |
47 | /** 为已有的会话添加消息 */
48 | export function appendMessage(chatId: string, prompt: string): Promise {
49 | return requestForData({
50 | method: 'PUT',
51 | url: `/ai/chat/${chatId}`,
52 | auth: false,
53 | data: {prompt},
54 | })
55 | }
56 |
--------------------------------------------------------------------------------
/miniprogram/app/http/request.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import {config} from '../core/config'
3 | import {aliyunApigwSignatureInterceptorBuilder} from './aliyun-apigw-signature-interceptor'
4 | import {authInterceptor} from './auth-interceptor'
5 | import {invalidTokenInterceptor} from './invalid-token-interceptor'
6 | import {miniprogramAdapter} from './miniprogram-adatper'
7 | import {requestLogInterceptor} from './request-log-interceptor'
8 | import {securityTokenInterceptor} from './security-token-interceptor'
9 |
10 | const instance = axios.create({
11 | baseURL: config.baseURL,
12 | adapter: miniprogramAdapter,
13 | validateStatus: function (status: number): boolean {
14 | return status >= 200 && status <= 599
15 | },
16 | })
17 |
18 | // 请求拦截器(先添加的后执行)
19 | instance.interceptors.request.use(
20 | aliyunApigwSignatureInterceptorBuilder(config.signature.key, config.signature.secret, false)
21 | )
22 | instance.interceptors.request.use(securityTokenInterceptor)
23 | instance.interceptors.request.use(authInterceptor)
24 | // instance.interceptors.request.use(showLoadingInterceptor)
25 |
26 | // 响应拦截器
27 | // instance.interceptors.response.use(hideLoadingInterceptor)
28 | instance.interceptors.response.use(invalidTokenInterceptor)
29 | instance.interceptors.response.use(requestLogInterceptor)
30 |
31 | export const request = instance
32 |
--------------------------------------------------------------------------------
/miniprogram/pages/me/me.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | {{ userInfo.nickName }}
17 |
18 | 欢迎使用小鸣助手
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
35 |
36 |
--------------------------------------------------------------------------------
/miniprogram/pages/great-day/detail/detail.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ day.icon }}
5 | {{ day.name }}
6 | {{ day.formattedDate }}
7 |
8 | 还有
9 | 已经过去
10 | {{ day.daysAbs }}
11 | 天
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 编辑
21 |
22 |
23 |
24 | 分享
25 |
26 |
27 |
28 | 删除
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/miniprogram/app/services/system.interface.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 小程序基础信息
3 | *
4 | * ## 说明
5 | * (1)将能够获取到的小程序基础信息汇总起来
6 | * (2)由于直接将返回值上传,不对数据做二次处理,所以每个类型就都直接标记为 any 了。
7 | */
8 | export interface MiniProgramInfo {
9 | /**
10 | * 窗口信息
11 | *
12 | * @see https://developers.weixin.qq.com/miniprogram/dev/api/base/system/wx.getWindowInfo.html
13 | */
14 | window: any
15 |
16 | /**
17 | * 系统信息
18 | *
19 | * @see https://developers.weixin.qq.com/miniprogram/dev/api/base/system/wx.getSystemInfoSync.html
20 | */
21 | system: any
22 |
23 | /**
24 | * 设备基础信息
25 | *
26 | * @see https://developers.weixin.qq.com/miniprogram/dev/api/base/system/wx.getDeviceInfo.html
27 | */
28 | device: any
29 |
30 | /**
31 | * 微信APP基础信息
32 | *
33 | * @see https://developers.weixin.qq.com/miniprogram/dev/api/base/system/wx.getAppBaseInfo.html
34 | */
35 | appBase: any
36 |
37 | /**
38 | * 当前帐号信息
39 | *
40 | * @see https://developers.weixin.qq.com/miniprogram/dev/api/open-api/account-info/wx.getAccountInfoSync.html
41 | */
42 | account: any
43 |
44 | /**
45 | * 设备电量
46 | *
47 | * @see https://developers.weixin.qq.com/miniprogram/dev/api/device/battery/wx.getBatteryInfo.html
48 | */
49 | battery: any
50 |
51 | /**
52 | * 网络类型
53 | *
54 | * @see https://developers.weixin.qq.com/miniprogram/dev/api/device/network/wx.getNetworkType.html
55 | */
56 | network: any
57 | }
58 |
--------------------------------------------------------------------------------
/miniprogram/pages/great-day/list/list.ts:
--------------------------------------------------------------------------------
1 | // pages/great-day/list/list.ts
2 |
3 | import {PageChannelEvent} from '../../../app/core/constant'
4 | import {GreatDay, listGreatDay} from '../../../app/services/great-day'
5 | import {TapEvent} from '../../../app/utils/types'
6 | import {themeBehavior} from '../../../behaviors/theme-behavior'
7 |
8 | Page({
9 | data: {
10 | // ============================= 从HTTP请求获取的数据 =============================
11 |
12 | /** 纪念日列表 */
13 | list: [] as GreatDay[],
14 | },
15 |
16 | behaviors: [themeBehavior],
17 |
18 | onLoad() {
19 | this.init()
20 | },
21 |
22 | /** 页面初始化方法 */
23 | async init() {
24 | const list = await listGreatDay()
25 | this.setData({list})
26 | },
27 |
28 | /** 跳转到“编辑”页 */
29 | goToEditPage() {
30 | const self = this
31 | wx.navigateTo({
32 | url: '/pages/great-day/edit/edit',
33 | events: {
34 | [PageChannelEvent.REFRESH_DATA]: function () {
35 | self.init()
36 | },
37 | },
38 | })
39 | },
40 |
41 | /** 跳转到“详情”页 */
42 | goToDetailPage(e: TapEvent<{id: string}>) {
43 | const self = this
44 | const id = e.currentTarget.dataset.id
45 | wx.navigateTo({
46 | url: `/pages/great-day/detail/detail?id=${id}`,
47 | events: {
48 | [PageChannelEvent.REFRESH_DATA]: function () {
49 | self.init()
50 | },
51 | },
52 | })
53 | },
54 | })
55 |
--------------------------------------------------------------------------------
/miniprogram/app/core/config.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 配置文件
3 | */
4 |
5 | import {secret, Signature} from 'life-helper-miniprogram-secret'
6 |
7 | /**
8 | * 环境名称
9 | * 1. 对接的服务端环境,发版前务必改为 'production'
10 | *
11 | * 取值:
12 | * - `production` => 生产环境
13 | * - `development` => 测试环境
14 | * - `local` => 本地开发环境
15 | */
16 | export type Stage = 'production' | 'development' | 'local'
17 |
18 | export const stage: Stage = 'production'
19 |
20 | /** 配置项数据类型 */
21 | export interface Config {
22 | /** 环境名称 */
23 | stage: Stage
24 |
25 | /** URL 前缀部分 */
26 | baseURL: string
27 |
28 | /** 签名密钥 */
29 | signature: Signature
30 | }
31 |
32 | const localConfig: Config = {
33 | stage: 'local',
34 | baseURL: 'https://api-local.lifehelper.com.cn',
35 | signature: secret.development.signature,
36 | }
37 |
38 | const devConfig: Config = {
39 | stage: 'development',
40 | baseURL: 'https://api-test.lifehelper.com.cn',
41 | signature: secret.development.signature,
42 | }
43 |
44 | const prodConfig: Config = {
45 | stage: 'production',
46 | baseURL: 'https://api.lifehelper.com.cn',
47 | signature: secret.production.signature,
48 | }
49 |
50 | /** 封装获取配置的方法 */
51 | function getConfig(stage: Stage): Config {
52 | if (stage === 'local') {
53 | return localConfig
54 | } else if (stage === 'production') {
55 | return prodConfig
56 | } else {
57 | return devConfig
58 | }
59 | }
60 |
61 | export const config = getConfig(stage)
62 |
--------------------------------------------------------------------------------
/miniprogram/app/services/authorization.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 信授权相关方法
3 | */
4 |
5 | /** 授权状态 */
6 | import {AuthSetting} from '../utils/wx-typings'
7 |
8 | export enum AuthorizeStatus {
9 | /** 已授权 */
10 | Authorized = 0,
11 |
12 | /** 未发起过授权请求 */
13 | NotApplied = 1,
14 |
15 | /** 发起过授权请求被拒绝 */
16 | Denied = 2,
17 | }
18 |
19 | /**
20 | * 根据授权结果检查授权状态
21 | * @param scope 授权项目
22 | * @param authSetting 用户授权结果
23 | */
24 | export function checkAuthSetting(scope: keyof AuthSetting, authSetting: AuthSetting): AuthorizeStatus {
25 | if (authSetting[scope] === true) {
26 | return AuthorizeStatus.Authorized
27 | } else if (authSetting[scope] === false) {
28 | return AuthorizeStatus.Denied
29 | } else {
30 | return AuthorizeStatus.NotApplied
31 | }
32 | }
33 |
34 | /**
35 | * 静默检测授权状态
36 | *
37 | * @param scope 权限名称
38 | *
39 | * `scope` 列表
40 | * @see https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/authorize.html
41 | */
42 | export async function getAuthorizeStatus(scope: keyof AuthSetting): Promise {
43 | const res = await wx.getSetting()
44 | return checkAuthSetting(scope, res.authSetting)
45 | }
46 |
47 | /**
48 | * 打开设置页然后返回再检查是否获取授权
49 | *
50 | * @param scope 权限名称
51 | */
52 | export async function openSettingAndCheck(scope: keyof AuthSetting): Promise {
53 | const res = await wx.openSetting()
54 | return checkAuthSetting(scope, res.authSetting)
55 | }
56 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/living/living.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
9 |
10 | {{ item.name }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | {{ daily.optimalDayOfWeek }}
20 | {{ daily.formattedDate }}
21 |
22 |
23 | {{ daily.category }}
24 | {{ daily.text }}
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/miniprogram/pages/great-day/edit/edit.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ icon }}
6 |
7 |
8 |
9 |
10 |
11 |
19 |
20 |
27 |
28 | 日期
29 |
30 | {{ formattedDate || '请选择日期' }}
31 |
32 |
33 |
34 |
35 |
36 |
45 |
46 |
--------------------------------------------------------------------------------
/miniprogram/pages/me/me.ts:
--------------------------------------------------------------------------------
1 | // pages/me/me.ts
2 |
3 | import {getVersion} from '../../app/core/system'
4 | import {getUserInfo, UserInfo} from '../../app/services/userinfo'
5 | import {shareAppBehavior} from '../../behaviors/share-app-behavior'
6 | import {themeBehavior} from '../../behaviors/theme-behavior'
7 |
8 | Page({
9 | /**
10 | * 页面的初始数据
11 | */
12 | data: {
13 | /** 用户资料 */
14 | userInfo: {} as UserInfo,
15 |
16 | /** 当前的版本号 */
17 | version: '',
18 | },
19 |
20 | behaviors: [themeBehavior, shareAppBehavior],
21 |
22 | /**
23 | * 页面初始化方法
24 | */
25 | async init() {
26 | const userInfo = await getUserInfo()
27 | const version = getVersion()
28 | this.setData({userInfo, version})
29 | },
30 |
31 | onShow() {
32 | this.init()
33 | },
34 |
35 | /**
36 | * 页面相关事件处理函数--监听用户下拉动作
37 | */
38 | onPullDownRefresh() {
39 | this.init()
40 | },
41 |
42 | /** 监听头像的图片加载完成 */
43 | onAvatarLoad() {
44 | this.animate(
45 | '.avatar',
46 | [
47 | {offset: 0, scale: [1], rotateX: 0},
48 | {offset: 0.5, scale: [0.9], rotateX: 180},
49 | {offset: 1, scale: [1], rotateX: 0},
50 | ],
51 | 700,
52 | () => {
53 | this.clearAnimation('.avatar', {scale: true})
54 | }
55 | )
56 | },
57 |
58 | /** 跳转到【个人信息】页面 */
59 | goToUserInfoPage() {
60 | wx.navigateTo({url: '/pages/user/user-info/user-info'})
61 | },
62 |
63 | /** 跳转到版本信息页 */
64 | goToVersionPage() {
65 | wx.navigateTo({url: '/pages/about/main/main'})
66 | },
67 | })
68 |
--------------------------------------------------------------------------------
/miniprogram/theme.json:
--------------------------------------------------------------------------------
1 | {
2 | "light": {
3 | "navigationBarBackgroundColor": "#ededed",
4 | "navigationBarTextStyle": "black",
5 | "backgroundColor": "#ededed",
6 | "backgroundTextStyle": "light",
7 | "backgroundColorTop": "#ededed",
8 | "backgroundColorBottom": "#ededed",
9 | "tabBarColor": "#000",
10 | "tabBarBackgroundColor": "#f7f7f7",
11 | "tabBarBorderStyle": "black",
12 | "tabBarIconPath_Home": "/assets/images/tabbar/home-outline-light.png",
13 | "tabBarIconPath_Robot": "/assets/images/tabbar/robot-outline-light.png",
14 | "tabBarIconPath_Calendar": "/assets/images/tabbar/calendar-outline-light.png",
15 | "tabBarIconPath_Album": "/assets/images/tabbar/album-outline-light.png",
16 | "tabBarIconPath_Me": "/assets/images/tabbar/me-outline-light.png"
17 | },
18 | "dark": {
19 | "navigationBarBackgroundColor": "#111",
20 | "navigationBarTextStyle": "white",
21 | "backgroundColor": "#111",
22 | "backgroundTextStyle": "dark",
23 | "backgroundColorTop": "#111",
24 | "backgroundColorBottom": "#111",
25 | "tabBarColor": "#fff",
26 | "tabBarBackgroundColor": "#1e1e1e",
27 | "tabBarBorderStyle": "white",
28 | "tabBarIconPath_Home": "/assets/images/tabbar/home-outline-dark.png",
29 | "tabBarIconPath_Robot": "/assets/images/tabbar/robot-outline-dark.png",
30 | "tabBarIconPath_Calendar": "/assets/images/tabbar/calendar-outline-dark.png",
31 | "tabBarIconPath_Album": "/assets/images/tabbar/album-outline-dark.png",
32 | "tabBarIconPath_Me": "/assets/images/tabbar/me-outline-dark.png"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/miniprogram/pages/ai/text/text.scss:
--------------------------------------------------------------------------------
1 | /* pages/ai/text/text.wxss */
2 |
3 | .page {
4 | .list {
5 | padding: 30rpx 0 100rpx;
6 |
7 | .item {
8 | display: flex;
9 | margin-bottom: 30rpx;
10 |
11 | .logo {
12 | width: 60rpx;
13 | margin-left: 20rpx;
14 | border-radius: 8rpx;
15 | }
16 |
17 | .avatar {
18 | font-size: 36rpx;
19 | display: flex;
20 | align-items: center;
21 | justify-content: center;
22 | width: 60rpx;
23 | height: 60rpx;
24 | margin-left: 20rpx;
25 | color: #fff;
26 | border-radius: 8rpx;
27 | border-radius: 8rpx;
28 | background-color: var(--weui-BLUE);
29 | }
30 |
31 | .text {
32 | max-width: 600rpx;
33 | margin-left: 20rpx;
34 | padding: 20rpx;
35 | border-radius: 12rpx;
36 | background-color: var(--weui-BG-2);
37 | }
38 | }
39 | }
40 |
41 | .operate {
42 | position: fixed;
43 | bottom: 0;
44 | left: 0;
45 | display: flex;
46 | align-items: center;
47 | width: 100%;
48 | height: 100rpx;
49 | padding: 0 15rpx;
50 | border-top: 2rpx solid var(--weui-BG-5);
51 | background-color: var(--weui-BG-1);
52 |
53 | .input {
54 | flex: 1;
55 | height: 70rpx;
56 | padding: 0 24rpx;
57 | border-radius: 4rpx;
58 | background-color: var(--weui-BG-2);
59 | }
60 |
61 | .button {
62 | line-height: 70rpx;
63 | height: 70rpx;
64 | margin-left: 20rpx;
65 | text-align: center;
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/miniprogram/app/core/auth.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 权限相关方法
3 | * @date 2022-02-09
4 | */
5 |
6 | import {StorageField} from './constant'
7 | import {requestForData} from './http'
8 | import {storage, Storage} from './storage'
9 |
10 | /**
11 | * 获取微信 code
12 | *
13 | * ## 说明
14 | * 1. 由于微信新增的特色机制,无法按需获取 `code`,因此对获取的 `code` 增加了一层缓存机制(服务端也配套做了缓存)
15 | * 2. [接口调用频率规范](https://developers.weixin.qq.com/miniprogram/dev/framework/performance/api-frequency.html)
16 | */
17 | export function getCode(): Promise {
18 | return new Promise((resolve, reject) => {
19 | const code = storage.get(StorageField.CODE)
20 | if (code) {
21 | resolve(code)
22 | return
23 | }
24 |
25 | wx.login({
26 | success(res) {
27 | resolve(res.code)
28 | storage.set(StorageField.CODE, res.code, Storage.ofMinutes(5))
29 | },
30 | fail(res) {
31 | reject(new Error(res.errMsg))
32 | },
33 | })
34 | })
35 | }
36 |
37 | /** 登录接口响应数据 */
38 | export interface SecurityToken {
39 | /** 鉴权令牌 */
40 | token: string
41 |
42 | /** 权令牌类型 */
43 | type: string
44 |
45 | /** 发起请求时,携带令牌的请求头名称 */
46 | headerName: string
47 |
48 | /** 创建时间 */
49 | createTime: string
50 |
51 | /** 过期时间 */
52 | expireTime: string
53 | }
54 |
55 | /**
56 | * 登录
57 | */
58 | export async function login(): Promise {
59 | const code = await getCode()
60 | const data = await requestForData({
61 | method: 'POST',
62 | url: '/login/wechat',
63 | data: {code, appId: 'wx09c0a1ea5251c75a'},
64 | auth: false,
65 | })
66 |
67 | storage.set(StorageField.TOKEN, data, data.expireTime)
68 |
69 | return data
70 | }
71 |
--------------------------------------------------------------------------------
/miniprogram/app/services/oss.ts:
--------------------------------------------------------------------------------
1 | import {requestForData} from '../core/http'
2 |
3 | /** OSS 临时上传凭证 */
4 | export interface OssPostCredential {
5 | /** 客户端使用时参数改为 `OSSAccessKeyId` */
6 | accessKeyId: string
7 |
8 | url: string
9 |
10 | key: string
11 |
12 | policy: string
13 |
14 | signature: string
15 | }
16 |
17 | /**
18 | * 获取阿里云 OSS 直传凭证
19 | *
20 | * @see https://help.aliyun.com/document_detail/92883.html
21 | *
22 | * @param type 凭证类型:`image` => 图片, `video` => 视频
23 | */
24 | export function getOssPostCredential(type: string): Promise {
25 | return requestForData({
26 | method: 'GET',
27 | url: '/oss/credential',
28 | auth: false,
29 | params: {type},
30 | })
31 | }
32 |
33 | /**
34 | * 将资源直传至 OSS
35 | *
36 | * @param tempFilePath 调用接口获取的本地临时文件路径
37 | * @param credential 阿里云 OSS 直传凭证
38 | */
39 | export function uploadToOss(tempFilePath: string, credential: OssPostCredential): WechatMiniprogram.UploadTask {
40 | return wx.uploadFile({
41 | filePath: tempFilePath,
42 | url: credential.url,
43 | name: 'file',
44 |
45 | formData: {
46 | key: credential.key,
47 | policy: credential.policy,
48 | signature: credential.signature,
49 | OSSAccessKeyId: credential.accessKeyId,
50 | },
51 | })
52 | }
53 |
54 | /**
55 | * 不包含上传进度的上传方法
56 | */
57 | export function uploadToOssWithoutProgress(tempFilePath: string, credential: OssPostCredential) {
58 | return new Promise((resolve) => {
59 | const task = uploadToOss(tempFilePath, credential)
60 | task.onProgressUpdate((listener) => {
61 | if (listener.progress === 100) {
62 | resolve(true)
63 | }
64 | })
65 | })
66 | }
67 |
--------------------------------------------------------------------------------
/miniprogram/pages/great-day/list/list.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ item.icon }}
10 |
11 | {{ item.name }}
12 |
13 | 距离
14 | {{ item.formattedDate }}
15 | 还有
16 |
17 |
18 | {{ item.formattedDate }}
19 | 已经过去
20 |
21 |
22 |
23 |
24 | {{ item.daysAbs }}
25 | 天
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 暂无记录
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/nick-name/nick-name.ts:
--------------------------------------------------------------------------------
1 | // pages/user/nick-name/nick-name.ts
2 |
3 | import {getUserInfo, updateNickName} from '../../../app/services/userinfo'
4 | import {themeBehavior} from '../../../behaviors/theme-behavior'
5 |
6 | Page({
7 | data: {
8 | /** 当前使用的昵称 */
9 | currentNickName: '',
10 |
11 | /** 输入框中输入的昵称 */
12 | inputNickName: '',
13 |
14 | // ================================ 页面状态数据 ================================
15 |
16 | /** 提交按钮是否禁用 */
17 | submitButtonDisabled: false,
18 |
19 | /** 提交按钮是否带 loading 图标 */
20 | submitButtonLoading: false,
21 | },
22 |
23 | behaviors: [themeBehavior],
24 |
25 | /** 页面初始化方法 */
26 | async init() {
27 | const userInfo = await getUserInfo()
28 | this.setData({currentNickName: userInfo.nickName, inputNickName: userInfo.nickName})
29 | },
30 |
31 | onLoad() {
32 | this.init()
33 | },
34 |
35 | /** 处理键盘输入事件 */
36 | handleInput(e: WechatMiniprogram.CustomEvent<{value: string}>) {
37 | this.setData({inputNickName: e.detail.value})
38 | },
39 |
40 | /** 提交修改昵称 */
41 | async submit() {
42 | const nickName = this.data.inputNickName
43 | this.setData({
44 | submitButtonDisabled: true,
45 | submitButtonLoading: true,
46 | })
47 |
48 | if (nickName === this.data.currentNickName) {
49 | wx.navigateBack()
50 | } else {
51 | await updateNickName(nickName)
52 |
53 | // 设置成功之后的提示
54 | this.setData({
55 | submitButtonDisabled: false,
56 | submitButtonLoading: false,
57 | })
58 |
59 | wx.showToast({
60 | title: '设置成功',
61 | icon: 'success',
62 | mask: true,
63 | duration: 1500,
64 | })
65 |
66 | setTimeout(() => {
67 | wx.navigateBack()
68 | }, 1500)
69 | }
70 | },
71 | })
72 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/living/living.ts:
--------------------------------------------------------------------------------
1 | // pages/weather/living/living.ts
2 | import {themeBehavior} from '../../../behaviors/theme-behavior'
3 | import {LivingIndex} from '../../../app/services/weather-data'
4 | import {PageChannelEvent} from '../../../app/core/constant'
5 | import {TapEvent} from '../../../app/utils/types'
6 |
7 | Page({
8 | data: {
9 | // ----------------------------- 从上个页面带来的数据 -----------------------------
10 |
11 | /** 生活指数列表 */
12 | indices: [] as LivingIndex[],
13 |
14 | /** 首要展示的指数类型,即默认选中该项 */
15 | type: '',
16 |
17 | // ------------------------------ 页面状态管理数据 ------------------------------
18 |
19 | /** 当前活跃索引 */
20 | activeIndex: 0,
21 | },
22 |
23 | behaviors: [themeBehavior],
24 |
25 | onLoad() {
26 | // 获取从上个页面的传值
27 | const eventChannel = this.getOpenerEventChannel()
28 | if (eventChannel) {
29 | eventChannel.on(PageChannelEvent.DATA_TRANSFER, (data) => {
30 | this.setData(data)
31 | })
32 | }
33 |
34 | this.init()
35 | },
36 |
37 | /** 页面初始化 */
38 | init() {
39 | const activeType = this.data.type ?? ''
40 |
41 | this.setActiveIndex(activeType)
42 | },
43 |
44 | /**
45 | * 处理顶部区域点击事件
46 | */
47 | handleHeadItemTap(e: TapEvent<{type: string}>) {
48 | const type = e.currentTarget.dataset.type
49 | this.setActiveIndex(type)
50 | },
51 |
52 | /**
53 | * 处理滑块切换事件
54 | */
55 | handleSwiperItemChange(e: WechatMiniprogram.SwiperChange) {
56 | if (e.detail.source === 'touch') {
57 | this.setData({activeIndex: e.detail.current})
58 | }
59 | },
60 |
61 | /**
62 | * 通过指数类型找到它在列表中的索引(如果未找到则返回 0),然后定义活跃索引
63 | *
64 | * @param type 指数类型
65 | */
66 | setActiveIndex(type: string): void {
67 | const index = this.data.indices.findIndex((item) => item.type === type)
68 | const activeIndex = index > 0 ? index : 0
69 | this.setData({activeIndex})
70 | },
71 | })
72 |
--------------------------------------------------------------------------------
/project.private.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "setting": {
3 | "compileHotReLoad": false,
4 | "urlCheck": false
5 | },
6 | "condition": {
7 | "miniprogram": {
8 | "list": [
9 | {
10 | "name": "测试页",
11 | "pathName": "pages/test/test",
12 | "query": "id=11&name=mark&good=true",
13 | "launchMode": "default",
14 | "scene": null
15 | },
16 | {
17 | "name": "网络异常页",
18 | "pathName": "pages/system/error/error",
19 | "query": "",
20 | "launchMode": "default",
21 | "scene": null
22 | },
23 | {
24 | "name": "纪念日(编辑)",
25 | "pathName": "pages/great-day/edit/edit",
26 | "query": "",
27 | "launchMode": "default",
28 | "scene": null
29 | },
30 | {
31 | "name": "纪念日(列表)",
32 | "pathName": "pages/great-day/list/list",
33 | "query": "",
34 | "launchMode": "default",
35 | "scene": null
36 | },
37 | {
38 | "name": "",
39 | "pathName": "pages/great-day/detail/detail",
40 | "query": "id=109bc54bdc3f4e60b2c91ae8083acc11",
41 | "launchMode": "default",
42 | "scene": null
43 | },
44 | {
45 | "name": "扫码登录页",
46 | "pathName": "pages/scan/login",
47 | "query": "scene=eeeeeeee",
48 | "launchMode": "default",
49 | "scene": null
50 | },
51 | {
52 | "name": "会话",
53 | "pathName": "pages/ai/text/text",
54 | "query": "",
55 | "launchMode": "default",
56 | "scene": null
57 | }
58 | ]
59 | }
60 | },
61 | "projectname": "life-helper-miniprogram",
62 | "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
63 | "libVersion": "2.29.1"
64 | }
--------------------------------------------------------------------------------
/miniprogram/pages/user/user-info/user-info.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
31 |
37 |
38 |
39 |
40 |
41 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/miniprogram/pages/ai/text/text.ts:
--------------------------------------------------------------------------------
1 | import {AiChatMessage, appendMessage, createAiChat} from './../../../app/services/ai'
2 | import {showSingleButtonModel} from '../../../app/utils/wx'
3 | import {themeBehavior} from '../../../behaviors/theme-behavior'
4 |
5 | Page({
6 | /**
7 | * 页面的初始数据
8 | */
9 | data: {
10 | id: '',
11 | messages: [] as AiChatMessage[],
12 |
13 | /** 用户输入的消息内容 */
14 | inputValue: '',
15 |
16 | /** 用户输入的消息内容 */
17 | prompt: '',
18 |
19 | /** 发送按钮禁用状态 */
20 | buttonDisabled: false,
21 |
22 | /** 是否等待中 */
23 | loading: false,
24 |
25 | logoUrl: 'https://static.lifehelper.com.cn/static/project/logo.png',
26 | },
27 |
28 | behaviors: [themeBehavior],
29 |
30 | /**
31 | * 生命周期函数--监听页面加载
32 | */
33 | async onLoad() {
34 | const {id, messages} = await createAiChat()
35 | this.setData({id, messages})
36 | },
37 |
38 | handleInput(e: any) {
39 | const prompt = e.detail.value
40 | this.setData({prompt})
41 | },
42 |
43 | resetInput() {
44 | this.setData({inputValue: ''})
45 | },
46 |
47 | /** 发送消息 */
48 | async send() {
49 | const {prompt, messages, id} = this.data
50 |
51 | if (!prompt) {
52 | showSingleButtonModel('你没有输入内容哦')
53 | return
54 | }
55 |
56 | // 定时器:防止出现异常,按钮一直禁用
57 | setTimeout(() => {
58 | if (this.data.buttonDisabled) {
59 | this.setData({buttonDisabled: false, loading: false})
60 | }
61 | }, 30000)
62 |
63 | messages.push({role: 'user', content: prompt, id: ''} as AiChatMessage)
64 |
65 | this.setData({buttonDisabled: true, loading: true, messages, prompt: ''})
66 | this.resetInput()
67 | wx.pageScrollTo({scrollTop: 99999})
68 |
69 | const aiChat = await appendMessage(id, prompt)
70 |
71 | // 这里可能报一个 504 错误,暂时还不想处理
72 | if (aiChat && aiChat.messages) {
73 | this.setData({buttonDisabled: false, messages: aiChat.messages, loading: false})
74 | wx.pageScrollTo({scrollTop: 99999})
75 | }
76 | },
77 | })
78 |
--------------------------------------------------------------------------------
/project.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "项目配置文件,详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
3 | "packOptions": {
4 | "ignore": [],
5 | "include": []
6 | },
7 | "miniprogramRoot": "miniprogram/",
8 | "compileType": "miniprogram",
9 | "libVersion": "2.20.1",
10 | "projectname": "life-helper-miniprogram",
11 | "setting": {
12 | "urlCheck": false,
13 | "es6": true,
14 | "enhance": true,
15 | "postcss": true,
16 | "preloadBackgroundData": false,
17 | "minified": true,
18 | "newFeature": false,
19 | "coverView": true,
20 | "nodeModules": true,
21 | "autoAudits": false,
22 | "showShadowRootInWxmlPanel": true,
23 | "scopeDataCheck": false,
24 | "uglifyFileName": false,
25 | "checkInvalidKey": true,
26 | "checkSiteMap": true,
27 | "uploadWithSourceMap": true,
28 | "compileHotReLoad": false,
29 | "lazyloadPlaceholderEnable": false,
30 | "useMultiFrameRuntime": true,
31 | "useApiHook": true,
32 | "useApiHostProcess": true,
33 | "babelSetting": {
34 | "ignore": [],
35 | "disablePlugins": [],
36 | "outputPath": ""
37 | },
38 | "useIsolateContext": false,
39 | "userConfirmedBundleSwitch": false,
40 | "packNpmManually": true,
41 | "packNpmRelationList": [
42 | {
43 | "packageJsonPath": "./package.json",
44 | "miniprogramNpmDistDir": "./miniprogram/"
45 | }
46 | ],
47 | "minifyWXSS": true,
48 | "disableUseStrict": false,
49 | "minifyWXML": true,
50 | "showES6CompileOption": false,
51 | "useCompilerPlugins": [
52 | "typescript",
53 | "sass"
54 | ],
55 | "ignoreDevUnusedFiles": false,
56 | "ignoreUploadUnusedFiles": true,
57 | "useStaticServer": true,
58 | "condition": false
59 | },
60 | "simulatorType": "wechat",
61 | "simulatorPluginLibVersion": {},
62 | "appid": "wx09c0a1ea5251c75a",
63 | "condition": {},
64 | "srcMiniprogramRoot": "miniprogram/",
65 | "editorSetting": {
66 | "tabIndent": "insertSpaces",
67 | "tabSize": 2
68 | }
69 | }
--------------------------------------------------------------------------------
/miniprogram/pages/weather/living/living.scss:
--------------------------------------------------------------------------------
1 | /* pages/weather/living/living.wxss */
2 |
3 | @import "../../../assets/styles/weather.scss";
4 |
5 | .empty {
6 | height: 24rpx;
7 | background-color: var(--weui-BG-0);
8 | }
9 |
10 | .head {
11 | display: flex;
12 | justify-content: space-between;
13 | height: 200rpx;
14 | margin: 0 20rpx;
15 | padding: 10rpx 20rpx;
16 | border-radius: 20rpx;
17 | background-color: var(--weui-BG-2);
18 |
19 | .item {
20 | display: flex;
21 | align-items: center;
22 | flex-direction: column;
23 | justify-content: center;
24 | padding-bottom: 12rpx;
25 |
26 | .image {
27 | width: 160rpx;
28 | }
29 |
30 | .name {
31 | font-size: 28rpx;
32 | color: var(--weui-FG-1);
33 | }
34 | }
35 |
36 | .active {
37 | color: var(--weui-GREEN);
38 | border: 4rpx solid var(--weui-BRAND);
39 | border-radius: 10rpx;
40 | background-color: var(--weui-FG-3);
41 | }
42 | }
43 |
44 | .body {
45 | height: 740rpx;
46 | padding: 20rpx;
47 |
48 |
49 | .item {
50 | height: 800rpx;
51 | border-radius: 10rpx;
52 | background-color: var(--weui-BG-2);
53 |
54 | // 一天的信息
55 | .daily {
56 | display: flex;
57 | justify-content: space-between;
58 | height: 220rpx;
59 | margin: 20rpx;
60 | padding: 16rpx;
61 | background-color: var(--weui-BG-3);
62 |
63 | .left {
64 | display: flex;
65 | align-items: center;
66 | flex-direction: column;
67 | justify-content: center;
68 | width: 200rpx;
69 |
70 | .weekday {
71 | font-size: 30rpx;
72 | }
73 |
74 | .date {
75 | font-size: 28rpx;
76 | color: var(--weui-FG-1);
77 | }
78 | }
79 |
80 | .right {
81 | display: flex;
82 | flex: 1;
83 | flex-direction: column;
84 | justify-content: center;
85 |
86 | .text {
87 | font-size: 26rpx;
88 | margin-top: 28rpx;
89 | padding-right: 10rpx;
90 | }
91 |
92 | }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/daily/daily.ts:
--------------------------------------------------------------------------------
1 | import {TapEvent} from '../../../app/utils/types'
2 | import {themeBehavior} from '../../../behaviors/theme-behavior'
3 | import {WeatherDaily} from '../../../app/services/weather-data'
4 | import {PageChannelEvent} from '../../../app/core/constant'
5 |
6 | // pages/weather/daily/daily.ts
7 | Page({
8 | data: {
9 | // ----------------------------- 从上个页面带来的数据 -----------------------------
10 |
11 | /** 未来15天预报数据 */
12 | daily: [] as WeatherDaily[],
13 |
14 | /** 选中的日期 */
15 | date: '',
16 |
17 | /** 地点名称 */
18 | locationName: '',
19 |
20 | // ------------------------------- 页面状态管理数据 -------------------------------
21 |
22 | /** 当前展示列表的索引 */
23 | currentIndex: 0,
24 |
25 | /** 置于最前面的滚动视图项目 */
26 | activeScrollItemId: 's-0',
27 | },
28 |
29 | behaviors: [themeBehavior],
30 |
31 | onLoad() {
32 | const eventChannel = this.getOpenerEventChannel()
33 | if (eventChannel) {
34 | eventChannel.on(PageChannelEvent.DATA_TRANSFER, (data) => {
35 | this.setData(data)
36 | })
37 | }
38 | },
39 |
40 | onReady() {
41 | const title = this.data.locationName
42 | wx.setNavigationBarTitle({title})
43 |
44 | const index = this.data.daily.findIndex((item: WeatherDaily) => item.date === this.data.date)
45 | this.show(index)
46 | },
47 |
48 | /** 处理滚动条点击事件 */
49 | handleScrollItemTap(event: TapEvent<{index: number}>) {
50 | const index = event.currentTarget.dataset.index
51 | this.show(index)
52 | },
53 |
54 | /** 处理滑块切换事件 */
55 | handleSwiperChange(event: any) {
56 | const {current, source} = event.detail
57 | if (source === 'touch') {
58 | this.show(current)
59 | }
60 | },
61 |
62 | /** 展示指定索引项目 */
63 | show(index: number) {
64 | if (index === 0 || index === 1) {
65 | this.setData({
66 | currentIndex: index,
67 | activeScrollItemId: 's-0',
68 | })
69 | } else {
70 | this.setData({
71 | currentIndex: index,
72 | activeScrollItemId: `s-${index - 2}`,
73 | })
74 | }
75 | },
76 | })
77 |
--------------------------------------------------------------------------------
/miniprogram/pages/user/user-info/user-info.ts:
--------------------------------------------------------------------------------
1 | import {getUserInfo, updateGenderType, updateRegion, UserInfo} from '../../../app/services/userinfo'
2 | import {themeBehavior} from '../../../behaviors/theme-behavior'
3 |
4 | // pages/user/user-info/user-info.ts
5 | Page({
6 | data: {
7 | /** 用户资料 */
8 | userInfo: {} as UserInfo,
9 |
10 | // ================================ 页面状态数据 ================================
11 | genders: ['男', '女'],
12 | },
13 |
14 | behaviors: [themeBehavior],
15 |
16 | /** 初始化方法 */
17 | async init() {
18 | const userInfo = await getUserInfo()
19 | this.setData({userInfo})
20 | },
21 |
22 | // 由于这几个页面经常往返跳转,因此简化处理,每次展示页面都直接刷新数据(从缓存中读取)
23 | onShow() {
24 | this.init()
25 | },
26 |
27 | /** 处理性别选择器改变事件 */
28 | handleGenderChange(e: WechatMiniprogram.CustomEvent<{value: string}>) {
29 | const index = parseInt(e.detail.value)
30 |
31 | // 性别直接在当前页面修改显示了,不等响应返回再改
32 | const gender = this.data.genders[index]
33 | const userInfo = this.data.userInfo
34 | userInfo.gender = gender
35 | this.setData({userInfo})
36 |
37 | // 性别类型:1-男,2-女
38 | const genderType = index + 1
39 | updateGenderType(genderType)
40 | },
41 |
42 | /** 处理地区选择器改变事件 */
43 | async handleRegionChange(e: WechatMiniprogram.CustomEvent<{code: string[]; value: string[]}>) {
44 | let cityId
45 |
46 | if (e.detail.value[0].indexOf('市') != -1) {
47 | cityId = parseInt(e.detail.code[0])
48 | } else {
49 | cityId = parseInt(e.detail.code[1])
50 | }
51 |
52 | if (cityId > 0) {
53 | await updateRegion(cityId)
54 | this.init()
55 | }
56 | },
57 |
58 | /** 跳转到【头像预览】页 */
59 | goToAvatarPage() {
60 | wx.navigateTo({url: '/pages/user/avatar/avatar'})
61 | },
62 |
63 | /** 跳转到【设置昵称】页 */
64 | goToNickNamePage() {
65 | wx.navigateTo({url: '/pages/user/nick-name/nick-name'})
66 | },
67 |
68 | /** 跳转到【账户 ID】页 */
69 | goToAccountIdPage() {
70 | wx.navigateTo({url: '/pages/user/account-id/account-id'})
71 | },
72 |
73 | /** 跳转到【账户 ID】页 */
74 | goToRegisterTime() {
75 | wx.navigateTo({url: '/pages/user/register-time/register-time'})
76 | },
77 | })
78 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 |
小鸣助手
6 |
让生活更简单一些
7 |
8 |
9 | ## 🤓 项目介绍
10 |
11 | 「小鸣助手」是一个生活服务类小程序,主要为用户的日常生活提供一些便捷工具,例如天气查询、时间规划、生活记录等。目前该小程序已稳定运行近 4 年,为近 10 万用户提供了生活帮助。
12 |
13 | 读者可直接扫描以下小程序码进行体验:
14 |
15 | 
16 |
17 | ## 💡 项目特点
18 |
19 | 1. 线上正式运行的项目,不是 demo,历经 4 年,久经用户考验。
20 | 2. 开发尽量遵照业界最佳实践,可作为学习样板。
21 | 3. 跟随版本更新,包含 Java、Spring Boot 等,尽量使用**最新稳定版**,保持技术栈不落后。
22 |
23 | ## 🍱 源码仓库
24 |
25 | 笔者在开发项目时,遵照了业界最佳实践,读者可通过研读项目源码来学习相关技术栈。按照功能分类,将项目拆分为了 4 个代码仓库,分别为:
26 |
27 | | 仓库 | 定位 | 技术栈 |
28 | | --------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------ |
29 | | [life-helper-server](https://github.com/inlym/life-helper-server) | 服务端 | `Spring Boot` + `Spring Security` + `JWT` + `MyBatis` + `MySQL` + `Redis` + `Docker` |
30 | | [life-helper-backend](https://github.com/inlym/life-helper-backend) | 服务端 | `Node.js` + `Nest.js` + `TypeScript` + `Typeorm` + `MySQL` + `Redis` + `Docker` |
31 | | [life-helper-miniprogram](https://github.com/inlym/life-helper-miniprogram) | 小程序端 | `TypeScript` + `Scss` |
32 | | [life-helper-web](https://github.com/inlym/life-helper-web) | Web 端 | `Angular` + `TypeScript` + `Scss` + `RxJS` + `Webpack` |
33 |
34 | ## ❓ 常见问题
35 |
36 | | 序号 | 问题 |
37 | | ---- | ------------------------------------------------------------- |
38 | | 1 | [如何启动项目?](https://github.com/inlym/life-helper-server) |
39 |
40 | ## 📞 交流沟通
41 |
42 | 如果你在使用「小鸣助手」小程序过程中,遇到任何问题,或者有任何意见建议,你可以通过以下方式联系我:
43 |
44 | - [x] 邮箱:_inlym@qq.com_
45 | - [x] 公众号:搜索公众号「**1 鸣的写字台**」或微信号 `iam1ming`。
46 |
47 | ## 📄 许可证
48 |
49 | 本项目使用 [MIT](LICENSE) 许可证。
50 |
--------------------------------------------------------------------------------
/miniprogram/pages/great-day/detail/detail.scss:
--------------------------------------------------------------------------------
1 | /* pages/great-day/detail/detail.wxss */
2 |
3 | page,
4 | .page {
5 | background-color: var(--weui-BG-2);
6 | }
7 |
8 | .container {
9 | display: flex;
10 | align-items: center;
11 | flex-direction: column;
12 |
13 | padding: 120rpx 0 80rpx;
14 |
15 | .icon {
16 | font-size: 120rpx;
17 | }
18 |
19 | .name {
20 | font-size: 50rpx;
21 | font-weight: bold;
22 |
23 | margin-top: 60rpx;
24 | }
25 |
26 | .date {
27 | font-size: 28rpx;
28 |
29 | margin-top: 18rpx;
30 |
31 | color: var(--weui-FG-1);
32 | }
33 |
34 | .days-box {
35 | display: flex;
36 | align-items: center;
37 | justify-content: space-between;
38 |
39 | // width: 560rpx;
40 |
41 | height: 160rpx;
42 | margin-top: 80rpx;
43 | padding: 0 60rpx;
44 |
45 | border-radius: 40rpx;
46 | background-color: var(--weui-BG-0);
47 |
48 | .text {
49 | font-size: 32rpx;
50 |
51 | color: var(--weui-FG-1);
52 | }
53 |
54 | .days {
55 | font-size: 80rpx;
56 | font-weight: bold;
57 |
58 | margin: 0 40rpx;
59 | }
60 |
61 | .positive {
62 | color: var(--weui-TEXTGREEN);
63 | }
64 |
65 | .negative {
66 | color: var(--weui-ORANGE);
67 | }
68 | }
69 | }
70 |
71 | .button-area {
72 | position: fixed;
73 | bottom: 0rpx;
74 |
75 | padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
76 |
77 | .buttons {
78 | display: flex;
79 | align-items: center;
80 | justify-content: space-between;
81 |
82 | width: 750rpx;
83 | padding: 0 100rpx;
84 |
85 | .delete {
86 | color: var(--weui-RED) !important;
87 | }
88 |
89 | .item {
90 | display: flex;
91 | align-items: center;
92 | flex-direction: column;
93 | justify-content: space-between;
94 |
95 | width: 100rpx;
96 | height: 120rpx;
97 |
98 | color: var(--weui-FG-1);
99 |
100 | .iconfont {
101 | font-size: 56rpx;
102 | }
103 |
104 | .text {
105 | font-size: 28rpx;
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/miniprogram/pages/great-day/list/list.scss:
--------------------------------------------------------------------------------
1 | /* pages/great-day/list/list.wxss */
2 |
3 | .unnecessary-space {
4 | height: 1rpx;
5 | }
6 |
7 | // 悬浮固定的“+”按钮
8 | .add {
9 | position: fixed;
10 | right: 30rpx;
11 | bottom: 100rpx;
12 |
13 | display: flex;
14 | align-items: center;
15 | justify-content: center;
16 |
17 | width: 100rpx;
18 | height: 100rpx;
19 |
20 | border-radius: 50%;
21 | background-color: var(--weui-BG-1);
22 |
23 | .iconfont {
24 | font-size: 80rpx;
25 |
26 | color: var(--weui-BRAND);
27 | }
28 | }
29 |
30 | .list {
31 | padding: 0 20rpx;
32 |
33 | .item {
34 | display: flex;
35 | align-items: center;
36 | justify-content: space-between;
37 |
38 | height: 150rpx;
39 | margin-top: 20rpx;
40 | padding: 0 30rpx;
41 |
42 | border-radius: 20rpx;
43 | background-color: var(--weui-BG-2);
44 |
45 | .icon {
46 | font-size: 60rpx;
47 | }
48 |
49 | .text {
50 | margin-left: 30rpx;
51 |
52 | .name {
53 | font-size: 32rpx;
54 |
55 | max-width: 420rpx;
56 | }
57 |
58 | .desc {
59 | font-size: 24rpx;
60 |
61 | margin-top: 16rpx;
62 |
63 | color: var(--weui-FG-1);
64 |
65 | .date {
66 | color: var(--weui-PURPLE);
67 | }
68 | }
69 | }
70 |
71 | .space {
72 | flex: 1;
73 | }
74 |
75 | .days {
76 | display: flex;
77 | align-items: flex-end;
78 |
79 | margin-right: 10rpx;
80 |
81 | .num {
82 | font-size: 60rpx;
83 | font-weight: bold;
84 | }
85 |
86 | .positive {
87 | color: var(--weui-TEXTGREEN);
88 | }
89 |
90 | .negative {
91 | color: var(--weui-ORANGE);
92 | }
93 |
94 | .text {
95 | font-size: 26rpx;
96 |
97 | margin-bottom: 14rpx;
98 | margin-left: 8rpx;
99 |
100 | color: var(--weui-FG-1);
101 | }
102 | }
103 | }
104 | }
105 |
106 | .empty-list {
107 | display: flex;
108 | align-items: center;
109 | flex-direction: column;
110 |
111 | padding-top: 100rpx;
112 |
113 | color: var(--weui-FG-1);
114 |
115 | .iconfont {
116 | font-size: 60rpx;
117 | }
118 |
119 | .desc {
120 | font-size: 28rpx;
121 |
122 | margin-top: 20rpx;
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/miniprogram/pages/album/detail/detail.scss:
--------------------------------------------------------------------------------
1 | /* pages/album/detail/detail.wxss */
2 |
3 | .container {
4 | .content {
5 | padding: 0 18rpx;
6 | }
7 |
8 | // 按钮区
9 | .buttons {
10 | display: flex;
11 | align-items: center;
12 | justify-content: space-between;
13 |
14 | padding-top: 36rpx;
15 |
16 | .info {
17 | font-size: 22rpx;
18 |
19 | color: var(--weui-FG-1);
20 | }
21 |
22 | .placeholder {
23 | flex: 1;
24 | }
25 |
26 | .button {
27 | font-size: 26rpx;
28 |
29 | display: flex;
30 | align-items: center;
31 | justify-content: space-between;
32 |
33 | height: 60rpx;
34 | margin-left: 16rpx;
35 | padding: 0 20rpx;
36 |
37 | color: var(--weui-FG-1);
38 | border-radius: 30rpx;
39 | background-color: var(--weui-BG-3);
40 |
41 | .icon {
42 | width: 28rpx;
43 | }
44 |
45 | .title {
46 | margin-left: 10rpx;
47 | }
48 | }
49 |
50 | .button:first-child {
51 | margin-left: 0;
52 | }
53 | }
54 |
55 | .list {
56 | display: flex;
57 | flex-wrap: wrap;
58 | justify-content: flex-start;
59 |
60 | margin-top: 22rpx;
61 |
62 | .item {
63 | overflow: hidden;
64 |
65 | margin: 10rpx 0 0 8rpx;
66 |
67 | border-radius: 8rpx;
68 |
69 | .image {
70 | position: relative;
71 |
72 | width: 170rpx;
73 | height: 170rpx;
74 |
75 | .icon {
76 | position: absolute;
77 | z-index: 10;
78 | top: calc(50% - 30px);
79 | left: calc(50% - 30px);
80 | }
81 | }
82 | }
83 | }
84 | }
85 |
86 | // 空列表情况
87 | .empty-list {
88 | display: flex;
89 | align-items: center;
90 | flex-direction: column;
91 | justify-content: center;
92 |
93 | padding-bottom: 80rpx;
94 |
95 | background-color: var(--weui-BG-2);
96 |
97 | .iconfont {
98 | font-size: 240rpx;
99 |
100 | margin-top: 200rpx;
101 |
102 | color: var(--weui-FG-2);
103 | }
104 |
105 | .tips {
106 | font-size: 28rpx;
107 |
108 | margin-top: 20rpx;
109 | }
110 |
111 | .add {
112 | margin-top: 60rpx;
113 | }
114 |
115 | .delete {
116 | font-size: 22rpx;
117 |
118 | margin-top: 300rpx;
119 |
120 | color: var(--weui-RED);
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/miniprogram/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": [
3 | "pages/index/index",
4 | "pages/test/test",
5 | "pages/scan/login",
6 | "pages/me/me",
7 | "pages/weather/warning/warning",
8 | "pages/weather/place/place",
9 | "pages/weather/daily/daily",
10 | "pages/weather/rain/rain",
11 | "pages/album/list/list",
12 | "pages/album/detail/detail",
13 | "pages/album/edit/edit",
14 | "pages/weather/living/living",
15 | "pages/user/user-info/user-info",
16 | "pages/system/error/error",
17 | "pages/user/avatar/avatar",
18 | "pages/user/nick-name/nick-name",
19 | "pages/user/account-id/account-id",
20 | "pages/user/register-time/register-time",
21 | "pages/about/main/main",
22 | "pages/great-day/detail/detail",
23 | "pages/great-day/edit/edit",
24 | "pages/great-day/list/list",
25 | "pages/ai/text/text"
26 | ],
27 | "entryPagePath": "pages/index/index",
28 | "sitemapLocation": "sitemap.json",
29 | "window": {
30 | "navigationBarBackgroundColor": "@navigationBarBackgroundColor",
31 | "navigationBarTextStyle": "@navigationBarTextStyle",
32 | "navigationBarTitleText": "小鸣助手",
33 | "backgroundColor": "@backgroundColor",
34 | "backgroundTextStyle": "@backgroundTextStyle",
35 | "backgroundColorTop": "@backgroundColorTop",
36 | "backgroundColorBottom": "@backgroundColorBottom",
37 | "enablePullDownRefresh": true
38 | },
39 | "tabBar": {
40 | "color": "@tabBarColor",
41 | "selectedColor": "#07c160",
42 | "backgroundColor": "@tabBarBackgroundColor",
43 | "borderStyle": "@tabBarBorderStyle",
44 | "list": [
45 | {
46 | "pagePath": "pages/index/index",
47 | "text": "天气",
48 | "iconPath": "@tabBarIconPath_Home",
49 | "selectedIconPath": "/assets/images/tabbar/home-filled.png"
50 | },
51 | {
52 | "pagePath": "pages/me/me",
53 | "text": "我",
54 | "iconPath": "@tabBarIconPath_Me",
55 | "selectedIconPath": "/assets/images/tabbar/me-filled.png"
56 | }
57 | ]
58 | },
59 | "style": "v2",
60 | "useExtendedLib": {
61 | "weui": true
62 | },
63 | "permission": {
64 | "scope.userLocation": {
65 | "desc": "你的位置信息将用于小程序位置接口的效果展示"
66 | }
67 | },
68 | "requiredPrivateInfos": ["chooseLocation"],
69 | "debug": false,
70 | "lazyCodeLoading": "requiredComponents",
71 | "darkmode": true,
72 | "themeLocation": "theme.json",
73 | "resizable": true
74 | }
75 |
--------------------------------------------------------------------------------
/miniprogram/app/http/loading-interceptor.ts:
--------------------------------------------------------------------------------
1 | import {AxiosRequestConfig, AxiosResponse} from 'axios'
2 | import {storage} from '../core/storage'
3 | import {ParamsUtils} from '../utils/params-utils'
4 | import {RequestOptionsInternal} from './types'
5 |
6 | /**
7 | * 静默时长,在该时间内结束请求则不弹 Loading
8 | */
9 | const SILENCE_DURATION = 500
10 |
11 | /**
12 | * 等待标志展示时长,超过该时间仍未结束请求则隐藏 Loading
13 | */
14 | const LOADING_DURATION = 2000
15 |
16 | /**
17 | * 请求状态
18 | */
19 | enum RequestStatus {
20 | /** 请求未完成 */
21 | UNFINISHED = 1,
22 |
23 | /** 请求已完成 */
24 | FINISHED = 2,
25 | }
26 |
27 | function getKey(config: AxiosRequestConfig): string {
28 | return `loading:${config.method?.toLowerCase()}:${config.url}:${ParamsUtils.encode(config.params)}`
29 | }
30 |
31 | /**
32 | * 等待标志展示拦截器
33 | *
34 | * ### 主要用途
35 | * 个别需要即时反馈的 API,如果请求时间较久,则展示 Loading 标志。
36 | *
37 | * ### 不需要 Loading 的情况
38 | * 1. API 在背后默默运行,不需要即时反馈。
39 | * 2. 请求等待状态被页面接管,需要反映在页面上。例如提交时,提交按钮一直转圈等待。
40 | */
41 | export function showLoadingInterceptor(config: AxiosRequestConfig): AxiosRequestConfig {
42 | const loading = (config as unknown as RequestOptionsInternal).loading
43 |
44 | if (loading) {
45 | const key = getKey(config)
46 | storage.set(key, RequestStatus.UNFINISHED, 60 * 1000)
47 |
48 | setTimeout(() => {
49 | if (storage.get(key) === RequestStatus.UNFINISHED) {
50 | wx.showLoading({
51 | title: '加载中...',
52 | mask: true,
53 | })
54 | storage.set('isShowingLoading', true)
55 |
56 | setTimeout(() => {
57 | if (storage.get(key) === RequestStatus.UNFINISHED) {
58 | if (storage.get('isShowingLoading') === true) {
59 | storage.remove('isShowingLoading')
60 | wx.hideLoading({
61 | noConflict: true,
62 | })
63 | }
64 | }
65 | }, LOADING_DURATION)
66 | }
67 | }, SILENCE_DURATION)
68 | }
69 |
70 | return config
71 | }
72 |
73 | /**
74 | * 关闭等待标志拦截器
75 | */
76 | export function hideLoadingInterceptor(response: AxiosResponse): AxiosResponse {
77 | const config = response.config
78 | const loading = (config as unknown as RequestOptionsInternal).loading
79 |
80 | if (loading) {
81 | const key = getKey(config)
82 | storage.set(key, RequestStatus.FINISHED, 60 * 1000)
83 | if (storage.get('isShowingLoading') === true) {
84 | storage.remove('isShowingLoading')
85 | wx.hideLoading({
86 | noConflict: true,
87 | })
88 | }
89 | }
90 |
91 | return response
92 | }
93 |
--------------------------------------------------------------------------------
/miniprogram/app/services/weather-canvas.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 在 canvas 中画逐日温度折线图
3 | */
4 |
5 | import {WeatherHourly} from './weather-data'
6 |
7 | /** X 轴和 Y 轴上的坐标 */
8 | export interface Point {
9 | x: number
10 |
11 | y: number
12 | }
13 |
14 | /**
15 | * 画未来 24 小时预报温度折线图
16 | *
17 | * @param ctx 画笔
18 | * @param list 未来 24 小时预报数据
19 | * @param theme 主题(`light` | `dark`)
20 | */
21 | export function drawWeatherHourlyLineChart(ctx: CanvasRenderingContext2D, list: WeatherHourly[], theme: string): void {
22 | /** 线条颜色 */
23 | const LINE_COLOR = '#427bff'
24 |
25 | /** 温度字体颜色 */
26 | const TEXT_COLOR = theme === 'dark' ? '#d6d6d6' : '#040404'
27 |
28 | /** 线条宽度 */
29 | const LINE_WIDTH = 6
30 |
31 | /** 文字宽度 */
32 | const TEXT_WIDTH = 2
33 |
34 | // 一天一个格子,计算每个格子的宽度
35 | const gridWidth = ctx.canvas.width / list.length
36 |
37 | // 格子高度等同于 canvas 的高度
38 | const gridHeight = ctx.canvas.height
39 |
40 | /** 折线图底部留空区域占比 */
41 | const bottomFreeRatio = 0.05
42 |
43 | /** 折线图顶部留空区域占比(顶部要留多一点,因为还要放温度数字) */
44 | const topFreeRatio = 0.3
45 |
46 | /** 温度列表 */
47 | const tempList: number[] = list.map((item: WeatherHourly) => parseInt(item.temp))
48 |
49 | /** 温度最小值 */
50 | const minTemp = Math.min(...tempList)
51 |
52 | /** 温度最大值 */
53 | const maxTemp = Math.max(...tempList)
54 |
55 | /** 每 1 度温度占有的高度 */
56 | const heightPerTemp = (gridHeight * (1 - bottomFreeRatio - topFreeRatio)) / (maxTemp - minTemp)
57 |
58 | /** 坐标点 */
59 | const pointList: Point[] = tempList.map((temp: number, index: number) => {
60 | const x = (index + 0.5) * gridWidth
61 | const y = topFreeRatio * gridHeight + (maxTemp - temp) * heightPerTemp
62 |
63 | return {x, y}
64 | })
65 |
66 | // 开始画图
67 | ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
68 |
69 | ctx.lineWidth = LINE_WIDTH
70 |
71 | // 画温度线
72 | ctx.beginPath()
73 | ctx.strokeStyle = LINE_COLOR
74 | for (let i = 0; i < pointList.length; i++) {
75 | ctx.lineTo(pointList[i].x, pointList[i].y)
76 | }
77 | ctx.stroke()
78 |
79 | // 画温度线上的点
80 | ctx.fillStyle = LINE_COLOR
81 | pointList.forEach((point: Point) => {
82 | ctx.beginPath()
83 | ctx.arc(point.x, point.y, 8, 0, 2 * Math.PI)
84 | ctx.fill()
85 | })
86 |
87 | // 标记温度文字
88 | ctx.beginPath()
89 | ctx.lineWidth = TEXT_WIDTH
90 | ctx.fillStyle = TEXT_COLOR
91 | ctx.font = '32px sans-serif'
92 | ctx.textAlign = 'center'
93 |
94 | pointList.forEach((point: Point, index: number) => {
95 | ctx.beginPath()
96 | ctx.fillText(`${tempList[index]}°`, point.x, point.y - 24)
97 | })
98 | }
99 |
--------------------------------------------------------------------------------
/miniprogram/app/http/miniprogram-adatper.ts:
--------------------------------------------------------------------------------
1 | import {AxiosRequestConfig, AxiosResponse} from 'axios'
2 | import {ParamsUtils} from '../utils/params-utils'
3 |
4 | /** 请求方法 */
5 | export type Method = 'GET' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'POST' | 'PUT'
6 |
7 | /**
8 | * 处理请求方法类型
9 | * @param {string} method 请求方法类型
10 | */
11 | export function handleMethod(method?: string): Method {
12 | if (typeof method !== 'string') {
13 | return 'GET'
14 | } else {
15 | return <'GET' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'POST' | 'PUT'>method.toUpperCase()
16 | }
17 | }
18 |
19 | /**
20 | * Axios 自定义错误
21 | */
22 | export class AxiosError extends Error {
23 | config: AxiosRequestConfig
24 |
25 | code: number
26 |
27 | request?: any
28 |
29 | response?: AxiosResponse
30 |
31 | constructor(message: string, config: AxiosRequestConfig, code?: number, request?: any, response?: AxiosResponse) {
32 | super(message)
33 |
34 | this.config = config
35 | this.code = code || 0
36 | this.request = request
37 | this.response = response
38 | }
39 |
40 | toJSON() {
41 | return {
42 | message: this.message,
43 | config: this.config,
44 | code: this.code,
45 | }
46 | }
47 | }
48 |
49 | /**
50 | * 微信小程序请求适配器
51 | * @param {AxiosRequestConfig} config
52 | */
53 | export function miniprogramAdapter(config: AxiosRequestConfig): Promise> {
54 | config.params = config.params || {}
55 | config.headers = config.headers || {}
56 | config.method = handleMethod(config.method)
57 |
58 | const querystring = ParamsUtils.encode(config.params)
59 | const wholeUrl = (config.baseURL || '') + (config.url || '') + (querystring ? '?' + querystring : '')
60 |
61 | return new Promise((resolve, reject) => {
62 | const request = wx.request({
63 | method: config.method,
64 | url: wholeUrl,
65 | header: config.headers,
66 | timeout: config.timeout || 30000,
67 | data: config.data,
68 | success(res) {
69 | const response: AxiosResponse = {
70 | data: res.data,
71 | status: res.statusCode,
72 | statusText: 'OK',
73 | headers: res.header,
74 | config,
75 | request,
76 | }
77 | if (!response.status || !config.validateStatus || config.validateStatus(response.status)) {
78 | resolve(response)
79 | } else {
80 | reject(new AxiosError(`请求失败,状态码: ${response.status}`, config, response.status, request, response))
81 | }
82 | },
83 | fail(res) {
84 | reject(new AxiosError(res.errMsg, config))
85 | },
86 | })
87 | })
88 | }
89 |
--------------------------------------------------------------------------------
/miniprogram/app/services/weather-place.ts:
--------------------------------------------------------------------------------
1 | /** 天气地点 */
2 | import {StorageField} from '../core/constant'
3 | import {requestForData} from '../core/http'
4 | import {storage} from '../core/storage'
5 |
6 | /** 基础天气数据 */
7 | export interface BasicWeather {
8 | /** 天气图标的 URL 地址 */
9 | iconUrl: string
10 |
11 | /** 温度,默认单位:摄氏度 */
12 | temp: string
13 |
14 | /** 天气状况的文字描述,包括阴晴雨雪等天气状态的描述 */
15 | text: string
16 | }
17 |
18 | /** 天气地点 */
19 | export interface WeatherPlace {
20 | /** 地点 ID */
21 | id: string
22 |
23 | /** 位置名称 */
24 | name: string
25 |
26 | /** 所在地区,市 + 区,例如:“杭州市西湖区” */
27 | region: string
28 |
29 | /** 附带的天气数据 */
30 | weather: BasicWeather
31 | }
32 |
33 | /** 删除天气地点响应数据 */
34 | export type DeleteWeatherPlaceResponse = Pick
35 |
36 | /** IP 定位地点 */
37 | export interface IpLocatedPlace {
38 | /** 地点名称,取“市”,例如“杭州市” */
39 | name: string
40 |
41 | /** 附带的天气数据 */
42 | weather: BasicWeather
43 | }
44 |
45 | /** 获取天气地点列表响应数据 */
46 | export interface GetWeatherPlaceListResponse {
47 | /** 天气地点列表 */
48 | list: WeatherPlace[]
49 |
50 | /** IP 定位地点 */
51 | ipLocated: IpLocatedPlace
52 | }
53 |
54 | /**
55 | * 新增一个天气地点
56 | *
57 | * @param result 微信选择定位的结果
58 | */
59 | export function addWeatherPlace(result: WechatMiniprogram.ChooseLocationSuccessCallbackResult): Promise {
60 | return requestForData({
61 | method: 'POST',
62 | url: '/weather/place',
63 | data: result,
64 | auth: true,
65 | })
66 | }
67 |
68 | /**
69 | * 删除天气地点
70 | *
71 | * @param id 天气地点 ID
72 | */
73 | export function deleteWeatherPlace(id: string): Promise {
74 | return requestForData({
75 | method: 'DELETE',
76 | url: `/weather/place/${id}`,
77 | auth: true,
78 | })
79 | }
80 |
81 | /**
82 | * 获取天气地点列表
83 | */
84 | export function getWeatherPlaceList(): Promise {
85 | return requestForData({
86 | method: 'GET',
87 | url: '/weather/places',
88 | auth: true,
89 | })
90 | }
91 |
92 | /**
93 | * 获取当前选中的天气地点 ID
94 | */
95 | export function getSelectWeatherPlaceId(): string {
96 | const id = storage.get(StorageField.SELECTED_WEATHER_PLACE_ID)
97 | return id ? id : ''
98 | }
99 |
100 | /**
101 | * 设置当前选中的天气地点 ID
102 | */
103 | export function setSelectWeatherPlaceId(id: string): void {
104 | storage.set(StorageField.SELECTED_WEATHER_PLACE_ID, id)
105 | }
106 |
107 | /**
108 | * 设置当前选中的天气地点 ID
109 | */
110 | export function removeSelectWeatherPlaceId(): void {
111 | storage.remove(StorageField.SELECTED_WEATHER_PLACE_ID)
112 | }
113 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/place/place.scss:
--------------------------------------------------------------------------------
1 | /* pages/weather/place/place.wxss */
2 | .main {
3 | padding: 20rpx 40rpx;
4 |
5 | .title {
6 | font-size: 24rpx;
7 |
8 | color: var(--weui-FG-1);
9 | }
10 |
11 | .located {
12 | margin-top: 10rpx;
13 | margin-bottom: 80rpx;
14 |
15 | .item {
16 | display: flex;
17 | align-items: center;
18 | justify-content: space-between;
19 |
20 | padding: 0 20rpx;
21 |
22 | border-radius: 16rpx;
23 | background-color: var(--weui-BG-2);
24 | }
25 | }
26 |
27 | .edit {
28 | font-size: 24rpx;
29 |
30 | color: var(--weui-LINK);
31 | }
32 |
33 | .favorite {
34 | margin-top: 10rpx;
35 |
36 | .head {
37 | display: flex;
38 | align-items: center;
39 | justify-content: space-between;
40 | }
41 |
42 | .operator {
43 | display: flex;
44 | align-items: center;
45 | justify-content: center;
46 |
47 | width: 80rpx;
48 | height: 80rpx;
49 | margin-left: 30rpx;
50 |
51 | border-radius: 40rpx;
52 | background-color: var(--weui-BG-2);
53 | }
54 | }
55 |
56 | .list {
57 | margin-top: 20rpx;
58 |
59 | .item {
60 | display: flex;
61 | align-items: center;
62 |
63 | height: 130rpx;
64 | margin-bottom: 20rpx;
65 |
66 | .content {
67 | display: flex;
68 | align-items: center;
69 | flex-grow: 1;
70 | justify-content: space-between;
71 |
72 | height: 100%;
73 | padding: 0 20rpx;
74 |
75 | border-radius: 16rpx;
76 | background-color: var(--weui-BG-2);
77 | }
78 |
79 | .active {
80 | border: 2rpx solid var(--weui-GREEN);
81 | }
82 |
83 | .left {
84 | display: flex;
85 | flex-direction: column;
86 |
87 | .region {
88 | font-size: 26rpx;
89 |
90 | color: var(--weui-FG-2);
91 | }
92 | }
93 |
94 | .right {
95 | display: flex;
96 | align-items: center;
97 | justify-content: space-between;
98 |
99 | .icon {
100 | width: 60rpx;
101 | height: 60rpx;
102 | margin-left: 20rpx;
103 | }
104 | }
105 | }
106 | }
107 |
108 | .bottom {
109 | margin-top: 100rpx;
110 | }
111 |
112 | .empty {
113 | margin-top: 400rpx;
114 |
115 | .desc {
116 | font-size: 28rpx;
117 |
118 | margin-bottom: 40rpx;
119 |
120 | text-align: center;
121 |
122 | color: var(--weui-FG-2);
123 | }
124 | }
125 | }
126 |
127 | // 页面未完成初始化时展示的空白页面
128 | .empty-page {
129 | height: 1200rpx;
130 |
131 | background-color: var(--weui-BG-0);
132 | }
133 |
--------------------------------------------------------------------------------
/miniprogram/pages/album/detail/detail.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{ albumDetail.imageCount }} 张照片,{{ albumDetail.videoCount }} 个视频,占用 {{ sizeMB }}MB
12 |
13 |
14 |
15 |
16 |
17 | 编辑
18 |
19 |
20 |
21 | 上传
22 |
23 |
24 |
25 |
26 |
27 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | 暂无照片,上传一张试试吧
49 |
50 | 删除相册
51 |
52 |
53 |
54 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/miniprogram/pages/scan/login.ts:
--------------------------------------------------------------------------------
1 | import {confirmQrcode, scanQrcode} from '../../app/services/scan-login'
2 | import {themeBehavior} from '../../behaviors/theme-behavior'
3 |
4 | /** 扫码操作状态 */
5 | export type ScanStatus = 'initial' | 'scanned' | 'succeeded' | 'failed'
6 |
7 | // pages/scan/login.ts
8 | Page({
9 | /** 页面的初始数据 */
10 | data: {
11 | /** 扫码登录凭据 ID */
12 | id: '',
13 |
14 | /** IP 地址 */
15 | ip: '',
16 |
17 | /** IP 地址所在区域,包含省和市,例如:浙江杭州 */
18 | region: '',
19 |
20 | /** ====================== 页面状态 ====================== */
21 |
22 | /** “确认登录”按钮是否显示 Loading */
23 | confirmLoading: false,
24 |
25 | status: 'initial' as ScanStatus,
26 |
27 | /** 错误消息 */
28 | errorMsg: '',
29 |
30 | /** ==================== 静态资源地址 ==================== */
31 |
32 | /** 电脑图案的图片的 URL 地址 */
33 | pcImageUrl: 'https://static.lifehelper.com.cn/images/pc.png',
34 | },
35 |
36 | behaviors: [themeBehavior],
37 |
38 | async onLoad(query) {
39 | // 从小程序码中获取「扫码登录凭据」的 ID
40 | const id = query.scene
41 | this.setData({id})
42 |
43 | wx.showLoading({title: '加载中'})
44 | await this.scan()
45 | wx.hideLoading()
46 | },
47 |
48 | /** 进行【扫码】操作,用于获取凭据的基本信息 */
49 | async scan() {
50 | const id = this.data.id
51 |
52 | const result = await scanQrcode(id)
53 | if (result.errorCode) {
54 | this.setData({
55 | status: 'failed',
56 | errorMsg: result.errorMessage,
57 | })
58 | } else {
59 | this.setData({
60 | ip: result.ip,
61 | region: result.region,
62 | status: 'scanned',
63 | })
64 | }
65 | },
66 |
67 | /** 进行“确认登录”操作 */
68 | async confirm() {
69 | const id = this.data.id
70 | this.setData({confirmLoading: true})
71 | const result = await confirmQrcode(id)
72 | if (result.errorCode) {
73 | this.setData({
74 | status: 'failed',
75 | errorMsg: result.errorMessage,
76 | })
77 | } else {
78 | this.setData({
79 | status: 'succeeded',
80 | confirmLoading: false,
81 | })
82 | }
83 | },
84 |
85 | /** 跳转到首页 */
86 | goToIndex() {
87 | // 实际上可以直接跳转首页的,但是为了让用户知道发生了什么,加了这个等待提示
88 | wx.showToast({
89 | title: '正在跳转首页',
90 | icon: 'loading',
91 | mask: true,
92 | duration: 2000,
93 | })
94 |
95 | setTimeout(() => {
96 | wx.switchTab({
97 | url: '/pages/index/index',
98 | })
99 | }, 1500)
100 | },
101 |
102 | /** 点击“取消登录”按钮 */
103 | cancel() {
104 | wx.showToast({
105 | title: '已取消登录',
106 | icon: 'none',
107 | duration: 2000,
108 | mask: true,
109 | }).then(() => {
110 | setTimeout(() => {
111 | this.goToIndex()
112 | }, 1000)
113 | })
114 | },
115 | })
116 |
--------------------------------------------------------------------------------
/miniprogram/pages/great-day/detail/detail.ts:
--------------------------------------------------------------------------------
1 | // pages/great-day/detail/detail.ts
2 |
3 | import {CommonColor, PageChannelEvent} from '../../../app/core/constant'
4 | import {deleteGreatDay, getGreatDayDetail, GreatDay} from '../../../app/services/great-day'
5 | import {Id} from '../../../app/utils/types'
6 | import {showSingleButtonModel} from '../../../app/utils/wx'
7 | import {themeBehavior} from '../../../behaviors/theme-behavior'
8 |
9 | Page({
10 | data: {
11 | // ================================ 页面传值数据 ================================
12 |
13 | /**
14 | * 纪念日 ID
15 | *
16 | * ### 说明
17 | * 该值从路径参数获取,必须包含该值,否则报错。
18 | */
19 | id: '',
20 |
21 | // ============================= 从HTTP请求获取的数据 =============================
22 |
23 | /** 纪念日详情数据 */
24 | day: {} as GreatDay,
25 | },
26 |
27 | behaviors: [themeBehavior],
28 |
29 | onLoad(query: Record) {
30 | const id = (query as unknown as Id).id
31 | this.setData({id})
32 |
33 | this.init()
34 | },
35 |
36 | /** 页面初始化方法 */
37 | async init() {
38 | const id = this.data.id
39 | const day = await getGreatDayDetail(id)
40 | this.setData({day})
41 |
42 | wx.setNavigationBarTitle({title: day.name})
43 | },
44 |
45 | /** 通知前一个页面(列表页)刷新 */
46 | notifyListPageRefresh() {
47 | const channel = this.getOpenerEventChannel()
48 | if (typeof channel.emit === 'function') {
49 | channel.emit(PageChannelEvent.REFRESH_DATA)
50 | }
51 | },
52 |
53 | /** 跳转到“编辑”页 */
54 | goToEditPage() {
55 | const self = this
56 | const {id, day} = this.data
57 | wx.navigateTo({
58 | url: `/pages/great-day/edit/edit?id=${id}`,
59 | events: {
60 | [PageChannelEvent.REFRESH_DATA]: function () {
61 | self.init()
62 | self.notifyListPageRefresh()
63 | },
64 | },
65 | }).then((res) => {
66 | res.eventChannel.emit(PageChannelEvent.DATA_TRANSFER, {day})
67 | })
68 | },
69 |
70 | /** 点击“分享”操作 */
71 | share() {
72 | showSingleButtonModel('功能开发中,敬请期待!')
73 | },
74 |
75 | /** 点击“删除”操作 */
76 | async delete() {
77 | const id = this.data.id
78 | const name = this.data.day.name
79 |
80 | if (this.data.day.systemCreated) {
81 | showSingleButtonModel('系统默认数据,不允许操作删除!')
82 | } else {
83 | const res1 = await wx.showModal({
84 | title: '提示',
85 | content: `是否删除 ${name} ?`,
86 | confirmText: '立即删除',
87 | confirmColor: CommonColor.RED,
88 | })
89 |
90 | // 点击“确定”按钮
91 | if (res1.confirm) {
92 | await deleteGreatDay(id)
93 | this.notifyListPageRefresh()
94 | wx.showToast({
95 | title: '删除成功',
96 | icon: 'success',
97 | })
98 |
99 | setTimeout(() => {
100 | wx.navigateBack()
101 | }, 1000)
102 | }
103 | }
104 | },
105 | })
106 |
--------------------------------------------------------------------------------
/miniprogram/app/utils/wx-typings.ts:
--------------------------------------------------------------------------------
1 | // 存放微信原生 TypeScript 类型(直接从 TypeScript 类型库中复制过来的)
2 | // 为什么要有这份文件?
3 | // 微信虽然给出了类型,但是在项目内如果直接引用(如下所示),则会报错,因此只能复制过来。
4 | // import BaseEvent = WechatMiniprogram.BaseEvent
5 |
6 | export type IAnyObject = Record
7 |
8 | export interface Target {
9 | /** 事件组件的 id */
10 | id: string
11 | /** 当前组件的类型 */
12 | tagName?: string
13 | /** 事件组件上由 `data-` 开头的自定义属性组成的集合 */
14 | dataset: DataSet
15 | /** 距离页面顶部的偏移量 */
16 | offsetTop: number
17 | /** 距离页面左边的偏移量 */
18 | offsetLeft: number
19 | }
20 |
21 | /** 基础事件参数 */
22 | export interface BaseEvent<
23 | Mark extends IAnyObject = IAnyObject,
24 | CurrentTargetDataset extends IAnyObject = IAnyObject,
25 | TargetDataset extends IAnyObject = CurrentTargetDataset
26 | > {
27 | /** 事件类型 */
28 | type: string
29 | /** 页面打开到触发事件所经过的毫秒数 */
30 | timeStamp: number
31 | /** 事件冒泡路径上所有由 `mark:` 开头的自定义属性组成的集合 */
32 | mark?: Mark
33 | /** 触发事件的源组件 */
34 | target: Target
35 | /** 事件绑定的当前组件 */
36 | currentTarget: Target
37 | }
38 |
39 | /** 自定义事件 */
40 | export interface CustomEvent<
41 | Detail extends IAnyObject = IAnyObject,
42 | Mark extends IAnyObject = IAnyObject,
43 | CurrentTargetDataset extends IAnyObject = IAnyObject,
44 | TargetDataset extends IAnyObject = CurrentTargetDataset
45 | > extends BaseEvent {
46 | /** 额外的信息 */
47 | detail: Detail
48 | }
49 |
50 | export interface AuthSetting {
51 | /** 是否授权通讯地址,已取消此项授权,会默认返回true */
52 | 'scope.address'?: boolean
53 | /** 是否授权小程序在后台运行蓝牙,对应接口 [wx.openBluetoothAdapterBackground](#) */
54 | 'scope.bluetoothBackground'?: boolean
55 | /** 是否授权摄像头,对应[[camera](https://developers.weixin.qq.com/miniprogram/dev/component/camera.html)](https://developers.weixin.qq.com/miniprogram/dev/component/camera.html) 组件 */
56 | 'scope.camera'?: boolean
57 | /** 是否授权获取发票,已取消此项授权,会默认返回true */
58 | 'scope.invoice'?: boolean
59 | /** 是否授权发票抬头,已取消此项授权,会默认返回true */
60 | 'scope.invoiceTitle'?: boolean
61 | /** 是否授权录音功能,对应接口 [wx.startRecord](https://developers.weixin.qq.com/miniprogram/dev/api/media/recorder/wx.startRecord.html) */
62 | 'scope.record'?: boolean
63 | /** 是否授权用户信息,对应接口 [wx.getUserInfo](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserInfo.html) */
64 | 'scope.userInfo'?: boolean
65 | /** 是否授权地理位置,对应接口 [wx.getLocation](https://developers.weixin.qq.com/miniprogram/dev/api/location/wx.getLocation.html), [wx.chooseLocation](https://developers.weixin.qq.com/miniprogram/dev/api/location/wx.chooseLocation.html) */
66 | 'scope.userLocation'?: boolean
67 | /** 是否授权微信运动步数,对应接口 [wx.getWeRunData](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/werun/wx.getWeRunData.html) */
68 | 'scope.werun'?: boolean
69 | /** 是否授权保存到相册 [wx.saveImageToPhotosAlbum](https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.saveImageToPhotosAlbum.html), [wx.saveVideoToPhotosAlbum](https://developers.weixin.qq.com/miniprogram/dev/api/media/video/wx.saveVideoToPhotosAlbum.html) */
70 | 'scope.writePhotosAlbum'?: boolean
71 | }
72 |
--------------------------------------------------------------------------------
/miniprogram/pages/album/list/list.scss:
--------------------------------------------------------------------------------
1 | /* pages/album/list/list.wxss */
2 |
3 | .container {
4 | padding: 0 30rpx 80rpx;
5 |
6 | background-color: var(--weui-BG-2);
7 | }
8 |
9 | // 按钮区
10 | .buttons {
11 | display: flex;
12 | align-items: center;
13 | justify-content: space-between;
14 |
15 | padding-top: 36rpx;
16 |
17 | .info {
18 | font-size: 22rpx;
19 |
20 | color: var(--weui-FG-1);
21 | }
22 |
23 | .placeholder {
24 | flex: 1;
25 | }
26 |
27 | .button {
28 | font-size: 26rpx;
29 |
30 | display: flex;
31 | align-items: center;
32 | justify-content: space-between;
33 |
34 | height: 60rpx;
35 | margin-left: 16rpx;
36 | padding: 0 20rpx;
37 |
38 | color: var(--weui-FG-1);
39 | border-radius: 30rpx;
40 | background-color: var(--weui-BG-3);
41 |
42 | .icon {
43 | width: 28rpx;
44 | }
45 |
46 | .title {
47 | margin-left: 10rpx;
48 | }
49 | }
50 |
51 | .button:first-child {
52 | margin-left: 0;
53 | }
54 | }
55 |
56 | // 相册列表区
57 | .list {
58 | display: flex;
59 | flex-wrap: wrap;
60 | justify-content: space-between;
61 |
62 | margin-top: 30rpx;
63 |
64 | // 单个相册项目
65 | .item {
66 | display: flex;
67 | overflow: hidden;
68 | flex-direction: column;
69 |
70 | width: 330rpx;
71 | margin-bottom: 20rpx;
72 |
73 | border: 2rpx solid var(--weui-FG-3);
74 | border-radius: 10rpx;
75 |
76 | // 封面图部分(包含数字)
77 | .cover {
78 | position: relative;
79 |
80 | width: 330rpx;
81 | height: 330rpx;
82 |
83 | .total {
84 | font-size: 24rpx;
85 |
86 | position: absolute;
87 | right: 20rpx;
88 | bottom: 20rpx;
89 |
90 | padding: 4rpx 10rpx;
91 |
92 | color: var(--weui-WHITE);
93 | border-radius: 14rpx;
94 | background-color: var(--weui-FG-1);
95 | }
96 | }
97 |
98 | .empty-cover {
99 | display: flex;
100 | align-items: center;
101 | justify-content: center;
102 |
103 | background-color: var(--weui-BG-1);
104 |
105 | .iconfont {
106 | font-size: 80rpx;
107 |
108 | color: var(--weui-FG-2);
109 | }
110 | }
111 |
112 | // 相册名称和描述
113 | .text-area {
114 | padding: 18rpx 12rpx;
115 |
116 | .name {
117 | font-size: 30rpx;
118 | }
119 |
120 | .desc {
121 | font-size: 24rpx;
122 |
123 | margin-top: 8rpx;
124 |
125 | color: var(--weui-FG-1);
126 | }
127 | }
128 | }
129 | }
130 |
131 | .list-end {
132 | font-size: 24rpx;
133 |
134 | padding: 40rpx 0 10rpx;
135 |
136 | text-align: center;
137 |
138 | color: var(--weui-FG-1);
139 | }
140 |
141 | // 空列表区
142 | .empty-list {
143 | display: flex;
144 | align-items: center;
145 | flex-direction: column;
146 | justify-content: center;
147 |
148 | height: 1000rpx;
149 |
150 | .tips {
151 | font-size: 28rpx;
152 |
153 | margin-top: 50rpx;
154 | }
155 |
156 | .add {
157 | margin-top: 30rpx;
158 | }
159 | }
160 |
161 | .weui-footer {
162 | margin-top: 60rpx;
163 | }
164 |
--------------------------------------------------------------------------------
/miniprogram/app/core/storage.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 对微信小程序原生的数据缓存做二次封装
3 | */
4 |
5 | import dayjs from 'dayjs'
6 | import {logger} from './logger'
7 |
8 | /**
9 | * 计算到期时间的时间戳
10 | *
11 | * @param time 到期时间或有效时长(毫秒)
12 | */
13 | function calcExpireTime(time?: string | number): number {
14 | // 使用 2 年的毫秒数表示一个足够长的时间区间
15 | const LONG_ENOUGH_MS = 2 * 365 * 24 * 3600 * 1000
16 |
17 | // 情况(1):形如 `2022-12-01 19:44:13` 格式的字符串
18 | if (typeof time === 'string') {
19 | return dayjs(time).valueOf()
20 | }
21 |
22 | if (typeof time === 'number') {
23 | // 情况(2):是一个时间戳
24 | if (time > LONG_ENOUGH_MS) {
25 | return time
26 | } else {
27 | // 情况(3):表示有效时长(毫秒)
28 | return Date.now() + time
29 | }
30 | }
31 |
32 | return Date.now() + LONG_ENOUGH_MS
33 | }
34 |
35 | /** 最终存入数据缓存的数据格式 */
36 | export interface StorageWrapper {
37 | /** 创建时间(时间戳,毫秒) */
38 | createTime: number
39 |
40 | /** 过期时间(时间戳,毫秒) */
41 | expireTime: number
42 |
43 | /** 存储数据 */
44 | data: T
45 | }
46 |
47 | /**
48 | * 二次封装的数据缓存方法
49 | */
50 | export class Storage {
51 | constructor() {
52 | // 空
53 | }
54 |
55 | /**
56 | * 将分钟数转换为毫秒值
57 | *
58 | * @param minutes 分钟数
59 | */
60 | static ofMinutes(minutes: number): number {
61 | return minutes * 60 * 1000
62 | }
63 |
64 | /**
65 | * 保存数据
66 | *
67 | * @param key 键名
68 | * @param data 要存入的数据
69 | */
70 | set(key: string, data: T): void
71 |
72 | /**
73 | * 保存数据
74 | *
75 | * @param key 键名
76 | * @param data 要存入的数据
77 | * @param expiration 有效期或到期时间,单位:毫秒
78 | */
79 | set(key: string, data: T, expiration: number): void
80 |
81 | /**
82 | * 保存数据
83 | *
84 | * @param key 键名
85 | * @param data 要存入的数据
86 | * @param expiration 到期时间字符串
87 | */
88 | set(key: string, data: T, expiration: string): void
89 |
90 | set(first: string, second: T, third?: number | string): void {
91 | const key = first
92 | const data = second
93 | const createTime = Date.now()
94 | const expireTime = calcExpireTime(third)
95 |
96 | const wrapper: StorageWrapper = {createTime, expireTime, data}
97 | logger.debug(`[STORAGE][SET] key=${key}, data=${JSON.stringify(data)}`)
98 |
99 | wx.setStorageSync(key, wrapper)
100 | }
101 |
102 | /**
103 | * 获取数据
104 | *
105 | * @param key 键名
106 | */
107 | get(key: string): T | null {
108 | const wrapper: StorageWrapper = wx.getStorageSync(key)
109 |
110 | // 经过封装后,必定是一个对象,该情况说明该键值不存在
111 | if (typeof wrapper !== 'object') {
112 | return null
113 | }
114 |
115 | // 过期情况
116 | if (Date.now() > wrapper.expireTime) {
117 | wx.removeStorageSync(key)
118 | return null
119 | }
120 |
121 | logger.debug(`[STORAGE][GET] key=${key}, data=${JSON.stringify(wrapper.data)}`)
122 |
123 | return wrapper.data
124 | }
125 |
126 | /**
127 | * 删除数据缓存
128 | *
129 | * @param key 键名
130 | */
131 | remove(key: string): void {
132 | wx.removeStorageSync(key)
133 | }
134 | }
135 |
136 | export const storage = new Storage()
137 |
--------------------------------------------------------------------------------
/miniprogram/components/authorize-element/authorize-element.ts:
--------------------------------------------------------------------------------
1 | // components/authorize-element/authorize-element.ts
2 |
3 | import {AuthorizeStatus, getAuthorizeStatus, openSettingAndCheck} from '../../app/services/authorization'
4 | import {AuthSetting, CustomEvent} from '../../app/utils/wx-typings'
5 |
6 | interface Detail {
7 | index: number
8 | }
9 |
10 | /**
11 | * 强制授权组件
12 | *
13 | * ## 说明
14 | * 由于微信的限制(https://developers.weixin.qq.com/community/develop/doc/000cea2305cc5047af5733de751008),实测 `wx.openSetting`
15 | * 方法在按钮绑定点击事件的函数内中间不能有被 `await` 的异步方法,造成很多逻辑会写的很绕。这个组件利用 weui 的 Dialog 组件,将上面方法的调用绑定
16 | * 到它的按钮上。
17 | */
18 | Component({
19 | /**
20 | * 组件的属性列表
21 | */
22 | properties: {
23 | /** 授权项名称 */
24 | scope: {
25 | type: String,
26 | value: '',
27 | },
28 | },
29 |
30 | /**
31 | * 组件的初始数据
32 | */
33 | data: {
34 | /** 弹窗的内容 */
35 | dialog: {
36 | title: '系统提示',
37 | content: '为保证功能正常使用,需要您开启相应权限!',
38 | buttons: [{text: '我不想用'}, {text: '去设置'}],
39 | },
40 |
41 | /** 是否展示弹窗 */
42 | showDialog: false,
43 | },
44 |
45 | /**
46 | * 组件的方法列表
47 | */
48 | methods: {
49 | /** 处理主要内容被点击 */
50 | async viewTap() {
51 | const {triggerEvent} = this
52 | const scope = this.data.scope as keyof AuthSetting
53 | const res = await getAuthorizeStatus(scope)
54 |
55 | if (res === AuthorizeStatus.Authorized) {
56 | this.triggerEvent('success')
57 | return
58 | }
59 |
60 | if (res === AuthorizeStatus.NotApplied) {
61 | wx.showModal({
62 | title: '系统提示',
63 | content: '为保证功能正常使用,需要您开启相应权限!',
64 | showCancel: true,
65 | cancelText: '取消',
66 | confirmText: '去设置',
67 | success(res2) {
68 | // 点了「确定」按钮
69 | if (res2.confirm) {
70 | wx.authorize({
71 | scope,
72 | success() {
73 | getAuthorizeStatus(scope).then((res3) => {
74 | if (res3 === AuthorizeStatus.Authorized) {
75 | triggerEvent('success')
76 | } else {
77 | triggerEvent('fail')
78 | }
79 | })
80 | },
81 | fail() {
82 | triggerEvent('fail')
83 | },
84 | })
85 | }
86 | },
87 | })
88 | }
89 |
90 | if (res === AuthorizeStatus.Denied) {
91 | this.setData({showDialog: true})
92 | }
93 | },
94 |
95 | /** 处理弹窗按钮被点击 */
96 | async dialogButtonTap(event: CustomEvent) {
97 | this.setData({showDialog: false})
98 |
99 | // 表示点了「确定」按钮
100 | if (event.detail.index === 1) {
101 | const scope = this.data.scope as keyof AuthSetting
102 | const res = await openSettingAndCheck(scope)
103 | if (res === AuthorizeStatus.Authorized) {
104 | this.triggerEvent('success')
105 | return
106 | }
107 | }
108 |
109 | this.triggerEvent('fail')
110 | },
111 | },
112 | })
113 |
--------------------------------------------------------------------------------
/miniprogram/app/services/great-day.ts:
--------------------------------------------------------------------------------
1 | // 「纪念日」模块服务
2 |
3 | import dayjs from 'dayjs'
4 | import {requestForData} from '../core/http'
5 | import {CommonListResponse} from '../utils/types'
6 |
7 | /** 纪念日对象 */
8 | export interface GreatDay {
9 | // =============================== HTTP 响应数据字段 ===============================
10 |
11 | /** 纪念日 ID */
12 | id: number
13 |
14 | /** 纪念日名称 */
15 | name: string
16 |
17 | /** 日期 */
18 | date: string
19 |
20 | /** emoji 表情 */
21 | icon: string
22 |
23 | /**
24 | * 今天距离纪念日的天数(纪念日减去今天的天数)
25 | *
26 | * ### 含义
27 | * - [ >0 ]:纪念日在未来,还未到。
28 | * - [ =0 ]:纪念日就是今天。
29 | * - [ <0 ]:纪念日已过去。
30 | */
31 | days: number
32 |
33 | /**
34 | * 是否是系统创建的
35 | *
36 | * ### 说明
37 | * 如果是系统返回的默认数据,该字段为 `true`,该类数据不允许操作。
38 | */
39 | systemCreated: boolean
40 |
41 | // =============================== 二次处理后新增的字段 ===============================
42 |
43 | /** 格式化的日期 */
44 | formattedDate: string
45 |
46 | /** 非负的天数 */
47 | daysAbs: number
48 | }
49 |
50 | /** 新增、编辑情况时提交的请求数据 */
51 | export type SaveGreatDayDTO = Partial
52 |
53 | /** 对响应数据做二次处理,添加了一些字段 */
54 | export function processGreatDay(day: GreatDay): GreatDay {
55 | day.formattedDate = getDateText(day.date)
56 | day.daysAbs = Math.abs(day.days)
57 |
58 | return day
59 | }
60 |
61 | /**
62 | * 新增
63 | */
64 | export function createGreatDay(day: SaveGreatDayDTO): Promise {
65 | return requestForData({
66 | method: 'POST',
67 | url: '/greatday',
68 | data: day,
69 | auth: true,
70 | })
71 | }
72 |
73 | /**
74 | * 删除
75 | */
76 | export function deleteGreatDay(id: string): Promise {
77 | return requestForData({
78 | method: 'DELETE',
79 | url: `/greatday/${id}`,
80 | auth: true,
81 | })
82 | }
83 |
84 | /**
85 | * 更新
86 | */
87 | export function updateGreatDay(id: string, day: SaveGreatDayDTO): Promise {
88 | return requestForData({
89 | method: 'PUT',
90 | url: `/greatday/${id}`,
91 | data: day,
92 | auth: true,
93 | })
94 | }
95 |
96 | /**
97 | * 获取列表
98 | */
99 | export async function listGreatDay(): Promise {
100 | const res = await requestForData>({
101 | method: 'GET',
102 | url: '/greatdays',
103 | auth: true,
104 | })
105 |
106 | return res.list.map((item) => {
107 | return processGreatDay(item)
108 | })
109 | }
110 |
111 | /**
112 | * 获取单个详情
113 | */
114 | export async function getGreatDayDetail(id: string): Promise {
115 | const day = await requestForData({
116 | method: 'GET',
117 | url: `/greatday/${id}`,
118 | auth: true,
119 | })
120 |
121 | return processGreatDay(day)
122 | }
123 |
124 | /** 获取 emoji 列表 */
125 | export async function getEmojiList(): Promise {
126 | const res = await requestForData>({
127 | method: 'GET',
128 | url: `/greatday-icon`,
129 | auth: false,
130 | })
131 |
132 | return res.list
133 | }
134 |
135 | /** 获取文本格式的日期 */
136 | export function getDateText(date: string) {
137 | const day = dayjs(date)
138 | const month = day.month() + 1
139 |
140 | return `${day.year()}年${month}月${day.date()}日`
141 | }
142 |
--------------------------------------------------------------------------------
/miniprogram/app/services/userinfo.ts:
--------------------------------------------------------------------------------
1 | /** 用户资料 */
2 | import {StorageField} from '../core/constant'
3 | import {requestForData} from '../core/http'
4 | import {Storage, storage} from '../core/storage'
5 | import {getOssPostCredential, uploadToOssWithoutProgress} from './oss'
6 |
7 | /** 用户地区 */
8 | export interface UserRegion {
9 | /** 第1级行政区划的 adcode */
10 | admin1Id: number
11 |
12 | /** 第1级行政区划的简称 */
13 | admin1ShortName: string
14 |
15 | /** 第2级行政区划的 adcode */
16 | admin2Id: number
17 |
18 | /** 第2级行政区划的简称 */
19 | admin2ShortName: string
20 | }
21 |
22 | /** 用户信息 */
23 | export interface UserInfo {
24 | /** 账户 ID */
25 | accountId: number
26 |
27 | /** 注册时间 */
28 | registerTime: string
29 |
30 | /** 已注册天数 */
31 | registeredDays: number
32 |
33 | /** 用户昵称 */
34 | nickName: string
35 |
36 | /** 头像图片的 URL 地址 */
37 | avatarUrl: string
38 |
39 | /** 性别:男、女、未知 */
40 | gender: string
41 |
42 | /** 用户所在地区 */
43 | region: UserRegion
44 |
45 | /** 用户所在地区名称 */
46 | regionDisplayName: string
47 | }
48 |
49 | /** 修改用户信息提交的数据 */
50 | export interface UpdateUserInfo {
51 | /** 用户昵称 */
52 | nickName: string
53 |
54 | /** 头像图片的 URL 地址 */
55 | avatarUrl: string
56 |
57 | /**
58 | * 性别
59 | *
60 | * ### 值范围
61 | * - [1] - 男
62 | * - [2] - 女
63 | */
64 | genderType: number
65 |
66 | /** 所在城市的 adcode */
67 | cityId: number
68 | }
69 |
70 | /**
71 | * 获取用户个人资料
72 | *
73 | * ### 备注
74 | * 由于多个页面均用到用户信息,使用页面传值较为繁琐,因此使用全局缓存
75 | */
76 | export async function getUserInfo(): Promise {
77 | const cacheData = storage.get(StorageField.USER_INFO)
78 | if (cacheData) {
79 | return cacheData
80 | } else {
81 | const data = await requestForData({
82 | method: 'GET',
83 | url: '/userinfo',
84 | auth: true,
85 | })
86 |
87 | storage.set(StorageField.USER_INFO, data, Storage.ofMinutes(30))
88 | return data
89 | }
90 | }
91 |
92 | /**
93 | * 修改用户信息
94 | *
95 | * @param userInfo 要修改的用户信息
96 | */
97 | export async function updateUserInfo(userInfo: Partial): Promise {
98 | const data = await requestForData({
99 | method: 'PUT',
100 | url: '/userinfo',
101 | data: userInfo,
102 | auth: true,
103 | })
104 |
105 | storage.set(StorageField.USER_INFO, data, Storage.ofMinutes(30))
106 | return data
107 | }
108 |
109 | /**
110 | * 修改头像
111 | *
112 | * @param avatarUrl 本地临时头像图片地址
113 | */
114 | export async function updateAvatar(avatarUrl: string): Promise {
115 | const credential = await getOssPostCredential('image')
116 | await uploadToOssWithoutProgress(avatarUrl, credential)
117 |
118 | const newAvatarUrl = credential.url + '/' + credential.key
119 | return updateUserInfo({avatarUrl: newAvatarUrl})
120 | }
121 |
122 | /**
123 | * 修改昵称
124 | *
125 | * @param nickName 新的昵称
126 | */
127 | export async function updateNickName(nickName: string) {
128 | return updateUserInfo({nickName})
129 | }
130 |
131 | /**
132 | * 修改性别
133 | *
134 | * @param genderType 新的性别类型
135 | */
136 | export async function updateGenderType(genderType: number) {
137 | return updateUserInfo({genderType})
138 | }
139 |
140 | /**
141 | * 修改地区
142 | *
143 | * @param cityId 地区对应的 adcode
144 | */
145 | export async function updateRegion(cityId: number) {
146 | return updateUserInfo({cityId})
147 | }
148 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/place/place.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 当前位置
6 |
7 |
8 |
9 | {{ ipLocated.name }}
10 |
11 |
12 | {{ ipLocated.weather.temp }}℃
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | 我的关注
23 | {{ isEdit ? '完成' : '编辑' }}
24 |
25 |
26 |
27 |
33 |
34 | {{ item.name }}
35 | {{ item.region }}
36 |
37 |
38 | {{ item.weather.temp }}℃
39 |
40 |
41 |
42 |
43 |
44 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 点击下方“添加地点”可关注其他城市天气
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/miniprogram/pages/album/list/list.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ list.length }} 个相册,包含 {{ totalCount }} 个文件,占用 {{ totalSizeMB }}MB
10 |
11 |
12 |
13 |
14 | 排序
15 |
16 |
17 |
18 | 创建
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
34 |
35 |
36 |
37 | {{ item.count }}
38 |
39 |
40 |
41 |
42 |
43 | {{ item.count }}
44 |
45 |
46 |
47 |
48 | {{ item.name }}
49 |
50 |
51 |
52 |
53 |
54 | 没有更多相册了
55 |
56 |
57 |
58 |
59 |
60 |
61 | 用照片和视频记录生活
62 |
63 |
64 |
65 |
66 |
67 |
68 |
76 |
77 |
78 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/miniprogram/pages/album/list/list.ts:
--------------------------------------------------------------------------------
1 | // pages/album/list/list.ts
2 | // 相册模块主页:相册列表页
3 |
4 | import {Album, getAlbumList} from '../../../app/services/album'
5 | import {shareAppBehavior} from '../../../behaviors/share-app-behavior'
6 | import {themeBehavior} from '../../../behaviors/theme-behavior'
7 |
8 | Page({
9 | data: {
10 | /** ============================== 直接从 HTTP 请求获取的数据 ============================== */
11 |
12 | /** 相册列表 */
13 | list: [] as Album[],
14 |
15 | /** 所有相册的资源数量之和 */
16 | totalCount: 0,
17 |
18 | /** 所有资源的文件大小之和 */
19 | totalSizeMB: 0,
20 |
21 | /** ============================== 页面状态数据 ============================== */
22 |
23 | /** 当前的排序模式 */
24 | mode: 'createTimeDesc',
25 |
26 | /** 是否展示操作按钮组件 */
27 | showActionSheet: false,
28 |
29 | /** 操作按钮组件数据 */
30 | actionSheet: {
31 | title: '请选择排序方式',
32 | actions: [
33 | {text: '创建时间(升序)', value: 'createTimeAsc'},
34 | {text: '创建时间(降序)', value: 'createTimeDesc'},
35 | {text: '更新时间(升序)', value: 'updateTimeAsc'},
36 | {text: '更新时间(降序)', value: 'updateTimeDesc'},
37 | ],
38 | },
39 |
40 | /** ============================== 其他页面数据 ============================== */
41 |
42 | albumsImageUrl: 'https://static.lifehelper.com.cn/images/albums.svg',
43 | },
44 |
45 | behaviors: [themeBehavior, shareAppBehavior],
46 |
47 | // 从“新增页”返回时,页面数据也需要刷新,因此把初始化放在这个生命周期
48 | onShow() {
49 | this.init()
50 | },
51 |
52 | /** 页面初始化 */
53 | init() {
54 | this.getAlbumList()
55 | },
56 |
57 | /** 获取相册列表数据并赋值 */
58 | async getAlbumList() {
59 | const {list, totalCount, totalSize} = await getAlbumList()
60 | const totalSizeMB = Math.ceil(totalSize / (1024 * 1024))
61 | this.setData({list, totalCount, totalSizeMB})
62 | this.reorderList()
63 | },
64 |
65 | /** 打开排序操作面板 */
66 | showOrderOptionsPanel() {
67 | this.setData({showActionSheet: true})
68 | },
69 |
70 | onActionTap(event: any) {
71 | const mode = event.detail.value
72 | this.setData({mode, showActionSheet: false})
73 |
74 | this.reorderList()
75 | },
76 |
77 | /** 将相册列表数据重排序 */
78 | reorderList() {
79 | const mode = this.data.mode
80 |
81 | if (mode === 'createTimeAsc') {
82 | this.sortByCreateTimeAsc()
83 | } else if (mode === 'createTimeDesc') {
84 | this.sortByCreateTimeDesc()
85 | } else if (mode === 'updateTimeAsc') {
86 | this.sortByUpdateTimeAsc()
87 | } else if (mode === 'updateTimeDesc') {
88 | this.sortByUpdateTimeDesc()
89 | }
90 | },
91 |
92 | /** 按创建时间升序排列 */
93 | sortByCreateTimeAsc() {
94 | const list = this.data.list
95 | list.sort((a, b) => a.createTime - b.createTime)
96 | this.setData({list})
97 | },
98 |
99 | /** 按创建时间降序排列 */
100 | sortByCreateTimeDesc() {
101 | const list = this.data.list
102 | list.sort((a, b) => b.createTime - a.createTime)
103 | this.setData({list})
104 | },
105 |
106 | /** 按更新时间升序排列 */
107 | sortByUpdateTimeAsc() {
108 | const list = this.data.list
109 | list.sort((a, b) => a.updateTime - b.updateTime)
110 | this.setData({list})
111 | },
112 |
113 | /** 按更新时间降序排列 */
114 | sortByUpdateTimeDesc() {
115 | const list = this.data.list
116 | list.sort((a, b) => b.updateTime - a.updateTime)
117 | this.setData({list})
118 | },
119 |
120 | /** 跳转到创建相册页面 */
121 | goToAddPage() {
122 | wx.navigateTo({url: '/pages/album/edit/edit'})
123 | },
124 |
125 | /** 处理相册列表项目点击事件 */
126 | onAlbumItemTap(e: any) {
127 | const albumId = e.currentTarget.dataset.id
128 | const url = `/pages/album/detail/detail?id=${albumId}`
129 | wx.navigateTo({url})
130 | },
131 | })
132 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/place/place.ts:
--------------------------------------------------------------------------------
1 | import {
2 | addWeatherPlace,
3 | deleteWeatherPlace,
4 | getSelectWeatherPlaceId,
5 | getWeatherPlaceList,
6 | IpLocatedPlace,
7 | removeSelectWeatherPlaceId,
8 | setSelectWeatherPlaceId,
9 | WeatherPlace,
10 | } from '../../../app/services/weather-place'
11 | import {themeBehavior} from '../../../behaviors/theme-behavior'
12 | import {CommonColor} from '../../../app/core/constant'
13 | import {TapEvent} from '../../../app/utils/types'
14 |
15 | Page({
16 | data: {
17 | // --------------------------- 从 HTTP 请求获取的数据 ---------------------------
18 |
19 | /** 天气地点列表 */
20 | list: [] as WeatherPlace[],
21 |
22 | /** IP 定位地点 */
23 | ipLocated: {} as IpLocatedPlace,
24 |
25 | // -------------------------------- 页面状态数据 --------------------------------
26 |
27 | /** 页面是否加载完毕 */
28 | loaded: false,
29 |
30 | /** 当前选中的天气地点 ID */
31 | selectedId: '',
32 |
33 | /** 当前是否为编辑状态 */
34 | isEdit: false,
35 | },
36 |
37 | behaviors: [themeBehavior],
38 |
39 | onLoad() {
40 | this.init()
41 | },
42 |
43 | /** 页面初始化方法 */
44 | async init() {
45 | const {list, ipLocated} = await getWeatherPlaceList()
46 | const selectedId = getSelectWeatherPlaceId()
47 | this.setData({list, ipLocated, selectedId, loaded: true})
48 | },
49 |
50 | /** 跳回首页 */
51 | goToIndexPage() {
52 | wx.switchTab({url: '/pages/index/index'})
53 | },
54 |
55 | /** 绑定添加天气地点事件 */
56 | async add() {
57 | try {
58 | const result = await wx.chooseLocation({})
59 |
60 | // 发起添加天气地点请求
61 | const place = await addWeatherPlace(result)
62 |
63 | // 将新增的地点设为活跃项
64 | setSelectWeatherPlaceId(place.id)
65 |
66 | // 提示添加成功
67 | wx.showToast({
68 | title: '添加成功',
69 | icon: 'success',
70 | })
71 |
72 | // 1秒后自动跳走
73 | setTimeout(() => {
74 | this.goToIndexPage()
75 | }, 1000)
76 | } catch (e) {
77 | // 备注(2022.11.04)
78 | // 直接点“取消”则会报错进入此处,这种情况不需要处理
79 | }
80 | },
81 |
82 | /** 删除一个天气地点 */
83 | async remove(id: string) {
84 | // 当前操作的天气地点
85 | const place = this.data.list.find((item) => item.id === id)
86 |
87 | // “删除”操作二次确认
88 | const result = await wx.showModal({
89 | title: '提示',
90 | content: `是否确认不再关注 ${place?.name} 的天气?`,
91 | cancelText: '继续关注',
92 | confirmText: '不再关注',
93 | confirmColor: CommonColor.RED,
94 | })
95 |
96 | // 点了“确认”才继续
97 | if (result.confirm) {
98 | const deletedPlace = await deleteWeatherPlace(id)
99 |
100 | // 如果被删除的是当前选中的,则清除
101 | if (this.data.selectedId === deletedPlace.id) {
102 | removeSelectWeatherPlaceId()
103 | }
104 |
105 | // 从列表中删除该项(纯客户端处理)
106 | const list = this.data.list
107 | const index = list.findIndex((item) => item.id === deletedPlace.id)
108 | list.splice(index, 1)
109 | this.setData({list})
110 |
111 | // 提示删除成功
112 | await wx.showToast({
113 | title: '删除成功',
114 | icon: 'success',
115 | })
116 | }
117 | },
118 |
119 | /** 变更“编辑”状态 */
120 | switchEdit() {
121 | const isEdit = this.data.isEdit
122 | this.setData({isEdit: !isEdit})
123 | },
124 |
125 | /** 处理 IP 定位栏点击事件 */
126 | handleIpLocatedItemTap() {
127 | removeSelectWeatherPlaceId()
128 | this.goToIndexPage()
129 | },
130 |
131 | /** 处理“删除”按钮点击事件 */
132 | handleRemoveButtonTap(e: TapEvent<{id: string}>) {
133 | const placeId = e.currentTarget.dataset.id
134 | this.remove(placeId)
135 | },
136 |
137 | /** 处理天气地点列表项点击事件 */
138 | handleWeatherPlaceItemTap(e: TapEvent<{id: string}>) {
139 | const placeId = e.currentTarget.dataset.id
140 |
141 | // 不在“编辑”状态才继续
142 | if (!this.data.isEdit) {
143 | setSelectWeatherPlaceId(placeId)
144 | this.goToIndexPage()
145 | }
146 | },
147 | })
148 |
--------------------------------------------------------------------------------
/miniprogram/pages/weather/daily/daily.scss:
--------------------------------------------------------------------------------
1 | @import "../../../assets/styles/weather.scss";
2 |
3 | .tabs {
4 | .list {
5 | display: flex;
6 |
7 | width: 1600rpx;
8 |
9 | .active {
10 | background-color: var(--weui-BG-2);
11 | }
12 |
13 | .item {
14 | padding: 10rpx;
15 |
16 | .wrapper {
17 | display: flex;
18 | align-items: center;
19 | flex-direction: column;
20 |
21 | width: 120rpx;
22 | padding: 16rpx;
23 |
24 | border-radius: 6rpx;
25 |
26 | .weekday {
27 | font-size: 30rpx;
28 | }
29 |
30 | .date {
31 | font-size: 24rpx;
32 |
33 | margin-top: 10rpx;
34 |
35 | color: var(--weui-FG-1);
36 | }
37 | }
38 | }
39 | }
40 | }
41 |
42 | .content {
43 | margin-top: 20rpx;
44 |
45 | .swiper {
46 | height: 1800rpx;
47 | }
48 |
49 | .item {
50 | height: 1000rpx;
51 | padding: 10rpx 0;
52 | }
53 |
54 | .module {
55 | margin: 0 18rpx 18rpx;
56 |
57 | border-radius: 18rpx;
58 | background-color: var(--weui-BG-2);
59 | }
60 |
61 | .title {
62 | font-size: 30rpx;
63 | line-height: 80rpx;
64 |
65 | width: 100%;
66 | height: 80rpx;
67 | padding: 0 28rpx;
68 |
69 | border-bottom: 2rpx solid var(--weui-FG-3);
70 | }
71 |
72 | .info {
73 | display: flex;
74 | align-items: center;
75 | justify-content: center;
76 |
77 | height: 300rpx;
78 |
79 | .weather-icon {
80 | width: 200rpx;
81 | margin-right: 80rpx;
82 | }
83 |
84 | .right {
85 | display: flex;
86 | align-items: center;
87 | flex-direction: column;
88 |
89 | .temp {
90 | font-size: 54rpx;
91 |
92 | margin-bottom: 10rpx;
93 | }
94 |
95 | .aqi {
96 | font-size: 22rpx;
97 |
98 | margin-top: 12rpx;
99 | padding: 6rpx 24rpx;
100 |
101 | border-radius: 30rpx;
102 | }
103 | }
104 | }
105 |
106 | .sun, .moon {
107 | .body {
108 | display: flex;
109 | align-items: center;
110 | flex-direction: column;
111 |
112 | padding: 26rpx;
113 | padding-top: 60rpx;
114 |
115 | .box {
116 | overflow: hidden;
117 |
118 | width: 600rpx;
119 | height: 150rpx;
120 |
121 | .line {
122 | position: relative;
123 | left: -100rpx;
124 |
125 | width: 800rpx;
126 | height: 800rpx;
127 |
128 | border-radius: 50%;
129 | }
130 | }
131 |
132 | .value {
133 | display: flex;
134 | justify-content: space-between;
135 |
136 | width: 660rpx;
137 |
138 | .wrapper {
139 | display: flex;
140 | align-items: center;
141 | flex-direction: column;
142 |
143 | .name {
144 | font-size: 28rpx;
145 | }
146 |
147 | .time {
148 | font-size: 26rpx;
149 |
150 | color: var(--weui-FG-1);
151 | }
152 | }
153 | }
154 | }
155 | }
156 |
157 | .sun .line {
158 | border: 8rpx dotted var(--weui-ORANGE);
159 | }
160 |
161 | .moon .line {
162 | border: 8rpx dotted var(--weui-PURPLE);
163 | }
164 |
165 | .wind {
166 | .body {
167 | padding: 26rpx;
168 |
169 | .line {
170 | display: flex;
171 | align-items: center;
172 | justify-content: space-between;
173 |
174 | height: 200rpx;
175 | margin-top: 20rpx;
176 | padding: 0 40rpx;
177 |
178 | border-radius: 20rpx;
179 | background-color: var(--weui-BG-1);
180 |
181 | .one {
182 | display: flex;
183 | align-items: center;
184 | flex-direction: column;
185 |
186 | .value {
187 | font-size: 34rpx;
188 | font-weight: bold;
189 | }
190 |
191 | .name {
192 | font-size: 20rpx;
193 | }
194 |
195 | .small {
196 | font-size: 80%;
197 | }
198 | }
199 | }
200 | }
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/miniprogram/behaviors/page-lifetimes-behavior.ts:
--------------------------------------------------------------------------------
1 | import {logger} from '../app/core/logger'
2 | import {ParamsUtils} from '../app/utils/params-utils'
3 |
4 | export const pageLifetimesBehavior = Behavior({
5 | data: {
6 | /** 页面入参 */
7 | __page_query__: {},
8 |
9 | /** 页面完整路径 */
10 | __page_url__: '',
11 |
12 | /** 是否正在展示 loading 提示框 */
13 | __page_on_show_loading__: false,
14 |
15 | /** 是否正在发送请求 */
16 | __page_on_requesting__: false,
17 |
18 | /** 是否正在进行下拉刷新 */
19 | __page_on_pull_down_refresh__: false,
20 |
21 | /** 上一次进行数据刷新的时间戳 */
22 | __page_last_refresh_time__: 0,
23 | },
24 |
25 | methods: {
26 | /** 生命周期事件 —— 页面加载 */
27 | onLoad(query: Record) {
28 | this.pageInit(query)
29 | this.init()
30 | },
31 |
32 | /** 生命周期事件 —— 用户进行下拉刷新 */
33 | onPullDownRefresh() {
34 | // 如果发起请求时,间隔小于该时间,则直接使用旧数据
35 | const freeDuration = 10 * 1000
36 |
37 | if (Date.now() - this.data.__page_last_refresh_time__ < freeDuration) {
38 | setTimeout(() => {
39 | wx.stopPullDownRefresh()
40 | this._showSuccessToast()
41 | }, 1000)
42 | } else {
43 | this.setData({__page_on_pull_down_refresh__: true})
44 | this.init().then(() => {
45 | this._showSuccessToast()
46 | })
47 | }
48 | },
49 |
50 | /** 生命周期事件 —— 用户点击转发按钮 */
51 | onShareAppMessage() {
52 | return {
53 | title: '我最近一直在用这个小程序查天气,你也来试试吧!',
54 | path: 'pages/index/index',
55 | imageUrl: 'https://static.lifehelper.com.cn/static/project/share.jpeg',
56 | }
57 | },
58 |
59 | /** 页面初始化,页面生命周期内执行一次就行了 */
60 | pageInit(query: Record) {
61 | // 本地存储查询参数
62 | this.setData({__page_query__: query})
63 |
64 | // 计算并存储页面完整 URL
65 | const url = this.getPageUrl()
66 | this.setData({__page_url__: url})
67 | logger.debug('进入页面, url=' + url)
68 | },
69 |
70 | /** 页面数据初始化,在页面生命周期内可能多次调用 */
71 | async init() {
72 | this.setData({__page_on_requesting__: true})
73 | setTimeout(() => {
74 | if (this.data.__page_on_requesting__) {
75 | this._showLoading()
76 | }
77 | }, 2000)
78 |
79 | await this.start()
80 | this.setData({__page_on_requesting__: false})
81 | this._hideLoading()
82 | this._stopPullDownRefresh()
83 | this._saveRefreshTime()
84 | },
85 |
86 | /**
87 | * 获取某个查询参数的值
88 | */
89 | getQuery(key: string): string {
90 | return this.data.__page_query__[key]
91 | },
92 |
93 | /**
94 | * 获取页面路径(不带查询字符串)
95 | */
96 | getPagePath(): string {
97 | const pages = getCurrentPages()
98 | return pages[pages.length - 1].route
99 | },
100 |
101 | /**
102 | * 获取页面完成路径(带查询字符串)
103 | */
104 | getPageUrl(): string {
105 | const path = this.getPagePath()
106 | const qs = ParamsUtils.encode(this.data.__page_query__)
107 |
108 | return qs ? path + '?' + qs : path
109 | },
110 |
111 | /**
112 | * 显示 loading 提示框
113 | */
114 | _showLoading() {
115 | wx.showLoading({title: '数据加载中'}).then(() => {
116 | this.setData({__page_on_show_loading__: true})
117 | setTimeout(() => {
118 | if (this.data.__page_on_show_loading__) {
119 | this._hideLoading()
120 | }
121 | }, 5000)
122 | })
123 | },
124 |
125 | /**
126 | * 隐藏 loading 提示框
127 | */
128 | _hideLoading() {
129 | if (this.data.__page_on_show_loading__) {
130 | this.setData({__page_on_show_loading__: false})
131 | wx.hideLoading()
132 | }
133 | },
134 |
135 | /** 记录数据刷新时间 */
136 | _saveRefreshTime() {
137 | this.setData({__page_last_refresh_time__: Date.now()})
138 | },
139 |
140 | /** 停止下拉刷新 */
141 | _stopPullDownRefresh() {
142 | if (this.data.__page_on_pull_down_refresh__) {
143 | wx.stopPullDownRefresh()
144 | }
145 | },
146 |
147 | /** 弹出数据更新成功的提示 */
148 | _showSuccessToast() {
149 | wx.showToast({
150 | icon: 'success',
151 | title: '数据已更新',
152 | })
153 | },
154 | },
155 | })
156 |
--------------------------------------------------------------------------------
/miniprogram/pages/album/edit/edit.ts:
--------------------------------------------------------------------------------
1 | // pages/album/edit/edit.ts
2 | // 页面名称:相册编辑页
3 | // 主要用途:对相册进行“新增”、“编辑”操作时,用于填写相册的名称和描述等信息。
4 |
5 | // 主要逻辑(1):操作类型为新增还是删除
6 | // 根据页面路径参数,如果带有有效的 `album_id` 参数,则为编辑该相册,否则为新增。
7 |
8 | import {createAlbum, updateAlbum} from '../../../app/services/album'
9 | import {themeBehavior} from '../../../behaviors/theme-behavior'
10 |
11 | /** 操作类型:新增、编辑 */
12 | export type OperateType = 'add' | 'edit'
13 |
14 | /** 页面路径参数 */
15 | export interface PageQuery {
16 | /** 相册 ID */
17 | album_id?: string
18 | }
19 |
20 | /** 页面传值事件 */
21 | export const TRANSFER_VALUE_EVENT = 'TRANSFER_VALUE_EVENT'
22 |
23 | /** 页面传值数据 */
24 | export interface PageTransferedValue {
25 | /** 相册名称 */
26 | name: string
27 | /** 相册描述 */
28 | description: string
29 | }
30 |
31 | /** 新增相册情况的几处文案 */
32 | const AddTypeText = {
33 | /** 输入框的默认文案 */
34 | text1: '请输入相册名称',
35 | /** 底部操作按钮文案 */
36 | text2: '新建相册',
37 | }
38 |
39 | const EditTypeText = {
40 | /** 输入框的默认文案 */
41 | text1: '请输入相册名称',
42 | /** 底部操作按钮文案 */
43 | text2: '确定修改',
44 | }
45 |
46 | Page({
47 | data: {
48 | // =============================== 页面初始化数据 ================================
49 |
50 | /** 操作类型:新增(默认)、编辑 */
51 | type: 'add' as OperateType,
52 |
53 | /** 相册 ID,从页面路径参数中获取,可能为空 */
54 | albumId: '',
55 |
56 | /** 文本集合 */
57 | textWrapper: AddTypeText,
58 |
59 | /** 相册名称 */
60 | albumName: '',
61 |
62 | // ================================ 表单数据 ==================================
63 |
64 | /** 表单校验规则 */
65 | formRules: [
66 | {
67 | name: 'albumName',
68 | rules: [
69 | {required: true, message: '请填写相册名称'},
70 | {maxlength: 10, message: '相册名称最长10个字哦'},
71 | ],
72 | },
73 | ],
74 |
75 | /** 表单数据 */
76 | formData: {
77 | albumName: '',
78 | },
79 |
80 | // =============================== 页面状态数据 =================================
81 |
82 | /** 顶部错误提示组件文案 */
83 | error: '',
84 | },
85 |
86 | behaviors: [themeBehavior],
87 |
88 | onLoad(query: PageQuery) {
89 | if (query.album_id) {
90 | // 页面路径包含 `album_id` 参数表示当前“编辑”相册情况
91 |
92 | this.setData({
93 | type: 'edit',
94 | albumId: query.album_id,
95 | textWrapper: EditTypeText,
96 | })
97 |
98 | wx.setNavigationBarTitle({title: '编辑相册'})
99 |
100 | // -- 备注(2022.09.20) --
101 | // 目前设定:“编辑相册”情况不会从外链直接跳入,而是从相册详情页跳转进入,因此待编辑的相册数据由上个页面通过事件通道传
102 | // 递,不需要发起 HTTP 请求获取。若该设定发生变化,后续需要调整为通过 `albumId` 参数发情 HTTP 请求获取待编辑的相册信息。
103 | const eventChannel = this.getOpenerEventChannel()
104 | if (typeof eventChannel.on === 'function') {
105 | eventChannel.on(TRANSFER_VALUE_EVENT, (data: PageTransferedValue) => {
106 | this.setData({
107 | albumName: data.name,
108 | })
109 | })
110 | }
111 | } else {
112 | // 页面路径不包含 `album_id` 参数表示当前“新增”相册情况
113 | // 目前设定只有“新增”和“编辑”两种情况,因此这些逻辑就直接写在 `else` 分支中了
114 |
115 | this.setData({type: 'add', textWrapper: AddTypeText})
116 | wx.setNavigationBarTitle({title: '创建相册'})
117 | }
118 | },
119 |
120 | /** 处理输入框输入变化事件 */
121 | onAlbumNameInputChange(e: any) {
122 | this.setData({[`formData.albumName`]: e.detail.value})
123 | },
124 |
125 | /** 处理点击“提交”按钮事件 */
126 | onSubmitTap() {
127 | // 表单校验
128 | this.selectComponent('#form').validate((isValid: boolean, errors: any[]) => {
129 | if (!isValid) {
130 | this.setData({error: errors[0].message})
131 | } else {
132 | const albumName = this.data.formData.albumName
133 |
134 | if (this.data.type === 'add') {
135 | createAlbum({name: albumName})
136 | wx.showToast({
137 | title: '创建成功',
138 | icon: 'success',
139 | })
140 | } else if (this.data.type === 'edit') {
141 | const id = this.data.albumId
142 | updateAlbum(id, {name: albumName})
143 | wx.showToast({
144 | title: '修改成功',
145 | icon: 'success',
146 | })
147 | }
148 |
149 | setTimeout(() => {
150 | wx.navigateBack()
151 | }, 1000)
152 | }
153 | })
154 | },
155 | })
156 |
--------------------------------------------------------------------------------
/miniprogram/app/http/aliyun-apigw-signature-interceptor.ts:
--------------------------------------------------------------------------------
1 | import { AxiosRequestConfig } from 'axios'
2 | import * as CryptoJS from 'crypto-js'
3 | import { ParamsUtils } from '../utils/params-utils'
4 | import { handleMethod, Method } from './miniprogram-adatper'
5 |
6 | /**
7 | * 规格化请求头
8 | *
9 | * [说明]
10 | * 1. 将字段名统一变为小写
11 | * 2. 去除重复的请求头
12 | * @param {Record} headers
13 | */
14 | function normalizeHeaders(headers: Record): Record {
15 | if (typeof headers !== 'object') {
16 | return {}
17 | }
18 |
19 | /** 放在 `config.headers` 中但是非正常请求头字段的字段 */
20 | const axiosExtraHeaderFieldList = ['common', 'delete', 'get', 'head', 'patch', 'post', 'put']
21 | const result: Record = {}
22 |
23 | Object.keys(headers).forEach((name: string) => {
24 | const lowerName = name.toLowerCase()
25 | if (!result[lowerName] && headers[name] && !axiosExtraHeaderFieldList.includes(name)) {
26 | result[lowerName] = headers[name]
27 | }
28 | })
29 |
30 | return result
31 | }
32 |
33 | /**
34 | * 获取参与签名的请求头列表
35 | * @param {Record} headers 请求头
36 | */
37 | function getSignHeaderKeys(headers: Record): string[] {
38 | /** 不参与 Header 签名的请求头 */
39 | const EXCLUDE_SIGN_HEADERS = ['x-ca-signature', 'x-xa-signature-headers', 'accept', 'content-md5', 'content-type', 'date']
40 |
41 | return Object.keys(headers)
42 | .filter((name: string) => {
43 | return !EXCLUDE_SIGN_HEADERS.includes(name)
44 | })
45 | .sort()
46 | }
47 |
48 | function getSignedHeadersString(signHeaderKeys: string[], headers: Record): string {
49 | return signHeaderKeys
50 | .map((key: string) => {
51 | const value = headers[key]
52 | return key + ':' + (value ? value : '')
53 | })
54 | .join('\n')
55 | }
56 |
57 | function getPathAndParams(url: string): string {
58 | const urlRaw = url.replace('https://', '').replace('http://', '')
59 | return urlRaw.substr(urlRaw.indexOf('/'))
60 | }
61 |
62 | function md5(content: string): string {
63 | return CryptoJS.MD5(content).toString(CryptoJS.enc.Base64)
64 | }
65 |
66 | function buildStringToSign(method: Method, headers: Record, signedHeadersString: string, pathAndParams: string) {
67 | const lf = '\n'
68 | const list: string[] = [method.toUpperCase(), lf]
69 |
70 | const arr = ['accept', 'content-md5', 'content-type', 'date']
71 | for (let i = 0; i < arr.length; i++) {
72 | const key = arr[i]
73 | if (headers[key]) {
74 | list.push(headers[key])
75 | }
76 | list.push(lf)
77 | }
78 |
79 | if (signedHeadersString) {
80 | list.push(signedHeadersString)
81 | list.push(lf)
82 | }
83 |
84 | if (pathAndParams) {
85 | list.push(pathAndParams)
86 | }
87 |
88 | return list.join('')
89 | }
90 |
91 | /**
92 | * 阿里云 API 网关签名拦截器
93 | * @see https://help.aliyun.com/document_detail/29475.html
94 | */
95 | export function aliyunApigwSignatureInterceptorBuilder(appKey: string, appSecret: string, debug = true) {
96 | return function aliyunApigwSignatureInterceptor(config: AxiosRequestConfig): AxiosRequestConfig {
97 | const headers = normalizeHeaders(config.headers)
98 |
99 | // 给请求头添加一些要求添加的字段
100 | headers['x-ca-key'] = appKey
101 | headers['x-ca-timestamp'] = Date.now().toString()
102 | headers['accept'] = headers['accept'] || '*/*'
103 | headers['content-type'] = headers['content-type'] || 'application/json'
104 |
105 | // 该请求头要求为一个随机字符串,理论上使用 `UUID` 更好,为了少引入依赖,使用这种方法
106 | headers['x-ca-nonce'] = CryptoJS.MD5(Date.now().toString() + Math.random() * 10000).toString(CryptoJS.enc.Hex)
107 |
108 | if (config.data) {
109 | headers['content-md5'] = md5(JSON.stringify(config.data))
110 | }
111 |
112 | const signHeaderKeys = getSignHeaderKeys(headers)
113 | headers['x-ca-signature-headers'] = signHeaderKeys.join(',')
114 |
115 | const querystring = ParamsUtils.encode(config.params)
116 | const wholeUrl = (config.baseURL || '') + (config.url || '') + (querystring ? '?' + querystring : '')
117 | const pathAndParams = getPathAndParams(wholeUrl)
118 | const signedHeadersString = getSignedHeadersString(signHeaderKeys, headers)
119 | const stringToSign = buildStringToSign(handleMethod(config.method), headers, signedHeadersString, pathAndParams)
120 | headers['x-ca-signature'] = CryptoJS.HmacSHA256(stringToSign, appSecret).toString(CryptoJS.enc.Base64)
121 |
122 | if (debug) {
123 | console.info(`当前签名字符串:\`${stringToSign.replace(/\n/g, '#')}\``)
124 | }
125 |
126 | config.headers = headers
127 | return config
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/miniprogram/assets/styles/weather.scss:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // ------------------ 天气类样式,包含实时天气背景图、空气质量指数 -------------------
3 | // ----------------------------------------------------------------------------
4 |
5 | // =================================== 天气背景 ===================================
6 | [data-weui-theme="light"] .weather-bg {
7 | background-repeat: no-repeat;
8 | background-position: 0 5%;
9 | background-size: 100% auto;
10 |
11 | &--sun {
12 | background-image: linear-gradient(225deg, #fee5ca, #e9f0ff 55%, #dce3fb);
13 | }
14 |
15 | &--cloudy {
16 | background-image: url("https://static.lifehelper.com.cn/qweather/bg/cloudy_day.png"),
17 | linear-gradient(225deg, #d0dae8, #ccd4df 55%, #a5b3c5);
18 | }
19 |
20 | &--rain {
21 | background-image: url("https://static.lifehelper.com.cn/qweather/bg/rain_day.png"),
22 | linear-gradient(225deg, #dbebff, #dde5f4 55%, #becddd);
23 | }
24 |
25 | &--snow {
26 | background-image: url("https://static.lifehelper.com.cn/qweather/bg/snow_day.png"),
27 | linear-gradient(225deg, #d2f0fd, #e6f8fe 55%, #d6eded);
28 | }
29 |
30 | &--haze {
31 | background-image: url("https://static.lifehelper.com.cn/qweather/bg/haze_day.png"),
32 | linear-gradient(225deg, #b29c82, #ded9d7 64.29%, #d9d7dc);
33 | }
34 | }
35 |
36 | [data-weui-theme="dark"] .weather-bg {
37 | background-repeat: no-repeat;
38 | background-position: 0 5%;
39 | background-size: 100% auto;
40 |
41 | &--sun {
42 | background-image: linear-gradient(225deg, #2a427f, #1b2751);
43 | }
44 |
45 | &--cloudy {
46 | background-image: url("https://static.lifehelper.com.cn/qweather/bg/cloudy_night.png"),
47 | linear-gradient(225deg, #8089a5, #424c66);
48 | }
49 |
50 | &--rain {
51 | background-image: url("https://static.lifehelper.com.cn/qweather/bg/rain_night.png"),
52 | linear-gradient(225deg, #2c4a77, #1f3b59);
53 | }
54 |
55 | &--snow {
56 | background-image: url("https://static.lifehelper.com.cn/qweather/bg/snow_night.png"),
57 | linear-gradient(225deg, #18779e, #0d3f54);
58 | }
59 |
60 | &--haze {
61 | background-image: url("https://static.lifehelper.com.cn/qweather/bg/haze_night.png"),
62 | linear-gradient(225deg, #8f8d52, #2b333c);
63 | }
64 | }
65 |
66 | // ================================= 空气质量背景颜色 =================================
67 | .aqi {
68 | color: #fff;
69 |
70 | &-level--1 {
71 | background-color: #95b359;
72 | }
73 |
74 | &-level--2 {
75 | background-color: #a9a538;
76 | }
77 |
78 | &-level--3 {
79 | background-color: #e0991d;
80 | }
81 |
82 | &-level--4 {
83 | background-color: #d96161;
84 | }
85 |
86 | &-level--5 {
87 | background-color: #a257d0;
88 | }
89 |
90 | &-level--6 {
91 | background-color: #d94371;
92 | }
93 | }
94 |
95 | // ================================= 天气预警条背景颜色 =================================
96 | .warning {
97 | padding: 4rpx 30rpx 4rpx 24rpx;
98 |
99 | border-radius: 24rpx;
100 |
101 | &-level--0 {
102 | background-color: var(--weui-RED);
103 | }
104 |
105 | &-level--1 {
106 | background-color: var(--weui-RED);
107 | }
108 |
109 | &-level--2 {
110 | background-color: var(--weui-ORANGE);
111 | }
112 |
113 | &-level--3 {
114 | background-color: var(--weui-YELLOW);
115 | }
116 |
117 | &-level--4 {
118 | background-color: var(--weui-BLUE);
119 | }
120 |
121 | &-level--5 {
122 | background-color: var(--weui-RED);
123 | }
124 | }
125 |
126 | // ================================= 生活指数等级颜色 =================================
127 | .living-level {
128 | font-size: 28rpx;
129 | margin-right: 20rpx;
130 | padding: 6rpx 20rpx;
131 | color: var(--weui-WHITE);
132 | border-radius: 10rpx;
133 |
134 | &--1 {
135 | background-color: var(--weui-GREEN);
136 | }
137 |
138 | &--2 {
139 | background-color: var(--weui-LIGHTGREEN);
140 | }
141 |
142 | &--3 {
143 | background-color: var(--weui-BLUE);
144 | }
145 |
146 | &--4 {
147 | background-color: var(--weui-INDIGO);
148 | }
149 |
150 | &--5 {
151 | background-color: var(--weui-YELLOW);
152 | }
153 |
154 | &--6 {
155 | background-color: var(--weui-ORANGE);
156 | }
157 |
158 | &--7, &--7 {
159 | background-color: var(--weui-RED);
160 | }
161 |
162 | }
163 |
164 | // 降水相关
165 | [data-weui-theme="light"] .rain-container{
166 | background-color: #d7effb;
167 |
168 | .rain-bar{
169 | background-color: #5077fe;
170 | }
171 | }
172 |
173 | [data-weui-theme="dark"] .rain-container{
174 | background-color: #1e385d;
175 |
176 | .rain-bar{
177 | background-color: #a7c8f1;
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/miniprogram/app/services/album.ts:
--------------------------------------------------------------------------------
1 | import {requestForData} from '../core/http'
2 | import {getOssPostCredential, uploadToOss} from './oss'
3 |
4 | /** 相册 */
5 | export interface Album {
6 | /** 相册 ID */
7 | id: string
8 |
9 | /** 相册名称 */
10 | name: string
11 |
12 | /** 相册描述 */
13 | description: string
14 |
15 | /** 创建时间 */
16 | createTime: number
17 |
18 | /** 更新时间 */
19 | updateTime: number
20 |
21 | /** 资源(照片和视频)数量 */
22 | count: number
23 |
24 | /** 相册文件总大小(单位:B) */
25 | size: number
26 |
27 | /** 相册封面图地址 */
28 | coverImageUrl: string
29 |
30 | /** 媒体文件列表,仅查看相册详情时返回 */
31 | medias: Media[]
32 |
33 | /** 图片总数 */
34 | imageCount: number
35 |
36 | /** 视频总数 */
37 | videoCount: number
38 | }
39 |
40 | /** 媒体文件(照片和视频) */
41 | export interface Media {
42 | /** 媒体文件 ID */
43 | id: string
44 |
45 | /**
46 | * 类型
47 | *
48 | * 1. `image` => 图片
49 | * 2. `video` => 视频
50 | */
51 | type: string
52 |
53 | /** 文件地址 */
54 | url: string
55 |
56 | /** 上传时间 */
57 | uploadTime: number
58 |
59 | /** 视频缩略图地址 */
60 | thumbUrl: string
61 | }
62 |
63 | /** 创建或修改接口需要的相册数据 */
64 | export interface ModifyAlbumData {
65 | /** 相册名称 */
66 | name: string
67 | }
68 |
69 | /** 获取相册列表接口响应数据 */
70 | export interface AlbumListResponse {
71 | list: Album[]
72 |
73 | /** 所有相册的资源数量之和 */
74 | totalCount: number
75 |
76 | /** 所有资源的文件大小之和(单位:B) */
77 | totalSize: number
78 | }
79 |
80 | /** 删除相册接口响应数据 */
81 | export interface DeleteAlbumResponse {
82 | /** 相册 ID */
83 | id: string
84 | }
85 |
86 | /**
87 | * 获取相册列表
88 | */
89 | export function getAlbumList(): Promise {
90 | return requestForData({
91 | method: 'GET',
92 | url: '/albums',
93 | auth: true,
94 | })
95 | }
96 |
97 | /**
98 | * 创建一个相册
99 | */
100 | export function createAlbum(album: ModifyAlbumData): Promise {
101 | return requestForData({
102 | method: 'POST',
103 | url: '/album',
104 | auth: true,
105 | data: album,
106 | })
107 | }
108 |
109 | /**
110 | * 修改相册信息
111 | */
112 | export function updateAlbum(id: string, album: ModifyAlbumData): Promise {
113 | return requestForData({
114 | method: 'PUT',
115 | url: `/album/${id}`,
116 | auth: true,
117 | data: album,
118 | })
119 | }
120 |
121 | /**
122 | * 删除一个相册
123 | */
124 | export function deleteAlbum(id: string): Promise {
125 | return requestForData({
126 | method: 'DELETE',
127 | url: `/album/${id}`,
128 | auth: true,
129 | })
130 | }
131 |
132 | /**
133 | * 获取包含媒体文件列表的相册详情
134 | *
135 | * @param id 相册 ID
136 | */
137 | export function getAlbumDetail(id: string): Promise {
138 | return requestForData({
139 | method: 'GET',
140 | url: `/album/${id}`,
141 | auth: true,
142 | })
143 | }
144 |
145 | /**
146 | * 删除媒体资源
147 | *
148 | * @param albumId 相册 ID
149 | * @param mediaId 媒体资源 ID
150 | */
151 | export function deleteMedia(albumId: string, mediaId: string) {
152 | return requestForData({
153 | method: 'DELETE',
154 | url: `/album/${albumId}/media/${mediaId}`,
155 | auth: true,
156 | })
157 | }
158 |
159 | /**
160 | * 上传媒体文件至 OSS
161 | *
162 | * @param albumId 相册 ID
163 | * @param media 从微信选取的媒体文件
164 | * @param onProgressUpdate 进度变化回调函数
165 | */
166 | export async function uploadMediaFile(
167 | albumId: string,
168 | media: WechatMiniprogram.MediaFile,
169 | onProgressUpdate: WechatMiniprogram.UploadTaskOnProgressUpdateCallback
170 | ): Promise {
171 | if (media.fileType === 'image') {
172 | const credential = await getOssPostCredential('image')
173 |
174 | uploadToOss(media.tempFilePath, credential).onProgressUpdate((res) => {
175 | if (res.progress === 100) {
176 | requestForData({
177 | method: 'POST',
178 | url: `/album/${albumId}/media`,
179 | params: {type: 'image'},
180 | auth: true,
181 | data: {
182 | path: credential.key,
183 | size: media.size,
184 | uploadTime: Date.now(),
185 | },
186 | })
187 | }
188 |
189 | onProgressUpdate(res)
190 | })
191 | } else if (media.fileType === 'video') {
192 | const [c1, c2] = await Promise.all([getOssPostCredential('image'), getOssPostCredential('video')])
193 |
194 | // 上传封面图
195 | uploadToOss(media.thumbTempFilePath, c1)
196 |
197 | // 备注(2022.09.07)
198 | // 这里假定了上传封面图一定会成功,临时先这么处理了。
199 |
200 | // 上传视频
201 | uploadToOss(media.tempFilePath, c2).onProgressUpdate((res) => {
202 | if (res.progress === 100) {
203 | requestForData({
204 | method: 'POST',
205 | url: `/album/${albumId}/media`,
206 | params: {type: 'video'},
207 | auth: true,
208 | data: {
209 | path: c2.key,
210 | size: media.size,
211 | uploadTime: Date.now(),
212 | width: media.width,
213 | height: media.height,
214 | thumbPath: c1.key,
215 | duration: media.duration,
216 | },
217 | })
218 | }
219 |
220 | onProgressUpdate(res)
221 | })
222 | } else {
223 | throw new Error('未支持的媒体类型!')
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/cssconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "exclude": ["node_modules/**"],
3 | "always-semicolon": true,
4 | "block-indent": 2,
5 | "color-case": "lower",
6 | "color-shorthand": true,
7 | "element-case": "lower",
8 | "eof-newline": true,
9 | "leading-zero": false,
10 | "quotes": "double",
11 | "remove-empty-rulesets": true,
12 | "space-before-colon": "",
13 | "space-after-colon": 1,
14 | "space-before-combinator": 1,
15 | "space-after-combinator": 1,
16 | "space-between-declarations": "\n",
17 | "space-after-opening-brace": "\n",
18 | "space-before-opening-brace": "1",
19 | "space-before-closing-brace": "\n",
20 | "space-before-selector-delimiter": "",
21 | "space-after-selector-delimiter": 1,
22 | "strip-spaces": true,
23 | "unitless-zero": true,
24 | "vendor-prefix-align": true,
25 | "lines-between-rulesets": 1,
26 | "sort-order": [
27 | "font",
28 | "font-family",
29 | "font-size",
30 | "font-weight",
31 | "font-style",
32 | "font-variant",
33 | "font-size-adjust",
34 | "font-stretch",
35 | "font-effect",
36 | "font-emphasize",
37 | "font-emphasize-position",
38 | "font-emphasize-style",
39 | "font-smooth",
40 | "line-height",
41 | "position",
42 | "z-index",
43 | "top",
44 | "right",
45 | "bottom",
46 | "left",
47 | "display",
48 | "visibility",
49 | "float",
50 | "clear",
51 | "overflow",
52 | "overflow-x",
53 | "overflow-y",
54 | "clip",
55 | "zoom",
56 | "align-content",
57 | "align-items",
58 | "align-self",
59 | "flex",
60 | "flex-flow",
61 | "flex-basis",
62 | "flex-direction",
63 | "flex-grow",
64 | "flex-shrink",
65 | "flex-wrap",
66 | "justify-content",
67 | "order",
68 | "box-sizing",
69 | "width",
70 | "min-width",
71 | "max-width",
72 | "height",
73 | "min-height",
74 | "max-height",
75 | "margin",
76 | "margin-top",
77 | "margin-right",
78 | "margin-bottom",
79 | "margin-left",
80 | "padding",
81 | "padding-top",
82 | "padding-right",
83 | "padding-bottom",
84 | "padding-left",
85 | "table-layout",
86 | "empty-cells",
87 | "caption-side",
88 | "border-spacing",
89 | "border-collapse",
90 | "list-style",
91 | "list-style-position",
92 | "list-style-type",
93 | "list-style-image",
94 | "content",
95 | "quotes",
96 | "counter-reset",
97 | "counter-increment",
98 | "resize",
99 | "cursor",
100 | "user-select",
101 | "nav-index",
102 | "nav-up",
103 | "nav-right",
104 | "nav-down",
105 | "nav-left",
106 | "transition",
107 | "transition-delay",
108 | "transition-timing-function",
109 | "transition-duration",
110 | "transition-property",
111 | "transform",
112 | "transform-origin",
113 | "animation",
114 | "animation-name",
115 | "animation-duration",
116 | "animation-play-state",
117 | "animation-timing-function",
118 | "animation-delay",
119 | "animation-iteration-count",
120 | "animation-direction",
121 | "text-align",
122 | "text-align-last",
123 | "vertical-align",
124 | "white-space",
125 | "text-decoration",
126 | "text-emphasis",
127 | "text-emphasis-color",
128 | "text-emphasis-style",
129 | "text-emphasis-position",
130 | "text-indent",
131 | "text-justify",
132 | "letter-spacing",
133 | "word-spacing",
134 | "text-outline",
135 | "text-transform",
136 | "text-wrap",
137 | "text-overflow",
138 | "text-overflow-ellipsis",
139 | "text-overflow-mode",
140 | "word-wrap",
141 | "word-break",
142 | "tab-size",
143 | "hyphens",
144 | "pointer-events",
145 | "opacity",
146 | "color",
147 | "border",
148 | "border-width",
149 | "border-style",
150 | "border-color",
151 | "border-top",
152 | "border-top-width",
153 | "border-top-style",
154 | "border-top-color",
155 | "border-right",
156 | "border-right-width",
157 | "border-right-style",
158 | "border-right-color",
159 | "border-bottom",
160 | "border-bottom-width",
161 | "border-bottom-style",
162 | "border-bottom-color",
163 | "border-left",
164 | "border-left-width",
165 | "border-left-style",
166 | "border-left-color",
167 | "border-radius",
168 | "border-top-left-radius",
169 | "border-top-right-radius",
170 | "border-bottom-right-radius",
171 | "border-bottom-left-radius",
172 | "border-image",
173 | "border-image-source",
174 | "border-image-slice",
175 | "border-image-width",
176 | "border-image-outset",
177 | "border-image-repeat",
178 | "outline",
179 | "outline-width",
180 | "outline-style",
181 | "outline-color",
182 | "outline-offset",
183 | "background",
184 | "background-color",
185 | "background-image",
186 | "background-repeat",
187 | "background-attachment",
188 | "background-position",
189 | "background-position-x",
190 | "background-position-y",
191 | "background-clip",
192 | "background-origin",
193 | "background-size",
194 | "box-decoration-break",
195 | "box-shadow",
196 | "text-shadow"
197 | ]
198 | }
199 |
--------------------------------------------------------------------------------
/miniprogram/pages/great-day/edit/edit.ts:
--------------------------------------------------------------------------------
1 | // pages/great-day/edit/edit.ts
2 |
3 | import {PageChannelEvent} from '../../../app/core/constant'
4 | import {createGreatDay, getDateText, getEmojiList, GreatDay, updateGreatDay} from '../../../app/services/great-day'
5 | import {Id} from '../../../app/utils/types'
6 | import {showSingleButtonModel} from '../../../app/utils/wx'
7 | import {themeBehavior} from '../../../behaviors/theme-behavior'
8 |
9 | Page({
10 | data: {
11 | // ============================= 从HTTP请求获取的数据 =============================
12 |
13 | /** 可选择的 emoji 列表 */
14 | emojis: [] as string[],
15 |
16 | // ================================ 页面状态数据 ================================
17 |
18 | /** 操作类型:新增、编辑 */
19 | type: 'create' as 'create' | 'update',
20 |
21 | /** 提交按钮文案 */
22 | submitButtonText: '创建',
23 |
24 | /** 提交按钮是否带 loading 图标 */
25 | submitButtonLoading: false,
26 |
27 | /** 提交按钮是否禁用 */
28 | submitButtonDisabled: false,
29 |
30 | // ================================ 页面传值数据 ================================
31 |
32 | /**
33 | * 纪念日 ID
34 | *
35 | * ### 说明
36 | * 该值从路径参数获取,有该值说明是“编辑”操作,否则新增。
37 | */
38 | id: '',
39 |
40 | /**
41 | * 详情数据(“编辑”情况用到)
42 | */
43 | day: {} as GreatDay,
44 |
45 | // ================================= 表单数据 =================================
46 |
47 | /** 纪念日名称 */
48 | name: '',
49 |
50 | /** 日期 */
51 | date: '',
52 |
53 | /** emoji 表情 */
54 | icon: '',
55 |
56 | // ================================ 展示专用数据 ================================
57 |
58 | /** 格式化显示的日期 */
59 | formattedDate: '',
60 | },
61 |
62 | behaviors: [themeBehavior],
63 |
64 | onLoad(query: Record) {
65 | const id = (query as unknown as Id).id
66 | if (id) {
67 | // 带了 id 说明是“编辑”情况,目前唯一入口是“详情页”跳转过来
68 | this.setData({id, type: 'update', submitButtonText: '保存'})
69 |
70 | this.getOpenerEventChannel().on(PageChannelEvent.DATA_TRANSFER, (data) => {
71 | this.setData(data)
72 |
73 | const {name, date, icon, formattedDate} = this.data.day
74 | this.setData({name, date, icon, formattedDate})
75 | })
76 | }
77 |
78 | this.init()
79 | },
80 |
81 | /** 页面初始化方法 */
82 | async init() {
83 | wx.showNavigationBarLoading()
84 | await this.getEmojis()
85 | wx.hideNavigationBarLoading()
86 |
87 | if (this.data.type === 'create') {
88 | this.changeEmoji()
89 | }
90 |
91 | if (this.data.type === 'update') {
92 | //
93 | }
94 | },
95 |
96 | /** 获取 emoji 列表 */
97 | async getEmojis(): Promise {
98 | const list = await getEmojiList()
99 | this.setData({emojis: list})
100 | },
101 |
102 | /** 换一个 emoji */
103 | changeEmoji(): void {
104 | const list = this.data.emojis
105 | const icon = list[Math.floor(Math.random() * list.length)]
106 | this.setData({icon})
107 | wx.setNavigationBarTitle({title: icon})
108 | },
109 |
110 | /** 处理名称输入框失去焦点事件 */
111 | handleNameInputBlur(e: WechatMiniprogram.CustomEvent<{value: string}>) {
112 | const name = e.detail.value
113 | this.setData({name})
114 | },
115 |
116 | /** 处理日期选择器修改事件 */
117 | handleDatePickerChange(e: WechatMiniprogram.CustomEvent<{value: string}>) {
118 | const date = e.detail.value
119 | const formattedDate = getDateText(date)
120 | this.setData({date, formattedDate})
121 | },
122 |
123 | /** 点击“提交”按钮 */
124 | async submit() {
125 | const {id, name, date, icon} = this.data
126 |
127 | if (!name) {
128 | showSingleButtonModel('请输入事件名称!')
129 | } else if (!date) {
130 | showSingleButtonModel('请选择日期!')
131 | } else {
132 | if (this.data.type === 'create') {
133 | // 提交按钮状态变更
134 | this.setData({
135 | submitButtonText: '正在创建 ...',
136 | submitButtonLoading: true,
137 | submitButtonDisabled: true,
138 | })
139 |
140 | await createGreatDay({name, date, icon})
141 | }
142 |
143 | if (this.data.type === 'update') {
144 | // 提交按钮状态变更
145 | this.setData({
146 | submitButtonText: '正在保存 ...',
147 | submitButtonLoading: true,
148 | submitButtonDisabled: true,
149 | })
150 |
151 | if (this.data.day.systemCreated) {
152 | // 系统的默认数据,允许操作编辑,已“新增”的形式提交
153 | await createGreatDay({name, date, icon})
154 |
155 | // 通过上个页面刷新数据
156 | this.getOpenerEventChannel().emit(PageChannelEvent.REFRESH_DATA)
157 |
158 | // 成功提示然后跳转返回
159 | wx.showToast({
160 | title: '恭喜创建您的第一个纪念日',
161 | icon: 'none',
162 | })
163 |
164 | // 1秒后再返回,否则显得太快
165 | setTimeout(() => {
166 | wx.switchTab({url: '/pages/great-day/list/list'})
167 | }, 1000)
168 | } else {
169 | await updateGreatDay(id, {name, date, icon})
170 |
171 | // 通过上个页面刷新数据
172 | this.getOpenerEventChannel().emit(PageChannelEvent.REFRESH_DATA)
173 |
174 | // 成功提示然后跳转返回
175 | wx.showToast({
176 | title: '保存成功',
177 | icon: 'success',
178 | })
179 |
180 | // 1秒后再返回,否则显得太快
181 | setTimeout(() => {
182 | wx.navigateBack()
183 | }, 1000)
184 | }
185 | }
186 | }
187 | },
188 | })
189 |
--------------------------------------------------------------------------------
/miniprogram/pages/album/detail/detail.ts:
--------------------------------------------------------------------------------
1 | // pages/album/detail/detail.ts
2 | // 相册详情页
3 |
4 | import {Album, deleteMedia, getAlbumDetail, uploadMediaFile, deleteAlbum} from '../../../app/services/album'
5 | import {themeBehavior} from '../../../behaviors/theme-behavior'
6 |
7 | // 注意事项(1)
8 | // 相册 ID 必须在页面路径中传参,因为该页面可能直接从外部链接进入。
9 |
10 | /** 页面路径参数 */
11 | export interface QueryParams {
12 | /** 相册 ID */
13 | id: string
14 | }
15 |
16 | /** 页面传值事件 */
17 | const TRANSFER_VALUE_EVENT = 'TRANSFER_VALUE_EVENT'
18 |
19 | Page({
20 | data: {
21 | // ================================ 页面传参数据 ================================
22 |
23 | /** 相册 ID,从页面路径参数获取 */
24 | albumId: '',
25 |
26 | // ============================= HTTP 请求获取的数据 =============================
27 |
28 | /** 相册详情 */
29 | albumDetail: {} as Album,
30 |
31 | // ====================== HTTP 请求获取的数据二次计算后的数据 =======================
32 |
33 | /** 相册占用空间 */
34 | sizeMB: 0,
35 |
36 | // ================================ 页面状态数据 ================================
37 |
38 | /** 当前正要操作的媒体资源索引值 */
39 | currentIndex: 0,
40 |
41 | /** 是否展示顶部提示条 */
42 | showTopTips: false,
43 |
44 | // ================= ActionSheet 组件数据 =================
45 |
46 | /** 是否展示底部操作按钮组件 */
47 | showActionSheet: false,
48 |
49 | actionSheetActions: [
50 | {text: '预览', value: 'preview'},
51 | {text: '删除', type: 'warn', value: 'delete'},
52 | ],
53 | },
54 |
55 | behaviors: [themeBehavior],
56 |
57 | /**
58 | * ## 页面说明
59 | * 进入页面必须携带参数 `id` - 相册 ID
60 | */
61 | onLoad(query: Record) {
62 | const albumId = (query as unknown as QueryParams).id
63 | this.setData({albumId})
64 |
65 | this.init()
66 | },
67 |
68 | /** 页面初始化 */
69 | async init() {
70 | const albumId = this.data.albumId
71 | const album = await getAlbumDetail(albumId)
72 | const sizeMB = Math.ceil(album.size / (1024 * 1024))
73 | this.setData({albumDetail: album, sizeMB})
74 |
75 | wx.setNavigationBarTitle({title: album.name})
76 | },
77 |
78 | /** 选择并上传照片(视频) */
79 | async chooseAndUpload() {
80 | const albumId = this.data.albumId
81 |
82 | const result = await wx.chooseMedia({
83 | maxDuration: 60,
84 | })
85 |
86 | // 使用顶部提示条进行操作提示
87 | this.setData({showTopTips: true})
88 |
89 | result.tempFiles.forEach((item) => {
90 | uploadMediaFile(albumId, item, (res) => {
91 | if (res.progress === 100) {
92 | setTimeout(() => {
93 | this.init()
94 | }, 500)
95 | }
96 | })
97 | })
98 | },
99 |
100 | /** 处理媒体文件点击事件 */
101 | onMediaItemTap(e: any) {
102 | const index = e.currentTarget.dataset.index
103 |
104 | this.setData({
105 | currentIndex: index,
106 | showActionSheet: true,
107 | })
108 | },
109 |
110 | /** 处理 ActionSheet 组件点击事件 */
111 | async onActionTap(e: any) {
112 | const operator = e.detail.value as 'preview' | 'delete'
113 | const index = this.data.currentIndex
114 | const albumId = this.data.albumId
115 |
116 | this.setData({showActionSheet: false})
117 |
118 | if (operator === 'preview') {
119 | const list = this.getPreviewList()
120 | wx.previewMedia({
121 | sources: list,
122 | current: index,
123 | })
124 | } else if (operator === 'delete') {
125 | const result = await wx.showModal({
126 | title: '提示',
127 | content: '是否确认删除?',
128 | confirmText: '确认删除',
129 | confirmColor: '#fa5151',
130 | })
131 |
132 | if (result.confirm) {
133 | // 删除资源
134 | const albumDetail = this.data.albumDetail
135 | const medias = albumDetail.medias
136 | const mediaId = medias[index].id
137 | medias.splice(index, 1)
138 | albumDetail.medias = medias
139 | this.setData({albumDetail})
140 | deleteMedia(albumId, mediaId)
141 | wx.showToast({
142 | title: '删除成功',
143 | icon: 'success',
144 | })
145 | }
146 | }
147 | },
148 |
149 | /** 获取预览列表 */
150 | getPreviewList(): WechatMiniprogram.MediaSource[] {
151 | return this.data.albumDetail.medias.map((item) => {
152 | return {
153 | url: item.url,
154 | type: item.type as 'image' | 'video',
155 | poster: item.type === 'video' ? item.thumbUrl : undefined,
156 | }
157 | })
158 | },
159 |
160 | /** 处理“编辑”按钮点击事件 */
161 | onEditButtonTap() {
162 | const albumId = this.data.albumId
163 | const albumName = this.data.albumDetail.name
164 |
165 | wx.navigateTo({
166 | url: `/pages/album/edit/edit?album_id=${albumId}`,
167 | success(res) {
168 | res.eventChannel.emit(TRANSFER_VALUE_EVENT, {name: albumName})
169 | },
170 | })
171 | },
172 |
173 | /** 操作删除相册 */
174 | async deleteAlbum() {
175 | // 相册中资源不为空时则不允许删除
176 | if (this.data.albumDetail.medias?.length > 0) {
177 | wx.showModal({
178 | title: '提示',
179 | content: '当前相册不为空,无法删除!若需删除,请先清空相册内的照片和视频。',
180 | showCancel: false,
181 | confirmText: '我知道了',
182 | })
183 | } else {
184 | const albumName = this.data.albumDetail.name
185 |
186 | const result = await wx.showModal({
187 | title: '提示',
188 | content: `是否删除相册 ${albumName} ?`,
189 | showCancel: true,
190 | confirmText: '确认删除',
191 | confirmColor: '#fa5151',
192 | cancelText: '暂不删除',
193 | })
194 |
195 | if (result.confirm) {
196 | const albumId = this.data.albumId
197 | deleteAlbum(albumId)
198 |
199 | wx.showToast({
200 | title: '删除成功',
201 | icon: 'success',
202 | })
203 |
204 | setTimeout(() => {
205 | wx.navigateBack()
206 | }, 1500)
207 | }
208 | }
209 | },
210 | })
211 |
--------------------------------------------------------------------------------
/miniprogram/assets/styles/iconfont/iconfont.wxss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "iconfont"; /* Project id 2362383 */
3 | src:
4 | url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAA3IAAsAAAAAF0gAAA17AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACFMAqeHJg0ATYCJANICyYABCAFhGcHgTYbdhNRlJBWMtnPA9s9SlEMhBBCG93BvxQxSMi7sOR+kKuhlPA82b1/7swsH5R4k3zBFVzJncz2t3AtY4enm/9axOaGW8sDECa0Gli+BV2fbB8y53u3cmalD2ZgNSF4WD0zQ07Uqcy0nrQegRPFc6bIuYZQtS99/+0BACGuf6692U2R1J8qTAmFIiuScjYpvPI2ZUp5q/jInSPjKtSmgGDkzcmbE0qdMv8g15SXYYkRGHpVv5dN+1OVWNsNOeTBIRom+hQE8KlFHj7t5BX1gDZGjdhACBgu3wu8zJYxsFENCOtaTEy44FbaaY80WlYBLKjFPmcBrLR/Xt6AOkIAhaW5XKvrMJkOHPgdvDylZ/0fFNB3wSy+RdhaARaggWo1q5er8fI80A9WzXKfshv2AEAAYz0h+hw8r3zel4191uVZ92e9n61/9vHzL5/jXhBebgj/A2IKRFjdRQrBNT/uYwEeKRjQiRMjioVNBAeNwuD32D8eYiN4VFfc9YykAM+BBLyDqEQRJUwAkhBmAAkIBCAOgQHEICwwsaITTLhEFwA2RHeYZBG9ATgQ6wFoiI8BKIgvkQoDL14BfCtMQAS9PFUyKXlaH1EOQG2AFQO7DJeKZVYq7CnB5CEV1rpw7WkZZkLFgEMixeRcPpvIDEIElPIgHMXEL5CfzQ3aTUevdl2cdbxjatNQu83hHVJRV1/R3hqmmpgGipy6h6qEB2dgJT37EFvFNEkhi5AChqRxtajB+UFlrxZnA3IkhZCg6wAMSgkZCGmphMMI0ivEPnkASZCIjHEFOX0/h3M+PAsrsahSZT3VMBbTAMoqkudUr8DK6pWiWpBNESRgiBx85VluqFxWKyykDJMqxpQFUxfhGs2gPCqwq8mUzGK/JLNl7htSuRXa0SNX7txqu1SatEJtgsIs9YdfGLVt0klRfmKplxozUc3LURXWisP210Bu8RMWCQmbcrDDCo22X545cPViy7L0aTBr6lIp1ksG6JHnk/yeXmixFMvs4VaoyXSUCZ/of8QYfMXorQUdU259akoHVeC4AY6CYUB69urDNpzKSTl4s8NLLEYogLAye94MXruHhBQjrfA+kC5UBMYvkbTIzQA6SoNwmmyntnCVv6y2fOHYsVurs1lC1YHlGKDFHQJtl4Ta+RcJQcOwi4ZKJZcYKBariR+CtVvwayABH4ghGmgwyQazkJ8gwaa44BwgrVM5RkmejwPIfTvHTpolFK5DYKgisDJgLqOaK2mmFyCo1JmxYpALqXQRMxmLAaQwjRTFt9MfDrkZKq30CiPZIwNTU/Ntj7XVKwGy7H0QE2mOReIXZxtBqjB6YH/AI0voQuuFzieCBgftlvtF20UHpdm0I8pKjB4OKgkpAqAElVgswgbyZ/EiVVyGynA5luWoJSByNagTvtcOsSWeZva1lRipmbnXNsa0tR1pOtrlZJvkzF7x4yBom0JCa39vu8n7jgvFFrkdvs+Lt/Ri8n0vIcmWaE20V04497hUDiYXwfuZl4vuyKnmWE+dP+NUnBTCo949zvlC30sdr/kPBS7pgLDpq/tnzz2Q0HxObzg8qBCQsTghN+QyFgdEHVEfcegztkuLpS4n+aPdz0x+EOSo0VvltAE5nF0av/eGMceK714vGTJ0AKRpb5TcC+nNp+Lj6+TaI2SRUtF0WNCvuaV0XOO4vVdvFw7nmxlETRVDU4fsLJC8UDmbkohJEXZ/v3I3eXj1LKROzOGLrHyDG54ndHbG1SFYTbKFQc+7FQHR9dnNoWMq0bWCKVvkhOcI3piJEtkTR1xQTcZlwBTgnjUceA9stVDlWDUdjTAw5tgDLqqqsjQMjBy+fn+OYINDyimbkef+dTMuNsQviEoMvKSP1l2Q1iKyFHo5Q4GFQcUAjLEktGEUt5xZjsoixy4J2RNOVBe2SNbxKMhgDD0evLYW1oRRSXXA6J24lXdHfBNqCnOsb6wZfY4U9gaMOJv/tKOQnsjWBKkT08+Qi/Satsba6KqzYt9v831DffN/8/keOraq1lVXxdr+svkkGvq27fhZ8z9A3T794EPpA3Nt1aF2I9q3GdvxmrXR2Wi9ltt3+aIB/UHfF6WveW/Uo8d9vfwPplxt1hSg3uudPIe9nuteOtGUrc25vdZGqz7LwjOHU573PYdtb3RyfmDKmhtTT9RMjebM6wOurUyaOVubMWumjHvhX5N1zoL600RLD8vQykAFybBKUuE01NLD0d0ylM95SYTxxPv2Qy1Nj2r410xD/Durza/xb/k7OK8YzqWX0tzYyJg4mUPI94Zzwr18YSEDJF/OhYu2xVEDBkgWbSKzxNM6m32gcSuJO4GMwdSgRf9zas2lgIOzETOHdbh6dMbU941SzgtTVLXUN9KTi5LpvyDDF24M16byATRaCClbalmAMmAM02g1cf+EjUXsUcqB3YvfrBtkrt7+/XPvTlrjbkdsXkp7RHXO9o5Doxm4/WFV4niOorBQwYmnS8Ol9JhOYnFJTJisRbzZmVCUMnbyvmv/TI1XRRVJopQJrJvLnX0SC0WR+IGRdFmYlJ7SLzm5MaVoid9uaLXdNitcGrYq+Q0HvJF43ix8FUhR9BmDXzQO5lT6RDwZdGHyZT7YcHSM8ah8q81YAUG+W6PdVR5SZ9it1YD/A01tghb+mLG8FkH7BZMhH/n/vE1wgdgLN3RPL0OONitTm234mL0iSbYacj4EDKPX8rd23rwLnnXgB5jlqDd36L1mhNGmIwvvfUsprk2h3L1yoccOVL4zhQGYg9O8bO/y5VXOTOlyfR0P1yeHc0ZHSPf3d1xwhtP1hRZZyKR9dDq62Sy2d8WKmSEzE7shcKC1vmfqpIw50I3KWL3poX2Z1ZGtmZvIsmxj3u3KofhBqZsha053a3FqvTUgHUKSPw5cPok6gwy3NSkjxk+5u/9Hy9jRtsC2YclD6HvwH/HE7rSKfgGSDYFzCLVun5z7BnSho8bg0OpETQkvKl5uvdhrz7lZVw/NHG0yzwQ5j1d76xHGj0+Jda/8tnhBZ5iyNG/u1pcpw4I8T2O7UqRKKXNG+v+b8r4ncf4P1lavUjueNQ5s+hcqNPWLRSQzfy21APbesPdPsH8Tei9x/N/7+Gr4lFO03R0RXIGpx+PzN5UVpfnB1/6D1Le9ETnwTnvRwTHostgFvOd8xUNatwDV9yrwQPvPD5myP76Ww6L23G7JQVMKhVCnmZeSW1Wx5GU3jCSTIQXCVgIyu6zg9DfKwDuekJtz2YoLP7uPD8ctJDfa+fMeplLtDq0gDytMxS07fXE6ujFRnScnoc67PaKYRRBVl+Svfv3mq8ZX9JppbEdJMQcIZt20dG/OSGxFd2U0tCdZUS1pqU1ppAXBfsETSGUqTZQvDJIGLYi691+LR5PyYYlQypvPo6Vd3hhLrnh8djFx6qVP9THslIKHvUMVjIFsbSon9dgo4j2+5O3i4PkhlRnsKEMSO/LNgALykEIxT4KqEhXK+OBRr4y29DTvVcavuduJUIiSMVSF+Nh7FysnztieLrwDoH1KhrqEr9evTMgvLOJXLzLuVViR3IAXNsNMuPbn5Ekgo4rkPDZ5jv89JgFFJV9c4gcvoA0g4PEpfNNpLampLemBSsKcb5/uVxHAjJPuNWeyyTEgo3t5CUDRMVOMOrO0tYKvSLaoi2U9e0rlNaCouvLzV43RXg/GtodDKlBatUQsKo4ua+hZMCRRU6fJVaNkQEntyOyCGAlQJe0vzeSd7X6r+7H3/if9j9a96vPqhF+7slAqFSogRHy1iuoISeHdwenuZ9bv9PDTPUmLQNruB4UiE0lRkVBgOzGWcXTboZVNWYVw0vETQ9tozPgBeWRUVcvHkKBfAa18N61wee+IMB4IPQPDA0H4ygZ0jo0+YDe6RfR9k3x+yUNOeaPnQFeVz2yB8t5pdIpLpz7j808MDvIfPUiw/RRvhfD+6GHRi9xHF0w47yC6dqjBD8ZHJ0X0EYfQzYa/6fvo4rJaT6PL/bPeildu1Y0F9JxvJvIf8L4HufbdlmZHgyhvLe4/4qLURf+arVaTEGP8W+7YA2nzl+UDyM9qeb8K/9NFrLFptgDcoAL86zcQltPIvbKrMv+CLMH1FK+wwqEQa1zKIONdBVtEqINtXJpjn6rIJkbI4ACNv/FaKzAUWEiyEiviHMaaJMeR8b6OLXJ4jm2S/MU+vYV6tggVRCtx6dWIXgdpekOw1m4zuBKx663TGz0WtStsQKV3uWH6oMS4BP9Ysd6md+Wxj7uHMQlBDJDBZbdC8uX19BaLHXK47F16LRJnQhBHVny8gdonTmu3Agku9KhBdFIdEBp654TRYseG4cuI9++rQ48RDxbUuFIileXCPS8U64dIJI6EFKiY21ItWxO7mx4YSQIRlgFIMpcYtQIhXxOYZTVsB8JBD3ahR3uQcZgyDDrIIv76JUNaHld+hdXpOW634uVt0uIjWoxYYosjrnjiS0SiEpO4JCQpTGGJL07D9LZesNpmxLtNfW6tyYPR62CEoNbpYg2wxYKxwDYzThO5bxeM8zgsdrUOq7c6kN7opQ7rUMO9PGi1EyYycOPSxD1q3R5zEQuMdZtgnQfjhHur0X3UNgAA') format('woff2');
5 | }
6 |
7 | .iconfont {
8 | font-family: "iconfont" !important;
9 | font-size: 16px;
10 | font-style: normal;
11 | -webkit-font-smoothing: antialiased;
12 | -moz-osx-font-smoothing: grayscale;
13 | }
14 |
15 | .icon-fenxiang:before {
16 | content: "\e8b0";
17 | }
18 |
19 | .icon-shanchu:before {
20 | content: "\e68e";
21 | }
22 |
23 | .icon-edit:before {
24 | content: "\e66e";
25 | }
26 |
27 | .icon-add-fill:before {
28 | content: "\e6d8";
29 | }
30 |
31 | .icon-link:before {
32 | content: "\e62d";
33 | }
34 |
35 | .icon-bianji:before {
36 | content: "\e62f";
37 | }
38 |
39 | .icon-upload:before {
40 | content: "\e636";
41 | }
42 |
43 | .icon-empty:before {
44 | content: "\e708";
45 | }
46 |
47 | .icon-add:before {
48 | content: "\e600";
49 | }
50 |
51 | .icon-paixu:before {
52 | content: "\e706";
53 | }
54 |
55 | .icon-aqi:before {
56 | content: "\e632";
57 | }
58 |
59 | .icon-fengxiang:before {
60 | content: "\e60c";
61 | }
62 |
63 | .icon-fengsu:before {
64 | content: "\e60b";
65 | }
66 |
67 | .icon-fengli:before {
68 | content: "\e60a";
69 | }
70 |
71 | .icon-shidu:before {
72 | content: "\e609";
73 | }
74 |
75 | .icon-qiya:before {
76 | content: "\e608";
77 | }
78 |
79 | .icon-zan:before {
80 | content: "\e605";
81 | }
82 |
83 |
--------------------------------------------------------------------------------