├── 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 | 30 | 31 | 小鸣助手 - 为生活赋能 32 | 33 | Copyright © 2018-2022 lifehelper.com.cn 34 | 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 | 小鸣助手 Logo 4 |
5 |

小鸣助手

6 |
让生活更简单一些
7 |
8 | 9 | ## 🤓 项目介绍 10 | 11 | 「小鸣助手」是一个生活服务类小程序,主要为用户的日常生活提供一些便捷工具,例如天气查询、时间规划、生活记录等。目前该小程序已稳定运行近 4 年,为近 10 万用户提供了生活帮助。 12 | 13 | 读者可直接扫描以下小程序码进行体验: 14 | 15 | ![image](https://static.lifehelper.com.cn/static/project/qrcode.jpg) 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 | 69 | 70 | 小鸣相册 - 用照片和视频记录生活 73 | 74 | Copyright © 2018-2022 lifehelper.com.cn 75 | 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 | --------------------------------------------------------------------------------