Compare commits

..

10 Commits

Author SHA1 Message Date
clz
5d3642f516 fix: revise .dockerignore 2023-06-02 19:10:09 +08:00
d2edaa57e9 fix🛠️: 进入页面请求两次的bug;部署脚本 2022-12-02 18:49:36 +08:00
293df6e0a1 feat🔫: 修改部署脚本 2022-11-30 23:45:42 +08:00
1d928b8a23 feat🔫: new labels 2022-11-26 01:17:48 +08:00
53ac383f5b fix🛠️: data transfer 2022-11-18 00:30:36 +08:00
b2ce1fe392 to🛠️: new api 2022-11-17 17:35:10 +08:00
909adfead4 to🛠️: new api 2022-11-17 17:13:09 +08:00
c4e66e2e52 feat🔫: 单个cls的label pie 2022-10-18 17:13:04 +08:00
d27e2d9aae feat🔫: bar点击 & fix🛠️: 修改部署脚本 2022-10-09 12:55:04 +08:00
clz
820226c08d style👗: 修改图表样式 2022-09-17 22:21:06 +08:00
19 changed files with 420 additions and 333 deletions

View File

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

View File

@ -5,7 +5,8 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build && pwsh scripts/build.ps1",
"build": "tsc && vite build",
"deploy": "tsc && vite build && pwsh scripts/build.ps1",
"preview": "vite preview"
},
"dependencies": {

View File

@ -1,8 +1,5 @@
docker build . -t registry.cn-hangzhou.aliyuncs.com/fadinglight/bill-react:dev
$server_path='fadinglight:/root/docker/caddy/site/www'
docker push registry.cn-hangzhou.aliyuncs.com/fadinglight/bill-react:dev
ssh fadinglight "rm ${server_path}/* -r"
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"
scp -r dist/* $server_path

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
ssh aliyun "cd /root/docker/bill-sys/;
ssh fadinglight "cd /root/docker/bill-sys/;
docker compose down;
docker pull registry.cn-hangzhou.aliyuncs.com/fadinglight/bill-go:dev;
docker compose up -d"

View File

@ -1,6 +1,5 @@
import Layout from './components/layout'
import { Routes, Route } from 'react-router-dom'
import {useEffect} from "react";
import Home from './pages/Home/Home'
import NotFound from './pages/NotFound'
import { Bill, BillContext } from "./store";
@ -9,13 +8,6 @@ import Record from "./pages/Record/Record";
function App() {
const billStore = new Bill()
const now = new Date()
useEffect(() => {
billStore.fetch(now.getFullYear(), now.getMonth() + 1)
.then()
.catch(console.dir)
}, [])
return (
<div className="App">

View File

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

View File

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

View File

@ -17,6 +17,7 @@ interface IProps {
data: BarData[];
title?: string;
subTitle?: string;
onClickItem?: (date: string) => void;
}
export const Bar = (props: IProps) => {
@ -37,6 +38,23 @@ export const Bar = (props: IProps) => {
subtext: subTitle ?? "",
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: {
type: 'category',
data: data.map(item => {
@ -64,29 +82,16 @@ 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])
const bar = useMemo(() => {
return chartRef.current ? echarts.init(chartRef.current, undefined, { renderer: "svg" }) : null;
const chart = 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])
useEffect(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,4 +9,13 @@
gap: 10px;
align-items: flex-start;
}
.monthBar {
height: 300px;
}
.cards {
display: grid;
grid: repeat(2, auto) / auto-flow auto;
}
}

View File

@ -6,7 +6,7 @@ import { useContext, useEffect, useState } from "react";
import { BillContext } from "../../store";
import { observer } from "mobx-react-lite";
import Pie from "../../components/charts/pie";
import { Card, DatePicker, Radio, Space } from "antd";
import { Card, Modal, DatePicker, Radio, Space } from "antd";
import moment from 'moment';
import 'moment/locale/zh-cn';
import dayjs from 'dayjs'
@ -47,10 +47,22 @@ const Home = () => {
}
const typeOpt = [
{ label: '支出', value: BillType.consume },
{ label: '收入', value: BillType.income },
{ label: '支出', value: BillType.CONSUME },
{ 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 (
<div className={styles.home}>
@ -74,8 +86,44 @@ const Home = () => {
</Card>
</Space>
</div>
<Bar data={transformer(billStore.groupByDate(billType))} />
<Pie data={transformer(billStore.groupByClass(billType))} />
<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>
)
}

View File

@ -7,7 +7,7 @@ import {BillContext} from "../../store";
import { observer } from "mobx-react-lite";
import styles from "./Record.module.scss"
import { BaseSelectRef } from "rc-select/lib/BaseSelect";
import {createBill} from "../../api/bills";
import { postBills } from "../../api/bills";
function Record() {
@ -28,6 +28,9 @@ function Record() {
if (!!clsRef.current) clsRef.current.focus()
}, [clsRef])
useEffect(() => {
billStore.fetchLabels().then()
}, [])
// table
const columns = [
@ -50,7 +53,7 @@ function Record() {
title: "金额",
key: "money",
render: (_: any, record: IBill) => {
const isConsume = record.type === BillType.consume
const isConsume = record.type === BillType.CONSUME
const color = isConsume ? "red" : "green"
const flag = isConsume ? "-" : "+"
return <Tag color={color}>{flag}{record.money}</Tag>
@ -79,8 +82,8 @@ function Record() {
const [datasource, setDataSource] = useState<IBill[]>([])
const typeOpt = [
{label: '支出', value: BillType.consume},
{label: '收入', value: BillType.income},
{ label: '支出', value: BillType.CONSUME },
{ label: '收入', value: BillType.INCOME },
];
// 提交到表格
@ -93,7 +96,7 @@ function Record() {
bill.money = Number(money)
bill.options = options
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 = () => {
setCls("")
@ -117,26 +120,22 @@ function Record() {
const [uploadLoading, setUploadLoading] = useState(false)
const upload = async () => {
setUploadLoading(true)
const failures = []
for (let bill of datasource) {
datasource.forEach(it => Reflect.deleteProperty(it, "key"))
try {
const {id} = await createBill(bill)
if (!id) failures.push(bill)
} catch (e) {
failures.push(bill)
await postBills(datasource)
setDataSource([])
} catch (err) {
}
}
setDataSource(failures)
setUploadLoading(false)
}
let classData: string[]
let classData: string[] = []
switch (billType) {
case BillType.consume:
classData = Object.keys(cls2label.consume)
case BillType.CONSUME:
classData = cls2label.consume.map(it => it.name)
break
case BillType.income:
classData = cls2label.income
case BillType.INCOME:
classData = cls2label.income.map(it => it.name)
break
}
@ -170,14 +169,14 @@ function Record() {
value={cls === "" ? null : cls}
onChange={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>)
}
</Select>
{billType === BillType.consume && (
{billType === BillType.CONSUME && (
<Select
style={{ width: 120 }}
// showSearch
@ -189,7 +188,8 @@ function Record() {
value={label === "" ? null : label}
onChange={setLabel}>
{cls !== "" &&
cls2label.consume[cls]
cls2label.consume
.find(it => it.name === cls)?.labels
.map(la => <Select.Option key={la} value={la}>{la}</Select.Option>)
}
<Select.Option key={"other"} value={"其他"}>{"其他"}</Select.Option>)

View File

@ -1,8 +1,10 @@
import { makeAutoObservable, runInAction } from "mobx";
import { createContext } from "react";
import { createBill, getBills, getClass } from "../api/bills";
import { postBills, getBills, getLabels } from "../api/bills";
import { BillType, IBill } from "../model";
import * as R from "ramda"
import { BillLabel } from "./types";
/**
*
@ -10,11 +12,10 @@ import * as R from "ramda"
export class Bill {
private _bills: IBill[] = [];
// _cls2label: IClass = {consume: new Map<string, string[]>(), income: []}
private _cls2label: { consume: Record<string, string[]>, income: [] } = { consume: {}, income: [] }
private _cls2label: BillLabel = { consume: [], income: [] }
constructor() {
makeAutoObservable(this)
this.fetchClass().then()
}
get bills() {
@ -34,8 +35,8 @@ export class Bill {
return functions(this._bills)
}
groupByClass(type?: BillType) {
const classFun = R.filter((bill: IBill) => R.of(bill.type).length === 0 || bill.type === type)
groupByClass(type?: BillType, date?: string) {
const classFun = R.filter((bill: IBill) => R.of(bill.type).length === 0 || bill.type === type && (date ? bill.date === date : true))
const functions = R.compose(
R.groupBy((bill: IBill) => bill.cls),
classFun,
@ -43,6 +44,16 @@ export class Bill {
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() {
return this.groupByDate
}
@ -63,7 +74,7 @@ export class Bill {
(s: number) => s.toFixed(2),
R.sum,
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)
}
@ -74,16 +85,16 @@ export class Bill {
(s: number) => s.toFixed(2),
R.sum,
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)
}
getTotalMoney(type?: BillType) {
switch (type) {
case BillType.income:
case BillType.INCOME:
return this.incomeMoney
case BillType.consume:
case BillType.CONSUME:
return this.consumeMoney
default:
return this.totalMoney
@ -98,7 +109,7 @@ export class Bill {
async add(bill: IBill) {
const { id } = await createBill(bill)
const { id } = await postBills([bill])
bill.id = id
runInAction(() => {
this._bills.push(bill);
@ -108,12 +119,22 @@ export class Bill {
async fetch(year: number, month: number) {
const data = await getBills(year, month)
runInAction(() => {
this._bills = data
this._bills = data.map((data: any) => {
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 fetchClass() {
const cls2label = await getClass()
async fetchLabels() {
const cls2label = await getLabels()
runInAction(() => {
this._cls2label = cls2label
})

9
src/store/types.ts Normal file
View File

@ -0,0 +1,9 @@
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: {
proxy: {
"/api/": {
target: "http://www.fadinglight.cn:8080/",
target: "https://bill.fadinglight.cn/api/",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
}