Compare commits

..

No commits in common. "5d3642f5168a4951cc615ccc4cf9ccff4f9d66d9" and "36392b9bde7a29a10b96b92bf8c14dca63846d62" have entirely different histories.

19 changed files with 323 additions and 410 deletions

View File

@ -11,7 +11,6 @@
script script
README.md README.md
.dockerignore
docker-compose.yaml docker-compose.yaml
Dockerfile Dockerfile
node_modules

View File

@ -5,8 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build && pwsh scripts/build.ps1",
"deploy": "tsc && vite build && pwsh scripts/build.ps1",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
@ -33,4 +32,4 @@
"@vitejs/plugin-react": "^2.0.0", "@vitejs/plugin-react": "^2.0.0",
"vite": "^3.0.0" "vite": "^3.0.0"
} }
} }

View File

@ -1,5 +1,8 @@
$server_path='fadinglight:/root/docker/caddy/site/www' docker build . -t registry.cn-hangzhou.aliyuncs.com/fadinglight/bill-react:dev
ssh fadinglight "rm ${server_path}/* -r" docker push registry.cn-hangzhou.aliyuncs.com/fadinglight/bill-react:dev
scp -r dist/* $server_path ssh aliyun "cd /root/docker/bill-sys/;
docker compose down;
docker pull registry.cn-hangzhou.aliyuncs.com/fadinglight/bill-react:dev;
docker compose up -d"

View File

@ -2,7 +2,7 @@ docker build . -t registry.cn-hangzhou.aliyuncs.com/fadinglight/bill-react:dev
docker push registry.cn-hangzhou.aliyuncs.com/fadinglight/bill-react:dev docker push registry.cn-hangzhou.aliyuncs.com/fadinglight/bill-react:dev
ssh fadinglight "cd /root/docker/bill-sys/; ssh aliyun "cd /root/docker/bill-sys/;
docker compose down; docker compose down;
docker pull registry.cn-hangzhou.aliyuncs.com/fadinglight/bill-go:dev; docker pull registry.cn-hangzhou.aliyuncs.com/fadinglight/bill-go:dev;
docker compose up -d" docker compose up -d"

View File

@ -1,23 +1,31 @@
import Layout from './components/layout' import Layout from './components/layout'
import { Routes, Route } from 'react-router-dom' import {Routes, Route} from 'react-router-dom'
import {useEffect} from "react";
import Home from './pages/Home/Home' import Home from './pages/Home/Home'
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"; import Record from "./pages/Record/Record";
function App() { function App() {
const billStore = new Bill() const billStore = new Bill()
const now = new Date()
useEffect(() => {
billStore.fetch(now.getFullYear(), now.getMonth() + 1)
.then()
.catch(console.dir)
}, [])
return ( return (
<div className="App"> <div className="App">
<BillContext.Provider value={billStore}> <BillContext.Provider value={billStore}>
<Layout home> <Layout home>
<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={"/record"} element={<Record/>}/>
<Route path={"/*"} element={<NotFound />} /> <Route path={"/*"} element={<NotFound/>}/>
</Routes> </Routes>
</Layout> </Layout>
</BillContext.Provider> </BillContext.Provider>

View File

@ -2,16 +2,16 @@ import request from "./request";
import {IBill} from "../model"; 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(`/bill/${year}/${month}`) const data = await request.get(`/search/${year}/${month}`)
return data.data return data.data
} }
export async function getLabels() { export async function getClass() {
const data = await request.get(`/label/`) const data = await request.get(`/class`)
return data.data return data.data
} }
export async function postBills(bills: Array<IBill>) { export async function createBill(bill: IBill) {
const data = await request.post(`/bill/`, bills) const data = await request.post(`/create`, bill)
return data.data return data.data
} }

View File

@ -1,7 +1,7 @@
import axios from "axios"; import axios from "axios";
const request = axios.create({ const request = axios.create({
baseURL: "/api/", baseURL: "/api",
timeout: 30000, timeout: 30000,
}) })

View File

@ -17,7 +17,6 @@ interface IProps {
data: BarData[]; data: BarData[];
title?: string; title?: string;
subTitle?: string; subTitle?: string;
onClickItem?: (date: string) => void;
} }
export const Bar = (props: IProps) => { export const Bar = (props: IProps) => {
@ -38,23 +37,6 @@ export const Bar = (props: IProps) => {
subtext: subTitle ?? "", subtext: subTitle ?? "",
left: 'center' left: 'center'
}, },
tooltip: {
show: true,
trigger: "item",
triggerOn: "mousemove|click",
axisPointer: {
type: "line"
},
showContent: true,
alwaysShowContent: false,
showDelay: 0,
hideDelay: 100,
textStyle: {
fontSize: 14
},
borderWidth: 0,
padding: 5
},
xAxis: { xAxis: {
type: 'category', type: 'category',
data: data.map(item => { data: data.map(item => {
@ -71,7 +53,7 @@ export const Bar = (props: IProps) => {
show: true, show: true,
position: "top", position: "top",
margin: 8 margin: 8
}, },
data: data.map(item => { data: data.map(item => {
return { return {
value: item.y, value: item.y,
@ -82,16 +64,29 @@ export const Bar = (props: IProps) => {
}), }),
}, },
], ],
tooltip: {
show: true,
trigger: "item",
triggerOn: "mousemove|click",
axisPointer: {
type: "line"
},
showContent: true,
alwaysShowContent: false,
showDelay: 0,
hideDelay: 100,
textStyle: {
fontSize: 14
},
borderWidth: 0,
padding: 5
}
}; };
}, [data, title, subTitle]) }, [data, title, subTitle])
const bar = useMemo(() => { const bar = useMemo(() => {
const chart = chartRef.current ? echarts.init(chartRef.current, undefined, { renderer: "svg" }) : null; return chartRef.current ? echarts.init(chartRef.current, undefined, { renderer: "svg" }) : null;
chart?.on('click', (params) => {
const date = dayjs(props.data[0].x).format("YYYY-") + params.name
if (props.onClickItem) props.onClickItem(date)
})
return chart
}, [chartRef.current]) }, [chartRef.current])
useEffect(() => { useEffect(() => {

View File

@ -14,8 +14,6 @@ import {
TitleComponentOption, TitleComponentOption,
TooltipComponent, TooltipComponent,
TooltipComponentOption, TooltipComponentOption,
LegendComponent,
LegendComponentOption,
GridComponent, GridComponent,
GridComponentOption, GridComponentOption,
// 数据集组件 // 数据集组件
@ -36,7 +34,6 @@ export type ECOption = echarts.ComposeOption<
| PieSeriesOption | PieSeriesOption
| TitleComponentOption | TitleComponentOption
| TooltipComponentOption | TooltipComponentOption
| LegendComponentOption
| GridComponentOption | GridComponentOption
| DatasetComponentOption>; | DatasetComponentOption>;
@ -44,7 +41,6 @@ export type ECOption = echarts.ComposeOption<
echarts.use([ echarts.use([
TitleComponent, TitleComponent,
TooltipComponent, TooltipComponent,
LegendComponent,
GridComponent, GridComponent,
DatasetComponent, DatasetComponent,
TransformComponent, TransformComponent,

View File

@ -1,4 +1,4 @@
.pie { .pie {
width: 400px; width: 100%;
height: 400px; height: 100%;
} }

View File

@ -33,15 +33,15 @@ export default function Pie(props: IProps) {
}, },
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
formatter: '{b} : ¥{c} ({d}%)', formatter: '{a} <br/>{b} : {c} ({d}%)',
},
legend: {
orient: 'vertical',
left: 'left'
}, },
// legend: {
// orient: 'vertical',
// left: 'left',
// },
series: [ series: [
{ {
// name: "金额", name: "金额",
type: "pie", type: "pie",
radius: ['40%', '70%'], radius: ['40%', '70%'],
// roseType: "radius", // roseType: "radius",
@ -49,17 +49,17 @@ export default function Pie(props: IProps) {
borderRadius: 10, borderRadius: 10,
borderColor: '#fff', borderColor: '#fff',
borderWidth: 2, borderWidth: 2,
}, },
label: { label: {
show: false, show: true,
// position: "top", // position: "top",
margin: 8, margin: 8,
formatter: "{b}: {c}" formatter: "{b}: {c}"
}, },
emphasis: { emphasis: {
label: { label: {
show: false, show: true
} }
}, },
data: data.map(item => { data: data.map(item => {

View File

@ -6,9 +6,9 @@ import 'antd/dist/antd.css';
import './index.css' import './index.css'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
// <React.StrictMode> <React.StrictMode>
<BrowserRouter> <BrowserRouter>
<App /> <App />
</BrowserRouter> </BrowserRouter>
// </React.StrictMode> </React.StrictMode>
) )

View File

@ -1,8 +1,8 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
export enum BillType { export enum BillType {
CONSUME = 0, consume = 0,
INCOME, income,
} }
export interface IBill { export interface IBill {
@ -21,7 +21,7 @@ export function EmptyBill(): IBill {
return { return {
date: dayjs().format("YYYY-MM-DD"), date: dayjs().format("YYYY-MM-DD"),
money: 0, money: 0,
type: BillType.CONSUME, type: BillType.consume,
cls: "", cls: "",
label: "", label: "",
options: "", options: "",

View File

@ -1,21 +1,12 @@
.home { .home {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.total { .total {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
gap: 10px; gap: 10px;
align-items: flex-start; align-items: flex-start;
} }
}
.monthBar {
height: 300px;
}
.cards {
display: grid;
grid: repeat(2, auto) / auto-flow auto;
}
}

View File

@ -1,131 +1,83 @@
import styles from "./Home.module.scss" import styles from "./Home.module.scss"
import * as R from 'ramda' import * as R from 'ramda'
import { BillType, IBill } from "../../model" import { BillType, 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, Modal, DatePicker, Radio, Space } from "antd"; import { Card, DatePicker, Radio, Space } from "antd";
import moment from 'moment'; import moment from 'moment';
import 'moment/locale/zh-cn'; import 'moment/locale/zh-cn';
import dayjs from 'dayjs' import dayjs from 'dayjs'
const Home = () => { const Home = () => {
const billStore = useContext(BillContext) const billStore = useContext(BillContext)
const transformer = (record: Record<string, IBill[]>) => { const transformer = (record: Record<string, IBill[]>) => {
const funcs = R.compose( const funcs = R.compose(
R.sort((a: { x: string, y: number }, b) => { R.sort((a: { x: string, y: number }, b) => {
const date1 = dayjs(a.x).toDate().getTime() const date1 = dayjs(a.x).toDate().getTime()
const date2 = dayjs(b.x).toDate().getTime() const date2 = dayjs(b.x).toDate().getTime()
return date1 - date2 return date1 - date2
}), }),
R.map((key: string) => { R.map((key: string) => {
const moneys = record[key].map(bill => bill.money) const moneys = record[key].map(bill => bill.money)
return { return {
x: key, x: key,
y: Number(R.sum(moneys).toFixed(2)), y: Number(R.sum(moneys).toFixed(2)),
} }
})) }))
return funcs(R.keys(record)) return funcs(R.keys(record))
} }
const now = new Date(); const now = new Date();
const [year, setYear] = useState(now.getFullYear()) const [year, setYear] = useState(now.getFullYear())
const [month, setMonth] = useState(now.getMonth() + 1) const [month, setMonth] = useState(now.getMonth() + 1)
useEffect(() => { useEffect(() => {
billStore.fetch(year, month).then() 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) => {
const d = date?.toDate() ?? new Date() const d = date?.toDate() ?? new Date()
setYear(d.getFullYear()) setYear(d.getFullYear())
setMonth(d.getMonth() + 1) setMonth(d.getMonth() + 1)
} }
const typeOpt = [ const typeOpt = [
{ label: '支出', value: BillType.CONSUME }, { label: '支出', value: BillType.consume },
{ label: '收入', value: BillType.INCOME }, { label: '收入', value: BillType.income },
]; ];
const [billType, setBillType] = useState(BillType.CONSUME) const [billType, setBillType] = useState(BillType.consume)
// 点击bar弹出当天pie return (
const [isModalOpen, setIsModalOpen] = useState(false); <div className={styles.home}>
const [modalTitle, setModalTitle] = useState(""); <div className={styles.total}>
const [modalData, setModalData] = useState<{ <Space align="start">
x: string <DatePicker
y: number picker="month"
}[]>([]); value={moment(`${year}-${month}`, 'YYYY-MM')}
onChange={changeDate}
// 显示单个cls的饼状图查看cls内部的label的消费情况 />
// 这里有一个cls列表 <Radio.Group
const clsesForShow = ["餐饮", "恋爱"] options={typeOpt}
optionType="button"
return ( buttonStyle="solid"
<div className={styles.home}> value={billType}
<div className={styles.total}> onChange={e => setBillType(e.target.value)}
<Space align="start"> />
<DatePicker <Card>
picker="month" {"总金额"}
value={moment(`${year}-${month}`, 'YYYY-MM')} {billStore.getTotalMoney(billType)}
onChange={changeDate} </Card>
/> </Space>
<Radio.Group </div>
options={typeOpt} <Bar data={transformer(billStore.groupByDate(billType))} />
optionType="button" <Pie data={transformer(billStore.groupByClass(billType))} />
buttonStyle="solid" </div>
value={billType} )
onChange={e => setBillType(e.target.value)} }
/>
<Card>
{"总金额"}
{billStore.getTotalMoney(billType)}
</Card>
</Space>
</div>
<div className={styles.monthBar}>
<Bar
data={transformer(billStore.groupByDate(billType))}
onClickItem={date => {
setIsModalOpen(true)
setModalTitle(date)
setModalData(transformer(billStore.groupByClass(billType, date)))
}}
/>
</div>
<div className={styles.cards}>
<Card >
<Pie
title="本月消费分类"
data={transformer(billStore.groupByClass(billType))}
/>
</Card>
{
clsesForShow.map(cls => {
return (<Card key={cls.toString()}>
<Pie
title={cls}
data={transformer(billStore.groupByLabelOfClass(cls))}
/>
</Card>)
})
}
</div>
<Modal
visible={isModalOpen}
onOk={() => setIsModalOpen(false)}
onCancel={() => setIsModalOpen(false)}
title={modalTitle}
>
<Pie
data={modalData}
/>
</Modal>
</div>
)
}
export default observer(Home) export default observer(Home)

View File

@ -1,13 +1,13 @@
import { Button, DatePicker, Input, InputNumber, message, Radio, Select, Space, Table, Tag } from "antd"; import {Button, DatePicker, Input, InputNumber, message, Radio, Select, Space, Table, Tag} from "antd";
import { ArrowDownOutlined, CloudUploadOutlined, DeleteOutlined, } from "@ant-design/icons"; import {ArrowDownOutlined, CloudUploadOutlined, DeleteOutlined,} from "@ant-design/icons";
import { useContext, useEffect, useRef, useState } from "react"; import {useContext, useEffect, useRef, useState} from "react";
import { BillType, EmptyBill, IBill } from "../../model"; import {BillType, EmptyBill, IBill} from "../../model";
import moment from "moment/moment"; import moment from "moment/moment";
import { BillContext } from "../../store"; import {BillContext} from "../../store";
import { observer } from "mobx-react-lite"; 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"; import {BaseSelectRef} from "rc-select/lib/BaseSelect";
import { postBills } from "../../api/bills"; import {createBill} from "../../api/bills";
function Record() { function Record() {
@ -28,9 +28,6 @@ function Record() {
if (!!clsRef.current) clsRef.current.focus() if (!!clsRef.current) clsRef.current.focus()
}, [clsRef]) }, [clsRef])
useEffect(() => {
billStore.fetchLabels().then()
}, [])
// table // table
const columns = [ const columns = [
@ -53,7 +50,7 @@ function Record() {
title: "金额", title: "金额",
key: "money", key: "money",
render: (_: any, record: IBill) => { render: (_: any, record: IBill) => {
const isConsume = record.type === BillType.CONSUME const isConsume = record.type === BillType.consume
const color = isConsume ? "red" : "green" const color = isConsume ? "red" : "green"
const flag = isConsume ? "-" : "+" const flag = isConsume ? "-" : "+"
return <Tag color={color}>{flag}{record.money}</Tag> return <Tag color={color}>{flag}{record.money}</Tag>
@ -72,7 +69,7 @@ function Record() {
<Button <Button
type="primary" type="primary"
danger danger
icon={<DeleteOutlined />} icon={<DeleteOutlined/>}
onClick={() => setDataSource(datasource.filter(bill => bill !== record))} onClick={() => setDataSource(datasource.filter(bill => bill !== record))}
/> />
</Space> </Space>
@ -82,8 +79,8 @@ function Record() {
const [datasource, setDataSource] = useState<IBill[]>([]) const [datasource, setDataSource] = useState<IBill[]>([])
const typeOpt = [ const typeOpt = [
{ label: '支出', value: BillType.CONSUME }, {label: '支出', value: BillType.consume},
{ label: '收入', value: BillType.INCOME }, {label: '收入', value: BillType.income},
]; ];
// 提交到表格 // 提交到表格
@ -96,7 +93,7 @@ function Record() {
bill.money = Number(money) bill.money = Number(money)
bill.options = options bill.options = options
const checkBill = () => { const checkBill = () => {
return bill.cls !== '' && (billType === BillType.INCOME || bill.label !== '') && bill.money > 0 return bill.cls !== '' && (billType === BillType.income || bill.label !== '') && bill.money > 0
} }
const reset = () => { const reset = () => {
setCls("") setCls("")
@ -120,22 +117,26 @@ function Record() {
const [uploadLoading, setUploadLoading] = useState(false) const [uploadLoading, setUploadLoading] = useState(false)
const upload = async () => { const upload = async () => {
setUploadLoading(true) setUploadLoading(true)
datasource.forEach(it => Reflect.deleteProperty(it, "key")) const failures = []
try { for (let bill of datasource) {
await postBills(datasource) try {
setDataSource([]) const {id} = await createBill(bill)
} catch (err) { if (!id) failures.push(bill)
} catch (e) {
failures.push(bill)
}
} }
setDataSource(failures)
setUploadLoading(false) setUploadLoading(false)
} }
let classData: string[] = [] let classData: string[]
switch (billType) { switch (billType) {
case BillType.CONSUME: case BillType.consume:
classData = cls2label.consume.map(it => it.name) classData = Object.keys(cls2label.consume)
break break
case BillType.INCOME: case BillType.income:
classData = cls2label.income.map(it => it.name) classData = cls2label.income
break break
} }
@ -144,7 +145,7 @@ function Record() {
<div className={styles.new}> <div className={styles.new}>
<Space align="start"> <Space align="start">
<Radio.Group <Radio.Group
style={{ width: 120 }} style={{width: 120}}
options={typeOpt} options={typeOpt}
optionType="button" optionType="button"
buttonStyle="solid" buttonStyle="solid"
@ -152,14 +153,14 @@ function Record() {
onChange={e => setBillType(e.target.value)} onChange={e => setBillType(e.target.value)}
/> />
<DatePicker <DatePicker
style={{ width: 120 }} style={{width: 120}}
allowClear={false} allowClear={false}
value={moment(date, 'YYYY-MM-DD')} value={moment(date, 'YYYY-MM-DD')}
onChange={(_, dateStr) => setDate(dateStr)} onChange={(_, dateStr) => setDate(dateStr)}
/> />
<Select <Select
ref={clsRef} ref={clsRef}
style={{ width: 120 }} style={{width: 120}}
// showSearch // showSearch
placeholder="类别" placeholder="类别"
optionFilterProp="children" optionFilterProp="children"
@ -169,16 +170,16 @@ function Record() {
value={cls === "" ? null : cls} value={cls === "" ? null : cls}
onChange={c => { onChange={c => {
setCls(c) setCls(c)
// if (billType === BillType.CONSUME) setLabel(cls2label.consume[c][0]) if (billType === BillType.consume) setLabel(cls2label.consume[c][0])
}} }}
> >
{ {
classData.map(c => <Select.Option key={c} value={c}>{c}</Select.Option>) classData.map(c => <Select.Option key={c} value={c}>{c}</Select.Option>)
} }
</Select> </Select>
{billType === BillType.CONSUME && ( {billType === BillType.consume && (
<Select <Select
style={{ width: 120 }} style={{width: 120}}
// showSearch // showSearch
placeholder="标签" placeholder="标签"
optionFilterProp="children" optionFilterProp="children"
@ -188,15 +189,14 @@ function Record() {
value={label === "" ? null : label} value={label === "" ? null : label}
onChange={setLabel}> onChange={setLabel}>
{cls !== "" && {cls !== "" &&
cls2label.consume cls2label.consume[cls]
.find(it => it.name === cls)?.labels
.map(la => <Select.Option key={la} value={la}>{la}</Select.Option>) .map(la => <Select.Option key={la} value={la}>{la}</Select.Option>)
} }
<Select.Option key={"other"} value={"其他"}>{"其他"}</Select.Option>) <Select.Option key={"other"} value={"其他"}>{"其他"}</Select.Option>)
</Select> </Select>
)} )}
<InputNumber <InputNumber
style={{ width: 120 }} style={{width: 120}}
placeholder="money" placeholder="money"
prefix="¥" prefix="¥"
value={money} value={money}
@ -204,7 +204,7 @@ function Record() {
onKeyDown={e => e.key === "Enter" && submit()} onKeyDown={e => e.key === "Enter" && submit()}
/> />
<Input.TextArea <Input.TextArea
style={{ width: 180 }} style={{width: 180}}
rows={1} rows={1}
placeholder="备注" placeholder="备注"
value={options} value={options}
@ -213,7 +213,7 @@ function Record() {
/> />
<Button <Button
type="primary" type="primary"
icon={<ArrowDownOutlined />} icon={<ArrowDownOutlined/>}
onKeyUp={e => e.key === "Tab" && clsRef.current!.focus()} onKeyUp={e => e.key === "Tab" && clsRef.current!.focus()}
onClick={submit} onClick={submit}
> >
@ -228,7 +228,7 @@ function Record() {
size="small" size="small"
/> />
<Button <Button
icon={<CloudUploadOutlined />} icon={<CloudUploadOutlined/>}
type="primary" type="primary"
loading={uploadLoading} loading={uploadLoading}
onClick={upload} onClick={upload}

View File

@ -1,144 +1,123 @@
import { makeAutoObservable, runInAction } from "mobx"; import { makeAutoObservable, runInAction } from "mobx";
import { createContext } from "react"; import { createContext } from "react";
import { postBills, getBills, getLabels } from "../api/bills"; import { createBill, getBills, getClass } from "../api/bills";
import { BillType, IBill } from "../model"; import { BillType, IBill } from "../model";
import * as R from "ramda" import * as R from "ramda"
import { BillLabel } from "./types";
/**
*
/** */
* export class Bill {
*/ private _bills: IBill[] = [];
export class Bill { // _cls2label: IClass = {consume: new Map<string, string[]>(), income: []}
private _bills: IBill[] = []; private _cls2label: { consume: Record<string, string[]>, income: [] } = { consume: {}, income: [] }
// _cls2label: IClass = {consume: new Map<string, string[]>(), income: []}
private _cls2label: BillLabel = { consume: [], income: [] } constructor() {
makeAutoObservable(this)
constructor() { this.fetchClass().then()
makeAutoObservable(this) }
}
get bills() {
get bills() { return this._bills
return this._bills }
}
get cls2label() {
get cls2label() { return this._cls2label
return this._cls2label }
}
groupByDate(type?: BillType) {
groupByDate(type?: BillType) { const classFun = R.filter((bill: IBill) => R.of(bill.type).length === 0 || bill.type === type)
const classFun = R.filter((bill: IBill) => R.of(bill.type).length === 0 || bill.type === type) const functions = R.compose(
const functions = R.compose( R.groupBy((bill: IBill) => bill.date),
R.groupBy((bill: IBill) => bill.date), classFun,
classFun, )
) return functions(this._bills)
return functions(this._bills) }
}
groupByClass(type?: BillType) {
groupByClass(type?: BillType, date?: string) { const classFun = R.filter((bill: IBill) => R.of(bill.type).length === 0 || bill.type === type)
const classFun = R.filter((bill: IBill) => R.of(bill.type).length === 0 || bill.type === type && (date ? bill.date === date : true)) const functions = R.compose(
const functions = R.compose( R.groupBy((bill: IBill) => bill.cls),
R.groupBy((bill: IBill) => bill.cls), classFun,
classFun, )
) return functions(this._bills)
return functions(this._bills) }
}
get listDailyMoney() {
groupByLabelOfClass(className: string) { return this.groupByDate
const classFun = R.filter((bill: IBill) => R.of(bill.type).length === 0 || bill.cls === className) }
const functions = R.compose(
R.groupBy((bill: IBill) => bill.label), get totalMoney() {
classFun, const functions = R.compose(
) Number,
return functions(this._bills) (s: number) => s.toFixed(2),
} R.sum,
R.map((bill: IBill) => bill.money)
)
get listDailyMoney() { return functions(this._bills)
return this.groupByDate }
}
get consumeMoney() {
get totalMoney() { const functions = R.compose(
const functions = R.compose( Number,
Number, (s: number) => s.toFixed(2),
(s: number) => s.toFixed(2), R.sum,
R.sum, R.map((bill: IBill) => bill.money),
R.map((bill: IBill) => bill.money) R.filter((bill: IBill) => bill.type === BillType.consume),
) )
return functions(this._bills) return functions(this._bills)
} }
get consumeMoney() { get incomeMoney() {
const functions = R.compose( const functions = R.compose(
Number, Number,
(s: number) => s.toFixed(2), (s: number) => s.toFixed(2),
R.sum, R.sum,
R.map((bill: IBill) => bill.money), R.map((bill: IBill) => bill.money),
R.filter((bill: IBill) => bill.type === BillType.CONSUME), R.filter((bill: IBill) => bill.type === BillType.income),
) )
return functions(this._bills) return functions(this._bills)
} }
get incomeMoney() { getTotalMoney(type?: BillType) {
const functions = R.compose( switch (type) {
Number, case BillType.income:
(s: number) => s.toFixed(2), return this.incomeMoney
R.sum, case BillType.consume:
R.map((bill: IBill) => bill.money), return this.consumeMoney
R.filter((bill: IBill) => bill.type === BillType.INCOME), default:
) return this.totalMoney
return functions(this._bills) }
} }
getTotalMoney(type?: BillType) { get meanMoneyByDate() {
switch (type) { const days = Reflect.ownKeys(this.groupByDate).length
case BillType.INCOME: if (days === 0) return 0
return this.incomeMoney return this.totalMoney / days
case BillType.CONSUME: }
return this.consumeMoney
default:
return this.totalMoney async add(bill: IBill) {
} const { id } = await createBill(bill)
} bill.id = id
runInAction(() => {
get meanMoneyByDate() { this._bills.push(bill);
const days = Reflect.ownKeys(this.groupByDate).length })
if (days === 0) return 0 }
return this.totalMoney / days
} async fetch(year: number, month: number) {
const data = await getBills(year, month)
runInAction(() => {
async add(bill: IBill) { this._bills = data
const { id } = await postBills([bill]) })
bill.id = id }
runInAction(() => {
this._bills.push(bill); async fetchClass() {
}) const cls2label = await getClass()
} runInAction(() => {
this._cls2label = cls2label
async fetch(year: number, month: number) { })
const data = await getBills(year, month) }
runInAction(() => { }
this._bills = data.map((data: any) => {
return { export const BillContext = createContext<Bill>(new Bill());
id: data.id,
type: data.type.toUpperCase === 'INCOME' ? BillType.INCOME : BillType.CONSUME,
date: data.date,
money: data.money,
cls: data.cls,
label: data.label,
options: data.options
}
})
})
}
async fetchLabels() {
const cls2label = await getLabels()
runInAction(() => {
this._cls2label = cls2label
})
}
}
export const BillContext = createContext<Bill>(new Bill());

View File

@ -1,9 +0,0 @@
export type BillLabel = {
consume: Array<BillLabelOption>,
income: Array<BillLabelOption>,
}
type BillLabelOption = {
name: string,
labels: Array<string>,
}

View File

@ -7,7 +7,7 @@ export default defineConfig({
server: { server: {
proxy: { proxy: {
"/api/": { "/api/": {
target: "https://bill.fadinglight.cn/api/", target: "http://www.fadinglight.cn:8080/",
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""), rewrite: (path) => path.replace(/^\/api/, ""),
} }