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 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/>}/>
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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[],
|
||||||
|
}
|
|
@ -6,10 +6,9 @@ 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) => {
|
||||||
|
|
|
@ -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 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)
|
|
@ -1,6 +1,6 @@
|
||||||
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());
|
||||||
|
|
Loading…
Reference in New Issue
Block a user