├── .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 | ![license](https://img.shields.io/github/license/BaoXuebin/beancount-gs) 4 | [![docker image size](https://img.shields.io/docker/image-size/xdbin/beancount-gs/latest?label=docker-image)](https://hub.docker.com/repository/docker/xdbin/beancount-gs/general) 5 | [![docker pulls](https://img.shields.io/docker/pulls/xdbin/beancount-gs)](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 | ![snapshot](./snapshot.png) 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 --------------------------------------------------------------------------------