Compare commits
No commits in common. "5d3642f5168a4951cc615ccc4cf9ccff4f9d66d9" and "36392b9bde7a29a10b96b92bf8c14dca63846d62" have entirely different histories.
5d3642f516
...
36392b9bde
|
@ -11,7 +11,6 @@
|
||||||
script
|
script
|
||||||
|
|
||||||
README.md
|
README.md
|
||||||
.dockerignore
|
|
||||||
docker-compose.yaml
|
docker-compose.yaml
|
||||||
Dockerfile
|
Dockerfile
|
||||||
node_modules
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
20
src/App.tsx
20
src/App.tsx
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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 => {
|
||||||
|
@ -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(() => {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.pie {
|
.pie {
|
||||||
width: 400px;
|
width: 100%;
|
||||||
height: 400px;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
@ -52,14 +52,14 @@ export default function Pie(props: IProps) {
|
||||||
|
|
||||||
},
|
},
|
||||||
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 => {
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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: "",
|
||||||
|
|
|
@ -9,13 +9,4 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ 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'
|
||||||
|
@ -47,22 +47,10 @@ const Home = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [modalTitle, setModalTitle] = useState("");
|
|
||||||
const [modalData, setModalData] = useState<{
|
|
||||||
x: string
|
|
||||||
y: number
|
|
||||||
}[]>([]);
|
|
||||||
|
|
||||||
// 显示单个cls的饼状图,查看cls内部的label的消费情况,
|
|
||||||
// 这里有一个cls列表
|
|
||||||
const clsesForShow = ["餐饮", "恋爱"]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.home}>
|
<div className={styles.home}>
|
||||||
|
@ -86,44 +74,8 @@ const Home = () => {
|
||||||
</Card>
|
</Card>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.monthBar}>
|
<Bar data={transformer(billStore.groupByDate(billType))} />
|
||||||
<Bar
|
<Pie data={transformer(billStore.groupByClass(billType))} />
|
||||||
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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = []
|
||||||
|
for (let bill of datasource) {
|
||||||
try {
|
try {
|
||||||
await postBills(datasource)
|
const {id} = await createBill(bill)
|
||||||
setDataSource([])
|
if (!id) failures.push(bill)
|
||||||
} catch (err) {
|
} 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}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
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";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仅存储一个月的数据
|
* 仅存储一个月的数据
|
||||||
|
@ -12,10 +10,11 @@ import { BillLabel } from "./types";
|
||||||
export class Bill {
|
export class Bill {
|
||||||
private _bills: IBill[] = [];
|
private _bills: IBill[] = [];
|
||||||
// _cls2label: IClass = {consume: new Map<string, string[]>(), income: []}
|
// _cls2label: IClass = {consume: new Map<string, string[]>(), income: []}
|
||||||
private _cls2label: BillLabel = { consume: [], income: [] }
|
private _cls2label: { consume: Record<string, string[]>, income: [] } = { consume: {}, income: [] }
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this)
|
makeAutoObservable(this)
|
||||||
|
this.fetchClass().then()
|
||||||
}
|
}
|
||||||
|
|
||||||
get bills() {
|
get bills() {
|
||||||
|
@ -35,8 +34,8 @@ export class Bill {
|
||||||
return functions(this._bills)
|
return functions(this._bills)
|
||||||
}
|
}
|
||||||
|
|
||||||
groupByClass(type?: BillType, date?: string) {
|
groupByClass(type?: BillType) {
|
||||||
const classFun = R.filter((bill: IBill) => R.of(bill.type).length === 0 || bill.type === type && (date ? bill.date === date : true))
|
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.cls),
|
R.groupBy((bill: IBill) => bill.cls),
|
||||||
classFun,
|
classFun,
|
||||||
|
@ -44,16 +43,6 @@ export class Bill {
|
||||||
return functions(this._bills)
|
return functions(this._bills)
|
||||||
}
|
}
|
||||||
|
|
||||||
groupByLabelOfClass(className: string) {
|
|
||||||
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),
|
|
||||||
classFun,
|
|
||||||
)
|
|
||||||
return functions(this._bills)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
get listDailyMoney() {
|
get listDailyMoney() {
|
||||||
return this.groupByDate
|
return this.groupByDate
|
||||||
}
|
}
|
||||||
|
@ -74,7 +63,7 @@ export class Bill {
|
||||||
(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.consume),
|
||||||
)
|
)
|
||||||
return functions(this._bills)
|
return functions(this._bills)
|
||||||
}
|
}
|
||||||
|
@ -85,16 +74,16 @@ export class Bill {
|
||||||
(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.INCOME),
|
R.filter((bill: IBill) => bill.type === BillType.income),
|
||||||
)
|
)
|
||||||
return functions(this._bills)
|
return functions(this._bills)
|
||||||
}
|
}
|
||||||
|
|
||||||
getTotalMoney(type?: BillType) {
|
getTotalMoney(type?: BillType) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case BillType.INCOME:
|
case BillType.income:
|
||||||
return this.incomeMoney
|
return this.incomeMoney
|
||||||
case BillType.CONSUME:
|
case BillType.consume:
|
||||||
return this.consumeMoney
|
return this.consumeMoney
|
||||||
default:
|
default:
|
||||||
return this.totalMoney
|
return this.totalMoney
|
||||||
|
@ -109,7 +98,7 @@ export class Bill {
|
||||||
|
|
||||||
|
|
||||||
async add(bill: IBill) {
|
async add(bill: IBill) {
|
||||||
const { id } = await postBills([bill])
|
const { id } = await createBill(bill)
|
||||||
bill.id = id
|
bill.id = id
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this._bills.push(bill);
|
this._bills.push(bill);
|
||||||
|
@ -119,22 +108,12 @@ export class Bill {
|
||||||
async fetch(year: number, month: number) {
|
async fetch(year: number, month: number) {
|
||||||
const data = await getBills(year, month)
|
const data = await getBills(year, month)
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this._bills = data.map((data: any) => {
|
this._bills = data
|
||||||
return {
|
|
||||||
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() {
|
async fetchClass() {
|
||||||
const cls2label = await getLabels()
|
const cls2label = await getClass()
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this._cls2label = cls2label
|
this._cls2label = cls2label
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
export type BillLabel = {
|
|
||||||
consume: Array<BillLabelOption>,
|
|
||||||
income: Array<BillLabelOption>,
|
|
||||||
}
|
|
||||||
|
|
||||||
type BillLabelOption = {
|
|
||||||
name: string,
|
|
||||||
labels: Array<string>,
|
|
||||||
}
|
|
|
@ -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/, ""),
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user