├── .github
└── workflows
│ ├── build_beancount.yml
│ ├── docker.yml
│ └── push_aliyun_image.yml
├── .gitignore
├── Dockerfile
├── License
├── README.md
├── config
└── white_list.json
├── docker-compose.yml
├── go.mod
├── go.sum
├── lib
├── Dockerfile
└── beancount-2.3.6.tar.gz
├── logs
└── log
├── public
├── asset-manifest.json
├── favicon.ico
├── icons
│ ├── Assets_Fixed_House_商品房.png
│ ├── Assets_Flow_Bank_ICBC_工商银行.png
│ ├── Assets_Flow_Cash_现金.png
│ ├── Assets_Flow_EBank_AliPay_支付宝.png
│ ├── Assets_Flow_EBank_WxPay_微信支付.png
│ ├── Assets_Invest_Deposit_定期.png
│ ├── Assets_Invest_Fund_自定义基金.png
│ ├── Assets_Invest_Gold_黄金.png
│ ├── Assets_Invest_Stock_股票.png
│ ├── Equity_OpeningBalances.png
│ ├── Expenses_Life_Food_Coffee_咖啡.png
│ ├── Expenses_Life_Food_Drink_饮料.png
│ ├── Expenses_Life_Food_Fruit_水果.png
│ ├── Expenses_Life_Food_Meal_午餐.png
│ ├── Expenses_Life_Food_Meal_早餐.png
│ ├── Expenses_Life_Food_Meal_晚餐.png
│ ├── Expenses_Life_Food_Meal_聚餐.png
│ ├── Expenses_Life_Food_Snack_零食.png
│ ├── Expenses_Life_Hobby_Book_图书.png
│ ├── Expenses_Life_Hobby_Camera_摄影.png
│ ├── Expenses_Life_Hobby_Travel_Souvenir_纪念品.png
│ ├── Expenses_Life_Hobby_Travel_Ticket_门票.png
│ ├── Expenses_Life_House_Electricity_用电.png
│ ├── Expenses_Life_House_Gas_天然气.png
│ ├── Expenses_Life_House_Hotel_酒店.png
│ ├── Expenses_Life_House_Rent_房租.png
│ ├── Expenses_Life_House_Water_用水.png
│ ├── Expenses_Life_Other_Commission_手续费.png
│ ├── Expenses_Life_Other_Exchange_红包转账.png
│ ├── Expenses_Life_Shopping_Clothes_衣服.png
│ ├── Expenses_Life_Shopping_Shoe_鞋.png
│ ├── Expenses_Life_Shopping_Sock_袜子.png
│ ├── Expenses_Life_Shopping_购物.png
│ ├── Expenses_Life_Subscribe_Mobile_手机话费.png
│ ├── Expenses_Life_Subscribe_会员订阅.png
│ ├── Expenses_Life_Travel_Airplane_飞机.png
│ ├── Expenses_Life_Travel_Bike_共享单车.png
│ ├── Expenses_Life_Travel_Bus_公交地铁.png
│ ├── Expenses_Life_Travel_Taxi_出租车.png
│ ├── Expenses_Life_Travel_Train_火车.png
│ ├── Expenses_Work_Insurance_三险.png
│ ├── Expenses_Work_Punish_考勤.png
│ ├── Expenses_Work_Tax_个人所得税.png
│ ├── Income_Gov_政府补贴.png
│ ├── Income_Gov_退税.png
│ ├── Income_Invest_投资收益.png
│ ├── Income_Work_Bonus_奖金.png
│ ├── Income_Work_HouseFund_单位公积金.png
│ ├── Income_Work_Salary_工作收入.png
│ ├── Liabilities_Cycle_ICBC_房贷.png
│ ├── Liabilities_Life_CreditCard_信用卡.png
│ ├── Liabilities_Life_Huabei_花呗.png
│ └── Liabilities_Life_JD_京东白条.png
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
├── robots.txt
└── static
│ ├── css
│ ├── 107.e31e2bec.chunk.css
│ ├── 48.b1fca4ab.chunk.css
│ ├── 550.e2ad996c.chunk.css
│ ├── 751.31d6cfe0.chunk.css
│ ├── 832.a9c07082.chunk.css
│ ├── 967.892220ee.chunk.css
│ └── main.c87495cb.css
│ └── js
│ ├── 100.762fab30.chunk.js
│ ├── 107.022f790d.chunk.js
│ ├── 113.1a6f29cb.chunk.js
│ ├── 185.7c0dab03.chunk.js
│ ├── 316.cc4bdd3d.chunk.js
│ ├── 332.e55ab720.chunk.js
│ ├── 475.25294e0d.chunk.js
│ ├── 48.d4b23ad9.chunk.js
│ ├── 508.99bcae1b.chunk.js
│ ├── 510.abd28793.chunk.js
│ ├── 550.19e37c86.chunk.js
│ ├── 619.63e56027.chunk.js
│ ├── 636.ccc0986a.chunk.js
│ ├── 668.c1806339.chunk.js
│ ├── 682.43b1b035.chunk.js
│ ├── 691.3c4589f4.chunk.js
│ ├── 719.2845bca8.chunk.js
│ ├── 719.2845bca8.chunk.js.LICENSE.txt
│ ├── 751.470004cf.chunk.js
│ ├── 756.c03ffef3.chunk.js
│ ├── 8.7bdc2409.chunk.js
│ ├── 806.c1039356.chunk.js
│ ├── 806.c1039356.chunk.js.LICENSE.txt
│ ├── 811.8d4e3c76.chunk.js
│ ├── 811.8d4e3c76.chunk.js.LICENSE.txt
│ ├── 828.e991df23.chunk.js
│ ├── 832.dcbb6410.chunk.js
│ ├── 965.8a9e5189.chunk.js
│ ├── 967.86639f46.chunk.js
│ ├── 999.1397823d.chunk.js
│ ├── main.ad3a4211.js
│ └── main.ad3a4211.js.LICENSE.txt
├── script
├── bql.go
├── config.go
├── file.go
├── log.go
├── paths.go
├── platform.go
├── sort.go
└── utils.go
├── server.go
├── service
├── accounts.go
├── bean.go
├── commodity.go
├── error.go
├── events.go
├── import.go
├── ledger.go
├── source_file.go
├── stats.go
├── tags.go
├── transactions.go
└── version.go
├── snapshot.png
├── template
├── .beancount-gs
│ └── account_type.json
├── account
│ ├── assets.bean
│ ├── equity.bean
│ ├── expenses.bean
│ ├── income.bean
│ └── liabilities.bean
├── event
│ └── events.bean
├── history.bean
├── includes.bean
├── index.bean
├── month
│ └── months.bean
└── price
│ ├── commodities.bean
│ └── prices.bean
├── tests
└── main_test.go
└── var.env
/.github/workflows/build_beancount.yml:
--------------------------------------------------------------------------------
1 | name: Build and Push Docker Image to Docker Hub
2 |
3 | # 仅手动触发
4 | on:
5 | workflow_dispatch:
6 | inputs:
7 | tag:
8 | description: 'Tag for the Docker image'
9 | required: false
10 | default: '2.3.6'
11 |
12 | jobs:
13 | build-and-push:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout repository
18 | uses: actions/checkout@v3
19 |
20 | - name: Set up Docker Buildx
21 | uses: docker/setup-buildx-action@v2
22 |
23 | - name: Log in to Docker Hub
24 | uses: docker/login-action@v2
25 | with:
26 | username: ${{ secrets.DOCKER_USERNAME }}
27 | password: ${{ secrets.DOCKER_PASSWORD }}
28 | -
29 | name: Build and push
30 | uses: docker/build-push-action@v4
31 | with:
32 | context: ./lib
33 | platforms: linux/amd64,linux/arm64
34 | push: true
35 | tags: xdbin/beancount-alpine:${{ github.event.inputs.tag }}
36 | labels: ${{ steps.meta.outputs.labels }}
37 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 | name: Publish Docker image
2 | on:
3 | workflow_dispatch:
4 | release:
5 | types: [published]
6 | jobs:
7 | push_to_registry:
8 | name: Push Docker image to Docker Hub
9 | runs-on: ubuntu-latest
10 | steps:
11 | -
12 | name: Checkout
13 | uses: actions/checkout@v3
14 | - name: Docker meta
15 | id: meta
16 | uses: docker/metadata-action@v4
17 | with:
18 | # list of Docker images to use as base name for tags
19 | images: |
20 | xdbin/beancount-gs
21 | # generate Docker tags based on the following events/attributes
22 | tags: |
23 | type=semver,pattern={{version}}
24 | type=ref,event=branch
25 | -
26 | name: Set up QEMU
27 | uses: docker/setup-qemu-action@v2
28 | -
29 | name: Set up Docker Buildx
30 | uses: docker/setup-buildx-action@v2
31 | -
32 | name: Login to DockerHub
33 | uses: docker/login-action@v2
34 | with:
35 | username: ${{ secrets.DOCKER_USERNAME }}
36 | password: ${{ secrets.DOCKER_PASSWORD }}
37 | -
38 | name: Build and push
39 | uses: docker/build-push-action@v4
40 | with:
41 | context: .
42 | platforms: linux/amd64,linux/arm64
43 | push: true
44 | tags: ${{ steps.meta.outputs.tags }}
45 | labels: ${{ steps.meta.outputs.labels }}
--------------------------------------------------------------------------------
/.github/workflows/push_aliyun_image.yml:
--------------------------------------------------------------------------------
1 | name: Manual Push Aliyun Image Registry
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | tag:
7 | description: 'Tag for the Docker image'
8 | required: true
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout repository
16 | uses: actions/checkout@v2
17 |
18 | - name: Set up Docker Buildx
19 | uses: docker/setup-buildx-action@v2
20 |
21 | - name: Log in to Alibaba Cloud Container Registry
22 | env:
23 | REGISTRY: ${{ secrets.ALIYUN_REGISTRY }}
24 | USERNAME: ${{ secrets.ALIYUN_USERNAME }}
25 | PASSWORD: ${{ secrets.ALIYUN_PASSWORD }}
26 | run: echo "${{ secrets.ALIYUN_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u ${{ secrets.ALIYUN_USERNAME }} --password-stdin
27 |
28 | - name: Extract branch name
29 | id: extract_branch
30 | run: echo "branch_name=${GITHUB_REF##*/}" >> $GITHUB_ENV
31 |
32 | - name: Build and Push Docker image
33 | env:
34 | ## 修改为你对应的镜像名称
35 | IMAGE_NAME: ${{ secrets.ALIYUN_REGISTRY }}/xdbin/beancount-gs
36 | TAG: ${{ github.event.inputs.tag }}
37 | run: |
38 | docker build -t $IMAGE_NAME:$TAG .
39 | docker push $IMAGE_NAME:$TAG
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .git
3 | .vscode
4 | bindata.go
5 | *.exe
6 | beancount-gs
7 | gin.log
8 | config.json
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # 构建 beancount-gs
2 | FROM golang:1.17.3 AS go_builder
3 |
4 | ENV GO111MODULE=on \
5 | GOPROXY=https://goproxy.cn,direct \
6 | GIN_MODE=release \
7 | CGO_ENABLED=0 \
8 | PORT=80
9 |
10 | WORKDIR /app
11 | COPY . .
12 | COPY public/icons ./public/default_icons
13 | RUN go build .
14 |
15 | # 镜像
16 | FROM xdbin/beancount-alpine:2.3.6
17 |
18 | WORKDIR /app
19 | COPY --from=go_builder /app/beancount-gs ./
20 | COPY --from=go_builder /app/template ./template
21 | COPY --from=go_builder /app/config ./config
22 | COPY --from=go_builder /app/public ./public
23 | COPY --from=go_builder /app/logs ./logs
24 |
25 | EXPOSE 80
26 |
27 | ENTRYPOINT [ "/bin/sh", "-c", "cp -rn /app/public/default_icons/* /app/public/icons && /app/beancount-gs -p 80" ]
--------------------------------------------------------------------------------
/License:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2021 BaoXuebin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # beancount-gs
2 |
3 | 
4 | [](https://hub.docker.com/repository/docker/xdbin/beancount-gs/general)
5 | [](https://hub.docker.com/repository/docker/xdbin/beancount-gs/general)
6 |
7 | [前端项目地址](https://github.com/BaoXuebin/beancount-web)
8 | [演示地址](https://beancount.xdbin.com/)
9 | [使用文档](https://www.yuque.com/chuyi-ble7p/beancount-gs)
10 |
11 | ## 介绍
12 |
13 | [beancount](https://github.com/beancount/) 是一个优秀的开源复式记账工具,因为其基于文本记录的特性,难以拓展到移动端;本项目旨在将常见的记账行为封装为 RESTful API。
14 |
15 | 本仓库使用 `Golang` 进行文本的读写和接口服务支持,利用 `bean-query` 获取内容并解析,以 Json 格式返回。并基于已实现的接口内置实现了前端页面(适配移动端)。
16 |
17 | 
18 |
19 | ## 特性
20 |
21 | - [X] 私有部署
22 | - [X] 多账本
23 | - [X] 账户,资产管理
24 | - [X] 统计图表
25 | - [X] 多币种
26 | - [X] 标签
27 | - [X] 投资管理(FIFO)
28 | - [X] 第三方账单导入(支付宝,微信,工商银行,农业银行)
29 | - [X] 分期记账
30 | - [X] 事件
31 |
32 | ## 如何使用
33 |
34 | **本地打包**
35 |
36 | 1. 克隆本项目到本地
37 | 2. 根目录执行 `go build`
38 | 3. 执行 `./beancount-gs` (`-p` 指定端口号,`-secret` 指定配置密钥)
39 |
40 | **release**
41 |
42 | 1. 下载并解压项目的 `release` 包
43 | 2. 执行根目录下的 `./beancount-gs.exe`
44 |
45 | **docker**
46 |
47 | ```shell
48 | docker run --name beancount-gs -dp 10000:80 \
49 | -w /app \
50 | -v "/data/beancount:/data/beancount" \
51 | -v "/data/beancount/icons:/app/public/icons" \
52 | -v "/data/beancount/config:/app/config" \
53 | -v "/data/beancount/bak:/app/bak" \
54 | xdbin/beancount-gs:latest
55 | ```
56 |
57 | **docker-compose**
58 |
59 | 在指定目录创建文件 `docker-compose.yml`,然后复制下面内容到这个文件,执行 `docker-compose up -d`
60 |
61 | ```yaml
62 | version: "3.9"
63 | services:
64 | app:
65 | container_name: beancount-gs
66 | image: xdbin/beancount-gs:${tag:-latest}
67 | ports:
68 | - "10000:80"
69 | volumes:
70 | - "${dataPath:-/data/beancount}:/data/beancount"
71 | - "${dataPath:-/data/beancount}/icons:/app/public/icons"
72 | - "${dataPath:-/data/beancount}/config:/app/config"
73 | - "${dataPath:-/data/beancount}/bak:/app/bak"
74 | - "${dataPath:-/data/beancount}/logs:/app/logs"
75 | ```
76 |
77 | 默认的文件存储路径为 `/data/beancount`,如果你想更换其他路径,可以在当前目录下新建 `var.env`,然后将下面内容复制到这个文件
78 |
79 | ```properties
80 | tag=latest
81 | dataPath=自定义的目录
82 | ```
83 |
84 | 执行 `docker-compose --env-file ./var.env up -d` 即可
85 |
86 | ## 项目负责人
87 |
88 | [@BaoXuebin](https://github.com/BaoXuebin)
89 |
90 | ## 开源协议
91 |
92 | [MIT](https://github.com/BaoXuebin/beancount-gs/blob/main/License) @BaoXuebin
93 |
94 | ## 感谢️
95 |
96 | [赞助地址](https://xdbin.com/sponsor)
97 |
98 | 感谢 **@Cabin**,**@潇** 两位朋友的赞助支持❤️
--------------------------------------------------------------------------------
/config/white_list.json:
--------------------------------------------------------------------------------
1 | []
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.9"
2 | services:
3 | app:
4 | container_name: beancount-gs
5 | image: xdbin/beancount-gs:${tag:-latest}
6 | ports:
7 | - "10000:80"
8 | volumes:
9 | - "${dataPath:-/data/beancount}:/data/beancount"
10 | - "${dataPath:-/data/beancount}/icons:/app/public/icons"
11 | - "${dataPath:-/data/beancount}/config:/app/config"
12 | - "${dataPath:-/data/beancount}/bak:/app/bak"
13 | - "${dataPath:-/data/beancount}/logs:/app/logs"
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/beancount-gs
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/gin-gonic/gin v1.7.4
7 | github.com/shopspring/decimal v1.3.1
8 | github.com/stretchr/testify v1.7.0
9 | golang.org/x/text v0.3.7
10 | )
11 |
12 | require (
13 | github.com/davecgh/go-spew v1.1.1 // indirect
14 | github.com/gin-contrib/sse v0.1.0 // indirect
15 | github.com/go-playground/locales v0.14.0 // indirect
16 | github.com/go-playground/universal-translator v0.18.0 // indirect
17 | github.com/go-playground/validator/v10 v10.9.0 // indirect
18 | github.com/golang/protobuf v1.5.2 // indirect
19 | github.com/json-iterator/go v1.1.12 // indirect
20 | github.com/leodido/go-urn v1.2.1 // indirect
21 | github.com/mattn/go-isatty v0.0.14 // indirect
22 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
23 | github.com/modern-go/reflect2 v1.0.2 // indirect
24 | github.com/pmezard/go-difflib v1.0.0 // indirect
25 | github.com/ugorji/go/codec v1.2.6 // indirect
26 | golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect
27 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect
28 | google.golang.org/protobuf v1.27.1 // indirect
29 | gopkg.in/yaml.v2 v2.4.0 // indirect
30 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
31 | )
32 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
6 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
7 | github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM=
8 | github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
9 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
10 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
11 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
12 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
13 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
14 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
15 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
16 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
17 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
18 | github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A=
19 | github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
20 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
21 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
22 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
23 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
24 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
25 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
26 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
27 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
28 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
29 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
30 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
31 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
32 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
33 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
34 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
35 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
36 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
37 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
38 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
39 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
40 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
41 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
42 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
43 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
44 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
46 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
47 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
48 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
49 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
50 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
51 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
52 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
53 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
54 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
55 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
56 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
57 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
58 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
59 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
60 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
61 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
62 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
63 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
64 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
65 | github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E=
66 | github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
67 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
68 | github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ=
69 | github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=
70 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
71 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
72 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
73 | golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI=
74 | golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
75 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
76 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
77 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
78 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
79 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
80 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
81 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
82 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
83 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
84 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
85 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
86 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI=
87 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
88 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
89 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
90 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
91 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
92 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
93 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
94 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
95 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
96 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
97 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
98 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
99 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
100 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
101 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
102 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
103 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
104 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
105 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
106 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
107 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
108 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
109 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
110 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
111 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
112 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
113 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
114 |
--------------------------------------------------------------------------------
/lib/Dockerfile:
--------------------------------------------------------------------------------
1 | # 第一阶段:构建阶段
2 | FROM python:3.11.9-alpine3.19 as builder
3 |
4 | # 设置环境变量,防止 Python 创建 .pyc 文件
5 | ENV PYTHONUNBUFFERED=1
6 |
7 | # 替换为阿里云的镜像源,并安装必要的依赖
8 | RUN echo "https://mirrors.aliyun.com/alpine/v3.15/main/" > /etc/apk/repositories && \
9 | echo "https://mirrors.aliyun.com/alpine/v3.15/community/" >> /etc/apk/repositories && \
10 | apk update && \
11 | apk add --no-cache --virtual .build-deps \
12 | gcc \
13 | g++ \
14 | musl-dev
15 |
16 | # 设置工作目录
17 | WORKDIR /app
18 |
19 | # 创建虚拟环境
20 | RUN python3 -m venv /app/venv
21 |
22 | # 将 Beancount 源码压缩包复制到容器中
23 | COPY beancount-2.3.6.tar.gz /app
24 |
25 | # 解压 Beancount 源码到 /beancount 目录
26 | RUN mkdir /beancount && \
27 | tar -xzf /app/beancount-2.3.6.tar.gz -C /beancount --strip-components=1
28 |
29 | # 激活虚拟环境并安装 Beancount
30 | RUN /app/venv/bin/pip install --upgrade pip -i https://mirrors.aliyun.com/pypi/simple/ && \
31 | /app/venv/bin/pip install /beancount -i https://mirrors.aliyun.com/pypi/simple/ && \
32 | # 清理不必要的文件
33 | rm -rf /app/beancount-2.3.6.tar.gz && \
34 | find /app -name __pycache__ -exec rm -rf -v {} +
35 |
36 | # 第二阶段:运行阶段
37 | FROM python:3.11.9-alpine3.19
38 |
39 | # 设置环境变量,防止 Python 创建 .pyc 文件
40 | ENV PYTHONUNBUFFERED=1
41 |
42 | # 设置工作目录
43 | WORKDIR /app
44 |
45 | # 从构建阶段复制虚拟环境到当前镜像
46 | COPY --from=builder /app/venv /app/venv
47 |
48 | # 将虚拟环境的 bin 目录添加到 PATH 环境变量
49 | ENV PATH="/app/venv/bin:$PATH"
50 |
--------------------------------------------------------------------------------
/lib/beancount-2.3.6.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/lib/beancount-2.3.6.tar.gz
--------------------------------------------------------------------------------
/logs/log:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/logs/log
--------------------------------------------------------------------------------
/public/asset-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": {
3 | "main.css": "/web/static/css/main.c87495cb.css",
4 | "main.js": "/web/static/js/main.ad3a4211.js",
5 | "static/js/999.1397823d.chunk.js": "/web/static/js/999.1397823d.chunk.js",
6 | "static/css/832.a9c07082.chunk.css": "/web/static/css/832.a9c07082.chunk.css",
7 | "static/js/832.dcbb6410.chunk.js": "/web/static/js/832.dcbb6410.chunk.js",
8 | "static/css/967.892220ee.chunk.css": "/web/static/css/967.892220ee.chunk.css",
9 | "static/js/967.86639f46.chunk.js": "/web/static/js/967.86639f46.chunk.js",
10 | "static/css/751.31d6cfe0.chunk.css": "/web/static/css/751.31d6cfe0.chunk.css",
11 | "static/js/751.470004cf.chunk.js": "/web/static/js/751.470004cf.chunk.js",
12 | "static/css/107.e31e2bec.chunk.css": "/web/static/css/107.e31e2bec.chunk.css",
13 | "static/js/107.022f790d.chunk.js": "/web/static/js/107.022f790d.chunk.js",
14 | "static/js/185.7c0dab03.chunk.js": "/web/static/js/185.7c0dab03.chunk.js",
15 | "static/js/756.c03ffef3.chunk.js": "/web/static/js/756.c03ffef3.chunk.js",
16 | "static/css/48.b1fca4ab.chunk.css": "/web/static/css/48.b1fca4ab.chunk.css",
17 | "static/js/48.d4b23ad9.chunk.js": "/web/static/js/48.d4b23ad9.chunk.js",
18 | "static/js/332.e55ab720.chunk.js": "/web/static/js/332.e55ab720.chunk.js",
19 | "static/css/550.e2ad996c.chunk.css": "/web/static/css/550.e2ad996c.chunk.css",
20 | "static/js/550.19e37c86.chunk.js": "/web/static/js/550.19e37c86.chunk.js",
21 | "static/js/475.25294e0d.chunk.js": "/web/static/js/475.25294e0d.chunk.js",
22 | "static/js/636.ccc0986a.chunk.js": "/web/static/js/636.ccc0986a.chunk.js",
23 | "static/js/100.762fab30.chunk.js": "/web/static/js/100.762fab30.chunk.js",
24 | "static/js/691.3c4589f4.chunk.js": "/web/static/js/691.3c4589f4.chunk.js",
25 | "static/js/668.c1806339.chunk.js": "/web/static/js/668.c1806339.chunk.js",
26 | "static/js/811.8d4e3c76.chunk.js": "/web/static/js/811.8d4e3c76.chunk.js",
27 | "static/js/8.7bdc2409.chunk.js": "/web/static/js/8.7bdc2409.chunk.js",
28 | "static/js/619.63e56027.chunk.js": "/web/static/js/619.63e56027.chunk.js",
29 | "static/js/806.c1039356.chunk.js": "/web/static/js/806.c1039356.chunk.js",
30 | "static/js/113.1a6f29cb.chunk.js": "/web/static/js/113.1a6f29cb.chunk.js",
31 | "static/js/316.cc4bdd3d.chunk.js": "/web/static/js/316.cc4bdd3d.chunk.js",
32 | "static/js/828.e991df23.chunk.js": "/web/static/js/828.e991df23.chunk.js",
33 | "static/js/510.abd28793.chunk.js": "/web/static/js/510.abd28793.chunk.js",
34 | "static/js/965.8a9e5189.chunk.js": "/web/static/js/965.8a9e5189.chunk.js",
35 | "static/js/719.2845bca8.chunk.js": "/web/static/js/719.2845bca8.chunk.js",
36 | "static/js/508.99bcae1b.chunk.js": "/web/static/js/508.99bcae1b.chunk.js",
37 | "static/js/682.43b1b035.chunk.js": "/web/static/js/682.43b1b035.chunk.js",
38 | "index.html": "/web/index.html"
39 | },
40 | "entrypoints": [
41 | "static/css/main.c87495cb.css",
42 | "static/js/main.ad3a4211.js"
43 | ]
44 | }
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/favicon.ico
--------------------------------------------------------------------------------
/public/icons/Assets_Fixed_House_商品房.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Assets_Fixed_House_商品房.png
--------------------------------------------------------------------------------
/public/icons/Assets_Flow_Bank_ICBC_工商银行.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Assets_Flow_Bank_ICBC_工商银行.png
--------------------------------------------------------------------------------
/public/icons/Assets_Flow_Cash_现金.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Assets_Flow_Cash_现金.png
--------------------------------------------------------------------------------
/public/icons/Assets_Flow_EBank_AliPay_支付宝.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Assets_Flow_EBank_AliPay_支付宝.png
--------------------------------------------------------------------------------
/public/icons/Assets_Flow_EBank_WxPay_微信支付.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Assets_Flow_EBank_WxPay_微信支付.png
--------------------------------------------------------------------------------
/public/icons/Assets_Invest_Deposit_定期.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Assets_Invest_Deposit_定期.png
--------------------------------------------------------------------------------
/public/icons/Assets_Invest_Fund_自定义基金.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Assets_Invest_Fund_自定义基金.png
--------------------------------------------------------------------------------
/public/icons/Assets_Invest_Gold_黄金.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Assets_Invest_Gold_黄金.png
--------------------------------------------------------------------------------
/public/icons/Assets_Invest_Stock_股票.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Assets_Invest_Stock_股票.png
--------------------------------------------------------------------------------
/public/icons/Equity_OpeningBalances.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Equity_OpeningBalances.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Food_Coffee_咖啡.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Food_Coffee_咖啡.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Food_Drink_饮料.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Food_Drink_饮料.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Food_Fruit_水果.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Food_Fruit_水果.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Food_Meal_午餐.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Food_Meal_午餐.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Food_Meal_早餐.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Food_Meal_早餐.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Food_Meal_晚餐.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Food_Meal_晚餐.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Food_Meal_聚餐.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Food_Meal_聚餐.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Food_Snack_零食.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Food_Snack_零食.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Hobby_Book_图书.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Hobby_Book_图书.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Hobby_Camera_摄影.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Hobby_Camera_摄影.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Hobby_Travel_Souvenir_纪念品.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Hobby_Travel_Souvenir_纪念品.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Hobby_Travel_Ticket_门票.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Hobby_Travel_Ticket_门票.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_House_Electricity_用电.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_House_Electricity_用电.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_House_Gas_天然气.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_House_Gas_天然气.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_House_Hotel_酒店.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_House_Hotel_酒店.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_House_Rent_房租.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_House_Rent_房租.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_House_Water_用水.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_House_Water_用水.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Other_Commission_手续费.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Other_Commission_手续费.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Other_Exchange_红包转账.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Other_Exchange_红包转账.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Shopping_Clothes_衣服.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Shopping_Clothes_衣服.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Shopping_Shoe_鞋.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Shopping_Shoe_鞋.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Shopping_Sock_袜子.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Shopping_Sock_袜子.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Shopping_购物.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Shopping_购物.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Subscribe_Mobile_手机话费.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Subscribe_Mobile_手机话费.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Subscribe_会员订阅.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Subscribe_会员订阅.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Travel_Airplane_飞机.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Travel_Airplane_飞机.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Travel_Bike_共享单车.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Travel_Bike_共享单车.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Travel_Bus_公交地铁.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Travel_Bus_公交地铁.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Travel_Taxi_出租车.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Travel_Taxi_出租车.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Life_Travel_Train_火车.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Life_Travel_Train_火车.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Work_Insurance_三险.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Work_Insurance_三险.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Work_Punish_考勤.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Work_Punish_考勤.png
--------------------------------------------------------------------------------
/public/icons/Expenses_Work_Tax_个人所得税.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Expenses_Work_Tax_个人所得税.png
--------------------------------------------------------------------------------
/public/icons/Income_Gov_政府补贴.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Income_Gov_政府补贴.png
--------------------------------------------------------------------------------
/public/icons/Income_Gov_退税.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Income_Gov_退税.png
--------------------------------------------------------------------------------
/public/icons/Income_Invest_投资收益.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Income_Invest_投资收益.png
--------------------------------------------------------------------------------
/public/icons/Income_Work_Bonus_奖金.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Income_Work_Bonus_奖金.png
--------------------------------------------------------------------------------
/public/icons/Income_Work_HouseFund_单位公积金.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Income_Work_HouseFund_单位公积金.png
--------------------------------------------------------------------------------
/public/icons/Income_Work_Salary_工作收入.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Income_Work_Salary_工作收入.png
--------------------------------------------------------------------------------
/public/icons/Liabilities_Cycle_ICBC_房贷.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Liabilities_Cycle_ICBC_房贷.png
--------------------------------------------------------------------------------
/public/icons/Liabilities_Life_CreditCard_信用卡.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Liabilities_Life_CreditCard_信用卡.png
--------------------------------------------------------------------------------
/public/icons/Liabilities_Life_Huabei_花呗.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Liabilities_Life_Huabei_花呗.png
--------------------------------------------------------------------------------
/public/icons/Liabilities_Life_JD_京东白条.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/icons/Liabilities_Life_JD_京东白条.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
你的个人财务管理软件 | beancount-gs
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "beancount-gs",
3 | "name": "基于beancount的个人记账财务管理软件",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/static/css/107.e31e2bec.chunk.css:
--------------------------------------------------------------------------------
1 | .stats-page{text-align:center}
--------------------------------------------------------------------------------
/public/static/css/48.b1fca4ab.chunk.css:
--------------------------------------------------------------------------------
1 | .import-page .action-container{display:flex;justify-content:space-between}
--------------------------------------------------------------------------------
/public/static/css/550.e2ad996c.chunk.css:
--------------------------------------------------------------------------------
1 | .event-page .top-wrapper{display:flex;justify-content:space-between;margin-bottom:2rem}
--------------------------------------------------------------------------------
/public/static/css/751.31d6cfe0.chunk.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/public/static/css/751.31d6cfe0.chunk.css
--------------------------------------------------------------------------------
/public/static/css/832.a9c07082.chunk.css:
--------------------------------------------------------------------------------
1 | .calendar-drawer .date-cell{height:80px}.calendar-drawer .calendar{max-width:800px}.calendar-drawer .date-cell{font-size:12px}.calendar-drawer .date-cell .date{font-size:20px;font-weight:bolder}.calendar-drawer .date-cell .expenses{color:#1da57a}.calendar-drawer .date-cell .income{color:#ff4d4f}.index-page .top-wrapper{display:flex;justify-content:space-between;margin-bottom:2rem}
--------------------------------------------------------------------------------
/public/static/css/967.892220ee.chunk.css:
--------------------------------------------------------------------------------
1 | .account-page .button-wrapper{display:flex;justify-content:space-between}.ant-upload.ant-upload-select{display:block}
--------------------------------------------------------------------------------
/public/static/js/185.7c0dab03.chunk.js:
--------------------------------------------------------------------------------
1 | "use strict";(self.webpackChunkbeancount_web=self.webpackChunkbeancount_web||[]).push([[185],{1772:(t,e,n)=>{n.d(e,{A:()=>a});var s=n(9284),o=n(8828),i=n(712);const a=t=>{const e=(0,s.useRef)(null);return(0,i.jsx)(o.KE,{height:t.height||"75vh",defaultLanguage:"bean"===t.lang?"beancount":t.lang,theme:"light",onMount:(n,s)=>{e.current=n,n.onDidChangeModelContent((()=>{s.languages.register({id:"beancount"}),s.languages.setMonarchTokensProvider("beancount",{tokenizer:{root:[[/\*|\!/,"keyword"],[/\d{4}-\d{2}-\d{2}/,"number"],[/\b(Assets|Liabilities|Equity|Income|Expenses)(:[\w\-]+)+\b/,"type.identifier"],[/-?\d+(\.\d+)?\s*(USD|CNY|EUR)?/,"number"],[/;.*/,"comment"],[/^\s*(include|option|plugin)\b/,"keyword"],[/\".*\"/,"string"]]}}),s.languages.setLanguageConfiguration("beancount",{comments:{lineComment:";"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:'"',close:'"'},{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"}]});const e=n.getValue();t.onContentChange&&"function"===typeof t.onContentChange&&t.onContentChange(e)}))},options:{selectOnLineNumbers:!0,automaticLayout:!0,scrollBeyondLastLine:!1,wordWrap:"on",fontFamily:"'Consolas', monospace",fontSize:14,lineHeight:20,fontWeight:"500"},...t})}},1185:(t,e,n)=>{n.r(e),n.d(e,{default:()=>y});var s=n(9379),o=n(9284);const i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M893.3 293.3L730.7 130.7c-7.5-7.5-16.7-13-26.7-16V112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V338.5c0-17-6.7-33.2-18.7-45.2zM384 184h256v104H384V184zm456 656H184V184h136v136c0 17.7 14.3 32 32 32h320c17.7 0 32-14.3 32-32V205.8l136 136V840zM512 442c-79.5 0-144 64.5-144 144s64.5 144 144 144 144-64.5 144-144-64.5-144-144-144zm0 224c-44.2 0-80-35.8-80-80s35.8-80 80-80 80 35.8 80 80-35.8 80-80 80z"}}]},name:"save",theme:"outlined"};var a=n(3768),h=function(t,e){return o.createElement(a.A,(0,s.A)((0,s.A)({},t),{},{ref:e,icon:i}))};const l=o.forwardRef(h);var c=n(4412),r=n(9636),d=n(1896),u=n(4760),g=n(2475),m=n(2069),p=n(1772),C=n(712);class f extends o.Component{constructor(){super(...arguments),this.theme=this.context.theme,this.state={loading:!1,lang:"beancount",path:null,files:[],rawContent:"",content:""},this.fetchFileDir=()=>{this.setState({loading:!0}),(0,u.hd)("/api/auth/file/dir").then((t=>{this.setState({files:t})})).finally((()=>{this.setState({loading:!1})}))},this.handldEditContent=t=>{this.setState({content:t})},this.handleChangeFile=t=>{let e=this.state.lang;const n=t.split(".");e=n[n.length-1],this.setState({path:t,lang:e},(()=>{this.fetchFileContent(t)}))},this.fetchFileContent=()=>{this.setState({loading:!0}),(0,u.hd)(`/api/auth/file/content?path=${this.state.path}`).then((t=>{this.setState({rawContent:t,content:t})})).finally((()=>{this.setState({loading:!1})}))},this.saveFileContent=()=>{const{path:t,content:e}=this.state;this.setState({loading:!0}),(0,u.hd)("/api/auth/file",{method:"POST",body:{path:t,content:e}}).then((()=>{this.setState({rawContent:e}),c.Ay.success("\u4fdd\u5b58\u6210\u529f")})).finally((()=>{this.setState({loading:!1})}))}}componentDidMount(){this.fetchFileDir()}render(){return this.context.theme!==this.theme&&(this.theme=this.context.theme),(0,C.jsxs)("div",{className:"edit-page",children:[(0,C.jsxs)("div",{children:[(0,C.jsx)(r.A,{showSearch:!0,placeholder:"\u8bf7\u9009\u62e9\u6e90\u6587\u4ef6",style:{width:"200px"},onChange:this.handleChangeFile,children:this.state.files.map((t=>(0,C.jsx)(r.A.Option,{value:t,children:t},t)))}),"\xa0\xa0",(0,C.jsx)(d.A,{type:"primary",icon:(0,C.jsx)(l,{}),disabled:this.state.rawContent===this.state.content||!this.state.path,loading:this.state.loading,onClick:this.saveFileContent,children:"\u4fdd\u5b58"})]}),(0,C.jsx)("div",{style:{marginTop:"1rem"},children:(0,C.jsx)(p.A,{lang:this.state.lang,value:this.state.content,onContentChange:this.handldEditContent})})]})}}f.contextType=g.A;const y=(0,m.A)(f)},2069:(t,e,n)=>{n.d(e,{A:()=>i});var s=n(9284),o=n(712);const i=t=>class extends s.Component{constructor(){super(...arguments),this.defaultCommodity={currency:"CNY",symbol:"\uffe5"},this.currentCommodity=window.localStorage.getItem("ledgerCurrency")}render(){return(0,o.jsx)(t,{...this.props,commodity:this.currentCommodity?JSON.parse(this.currentCommodity):this.defaultCommodity})}}}}]);
--------------------------------------------------------------------------------
/public/static/js/316.cc4bdd3d.chunk.js:
--------------------------------------------------------------------------------
1 | (self.webpackChunkbeancount_web=self.webpackChunkbeancount_web||[]).push([[316],{9482:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});const a=n(9126).A},1035:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});const a=n(5164).A},54:(e,t,n)=>{"use strict";n.d(t,{A:()=>z});var a=n(4467),r=n(8168),c=n(2284),o=n(4480),l=n.n(o),s=n(9284),i=n(3135),u=n(37);const f=function(e){var t=e.prefixCls,n=e.className,c=e.style,o=e.size,i=e.shape,u=l()((0,a.A)((0,a.A)({},"".concat(t,"-lg"),"large"===o),"".concat(t,"-sm"),"small"===o)),f=l()((0,a.A)((0,a.A)((0,a.A)({},"".concat(t,"-circle"),"circle"===i),"".concat(t,"-square"),"square"===i),"".concat(t,"-round"),"round"===i)),v=s.useMemo((function(){return"number"===typeof o?{width:o,height:o,lineHeight:"".concat(o,"px")}:{}}),[o]);return s.createElement("span",{className:l()(t,u,f,n),style:(0,r.A)((0,r.A)({},v),c)})};const v=function(e){var t=e.prefixCls,n=e.className,c=e.active,o=e.shape,v=void 0===o?"circle":o,m=e.size,d=void 0===m?"default":m,p=(0,s.useContext(i.QO).getPrefixCls)("skeleton",t),A=(0,u.A)(e,["prefixCls","className"]),x=l()(p,"".concat(p,"-element"),(0,a.A)({},"".concat(p,"-active"),c),n);return s.createElement("div",{className:x},s.createElement(f,(0,r.A)({prefixCls:"".concat(p,"-avatar"),shape:v,size:d},A)))};const m=function(e){var t=e.prefixCls,n=e.className,c=e.active,o=e.block,v=void 0!==o&&o,m=e.size,d=void 0===m?"default":m,p=(0,s.useContext(i.QO).getPrefixCls)("skeleton",t),A=(0,u.A)(e,["prefixCls"]),x=l()(p,"".concat(p,"-element"),(0,a.A)((0,a.A)({},"".concat(p,"-active"),c),"".concat(p,"-block"),v),n);return s.createElement("div",{className:x},s.createElement(f,(0,r.A)({prefixCls:"".concat(p,"-button"),size:d},A)))};var d=n(9379);const p={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M888 792H200V168c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v688c0 4.4 3.6 8 8 8h752c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM288 604a64 64 0 10128 0 64 64 0 10-128 0zm118-224a48 48 0 1096 0 48 48 0 10-96 0zm158 228a96 96 0 10192 0 96 96 0 10-192 0zm148-314a56 56 0 10112 0 56 56 0 10-112 0z"}}]},name:"dot-chart",theme:"outlined"};var A=n(3768),x=function(e,t){return s.createElement(A.A,(0,d.A)((0,d.A)({},e),{},{ref:t,icon:p}))};const g=s.forwardRef(x);const h=function(e){var t=e.prefixCls,n=e.className,r=e.style,c=e.active,o=e.children,u=(0,s.useContext(i.QO).getPrefixCls)("skeleton",t),f=l()(u,"".concat(u,"-element"),(0,a.A)({},"".concat(u,"-active"),c),n),v=null!==o&&void 0!==o?o:s.createElement(g,null);return s.createElement("div",{className:f},s.createElement("div",{className:l()("".concat(u,"-image"),n),style:r},v))};const E=function(e){var t=e.prefixCls,n=e.className,r=e.style,c=e.active,o=(0,s.useContext(i.QO).getPrefixCls)("skeleton",t),u=l()(o,"".concat(o,"-element"),(0,a.A)({},"".concat(o,"-active"),c),n);return s.createElement("div",{className:u},s.createElement("div",{className:l()("".concat(o,"-image"),n),style:r},s.createElement("svg",{viewBox:"0 0 1098 1024",xmlns:"http://www.w3.org/2000/svg",className:"".concat(o,"-image-svg")},s.createElement("path",{d:"M365.714286 329.142857q0 45.714286-32.036571 77.677714t-77.677714 32.036571-77.677714-32.036571-32.036571-77.677714 32.036571-77.677714 77.677714-32.036571 77.677714 32.036571 32.036571 77.677714zM950.857143 548.571429l0 256-804.571429 0 0-109.714286 182.857143-182.857143 91.428571 91.428571 292.571429-292.571429zM1005.714286 146.285714l-914.285714 0q-7.460571 0-12.873143 5.412571t-5.412571 12.873143l0 694.857143q0 7.460571 5.412571 12.873143t12.873143 5.412571l914.285714 0q7.460571 0 12.873143-5.412571t5.412571-12.873143l0-694.857143q0-7.460571-5.412571-12.873143t-12.873143-5.412571zM1097.142857 164.571429l0 694.857143q0 37.741714-26.843429 64.585143t-64.585143 26.843429l-914.285714 0q-37.741714 0-64.585143-26.843429t-26.843429-64.585143l0-694.857143q0-37.741714 26.843429-64.585143t64.585143-26.843429l914.285714 0q37.741714 0 64.585143 26.843429t26.843429 64.585143z",className:"".concat(o,"-image-path")}))))};const C=function(e){var t=e.prefixCls,n=e.className,c=e.active,o=e.block,v=e.size,m=void 0===v?"default":v,d=(0,s.useContext(i.QO).getPrefixCls)("skeleton",t),p=(0,u.A)(e,["prefixCls"]),A=l()(d,"".concat(d,"-element"),(0,a.A)((0,a.A)({},"".concat(d,"-active"),c),"".concat(d,"-block"),o),n);return s.createElement("div",{className:A},s.createElement(f,(0,r.A)({prefixCls:"".concat(d,"-input"),size:m},p)))};var N=n(436);const w=function(e){var t=function(t){var n=e.width,a=e.rows,r=void 0===a?2:a;return Array.isArray(n)?n[t]:r-1===t?n:void 0},n=e.prefixCls,a=e.className,r=e.style,c=e.rows,o=(0,N.A)(Array(c)).map((function(e,n){return s.createElement("li",{key:n,style:{width:t(n)}})}));return s.createElement("ul",{className:l()(n,a),style:r},o)};const y=function(e){var t=e.prefixCls,n=e.className,a=e.width,c=e.style;return s.createElement("h3",{className:l()(t,n),style:(0,r.A)({width:a},c)})};function b(e){return e&&"object"===(0,c.A)(e)?e:{}}var k=function(e){var t=e.prefixCls,n=e.loading,c=e.className,o=e.style,u=e.children,v=e.avatar,m=void 0!==v&&v,d=e.title,p=void 0===d||d,A=e.paragraph,x=void 0===A||A,g=e.active,h=e.round,E=s.useContext(i.QO),C=E.getPrefixCls,N=E.direction,k=C("skeleton",t);if(n||!("loading"in e)){var z,M,q=!!m,S=!!p,D=!!x;if(q){var O=(0,r.A)((0,r.A)({prefixCls:"".concat(k,"-avatar")},function(e,t){return e&&!t?{size:"large",shape:"square"}:{size:"large",shape:"circle"}}(S,D)),b(m));z=s.createElement("div",{className:"".concat(k,"-header")},s.createElement(f,(0,r.A)({},O)))}if(S||D){var I,P;if(S){var Q=(0,r.A)((0,r.A)({prefixCls:"".concat(k,"-title")},function(e,t){return!e&&t?{width:"38%"}:e&&t?{width:"50%"}:{}}(q,D)),b(p));I=s.createElement(y,(0,r.A)({},Q))}if(D){var R=(0,r.A)((0,r.A)({prefixCls:"".concat(k,"-paragraph")},function(e,t){var n={};return e&&t||(n.width="61%"),n.rows=!e&&t?3:2,n}(q,S)),b(x));P=s.createElement(w,(0,r.A)({},R))}M=s.createElement("div",{className:"".concat(k,"-content")},I,P)}var H=l()(k,(0,a.A)((0,a.A)((0,a.A)((0,a.A)({},"".concat(k,"-with-avatar"),q),"".concat(k,"-active"),g),"".concat(k,"-rtl"),"rtl"===N),"".concat(k,"-round"),h),c);return s.createElement("div",{className:H,style:o},z,M)}return"undefined"!==typeof u?u:null};k.Button=m,k.Avatar=v,k.Input=C,k.Image=E,k.Node=h;const z=k},532:(e,t,n)=>{"use strict";n.d(t,{A:()=>y});var a=n(8168),r=n(9284),c=n(1439),o=n(7458),l=n(4467),s=n(4480),i=n.n(s),u=n(3135),f=n(54),v=n(7860),m=n.n(v);const d=function(e){var t,n=e.value,a=e.formatter,c=e.precision,o=e.decimalSeparator,l=e.groupSeparator,s=void 0===l?"":l,i=e.prefixCls;if("function"===typeof a)t=a(n);else{var u=String(n),f=u.match(/^(-?)(\d*)(\.(\d+))?$/);if(f&&"-"!==u){var v=f[1],d=f[2]||"0",p=f[4]||"";d=d.replace(/\B(?=(\d{3})+(?!\d))/g,s),"number"===typeof c&&(p=m()(p,c,"0").slice(0,c>0?c:0)),p&&(p="".concat(o).concat(p)),t=[r.createElement("span",{key:"int",className:"".concat(i,"-content-value-int")},v,d),p&&r.createElement("span",{key:"decimal",className:"".concat(i,"-content-value-decimal")},p)]}else t=u}return r.createElement("span",{className:"".concat(i,"-content-value")},t)};const p=(0,u.by)({prefixCls:"statistic"})((function(e){var t=e.prefixCls,n=e.className,c=e.style,o=e.valueStyle,s=e.value,u=void 0===s?0:s,v=e.title,m=e.valueRender,p=e.prefix,A=e.suffix,x=e.loading,g=void 0!==x&&x,h=e.direction,E=e.onMouseEnter,C=e.onMouseLeave,N=e.decimalSeparator,w=void 0===N?".":N,y=e.groupSeparator,b=void 0===y?",":y,k=r.createElement(d,(0,a.A)({decimalSeparator:w,groupSeparator:b},e,{value:u})),z=i()(t,(0,l.A)({},"".concat(t,"-rtl"),"rtl"===h),n);return r.createElement("div",{className:z,style:c,onMouseEnter:E,onMouseLeave:C},v&&r.createElement("div",{className:"".concat(t,"-title")},v),r.createElement(f.A,{paragraph:!1,loading:g,className:"".concat(t,"-skeleton")},r.createElement("div",{style:o,className:"".concat(t,"-content")},p&&r.createElement("span",{className:"".concat(t,"-content-prefix")},p),m?m(k):k,A&&r.createElement("span",{className:"".concat(t,"-content-suffix")},A))))}));var A=n(5544),x=n(4765),g=n.n(x),h=[["Y",31536e6],["M",2592e6],["D",864e5],["H",36e5],["m",6e4],["s",1e3],["S",1]];function E(e,t){var n=t.format,a=void 0===n?"":n,r=new Date(e).getTime(),c=Date.now();return function(e,t){var n=e,a=/\[[^\]]*]/g,r=(t.match(a)||[]).map((function(e){return e.slice(1,-1)})),c=t.replace(a,"[]"),o=h.reduce((function(e,t){var a=(0,A.A)(t,2),r=a[0],c=a[1];if(e.includes(r)){var o=Math.floor(n/c);return n-=o*c,e.replace(new RegExp("".concat(r,"+"),"g"),(function(e){var t=e.length;return g()(o.toString(),t,"0")}))}return e}),c),l=0;return o.replace(a,(function(){var e=r[l];return l+=1,e}))}(Math.max(r-c,0),a)}var C=1e3/30;var N=function(e){var t=e.value,n=e.format,l=void 0===n?"HH:mm:ss":n,s=e.onChange,i=e.onFinish,u=(0,c.A)(),f=r.useRef(null),v=function(){var e=function(e){return new Date(e).getTime()}(t);e>=Date.now()&&(f.current=setInterval((function(){u(),null===s||void 0===s||s(e-Date.now()),e{var a=n(3885)("length");e.exports=a},3885:e=>{e.exports=function(e){return function(t){return null==t?void 0:t[e]}}},7723:e=>{var t=Math.floor;e.exports=function(e,n){var a="";if(!e||n<1||n>9007199254740991)return a;do{n%2&&(a+=e),(n=t(n/2))&&(e+=e)}while(n);return a}},3416:(e,t,n)=>{var a=n(7723),r=n(5532),c=n(9418),o=n(9434),l=n(5345),s=n(2880),i=Math.ceil;e.exports=function(e,t){var n=(t=void 0===t?" ":r(t)).length;if(n<2)return n?a(t,e):t;var u=a(t,i(e/l(t)));return o(t)?c(s(u),0,e).join(""):u.slice(0,e)}},5345:(e,t,n)=>{var a=n(3979),r=n(9434),c=n(5935);e.exports=function(e){return r(e)?c(e):a(e)}},5935:e=>{var t="\\ud800-\\udfff",n="["+t+"]",a="[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]",r="\\ud83c[\\udffb-\\udfff]",c="[^"+t+"]",o="(?:\\ud83c[\\udde6-\\uddff]){2}",l="[\\ud800-\\udbff][\\udc00-\\udfff]",s="(?:"+a+"|"+r+")"+"?",i="[\\ufe0e\\ufe0f]?",u=i+s+("(?:\\u200d(?:"+[c,o,l].join("|")+")"+i+s+")*"),f="(?:"+[c+a+"?",a,o,l,n].join("|")+")",v=RegExp(r+"(?="+r+")|"+f+u,"g");e.exports=function(e){for(var t=v.lastIndex=0;v.test(e);)++t;return t}},7860:(e,t,n)=>{var a=n(3416),r=n(5345),c=n(3609),o=n(8814);e.exports=function(e,t,n){e=o(e);var l=(t=c(t))?r(e):0;return t&&l{var a=n(3416),r=n(5345),c=n(3609),o=n(8814);e.exports=function(e,t,n){e=o(e);var l=(t=c(t))?r(e):0;return t&&l{var a=n(150),r=1/0;e.exports=function(e){return e?(e=a(e))===r||e===-1/0?17976931348623157e292*(e<0?-1:1):e===e?e:0:0===e?e:0}},3609:(e,t,n)=>{var a=n(9776);e.exports=function(e){var t=a(e),n=t%1;return t===t?n?t-n:t:0}}}]);
--------------------------------------------------------------------------------
/public/static/js/332.e55ab720.chunk.js:
--------------------------------------------------------------------------------
1 | "use strict";(self.webpackChunkbeancount_web=self.webpackChunkbeancount_web||[]).push([[332],{9332:(e,t,n)=>{n.r(t),n.d(t,{default:()=>h});var r=n(8380),i=n(5231),a=n(4412),l=n(9663),o=n(1896),s=n(9284),c=n(4760),d=n(2475),p=n(2069),u=n(712);class m extends s.Component{constructor(){super(...arguments),this.ledgerTitle=localStorage.getItem("ledgerTitle")||"\u8d26\u672c",this.state={loading:!1},this.handleOpenDeleteModal=()=>{i.A.confirm({title:`\u786e\u8ba4\u5220\u9664${this.ledgerTitle}\uff1f`,icon:(0,u.jsx)(r.A,{}),content:"\u5220\u9664\u540e\u5c06\u4e0d\u80fd\u6062\u590d",okText:"\u5220\u9664",onOk:this.handleDelete,okButtonProps:{danger:!0},cancelText:"\u53d6\u6d88"})},this.handleDelete=()=>{this.setState({loading:!0}),(0,c.hd)("/api/auth/ledger",{method:"DELETE"}).then((()=>{localStorage.clear(),a.Ay.success(`${this.ledgerTitle}\u5df2\u5220\u9664`),this.props.history.replace("/ledger")})).finally((()=>{this.setState({loading:!1})}))}}render(){return this.context.theme!==this.theme&&(this.theme=this.context.theme),(0,u.jsx)("div",{className:"setting-page",children:(0,u.jsx)(l.A,{direction:"vertical",size:"middle",style:{display:"flex"},children:(0,u.jsx)(o.A,{block:!0,danger:!0,loading:this.state.loading,onClick:this.handleOpenDeleteModal,children:"\u5220\u9664\u8d26\u672c"})})})}}m.contextType=d.A;const h=(0,p.A)(m)},2069:(e,t,n)=>{n.d(t,{A:()=>a});var r=n(9284),i=n(712);const a=e=>class extends r.Component{constructor(){super(...arguments),this.defaultCommodity={currency:"CNY",symbol:"\uffe5"},this.currentCommodity=window.localStorage.getItem("ledgerCurrency")}render(){return(0,i.jsx)(e,{...this.props,commodity:this.currentCommodity?JSON.parse(this.currentCommodity):this.defaultCommodity})}}},9309:(e,t,n)=>{n.d(t,{A:()=>l});var r=n(5544),i=n(9284),a=n(209);const l=function(){var e=i.useState(!1),t=(0,r.A)(e,2),n=t[0],l=t[1];return i.useEffect((function(){l((0,a.Pu)())}),[]),n}},9663:(e,t,n)=>{n.d(t,{e:()=>g,A:()=>v});var r=n(8168),i=n(4467),a=n(5544),l=n(4480),o=n.n(l),s=n(4650),c=n(9284),d=n(3135),p=n(9309);function u(e){var t=e.className,n=e.direction,a=e.index,l=e.marginDirection,o=e.children,s=e.split,d=e.wrap,p=c.useContext(g),u=p.horizontalSize,m=p.verticalSize,h=p.latestIndex,y={};return p.supportFlexGap||("vertical"===n?a{a.d(t,{A:()=>i});var n=a(9379),r=a(9284);const c={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z"}}]},name:"edit",theme:"outlined"};var l=a(3768),o=function(e,t){return r.createElement(l.A,(0,n.A)((0,n.A)({},e),{},{ref:t,icon:c}))};const i=r.forwardRef(o)},8405:(e,t,a)=>{a.d(t,{A:()=>i});var n=a(9379),r=a(9284);const c={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M925.9 804l-24-199.2c-.8-6.6-8.9-9.4-13.6-4.7L829 659.5 557.7 388.3c-6.3-6.2-16.4-6.2-22.6 0L433.3 490 156.6 213.3a8.03 8.03 0 00-11.3 0l-45 45.2a8.03 8.03 0 000 11.3L422 591.7c6.2 6.3 16.4 6.3 22.6 0L546.4 490l226.1 226-59.3 59.3a8.01 8.01 0 004.7 13.6l199.2 24c5.1.7 9.5-3.7 8.8-8.9z"}}]},name:"fall",theme:"outlined"};var l=a(3768),o=function(e,t){return r.createElement(l.A,(0,n.A)((0,n.A)({},e),{},{ref:t,icon:c}))};const i=r.forwardRef(o)},4680:(e,t,a)=>{a.d(t,{A:()=>i});var n=a(9379),r=a(9284);const c={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M904 512h-56c-4.4 0-8 3.6-8 8v320H184V184h320c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V520c0-4.4-3.6-8-8-8z"}},{tag:"path",attrs:{d:"M355.9 534.9L354 653.8c-.1 8.9 7.1 16.2 16 16.2h.4l118-2.9c2-.1 4-.9 5.4-2.3l415.9-415c3.1-3.1 3.1-8.2 0-11.3L785.4 114.3c-1.6-1.6-3.6-2.3-5.7-2.3s-4.1.8-5.7 2.3l-415.8 415a8.3 8.3 0 00-2.3 5.6zm63.5 23.6L779.7 199l45.2 45.1-360.5 359.7-45.7 1.1.7-46.4z"}}]},name:"form",theme:"outlined"};var l=a(3768),o=function(e,t){return r.createElement(l.A,(0,n.A)((0,n.A)({},e),{},{ref:t,icon:c}))};const i=r.forwardRef(o)},6684:(e,t,a)=>{a.d(t,{A:()=>i});var n=a(9379),r=a(9284);const c={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M742 318V184h86c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H196c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h86v134c0 81.5 42.4 153.2 106.4 194-64 40.8-106.4 112.5-106.4 194v134h-86c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h632c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-86V706c0-81.5-42.4-153.2-106.4-194 64-40.8 106.4-112.5 106.4-194zm-72 388v134H354V706c0-42.2 16.4-81.9 46.3-111.7C430.1 564.4 469.8 548 512 548s81.9 16.4 111.7 46.3C653.6 624.1 670 663.8 670 706zm0-388c0 42.2-16.4 81.9-46.3 111.7C593.9 459.6 554.2 476 512 476s-81.9-16.4-111.7-46.3A156.63 156.63 0 01354 318V184h316v134z"}}]},name:"hourglass",theme:"outlined"};var l=a(3768),o=function(e,t){return r.createElement(l.A,(0,n.A)((0,n.A)({},e),{},{ref:t,icon:c}))};const i=r.forwardRef(o)},5504:(e,t,a)=>{a.d(t,{A:()=>i});var n=a(9379),r=a(9284);const c={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M917 211.1l-199.2 24c-6.6.8-9.4 8.9-4.7 13.6l59.3 59.3-226 226-101.8-101.7c-6.3-6.3-16.4-6.2-22.6 0L100.3 754.1a8.03 8.03 0 000 11.3l45 45.2c3.1 3.1 8.2 3.1 11.3 0L433.3 534 535 635.7c6.3 6.2 16.4 6.2 22.6 0L829 364.5l59.3 59.3a8.01 8.01 0 0013.6-4.7l24-199.2c.7-5.1-3.7-9.5-8.9-8.8z"}}]},name:"rise",theme:"outlined"};var l=a(3768),o=function(e,t){return r.createElement(l.A,(0,n.A)((0,n.A)({},e),{},{ref:t,icon:c}))};const i=r.forwardRef(o)},6411:(e,t,a)=>{a.d(t,{A:()=>i});var n=a(9379),r=a(9284);const c={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"}}]},name:"setting",theme:"outlined"};var l=a(3768),o=function(e,t){return r.createElement(l.A,(0,n.A)((0,n.A)({},e),{},{ref:t,icon:c}))};const i=r.forwardRef(o)},323:(e,t,a)=>{a.d(t,{A:()=>i});var n=a(9379),r=a(9284);const c={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M483.2 790.3L861.4 412c1.7-1.7 2.5-4 2.3-6.3l-25.5-301.4c-.7-7.8-6.8-13.9-14.6-14.6L522.2 64.3c-2.3-.2-4.7.6-6.3 2.3L137.7 444.8a8.03 8.03 0 000 11.3l334.2 334.2c3.1 3.2 8.2 3.2 11.3 0zm62.6-651.7l224.6 19 19 224.6L477.5 694 233.9 450.5l311.9-311.9zm60.16 186.23a48 48 0 1067.88-67.89 48 48 0 10-67.88 67.89zM889.7 539.8l-39.6-39.5a8.03 8.03 0 00-11.3 0l-362 361.3-237.6-237a8.03 8.03 0 00-11.3 0l-39.6 39.5a8.03 8.03 0 000 11.3l243.2 242.8 39.6 39.5c3.1 3.1 8.2 3.1 11.3 0l407.3-406.6c3.1-3.1 3.1-8.2 0-11.3z"}}]},name:"tags",theme:"outlined"};var l=a(3768),o=function(e,t){return r.createElement(l.A,(0,n.A)((0,n.A)({},e),{},{ref:t,icon:c}))};const i=r.forwardRef(o)},4923:(e,t,a)=>{function n(e){return Object.keys(e).reduce((function(t,a){return!a.startsWith("data-")&&!a.startsWith("aria-")&&"role"!==a||a.startsWith("data-__")||(t[a]=e[a]),t}),{})}a.d(t,{A:()=>n})},9641:(e,t,a)=>{a.d(t,{A:()=>g});var n=a(8168),r=a(2284),c=a(5544),l=a(4480),o=a.n(l),i=a(4650),s=a(37),u=a(9284),d=a(3135),f=a(9636),p=a(7458),m=f.A.Option;function v(e){return e&&e.type&&(e.type.isSelectOption||e.type.isSelectOptGroup)}var h=function(e,t){var a,l=e.prefixCls,h=e.className,A=e.popupClassName,g=e.dropdownClassName,y=e.children,x=e.dataSource,z=(0,i.A)(y);if(1===z.length&&(0,p.zO)(z[0])&&!v(z[0])){var w=(0,c.A)(z,1);a=w[0]}var O,b=a?function(){return a}:void 0;return O=z.length&&v(z[0])?y:x?x.map((function(e){if((0,p.zO)(e))return e;switch((0,r.A)(e)){case"string":return u.createElement(m,{key:e,value:e},e);case"object":var t=e.value;return u.createElement(m,{key:t,value:t},e.text);default:return}})):[],u.createElement(d.TG,null,(function(a){var r=(0,a.getPrefixCls)("select",l);return u.createElement(f.A,(0,n.A)({ref:t},(0,s.A)(e,["dataSource"]),{prefixCls:r,popupClassName:A||g,className:o()("".concat(r,"-auto-complete"),h),mode:f.A.SECRET_COMBOBOX_MODE_DO_NOT_USE},{getInputElement:b}),O)}))},A=u.forwardRef(h);A.Option=m;const g=A},2125:(e,t,a)=>{a.d(t,{A:()=>u});var n=a(8168),r=a(4467),c=a(4480),l=a.n(c),o=a(9284),i=a(3135),s=function(e,t){var a={};for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.indexOf(n)<0&&(a[n]=e[n]);if(null!=e&&"function"===typeof Object.getOwnPropertySymbols){var r=0;for(n=Object.getOwnPropertySymbols(e);r0?"-".concat(m):m,O=!!A,b="left"===m&&null!=v,E="right"===m&&null!=v,C=l()(z,"".concat(z,"-").concat(f),(0,r.A)((0,r.A)((0,r.A)((0,r.A)((0,r.A)((0,r.A)((0,r.A)({},"".concat(z,"-with-text"),O),"".concat(z,"-with-text").concat(w),O),"".concat(z,"-dashed"),!!g),"".concat(z,"-plain"),!!y),"".concat(z,"-rtl"),"rtl"===c),"".concat(z,"-no-default-orientation-margin-left"),b),"".concat(z,"-no-default-orientation-margin-right"),E),h),L=(0,n.A)((0,n.A)({},b&&{marginLeft:v}),E&&{marginRight:v});return o.createElement("div",(0,n.A)({className:C},x,{role:"separator"}),A&&"vertical"!==f&&o.createElement("span",{className:"".concat(z,"-inner-text"),style:L},A))}},9663:(e,t,a)=>{a.d(t,{e:()=>v,A:()=>g});var n=a(8168),r=a(4467),c=a(5544),l=a(4480),o=a.n(l),i=a(4650),s=a(9284),u=a(3135),d=a(9309);function f(e){var t=e.className,a=e.direction,c=e.index,l=e.marginDirection,o=e.children,i=e.split,u=e.wrap,d=s.useContext(v),f=d.horizontalSize,p=d.verticalSize,m=d.latestIndex,h={};return d.supportFlexGap||("vertical"===a?c{n.r(t),n.d(t,{default:()=>D});var s=n(9284),r=n(9379);const i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-.3-4.2-3.9-7.4-8.1-7.4H360a8 8 0 00-8 8.4c4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6a8 8 0 00-8-8.4z"}}]},name:"smile",theme:"outlined"};var a=n(3768),o=function(e,t){return s.createElement(a.A,(0,r.A)((0,r.A)({},e),{},{ref:t,icon:i}))};const l=s.forwardRef(o);var c=n(2069),d=n(2475),p=n(1896),h=n(7691),m=n(8168),u=n(4467),v=n(5547),y=n(4480),f=n.n(y),g=n(3135),x=n(7458),A=function(e,t){var n={};for(var s in e)Object.prototype.hasOwnProperty.call(e,s)&&t.indexOf(s)<0&&(n[s]=e[s]);if(null!=e&&"function"===typeof Object.getOwnPropertySymbols){var r=0;for(s=Object.getOwnPropertySymbols(e);r{const[t]=E.A.useForm(),[n,r]=(0,s.useState)(!1);return(0,P.jsx)("div",{className:"add-event-drawer component",children:(0,P.jsx)(S.A,{title:"\u65b0\u589e\u4e8b\u4ef6",placement:"bottom",closable:!0,height:"60vh",className:"page-drawer",bodyStyle:{display:"flex",justifyContent:"center"},forceRender:!0,...e,children:(0,P.jsx)("div",{className:"page-form",children:(0,P.jsxs)(E.A,{name:"add-event-form",className:"page-form",size:"large",style:{textAlign:"left"},form:t,onFinish:()=>{const n=t.getFieldsValue();r(!0),(0,I.hd)("/api/auth/event",{method:"POST",body:n}).then((n=>{t.resetFields(),e.onClose(n)})).catch(console.error).finally((()=>{r(!1)}))},validateMessages:Y,children:[(0,P.jsx)(E.A.Item,{name:"date",initialValue:N()().format("YYYY-MM-DD"),rules:[{required:!0}],children:(0,P.jsx)(T.A,{type:"date",placeholder:"\u65f6\u95f4"})}),(0,P.jsx)(E.A.Item,{name:"types",rules:[{required:!0}],children:(0,P.jsx)(M.A,{mode:"tags",allowClear:!0,placeholder:"\u4e8b\u4ef6\u540d\u79f0",options:(e.types||[]).map((e=>({label:e,value:e})))})}),(0,P.jsx)(E.A.Item,{name:"description",rules:[{required:!0}],children:(0,P.jsx)(T.A,{placeholder:"\u4e8b\u4ef6\u5185\u5bb9"})}),(0,P.jsx)(E.A.Item,{children:(0,P.jsx)(p.A,{type:"primary",htmlType:"submit",loading:n,className:"submit-button",children:"\u4fdd\u5b58"})})]})})})})};var L=n(9811);class k extends s.Component{constructor(){super(...arguments),this.theme=this.context.theme,this.currentMonth=N()().format("YYYY-M"),this.eventTypeList=[],this.eventTypes=[],this.state={loading:!1,events:[],selectedMonth:this.currentMonth,drawerOpen:!1},this.handleOpenAddrawer=()=>{this.setState({drawerOpen:!0})},this.handleCloseAddDrawer=e=>{e&&e instanceof Array&&this.setState({events:[...this.state.events,...e]},(()=>{this.formatEventTypeList(this.state.events)})),this.setState({drawerOpen:!1})},this.formatEventTypeList=e=>{const t={};e.forEach((e=>{let{date:n,type:s,description:r}=e;t[s]?t[s].push({date:n,type:s,description:r}):t[s]=[{date:n,type:s,description:r}]})),this.eventTypeList=[],this.eventTypes=Object.keys(t).sort(),this.eventTypes.forEach((e=>{this.eventTypeList.push({type:e,events:t[e]||[]})}))},this.getAllEvents=()=>{this.setState({loading:!0}),(0,I.hd)("/api/auth/event/all").then((e=>{this.setState({events:e},(()=>{this.formatEventTypeList(e)}))})).catch(console.error).finally((()=>{this.setState({loading:!1})}))}}componentDidMount(){this.getAllEvents()}render(){return this.context.theme!==this.theme&&(this.theme=this.context.theme),(0,P.jsxs)("div",{className:"event-page",children:[(0,P.jsx)(z,{open:this.state.drawerOpen,types:this.eventTypes,onClose:this.handleCloseAddDrawer}),(0,P.jsx)("div",{className:"top-wrapper",children:(0,P.jsx)("div",{children:(0,P.jsx)(p.A,{size:"small",icon:(0,P.jsx)(l,{}),onClick:this.handleOpenAddrawer,children:"\u8bb0\u5f55\u4e8b\u4ef6"})})}),(0,P.jsx)("div",{children:this.state.loading?(0,P.jsx)(L.A,{}):(0,P.jsx)(h.A,{defaultActiveKey:"1",items:this.eventTypeList.map((e=>{let{type:t,events:n}=e;return{label:t,key:t,children:(0,P.jsx)(O,{children:n.map((e=>{let{date:t,description:n}=e;return(0,P.jsxs)(O.Item,{children:[n,(0,P.jsx)("span",{style:{fontSize:"12px",marginLeft:"10px",color:"gray"},children:t})]})}))})}}))})})]})}}k.contextType=d.A;const D=(0,c.A)(k)},2069:(e,t,n)=>{n.d(t,{A:()=>i});var s=n(9284),r=n(712);const i=e=>class extends s.Component{constructor(){super(...arguments),this.defaultCommodity={currency:"CNY",symbol:"\uffe5"},this.currentCommodity=window.localStorage.getItem("ledgerCurrency")}render(){return(0,r.jsx)(e,{...this.props,commodity:this.currentCommodity?JSON.parse(this.currentCommodity):this.defaultCommodity})}}}}]);
--------------------------------------------------------------------------------
/public/static/js/619.63e56027.chunk.js:
--------------------------------------------------------------------------------
1 | "use strict";(self.webpackChunkbeancount_web=self.webpackChunkbeancount_web||[]).push([[619],{1619:(e,n,t)=>{t.d(n,{A:()=>D});var o=t(8168),a=t(4467),r=t(5544),l=t(5691),i=t(4480),c=t.n(i),s=t(9379),u=t(9284),d=t(9471),v=t(909),f=t(7285),m=t(6184),p=t(3209);const y=u.createContext(null);const h=function(e){var n=e.prefixCls,t=e.className,a=e.style,r=e.children,l=e.containerRef,i=e.id,d={onMouseEnter:e.onMouseEnter,onMouseOver:e.onMouseOver,onMouseLeave:e.onMouseLeave,onClick:e.onClick,onKeyDown:e.onKeyDown,onKeyUp:e.onKeyUp};return u.createElement(u.Fragment,null,u.createElement("div",(0,o.A)({id:i,className:c()("".concat(n,"-content"),t),style:(0,s.A)({},a),"aria-modal":"true",role:"dialog",ref:l},d),r))};var C=t(2042);function b(e){return"string"===typeof e&&String(Number(e))===e?((0,C.Ay)(!1,"Invalid value type of `width` or `height` which should be number type instead."),Number(e)):e}var E={width:0,height:0,overflow:"hidden",outline:"none",position:"absolute"};function k(e,n){var t,l,i,d,v=e.prefixCls,C=e.open,k=e.placement,A=e.inline,g=e.push,w=e.forceRender,N=e.autoFocus,x=e.keyboard,O=e.rootClassName,M=e.rootStyle,S=e.zIndex,K=e.className,D=e.id,R=e.style,I=e.motion,L=e.width,P=e.height,U=e.children,j=e.contentWrapperStyle,z=e.mask,F=e.maskClosable,V=e.maskMotion,B=e.maskClassName,X=e.maskStyle,T=e.afterOpenChange,Y=e.onClose,_=e.onMouseEnter,H=e.onMouseOver,Q=e.onMouseLeave,W=e.onClick,q=e.onKeyDown,G=e.onKeyUp,J=u.useRef(),Z=u.useRef(),$=u.useRef();u.useImperativeHandle(n,(function(){return J.current}));u.useEffect((function(){var e;C&&N&&(null===(e=J.current)||void 0===e||e.focus({preventScroll:!0}))}),[C]);var ee=u.useState(!1),ne=(0,r.A)(ee,2),te=ne[0],oe=ne[1],ae=u.useContext(y),re=null!==(t=null!==(l=null===(i=!1===g?{distance:0}:!0===g?{}:g||{})||void 0===i?void 0:i.distance)&&void 0!==l?l:null===ae||void 0===ae?void 0:ae.pushDistance)&&void 0!==t?t:180,le=u.useMemo((function(){return{pushDistance:re,push:function(){oe(!0)},pull:function(){oe(!1)}}}),[re]);u.useEffect((function(){var e,n;C?null===ae||void 0===ae||null===(e=ae.push)||void 0===e||e.call(ae):null===ae||void 0===ae||null===(n=ae.pull)||void 0===n||n.call(ae)}),[C]),u.useEffect((function(){return function(){var e;null===ae||void 0===ae||null===(e=ae.pull)||void 0===e||e.call(ae)}}),[]);var ie=z&&u.createElement(f.Ay,(0,o.A)({key:"mask"},V,{visible:C}),(function(e,n){var t=e.className,o=e.style;return u.createElement("div",{className:c()("".concat(v,"-mask"),t,B),style:(0,s.A)((0,s.A)({},o),X),onClick:F&&C?Y:void 0,ref:n})})),ce="function"===typeof I?I(k):I,se={};if(te&&re)switch(k){case"top":se.transform="translateY(".concat(re,"px)");break;case"bottom":se.transform="translateY(".concat(-re,"px)");break;case"left":se.transform="translateX(".concat(re,"px)");break;default:se.transform="translateX(".concat(-re,"px)")}"left"===k||"right"===k?se.width=b(L):se.height=b(P);var ue={onMouseEnter:_,onMouseOver:H,onMouseLeave:Q,onClick:W,onKeyDown:q,onKeyUp:G},de=u.createElement(f.Ay,(0,o.A)({key:"panel"},ce,{visible:C,forceRender:w,onVisibleChanged:function(e){null===T||void 0===T||T(e)},removeOnLeave:!1,leavedClassName:"".concat(v,"-content-wrapper-hidden")}),(function(n,t){var a=n.className,r=n.style;return u.createElement("div",(0,o.A)({className:c()("".concat(v,"-content-wrapper"),a),style:(0,s.A)((0,s.A)((0,s.A)({},se),r),j)},(0,p.A)(e,{data:!0})),u.createElement(h,(0,o.A)({id:D,containerRef:t,prefixCls:v,className:K,style:R},ue),U))})),ve=(0,s.A)({},M);return S&&(ve.zIndex=S),u.createElement(y.Provider,{value:le},u.createElement("div",{className:c()(v,"".concat(v,"-").concat(k),O,(d={},(0,a.A)(d,"".concat(v,"-open"),C),(0,a.A)(d,"".concat(v,"-inline"),A),d)),style:ve,tabIndex:-1,ref:J,onKeyDown:function(e){var n=e.keyCode,t=e.shiftKey;switch(n){case m.A.TAB:var o;if(n===m.A.TAB)if(t||document.activeElement!==$.current){if(t&&document.activeElement===Z.current){var a;null===(a=$.current)||void 0===a||a.focus({preventScroll:!0})}}else null===(o=Z.current)||void 0===o||o.focus({preventScroll:!0});break;case m.A.ESC:Y&&x&&(e.stopPropagation(),Y(e))}}},ie,u.createElement("div",{tabIndex:0,ref:Z,style:E,"aria-hidden":"true","data-sentinel":"start"}),de,u.createElement("div",{tabIndex:0,ref:$,style:E,"aria-hidden":"true","data-sentinel":"end"})))}const A=u.forwardRef(k);const g=function(e){var n=e.open,t=void 0!==n&&n,o=e.prefixCls,a=void 0===o?"rc-drawer":o,l=e.placement,i=void 0===l?"right":l,c=e.autoFocus,f=void 0===c||c,m=e.keyboard,p=void 0===m||m,y=e.width,h=void 0===y?378:y,C=e.mask,b=void 0===C||C,E=e.maskClosable,k=void 0===E||E,g=e.getContainer,w=e.forceRender,N=e.afterOpenChange,x=e.destroyOnClose,O=e.onMouseEnter,M=e.onMouseOver,S=e.onMouseLeave,K=e.onClick,D=e.onKeyDown,R=e.onKeyUp,I=u.useState(!1),L=(0,r.A)(I,2),P=L[0],U=L[1];var j=u.useState(!1),z=(0,r.A)(j,2),F=z[0],V=z[1];(0,v.A)((function(){V(!0)}),[]);var B=!!F&&t,X=u.useRef(),T=u.useRef();(0,v.A)((function(){B&&(T.current=document.activeElement)}),[B]);if(!w&&!P&&!B&&x)return null;var Y={onMouseEnter:O,onMouseOver:M,onMouseLeave:S,onClick:K,onKeyDown:D,onKeyUp:R},_=(0,s.A)((0,s.A)({},e),{},{open:B,prefixCls:a,placement:i,autoFocus:f,keyboard:p,width:h,mask:b,maskClosable:k,inline:!1===g,afterOpenChange:function(e){var n,t;(U(e),null===N||void 0===N||N(e),e||!T.current||(null===(n=X.current)||void 0===n?void 0:n.contains(T.current)))||(null===(t=T.current)||void 0===t||t.focus({preventScroll:!0}))},ref:X},Y);return u.createElement(d.A,{open:B||w||P,autoDestroy:!1,getContainer:g,autoLock:b&&(B||P)},u.createElement(A,_))};var w=t(3135),N=t(713),x=t(259),O=t(993),M=t(4575),S=function(e,n){var t={};for(var o in e)Object.prototype.hasOwnProperty.call(e,o)&&n.indexOf(o)<0&&(t[o]=e[o]);if(null!=e&&"function"===typeof Object.getOwnPropertySymbols){var a=0;for(o=Object.getOwnPropertySymbols(e);a{s.r(e),s.d(e,{default:()=>h});var o=s(9284),r=s(2069),n=s(2475),c=s(712);class m extends o.Component{constructor(){super(...arguments),this.theme=this.context.theme}render(){return this.context.theme!==this.theme&&(this.theme=this.context.theme),(0,c.jsx)("div",{className:"about-page",children:"\u5173\u4e8e"})}}m.contextType=n.A;const h=(0,r.A)(m)},2069:(t,e,s)=>{s.d(e,{A:()=>n});var o=s(9284),r=s(712);const n=t=>class extends o.Component{constructor(){super(...arguments),this.defaultCommodity={currency:"CNY",symbol:"\uffe5"},this.currentCommodity=window.localStorage.getItem("ledgerCurrency")}render(){return(0,r.jsx)(t,{...this.props,commodity:this.currentCommodity?JSON.parse(this.currentCommodity):this.defaultCommodity})}}}}]);
--------------------------------------------------------------------------------
/public/static/js/756.c03ffef3.chunk.js:
--------------------------------------------------------------------------------
1 | (self.webpackChunkbeancount_web=self.webpackChunkbeancount_web||[]).push([[756],{5756:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>x});var a=n(1516),r=n(1896),o=n(6668),i=n(1961),c=n(2942),s=n(6694),l=n(9492),u=n(5069),d=n(9284),m=n(4760),f=n(2475),h=n(2069),p=n(712);const v={required:"${label} \u4e0d\u80fd\u4e3a\u7a7a\uff01"};class y extends d.Component{constructor(){super(...arguments),this.formRef=d.createRef(),this.state={loading:!1,checkStatus:"loading",showForm:!1,config:{}},this.checkReq=()=>{this.setState({loading:!0}),(0,m.hd)("/api/check",{method:"POST"}).then((e=>{this.setState({checkStatus:"ok"})})).catch((()=>{this.setState({checkStatus:"error"})})).finally((()=>{this.setState({loading:!1})}))},this.handleNextStep=()=>{this.setState({loading:!0}),(0,m.hd)("/api/config",{method:"GET"}).then((e=>{this.setState({config:e,showForm:!0})})).finally((()=>{this.setState({loading:!1})}))},this.handleReCheck=()=>{this.checkReq()},this.handleSubmitServerConfig=e=>{this.setState({loading:!0}),(0,m.hd)("/api/config",{method:"POST",body:e}).then((()=>{this.props.history.replace("/ledger")})).finally((()=>{this.setState({loading:!1})}))}}componentDidMount(){this.checkReq()}render(){return"error"===this.state.checkStatus?(0,p.jsxs)("div",{children:[(0,p.jsx)(a.A,{message:"\u68c0\u6d4b\u5931\u8d25",description:"\u4f9d\u8d56\u672a\u5b89\u88c5\uff0c\u8bf7\u5148\u5b89\u88c5 beancount",type:"error",showIcon:!0}),(0,p.jsx)("div",{style:{marginTop:"1rem"},children:(0,p.jsx)(r.A,{block:!0,type:"danger",loading:this.state.loading,onClick:this.handleReCheck,children:"\u91cd\u65b0\u68c0\u6d4b"})}),(0,p.jsx)("div",{style:{marginTop:"1rem"},children:(0,p.jsx)("a",{href:"https://www.yuque.com/chuyi-ble7p/beancount-ns/sqwwqa#RwqnF",target:"_blank",children:"\u600e\u4e48\u5b89\u88c5 beancount ?"})})]}):"ok"===this.state.checkStatus?this.state.showForm?(0,p.jsx)("div",{children:(0,p.jsxs)(o.A,{name:"init-form",className:"page-form",size:"middle",layout:"vertical",style:{textAlign:"left"},ref:this.formRef,onFinish:this.handleSubmitServerConfig,validateMessages:v,children:[(0,p.jsx)(o.A.Item,{label:(0,p.jsxs)(d.Fragment,{children:[(0,p.jsx)("span",{children:"\u8d26\u672c\u5b58\u50a8\u4f4d\u7f6e"}),"\xa0",(0,p.jsx)(i.A,{title:"\u5982\u679c\u662fdocker\u5bb9\u5668\u90e8\u7f72\uff0c\u6b64\u5904\u9ed8\u8ba4\u4e3a\uff1a/data/beancount",children:(0,p.jsx)(u.A,{})})]}),name:"dataPath",initialValue:this.state.config.dataPath,rules:[{required:!0}],children:(0,p.jsx)(c.A,{placeholder:"\u8d26\u672c\u5b58\u50a8\u4f4d\u7f6e"})}),(0,p.jsx)(o.A.Item,{label:"\u8d26\u672c\u5f00\u59cb\u65e5\u671f",name:"startDate",initialValue:this.state.config.startDate,rules:[{required:!0}],children:(0,p.jsx)(c.A,{type:"date",placeholder:"\u8d26\u672c\u5f00\u59cb\u65e5\u671f"})}),(0,p.jsx)(o.A.Item,{label:"\u5e01\u79cd",name:"operatingCurrency",initialValue:this.state.config.operatingCurrency,rules:[{required:!0}],children:(0,p.jsx)(c.A,{placeholder:"\u5e01\u79cd"})}),(0,p.jsx)(o.A.Item,{label:"\u5e73\u8861\u8d26\u6237\u540d\u79f0\u8bbe\u7f6e",name:"openingBalances",initialValue:this.state.config.openingBalances,rules:[{required:!0}],children:(0,p.jsx)(c.A,{placeholder:"\u5e73\u8861\u8d26\u6237\u540d\u79f0\u8bbe\u7f6e"})}),(0,p.jsx)(o.A.Item,{label:"\u4fee\u6539\u6e90\u6587\u4ef6\u65f6\u662f\u5426\u5907\u4efd\u6570\u636e",name:"isBak",valuePropName:"checked",initialValue:this.state.config.isBak,children:(0,p.jsx)(s.A,{})}),(0,p.jsx)(o.A.Item,{label:(0,p.jsxs)(d.Fragment,{children:[(0,p.jsx)("span",{children:"\u5bc6\u94a5"}),"\xa0",(0,p.jsx)(i.A,{title:"\u53ef\u4ee5\u5728\u542f\u52a8\u65e5\u5fd7\u4e2d\u67e5\u770b",children:(0,p.jsx)(u.A,{})})]}),name:"secret",rules:[{required:!0}],children:(0,p.jsx)(c.A.Password,{placeholder:"\u5bc6\u94a5"})}),(0,p.jsx)(o.A.Item,{children:(0,p.jsx)(r.A,{block:!0,type:"primary",htmlType:"submit",loading:this.state.loading,className:"submit-button",children:"\u786e\u8ba4"})})]})}):(0,p.jsxs)("div",{children:[(0,p.jsx)(a.A,{message:"\u68c0\u6d4b\u901a\u8fc7",description:"beancount\u5df2\u5b89\u88c5\uff0c\u70b9\u51fb\u4e0b\u4e00\u6b65\u6765\u5b8c\u6210\u521d\u59cb\u914d\u7f6e",type:"success",showIcon:!0}),(0,p.jsx)("div",{style:{marginTop:"1rem"}}),(0,p.jsx)(r.A,{type:"primary",block:!0,onClick:this.handleNextStep,children:"\u4e0b\u4e00\u6b65"})]}):(0,p.jsx)(l.A,{tip:"Loading...",children:(0,p.jsx)(a.A,{message:"\u68c0\u6d4b\u4e2d",description:"\u6b63\u5728\u68c0\u6d4b beancount \u662f\u5426\u5df2\u5b89\u88c5",type:"info",showIcon:!0})})}}y.contextType=f.A;const x=(0,h.A)(y)},2069:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var a=n(9284),r=n(712);const o=e=>class extends a.Component{constructor(){super(...arguments),this.defaultCommodity={currency:"CNY",symbol:"\uffe5"},this.currentCommodity=window.localStorage.getItem("ledgerCurrency")}render(){return(0,r.jsx)(e,{...this.props,commodity:this.currentCommodity?JSON.parse(this.currentCommodity):this.defaultCommodity})}}},4923:(e,t,n)=>{"use strict";function a(e){return Object.keys(e).reduce((function(t,n){return!n.startsWith("data-")&&!n.startsWith("aria-")&&"role"!==n||n.startsWith("data-__")||(t[n]=e[n]),t}),{})}n.d(t,{A:()=>a})},1516:(e,t,n)=>{"use strict";n.d(t,{A:()=>M});var a=n(8168),r=n(5544),o=n(4467),i=n(9335),c=n(5185),s=n(5252),l=n(8943),u=n(5691),d=n(8994),m=n(8380),f=n(9708),h=n(8713),p=n(4480),v=n.n(p),y=n(7285),x=n(9284),g=n(3135),A=n(4923),b=n(7458),C=n(3029),N=n(2901),j=n(6822),k=n(2176),w=n(3954),E=n(5501);const O=function(e){function t(){var e,n,a,r;return(0,C.A)(this,t),n=this,a=t,r=arguments,a=(0,w.A)(a),(e=(0,j.A)(n,(0,k.A)()?Reflect.construct(a,r||[],(0,w.A)(n).constructor):a.apply(n,r))).state={error:void 0,info:{componentStack:""}},e}return(0,E.A)(t,e),(0,N.A)(t,[{key:"componentDidCatch",value:function(e,t){this.setState({error:e,info:t})}},{key:"render",value:function(){var e=this.props,t=e.message,n=e.description,a=e.children,r=this.state,o=r.error,i=r.info,c=i&&i.componentStack?i.componentStack:null,s="undefined"===typeof t?(o||"").toString():t,l="undefined"===typeof n?c:n;return o?x.createElement(M,{type:"error",message:s,description:x.createElement("pre",null,l)}):a}}]),t}(x.Component);var S=function(e,t){var n={};for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&t.indexOf(a)<0&&(n[a]=e[a]);if(null!=e&&"function"===typeof Object.getOwnPropertySymbols){var r=0;for(a=Object.getOwnPropertySymbols(e);r{"use strict";n.d(t,{A:()=>g});var a=n(8168),r=n(4467),o=n(5544),i=n(4480),c=n.n(i),s=n(3781),l=n.n(s),u=n(37),d=n(9284),m=n(3135),f=n(7458),h=n(993),p=function(e,t){var n={};for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&t.indexOf(a)<0&&(n[a]=e[a]);if(null!=e&&"function"===typeof Object.getOwnPropertySymbols){var r=0;for(a=Object.getOwnPropertySymbols(e);r{"use strict";n.d(t,{A:()=>b});var a=n(8168),r=n(4467),o=n(5547),i=n(4480),c=n.n(i),s=n(5544),l=n(45),u=n(9284),d=n(4413),m=n(6184),f=u.forwardRef((function(e,t){var n,a=e.prefixCls,o=void 0===a?"rc-switch":a,i=e.className,f=e.checked,h=e.defaultChecked,p=e.disabled,v=e.loadingIcon,y=e.checkedChildren,x=e.unCheckedChildren,g=e.onClick,A=e.onChange,b=e.onKeyDown,C=(0,l.A)(e,["prefixCls","className","checked","defaultChecked","disabled","loadingIcon","checkedChildren","unCheckedChildren","onClick","onChange","onKeyDown"]),N=(0,d.A)(!1,{value:f,defaultValue:h}),j=(0,s.A)(N,2),k=j[0],w=j[1];function E(e,t){var n=k;return p||(w(n=e),null===A||void 0===A||A(n,t)),n}var O=c()(o,i,(n={},(0,r.A)(n,"".concat(o,"-checked"),k),(0,r.A)(n,"".concat(o,"-disabled"),p),n));return u.createElement("button",Object.assign({},C,{type:"button",role:"switch","aria-checked":k,disabled:p,className:O,ref:t,onKeyDown:function(e){e.which===m.A.LEFT?E(!1,e):e.which===m.A.RIGHT&&E(!0,e),null===b||void 0===b||b(e)},onClick:function(e){var t=E(!k,e);null===g||void 0===g||g(t,e)}}),v,u.createElement("span",{className:"".concat(o,"-inner")},k?y:x))}));f.displayName="Switch";const h=f;var p=n(3135),v=n(1151),y=n(9416),x=n(8394),g=function(e,t){var n={};for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&t.indexOf(a)<0&&(n[a]=e[a]);if(null!=e&&"function"===typeof Object.getOwnPropertySymbols){var r=0;for(a=Object.getOwnPropertySymbols(e);r{var a=n(3712),r=/^\s+/;e.exports=function(e){return e?e.slice(0,a(e)+1).replace(r,""):e}},3712:e=>{var t=/\s/;e.exports=function(e){for(var n=e.length;n--&&t.test(e.charAt(n)););return n}},3781:(e,t,n)=>{var a=n(8469),r=n(5236),o=n(150),i=Math.max,c=Math.min;e.exports=function(e,t,n){var s,l,u,d,m,f,h=0,p=!1,v=!1,y=!0;if("function"!=typeof e)throw new TypeError("Expected a function");function x(t){var n=s,a=l;return s=l=void 0,h=t,d=e.apply(a,n)}function g(e){var n=e-f;return void 0===f||n>=t||n<0||v&&e-h>=u}function A(){var e=r();if(g(e))return b(e);m=setTimeout(A,function(e){var n=t-(e-f);return v?c(n,u-(e-h)):n}(e))}function b(e){return m=void 0,y&&s?x(e):(s=l=void 0,d)}function C(){var e=r(),n=g(e);if(s=arguments,l=this,f=e,n){if(void 0===m)return function(e){return h=e,m=setTimeout(A,t),p?x(e):d}(f);if(v)return clearTimeout(m),m=setTimeout(A,t),x(f)}return void 0===m&&(m=setTimeout(A,t)),d}return t=o(t)||0,a(n)&&(p=!!n.leading,u=(v="maxWait"in n)?i(o(n.maxWait)||0,t):u,y="trailing"in n?!!n.trailing:y),C.cancel=function(){void 0!==m&&clearTimeout(m),h=0,s=f=l=m=void 0},C.flush=function(){return void 0===m?d:b(r())},C}},8469:e=>{e.exports=function(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}},5236:(e,t,n)=>{var a=n(4133);e.exports=function(){return a.Date.now()}},150:(e,t,n)=>{var a=n(5464),r=n(8469),o=n(242),i=/^[-+]0x[0-9a-f]+$/i,c=/^0b[01]+$/i,s=/^0o[0-7]+$/i,l=parseInt;e.exports=function(e){if("number"==typeof e)return e;if(o(e))return NaN;if(r(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=r(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=a(e);var n=c.test(e);return n||s.test(e)?l(e.slice(2),n?2:8):i.test(e)?NaN:+e}}}]);
--------------------------------------------------------------------------------
/public/static/js/806.c1039356.chunk.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /*! *****************************************************************************
8 | Copyright (c) Microsoft Corporation.
9 |
10 | Permission to use, copy, modify, and/or distribute this software for any
11 | purpose with or without fee is hereby granted.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
14 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
16 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
17 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
18 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
19 | PERFORMANCE OF THIS SOFTWARE.
20 | ***************************************************************************** */
21 |
22 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
23 |
24 | /** @license React v0.19.1
25 | * scheduler.production.min.js
26 | *
27 | * Copyright (c) Facebook, Inc. and its affiliates.
28 | *
29 | * This source code is licensed under the MIT license found in the
30 | * LICENSE file in the root directory of this source tree.
31 | */
32 |
33 | /** @license React v0.25.1
34 | * react-reconciler.production.min.js
35 | *
36 | * Copyright (c) Facebook, Inc. and its affiliates.
37 | *
38 | * This source code is licensed under the MIT license found in the
39 | * LICENSE file in the root directory of this source tree.
40 | */
41 |
--------------------------------------------------------------------------------
/public/static/js/811.8d4e3c76.chunk.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*!
2 | * decimal.js v10.4.3
3 | * An arbitrary-precision Decimal type for JavaScript.
4 | * https://github.com/MikeMcl/decimal.js
5 | * Copyright (c) 2022 Michael Mclaughlin
6 | * MIT Licence
7 | */
8 |
--------------------------------------------------------------------------------
/public/static/js/828.e991df23.chunk.js:
--------------------------------------------------------------------------------
1 | "use strict";(self.webpackChunkbeancount_web=self.webpackChunkbeancount_web||[]).push([[828],{8828:(e,t,n)=>{function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function i(e){for(var t=1;t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function c(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?e.apply(this,o):function(){for(var e=arguments.length,r=new Array(e),i=0;ioe});var p=d((function(e,t){throw new Error(e[t]||e.default)}))({initialIsRequired:"initial state is required",initialType:"initial state should be an object",initialContent:"initial state shouldn't be an empty object",handlerType:"handler should be an object or a function",handlersType:"all handlers should be a functions",selectorType:"selector should be a function",changeType:"provided value of changes should be an object",changeField:'it seams you want to change a field in the state which is not specified in the "initial" state',default:"an unknown error accured in `state-local` package"}),v={changes:function(e,t){return f(t)||p("changeType"),Object.keys(t).some((function(t){return n=e,r=t,!Object.prototype.hasOwnProperty.call(n,r);var n,r}))&&p("changeField"),t},selector:function(e){g(e)||p("selectorType")},handler:function(e){g(e)||f(e)||p("handlerType"),f(e)&&Object.values(e).some((function(e){return!g(e)}))&&p("handlersType")},initial:function(e){var t;e||p("initialIsRequired"),f(e)||p("initialType"),t=e,Object.keys(t).length||p("initialContent")}};function h(e,t){return g(t)?t(e.current):t}function y(e,t){return e.current=s(s({},e.current),t),t}function m(e,t,n){return g(t)?t(e.current):Object.keys(n).forEach((function(n){var r;return null===(r=t[n])||void 0===r?void 0:r.call(t,e.current[n])})),n}const b={create:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};v.initial(e),v.handler(t);var n={current:e},r=d(m)(n,t),o=d(y)(n),i=d(v.changes)(e),u=d(h)(n);return[function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(e){return e};return v.selector(e),e(n.current)},function(e){!function(){for(var e=arguments.length,t=new Array(e),n=0;n=e.length?e.apply(this,o):function(){for(var e=arguments.length,r=new Array(e),i=0;i2&&void 0!==arguments[2])||arguments[2],r=(0,B.useRef)(!0);(0,B.useEffect)(r.current||!n?()=>{r.current=!1}:e,t)};function Q(){}function X(e,t,n,r){return function(e,t){return e.editor.getModel(Z(e,t))}(e,r)||function(e,t,n,r){return e.editor.createModel(t,n,r?Z(e,r):void 0)}(e,t,n,r)}function Z(e,t){return e.Uri.parse(t)}var ee=function(e){let{original:t,modified:n,language:r,originalLanguage:o,modifiedLanguage:i,originalModelPath:u,modifiedModelPath:c,keepCurrentOriginalModel:a=!1,keepCurrentModifiedModel:l=!1,theme:s="light",loading:d="Loading...",options:f={},height:g="100%",width:p="100%",className:v,wrapperProps:h={},beforeMount:y=Q,onMount:m=Q}=e,[b,w]=(0,B.useState)(!1),[O,j]=(0,B.useState)(!0),M=(0,B.useRef)(null),E=(0,B.useRef)(null),P=(0,B.useRef)(null),R=(0,B.useRef)(m),k=(0,B.useRef)(y),S=(0,B.useRef)(!1);H((()=>{let e=U.init();return e.then((e=>(E.current=e)&&j(!1))).catch((e=>"cancelation"!==(null===e||void 0===e?void 0:e.type)&&console.error("Monaco initialization: error:",e))),()=>M.current?function(){var e,t,n,r;let o=null===(e=M.current)||void 0===e?void 0:e.getModel();a||null!==o&&void 0!==o&&null!==(t=o.original)&&void 0!==t&&t.dispose(),l||null!==o&&void 0!==o&&null!==(n=o.modified)&&void 0!==n&&n.dispose(),null===(r=M.current)||void 0===r||r.dispose()}():e.cancel()})),J((()=>{if(M.current&&E.current){let e=M.current.getOriginalEditor(),n=X(E.current,t||"",o||r||"text",u||"");n!==e.getModel()&&e.setModel(n)}}),[u],b),J((()=>{if(M.current&&E.current){let e=M.current.getModifiedEditor(),t=X(E.current,n||"",i||r||"text",c||"");t!==e.getModel()&&e.setModel(t)}}),[c],b),J((()=>{let e=M.current.getModifiedEditor();e.getOption(E.current.editor.EditorOption.readOnly)?e.setValue(n||""):n!==e.getValue()&&(e.executeEdits("",[{range:e.getModel().getFullModelRange(),text:n||"",forceMoveMarkers:!0}]),e.pushUndoStop())}),[n],b),J((()=>{var e,n;null===(e=M.current)||void 0===e||null===(n=e.getModel())||void 0===n||n.original.setValue(t||"")}),[t],b),J((()=>{let{original:e,modified:t}=M.current.getModel();E.current.editor.setModelLanguage(e,o||r||"text"),E.current.editor.setModelLanguage(t,i||r||"text")}),[r,o,i],b),J((()=>{var e;null===(e=E.current)||void 0===e||e.editor.setTheme(s)}),[s],b),J((()=>{var e;null===(e=M.current)||void 0===e||e.updateOptions(f)}),[f],b);let C=(0,B.useCallback)((()=>{var e;if(!E.current)return;k.current(E.current);let a=X(E.current,t||"",o||r||"text",u||""),l=X(E.current,n||"",i||r||"text",c||"");null===(e=M.current)||void 0===e||e.setModel({original:a,modified:l})}),[r,n,i,t,o,u,c]),T=(0,B.useCallback)((()=>{var e;!S.current&&P.current&&(M.current=E.current.editor.createDiffEditor(P.current,{automaticLayout:!0,...f}),C(),null!==(e=E.current)&&void 0!==e&&e.editor.setTheme(s),w(!0),S.current=!0)}),[f,s,C]);return(0,B.useEffect)((()=>{b&&R.current(M.current,E.current)}),[b]),(0,B.useEffect)((()=>{!O&&!b&&T()}),[O,b,T]),B.createElement(G,{width:p,height:g,isEditorReady:b,loading:d,_ref:P,className:v,wrapperProps:h})};(0,B.memo)(ee);var te=function(e){let t=(0,B.useRef)();return(0,B.useEffect)((()=>{t.current=e}),[e]),t.current},ne=new Map;var re=function(e){let{defaultValue:t,defaultLanguage:n,defaultPath:r,value:o,language:i,path:u,theme:c="light",line:a,loading:l="Loading...",options:s={},overrideServices:d={},saveViewState:f=!0,keepCurrentModel:g=!1,width:p="100%",height:v="100%",className:h,wrapperProps:y={},beforeMount:m=Q,onMount:b=Q,onChange:w,onValidate:O=Q}=e,[j,M]=(0,B.useState)(!1),[E,P]=(0,B.useState)(!0),R=(0,B.useRef)(null),k=(0,B.useRef)(null),S=(0,B.useRef)(null),C=(0,B.useRef)(b),T=(0,B.useRef)(m),x=(0,B.useRef)(),I=(0,B.useRef)(o),A=te(u),V=(0,B.useRef)(!1),D=(0,B.useRef)(!1);H((()=>{let e=U.init();return e.then((e=>(R.current=e)&&P(!1))).catch((e=>"cancelation"!==(null===e||void 0===e?void 0:e.type)&&console.error("Monaco initialization: error:",e))),()=>k.current?function(){var e,t;null!==(e=x.current)&&void 0!==e&&e.dispose(),g?f&&ne.set(u,k.current.saveViewState()):null===(t=k.current.getModel())||void 0===t||t.dispose(),k.current.dispose()}():e.cancel()})),J((()=>{var e,c,a,l;let s=X(R.current,t||o||"",n||i||"",u||r||"");s!==(null===(e=k.current)||void 0===e?void 0:e.getModel())&&(f&&ne.set(A,null===(c=k.current)||void 0===c?void 0:c.saveViewState()),null!==(a=k.current)&&void 0!==a&&a.setModel(s),f&&(null===(l=k.current)||void 0===l||l.restoreViewState(ne.get(u))))}),[u],j),J((()=>{var e;null===(e=k.current)||void 0===e||e.updateOptions(s)}),[s],j),J((()=>{!k.current||void 0===o||(k.current.getOption(R.current.editor.EditorOption.readOnly)?k.current.setValue(o):o!==k.current.getValue()&&(D.current=!0,k.current.executeEdits("",[{range:k.current.getModel().getFullModelRange(),text:o,forceMoveMarkers:!0}]),k.current.pushUndoStop(),D.current=!1))}),[o],j),J((()=>{var e,t;let n=null===(e=k.current)||void 0===e?void 0:e.getModel();n&&i&&(null===(t=R.current)||void 0===t||t.editor.setModelLanguage(n,i))}),[i],j),J((()=>{var e;void 0!==a&&(null===(e=k.current)||void 0===e||e.revealLine(a))}),[a],j),J((()=>{var e;null===(e=R.current)||void 0===e||e.editor.setTheme(c)}),[c],j);let L=(0,B.useCallback)((()=>{if(S.current&&R.current&&!V.current){var e;T.current(R.current);let l=u||r,g=X(R.current,o||t||"",n||i||"",l||"");k.current=null===(e=R.current)||void 0===e?void 0:e.editor.create(S.current,{model:g,automaticLayout:!0,...s},d),f&&k.current.restoreViewState(ne.get(l)),R.current.editor.setTheme(c),void 0!==a&&k.current.revealLine(a),M(!0),V.current=!0}}),[t,n,r,o,i,u,s,d,f,c,a]);return(0,B.useEffect)((()=>{j&&C.current(k.current,R.current)}),[j]),(0,B.useEffect)((()=>{!E&&!j&&L()}),[E,j,L]),I.current=o,(0,B.useEffect)((()=>{var e,t;j&&w&&(null!==(e=x.current)&&void 0!==e&&e.dispose(),x.current=null===(t=k.current)||void 0===t?void 0:t.onDidChangeModelContent((e=>{D.current||w(k.current.getValue(),e)})))}),[j,w]),(0,B.useEffect)((()=>{if(j){let e=R.current.editor.onDidChangeMarkers((e=>{var t;let n=null===(t=k.current.getModel())||void 0===t?void 0:t.uri;if(n&&e.find((e=>e.path===n.path))){let e=R.current.editor.getModelMarkers({resource:n});null===O||void 0===O||O(e)}}));return()=>{null===e||void 0===e||e.dispose()}}return()=>{}}),[j,O]),B.createElement(G,{width:p,height:v,isEditorReady:j,loading:l,_ref:S,className:h,wrapperProps:y})},oe=(0,B.memo)(re)}}]);
--------------------------------------------------------------------------------
/public/static/js/999.1397823d.chunk.js:
--------------------------------------------------------------------------------
1 | "use strict";(self.webpackChunkbeancount_web=self.webpackChunkbeancount_web||[]).push([[999],{2999:(e,t,s)=>{s.r(t),s.d(t,{default:()=>w});var i=s(8380),r=s(6445),n=s(849),a=s(1141),l=s(3003),d=s(4747),h=s(5231),c=s(1896),o=s(5750),g=s(6668),m=s(2942),p=s(6694),u=s(9284),x=s(4760),j=s(2475),y=s(2069),A=s(712);const f={required:"${label} \u4e0d\u80fd\u4e3a\u7a7a\uff01"};class C extends u.Component{constructor(){super(...arguments),this.theme=this.context.theme,this.formRef=u.createRef(),this.state={loading:!1,expand:!1,newLedger:!1,selectedLedger:{},ledgers:[],config:{}},this.handleQueryLedgerList=()=>{this.setState({loading:!0}),(0,x.hd)("/api/ledger",{method:"GET"}).then((e=>{this.setState({ledgers:e,newLedger:0===e.length})})).finally((()=>{this.setState({loading:!1})}))},this.handleQueryServerConfig=()=>{this.setState({loading:!0}),(0,x.hd)("/api/config",{method:"GET"}).then((e=>{e.dataPath?this.setState({config:e}):window.location.href="/web/#/init"})).finally((()=>{this.setState({loading:!1})}))},this.handleCreateLedger=e=>{e.secret||!this.state.newLedger?this.handleReqCreateLedger(e):h.A.confirm({title:"\u63d0\u9192",icon:(0,A.jsx)(i.A,{}),content:"\u672a\u8bbe\u7f6e\u5bc6\u7801\uff0c\u8fd9\u53ef\u80fd\u4f1a\u5bfc\u81f4\u6570\u636e\u4e0d\u5b89\u5168",okText:"\u786e\u8ba4\u4e0d\u8bbe\u7f6e\u5bc6\u7801",cancelText:"\u53d6\u6d88",onOk:()=>this.handleReqCreateLedger(e)})},this.handleReqCreateLedger=e=>{this.setState({loading:!0}),(0,x.hd)("/api/ledger",{method:"POST",headers:{"Content-Type":"application/json"},body:e}).then((e=>{window.localStorage.setItem("ledgerId",e.ledgerId),e.title&&window.localStorage.setItem("ledgerTitle",e.title),e.currency&&window.localStorage.setItem("ledgerCurrency",JSON.stringify({currency:e.currency,symbol:e.currencySymbol})),this.props.history.replace("/")})).finally((()=>{this.setState({loading:!1})}))},this.handleSelectLedger=e=>{this.setState({selectedLedger:e})}}componentWillMount(){window.localStorage.getItem("ledgerId")&&(0,x.X3)()}componentDidMount(){this.handleQueryServerConfig(),this.handleQueryLedgerList()}render(){return this.context.theme!==this.theme&&(this.theme=this.context.theme),this.state.selectedLedger.mail||this.state.newLedger?(0,A.jsx)("div",{className:"ledger-page",children:(0,A.jsx)("div",{children:(0,A.jsxs)(g.A,{name:"add-account-form",size:"middle",layout:"vertical",ref:this.formRef,onFinish:this.handleCreateLedger,validateMessages:f,loading:this.state.loading,children:[(0,A.jsx)(g.A.Item,{name:"ledgerName",label:"\u8d26\u672c\u540d\u79f0",initialValue:this.state.selectedLedger.mail||"",rules:[{required:!0}],children:(0,A.jsx)(m.A,{placeholder:"\u4f60\u53ef\u4ee5\u521b\u5efa\u591a\u4e2a\u7684\u8d26\u672c\uff0c\u8d26\u672c\u4e4b\u95f4\u7684\u6570\u636e\u65e0\u6cd5\u4e92\u901a"})}),(0,A.jsx)(g.A.Item,{label:"\u4fee\u6539\u6e90\u6587\u4ef6\u65f6\u662f\u5426\u5907\u4efd\u6570\u636e",name:"isBak",valuePropName:"checked",rules:[{required:!0}],initialValue:this.state.config.isBak,children:(0,A.jsx)(p.A,{})}),(0,A.jsx)(g.A.Item,{name:"secret",label:"\u8d26\u672c\u5bc6\u7801",children:(0,A.jsx)(m.A,{type:"password",placeholder:"\u8d26\u672c\u5bc6\u7801"})}),!this.state.selectedLedger.mail&&(0,A.jsxs)(u.Fragment,{children:[(0,A.jsx)("div",{style:{fontSize:14,marginBottom:"2rem",textAlign:"center"},children:(0,A.jsxs)("a",{onClick:()=>{this.setState({expand:!this.state.expand})},children:[this.state.expand?(0,A.jsx)(l.A,{}):(0,A.jsx)(d.A,{})," \u66f4\u591a\u8d26\u672c\u8bbe\u7f6e"]})}),this.state.expand&&(0,A.jsxs)(u.Fragment,{children:[(0,A.jsx)(g.A.Item,{label:"\u8d26\u672c\u5f00\u59cb\u65e5\u671f",name:"startDate",initialValue:this.state.config.startDate,rules:[{required:!0}],children:(0,A.jsx)(m.A,{type:"date",placeholder:"\u8d26\u672c\u5f00\u59cb\u65e5\u671f"})}),(0,A.jsx)(g.A.Item,{label:"\u5e01\u79cd",name:"operatingCurrency",initialValue:this.state.config.operatingCurrency,rules:[{required:!0}],children:(0,A.jsx)(m.A,{placeholder:"\u5e01\u79cd"})}),(0,A.jsx)(g.A.Item,{label:"\u5e73\u8861\u8d26\u6237\u540d\u79f0\u8bbe\u7f6e",name:"openingBalances",initialValue:this.state.config.openingBalances,rules:[{required:!0}],children:(0,A.jsx)(m.A,{placeholder:"\u5e73\u8861\u8d26\u6237\u540d\u79f0\u8bbe\u7f6e"})})]})]}),(0,A.jsx)(g.A.Item,{children:(0,A.jsx)(c.A,{type:"primary",block:!0,htmlType:"submit",children:"\u521b\u5efa/\u8fdb\u5165\u4e2a\u4eba\u8d26\u672c"})}),this.state.ledgers.length>0&&(0,A.jsx)(g.A.Item,{children:(0,A.jsx)(c.A,{block:!0,onClick:()=>{this.setState({selectedLedger:{},newLedger:!1})},children:"\u8fd4\u56de\u8d26\u672c"})})]})})}):(0,A.jsxs)("div",{className:"ledger-page",children:[(0,A.jsx)("div",{children:(0,A.jsx)(c.A,{block:!0,type:"dashed",icon:(0,A.jsx)(r.A,{}),onClick:()=>{this.setState({newLedger:!0})},children:"\u521b\u5efa\u65b0\u8d26\u672c"})}),this.state.ledgers.map((e=>(0,A.jsx)(o.A,{style:{width:"100%",marginTop:16,cursor:"pointer"},loading:this.state.loading,children:(0,A.jsx)(o.A.Meta,{onClick:()=>{this.handleSelectLedger(e)},title:e.title,description:(0,A.jsxs)("div",{style:{display:"flex",justifyContent:"space-between"},children:[(0,A.jsxs)("div",{children:[(0,A.jsxs)("span",{children:[(0,A.jsx)(n.A,{}),"\xa0",e.mail]}),"\xa0\xa0",e.createDate&&(0,A.jsxs)("span",{children:[(0,A.jsx)(a.A,{}),"\xa0",e.createDate]})]}),(0,A.jsx)("div",{children:(0,A.jsx)("span",{children:e.operatingCurrency})})]})})})))]})}}C.contextType=j.A;const w=(0,y.A)(C)},2069:(e,t,s)=>{s.d(t,{A:()=>n});var i=s(9284),r=s(712);const n=e=>class extends i.Component{constructor(){super(...arguments),this.defaultCommodity={currency:"CNY",symbol:"\uffe5"},this.currentCommodity=window.localStorage.getItem("ledgerCurrency")}render(){return(0,r.jsx)(e,{...this.props,commodity:this.currentCommodity?JSON.parse(this.currentCommodity):this.defaultCommodity})}}}}]);
--------------------------------------------------------------------------------
/public/static/js/main.ad3a4211.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /*!
8 | Copyright (c) 2018 Jed Watson.
9 | Licensed under the MIT License (MIT), see
10 | http://jedwatson.github.io/classnames
11 | */
12 |
13 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
14 |
15 | /**
16 | * @license React
17 | * react-is.production.min.js
18 | *
19 | * Copyright (c) Facebook, Inc. and its affiliates.
20 | *
21 | * This source code is licensed under the MIT license found in the
22 | * LICENSE file in the root directory of this source tree.
23 | */
24 |
25 | /** @license React v0.20.2
26 | * scheduler.production.min.js
27 | *
28 | * Copyright (c) Facebook, Inc. and its affiliates.
29 | *
30 | * This source code is licensed under the MIT license found in the
31 | * LICENSE file in the root directory of this source tree.
32 | */
33 |
34 | /** @license React v16.13.1
35 | * react-is.production.min.js
36 | *
37 | * Copyright (c) Facebook, Inc. and its affiliates.
38 | *
39 | * This source code is licensed under the MIT license found in the
40 | * LICENSE file in the root directory of this source tree.
41 | */
42 |
43 | /** @license React v17.0.2
44 | * react-dom.production.min.js
45 | *
46 | * Copyright (c) Facebook, Inc. and its affiliates.
47 | *
48 | * This source code is licensed under the MIT license found in the
49 | * LICENSE file in the root directory of this source tree.
50 | */
51 |
52 | /** @license React v17.0.2
53 | * react-jsx-runtime.production.min.js
54 | *
55 | * Copyright (c) Facebook, Inc. and its affiliates.
56 | *
57 | * This source code is licensed under the MIT license found in the
58 | * LICENSE file in the root directory of this source tree.
59 | */
60 |
61 | /** @license React v17.0.2
62 | * react.production.min.js
63 | *
64 | * Copyright (c) Facebook, Inc. and its affiliates.
65 | *
66 | * This source code is licensed under the MIT license found in the
67 | * LICENSE file in the root directory of this source tree.
68 | */
69 |
70 | //! moment.js
71 |
72 | //! moment.js locale configuration
73 |
--------------------------------------------------------------------------------
/script/bql.go:
--------------------------------------------------------------------------------
1 | package script
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os/exec"
7 | "reflect"
8 | "regexp"
9 | "strconv"
10 | "strings"
11 |
12 | "github.com/gin-gonic/gin"
13 | )
14 |
15 | type QueryParams struct {
16 | From bool `bql:"From"`
17 | FromYear int `bql:"year ="`
18 | FromMonth int `bql:"month ="`
19 | Where bool `bql:"where"`
20 | ID string `bql:"id ="`
21 | IDList string `bql:"id in"`
22 | Currency string `bql:"currency ="`
23 | Year int `bql:"year ="`
24 | Month int `bql:"month ="`
25 | Tag string `bql:"in tags"`
26 | Account string `bql:"account ="`
27 | AccountLike string `bql:"account ~"`
28 | GroupBy string `bql:"group by"`
29 | OrderBy string `bql:"order by"`
30 | Limit int `bql:"limit"`
31 | Path string
32 | }
33 |
34 | func GetQueryParams(c *gin.Context) QueryParams {
35 | var queryParams QueryParams
36 | var hasWhere bool
37 | if c.Query("year") != "" {
38 | val, err := strconv.Atoi(c.Query("year"))
39 | if err == nil {
40 | queryParams.Year = val
41 | hasWhere = true
42 | }
43 | }
44 | if c.Query("month") != "" {
45 | val, err := strconv.Atoi(c.Query("month"))
46 | if err == nil {
47 | queryParams.Month = val
48 | hasWhere = true
49 | }
50 | }
51 | if c.Query("tag") != "" {
52 | queryParams.Tag = c.Query("tag")
53 | hasWhere = true
54 | }
55 | if c.Query("type") != "" {
56 | queryParams.AccountLike = c.Query("type")
57 | hasWhere = true
58 | }
59 | if c.Query("account") != "" {
60 | queryParams.Account = c.Query("account")
61 | queryParams.Limit = 100
62 | hasWhere = true
63 | }
64 | if c.Query("id") != "" {
65 | queryParams.ID = c.Query("id")
66 | hasWhere = true
67 | }
68 | queryParams.Where = hasWhere
69 | if c.Query("path") != "" {
70 | queryParams.Path = c.Query("path")
71 | }
72 | return queryParams
73 | }
74 |
75 | //func BQLQueryOne(ledgerConfig *Config, queryParams *QueryParams, queryResultPtr interface{}) error {
76 | // assertQueryResultIsPointer(queryResultPtr)
77 | // output, err := bqlRawQuery(ledgerConfig, "", queryParams, queryResultPtr)
78 | // if err != nil {
79 | // return err
80 | // }
81 | // err = parseResult(output, queryResultPtr, true)
82 | // if err != nil {
83 | // return err
84 | // }
85 | // return nil
86 | //}
87 |
88 | func BQLPrint(ledgerConfig *Config, transactionId string) (string, error) {
89 | // PRINT FROM id = 'xxx'
90 | output, err := queryByBQL(ledgerConfig, "PRINT FROM id = '"+transactionId+"'")
91 | if err != nil {
92 | return "", err
93 | }
94 | utf8, err := ConvertGBKToUTF8(output)
95 | if err != nil {
96 | return "", err
97 | }
98 | return utf8, nil
99 | }
100 |
101 | func BQLQueryList(ledgerConfig *Config, queryParams *QueryParams, queryResultPtr interface{}) error {
102 | assertQueryResultIsPointer(queryResultPtr)
103 | output, err := bqlRawQuery(ledgerConfig, "", queryParams, queryResultPtr)
104 | if err != nil {
105 | return err
106 | }
107 | err = parseResult(output, queryResultPtr, false)
108 | if err != nil {
109 | return err
110 | }
111 | return nil
112 | }
113 |
114 | func BQLQueryListByCustomSelect(ledgerConfig *Config, selectBql string, queryParams *QueryParams, queryResultPtr interface{}) error {
115 | assertQueryResultIsPointer(queryResultPtr)
116 | output, err := bqlRawQuery(ledgerConfig, selectBql, queryParams, queryResultPtr)
117 | if err != nil {
118 | return err
119 | }
120 | err = parseResult(output, queryResultPtr, false)
121 | if err != nil {
122 | return err
123 | }
124 | return nil
125 | }
126 |
127 | func BeanReportAllPrices(ledgerConfig *Config) []CommodityPrice {
128 | beanFilePath := GetLedgerPriceFilePath(ledgerConfig.DataPath)
129 | var (
130 | command string
131 | useBeanReport = checkCommandExists("bean-report")
132 | )
133 | // `bean-report` had been deprecated since https://github.com/beancount/beancount/commit/a7c4f14f083de63e8d4e5a8d3664209daf95e1ec,
134 | // we use `bean-query` instead. Here we add a check to use `bean-report` if `bean-query` is not installed for better compatibility.
135 | if useBeanReport {
136 | command = fmt.Sprintf("bean-report %s all_prices", beanFilePath)
137 | } else {
138 | // 'price' column works as a column placeholder to be consistent with the output of `bean-report`.
139 | command = fmt.Sprintf(`bean-query %s "SELECT date, 'price', currency, price FROM account ~ 'Assets' WHERE price is not NULL"`, beanFilePath)
140 | }
141 | LogInfo(ledgerConfig.Mail, command)
142 | re := regexp.MustCompile(`"([^"]*)"|(\S+)`)
143 | cmds := re.FindAllString(command, -1)
144 | cmd := exec.Command(cmds[0], cmds[1:]...)
145 | output, _ := cmd.Output()
146 | outputStr := string(output)
147 | lines := strings.Split(outputStr, "\n")
148 | LogInfo(ledgerConfig.Mail, outputStr)
149 | // Remove the first two lines of the output since they are the header and separator with BQL output.
150 | if !useBeanReport && len(lines) > 2 {
151 | lines = lines[2:]
152 | }
153 | return newCommodityPriceListFromString(lines)
154 | }
155 |
156 | func bqlRawQuery(ledgerConfig *Config, selectBql string, queryParamsPtr *QueryParams, queryResultPtr interface{}) (string, error) {
157 | var bql string
158 | if selectBql == "" {
159 | bql = "select"
160 | queryResultPtrType := reflect.TypeOf(queryResultPtr)
161 | queryResultType := queryResultPtrType.Elem()
162 |
163 | if queryResultType.Kind() == reflect.Slice {
164 | queryResultType = queryResultType.Elem()
165 | }
166 |
167 | for i := 0; i < queryResultType.NumField(); i++ {
168 | typeField := queryResultType.Field(i)
169 | // 字段的 tag 不带 bql 的不进行拼接
170 | b := typeField.Tag.Get("bql")
171 | if b != "" {
172 | if strings.Contains(b, "distinct") {
173 | b = strings.ReplaceAll(b, "distinct", "")
174 | bql = fmt.Sprintf("%s distinct '\\', %s, ", bql, b)
175 | } else {
176 | bql = fmt.Sprintf("%s '\\', %s, ", bql, typeField.Tag.Get("bql"))
177 | }
178 | }
179 | }
180 | bql += " '\\'"
181 | } else {
182 | bql = selectBql
183 | }
184 |
185 | // 查询条件不为空时,拼接查询条件
186 | if queryParamsPtr != nil {
187 | queryParamsType := reflect.TypeOf(queryParamsPtr).Elem()
188 | queryParamsValue := reflect.ValueOf(queryParamsPtr).Elem()
189 | for i := 0; i < queryParamsType.NumField(); i++ {
190 | typeField := queryParamsType.Field(i)
191 | valueField := queryParamsValue.Field(i)
192 | switch valueField.Kind() {
193 | case reflect.String:
194 | val := valueField.String()
195 | if val != "" {
196 | if typeField.Name == "OrderBy" || typeField.Name == "GroupBy" {
197 | // 去除上一个条件后缀的 AND 关键字
198 | bql = strings.Trim(bql, " AND")
199 | bql = fmt.Sprintf("%s %s %s", bql, typeField.Tag.Get("bql"), val)
200 | } else if typeField.Name == "Tag" {
201 | bql = fmt.Sprintf("%s '%s' %s", bql, strings.Trim(val, " "), typeField.Tag.Get("bql"))
202 | } else {
203 | bql = fmt.Sprintf("%s %s '%s' AND", bql, typeField.Tag.Get("bql"), val)
204 | }
205 | }
206 | case reflect.Int:
207 | val := valueField.Int()
208 | if val != 0 {
209 | bql = fmt.Sprintf("%s %s %d AND", bql, typeField.Tag.Get("bql"), val)
210 | }
211 | case reflect.Bool:
212 | val := valueField.Bool()
213 | // where 前的 from 可能会带有 and
214 | if typeField.Name == "Where" {
215 | bql = strings.Trim(bql, " AND")
216 | }
217 | if val {
218 | bql = fmt.Sprintf("%s %s ", bql, typeField.Tag.Get("bql"))
219 | }
220 | }
221 | }
222 | bql = strings.TrimRight(bql, " AND")
223 | }
224 | return queryByBQL(ledgerConfig, bql)
225 | }
226 |
227 | func parseResult(output string, queryResultPtr interface{}, selectOne bool) error {
228 | queryResultPtrType := reflect.TypeOf(queryResultPtr)
229 | queryResultType := queryResultPtrType.Elem()
230 |
231 | if queryResultType.Kind() == reflect.Slice {
232 | queryResultType = queryResultType.Elem()
233 | }
234 |
235 | lines := strings.Split(output, "\n")[2:]
236 | if selectOne && len(lines) >= 3 {
237 | lines = lines[2:3]
238 | }
239 |
240 | l := make([]map[string]interface{}, 0)
241 | for _, line := range lines {
242 | if line != "" {
243 | values := strings.Split(line, "\\")
244 | // 去除 '\' 分割带来的空字符串
245 | values = values[1 : len(values)-1]
246 | temp := make(map[string]interface{})
247 | for i, val := range values {
248 | field := queryResultType.Field(i)
249 | jsonName := field.Tag.Get("json")
250 | if jsonName == "" {
251 | jsonName = field.Name
252 | }
253 | switch field.Type.Kind() {
254 | case reflect.Int, reflect.Int32:
255 | i, err := strconv.Atoi(strings.Trim(val, " "))
256 | if err != nil {
257 | panic(err)
258 | }
259 | temp[jsonName] = i
260 | // decimal
261 | case reflect.String, reflect.Struct:
262 | v := strings.Trim(val, " ")
263 | if v != "" {
264 | temp[jsonName] = v
265 | }
266 | case reflect.Array, reflect.Slice:
267 | // 去除空格
268 | strArray := strings.Split(val, ",")
269 | notBlanks := make([]string, 0)
270 | for _, s := range strArray {
271 | if strings.Trim(s, " ") != "" {
272 | notBlanks = append(notBlanks, s)
273 | }
274 | }
275 | temp[jsonName] = notBlanks
276 | default:
277 | panic("Unsupported field type")
278 | }
279 | }
280 | l = append(l, temp)
281 | }
282 | }
283 |
284 | var jsonBytes []byte
285 | var err error
286 | if selectOne {
287 | jsonBytes, err = json.Marshal(l[0])
288 | } else {
289 | jsonBytes, err = json.Marshal(l)
290 | }
291 | if err != nil {
292 | return err
293 | }
294 | err = json.Unmarshal(jsonBytes, queryResultPtr)
295 | if err != nil {
296 | return err
297 | }
298 | return nil
299 | }
300 |
301 | func queryByBQL(ledgerConfig *Config, bql string) (string, error) {
302 | beanFilePath := ledgerConfig.DataPath + "/index.bean"
303 | LogInfo(ledgerConfig.Mail, bql)
304 | cmd := exec.Command("bean-query", beanFilePath, bql)
305 | output, err := cmd.Output()
306 | if err != nil {
307 | return "", err
308 | }
309 | return string(output), nil
310 | }
311 |
312 | func assertQueryResultIsPointer(queryResult interface{}) {
313 | k := reflect.TypeOf(queryResult).Kind()
314 | if k != reflect.Ptr {
315 | panic("QueryResult type must be pointer, it's " + k.String())
316 | }
317 | }
318 |
--------------------------------------------------------------------------------
/script/file.go:
--------------------------------------------------------------------------------
1 | package script
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "path/filepath"
9 | "regexp"
10 | "strings"
11 | )
12 |
13 | func FileIfExist(filePath string) bool {
14 | _, err := os.Stat(filePath)
15 | if nil != err {
16 | return false
17 | }
18 | if os.IsNotExist(err) {
19 | return false
20 | }
21 | return true
22 | }
23 |
24 | func ReadFile(filePath string) ([]byte, error) {
25 | content, err := ioutil.ReadFile(filePath)
26 | if nil != err {
27 | LogSystemError("Failed to read file (" + filePath + ")")
28 | return content, err
29 | }
30 | LogSystemInfo("Success read file (" + filePath + ")")
31 | return content, nil
32 | }
33 |
34 | func WriteFile(filePath string, content string) error {
35 | err := ioutil.WriteFile(filePath, []byte(content), 0777)
36 | if err != nil {
37 | LogSystemError("Failed to write file (" + filePath + ")")
38 | return err
39 | }
40 | LogSystemInfo("Success write file (" + filePath + ")")
41 | return nil
42 | }
43 |
44 | func AppendFileInNewLine(filePath string, content string) error {
45 | err := CreateFileIfNotExist(filePath)
46 | if err != nil {
47 | return err
48 | }
49 | content = "\r\n" + content
50 | file, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
51 | if err != nil {
52 | LogSystemError("Failed to open file (" + filePath + ")")
53 | return err
54 | } else {
55 | _, err = file.WriteString(content)
56 | if err != nil {
57 | LogSystemError("Failed to append file (" + filePath + ")")
58 | return err
59 | }
60 | }
61 | defer file.Close()
62 | LogSystemInfo("Success append file (" + filePath + ")")
63 | return err
64 | }
65 |
66 | func DeleteLinesWithText(filePath string, textToDelete string) error {
67 | // 打开文件以供读写
68 | file, err := os.OpenFile(filePath, os.O_RDWR, 0644)
69 | if err != nil {
70 | return err
71 | }
72 | defer file.Close()
73 |
74 | // 创建一个缓冲读取器
75 | scanner := bufio.NewScanner(file)
76 |
77 | // 创建一个字符串切片,用于保存文件的每一行
78 | var lines []string
79 |
80 | // 逐行读取文件内容
81 | for scanner.Scan() {
82 | line := scanner.Text()
83 |
84 | // 检查行是否包含要删除的文本
85 | if !strings.Contains(line, textToDelete) {
86 | lines = append(lines, line)
87 | }
88 | }
89 |
90 | // 关闭文件
91 | file.Close()
92 |
93 | // 重新打开文件以供写入
94 | file, err = os.OpenFile(filePath, os.O_RDWR|os.O_TRUNC, 0644)
95 | if err != nil {
96 | return err
97 | }
98 | defer file.Close()
99 |
100 | // 创建一个写入器
101 | writer := bufio.NewWriter(file)
102 |
103 | // 将修改后的内容写回文件
104 | for _, line := range lines {
105 | _, err := writer.WriteString(line + "\n")
106 | if err != nil {
107 | return err
108 | }
109 | }
110 |
111 | // 刷新缓冲区,确保所有数据被写入文件
112 | err = writer.Flush()
113 | if err != nil {
114 | return err
115 | }
116 |
117 | return nil
118 | }
119 |
120 | func CreateFile(filePath string) error {
121 | if _, e := os.Stat(filePath); os.IsNotExist(e) {
122 | _ = os.MkdirAll(filepath.Dir(filePath), os.ModePerm)
123 | f, err := os.Create(filePath)
124 | if nil != err {
125 | LogSystemError(filePath + " create failed")
126 | return err
127 | }
128 | defer f.Close()
129 | LogSystemInfo("Success create file " + filePath)
130 | } else {
131 | LogSystemInfo("File is exist " + filePath)
132 | }
133 | return nil
134 | }
135 |
136 | func CreateFileIfNotExist(filePath string) error {
137 | if _, e := os.Stat(filePath); os.IsNotExist(e) {
138 | _ = os.MkdirAll(filepath.Dir(filePath), os.ModePerm)
139 | f, err := os.Create(filePath)
140 | if nil != err {
141 | LogSystemError(filePath + " create failed")
142 | return err
143 | }
144 | defer f.Close()
145 | LogSystemInfo("Success create file " + filePath)
146 | }
147 | return nil
148 | }
149 |
150 | func CopyFile(sourceFilePath string, targetFilePath string) error {
151 | if !FileIfExist(sourceFilePath) {
152 | panic("File is not found, " + sourceFilePath)
153 | }
154 | if !FileIfExist(targetFilePath) {
155 | err := CreateFile(targetFilePath)
156 | if err != nil {
157 | return err
158 | }
159 | }
160 | bytes, err := ReadFile(sourceFilePath)
161 | if err != nil {
162 | return err
163 | }
164 | err = WriteFile(targetFilePath, string(bytes))
165 | if err != nil {
166 | return err
167 | }
168 | return nil
169 | }
170 |
171 | func CopyDir(sourceDir string, targetDir string) error {
172 | dirs, err := os.ReadDir(sourceDir)
173 | if err != nil {
174 | return err
175 | }
176 | err = MkDir(targetDir)
177 | if err != nil {
178 | return err
179 | }
180 | for _, dir := range dirs {
181 | newSourceDir := filepath.Join(sourceDir, dir.Name())
182 | newTargetDir := filepath.Join(targetDir, dir.Name())
183 | if dir.IsDir() {
184 | err := CopyFile(newSourceDir, newTargetDir)
185 | if err != nil {
186 | LogSystemError("Failed to copy dir from [" + newSourceDir + "] to [" + newTargetDir + "]")
187 | return err
188 | }
189 | } else {
190 | err := CreateFileIfNotExist(newTargetDir)
191 | if err != nil {
192 | return err
193 | }
194 | err = CopyFile(newSourceDir, newTargetDir)
195 | if err != nil {
196 | return err
197 | }
198 | }
199 | }
200 | return nil
201 | }
202 |
203 | func MkDir(dirPath string) error {
204 | err := os.MkdirAll(dirPath, os.ModePerm)
205 | if nil != err {
206 | LogSystemError("Failed mkdir " + dirPath)
207 | return err
208 | }
209 | LogSystemInfo("Success mkdir " + dirPath)
210 | return nil
211 | }
212 |
213 | // FindConsecutiveMultilineTextInFile 查找文件中连续多行文本片段的开始和结束行号
214 | func FindConsecutiveMultilineTextInFile(filePath string, multilineLines []string) (startLine, endLine int, err error) {
215 | for i := range multilineLines {
216 | multilineLines[i] = CleanString(multilineLines[i])
217 | }
218 |
219 | file, err := os.Open(filePath)
220 | if err != nil {
221 | return -1, -1, err
222 | }
223 | defer file.Close()
224 |
225 | scanner := bufio.NewScanner(file)
226 | startLine = -1
227 | endLine = -1
228 | lineNumber := 0
229 | matchIndex := 0
230 |
231 | for scanner.Scan() {
232 | lineNumber++
233 | // 清理文件中的当前行
234 | lineText := CleanString(scanner.Text())
235 |
236 | // 检查当前行是否匹配多行文本片段的当前行
237 | if lineText == multilineLines[matchIndex] {
238 | if startLine == -1 {
239 | startLine = lineNumber // 记录起始行号
240 | }
241 | matchIndex++
242 | // 如果所有行都匹配完成,记录结束行号并退出循环
243 | if matchIndex == len(multilineLines) {
244 | endLine = lineNumber
245 | break
246 | }
247 | } else {
248 | // 如果匹配失败,重置匹配索引和起始行号
249 | matchIndex = 0
250 | startLine = -1
251 | }
252 | }
253 |
254 | if err := scanner.Err(); err != nil {
255 | return -1, -1, err
256 | }
257 |
258 | // 如果未找到完整的多行文本片段,则返回 -1
259 | if startLine == -1 || endLine == -1 {
260 | return -1, -1, fmt.Errorf("未找到连续的多行文本片段")
261 | }
262 |
263 | LogSystemInfo("Success find content in file " + filePath + " line range: " + string(rune(startLine)) + "," + string(rune(endLine)))
264 | return startLine, endLine, nil
265 | }
266 |
267 | // CleanString 去除字符串中的首尾空白和中间的所有空格字符
268 | func CleanString(str string) string {
269 | if IsComment(str) {
270 | return ""
271 | }
272 | result := getAccountWithNumber(str)
273 | // 去除 " ", ";", "\r"
274 | result = strings.ReplaceAll(result, ",", "")
275 | result = strings.ReplaceAll(result, " ", "")
276 | // 过滤空白的商户信息 ““
277 | result = strings.ReplaceAll(result, "\"\"", "")
278 | result = strings.ReplaceAll(result, ";", "")
279 | result = strings.ReplaceAll(result, "\r", "")
280 | // 清楚汇率转换
281 |
282 | return result
283 | }
284 |
285 | // 正则提取:
286 | // Assets:Flow:Cash:现金 -20.00 USD {xxx CNY, 2025-01-01} -> Assets:Flow:Cash:现金 -20.00 USD
287 | func getAccountWithNumber(str string) string {
288 | // 定义正则表达式模式
289 | pattern := `^[^\{]+`
290 | // 编译正则表达式
291 | re := regexp.MustCompile(pattern)
292 | // 使用正则提取匹配的部分
293 | return re.FindString(str)
294 | }
295 |
296 | func IsComment(line string) bool {
297 | trimmed := strings.TrimLeft(line, " ")
298 | if strings.HasPrefix(trimmed, ";") {
299 | return true
300 | }
301 | return false
302 | }
303 |
304 | // 删除指定行范围的内容
305 | func RemoveLines(filePath string, startLineNo, endLineNo int) ([]string, error) {
306 | file, err := os.Open(filePath)
307 | if err != nil {
308 | return nil, err
309 | }
310 | defer file.Close()
311 |
312 | // 读取文件的每一行
313 | var lines []string
314 | scanner := bufio.NewScanner(file)
315 | for scanner.Scan() {
316 | lines = append(lines, scanner.Text())
317 | }
318 |
319 | if err := scanner.Err(); err != nil {
320 | return nil, err
321 | }
322 |
323 | // 检查行号的有效性
324 | if startLineNo < 1 || endLineNo > len(lines) || startLineNo > endLineNo {
325 | return nil, fmt.Errorf("行号范围无效")
326 | }
327 |
328 | // 删除从 startLineNo 到 endLineNo 的行(下标从 0 开始)
329 | modifiedLines := append(lines[:startLineNo-1], lines[endLineNo:]...)
330 | return modifiedLines, nil
331 | }
332 |
333 | // 在指定行号插入多行文本
334 | func InsertLines(lines []string, startLineNo int, newLines []string) ([]string, error) {
335 | // 检查插入位置的有效性
336 | if startLineNo < 1 || startLineNo > len(lines)+1 {
337 | return nil, fmt.Errorf("插入行号无效")
338 | }
339 | // 在指定位置插入新的内容
340 | modifiedLines := append(lines[:startLineNo-1], append(newLines, lines[startLineNo-1:]...)...)
341 | return modifiedLines, nil
342 | }
343 |
344 | // 写回文件
345 | func WriteToFile(filePath string, lines []string) error {
346 | file, err := os.Create(filePath)
347 | if err != nil {
348 | return err
349 | }
350 | defer file.Close()
351 |
352 | // 将修改后的内容写回文件
353 | writer := bufio.NewWriter(file)
354 | for _, line := range lines {
355 | _, err := writer.WriteString(line + "\n")
356 | if err != nil {
357 | return err
358 | }
359 | }
360 | LogSystemInfo("Success write content in file " + filePath)
361 | return writer.Flush()
362 | }
363 |
--------------------------------------------------------------------------------
/script/log.go:
--------------------------------------------------------------------------------
1 | package script
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | )
7 |
8 | func LogInfo(ledgerName string, message string) {
9 | fmt.Printf("[Info] [%s] [%s]: %s\n", time.Now().Format("2006-01-02 15:04:05"), ledgerName, message)
10 | }
11 |
12 | func LogSystemInfo(message string) {
13 | LogInfo("System", message)
14 | }
15 |
16 | func LogError(ledgerName string, message string) {
17 | fmt.Printf("[Error] [%s] [%s]: %s\n", time.Now().Format("2006-01-02 15:04:05"), ledgerName, message)
18 | }
19 |
20 | func LogSystemError(message string) {
21 | LogError("System", message)
22 | }
23 |
--------------------------------------------------------------------------------
/script/paths.go:
--------------------------------------------------------------------------------
1 | package script
2 |
3 | import "os"
4 |
5 | func GetServerConfigFilePath() string {
6 | currentPath, _ := os.Getwd()
7 | return currentPath + "/config/config.json"
8 | }
9 |
10 | func GetServerWhiteListFilePath() string {
11 | currentPath, _ := os.Getwd()
12 | return currentPath + "/config/white_list.json"
13 | }
14 |
15 | func GetServerLedgerConfigFilePath() string {
16 | return GetServerConfig().DataPath + "/ledger_config.json"
17 | }
18 |
19 | func GetTemplateLedgerConfigDirPath() string {
20 | currentPath, err := os.Getwd()
21 | if err != nil {
22 | return ""
23 | }
24 | return currentPath + "/template"
25 | }
26 |
27 | func GetLedgerConfigDocument(dataPath string) string {
28 | return dataPath + "/.beancount-gs"
29 | }
30 |
31 | func GetCompatibleLedgerConfigDocument(dataPath string) string {
32 | return dataPath + "/.beancount-ns"
33 | }
34 |
35 | func GetLedgerTransactionsTemplateFilePath(dataPath string) string {
36 | return dataPath + "/.beancount-gs/transaction_template.json"
37 | }
38 |
39 | func GetLedgerAccountTypeFilePath(dataPath string) string {
40 | return dataPath + "/.beancount-gs/account_type.json"
41 | }
42 |
43 | func GetLedgerCurrenciesFilePath(dataPath string) string {
44 | return dataPath + "/.beancount-gs/currency.json"
45 | }
46 |
47 | func GetLedgerPriceFilePath(dataPath string) string {
48 | return dataPath + "/price/prices.bean"
49 | }
50 |
51 | func GetLedgerMonthsFilePath(dataPath string) string {
52 | return dataPath + "/month/months.bean"
53 | }
54 |
55 | func GetLedgerMonthFilePath(dataPath string, month string) string {
56 | return dataPath + "/month/" + month + ".bean"
57 | }
58 |
59 | func GetLedgerIndexFilePath(dataPath string) string {
60 | LogInfo(dataPath, dataPath+"/index.bean")
61 | return dataPath + "/index.bean"
62 | }
63 |
64 | func GetLedgerIncludesFilePath(dataPath string) string {
65 | LogInfo(dataPath, dataPath+"/includes.bean")
66 | return dataPath + "/includes.bean"
67 | }
68 |
69 | func GetLedgerEventsFilePath(dataPath string) string {
70 | LogInfo(dataPath, dataPath+"/event/events.bean")
71 | return dataPath + "/event/events.bean"
72 | }
73 |
--------------------------------------------------------------------------------
/script/platform.go:
--------------------------------------------------------------------------------
1 | package script
2 |
3 | import (
4 | "os/exec"
5 | "runtime"
6 | )
7 |
8 | func isWindows() bool {
9 | os := runtime.GOOS
10 | return os == "windows"
11 | }
12 |
13 | func isMacOS() bool {
14 | os := runtime.GOOS
15 | return os == "darwin"
16 | }
17 |
18 | func OpenBrowser(url string) {
19 | if isWindows() {
20 | cmd := exec.Command("cmd", "/C", "start", url)
21 | err := cmd.Start()
22 | if err != nil {
23 | LogSystemError("Failed to open browser, error is " + err.Error())
24 | }
25 | } else if isMacOS() {
26 | cmd := exec.Command("open", url)
27 | err := cmd.Start()
28 | if err != nil {
29 | LogSystemError("Failed to open browser, error is " + err.Error())
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/script/sort.go:
--------------------------------------------------------------------------------
1 | package script
2 |
3 | type AccountTypeSort []AccountType
4 |
5 | func (s AccountTypeSort) Len() int {
6 | return len(s)
7 | }
8 |
9 | func (s AccountTypeSort) Swap(i, j int) {
10 | s[i], s[j] = s[j], s[i]
11 | }
12 |
13 | func (s AccountTypeSort) Less(i, j int) bool {
14 | return s[i].Key <= s[j].Key
15 | }
16 |
17 | type AccountSort []Account
18 |
19 | func (s AccountSort) Len() int {
20 | return len(s)
21 | }
22 |
23 | func (s AccountSort) Swap(i, j int) {
24 | s[i], s[j] = s[j], s[i]
25 | }
26 |
27 | func (s AccountSort) Less(i, j int) bool {
28 | return s[i].Acc <= s[j].Acc
29 | }
30 |
--------------------------------------------------------------------------------
/script/utils.go:
--------------------------------------------------------------------------------
1 | package script
2 |
3 | import (
4 | "bytes"
5 | "golang.org/x/text/encoding/simplifiedchinese"
6 | "golang.org/x/text/transform"
7 | "io/ioutil"
8 | "math/rand"
9 | "net"
10 | "os/exec"
11 | "time"
12 | )
13 |
14 | func checkCommandExists(command string) bool {
15 | cmd := exec.Command(command, "--version")
16 | _, err := cmd.Output()
17 | return err == nil
18 | }
19 |
20 | func GetIpAddress() string {
21 | addrs, _ := net.InterfaceAddrs()
22 | for _, value := range addrs {
23 | if ipnet, ok := value.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
24 | if ipnet.IP.To4() != nil {
25 | return ipnet.IP.String()
26 | }
27 | }
28 | }
29 | return ""
30 | }
31 |
32 | const char = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
33 |
34 | func RandChar(size int) string {
35 | source := rand.NewSource(time.Now().UnixNano()) // 产生随机种子
36 | var s bytes.Buffer
37 | for i := 0; i < size; i++ {
38 | s.WriteByte(char[source.Int63()%int64(len(char))])
39 | }
40 | return s.String()
41 | }
42 |
43 | type Timestamp int64
44 |
45 | const time_layout string = "2006-01-02 15:04:05"
46 |
47 | // 日期字符串转为时间戳 工具函数
48 | func getTimeStamp(str_date string) Timestamp {
49 | if len(str_date) == 10 {
50 | str_date = str_date + " 00:00:00"
51 | }
52 | // 获取时区
53 | loc, err := time.LoadLocation("Local")
54 | if err != nil {
55 | return 0
56 | }
57 | // 转换为时间戳
58 | the_time, err := time.ParseInLocation(time_layout, str_date, loc)
59 | if err != nil {
60 | return 0
61 | }
62 | // 返回时间戳
63 | return Timestamp(the_time.Unix())
64 | }
65 |
66 | // 获取1到2个日期字符串中更大的日期
67 | func getMaxDate(str_date1 string, str_date2 string) string {
68 | var max_date string
69 | if str_date1 != "" && str_date2 == "" {
70 | // 只定义了第一个账户,取第一个账户的日期为准
71 | max_date = str_date1
72 | } else if str_date1 == "" && str_date2 != "" {
73 | // 只定义了第二个账户,取第二个账户的日期为准
74 | max_date = str_date2
75 | } else if str_date1 != "" && str_date2 != "" {
76 | // 重复定义的账户,取最晚的时间为准
77 | t1 := getTimeStamp(str_date1)
78 | t2 := getTimeStamp(str_date2)
79 | if t1 > t2 {
80 | max_date = str_date1
81 | } else {
82 | max_date = str_date2
83 | }
84 | } else if str_date1 == "" && str_date2 == "" {
85 | // 没有定义账户,取当前日期为准
86 | max_date = time.Now().Format(time_layout)
87 | }
88 | return max_date
89 | }
90 |
91 | // ConvertGBKToUTF8 将 GBK 编码的字符串转换为 UTF-8 编码
92 | func ConvertGBKToUTF8(gbkStr string) (string, error) {
93 | if !isWindows() {
94 | return gbkStr, nil
95 | }
96 | // 创建一个 GBK 到 UTF-8 的转换器
97 | reader := transform.NewReader(bytes.NewReader([]byte(gbkStr)), simplifiedchinese.GBK.NewDecoder())
98 |
99 | // 将转换后的内容读出为 UTF-8 字符串
100 | utf8Bytes, err := ioutil.ReadAll(reader)
101 | if err != nil {
102 | return "", err
103 | }
104 |
105 | return string(utf8Bytes), nil
106 | }
107 |
108 | func GetMonth(date string) (string, error) {
109 | // 解析日期字符串
110 | parsedDate, err := time.Parse("2006-01-02", date)
111 | if err != nil {
112 | return "", err
113 | }
114 | // 格式化日期为 "YYYY-MM" 格式
115 | formattedDate := parsedDate.Format("2006-01")
116 | return formattedDate, nil
117 | }
118 |
--------------------------------------------------------------------------------
/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "github.com/beancount-gs/script"
7 | "github.com/beancount-gs/service"
8 | "github.com/gin-gonic/gin"
9 | "io"
10 | "net/http"
11 | "os"
12 | )
13 |
14 | func InitServerFiles() error {
15 | dataPath := script.GetServerConfig().DataPath
16 | // 账本目录不存在,则创建
17 | if dataPath != "" && !script.FileIfExist(dataPath) {
18 | return script.MkDir(dataPath)
19 | }
20 | return nil
21 | }
22 |
23 | func LoadServerCache() error {
24 | err := script.LoadLedgerConfigMap()
25 | if err != nil {
26 | return err
27 | }
28 | return script.LoadLedgerAccountsMap()
29 | }
30 |
31 | func AuthorizedHandler() gin.HandlerFunc {
32 | return func(c *gin.Context) {
33 | ledgerId := c.GetHeader("ledgerId")
34 | ledgerConfig := script.GetLedgerConfig(ledgerId)
35 | if ledgerConfig != nil {
36 | c.Set("LedgerConfig", ledgerConfig)
37 | c.Next()
38 | } else {
39 | service.Unauthorized(c)
40 | c.Abort()
41 | }
42 | }
43 | }
44 |
45 | func RegisterRouter(router *gin.Engine) {
46 | // fix wildcard and static file router conflict, https://github.com/gin-gonic/gin/issues/360
47 | router.GET("/", func(c *gin.Context) {
48 | c.Redirect(http.StatusMovedPermanently, "/web")
49 | })
50 | router.StaticFS("/web", http.Dir("./public"))
51 | router.GET("/api/version", service.QueryVersion)
52 | router.POST("/api/check", service.CheckBeancount)
53 | router.GET("/api/config", service.QueryServerConfig)
54 | router.POST("/api/config", service.UpdateServerConfig)
55 | router.GET("/api/ledger", service.QueryLedgerList)
56 | router.POST("/api/ledger", service.OpenOrCreateLedger)
57 | authorized := router.Group("/api/auth/")
58 | authorized.Use(AuthorizedHandler())
59 | {
60 | // need authorized
61 | authorized.GET("/account/valid", service.QueryValidAccount)
62 | authorized.GET("/account/all", service.QueryAllAccount)
63 | authorized.GET("/account/type", service.QueryAccountType)
64 | authorized.POST("/account", service.AddAccount)
65 | authorized.POST("/account/type", service.AddAccountType)
66 | authorized.POST("/account/close", service.CloseAccount)
67 | authorized.POST("/account/icon", service.ChangeAccountIcon)
68 | authorized.POST("/account/balance", service.BalanceAccount)
69 | authorized.POST("/account/refresh", service.RefreshAccountCache)
70 | authorized.POST("/commodity/price", service.SyncCommodityPrice)
71 | authorized.GET("/commodity/currencies", service.QueryAllCurrencies)
72 | authorized.GET("/stats/months", service.MonthsList)
73 | authorized.GET("/stats/total", service.StatsTotal)
74 | authorized.GET("/stats/payee", service.StatsPayee)
75 | authorized.GET("/stats/account/percent", service.StatsAccountPercent)
76 | authorized.GET("/stats/account/trend", service.StatsAccountTrend)
77 | authorized.GET("/stats/account/balance", service.StatsAccountBalance)
78 | authorized.GET("/stats/account/flow", service.StatsAccountSankey)
79 | authorized.GET("/stats/month/total", service.StatsMonthTotal)
80 | authorized.GET("/stats/month/calendar", service.StatsMonthCalendar)
81 | authorized.GET("/stats/commodity/price", service.StatsCommodityPrice)
82 | authorized.GET("/transaction/detail", service.QueryTransactionDetailById)
83 | authorized.GET("/transaction/raw", service.QueryTransactionRawTextById)
84 | authorized.GET("/transaction", service.QueryTransactions)
85 | authorized.POST("/transaction", service.AddTransactions)
86 | authorized.POST("/transaction/raw", service.UpdateTransactionRawTextById)
87 | authorized.DELETE("/transaction", service.DeleteTransactionById)
88 | authorized.POST("/transaction/batch", service.AddBatchTransactions)
89 | authorized.GET("/transaction/payee", service.QueryTransactionPayees)
90 | authorized.GET("/transaction/template", service.QueryTransactionTemplates)
91 | authorized.POST("/transaction/template", service.AddTransactionTemplate)
92 | authorized.DELETE("/transaction/template", service.DeleteTransactionTemplate)
93 | authorized.GET("/event/all", service.GetAllEvents)
94 | authorized.POST("/event", service.AddEvent)
95 | authorized.DELETE("/event", service.DeleteEvent)
96 | authorized.GET("/tags", service.QueryTags)
97 | authorized.GET("/file/dir", service.QueryLedgerSourceFileDir)
98 | authorized.GET("/file/content", service.QueryLedgerSourceFileContent)
99 | authorized.POST("/file", service.UpdateLedgerSourceFileContent)
100 | authorized.POST("/import/alipay", service.ImportAliPayCSV)
101 | authorized.POST("/import/wx", service.ImportWxPayCSV)
102 | authorized.POST("/import/icbc", service.ImportICBCCSV)
103 | authorized.POST("/import/abc", service.ImportABCCSV)
104 | authorized.GET("/ledger/check", service.CheckLedger)
105 | authorized.DELETE("/ledger", service.DeleteLedger)
106 | }
107 | }
108 |
109 | func main() {
110 | var secret string
111 | var port int
112 | flag.StringVar(&secret, "secret", "", "服务器密钥")
113 | flag.IntVar(&port, "p", 10000, "端口号")
114 | flag.Parse()
115 |
116 | // 读取配置文件
117 | err := script.LoadServerConfig()
118 | if err != nil {
119 | script.LogSystemError("Failed to load server config, " + err.Error())
120 | return
121 | }
122 | serverConfig := script.GetServerConfig()
123 | // 若 DataPath == "" 则配置未初始化
124 | if serverConfig.DataPath != "" {
125 | // 初始化账本文件结构
126 | err = InitServerFiles()
127 | if err != nil {
128 | script.LogSystemError("Failed to init server files, " + err.Error())
129 | return
130 | }
131 | // 加载缓存
132 | err = LoadServerCache()
133 | if err != nil {
134 | script.LogSystemError("Failed to load server cache, " + err.Error())
135 | return
136 | }
137 | }
138 | // gin 日志设置
139 | gin.DisableConsoleColor()
140 | fs, _ := os.Create("logs/gin.log")
141 | gin.DefaultWriter = io.MultiWriter(fs, os.Stdout)
142 | router := gin.Default()
143 | // 注册路由
144 | RegisterRouter(router)
145 |
146 | portStr := fmt.Sprintf(":%d", port)
147 | url := "http://localhost" + portStr
148 | ip := script.GetIpAddress()
149 | startLog := "beancount-gs start at " + url
150 | if ip != "" {
151 | startLog += " or http://" + ip + portStr
152 | }
153 | script.LogSystemInfo(startLog)
154 | // 打开浏览器
155 | script.OpenBrowser(url)
156 | // 打印密钥
157 | script.LogSystemInfo("Secret token is " + script.GenerateServerSecret(secret))
158 | // 启动服务
159 | err = router.Run(portStr)
160 | if err != nil {
161 | script.LogSystemError("Failed to start server, " + err.Error())
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/service/accounts.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/beancount-gs/script"
7 | "github.com/gin-gonic/gin"
8 | "regexp"
9 | "sort"
10 | "strings"
11 | "time"
12 | )
13 |
14 | func QueryValidAccount(c *gin.Context) {
15 | ledgerConfig := script.GetLedgerConfigFromContext(c)
16 | allAccounts := script.GetLedgerAccounts(ledgerConfig.Id)
17 | currencyMap := script.GetLedgerCurrencyMap(ledgerConfig.Id)
18 | result := make([]script.Account, 0)
19 | for _, account := range allAccounts {
20 | if account.EndDate == "" {
21 | // 多个货币处理
22 | multiCurrency := strings.Split(account.Currency, ",")
23 | account.Currency = multiCurrency[0]
24 | account.Currencies = multiCurrencies(*ledgerConfig, multiCurrency, currencyMap)
25 | result = append(result, account)
26 | }
27 | }
28 | OK(c, result)
29 | }
30 |
31 | type accountPosition struct {
32 | Account string `json:"account"`
33 | MarketPosition string `json:"market_position"`
34 | Position string `json:"position"`
35 | }
36 |
37 | func QueryAllAccount(c *gin.Context) {
38 | ledgerConfig := script.GetLedgerConfigFromContext(c)
39 |
40 | bql := fmt.Sprintf("select '\\', account, '\\', sum(convert(value(position), '%s')) as market_position, '\\', sum(convert(value(position), currency)) as position, '\\'", ledgerConfig.OperatingCurrency)
41 | accountPositions := make([]accountPosition, 0)
42 | err := script.BQLQueryListByCustomSelect(ledgerConfig, bql, nil, &accountPositions)
43 | if err != nil {
44 | InternalError(c, err.Error())
45 | return
46 | }
47 | // 将查询结果放入 map 中方便查询账户金额
48 | accountPositionMap := make(map[string]accountPosition)
49 | for _, ap := range accountPositions {
50 | accountPositionMap[ap.Account] = ap
51 | }
52 |
53 | currencyMap := script.GetLedgerCurrencyMap(ledgerConfig.Id)
54 | accounts := script.GetLedgerAccounts(ledgerConfig.Id)
55 | result := make([]script.Account, 0, len(accounts))
56 | for i := 0; i < len(accounts); i++ {
57 | account := accounts[i]
58 | // 过滤已结束的账户
59 | if account.EndDate != "" {
60 | continue
61 | }
62 | // 多个货币处理
63 | multiCurrency := strings.Split(account.Currency, ",")
64 | // 账户主货币
65 | account.Currency = multiCurrency[0]
66 | account.Currencies = multiCurrencies(*ledgerConfig, multiCurrency, currencyMap)
67 |
68 | key := account.Acc
69 | typ := script.GetAccountType(ledgerConfig.Id, key)
70 | account.Type = &typ
71 | marketPosition := strings.Trim(accountPositionMap[key].MarketPosition, " ")
72 | if marketPosition != "" {
73 | fields := strings.Fields(marketPosition)
74 | account.MarketNumber = fields[0]
75 | account.MarketCurrency = fields[1]
76 | account.MarketCurrencySymbol = script.GetCommoditySymbol(ledgerConfig.Id, fields[1])
77 | }
78 | position := strings.Trim(accountPositionMap[key].Position, " ")
79 | if position != "" {
80 | account.Positions = parseAccountPositions(ledgerConfig.Id, position)
81 | }
82 | result = append(result, account)
83 | }
84 | OK(c, result)
85 | }
86 |
87 | func multiCurrencies(ledgerConfig script.Config, multiCurrencyStr []string, currencyMap map[string]script.LedgerCurrency) []script.AccountCurrency {
88 | currencies := make([]script.AccountCurrency, 0)
89 | for i := 0; i < len(multiCurrencyStr); i++ {
90 | accCurrency := script.AccountCurrency{
91 | Currency: multiCurrencyStr[i],
92 | CurrencySymbol: script.GetCommoditySymbol(ledgerConfig.Id, multiCurrencyStr[i]),
93 | }
94 | // 从 map 中获取对应货币的实时汇率和符号
95 | currency, ok := currencyMap[multiCurrencyStr[i]]
96 | if ok {
97 | accCurrency.CurrencySymbol = currency.Symbol
98 | accCurrency.Price = currency.Price
99 | accCurrency.PriceDate = currency.PriceDate
100 | accCurrency.IsAnotherCurrency = multiCurrencyStr[i] != ledgerConfig.OperatingCurrency
101 | }
102 | currencies = append(currencies, accCurrency)
103 | }
104 | return currencies
105 | }
106 |
107 | func parseAccountPositions(ledgerId string, input string) []script.AccountPosition {
108 | // 使用正则表达式提取数字、货币代码和金额
109 | re := regexp.MustCompile(`(-?\d+\.\d+) (\w+)`)
110 | matches := re.FindAllStringSubmatch(input, -1)
111 |
112 | var positions []script.AccountPosition
113 |
114 | // 遍历匹配项并创建 AccountPosition
115 | for _, match := range matches {
116 | number := match[1]
117 | currency := match[2]
118 |
119 | // 获取货币符号
120 | symbol := script.GetCommoditySymbol(ledgerId, currency)
121 |
122 | // 创建 AccountPosition
123 | position := script.AccountPosition{
124 | Number: number,
125 | Currency: currency,
126 | CurrencySymbol: symbol,
127 | }
128 |
129 | // 添加到切片中
130 | positions = append(positions, position)
131 | }
132 |
133 | return positions
134 | }
135 |
136 | func QueryAccountType(c *gin.Context) {
137 | ledgerConfig := script.GetLedgerConfigFromContext(c)
138 | accountTypes := script.GetLedgerAccountTypes(ledgerConfig.Id)
139 |
140 | result := make([]script.AccountType, 0)
141 | for k, v := range accountTypes {
142 | result = append(result, script.AccountType{Key: k, Name: v})
143 | }
144 | sort.Sort(script.AccountTypeSort(result))
145 | OK(c, result)
146 | }
147 |
148 | type AddAccountForm struct {
149 | Date string `form:"date" binding:"required"`
150 | Account string `form:"account" binding:"required"`
151 | // 账户计量单位可以为空
152 | Currency string `form:"currency"`
153 | }
154 |
155 | func AddAccount(c *gin.Context) {
156 | var accountForm AddAccountForm
157 | if err := c.ShouldBindJSON(&accountForm); err != nil {
158 | BadRequest(c, err.Error())
159 | return
160 | }
161 | ledgerConfig := script.GetLedgerConfigFromContext(c)
162 | // 判断账户是否已存在
163 | accounts := script.GetLedgerAccounts(ledgerConfig.Id)
164 | for _, acc := range accounts {
165 | if acc.Acc == accountForm.Account {
166 | DuplicateAccount(c)
167 | return
168 | }
169 | }
170 | line := fmt.Sprintf("%s open %s %s", accountForm.Date, accountForm.Account, accountForm.Currency)
171 | if accountForm.Currency != "" && accountForm.Currency != ledgerConfig.OperatingCurrency {
172 | line += " \"FIFO\""
173 | }
174 | // 写入文件
175 | filePath := ledgerConfig.DataPath + "/account/" + strings.ToLower(script.GetAccountPrefix(accountForm.Account)) + ".bean"
176 | err := script.AppendFileInNewLine(filePath, line)
177 | if err != nil {
178 | InternalError(c, err.Error())
179 | return
180 | }
181 | // 更新缓存
182 | typ := script.GetAccountType(ledgerConfig.Id, accountForm.Account)
183 | account := script.Account{Acc: accountForm.Account, StartDate: accountForm.Date, Currency: accountForm.Currency, Type: &typ}
184 | accounts = append(accounts, account)
185 | script.UpdateLedgerAccounts(ledgerConfig.Id, accounts)
186 | OK(c, account)
187 | }
188 |
189 | type AddAccountTypeForm struct {
190 | Type string `form:"type" binding:"required"`
191 | Name string `form:"name" binding:"required"`
192 | }
193 |
194 | func AddAccountType(c *gin.Context) {
195 | var addAccountTypeForm AddAccountTypeForm
196 | if err := c.ShouldBindJSON(&addAccountTypeForm); err != nil {
197 | BadRequest(c, err.Error())
198 | return
199 | }
200 | ledgerConfig := script.GetLedgerConfigFromContext(c)
201 | accountTypesMap := script.GetLedgerAccountTypes(ledgerConfig.Id)
202 | typ := addAccountTypeForm.Type
203 | accountTypesMap[typ] = addAccountTypeForm.Name
204 | // 更新文件
205 | pathFile := script.GetLedgerAccountTypeFilePath(ledgerConfig.DataPath)
206 | bytes, err := json.Marshal(accountTypesMap)
207 | if err != nil {
208 | InternalError(c, err.Error())
209 | return
210 | }
211 | err = script.WriteFile(pathFile, string(bytes))
212 | if err != nil {
213 | InternalError(c, err.Error())
214 | return
215 | }
216 | // 更新缓存
217 | script.UpdateLedgerAccountTypes(ledgerConfig.Id, accountTypesMap)
218 | OK(c, script.AccountType{
219 | Key: addAccountTypeForm.Type,
220 | Name: addAccountTypeForm.Name,
221 | })
222 | }
223 |
224 | type CloseAccountForm struct {
225 | Date string `form:"date" binding:"required"`
226 | Account string `form:"account" binding:"required"`
227 | }
228 |
229 | func CloseAccount(c *gin.Context) {
230 | var accountForm CloseAccountForm
231 | if err := c.ShouldBindJSON(&accountForm); err != nil {
232 | BadRequest(c, err.Error())
233 | return
234 | }
235 | ledgerConfig := script.GetLedgerConfigFromContext(c)
236 | line := fmt.Sprintf("%s close %s", accountForm.Date, accountForm.Account)
237 | // 写入文件
238 | filePath := ledgerConfig.DataPath + "/account/" + strings.ToLower(script.GetAccountPrefix(accountForm.Account)) + ".bean"
239 | err := script.AppendFileInNewLine(filePath, line)
240 | if err != nil {
241 | InternalError(c, err.Error())
242 | return
243 | }
244 | // 更新缓存
245 | accounts := script.GetLedgerAccounts(ledgerConfig.Id)
246 | for i := 0; i < len(accounts); i++ {
247 | if accounts[i].Acc == accountForm.Account {
248 | accounts[i].EndDate = accountForm.Date
249 | }
250 | }
251 | script.UpdateLedgerAccounts(ledgerConfig.Id, accounts)
252 | OK(c, script.Account{
253 | Acc: accountForm.Account, EndDate: accountForm.Date,
254 | })
255 | }
256 |
257 | func ChangeAccountIcon(c *gin.Context) {
258 | account := c.Query("account")
259 | if account == "" {
260 | BadRequest(c, "account is not blank")
261 | return
262 | }
263 | file, _ := c.FormFile("file")
264 | filePath := "./public/icons/" + script.GetAccountIconName(account) + ".png"
265 | if err := c.SaveUploadedFile(file, filePath); err != nil {
266 | InternalError(c, err.Error())
267 | // 自己完成信息提示
268 | return
269 | }
270 | var result = make(map[string]string)
271 | result["filename"] = filePath
272 | OK(c, result)
273 | }
274 |
275 | type BalanceAccountForm struct {
276 | Date string `form:"date" binding:"required" json:"date"`
277 | Account string `form:"account" binding:"required" json:"account"`
278 | Number string `form:"number" binding:"required" json:"number"`
279 | }
280 |
281 | func BalanceAccount(c *gin.Context) {
282 | var accountForm BalanceAccountForm
283 | if err := c.ShouldBindJSON(&accountForm); err != nil {
284 | BadRequest(c, err.Error())
285 | return
286 | }
287 | ledgerConfig := script.GetLedgerConfigFromContext(c)
288 |
289 | // 获取当前账户信息
290 | var acc script.Account
291 | accounts := script.GetLedgerAccounts(ledgerConfig.Id)
292 | for _, account := range accounts {
293 | if account.Acc == accountForm.Account {
294 | acc = account
295 | }
296 | }
297 |
298 | today, err := time.Parse("2006-01-02", accountForm.Date)
299 | if err != nil {
300 | InternalError(c, err.Error())
301 | return
302 | }
303 | todayStr := today.Format("2006-01-02")
304 | yesterdayStr := today.AddDate(0, 0, -1).Format("2006-01-02")
305 | month := today.Format("2006-01")
306 | line := fmt.Sprintf("\r\n%s pad %s Equity:OpeningBalances", yesterdayStr, accountForm.Account)
307 | line += fmt.Sprintf("\r\n%s balance %s %s %s", todayStr, accountForm.Account, accountForm.Number, acc.Currency)
308 |
309 | // check month bean file exist
310 | err = CreateMonthBeanFileIfNotExist(ledgerConfig.DataPath, month)
311 | if err != nil {
312 | if c != nil {
313 | InternalError(c, err.Error())
314 | }
315 | return
316 | }
317 |
318 | // append padding content to month bean file
319 | err = script.AppendFileInNewLine(script.GetLedgerMonthFilePath(ledgerConfig.DataPath, month), line)
320 | if err != nil {
321 | InternalError(c, err.Error())
322 | return
323 | }
324 | result := make(map[string]string)
325 | result["account"] = accountForm.Account
326 | result["date"] = accountForm.Date
327 | result["marketNumber"] = accountForm.Number
328 | result["marketCurrency"] = ledgerConfig.OperatingCurrency
329 | result["marketCurrencySymbol"] = script.GetCommoditySymbol(ledgerConfig.Id, ledgerConfig.OperatingCurrency)
330 | OK(c, result)
331 | }
332 |
333 | func RefreshAccountCache(c *gin.Context) {
334 | ledgerConfig := script.GetLedgerConfigFromContext(c)
335 | // 加载账户缓存
336 | err := script.LoadLedgerAccounts(ledgerConfig.Id)
337 | if err != nil {
338 | InternalError(c, err.Error())
339 | return
340 | }
341 | // 加载货币缓存
342 | err = script.LoadLedgerCurrencyMap(ledgerConfig)
343 | if err != nil {
344 | InternalError(c, err.Error())
345 | return
346 | }
347 | OK(c, nil)
348 | }
349 |
--------------------------------------------------------------------------------
/service/bean.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "github.com/beancount-gs/script"
7 | )
8 |
9 | // CreateMonthBeanFileIfNotExist create month bean file if not exist, otherwise return.
10 | func CreateMonthBeanFileIfNotExist(ledgerDataPath string, month string) error {
11 | // 文件不存在,则创建
12 | filePath := fmt.Sprintf("%s/month/%s.bean", ledgerDataPath, month)
13 | if !script.FileIfExist(filePath) {
14 | err := script.CreateFile(filePath)
15 | if err != nil {
16 | return errors.New("failed to create file")
17 | }
18 | // include ./2021-11.bean
19 | err = script.AppendFileInNewLine(script.GetLedgerMonthsFilePath(ledgerDataPath), fmt.Sprintf("include \"./%s.bean\"", month))
20 | if err != nil {
21 | return errors.New("failed to append content to months.bean")
22 | }
23 | }
24 | return nil
25 | }
26 |
--------------------------------------------------------------------------------
/service/commodity.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "fmt"
5 | "github.com/beancount-gs/script"
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | type SyncCommodityPriceForm struct {
10 | Commodity string `form:"commodity" binding:"required" json:"commodity"`
11 | Date string `form:"date" binding:"required" json:"date"`
12 | Price string `form:"price" binding:"required" json:"price"`
13 | }
14 |
15 | func SyncCommodityPrice(c *gin.Context) {
16 | var syncCommodityPriceForm SyncCommodityPriceForm
17 | if err := c.ShouldBindJSON(&syncCommodityPriceForm); err != nil {
18 | BadRequest(c, err.Error())
19 | return
20 | }
21 |
22 | ledgerConfig := script.GetLedgerConfigFromContext(c)
23 | filePath := script.GetLedgerPriceFilePath(ledgerConfig.DataPath)
24 | line := fmt.Sprintf("%s price %s %s %s", syncCommodityPriceForm.Date, syncCommodityPriceForm.Commodity, syncCommodityPriceForm.Price, ledgerConfig.OperatingCurrency)
25 | // 写入文件
26 | err := script.AppendFileInNewLine(filePath, line)
27 | if err != nil {
28 | InternalError(c, err.Error())
29 | return
30 | }
31 |
32 | // 刷新货币最新汇率值
33 | err = script.LoadLedgerCurrencyMap(ledgerConfig)
34 | if err != nil {
35 | InternalError(c, err.Error())
36 | return
37 | }
38 | OK(c, syncCommodityPriceForm)
39 | }
40 |
41 | func QueryAllCurrencies(c *gin.Context) {
42 | ledgerConfig := script.GetLedgerConfigFromContext(c)
43 | // 查询货币获取当前汇率
44 | currency := script.RefreshLedgerCurrency(ledgerConfig)
45 | OK(c, currency)
46 | }
47 |
--------------------------------------------------------------------------------
/service/error.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "net/http"
6 | )
7 |
8 | func OK(c *gin.Context, data interface{}) {
9 | c.JSON(http.StatusOK, gin.H{"code": 200, "message": "ok", "data": data})
10 | }
11 |
12 | func BadRequest(c *gin.Context, message string) {
13 | c.JSON(http.StatusOK, gin.H{"code": 400, "message": message})
14 | }
15 |
16 | func Unauthorized(c *gin.Context) {
17 | c.JSON(http.StatusOK, gin.H{"code": 401})
18 | }
19 |
20 | func InternalError(c *gin.Context, message string) {
21 | c.JSON(http.StatusOK, gin.H{"code": 500, "message": message})
22 | }
23 |
24 | func TransactionNotBalance(c *gin.Context) {
25 | c.JSON(http.StatusOK, gin.H{"code": 1001})
26 | }
27 |
28 | func LedgerIsNotExist(c *gin.Context) {
29 | c.JSON(http.StatusOK, gin.H{"code": 1006})
30 | }
31 |
32 | func LedgerIsNotAllowAccess(c *gin.Context) {
33 | c.JSON(http.StatusOK, gin.H{"code": 1006})
34 | }
35 |
36 | func DuplicateAccount(c *gin.Context) {
37 | c.JSON(http.StatusOK, gin.H{"code": 1007})
38 | }
39 |
40 | func ServerSecretNotMatch(c *gin.Context) {
41 | c.JSON(http.StatusOK, gin.H{"code": 1008})
42 | }
43 |
--------------------------------------------------------------------------------
/service/events.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 | "strings"
7 |
8 | "github.com/beancount-gs/script"
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | type Event struct {
13 | Date string `form:"date" binding:"required" json:"date"`
14 | Stage string `form:"stage" json:"stage"`
15 | Type string `form:"type" json:"type"`
16 | Types []string `form:"types" json:"types"`
17 | Description string `form:"description" binding:"required" json:"description"`
18 | }
19 |
20 | // Events 切片包含多个事件
21 | type Events []Event
22 |
23 | func (e Events) Len() int {
24 | return len(e)
25 | }
26 |
27 | func (e Events) Less(i, j int) bool {
28 | return strings.Compare(e[i].Date, e[j].Date) < 0
29 | }
30 |
31 | func (e Events) Swap(i, j int) {
32 | e[i], e[j] = e[j], e[i]
33 | }
34 |
35 | func GetAllEvents(c *gin.Context) {
36 | ledgerConfig := script.GetLedgerConfigFromContext(c)
37 |
38 | beanFilePath := script.GetLedgerEventsFilePath(ledgerConfig.DataPath)
39 | bytes, err := script.ReadFile(beanFilePath)
40 | if err != nil {
41 | InternalError(c, err.Error())
42 | return
43 | }
44 | lines := strings.Split(string(bytes), "\n")
45 | events := Events{}
46 | // foreach lines
47 | for _, line := range lines {
48 | if strings.Trim(line, " ") == "" {
49 | continue
50 | }
51 | // split line by " "
52 | words := strings.Fields(line)
53 | if len(words) < 4 {
54 | continue
55 | }
56 | if words[1] != "event" {
57 | continue
58 | }
59 | events = append(events, Event{
60 | Date: words[0],
61 | Type: strings.ReplaceAll(words[2], "\"", ""),
62 | Description: strings.ReplaceAll(words[3], "\"", ""),
63 | })
64 | }
65 | if len(events) > 0 {
66 | // events 按时间倒序排列
67 | sort.Sort(sort.Reverse(events))
68 | }
69 | OK(c, events)
70 | }
71 |
72 | func AddEvent(c *gin.Context) {
73 | var event Event
74 | if err := c.ShouldBindJSON(&event); err != nil {
75 | BadRequest(c, err.Error())
76 | return
77 | }
78 |
79 | ledgerConfig := script.GetLedgerConfigFromContext(c)
80 | filePath := script.GetLedgerEventsFilePath(ledgerConfig.DataPath)
81 |
82 | if event.Type != "" {
83 | event.Types = []string{event.Type}
84 | }
85 |
86 | // 定义Event类型的数组
87 | events := make([]Event, 0)
88 |
89 | if event.Types != nil {
90 | for _, t := range event.Types {
91 | events = append(events, Event{
92 | Date: event.Date,
93 | Type: t,
94 | Description: event.Description,
95 | })
96 | line := fmt.Sprintf("%s event \"%s\" \"%s\"", event.Date, t, event.Description)
97 | // 写入文件
98 | err := script.AppendFileInNewLine(filePath, line)
99 | if err != nil {
100 | InternalError(c, err.Error())
101 | return
102 | }
103 | }
104 | }
105 |
106 | OK(c, events)
107 | }
108 |
109 | func DeleteEvent(c *gin.Context) {
110 | var event Event
111 | if err := c.ShouldBindJSON(&event); err != nil {
112 | BadRequest(c, err.Error())
113 | return
114 | }
115 |
116 | ledgerConfig := script.GetLedgerConfigFromContext(c)
117 | filePath := script.GetLedgerEventsFilePath(ledgerConfig.DataPath)
118 |
119 | line := fmt.Sprintf("%s event \"%s\" \"%s\"", event.Date, event.Type, event.Description)
120 | err := script.DeleteLinesWithText(filePath, line)
121 | if err != nil {
122 | InternalError(c, err.Error())
123 | return
124 | }
125 | OK(c, nil)
126 | }
127 |
--------------------------------------------------------------------------------
/service/import.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "bufio"
5 | "encoding/csv"
6 | "errors"
7 | "github.com/beancount-gs/script"
8 | "github.com/gin-gonic/gin"
9 | "golang.org/x/text/encoding/simplifiedchinese"
10 | "io"
11 | "strconv"
12 | "strings"
13 | "time"
14 | )
15 |
16 | func ImportAliPayCSV(c *gin.Context) {
17 | ledgerConfig := script.GetLedgerConfigFromContext(c)
18 |
19 | file, _ := c.FormFile("file")
20 | f, _ := file.Open()
21 | reader := csv.NewReader(simplifiedchinese.GBK.NewDecoder().Reader(bufio.NewReader(f)))
22 |
23 | result := make([]Transaction, 0)
24 |
25 | currency := "CNY"
26 | currencySymbol := script.GetCommoditySymbol(ledgerConfig.Id, currency)
27 |
28 | for {
29 | lines, err := reader.Read()
30 | if errors.Is(err, io.EOF) {
31 | break
32 | } else if err != nil {
33 | script.LogError(ledgerConfig.Mail, err.Error())
34 | }
35 | if len(lines) == 17 {
36 | transaction, err := importBrowserAliPayCSV(lines, currency, currencySymbol)
37 | if err != nil {
38 | script.LogInfo(ledgerConfig.Mail, err.Error())
39 | continue
40 | }
41 | if transaction.Account == "" {
42 | script.LogInfo(ledgerConfig.Mail, "Invalid transaction")
43 | continue
44 | }
45 | result = append(result, transaction)
46 | } else if len(lines) == 12 || len(lines) == 13 {
47 | transaction, err := importMobileAliPayCSV(lines, currency, currencySymbol)
48 | if err != nil {
49 | script.LogInfo(ledgerConfig.Mail, err.Error())
50 | continue
51 | }
52 | if transaction.Account == "" {
53 | script.LogInfo(ledgerConfig.Mail, "Invalid transaction")
54 | continue
55 | }
56 | result = append(result, transaction)
57 | }
58 | }
59 |
60 | OK(c, result)
61 | }
62 |
63 | func importBrowserAliPayCSV(lines []string, currency string, currencySymbol string) (Transaction, error) {
64 | dateColumn := strings.Fields(lines[2])
65 | status := strings.Trim(lines[15], " ")
66 | account := ""
67 | if status == "" {
68 | account = ""
69 | } else if status == "已收入" {
70 | account = "Income:"
71 | } else {
72 | account = "Expenses:"
73 | }
74 |
75 | if len(dateColumn) >= 2 {
76 | return Transaction{
77 | Id: strings.Trim(lines[0], " "),
78 | Date: strings.Trim(dateColumn[0], " "),
79 | Payee: strings.Trim(lines[7], " "),
80 | Narration: strings.Trim(lines[8], " "),
81 | Number: strings.Trim(lines[9], " "),
82 | Account: account,
83 | Currency: currency,
84 | CurrencySymbol: currencySymbol,
85 | }, nil
86 | }
87 | return Transaction{}, errors.New("parse error")
88 | }
89 |
90 | func importMobileAliPayCSV(lines []string, currency string, currencySymbol string) (Transaction, error) {
91 | dateColumn := strings.Fields(lines[0])
92 | status := strings.Trim(lines[5], " ")
93 | account := ""
94 | if status == "" {
95 | account = ""
96 | } else if status == "支出" {
97 | account = "Expenses:"
98 | } else {
99 | account = "Income:"
100 | }
101 |
102 | if len(dateColumn) >= 2 {
103 | return Transaction{
104 | Id: strings.Trim(lines[9], " "),
105 | Date: strings.Trim(dateColumn[0], " "),
106 | Payee: strings.Trim(lines[2], " "),
107 | Narration: strings.Trim(lines[4], " "),
108 | Number: strings.Trim(lines[6], " "),
109 | Account: account,
110 | Currency: currency,
111 | CurrencySymbol: currencySymbol,
112 | }, nil
113 | }
114 | return Transaction{}, errors.New("parse error")
115 | }
116 |
117 | func ImportWxPayCSV(c *gin.Context) {
118 | ledgerConfig := script.GetLedgerConfigFromContext(c)
119 |
120 | file, _ := c.FormFile("file")
121 | f, _ := file.Open()
122 | reader := csv.NewReader(bufio.NewReader(f))
123 |
124 | result := make([]Transaction, 0)
125 |
126 | currency := "CNY"
127 | currencySymbol := script.GetCommoditySymbol(ledgerConfig.Id, currency)
128 |
129 | for {
130 | lines, err := reader.Read()
131 | if err == io.EOF {
132 | break
133 | } else if err != nil {
134 | script.LogError(ledgerConfig.Mail, err.Error())
135 | }
136 | if len(lines) > 8 {
137 | fields := strings.Fields(lines[0])
138 | status := strings.Trim(lines[4], " ")
139 | account := ""
140 | if status == "收入" {
141 | account = "Income:"
142 | } else if status == "支出" {
143 | account = "Expenses:"
144 | } else {
145 | continue
146 | }
147 |
148 | if len(fields) >= 2 {
149 | result = append(result, Transaction{
150 | Id: strings.Trim(lines[8], " "),
151 | Date: strings.Trim(fields[0], " "),
152 | Payee: strings.Trim(lines[2], " "),
153 | Narration: strings.Trim(lines[3], " "),
154 | Number: strings.Trim(lines[5], "¥"),
155 | Account: account,
156 | Currency: currency,
157 | CurrencySymbol: currencySymbol,
158 | })
159 | }
160 | }
161 | }
162 |
163 | OK(c, result)
164 | }
165 |
166 | func ImportICBCCSV(c *gin.Context) {
167 | ledgerConfig := script.GetLedgerConfigFromContext(c)
168 |
169 | file, _ := c.FormFile("file")
170 | f, _ := file.Open()
171 | reader := csv.NewReader(bufio.NewReader(f))
172 |
173 | result := make([]Transaction, 0)
174 |
175 | currency := "CNY"
176 | currencySymbol := script.GetCommoditySymbol(ledgerConfig.Id, currency)
177 |
178 | id := 0
179 | for {
180 | lines, err := reader.Read()
181 | if errors.Is(err, io.EOF) {
182 | break
183 | } else if err != nil {
184 | script.LogError(ledgerConfig.Mail, err.Error())
185 | }
186 | if len(lines) >= 13 && lines[0] != "交易日期" {
187 | incomeAmount := formatStr(lines[8])
188 | expensesAmount := formatStr(lines[9])
189 | account := ""
190 | number := ""
191 | switch {
192 | case incomeAmount != "":
193 | account = "Income:"
194 | number = strings.ReplaceAll(incomeAmount, ",", "")
195 | case expensesAmount != "":
196 | account = "Expenses:"
197 | number = strings.ReplaceAll(expensesAmount, ",", "")
198 | default:
199 | continue
200 | }
201 |
202 | id++
203 | result = append(result, Transaction{
204 | Id: strconv.Itoa(id),
205 | Date: formatStr(lines[0]),
206 | Payee: formatStr(lines[12]),
207 | Narration: formatStr(lines[1]),
208 | Number: number,
209 | Account: account,
210 | Currency: currency,
211 | CurrencySymbol: currencySymbol,
212 | })
213 | }
214 | }
215 |
216 | OK(c, result)
217 | }
218 |
219 | func ImportABCCSV(c *gin.Context) {
220 | ledgerConfig := script.GetLedgerConfigFromContext(c)
221 |
222 | file, _ := c.FormFile("file")
223 | f, _ := file.Open()
224 | reader := csv.NewReader(bufio.NewReader(f))
225 |
226 | result := make([]Transaction, 0)
227 |
228 | currency := "CNY"
229 | currencySymbol := script.GetCommoditySymbol(ledgerConfig.Id, currency)
230 |
231 | id := 0
232 | for {
233 | lines, err := reader.Read()
234 | if errors.Is(err, io.EOF) {
235 | break
236 | } else if err != nil {
237 | script.LogError(ledgerConfig.Mail, err.Error())
238 | }
239 | if len(lines) >= 11 && lines[0] != "交易日期" {
240 | amount := formatStr(lines[2])
241 | account := ""
242 | number := ""
243 | switch {
244 | case strings.HasPrefix(amount, "+"):
245 | account = "Income:"
246 | number = strings.ReplaceAll(amount, "+", "")
247 | case strings.HasPrefix(amount, "-"):
248 | account = "Expenses:"
249 | number = strings.ReplaceAll(amount, "-", "")
250 | default:
251 | continue
252 | }
253 |
254 | id++
255 | date, err := time.Parse("20060102", formatStr(lines[0]))
256 | if err != nil {
257 | continue
258 | }
259 | result = append(result, Transaction{
260 | Id: strconv.Itoa(id),
261 | Date: date.Format("2006-01-02"),
262 | Payee: formatStr(lines[10]),
263 | Narration: formatStr(lines[9]),
264 | Number: number,
265 | Account: account,
266 | Currency: currency,
267 | CurrencySymbol: currencySymbol,
268 | })
269 | }
270 | }
271 |
272 | OK(c, result)
273 | }
274 |
275 | func formatStr(str string) string {
276 | str = strings.Trim(str, "\t")
277 | return strings.Trim(str, " ")
278 | }
279 |
--------------------------------------------------------------------------------
/service/ledger.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "bytes"
5 | "crypto/sha1"
6 | "encoding/hex"
7 | "io"
8 | "io/ioutil"
9 | "os"
10 | "os/exec"
11 | "sort"
12 | "strings"
13 | "time"
14 |
15 | "github.com/beancount-gs/script"
16 | "github.com/gin-gonic/gin"
17 | )
18 |
19 | func CheckBeancount(c *gin.Context) {
20 | cmd := exec.Command("bean-query", "--version")
21 | output, err := cmd.Output()
22 | if err != nil {
23 | InternalError(c, err.Error())
24 | return
25 | }
26 | OK(c, string(output))
27 | }
28 |
29 | func QueryServerConfig(c *gin.Context) {
30 | OK(c, script.GetServerConfig())
31 | }
32 |
33 | type QueryLedgerResult struct {
34 | Mail string `json:"mail"`
35 | Title string `json:"title"`
36 | CreateDate string `json:"createDate"`
37 | OperatingCurrency string `json:"operatingCurrency"`
38 | }
39 |
40 | type LedgerSort []QueryLedgerResult
41 |
42 | func (s LedgerSort) Len() int {
43 | return len(s)
44 | }
45 |
46 | func (s LedgerSort) Swap(i, j int) {
47 | s[i], s[j] = s[j], s[i]
48 | }
49 |
50 | func (s LedgerSort) Less(i, j int) bool {
51 | return s[i].CreateDate <= s[j].CreateDate && s[i].Mail <= s[j].Mail
52 | }
53 |
54 | func QueryLedgerList(c *gin.Context) {
55 | result := make([]QueryLedgerResult, 0)
56 | for _, config := range script.GetLedgerConfigMap() {
57 | result = append(result, QueryLedgerResult{
58 | Title: config.Title,
59 | Mail: config.Mail,
60 | CreateDate: config.CreateDate,
61 | OperatingCurrency: config.OperatingCurrency,
62 | })
63 | }
64 | sort.Sort(LedgerSort(result))
65 | OK(c, result)
66 | }
67 |
68 | type UpdateConfigForm struct {
69 | Secret string `form:"secret" binding:"required"`
70 | StartDate string `form:"startDate" binding:"required"`
71 | DataPath string `form:"dataPath" binding:"required"`
72 | OperatingCurrency string `form:"operatingCurrency" binding:"required"`
73 | OpeningBalances string `form:"openingBalances" binding:"required"`
74 | IsBak bool `form:"isBak" binding:"required"`
75 | }
76 |
77 | func UpdateServerConfig(c *gin.Context) {
78 | var updateConfigForm UpdateConfigForm
79 | if err := c.ShouldBindJSON(&updateConfigForm); err != nil {
80 | BadRequest(c, err.Error())
81 | return
82 | }
83 | if !script.EqualServerSecret(updateConfigForm.Secret) {
84 | ServerSecretNotMatch(c)
85 | return
86 | }
87 | var serverConfig = script.Config{
88 | OperatingCurrency: updateConfigForm.OperatingCurrency,
89 | DataPath: updateConfigForm.DataPath,
90 | StartDate: updateConfigForm.StartDate,
91 | OpeningBalances: updateConfigForm.OpeningBalances,
92 | IsBak: updateConfigForm.IsBak,
93 | }
94 | // 更新配置
95 | err := script.UpdateServerConfig(serverConfig)
96 | if err != nil {
97 | InternalError(c, err.Error())
98 | return
99 | }
100 | // 账本目录不存在,则创建
101 | dataPath := serverConfig.DataPath
102 | if !script.FileIfExist(dataPath) {
103 | err = script.MkDir(dataPath)
104 | if err != nil {
105 | InternalError(c, err.Error())
106 | return
107 | }
108 | }
109 | // 加载账户缓存
110 | err = script.LoadLedgerConfigMap()
111 | if err != nil {
112 | InternalError(c, err.Error())
113 | return
114 | }
115 | err = script.LoadLedgerAccountsMap()
116 | if err != nil {
117 | InternalError(c, err.Error())
118 | return
119 | }
120 | QueryServerConfig(c)
121 | }
122 |
123 | type LoginForm struct {
124 | LedgerName string `form:"ledgerName" binding:"required"`
125 | Secret string `form:"secret"`
126 | OperatingCurrency string `form:"operatingCurrency"`
127 | StartDate string `form:"startDate"`
128 | OpeningBalances string `form:"openingBalances"`
129 | IsBak bool `form:"isBak"`
130 | }
131 |
132 | func OpenOrCreateLedger(c *gin.Context) {
133 | var loginForm LoginForm
134 | if err := c.ShouldBindJSON(&loginForm); err != nil {
135 | BadRequest(c, err.Error())
136 | return
137 | }
138 | // is mail exist white list
139 | if !script.IsInWhiteList(loginForm.LedgerName) {
140 | LedgerIsNotExist(c)
141 | return
142 | }
143 |
144 | t := sha1.New()
145 | _, err := io.WriteString(t, loginForm.LedgerName+loginForm.Secret)
146 | if err != nil {
147 | LedgerIsNotAllowAccess(c)
148 | return
149 | }
150 |
151 | ledgerId := hex.EncodeToString(t.Sum(nil))
152 | userLedger := script.GetLedgerConfigByMail(loginForm.LedgerName)
153 | if userLedger != nil {
154 | if ledgerId != userLedger.Id {
155 | LedgerIsNotAllowAccess(c)
156 | return
157 | }
158 | // 账本已存在,返回账本信息
159 | resultMap := make(map[string]string)
160 | resultMap["ledgerId"] = ledgerId
161 | resultMap["title"] = userLedger.Title
162 | resultMap["currency"] = userLedger.OperatingCurrency
163 | resultMap["currencySymbol"] = script.GetServerCommoditySymbol(userLedger.OperatingCurrency)
164 | resultMap["createDate"] = userLedger.CreateDate
165 | OK(c, resultMap)
166 | return
167 | }
168 |
169 | userLedger, err = createNewLedger(loginForm, ledgerId)
170 | if err != nil {
171 | InternalError(c, err.Error())
172 | return
173 | }
174 |
175 | resultMap := make(map[string]string)
176 | resultMap["ledgerId"] = ledgerId
177 | resultMap["title"] = userLedger.Title
178 | resultMap["currency"] = userLedger.OperatingCurrency
179 | resultMap["currencySymbol"] = script.GetCommoditySymbol(ledgerId, userLedger.OperatingCurrency)
180 | resultMap["createDate"] = userLedger.CreateDate
181 | OK(c, resultMap)
182 | }
183 |
184 | func DeleteLedger(c *gin.Context) {
185 | ledgerConfig := script.GetLedgerConfigFromContext(c)
186 | // remove from ledger_config.json
187 | ledgerConfigMap := script.GetLedgerConfigMap()
188 | delete(ledgerConfigMap, ledgerConfig.Id)
189 | err := script.WriteLedgerConfigMap(ledgerConfigMap)
190 | if err != nil {
191 | InternalError(c, "Failed to update ledger_config.json")
192 | return
193 | }
194 | // remove from account cache
195 | script.ClearLedgerAccounts(ledgerConfig.Id)
196 | script.LogInfo(ledgerConfig.Mail, "Success clear ledger account cache "+ledgerConfig.Id)
197 | // remove from account types cache
198 | script.ClearLedgerAccountTypes(ledgerConfig.Id)
199 | script.LogInfo(ledgerConfig.Mail, "Success clear ledger account types cache "+ledgerConfig.Id)
200 | // delete source file
201 | err = os.RemoveAll(ledgerConfig.DataPath)
202 | if err != nil {
203 | script.LogError(ledgerConfig.Mail, "Failed to delete ledger, cause by "+err.Error())
204 | InternalError(c, "Failed to delete ledger")
205 | return
206 | }
207 | script.LogInfo(ledgerConfig.Mail, "Success delete "+ledgerConfig.DataPath)
208 | OK(c, "OK")
209 | }
210 |
211 | func CheckLedger(c *gin.Context) {
212 | var stderr bytes.Buffer
213 | ledgerConfig := script.GetLedgerConfigFromContext(c)
214 | cmd := exec.Command("bean-check", script.GetLedgerIndexFilePath(ledgerConfig.DataPath))
215 | cmd.Stderr = &stderr
216 | _, err := cmd.Output()
217 | result := make([]string, 0)
218 | if err != nil {
219 | errors := strings.Split(stderr.String(), "\r\n")
220 | for _, e := range errors {
221 | if e == "" {
222 | continue
223 | }
224 | result = append(result, e)
225 | }
226 | }
227 | OK(c, result)
228 | }
229 |
230 | func createNewLedger(loginForm LoginForm, ledgerId string) (*script.Config, error) {
231 | // create new ledger
232 | serverConfig := script.GetServerConfig()
233 | ledgerConfigMap := script.GetLedgerConfigMap()
234 |
235 | currency := loginForm.OperatingCurrency
236 | if currency == "" {
237 | currency = serverConfig.OperatingCurrency
238 | }
239 | startDate := loginForm.StartDate
240 | if startDate == "" {
241 | startDate = serverConfig.StartDate
242 | }
243 | openingBalances := loginForm.OpeningBalances
244 | if openingBalances == "" {
245 | openingBalances = serverConfig.OpeningBalances
246 | }
247 |
248 | ledgerConfig := script.Config{
249 | Id: ledgerId,
250 | Mail: loginForm.LedgerName,
251 | Title: loginForm.LedgerName,
252 | DataPath: serverConfig.DataPath + "/" + ledgerId,
253 | OperatingCurrency: currency,
254 | StartDate: startDate,
255 | OpeningBalances: openingBalances,
256 | IsBak: loginForm.IsBak,
257 | CreateDate: time.Now().Format("2006-01-02"),
258 | }
259 | // init ledger files
260 | err := initLedgerFiles(script.GetTemplateLedgerConfigDirPath(), ledgerConfig.DataPath, ledgerConfig)
261 | if err != nil {
262 | return nil, err
263 | }
264 | // add ledger config to ledger_config.json
265 | ledgerConfigMap[ledgerId] = ledgerConfig
266 | err = script.WriteLedgerConfigMap(ledgerConfigMap)
267 | if err != nil {
268 | return nil, err
269 | }
270 | // add accounts cache
271 | err = script.LoadLedgerAccounts(ledgerId)
272 | if err != nil {
273 | return nil, err
274 | }
275 | // add currency cache
276 | err = script.LoadLedgerCurrencyMap(&ledgerConfig)
277 | if err != nil {
278 | return nil, err
279 | }
280 | return &ledgerConfig, nil
281 | }
282 |
283 | func initLedgerFiles(sourceFilePath string, targetFilePath string, ledgerConfig script.Config) error {
284 | return copyFile(sourceFilePath, targetFilePath, ledgerConfig)
285 | }
286 |
287 | func copyFile(sourceFilePath string, targetFilePath string, ledgerConfig script.Config) error {
288 | rd, err := ioutil.ReadDir(sourceFilePath)
289 | if err != nil {
290 | return err
291 | }
292 | for _, fi := range rd {
293 | newSourceFilePath := sourceFilePath + "/" + fi.Name()
294 | newTargetFilePath := targetFilePath + "/" + fi.Name()
295 | if fi.IsDir() {
296 | err = script.MkDir(newTargetFilePath)
297 | if err == nil {
298 | err = copyFile(newSourceFilePath, newTargetFilePath, ledgerConfig)
299 | }
300 | } else if !script.FileIfExist(newTargetFilePath) {
301 | var fileContent, err = script.ReadFile(newSourceFilePath)
302 | if err != nil {
303 | return err
304 | }
305 | err = script.WriteFile(newTargetFilePath, strings.ReplaceAll(strings.ReplaceAll(string(fileContent), "%startDate%", ledgerConfig.StartDate), "%operatingCurrency%", ledgerConfig.OperatingCurrency))
306 | if err != nil {
307 | return err
308 | }
309 | script.LogInfo(ledgerConfig.Mail, "Success create file "+newTargetFilePath)
310 | }
311 | if err != nil {
312 | return err
313 | }
314 | }
315 | return nil
316 | }
317 |
--------------------------------------------------------------------------------
/service/source_file.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "fmt"
5 | "github.com/beancount-gs/script"
6 | "github.com/gin-gonic/gin"
7 | "os"
8 | "strings"
9 | "time"
10 | )
11 |
12 | func QueryLedgerSourceFileDir(c *gin.Context) {
13 | ledgerConfig := script.GetLedgerConfigFromContext(c)
14 | result, err := dirs(ledgerConfig.DataPath, ledgerConfig.DataPath)
15 | if err != nil {
16 | InternalError(c, err.Error())
17 | return
18 | }
19 | OK(c, result)
20 | }
21 |
22 | func dirs(parent string, dirPath string) ([]string, error) {
23 | result := make([]string, 0)
24 | rd, err := os.ReadDir(dirPath)
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | for _, dir := range rd {
30 | parentDir := dirPath + "/" + dir.Name()
31 | if dir.IsDir() {
32 | // 跳过备份文件夹
33 | if dir.Name() == "bak" {
34 | continue
35 | }
36 | files, err := dirs(parent, parentDir)
37 | if err != nil {
38 | return nil, err
39 | }
40 | result = append(result, files...)
41 | } else {
42 | fmt.Println(parentDir)
43 | result = append(result, strings.ReplaceAll(parentDir, parent+"/", ""))
44 | }
45 | }
46 | return result, nil
47 | }
48 |
49 | func QueryLedgerSourceFileContent(c *gin.Context) {
50 | ledgerConfig := script.GetLedgerConfigFromContext(c)
51 | queryParams := script.GetQueryParams(c)
52 | if queryParams.Path == "" {
53 | BadRequest(c, "params must not be blank")
54 | return
55 | }
56 | bytes, err := script.ReadFile(ledgerConfig.DataPath + "/" + queryParams.Path)
57 | if err != nil {
58 | InternalError(c, err.Error())
59 | return
60 | }
61 | OK(c, string(bytes))
62 | }
63 |
64 | type UpdateSourceFileForm struct {
65 | Path string `form:"path" binding:"required"`
66 | Content string `form:"content"`
67 | }
68 |
69 | func UpdateLedgerSourceFileContent(c *gin.Context) {
70 | ledgerConfig := script.GetLedgerConfigFromContext(c)
71 |
72 | var updateSourceFileForm UpdateSourceFileForm
73 | if err := c.ShouldBindJSON(&updateSourceFileForm); err != nil {
74 | BadRequest(c, err.Error())
75 | return
76 | }
77 |
78 | sourceFilePath := ledgerConfig.DataPath + "/" + updateSourceFileForm.Path
79 | targetFilePath := ledgerConfig.DataPath + "/bak/" + time.Now().Format("20060102150405") + "_" + strings.ReplaceAll(updateSourceFileForm.Path, "/", "_")
80 | // 备份数据
81 | if ledgerConfig.IsBak {
82 | err := script.CopyFile(sourceFilePath, targetFilePath)
83 | if err != nil {
84 | InternalError(c, err.Error())
85 | return
86 | }
87 | }
88 |
89 | err := script.WriteFile(sourceFilePath, updateSourceFileForm.Content)
90 | if err != nil {
91 | InternalError(c, err.Error())
92 | return
93 | }
94 |
95 | // 更新外币种源文件后,更新缓存
96 | if strings.Contains(updateSourceFileForm.Path, "currency.json") {
97 | err = script.LoadLedgerCurrencyMap(ledgerConfig)
98 | if err != nil {
99 | InternalError(c, err.Error())
100 | return
101 | }
102 | }
103 |
104 | OK(c, nil)
105 | }
106 |
--------------------------------------------------------------------------------
/service/tags.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "github.com/beancount-gs/script"
5 | "github.com/gin-gonic/gin"
6 | )
7 |
8 | type Tags struct {
9 | Value string `bql:"distinct tags" json:"value"`
10 | }
11 |
12 | func QueryTags(c *gin.Context) {
13 | ledgerConfig := script.GetLedgerConfigFromContext(c)
14 | tags := make([]Tags, 0)
15 | err := script.BQLQueryList(ledgerConfig, nil, &tags)
16 | if err != nil {
17 | InternalError(c, err.Error())
18 | return
19 | }
20 |
21 | result := make([]string, 0)
22 | for _, t := range tags {
23 | if t.Value != "" {
24 | result = append(result, t.Value)
25 | }
26 | }
27 |
28 | OK(c, result)
29 | }
30 |
--------------------------------------------------------------------------------
/service/version.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import "github.com/gin-gonic/gin"
4 |
5 | func QueryVersion(c *gin.Context) {
6 | OK(c, "v1.2.2")
7 | }
8 |
--------------------------------------------------------------------------------
/snapshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/snapshot.png
--------------------------------------------------------------------------------
/template/.beancount-gs/account_type.json:
--------------------------------------------------------------------------------
1 | {
2 | "Assets:Fixed": "固定资产",
3 | "Assets:Invest": "投资",
4 | "Assets:Invest:Fund": "基金",
5 | "Assets:Flow": "现金流",
6 | "Expenses:Life:Food": "饮食",
7 | "Expenses:Life:Travel": "出行",
8 | "Expenses:Life:Shopping": "购物",
9 | "Expenses:Life:House": "居住",
10 | "Expenses:Life:Subscribe": "订阅",
11 | "Expenses:Life:Other": "其他",
12 | "Expenses:Life:Hobby": "爱好",
13 | "Expenses:Work": "工作支出",
14 | "Expenses:Life": "生活消费",
15 | "Income:Work": "工作收入",
16 | "Income:Gov": "财政补贴",
17 | "Income:Invest": "投资收益",
18 | "Liabilities:Cycle": "周期性贷款",
19 | "Liabilities:Life": "消费贷款"
20 | }
--------------------------------------------------------------------------------
/template/account/assets.bean:
--------------------------------------------------------------------------------
1 | %startDate% open Assets:Fixed:House:商品房 %operatingCurrency%
2 | %startDate% open Assets:Invest:Stock:股票 %operatingCurrency%
3 | %startDate% open Assets:Invest:Fund:自定义基金 SHR
4 | %startDate% open Assets:Invest:Deposit:定期 %operatingCurrency%
5 | %startDate% open Assets:Invest:Gold:黄金 %operatingCurrency%
6 | %startDate% open Assets:Flow:Bank:ICBC:工商银行 %operatingCurrency%
7 | %startDate% open Assets:Flow:EBank:AliPay:支付宝 %operatingCurrency%
8 | %startDate% open Assets:Flow:EBank:WxPay:微信支付 %operatingCurrency%
9 | %startDate% open Assets:Flow:Cash:现金 %operatingCurrency%
--------------------------------------------------------------------------------
/template/account/equity.bean:
--------------------------------------------------------------------------------
1 | %startDate% open Equity:OpeningBalances
--------------------------------------------------------------------------------
/template/account/expenses.bean:
--------------------------------------------------------------------------------
1 | %startDate% open Expenses:Life:Food:Meal:早餐 %operatingCurrency%
2 | %startDate% open Expenses:Life:Food:Meal:午餐 %operatingCurrency%
3 | %startDate% open Expenses:Life:Food:Meal:晚餐 %operatingCurrency%
4 | %startDate% open Expenses:Life:Food:Meal:聚餐 %operatingCurrency%
5 | %startDate% open Expenses:Life:Food:Coffee:咖啡 %operatingCurrency%
6 | %startDate% open Expenses:Life:Food:Drink:饮料 %operatingCurrency%
7 | %startDate% open Expenses:Life:Food:Fruit:水果 %operatingCurrency%
8 | %startDate% open Expenses:Life:Food:Snack:零食 %operatingCurrency%
9 | %startDate% open Expenses:Life:Travel:Bus:公交地铁 %operatingCurrency%
10 | %startDate% open Expenses:Life:Travel:Taxi:出租车 %operatingCurrency%
11 | %startDate% open Expenses:Life:Travel:Train:火车 %operatingCurrency%
12 | %startDate% open Expenses:Life:Travel:Airplane:飞机 %operatingCurrency%
13 | %startDate% open Expenses:Life:Travel:Bike:共享单车 %operatingCurrency%
14 | %startDate% open Expenses:Life:Shopping:Clothes:衣服 %operatingCurrency%
15 | %startDate% open Expenses:Life:Shopping:Shoe:鞋 %operatingCurrency%
16 | %startDate% open Expenses:Life:Shopping:Sock:袜子 %operatingCurrency%
17 | %startDate% open Expenses:Life:Shopping:购物 %operatingCurrency%
18 | %startDate% open Expenses:Life:House:Rent:房租 %operatingCurrency%
19 | %startDate% open Expenses:Life:House:Hotel:酒店 %operatingCurrency%
20 | %startDate% open Expenses:Life:House:Water:用水 %operatingCurrency%
21 | %startDate% open Expenses:Life:House:Gas:天然气 %operatingCurrency%
22 | %startDate% open Expenses:Life:House:Electricity:用电 %operatingCurrency%
23 | %startDate% open Expenses:Life:Subscribe:Mobile:手机话费 %operatingCurrency%
24 | %startDate% open Expenses:Life:Subscribe:会员订阅 %operatingCurrency%
25 | %startDate% open Expenses:Life:Other:Exchange:红包转账 %operatingCurrency%
26 | %startDate% open Expenses:Life:Other:Commission:手续费 %operatingCurrency%
27 | %startDate% open Expenses:Life:Hobby:Book:图书 %operatingCurrency%
28 | %startDate% open Expenses:Life:Hobby:Camera:摄影 %operatingCurrency%
29 | %startDate% open Expenses:Life:Hobby:Travel:Ticket:门票 %operatingCurrency%
30 | %startDate% open Expenses:Life:Hobby:Travel:Souvenir:纪念品 %operatingCurrency%
31 | %startDate% open Expenses:Work:Tax:个人所得税 %operatingCurrency%
32 | %startDate% open Expenses:Work:Insurance:三险 %operatingCurrency%
33 | %startDate% open Expenses:Work:Punish:考勤 %operatingCurrency%
--------------------------------------------------------------------------------
/template/account/income.bean:
--------------------------------------------------------------------------------
1 | %startDate% open Income:Work:Salary:工作收入 %operatingCurrency%
2 | %startDate% open Income:Work:Bonus:奖金 %operatingCurrency%
3 | %startDate% open Income:Work:HouseFund:单位公积金 %operatingCurrency%
4 | %startDate% open Income:Gov:退税 %operatingCurrency%
5 | %startDate% open Income:Gov:政府补贴 %operatingCurrency%
6 | %startDate% open Income:Invest:投资收益 %operatingCurrency%
--------------------------------------------------------------------------------
/template/account/liabilities.bean:
--------------------------------------------------------------------------------
1 | %startDate% open Liabilities:Cycle:ICBC:房贷 %operatingCurrency%
2 | %startDate% open Liabilities:Life:JD:京东白条 %operatingCurrency%
3 | %startDate% open Liabilities:Life:Huabei:花呗 %operatingCurrency%
4 | %startDate% open Liabilities:Life:CreditCard:信用卡 %operatingCurrency%
--------------------------------------------------------------------------------
/template/event/events.bean:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/template/event/events.bean
--------------------------------------------------------------------------------
/template/history.bean:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/template/history.bean
--------------------------------------------------------------------------------
/template/includes.bean:
--------------------------------------------------------------------------------
1 | ; 账户定义
2 | include "./account/assets.bean"
3 | include "./account/equity.bean"
4 | include "./account/expenses.bean"
5 | include "./account/income.bean"
6 | include "./account/liabilities.bean"
7 | ; 多币种配置
8 | include "./price/prices.bean"
9 | ; 历史数据(用于 导入 之前的数据)
10 | include "./history.bean"
11 | ; 新的数据(按月拆分)
12 | include "./month/months.bean"
13 | ; 事件数据
14 | include "./event/events.bean"
--------------------------------------------------------------------------------
/template/index.bean:
--------------------------------------------------------------------------------
1 | option "title" "我的账本"
2 | option "operating_currency" "%operatingCurrency%"
3 | option "render_commas" "TRUE"
4 |
5 | ; fava 配置
6 | 1970-01-01 custom "fava-option" "interval" "day"
7 | 1970-01-01 custom "fava-option" "language" "zh_CN"
8 |
9 | include "./includes.bean"
10 |
11 |
12 |
--------------------------------------------------------------------------------
/template/month/months.bean:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/template/month/months.bean
--------------------------------------------------------------------------------
/template/price/commodities.bean:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaoXuebin/beancount-gs/2c18b17b0947564af3adb3d2d4de6ed2c1426cfb/template/price/commodities.bean
--------------------------------------------------------------------------------
/template/price/prices.bean:
--------------------------------------------------------------------------------
1 | include "./commodities.bean"
2 |
3 | %startDate% price SHR 2.00 %operatingCurrency%
--------------------------------------------------------------------------------
/tests/main_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/stretchr/testify/assert"
6 | "net/http"
7 | "net/http/httptest"
8 | "testing"
9 | )
10 |
11 | func TestPingRoute(t *testing.T) {
12 | // 设置Gin的模式为测试模式
13 | gin.SetMode(gin.TestMode)
14 |
15 | // 创建一个Gin引擎
16 | r := gin.Default()
17 |
18 | // 创建一个模拟的HTTP请求
19 | req, err := http.NewRequest(http.MethodGet, "/ping", nil)
20 | assert.NoError(t, err)
21 |
22 | // 使用httptest包创建一个ResponseRecorder,用于记录响应
23 | w := httptest.NewRecorder()
24 |
25 | // 使用Gin的ServeHTTP方法处理请求
26 | r.ServeHTTP(w, req)
27 |
28 | // 断言状态码为200
29 | assert.Equal(t, http.StatusOK, w.Code)
30 |
31 | // 断言响应体中的内容
32 | assert.JSONEq(t, `{"message": "pong"}`, w.Body.String())
33 | }
34 |
--------------------------------------------------------------------------------
/var.env:
--------------------------------------------------------------------------------
1 | tag=1.2.2
2 | dataPath=/data/beancount
--------------------------------------------------------------------------------