├── .vscode
└── settings.json
├── src
├── pages
│ ├── statement
│ │ ├── form.config.ts
│ │ └── form.tsx
│ ├── account_books
│ │ ├── edit.config.ts
│ │ ├── list.config.ts
│ │ ├── create.config.ts
│ │ ├── list.styl
│ │ ├── list.tsx
│ │ ├── create.styl
│ │ └── edit.tsx
│ ├── assets_flow
│ │ └── index.config.ts
│ ├── payee
│ │ ├── list.config.ts
│ │ ├── list.scss
│ │ └── list.tsx
│ ├── setting
│ │ ├── asset
│ │ │ ├── index.config.ts
│ │ │ └── index.tsx
│ │ ├── search
│ │ │ ├── search.config.ts
│ │ │ └── search.tsx
│ │ ├── budget
│ │ │ └── index.config.ts
│ │ ├── category
│ │ │ ├── index.config.ts
│ │ │ └── form.tsx
│ │ ├── feedback
│ │ │ ├── index.config.ts
│ │ │ └── index.tsx
│ │ ├── messages
│ │ │ ├── detail.config.ts
│ │ │ ├── index.config.ts
│ │ │ ├── detail.tsx
│ │ │ └── index.tsx
│ │ ├── user_info
│ │ │ ├── index.config.ts
│ │ │ └── index.tsx
│ │ ├── child_budget
│ │ │ ├── index.config.ts
│ │ │ └── index.tsx
│ │ ├── statements_flow
│ │ │ ├── index.config.ts
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ ├── statement_imgs
│ │ │ ├── index.config.ts
│ │ │ └── index.tsx
│ │ ├── statements_manage
│ │ │ ├── data_in.config.ts
│ │ │ ├── data_out.config.ts
│ │ │ ├── data_in.tsx
│ │ │ └── data_out.tsx
│ │ └── chart
│ │ │ ├── category_statement.config.ts
│ │ │ └── category_statement.tsx
│ ├── sub
│ │ └── chart
│ │ │ └── index.config.ts
│ ├── statement_detail
│ │ └── index.config.ts
│ ├── home
│ │ ├── index.config.ts
│ │ └── index.tsx
│ ├── share
│ │ ├── index.config.ts
│ │ ├── public.config.ts
│ │ └── public.tsx
│ └── friends
│ │ ├── index.config.ts
│ │ ├── invite_info.config.ts
│ │ ├── invite_info.tsx
│ │ └── index.scss
├── assets
│ ├── fonts
│ │ ├── iconfont.ttf
│ │ ├── iconfont.woff
│ │ ├── iconfont.woff2
│ │ └── index.styl
│ ├── images
│ │ ├── default-bg.jpg
│ │ ├── empty_no_data.png
│ │ └── icon_select_default.png
│ └── styl
│ │ ├── pages
│ │ ├── settings
│ │ │ ├── export_page.styl
│ │ │ ├── category_manager.styl
│ │ │ ├── user_info.styl
│ │ │ ├── statement_imgs.styl
│ │ │ └── chart.styl
│ │ ├── profile
│ │ │ ├── category-form.styl
│ │ │ └── index.styl
│ │ ├── statistic
│ │ │ ├── index.styl
│ │ │ └── calendar.styl
│ │ ├── friends
│ │ │ └── invite_info.styl
│ │ ├── home
│ │ │ └── index.styl
│ │ ├── finance
│ │ │ ├── index.styl
│ │ │ └── asset_flow.styl
│ │ └── statements
│ │ │ └── detail.styl
│ │ ├── common
│ │ ├── border.styl
│ │ ├── input.styl
│ │ ├── index.styl
│ │ ├── text.styl
│ │ ├── common.styl
│ │ ├── image.styl
│ │ ├── header.styl
│ │ ├── mask.styl
│ │ ├── calculator.styl
│ │ └── flex.styl
│ │ ├── themes
│ │ ├── components
│ │ │ ├── form.styl
│ │ │ ├── loading.styl
│ │ │ ├── button.styl
│ │ │ └── tab.styl
│ │ ├── root.styl
│ │ └── index.styl
│ │ ├── index.styl
│ │ ├── components
│ │ ├── slide_sidebar.styl
│ │ ├── asset_banner.styl
│ │ ├── select_component.styl
│ │ └── statement.styl
│ │ └── vars.styl
├── components
│ ├── Home
│ │ ├── index.js
│ │ └── StatisticPage
│ │ │ └── index.tsx
│ ├── UiComponents
│ │ ├── index.js
│ │ ├── Button
│ │ │ └── index.tsx
│ │ ├── Loading
│ │ │ └── index.tsx
│ │ ├── Tabs
│ │ │ └── index.tsx
│ │ └── Form
│ │ │ └── index.tsx
│ ├── Statistic
│ │ ├── ExpendCategory.tsx
│ │ ├── ExpendTrend.tsx
│ │ ├── ExpendList.tsx
│ │ └── Summary.tsx
│ ├── Statements
│ │ └── index.jsx
│ ├── EmptyTips
│ │ └── index.tsx
│ ├── Select
│ │ └── index.jsx
│ ├── Avatar
│ │ └── index.jsx
│ ├── Calculator
│ │ └── index.scss
│ ├── AssetBanner
│ │ └── index.jsx
│ ├── statementForm
│ │ ├── PayeeSelect.tsx
│ │ └── CategorySelect.tsx
│ ├── SlideSetting
│ │ └── index.tsx
│ └── Statement
│ │ └── index.jsx
├── stores
│ ├── index.ts
│ ├── theme_store.ts
│ └── home_store.ts
├── config
│ ├── config.ts.example
│ └── index.ts
├── api
│ ├── logic
│ │ ├── chaos.ts
│ │ ├── message.ts
│ │ ├── superStatement.ts
│ │ ├── user.ts
│ │ ├── main.ts
│ │ ├── statistic.ts
│ │ ├── category.ts
│ │ ├── asset.ts
│ │ ├── budget.ts
│ │ ├── superChart.ts
│ │ ├── finance.ts
│ │ ├── account_book.ts
│ │ ├── friend.ts
│ │ ├── payee.ts
│ │ └── statement.ts
│ ├── http-result.ts
│ ├── types.ts
│ ├── index.ts
│ └── request.ts
├── app.ts
├── utils
│ ├── event.ts
│ └── echart_option.ts
├── index.html
├── app.config.ts
├── router
│ └── index.ts
├── storage
│ └── index.ts
└── jz.ts
├── .gitignore
├── .eslintrc.js
├── .editorconfig
├── babel.config.js
├── config
├── dev.js
├── prod.js
└── index.js
├── tsconfig.json
├── plugins
└── view-data-plugin.js
├── project.config.json
├── README.md
└── package.json
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "compile-hero.disable-compile-files-on-did-save-code": true
3 | }
--------------------------------------------------------------------------------
/src/pages/statement/form.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '记一笔'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/account_books/edit.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '编辑账簿'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/account_books/list.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '账簿列表'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/assets_flow/index.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '资金流水'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/payee/list.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '商家管理',
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/setting/asset/index.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '资产管理'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/setting/search/search.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '搜索'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/sub/chart/index.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '消费报表'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/account_books/create.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '创建账簿'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/setting/budget/index.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '预算管理'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/setting/category/index.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '分类管理'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/setting/feedback/index.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '反馈'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/setting/messages/detail.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '消息详情'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/setting/messages/index.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '站内消息'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/setting/user_info/index.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '用户信息'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/statement_detail/index.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '账单详情'
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/fonts/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yigger/jiezhang/HEAD/src/assets/fonts/iconfont.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/iconfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yigger/jiezhang/HEAD/src/assets/fonts/iconfont.woff
--------------------------------------------------------------------------------
/src/assets/fonts/iconfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yigger/jiezhang/HEAD/src/assets/fonts/iconfont.woff2
--------------------------------------------------------------------------------
/src/pages/setting/child_budget/index.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '分类预算管理'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/setting/statements_flow/index.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '流水'
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/images/default-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yigger/jiezhang/HEAD/src/assets/images/default-bg.jpg
--------------------------------------------------------------------------------
/src/pages/setting/statement_imgs/index.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '账单图库'
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/setting/statements_manage/data_in.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '登录PC端'
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/images/empty_no_data.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yigger/jiezhang/HEAD/src/assets/images/empty_no_data.png
--------------------------------------------------------------------------------
/src/pages/setting/chart/category_statement.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '分类报表'
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | deploy_versions/
3 | .temp/
4 | .rn_temp/
5 | node_modules/
6 | .DS_Store
7 | .swc/
8 | src/config/config.ts
--------------------------------------------------------------------------------
/src/pages/home/index.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '首页',
3 | enableShareAppMessage: true
4 | }
5 |
--------------------------------------------------------------------------------
/src/pages/setting/statements_manage/data_out.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '账单导出'
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/images/icon_select_default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yigger/jiezhang/HEAD/src/assets/images/icon_select_default.png
--------------------------------------------------------------------------------
/src/pages/share/index.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '账单分享',
3 | enableShareAppMessage: true
4 | }
5 |
--------------------------------------------------------------------------------
/src/pages/share/public.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '账单分享',
3 | enableShareAppMessage: true
4 | }
5 |
--------------------------------------------------------------------------------
/src/pages/friends/index.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '好友列表',
3 | enableShareAppMessage: true
4 | }
5 |
--------------------------------------------------------------------------------
/src/pages/friends/invite_info.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | navigationBarTitleText: '邀请好友',
3 | enableShareAppMessage: true
4 | }
5 |
--------------------------------------------------------------------------------
/src/assets/styl/pages/settings/export_page.styl:
--------------------------------------------------------------------------------
1 | .export_pages {
2 | .item-selected {
3 | background-color: #e6f3ff !important;
4 | }
5 | }
--------------------------------------------------------------------------------
/src/components/Home/index.js:
--------------------------------------------------------------------------------
1 | export * from './FinancePage'
2 | export * from './IndexPage/index'
3 | export * from './ProfilePage'
4 | export * from './StatisticPage'
--------------------------------------------------------------------------------
/src/pages/account_books/list.styl:
--------------------------------------------------------------------------------
1 | .account-list {
2 | .account {
3 | background: #fff
4 | &.active {
5 | border: 1PX solid #6bb16b
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/src/components/UiComponents/index.js:
--------------------------------------------------------------------------------
1 | export * from './Button'
2 | export * from './Loading'
3 | export * from './Calculator'
4 | export * from './Tabs'
5 | export * from './Form'
--------------------------------------------------------------------------------
/src/stores/index.ts:
--------------------------------------------------------------------------------
1 | import { HomeStoreContext } from './home_store'
2 | import { ThemeStoreContext } from './theme_store'
3 |
4 | export { HomeStoreContext, ThemeStoreContext };
--------------------------------------------------------------------------------
/src/assets/styl/common/border.styl:
--------------------------------------------------------------------------------
1 | .jz-border-bottom-1 {
2 | border-bottom: 1PX solid $borderColor
3 | }
4 |
5 | .jz-border-top-1 {
6 | border-top: 1PX solid $borderColor
7 | }
--------------------------------------------------------------------------------
/src/assets/styl/common/input.styl:
--------------------------------------------------------------------------------
1 | .jz-component__input {
2 | background: #ffffff
3 | border-bottom: 1rpx solid #f4f4f4
4 | input {
5 | padding: 14PX
6 | }
7 | }
--------------------------------------------------------------------------------
/src/assets/styl/pages/profile/category-form.styl:
--------------------------------------------------------------------------------
1 | .category-icon-default-select {
2 | background: #f3f3f3;
3 | padding: 0 6PX
4 | border-left: 1px dashed #f4f4f4
5 | }
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // module.exports = {
2 | // "extends": ["taro/react"],
3 | // "rules": {
4 | // "react/jsx-uses-react": "off",
5 | // "react/react-in-jsx-scope": "off"
6 | // }
7 | // }
8 |
--------------------------------------------------------------------------------
/src/config/config.ts.example:
--------------------------------------------------------------------------------
1 | export default {
2 | appid: '填入小程序app_id',
3 | dev_host: 'http://127.0.0.1:3000',
4 | production_host: 'https://example-production.com',
5 | web_host: 'http://127.0.0.1:3000'
6 | }
--------------------------------------------------------------------------------
/src/assets/styl/pages/settings/category_manager.styl:
--------------------------------------------------------------------------------
1 | .category-manager > .header {
2 | background-color: var(--primary-color)
3 | color: #ffffff
4 | padding: 24px 16px
5 | font-size: 16px
6 | font-weight: 500
7 | }
--------------------------------------------------------------------------------
/src/assets/styl/common/index.styl:
--------------------------------------------------------------------------------
1 | @import "./common"
2 | @import "./flex"
3 | @import "./shift"
4 | @import "./text"
5 | @import "./image"
6 | @import "./header"
7 | @import "./calculator"
8 | @import "./border"
9 | @import "./mask"
10 | @import "./input"
--------------------------------------------------------------------------------
/src/assets/styl/pages/statistic/index.styl:
--------------------------------------------------------------------------------
1 | .summary-component__header {
2 | display: flex
3 | background: #ffffff
4 | border-bottom: 1px solid #f4f4f4
5 | .header-item {
6 | flex: 1
7 | text-align: center
8 | padding: 12PX
9 | }
10 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | // babel-preset-taro 更多选项和默认值:
2 | // https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md
3 | module.exports = {
4 | presets: [
5 | ['taro', {
6 | framework: 'react',
7 | ts: true
8 | }]
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/config/dev.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | projectname: "jz-taro-development",
3 | outputRoot: 'dist/development',
4 | env: {
5 | NODE_ENV: '"development"'
6 | },
7 | defineConstants: {
8 | },
9 | mini: {},
10 | h5: {
11 | esnextModules: ['taro-ui']
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Statistic/ExpendCategory.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { View } from '@tarojs/components'
3 |
4 | export default function ExpendCategory() {
5 |
6 | useEffect(() => {
7 |
8 | })
9 |
10 | return (
11 |
12 | Hi, 分类
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/assets/styl/pages/settings/user_info.styl:
--------------------------------------------------------------------------------
1 | .user-info-page {
2 | image {
3 | width: 80PX
4 | height: 80PX
5 | }
6 |
7 | .nickname-input {
8 | margin: 24PX 12PX
9 | border-radius: 21PX
10 | }
11 |
12 | button{
13 | background: transparent
14 | border: none
15 | }
16 | button::after {
17 | border: none
18 | }
19 | }
--------------------------------------------------------------------------------
/src/assets/styl/common/text.styl:
--------------------------------------------------------------------------------
1 | .text-bold {
2 | font-weight: bold
3 | }
4 |
5 | for fontSize in (12..32)
6 | .fs-{fontSize}
7 | font-size "%sPX !important" % (fontSize)
8 |
9 | .text-align-center {
10 | text-align: center;
11 | }
12 |
13 | .text-align-left {
14 | text-align: left;
15 | }
16 |
17 | .text-align-right {
18 | text-align: right;
19 | }
--------------------------------------------------------------------------------
/src/api/logic/chaos.ts:
--------------------------------------------------------------------------------
1 | import Request from '../request'
2 |
3 | export default class Chaos {
4 | private _request: Request
5 | constructor (request: Request) {
6 | this._request = request
7 | }
8 |
9 | async submitFeedback({ content }) {
10 | return await this._request.post('settings/feedback', {
11 | content: content,
12 | type: 0
13 | })
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/Statistic/ExpendTrend.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { format } from 'date-fns'
3 | import BaseForm from './baseForm'
4 | import { View } from '@tarojs/components'
5 |
6 | export default function ExpendTrend() {
7 |
8 | useEffect(() => {
9 |
10 | })
11 |
12 | return (
13 |
14 | Hi, 趋势
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/api/logic/message.ts:
--------------------------------------------------------------------------------
1 | import Request from '../request'
2 |
3 | export default class Message {
4 | private _request: Request
5 | constructor (request: Request) {
6 | this._request = request
7 | }
8 |
9 | async getList () {
10 | return await this._request.get('message')
11 | }
12 |
13 | async getMessage (id) {
14 | return await this._request.get(`message/${id}`)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "jsx": "react",
5 | "baseUrl": ".",
6 | "paths": {
7 | "@/src": ["./src/*"],
8 | "@/jz": ["./src/jz.ts"],
9 | "@/api": ["./src/api/*"],
10 | "@/components": ["./src/components/*"],
11 | "@/assets": ["./src/assets/*"],
12 | "@/utils": ["./src/utils/*"]
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/src/components/Statements/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View } from '@tarojs/components'
3 | import Statement from '../Statement'
4 |
5 | export default function Statements({ statements, editable = true }) {
6 | return (
7 |
8 | { statements.map((statement) => ) }
9 |
10 | )
11 | }
--------------------------------------------------------------------------------
/src/components/UiComponents/Button/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button as B } from '@tarojs/components'
3 |
4 | export const Button = ({
5 | title,
6 | className = '',
7 | ...props
8 | }) => {
9 | return (
10 |
14 | { title }
15 |
16 | )
17 | }
--------------------------------------------------------------------------------
/src/api/logic/superStatement.ts:
--------------------------------------------------------------------------------
1 | import Request from '../request'
2 |
3 | export default class SuperStatement {
4 | private _request: Request
5 | constructor (request: Request) {
6 | this._request = request
7 | }
8 |
9 | getTime() {
10 | return this._request.get('super_statements/time')
11 | }
12 |
13 | getStatements(params) {
14 | return this._request.get('super_statements/list', params)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/api/http-result.ts:
--------------------------------------------------------------------------------
1 | export default class HttpResult {
2 |
3 | public data: any
4 | public message: string
5 | public status: number
6 | public header: any
7 | public st: any
8 |
9 | constructor (st) {
10 | this.st = st
11 | this.data = st.data
12 | this.status = st.statusCode
13 | this.header = st.header
14 | }
15 |
16 | get isSuccess(): boolean {
17 | return this.status === 200
18 | }
19 |
20 | }
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | // 从本地配置文件导入
2 | import devConfig from './config'
3 |
4 | type Config = {
5 | appid: string,
6 | host: string,
7 | api_url: string,
8 | web_host: string
9 | }
10 |
11 | const host = process.env.NODE_ENV === 'development' ? devConfig.dev_host : devConfig.production_host
12 |
13 | const config: Config = {
14 | appid: devConfig.appid,
15 | host: host,
16 | api_url: `${host}/api`,
17 | web_host: devConfig.web_host,
18 | }
19 |
20 | export default config
21 |
--------------------------------------------------------------------------------
/plugins/view-data-plugin.js:
--------------------------------------------------------------------------------
1 | // NOTE:Taro 的 View 不支持自定义属性,比如 ,编译会过滤掉 theme 属性
2 | // https://github.com/NervJS/taro/issues/11530#issuecomment-2196396686
3 | export default ctx => {
4 | ctx.registerMethod({
5 | name: 'onSetupClose',
6 | fn(platform) {
7 | const template = platform.template
8 | template.mergeComponents(ctx, {
9 | View: {
10 | 'data-theme-name': 'i.dataThemeName'
11 | }
12 | })
13 | }
14 | })
15 | }
--------------------------------------------------------------------------------
/src/components/UiComponents/Loading/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View } from '@tarojs/components'
3 | export const Loading = function ({
4 | active,
5 | title = '加载中...'
6 | }) {
7 | if (!active) {
8 | return null
9 | }
10 |
11 | return (
12 |
13 |
14 | {title}
15 |
16 | )
17 | }
--------------------------------------------------------------------------------
/src/assets/styl/common/common.styl:
--------------------------------------------------------------------------------
1 | .w-100 {
2 | width: 100%
3 | box-sizing: border-box
4 | }
5 |
6 | .h-100 {
7 | height: 100%
8 | }
9 |
10 | // 定位
11 | .p-absolute {
12 | position: absolute
13 | }
14 |
15 | .p-relative {
16 | position: relative
17 | }
18 |
19 | .p-top-0 {
20 | top: 0
21 | }
22 |
23 | .p-left-0 {
24 | left: 0
25 | }
26 |
27 | .p-bottom-0 {
28 | bottom: 0
29 | }
30 |
31 | .p-right-0 {
32 | right: 0
33 | }
34 |
35 | .at-input {
36 | margin-left: 0 !important
37 | }
--------------------------------------------------------------------------------
/src/assets/styl/common/image.styl:
--------------------------------------------------------------------------------
1 | .jz-image {
2 | display: inline-block
3 |
4 | &-normal {
5 | width: 100px;
6 | height: 100px;
7 |
8 | image {
9 | width: 100%;
10 | height: 100%;
11 | }
12 | }
13 |
14 | &-icon {
15 | width: 60px;
16 | height: 60px;
17 |
18 | image {
19 | width: 100%;
20 | height: 100%;
21 | }
22 | }
23 |
24 |
25 | &.radius {
26 | border-radius: 30PX
27 | image {
28 | border-radius: 30PX
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/config/prod.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | projectname: "jz-taro-production",
3 | outputRoot: 'dist/production',
4 | env: {
5 | NODE_ENV: '"production"'
6 | },
7 | defineConstants: {
8 | },
9 | mini: {},
10 | h5: {
11 | /**
12 | * 如果h5端编译后体积过大,可以使用webpack-bundle-analyzer插件对打包体积进行分析。
13 | * 参考代码如下:
14 | * webpackChain (chain) {
15 | * chain.plugin('analyzer')
16 | * .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
17 | * }
18 | */
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/assets/styl/themes/components/form.styl:
--------------------------------------------------------------------------------
1 | .jz-common-component__input, .jz-common-component__textarea {
2 | .label {
3 | margin: 21PX 0 0PX 12PX
4 | }
5 | input {
6 | background: #ffffff
7 | padding: 12PX 18PX
8 | border-radius: 8PX
9 | }
10 | textarea {
11 | background: #ffffff
12 | padding: 12PX 18PX
13 | border-radius: 8PX
14 | width: 100%
15 | }
16 | .virtual-input {
17 | background: #ffffff
18 | padding: 12PX 18PX
19 | border-radius: 8PX
20 | margin: 12PX
21 | }
22 | }
--------------------------------------------------------------------------------
/src/components/EmptyTips/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View } from '@tarojs/components'
3 | import EmptyNoData from '@/assets/images/empty_no_data.png'
4 | import { Image } from '@tarojs/components'
5 |
6 | export default function EmptyTips({
7 | content='查无数据'
8 | }) {
9 | return (
10 |
11 |
12 | {content}
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/assets/fonts/index.styl:
--------------------------------------------------------------------------------
1 | @import 'iconfont.css'
2 |
3 | @font-face {
4 | font-family: "iconfont"; /* Project id 2523724 */
5 | src: url('iconfont.woff2?t=1619851634149') format('woff2'),
6 | url('iconfont.woff?t=1619851634149') format('woff'),
7 | url('iconfont.ttf?t=1619851634149') format('truetype');
8 | }
9 |
10 | .iconfont {
11 | font-family: "iconfont" !important;
12 | font-size: 16PX;
13 | display: inline-block;
14 | font-style: normal;
15 | -webkit-font-smoothing: antialiased;
16 | -moz-osx-font-smoothing: grayscale;
17 | }
--------------------------------------------------------------------------------
/src/assets/styl/themes/components/loading.styl:
--------------------------------------------------------------------------------
1 | .jz-common-component__loading:after {
2 | content: " ";
3 | display: block;
4 | width: 30PX;
5 | height: 30PX;
6 | border-radius: 50%;
7 | border: 4PX solid var(--primary-bg-color)
8 | border-color: var(--primary-bg-color) transparent var(--primary-bg-color) transparent;
9 | animation: jz-common-component__loading 0.8s linear infinite;
10 | }
11 | @keyframes jz-common-component__loading {
12 | 0% {
13 | transform: rotate(0deg);
14 | }
15 | 100% {
16 | transform: rotate(360deg);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/api/logic/user.ts:
--------------------------------------------------------------------------------
1 | import Request from '../request'
2 |
3 | export default class User {
4 | private _request: Request
5 | constructor (request: Request) {
6 | this._request = request
7 | }
8 |
9 | getSettingsData() {
10 | return this._request.get('settings')
11 | }
12 |
13 | getUserInfo() {
14 | return this._request.get('users')
15 | }
16 |
17 | updateUserInfo(params) {
18 | return this._request.put('users/update_user', { user: params })
19 | }
20 |
21 | loginPc(code) {
22 | return this._request.post('users/scan_login', { qr_code: code })
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/api/logic/main.ts:
--------------------------------------------------------------------------------
1 | import Request from '../request'
2 | import { HeaderResponse, StatementsResponse } from '../types'
3 |
4 | export default class Main {
5 | private readonly _request: Request
6 |
7 | constructor(request: Request) {
8 | this._request = request
9 | }
10 |
11 | public async header(): Promise {
12 | const st = await this._request.get('header')
13 | return st
14 | }
15 |
16 | public async statements(range: string): Promise {
17 | const st = await this._request.get('index', {range})
18 | return st
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/assets/styl/common/header.styl:
--------------------------------------------------------------------------------
1 | .header-1 {
2 | display: block
3 | font-weight: bold
4 | font-size: 36px
5 | padding: 6px
6 | }
7 |
8 | .header-with-color-bottom {
9 | position: relative;
10 | z-index: 998;
11 | display: inline-block;
12 | padding-bottom: 4px;
13 | margin-bottom:12px;
14 | font-size: 36px;
15 | color: #40485B;
16 | font-weight: bolder;
17 |
18 | &:before {
19 | content: '';
20 | position: absolute;
21 | background: #FFF3AC;
22 | width: calc(100% + 16px);
23 | height: 12px;
24 | bottom: 0;
25 | left: 0;
26 | border-radius: 12px;
27 | z-index: -1;
28 | }
29 | }
--------------------------------------------------------------------------------
/src/assets/styl/pages/settings/statement_imgs.styl:
--------------------------------------------------------------------------------
1 | .statement-images {
2 | .year {
3 | font-size: 40rpx;
4 | font-weight: bold;
5 | margin: 12px 0;
6 | }
7 | .month {
8 | font-size: 36rpx;
9 | margin: 8px 0;
10 | }
11 | .image-item-list {
12 | .image-item {
13 | width: 25%;
14 | overflow: hidden;
15 | text-align:center;
16 | margin-bottom:12px;
17 | display: inline-block;
18 | }
19 | .statement-info {
20 | font-size: 24rpx;
21 | margin-top:-4px;
22 | text-align: left;
23 | navigator {
24 | color: #537c8d;
25 | }
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/stores/theme_store.ts:
--------------------------------------------------------------------------------
1 | import {observable, action} from 'mobx';
2 | import jz from '@/jz'
3 | import { createContext } from "react";
4 |
5 | class ThemeStore {
6 | themes = [
7 | {
8 | name: '默认主题',
9 | value: 'default'
10 | },
11 | {
12 | name: '纯净白',
13 | value: 'pure'
14 | },
15 | {
16 | name: '樱花粉',
17 | value: 'pink'
18 | },
19 | {
20 | name: '黑夜模式',
21 | value: 'black'
22 | }
23 | ]
24 |
25 | // 初始化的默认主题
26 | // value: default, pink, pure
27 | @observable currentTheme = this.themes[3]
28 |
29 | @action setTheme(theme) {
30 | this.currentTheme = theme
31 | }
32 | }
33 |
34 | export const ThemeStoreContext = createContext(new ThemeStore());
--------------------------------------------------------------------------------
/src/assets/styl/common/mask.styl:
--------------------------------------------------------------------------------
1 | .jz-mask__main {
2 | position: fixed
3 | bottom: 0
4 | height: 100vh
5 | width: 100%
6 | z-index: 999
7 |
8 | .jz-mask {
9 | background: rgba(0,0,0, 0.5)
10 | position: absolute
11 | bottom: 0
12 | height: 100vh
13 | width: 100%
14 | z-index: 99
15 | }
16 |
17 | .jz-mask__body {
18 | background: #f9f9f9
19 | border-radius: 6PX 6PX 0 0
20 | display: flex
21 | flex-direction: column
22 | position: absolute
23 | bottom: 0
24 | left: 0
25 | width: 100%
26 | height: 80%
27 | z-index: 100
28 | overflow-y: scroll
29 |
30 | animation:category-select 0.5s;
31 | -webkit-animation:category-select 0.5s;
32 | animation-fill-mode: forwards;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/app.ts:
--------------------------------------------------------------------------------
1 | import { Component } from 'react'
2 | import Taro from "@tarojs/taro"
3 | import jz from './jz'
4 |
5 | class App extends Component {
6 | onLaunch () {
7 | const updateManager = Taro.getUpdateManager()
8 | updateManager.onCheckForUpdate(function () {
9 | })
10 |
11 | updateManager.onUpdateReady(function () {
12 | Taro.showModal({
13 | title: '洁账版本升级',
14 | content: '版本已更新,请重启应用后使用',
15 | success(res) {
16 | if (res.confirm) {
17 | updateManager.applyUpdate()
18 | }
19 | }
20 | })
21 | })
22 |
23 | jz.initialize()
24 | }
25 |
26 | // this.props.children 是将要会渲染的页面
27 | render () {
28 | return this.props.children
29 | }
30 | }
31 |
32 | export default App
33 |
--------------------------------------------------------------------------------
/src/api/logic/statistic.ts:
--------------------------------------------------------------------------------
1 | import Request from '../request'
2 | export default class Statistic {
3 | private _request: Request
4 | constructor (request: Request) {
5 | this._request = request
6 | }
7 |
8 | getCalendarData(date: string) {
9 | return this._request.get('chart/calendar_data', {
10 | date: date
11 | })
12 | }
13 |
14 | getOverviewHeader(date: string) {
15 | return this._request.get('chart/overview_header', {
16 | date: date
17 | })
18 | }
19 |
20 | getOverviewStatements(date: string) {
21 | return this._request.get('chart/overview_statements', {
22 | date: date
23 | })
24 | }
25 |
26 | getRate(date: string, type: string) {
27 | return this._request.get('chart/rate', {
28 | date: date,
29 | type: type
30 | })
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/UiComponents/Tabs/index.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react'
3 | import { ScrollView, View } from '@tarojs/components'
4 |
5 | export const Tabs = ({
6 | tabs,
7 | current,
8 | onChange
9 | }) => {
10 | return (
11 |
17 |
18 | {tabs.map((item) => {
19 | return (
20 | onChange(item.id)}
23 | >
24 | {item.title}
25 |
26 | )
27 | })}
28 |
29 |
30 | )
31 | }
--------------------------------------------------------------------------------
/project.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "miniprogramRoot": "dist/",
3 | "projectname": "jz-taro",
4 | "description": "jiezhang",
5 | "appid": "wx414689c547b71ab0",
6 | "setting": {
7 | "urlCheck": false,
8 | "es6": false,
9 | "postcss": false,
10 | "preloadBackgroundData": false,
11 | "minified": false,
12 | "newFeature": true,
13 | "autoAudits": false,
14 | "coverView": true,
15 | "showShadowRootInWxmlPanel": false,
16 | "scopeDataCheck": false,
17 | "useCompilerModule": false,
18 | "babelSetting": {
19 | "ignore": [],
20 | "disablePlugins": [],
21 | "outputPath": ""
22 | }
23 | },
24 | "compileType": "miniprogram",
25 | "simulatorType": "wechat",
26 | "simulatorPluginLibVersion": {},
27 | "condition": {
28 | "miniprogram": {
29 | "list": [
30 |
31 | ]
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/api/logic/category.ts:
--------------------------------------------------------------------------------
1 | import Request from '../request'
2 |
3 | export default class Category {
4 | private _request: Request
5 | constructor (request: Request) {
6 | this._request = request
7 | }
8 |
9 | getSettingList({ type = 'expend', parent_id = 0 }) {
10 | return this._request.get('categories/category_list', {
11 | type: type,
12 | parent_id: parent_id
13 | })
14 | }
15 |
16 | getCategoryDetail(id) {
17 | return this._request.get(`categories/${id}`)
18 | }
19 |
20 | deleteCategory(id) {
21 | return this._request.delete(`categories/${id}`, {})
22 | }
23 |
24 | getCategoryIcon() {
25 | return this._request.get('icons/categories_with_url')
26 | }
27 |
28 | updateCategory(id, data) {
29 | return this._request.put(`categories/${id}`, data)
30 | }
31 |
32 | create(data) {
33 | return this._request.post('categories', data)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/assets/styl/index.styl:
--------------------------------------------------------------------------------
1 | // 通用组件样式
2 | @import "./vars"
3 | @import "./themes/index"
4 | @import "./common/index"
5 |
6 | // 组件通用
7 | @import "./components/statement"
8 | @import "./components/statement_form"
9 | @import "./components/asset_banner"
10 | @import "./components/select_component"
11 | @import "./components/slide_sidebar"
12 |
13 | // 页面独立样式
14 | @import "./pages/home/index"
15 | @import "./pages/profile/index"
16 | @import "./pages/profile/category-form"
17 | @import "./pages/finance/index"
18 | @import "./pages/finance/asset_flow"
19 | @import "./pages/statements/detail"
20 | @import "./pages/settings/chart"
21 | @import "./pages/settings/user_info"
22 | @import "./pages/settings/category_manager"
23 | @import "./pages/settings/statement_imgs"
24 | @import "./pages/settings/export_page"
25 | @import "./pages/budget/index"
26 | @import "./pages/statistic/index"
27 | @import "./pages/statistic/calendar"
28 | @import "./pages/friends/invite_info"
--------------------------------------------------------------------------------
/src/api/logic/asset.ts:
--------------------------------------------------------------------------------
1 | import Request from '../request'
2 |
3 | export default class Asset {
4 | private _request: Request
5 | constructor (request: Request) {
6 | this._request = request
7 | }
8 |
9 | getSettingList({ parentId }) {
10 | return this._request.get('assets', { parent_id: parentId })
11 | }
12 |
13 | getAssetDetail(id) {
14 | return this._request.get(`assets/${id}`)
15 | }
16 |
17 | deleteAsset(id) {
18 | return this._request.delete(`assets/${id}`, {})
19 | }
20 |
21 | getAssetIcon() {
22 | return this._request.get('icons/assets_with_url')
23 | }
24 |
25 | updateAsset(id, data) {
26 | return this._request.put(`assets/${id}`, { wallet: data })
27 | }
28 |
29 | create(data) {
30 | return this._request.post('assets', { wallet: data })
31 | }
32 |
33 | updateAssetAmount(id, amount) {
34 | return this._request.put(`wallet/surplus`, { asset_id: id, amount: amount })
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/api/logic/budget.ts:
--------------------------------------------------------------------------------
1 | import Request from '../request'
2 |
3 | export default class Budget {
4 | private _request: Request
5 | constructor (request: Request) {
6 | this._request = request
7 | }
8 |
9 | getSummary({
10 | year: year,
11 | month: month
12 | }) {
13 | return this._request.get('budgets', { year, month })
14 | }
15 |
16 | getParentList({
17 | year: year,
18 | month: month
19 | }) {
20 | return this._request.get('budgets/parent', { year, month })
21 | }
22 |
23 | getCategoryBudget({category_id, year, month}) {
24 | return this._request.get('budgets/' + category_id, { year, month })
25 | }
26 |
27 | updateRootAmount({amount}) {
28 | return this._request.put('budgets/0', { type: 'user', amount: amount})
29 | }
30 |
31 | updateCategoryAmount({amount, category_id}) {
32 | return this._request.put('budgets/0', { type: 'category', category_id: category_id, amount: amount})
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/api/logic/superChart.ts:
--------------------------------------------------------------------------------
1 | import Request from '../request'
2 |
3 | export default class SuperChart {
4 | private _request: Request
5 | constructor (request: Request) {
6 | this._request = request
7 | }
8 |
9 | getHeader(params) {
10 | return this._request.get('super_chart/header', params)
11 | }
12 |
13 | getPieData(params) {
14 | return this._request.get("super_chart/get_pie_data", params)
15 | }
16 |
17 | getWeekData(params) {
18 | return this._request.get("super_chart/week_data", params)
19 | }
20 |
21 | getLineData({ year }) {
22 | return this._request.get("super_chart/line_chart", { year: year })
23 | }
24 |
25 | getCategoriesTop({ year, month }) {
26 | return this._request.get("super_chart/categories_list", { year: year, month: month })
27 | }
28 |
29 | getTableSumary({ year, month }) {
30 | return this._request.get("super_chart/table_sumary", { year: year, month: month })
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/src/assets/styl/pages/settings/chart.styl:
--------------------------------------------------------------------------------
1 | .setting-chart-page {
2 | .header{
3 | background-color: #fbfbfb
4 | }
5 | .pie-echart {
6 | background-color: #fbfbfb
7 | }
8 | canvas {
9 | position: relative
10 | z-index: 0
11 | width: 100%
12 | }
13 |
14 | .top-rate {
15 | .item {
16 | border-bottom: 1PX dashed #d4d2d2;
17 | background-repeat: no-repeat;
18 | background-image: linear-gradient(#c1f5e7, #c1f5e7);
19 | }
20 | }
21 |
22 | .title {
23 | position: relative;
24 | z-index: 998;
25 | display: inline-block;
26 | color: #40485B;
27 | font-weight: bolder;
28 | border-bottom: 2PX dashed #f4f4f4;
29 | margin: 4PX
30 | &:before {
31 | content: '';
32 | position: absolute;
33 | background: #FFF3AC;
34 | width: calc(100% + 16px);
35 | height: 12PX;
36 | bottom: 0;
37 | left: 0;
38 | border-radius: 12px;
39 | z-index: -1;
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/src/api/logic/finance.ts:
--------------------------------------------------------------------------------
1 | import Request from '../request'
2 | type AssetStatementParams = {
3 | asset_id: number;
4 | year: number;
5 | month: number;
6 | }
7 |
8 | export default class Finance {
9 | private _request: Request
10 | constructor (request: Request) {
11 | this._request = request
12 | }
13 |
14 | async index () {
15 | return await this._request.get('wallet')
16 | }
17 |
18 | async getAssetDetail(assetId: number) {
19 | return await this._request.get('wallet/information', { asset_id: assetId })
20 | }
21 |
22 | async getAssetTimeline(assetId: number) {
23 | return await this._request.get('wallet/time_line', { asset_id: assetId })
24 | }
25 |
26 | async getAssetStatements(params: AssetStatementParams) {
27 | return await this._request.get('wallet/statement_list', params)
28 | }
29 |
30 | async updateAmountVisible({visible}) {
31 | return await this._request.put('users/update_user', { user: { hidden_asset_money: visible } })
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/src/pages/setting/statements_manage/data_in.tsx:
--------------------------------------------------------------------------------
1 | import { Component, useEffect, useState } from 'react'
2 | import { View } from '@tarojs/components'
3 | import jz from '@/jz'
4 | import BasePage from '@/components/BasePage'
5 | import { Button } from '@/src/components/UiComponents'
6 | import config from '@/src/config'
7 | import Taro from '@tarojs/taro'
8 |
9 | const LoginPc: React.FC = () => {
10 | const handleScan = async () => {
11 | const { result } = await Taro.scanCode({
12 | onlyFromCamera: true,
13 | scanType: ['qrCode']
14 | })
15 | const codeData = JSON.parse(result)
16 | await jz.api.users.loginPc(codeData.qr_code_id)
17 | }
18 |
19 | return (
20 |
23 |
24 | 由于小程序端限制且操作麻烦,所以请登录PC端,然后扫码登录。
25 | PC端地址:{config.web_host}
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | export default LoginPc
--------------------------------------------------------------------------------
/src/utils/event.ts:
--------------------------------------------------------------------------------
1 | type EventHandler = (...args: any[]) => void
2 |
3 | export class EventEmitter {
4 | private events: Map
5 |
6 | constructor() {
7 | this.events = new Map()
8 | }
9 |
10 | on(event: string, handler: EventHandler) {
11 | if (!this.events.has(event)) {
12 | this.events.set(event, [])
13 | }
14 | this.events.get(event)!.push(handler)
15 | }
16 |
17 | off(event: string, handler?: EventHandler) {
18 | if (!handler) {
19 | this.events.delete(event)
20 | return
21 | }
22 |
23 | const handlers = this.events.get(event)
24 | if (handlers) {
25 | const index = handlers.indexOf(handler)
26 | if (index > -1) {
27 | handlers.splice(index, 1)
28 | }
29 | if (handlers.length === 0) {
30 | this.events.delete(event)
31 | }
32 | }
33 | }
34 |
35 | emit(event: string, ...args: any[]) {
36 | const handlers = this.events.get(event)
37 | if (handlers) {
38 | handlers.forEach(handler => handler(...args))
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/api/logic/account_book.ts:
--------------------------------------------------------------------------------
1 | import Request from '../request'
2 |
3 | export default class AccountBook {
4 | private _request: Request
5 | constructor (request: Request) {
6 | this._request = request
7 | }
8 |
9 | getAccountBooks() {
10 | return this._request.get('account_books')
11 | }
12 |
13 | getAccountBook(id) {
14 | return this._request.get(`account_books/${id}`)
15 | }
16 |
17 | getAccountBookTypes() {
18 | return this._request.get('account_books/types')
19 | }
20 |
21 | getCategoriesList({ accountType }) {
22 | return this._request.get('account_books/preset_categories', { account_type: accountType })
23 | }
24 |
25 | updateDefaultAccount(accountBook) {
26 | return this._request.put(`account_books/${accountBook.id}/switch`, {})
27 | }
28 |
29 | create(data) {
30 | return this._request.post('account_books', data)
31 | }
32 |
33 | update(id, data) {
34 | return this._request.put(`account_books/${id}`, data)
35 | }
36 |
37 | destroy(id) {
38 | return this._request.delete(`account_books/${id}`)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/pages/setting/feedback/index.tsx:
--------------------------------------------------------------------------------
1 | import { Component, useEffect, useState } from 'react'
2 | import { View, Image } from '@tarojs/components'
3 | import jz from '@/jz'
4 | import BasePage from '@/components/BasePage'
5 | import Statements from '@/components/Statements'
6 | import { AtTextarea } from 'taro-ui'
7 | import { Button } from '@/src/components/UiComponents'
8 |
9 | const Feedback: React.FC = () => {
10 | const [text, setText] = useState("")
11 |
12 | const submit = async () => {
13 | jz.api.chaos.submitFeedback({content: text})
14 | jz.router.navigateBack()
15 | }
16 |
17 | return (
18 |
21 |
22 |
23 | 感谢您的支持与反馈,如有需要可以微信联系我sheepzom,或者也可以在此写下您的建议,谢谢你的使用。
24 |
25 | setText(t)}
28 | placeholder='在此写下您的反馈...'
29 | />
30 |
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | export default Feedback
--------------------------------------------------------------------------------
/src/pages/payee/list.scss:
--------------------------------------------------------------------------------
1 | .payee-list {
2 | .payee-item {
3 | border-bottom: 1px solid #eee;
4 |
5 | .order-buttons {
6 | display: flex;
7 | flex-direction: column;
8 |
9 | .iconfont {
10 | font-size: 24px;
11 | color: #666;
12 | padding: 4px;
13 |
14 | &.disabled {
15 | color: #ccc;
16 | cursor: not-allowed;
17 | }
18 |
19 | &:not(.disabled):hover {
20 | color: #1890ff;
21 | }
22 | }
23 | }
24 |
25 | .action-btn {
26 | display: flex;
27 | align-items: center;
28 | padding: 4px 8px;
29 | border-radius: 4px;
30 | transition: all 0.3s;
31 |
32 | .iconfont {
33 | font-size: 16px;
34 | }
35 |
36 | &.edit-btn {
37 | color: #1890ff;
38 |
39 | &:hover {
40 | background: rgba(24, 144, 255, 0.1);
41 | }
42 | }
43 |
44 | &.delete-btn {
45 | color: #ff4d4f;
46 |
47 | &:hover {
48 | background: rgba(255, 77, 79, 0.1);
49 | }
50 | }
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/src/assets/styl/pages/friends/invite_info.styl:
--------------------------------------------------------------------------------
1 | .friend-invite-page
2 | padding: 16PX
3 |
4 | .invite-info-card
5 | background: #fff
6 | border-radius: 12PX
7 | padding: 24PX
8 | box-shadow: 0 2PX 8PX var(--border-color)
9 |
10 | .avatar-section
11 | display: flex
12 | flex-direction: column
13 | align-items: center
14 | margin-bottom: 24PX
15 |
16 | .avatar
17 | width: 80PX
18 | height: 80PX
19 | border-radius: 50%
20 | margin-bottom: 12PX
21 |
22 | .nickname
23 | font-size: 18PX
24 | font-weight: 500
25 | color: var(--text-color)
26 |
27 | .info-section
28 | .info-item
29 | display: flex
30 | justify-content: space-between
31 | align-items: center
32 | padding: 12PX 0
33 | border-bottom: 1PX solid var(--border-color)
34 |
35 | .label
36 | color: var(--secondary-text)
37 | font-size: 14PX
38 |
39 | .value
40 | color: var(--text-color)
41 | font-size: 14PX
42 |
43 | .accept-btn
44 | margin-top: 32PX
45 | width: 100%
46 | background: var(--primary-bg-color)
47 | color: var(--primary-bg-text-color)
48 | border-radius: 8PX
--------------------------------------------------------------------------------
/src/pages/setting/search/search.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { View, Input } from '@tarojs/components'
3 | import jz from '@/jz'
4 | import BasePage from '@/components/BasePage'
5 | import Statements from '@/components/Statements'
6 |
7 | const Search: React.FC = () => {
8 | const [keyword, setKeyword] = useState('')
9 | const [statements, setStatements] = useState([])
10 |
11 | const onChange = async (keyword) => {
12 | setKeyword(keyword)
13 | const {data} = await jz.api.statements.searchStatements(keyword)
14 | if (data.status === 404) {
15 | setStatements([])
16 | } else {
17 | setStatements(data)
18 | }
19 | }
20 |
21 | return (
22 |
25 |
26 | { onChange(e.detail.value) }}
32 | />
33 |
34 |
35 |
36 | { statements.length === 0 ? '未找到相应账单' : }
37 |
38 |
39 | )
40 | }
41 |
42 | export default Search
--------------------------------------------------------------------------------
/src/components/Select/index.jsx:
--------------------------------------------------------------------------------
1 | import { View } from '@tarojs/components'
2 |
3 | export default function Select ({
4 | children,
5 | onSubmit,
6 | onCancel,
7 | onToggle,
8 | permitCloseMask=true,
9 | title='标题',
10 | open=false
11 | }) {
12 | const cancelBtn = () => {
13 | if (typeof onCancel === 'function') {
14 | onCancel()
15 | }
16 | onToggle()
17 | }
18 |
19 | const okBtn = () => {
20 | if (typeof onSubmit === 'function') {
21 | onSubmit()
22 | }
23 | }
24 |
25 | if (!open) {
26 | return
27 | }
28 |
29 | return (
30 |
31 | { permitCloseMask && onToggle() }}>
32 |
33 |
34 | {title}
35 |
36 |
37 | {children}
38 |
39 |
40 |
41 | cancelBtn()}>取消
42 | okBtn()}>确认
43 |
44 |
45 |
46 | )
47 | }
--------------------------------------------------------------------------------
/src/components/Avatar/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 | import { View, Text } from '@tarojs/components'
3 |
4 | const COLORS = [
5 | '#f56a00', '#7265e6', '#ffbf00', '#00a2ae',
6 | '#1890ff', '#52c41a', '#722ed1', '#eb2f96',
7 | '#faad14', '#13c2c2', '#fa541c', '#a0d911'
8 | ]
9 |
10 | export default function Avatar({ text, size = 34, backgroundColor }) {
11 | const getInitials = (text) => {
12 | if (!text) return '?'
13 | return text.charAt(0).toUpperCase()
14 | }
15 |
16 | const generateColor = useMemo(() => {
17 | if (backgroundColor) return backgroundColor
18 | if (!text) return COLORS[0]
19 | // 使用字符串的 charCode 之和作为随机种子
20 | const total = text.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
21 | return COLORS[total % COLORS.length]
22 | }, [text, backgroundColor])
23 |
24 | return (
25 |
39 | {getInitials(text)}
40 |
41 | )
42 | }
--------------------------------------------------------------------------------
/src/pages/setting/messages/detail.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { View } from '@tarojs/components'
3 | import jz from '@/jz'
4 | import BasePage from '@/components/BasePage'
5 |
6 | const MessageDetail: React.FC = () => {
7 | const [message, setMessage] = useState({})
8 |
9 | const getMessage = async () => {
10 | const params = jz.router.getParams()
11 | const messageId = params.messageId
12 | const { data } = await jz.api.messages.getMessage(messageId)
13 | setMessage(data)
14 | }
15 |
16 | useEffect(() => {
17 | getMessage()
18 | }, [])
19 |
20 | const contentStyle = {
21 | background: "white !important"
22 | }
23 |
24 | return (
25 |
29 |
30 |
31 | {message.title}
32 |
33 |
34 | {message.msg_type} {message.time}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | )
45 | }
46 |
47 | export default MessageDetail
--------------------------------------------------------------------------------
/src/api/logic/friend.ts:
--------------------------------------------------------------------------------
1 | import jz from '@/jz'
2 | import Request from '../request'
3 | import { FriendInviteRequest, FriendInviteResponse } from '../types'
4 |
5 | export default class Friend {
6 | private readonly _request: Request
7 |
8 | constructor(request: Request) {
9 | this._request = request
10 | }
11 |
12 | public async list({
13 | account_book_id
14 | }) {
15 | const st = await this._request.get('friends', { account_book_id: account_book_id })
16 | return st
17 | }
18 |
19 | public async invite(data: FriendInviteRequest) {
20 | const st = await this._request.post('friends/invite', data)
21 | return st
22 | }
23 |
24 | public async information(token: string) {
25 | const st = await this._request.get('friends/invite_information', { invite_token: token })
26 | return st
27 | }
28 |
29 | public async accept(token: string, nickname: string) {
30 | const st = await this._request.post('friends/accept_apply', { invite_token: token, nickname: nickname })
31 | return st
32 | }
33 |
34 | public async remove(data) {
35 | const st = await this._request.delete(`friends/${data.collaborator_id}`, { account_book_id: data.account_book_id })
36 | return st
37 | }
38 |
39 | public async update(data) {
40 | const st = await this._request.put(`friends/${data.collaborator_id}`, data)
41 | return st
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/api/logic/payee.ts:
--------------------------------------------------------------------------------
1 | import Request from '../request'
2 | import jz from '../../jz'
3 |
4 | export interface PayeeType {
5 | id: number
6 | name: string
7 | }
8 |
9 | export default class Payee {
10 | private _request: Request
11 | constructor (request: Request) {
12 | this._request = request
13 | }
14 |
15 | async list(): Promise {
16 | const currentAccountBook = jz.storage.getCurrentAccountBook()
17 | const res = await this._request.get('payees', { account_book_id: currentAccountBook.id })
18 | return res.data
19 | }
20 |
21 | async create(payee: PayeeType): Promise {
22 | const currentAccountBook = jz.storage.getCurrentAccountBook()
23 | const res = await this._request.post('payees', { account_book_id: currentAccountBook.id, payee })
24 | return res.data
25 | }
26 |
27 | async update(payeeId: string, payee: PayeeType): Promise {
28 | const currentAccountBook = jz.storage.getCurrentAccountBook()
29 | const res = await this._request.put(`payees/${payeeId}`, { account_book_id: currentAccountBook.id, payee })
30 | return res.data
31 | }
32 |
33 | async delete(payee: PayeeType): Promise {
34 | const currentAccountBook = jz.storage.getCurrentAccountBook()
35 | const res = await this._request.delete(`payees/${payee.id}`, { account_book_id: currentAccountBook.id })
36 | return res.data
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/assets/styl/themes/root.styl:
--------------------------------------------------------------------------------
1 | $header-height ?= 68PX
2 | $bottom-height ?= 80PX
3 |
4 | .page-root-component {
5 | position relative
6 | display flex
7 | flex-direction column
8 | min-height 100vh
9 |
10 | > .page-root__header-component {
11 | display flex
12 | align-items center
13 | width 100%
14 |
15 | position fixed
16 | top 0
17 | left 0
18 | bottom 0
19 | right 0
20 | z-index 999
21 |
22 | padding-left 1em
23 | padding-right 100PX
24 | background var(--primary-bg-color)
25 | color var(--primary-bg-text-color)
26 | // border-bottom: 1px solid $borderColor
27 | // transition $header-height 200ms
28 |
29 | .header-title {
30 | color: var(--primary-bg-text-color)
31 | }
32 | }
33 |
34 | > .page-root__tab-bar-component {
35 | position fixed
36 | left 0
37 | bottom 0
38 | right 0
39 | z-index 999
40 | background #ffffff
41 |
42 | display flex
43 | align-items center
44 |
45 | min-height $bottom-height
46 | padding-bottom 12PX
47 |
48 | width 100%
49 | border-top: 1PX solid var(--border-color)
50 | > view {
51 | &.active {
52 | color var(--primary-color)
53 | }
54 | }
55 | }
56 |
57 | > .page-root__main-content {
58 | flex: 1
59 | background: var(--background-color)
60 | }
61 | .page-root__main-height-gap {
62 | height: $bottom-height
63 | }
64 | }
--------------------------------------------------------------------------------
/src/components/Statistic/ExpendList.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { Tabs } from '@/src/components/UiComponents'
3 | import { View } from '@tarojs/components'
4 | import jz from '@/jz'
5 | import Statements from '@/components/Statements'
6 | import EmptyTips from '@/components/EmptyTips'
7 | import { format } from 'date-fns'
8 |
9 | const tabs = [
10 | { id: 1, title: '支出' },
11 | { id: 2, title: '收入' }
12 | ]
13 |
14 | export default function ExpendList({
15 | currentDate
16 | }) {
17 | const [currentTab, setCurrentTab] = useState(1)
18 | const [statements, setStatements] = useState([])
19 |
20 | const getStatements = async (tabId) => {
21 | setCurrentTab(tabId)
22 | let type = 'expend'
23 | if (tabId === 2) {
24 | type = 'income'
25 | }
26 | const { data } = await jz.api.statistics.getRate(format(currentDate, 'yyyy-MM'), type)
27 | if (data) {
28 | setStatements(data)
29 | }
30 | }
31 |
32 | useEffect(() => {
33 | getStatements(currentTab)
34 | }, [currentDate]);
35 |
36 | return (
37 |
38 | {
41 | getStatements(tabId)
42 | }}
43 | />
44 |
45 |
46 | { statements.length === 0 && }
47 |
48 |
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/src/pages/setting/messages/index.tsx:
--------------------------------------------------------------------------------
1 | import { Component, useEffect, useState } from 'react'
2 | import { View, Image } from '@tarojs/components'
3 | import jz from '@/jz'
4 | import BasePage from '@/components/BasePage'
5 | import EmptyTips from '@/components/EmptyTips'
6 |
7 |
8 | const MessagePage: React.FC = () => {
9 | const [messages, setMessages] = useState([])
10 |
11 | const getMessages = async () => {
12 | const { data } = await jz.api.messages.getList()
13 | setMessages(data)
14 | }
15 |
16 | useEffect(() => {
17 | getMessages()
18 | }, [])
19 |
20 | return (
21 |
24 |
25 | {messages.length === 0 && }
26 | { messages.map((message) => {
27 | return (
28 | { jz.router.navigateTo({ url: `/pages/setting/messages/detail?messageId=${message.id}` })}}
31 | >
32 | {message.title}
33 |
34 | {message.msg_type}
35 | {message.time}
36 |
37 |
38 | )
39 | }) }
40 |
41 |
42 |
43 | )
44 | }
45 |
46 | export default MessagePage
--------------------------------------------------------------------------------
/src/assets/styl/themes/components/button.styl:
--------------------------------------------------------------------------------
1 | .jz-common-components__button {
2 | background: var(--primary-bg-color)
3 | color: var(--primary-bg-text-color)
4 | margin: 12PX
5 | border-radius: 8PX
6 | transition: opacity 0.3s
7 | box-shadow: 0 2PX 8PX rgba(0, 0, 0, 0.1)
8 |
9 | &:active{
10 | opacity: 0.85
11 | }
12 |
13 | &.primary {
14 | background: #ffffff
15 | color: var(--primary-color)
16 | border: 1PX solid var(--primary-color)
17 | }
18 |
19 | &.dangerous {
20 | background: linear-gradient(135deg, #ff4d4f, #ff7875)
21 | color: #ffffff
22 | border: none
23 | }
24 |
25 | &.disabled {
26 | background: #f5f5f5
27 | color: #999999
28 | box-shadow: none
29 | }
30 | }
31 |
32 | .jz-common-components__small-button {
33 | background: var(--primary-bg-color)
34 | color: var(--primary-bg-text-color)
35 | display: inline
36 | margin: 12PX
37 | border-radius: 6PX
38 | transition: opacity 0.3s
39 | box-shadow: 0 2PX 4PX rgba(0, 0, 0, 0.08)
40 |
41 | &:active {
42 | opacity: 0.85
43 | }
44 |
45 | &.primary {
46 | background: #ffffff
47 | color: var(--primary-color)
48 | border: 1PX solid var(--primary-color)
49 | }
50 |
51 | &.dangerous {
52 | background: linear-gradient(135deg, #ff4d4f, #ff7875)
53 | color: #ffffff
54 | border: none
55 | }
56 |
57 | &.disabled {
58 | background: #f5f5f5
59 | color: #999999
60 | box-shadow: none
61 | }
62 | }
--------------------------------------------------------------------------------
/src/assets/styl/components/slide_sidebar.styl:
--------------------------------------------------------------------------------
1 | .slide-sidebar {
2 | position: fixed
3 | bottom: 0
4 | height: 100vh
5 | width: 100%
6 | z-index: 1000
7 | > .slide-sidebar__mask {
8 | background: rgba(0,0,0, 0.5)
9 | position: absolute
10 | bottom: 0
11 | height: 100vh
12 | width: 100%
13 | z-index: 99
14 | }
15 | > .slide-sidebar__main {
16 | background: #f8f8f8
17 | display: flex
18 | flex-direction: column
19 | position: absolute
20 | top: 0
21 | left: 0
22 | width: 70%
23 | height: 100%
24 | z-index: 100
25 |
26 | animation: slide-animation 0.5s;
27 | -webkit-animation: slide-animation 0.5s;
28 | // animation-fill-mode: forwards;
29 |
30 | .slide-sidebar__main-title {
31 | padding: 12PX;
32 | border-bottom: 1px solid $borderColor;
33 | }
34 | .slide-sidebar__main-content {
35 | overflow-y: auto
36 | flex: 1
37 | }
38 | .slide-header {
39 | font-size: 21PX
40 | margin: 8PX 16PX
41 | }
42 | .account-list {
43 | .account {
44 | border: 1px solid $borderColor
45 | border-radius: 8PX
46 | background: #ffffff
47 | color: var(--text-color)
48 | &.active {
49 | background-color: var(--primary-color) !important
50 | color: #ffffff !important
51 | }
52 | }
53 | }
54 | }
55 |
56 | @-webkit-keyframes slide-animation
57 | {
58 | 0% {width: 0%}
59 | 100% {width: 70%}
60 | }
61 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 洁账小程序
2 |
3 | 重要:基于 wepy 的老版本已经不维护了,请切换到 Taro 最新版本!!
4 |
5 | 一款简单易用的记账小程序,帮助用户轻松管理个人财务。
6 |
7 | 后端代码请切换到旧版本-配合 wepy 前端使用:https://github.com/yigger/jiezhang/tree/old-version-wepy
8 |
9 | 新版(此版本)的后端暂未开源,敬请期待!
10 |
11 | ### 体验二维码
12 | 
13 |
14 | ## 功能特点
15 |
16 | - 📝 快速记账:支持收入、支出、转账等多种记账类型
17 | - 👥 多人协作:支持添加好友共同记账
18 | - 📊 数据统计:直观的图表展示收支情况
19 | - 💰 预算管理:设置预算,控制支出
20 | - 📱 跨端同步:数据云端同步,随时随地记账
21 | - 🔍 账单搜索:快速查找历史账单
22 |
23 | ## 技术栈
24 | - Taro v3.6.32
25 | - React
26 | - TypeScript
27 | - Taro UI
28 | - 微信小程序原生能力
29 |
30 | ## 环境要求
31 | - Node.js 16.14.0
32 | - Taro CLI 3.6.32
33 |
34 | ## 开始使用
35 |
36 | 1. 克隆项目
37 | ```bash
38 | git clone git@github.com:yigger/jiezhang.git jiezhang-miniapp
39 | cd jiezhang-miniapp
40 | ```
41 |
42 | 2. 安装依赖
43 | ```bash
44 | npm install
45 | ```
46 |
47 | 3. 配置
48 | ```bash
49 | // 配置小程序 appid, 服务端地址
50 | cp src/config/config.ts.example src/config/config.ts
51 |
52 | // 开发环境构建
53 | npm run dev:weapp
54 |
55 | // 生产环境构建
56 | npm run build:weapp
57 | ```
58 |
59 | ## 项目结构
60 | ```
61 | src/
62 | ├── api/ # API 接口
63 | ├── assets/ # 静态资源
64 | ├── components/ # 公共组件
65 | ├── config/ # 配置文件
66 | ├── pages/ # 页面文件
67 | ├── router/ # 路由组件
68 | ├── store/ # 状态管理
69 | ├── utils/ # 工具函数
70 | └── app.tsx # 应用入口
71 | └── app.config.ts # 小程序配置文件
72 | └── jz.ts # 全局变量
73 | ```
74 |
75 | ## 贡献
76 | 欢迎贡献代码,提交 issue,或者提供反馈。
77 |
78 | ## 许可证
79 | MIT
80 |
--------------------------------------------------------------------------------
/src/assets/styl/vars.styl:
--------------------------------------------------------------------------------
1 | // 主题色(深色)
2 | $darkPrimaryColor ?= #1976D2
3 |
4 | // 主题色(浅色)
5 | $primaryColor ?= #2196F3
6 |
7 | // 文本颜色
8 | $primaryText ?= #212121
9 |
10 | // 二级文本颜色
11 | $secondaryText ?= #757575
12 |
13 | // 分隔符颜色
14 | $divideText ?= #BDBDBD
15 |
16 | // 金额支出颜色
17 | $expendColor ?= #2ecc71
18 |
19 | // 金额收入颜色
20 | $incomeColor ?= #e74c3c
21 |
22 | // 分隔符背景色
23 | $secondaryBackgroundColor ?= #f1f1f1
24 |
25 | $iconGrey ?= #bcbcbc
26 |
27 | $linkColor ?= #537c8d
28 |
29 |
30 | // ======= 全局通用
31 | // 边框颜色
32 | $borderColor ?= #e4e4e4
33 |
34 | // 背景色
35 | $backgroundColor ?= #f4f4f4
36 |
37 | .col-expend, .col-debt {
38 | color: $expendColor
39 | }
40 |
41 | .col-transfer {
42 | color: #95a5a6
43 | }
44 |
45 | .col-loan_in {
46 | color: #3498db
47 | }
48 |
49 | .col-loan_out {
50 | color: #2980b9
51 | }
52 |
53 | .col-repayment {
54 | color: #9b59b6
55 | }
56 |
57 | .col-reimburse {
58 | color: #f39c12
59 | }
60 |
61 | .col-payment_proxy {
62 | color: #1abc9c
63 | }
64 |
65 | .col-income, .col-deposit {
66 | color: $incomeColor
67 | }
68 |
69 | .col-text {
70 | color: $primaryText
71 | }
72 |
73 | .col-text-mute {
74 | color: $secondaryText
75 | }
76 |
77 | .col-text-warn {
78 | color: #e74c3c
79 | }
80 |
81 | .col-text-link {
82 | color: $linkColor
83 | }
84 |
85 | .col-pure-white {
86 | color: #FFFFFF
87 | }
88 |
89 | .bg-color-white {
90 | background-color: #FFFFFF
91 | }
92 |
93 | .bg-color-fbfbfb {
94 | background-color: #FBFBFB
95 | }
96 |
97 | view {
98 | box-sizing border-box
99 | }
--------------------------------------------------------------------------------
/src/pages/home/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import BasePage from '@/components/BasePage'
3 | import { View } from '@tarojs/components'
4 | import { useShareAppMessage } from "@tarojs/taro"
5 | import { IndexPage, StatisticPage, FinancePage, ProfilePage } from '@/components/Home'
6 | import config from '../../config'
7 |
8 | const tabs = [
9 | {
10 | page: 'index',
11 | name: '首页',
12 | icon: 'jcon-home1'
13 | },
14 | {
15 | page: 'statistic',
16 | name: '统计',
17 | icon: 'jcon-linechart'
18 | },
19 | {
20 | page: 'asset',
21 | name: '资产',
22 | icon: 'jcon-creditcard'
23 | },
24 | {
25 | page: 'profile',
26 | name: '我的',
27 | icon: 'jcon-user'
28 | }
29 | ]
30 |
31 | export default function Home() {
32 | const [activeTab, setActiveTab] = useState(tabs[0])
33 |
34 | useShareAppMessage(async () => {
35 | return {
36 | title: '我在使用洁账记账,快来一起记账吧',
37 | path: `/pages/home/index`,
38 | imageUrl: `${config.host}/logo.png`
39 | }
40 | })
41 |
42 | return (
43 | setActiveTab(tab)}
49 | >
50 |
51 | { activeTab.page === 'index' && }
52 | { activeTab.page === 'statistic' && }
53 | { activeTab.page === 'asset' && }
54 | { activeTab.page === 'profile' && }
55 |
56 |
57 | )
58 | }
--------------------------------------------------------------------------------
/src/utils/echart_option.ts:
--------------------------------------------------------------------------------
1 | export const getExpendLineOption = (data) => {
2 | return {
3 | legend: {
4 | },
5 | tooltip: {},
6 | xAxis: { type: 'category', gridIndex: 0, data: data.months.map((item) => `${item}月`)},
7 | yAxis: [
8 | {
9 | type: 'value'
10 | }
11 | ],
12 | series: [
13 | {
14 | name: '支出',
15 | type: 'bar',
16 | barGap: 0,
17 | label: {
18 | show: true
19 | },
20 | itemStyle: {
21 | normal: {
22 | color: '#008000'
23 | }
24 | },
25 | emphasis: {
26 | focus: 'series'
27 | },
28 | data: data.expends
29 | },
30 | {
31 | name: '收入',
32 | type: 'bar',
33 | barGap: 0,
34 | label: {
35 | show: true
36 | },
37 | itemStyle: {
38 | normal: {
39 | color: '#FF0000'
40 | }
41 | },
42 | emphasis: {
43 | focus: 'series'
44 | },
45 | data: data.incomes
46 | }
47 | ]
48 | };
49 | }
50 |
51 | export const getPieOption = (data) => {
52 | return {
53 | title: {
54 | text: '消费分类占比',
55 | left: 'center'
56 | },
57 | tooltip: {
58 | trigger: 'item'
59 | },
60 | legend: {
61 | orient: 'vertical',
62 | left: 'left'
63 | },
64 | series: [
65 | {
66 | type: 'pie',
67 | radius: '50%',
68 | data: data,
69 | emphasis: {
70 | itemStyle: {
71 | shadowBlur: 10,
72 | shadowOffsetX: 0,
73 | shadowColor: 'rgba(0, 0, 0, 0.5)'
74 | }
75 | }
76 | }
77 | ]
78 | }
79 | }
--------------------------------------------------------------------------------
/src/assets/styl/components/asset_banner.styl:
--------------------------------------------------------------------------------
1 | .asset-banner-component
2 | .banner-card
3 | background: var(--primary-bg-color)
4 | border-radius: 20PX
5 | padding: 21PX
6 | color: var(--primary-bg-text-color)
7 | margin-bottom: 16PX
8 | box-shadow: 0 4PX 16PX rgba(0, 0, 0, 0.1)
9 |
10 | .banner-main
11 | margin-bottom: 24PX
12 |
13 | .banner-title
14 | display: flex
15 | justify-content: space-between
16 | align-items: center
17 | margin-bottom: 16PX
18 |
19 | .title
20 | font-size: 16PX
21 | font-weight: 500
22 |
23 | .visibility-toggle
24 | .toggle-btn
25 | display: flex
26 | align-items: center
27 | opacity: 0.9
28 | transition: opacity 0.3s
29 |
30 | &:active
31 | opacity: 0.7
32 |
33 | .iconfont
34 | font-size: 18PX
35 | margin-right: 4PX
36 |
37 | .toggle-text
38 | font-size: 14PX
39 |
40 | .banner-amount
41 | .amount-prefix
42 | font-size: 20PX
43 | opacity: 0.9
44 | margin-right: 4PX
45 |
46 | .amount-value
47 | font-size: 24PX
48 | font-weight: 600
49 |
50 | .banner-footer
51 | display: flex
52 | justify-content: space-between
53 | padding-top: 16PX
54 | border-top: 1PX solid rgba(255, 255, 255, 0.15)
55 |
56 | .footer-item
57 | display: flex
58 | flex-direction: column
59 |
60 | .item-label
61 | font-size: 14PX
62 | opacity: 0.8
63 | margin-bottom: 4PX
64 |
65 | .item-value
66 | font-size: 16PX
67 | font-weight: 500
--------------------------------------------------------------------------------
/src/app.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | pages: [
3 | // 首页
4 | 'pages/home/index',
5 | // 创建账单的表单
6 | 'pages/statement/form',
7 | 'pages/statement_detail/index',
8 | // 分类管理
9 | 'pages/setting/category/index',
10 | 'pages/setting/category/form',
11 | // 资产管理
12 | 'pages/setting/asset/index',
13 | 'pages/setting/asset/form',
14 | // 预算管理
15 | 'pages/setting/budget/index',
16 | 'pages/setting/child_budget/index',
17 | // 账簿管理
18 | "pages/account_books/create",
19 | "pages/account_books/edit",
20 | "pages/account_books/list",
21 | // 设置的相关页面
22 | 'pages/setting/search/search',
23 | 'pages/setting/statements_flow/index',
24 | 'pages/assets_flow/index',
25 | "pages/setting/feedback/index",
26 | "pages/setting/messages/index",
27 | "pages/setting/messages/detail",
28 | "pages/setting/user_info/index",
29 | 'pages/setting/chart/category_statement',
30 | 'pages/setting/statement_imgs/index',
31 | // 导出
32 | 'pages/setting/statements_manage/data_out',
33 | // 登录网页端
34 | 'pages/setting/statements_manage/data_in',
35 | // 账单的分享页面
36 | 'pages/share/index',
37 | 'pages/share/public',
38 | // 商家管理界面
39 | 'pages/payee/list',
40 | // 好友管理界面
41 | 'pages/friends/index',
42 | 'pages/friends/invite_info',
43 | ],
44 | subPackages: [
45 | {
46 | root: 'pages/sub',
47 | pages: ['chart/index'],
48 | independent: true,
49 | },
50 | ],
51 | requiredPrivateInfos: [
52 | 'chooseLocation'
53 | ],
54 | window: {
55 | navigationBarTitleText: 'WeChat',
56 | backgroundTextStyle : 'light',
57 | navigationBarBackgroundColor: '#fff',
58 | navigationBarTextStyle : 'white',
59 | navigationStyle : 'custom',
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/assets/styl/components/select_component.styl:
--------------------------------------------------------------------------------
1 | .select-component {
2 | position: fixed
3 | bottom: 0
4 | min-height: 400PX
5 | max-height: 780PX
6 | width: 100%
7 | z-index: 1000
8 | > .select__mask {
9 | background: rgba(0,0,0, 0.5)
10 | position: absolute
11 | bottom: 0
12 | height: 100vh
13 | width: 100%
14 | z-index: 99
15 | }
16 | > .select__main {
17 | background: #f9f9f9
18 | border-radius: 6PX 6PX 0 0
19 | display: flex
20 | flex-direction: column
21 | position: absolute
22 | bottom: 0
23 | left: 0
24 | width: 100%
25 | height: 80%
26 | z-index: 100
27 |
28 | animation: select-animation 0.5s;
29 | -webkit-animation: select-animation 0.5s;
30 | animation-fill-mode: forwards;
31 |
32 | .select__main-title {
33 | padding: 12PX;
34 | border-bottom: 1px solid $borderColor;
35 | }
36 | .select__main-content {
37 | overflow-y: auto
38 | flex: 1
39 | }
40 | .select-item.active {
41 | background-color: #e8e8e8;
42 | }
43 | }
44 |
45 | .button-group {
46 | .btn {
47 | // border: 1px solid var(--primary-bg-color)
48 | padding: 8px 28px;
49 | }
50 | .ok-btn {
51 | background: #0ba71e;
52 | color: white;
53 | border-radius: 26px;
54 | }
55 | .cancel-btn {
56 | background: #8a8e8b;
57 | color: white;
58 | border-radius: 26px;
59 | }
60 |
61 | }
62 |
63 | > .list__item {
64 | display: inline-block
65 | text-align: center
66 | width: 20%
67 | margin: 12PX 0
68 | image {
69 | width: 30PX
70 | height: 30PX
71 | }
72 | }
73 | > .select__header {
74 | font-size: 16PX
75 | margin: 12PX
76 | }
77 |
78 | @-webkit-keyframes select-animation
79 | {
80 | 0% {height: 0%}
81 | 100% {height: 80%}
82 | }
83 | }
--------------------------------------------------------------------------------
/src/components/Calculator/index.scss:
--------------------------------------------------------------------------------
1 | .calculator {
2 | position: fixed;
3 | bottom: 0;
4 | left: 0;
5 | right: 0;
6 | background: #f5f5f5;
7 | z-index: 100;
8 | border-top: 1px solid #d6d6d6;
9 |
10 | &__display {
11 | padding: 30px 20px;
12 | background: #fff;
13 | text-align: right;
14 |
15 | .prev-value {
16 | font-size: 24px;
17 | color: #999;
18 | margin-bottom: 10px;
19 | }
20 |
21 | .amount {
22 | font-size: 40px;
23 | color: var(--col-expend);
24 | }
25 | }
26 |
27 | &__keypad {
28 | display: flex;
29 | padding: 15px;
30 | }
31 |
32 | .keypad-left {
33 | flex: 1;
34 | margin-right: 15px;
35 |
36 | .row {
37 | display: flex;
38 | margin-bottom: 15px;
39 |
40 | &:last-child {
41 | margin-bottom: 0;
42 | }
43 |
44 | .key {
45 | flex: 1;
46 | margin-right: 15px;
47 |
48 | &:last-child {
49 | margin-right: 0;
50 | }
51 | }
52 | }
53 | }
54 |
55 | .keypad-right {
56 | width: 130px;
57 | display: flex;
58 | flex-direction: column;
59 |
60 | .key {
61 | margin-bottom: 12px;
62 |
63 | &:last-child {
64 | margin-bottom: 0;
65 | }
66 |
67 | &.confirm {
68 | flex: 1;
69 | height: auto;
70 | }
71 | }
72 | }
73 |
74 | .key {
75 | height: 130px;
76 | background: #fff;
77 | border-radius: 8px;
78 | display: flex;
79 | align-items: center;
80 | justify-content: center;
81 | font-size: 32px;
82 |
83 | &.operator {
84 | &:active {
85 | background: #e8e8e8;
86 | }
87 |
88 | &.confirm:active {
89 | background: #e8e8e8;
90 | }
91 | }
92 |
93 | &:active {
94 | background: #e0e0e0;
95 | transform: scale(0.95);
96 | }
97 | }
98 | }
--------------------------------------------------------------------------------
/src/pages/statement/form.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { View } from '@tarojs/components'
3 | import BasePage from '@/components/BasePage'
4 | import { Tabs } from '@/src/components/UiComponents'
5 | import { format } from 'date-fns'
6 | import BaseForm from '@/components/statementForm/baseForm'
7 |
8 | const tabs = [
9 | { id: 1, title: '支出', type: 'expend' },
10 | { id: 2, title: '收入', type: 'income' },
11 | { id: 3, title: '转账', type: 'transfer' },
12 | { id: 4, title: '还债', type: 'repayment' },
13 | { id: 5, title: '代付', type: 'payment_proxy' },
14 | { id: 6, title: '报销', type: 'reimburse' },
15 | { id: 7, title: '借入', type: 'loan_in' },
16 | { id: 8, title: '借出', type: 'loan_out' },
17 | ]
18 |
19 | const StatementForm: React.FC = () => {
20 | const [typeName, setTypeName] = useState('支出')
21 | const [currentTab, setCurrentTab] = useState(1)
22 | const [statement, setStatement] = useState({
23 | id: 0,
24 | type: 'expend',
25 | amount: '',
26 | category_id: 0,
27 | asset_id: 0,
28 | upload_files: [],
29 | date: format(new Date(), 'yyyy-MM-dd'),
30 | time: format(new Date(), 'HH:mm'),
31 | description: ''
32 | })
33 |
34 | return (
35 |
38 | {
41 | setTypeName(tabs[tabId-1].title)
42 | setStatement({
43 | ...statement,
44 | type: tabs[tabId-1].type
45 | })
46 | setCurrentTab(tabId)
47 | }}
48 | />
49 |
50 |
56 |
57 |
58 | )
59 | }
60 |
61 | export default StatementForm
--------------------------------------------------------------------------------
/src/api/types.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | id: number
3 | nickname: string
4 | avatar_path: string
5 | }
6 |
7 | // 请求失败的响应通用
8 | type FailureResponse = {
9 | status: Exclude
10 | data?: never
11 | msg: string
12 | }
13 |
14 | // AccountBook 通用格式
15 | export interface AccountBook {
16 | id: number
17 | name: string
18 | }
19 |
20 | export interface HeaderMessage {
21 | id: number
22 | title: string
23 | }
24 |
25 | export interface HeaderResponse {
26 | message: HeaderMessage
27 | month_budget: string
28 | month_expend: string
29 | today_expend: string
30 | use_pencentage: number
31 | }
32 |
33 | export interface Statement {
34 | id: number
35 | type: 'expend' | 'income' | 'transfer'
36 | description: string
37 | title: string | null
38 | amount: string
39 | money: string
40 | asset: string
41 | category: string
42 | city: string | null
43 | date: string
44 | icon_path: string
45 | location: string | null
46 | month_day: string
47 | province: string | null
48 | street: string | null
49 | time: string
50 | timeStr: string
51 | week: string
52 | }
53 |
54 | export interface StatementsResponse {
55 | statements: Statement[]
56 | }
57 |
58 | // 邀请好友的请求
59 | export interface FriendInviteRequest {
60 | account_book_id: number
61 | role: string
62 | }
63 |
64 | // 邀请好友成功的响应
65 | type FriendInviteSuccessResponse = {
66 | status: 200
67 | data: string
68 | msg?: never
69 | }
70 | export type FriendInviteResponse = FriendInviteSuccessResponse | FailureResponse
71 |
72 | // 邀请成功的邀请信息的响应
73 | // 包含了被邀请人的信息和被邀请的账簿的信息
74 | export interface InviteInfo {
75 | account_book: AccountBook
76 | invite_user: User
77 | role_name: string
78 | }
79 |
80 | interface InviteInfoSuccessResponse {
81 | status: 200
82 | data: InviteInfo
83 | msg?: never
84 | }
85 | export type InviteInfoResponse = InviteInfoSuccessResponse | FailureResponse
--------------------------------------------------------------------------------
/src/assets/styl/pages/home/index.styl:
--------------------------------------------------------------------------------
1 | .jz-pages__index {
2 | .jz-pages__index-header {
3 | .row-content-block {
4 | background: #ffffff
5 | padding: 16PX
6 | border-radius: 21PX
7 | width: 45%
8 | text-align: center
9 | color: var(--primary-color)
10 | box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1)
11 |
12 | .amount-item {
13 | font-weight: 600
14 | }
15 |
16 | .col-text-mute {
17 | color: var(--secondary-text)
18 | }
19 | }
20 |
21 | .trend-block {
22 | background: #ffffff
23 | border-radius: 12PX
24 | padding: 16PX
25 | box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1)
26 |
27 | .trend-item {
28 | padding: 12PX
29 | border-radius: 8PX
30 | width: 30%
31 | }
32 | }
33 | }
34 | .jz-pages__notification {
35 | background: #ece9e9
36 | }
37 |
38 | .remark-statement-btn {
39 | background-color: $primaryBgColor
40 | color: #fbfbfb;
41 | border-radius: 18PX;
42 | padding: 0 16PX;
43 | }
44 |
45 | .budget-item {
46 | background: #ffffff
47 | padding: 16PX
48 | border-radius: 21PX
49 | border: 1PX solid var(--border-color)
50 |
51 | .col-text-mute {
52 | color: var(--secondary-text)
53 | }
54 |
55 | .at-progress__outer {
56 | background: var(--border-color)
57 | }
58 |
59 | // .at-progress__outer-inner {
60 | // background: var(--primary-color)
61 | // }
62 | }
63 | .time-range-tabs {
64 | .tab-item {
65 | padding: 4px 12px
66 | margin: 0 4px
67 | border-radius: 12px
68 | color: var(--text-color-light)
69 | background: var(--bg-color-light)
70 |
71 | &.active {
72 | color: var(--primary-color)
73 | background: var(--primary-color-light)
74 | }
75 |
76 | &:active {
77 | opacity: 0.8
78 | }
79 | }
80 | }
81 | }
82 |
83 |
--------------------------------------------------------------------------------
/src/components/AssetBanner/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View, Text } from '@tarojs/components'
3 |
4 | const AssetBanner = ({
5 | firstColumn={},
6 | secColumn={},
7 | thirdColumn={},
8 | amountVisible=false,
9 | updateSecretAmount=null
10 | }) => {
11 | return (
12 |
13 |
14 |
15 |
16 | {firstColumn['title']}
17 |
18 | {!amountVisible ? (
19 |
20 |
21 | 显示数额
22 |
23 | ) : (
24 |
25 |
26 | 隐藏数额
27 |
28 | )}
29 |
30 |
31 |
32 | ¥
33 | {firstColumn['amount']}
34 |
35 |
36 |
37 |
38 |
39 | {secColumn['title']}
40 | ¥{secColumn['amount']}
41 |
42 |
43 | {thirdColumn['title']}
44 | ¥{thirdColumn['amount']}
45 |
46 |
47 |
48 |
49 | )
50 | }
51 |
52 | export default AssetBanner
--------------------------------------------------------------------------------
/src/assets/styl/pages/finance/index.styl:
--------------------------------------------------------------------------------
1 | .jz-pages__finance
2 | padding: 16PX
3 | min-height: 100vh
4 | background: var(--background-color)
5 |
6 | .jz-pages__finance-list
7 | margin-top: 16PX
8 |
9 | .jz-pages__finance-list__item
10 | background-color: white
11 | border-radius: 16PX
12 | margin-bottom: 16PX
13 | box-shadow: 0 2PX 12PX rgba(0, 0, 0, 0.05)
14 | overflow: hidden
15 |
16 | .jz-pages__finance__child-total
17 | padding: 16PX
18 | border-bottom: 1px solid rgba(0, 0, 0, 0.06)
19 | display: flex
20 | justify-content: space-between
21 | align-items: center
22 |
23 | .asset-name
24 | font-size: 15PX
25 | color: #333
26 | font-weight: 500
27 |
28 | .asset-amount
29 | font-size: 16PX
30 | color: #1890ff
31 | font-weight: 500
32 |
33 | .jz-pages__finance__child-list
34 | padding: 12PX 16PX
35 | position: relative
36 | transition: all 0.3s
37 |
38 | &:active
39 | background-color: rgba(0, 0, 0, 0.02)
40 |
41 | &:not(:last-child)
42 | border-bottom: 1px solid rgba(0, 0, 0, 0.04)
43 |
44 | .icon-wrapper
45 | margin-right: 12PX
46 | width: 40PX
47 | height: 40PX
48 | border-radius: 50%
49 | overflow: hidden
50 | display: flex
51 | align-items: center
52 | justify-content: center
53 |
54 | .asset-icon
55 | width: 100%
56 | height: 100%
57 | object-fit: cover
58 |
59 | .asset-detail
60 | flex: 1
61 | display: flex
62 | justify-content: space-between
63 | align-items: center
64 |
65 | .asset-name
66 | font-size: 14PX
67 | color: #333
68 |
69 | .asset-amount
70 | font-size: 14PX
71 | color: #666
72 | font-weight: 500
--------------------------------------------------------------------------------
/src/components/Statistic/Summary.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useContext } from 'react'
2 | import { View, Text } from '@tarojs/components'
3 | import Statements from '@/components/Statements'
4 | import { HomeStoreContext } from "@/src/stores"
5 | import { observer } from 'mobx-react'
6 | import EmptyTips from '@/components/EmptyTips'
7 | import { format } from 'date-fns'
8 |
9 | export default observer(function Summary({
10 | currentDate
11 | }) {
12 | const store: HomeStoreContext = useContext(HomeStoreContext)
13 | useEffect(() => {
14 | store.getSummaryData(format(currentDate, 'yyyy-MM'))
15 | }, [currentDate])
16 |
17 | return (
18 |
19 |
20 |
21 | {store.summaryData.header['expend']}
22 | 总支出
23 |
24 |
25 |
26 | {store.summaryData.header['income']}
27 | 总收入
28 |
29 |
30 |
31 | {store.summaryData.header['transfer']}
32 | 转账
33 |
34 |
35 |
36 | {store.summaryData.header['repay']}
37 | 还款
38 |
39 |
40 |
41 | 总计(收入-支出-还款):{ store.summaryData.header['total'] }
42 |
43 |
44 |
45 | 当月账单列表
46 |
47 | { store.summaryData.statements.length === 0 && }
48 |
49 |
50 |
51 |
52 | )
53 | })
--------------------------------------------------------------------------------
/src/pages/setting/statement_imgs/index.tsx:
--------------------------------------------------------------------------------
1 | import { Component, useEffect, useState } from 'react'
2 | import { View, Image } from '@tarojs/components'
3 | import jz from '@/jz'
4 | import BasePage from '@/components/BasePage'
5 | import Taro from "@tarojs/taro"
6 |
7 | const StatementImgs: React.FC = () => {
8 | const [dataTimeline, setDateTimeline] = useState([])
9 | const itemWidth = jz.systemInfo.screenWidth / 4 - 2
10 | const fetchImages = async () => {
11 | const {data} = await jz.api.statements.getStatementImages()
12 | setDateTimeline(data.data.avatar_timeline)
13 |
14 | }
15 | const showPicturePreview = (e) => {
16 | Taro.previewImage({
17 | enablesavephoto: true,
18 | current: e,
19 | urls: [e.path]
20 | })
21 | }
22 |
23 | useEffect(() => {
24 | fetchImages()
25 | }, [])
26 |
27 | return (
28 |
31 |
32 |
33 | {dataTimeline.map((item, index) => (
34 |
35 |
36 | {item.year}年
37 |
38 | {item.data.map((data, dataIndex) => (
39 |
40 | {data.month}月
41 |
42 | {data.data && data.data.map((e, eIndex) => (
43 |
44 | showPicturePreview(e)}
47 | lazy-load="true"
48 | src={e.path}
49 | >
50 | { jz.router.navigateTo({ url: `/pages/statement_detail/index?statement_id=${e.statement_id}` }) }}>
51 | 查看账单详情
52 |
53 |
54 | ))}
55 |
56 |
57 | ))}
58 |
59 | ))}
60 |
61 |
62 |
63 | )
64 | }
65 |
66 | export default StatementImgs
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const config = {
4 | projectName: 'jz-taro',
5 | date: '2021-4-4',
6 | designWidth: 750,
7 | deviceRatio: {
8 | 640: 2.34 / 2,
9 | 750: 1,
10 | 828: 1.81 / 2
11 | },
12 | sourceRoot: 'src',
13 | outputRoot: 'dist',
14 | plugins: [
15 | path.resolve(__dirname, '../plugins/view-data-plugin.js')
16 | ],
17 | defineConstants: {
18 | },
19 | copy: {
20 | patterns: [
21 | ],
22 | options: {
23 | }
24 | },
25 | framework: 'react',
26 | mini: {
27 | postcss: {
28 | pxtransform: {
29 | enable: true,
30 | config: {
31 |
32 | }
33 | },
34 | url: {
35 | enable: true,
36 | config: {
37 | limit: 1024 // 设定转换尺寸上限
38 | }
39 | },
40 | cssModules: {
41 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
42 | config: {
43 | namingPattern: 'module', // 转换模式,取值为 global/module
44 | generateScopedName: '[name]__[local]___[hash:base64:5]'
45 | }
46 | }
47 | }
48 | },
49 | h5: {
50 | publicPath: '/',
51 | staticDirectory: 'static',
52 | postcss: {
53 | autoprefixer: {
54 | enable: true,
55 | config: {
56 | }
57 | },
58 | cssModules: {
59 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
60 | config: {
61 | namingPattern: 'module', // 转换模式,取值为 global/module
62 | generateScopedName: '[name]__[local]___[hash:base64:5]'
63 | }
64 | }
65 | }
66 | },
67 | alias: {
68 | '@/src': path.resolve(__dirname, '..', 'src'),
69 | '@/jz': path.resolve(__dirname, '..', 'src/jz'),
70 | '@/api': path.resolve(__dirname, '..', 'src/api'),
71 | '@/components': path.resolve(__dirname, '..', 'src/components'),
72 | '@/assets': path.resolve(__dirname, '..', 'src/assets'),
73 | '@/utils': path.resolve(__dirname, '..', 'src/utils')
74 | }
75 | }
76 |
77 | module.exports = function (merge) {
78 | if (process.env.NODE_ENV === 'development') {
79 | return merge({}, config, require('./dev'))
80 | }
81 | return merge({}, config, require('./prod'))
82 | }
83 |
--------------------------------------------------------------------------------
/src/assets/styl/common/calculator.styl:
--------------------------------------------------------------------------------
1 | // 按钮的高度
2 | $calculatorHeight = 60
3 | // 背景色
4 | $itemBgColor = #dee2e6
5 | // 边框颜色
6 | $itemBorderColor = #ced4da
7 |
8 |
9 | .jz-calculator__main {
10 | position: fixed
11 | top: 0
12 | height: 100vh
13 | width: 100%
14 |
15 | .jz-calculator__mask {
16 | position: absolute
17 | height: 100vh
18 | width: 100%
19 | z-index: 99
20 | }
21 |
22 | .jz-calculator__bottom {
23 | position: absolute
24 | bottom: 0
25 | width: 100%
26 | z-index: 999
27 | font-size: 21PX
28 | animation: calculator-act 0.5s
29 | -webkit-animation: calculator-act 0.5s
30 | // animation-fill-mode: forwards;
31 | }
32 |
33 | .calculator-keys__collect {
34 | .calculator-keys__top {
35 | display: flex
36 | > view {
37 | width: 25%
38 | }
39 | }
40 |
41 | .calculator-keys__left {
42 | display: flex
43 | flex: 1
44 | flex-direction: column
45 | }
46 |
47 | .calculator-keys__right {
48 | width: 25%
49 | }
50 |
51 | .normal-item__key {
52 | height: "%sPX" % ($calculatorHeight)
53 | display: flex
54 | align-items: center
55 | justify-content: center
56 | flex: 1
57 | background: $itemBgColor
58 | border: 1px solid $itemBorderColor
59 | }
60 |
61 | .double-height__normal-item {
62 | height: "%sPX" % ($calculatorHeight * 2)
63 | display: flex
64 | align-items: center
65 | justify-content: center
66 | flex: 1
67 | background: $itemBgColor
68 | border: 1px solid $itemBorderColor
69 | }
70 |
71 | .double-width__normal-item {
72 | width: 66.66%
73 | display: flex
74 | align-items: center
75 | justify-content: center
76 | background: $itemBgColor
77 | border: 1px solid $itemBorderColor
78 | }
79 | }
80 |
81 | .jz-calculator__process {
82 | background: $itemBgColor
83 | padding: 12PX;
84 | text-align: right
85 | // border-top: 1px solid $itemBorderColor
86 | font-size: 18PX
87 | font-weight: bold
88 | }
89 |
90 | @-webkit-keyframes calculator-act
91 | {
92 | 0% {bottom: -200PX}
93 | 100% {bottom: 0}
94 | }
95 | }
--------------------------------------------------------------------------------
/src/pages/setting/chart/category_statement.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { View } from '@tarojs/components'
3 | import jz from '@/jz'
4 | import BasePage from '@/components/BasePage'
5 | import Statements from '@/components/Statements'
6 | import { AtTag } from 'taro-ui'
7 | import EmptyTips from '@/components/EmptyTips'
8 |
9 | const CategoryStatement: React.FC = () => {
10 | const params = jz.router.getParams()
11 | const [statements, setStatements] = useState([])
12 | const [orderBy, setOrderBy] = useState('created_at')
13 |
14 | const fetchStatements = async (params) => {
15 | const { data } = await jz.api.superStatements.getStatements({
16 | ...params,
17 | order_by: orderBy // Add order_by parameter
18 | })
19 | setStatements(data.data)
20 | }
21 |
22 | useEffect(() => {
23 | const date = params.date
24 | const categoryId = params.category_id
25 | if (date) {
26 | const [year, month] = date.split('-')
27 | fetchStatements({ year: year, month: month, category_id: categoryId })
28 | } else {
29 | fetchStatements({ category_id: categoryId })
30 | }
31 | }, [params, orderBy])
32 |
33 | return (
34 |
38 |
39 |
40 | setOrderBy('created_at')}
45 | customStyle={{
46 | marginRight: '8px'
47 | }}
48 | >
49 | 按日期排序
50 |
51 | setOrderBy('amount')}
56 | >
57 | 按金额排序
58 |
59 |
60 | { statements.length === 0 && }
61 |
62 |
63 |
64 | )
65 | }
66 |
67 | export default CategoryStatement
--------------------------------------------------------------------------------
/src/components/statementForm/PayeeSelect.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text } from '@tarojs/components'
2 | import jz from '@/jz'
3 | import { Loading } from '@/src/components/UiComponents'
4 |
5 | type PayeeSelectProps = {
6 | title?: string
7 | handleClick: (tag: any) => void
8 | data: any[]
9 | setActive: (active: boolean) => void
10 | loading?: boolean
11 | }
12 |
13 | export default function PayeeSelect({
14 | title = '选择商家',
15 | handleClick,
16 | data,
17 | setActive,
18 | loading = false
19 | }: PayeeSelectProps) {
20 | return (
21 |
22 | setActive(false)} />
23 |
24 |
25 | {title}
26 | {
29 | e.stopPropagation()
30 | jz.router.navigateTo({url: '/pages/payee/list'})
31 | }}
32 | >
33 | 管理商家
34 |
35 |
36 |
37 | {loading ? (
38 |
39 |
40 |
41 | ) : data.length === 0 ? (
42 |
43 | 还没有商家记录
44 | jz.router.navigateTo({url: '/pages/payee/list'})}
47 | >
48 | 去添加
49 |
50 |
51 | ) : (
52 | data.map((tag) => (
53 | handleClick(tag)}
57 | >
58 | {tag.name}
59 |
60 | ))
61 | )}
62 |
63 |
64 |
65 | )
66 | }
--------------------------------------------------------------------------------
/src/pages/setting/statements_flow/index.scss:
--------------------------------------------------------------------------------
1 | .jz-pages-assets-flow {
2 | min-height: 100vh;
3 | background: var(--page-bg-color);
4 |
5 | &__header {
6 | padding: 24PX;
7 | margin-bottom: 12PX;
8 | }
9 |
10 | &__list {
11 | padding: 0 24PX;
12 |
13 | .flow-item {
14 | background: #fff;
15 | border-radius: 12PX;
16 | margin-bottom: 12PX;
17 | overflow: hidden;
18 |
19 | &__header {
20 | display: flex;
21 | align-items: center;
22 | padding: 16PX;
23 | position: relative;
24 |
25 | &.active {
26 | background: var(--primary-color-light);
27 | }
28 |
29 | .time-info {
30 | min-width: 80PX;
31 | text-align: center;
32 |
33 | .month {
34 | font-size: 24PX;
35 | font-weight: 500;
36 | display: block;
37 | }
38 |
39 | .year {
40 | font-size: 12PX;
41 | color: var(--text-color-light);
42 | }
43 | }
44 |
45 | .amount-info {
46 | flex: 1;
47 | margin: 0 16PX;
48 |
49 | .income-expend {
50 | display: flex;
51 | flex-direction: column;
52 | gap: 4PX;
53 |
54 | .income {
55 | color: var(--income-color);
56 | font-size: 14PX;
57 | }
58 |
59 | .expend {
60 | color: var(--expend-color);
61 | font-size: 14PX;
62 | }
63 | }
64 |
65 | .surplus {
66 | margin-top: 4PX;
67 |
68 | .amount {
69 | font-size: 16PX;
70 | font-weight: 500;
71 | }
72 |
73 | .label {
74 | font-size: 12PX;
75 | color: var(--text-color-light);
76 | margin-left: 4PX;
77 | }
78 | }
79 | }
80 |
81 | .arrow {
82 | font-size: 20PX;
83 | color: var(--text-color-light);
84 | transition: transform 0.3s;
85 |
86 | &.jcon-arrow-down {
87 | transform: rotate(90deg);
88 | }
89 | }
90 | }
91 |
92 | &__content {
93 | border-top: 1PX solid var(--border-color);
94 | padding: 16PX;
95 | }
96 | }
97 | }
98 | }
--------------------------------------------------------------------------------
/src/pages/account_books/list.tsx:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro'
2 | import { View } from "@tarojs/components"
3 | import BasePage from '@/components/BasePage'
4 | import { Button } from '@/src/components/UiComponents'
5 | import { HomeStoreContext } from "@/src/stores"
6 | import { useEffect, useState, useContext } from "react"
7 | import jz from '@/jz'
8 | import { format } from 'date-fns'
9 |
10 | import './list.styl'
11 |
12 | const AccountBookList = () => {
13 | const [accountBooks, setAccountBooks] = useState([])
14 | const homeStore: HomeStoreContext = useContext(HomeStoreContext)
15 |
16 | const getAccountBooks = async () => {
17 | Taro.showLoading()
18 | const { data } = await jz.api.account_books.getAccountBooks()
19 | Taro.hideLoading()
20 | setAccountBooks(data)
21 | }
22 |
23 | useEffect(() => {
24 | getAccountBooks()
25 | }, [])
26 |
27 | return (
28 |
31 |
32 | {
33 | accountBooks.map((account_book) => {
34 | return (
35 | jz.router.navigateTo({ url: `/pages/account_books/edit?id=${account_book.id}` })}>
36 |
37 |
38 |
39 | { account_book.name }
40 | { account_book.account_type_name }
41 |
42 | { account_book.description }
43 |
44 | 创建者 { account_book.user?.nickname }
45 | { format(new Date(account_book.created_at), 'yyyy-MM-dd') }
46 |
47 |
48 |
49 |
50 | )
51 | })
52 | }
53 |
54 |
55 |
56 |
57 |
58 |
59 | )
60 | }
61 |
62 | export default AccountBookList
--------------------------------------------------------------------------------
/src/assets/styl/pages/profile/index.styl:
--------------------------------------------------------------------------------
1 | .jz-pages__profile {
2 | background: var(--background-color)
3 | min-height: 100vh
4 | padding: 24PX 0
5 | font-size: 14PX
6 |
7 | .user-info {
8 | background-color: #ffffff
9 | border-radius: 24PX
10 | margin: 0 24PX
11 | box-shadow: 0 4PX 12PX rgba(0,0,0,0.05)
12 |
13 | .username {
14 | .name {
15 | font-size: 18PX
16 | font-weight: 500
17 | color: var(--text-color)
18 | }
19 |
20 | .edit-text {
21 | font-size: 12PX
22 | color: var(--primary-color)
23 | }
24 | }
25 | }
26 |
27 | .setting-group {
28 | margin: 12PX
29 |
30 | .group-title {
31 | // font-size: 14PX
32 | color: var(--text-color-light)
33 | margin-bottom: 12PX
34 | padding-left: 8PX
35 | }
36 |
37 | .at-list {
38 | border-radius: 16PX
39 | overflow: hidden
40 | background: #ffffff
41 | box-shadow: 0 4PX 12PX rgba(0,0,0,0.05)
42 |
43 | .at-list__item {
44 | padding: 12PX
45 |
46 | &:not(:last-child) {
47 | border-bottom: 1PX solid #f5f5f5
48 | }
49 |
50 | .item-content {
51 | // font-size: 14PX
52 | }
53 |
54 | .item-extra {
55 | // font-size: 12PX
56 | color: var(--text-color-light)
57 | }
58 | }
59 | }
60 | }
61 |
62 | .feature {
63 | background-color: #ffffff
64 | border-top: 1px dashed #e8e8e8
65 | padding-bottom: 12PX
66 |
67 | .iconfont {
68 | color: var(--primary-color)
69 | margin-bottom: 8PX
70 | }
71 |
72 | .fs-14 {
73 | color: var(--text-color)
74 | }
75 | }
76 |
77 | image {
78 | border-radius: 50%
79 | width: 50PX
80 | height: 50PX
81 | }
82 |
83 | // 列表样式优化
84 | .at-list {
85 | margin: 12PX
86 | border-radius: 16PX
87 | overflow: hidden
88 | background: #ffffff
89 | box-shadow: 0 4PX 12PX rgba(0,0,0,0.05)
90 |
91 | .at-list__item {
92 | padding: 12PX
93 | margin: 0
94 | font-size: 16PX
95 |
96 | &:not(:last-child) {
97 | border-bottom: 1PX solid #f5f5f5
98 | }
99 |
100 | .at-list__item-content {
101 | color: var(--text-color)
102 | }
103 |
104 | .at-list__item-extra {
105 | color: var(--text-color-light)
106 | }
107 | }
108 | }
109 | }
--------------------------------------------------------------------------------
/src/components/Home/StatisticPage/index.tsx:
--------------------------------------------------------------------------------
1 | import { Component, useState, useMemo } from 'react'
2 | import { View, Picker } from '@tarojs/components'
3 | import { Tabs } from '@/src/components/UiComponents'
4 | import Summary from '@/components/Statistic/Summary'
5 | import ExpendList from '@/components/Statistic/ExpendList'
6 | import CalendarStatistic from '@/components/Statistic/CalendarStatistic'
7 | import { format } from 'date-fns'
8 |
9 | const tabs = [
10 | { id: 1, title: '日历总览' },
11 | { id: 2, title: '收支总览' },
12 | // { id: 3, title: '消费趋势' },
13 | { id: 4, title: '消费排行' }
14 | ]
15 |
16 | export const StatisticPage: React.FC = () => {
17 | const [currentDate, setCurrentDate] = useState(new Date())
18 | const [currentTab, setCurrentTab] = useState(1)
19 |
20 | const handleMonthChange = (e) => {
21 | const [year, month] = e.detail.value.split('-')
22 | setCurrentDate(new Date(Number(year), Number(month) - 1))
23 | }
24 |
25 | const handlePrevMonth = () => {
26 | setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1))
27 | }
28 |
29 | const handleNextMonth = () => {
30 | setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1))
31 | }
32 |
33 | const currentComponent = useMemo(() => {
34 | switch(currentTab) {
35 | case 1:
36 | return
37 | case 2:
38 | return
39 | case 4:
40 | return
41 | default:
42 | return null
43 | }
44 | }, [currentTab, currentDate])
45 |
46 | return (
47 |
48 |
49 | ◀
50 |
56 |
57 | {format(currentDate, 'yyyy年MM月')}
58 |
59 |
60 | ▶
61 |
62 |
63 | {
66 | setCurrentTab(tabId)
67 | }}
68 | />
69 |
70 |
71 | {currentComponent}
72 |
73 |
74 | )
75 | }
--------------------------------------------------------------------------------
/src/api/index.ts:
--------------------------------------------------------------------------------
1 | import Request from './request'
2 | import Statement from './logic/statement'
3 | import Main from './logic/main'
4 | import User from './logic/user'
5 | import Category from './logic/category'
6 | import Asset from './logic/asset'
7 | import AccountBook from './logic/account_book'
8 | import Finance from './logic/finance'
9 | import SuperStatement from './logic/superStatement'
10 | import SuperChart from './logic/superChart'
11 | import Budget from './logic/budget'
12 | import Chaos from './logic/chaos'
13 | import Statistic from './logic/statistic'
14 | import Message from './logic/message'
15 | import Payee from './logic/payee'
16 | import Friend from './logic/friend'
17 |
18 | export class Api extends Request {
19 | private createLazyService(key: string, Constructor: new (api: Api) => T): T {
20 | if (!this[key]) {
21 | this[key] = new Constructor(this)
22 | }
23 | return this[key]
24 | }
25 |
26 | get main(): Main {
27 | return this.createLazyService('_main', Main)
28 | }
29 |
30 | get statements(): Statement {
31 | return this.createLazyService('_statement', Statement)
32 | }
33 |
34 | get users(): User {
35 | return this.createLazyService('_user', User)
36 | }
37 |
38 | get categories(): Category {
39 | return this.createLazyService('_category', Category)
40 | }
41 |
42 | get assets(): Asset {
43 | return this.createLazyService('_asset', Asset)
44 | }
45 |
46 | get account_books(): AccountBook {
47 | return this.createLazyService('_account_book', AccountBook)
48 | }
49 |
50 | get finances(): Finance {
51 | return this.createLazyService('_finance', Finance)
52 | }
53 |
54 | get superStatements(): SuperStatement {
55 | return this.createLazyService('_super_statement', SuperStatement)
56 | }
57 |
58 | get superCharts(): SuperChart {
59 | return this.createLazyService('_super_chart', SuperChart)
60 | }
61 |
62 | get budgets(): Budget {
63 | return this.createLazyService('_budget', Budget)
64 | }
65 |
66 | get chaos(): Chaos {
67 | return this.createLazyService('_chaos', Chaos)
68 | }
69 |
70 | get statistics(): Statistic {
71 | return this.createLazyService('_statistic', Statistic)
72 | }
73 |
74 | get messages(): Message {
75 | return this.createLazyService('_message', Message)
76 | }
77 |
78 | get payees(): Payee {
79 | return this.createLazyService('_payee', Payee)
80 | }
81 |
82 | get friends(): Friend {
83 | return this.createLazyService('_friend', Friend)
84 | }
85 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jz-taro",
3 | "version": "4.0.0",
4 | "private": true,
5 | "description": "jiezhang",
6 | "templateInfo": {
7 | "name": "taro-ui",
8 | "typescript": true,
9 | "css": "stylus"
10 | },
11 | "scripts": {
12 | "build:weapp": "set NODE_ENV=production && taro build --type weapp",
13 | "build:swan": "taro build --type swan",
14 | "build:alipay": "taro build --type alipay",
15 | "build:tt": "taro build --type tt",
16 | "build:h5": "taro build --type h5",
17 | "build:rn": "taro build --type rn",
18 | "build:qq": "taro build --type qq",
19 | "build:jd": "taro build --type jd",
20 | "build:quickapp": "taro build --type quickapp",
21 | "pro:weapp": "set NODE_ENV=production && taro build --type weapp --watch",
22 | "dev:weapp": "taro build --type weapp --watch",
23 | "dev:swan": "npm run build:swan -- --watch",
24 | "dev:alipay": "npm run build:alipay -- --watch",
25 | "dev:tt": "npm run build:tt -- --watch",
26 | "dev:h5": "npm run build:h5 -- --watch",
27 | "dev:rn": "npm run build:rn -- --watch",
28 | "dev:qq": "npm run build:qq -- --watch",
29 | "dev:jd": "npm run build:jd -- --watch",
30 | "dev:quickapp": "npm run build:quickapp -- --watch"
31 | },
32 | "browserslist": [
33 | "last 3 versions",
34 | "Android >= 4.1",
35 | "ios >= 8"
36 | ],
37 | "author": "",
38 | "dependencies": {
39 | "@babel/runtime": "^7.7.7",
40 | "@tarojs/cli": "3.6.32",
41 | "@tarojs/components": "3.6.32",
42 | "@tarojs/plugin-framework-react": "3.6.32",
43 | "@tarojs/plugin-platform-weapp": "^3.6.32",
44 | "@tarojs/react": "3.6.32",
45 | "@tarojs/runtime": "3.6.32",
46 | "@tarojs/taro": "3.6.32",
47 | "date-fns": "^2.21.3",
48 | "lodash": "4.17.15",
49 | "mobx": "5.15.7",
50 | "mobx-react": "6.3.1",
51 | "react": "^16.8.0",
52 | "react-dom": "^16.8.0",
53 | "taro-react-echarts": "^1.2.2",
54 | "taro-ui": "^3.3.0"
55 | },
56 | "devDependencies": {
57 | "@babel/core": "^7.8.0",
58 | "@tarojs/mini-runner": "3.6.32",
59 | "@tarojs/webpack-runner": "3.6.32",
60 | "@types/react": "^17.0.2",
61 | "@types/webpack-env": "^1.13.6",
62 | "@typescript-eslint/eslint-plugin": "^4.15.1",
63 | "@typescript-eslint/parser": "^4.15.1",
64 | "babel-preset-taro": "3.2.0",
65 | "eslint": "^6.8.0",
66 | "eslint-config-taro": "3.2.0",
67 | "eslint-plugin-import": "^2.12.0",
68 | "eslint-plugin-react": "^7.8.2",
69 | "eslint-plugin-react-hooks": "^4.2.0",
70 | "node-sass": "^9.0.0",
71 | "stylelint": "9.3.0",
72 | "typescript": "^4.1.0"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/pages/setting/user_info/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { View, Image, Button, Input, Text } from '@tarojs/components'
3 | import jz from '@/jz'
4 | import BasePage from '@/components/BasePage'
5 | import { Button as JBTN } from '@/src/components/UiComponents'
6 | import Taro from "@tarojs/taro"
7 |
8 | const UserInfoPage: React.FC = () => {
9 | const [userInfo, setUserInfo] = useState({
10 | avatar_path: '',
11 | nickname: ''
12 | })
13 | const [changeAvatar, setChangeAvatar] = useState(false)
14 |
15 | const getUserInfo = async () => {
16 | Taro.showLoading({
17 | title: 'loading',
18 | })
19 | const { data } = await jz.api.users.getUserInfo()
20 | Taro.hideLoading()
21 | setUserInfo(data.data)
22 | }
23 |
24 | useEffect(() => {
25 | getUserInfo()
26 | }, [])
27 |
28 | const onSubmit = async () => {
29 | if (userInfo.nickname === '' || !userInfo.nickname) {
30 | jz.toastError('昵称不能为空哦~')
31 | return
32 | }
33 | await jz.api.users.updateUserInfo({nickname: userInfo.nickname})
34 | if (changeAvatar) {
35 | await jz.api.upload(userInfo.avatar_path, {
36 | type: 'user_avatar'
37 | })
38 | }
39 | jz.router.navigateBack()
40 | }
41 |
42 | const onChooseAvatar = (e) => {
43 | const { avatarUrl } = e.detail
44 | if (avatarUrl) {
45 | const info = Object.assign({...userInfo, avatar_path: avatarUrl})
46 | setChangeAvatar(true)
47 | setUserInfo(info)
48 | }
49 | }
50 |
51 | return (
52 |
55 |
56 |
57 |
60 |
61 |
62 |
63 |
64 | 昵称
65 | { setUserInfo(Object.assign({...userInfo, nickname: e.detail.value})) }}
72 | onInput={(e) => { setUserInfo(Object.assign({...userInfo, nickname: e.detail.value})) }}
73 | />
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | )
84 | }
85 |
86 | export default UserInfoPage
--------------------------------------------------------------------------------
/src/assets/styl/pages/statistic/calendar.styl:
--------------------------------------------------------------------------------
1 | .month-selector {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | padding: 20px 0;
6 |
7 | .month-arrow {
8 | padding: 0 20px;
9 | color: #666;
10 | font-size: 24px;
11 | cursor: pointer;
12 |
13 | &:active {
14 | opacity: 0.7;
15 | }
16 | }
17 |
18 | .month-text {
19 | font-size: 32px;
20 | padding: 0 20px;
21 | }
22 | }
23 |
24 | .calendar-statistic {
25 | .calendar-grid {
26 | display: grid;
27 | grid-template-columns: repeat(7, 1fr);
28 | gap: 2px;
29 | background-color: #f5f5f5;
30 |
31 | .week-day {
32 | padding: 10px;
33 | text-align: center;
34 | background-color: #fff;
35 | font-size: 24px;
36 | }
37 |
38 | .day-cell {
39 | aspect-ratio: 1;
40 | background-color: #fff;
41 | display: flex;
42 | flex-direction: column;
43 | width: 0;
44 | height: 100%;
45 | min-width: 100%;
46 | box-sizing: border-box;
47 |
48 | &.today {
49 | background-color: #f0f9eb;
50 | .date-number {
51 | color: #67c23a;
52 | font-weight: bold;
53 | }
54 | }
55 |
56 | &.selected {
57 | background-color: #ecf5ff;
58 | .date-number {
59 | color: #409eff;
60 | font-weight: bold;
61 | }
62 | }
63 |
64 | .date-number {
65 | font-size: 28px;
66 | margin-bottom: 10px;
67 | }
68 |
69 | .amount-bars {
70 | flex: 1;
71 | display: flex;
72 | flex-direction: column;
73 | justify-content: flex-end;
74 | .amount-text {
75 | color: white
76 | }
77 | .income-bar {
78 | background-color: #f56c6c
79 | text-align: center
80 | }
81 |
82 | .expend-bar {
83 | background-color: #67c23a
84 | text-align: center
85 | }
86 | }
87 | }
88 | }
89 | }
90 |
91 | .day-summary {
92 | background: #fff;
93 | margin: 20px 0;
94 | padding: 20px;
95 | border-radius: 8px;
96 |
97 | .summary-header {
98 | font-size: 28px;
99 | font-weight: bold;
100 | margin-bottom: 20px;
101 | }
102 |
103 | .summary-content {
104 | display: flex;
105 | justify-content: space-around;
106 | }
107 |
108 | .summary-item {
109 | text-align: center;
110 |
111 | .label {
112 | color: #999;
113 | font-size: 24px;
114 | margin-bottom: 10px;
115 | }
116 |
117 | .amount {
118 | font-size: 32px;
119 | font-weight: bold;
120 | }
121 | }
122 | }
--------------------------------------------------------------------------------
/src/pages/setting/statements_manage/data_out.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { View } from '@tarojs/components'
3 | import { AtButton, AtList, AtListItem } from 'taro-ui'
4 | import jz from '@/jz'
5 | import BasePage from '@/components/BasePage'
6 | import Taro from '@tarojs/taro'
7 |
8 | type ExportRange = '1month' | '3months' | 'all'
9 |
10 | export default function ExportIndex(): JSX.Element {
11 | const [selectedRange, setSelectedRange] = useState('1month')
12 | const rangeOptions = [
13 | { value: '1month', label: '近1个月' },
14 | { value: '3months', label: '近3个月' },
15 | { value: 'all', label: '全部' }
16 | ]
17 |
18 | const handleExport = async () => {
19 | try {
20 | // 先检查是否超过导出限制
21 | const { data } = await jz.api.statements.pre_check_export(selectedRange)
22 | if (data.status !== 200) {
23 | jz.toastError(data.msg || '无法导出,联系管理员')
24 | return
25 | }
26 |
27 | const res = await jz.withLoading(jz.api.statements.export_excel(selectedRange))
28 | if (res.statusCode === 200) {
29 | const filePath = res.tempFilePath
30 | try {
31 | // 保存文件到本地
32 | const saveRes = await Taro.saveFile({
33 | tempFilePath: filePath
34 | })
35 |
36 | // 打开文件
37 | await Taro.openDocument({
38 | filePath: saveRes.savedFilePath,
39 | fileType: 'xlsx',
40 | showMenu: true
41 | })
42 |
43 | Taro.showToast({
44 | title: '文件已保存到本地',
45 | icon: 'success'
46 | })
47 | } catch (error) {
48 | jz.toastError('文件保存失败')
49 | }
50 | }
51 | } catch (error) {
52 | jz.toastError('导出失败')
53 | }
54 | }
55 |
56 | return (
57 |
58 |
59 | 选择导出范围:
60 |
61 | {rangeOptions.map(option => (
62 | setSelectedRange(option.value as ExportRange)}
67 | />
68 | ))}
69 |
70 |
71 | **每天仅能导出 5 次!
72 | **导出后自动打开文档,请自行保存。
73 | **最多仅支持导出 3000 条数据
74 |
75 |
76 |
80 | 导出账单
81 |
82 |
83 |
84 |
85 | )
86 | }
--------------------------------------------------------------------------------
/src/pages/share/public.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { View } from '@tarojs/components'
3 | import { AtTag } from 'taro-ui'
4 | import Statements from '@/components/Statements'
5 | import BasePage from '@/components/BasePage'
6 | import jz from '@/jz'
7 |
8 | const PublicPage: React.FC = () => {
9 | const [orderBy, setOrderBy] = useState('created_at')
10 | const [dateRange, setDateRange] = useState<{ start_date: string; end_date: string }>({ start_date: '', end_date: '' })
11 | const [user, setUser] = useState<{ nickname: string, avatar_path: string }>({nickname: '', avatar_path: ''})
12 | const [statements, setStatements] = useState>([])
13 |
14 | useEffect(() => {
15 | const fetchStatements = async () => {
16 | const params = jz.router.getParams()
17 | const { data } = await jz.withLoading(
18 | jz.api.statements.getListByToken(params.token, orderBy)
19 | )
20 | if (data.status === 200) {
21 | setStatements(data.data.data)
22 | setDateRange(data.data.date_range)
23 | setUser(data.data.shared_user)
24 | } else {
25 | jz.toastError('分享链接已失效,请联系分享者重新分享', 3000)
26 | }
27 | }
28 | fetchStatements()
29 | }, [orderBy])
30 |
31 | return (
32 |
35 |
36 |
37 | 【{user.nickname}】分享的账单列表
38 | 日期范围:{dateRange.start_date} ~ {dateRange.end_date}
39 |
40 |
41 |
42 | setOrderBy('created_at')}
47 | customStyle={{
48 | marginRight: '8px'
49 | }}
50 | >
51 | 按日期排序
52 |
53 | setOrderBy('amount')}
58 | >
59 | 按金额排序
60 |
61 |
62 |
63 |
64 |
65 | 共计 {statements.length} 笔账单,
66 | 支出 {statements.filter(s => s.type === 'expend').reduce((acc, cur) => acc + parseFloat(cur.amount), 0).toFixed(2)} 元,
67 | 收入 {statements.filter(s => s.type === 'income').reduce((acc, cur) => acc + parseFloat(cur.amount), 0).toFixed(2)} 元
68 |
69 |
70 |
71 |
72 |
73 | )
74 | }
75 |
76 | export default PublicPage
--------------------------------------------------------------------------------
/src/components/statementForm/CategorySelect.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View, Image, Text } from '@tarojs/components'
3 | import { Loading } from '@/src/components/UiComponents'
4 | import Avatar from '@/components/Avatar'
5 |
6 | export default function CategorySelect({
7 | title,
8 | data,
9 | frequent,
10 | handleClick,
11 | setActive,
12 | loading
13 | }) {
14 | return (
15 |
16 | setActive(false)} />
17 |
18 |
19 |
20 | { title }
21 |
22 |
23 |
24 | {loading ? (
25 |
26 |
27 |
28 | ) : (
29 | <>
30 | {frequent && frequent.length > 0 && (
31 |
36 | )}
37 |
38 | {data.map((item) => (
39 |
46 | ))}
47 | >
48 | )}
49 |
50 |
51 |
52 | )
53 | }
54 |
55 | function CategoryContent({ title, data, handleClick, parent = null }) {
56 | return (
57 |
58 |
59 | {title}
60 | ({data.length})
61 |
62 |
63 |
64 | {data.map((item) => (
65 | handleClick(e, parent || item?.parent, item)}
69 | >
70 |
71 | {item.icon_path ? (
72 |
73 | ) : (
74 |
78 | )}
79 |
80 | {item.name}
81 |
82 | ))}
83 |
84 |
85 | )
86 | }
87 |
--------------------------------------------------------------------------------
/src/assets/styl/themes/index.styl:
--------------------------------------------------------------------------------
1 |
2 | $default = {
3 | primaryColor: #2196F3
4 | primaryBgColor: linear-gradient(135deg, #2196F3, #64B5F6)
5 | primaryBgTextColor: #ffffff
6 | textColor: #333333
7 | secondaryText: #666666
8 | borderColor: #f0f0f0
9 | backgroundColor: #f8fafc
10 | }
11 |
12 | $green = {
13 | primaryColor: #4CAF50
14 | primaryBgColor: linear-gradient(135deg, #4CAF50, #81C784)
15 | primaryBgTextColor: #ffffff
16 | textColor: #333333
17 | secondaryText: #666666
18 | borderColor: #f0f0f0
19 | backgroundColor: #f6fbf6
20 | }
21 |
22 | $orange = {
23 | primaryColor: #FF9800
24 | primaryBgColor: linear-gradient(135deg, #FF9800, #FFB74D)
25 | primaryBgTextColor: #ffffff
26 | textColor: #333333
27 | secondaryText: #666666
28 | borderColor: #f0f0f0
29 | backgroundColor: #fff9f0
30 | }
31 |
32 | $yellow = {
33 | primaryColor: #FDD835
34 | primaryBgColor: linear-gradient(135deg, #FDD835, #FFF176)
35 | primaryBgTextColor: #333333
36 | textColor: #333333
37 | secondaryText: #666666
38 | borderColor: #f0f0f0
39 | backgroundColor: #fffdf5
40 | }
41 |
42 | $purple = {
43 | primaryColor: #673AB7
44 | primaryBgColor: linear-gradient(135deg, #673AB7, #9575CD)
45 | primaryBgTextColor: #ffffff
46 | textColor: #333333
47 | secondaryText: #666666
48 | borderColor: #f0f0f0
49 | backgroundColor: #f8f5ff
50 | }
51 |
52 | $pink = {
53 | primaryColor: #E91E63
54 | primaryBgColor: linear-gradient(135deg, #E91E63, #F06292)
55 | primaryBgTextColor: #ffffff
56 | textColor: #333333
57 | secondaryText: #666666
58 | borderColor: #f0f0f0
59 | backgroundColor: #fff5f8
60 | }
61 |
62 | $black = {
63 | primaryColor: #3c4b82
64 | primaryBgColor: linear-gradient(135deg, #3c4b82, #5c6ca8)
65 | primaryBgTextColor: #ffffff
66 | textColor: #333333
67 | secondaryText: #666666
68 | borderColor: #f0f0f0
69 | backgroundColor: #f5f6fa
70 | }
71 |
72 | $themes = {
73 | default: $default,
74 | green: $green,
75 | pink: $pink,
76 | black: $black,
77 | orange: $orange,
78 | yellow: $yellow,
79 | purple: $purple
80 | }
81 |
82 | for themeKey, _ in $themes {
83 | themeData = "jz-theme-" + themeKey
84 | .page-root[data-theme-name={themeData}] {
85 | --primary-color: $themes[themeKey]['primaryColor']
86 | --primary-bg-color: $themes[themeKey]['primaryBgColor']
87 | --primary-bg-text-color: $themes[themeKey]['primaryBgTextColor']
88 | --text-color: $themes[themeKey]['textColor']
89 | --secondary-text: $themes[themeKey]['secondaryText']
90 | --border-color: $themes[themeKey]['borderColor']
91 | --background-color: $themes[themeKey]['backgroundColor']
92 | }
93 | }
94 |
95 | .page-root {
96 | display: flex
97 | flex-direction: column
98 | min-height: 100vh
99 |
100 | @import "./root"
101 | @import "./components/button"
102 | @import "./components/tab"
103 | @import "./components/loading"
104 | @import "./components/form"
105 | }
106 |
--------------------------------------------------------------------------------
/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro'
2 |
3 | interface RouterOptions {
4 | url: string
5 | name?: string
6 | params?: Record
7 | }
8 |
9 | export default class Router {
10 | private _currentInstance: any
11 | private _beforeHooks: Array
12 |
13 | constructor() {
14 | this._beforeHooks = []
15 |
16 | // 监听页面显示事件,同步页面栈
17 | Taro.eventCenter.on('PAGE_SHOW', () => {
18 | this._currentInstance = null
19 | })
20 | }
21 |
22 | getParams() {
23 | return this.getCurrentInstance().router.params || {}
24 | }
25 |
26 | getCurrentInstance() {
27 | if (!this._currentInstance) {
28 | this._currentInstance = Taro.getCurrentInstance()
29 | }
30 | return this._currentInstance
31 | }
32 |
33 | // 获取当前页面栈
34 | getCurrentPages() {
35 | return Taro.getCurrentPages()
36 | }
37 |
38 | // 获取当前页面路径
39 | getCurrentPage() {
40 | const pages = this.getCurrentPages()
41 | const currentPage = pages[pages.length - 1]
42 | return currentPage ? `/${currentPage.route}` : ''
43 | }
44 |
45 | // 构建带参数的URL
46 | private buildUrl({ url, params }: RouterOptions): string {
47 | if (!params) return url
48 | const query = Object.entries(params)
49 | .map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`)
50 | .join('&')
51 | return `${url}${url.includes('?') ? '&' : '?'}${query}`
52 | }
53 |
54 | // 添加路由守卫
55 | beforeEach(callback: (to: string) => boolean | Promise) {
56 | this._beforeHooks.push(callback)
57 | }
58 |
59 | // 执行路由守卫
60 | private async runBeforeHooks(to: string): Promise {
61 | for (const hook of this._beforeHooks) {
62 | const result = await hook(to)
63 | if (!result) return false
64 | }
65 | return true
66 | }
67 |
68 | async navigateTo(options: RouterOptions) {
69 | try {
70 | const url = this.buildUrl(options)
71 | if (!(await this.runBeforeHooks(url))) {
72 | return false
73 | }
74 |
75 | await Taro.navigateTo({ url })
76 | return true
77 | } catch (error) {
78 | console.error('页面跳转失败:', error)
79 | return false
80 | }
81 | }
82 |
83 | async redirectTo(options: RouterOptions) {
84 | try {
85 | const url = this.buildUrl(options)
86 | if (!(await this.runBeforeHooks(url))) {
87 | return false
88 | }
89 |
90 | await Taro.redirectTo({ url })
91 | return true
92 | } catch (error) {
93 | console.error('页面重定向失败:', error)
94 | return false
95 | }
96 | }
97 |
98 | navigateBack(delta = 1) {
99 | const pages = this.getCurrentPages()
100 | if (pages.length > 1) {
101 | Taro.navigateBack({ delta })
102 | return true
103 | } else {
104 | console.warn('已经是第一个页面')
105 | return false
106 | }
107 | }
108 |
109 | // 检查是否可以返回
110 | canNavigateBack(): boolean {
111 | return this.getCurrentPages().length > 1
112 | }
113 | }
--------------------------------------------------------------------------------
/src/components/SlideSetting/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { View, Image } from '@tarojs/components'
3 | import jz from '@/jz'
4 | import { Input } from '@/src/components/UiComponents'
5 |
6 | const SlideSetting = ({
7 | open,
8 | category,
9 | onSubmit,
10 | setEditOpen
11 | }) => {
12 | const [icons, setIcons] = useState([])
13 | const [selectedIcon, setSelectedIcon] = useState(null)
14 | const [showIcon, setShowIcon] = useState(false)
15 | const [name, setName] = useState('')
16 |
17 | const getIcons = async () => {
18 | setShowIcon(!showIcon)
19 | if (icons.length === 0) {
20 | const res = await jz.api.categories.getCategoryIcon()
21 | setIcons(res.data)
22 | }
23 | }
24 |
25 | useEffect(() => {
26 | if (category) {
27 | setSelectedIcon({id: category?.icon_path})
28 | setName(category?.name)
29 | }
30 | }, [category])
31 |
32 | if (!open) {
33 | return
34 | }
35 |
36 | return (
37 |
38 | { }}>
39 |
40 |
41 | { category?.name !== '' ? '编辑分类' : '创建分类'}
42 |
43 |
44 |
45 |
51 |
52 |
53 | getIcons()}>
54 | 分类图标
55 |
56 | { selectedIcon?.id && }
57 |
58 |
59 |
60 |
61 | {
62 | showIcon &&
63 | {
64 | icons.map((icon) => {
65 | return (
66 | setSelectedIcon(icon)}>
67 |
68 |
69 | )
70 | })
71 | }
72 |
73 | }
74 |
75 |
76 |
77 | { setEditOpen(false) }}>取消
78 | { onSubmit({name: name, icon_path: selectedIcon.id})} }>确认
79 |
80 |
81 |
82 | )
83 | }
84 | export default SlideSetting
--------------------------------------------------------------------------------
/src/assets/styl/components/statement.styl:
--------------------------------------------------------------------------------
1 |
2 | .statement-component__icon-image {
3 | width: 60px
4 | height: 60px
5 | image {
6 | width: 100%
7 | height: 100%
8 | }
9 | }
10 | .statement-component__item {
11 | border-radius: 18PX
12 | padding: 0 12PX
13 | margin-bottom: 12PX
14 | border-left: 3px solid transparent
15 |
16 | .target-object {
17 | font-size: 16px
18 | font-weight: 600
19 | color: #ffffff
20 | padding: 2px 8px
21 | margin-right: 4px
22 | border-radius: 10px
23 |
24 | // 报销 - 使用橙色
25 | &.reimburse {
26 | background: #f39c12
27 | }
28 |
29 | // 代付 - 使用青色
30 | &.payment_proxy {
31 | background: #1abc9c
32 | }
33 |
34 | // 借入 - 使用浅蓝色
35 | &.loan_in {
36 | background: #3498db
37 | }
38 |
39 | // 借出 - 使用深蓝色
40 | &.loan_out {
41 | background: #2980b9
42 | }
43 | }
44 |
45 | // 支出 - 使用温和的红色
46 | &.expend {
47 | background: linear-gradient(to right, rgba(46, 204, 113, 0.05), #fbfbfb)
48 | border-left-color: #2ecc71
49 | }
50 |
51 | // 收入 - 使用清新的绿色
52 | &.income {
53 | background: linear-gradient(to right, rgba(231, 76, 60, 0.05), #fbfbfb)
54 | border-left-color: #e74c3c
55 | }
56 |
57 | // 还款 - 使用紫色
58 | &.repayment {
59 | background: linear-gradient(to right, rgba(155, 89, 182, 0.05), #fbfbfb)
60 | border-left-color: #9b59b6
61 | }
62 |
63 | // 转账 - 使用灰色
64 | &.transfer {
65 | background: linear-gradient(to right, rgba(149, 165, 166, 0.05), #fbfbfb)
66 | border-left-color: #95a5a6
67 | }
68 |
69 | // 报销 - 使用橙色
70 | &.reimburse {
71 | background: linear-gradient(to right, rgba(243, 156, 18, 0.05), #fbfbfb)
72 | border-left-color: #f39c12
73 | }
74 |
75 | // 代付 - 使用青色
76 | &.payment_proxy {
77 | background: linear-gradient(to right, rgba(26, 188, 156, 0.05), #fbfbfb)
78 | border-left-color: #1abc9c
79 | }
80 |
81 | // 借入 - 使用浅蓝色
82 | &.loan_in {
83 | background: linear-gradient(to right, rgba(52, 152, 219, 0.05), #fbfbfb)
84 | border-left-color: #3498db
85 | }
86 |
87 | // 借出 - 使用深蓝色
88 | &.loan_out {
89 | background: linear-gradient(to right, rgba(41, 128, 185, 0.05), #fbfbfb)
90 | border-left-color: #2980b9
91 | }
92 |
93 | .mood-tag {
94 | border-radius: 10px
95 | padding: 0 8px
96 | line-height: 1.5
97 | color: #ffffff
98 | }
99 |
100 | .flex-1 {
101 | min-width: 0 // 让 flex-1 可以正常收缩
102 | margin-right: 16PX // 与右侧金额保持间距
103 | }
104 |
105 | .description-wrapper {
106 | display: flex
107 | flex-wrap: wrap
108 | align-items: center
109 |
110 | .mood-tag {
111 | flex-shrink: 0 // 防止心情标签被压缩
112 | margin-right: 8PX
113 | }
114 |
115 | .col-text-mute {
116 | word-break: break-all
117 | white-space: normal
118 | line-height: 1.4
119 | }
120 | }
121 |
122 | // 右侧金额部分
123 | .flex-center-center {
124 | flex-shrink: 0 // 防止金额部分被压缩
125 | min-width: 80PX // 保证金额显示的最小宽度
126 | text-align: right
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/assets/styl/themes/components/tab.styl:
--------------------------------------------------------------------------------
1 | .jz-common-components__tab {
2 | background: #fff
3 | white-space: nowrap
4 | border-bottom: 1px solid #f4f4f4
5 | padding: 8PX 0
6 | position: relative
7 |
8 | // 添加右侧渐变遮罩
9 | &::after {
10 | content: ''
11 | position: absolute
12 | top: 0
13 | right: 0
14 | width: 40PX
15 | height: 100%
16 | background: linear-gradient(to right, rgba(255,255,255,0), rgba(255,255,255,1))
17 | pointer-events: none
18 | }
19 |
20 | // 添加滑动提示箭头
21 | &::before {
22 | content: ''
23 | position: absolute
24 | top: 50%
25 | right: 12PX
26 | width: 8PX
27 | height: 8PX
28 | border-right: 2PX solid #999
29 | border-bottom: 2PX solid #999
30 | transform: translateY(-50%) rotate(-45deg)
31 | animation: scroll-hint 1.5s infinite
32 | z-index: 1
33 | pointer-events: none
34 | }
35 |
36 | @keyframes scroll-hint {
37 | 0% {
38 | opacity: 0
39 | transform: translateY(-50%) translateX(-4PX) rotate(-45deg)
40 | }
41 | 50% {
42 | opacity: 1
43 | transform: translateY(-50%) translateX(0) rotate(-45deg)
44 | }
45 | 100% {
46 | opacity: 0
47 | transform: translateY(-50%) translateX(4PX) rotate(-45deg)
48 | }
49 | }
50 | .item {
51 | display: inline-block
52 | min-width: 100PX
53 | text-align: center
54 | position: relative
55 | font-size: 15PX
56 | transition: all 0.3s
57 |
58 | &.active {
59 | font-weight: 600
60 | transform: scale(1.05)
61 |
62 | &.expend {
63 | color: #2ecc71
64 | }
65 |
66 | &.income {
67 | color: #e74c3c
68 | }
69 |
70 | &.transfer {
71 | color: #95a5a6
72 | }
73 |
74 | &.loan_in {
75 | color: #3498db
76 | }
77 |
78 | &.loan_out {
79 | color: #2980b9
80 | }
81 |
82 | &.repayment {
83 | color: #9b59b6
84 | }
85 |
86 | &.reimburse {
87 | color: #f39c12
88 | }
89 |
90 | &.payment_proxy {
91 | color: #1abc9c
92 | }
93 |
94 | &::after {
95 | content: ''
96 | position: absolute
97 | bottom: -8PX
98 | left: 50%
99 | transform: translateX(-50%)
100 | width: 24PX
101 | height: 3PX
102 | border-radius: 2PX
103 | }
104 |
105 | &.expend::after {
106 | background: #2ecc71
107 | }
108 |
109 | &.income::after {
110 | background: #e74c3c
111 | }
112 |
113 | &.transfer::after {
114 | background: #95a5a6
115 | }
116 |
117 | &.loan_in::after {
118 | background: #3498db
119 | }
120 |
121 | &.loan_out::after {
122 | background: #2980b9
123 | }
124 |
125 | &.repayment::after {
126 | background: #9b59b6
127 | }
128 |
129 | &.reimburse::after {
130 | background: #f39c12
131 | }
132 |
133 | &.payment_proxy::after {
134 | background: #1abc9c
135 | }
136 | }
137 | }
138 | }
--------------------------------------------------------------------------------
/src/components/UiComponents/Form/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { View, Input as VInput, Textarea as VTextarea } from '@tarojs/components'
3 | import Select from '@/components/Select'
4 |
5 | export const Input = function ({
6 | data,
7 | setData,
8 | title='名称',
9 | placeholder='输入描述',
10 | showTitle=true,
11 | }) {
12 |
13 | return (
14 |
15 | { showTitle && { title } }
16 |
17 | setData(detail.value)}
23 | >
24 |
25 |
26 |
27 | )
28 | }
29 |
30 | export const Textarea = function ({
31 | data,
32 | setData,
33 | title='名称',
34 | placeholder='输入描述',
35 | showTitle=true,
36 | }) {
37 |
38 | return (
39 |
40 | { showTitle && { title } }
41 |
42 | setData(detail.value)}
47 | >
48 |
49 |
50 |
51 | )
52 | }
53 |
54 |
55 | export const SelectInput = function ({
56 | list,
57 | keyName,
58 | setSelected,
59 | selected=null,
60 | title='名称',
61 | showTitle=true,
62 | }) {
63 | const [selectOpen, setSelectOpen] = useState(false)
64 |
65 | const handleToggle = () => {
66 | setSelectOpen(!selectOpen)
67 | }
68 |
69 | const onSelectedItem = (data) => {
70 | setSelected(data)
71 | }
72 |
73 | const onSubmit = () => {
74 | // 初始化无选择 Select 时,确定默认用第一项
75 | if (!selected) {
76 | setSelected(list[0])
77 | }
78 | handleToggle()
79 | }
80 |
81 | return (
82 |
83 | { showTitle && { title } }
84 | handleToggle()}>
85 | { selected ? selected[keyName] : '未选择' }
86 |
87 |
88 |
89 |
114 |
115 | )
116 | }
--------------------------------------------------------------------------------
/src/storage/index.ts:
--------------------------------------------------------------------------------
1 | import Taro from "@tarojs/taro"
2 |
3 | export default class Storage {
4 | private _version = 'v1'
5 |
6 | saveLocal(key, value, expireDays = 7) {
7 | const data = {
8 | value,
9 | timestamp: Date.now(),
10 | expireDays
11 | }
12 | Taro.setStorage({
13 | key: key + '_' + this._version,
14 | data
15 | })
16 | }
17 |
18 | getLocal(key) {
19 | const data = Taro.getStorageSync(key + '_' + this._version)
20 | if (!data) return null
21 |
22 | const { value, timestamp, expireDays } = data
23 | const now = Date.now()
24 | const days = (now - timestamp) / (1000 * 60 * 60 * 24)
25 |
26 | if (days > expireDays) {
27 | this.delLocal(key)
28 | return null
29 | }
30 |
31 | return value
32 | }
33 |
34 | delLocal(key) {
35 | Taro.removeStorageSync(key + '_' + this._version)
36 | }
37 |
38 | setCurrentUser(user) {
39 | this.saveLocal('currentUser', user)
40 | }
41 |
42 | getCurrentUser() {
43 | return this.getLocal('currentUser')
44 | }
45 |
46 | getCurrentTheme() {
47 | return this.getLocal('currentTheme')
48 | }
49 |
50 | setCurrentTheme(data) {
51 | this.saveLocal('currentTheme', data)
52 | }
53 |
54 | setAccessToken(data) {
55 | this.saveLocal('accessToken', data)
56 | }
57 |
58 | getAccessToken() {
59 | return this.getLocal('accessToken')
60 | }
61 |
62 | delAccessToken() {
63 | this.delLocal('accessToken')
64 | }
65 |
66 | setWalletData(data) {
67 | this.saveLocal('walletPageData', data)
68 | }
69 |
70 | getWalletData() {
71 | return this.getLocal('walletPageData')
72 | }
73 |
74 | setCurrentAccountBook(data) {
75 | this.saveLocal('currentAccountBook', data)
76 | }
77 |
78 | getCurrentAccountBook() {
79 | return this.getLocal('currentAccountBook')
80 | }
81 |
82 | setStatementCategories(type, data) {
83 | const cacheAB = this.getCurrentAccountBook()
84 | if (cacheAB) {
85 | this.saveLocal(`currentCategories_${type}_${cacheAB.id}`, data)
86 | } else {
87 | return null
88 | }
89 | }
90 |
91 | getStatementCategories(type) {
92 | const cacheAB = this.getCurrentAccountBook()
93 | if (cacheAB) {
94 | return this.getLocal(`currentCategories_${type}_${cacheAB.id}`)
95 | } else {
96 | return null
97 | }
98 | }
99 |
100 | setStatementAssets(data) {
101 | const cacheAB = this.getCurrentAccountBook()
102 | if (cacheAB) {
103 | this.saveLocal(`currentAssets_${cacheAB.id}`, data)
104 | } else {
105 | return null
106 | }
107 | }
108 |
109 | getStatementAssets() {
110 | const cacheAB = this.getCurrentAccountBook()
111 | if (cacheAB) {
112 | return this.getLocal(`currentAssets_${cacheAB.id}`)
113 | } else {
114 | return null
115 | }
116 | }
117 |
118 | delStatementCategories() {
119 | const cacheAB = this.getCurrentAccountBook()
120 | if (cacheAB) {
121 | this.delLocal(`currentCategories_expend_${cacheAB.id}`)
122 | this.delLocal(`currentCategories_income_${cacheAB.id}`)
123 | } else {
124 | return null
125 | }
126 | }
127 |
128 | delStatementAssets() {
129 | const cacheAB = this.getCurrentAccountBook()
130 | if (cacheAB) {
131 | this.delLocal(`currentAssets_${cacheAB.id}`)
132 | } else {
133 | return null
134 | }
135 | }
136 | }
137 |
138 |
139 |
--------------------------------------------------------------------------------
/src/assets/styl/common/flex.styl:
--------------------------------------------------------------------------------
1 | // flex 子元素,小程序不支持 * 选择符
2 | $flexChildSelector := '*'
3 | $flexJustifyContentEnums := {
4 | start: flex-start,
5 | end: flex-end,
6 | center: center,
7 | fluid: stretch,
8 | around: space-around,
9 | between: space-between
10 | }
11 |
12 | $flexColumnPercentUnits := (5 10 15 20 25 30 33 35 40 45 50 55 60 65 70 75 80 85 90 95 100)
13 |
14 | loop-map-callback(maps, callback) {
15 | for key, value in maps {
16 | callback(key, value)
17 | }
18 | }
19 |
20 | unfold-justify-content(selector, value) {
21 | {selector} {
22 | justify-content value
23 | }
24 | }
25 |
26 | .d-flex {
27 | display flex !important
28 | }
29 |
30 | .d-iflex {
31 | display inline-flex !important
32 | }
33 |
34 | .d-iblock {
35 | display inline-block !important
36 | }
37 |
38 | .flex {
39 | &-row {
40 | flex-direction row
41 | }
42 |
43 | &-row-r {
44 | flex-direction row-reverse
45 | }
46 |
47 | &-column {
48 | flex-direction column
49 | }
50 |
51 | &-column-r {
52 | flex-direction column-reverse
53 | }
54 |
55 | &-wrap {
56 | flex-wrap wrap
57 | }
58 |
59 | &-nowrap {
60 | flex-wrap nowrap
61 | }
62 |
63 | &-align-baseline {
64 | align-items baseline
65 | }
66 |
67 | &-1 {
68 | flex 1
69 | }
70 |
71 | &-auto {
72 | flex auto
73 | }
74 |
75 | &-none {
76 | flex none
77 | }
78 |
79 | &-start {
80 | align-items flex-start
81 | loop-map-callback($flexJustifyContentEnums, @(key, value) {
82 | &-{key} {
83 | align-items flex-start
84 | justify-content value
85 | }
86 | });
87 | }
88 |
89 | &-end {
90 | align-items flex-end
91 | loop-map-callback($flexJustifyContentEnums, @(key, value) {
92 | &-{key} {
93 | align-items flex-end
94 | justify-content value
95 | }
96 | });
97 | }
98 |
99 | &-stretch {
100 | align-items stretch
101 | loop-map-callback($flexJustifyContentEnums, @(key, value) {
102 | &-{key} {
103 | align-items stretch
104 | justify-content value
105 | }
106 | });
107 | }
108 |
109 | &-center {
110 | align-items center
111 | loop-map-callback($flexJustifyContentEnums, @(key, value) {
112 | &-{key} {
113 | align-items center
114 | justify-content value
115 | }
116 | });
117 | }
118 |
119 | &-between {
120 | justify-content: space-between
121 | }
122 |
123 | loop-map-callback($flexJustifyContentEnums, @(key, value) {
124 | &-jc-{key} {
125 | justify-content value
126 | }
127 | });
128 |
129 | &-grow {
130 | flex-grow 1
131 |
132 | &-0 {
133 | flex-grow 0 !important
134 | }
135 | //& > {$flexChildSelector} {
136 | // flex-grow 1
137 | // &.shrink {
138 | // flex-grow 0
139 | // flex-shrink 1
140 | // }
141 | //}
142 | }
143 |
144 | &-shrink {
145 | flex-shrink 1
146 |
147 | &-0 {
148 | flex-shrink 0 !important
149 | }
150 | //& > {$flexChildSelector} {
151 | //
152 | // &.grow {
153 | // flex-grow 1
154 | // flex-shrink 0
155 | // }
156 | //}
157 | }
158 |
159 |
160 | }
161 |
162 | for percentValue in $flexColumnPercentUnits {
163 | .flex-col-p{percentValue} {
164 | flex-basis unit(percentValue, '%')
165 | }
166 | }
167 |
168 | .height-expand {
169 | height 100%
170 | }
171 |
172 | .min-height-expand {
173 | min-height 100%
174 | }
175 |
--------------------------------------------------------------------------------
/src/pages/payee/list.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { View, Text } from '@tarojs/components'
3 | import BasePage from '@/components/BasePage'
4 | import { Button } from '@/components/UiComponents'
5 | import jz from '@/jz'
6 | import Taro from '@tarojs/taro'
7 |
8 | import './list.scss'
9 |
10 | const PayeeList: React.FC = () => {
11 | const [payees, setPayees] = useState([])
12 |
13 | useEffect(() => {
14 | loadPayees()
15 | }, [])
16 |
17 | const loadPayees = async () => {
18 | const data = await jz.withLoading(await jz.api.payees.list())
19 | setPayees(data)
20 | }
21 |
22 | const handleSubmit = async (name: string) => {
23 | if (!name.trim()) {
24 | jz.toastError('请输入商家名称')
25 | return
26 | }
27 | try {
28 | await jz.api.payees.create({ name })
29 | loadPayees()
30 | } catch (error) {
31 | jz.toastError(error.message)
32 | }
33 | }
34 |
35 | const handleDelete = async (payee) => {
36 | const { confirm } = await Taro.showModal({
37 | title: '确认删除',
38 | content: `确定要删除商家"${payee.name}"吗?`,
39 | confirmText: '删除',
40 | confirmColor: '#ff4d4f'
41 | })
42 |
43 | if (confirm) {
44 | try {
45 | await jz.api.payees.delete(payee)
46 | loadPayees()
47 | } catch (error) {
48 | jz.toastError(error.message)
49 | }
50 | }
51 | }
52 |
53 | const handleAdd = async () => {
54 | const { confirm, content } = await Taro.showModal({
55 | title: '添加商家',
56 | content: '',
57 | editable: true,
58 | placeholderText: '请输入商家名称',
59 | confirmText: '添加',
60 | cancelText: '取消'
61 | })
62 | if (confirm && content) {
63 | handleSubmit(content)
64 | }
65 | }
66 |
67 | const handleEdit = async (payee) => {
68 | const { confirm, content } = await Taro.showModal({
69 | title: '编辑商家',
70 | content: payee.name,
71 | editable: true,
72 | placeholderText: '请输入商家名称',
73 | confirmText: '保存',
74 | cancelText: '取消'
75 | })
76 |
77 | if (confirm && content) {
78 | try {
79 | await jz.api.payees.update(payee.id, { name: content })
80 | loadPayees()
81 | } catch (error) {
82 | jz.toastError(error.message)
83 | }
84 | }
85 | }
86 |
87 | return (
88 |
89 |
90 | {payees.map((payee, _) => (
91 |
95 |
96 | {payee.name}
97 |
98 |
99 | handleEdit(payee)}
102 | >
103 | 编辑
104 |
105 | handleDelete(payee)}
108 | >
109 | 删除
110 |
111 |
112 |
113 | ))}
114 |
115 |
116 |
121 |
122 |
123 |
124 | )
125 | }
126 |
127 | export default PayeeList
--------------------------------------------------------------------------------
/src/stores/home_store.ts:
--------------------------------------------------------------------------------
1 | import {observable, action,computed } from 'mobx';
2 | import { createContext } from "react";
3 | import jz from '@/jz';
4 |
5 | // 此 Store 存在的意义在于在切换底部 Tab 的时候,首页的数据不会销毁后重新获取造成闪屏的现象!
6 | class HomeStore {
7 | @observable indexHeader = {
8 | month_budget: "0.00",
9 | month_expend: "0.00",
10 | today_expend: "0.00",
11 | use_pencentage: 0,
12 | message: null,
13 | trends: {
14 | day: {ratio: 0, trend: 'up', amount: 0},
15 | week: {ratio: 0, trend: 'up', amount: 0},
16 | month: {ratio: 0, trend: 'up', amount: 0}
17 | }
18 | }
19 | @observable statements = []
20 |
21 | async fetchStatements(range) {
22 | const {data} = await jz.withLoading(jz.api.main.statements(range))
23 | this.statements = data
24 | }
25 |
26 | @action async fetchHomeData(range = 'today') {
27 | const [headerSt, statementSt] = await Promise.all([jz.api.main.header(), jz.api.main.statements(range)])
28 | if (headerSt.isSuccess) {
29 | this.indexHeader = headerSt.data
30 | }
31 | if (statementSt.isSuccess) {
32 | this.statements = statementSt.data
33 | }
34 | this.getProfileData()
35 | }
36 |
37 | @observable
38 | financeData = {
39 | list: [],
40 | header: {},
41 | amount_visible: false
42 | }
43 | @action async getFinanceData() {
44 | const cacheData = jz.storage.getWalletData()
45 | if (cacheData) {
46 | this.financeData = JSON.parse(cacheData)
47 | }
48 |
49 | const { data } = await jz.api.finances.index()
50 | jz.storage.setWalletData(JSON.stringify(data))
51 | this.financeData = data
52 | }
53 | @action async updateFinanceAmountVisible() {
54 | await jz.api.finances.updateAmountVisible({visible: !this.financeData.amount_visible})
55 | this.financeData.amount_visible = !this.financeData.amount_visible
56 | this.getFinanceData()
57 | }
58 |
59 | @observable summaryData = {
60 | header: {},
61 | statements: []
62 | }
63 | @action async getSummaryData(date) {
64 | const [headerSt, statementSt] = await Promise.all([jz.api.statistics.getOverviewHeader(date), jz.api.statistics.getOverviewStatements(date)])
65 | if (headerSt.isSuccess) {
66 | this.summaryData['header'] = headerSt.data
67 | }
68 | if (statementSt.isSuccess) {
69 | this.summaryData['statements'] = statementSt.data
70 | }
71 | }
72 |
73 | @observable currentAccountBook = jz.storage.getCurrentAccountBook() || {}
74 | @observable profileData = {
75 | userInfo: {},
76 | version: '',
77 | theme: null,
78 | account_book: {}
79 | }
80 | @action async getProfileData() {
81 | const cacheAB = jz.storage.getCurrentAccountBook()
82 | if (cacheAB) {
83 | this.currentAccountBook = cacheAB
84 | }
85 |
86 | const { data } = await jz.api.users.getSettingsData()
87 | this.profileData['userInfo'] = data.user
88 | this.profileData['version'] = data.version
89 | this.profileData['theme'] = data.user.theme
90 | this.currentAccountBook = data.user.account_book
91 | jz.storage.setCurrentAccountBook(data.user.account_book)
92 | jz.storage.setCurrentTheme(data.user.theme['class_name'])
93 | }
94 |
95 | @computed
96 | get currentTheme() {
97 | if (this.profileData.theme) {
98 | return this.profileData.theme['class_name']
99 | } else {
100 | const stoTheme = jz.storage.getCurrentTheme()
101 | return stoTheme ? stoTheme : 'jz-theme-default'
102 | }
103 | }
104 | }
105 |
106 | export const HomeStoreContext = createContext(new HomeStore());
--------------------------------------------------------------------------------
/src/pages/account_books/create.styl:
--------------------------------------------------------------------------------
1 | .create-account-book__page {
2 |
3 | .btn-group {
4 | position: fixed
5 | bottom: 0
6 | }
7 |
8 | .edit-category-component {
9 | position: fixed
10 | bottom: 0
11 | min-height: 400PX
12 | max-height: 780PX
13 | width: 100%
14 | z-index: 1000
15 | > .edit-category__mask {
16 | background: rgba(0,0,0, 0.5)
17 | position: absolute
18 | bottom: 0
19 | height: 100vh
20 | width: 100%
21 | z-index: 99
22 | }
23 | > .edit-category__main {
24 | background: #f9f9f9
25 | border-radius: 6PX 6PX 0 0
26 | display: flex
27 | flex-direction: column
28 | position: absolute
29 | bottom: 0
30 | left: 0
31 | width: 100%
32 | height: 500PX
33 | z-index: 100
34 |
35 | animation: edit-category-animation 0.5s;
36 | -webkit-animation: edit-category-animation 0.5s;
37 | animation-fill-mode: forwards;
38 |
39 | .edit-category__main-title {
40 | padding: 12PX;
41 | border-bottom: 1px solid $borderColor;
42 | }
43 | .edit-category__main-content {
44 | overflow-y: auto
45 | flex: 1
46 | }
47 | .edit-category-item.active {
48 | background-color: #e8e8e8;
49 | }
50 | }
51 |
52 | .selected-category-icon image {
53 | width: 40PX
54 | height: 40PX
55 | }
56 |
57 | .icon-list {
58 | display: grid;
59 | grid-template-columns: repeat(5, 1fr);
60 | grid-gap: 10px;
61 | .icon .active {
62 | background-color: #dadada
63 | }
64 | image {
65 | width: 50PX
66 | height: 50PX
67 | }
68 | }
69 |
70 | .button-group {
71 | .btn {
72 | // border: 1px solid var(--primary-bg-color)
73 | padding: 8px 28px;
74 | }
75 | .ok-btn {
76 | background: #0ba71e;
77 | color: white;
78 | border-radius: 26px;
79 | }
80 | .cancel-btn {
81 | background: #8a8e8b;
82 | color: white;
83 | border-radius: 26px;
84 | }
85 |
86 | }
87 |
88 | > .list__item {
89 | display: inline-block
90 | text-align: center
91 | width: 20%
92 | margin: 12PX 0
93 | image {
94 | width: 30PX
95 | height: 30PX
96 | }
97 | }
98 | > .edit-category__header {
99 | font-size: 16PX
100 | margin: 12PX
101 | }
102 |
103 | @-webkit-keyframes edit-category-animation
104 | {
105 | 0% {height: 0%}
106 | 100% {height: 500PX}
107 | }
108 | }
109 | }
110 |
111 | .category-list__component {
112 | .list {
113 | background: #ffffff
114 | margin: 12PX
115 | border-radius: 21PX
116 | padding-top: 16PX
117 | padding-bottom: 16PX
118 |
119 | .parent-category {
120 | padding: 8PX
121 | border-bottom: 1px solid var(--border-color)
122 | }
123 |
124 | image {
125 | width: 25PX
126 | height: 25PX
127 | }
128 | }
129 | .tab-checkbox {
130 | display: flex
131 | background: #ffffff
132 | margin: 12PX
133 | // border-radius: 21PX
134 | .checkbox-title {
135 | padding: 8PX
136 | text-align: center
137 | // border-bottom: 1px solid #ccc
138 | // border-radius: 12PX
139 | .expend.active {
140 | padding: 8PX
141 | border-bottom: 4px solid green
142 | }
143 | .income.active {
144 | padding: 8PX
145 | border-bottom: 4px solid red
146 | }
147 | }
148 | }
149 |
150 | }
--------------------------------------------------------------------------------
/src/pages/account_books/edit.tsx:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro'
2 | import { View } from "@tarojs/components"
3 | import BasePage from '@/components/BasePage'
4 | import { useEffect, useState } from "react"
5 | import jz from '@/jz'
6 | import { format } from 'date-fns'
7 | import { Input, Button, Textarea, SelectInput } from '@/src/components/UiComponents'
8 |
9 | const AccountBookEdit = () => {
10 | const [accountBook, setAccountBook] = useState({
11 | id: 0,
12 | name: '',
13 | description: '',
14 | type: '',
15 | account_type: null
16 | })
17 | const [types, setTypes] = useState([])
18 | const getTypes = async () => {
19 | const { data } = await jz.api.account_books.getAccountBookTypes()
20 | if (data.data) {
21 | setTypes(data.data)
22 | }
23 | }
24 | const getAccountBook = async() => {
25 | const params = await jz.router.getParams()
26 | const { data } = await jz.api.account_books.getAccountBook(params.id)
27 | if (data.status === 200) {
28 | setAccountBook(data.data)
29 | } else {
30 | jz.toastError(data.msg)
31 | }
32 | }
33 |
34 | const onSubmit = async () => {
35 | if (accountBook.name === '') {
36 | jz.toastError('需要填写一个名称哦~')
37 | return false
38 | }
39 | const { data } = await jz.api.account_books.update(accountBook.id, accountBook)
40 | if (data.status === 200) {
41 | jz.router.navigateBack()
42 | } else [
43 | jz.toastError(data.msg)
44 | ]
45 | }
46 |
47 | const onSwitch = async () => {
48 | await jz.confirm('切换到新账簿吗?')
49 | Taro.showLoading('切换中')
50 | await jz.api.account_books.updateDefaultAccount(accountBook)
51 | Taro.hideLoading()
52 | jz.router.redirectTo({ url: '/pages/home/index' })
53 | }
54 |
55 | const onDelete = async () => {
56 | await jz.confirm("删除账簿会删除账簿下的账单/分类/资产,此操作不可恢复!", "重要提示!")
57 | const { data } = await jz.api.account_books.destroy(accountBook.id)
58 | if (data.status === 200) {
59 | await jz.toastError('删除成功', 1500)
60 | jz.router.redirectTo({ url: '/pages/home/index' })
61 | } else [
62 | jz.toastError(data.msg)
63 | ]
64 | }
65 |
66 | useEffect(() => {
67 | getTypes()
68 | getAccountBook()
69 | }, [])
70 |
71 | return (
72 |
75 |
76 |
77 | { setAccountBook({...accountBook, name: data}) }}
82 | >
83 |
84 |
85 |
86 |
87 | { setAccountBook({...accountBook, account_type: data}) }}
91 | selected={accountBook.account_type}
92 | list={types}
93 | >
94 |
95 |
96 |
97 |
98 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | )
113 | }
114 |
115 | export default AccountBookEdit
--------------------------------------------------------------------------------
/src/pages/setting/statements_flow/index.tsx:
--------------------------------------------------------------------------------
1 | import { Component, useEffect, useState } from 'react'
2 | import { View, Text } from '@tarojs/components'
3 | import BasePage from '@/components/BasePage'
4 | import jz from '@/jz'
5 | import AssetBanner from '@/components/AssetBanner'
6 | import Statements from '@/components/Statements'
7 | import './index.scss'
8 |
9 | const StatementFlow: React.FC = () => {
10 | const [firstColumn, setFirstColumn] = useState({})
11 | const [secColumn, setSecColumn] = useState({})
12 | const [thirdColumn, setThirdColumn] = useState({})
13 | const [statementRow, setStatementRow] = useState([])
14 |
15 | const getTimes = async () => {
16 | const {data} = await jz.api.superStatements.getTime()
17 | setFirstColumn({
18 | title: '结余',
19 | amount: data.data.header.left
20 | })
21 | setSecColumn({
22 | title: '收入',
23 | amount: data.data.header.income
24 | })
25 | setThirdColumn({
26 | title: '支出',
27 | amount: data.data.header.expend
28 | })
29 | setStatementRow(data.data.statements)
30 | }
31 |
32 | const getAssetStatements = async (index: number, year: number, month: number) => {
33 | const { data } = await jz.api.superStatements.getStatements({ year: year, month: month })
34 | setStatementRow(prevTimelines => {
35 | const updatedTimelines = prevTimelines.map((timeline, i) => {
36 | if (i === index) {
37 | return {
38 | ...timeline,
39 | statements: data.data,
40 | hidden: !timeline.hidden
41 | };
42 | } else {
43 | return {
44 | ...timeline,
45 | hidden: true
46 | };
47 | }
48 | });
49 | return updatedTimelines;
50 | });
51 | }
52 |
53 | useEffect(() => {
54 | getTimes()
55 | }, [])
56 |
57 | return (
58 |
59 |
60 |
61 |
66 |
67 |
68 |
69 | {statementRow.map((row, index) => (
70 |
71 | getAssetStatements(index, row.year, row.month)}
74 | >
75 |
76 | {row.month}月
77 | {row.year}年
78 |
79 |
80 |
81 | 收入:{row.income_amount}
82 | 支出:{row.expend_amount}
83 |
84 |
85 | {row.surplus}
86 | 结余
87 |
88 |
89 |
90 |
91 | {!row.hidden && (
92 |
93 |
94 |
95 | )}
96 |
97 | ))}
98 |
99 |
100 |
101 | )
102 | }
103 |
104 | export default StatementFlow
--------------------------------------------------------------------------------
/src/api/logic/statement.ts:
--------------------------------------------------------------------------------
1 | import Request from '../request'
2 | import jz from '@/jz'
3 | import Taro from '@tarojs/taro'
4 |
5 | export default class Statement {
6 | private _request: Request
7 | constructor (request: Request) {
8 | this._request = request
9 | }
10 |
11 | // 获取创建账单时的分类列表
12 | async categoriesWithForm(type: 'income' | 'expend') {
13 | const cacheCategories = jz.storage.getStatementCategories(type)
14 | if (cacheCategories) {
15 | return cacheCategories
16 | }
17 |
18 | const st = await this._request.get('statements/categories', {type: type})
19 | if (st.isSuccess) {
20 | const data = { frequent: st.data.frequent, data: st.data.categories }
21 | jz.storage.setStatementCategories(type, data)
22 | return data
23 | } else {
24 | return null
25 | }
26 | }
27 |
28 | // 获取创建账单时的资产列表
29 | async assetsWithForm(params={}) {
30 | const cache = jz.storage.getStatementAssets()
31 | if (cache) {
32 | return cache
33 | }
34 |
35 | const st = await this._request.get('statements/assets', params)
36 | if (st.isSuccess) {
37 | const data = { frequent: st.data.frequent, data: st.data.categories }
38 | jz.storage.setStatementAssets(data)
39 | return data
40 | } else {
41 | return null
42 | }
43 | }
44 |
45 | // 获取最近常用的三个分类
46 | categoryFrequent(type: 'income' | 'expend') {
47 | return this._request.get('statements/category_frequent', {
48 | type: type
49 | })
50 | }
51 |
52 | // 获取最近常用的三个资产
53 | assetFrequent() {
54 | return this._request.get('statements/asset_frequent')
55 | }
56 |
57 | // 获取账单列表
58 | list(params) {
59 | return this._request.get('statements', params)
60 | }
61 |
62 | getListByToken(token, orderBy) {
63 | return this._request.get('statements/list_by_token', {token: token, order_by: orderBy})
64 | }
65 |
66 | // 创建账单
67 | create(data) {
68 | return this._request.post('statements', { statement: data })
69 | }
70 |
71 | // 更新账单
72 | update(statementId, data) {
73 | return this._request.put(`statements/${statementId}`, { statement: data })
74 | }
75 |
76 | // 获取账单详情
77 | getStatement(statementId: number) {
78 | return this._request.get(`statements/${statementId}`)
79 | }
80 |
81 | // 删除账单
82 | deleteStatement(statementId: number) {
83 | return this._request.delete(`statements/${statementId}`)
84 | }
85 |
86 | // 搜索账单
87 | searchStatements(keyword: string) {
88 | return this._request.get('search', {keyword: keyword})
89 | }
90 |
91 | // 账单的详情
92 | getStatementImages() {
93 | return this._request.get('statements/images')
94 | }
95 |
96 | generateShareToken(params) {
97 | return this._request.post('statements/generate_share_key', params)
98 | }
99 |
100 | pre_check_export() {
101 | return this._request.post('statements/export_check', {})
102 | }
103 |
104 | async export_excel (timeRange: string) {
105 | const accessToken = await this._request.getAccessToken()
106 | const header = {
107 | 'content-type': 'application/json',
108 | 'X-WX-APP-ID': jz.appId,
109 | 'X-WX-Skey': accessToken,
110 | }
111 | return Taro.downloadFile({
112 | url: `${this._request._endpoint}/statements/export_excel?range=${timeRange}`,
113 | header: header
114 | })
115 | }
116 |
117 | targetObjects(statementType: string) {
118 | return this._request.get('statements/target_objects', {type: statementType})
119 | }
120 |
121 | removeAvatar(statementId: number, avatar_id: number) {
122 | return this._request.delete(`statements/${statementId}/avatar`, {avatar_id: avatar_id})
123 | }
124 |
125 | defaultCategoryAsset(statementType: string) {
126 | return this._request.get('statements/default_category_asset', {type: statementType})
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/assets/styl/pages/statements/detail.styl:
--------------------------------------------------------------------------------
1 | .jz-pages__statement-detail {
2 | min-height: 100vh
3 | background: #f8f9fa
4 |
5 | .detail-header {
6 | position: relative
7 | background: #ffffff
8 | padding: 32PX 24PX
9 | margin-bottom: 12PX
10 | text-align: center
11 | transition: all 0.3s ease
12 |
13 | .mood-tag {
14 | position: absolute
15 | top: 16PX
16 | right: 16PX
17 | padding: 4PX 12PX
18 | border-radius: 12PX
19 | font-size: 14PX
20 | }
21 |
22 | .type-label {
23 | position: absolute
24 | top: 16PX
25 | left: 16PX
26 | padding: 4PX 12PX
27 | border-radius: 12PX
28 | font-size: 14PX
29 | background: var(--primary-color)
30 | color: #ffffff
31 | }
32 |
33 | .statement-component__icon-image {
34 | width: 64PX
35 | height: 64PX
36 | margin: 0 auto 16PX
37 |
38 | image {
39 | width: 100%
40 | height: 100%
41 | border-radius: 50%
42 | }
43 | }
44 |
45 | .amount-wrapper {
46 | display: flex
47 | align-items: center
48 | justify-content: center
49 | gap: 12PX
50 | margin-top: 16PX
51 |
52 | .type-label {
53 | padding: 4PX 12PX
54 | border-radius: 12PX
55 | font-size: 14PX
56 | background: var(--primary-color)
57 | color: #ffffff
58 | }
59 | }
60 |
61 | .amount-text {
62 | font-size: 32PX
63 | font-weight: 600
64 | }
65 |
66 | .time-text {
67 | font-size: 14PX
68 | color: var(--secondary-text)
69 | margin-top: 8PX
70 | opacity: 0.8
71 |
72 | &.clickable {
73 | cursor: pointer
74 |
75 | .edit-icon {
76 | margin-left: 4PX
77 | font-size: 12PX
78 | }
79 | }
80 | }
81 | .target-object-text {
82 | margin-top: 8PX
83 | font-size: 14PX
84 |
85 | .label {
86 | color: var(--secondary-text)
87 | margin-right: 4PX
88 | }
89 |
90 | .value {
91 | color: var(--text-color)
92 | }
93 | }
94 | }
95 |
96 |
97 | .detail-footer {
98 | padding: 24PX 16PX
99 |
100 | .at-button {
101 | margin-bottom: 12PX
102 |
103 | &:last-child {
104 | margin-bottom: 0
105 | }
106 | }
107 | }
108 | }
109 | .detail-content {
110 | background: #ffffff
111 | margin-bottom: 12PX
112 |
113 | .detail-item {
114 | padding: 16PX 24PX
115 | border-bottom: 1PX solid #f0f0f0
116 |
117 | &:last-child {
118 | border-bottom: none
119 | }
120 |
121 | .item-label {
122 | display: flex
123 | align-items: center
124 | gap: 8PX
125 | margin-bottom: 8PX
126 | color: var(--secondary-text)
127 |
128 | .iconfont {
129 | font-size: 16PX
130 | }
131 | }
132 |
133 | .item-value {
134 | color: var(--text-color)
135 | font-size: 16PX
136 |
137 | &.clickable {
138 | cursor: pointer
139 |
140 | .edit-icon {
141 | margin-left: 8PX
142 | font-size: 14PX
143 | }
144 | }
145 | }
146 |
147 | &.is-description {
148 | .item-value {
149 | textarea {
150 | width: 100%
151 | min-height: 80PX
152 | padding: 8PX
153 | border: 1PX solid #e8e8e8
154 | border-radius: 4PX
155 | font-size: 14PX
156 | line-height: 1.5
157 |
158 | &:focus {
159 | border-color: var(--primary-color)
160 | outline: none
161 | }
162 | }
163 | }
164 | }
165 | }
166 | }
--------------------------------------------------------------------------------
/src/jz.ts:
--------------------------------------------------------------------------------
1 | import Taro from "@tarojs/taro"
2 |
3 | import { Api } from './api'
4 | import Router from './router'
5 | import Storage from './storage'
6 | import config from './config'
7 | import { EventEmitter } from './utils/event'
8 |
9 | class Jz {
10 | private static instance: Jz | null = null
11 | private _event: EventEmitter
12 | private _appid: string
13 | private _baseUrl: string
14 | private _apiUrl: string
15 | private _api: Api
16 | private _router: Router
17 | private _storage: Storage
18 | systemInfo: any
19 |
20 | private constructor() {
21 | // 私有构造函数,防止外部直接 new
22 | this._appid = ''
23 | this._baseUrl = ''
24 | this._apiUrl = ''
25 | this._event = new EventEmitter()
26 | }
27 |
28 | public static getInstance(): Jz {
29 | if (!Jz.instance) {
30 | Jz.instance = new Jz()
31 | }
32 | return Jz.instance
33 | }
34 |
35 | bootstrap({ appid = '', baseUrl = '', apiUrl = '' }) {
36 | this._appid = appid
37 | this._baseUrl = baseUrl
38 | this._apiUrl = apiUrl
39 | this._api = new Api(this.apiUrl)
40 | this._router = new Router()
41 | this._storage = new Storage()
42 | this.systemInfo = Taro.getSystemInfoSync()
43 | return this
44 | }
45 |
46 | async initialize() {
47 | // 初始化用户和账簿信息
48 | this._api.users.getUserInfo().then(res => {
49 | const data = res.data
50 | if (data?.status === 200) {
51 | console.log('当前用户信息:', data.data)
52 | this._storage.setCurrentUser(data.data)
53 | }
54 | })
55 | }
56 |
57 | toastError(content: string, duration = 1500, icon = 'none') {
58 | Taro.showToast({
59 | title: content,
60 | icon: icon,
61 | duration: duration
62 | })
63 | }
64 |
65 | toastSuccess(content: string, duration = 800, icon = 'success') {
66 | Taro.showToast({
67 | title: content,
68 | icon: icon,
69 | duration: duration
70 | })
71 | }
72 |
73 | confirm(text, title='提示', payload={}) {
74 | return new Promise((resolve, reject) => {
75 | Taro.showModal({
76 | title: title,
77 | content: text,
78 | showCancel: true,
79 | success: res => {
80 | if (res.confirm) {
81 | resolve(payload);
82 | } else if (res.cancel) {
83 | reject(payload);
84 | }
85 | },
86 | fail: res => {
87 | reject(payload);
88 | }
89 | });
90 | })
91 | }
92 |
93 | showNavigatorBack(): boolean {
94 | if (this._router.getCurrentInstance().router.path === '/pages/home/index') {
95 | return false
96 | }
97 | return this._router.canNavigateBack()
98 | }
99 |
100 | async withLoading(promise: Promise): Promise {
101 | try {
102 | Taro.showLoading({title: "加载中"})
103 | return await promise
104 | } finally {
105 | Taro.hideLoading()
106 | }
107 | }
108 |
109 | get currentUser() {
110 | return this._storage.getCurrentUser()
111 | }
112 |
113 | get router(): Router {
114 | return this._router
115 | }
116 |
117 | get baseUrl(): string {
118 | return this._baseUrl
119 | }
120 |
121 | set baseUrl(url: string) {
122 | this._baseUrl = url
123 | }
124 |
125 | get apiUrl(): string {
126 | return this._apiUrl
127 | }
128 |
129 | get appId(): string {
130 | return this._appid
131 | }
132 |
133 | get api(): Api {
134 | return this._api
135 | }
136 |
137 | get storage(): Storage {
138 | return this._storage
139 | }
140 |
141 | get event(): EventEmitter {
142 | return this._event
143 | }
144 | }
145 |
146 | const jz = Jz.getInstance().bootstrap({
147 | appid: config.appid,
148 | baseUrl: config.host,
149 | apiUrl: config.api_url
150 | })
151 |
152 | if (process.env.NODE_ENV !== 'production') {
153 | console.log("运行环境:", process.env.NODE_ENV)
154 | console.log("配置初始化", jz)
155 | }
156 |
157 | export default jz
--------------------------------------------------------------------------------
/src/pages/friends/invite_info.tsx:
--------------------------------------------------------------------------------
1 | import BasePage from '@/components/BasePage'
2 | import { View, Text, Image, Button } from '@tarojs/components'
3 | import { useState, useEffect } from 'react'
4 | import Taro from '@tarojs/taro'
5 | import jz from '@/jz'
6 | import { InviteInfoResponse } from '@/src/api/types'
7 |
8 | export default function FriendInvitePage() {
9 | const [inviteInfo, setInviteInfo] = useState({})
10 | const params = jz.router.getParams()
11 |
12 | useEffect(() => {
13 | const fetchInviteInfo = async () => {
14 | const token = decodeURIComponent(params.token)
15 | if (!token) {
16 | jz.toastError('无效的邀请或邀请已过期,请重新获取邀请。')
17 | return
18 | }
19 |
20 | try {
21 | const response: {
22 | status: number;
23 | message: string;
24 | data: InviteInfoResponse
25 | } = await jz.withLoading(jz.api.friends.information(token))
26 |
27 | const data: InviteInfoResponse = response.data
28 | if (data.status !== 200) {
29 | jz.toastError(data.msg)
30 | return
31 | }
32 |
33 | setInviteInfo(data.data)
34 | } catch (error) {
35 | jz.toastError('获取邀请信息失败')
36 | }
37 | }
38 |
39 | fetchInviteInfo()
40 | }, [])
41 |
42 |
43 | const handdleAcceptInvite = async () => {
44 | const { confirm, content } = await Taro.showModal({
45 | title: '设置昵称',
46 | content: '',
47 | editable: true,
48 | placeholderText: '请输入您的昵称',
49 | })
50 |
51 | if (!confirm || !content.trim()) {
52 | jz.toastError('请输入昵称')
53 | return
54 | }
55 |
56 | const token = decodeURIComponent(params.token)
57 | const response: {
58 | status: number;
59 | message: string;
60 | data: any;
61 | } = await jz.withLoading(jz.api.friends.accept(token, content.trim()))
62 | const data = response.data
63 | if (data.status === 401) {
64 | Taro.showToast({
65 | title: data.msg,
66 | duration: 2000,
67 | icon: 'none',
68 | success: async () => {
69 | await jz.api.account_books.updateDefaultAccount(inviteInfo.account_book)
70 | setTimeout(() => {
71 | jz.router.redirectTo({ url: '/pages/home/index' })
72 | }, 1500)
73 | }
74 | })
75 | } else if (data.status === 200) {
76 | jz.toastSuccess(data.msg)
77 | await jz.confirm('是否立即切换到新账本?')
78 | await jz.api.account_books.updateDefaultAccount(inviteInfo.account_book)
79 | jz.router.redirectTo({ url: '/pages/home/index' })
80 | } else {
81 | jz.toastError(data.msg)
82 | }
83 | }
84 |
85 | return (
86 |
87 |
88 |
89 |
90 |
95 | {inviteInfo.invite_user?.nickname}
96 | 邀请您加入一起记账~
97 |
98 |
99 |
100 |
101 | 账本名称
102 | {inviteInfo.account_book?.name}
103 |
104 |
105 | 角色
106 | {inviteInfo.role_name}
107 |
108 |
109 |
110 |
116 |
117 |
118 |
119 | )
120 | }
--------------------------------------------------------------------------------
/src/components/Statement/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View, Image, Text } from '@tarojs/components'
3 | import Avatar from '@/components/Avatar'
4 | import jz from '@/jz'
5 |
6 | const moodTags = [
7 | { name: '开心', color: '#52c41a' }, // 绿色,积极正面的心情
8 | { name: '纠结', color: '#1890ff' }, // 蓝色,表示思考和犹豫
9 | { name: '后悔', color: '#ff4d4f' }, // 红色,表示负面情绪
10 | { name: '无奈', color: '#faad14' }, // 黄色,表示中性情绪
11 | { name: '郁闷', color: '#722ed1' }, // 紫色,表示低落的情绪
12 | { name: '生气', color: '#f5222d' } // 深红色,表示强烈的负面情绪
13 | ]
14 |
15 | const isTheYear = (dateString) => {
16 | const date = new Date(dateString);
17 | const today = new Date();
18 | return date.getFullYear() === today.getFullYear()
19 | }
20 |
21 | const getColorClass = (type) => {
22 | switch(type) {
23 | case 'income':
24 | case 'loan_in':
25 | return 'income';
26 | case 'transfer':
27 | return 'transfer';
28 | default:
29 | return 'expend';
30 | }
31 | }
32 |
33 | const getMoodColor = (mood) => {
34 | const moodTag = moodTags.find(tag => tag.name === mood)
35 | return moodTag?.color || '#52c41a' // 默认使用"开心"的颜色
36 | }
37 |
38 | export default function Statement({ statement, editable = true }) {
39 | const getDisplayInfo = () => {
40 | const infoParts = []
41 |
42 | // 添加日期
43 | infoParts.push(isTheYear(statement.date) ? statement.timeStr : statement.date)
44 |
45 | // 添加备注
46 | if (statement.remark) {
47 | infoParts.push(statement.remark)
48 | }
49 |
50 | // 添加收款人
51 | if (statement.payee) {
52 | infoParts.push(statement.payee.name)
53 | }
54 |
55 | return infoParts.filter(Boolean).join(' · ')
56 | }
57 |
58 | return (
59 |
60 | { editable && jz.router.navigateTo({ url: `/pages/statement_detail/index?statement_id=${statement.id}` }) }}>
61 |
62 |
63 | {statement.icon_path ? (
64 |
65 | ) : (
66 |
70 | )}
71 |
72 |
73 |
74 | {statement.target_object && (
75 | {statement.target_object}
76 | )}
77 | {statement.category}
78 | {statement.mood && (
79 |
80 | {statement.mood}
81 |
82 | )}
83 |
84 |
85 |
86 |
87 | {statement.description && (
88 | {statement.description}
89 | )}
90 |
91 |
92 |
93 |
94 | {getDisplayInfo()}
95 | {statement.has_pic && (
96 |
97 | )}
98 |
99 |
100 |
101 |
102 |
103 | {statement.money}
104 |
105 |
106 |
107 | )
108 | }
109 |
--------------------------------------------------------------------------------
/src/pages/setting/asset/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { AtSwipeAction } from "taro-ui"
3 | import { View, Image, Text } from '@tarojs/components'
4 | import { useDidShow } from '@tarojs/taro'
5 | import BasePage from '@/components/BasePage'
6 | import { Button } from '@/src/components/UiComponents'
7 | import jz from '@/jz'
8 | import Avatar from '@/components/Avatar'
9 |
10 | import "taro-ui/dist/style/components/swipe-action.scss"
11 |
12 | function List ({
13 | data,
14 | handleClick
15 | }) {
16 | return (
17 |
18 | Tips: 左划可以对分类进行编辑和删除哟~
19 | {data.map((item) => (
20 |
21 | handleClick(text, item) }
24 | options={[
25 | {
26 | text: '编辑',
27 | style: {
28 | backgroundColor: '#6190E8'
29 | }
30 | },
31 | {
32 | text: '删除',
33 | style: {
34 | backgroundColor: '#FF4949'
35 | }
36 | }
37 | ]}>
38 | {
39 | if (item.parent_id === 0) {
40 | jz.router.navigateTo({url: `/pages/setting/asset/index?parentId=${item.id}`})
41 | }
42 | }}>
43 |
44 |
45 | {item.icon_url ? (
46 |
47 | ) : (
48 |
53 | )}
54 |
55 |
56 |
57 | {item.name}
58 | ({item.type == 'deposit' ? '存款账户' : '负债账户'})
59 |
60 | 结余 {item.amount}
61 |
62 |
63 |
64 |
65 |
66 | ))}
67 |
68 | )
69 | }
70 |
71 | export default function AssetSetting () {
72 | const params = jz.router.getParams()
73 | const [listData, setListData] = useState([])
74 | const parentId = Number.parseInt(params.parentId) || 0
75 |
76 | // 获取列表
77 | const getAssets = () => {
78 | jz.api.assets.getSettingList({ parentId: parentId }).then((res) => {
79 | jz.storage.delStatementAssets()
80 | setListData(res.data)
81 | })
82 | }
83 |
84 | useDidShow(() => {
85 | getAssets()
86 | })
87 |
88 | const handleClick = async (e, assetItem) => {
89 | if (e.text === '编辑') {
90 | jz.router.navigateTo({ url: `/pages/setting/asset/form?id=${assetItem.id}` })
91 | } else if (e.text === '删除') {
92 | await jz.confirm("是否删除该分类?删父级分类会把子分类也删除,统计数据将丢失,谨慎操作!")
93 | const res = await jz.api.assets.deleteAsset(assetItem.id)
94 | if (res.isSuccess && res.data && res.data.status === 200) {
95 | const deleteIndex = listData.findIndex((item) => item.id === assetItem.id)
96 | if (deleteIndex !== -1) {
97 | const data = [...listData]
98 | data.splice(deleteIndex, 1)
99 | setListData(data)
100 | }
101 | }
102 | }
103 | }
104 |
105 | return (
106 |
109 |
113 |
114 |
121 | )
122 | }
--------------------------------------------------------------------------------
/src/pages/setting/child_budget/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { View, Image, Text } from '@tarojs/components'
3 | import jz from '@/jz'
4 | import { useEffect } from 'react'
5 | import BasePage from '@/components/BasePage'
6 | import { AtProgress } from 'taro-ui'
7 | import Taro from '@tarojs/taro'
8 | import { format } from 'date-fns'
9 |
10 | export default function BudgetPage () {
11 | const [childList, setChildList] = useState([])
12 |
13 | const getBudgets = async () => {
14 | const params = jz.router.getParams()
15 | const { data } = await jz.api.budgets.getCategoryBudget({
16 | category_id: params.category_id,
17 | year: format(new Date(params.date), 'yyyy'),
18 | month: format(new Date(params.date), 'MM')
19 | })
20 | setChildList(data.childs)
21 | }
22 |
23 | useEffect(() => {
24 | getBudgets()
25 | }, [])
26 |
27 | const handleAccountBookBudget = (categoryId, amount) => {
28 | Taro.showModal({
29 | title: '修改预算',
30 | content: amount.toString().replace(/\.0$/, '') || amount.toFixed(2), // 处理金额显示,如果小数位是.00则只保留整数部分,否则保留两位小数
31 | inputType: 'number',
32 | editable: true,
33 | success: async (res) => {
34 | if (res.confirm) {
35 | const newBudget = parseFloat(res.content)
36 | if (!isNaN(newBudget) && newBudget >= 0) {
37 | const {data} = await jz.api.budgets.updateCategoryAmount({category_id: categoryId, amount: newBudget})
38 | if (data.status !== 200) {
39 | Taro.showToast({
40 | title: data.msg,
41 | icon: 'none'
42 | })
43 | return
44 | }
45 | jz.event.emit('budget:update')
46 | getBudgets()
47 | }
48 | }
49 | }
50 | })
51 | }
52 |
53 | return (
54 |
57 |
58 | { childList.map((item) => {
59 | return (
60 |
63 |
64 |
65 |
66 |
67 |
68 | jz.router.navigateTo({url: `/pages/setting/chart/category_statement?date=${format(new Date(jz.router.getParams().date), 'yyyy-MM')}&category_id=${item['id']}` })}
71 | >{item['name']}
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | 已用:¥{item.used_amount}
80 | {item.use_percent}%
81 |
82 |
83 |
84 |
85 |
86 | 预算金额
87 | ¥{item['amount']}
88 |
89 | {
92 | handleAccountBookBudget(item['id'], item['source_amount'])
93 | }}
94 | >调整预算
95 |
96 |
97 | 剩余可用
98 | ¥{item['surplus']}
99 |
100 |
101 |
102 |
103 | )
104 | })
105 | }
106 |
107 |
108 | )
109 | }
--------------------------------------------------------------------------------
/src/assets/styl/pages/finance/asset_flow.styl:
--------------------------------------------------------------------------------
1 | .jz-pages-assets-flow {
2 | background: #f8f9fa
3 |
4 | .asset-header {
5 | margin: 16PX
6 |
7 | .balance-section {
8 | background: #ffffff
9 | border: 1PX solid var(--border-color)
10 | border-radius: 16PX
11 | padding: 20PX
12 | box-shadow: 0 4PX 12PX rgba(0, 0, 0, 0.03)
13 |
14 | .label {
15 | color: var(--secondary-text)
16 | font-size: 14PX
17 | margin-right: 12PX
18 | }
19 |
20 | .amount-wrapper {
21 | display: flex
22 | align-items: center
23 |
24 | .amount {
25 | font-size: 24PX
26 | color: var(--primary-color)
27 | font-weight: 500
28 | margin-right: 12PX
29 | }
30 |
31 | .edit-btn {
32 | color: var(--primary-color)
33 | font-size: 14PX
34 | }
35 | }
36 |
37 | .edit-wrapper {
38 | display: flex
39 | align-items: center
40 | flex: 1
41 | }
42 |
43 | .summary-section {
44 | display: flex
45 | justify-content: space-between
46 | margin-top: 16PX
47 | padding-top: 16PX
48 | border-top: 1PX solid var(--border-color)
49 |
50 | .summary-item {
51 | display: flex
52 | flex-direction: column
53 | align-items: center
54 |
55 | .label {
56 | font-size: 12PX
57 | margin-bottom: 4PX
58 | }
59 |
60 | &.income .value {
61 | color: #2ecc71
62 | }
63 |
64 | &.expend .value {
65 | color: #e74c3c
66 | }
67 | }
68 | }
69 | }
70 | }
71 |
72 | .timeline-list {
73 | margin: 16PX
74 |
75 | .timeline-item {
76 | margin-bottom: 12PX
77 | background: #ffffff
78 | border: 1PX solid var(--border-color)
79 | border-radius: 12PX
80 | overflow: hidden
81 |
82 | .timeline-header {
83 | display: flex
84 | padding: 16PX
85 | align-items: center
86 |
87 | &.active {
88 | background: #f8f9fa
89 | border-left: 3PX solid var(--primary-color)
90 | }
91 |
92 | .date-section {
93 | display: flex
94 | flex-direction: column
95 | align-items: center
96 | margin-right: 16PX
97 | min-width: 60PX
98 |
99 | .month {
100 | font-size: 18PX
101 | font-weight: 500
102 | color: var(--primary-color)
103 | }
104 |
105 | .year {
106 | font-size: 12PX
107 | color: var(--secondary-text)
108 | }
109 | }
110 |
111 | .amount-section {
112 | flex: 1
113 |
114 | .amount-row {
115 | display: flex
116 | align-items: center
117 | margin: 4PX 0
118 |
119 | .label {
120 | color: var(--secondary-text)
121 | font-size: 12PX
122 | margin-right: 8PX
123 | }
124 |
125 | &.income .value {
126 | color: #2ecc71
127 | }
128 |
129 | &.expend .value {
130 | color: #e74c3c
131 | }
132 | }
133 | }
134 |
135 | .balance-section {
136 | display: flex
137 | flex-direction: column
138 | align-items: flex-end
139 | min-width: 100PX
140 |
141 | .value {
142 | color: var(--primary-color)
143 | font-weight: 500
144 | }
145 |
146 | .label {
147 | font-size: 12PX
148 | color: var(--secondary-text)
149 | }
150 |
151 | .iconfont {
152 | margin-top: 4PX
153 | color: var(--secondary-text)
154 | }
155 | }
156 | }
157 |
158 | .statement-list {
159 | padding: 0 16PX 16PX
160 | }
161 | }
162 | }
163 | }
164 |
165 | .adjust-balance-modal {
166 | padding: 16PX
167 |
168 | .button-group {
169 | display: flex
170 | justify-content: space-between
171 | margin-top: 24PX
172 | gap: 16PX
173 |
174 | .at-button {
175 | flex: 1
176 | }
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/src/pages/friends/index.scss:
--------------------------------------------------------------------------------
1 | .friends-page {
2 | padding: 16PX;
3 |
4 | .friends-list {
5 | margin-bottom: 20PX;
6 | }
7 |
8 | .friend-card {
9 | margin-bottom: 16PX;
10 | }
11 |
12 | .current-user {
13 | border: 1PX solid var(--primary-color)
14 | }
15 |
16 | .friend-item {
17 | display: flex;
18 | align-items: center;
19 | }
20 |
21 | .friend-avatar {
22 | width: 48PX;
23 | height: 48PX;
24 | border-radius: 50%;
25 | margin-right: 16PX;
26 | }
27 |
28 | .friend-info {
29 | flex: 1;
30 | }
31 |
32 | .friend-name {
33 | // font-size: 16PX;
34 | font-weight: 500;
35 | margin-bottom: 4px;
36 | display: block;
37 | }
38 |
39 | .friend-permissions {
40 | // font-size: 12px;
41 | color: #999;
42 | }
43 |
44 | .invite-button {
45 | position: fixed;
46 | bottom: 32px;
47 | left: 50%;
48 | transform: translateX(-50%);
49 | width: 90%;
50 | background-color: #07c160;
51 | color: #fff;
52 | }
53 |
54 | .custom-modal {
55 | position: fixed;
56 | top: 0;
57 | left: 0;
58 | right: 0;
59 | bottom: 0;
60 | z-index: 1000;
61 |
62 | .modal-mask {
63 | position: absolute;
64 | top: 0;
65 | left: 0;
66 | right: 0;
67 | bottom: 0;
68 | background: rgba(0, 0, 0, 0.5);
69 | }
70 |
71 | .modal-content {
72 | position: absolute;
73 | left: 50%;
74 | top: 50%;
75 | transform: translate(-50%, -50%);
76 | width: 80%;
77 | background: #fff;
78 | border-radius: 12px;
79 | overflow: hidden;
80 | }
81 |
82 | .modal-header {
83 | padding: 16PX;
84 | text-align: center;
85 | font-size: 16PX;
86 | font-weight: 500;
87 | border-bottom: 1PX solid #eee;
88 | }
89 |
90 | .modal-body {
91 | padding: 16PX;
92 |
93 | .modal-desc {
94 | margin-bottom: 16PX;
95 | font-size: 14PX;
96 | }
97 | }
98 |
99 | .modal-footer {
100 | display: flex;
101 | border-top: 1PX solid #eee;
102 |
103 | .modal-btn {
104 | flex: 1;
105 | height: 44PX;
106 | line-height: 44PX;
107 | text-align: center;
108 | font-size: 16PX;
109 | border: none;
110 | background: none;
111 |
112 | &.cancel {
113 | color: #666;
114 | border-right: 1PX solid #eee;
115 | }
116 |
117 | &.confirm {
118 | color: #07c160;
119 | }
120 | }
121 | }
122 | }
123 | }
124 |
125 | .modal-overlay {
126 | position: fixed;
127 | top: 0;
128 | left: 0;
129 | right: 0;
130 | bottom: 0;
131 | background-color: rgba(0, 0, 0, 0.5);
132 | display: flex;
133 | align-items: center;
134 | justify-content: center;
135 | z-index: 1000;
136 | }
137 |
138 | .modal-wrapper {
139 | position: fixed;
140 | bottom: 0;
141 | left: 0;
142 | right: 0;
143 | background: #fff;
144 | border-radius: 0 0 16PX 16PX;
145 | transform: translateY(100%);
146 | animation: slideDown 0.3s forwards;
147 | }
148 |
149 | @keyframes slideDown {
150 | to {
151 | transform: translateY(0);
152 | }
153 | }
154 |
155 | .permission-role {
156 | margin: 16PX 0;
157 | padding: 16PX;
158 | background: #f5f5f5;
159 | border-radius: 8PX;
160 | cursor: pointer;
161 | transition: all 0.3s;
162 |
163 | &.selected {
164 | background: #e6f3ff;
165 | border: 1PX solid var(--primary-color);
166 | }
167 |
168 | .role-title {
169 | font-weight: bold;
170 | margin-bottom: 8PX;
171 | color: #333;
172 | }
173 |
174 | .role-desc {
175 | color: #666;
176 | line-height: 1.5;
177 | }
178 | }
179 |
180 | .modal-header {
181 | display: flex;
182 | justify-content: space-between;
183 | align-items: center;
184 | margin-bottom: 20PX;
185 | }
186 |
187 | .modal-title {
188 | font-weight: bold;
189 | }
190 |
191 | .modal-close {
192 | color: #999;
193 | cursor: pointer;
194 | }
195 |
196 | .modal-body {
197 | margin-bottom: 20PX;
198 | }
199 |
200 | .modal-desc {
201 | margin-bottom: 16PX;
202 | color: #666;
203 | }
204 |
205 | .modal-footer {
206 | display: flex;
207 | justify-content: flex-end;
208 | gap: 16PX;
209 | }
210 |
211 | .modal-btn {
212 | min-width: 160PX;
213 | height: 72PX;
214 | line-height: 72PX;
215 | text-align: center;
216 | border-radius: 36PX;
217 |
218 | &.cancel {
219 | background: #f5f5f5;
220 | color: #666;
221 | }
222 |
223 | &.confirm {
224 | background: #07c160;
225 | color: #fff;
226 | }
227 | }
--------------------------------------------------------------------------------
/src/pages/setting/category/form.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { View, Image } from '@tarojs/components'
3 | import BasePage from '@/components/BasePage'
4 | import jz from '@/jz'
5 | import { AtInput } from 'taro-ui'
6 | import { Button } from '@/src/components/UiComponents'
7 | import iconSelectDefault from '@/assets/images/icon_select_default.png'
8 |
9 | import "taro-ui/dist/style/components/input.scss"
10 |
11 | function IconList({
12 | showMask = false,
13 | setShowMask,
14 | handleSelect,
15 | iconList
16 | }) {
17 | if (!showMask) {
18 | return null
19 | }
20 |
21 | return (
22 |
23 | setShowMask(!showMask)}>
24 |
25 |
26 | {iconList.map((icon) => {
27 | return (
28 | {
29 | setShowMask(!showMask)
30 | handleSelect(icon)
31 | }}>
32 |
33 |
34 | )
35 | })}
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | export default function EditCategory () {
43 | const params = jz.router.getParams()
44 | const [category, setCategory] = useState({
45 | id: 0,
46 | name: '',
47 | parent_id: Number.parseInt(params.parentId) || 0,
48 | type: params.type,
49 | parent_name: '',
50 | icon_id: ''
51 | })
52 |
53 | const [showIconList, setShowIconList] = useState(false)
54 | const [iconList, setIconList] = useState([])
55 | useEffect(() => {
56 | if (params.id) {
57 | jz.api.categories.getCategoryDetail(params.id).then((res) => {
58 | setCategory(res.data)
59 | })
60 | }
61 |
62 | jz.api.categories.getCategoryIcon().then((res) => {
63 | setIconList(res.data)
64 | })
65 |
66 | jz.api.categories.getSettingList({ type: params.type }).then((res) => {
67 | const data = res.data.categories.find((item) => item.id === Number.parseInt(params.parentId))
68 | if (data) {
69 | setCategory({...category, parent_name: data.name})
70 | }
71 | })
72 | }, [])
73 |
74 | const handleSubmit = async () => {
75 | const data = {
76 | name: category.name,
77 | parent_id: category.parent_id,
78 | icon_path: category.icon_id
79 | }
80 |
81 | let st
82 | if (category.id) {
83 | st = await jz.api.categories.updateCategory(category.id, { category: data })
84 | } else {
85 | st = await jz.api.categories.create({ category: { ...data, type: category.type }})
86 | }
87 |
88 | if (st.data.status === 200) {
89 | jz.router.navigateBack()
90 | } else {
91 | jz.toastError(st.data.msg)
92 | }
93 | }
94 |
95 | function handleIconSelect(icon) {
96 | const newIcon = { icon_url: icon.url, icon_id: icon.id }
97 | setCategory({ ...category, ...newIcon })
98 | }
99 |
100 | return (
101 |
104 | { category.id === 0 && category.parent_id > 0 && 在【{category.parent_name}】下创建子分类 }
105 |
106 |
107 |
108 |
109 | {
115 | setCategory(Object.assign({...category, name: value}))
116 | }}
117 | />
118 |
119 |
120 | setShowIconList(!showIconList)}>
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
132 |
133 |
139 |
140 |
141 | )
142 | }
--------------------------------------------------------------------------------
/src/api/request.ts:
--------------------------------------------------------------------------------
1 | import Taro from "@tarojs/taro"
2 | import HttpResult from './http-result'
3 | import jz from '../jz'
4 |
5 | class RequestManager {
6 | private static cache: { [key: string]: Promise } = {};
7 |
8 | private static getStoredToken() {
9 | const tokenData = Taro.getStorageSync('access_token_data')
10 | if (!tokenData) return null
11 |
12 | const { token, expireTime } = JSON.parse(tokenData)
13 | if (Date.now() > expireTime) {
14 | Taro.removeStorageSync('access_token_data')
15 | return null
16 | }
17 | return token
18 | }
19 |
20 | private static setStoredToken(token: string) {
21 | const expireTime = Date.now() + 2 * 60 * 60 * 1000 // 2小时过期
22 | Taro.setStorageSync('access_token_data', JSON.stringify({
23 | token,
24 | expireTime
25 | }))
26 | }
27 |
28 | static async get(url, endpoint, code): Promise {
29 | // 先检查本地存储的 token
30 | const storedToken = this.getStoredToken()
31 | if (storedToken) {
32 | return {
33 | statusCode: 200,
34 | data: { session: storedToken }
35 | }
36 | }
37 |
38 | // 如果没有有效的缓存 token,则发起请求
39 | if (this.cache[url]) {
40 | return this.cache[url];
41 | }
42 |
43 | const checkOpenId = Taro.request({
44 | method: 'POST',
45 | url: `${endpoint}/check_openid`,
46 | header: {
47 | 'X-WX-Code': code,
48 | 'X-WX-APP-ID': jz.appId,
49 | }
50 | }).then(res => {
51 | if (res.statusCode === 200 && res.data.session) {
52 | this.setStoredToken(res.data.session)
53 | }
54 | return res
55 | })
56 |
57 | this.cache[url] = checkOpenId
58 | return this.cache[url];
59 | }
60 |
61 | static async delCache() {
62 | this.cache = {}
63 | Taro.removeStorageSync('access_token_data')
64 | }
65 | }
66 |
67 | class Request {
68 | public _endpoint: string
69 | constructor (endpoint: string) {
70 | this._endpoint = endpoint
71 | }
72 |
73 | get (path, data?, options = {}) {
74 | return this.request('GET', path, data, options)
75 | }
76 |
77 | post (path, data, options = {}): Promise {
78 | return this.request('POST', path, data, options)
79 | }
80 |
81 | put (path, data, options = {}): Promise {
82 | return this.request('PUT', path, data, options)
83 | }
84 |
85 | delete (path, data={}, options = {}): Promise {
86 | return this.request('DELETE', path, data, options)
87 | }
88 |
89 | async upload (file_path, formData) {
90 | const accessToken = await this.getAccessToken()
91 | const header = {
92 | 'content-type': 'application/json',
93 | 'X-WX-APP-ID': jz.appId,
94 | 'X-WX-Skey': accessToken,
95 | }
96 |
97 | return Taro.uploadFile({
98 | url: `${this._endpoint}/upload`,
99 | header: header,
100 | filePath: file_path,
101 | formData: formData,
102 | name: 'file'
103 | })
104 | }
105 |
106 | async getAccessToken(): Promise {
107 | const loginCode = await Taro.login()
108 | const res = await RequestManager.get('check_openid', this._endpoint, loginCode.code)
109 | if (res.statusCode === 200) {
110 | return res.data.session
111 | }
112 | RequestManager.delCache()
113 | return ''
114 | }
115 |
116 | async request (method, path, data, options = {}): Promise {
117 | let retryCount = 5
118 | let lastResult: HttpResult | null = null
119 | // 处理路径,确保endpoint和path之间只有一个/
120 | const normalizedPath = path.startsWith('/') ? path.substring(1) : path
121 | const requestUrl = `${this._endpoint}/${normalizedPath}`
122 | const header = Object.assign({
123 | 'content-type': 'application/json',
124 | 'X-WX-APP-ID': jz.appId
125 | }, options['header'])
126 |
127 | while (retryCount >= 0) {
128 | const accessToken = await this.getAccessToken()
129 | header['X-WX-Skey'] = accessToken
130 | try {
131 | const res = await Taro.request({
132 | method: method,
133 | url: requestUrl,
134 | data: data,
135 | header: header,
136 | })
137 |
138 | const result = new HttpResult(res);
139 | if (result['data'] && result['data']['status'] === 301) {
140 | RequestManager.delCache()
141 | retryCount--
142 | continue
143 | } else {
144 | lastResult = result
145 | break
146 | }
147 | } catch (error) {
148 | RequestManager.delCache()
149 | retryCount--
150 | if (retryCount < 0) {
151 | throw error
152 | }
153 | }
154 | }
155 |
156 | return lastResult
157 | }
158 | }
159 |
160 | export default Request
--------------------------------------------------------------------------------