feat🔫: 记账功能

This commit is contained in:
clz 2022-09-05 17:57:22 +08:00
parent 3c65054da3
commit c77a6366a7
8 changed files with 230 additions and 33 deletions

View File

@ -8,6 +8,7 @@ import {MdEditor} from './components/editor'
import NotFound from './pages/NotFound' import NotFound from './pages/NotFound'
import {Bill, BillContext} from "./store"; import {Bill, BillContext} from "./store";
import './App.css' import './App.css'
import Record from "./pages/Record/Record";
function App() { function App() {
const billStore = new Bill() const billStore = new Bill()
@ -26,6 +27,7 @@ function App() {
<Routes> <Routes>
<Route path={"/"} element={<Home/>}/> <Route path={"/"} element={<Home/>}/>
<Route path={"/home"} element={<Home/>}/> <Route path={"/home"} element={<Home/>}/>
<Route path={"/record"} element={<Record/>}/>
<Route path={"/chat"} element={<Chat/>}/> <Route path={"/chat"} element={<Chat/>}/>
<Route path={"/editor"} element={<MdEditor/>}/> <Route path={"/editor"} element={<MdEditor/>}/>
<Route path={'/login'} element={<Login/>}/> <Route path={'/login'} element={<Login/>}/>

View File

@ -1,6 +1,17 @@
import request from "./request"; import request from "./request";
import {IBill} from "../model";
export async function getBills(year: number, month: number) { export async function getBills(year: number, month: number) {
const data = await request.get(`/search/${year}/${month}`) const data = await request.get(`/search/${year}/${month}`)
return data.data return data.data
} }
export async function getClass() {
const data = await request.get(`/class`)
return data.data
}
export async function createBill(bill: IBill) {
const data = await request.post(`/create`, bill)
return data.data
}

View File

@ -1,5 +1,5 @@
import styles from './index.module.scss' import styles from './index.module.scss'
import {NavLink} from "react-router-dom"; import {NavLink, useLocation, useRoutes} from "react-router-dom";
import React, {ReactElement, useState} from 'react'; import React, {ReactElement, useState} from 'react';
import { import {
Menu, Menu,
@ -7,7 +7,7 @@ import {
Layout as AntLayout Layout as AntLayout
} from 'antd'; } from 'antd';
import { import {
EditOutlined, EditOutlined, FormOutlined,
HomeOutlined, HomeOutlined,
LoginOutlined, LoginOutlined,
WechatOutlined WechatOutlined
@ -21,17 +21,18 @@ interface IProps {
export default function Layout(props: IProps) { export default function Layout(props: IProps) {
const {children, home} = props const {children, home} = props
const {Header, Content, Footer, Sider} = AntLayout;
const items = [ const items = [
{label: <NavLink to={"/home"}>Home</NavLink>, key: 'home', icon: <HomeOutlined/>}, {label: <NavLink to={"/home"}>Home</NavLink>, key: '/home', icon: <HomeOutlined/>},
{label: <NavLink to={"/login"}>Login</NavLink>, key: 'login', icon: <LoginOutlined/>}, {label: <NavLink to={"/record"}>Record</NavLink>, key: '/record', icon: <FormOutlined/>},
{label: <NavLink to={"/chat"}>Chat</NavLink>, key: 'chat', icon: <WechatOutlined/>}, {label: <NavLink to={"/login"}>Login</NavLink>, key: '/login', icon: <LoginOutlined/>},
{label: <NavLink to={"/editor"}>Editor</NavLink>, key: 'editor', icon: <EditOutlined/>}, {label: <NavLink to={"/chat"}>Chat</NavLink>, key: '/chat', icon: <WechatOutlined/>},
{label: <NavLink to={"/editor"}>Editor</NavLink>, key: '/editor', icon: <EditOutlined/>},
] ]
const SiderMenu = (sprops: { theme?: MenuTheme }) => { const SiderMenu = (sprops: { theme?: MenuTheme }) => {
const location = useLocation()
const siderTitleCSS: React.CSSProperties = { const siderTitleCSS: React.CSSProperties = {
color: "white", color: "white",
fontSize: "30px", fontSize: "30px",
@ -39,7 +40,7 @@ export default function Layout(props: IProps) {
} }
const [collapsed, setCollapsed] = useState(false) const [collapsed, setCollapsed] = useState(false)
return ( return (
<Sider <AntLayout.Sider
collapsible collapsible
theme={sprops.theme} theme={sprops.theme}
collapsed={collapsed} collapsed={collapsed}
@ -50,10 +51,9 @@ export default function Layout(props: IProps) {
items={items} items={items}
theme={sprops.theme} theme={sprops.theme}
mode="inline" mode="inline"
defaultSelectedKeys={["home"]} defaultSelectedKeys={[location.pathname]}
// style={{ width: 256 }}
/> />
</Sider> </AntLayout.Sider>
) )
} }

View File

@ -1,8 +1,32 @@
export enum BillType {
consume = 0,
income,
}
export interface IBill { export interface IBill {
_id?: string, id?: string,
type: BillType
date: string, date: string,
money: number, money: number,
cls: string, cls: string,
label: string, label: string,
options?: string options?: string
} }
export function EmptyBill(): IBill {
const now = new Date();
return {
date: `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`,
money: 0,
type: BillType.consume,
cls: "",
label: "",
options: "",
}
}
export interface IClass {
consume: Map<string, string[]>,
income: string[],
}

View File

@ -1,15 +1,14 @@
import styles from "./Home.module.scss" import styles from "./Home.module.scss"
import * as R from 'ramda' import * as R from 'ramda'
import { IBill } from "../../model" import {IBill} from "../../model"
import Bar from "../../components/charts/bar" import Bar from "../../components/charts/bar"
import { useContext, useEffect, useState } from "react"; import {useContext, useEffect, useState} from "react";
import { BillContext } from "../../store"; import {BillContext} from "../../store";
import { observer } from "mobx-react-lite"; import {observer} from "mobx-react-lite";
import Pie from "../../components/charts/pie"; import Pie from "../../components/charts/pie";
import { Card, ConfigProvider, DatePicker } from "antd"; import {Card, DatePicker} from "antd";
import moment from 'moment'; import moment from 'moment';
import 'moment/locale/zh-cn'; import 'moment/locale/zh-cn';
import locale from 'antd/es/locale/zh_CN';
const Home = () => { const Home = () => {
const billStore = useContext(BillContext) const billStore = useContext(BillContext)
@ -29,7 +28,7 @@ const Home = () => {
const [month, setMonth] = useState(now.getMonth() + 1) const [month, setMonth] = useState(now.getMonth() + 1)
useEffect(() => { useEffect(() => {
billStore.fetch(year, month) billStore.fetch(year, month).then()
}, [year, month]) }, [year, month])
const changeDate = (date: moment.Moment | null, datestring: string) => { const changeDate = (date: moment.Moment | null, datestring: string) => {
@ -51,11 +50,11 @@ const Home = () => {
value={moment(`${year}-${month}`, 'YYYY-MM')} value={moment(`${year}-${month}`, 'YYYY-MM')}
onChange={changeDate} onChange={changeDate}
/> />
<TotalMoney /> <TotalMoney/>
</div>
<Bar data={transformer(billStore.listAllByDate)}/>
<Pie data={transformer(billStore.listAllByClass)}/>
</div> </div>
<Bar data={transformer(billStore.listAllByDate)} />
<Pie data={transformer(billStore.listAllByClass)} />
</div >
) )
} }

View File

@ -0,0 +1,2 @@
.record {
}

View File

@ -1,9 +1,150 @@
import {
Button,
DatePicker,
Input,
InputNumber,
message,
Radio,
Select,
Space
} from "antd";
import {useContext, useEffect, useRef, useState} from "react";
import {BillType, EmptyBill} from "../../model";
import moment from "moment/moment";
import {BillContext} from "../../store";
import {observer} from "mobx-react-lite";
import styles from "./Record.module.scss" import styles from "./Record.module.scss"
import {BaseSelectRef} from "rc-select/lib/BaseSelect";
function Record() {
const billStore = useContext(BillContext)
const cls2label = billStore.cls2label
const emptyBill = EmptyBill()
const [billType, setBillType] = useState(emptyBill.type)
const [date, setDate] = useState(emptyBill.date)
const [cls, setCls] = useState(emptyBill.cls)
const [label, setLabel] = useState(emptyBill.label)
const [money, setMoney] = useState("")
const [options, setOptions] = useState(emptyBill.options)
const clsRef = useRef<BaseSelectRef>(null)
useEffect(() => {
if (!!clsRef.current) clsRef.current.focus()
}, [clsRef])
export default function Record() { const typeOpt = [
{label: '支出', value: BillType.consume},
{label: '收入', value: BillType.income},
];
const submit = async () => {
const bill = EmptyBill()
bill.type = billType
bill.date = date
bill.cls = cls
bill.label = label
bill.money = Number(money)
bill.options = options
const checkBill = () => {
return bill.cls !== '' && bill.label !== '' && bill.money !== 0
}
const reset = () => {
setCls("")
setLabel("")
setMoney("")
setOptions("")
if (!!clsRef.current) {
clsRef.current.focus()
}
}
if (checkBill()) {
await billStore.add(bill)
reset()
} else {
message.error("请输入完整")
}
}
return ( return (
<></> <div className={styles.record}>
<div className={styles.new}>
<Space align="start">
<Radio.Group
options={typeOpt}
optionType="button"
buttonStyle="solid"
value={billType}
onChange={e => setBillType(e.target.value)}
/>
<DatePicker
value={moment(date, 'YYYY-MM-DD')}
onChange={(_, dateStr) => setDate(dateStr)}
/>
<Select
ref={clsRef}
style={{width: 120}}
showSearch
placeholder="类别"
optionFilterProp="children"
filterOption={(input, option) =>
(option!.children as unknown as string).toLowerCase().includes(input.toLowerCase())
}
value={cls === "" ? null : cls}
onChange={c => {
setCls(c)
setLabel(cls2label.consume[c][0])
}}
>
{Object.keys(cls2label.consume)
.map(c => <Select.Option key={c} value={c}>{c}</Select.Option>)
}
</Select>
<Select
style={{width: 120}}
showSearch
placeholder="标签"
optionFilterProp="children"
filterOption={(input, option) =>
(option!.children as unknown as string).toLowerCase().includes(input.toLowerCase())
}
value={label === "" ? null : label}
onChange={setLabel}>
{cls !== "" &&
cls2label.consume[cls]
.map(la => <Select.Option key={la} value={la}>{la}</Select.Option>)
}
</Select>
<InputNumber
style={{width: 120}}
placeholder="money"
prefix="¥"
value={money}
onChange={setMoney}
onKeyDown={e => e.key === "Enter" && submit()}
/>
<Input.TextArea
value={options}
onChange={value => setOptions(value.target.value)}
rows={1}
placeholder="备注"
onKeyDown={e => e.key === "Enter" && submit()}
/>
<Button
type="primary"
onKeyUp={e => e.key === "Tab"
&& clsRef.current!.focus()
}
onClick={submit}
></Button>
</Space>
</div>
<div className={styles.table}></div>
</div>
) )
} }
export default observer(Record)

View File

@ -1,7 +1,7 @@
import { makeAutoObservable, runInAction } from "mobx"; import {makeAutoObservable, runInAction} from "mobx";
import { createContext } from "react"; import {createContext} from "react";
import { getBills } from "../api/bills"; import {createBill, getBills, getClass} from "../api/bills";
import { IBill } from "../model"; import {IBill} from "../model";
import * as R from "ramda" import * as R from "ramda"
/** /**
@ -9,9 +9,16 @@ import * as R from "ramda"
*/ */
export class Bill { export class Bill {
bills: IBill[] = []; bills: IBill[] = [];
// _cls2label: IClass = {consume: new Map<string, string[]>(), income: []}
_cls2label: { consume: Record<string, string[]>, income: [] } = {consume: {}, income: []}
constructor() { constructor() {
makeAutoObservable(this) makeAutoObservable(this)
this.fetchClass().then()
}
get cls2label() {
return this._cls2label
} }
get listAllByDate() { get listAllByDate() {
@ -43,8 +50,12 @@ export class Bill {
} }
add(bill: IBill) { async add(bill: IBill) {
const {id} = await createBill(bill)
bill.id = id
runInAction(() => {
this.bills.push(bill); this.bills.push(bill);
})
} }
async fetch(year: number, month: number) { async fetch(year: number, month: number) {
@ -53,6 +64,13 @@ export class Bill {
this.bills = data this.bills = data
}) })
} }
async fetchClass() {
const cls2label = await getClass()
runInAction(() => {
this._cls2label = cls2label
})
}
} }
export const BillContext = createContext<Bill>(new Bill()); export const BillContext = createContext<Bill>(new Bill());