├── .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 | ![二维码](https://github.com/yigger/jiezhang/raw/old-version-wepy/screenshots/qrcode.jpg) 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 | 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 |