feat🔫: 记账功能
This commit is contained in:
parent
3c65054da3
commit
c77a6366a7
|
@ -8,6 +8,7 @@ import {MdEditor} from './components/editor'
|
|||
import NotFound from './pages/NotFound'
|
||||
import {Bill, BillContext} from "./store";
|
||||
import './App.css'
|
||||
import Record from "./pages/Record/Record";
|
||||
|
||||
function App() {
|
||||
const billStore = new Bill()
|
||||
|
@ -26,6 +27,7 @@ function App() {
|
|||
<Routes>
|
||||
<Route path={"/"} element={<Home/>}/>
|
||||
<Route path={"/home"} element={<Home/>}/>
|
||||
<Route path={"/record"} element={<Record/>}/>
|
||||
<Route path={"/chat"} element={<Chat/>}/>
|
||||
<Route path={"/editor"} element={<MdEditor/>}/>
|
||||
<Route path={'/login'} element={<Login/>}/>
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
import request from "./request";
|
||||
import {IBill} from "../model";
|
||||
|
||||
export async function getBills(year: number, month: number) {
|
||||
const data = await request.get(`/search/${year}/${month}`)
|
||||
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
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
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 {
|
||||
Menu,
|
||||
|
@ -7,7 +7,7 @@ import {
|
|||
Layout as AntLayout
|
||||
} from 'antd';
|
||||
import {
|
||||
EditOutlined,
|
||||
EditOutlined, FormOutlined,
|
||||
HomeOutlined,
|
||||
LoginOutlined,
|
||||
WechatOutlined
|
||||
|
@ -21,17 +21,18 @@ interface IProps {
|
|||
export default function Layout(props: IProps) {
|
||||
const {children, home} = props
|
||||
|
||||
const {Header, Content, Footer, Sider} = AntLayout;
|
||||
|
||||
const items = [
|
||||
{label: <NavLink to={"/home"}>Home</NavLink>, key: 'home', icon: <HomeOutlined/>},
|
||||
{label: <NavLink to={"/login"}>Login</NavLink>, key: 'login', icon: <LoginOutlined/>},
|
||||
{label: <NavLink to={"/chat"}>Chat</NavLink>, key: 'chat', icon: <WechatOutlined/>},
|
||||
{label: <NavLink to={"/editor"}>Editor</NavLink>, key: 'editor', icon: <EditOutlined/>},
|
||||
{label: <NavLink to={"/home"}>Home</NavLink>, key: '/home', icon: <HomeOutlined/>},
|
||||
{label: <NavLink to={"/record"}>Record</NavLink>, key: '/record', icon: <FormOutlined/>},
|
||||
{label: <NavLink to={"/login"}>Login</NavLink>, key: '/login', icon: <LoginOutlined/>},
|
||||
{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 location = useLocation()
|
||||
|
||||
const siderTitleCSS: React.CSSProperties = {
|
||||
color: "white",
|
||||
fontSize: "30px",
|
||||
|
@ -39,7 +40,7 @@ export default function Layout(props: IProps) {
|
|||
}
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
return (
|
||||
<Sider
|
||||
<AntLayout.Sider
|
||||
collapsible
|
||||
theme={sprops.theme}
|
||||
collapsed={collapsed}
|
||||
|
@ -50,10 +51,9 @@ export default function Layout(props: IProps) {
|
|||
items={items}
|
||||
theme={sprops.theme}
|
||||
mode="inline"
|
||||
defaultSelectedKeys={["home"]}
|
||||
// style={{ width: 256 }}
|
||||
defaultSelectedKeys={[location.pathname]}
|
||||
/>
|
||||
</Sider>
|
||||
</AntLayout.Sider>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,32 @@
|
|||
export enum BillType {
|
||||
consume = 0,
|
||||
income,
|
||||
}
|
||||
|
||||
export interface IBill {
|
||||
_id?: string,
|
||||
id?: string,
|
||||
type: BillType
|
||||
date: string,
|
||||
money: number,
|
||||
cls: string,
|
||||
label: 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[],
|
||||
}
|
|
@ -1,15 +1,14 @@
|
|||
import styles from "./Home.module.scss"
|
||||
import * as R from 'ramda'
|
||||
import { IBill } from "../../model"
|
||||
import {IBill} from "../../model"
|
||||
import Bar from "../../components/charts/bar"
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { BillContext } from "../../store";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import {useContext, useEffect, useState} from "react";
|
||||
import {BillContext} from "../../store";
|
||||
import {observer} from "mobx-react-lite";
|
||||
import Pie from "../../components/charts/pie";
|
||||
import { Card, ConfigProvider, DatePicker } from "antd";
|
||||
import {Card, DatePicker} from "antd";
|
||||
import moment from 'moment';
|
||||
import 'moment/locale/zh-cn';
|
||||
import locale from 'antd/es/locale/zh_CN';
|
||||
|
||||
const Home = () => {
|
||||
const billStore = useContext(BillContext)
|
||||
|
@ -29,7 +28,7 @@ const Home = () => {
|
|||
const [month, setMonth] = useState(now.getMonth() + 1)
|
||||
|
||||
useEffect(() => {
|
||||
billStore.fetch(year, month)
|
||||
billStore.fetch(year, month).then()
|
||||
}, [year, month])
|
||||
|
||||
const changeDate = (date: moment.Moment | null, datestring: string) => {
|
||||
|
@ -51,11 +50,11 @@ const Home = () => {
|
|||
value={moment(`${year}-${month}`, 'YYYY-MM')}
|
||||
onChange={changeDate}
|
||||
/>
|
||||
<TotalMoney />
|
||||
<TotalMoney/>
|
||||
</div>
|
||||
<Bar data={transformer(billStore.listAllByDate)} />
|
||||
<Pie data={transformer(billStore.listAllByClass)} />
|
||||
</div >
|
||||
<Bar data={transformer(billStore.listAllByDate)}/>
|
||||
<Pie data={transformer(billStore.listAllByClass)}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
.record {
|
||||
}
|
|
@ -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 {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 (
|
||||
<></>
|
||||
<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)
|
|
@ -1,7 +1,7 @@
|
|||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
import { createContext } from "react";
|
||||
import { getBills } from "../api/bills";
|
||||
import { IBill } from "../model";
|
||||
import {makeAutoObservable, runInAction} from "mobx";
|
||||
import {createContext} from "react";
|
||||
import {createBill, getBills, getClass} from "../api/bills";
|
||||
import {IBill} from "../model";
|
||||
import * as R from "ramda"
|
||||
|
||||
/**
|
||||
|
@ -9,9 +9,16 @@ import * as R from "ramda"
|
|||
*/
|
||||
export class Bill {
|
||||
bills: IBill[] = [];
|
||||
// _cls2label: IClass = {consume: new Map<string, string[]>(), income: []}
|
||||
_cls2label: { consume: Record<string, string[]>, income: [] } = {consume: {}, income: []}
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
this.fetchClass().then()
|
||||
}
|
||||
|
||||
get cls2label() {
|
||||
return this._cls2label
|
||||
}
|
||||
|
||||
get listAllByDate() {
|
||||
|
@ -43,8 +50,12 @@ export class Bill {
|
|||
}
|
||||
|
||||
|
||||
add(bill: IBill) {
|
||||
this.bills.push(bill);
|
||||
async add(bill: IBill) {
|
||||
const {id} = await createBill(bill)
|
||||
bill.id = id
|
||||
runInAction(() => {
|
||||
this.bills.push(bill);
|
||||
})
|
||||
}
|
||||
|
||||
async fetch(year: number, month: number) {
|
||||
|
@ -53,6 +64,13 @@ export class Bill {
|
|||
this.bills = data
|
||||
})
|
||||
}
|
||||
|
||||
async fetchClass() {
|
||||
const cls2label = await getClass()
|
||||
runInAction(() => {
|
||||
this._cls2label = cls2label
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const BillContext = createContext<Bill>(new Bill());
|
||||
|
|
Loading…
Reference in New Issue
Block a user