Repository: markliu2013/bookkeeping Branch: main Commit: 10dbfc0ff078 Files: 1544 Total size: 3.2 MB Directory structure: gitextract_uxeyvckg/ ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── bookkeeping-user-api/ │ ├── .mvn/ │ │ └── wrapper/ │ │ ├── MavenWrapperDownloader.java │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties │ ├── Dockerfile │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ ├── restart.sh │ ├── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── jiukuaitech/ │ │ │ └── bookkeeping/ │ │ │ └── user/ │ │ │ ├── Application.java │ │ │ ├── RegexTest.java │ │ │ ├── account/ │ │ │ │ ├── Account.java │ │ │ │ ├── AccountAddRequest.java │ │ │ │ ├── AccountAdjustBalanceNotValidException.java │ │ │ │ ├── AccountController.java │ │ │ │ ├── AccountExceptionHandler.java │ │ │ │ ├── AccountHasTransactionException.java │ │ │ │ ├── AccountMaxCountException.java │ │ │ │ ├── AccountNameExistsException.java │ │ │ │ ├── AccountQueryRequest.java │ │ │ │ ├── AccountRepository.java │ │ │ │ ├── AccountService.java │ │ │ │ ├── AccountSpec.java │ │ │ │ ├── AccountSumVO.java │ │ │ │ ├── AccountUpdateRequest.java │ │ │ │ ├── AccountVOForExtend.java │ │ │ │ ├── AccountVOForList.java │ │ │ │ ├── DefaultExpenseAccountException.java │ │ │ │ ├── DefaultIncomeAccountException.java │ │ │ │ ├── DefaultTransferFromAccountException.java │ │ │ │ └── DefaultTransferToAccountException.java │ │ │ ├── adjust_balance/ │ │ │ │ ├── AdjustBalance.java │ │ │ │ ├── AdjustBalanceAddRequest.java │ │ │ │ ├── AdjustBalanceController.java │ │ │ │ ├── AdjustBalanceRepository.java │ │ │ │ ├── AdjustBalanceService.java │ │ │ │ └── AdjustBalanceVOForList.java │ │ │ ├── aop/ │ │ │ │ └── TestAspect.java │ │ │ ├── asset_account/ │ │ │ │ ├── AssetAccount.java │ │ │ │ ├── AssetAccountController.java │ │ │ │ ├── AssetAccountRepository.java │ │ │ │ ├── AssetAccountService.java │ │ │ │ └── AssetAccountVOForList.java │ │ │ ├── balance_flow/ │ │ │ │ ├── AccountInvalidateException.java │ │ │ │ ├── AmountInvalidateException.java │ │ │ │ ├── BalanceFlow.java │ │ │ │ ├── BalanceFlowAddRequest.java │ │ │ │ ├── BalanceFlowController.java │ │ │ │ ├── BalanceFlowExceptionHandler.java │ │ │ │ ├── BalanceFlowQueryRequest.java │ │ │ │ ├── BalanceFlowQueryResultVO.java │ │ │ │ ├── BalanceFlowRepository.java │ │ │ │ ├── BalanceFlowService.java │ │ │ │ ├── BalanceFlowSpec.java │ │ │ │ ├── BalanceFlowUpdateRequest.java │ │ │ │ ├── BalanceFlowVOForExtend.java │ │ │ │ ├── BalanceFlowVOForList.java │ │ │ │ └── StatusNotValidateException.java │ │ │ ├── balance_log/ │ │ │ │ ├── BalanceLog.java │ │ │ │ ├── BalanceLogAddRequest.java │ │ │ │ ├── BalanceLogController.java │ │ │ │ ├── BalanceLogRepository.java │ │ │ │ ├── BalanceLogService.java │ │ │ │ └── BalanceLogVOForList.java │ │ │ ├── base/ │ │ │ │ ├── BaseController.java │ │ │ │ ├── BaseEntity.java │ │ │ │ ├── BaseRepository.java │ │ │ │ ├── BaseRepositoryFactoryBean.java │ │ │ │ ├── BaseRepositoryImpl.java │ │ │ │ ├── BookNameNotesEnableEntity.java │ │ │ │ ├── BookNameNotesEnableSpec.java │ │ │ │ ├── HasBookEntity.java │ │ │ │ ├── HasBookRepository.java │ │ │ │ ├── JpaDataConfig.java │ │ │ │ ├── NameNotesEnableEntity.java │ │ │ │ └── TestController.java │ │ │ ├── book/ │ │ │ │ ├── Book.java │ │ │ │ ├── BookAddRequest.java │ │ │ │ ├── BookController.java │ │ │ │ ├── BookExceptionHandler.java │ │ │ │ ├── BookMaxCountException.java │ │ │ │ ├── BookRepository.java │ │ │ │ ├── BookService.java │ │ │ │ ├── BookUpdateRequest.java │ │ │ │ └── BookVOForList.java │ │ │ ├── category/ │ │ │ │ ├── Category.java │ │ │ │ ├── CategoryAddRequest.java │ │ │ │ ├── CategoryController.java │ │ │ │ ├── CategoryExceptionHandler.java │ │ │ │ ├── CategoryHasDealException.java │ │ │ │ ├── CategoryIsDefaultExpenseException.java │ │ │ │ ├── CategoryIsDefaultIncomeException.java │ │ │ │ ├── CategoryLevelException.java │ │ │ │ ├── CategoryMaxCountException.java │ │ │ │ ├── CategoryNameExistsException.java │ │ │ │ ├── CategoryQueryRequest.java │ │ │ │ ├── CategoryRepository.java │ │ │ │ ├── CategoryService.java │ │ │ │ ├── CategorySimpleVO.java │ │ │ │ ├── CategorySpec.java │ │ │ │ ├── CategoryTreeVO.java │ │ │ │ ├── CategoryUpdateRequest.java │ │ │ │ ├── DefaultExpenseCategoryException.java │ │ │ │ ├── DefaultIncomeCategoryException.java │ │ │ │ └── ParentCategoryNotEnableException.java │ │ │ ├── category_relation/ │ │ │ │ ├── CategoryRelation.java │ │ │ │ ├── CategoryRelationAddRequest.java │ │ │ │ ├── CategoryRelationRepository.java │ │ │ │ └── CategoryRelationVOForList.java │ │ │ ├── checking_account/ │ │ │ │ ├── CheckingAccount.java │ │ │ │ ├── CheckingAccountController.java │ │ │ │ ├── CheckingAccountRepository.java │ │ │ │ └── CheckingAccountService.java │ │ │ ├── credit_account/ │ │ │ │ ├── CreditAccount.java │ │ │ │ ├── CreditAccountAddRequest.java │ │ │ │ ├── CreditAccountController.java │ │ │ │ ├── CreditAccountRepository.java │ │ │ │ ├── CreditAccountService.java │ │ │ │ ├── CreditAccountSumVO.java │ │ │ │ └── CreditAccountVOForList.java │ │ │ ├── currency/ │ │ │ │ ├── Currency.java │ │ │ │ ├── CurrencyController.java │ │ │ │ ├── CurrencyRepository.java │ │ │ │ └── CurrencyService.java │ │ │ ├── dashboard/ │ │ │ │ ├── AssetOverviewVO.java │ │ │ │ ├── DashboardController.java │ │ │ │ └── DashboardService.java │ │ │ ├── deal/ │ │ │ │ ├── CategoryConflictException.java │ │ │ │ ├── Deal.java │ │ │ │ ├── DealAddRequest.java │ │ │ │ ├── DealController.java │ │ │ │ ├── DealExceptionHandler.java │ │ │ │ ├── DealQueryResultVO.java │ │ │ │ ├── DealRepository.java │ │ │ │ ├── DealService.java │ │ │ │ ├── DealSpec.java │ │ │ │ ├── DealUpdateRequest.java │ │ │ │ └── DealVOForList.java │ │ │ ├── debt_account/ │ │ │ │ ├── DebtAccount.java │ │ │ │ ├── DebtAccountAddRequest.java │ │ │ │ ├── DebtAccountController.java │ │ │ │ ├── DebtAccountRepository.java │ │ │ │ ├── DebtAccountService.java │ │ │ │ ├── DebtAccountSumVO.java │ │ │ │ └── DebtAccountVOForList.java │ │ │ ├── exception/ │ │ │ │ ├── GlobalExceptionHandler.java │ │ │ │ ├── InputNotValidException.java │ │ │ │ ├── ItemNotFoundException.java │ │ │ │ ├── NameExistsException.java │ │ │ │ ├── PermissionException.java │ │ │ │ ├── TokenEmptyException.java │ │ │ │ └── TokenNotValidException.java │ │ │ ├── expense/ │ │ │ │ ├── Expense.java │ │ │ │ ├── ExpenseController.java │ │ │ │ ├── ExpenseRepository.java │ │ │ │ └── ExpenseService.java │ │ │ ├── expense_category/ │ │ │ │ ├── ExpenseCategory.java │ │ │ │ └── ExpenseCategoryController.java │ │ │ ├── flow_images/ │ │ │ │ ├── FlowImage.java │ │ │ │ ├── FlowImageController.java │ │ │ │ ├── FlowImageExceptionHandler.java │ │ │ │ ├── FlowImageRepository.java │ │ │ │ ├── FlowImageService.java │ │ │ │ ├── FlowImageVOForList.java │ │ │ │ ├── ImageExistsException.java │ │ │ │ ├── UploadCallbackRequest.java │ │ │ │ └── UploadKeyEmptyException.java │ │ │ ├── group/ │ │ │ │ ├── Group.java │ │ │ │ ├── GroupAddRequest.java │ │ │ │ ├── GroupController.java │ │ │ │ ├── GroupExceptionHandler.java │ │ │ │ ├── GroupHasBookException.java │ │ │ │ ├── GroupMaxCountException.java │ │ │ │ ├── GroupRepository.java │ │ │ │ ├── GroupService.java │ │ │ │ ├── GroupUpdateRequest.java │ │ │ │ └── GroupVOForList.java │ │ │ ├── income/ │ │ │ │ ├── Income.java │ │ │ │ ├── IncomeController.java │ │ │ │ ├── IncomeRepository.java │ │ │ │ └── IncomeService.java │ │ │ ├── income_category/ │ │ │ │ ├── IncomeCategory.java │ │ │ │ └── IncomeCategoryController.java │ │ │ ├── interceptor/ │ │ │ │ ├── AuthInterceptor.java │ │ │ │ ├── MvcInterceptorConfig.java │ │ │ │ └── StringTrimModule.java │ │ │ ├── item/ │ │ │ │ ├── Item.java │ │ │ │ ├── ItemAddRequest.java │ │ │ │ ├── ItemController.java │ │ │ │ ├── ItemCountException.java │ │ │ │ ├── ItemExceptionHandler.java │ │ │ │ ├── ItemQueryRequest.java │ │ │ │ ├── ItemRepository.java │ │ │ │ ├── ItemService.java │ │ │ │ ├── ItemSpec.java │ │ │ │ ├── ItemUpdateRequest.java │ │ │ │ └── ItemVOForList.java │ │ │ ├── payee/ │ │ │ │ ├── Payee.java │ │ │ │ ├── PayeeAddRequest.java │ │ │ │ ├── PayeeController.java │ │ │ │ ├── PayeeExceptionHandler.java │ │ │ │ ├── PayeeHasDealException.java │ │ │ │ ├── PayeeMaxCountException.java │ │ │ │ ├── PayeeQueryRequest.java │ │ │ │ ├── PayeeRepository.java │ │ │ │ ├── PayeeService.java │ │ │ │ ├── PayeeSpec.java │ │ │ │ ├── PayeeUpdateRequest.java │ │ │ │ └── PayeeVOForList.java │ │ │ ├── permission/ │ │ │ │ ├── PermissionCheck.java │ │ │ │ └── PermissionCheckAspect.java │ │ │ ├── refund/ │ │ │ │ ├── Refund.java │ │ │ │ ├── RefundRepository.java │ │ │ │ └── RefundService.java │ │ │ ├── reports/ │ │ │ │ ├── BreakOutOfMaxException.java │ │ │ │ ├── ChartVO.java │ │ │ │ ├── ChartVO2.java │ │ │ │ ├── ExpenseIncomeTrendQueryRequest.java │ │ │ │ ├── ReportController.java │ │ │ │ ├── ReportService.java │ │ │ │ ├── ReportsExceptionHandler.java │ │ │ │ └── TrendTimeQueryRequest.java │ │ │ ├── response/ │ │ │ │ ├── BaseResponse.java │ │ │ │ ├── DataResponse.java │ │ │ │ ├── ErrorResponse.java │ │ │ │ └── HasNameVO.java │ │ │ ├── scheduled/ │ │ │ │ ├── Scheduled.java │ │ │ │ ├── ScheduledAddRequest.java │ │ │ │ ├── ScheduledController.java │ │ │ │ ├── ScheduledExpenseAddRequest.java │ │ │ │ ├── ScheduledRepository.java │ │ │ │ └── ScheduledService.java │ │ │ ├── setting/ │ │ │ │ └── Setting.java │ │ │ ├── tag/ │ │ │ │ ├── Tag.java │ │ │ │ ├── TagAddRequest.java │ │ │ │ ├── TagController.java │ │ │ │ ├── TagExceptionHandler.java │ │ │ │ ├── TagHasTransactionException.java │ │ │ │ ├── TagMaxCountException.java │ │ │ │ ├── TagQueryRequest.java │ │ │ │ ├── TagRepository.java │ │ │ │ ├── TagService.java │ │ │ │ ├── TagSpec.java │ │ │ │ ├── TagTreeVO.java │ │ │ │ ├── TagUpdateRequest.java │ │ │ │ └── TagVOForList.java │ │ │ ├── tag_relation/ │ │ │ │ ├── TagRelation.java │ │ │ │ ├── TagRelationController.java │ │ │ │ ├── TagRelationRepository.java │ │ │ │ ├── TagRelationService.java │ │ │ │ ├── TagRelationUpdateRequest.java │ │ │ │ └── TagRelationVOForList.java │ │ │ ├── transaction/ │ │ │ │ ├── Transaction.java │ │ │ │ ├── TransactionAddRequest.java │ │ │ │ ├── TransactionRepository.java │ │ │ │ ├── TransactionSpec.java │ │ │ │ ├── TransactionUpdateRequest.java │ │ │ │ └── TransactionVOForList.java │ │ │ ├── transfer/ │ │ │ │ ├── Transfer.java │ │ │ │ ├── TransferAddRequest.java │ │ │ │ ├── TransferController.java │ │ │ │ ├── TransferExceptionHandler.java │ │ │ │ ├── TransferFromEqualsToException.java │ │ │ │ ├── TransferQueryResultVO.java │ │ │ │ ├── TransferRepository.java │ │ │ │ ├── TransferService.java │ │ │ │ ├── TransferSpec.java │ │ │ │ ├── TransferUpdateRequest.java │ │ │ │ └── TransferVOForList.java │ │ │ ├── user/ │ │ │ │ ├── InviteCodeErrorException.java │ │ │ │ ├── IpNotAllowedException.java │ │ │ │ ├── OldPasswordErrorException.java │ │ │ │ ├── RegisterNameExistsException.java │ │ │ │ ├── SessionUserNotFoundException.java │ │ │ │ ├── SessionVO.java │ │ │ │ ├── SigninFailedException.java │ │ │ │ ├── UploadNotImageException.java │ │ │ │ ├── User.java │ │ │ │ ├── UserController.java │ │ │ │ ├── UserDisabledException.java │ │ │ │ ├── UserExceptionHandler.java │ │ │ │ ├── UserGroupRelation.java │ │ │ │ ├── UserGroupRelationRepository.java │ │ │ │ ├── UserRegisterRequest.java │ │ │ │ ├── UserRepository.java │ │ │ │ ├── UserService.java │ │ │ │ ├── UserSessionVO.java │ │ │ │ ├── UserSignInRequest.java │ │ │ │ ├── UserSignInResponse.java │ │ │ │ └── UserUpdatePasswordRequest.java │ │ │ ├── user_log/ │ │ │ │ ├── FlowMaxCountException.java │ │ │ │ ├── UserActionExceptionHandler.java │ │ │ │ ├── UserActionLog.java │ │ │ │ ├── UserActionLogRepository.java │ │ │ │ └── UserActionLogService.java │ │ │ ├── utils/ │ │ │ │ ├── CalendarUtils.java │ │ │ │ ├── CommonUtils.java │ │ │ │ └── EnumUtils.java │ │ │ └── validation/ │ │ │ ├── AmountAccumulateValidator.java │ │ │ ├── AmountValidator.java │ │ │ ├── AprValidator.java │ │ │ ├── AvatarValidator.java │ │ │ ├── BalanceValidator.java │ │ │ ├── BillDayValidator.java │ │ │ ├── CreditLimitValidator.java │ │ │ ├── DescriptionValidator.java │ │ │ ├── NameValidator.java │ │ │ ├── NoValidator.java │ │ │ ├── NotesValidator.java │ │ │ ├── PasswordValidator.java │ │ │ ├── TimeValidator.java │ │ │ ├── TransactionStatusValidator.java │ │ │ └── UserNameValidator.java │ │ └── resources/ │ │ ├── application.properties │ │ └── i18n/ │ │ └── messages.properties │ ├── start.sh │ ├── startup.sh │ └── stop.sh ├── bookkeeping-user-fe/ │ ├── .editorconfig │ ├── .eslintrc │ ├── .prettierignore │ ├── .prettierrc │ ├── .umirc.js │ ├── Dockerfile │ ├── docker/ │ │ ├── gzip.conf │ │ ├── nginx.conf │ │ └── nginx.conf.template │ ├── mock/ │ │ └── .gitkeep │ ├── package.json │ ├── src/ │ │ ├── app.js │ │ ├── components/ │ │ │ ├── AccountRecordTable/ │ │ │ │ └── index.jsx │ │ │ ├── AdjustBalanceModal/ │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── AlertTotalSearch/ │ │ │ │ └── index.jsx │ │ │ ├── AmountInput/ │ │ │ │ └── index.jsx │ │ │ ├── AmountInputPositive/ │ │ │ │ └── index.jsx │ │ │ ├── AvatarDropdown/ │ │ │ │ ├── UpdatePasswordModal.jsx │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── Breadcrumb/ │ │ │ │ └── index.jsx │ │ │ ├── ExpenseModal/ │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── FlagTag/ │ │ │ │ └── index.jsx │ │ │ ├── FlowButtons/ │ │ │ │ └── index.jsx │ │ │ ├── FlowImageUploadModal/ │ │ │ │ └── index.jsx │ │ │ ├── FlowRecordTable/ │ │ │ │ └── index.jsx │ │ │ ├── FlowStatusDisplay/ │ │ │ │ └── index.jsx │ │ │ ├── FlowTagDisplay/ │ │ │ │ ├── TagModal.jsx │ │ │ │ └── index.jsx │ │ │ ├── Footer/ │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── FormItemAccount/ │ │ │ │ └── index.jsx │ │ │ ├── FormItemAccounts/ │ │ │ │ └── index.jsx │ │ │ ├── FormItemAmountRange/ │ │ │ │ └── index.jsx │ │ │ ├── FormItemCategories/ │ │ │ │ └── index.jsx │ │ │ ├── FormItemCategoryReport/ │ │ │ │ └── index.jsx │ │ │ ├── FormItemCreateTime/ │ │ │ │ └── index.jsx │ │ │ ├── FormItemDateRange/ │ │ │ │ └── index.jsx │ │ │ ├── FormItemDateRangeWithBreak/ │ │ │ │ └── index.jsx │ │ │ ├── FormItemDescription/ │ │ │ │ └── index.jsx │ │ │ ├── FormItemDescriptionSearch/ │ │ │ │ └── index.jsx │ │ │ ├── FormItemPayee/ │ │ │ │ └── index.jsx │ │ │ ├── FormItemPayees/ │ │ │ │ └── index.jsx │ │ │ ├── FormItemStatus/ │ │ │ │ └── index.jsx │ │ │ ├── FormItemTag/ │ │ │ │ ├── AddTagModal.jsx │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── FormItemTagReport/ │ │ │ │ └── index.jsx │ │ │ ├── FormItemTags/ │ │ │ │ └── index.jsx │ │ │ ├── FormListCategory/ │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── FormModal/ │ │ │ │ └── index.jsx │ │ │ ├── HeaderDropdown/ │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── IncomeModal/ │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── Loading/ │ │ │ │ └── index.jsx │ │ │ ├── ModalRoot/ │ │ │ │ └── index.jsx │ │ │ ├── PrimaryMenu/ │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── TransferModal/ │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ └── charts/ │ │ │ ├── Bar/ │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── Line/ │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── Pie/ │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ └── Pie2/ │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── global.less │ │ ├── layouts/ │ │ │ ├── BasicLayout.jsx │ │ │ ├── BasicLayout.less │ │ │ ├── UserLayout.jsx │ │ │ ├── UserLayout.less │ │ │ └── index.js │ │ ├── locales/ │ │ │ ├── en-US/ │ │ │ │ ├── account.js │ │ │ │ ├── book.js │ │ │ │ ├── category.js │ │ │ │ ├── common.js │ │ │ │ ├── dashboard.js │ │ │ │ ├── flow.js │ │ │ │ ├── footer.js │ │ │ │ ├── group.js │ │ │ │ ├── header.js │ │ │ │ ├── menu.js │ │ │ │ ├── register.js │ │ │ │ ├── report.js │ │ │ │ ├── rules.js │ │ │ │ ├── schedule.js │ │ │ │ └── signin.js │ │ │ ├── en-US.js │ │ │ ├── zh-CN/ │ │ │ │ ├── account.js │ │ │ │ ├── book.js │ │ │ │ ├── category.js │ │ │ │ ├── common.js │ │ │ │ ├── flow.js │ │ │ │ ├── footer.js │ │ │ │ ├── group.js │ │ │ │ ├── menu.js │ │ │ │ ├── report.js │ │ │ │ ├── rules.js │ │ │ │ ├── schedule.js │ │ │ │ └── user.js │ │ │ └── zh-CN.js │ │ ├── models/ │ │ │ ├── account.js │ │ │ ├── currency.js │ │ │ ├── expenseCategory.js │ │ │ ├── flow.js │ │ │ ├── incomeCategory.js │ │ │ ├── modal.js │ │ │ ├── payee.js │ │ │ ├── session.js │ │ │ └── tag.js │ │ ├── pages/ │ │ │ ├── accounts/ │ │ │ │ ├── AssetAccountModal.jsx │ │ │ │ ├── AssetAccountTable.jsx │ │ │ │ ├── CheckingAccountModal.jsx │ │ │ │ ├── CheckingAccountTable.jsx │ │ │ │ ├── CreditAccountModal.jsx │ │ │ │ ├── CreditAccountTable.jsx │ │ │ │ ├── DebtAccountModal.jsx │ │ │ │ ├── DebtAccountTable.jsx │ │ │ │ ├── OperationBar.jsx │ │ │ │ ├── index.jsx │ │ │ │ └── model.js │ │ │ ├── asset-accounts/ │ │ │ │ ├── GeneralBar.jsx │ │ │ │ ├── ItemCard.jsx │ │ │ │ ├── List.jsx │ │ │ │ ├── index.jsx │ │ │ │ ├── index.less │ │ │ │ └── model.js │ │ │ ├── audit/ │ │ │ │ ├── OperationBar.jsx │ │ │ │ ├── RecordTable.jsx │ │ │ │ ├── TotalAlert.jsx │ │ │ │ ├── index.jsx │ │ │ │ └── model.js │ │ │ ├── balance-logs/ │ │ │ │ ├── OperationBar.jsx │ │ │ │ ├── OperationModal.jsx │ │ │ │ ├── RecordTable.jsx │ │ │ │ ├── index.jsx │ │ │ │ ├── index.less │ │ │ │ └── model.js │ │ │ ├── books/ │ │ │ │ ├── ConfigModal.jsx │ │ │ │ ├── OperationBar.jsx │ │ │ │ ├── OperationModal.jsx │ │ │ │ ├── RecordTable.jsx │ │ │ │ ├── index.jsx │ │ │ │ ├── index.less │ │ │ │ └── model.js │ │ │ ├── categories/ │ │ │ │ ├── ExpenseCategoryFilterBar.jsx │ │ │ │ ├── ExpenseCategoryModal.jsx │ │ │ │ ├── ExpenseCategoryTable.jsx │ │ │ │ ├── IncomeCategoryFilterBar.jsx │ │ │ │ ├── IncomeCategoryModal.jsx │ │ │ │ ├── IncomeCategoryTable.jsx │ │ │ │ ├── OperationBar.jsx │ │ │ │ ├── PayeeFilterBar.jsx │ │ │ │ ├── PayeeModal.jsx │ │ │ │ ├── PayeeTable.jsx │ │ │ │ ├── TagFilterBar.jsx │ │ │ │ ├── TagModal.jsx │ │ │ │ ├── TagTable.jsx │ │ │ │ ├── index.jsx │ │ │ │ ├── index.less │ │ │ │ └── model.js │ │ │ ├── checking-accounts/ │ │ │ │ ├── GeneralBar.jsx │ │ │ │ ├── ItemCard.jsx │ │ │ │ ├── List.jsx │ │ │ │ ├── index.jsx │ │ │ │ ├── index.less │ │ │ │ └── model.js │ │ │ ├── credit-accounts/ │ │ │ │ ├── GeneralBar.jsx │ │ │ │ ├── ItemCard.jsx │ │ │ │ ├── List.jsx │ │ │ │ ├── index.jsx │ │ │ │ ├── index.less │ │ │ │ └── model.js │ │ │ ├── dashboard/ │ │ │ │ ├── AssetBar.jsx │ │ │ │ ├── CardExtra.jsx │ │ │ │ ├── ExpenseCategory.jsx │ │ │ │ ├── ExpenseTrend.jsx │ │ │ │ ├── IncomeCategory.jsx │ │ │ │ ├── IncomeTrend.jsx │ │ │ │ ├── TransactionTable.jsx │ │ │ │ ├── TransactionTable.less │ │ │ │ ├── index.jsx │ │ │ │ ├── index.less │ │ │ │ └── model.js │ │ │ ├── debt-accounts/ │ │ │ │ ├── GeneralBar.jsx │ │ │ │ ├── ItemCard.jsx │ │ │ │ ├── List.jsx │ │ │ │ ├── index.jsx │ │ │ │ ├── index.less │ │ │ │ └── model.js │ │ │ ├── document.ejs │ │ │ ├── expenses/ │ │ │ │ ├── OperationBar.jsx │ │ │ │ ├── RecordTable.jsx │ │ │ │ ├── TotalAlert.jsx │ │ │ │ ├── index.jsx │ │ │ │ └── model.js │ │ │ ├── flows/ │ │ │ │ ├── OperationBar.jsx │ │ │ │ ├── RecordTable.jsx │ │ │ │ ├── TotalAlert.jsx │ │ │ │ ├── index.jsx │ │ │ │ └── model.js │ │ │ ├── groups/ │ │ │ │ ├── OperationBar.jsx │ │ │ │ ├── OperationModal.jsx │ │ │ │ ├── RecordTable.jsx │ │ │ │ ├── index.jsx │ │ │ │ ├── index.less │ │ │ │ └── model.js │ │ │ ├── incomes/ │ │ │ │ ├── OperationBar.jsx │ │ │ │ ├── RecordTable.jsx │ │ │ │ ├── TotalAlert.jsx │ │ │ │ ├── index.jsx │ │ │ │ └── model.js │ │ │ ├── index.jsx │ │ │ ├── items/ │ │ │ │ ├── OperationBar.jsx │ │ │ │ ├── OperationModal.jsx │ │ │ │ ├── RecordTable.jsx │ │ │ │ ├── index.jsx │ │ │ │ ├── index.less │ │ │ │ └── model.js │ │ │ ├── register/ │ │ │ │ ├── index.jsx │ │ │ │ ├── index.less │ │ │ │ └── model.js │ │ │ ├── reports/ │ │ │ │ ├── asset-debt-trend/ │ │ │ │ │ ├── Chart.jsx │ │ │ │ │ ├── OperationBar.jsx │ │ │ │ │ ├── index.jsx │ │ │ │ │ └── model.js │ │ │ │ ├── balance-sheet/ │ │ │ │ │ ├── AssetCategory.jsx │ │ │ │ │ ├── DebtCategory.jsx │ │ │ │ │ ├── index.jsx │ │ │ │ │ └── model.js │ │ │ │ ├── expense-category/ │ │ │ │ │ ├── CategoryPie.jsx │ │ │ │ │ ├── OperationBar.jsx │ │ │ │ │ ├── index.jsx │ │ │ │ │ └── model.js │ │ │ │ ├── expense-income-trend/ │ │ │ │ │ ├── Chart.jsx │ │ │ │ │ ├── OperationBar.jsx │ │ │ │ │ ├── index.jsx │ │ │ │ │ └── model.js │ │ │ │ ├── expense-tag/ │ │ │ │ │ ├── OperationBar.jsx │ │ │ │ │ ├── TagPie.jsx │ │ │ │ │ ├── index.jsx │ │ │ │ │ └── model.js │ │ │ │ ├── income-category/ │ │ │ │ │ ├── CategoryPie.jsx │ │ │ │ │ ├── OperationBar.jsx │ │ │ │ │ ├── index.jsx │ │ │ │ │ └── model.js │ │ │ │ └── income-tag/ │ │ │ │ ├── OperationBar.jsx │ │ │ │ ├── TagPie.jsx │ │ │ │ ├── index.jsx │ │ │ │ └── model.js │ │ │ ├── scheduled/ │ │ │ │ └── index.jsx │ │ │ ├── settings/ │ │ │ │ └── index.jsx │ │ │ ├── signin/ │ │ │ │ ├── RememberDropDown.js │ │ │ │ ├── index.jsx │ │ │ │ ├── index.less │ │ │ │ └── model.js │ │ │ ├── test/ │ │ │ │ └── index.jsx │ │ │ └── transfers/ │ │ │ ├── OperationBar.jsx │ │ │ ├── RecordTable.jsx │ │ │ ├── TotalAlert.jsx │ │ │ ├── index.jsx │ │ │ └── model.js │ │ ├── services/ │ │ │ ├── account.js │ │ │ ├── adjust-balance.js │ │ │ ├── asset-account.js │ │ │ ├── book.js │ │ │ ├── category.js │ │ │ ├── checking-account.js │ │ │ ├── credit-account.js │ │ │ ├── currency.js │ │ │ ├── dashboard.js │ │ │ ├── deal.js │ │ │ ├── debt-account.js │ │ │ ├── expense-category.js │ │ │ ├── expense.js │ │ │ ├── flow-image.js │ │ │ ├── flow.js │ │ │ ├── group.js │ │ │ ├── income-category.js │ │ │ ├── income.js │ │ │ ├── item.js │ │ │ ├── log.js │ │ │ ├── payee.js │ │ │ ├── report.js │ │ │ ├── schedule.js │ │ │ ├── tag-relation.js │ │ │ ├── tag.js │ │ │ ├── transfer.js │ │ │ └── user.js │ │ └── utils/ │ │ ├── columns.js │ │ ├── flow.js │ │ ├── hooks.js │ │ ├── model.js │ │ ├── request.js │ │ ├── rules.js │ │ ├── translate.js │ │ ├── util.js │ │ └── var.js │ └── webpack.config.js ├── bookkeeping_user_flutter/ │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── analysis_options.yaml │ ├── android/ │ │ ├── .gitignore │ │ ├── app/ │ │ │ ├── build.gradle │ │ │ └── src/ │ │ │ ├── debug/ │ │ │ │ └── AndroidManifest.xml │ │ │ ├── main/ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── whlcsj/ │ │ │ │ │ └── bookkeeping_user_flutter/ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── res/ │ │ │ │ ├── drawable/ │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21/ │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values/ │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night/ │ │ │ │ └── styles.xml │ │ │ └── profile/ │ │ │ └── AndroidManifest.xml │ │ ├── build.gradle │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ └── gradle-wrapper.properties │ │ ├── gradle.properties │ │ └── settings.gradle │ ├── ios/ │ │ ├── .gitignore │ │ ├── Flutter/ │ │ │ ├── AppFrameworkInfo.plist │ │ │ ├── Debug.xcconfig │ │ │ └── Release.xcconfig │ │ ├── Podfile │ │ ├── Runner/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── LaunchImage.imageset/ │ │ │ │ ├── Contents.json │ │ │ │ └── README.md │ │ │ ├── Base.lproj/ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ └── Runner-Bridging-Header.h │ │ ├── Runner.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace/ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata/ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ └── xcshareddata/ │ │ │ └── xcschemes/ │ │ │ └── Runner.xcscheme │ │ └── Runner.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ ├── lib/ │ │ ├── accounts/ │ │ │ ├── accounts.dart │ │ │ ├── bloc/ │ │ │ │ ├── account_adjust_balance/ │ │ │ │ │ ├── account_adjust_balance_bloc.dart │ │ │ │ │ ├── account_adjust_balance_event.dart │ │ │ │ │ └── account_adjust_balance_state.dart │ │ │ │ ├── account_enable/ │ │ │ │ │ ├── account_enable_bloc.dart │ │ │ │ │ ├── account_enable_event.dart │ │ │ │ │ └── account_enable_state.dart │ │ │ │ ├── account_expenseable/ │ │ │ │ │ ├── account_expenseable_bloc.dart │ │ │ │ │ ├── account_expenseable_event.dart │ │ │ │ │ └── account_expenseable_state.dart │ │ │ │ ├── account_fetch/ │ │ │ │ │ ├── account_fetch_bloc.dart │ │ │ │ │ ├── account_fetch_event.dart │ │ │ │ │ └── account_fetch_state.dart │ │ │ │ ├── account_form/ │ │ │ │ │ ├── account_form_bloc.dart │ │ │ │ │ ├── account_form_event.dart │ │ │ │ │ └── account_form_state.dart │ │ │ │ ├── account_incomeable/ │ │ │ │ │ ├── account_incomeable_bloc.dart │ │ │ │ │ ├── account_incomeable_event.dart │ │ │ │ │ └── account_incomeable_state.dart │ │ │ │ ├── account_transfer_from_able/ │ │ │ │ │ ├── account_transfer_from_able_bloc.dart │ │ │ │ │ ├── account_transfer_from_able_event.dart │ │ │ │ │ └── account_transfer_from_able_state.dart │ │ │ │ ├── account_transfer_to_able/ │ │ │ │ │ ├── account_transfer_to_able_bloc.dart │ │ │ │ │ ├── account_transfer_to_able_event.dart │ │ │ │ │ └── account_transfer_to_able_state.dart │ │ │ │ └── accounts/ │ │ │ │ ├── accounts_bloc.dart │ │ │ │ ├── accounts_event.dart │ │ │ │ └── accounts_state.dart │ │ │ ├── data/ │ │ │ │ ├── account_repository.dart │ │ │ │ └── models/ │ │ │ │ ├── account.dart │ │ │ │ ├── account.g.dart │ │ │ │ ├── account_form_request.dart │ │ │ │ ├── account_form_request.g.dart │ │ │ │ ├── account_query_request.dart │ │ │ │ ├── account_query_request.g.dart │ │ │ │ ├── adjust_balance_request.dart │ │ │ │ ├── adjust_balance_request.g.dart │ │ │ │ └── credit_account.dart │ │ │ └── ui/ │ │ │ ├── account_adjust_balance.dart │ │ │ ├── account_detail_page.dart │ │ │ ├── account_form_page.dart │ │ │ ├── accounts_page.dart │ │ │ ├── asset_account_form_page.dart │ │ │ ├── checking_account_form_page.dart │ │ │ ├── credit_account_form_page.dart │ │ │ ├── debt_account_form_page.dart │ │ │ └── widgets/ │ │ │ ├── account_form/ │ │ │ │ ├── account_apr_input.dart │ │ │ │ ├── account_balance_input.dart │ │ │ │ ├── account_billday_input.dart │ │ │ │ ├── account_limit_input.dart │ │ │ │ ├── currency_input.dart │ │ │ │ ├── expenseable_input.dart │ │ │ │ ├── inclue_input.dart │ │ │ │ ├── incomeable_input.dart │ │ │ │ ├── name_input.dart │ │ │ │ ├── no_input.dart │ │ │ │ ├── notes_input.dart │ │ │ │ ├── transfer_from_able_input.dart │ │ │ │ └── transfer_to_able_input.dart │ │ │ ├── adjust_balance/ │ │ │ │ ├── adjust_balance_form.dart │ │ │ │ ├── balance_input.dart │ │ │ │ ├── date_time_input.dart │ │ │ │ ├── description_input.dart │ │ │ │ └── notes_input.dart │ │ │ └── order_button.dart │ │ ├── add_flow/ │ │ │ ├── add_flow.dart │ │ │ ├── bloc/ │ │ │ │ ├── add_expense/ │ │ │ │ │ ├── add_expense_bloc.dart │ │ │ │ │ ├── add_expense_event.dart │ │ │ │ │ └── add_expense_state.dart │ │ │ │ ├── add_income/ │ │ │ │ │ ├── add_income_bloc.dart │ │ │ │ │ ├── add_income_event.dart │ │ │ │ │ └── add_income_state.dart │ │ │ │ ├── add_transfer/ │ │ │ │ │ ├── add_transfer_bloc.dart │ │ │ │ │ ├── add_transfer_event.dart │ │ │ │ │ └── add_transfer_state.dart │ │ │ │ └── models/ │ │ │ │ ├── deal_add_request.dart │ │ │ │ ├── deal_add_request.g.dart │ │ │ │ ├── transfer_add_request.dart │ │ │ │ └── transfer_add_request.g.dart │ │ │ └── ui/ │ │ │ ├── add_flow_page.dart │ │ │ ├── no_tab_page.dart │ │ │ ├── tab_page.dart │ │ │ └── widgets/ │ │ │ ├── expense/ │ │ │ │ ├── account_input.dart │ │ │ │ ├── add_expense_form.dart │ │ │ │ ├── category_input.dart │ │ │ │ ├── date_time_input.dart │ │ │ │ ├── description_input.dart │ │ │ │ ├── is_confirm_input.dart │ │ │ │ ├── notes_input.dart │ │ │ │ ├── payee_input.dart │ │ │ │ ├── tag_input.dart │ │ │ │ └── widgets.dart │ │ │ ├── income/ │ │ │ │ ├── account_input.dart │ │ │ │ ├── add_income_form.dart │ │ │ │ ├── category_input.dart │ │ │ │ ├── date_time_input.dart │ │ │ │ ├── description_input.dart │ │ │ │ ├── is_confirm_input.dart │ │ │ │ ├── notes_input.dart │ │ │ │ ├── payee_input.dart │ │ │ │ ├── tag_input.dart │ │ │ │ └── widgets.dart │ │ │ └── transfer/ │ │ │ ├── add_transfer_form.dart │ │ │ ├── amount_input.dart │ │ │ ├── converted_amount_input.dart │ │ │ ├── date_time_input.dart │ │ │ ├── description_input.dart │ │ │ ├── from_input.dart │ │ │ ├── is_confirm_input.dart │ │ │ ├── notes_input.dart │ │ │ ├── tag_input.dart │ │ │ ├── to_input.dart │ │ │ └── widgets.dart │ │ ├── app.dart │ │ ├── books/ │ │ │ ├── bloc/ │ │ │ │ ├── book_fetch/ │ │ │ │ │ ├── book_fetch_bloc.dart │ │ │ │ │ ├── book_fetch_event.dart │ │ │ │ │ └── book_fetch_state.dart │ │ │ │ └── books/ │ │ │ │ ├── books_bloc.dart │ │ │ │ ├── books_event.dart │ │ │ │ └── books_state.dart │ │ │ ├── books.dart │ │ │ ├── data/ │ │ │ │ ├── book_repository.dart │ │ │ │ └── models/ │ │ │ │ ├── book.dart │ │ │ │ ├── book.g.dart │ │ │ │ ├── book_query_request.dart │ │ │ │ └── book_query_request.g.dart │ │ │ └── ui/ │ │ │ ├── book_detail_page.dart │ │ │ └── books_page.dart │ │ ├── categories/ │ │ │ ├── bloc/ │ │ │ │ ├── category_fetch/ │ │ │ │ │ ├── category_fetch_bloc.dart │ │ │ │ │ ├── category_fetch_event.dart │ │ │ │ │ └── category_fetch_state.dart │ │ │ │ ├── category_form/ │ │ │ │ │ ├── category_form_bloc.dart │ │ │ │ │ ├── category_form_event.dart │ │ │ │ │ └── category_form_state.dart │ │ │ │ ├── category_tree/ │ │ │ │ │ ├── category_tree_bloc.dart │ │ │ │ │ ├── category_tree_event.dart │ │ │ │ │ └── category_tree_state.dart │ │ │ │ ├── expense_category_select/ │ │ │ │ │ ├── expense_category_select_bloc.dart │ │ │ │ │ ├── expense_category_select_event.dart │ │ │ │ │ └── expense_category_select_state.dart │ │ │ │ └── income_category_select/ │ │ │ │ ├── income_category_select_bloc.dart │ │ │ │ ├── income_category_select_event.dart │ │ │ │ └── income_category_select_state.dart │ │ │ ├── categories.dart │ │ │ ├── data/ │ │ │ │ ├── category_repository.dart │ │ │ │ └── models/ │ │ │ │ ├── category.dart │ │ │ │ ├── category.g.dart │ │ │ │ ├── category_form_request.dart │ │ │ │ ├── category_form_request.g.dart │ │ │ │ ├── category_tree.dart │ │ │ │ └── category_tree.g.dart │ │ │ └── ui/ │ │ │ ├── category_detail_page.dart │ │ │ ├── category_form_page.dart │ │ │ ├── expense_categories_page.dart │ │ │ ├── expense_category_form_page.dart │ │ │ ├── income_categories_page.dart │ │ │ ├── income_category_form_page.dart │ │ │ └── widgets/ │ │ │ └── category_form/ │ │ │ ├── expense_category_input.dart │ │ │ ├── income_category_input.dart │ │ │ ├── name_input.dart │ │ │ └── notes_input.dart │ │ ├── charts/ │ │ │ ├── bloc/ │ │ │ │ ├── report_asset/ │ │ │ │ │ ├── report_asset_bloc.dart │ │ │ │ │ ├── report_asset_event.dart │ │ │ │ │ └── report_asset_state.dart │ │ │ │ ├── report_debt/ │ │ │ │ │ ├── report_debt_bloc.dart │ │ │ │ │ ├── report_debt_event.dart │ │ │ │ │ └── report_debt_state.dart │ │ │ │ ├── report_expense_category/ │ │ │ │ │ ├── report_expense_category_bloc.dart │ │ │ │ │ ├── report_expense_category_event.dart │ │ │ │ │ └── report_expense_category_state.dart │ │ │ │ └── report_income_category/ │ │ │ │ ├── report_income_category_bloc.dart │ │ │ │ ├── report_income_category_event.dart │ │ │ │ └── report_income_category_state.dart │ │ │ ├── charts.dart │ │ │ ├── data/ │ │ │ │ ├── models/ │ │ │ │ │ ├── category_query_request.dart │ │ │ │ │ ├── category_query_request.g.dart │ │ │ │ │ ├── x_y.dart │ │ │ │ │ └── x_y.g.dart │ │ │ │ └── report_repository.dart │ │ │ └── ui/ │ │ │ ├── charts_expense_category_filter_page.dart │ │ │ ├── charts_income_category_filter_page.dart │ │ │ ├── charts_page.dart │ │ │ └── widgets/ │ │ │ ├── asset_sheet.dart │ │ │ ├── circular_legend.dart │ │ │ ├── date_input_expense.dart │ │ │ ├── date_input_income.dart │ │ │ ├── debt_sheet.dart │ │ │ ├── expense_category.dart │ │ │ ├── expense_category_input.dart │ │ │ ├── income_category.dart │ │ │ └── income_category_input.dart │ │ ├── commons/ │ │ │ ├── common_util.dart │ │ │ ├── commons.dart │ │ │ ├── enums.dart │ │ │ ├── http_client.dart │ │ │ ├── id_name_model.dart │ │ │ ├── id_name_model.g.dart │ │ │ ├── session.dart │ │ │ ├── web_view_page.dart │ │ │ └── widget_util.dart │ │ ├── components/ │ │ │ ├── components.dart │ │ │ ├── date_range_picker.dart │ │ │ ├── date_time_input.dart │ │ │ ├── empty.dart │ │ │ ├── lazy_indexed_stack.dart │ │ │ ├── page_error.dart │ │ │ ├── page_loading.dart │ │ │ └── popup_menu.dart │ │ ├── currency/ │ │ │ ├── bloc/ │ │ │ │ └── currency_all/ │ │ │ │ ├── currency_all_bloc.dart │ │ │ │ ├── currency_all_event.dart │ │ │ │ └── currency_all_state.dart │ │ │ ├── currency.dart │ │ │ └── data/ │ │ │ ├── currency_repository.dart │ │ │ └── models/ │ │ │ ├── currency.dart │ │ │ └── currency.g.dart │ │ ├── flows/ │ │ │ ├── bloc/ │ │ │ │ ├── flow_fetch/ │ │ │ │ │ ├── flow_fetch_bloc.dart │ │ │ │ │ ├── flow_fetch_event.dart │ │ │ │ │ └── flow_fetch_state.dart │ │ │ │ └── flows/ │ │ │ │ ├── flows_bloc.dart │ │ │ │ ├── flows_event.dart │ │ │ │ └── flows_state.dart │ │ │ ├── data/ │ │ │ │ ├── flow_repository.dart │ │ │ │ └── models/ │ │ │ │ ├── adjust_balance.dart │ │ │ │ ├── adjust_balance.g.dart │ │ │ │ ├── category.dart │ │ │ │ ├── category.g.dart │ │ │ │ ├── deal.dart │ │ │ │ ├── deal.g.dart │ │ │ │ ├── flow.dart │ │ │ │ ├── flow.g.dart │ │ │ │ ├── flow_image.dart │ │ │ │ ├── flow_image.g.dart │ │ │ │ ├── flow_query_request.dart │ │ │ │ ├── flow_query_request.g.dart │ │ │ │ ├── tag.dart │ │ │ │ ├── tag.g.dart │ │ │ │ ├── transfer.dart │ │ │ │ └── transfer.g.dart │ │ │ ├── flows.dart │ │ │ └── ui/ │ │ │ ├── flow_detail.dart │ │ │ ├── flows_filter.dart │ │ │ ├── flows_page.dart │ │ │ └── widgets/ │ │ │ ├── account_input.dart │ │ │ ├── date_input.dart │ │ │ ├── expense_category_input.dart │ │ │ ├── income_category_input.dart │ │ │ ├── order_button.dart │ │ │ ├── payee_input.dart │ │ │ ├── status_input.dart │ │ │ ├── tag_input.dart │ │ │ ├── type_input.dart │ │ │ └── widgets.dart │ │ ├── groups/ │ │ │ ├── data/ │ │ │ │ └── models/ │ │ │ │ ├── group.dart │ │ │ │ └── group.g.dart │ │ │ └── groups.dart │ │ ├── index.dart │ │ ├── items/ │ │ │ ├── bloc/ │ │ │ │ ├── item_form/ │ │ │ │ │ ├── item_form_bloc.dart │ │ │ │ │ ├── item_form_event.dart │ │ │ │ │ ├── item_form_state.dart │ │ │ │ │ └── models/ │ │ │ │ │ ├── models.dart │ │ │ │ │ └── title.dart │ │ │ │ └── items/ │ │ │ │ ├── items_bloc.dart │ │ │ │ ├── items_event.dart │ │ │ │ └── items_state.dart │ │ │ ├── data/ │ │ │ │ ├── item_repository.dart │ │ │ │ └── models/ │ │ │ │ ├── item.dart │ │ │ │ ├── item.g.dart │ │ │ │ ├── item_add_request.dart │ │ │ │ ├── item_add_request.g.dart │ │ │ │ ├── item_query_request.dart │ │ │ │ └── item_query_request.g.dart │ │ │ ├── items.dart │ │ │ └── ui/ │ │ │ ├── item_form/ │ │ │ │ ├── date_input.dart │ │ │ │ └── title_input.dart │ │ │ ├── item_form_page.dart │ │ │ ├── items_index.dart │ │ │ └── items_page.dart │ │ ├── login/ │ │ │ ├── bloc/ │ │ │ │ ├── auth/ │ │ │ │ │ ├── auth_bloc.dart │ │ │ │ │ ├── auth_event.dart │ │ │ │ │ └── auth_state.dart │ │ │ │ └── login/ │ │ │ │ ├── login_bloc.dart │ │ │ │ ├── login_event.dart │ │ │ │ ├── login_state.dart │ │ │ │ └── models/ │ │ │ │ ├── ApiUrl.dart │ │ │ │ ├── password.dart │ │ │ │ └── username.dart │ │ │ ├── data/ │ │ │ │ ├── login_repository.dart │ │ │ │ └── models/ │ │ │ │ ├── session.dart │ │ │ │ ├── session.g.dart │ │ │ │ ├── user.dart │ │ │ │ └── user.g.dart │ │ │ ├── login.dart │ │ │ └── ui/ │ │ │ ├── login_page.dart │ │ │ └── widgets/ │ │ │ ├── api_url_input.dart │ │ │ ├── login_form.dart │ │ │ ├── password_input.dart │ │ │ ├── submit_btn.dart │ │ │ └── user_name_input.dart │ │ ├── main.dart │ │ ├── my/ │ │ │ ├── my.dart │ │ │ └── my_page.dart │ │ ├── observer.dart │ │ ├── payees/ │ │ │ ├── bloc/ │ │ │ │ ├── payee_enable/ │ │ │ │ │ ├── payee_enable_bloc.dart │ │ │ │ │ ├── payee_enable_event.dart │ │ │ │ │ └── payee_enable_state.dart │ │ │ │ ├── payee_expenseable/ │ │ │ │ │ ├── payee_expenseable_bloc.dart │ │ │ │ │ ├── payee_expenseable_event.dart │ │ │ │ │ └── payee_expenseable_state.dart │ │ │ │ ├── payee_fetch/ │ │ │ │ │ ├── payee_fetch_bloc.dart │ │ │ │ │ ├── payee_fetch_event.dart │ │ │ │ │ └── payee_fetch_state.dart │ │ │ │ ├── payee_form/ │ │ │ │ │ ├── payee_form_bloc.dart │ │ │ │ │ ├── payee_form_event.dart │ │ │ │ │ └── payee_form_state.dart │ │ │ │ ├── payee_incomeable/ │ │ │ │ │ ├── payee_incomeable_bloc.dart │ │ │ │ │ ├── payee_incomeable_event.dart │ │ │ │ │ └── payee_incomeable_state.dart │ │ │ │ └── payees/ │ │ │ │ ├── payees_bloc.dart │ │ │ │ ├── payees_event.dart │ │ │ │ └── payees_state.dart │ │ │ ├── data/ │ │ │ │ ├── models/ │ │ │ │ │ ├── payee.dart │ │ │ │ │ ├── payee.g.dart │ │ │ │ │ ├── payee_form_request.dart │ │ │ │ │ ├── payee_form_request.g.dart │ │ │ │ │ ├── payee_query_request.dart │ │ │ │ │ └── payee_query_request.g.dart │ │ │ │ └── payee_repository.dart │ │ │ ├── payees.dart │ │ │ └── ui/ │ │ │ ├── payee_detail_page.dart │ │ │ ├── payee_form/ │ │ │ │ ├── expenseable_input.dart │ │ │ │ ├── incomeable_input.dart │ │ │ │ ├── name_input.dart │ │ │ │ └── notes_input.dart │ │ │ ├── payee_form_page.dart │ │ │ └── payees_page.dart │ │ ├── routes.dart │ │ ├── start_page.dart │ │ ├── tags/ │ │ │ ├── bloc/ │ │ │ │ ├── tag_enable/ │ │ │ │ │ ├── tag_enable_bloc.dart │ │ │ │ │ ├── tag_enable_event.dart │ │ │ │ │ └── tag_enable_state.dart │ │ │ │ ├── tag_expenseable/ │ │ │ │ │ ├── tag_expenseable_bloc.dart │ │ │ │ │ ├── tag_expenseable_event.dart │ │ │ │ │ └── tag_expenseable_state.dart │ │ │ │ ├── tag_fetch/ │ │ │ │ │ ├── tag_fetch_bloc.dart │ │ │ │ │ ├── tag_fetch_event.dart │ │ │ │ │ └── tag_fetch_state.dart │ │ │ │ ├── tag_form/ │ │ │ │ │ ├── tag_form_bloc.dart │ │ │ │ │ ├── tag_form_event.dart │ │ │ │ │ └── tag_form_state.dart │ │ │ │ ├── tag_incomeable/ │ │ │ │ │ ├── tag_incomeable_bloc.dart │ │ │ │ │ ├── tag_incomeable_event.dart │ │ │ │ │ └── tag_incomeable_state.dart │ │ │ │ ├── tag_transferable/ │ │ │ │ │ ├── tag_transferable_bloc.dart │ │ │ │ │ ├── tag_transferable_event.dart │ │ │ │ │ └── tag_transferable_state.dart │ │ │ │ └── tag_tree/ │ │ │ │ ├── tag_tree_bloc.dart │ │ │ │ ├── tag_tree_event.dart │ │ │ │ └── tag_tree_state.dart │ │ │ ├── data/ │ │ │ │ ├── models/ │ │ │ │ │ ├── tag.dart │ │ │ │ │ ├── tag.g.dart │ │ │ │ │ ├── tag_form_request.dart │ │ │ │ │ ├── tag_form_request.g.dart │ │ │ │ │ ├── tag_tree.dart │ │ │ │ │ └── tag_tree.g.dart │ │ │ │ └── tag_repository.dart │ │ │ ├── tags.dart │ │ │ └── ui/ │ │ │ ├── tag_detail_page.dart │ │ │ ├── tag_form_page.dart │ │ │ ├── tags_page.dart │ │ │ └── widgets/ │ │ │ └── tag_form/ │ │ │ ├── expenseable_input.dart │ │ │ ├── incomeable_input.dart │ │ │ ├── name_input.dart │ │ │ ├── notes_input.dart │ │ │ ├── parent_input.dart │ │ │ └── transferable_input.dart │ │ └── themes.dart │ ├── linux/ │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── flutter/ │ │ │ ├── CMakeLists.txt │ │ │ ├── generated_plugin_registrant.cc │ │ │ ├── generated_plugin_registrant.h │ │ │ └── generated_plugins.cmake │ │ ├── main.cc │ │ ├── my_application.cc │ │ └── my_application.h │ ├── macos/ │ │ ├── .gitignore │ │ ├── Flutter/ │ │ │ ├── Flutter-Debug.xcconfig │ │ │ ├── Flutter-Release.xcconfig │ │ │ └── GeneratedPluginRegistrant.swift │ │ ├── Podfile │ │ ├── Runner/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ └── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ └── MainMenu.xib │ │ │ ├── Configs/ │ │ │ │ ├── AppInfo.xcconfig │ │ │ │ ├── Debug.xcconfig │ │ │ │ ├── Release.xcconfig │ │ │ │ └── Warnings.xcconfig │ │ │ ├── DebugProfile.entitlements │ │ │ ├── Info.plist │ │ │ ├── MainFlutterWindow.swift │ │ │ └── Release.entitlements │ │ ├── Runner.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace/ │ │ │ │ └── xcshareddata/ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ └── xcshareddata/ │ │ │ └── xcschemes/ │ │ │ └── Runner.xcscheme │ │ └── Runner.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ ├── pubspec.yaml │ ├── test/ │ │ └── widget_test.dart │ └── windows/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter/ │ │ ├── CMakeLists.txt │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ └── generated_plugins.cmake │ └── runner/ │ ├── CMakeLists.txt │ ├── Runner.rc │ ├── flutter_window.cpp │ ├── flutter_window.h │ ├── main.cpp │ ├── resource.h │ ├── runner.exe.manifest │ ├── utils.cpp │ ├── utils.h │ ├── win32_window.cpp │ └── win32_window.h ├── bookkeeping_user_uniapp/ │ ├── App.vue │ ├── components/ │ │ ├── accounts/ │ │ │ └── accounts.vue │ │ ├── expense-form/ │ │ │ └── expense-form.vue │ │ ├── flows/ │ │ │ └── flows.vue │ │ ├── income-form/ │ │ │ └── income-form.vue │ │ ├── tab-bar/ │ │ │ └── tab-bar.vue │ │ └── transfer-form/ │ │ └── transfer-form.vue │ ├── config/ │ │ ├── api.js │ │ └── request.js │ ├── index.html │ ├── main.js │ ├── manifest.json │ ├── package.json │ ├── pages/ │ │ ├── account-detail/ │ │ │ └── account-detail.vue │ │ ├── accounts/ │ │ │ └── accounts.vue │ │ ├── books/ │ │ │ └── books.vue │ │ ├── charts/ │ │ │ └── charts.vue │ │ ├── flow-detail/ │ │ │ └── flow-detail.vue │ │ ├── flow-form/ │ │ │ └── flow-form.vue │ │ ├── flows/ │ │ │ └── flows.vue │ │ ├── index/ │ │ │ └── index.vue │ │ ├── login/ │ │ │ └── login.vue │ │ ├── my/ │ │ │ └── my.vue │ │ ├── select/ │ │ │ └── select.vue │ │ └── test/ │ │ └── test.vue │ ├── pages.json │ ├── store/ │ │ ├── index.js │ │ └── modules/ │ │ ├── account.js │ │ ├── expenseCategory.js │ │ ├── incomeCategory.js │ │ ├── modelForm.js │ │ ├── payee.js │ │ ├── select.js │ │ ├── session.js │ │ └── tag.js │ ├── uni.scss │ └── uni_modules/ │ ├── qiun-data-charts/ │ │ ├── changelog.md │ │ ├── components/ │ │ │ ├── qiun-data-charts/ │ │ │ │ └── qiun-data-charts.vue │ │ │ ├── qiun-error/ │ │ │ │ └── qiun-error.vue │ │ │ └── qiun-loading/ │ │ │ ├── loading1.vue │ │ │ ├── loading2.vue │ │ │ ├── loading3.vue │ │ │ ├── loading4.vue │ │ │ ├── loading5.vue │ │ │ └── qiun-loading.vue │ │ ├── js_sdk/ │ │ │ └── u-charts/ │ │ │ ├── config-echarts.js │ │ │ ├── config-ucharts.js │ │ │ ├── readme.md │ │ │ └── u-charts.js │ │ ├── license.md │ │ ├── package.json │ │ └── readme.md │ ├── uni-datetime-picker/ │ │ ├── changelog.md │ │ ├── components/ │ │ │ └── uni-datetime-picker/ │ │ │ ├── calendar-item.vue │ │ │ ├── calendar.vue │ │ │ ├── i18n/ │ │ │ │ ├── en.json │ │ │ │ ├── index.js │ │ │ │ ├── zh-Hans.json │ │ │ │ └── zh-Hant.json │ │ │ ├── keypress.js │ │ │ ├── time-picker.vue │ │ │ ├── uni-datetime-picker.vue │ │ │ └── util.js │ │ ├── package.json │ │ └── readme.md │ ├── uni-icons/ │ │ ├── changelog.md │ │ ├── components/ │ │ │ └── uni-icons/ │ │ │ ├── icons.js │ │ │ ├── uni-icons.vue │ │ │ └── uniicons.css │ │ ├── package.json │ │ └── readme.md │ ├── uni-list/ │ │ ├── changelog.md │ │ ├── components/ │ │ │ ├── uni-list/ │ │ │ │ ├── uni-list.vue │ │ │ │ ├── uni-refresh.vue │ │ │ │ └── uni-refresh.wxs │ │ │ ├── uni-list-ad/ │ │ │ │ └── uni-list-ad.vue │ │ │ ├── uni-list-chat/ │ │ │ │ ├── uni-list-chat.scss │ │ │ │ └── uni-list-chat.vue │ │ │ └── uni-list-item/ │ │ │ └── uni-list-item.vue │ │ ├── package.json │ │ └── readme.md │ └── uview-ui/ │ ├── LICENSE │ ├── README.md │ ├── changelog.md │ ├── components/ │ │ ├── u--form/ │ │ │ └── u--form.vue │ │ ├── u--image/ │ │ │ └── u--image.vue │ │ ├── u--input/ │ │ │ └── u--input.vue │ │ ├── u--text/ │ │ │ └── u--text.vue │ │ ├── u--textarea/ │ │ │ └── u--textarea.vue │ │ ├── u-action-sheet/ │ │ │ ├── props.js │ │ │ └── u-action-sheet.vue │ │ ├── u-album/ │ │ │ ├── props.js │ │ │ └── u-album.vue │ │ ├── u-alert/ │ │ │ ├── props.js │ │ │ └── u-alert.vue │ │ ├── u-avatar/ │ │ │ ├── props.js │ │ │ └── u-avatar.vue │ │ ├── u-avatar-group/ │ │ │ ├── props.js │ │ │ └── u-avatar-group.vue │ │ ├── u-back-top/ │ │ │ ├── props.js │ │ │ └── u-back-top.vue │ │ ├── u-badge/ │ │ │ ├── props.js │ │ │ └── u-badge.vue │ │ ├── u-button/ │ │ │ ├── nvue.scss │ │ │ ├── props.js │ │ │ ├── u-button.vue │ │ │ └── vue.scss │ │ ├── u-calendar/ │ │ │ ├── header.vue │ │ │ ├── month.vue │ │ │ ├── props.js │ │ │ ├── u-calendar.vue │ │ │ └── util.js │ │ ├── u-car-keyboard/ │ │ │ ├── props.js │ │ │ └── u-car-keyboard.vue │ │ ├── u-cell/ │ │ │ ├── props.js │ │ │ └── u-cell.vue │ │ ├── u-cell-group/ │ │ │ ├── props.js │ │ │ └── u-cell-group.vue │ │ ├── u-checkbox/ │ │ │ ├── props.js │ │ │ └── u-checkbox.vue │ │ ├── u-checkbox-group/ │ │ │ ├── props.js │ │ │ └── u-checkbox-group.vue │ │ ├── u-circle-progress/ │ │ │ ├── props.js │ │ │ └── u-circle-progress.vue │ │ ├── u-code/ │ │ │ ├── props.js │ │ │ └── u-code.vue │ │ ├── u-code-input/ │ │ │ ├── props.js │ │ │ └── u-code-input.vue │ │ ├── u-col/ │ │ │ ├── props.js │ │ │ └── u-col.vue │ │ ├── u-collapse/ │ │ │ ├── props.js │ │ │ └── u-collapse.vue │ │ ├── u-collapse-item/ │ │ │ ├── props.js │ │ │ └── u-collapse-item.vue │ │ ├── u-column-notice/ │ │ │ ├── props.js │ │ │ └── u-column-notice.vue │ │ ├── u-count-down/ │ │ │ ├── props.js │ │ │ ├── u-count-down.vue │ │ │ └── utils.js │ │ ├── u-count-to/ │ │ │ ├── props.js │ │ │ └── u-count-to.vue │ │ ├── u-datetime-picker/ │ │ │ ├── props.js │ │ │ └── u-datetime-picker.vue │ │ ├── u-divider/ │ │ │ ├── props.js │ │ │ └── u-divider.vue │ │ ├── u-dropdown/ │ │ │ ├── props.js │ │ │ └── u-dropdown.vue │ │ ├── u-dropdown-item/ │ │ │ ├── props.js │ │ │ └── u-dropdown-item.vue │ │ ├── u-empty/ │ │ │ ├── props.js │ │ │ └── u-empty.vue │ │ ├── u-form/ │ │ │ ├── props.js │ │ │ └── u-form.vue │ │ ├── u-form-item/ │ │ │ ├── props.js │ │ │ └── u-form-item.vue │ │ ├── u-gap/ │ │ │ ├── props.js │ │ │ └── u-gap.vue │ │ ├── u-grid/ │ │ │ ├── props.js │ │ │ └── u-grid.vue │ │ ├── u-grid-item/ │ │ │ ├── props.js │ │ │ └── u-grid-item.vue │ │ ├── u-icon/ │ │ │ ├── icons.js │ │ │ ├── props.js │ │ │ └── u-icon.vue │ │ ├── u-image/ │ │ │ ├── props.js │ │ │ └── u-image.vue │ │ ├── u-index-anchor/ │ │ │ ├── props.js │ │ │ └── u-index-anchor.vue │ │ ├── u-index-item/ │ │ │ ├── props.js │ │ │ └── u-index-item.vue │ │ ├── u-index-list/ │ │ │ ├── props.js │ │ │ └── u-index-list.vue │ │ ├── u-input/ │ │ │ ├── props.js │ │ │ └── u-input.vue │ │ ├── u-keyboard/ │ │ │ ├── props.js │ │ │ └── u-keyboard.vue │ │ ├── u-line/ │ │ │ ├── props.js │ │ │ └── u-line.vue │ │ ├── u-line-progress/ │ │ │ ├── props.js │ │ │ └── u-line-progress.vue │ │ ├── u-link/ │ │ │ ├── props.js │ │ │ └── u-link.vue │ │ ├── u-list/ │ │ │ ├── props.js │ │ │ └── u-list.vue │ │ ├── u-list-item/ │ │ │ ├── props.js │ │ │ └── u-list-item.vue │ │ ├── u-loading-icon/ │ │ │ ├── props.js │ │ │ └── u-loading-icon.vue │ │ ├── u-loading-page/ │ │ │ ├── props.js │ │ │ └── u-loading-page.vue │ │ ├── u-loadmore/ │ │ │ ├── props.js │ │ │ └── u-loadmore.vue │ │ ├── u-modal/ │ │ │ ├── props.js │ │ │ └── u-modal.vue │ │ ├── u-navbar/ │ │ │ ├── props.js │ │ │ └── u-navbar.vue │ │ ├── u-no-network/ │ │ │ ├── props.js │ │ │ └── u-no-network.vue │ │ ├── u-notice-bar/ │ │ │ ├── props.js │ │ │ └── u-notice-bar.vue │ │ ├── u-notify/ │ │ │ ├── props.js │ │ │ └── u-notify.vue │ │ ├── u-number-box/ │ │ │ ├── props.js │ │ │ └── u-number-box.vue │ │ ├── u-number-keyboard/ │ │ │ ├── props.js │ │ │ └── u-number-keyboard.vue │ │ ├── u-overlay/ │ │ │ ├── props.js │ │ │ └── u-overlay.vue │ │ ├── u-parse/ │ │ │ ├── node/ │ │ │ │ └── node.vue │ │ │ ├── parser.js │ │ │ ├── props.js │ │ │ └── u-parse.vue │ │ ├── u-picker/ │ │ │ ├── props.js │ │ │ └── u-picker.vue │ │ ├── u-picker-column/ │ │ │ ├── props.js │ │ │ └── u-picker-column.vue │ │ ├── u-popup/ │ │ │ ├── props.js │ │ │ └── u-popup.vue │ │ ├── u-radio/ │ │ │ ├── props.js │ │ │ └── u-radio.vue │ │ ├── u-radio-group/ │ │ │ ├── props.js │ │ │ └── u-radio-group.vue │ │ ├── u-rate/ │ │ │ ├── props.js │ │ │ └── u-rate.vue │ │ ├── u-read-more/ │ │ │ ├── props.js │ │ │ └── u-read-more.vue │ │ ├── u-row/ │ │ │ ├── props.js │ │ │ └── u-row.vue │ │ ├── u-row-notice/ │ │ │ ├── props.js │ │ │ └── u-row-notice.vue │ │ ├── u-safe-bottom/ │ │ │ ├── props.js │ │ │ └── u-safe-bottom.vue │ │ ├── u-scroll-list/ │ │ │ ├── nvue.js │ │ │ ├── other.js │ │ │ ├── props.js │ │ │ ├── scrollWxs.wxs │ │ │ └── u-scroll-list.vue │ │ ├── u-search/ │ │ │ ├── props.js │ │ │ └── u-search.vue │ │ ├── u-skeleton/ │ │ │ ├── props.js │ │ │ └── u-skeleton.vue │ │ ├── u-slider/ │ │ │ ├── mpother.js │ │ │ ├── mpwxs.js │ │ │ ├── mpwxs.wxs │ │ │ ├── nvue - 副本.js │ │ │ ├── nvue.js │ │ │ ├── props.js │ │ │ └── u-slider.vue │ │ ├── u-status-bar/ │ │ │ ├── props.js │ │ │ └── u-status-bar.vue │ │ ├── u-steps/ │ │ │ ├── props.js │ │ │ └── u-steps.vue │ │ ├── u-steps-item/ │ │ │ ├── props.js │ │ │ └── u-steps-item.vue │ │ ├── u-sticky/ │ │ │ ├── props.js │ │ │ └── u-sticky.vue │ │ ├── u-subsection/ │ │ │ ├── props.js │ │ │ └── u-subsection.vue │ │ ├── u-swipe-action/ │ │ │ ├── props.js │ │ │ └── u-swipe-action.vue │ │ ├── u-swipe-action-item/ │ │ │ ├── index - backup.wxs │ │ │ ├── index.wxs │ │ │ ├── nvue - backup.js │ │ │ ├── nvue.js │ │ │ ├── props.js │ │ │ ├── u-swipe-action-item.vue │ │ │ └── wxs.js │ │ ├── u-swiper/ │ │ │ ├── props.js │ │ │ └── u-swiper.vue │ │ ├── u-swiper-indicator/ │ │ │ ├── props.js │ │ │ └── u-swiper-indicator.vue │ │ ├── u-switch/ │ │ │ ├── props.js │ │ │ └── u-switch.vue │ │ ├── u-tabbar/ │ │ │ ├── props.js │ │ │ └── u-tabbar.vue │ │ ├── u-tabbar-item/ │ │ │ ├── props.js │ │ │ └── u-tabbar-item.vue │ │ ├── u-table/ │ │ │ ├── props.js │ │ │ └── u-table.vue │ │ ├── u-tabs/ │ │ │ ├── props.js │ │ │ └── u-tabs.vue │ │ ├── u-tabs-item/ │ │ │ ├── props.js │ │ │ └── u-tabs-item.vue │ │ ├── u-tag/ │ │ │ ├── props.js │ │ │ └── u-tag.vue │ │ ├── u-td/ │ │ │ ├── props.js │ │ │ └── u-td.vue │ │ ├── u-text/ │ │ │ ├── props.js │ │ │ ├── u-text.vue │ │ │ └── value.js │ │ ├── u-textarea/ │ │ │ ├── props.js │ │ │ └── u-textarea.vue │ │ ├── u-toast/ │ │ │ └── u-toast.vue │ │ ├── u-toolbar/ │ │ │ ├── props.js │ │ │ └── u-toolbar.vue │ │ ├── u-tooltip/ │ │ │ ├── props.js │ │ │ └── u-tooltip.vue │ │ ├── u-tr/ │ │ │ ├── props.js │ │ │ └── u-tr.vue │ │ ├── u-transition/ │ │ │ ├── nvue.ani-map.js │ │ │ ├── props.js │ │ │ ├── transition.js │ │ │ ├── u-transition.vue │ │ │ └── vue.ani-style.scss │ │ ├── u-upload/ │ │ │ ├── mixin.js │ │ │ ├── props.js │ │ │ ├── u-upload.vue │ │ │ └── utils.js │ │ └── uview-ui/ │ │ └── uview-ui.vue │ ├── index.js │ ├── index.scss │ ├── libs/ │ │ ├── config/ │ │ │ ├── color.js │ │ │ ├── config.js │ │ │ ├── props/ │ │ │ │ ├── actionSheet.js │ │ │ │ ├── album.js │ │ │ │ ├── alert.js │ │ │ │ ├── avatar.js │ │ │ │ ├── avatarGroup.js │ │ │ │ ├── backtop.js │ │ │ │ ├── badge.js │ │ │ │ ├── button.js │ │ │ │ ├── calendar.js │ │ │ │ ├── carKeyboard.js │ │ │ │ ├── cell.js │ │ │ │ ├── cellGroup.js │ │ │ │ ├── checkbox.js │ │ │ │ ├── checkboxGroup.js │ │ │ │ ├── circleProgress.js │ │ │ │ ├── code.js │ │ │ │ ├── codeInput.js │ │ │ │ ├── col.js │ │ │ │ ├── collapse.js │ │ │ │ ├── collapseItem.js │ │ │ │ ├── columnNotice.js │ │ │ │ ├── countDown.js │ │ │ │ ├── countTo.js │ │ │ │ ├── datetimePicker.js │ │ │ │ ├── divider.js │ │ │ │ ├── empty.js │ │ │ │ ├── form.js │ │ │ │ ├── formItem.js │ │ │ │ ├── gap.js │ │ │ │ ├── grid.js │ │ │ │ ├── gridItem.js │ │ │ │ ├── icon.js │ │ │ │ ├── image.js │ │ │ │ ├── indexAnchor.js │ │ │ │ ├── indexList.js │ │ │ │ ├── input.js │ │ │ │ ├── keyboard.js │ │ │ │ ├── line.js │ │ │ │ ├── lineProgress.js │ │ │ │ ├── link.js │ │ │ │ ├── list.js │ │ │ │ ├── listItem.js │ │ │ │ ├── loadingIcon.js │ │ │ │ ├── loadingPage.js │ │ │ │ ├── loadmore.js │ │ │ │ ├── modal.js │ │ │ │ ├── navbar.js │ │ │ │ ├── noNetwork.js │ │ │ │ ├── noticeBar.js │ │ │ │ ├── notify.js │ │ │ │ ├── numberBox.js │ │ │ │ ├── numberKeyboard.js │ │ │ │ ├── overlay.js │ │ │ │ ├── parse.js │ │ │ │ ├── picker.js │ │ │ │ ├── popup.js │ │ │ │ ├── radio.js │ │ │ │ ├── radioGroup.js │ │ │ │ ├── rate.js │ │ │ │ ├── readMore.js │ │ │ │ ├── row.js │ │ │ │ ├── rowNotice.js │ │ │ │ ├── scrollList.js │ │ │ │ ├── search.js │ │ │ │ ├── section.js │ │ │ │ ├── skeleton.js │ │ │ │ ├── slider.js │ │ │ │ ├── statusBar.js │ │ │ │ ├── steps.js │ │ │ │ ├── stepsItem.js │ │ │ │ ├── sticky.js │ │ │ │ ├── subsection.js │ │ │ │ ├── swipeAction.js │ │ │ │ ├── swipeActionItem.js │ │ │ │ ├── swiper.js │ │ │ │ ├── swipterIndicator.js │ │ │ │ ├── switch.js │ │ │ │ ├── tabbar.js │ │ │ │ ├── tabbarItem.js │ │ │ │ ├── tabs.js │ │ │ │ ├── tag.js │ │ │ │ ├── text.js │ │ │ │ ├── textarea.js │ │ │ │ ├── toast.js │ │ │ │ ├── toolbar.js │ │ │ │ ├── tooltip.js │ │ │ │ ├── transition.js │ │ │ │ └── upload.js │ │ │ ├── props.js │ │ │ └── zIndex.js │ │ ├── css/ │ │ │ ├── color.scss │ │ │ ├── common.scss │ │ │ ├── components.scss │ │ │ ├── flex.scss │ │ │ ├── h5.scss │ │ │ ├── mixin.scss │ │ │ ├── mp.scss │ │ │ ├── nvue.scss │ │ │ └── vue.scss │ │ ├── function/ │ │ │ ├── colorGradient.js │ │ │ ├── debounce.js │ │ │ ├── digit.js │ │ │ ├── index.js │ │ │ ├── platform.js │ │ │ ├── test.js │ │ │ └── throttle.js │ │ ├── luch-request/ │ │ │ ├── adapters/ │ │ │ │ └── index.js │ │ │ ├── core/ │ │ │ │ ├── InterceptorManager.js │ │ │ │ ├── Request.js │ │ │ │ ├── buildFullPath.js │ │ │ │ ├── defaults.js │ │ │ │ ├── dispatchRequest.js │ │ │ │ ├── mergeConfig.js │ │ │ │ └── settle.js │ │ │ ├── helpers/ │ │ │ │ ├── buildURL.js │ │ │ │ ├── combineURLs.js │ │ │ │ └── isAbsoluteURL.js │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── utils/ │ │ │ │ └── clone.js │ │ │ └── utils.js │ │ ├── mixin/ │ │ │ ├── button.js │ │ │ ├── mixin.js │ │ │ ├── mpMixin.js │ │ │ ├── mpShare.js │ │ │ ├── openType.js │ │ │ ├── style.js │ │ │ └── touch.js │ │ └── util/ │ │ ├── async-validator.js │ │ ├── calendar.js │ │ ├── dayjs.js │ │ ├── emitter.js │ │ └── route.js │ ├── package.json │ └── theme.scss ├── docker/ │ └── mysql/ │ ├── bookkeeping.sql │ ├── config/ │ │ └── my.cnf │ └── currency.sql ├── docker-compose.yaml ├── docker-compose2.yaml └── notes.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # https://github.com/alexkaratarakis/gitattributes # Handle line endings automatically for files detected as text # and leave all files detected as binary untouched. * text=auto # # The above will handle all files NOT found below # # These files are text and should be normalized (Convert crlf => lf) *.css text *.df text *.htm text *.html text *.java text *.js text *.json text *.jsp text *.jspf text *.jspx text *.properties text *.sh text *.tld text *.txt text *.tag text *.tagx text *.xml text *.yml text # These files are binary and should be left untouched # (binary is a macro for -text -diff) *.class binary *.dll binary *.ear binary *.gif binary *.ico binary *.jar binary *.jpg binary *.jpeg binary *.png binary *.so binary *.war binary ================================================ FILE: .gitignore ================================================ target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/** !**/src/test/** ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### nbproject/private/ nbbuild/ dist/ nbdist/ .nb-gradle/ build/ ### VS Code ### .vscode/ .gradle/ bin/ .DS_Store build/ .apt_generated_tests/ # dependencies node_modules npm-debug.log* yarn-error.log # umi .umi .umi-production #dist dist .hbuilderx unpackage .env ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [2022] [feiyu-rs] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # DEPRECATED This is no longer supported, please consider using [https://github.com/getmoneynote/moneynote-api](https://github.com/getmoneynote/moneynote-api) instead. ================================================ FILE: bookkeeping-user-api/.mvn/wrapper/MavenWrapperDownloader.java ================================================ /* * Copyright 2007-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.net.*; import java.io.*; import java.nio.channels.*; import java.util.Properties; public class MavenWrapperDownloader { private static final String WRAPPER_VERSION = "0.5.6"; /** * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. */ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; /** * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to * use instead of the default one. */ private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; /** * Path where the maven-wrapper.jar will be saved to. */ private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; /** * Name of the property which should be used to override the default download url for the wrapper. */ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; public static void main(String args[]) { System.out.println("- Downloader started"); File baseDirectory = new File(args[0]); System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); // If the maven-wrapper.properties exists, read it and check if it contains a custom // wrapperUrl parameter. File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); String url = DEFAULT_DOWNLOAD_URL; if(mavenWrapperPropertyFile.exists()) { FileInputStream mavenWrapperPropertyFileInputStream = null; try { mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); Properties mavenWrapperProperties = new Properties(); mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); } catch (IOException e) { System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); } finally { try { if(mavenWrapperPropertyFileInputStream != null) { mavenWrapperPropertyFileInputStream.close(); } } catch (IOException e) { // Ignore ... } } } System.out.println("- Downloading from: " + url); File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); if(!outputFile.getParentFile().exists()) { if(!outputFile.getParentFile().mkdirs()) { System.out.println( "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); } } System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); try { downloadFileFromURL(url, outputFile); System.out.println("Done"); System.exit(0); } catch (Throwable e) { System.out.println("- Error downloading"); e.printStackTrace(); System.exit(1); } } private static void downloadFileFromURL(String urlString, File destination) throws Exception { if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { String username = System.getenv("MVNW_USERNAME"); char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); Authenticator.setDefault(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } }); } URL website = new URL(urlString); ReadableByteChannel rbc; rbc = Channels.newChannel(website.openStream()); FileOutputStream fos = new FileOutputStream(destination); fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); fos.close(); rbc.close(); } } ================================================ FILE: bookkeeping-user-api/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar ================================================ FILE: bookkeeping-user-api/Dockerfile ================================================ #FROM openjdk:11-slim #COPY target/*.jar app.jar #ENTRYPOINT ["java","-jar","/app.jar"] FROM openjdk:11-slim as build WORKDIR /workspace/app COPY mvnw . COPY .mvn .mvn COPY pom.xml . COPY src src RUN ./mvnw -B -DskipTests clean package FROM openjdk:11-slim VOLUME /tmp COPY --from=build /workspace/app/target/*.jar app.jar ENTRYPOINT ["java", "-jar", "/app.jar"] ================================================ FILE: bookkeeping-user-api/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`which java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi if [ -n "$MVNW_REPOURL" ]; then jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" else jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" fi while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if $cygwin; then wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` fi if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then wget "$jarUrl" -O "$wrapperJarPath" else wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" fi elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then curl -o "$wrapperJarPath" "$jarUrl" -f else curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then javaClass=`cygpath --path --windows "$javaClass"` fi if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: bookkeeping-user-api/mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( if "%MVNW_VERBOSE%" == "true" ( echo Found %WRAPPER_JAR% ) ) else ( if not "%MVNW_REPOURL%" == "" ( SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% ) powershell -Command "&{"^ "$webclient = new-object System.Net.WebClient;"^ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% ) ) @REM End of extension @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%" == "on" pause if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% exit /B %ERROR_CODE% ================================================ FILE: bookkeeping-user-api/pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 2.6.0 com.jiukuaitech bookkeeping-user 0.1 bookkeeping-user Bookkeeping API for User jar UTF-8 11 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-validation mysql mysql-connector-java com.qiniu qiniu-java-sdk [7.7.0, 7.7.99] org.projectlombok lombok 1.18.20 provided org.hibernate hibernate-jpamodelgen 5.5.6.Final provided org.springframework.boot spring-boot-devtools true runtime org.springframework.boot spring-boot-maven-plugin maven-compiler-plugin org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor,lombok.launch.AnnotationProcessorHider$AnnotationProcessor ================================================ FILE: bookkeeping-user-api/restart.sh ================================================ #!/bin/bash . stop.sh rm -rf 1.log . startup.sh ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/Application.java ================================================ package com.jiukuaitech.bookkeeping.user; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/RegexTest.java ================================================ package com.jiukuaitech.bookkeeping.user; public class RegexTest implements Runnable { private static int i; public synchronized static void change() { for (int j = 0; j < 5000000; j++) { i++; } } @Override public void run() { synchronized ("123".intern()) { for (int j = 0; j < 5000000; j++) { i++; } } } public static void main(String[] args) throws Exception { new Thread(new RegexTest()).start(); new Thread(new RegexTest()).start(); Thread.sleep(3000); System.out.println(i); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/Account.java ================================================ package com.jiukuaitech.bookkeeping.user.account; import com.jiukuaitech.bookkeeping.user.base.NameNotesEnableEntity; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.validation.BalanceValidator; import com.jiukuaitech.bookkeeping.user.validation.NoValidator; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.NotNull; import java.math.BigDecimal; @Entity @Table(name = "t_account", uniqueConstraints = {@UniqueConstraint(columnNames = {"group_id", "name"})}) @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.INTEGER, columnDefinition = "TINYINT(1)") @Getter @Setter public class Account extends NameNotesEnableEntity { @Column(insertable = false, updatable = false) private Integer type; //1活期,2信用,3贷款,4资产 @ManyToOne(optional = false, fetch = FetchType.LAZY) @JoinColumn(name = "group_id") @NotNull private Group group; // 账簿必须属于某个组 @Column(length = 32) @NoValidator private String no; //卡号 @Column(nullable = false) //最多9亿 @NotNull @BalanceValidator private BigDecimal balance; // 当前余额 @Column(nullable = false) @NotNull private Boolean include = true; //净资产是否包含 @Column(nullable = false) @NotNull private Boolean expenseable = true; //是否可支出 @Column(nullable = false) @NotNull private Boolean incomeable = true; //是否可收入 @Column(nullable = false) @NotNull private Boolean transferFromAble = true; //是否转账可转出 @Column(nullable = false) @NotNull private Boolean transferToAble = true; //是否转账可转入 @Column(nullable = false, length = 8) @NotNull private String currencyCode; @Column @BalanceValidator private BigDecimal initialBalance; // 期初余额 public Account() { } public Account(Integer id) { super.setId(id); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.account; import java.math.BigDecimal; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import com.jiukuaitech.bookkeeping.user.validation.BalanceValidator; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.NoValidator; import com.jiukuaitech.bookkeeping.user.validation.NotesValidator; import lombok.Getter; import lombok.Setter; @Getter @Setter public class AccountAddRequest { @NotBlank(message="name must not be blank") @NameValidator private String name; @NoValidator private String no; @NotNull @BalanceValidator private BigDecimal balance; private Boolean include = true; private Boolean transferFromAble = true; private Boolean transferToAble = true; private Boolean expenseable = true; private Boolean incomeable = true; @NotesValidator private String notes; @NotBlank private String currencyCode; public void copyPrimitive(Account po) { po.setName(name); po.setNo(no); po.setBalance(balance); po.setInitialBalance(balance); po.setInclude(include); po.setTransferFromAble(transferFromAble); po.setTransferToAble(transferToAble); po.setExpenseable(expenseable); po.setIncomeable(incomeable); po.setNotes(notes); po.setCurrencyCode(currencyCode); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountAdjustBalanceNotValidException.java ================================================ package com.jiukuaitech.bookkeeping.user.account; public class AccountAdjustBalanceNotValidException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountController.java ================================================ package com.jiukuaitech.bookkeeping.user.account; import com.jiukuaitech.bookkeeping.user.adjust_balance.AdjustBalanceAddRequest; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/accounts") public class AccountController extends BaseController { @Resource private AccountService accountService; // 搜索流水的下拉框需要显示所有可用的account @RequestMapping(method = RequestMethod.GET, value = "/enable") public BaseResponse handleEnable(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.getEnable(userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/expenseable") public BaseResponse handleExpenseble(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.getAllExpenseable(userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/incomeable") public BaseResponse handleIncomeble(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.getAllIncomeable(userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/transfer-from-able") public BaseResponse handleTransferFromAble(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.getAllTransferFromAble(userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/transfer-to-able") public BaseResponse handleTransferToAble(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.getAllTransferToAble(userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/{id}") public BaseResponse handleGet(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.get(id, userSignInId)); } @RequestMapping(method = RequestMethod.POST, value = "/{id}/adjust-balance") public BaseResponse handleAdjustBalance( @PathVariable("id") Integer id, @Valid @RequestBody AdjustBalanceAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.adjustBalance(id, request, userSignInId)); } @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") public BaseResponse handleDelete(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.remove(id, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggle") public BaseResponse handleToggle(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.toggle(id, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggleInclude") public BaseResponse handleToggleInclude(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.toggleInclude(id, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggleExpenseable") public BaseResponse handleToggleExpenseable(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.toggleExpenseable(id, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggleIncomeable") public BaseResponse handleToggleIncomeable(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.toggleIncomeable(id, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggleTransferFromAble") public BaseResponse handleToggleTransferFromAble(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.toggleTransferFromAble(id, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggleTransferToAble") public BaseResponse handleToggleTransferToAble(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.toggleTransferToAble(id, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}") public BaseResponse handleUpdate( @PathVariable("id") Integer id, @Valid @RequestBody AccountUpdateRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.update(id, request, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountExceptionHandler.java ================================================ package com.jiukuaitech.bookkeeping.user.account; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.ErrorResponse; import org.springframework.context.MessageSource; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.annotation.Resource; import java.util.Locale; @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) public class AccountExceptionHandler { private static final Locale LANG = Locale.CHINA; @Resource private MessageSource messageSource; @ExceptionHandler(value = AccountNameExistsException.class) // @ResponseStatus(HttpStatus.CONFLICT) @ResponseBody public BaseResponse handleException(AccountNameExistsException e) { return new ErrorResponse(409, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = AccountMaxCountException.class) @ResponseBody public BaseResponse handleException(AccountMaxCountException e) { return new ErrorResponse(501, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = AccountHasTransactionException.class) @ResponseBody public BaseResponse handleException(AccountHasTransactionException e) { return new ErrorResponse(601, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = AccountAdjustBalanceNotValidException.class) @ResponseBody public BaseResponse handleException(AccountAdjustBalanceNotValidException e) { return new ErrorResponse(602, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = DefaultExpenseAccountException.class) @ResponseBody public BaseResponse handleException(DefaultExpenseAccountException e) { return new ErrorResponse(603, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = DefaultIncomeAccountException.class) @ResponseBody public BaseResponse handleException(DefaultIncomeAccountException e) { return new ErrorResponse(604, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = DefaultTransferFromAccountException.class) @ResponseBody public BaseResponse handleException(DefaultTransferFromAccountException e) { return new ErrorResponse(605, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = DefaultTransferToAccountException.class) @ResponseBody public BaseResponse handleException(DefaultTransferToAccountException e) { return new ErrorResponse(606, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountHasTransactionException.java ================================================ package com.jiukuaitech.bookkeeping.user.account; public class AccountHasTransactionException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountMaxCountException.java ================================================ package com.jiukuaitech.bookkeeping.user.account; public class AccountMaxCountException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountNameExistsException.java ================================================ package com.jiukuaitech.bookkeeping.user.account; public class AccountNameExistsException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountQueryRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.account; import lombok.Getter; import lombok.Setter; @Getter @Setter public class AccountQueryRequest { private Boolean enable; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.account; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.user.User; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository public interface AccountRepository extends BaseRepository { Optional findOneByGroupAndName(Group group, String name); Optional findOneByGroupAndId(Group group, Integer id); long countByGroup(Group group); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountService.java ================================================ package com.jiukuaitech.bookkeeping.user.account; import com.jiukuaitech.bookkeeping.user.adjust_balance.AdjustBalance; import com.jiukuaitech.bookkeeping.user.adjust_balance.AdjustBalanceAddRequest; import com.jiukuaitech.bookkeeping.user.adjust_balance.AdjustBalanceRepository; import com.jiukuaitech.bookkeeping.user.asset_account.AssetAccount; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.checking_account.CheckingAccount; import com.jiukuaitech.bookkeeping.user.credit_account.CreditAccount; import com.jiukuaitech.bookkeeping.user.debt_account.DebtAccount; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.credit_account.CreditAccountAddRequest; import com.jiukuaitech.bookkeeping.user.currency.CurrencyService; import com.jiukuaitech.bookkeeping.user.debt_account.DebtAccountAddRequest; import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException; import com.jiukuaitech.bookkeeping.user.transaction.TransactionRepository; import com.jiukuaitech.bookkeeping.user.transfer.TransferRepository; import com.jiukuaitech.bookkeeping.user.user.UserService; import com.jiukuaitech.bookkeeping.user.user_log.UserActionLog; import com.jiukuaitech.bookkeeping.user.user_log.UserActionLogRepository; import com.jiukuaitech.bookkeeping.user.user_log.UserActionLogService; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.math.BigDecimal; import java.time.Instant; import java.util.List; import java.util.stream.Collectors; @Service public class AccountService { @Resource private UserService userService; @Resource private AccountRepository accountRepository; @Resource private AdjustBalanceRepository adjustBalanceRepository; @Resource private TransactionRepository transactionRepository; @Resource private TransferRepository transferRepository; @Resource private UserActionLogRepository userActionLogRepository; @Resource private CurrencyService currencyService; @Resource private UserActionLogService userActionLogService; @Value("${account.max.count}") private Integer accountMaxCount; public List getEnable(Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); List entityList = accountRepository.findAll(AccountSpec.inGroupAndEnable(group)); return entityList.stream().map(AccountVOForExtend::fromEntity).collect(Collectors.toList()); } public List getAllExpenseable(Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); List entityList = accountRepository.findAll(AccountSpec.inGroupAndExpenseable(group)); return entityList.stream().map(AccountVOForExtend::fromEntity).collect(Collectors.toList()); } public List getAllIncomeable(Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); List entityList = accountRepository.findAll(AccountSpec.inGroupAndIncomeable(group)); return entityList.stream().map(AccountVOForExtend::fromEntity).collect(Collectors.toList()); } public List getAllTransferFromAble(Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); List entityList = accountRepository.findAll(AccountSpec.inGroupAndTransferFromAble(group)); return entityList.stream().map(AccountVOForExtend::fromEntity).collect(Collectors.toList()); } public List getAllTransferToAble(Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); List entityList = accountRepository.findAll(AccountSpec.inGroupAndTransferToAble(group)); return entityList.stream().map(AccountVOForExtend::fromEntity).collect(Collectors.toList()); } public AccountVOForList get(Integer id, Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new); AccountVOForList vo = new AccountVOForList(); vo.setValue(po); vo.setConvertedBalance(currencyService.convert(vo.getBalance(), vo.getCurrencyCode(), group.getDefaultCurrencyCode())); if (po instanceof CreditAccount) { CreditAccount po2 = (CreditAccount) po; vo.setLimit(po2.getLimit()); vo.setBillDay(po2.getBillDay()); } if (po instanceof DebtAccount) { DebtAccount po2 = (DebtAccount) po; vo.setApr(po2.getApr()); vo.setLimit(po2.getLimit()); } if (po instanceof AssetAccount) { AssetAccount po2 = (AssetAccount) po; vo.setAsOfDate(po2.getAsOfDate()); } return vo; } @Transactional public AccountVOForExtend adjustBalance(Integer id, AdjustBalanceAddRequest request, Integer userSignInId) { User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); Account account = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new); if (account.getBalance().compareTo(request.getBalance()) == 0) { throw new AccountAdjustBalanceNotValidException(); } BigDecimal adjustAmount = request.getBalance().subtract(account.getBalance()); account.setBalance(request.getBalance()); // 如果是资产账户改一下截止日期 if (account instanceof AssetAccount) { AssetAccount assetAccount = (AssetAccount) account; assetAccount.setAsOfDate(request.getCreateTime()); } accountRepository.save(account); AdjustBalance po = new AdjustBalance(); request.copyPrimitive(po); po.setCreator(user); po.setBook(user.getDefaultBook()); po.setGroup(user.getDefaultGroup()); po.setAmount(adjustAmount); po.setAccount(account); po.setStatus(1); adjustBalanceRepository.save(po); userActionLogRepository.save(new UserActionLog(user, 1, Instant.now().toEpochMilli())); return AccountVOForExtend.fromEntity(po.getAccount()); } // 1. 余额调整记录删除,2. 日志删除 @Transactional public AccountVOForExtend remove(Integer id, Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new); // 检查关联性,有账单关联的不能删除 if (transactionRepository.countByAccount_id(id) > 0 || transferRepository.countByTo_id(id) > 0) { throw new AccountHasTransactionException(); } adjustBalanceRepository.deleteByAccount_Id(id); accountRepository.delete(po); return AccountVOForExtend.fromEntity(po); } public AccountVOForExtend toggle(Integer id, Integer userSignInId) { User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new); Book book = user.getDefaultBook(); if (po.equals(book.getDefaultExpenseAccount())) throw new DefaultExpenseAccountException(); if (po.equals(book.getDefaultIncomeAccount())) throw new DefaultIncomeAccountException(); po.setEnable(!po.getEnable()); accountRepository.save(po); return AccountVOForExtend.fromEntity(accountRepository.save(po)); } public boolean toggleInclude(Integer id, Integer userSignInId) { User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new); po.setInclude(!po.getInclude()); accountRepository.save(po); return true; } public boolean toggleExpenseable(Integer id, Integer userSignInId) { User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new); Book book = user.getDefaultBook(); if (po.equals(book.getDefaultExpenseAccount())) throw new DefaultExpenseAccountException(); po.setExpenseable(!po.getExpenseable()); accountRepository.save(po); return true; } public boolean toggleIncomeable(Integer id, Integer userSignInId) { User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new); Book book = user.getDefaultBook(); if (po.equals(book.getDefaultIncomeAccount())) throw new DefaultIncomeAccountException(); po.setIncomeable(!po.getIncomeable()); accountRepository.save(po); return true; } public boolean toggleTransferFromAble(Integer id, Integer userSignInId) { User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new); Book book = user.getDefaultBook(); if (po.equals(book.getDefaultTransferFromAccount())) throw new DefaultTransferFromAccountException(); po.setTransferFromAble(!po.getTransferFromAble()); accountRepository.save(po); return true; } public boolean toggleTransferToAble(Integer id, Integer userSignInId) { User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new); Book book = user.getDefaultBook(); if (po.equals(book.getDefaultTransferToAccount())) throw new DefaultTransferToAccountException(); po.setTransferToAble(!po.getTransferToAble()); accountRepository.save(po); return true; } public boolean add(Integer type, AccountAddRequest request, Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); if (accountRepository.countByGroup(group) >= accountMaxCount) { throw new AccountMaxCountException(); } if (accountRepository.findOneByGroupAndName(group, request.getName()).isPresent()) { throw new AccountNameExistsException(); } currencyService.checkCode(request.getCurrencyCode()); Account po = null; switch (type) { case 1: po = new CheckingAccount(); request.copyPrimitive(po); break; case 2: po = new CreditAccount(); ((CreditAccountAddRequest) request).copyPrimitive((CreditAccount) po); break; case 3: po = new DebtAccount(); ((DebtAccountAddRequest) request).copyPrimitive((DebtAccount) po); break; case 4: po = new AssetAccount(); request.copyPrimitive(po); ((AssetAccount)po).setAsOfDate(Instant.now().toEpochMilli()); break; } po.setGroup(group); accountRepository.save(po); return true; } public AccountVOForExtend update(Integer id, AccountUpdateRequest request, Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new); if (StringUtils.hasText(request.getName())) { if (!po.getName().equals(request.getName())) { if (accountRepository.findOneByGroupAndName(group, request.getName()).isPresent()) { throw new AccountNameExistsException(); } } } request.updatePrimitive(po); accountRepository.save(po); return AccountVOForExtend.fromEntity(po); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountSpec.java ================================================ package com.jiukuaitech.bookkeeping.user.account; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.base.BookNameNotesEnableSpec; import org.springframework.data.jpa.domain.Specification; public final class AccountSpec { public static Specification includeTrue() { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Account_.include), true); } public static Specification expenseable() { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Account_.expenseable), true); } public static Specification incomeable() { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Account_.incomeable), true); } public static Specification transferFromAble() { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Account_.transferFromAble), true); } public static Specification transferToAble() { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Account_.transferToAble), true); } public static Specification inGroup(Group group) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Account_.group), group); } public static Specification inGroupAndEnable(Group group) { Specification specification = inGroup(group); return specification.and(BookNameNotesEnableSpec.enable()); } public static Specification inGroupAndInclude(Group group) { Specification specification = inGroupAndEnable(group); return specification.and(includeTrue()); } public static Specification inGroupAndExpenseable(Group group) { Specification specification = inGroupAndEnable(group); return specification.and(expenseable()); } public static Specification inGroupAndIncomeable(Group group) { Specification specification = inGroupAndEnable(group); return specification.and(incomeable()); } public static Specification inGroupAndTransferFromAble(Group group) { Specification specification = inGroupAndEnable(group); return specification.and(transferFromAble()); } public static Specification inGroupAndTransferToAble(Group group) { Specification specification = inGroupAndEnable(group); return specification.and(transferToAble()); } public static Specification buildSpecification(AccountQueryRequest request, Group group) { Specification specification = inGroup(group); if (request.getEnable() != null) { specification = specification.and(BookNameNotesEnableSpec.isEnable(request.getEnable())); } return specification; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountSumVO.java ================================================ package com.jiukuaitech.bookkeeping.user.account; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @Getter @Setter public class AccountSumVO { private BigDecimal balance; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountUpdateRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.account; import java.math.BigDecimal; import com.jiukuaitech.bookkeeping.user.credit_account.CreditAccount; import com.jiukuaitech.bookkeeping.user.debt_account.DebtAccount; import com.jiukuaitech.bookkeeping.user.validation.*; import lombok.Getter; import lombok.Setter; @Getter @Setter public class AccountUpdateRequest { @NameValidator private String name; @NoValidator private String no; private Boolean include; private Boolean transferFromAble; private Boolean transferToAble; private Boolean expenseable; private Boolean incomeable; @NotesValidator private String notes; // For Credit @CreditLimitValidator private BigDecimal limit; // 信用额度 @BillDayValidator private Integer billDay; // 每月多少号是账单日 @AprValidator private BigDecimal apr; public void updatePrimitive(Account po) { if (name != null) po.setName(name); if (no != null) po.setNo(no); if (include != null) po.setInclude(include); if (transferFromAble != null) po.setTransferFromAble(transferFromAble); if (transferToAble != null) po.setTransferToAble(transferToAble); if (expenseable != null) po.setExpenseable(expenseable); if (incomeable != null) po.setIncomeable(incomeable); if (notes != null) po.setNotes(notes); if (po instanceof CreditAccount) { if (limit != null) ((CreditAccount)po).setLimit(limit); if (billDay != null) ((CreditAccount)po).setBillDay(billDay); } else if (po instanceof DebtAccount) { if (limit != null) ((DebtAccount)po).setLimit(limit); if (apr != null) ((DebtAccount)po).setApr(apr); } } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountVOForExtend.java ================================================ package com.jiukuaitech.bookkeeping.user.account; import com.jiukuaitech.bookkeeping.user.utils.EnumUtils; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @Getter @Setter public class AccountVOForExtend { private Integer type; //1活期,2信用,3贷款,4资产 private Integer id; private String name; private String no; private BigDecimal balance; private BigDecimal convertedBalance; private String currencyCode; private Boolean enable; private Boolean include; private Boolean expenseable; private Boolean incomeable; private Boolean transferFromAble; private Boolean transferToAble; private BigDecimal initialBalance; private String notes; public void setValue(Account po) { setType(po.getType()); setId(po.getId()); setName(po.getName()); setNo(po.getNo()); setBalance(po.getBalance()); setEnable(po.getEnable()); setInclude(po.getInclude()); setExpenseable(po.getExpenseable()); setIncomeable(po.getIncomeable()); setTransferFromAble(po.getTransferFromAble()); setTransferToAble(po.getTransferToAble()); setInitialBalance(po.getInitialBalance()); setNotes(po.getNotes()); setCurrencyCode(po.getCurrencyCode()); } public static AccountVOForExtend fromEntity(Account po) { if (po == null) return null; AccountVOForExtend vo = new AccountVOForExtend(); vo.setValue(po); return vo; } public String getTypeName() { return EnumUtils.translateAccountType(type); } public String getBalanceFormatted() { return String.format("%.2f", balance); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.account; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @Getter @Setter public class AccountVOForList extends AccountVOForExtend { private BigDecimal limit; private Integer billDay; private BigDecimal apr; private Long asOfDate; public BigDecimal getRemainLimit() { if (limit == null) return null; return limit.add(getBalance()); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/DefaultExpenseAccountException.java ================================================ package com.jiukuaitech.bookkeeping.user.account; public class DefaultExpenseAccountException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/DefaultIncomeAccountException.java ================================================ package com.jiukuaitech.bookkeeping.user.account; public class DefaultIncomeAccountException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/DefaultTransferFromAccountException.java ================================================ package com.jiukuaitech.bookkeeping.user.account; public class DefaultTransferFromAccountException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/DefaultTransferToAccountException.java ================================================ package com.jiukuaitech.bookkeeping.user.account; public class DefaultTransferToAccountException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/adjust_balance/AdjustBalance.java ================================================ package com.jiukuaitech.bookkeeping.user.adjust_balance; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlow; import javax.persistence.*; @Entity @DiscriminatorValue(value = "4") public class AdjustBalance extends BalanceFlow { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/adjust_balance/AdjustBalanceAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.adjust_balance; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowAddRequest; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotNull; import java.math.BigDecimal; @Getter @Setter public class AdjustBalanceAddRequest extends BalanceFlowAddRequest { @NotNull private BigDecimal balance; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/adjust_balance/AdjustBalanceController.java ================================================ package com.jiukuaitech.bookkeeping.user.adjust_balance; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowUpdateRequest; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/adjust-balances") public class AdjustBalanceController extends BaseController { @Resource private AdjustBalanceService adjustBalanceService; @RequestMapping(method = RequestMethod.PUT, value = "/{id}") public BaseResponse handleUpdate( @PathVariable("id") Integer id, @Valid @RequestBody BalanceFlowUpdateRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(adjustBalanceService.update(id, request, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/adjust_balance/AdjustBalanceRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.adjust_balance; import com.jiukuaitech.bookkeeping.user.base.HasBookRepository; import org.springframework.stereotype.Repository; @Repository public interface AdjustBalanceRepository extends HasBookRepository { void deleteByAccount_Id(Integer id); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/adjust_balance/AdjustBalanceService.java ================================================ package com.jiukuaitech.bookkeeping.user.adjust_balance; import com.jiukuaitech.bookkeeping.user.account.Account; import com.jiukuaitech.bookkeeping.user.account.AccountRepository; import com.jiukuaitech.bookkeeping.user.account.AccountVOForExtend; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.user.UserService; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowUpdateRequest; import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; @Service public class AdjustBalanceService { @Resource private UserService userService; @Resource private AccountRepository accountRepository; @Resource private AdjustBalanceRepository adjustBalanceRepository; @Transactional public AccountVOForExtend remove(Integer id, Integer userSignInId) { User user = userService.getUser(userSignInId); AdjustBalance po = adjustBalanceRepository.findOneByBookAndId(user.getDefaultBook(), id).orElseThrow(ItemNotFoundException::new); // 处理退款 Account account = po.getAccount(); account.setBalance(account.getBalance().subtract(po.getAmount())); accountRepository.save(account); adjustBalanceRepository.delete(po); return AccountVOForExtend.fromEntity(po.getAccount()); } public boolean update(Integer id, BalanceFlowUpdateRequest request, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); AdjustBalance po = adjustBalanceRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); request.updatePrimitive(po); adjustBalanceRepository.save(po); return true; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/adjust_balance/AdjustBalanceVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.adjust_balance; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowVOForExtend; public class AdjustBalanceVOForList extends BalanceFlowVOForExtend { public static AdjustBalanceVOForList fromEntity(AdjustBalance po) { AdjustBalanceVOForList vo = new AdjustBalanceVOForList(); vo.setValue(po); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/aop/TestAspect.java ================================================ package com.jiukuaitech.bookkeeping.user.aop; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Configuration; import java.util.Arrays; @Aspect @Configuration public class TestAspect { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Before("execution(* com.jiukuaitech.bookkeeping.user.service.*.*(..))") public void before(JoinPoint joinPoint) { logger.info(" Check before-------------------------------------------------------- "); logger.info(" Allowed execution for {}", ""); logger.info(Arrays.toString(joinPoint.getArgs())); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/asset_account/AssetAccount.java ================================================ package com.jiukuaitech.bookkeeping.user.asset_account; import com.jiukuaitech.bookkeeping.user.account.Account; import com.jiukuaitech.bookkeeping.user.validation.TimeValidator; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Entity @DiscriminatorValue(value = "4") @Getter @Setter public class AssetAccount extends Account { @TimeValidator private Long asOfDate; //截止时间,资产余额对应的时间 } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/asset_account/AssetAccountController.java ================================================ package com.jiukuaitech.bookkeeping.user.asset_account; import com.jiukuaitech.bookkeeping.user.account.AccountQueryRequest; import com.jiukuaitech.bookkeeping.user.account.AccountService; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import com.jiukuaitech.bookkeeping.user.account.AccountAddRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/asset-accounts") public class AssetAccountController extends BaseController { @Resource private AssetAccountService assetAccountService; @Resource private AccountService accountService; @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleQuery( @Valid AccountQueryRequest request, @PageableDefault(sort = "balance", direction = Sort.Direction.DESC) Pageable page, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(assetAccountService.query(request, page, userSignInId)); } @RequestMapping(method = RequestMethod.POST, value = "") public BaseResponse handleAdd( @Valid @RequestBody AccountAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.add(4, request, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/sum") public BaseResponse handleSum(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(assetAccountService.sum(userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/asset_account/AssetAccountRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.asset_account; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import org.springframework.stereotype.Repository; @Repository public interface AssetAccountRepository extends BaseRepository { /* @Query("SELECT COALESCE(SUM(p.balance), 0), " + "COALESCE(SUM(p.expense), 0), " + "COALESCE(SUM(p.income), 0), " + "COALESCE(SUM(p.transferFrom), 0), " + "COALESCE(SUM(p.transferTo), 0) " + "FROM AssetAccount p WHERE p.book = :book AND p.enable = true") List findSum(Book book); @Query("SELECT COALESCE(SUM(p.balance), 0) FROM AssetAccount p WHERE p.book = :book AND p.enable = true AND p.include = true") BigDecimal findSumBalance(Book book); */ } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/asset_account/AssetAccountService.java ================================================ package com.jiukuaitech.bookkeeping.user.asset_account; import com.jiukuaitech.bookkeeping.user.account.Account; import com.jiukuaitech.bookkeeping.user.account.AccountQueryRequest; import com.jiukuaitech.bookkeeping.user.account.AccountSpec; import com.jiukuaitech.bookkeeping.user.currency.CurrencyService; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.user.UserService; import com.jiukuaitech.bookkeeping.user.account.AccountSumVO; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.util.List; @Service public class AssetAccountService { @Resource private UserService userService; @Resource private AssetAccountRepository assetAccountRepository; @Resource private CurrencyService currencyService; public Page query(AccountQueryRequest request, Pageable page, Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); Specification specification = AccountSpec.buildSpecification(request, group); Page poPage = assetAccountRepository.findAll(specification, page); Page voPage = poPage.map(AssetAccountVOForList::fromEntity); voPage.map(vo->{ vo.setConvertedBalance(currencyService.convert(vo.getBalance(), vo.getCurrencyCode(), group.getDefaultCurrencyCode())); return vo; }); return voPage; } public AccountSumVO sum(Integer userSignInId) { AccountSumVO vo = new AccountSumVO(); Group group = userService.getUser(userSignInId).getDefaultGroup(); List accounts = assetAccountRepository.findAll(AccountSpec.inGroupAndEnable(group)); BigDecimal balance = BigDecimal.ZERO; for (int i = 0; i < accounts.size(); i++) { Account account = accounts.get(i); balance = balance.add(currencyService.convert(account.getBalance(), account.getCurrencyCode(), group.getDefaultCurrencyCode())); } vo.setBalance(balance); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/asset_account/AssetAccountVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.asset_account; import com.jiukuaitech.bookkeeping.user.account.AccountVOForExtend; import lombok.Getter; import lombok.Setter; @Getter @Setter public class AssetAccountVOForList extends AccountVOForExtend { private Long asOfDate; public static AssetAccountVOForList fromEntity(AssetAccount po) { AssetAccountVOForList vo = new AssetAccountVOForList(); vo.setValue(po); vo.setAsOfDate(po.getAsOfDate()); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/AccountInvalidateException.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_flow; public class AccountInvalidateException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/AmountInvalidateException.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_flow; public class AmountInvalidateException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlow.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_flow; import com.jiukuaitech.bookkeeping.user.account.Account; import com.jiukuaitech.bookkeeping.user.base.HasBookEntity; import com.jiukuaitech.bookkeeping.user.flow_images.FlowImage; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.validation.*; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.NotNull; import java.math.BigDecimal; import java.util.HashSet; import java.util.Set; @Entity @Table(name="t_balance_flow") @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.INTEGER, columnDefinition = "TINYINT(1)") @Getter @Setter public class BalanceFlow extends HasBookEntity { @Column(nullable = false) @NotNull @AmountValidator private BigDecimal amount; // 金额 @AmountValidator private BigDecimal convertedAmount; //汇率换算之后的金额 @ManyToOne(fetch = FetchType.LAZY, optional = false) @NotNull private Account account; @Column(nullable = false) @NotNull @TimeValidator private Long createTime; @Column(nullable = false, columnDefinition = "TINYINT(1)") @NotNull @TransactionStatusValidator private Integer status = 1; // 1正常 2是待确认 3是已退款 @Column(insertable = false, updatable = false) private Integer type; //1支出,2收入,3转账,4余额调整 @Column(length = 16) @DescriptionValidator private String description; //描述 @Column(length = 1024) @NotesValidator private String notes; //备注 @ManyToOne(optional = false, fetch = FetchType.LAZY) @NotNull private User creator; @ManyToOne(optional = false, fetch = FetchType.LAZY) @NotNull private Group group; @OneToMany(mappedBy = "flow", fetch = FetchType.LAZY) private Set images = new HashSet<>(); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_flow; import com.jiukuaitech.bookkeeping.user.validation.DescriptionValidator; import com.jiukuaitech.bookkeeping.user.validation.NotesValidator; import com.jiukuaitech.bookkeeping.user.validation.TimeValidator; import lombok.Getter; import lombok.Setter; import java.time.Instant; @Getter @Setter public class BalanceFlowAddRequest { @TimeValidator private Long createTime; @DescriptionValidator private String description; @NotesValidator private String notes; public void copyPrimitive(BalanceFlow po) { if (createTime == null) { po.setCreateTime(Instant.now().toEpochMilli()); } else { po.setCreateTime(createTime); } po.setDescription(description); po.setNotes(notes); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowController.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_flow; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; import java.util.Set; @RestController @RequestMapping("/flows") public class BalanceFlowController { @Resource private BalanceFlowService balanceFlowService; @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleQuery( @Valid BalanceFlowQueryRequest request, @PageableDefault(sort = {"createTime", "id"}, direction = Sort.Direction.DESC) Pageable page, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(balanceFlowService.queryWithDefaultBook(request, page, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "audit") public BaseResponse handleAudit( @Valid BalanceFlowQueryRequest request, @PageableDefault(sort = {"createTime", "id"}, direction = Sort.Direction.DESC) Pageable page, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(balanceFlowService.query(request, page, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/{id}") public BaseResponse handleGet(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(balanceFlowService.get(id, userSignInId)); } @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") public BaseResponse handleDelete( @PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(balanceFlowService.remove(id, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}/confirm") public BaseResponse handleConfirm( @PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(balanceFlowService.confirm(id, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/{id}/images") public BaseResponse handleImages(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(balanceFlowService.getImages(id, userSignInId)); } @RequestMapping(method = RequestMethod.POST, value = "/{id}/images") public BaseResponse handleUpdateImages( @PathVariable("id") Integer id, @Valid @RequestBody Set images, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(balanceFlowService.updateImages(id, images, userSignInId)); } @RequestMapping(method = RequestMethod.POST, value = "/{id}/image") public BaseResponse handleAddImage( @PathVariable("id") Integer id, @Valid @RequestBody Integer imageId, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(balanceFlowService.addImage(id, imageId, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowExceptionHandler.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_flow; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.ErrorResponse; import org.springframework.context.MessageSource; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.annotation.Resource; import java.util.Locale; @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) public class BalanceFlowExceptionHandler { private static final Locale LANG = Locale.CHINA; @Resource private MessageSource messageSource; @ExceptionHandler(value = AccountInvalidateException.class) @ResponseBody public BaseResponse handleException(AccountInvalidateException e) { return new ErrorResponse(702, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = StatusNotValidateException.class) @ResponseBody public BaseResponse handleException(StatusNotValidateException e) { return new ErrorResponse(703, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = AmountInvalidateException.class) @ResponseBody public BaseResponse handleException(AmountInvalidateException e) { return new ErrorResponse(704, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowQueryRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_flow; import java.util.Set; import lombok.Getter; import lombok.Setter; @Getter @Setter public class BalanceFlowQueryRequest { private Double minAmount; private Double maxAmount; private Long minTime; private Long maxTime; private Integer bookId; private Integer accountId; private Set accounts; private Integer status; private Integer type; private String description; private Integer creatorId; private Set tags; private Set categories; private Set payees; private Set fromAccounts; private Set toAccounts; // 统计支出分类用到 private Integer categoryId; // 按id查询,比如找出退款对应的两条记录 private Set id; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowQueryResultVO.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_flow; import lombok.Getter; import lombok.Setter; import org.springframework.data.domain.Page; import java.math.BigDecimal; @Getter @Setter public class BalanceFlowQueryResultVO { private Page result; private BigDecimal expense; private BigDecimal income; public BigDecimal getSurplus() { return income.subtract(expense); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_flow; import com.jiukuaitech.bookkeeping.user.base.HasBookRepository; import org.springframework.stereotype.Repository; @Repository public interface BalanceFlowRepository extends HasBookRepository { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowService.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_flow; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.expense.Expense; import com.jiukuaitech.bookkeeping.user.flow_images.FlowImageRepository; import com.jiukuaitech.bookkeeping.user.flow_images.FlowImageVOForList; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.income.Income; import com.jiukuaitech.bookkeeping.user.transfer.Transfer; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.user.UserService; import com.jiukuaitech.bookkeeping.user.adjust_balance.AdjustBalanceService; import com.jiukuaitech.bookkeeping.user.deal.DealService; import com.jiukuaitech.bookkeeping.user.deal.DealSpec; import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException; import com.jiukuaitech.bookkeeping.user.expense.Expense_; import com.jiukuaitech.bookkeeping.user.flow_images.FlowImage; import com.jiukuaitech.bookkeeping.user.income.Income_; import com.jiukuaitech.bookkeeping.user.transfer.TransferService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @Service public class BalanceFlowService { @Resource private UserService userService; @Resource private BalanceFlowRepository balanceFlowRepository; @Resource private DealService dealService; @Resource private TransferService transferService; @Resource private AdjustBalanceService adjustBalanceService; @Resource private FlowImageRepository flowImageRepository; public BalanceFlowQueryResultVO queryWithDefaultBook(BalanceFlowQueryRequest request, Pageable page, Integer userSignInId) { User user = userService.getUser(userSignInId); request.setBookId(user.getDefaultBook().getId()); return query(request, page, userSignInId); } public BalanceFlowQueryResultVO query(BalanceFlowQueryRequest request, Pageable page, Integer userSignInId) { BalanceFlowQueryResultVO result = new BalanceFlowQueryResultVO(); User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); Specification specification = BalanceFlowSpec.buildFlowSpecification(request, group); Page poPage = balanceFlowRepository.findAll(specification, page); Page voPage = poPage.map(flow-> { BalanceFlowVOForList vo = BalanceFlowVOForList.fromEntity(flow); vo.setCurrencyCode(flow.getAccount().getCurrencyCode()); if (flow.getType() == 1 || flow.getType() == 2) { if (flow.getBook().getDefaultCurrencyCode().equals(flow.getAccount().getCurrencyCode())) { vo.setNeedConvert(false); } else { vo.setNeedConvert(true); vo.setToCurrencyCode(flow.getBook().getDefaultCurrencyCode()); } } else if (flow.getType() == 3) { String toCurrencyCode = ((Transfer) flow).getTo().getCurrencyCode(); if (flow.getAccount().getCurrencyCode().equals(toCurrencyCode)) { vo.setNeedConvert(false); } else { vo.setNeedConvert(true); vo.setToCurrencyCode(toCurrencyCode); } } else { vo.setNeedConvert(false); } return vo; }); result.setResult(voPage); Specification specification1 = DealSpec.buildSpecification(request, group); if (request.getType() == null || request.getType() == 1) { //只有支出类型才需要计算总额 result.setExpense(balanceFlowRepository.calcAggregate(specification1, Expense_.convertedAmount, Expense.class)); } else { //不是查询的支出类型,则支出总额肯定为空 result.setExpense(BigDecimal.ZERO); } Specification specification2 = DealSpec.buildSpecification(request, group); if (request.getType() == null || request.getType() == 2) { result.setIncome(balanceFlowRepository.calcAggregate(specification2, Income_.convertedAmount, Income.class)); } else { result.setIncome(BigDecimal.ZERO); } return result; } public BalanceFlowVOForList get(Integer id, Integer userSignInId) { User user = userService.getUser(userSignInId); Book book = user.getDefaultBook(); BalanceFlow flow = balanceFlowRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); BalanceFlowVOForList vo = BalanceFlowVOForList.fromEntity(flow); vo.setCurrencyCode(flow.getAccount().getCurrencyCode()); if (flow.getType() == 1 || flow.getType() == 2) { if (flow.getBook().getDefaultCurrencyCode().equals(flow.getAccount().getCurrencyCode())) { vo.setNeedConvert(false); } else { vo.setNeedConvert(true); vo.setToCurrencyCode(flow.getBook().getDefaultCurrencyCode()); } } else if (flow.getType() == 3) { String toCurrencyCode = ((Transfer) flow).getTo().getCurrencyCode(); if (flow.getAccount().getCurrencyCode().equals(toCurrencyCode)) { vo.setNeedConvert(false); } else { vo.setNeedConvert(true); vo.setToCurrencyCode(toCurrencyCode); } } else { vo.setNeedConvert(false); } return vo; } public boolean remove(Integer id, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); BalanceFlow flow = balanceFlowRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); switch (flow.getType()) { case 1: return dealService.remove(1, id, userSignInId); case 2: return dealService.remove(2, id, userSignInId); case 3: return transferService.remove(id, userSignInId); case 4: adjustBalanceService.remove(id, userSignInId); return true; default: throw new ItemNotFoundException(); } } public boolean confirm(Integer id, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); BalanceFlow flow = balanceFlowRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); switch (flow.getType()) { case 1: return dealService.confirm(1, id, userSignInId); case 2: return dealService.confirm(2, id, userSignInId); case 3: return transferService.confirm(id, userSignInId); default: throw new ItemNotFoundException(); } } public boolean updateImages(Integer id, Set images, Integer userSignInId) { User user = userService.getUser(userSignInId); Book book = user.getDefaultBook(); BalanceFlow po = balanceFlowRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); po.getImages().forEach(i -> { i.setFlow(null); flowImageRepository.save(i); }); images.forEach(i -> { FlowImage image = flowImageRepository.getById(i); image.setFlow(po); //必须加上这个才能关联上 po.getImages().add(image); }); balanceFlowRepository.save(po); return true; } public List getImages(Integer id, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); BalanceFlow po = balanceFlowRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); List images = flowImageRepository.findByFlow(po); return images.stream().map(FlowImageVOForList::fromEntity).collect(Collectors.toList()); } public boolean addImage(Integer id, Integer imageId, Integer userSignInId) { User user = userService.getUser(userSignInId); Book book = user.getDefaultBook(); BalanceFlow po = balanceFlowRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); FlowImage image = flowImageRepository.getById(imageId); image.setFlow(po); flowImageRepository.save(image); return true; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowSpec.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_flow; import com.jiukuaitech.bookkeeping.user.account.Account; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelation; import com.jiukuaitech.bookkeeping.user.deal.Deal; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.payee.Payee; import com.jiukuaitech.bookkeeping.user.tag_relation.TagRelation; import com.jiukuaitech.bookkeeping.user.transfer.Transfer; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.base.BookNameNotesEnableSpec; import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelation_; import com.jiukuaitech.bookkeeping.user.deal.Deal_; import com.jiukuaitech.bookkeeping.user.tag_relation.TagRelation_; import com.jiukuaitech.bookkeeping.user.transaction.Transaction; import com.jiukuaitech.bookkeeping.user.transaction.Transaction_; import com.jiukuaitech.bookkeeping.user.transfer.Transfer_; import com.jiukuaitech.bookkeeping.user.utils.CalendarUtils; import org.springframework.data.jpa.domain.Specification; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import javax.persistence.criteria.*; import java.util.Set; public final class BalanceFlowSpec { public static Specification amountGreaterThanOrEqualTo(Double amount) { return (root, query, criteriaBuilder) -> criteriaBuilder.ge(root.get(BalanceFlow_.AMOUNT), amount); } public static Specification amountLessThanOrEqualTo(Double amount) { return (root, query, criteriaBuilder) -> criteriaBuilder.le(root.get(BalanceFlow_.AMOUNT), amount); } public static Specification createTimeGreaterThanOrEqualTo(Long time) { return (root, query, criteriaBuilder) -> criteriaBuilder.ge(root.get(BalanceFlow_.CREATE_TIME), time); } public static Specification createTimeLessThanOrEqualTo(Long time) { return (root, query, criteriaBuilder) -> criteriaBuilder.le(root.get(BalanceFlow_.CREATE_TIME), time); } public static Specification creatorEqual(User user) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(BalanceFlow_.CREATOR), user); } public static Specification accountIn(Set accounts) { return (root, query, criteriaBuilder) -> { CriteriaBuilder.In in = criteriaBuilder.in(root.get(BalanceFlow_.ACCOUNT)); accounts.forEach(i -> in.value(new Account(i))); return in; }; } public static Specification idIn(Set id) { return (root, query, criteriaBuilder) -> { CriteriaBuilder.In in = criteriaBuilder.in(root.get(BalanceFlow_.ID)); id.forEach(i -> in.value(i)); return in; }; } public static Specification typeEqual(Integer type) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(BalanceFlow_.TYPE), type); } public static Specification isGroup(Group group) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(BalanceFlow_.GROUP), group); } public static Specification statusEqual(Integer status) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(BalanceFlow_.STATUS), status); } // 不是待确认的状态 public static Specification statusConfirmed() { return (root, query, criteriaBuilder) -> criteriaBuilder.notEqual(root.get(BalanceFlow_.STATUS), 2); } public static Specification descriptionEqual(String description) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(BalanceFlow_.DESCRIPTION), description); } public static Specification distinct() { return (root, query, cb) -> { query.distinct(true); // query.groupBy(root.get(BalanceFlow_.id)); return null; }; } private static Specification buildBaseSpecification(BalanceFlowQueryRequest request, Group group) { Specification specification = isGroup(group); if (request.getBookId() != null) { specification = specification.and(BookNameNotesEnableSpec.isBook(new Book(request.getBookId()))); } if (request.getMinAmount() != null) { specification = specification.and(amountGreaterThanOrEqualTo(request.getMinAmount())); } if (request.getMaxAmount() != null) { specification = specification.and(amountLessThanOrEqualTo(request.getMaxAmount())); } if (request.getMinTime() != null) { specification = specification.and(createTimeGreaterThanOrEqualTo(CalendarUtils.getStartOfDay(request.getMinTime()))); } if (request.getMaxTime() != null) { specification = specification.and(createTimeLessThanOrEqualTo(CalendarUtils.getEndOfDay(request.getMaxTime()))); } if (request.getCreatorId() != null) { specification = specification.and(creatorEqual(new User(request.getCreatorId()))); } if (request.getStatus() != null) { specification = specification.and(statusEqual(request.getStatus())); } if (StringUtils.hasText(request.getDescription())) { specification = specification.and(descriptionEqual(request.getDescription())); } if (!CollectionUtils.isEmpty(request.getId())) { specification = specification.and(idIn(request.getId())); } specification = specification.and(distinct()); return specification; } // 给子类调用 public static Specification buildSpecification(BalanceFlowQueryRequest request, Group group) { Specification specification = buildBaseSpecification(request, group); if (!CollectionUtils.isEmpty(request.getAccounts())) { specification = specification.and(accountIn(request.getAccounts())); } return specification; } public static Specification buildFlowSpecification(BalanceFlowQueryRequest request, Group group) { Specification specification = buildBaseSpecification(request, group); if (request.getAccountId() != null) { specification = specification.and((Specification) (root, query, criteriaBuilder) -> { Account account = new Account(request.getAccountId()); Predicate predicate1 = criteriaBuilder.equal(root.get(BalanceFlow_.account), account); // https://stackoverflow.com/questions/34251930/jpa-criteria-api-query-subclass-property // Root transferRoot = criteriaBuilder.treat(root, Transfer.class); @SuppressWarnings("unchecked") Root transferRoot = (Root) (Root) root; Predicate predicate2 = criteriaBuilder.equal(transferRoot.get(Transfer_.to), account); return criteriaBuilder.or(predicate1, predicate2); }); } else if (!CollectionUtils.isEmpty(request.getAccounts())) { specification = specification.and((Specification) (root, query, criteriaBuilder) -> { CriteriaBuilder.In in1 = criteriaBuilder.in(root.get(BalanceFlow_.account)); request.getAccounts().forEach(i -> in1.value(new Account(i))); @SuppressWarnings("unchecked") Root transferRoot = (Root) (Root) root; CriteriaBuilder.In in2 = criteriaBuilder.in(transferRoot.get(Transfer_.to)); request.getAccounts().forEach(i -> in2.value(new Account(i))); return criteriaBuilder.or(in1, in2); }); } if (!CollectionUtils.isEmpty(request.getPayees())) { specification = specification.and((Specification) (root, query, criteriaBuilder) -> { // Root dealRoot = criteriaBuilder.treat(root, Deal.class); Root dealRoot = (Root) (Root) root; CriteriaBuilder.In in = criteriaBuilder.in(dealRoot.get(Deal_.payee)); request.getPayees().forEach(i -> in.value(new Payee(i))); return in; }); } if (!CollectionUtils.isEmpty(request.getCategories())) { specification = specification.and((Specification) (root, query, criteriaBuilder) -> { Root dealRoot = (Root) (Root) root; Join join = dealRoot.join(Deal_.categories); return join.get(CategoryRelation_.category).in(request.getCategories()); }); } if (!CollectionUtils.isEmpty(request.getTags())) { specification = specification.and((Specification) (root, query, criteriaBuilder) -> { Root transactionRoot = criteriaBuilder.treat(root, Transaction.class); Join join = transactionRoot.join(Transaction_.tags); return join.get(TagRelation_.tag).in(request.getTags()); }); } if (request.getType() != null) { specification = specification.and(typeEqual(request.getType())); } return specification; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowUpdateRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_flow; import com.jiukuaitech.bookkeeping.user.validation.DescriptionValidator; import com.jiukuaitech.bookkeeping.user.validation.NotesValidator; import com.jiukuaitech.bookkeeping.user.validation.TimeValidator; import lombok.Getter; import lombok.Setter; @Getter @Setter public class BalanceFlowUpdateRequest { @TimeValidator private Long createTime; @DescriptionValidator private String description; @NotesValidator private String notes; public void updatePrimitive(BalanceFlow po) { if (createTime != null) po.setCreateTime(createTime); if (description != null) po.setDescription(description); if (notes != null) po.setNotes(notes); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowVOForExtend.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_flow; import com.jiukuaitech.bookkeeping.user.account.AccountVOForExtend; import com.jiukuaitech.bookkeeping.user.utils.EnumUtils; import com.jiukuaitech.bookkeeping.user.response.HasNameVO; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @Getter @Setter public class BalanceFlowVOForExtend { private Integer id; private HasNameVO book; private BigDecimal amount; private BigDecimal convertedAmount; private String currencyCode; private Boolean needConvert; private String toCurrencyCode; private AccountVOForExtend account; private String accountName; private Long createTime; private String description; private String notes; private Integer status; public void setValue(BalanceFlow po) { setId(po.getId()); setBook(new HasNameVO(po.getBook().getId(), po.getBook().getName())); setAmount(po.getAmount()); setConvertedAmount(po.getConvertedAmount()); if (po.getAccount() != null) setAccount(AccountVOForExtend.fromEntity(po.getAccount())); setAccountName(getAccount() == null ? null : getAccount().getName()); setCreateTime(po.getCreateTime()); setDescription(po.getDescription()); setNotes(po.getNotes()); setStatus(po.getStatus()); } public static BalanceFlowVOForExtend fromEntity(BalanceFlow balanceFlow) { BalanceFlowVOForExtend result = new BalanceFlowVOForExtend(); result.setValue(balanceFlow); return result; } public String getStatusName() { return EnumUtils.translateFlowStatus(getStatus()); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_flow; import com.jiukuaitech.bookkeeping.user.adjust_balance.AdjustBalance; import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelationVOForList; import com.jiukuaitech.bookkeeping.user.deal.DealVOForList; import com.jiukuaitech.bookkeeping.user.expense.Expense; import com.jiukuaitech.bookkeeping.user.income.Income; import com.jiukuaitech.bookkeeping.user.tag_relation.TagRelationVOForList; import com.jiukuaitech.bookkeeping.user.transfer.Transfer; import com.jiukuaitech.bookkeeping.user.utils.EnumUtils; import com.jiukuaitech.bookkeeping.user.adjust_balance.AdjustBalanceVOForList; import com.jiukuaitech.bookkeeping.user.response.HasNameVO; import com.jiukuaitech.bookkeeping.user.transfer.TransferVOForList; import lombok.Getter; import lombok.Setter; import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.util.*; import java.util.stream.Collectors; @Getter @Setter public class BalanceFlowVOForList { private Integer id; private Integer type; // 1是expense,2是income,3是transfer,4是调整余额 private DealVOForList expense; private DealVOForList income; private TransferVOForList transfer; private AdjustBalanceVOForList adjustBalance; private BigDecimal amount; private BigDecimal convertedAmount; private String currencyCode; private Boolean needConvert; private String toCurrencyCode; private Long createTime; private Integer status; private String description; private Integer accountId; private Integer toId; private String accountName; private String fromAccountName; private String toAccountName; private String categoryName; private Set categories = new HashSet<>(); private String bookName; private Set tags = new HashSet<>(); private HasNameVO payee; private String notes; public static BalanceFlowVOForList fromEntity(BalanceFlow po) { if (po == null) return null; BalanceFlowVOForList vo = new BalanceFlowVOForList(); vo.setId(po.getId()); vo.setBookName(po.getBook().getName()); vo.setType(po.getType()); vo.setStatus(po.getStatus()); vo.setDescription(po.getDescription()); vo.setAmount(po.getAmount()); vo.setConvertedAmount(po.getConvertedAmount()); vo.setCreateTime(po.getCreateTime()); vo.setNotes(po.getNotes()); if (po instanceof Expense) { vo.setExpense(DealVOForList.fromEntity((Expense) po)); vo.setAccountId(vo.getExpense().getAccount().getId()); vo.setAccountName(vo.getExpense().getAccountName()); vo.setCategoryName(vo.getExpense().getCategoryName()); vo.setCategories(vo.getExpense().getCategories()); vo.setTags(vo.getExpense().getTags()); vo.setPayee(vo.getExpense().getPayee()); } else if (po instanceof Income) { vo.setIncome(DealVOForList.fromEntity((Income) po)); vo.setAccountId(vo.getIncome().getAccount().getId()); vo.setAccountName(vo.getIncome().getAccountName()); vo.setCategoryName(vo.getIncome().getCategoryName()); vo.setCategories(vo.getIncome().getCategories()); vo.setTags(vo.getIncome().getTags()); vo.setPayee(vo.getIncome().getPayee()); } else if (po instanceof Transfer) { vo.setTransfer(TransferVOForList.fromEntity((Transfer) po)); vo.setAccountId(vo.getTransfer().getFromId()); vo.setToId(vo.getTransfer().getToId()); vo.setAccountName(vo.getTransfer().getAccountName()); vo.setFromAccountName(vo.getTransfer().getFrom().getName()); vo.setToAccountName(vo.getTransfer().getTo().getName()); vo.setTags(vo.getTransfer().getTags()); } else if (po instanceof AdjustBalance) { vo.setAdjustBalance(AdjustBalanceVOForList.fromEntity((AdjustBalance) po)); vo.setAccountName(vo.getAdjustBalance().getAccountName()); } return vo; } public String getCreateTimeFormatted() { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); return simpleDateFormat.format(new Date(createTime)); } private String getCreateDateFormatted() { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); return simpleDateFormat.format(new Date(createTime)); } public String getAmountFormatted() { return String.format("%.2f", amount); } public String getTitle() { StringBuilder result = new StringBuilder(); if (StringUtils.hasText(description)) { result.append(description); } else { if (type == 1) { result.append(expense.getCategories().stream().map(CategoryRelationVOForList::getCategoryName).collect(Collectors.joining(", "))); } else if (type == 2) { result.append(income.getCategories().stream().map(CategoryRelationVOForList::getCategoryName).collect(Collectors.joining(", "))); } else if (type == 3) { result.append(accountName); } else { result.append("调整余额"); } } if (getPayee() != null) { result.append(" - ").append(getPayee().getName()); } return result.toString(); } public String getSubTitle() { return getTypeName() + " " + getCreateDateFormatted() + " " + getTagsName(); } public String getTagsName() { if (type == 1) { return expense.getTags().stream().map(TagRelationVOForList::getTagName).collect(Collectors.joining(", ")); } if (type == 2) { return income.getTags().stream().map(TagRelationVOForList::getTagName).collect(Collectors.joining(", ")); } if (type == 3) { return transfer.getTags().stream().map(TagRelationVOForList::getTagName).collect(Collectors.joining(", ")); } return ""; } public String getTypeName() { return EnumUtils.translateFlowType(type); } public String getStatusName() { return EnumUtils.translateFlowStatus(status); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/StatusNotValidateException.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_flow; public class StatusNotValidateException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_log/BalanceLog.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_log; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.validation.BalanceValidator; import com.jiukuaitech.bookkeeping.user.validation.TimeValidator; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.NotNull; import java.math.BigDecimal; @Entity @Table(name="t_balance_log") @Getter @Setter public class BalanceLog extends BaseEntity { @ManyToOne(optional = false, fetch = FetchType.LAZY) @JoinColumn(name = "group_id") @NotNull private Group group; // 账簿必须属于某个组 @ManyToOne(optional = false, fetch = FetchType.LAZY) @NotNull private User creator; @Column(nullable = false) //最多9亿 @NotNull @BalanceValidator private BigDecimal asset; @Column(nullable = false) //最多9亿 @NotNull @BalanceValidator private BigDecimal debt; @Column(nullable = false) @NotNull @TimeValidator private Long createTime; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_log/BalanceLogAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_log; import com.jiukuaitech.bookkeeping.user.validation.BalanceValidator; import com.jiukuaitech.bookkeeping.user.validation.TimeValidator; import lombok.Getter; import lombok.Setter; import javax.persistence.Column; import javax.validation.constraints.NotNull; import java.math.BigDecimal; @Getter @Setter public class BalanceLogAddRequest { @NotNull @BalanceValidator private BigDecimal asset; @NotNull @BalanceValidator private BigDecimal debt; @Column(nullable = false) @NotNull @TimeValidator private Long createTime; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_log/BalanceLogController.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_log; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/logs") public class BalanceLogController { @Resource private BalanceLogService balanceLogService; @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleQuery( @PageableDefault(sort = "createTime", direction = Sort.Direction.DESC) Pageable page, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(balanceLogService.query(page, userSignInId)); } @RequestMapping(method = RequestMethod.POST, value = "") public BaseResponse handleAdd( @Valid @RequestBody BalanceLogAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(balanceLogService.add(request, userSignInId)); } @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") public BaseResponse handleDelete(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(balanceLogService.remove(id, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_log/BalanceLogRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_log; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import com.jiukuaitech.bookkeeping.user.group.Group; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Repository public interface BalanceLogRepository extends BaseRepository { Optional findOneByGroupAndId(Group group, Integer integer); List findByGroupAndCreateTimeBetweenOrderByCreateTime(Group group, Long min, Long max); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_log/BalanceLogService.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_log; import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.user.UserService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import javax.annotation.Resource; import javax.persistence.criteria.Predicate; import java.util.ArrayList; import java.util.List; @Service public class BalanceLogService { @Resource private BalanceLogRepository balanceLogRepository; @Resource private UserService userService; public Page query(Pageable page, Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); Specification specification = (root, query, criteriaBuilder) -> { List predicates = new ArrayList<>(); predicates.add(criteriaBuilder.equal(root.get(BalanceLog_.group), group)); return criteriaBuilder.and(predicates.toArray(new Predicate[0])); }; Page poPage = balanceLogRepository.findAll(specification, page); return poPage.map(BalanceLogVOForList::fromEntity); } public BalanceLogVOForList add(BalanceLogAddRequest request, Integer userSignInId) { User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); BalanceLog po = new BalanceLog(); po.setGroup(group); po.setCreator(user); po.setAsset(request.getAsset()); po.setDebt(request.getDebt()); po.setCreateTime(request.getCreateTime()); return BalanceLogVOForList.fromEntity(balanceLogRepository.save(po)); } public BalanceLogVOForList remove(Integer id, Integer userSignInId) { User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); BalanceLog po = balanceLogRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new); balanceLogRepository.delete(po); return BalanceLogVOForList.fromEntity(po); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_log/BalanceLogVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.balance_log; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @Getter @Setter public class BalanceLogVOForList { private Integer id; private BigDecimal asset; private BigDecimal debt; private Long createTime; public static BalanceLogVOForList fromEntity(BalanceLog po) { BalanceLogVOForList vo = new BalanceLogVOForList(); vo.setId(po.getId()); vo.setAsset(po.getAsset()); vo.setDebt(po.getDebt()); vo.setCreateTime(po.getCreateTime()); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/BaseController.java ================================================ package com.jiukuaitech.bookkeeping.user.base; import lombok.Getter; import lombok.Setter; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Getter @Setter public class BaseController { @Resource private HttpServletRequest request; @Resource private HttpServletResponse response; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/BaseEntity.java ================================================ package com.jiukuaitech.bookkeeping.user.base; import lombok.Getter; import lombok.Setter; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.MappedSuperclass; @MappedSuperclass @Getter @Setter public abstract class BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; // 不要用id重写hashcode和equals,之前的tag更新出了Bug } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/BaseRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.base; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.repository.NoRepositoryBean; import javax.persistence.metamodel.SingularAttribute; import java.io.Serializable; import java.math.BigDecimal; import java.util.List; /* https://www.baeldung.com/spring-data-jpa-method-in-all-repositories https://github.com/pkainulainen/spring-data-jpa-examples/blob/master/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/BaseRepository.java */ @NoRepositoryBean public interface BaseRepository extends JpaRepository, JpaSpecificationExecutor { // P extends Number change to BigDecimal BigDecimal calcAggregate(Specification spec, SingularAttribute column, Class clazz); List calcAggregate(Specification spec, List> columns, Class clazz); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/BaseRepositoryFactoryBean.java ================================================ package com.jiukuaitech.bookkeeping.user.base; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import javax.persistence.EntityManager; import java.io.Serializable; public class BaseRepositoryFactoryBean, T, I extends Serializable> extends JpaRepositoryFactoryBean { public BaseRepositoryFactoryBean(Class repositoryInterface) { super(repositoryInterface); } @SuppressWarnings("rawtypes") protected RepositoryFactorySupport createRepositoryFactory(EntityManager em) { return new CustomRepositoryFactory(em); } private static class CustomRepositoryFactory extends JpaRepositoryFactory { private final EntityManager em; public CustomRepositoryFactory(EntityManager em) { super(em); this.em = em; } @SuppressWarnings("unchecked") protected Object getTargetRepository(RepositoryMetadata metadata) { return new BaseRepositoryImpl( (Class) metadata.getDomainType(), em); } protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { return BaseRepositoryImpl.class; } } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/BaseRepositoryImpl.java ================================================ package com.jiukuaitech.bookkeeping.user.base; import com.jiukuaitech.bookkeeping.user.utils.CommonUtils; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.support.JpaEntityInformation; import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Root; import javax.persistence.criteria.Selection; import javax.persistence.metamodel.SingularAttribute; import java.io.Serializable; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /* https://stackoverflow.com/questions/38934549/spring-specification-with-sum-function */ public class BaseRepositoryImpl extends SimpleJpaRepository implements BaseRepository { private final EntityManager entityManager; public BaseRepositoryImpl(Class domainClass, EntityManager entityManager) { super(domainClass, entityManager); this.entityManager = entityManager; } public BaseRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager) { super(entityInformation, entityManager); this.entityManager = entityManager; } @Override public BigDecimal calcAggregate(Specification spec, SingularAttribute column, Class clazz) { CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); CriteriaQuery query = criteriaBuilder.createQuery(column.getJavaType()); Root root = query.from(clazz); if (spec != null) { query.where(spec.toPredicate(root, query, criteriaBuilder)); } query.select(criteriaBuilder.sum(root.get(column.getName()))); TypedQuery typedQuery = entityManager.createQuery(query); BigDecimal result = typedQuery.getSingleResult(); if (result == null) return BigDecimal.valueOf(0); return result; } @Override public List calcAggregate(Specification spec, List> columns, Class clazz) { CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); CriteriaQuery query = criteriaBuilder.createQuery(BigDecimal[].class); Root root = query.from(clazz); if (spec != null) { query.where(spec.toPredicate(root, query, criteriaBuilder)); } List> selectionList = new ArrayList<>(); columns.forEach(i -> selectionList.add(criteriaBuilder.sum(root.get(i.getName())))); query.multiselect(selectionList); Object[] result = entityManager.createQuery(query).getSingleResult(); return CommonUtils.coalesce(Arrays.copyOf(result, result.length, BigDecimal[].class)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/BookNameNotesEnableEntity.java ================================================ package com.jiukuaitech.bookkeeping.user.base; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.NotesValidator; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.NotNull; @MappedSuperclass @Getter @Setter public abstract class BookNameNotesEnableEntity extends HasBookEntity { @Column(length = 16, nullable = false) @NotNull @NameValidator private String name; @Column(length = 128) @NotesValidator private String notes; @Column(nullable = false) @NotNull private Boolean enable = true; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/BookNameNotesEnableSpec.java ================================================ package com.jiukuaitech.bookkeeping.user.base; import com.jiukuaitech.bookkeeping.user.book.Book; import org.springframework.data.jpa.domain.Specification; import javax.persistence.criteria.CriteriaBuilder; import java.util.Set; public final class BookNameNotesEnableSpec { public static Specification isBook(Book book) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(BookNameNotesEnableEntity_.BOOK), book); } public static Specification inBooks(Set books) { return (root, query, criteriaBuilder) -> { CriteriaBuilder.In in = criteriaBuilder.in(root.get(BookNameNotesEnableEntity_.BOOK)); books.forEach(i -> in.value(i)); return in; }; } public static Specification enable() { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(BookNameNotesEnableEntity_.ENABLE), true); } public static Specification isEnable(Boolean enable) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(BookNameNotesEnableEntity_.ENABLE), enable); } public static Specification nameLike(String name) { return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get(BookNameNotesEnableEntity_.NAME), "%"+name+"%"); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/HasBookEntity.java ================================================ package com.jiukuaitech.bookkeeping.user.base; import com.jiukuaitech.bookkeeping.user.book.Book; import lombok.Getter; import lombok.Setter; import javax.persistence.FetchType; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.MappedSuperclass; import javax.validation.constraints.NotNull; @MappedSuperclass @Getter @Setter public abstract class HasBookEntity extends BaseEntity { @ManyToOne(optional = false, fetch = FetchType.LAZY) @JoinColumn(name = "book_id") @NotNull private Book book; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/HasBookRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.base; import com.jiukuaitech.bookkeeping.user.book.Book; import org.springframework.data.repository.NoRepositoryBean; import java.util.Optional; @NoRepositoryBean public interface HasBookRepository extends BaseRepository { Optional findOneByBookAndId(Book book, Integer id); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/JpaDataConfig.java ================================================ package com.jiukuaitech.bookkeeping.user.base; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.web.config.EnableSpringDataWebSupport; @Configuration @EnableJpaRepositories(basePackages = "com.jiukuaitech.bookkeeping.user", repositoryFactoryBeanClass = BaseRepositoryFactoryBean.class) @EnableSpringDataWebSupport public class JpaDataConfig { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/NameNotesEnableEntity.java ================================================ package com.jiukuaitech.bookkeeping.user.base; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.NotesValidator; import lombok.Getter; import lombok.Setter; import javax.persistence.Column; import javax.persistence.MappedSuperclass; import javax.validation.constraints.NotNull; @MappedSuperclass @Getter @Setter public abstract class NameNotesEnableEntity extends BaseEntity { @Column(length = 16, nullable = false) @NotNull @NameValidator private String name; @Column(length = 1024) @NotesValidator private String notes; @Column(nullable = false) @NotNull private Boolean enable = true; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/TestController.java ================================================ package com.jiukuaitech.bookkeeping.user.base; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import javax.servlet.http.HttpServletRequest; @RestController public class TestController { @RequestMapping(method = RequestMethod.GET, value = "/test1") public BaseResponse handleTest1() { return new DataResponse<>(19); } @GetMapping("/test2") public BaseResponse getBaseUrl(HttpServletRequest request) { String baseUrl = ServletUriComponentsBuilder .fromRequestUri(request) .replacePath(null) .build() .toUriString(); return new DataResponse<>(baseUrl); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/Book.java ================================================ package com.jiukuaitech.bookkeeping.user.book; import com.jiukuaitech.bookkeeping.user.expense_category.ExpenseCategory; import com.jiukuaitech.bookkeeping.user.income_category.IncomeCategory; import com.jiukuaitech.bookkeeping.user.account.Account; import com.jiukuaitech.bookkeeping.user.base.NameNotesEnableEntity; import com.jiukuaitech.bookkeeping.user.group.Group; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.NotNull; @Entity @Table(name="t_book", uniqueConstraints = {@UniqueConstraint(columnNames = {"group_id", "name"})}) @Getter @Setter /** * 账簿类,它属于某个组管理。下面会有很多账户。 * group+name决定一个账簿。 */ public class Book extends NameNotesEnableEntity { @ManyToOne(optional = false, fetch = FetchType.LAZY) @JoinColumn(name = "group_id") @NotNull private Group group; // 账簿必须属于某个组 @OneToOne private Account defaultExpenseAccount; @OneToOne private Account defaultIncomeAccount; @OneToOne private Account defaultTransferFromAccount; @OneToOne private Account defaultTransferToAccount; @OneToOne private ExpenseCategory defaultExpenseCategory; @OneToOne private IncomeCategory defaultIncomeCategory; @Column(nullable = false) @NotNull private Boolean descriptionEnable = true; @Column(nullable = false) @NotNull private Boolean timeEnable = false; @Column(nullable = false) @NotNull private Boolean imageEnable = false; @Column(nullable = false, length = 8) @NotNull private String defaultCurrencyCode;//默认的币种 public Book() { } public Book(Integer id) { super.setId(id); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/BookAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.book; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.NotesValidator; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotBlank; @Getter @Setter public class BookAddRequest { @NotBlank @NameValidator private String name; @NotesValidator private String notes; private Boolean descriptionEnable = true; private Boolean timeEnable = false; private Boolean imageEnable = false; @NotBlank private String defaultCurrencyCode; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/BookController.java ================================================ package com.jiukuaitech.bookkeeping.user.book; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/books") public class BookController extends BaseController { @Resource private BookService bookService; /* @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleGetAll(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(bookService.getAll(userSignInId)); } */ @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleQuery( @PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable page, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(bookService.query(page, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/{id}") public BaseResponse handleGet(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(bookService.get(id, userSignInId)); } @RequestMapping(method = RequestMethod.POST, value = "") public BaseResponse handleAdd(@Valid @RequestBody BookAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(bookService.add(request, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}") public BaseResponse handleUpdate( @PathVariable("id") Integer id, @Valid @RequestBody BookUpdateRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(bookService.update(id, request, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/config") public BaseResponse handleConfig( @Valid @RequestBody BookUpdateRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(bookService.config(request, userSignInId)); } @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") public BaseResponse handleDelete(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(bookService.remove(id, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggle") public BaseResponse handleToggle(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(bookService.toggle(id, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/BookExceptionHandler.java ================================================ package com.jiukuaitech.bookkeeping.user.book; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.ErrorResponse; import org.springframework.context.MessageSource; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.annotation.Resource; import java.util.Locale; @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) public class BookExceptionHandler { private static final Locale LANG = Locale.CHINA; @Resource private MessageSource messageSource; @ExceptionHandler(value = BookMaxCountException.class) @ResponseBody public BaseResponse handleException(BookMaxCountException e) { return new ErrorResponse(602, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/BookMaxCountException.java ================================================ package com.jiukuaitech.bookkeeping.user.book; public class BookMaxCountException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/BookRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.book; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import com.jiukuaitech.bookkeeping.user.group.Group; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository public interface BookRepository extends BaseRepository { Integer countByGroup_id(Integer groupId); Optional findOneByGroupAndId(Group group, Integer id); Optional findOneByGroupAndName(Group group, String name); long deleteByGroup_id(Integer groupId); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/BookService.java ================================================ package com.jiukuaitech.bookkeeping.user.book; import java.util.ArrayList; import java.util.List; import com.jiukuaitech.bookkeeping.user.currency.CurrencyService; import com.jiukuaitech.bookkeeping.user.expense_category.ExpenseCategory; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.group.GroupRepository; import com.jiukuaitech.bookkeeping.user.income_category.IncomeCategory; import com.jiukuaitech.bookkeeping.user.user.*; import com.jiukuaitech.bookkeeping.user.account.AccountRepository; import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException; import com.jiukuaitech.bookkeeping.user.exception.NameExistsException; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.annotation.Resource; import javax.persistence.criteria.Predicate; @Service public class BookService { @Resource private UserRepository userRepository; @Resource private UserService userService; @Resource private GroupRepository groupRepository; @Resource private BookRepository bookRepository; @Resource private AccountRepository accountRepository; @Resource private CurrencyService currencyService; @Value("${book.max.count}") private Integer maxCount; public Page query(Pageable page, Integer userSignInId) { Specification specification = (root, query, criteriaBuilder) -> { List predicates = new ArrayList<>(); User user = userService.getUser(userSignInId); predicates.add(criteriaBuilder.equal(root.get(Book_.group), user.getDefaultGroup())); return criteriaBuilder.and(predicates.toArray(new Predicate[0])); }; Page poPage = bookRepository.findAll(specification, page); return poPage.map(BookVOForList::fromEntity); } public BookVOForList get(Integer id, Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); Book po = bookRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new); return BookVOForList.fromEntity(po); } public boolean add(BookAddRequest request, Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); if (bookRepository.findOneByGroupAndName(group, request.getName()).isPresent()) { throw new NameExistsException(); } if (bookRepository.countByGroup_id(group.getId()) >= maxCount) { throw new BookMaxCountException(); } currencyService.checkCode(request.getDefaultCurrencyCode()); Book po = new Book(); po.setName(request.getName()); po.setDefaultCurrencyCode(request.getDefaultCurrencyCode()); po.setNotes(request.getNotes()); po.setGroup(group); po.setDescriptionEnable(request.getDescriptionEnable()); po.setTimeEnable(request.getTimeEnable()); po.setImageEnable(request.getImageEnable()); bookRepository.save(po); return true; } public BookVOForList update(Integer id, BookUpdateRequest request, Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); Book po = bookRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new); if (StringUtils.hasText(request.getName())) { if (!po.getName().equals(request.getName())) { if (bookRepository.findOneByGroupAndName(po.getGroup(), request.getName()).isPresent()) { throw new NameExistsException(); } } } if (StringUtils.hasText(request.getName())) po.setName(request.getName()); if (request.getNotes() != null) po.setNotes(request.getNotes()); bookRepository.save(po); return BookVOForList.fromEntity(po); } public BookVOForList config(BookUpdateRequest request, Integer userSignInId) { User user = userService.getUser(userSignInId); Book po = user.getDefaultBook(); // 不检查默认的账户和分类是不是该账本下的了,后果就是无法记账默认值而已。 if (request.getDefaultExpenseAccountId() != null) { po.setDefaultExpenseAccount(accountRepository.getById(request.getDefaultExpenseAccountId())); } else { po.setDefaultExpenseAccount(null); } if (request.getDefaultIncomeAccountId() != null) { po.setDefaultIncomeAccount(accountRepository.getById(request.getDefaultIncomeAccountId())); } else { po.setDefaultIncomeAccount(null); } if (request.getDefaultTransferFromAccountId() != null) { po.setDefaultTransferFromAccount(accountRepository.getById(request.getDefaultTransferFromAccountId())); } else { po.setDefaultTransferFromAccount(null); } if (request.getDefaultTransferToAccountId() != null) { po.setDefaultTransferToAccount(accountRepository.getById(request.getDefaultTransferToAccountId())); } else { po.setDefaultTransferToAccount(null); } if (request.getDefaultExpenseCategoryId() != null) { po.setDefaultExpenseCategory(new ExpenseCategory(request.getDefaultExpenseCategoryId())); } else { po.setDefaultExpenseCategory(null); } if (request.getDefaultIncomeCategoryId() != null) { po.setDefaultIncomeCategory(new IncomeCategory(request.getDefaultIncomeCategoryId())); } else { po.setDefaultIncomeCategory(null); } if (request.getDescriptionEnable() != null) po.setDescriptionEnable(request.getDescriptionEnable()); if (request.getTimeEnable() != null) po.setTimeEnable(request.getTimeEnable()); if (request.getImageEnable() != null) po.setImageEnable(request.getImageEnable()); bookRepository.save(po); return BookVOForList.fromEntity(po); } public boolean remove(Integer id, Integer userSignInId) { User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); Book po = bookRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new); // UserGroupRelation userGroupRelation = userGroupRelationRepository.findOneByUserAndGroup(user, group); // if (userGroupRelation == null || userGroupRelation.getRole() != 1) { // throw new PermissionException("No Permission"); // } if (user.getDefaultBook().getId().equals(po.getId())) { user.setDefaultBook(null); userRepository.save(user); } if (group.getDefaultBook().getId().equals(po.getId())) { group.setDefaultBook(null); groupRepository.save(group); } bookRepository.delete(po); return true; } public boolean toggle(Integer id, Integer userSignInId) { User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); Book po = bookRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new); if (user.getDefaultBook().equals(po)) { throw new ItemNotFoundException(); } po.setEnable(!po.getEnable()); bookRepository.save(po); return true; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/BookUpdateRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.book; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.NotesValidator; import lombok.Getter; import lombok.Setter; @Getter @Setter public class BookUpdateRequest { @NameValidator private String name; @NotesValidator private String notes; private Integer defaultExpenseAccountId; private Integer defaultIncomeAccountId; private Integer defaultTransferFromAccountId; private Integer defaultTransferToAccountId; private Integer defaultExpenseCategoryId; private Integer defaultIncomeCategoryId; private Boolean descriptionEnable; private Boolean timeEnable; private Boolean imageEnable; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/BookVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.book; import com.jiukuaitech.bookkeeping.user.account.AccountVOForExtend; import com.jiukuaitech.bookkeeping.user.response.HasNameVO; import lombok.Getter; import lombok.Setter; @Getter @Setter public class BookVOForList { private Integer id; private String name; private String notes; private HasNameVO group; private AccountVOForExtend defaultExpenseAccount; private AccountVOForExtend defaultIncomeAccount; private AccountVOForExtend defaultTransferFromAccount; private AccountVOForExtend defaultTransferToAccount; private HasNameVO defaultExpenseCategory; private HasNameVO defaultIncomeCategory; private Boolean descriptionEnable; private Boolean timeEnable; private Boolean imageEnable; private Boolean enable; private String defaultCurrencyCode; public static BookVOForList fromEntity(Book po) { BookVOForList vo = new BookVOForList(); vo.setId(po.getId()); vo.setName(po.getName()); vo.setDefaultCurrencyCode(po.getDefaultCurrencyCode()); vo.setNotes(po.getNotes()); vo.setGroup(new HasNameVO(po.getGroup().getId(), po.getGroup().getName())); vo.setDescriptionEnable(po.getDescriptionEnable()); vo.setTimeEnable(po.getTimeEnable()); vo.setImageEnable(po.getImageEnable()); vo.setEnable(po.getEnable()); if (po.getDefaultExpenseAccount() != null) { vo.setDefaultExpenseAccount(AccountVOForExtend.fromEntity(po.getDefaultExpenseAccount())); } if (po.getDefaultIncomeAccount() != null) { vo.setDefaultIncomeAccount(AccountVOForExtend.fromEntity(po.getDefaultIncomeAccount())); } if (po.getDefaultTransferFromAccount() != null) { vo.setDefaultTransferFromAccount(AccountVOForExtend.fromEntity(po.getDefaultTransferFromAccount())); } if (po.getDefaultTransferToAccount() != null) { vo.setDefaultTransferToAccount(AccountVOForExtend.fromEntity(po.getDefaultTransferToAccount())); } if (po.getDefaultExpenseCategory() != null) { vo.setDefaultExpenseCategory(new HasNameVO(po.getDefaultExpenseCategory().getId(), po.getDefaultExpenseCategory().getName())); } if (po.getDefaultIncomeCategory() != null) { vo.setDefaultIncomeCategory(new HasNameVO(po.getDefaultIncomeCategory().getId(), po.getDefaultIncomeCategory().getName())); } return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/Category.java ================================================ package com.jiukuaitech.bookkeeping.user.category; import com.jiukuaitech.bookkeeping.user.base.BookNameNotesEnableEntity; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.springframework.util.CollectionUtils; import javax.persistence.*; import javax.validation.constraints.NotNull; import java.util.ArrayList; import java.util.List; @Entity @Table(name="t_category", uniqueConstraints = {@UniqueConstraint(columnNames = {"parent_id", "name"})}) @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.INTEGER, columnDefinition = "TINYINT(1)") @Getter @Setter @NoArgsConstructor public class Category extends BookNameNotesEnableEntity { @Column(insertable = false, updatable = false) private Integer type; //1支出分类,2收入分类 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") private Category parent; @Column(nullable = false) @NotNull private Integer level; @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private List children = new ArrayList<>(); public Category(Integer id) { super.setId(id); } public List getChildren(List categories) { List result = new ArrayList<>(); for (Category item : categories) { if (item.getParent() != null && this.getId().equals(item.getParent().getId())) { result.add(item); } } return result; } public List getOffspring(List categories) { List result = new ArrayList<>(); List children = this.getChildren(categories); if (!CollectionUtils.isEmpty(children)) { result.addAll(children); for (Category item : children) { result.addAll(item.getOffspring(categories)); } } return result; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.category; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.NotesValidator; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotBlank; @Getter @Setter public class CategoryAddRequest { @NotBlank @NameValidator private String name; @NotesValidator private String notes; private Integer parentId; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryController.java ================================================ package com.jiukuaitech.bookkeeping.user.category; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; @RestController @RequestMapping("/categories") public class CategoryController extends BaseController { @Resource private CategoryService categoryService; @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") public BaseResponse handleDelete(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(categoryService.remove(id, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggle") public BaseResponse handleToggle(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(categoryService.toggle(id, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/{id}") public BaseResponse handleGet(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(categoryService.get(id, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryExceptionHandler.java ================================================ package com.jiukuaitech.bookkeeping.user.category; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.ErrorResponse; import org.springframework.context.MessageSource; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.annotation.Resource; import java.util.Locale; @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) public class CategoryExceptionHandler { private static final Locale LANG = Locale.CHINA; @Resource private MessageSource messageSource; @ExceptionHandler(value = ParentCategoryNotEnableException.class) @ResponseBody public BaseResponse handleException(ParentCategoryNotEnableException e) { return new ErrorResponse(607, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = CategoryNameExistsException.class) @ResponseStatus(HttpStatus.CONFLICT) public BaseResponse handleException(CategoryNameExistsException e) { return new ErrorResponse(409, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = CategoryHasDealException.class) @ResponseBody public BaseResponse handleException(CategoryHasDealException e) { return new ErrorResponse(410, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = CategoryLevelException.class) @ResponseBody public BaseResponse handleException(CategoryLevelException e) { return new ErrorResponse(703, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = CategoryIsDefaultExpenseException.class) @ResponseBody public BaseResponse handleException(CategoryIsDefaultExpenseException e) { return new ErrorResponse(704, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = CategoryIsDefaultIncomeException.class) @ResponseBody public BaseResponse handleException(CategoryIsDefaultIncomeException e) { return new ErrorResponse(705, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = CategoryMaxCountException.class) @ResponseBody public BaseResponse handleException(CategoryMaxCountException e) { return new ErrorResponse(706, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryHasDealException.java ================================================ package com.jiukuaitech.bookkeeping.user.category; /* 删除有账单的分类 */ public class CategoryHasDealException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryIsDefaultExpenseException.java ================================================ package com.jiukuaitech.bookkeeping.user.category; /* CategoryIsDefaultExpenseException */ public class CategoryIsDefaultExpenseException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryIsDefaultIncomeException.java ================================================ package com.jiukuaitech.bookkeeping.user.category; /* 账本的默认收入类别不能删除 */ public class CategoryIsDefaultIncomeException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryLevelException.java ================================================ package com.jiukuaitech.bookkeeping.user.category; /* 添加支出时,层级超过 */ public class CategoryLevelException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryMaxCountException.java ================================================ package com.jiukuaitech.bookkeeping.user.category; public class CategoryMaxCountException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryNameExistsException.java ================================================ package com.jiukuaitech.bookkeeping.user.category; /* 添加分类时,名称重复 */ public class CategoryNameExistsException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryQueryRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.category; import lombok.Getter; import lombok.Setter; @Getter @Setter public class CategoryQueryRequest { private String name; private Boolean enable; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.category; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.base.HasBookRepository; import com.jiukuaitech.bookkeeping.user.group.Group; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Repository public interface CategoryRepository extends HasBookRepository { Optional findOneByBookAndNameAndParentAndType(Book book, String name, Category parent, Integer type); List findAllByBookAndType(Book book, Integer type); List findAllByBook(Book book); List findAllByBookAndTypeAndEnable(Book book, Integer type, Boolean enable); long countByBookAndType(Book book, Integer type); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryService.java ================================================ package com.jiukuaitech.bookkeeping.user.category; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.expense_category.ExpenseCategory; import com.jiukuaitech.bookkeeping.user.income_category.IncomeCategory; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.base.BookNameNotesEnableSpec; import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelationRepository; import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException; import com.jiukuaitech.bookkeeping.user.user.UserService; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.util.List; import java.util.stream.Collectors; @Service public class CategoryService { @Resource private CategoryRepository categoryRepository; @Resource private CategoryRelationRepository categoryRelationRepository; @Resource private UserService userService; @Value("${category.max.level}") private Integer maxLevel; @Value("${category.max.count}") private Integer maxCount; /* 删除分类需要检查:1. 有无支出;2. 有无账本的默认; 3. 有无子类 */ public boolean remove(Integer id, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Category po = categoryRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); // 检查关联性,有账单关联的不能删除 if (categoryRelationRepository.countByCategory_id(id) > 0) { throw new CategoryHasDealException(); } if (po.equals(book.getDefaultExpenseCategory())) throw new CategoryIsDefaultExpenseException(); if (po.equals(book.getDefaultIncomeCategory())) throw new CategoryIsDefaultIncomeException(); categoryRepository.delete(po); return true; } @Transactional public boolean toggle(Integer id, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Category po = categoryRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); List entityList = categoryRepository.findAll(BookNameNotesEnableSpec.isBook(po.getBook())); List offSpring = po.getOffspring(entityList); offSpring.add(po); for (Category item : offSpring) { if (item.equals(book.getDefaultExpenseCategory())) throw new CategoryIsDefaultExpenseException(); if (item.equals(book.getDefaultIncomeCategory())) throw new CategoryIsDefaultIncomeException(); item.setEnable(!po.getEnable()); } categoryRepository.saveAll(offSpring); return true; } public boolean add(Integer type, CategoryAddRequest request, Integer userSignInId) { User user = userService.getUser(userSignInId); Book book = user.getDefaultBook(); Category parent = null; if (request.getParentId() != null) { parent = categoryRepository.findOneByBookAndId(book, request.getParentId()).orElseThrow(ItemNotFoundException::new); if (!parent.getEnable()) { throw new ParentCategoryNotEnableException(); } if (parent.getLevel().equals(maxLevel-1)) { throw new CategoryLevelException(); } } if (categoryRepository.countByBookAndType(book, type) >= maxCount) { throw new CategoryMaxCountException(); } if (categoryRepository.findOneByBookAndNameAndParentAndType(book, request.getName(), parent, type).isPresent()) { throw new CategoryNameExistsException(); } Category po = null; if (type == 1) { po = new ExpenseCategory(); } else { po = new IncomeCategory(); } po.setName(request.getName()); po.setNotes(request.getNotes()); po.setBook(book); po.setParent(parent); if (parent == null) po.setLevel(0); else po.setLevel(parent.getLevel()+1); categoryRepository.save(po); return true; } public boolean update(Integer type, Integer id, CategoryUpdateRequest request, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Category po = categoryRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); Category parent = null; if (request.getParentId() != null) { parent = categoryRepository.findOneByBookAndId(book, request.getParentId()).orElseThrow(ItemNotFoundException::new); if (!parent.getEnable()) { throw new ParentCategoryNotEnableException(); } if (parent.getLevel().equals(maxLevel-1)) { throw new CategoryLevelException(); } } po.setParent(parent); if (StringUtils.hasText(request.getName())) { if(!request.getName().equals(po.getName())) { if (categoryRepository.findOneByBookAndNameAndParentAndType(book, request.getName(), parent, type).isPresent()) { throw new CategoryNameExistsException(); } po.setName(request.getName()); } } if (request.getNotes() != null) po.setNotes(request.getNotes()); if (parent == null) po.setLevel(0); else po.setLevel(parent.getLevel()+1); categoryRepository.save(po); return true; } public List getAllEnable(Integer type, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); List entityList = categoryRepository.findAllByBookAndTypeAndEnable(book, type, true); return entityList.stream().map(CategorySimpleVO::fromEntity).collect(Collectors.toList()); } public List getAllTree(CategoryQueryRequest request, Integer type, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Specification specification = CategorySpec.buildSpecification(request, type, book); List entityList = categoryRepository.findAll(specification); return CategoryTreeVO.valueOfList(entityList); } public Page query(Integer type, Pageable page, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Specification specification = CategorySpec.buildSpecification(null, type, book); Page poPage = categoryRepository.findAll(specification, page); return poPage.map(CategorySimpleVO::fromEntity); } public CategoryTreeVO get(Integer id, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Category category = categoryRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); return CategoryTreeVO.valueOf(category); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategorySimpleVO.java ================================================ package com.jiukuaitech.bookkeeping.user.category; import lombok.Getter; import lombok.Setter; @Getter @Setter public class CategorySimpleVO { private Integer id; private String name; private String notes; private Boolean enable; private Integer parentId; public static CategorySimpleVO fromEntity(Category po) { if (po == null) return null; CategorySimpleVO vo = new CategorySimpleVO(); vo.setId(po.getId()); vo.setName(po.getName()); vo.setNotes(po.getNotes()); vo.setEnable(po.getEnable()); vo.setParentId(po.getParent() != null ? po.getParent().getId() : 0); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategorySpec.java ================================================ package com.jiukuaitech.bookkeeping.user.category; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.base.BookNameNotesEnableSpec; import org.springframework.data.jpa.domain.Specification; import org.springframework.util.StringUtils; public final class CategorySpec { public static Specification typeEqual(Integer type) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Category_.type), type); } public static Specification buildSpecification(CategoryQueryRequest request, Integer type, Book book) { Specification specification = BookNameNotesEnableSpec.isBook(book); if (request != null) { if (StringUtils.hasText(request.getName())) { specification = specification.and(BookNameNotesEnableSpec.nameLike(request.getName())); } if (request.getEnable() != null) { specification = specification.and(BookNameNotesEnableSpec.isEnable(request.getEnable())); } } specification = specification.and(typeEqual(type)); return specification; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryTreeVO.java ================================================ package com.jiukuaitech.bookkeeping.user.category; import lombok.Getter; import lombok.Setter; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.List; @Getter @Setter public class CategoryTreeVO { private Integer id; private String name; private String notes; private Boolean enable; private List children; private Integer parentId; private String parentName; public static List valueOfList(List categories) { List categoryTreeVOList = new ArrayList<>(); for (Category item : categories) { if (item.getParent() == null) { categoryTreeVOList.add(CategoryTreeVO.valueOf(item, categories)); } } return categoryTreeVOList; } public static CategoryTreeVO valueOf(Category category, List categories) { CategoryTreeVO categoryTreeVO = new CategoryTreeVO(); categoryTreeVO.setId(category.getId()); categoryTreeVO.setName(category.getName()); categoryTreeVO.setNotes(category.getNotes()); categoryTreeVO.setEnable(category.getEnable()); categoryTreeVO.setParentId(category.getParent() == null ? null : category.getParent().getId()); categoryTreeVO.setParentName(category.getParent() == null ? null : category.getParent().getName()); if (!CollectionUtils.isEmpty(category.getChildren(categories))) { for (Category item : category.getChildren(categories)) { if (categoryTreeVO.getChildren() == null) { categoryTreeVO.setChildren(new ArrayList<>()); } categoryTreeVO.getChildren().add(valueOf(item, categories)); } } return categoryTreeVO; } public static CategoryTreeVO valueOf(Category category) { CategoryTreeVO categoryTreeVO = new CategoryTreeVO(); categoryTreeVO.setId(category.getId()); categoryTreeVO.setName(category.getName()); categoryTreeVO.setNotes(category.getNotes()); categoryTreeVO.setEnable(category.getEnable()); if (category.getParent() != null) categoryTreeVO.setParentId(category.getParent().getId()); if (category.getParent() != null) categoryTreeVO.setParentName(category.getParent().getName()); return categoryTreeVO; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryUpdateRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.category; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.NotesValidator; import lombok.Getter; import lombok.Setter; @Getter @Setter public class CategoryUpdateRequest { @NameValidator private String name; @NotesValidator private String notes; private Integer parentId; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/DefaultExpenseCategoryException.java ================================================ package com.jiukuaitech.bookkeeping.user.category; public class DefaultExpenseCategoryException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/DefaultIncomeCategoryException.java ================================================ package com.jiukuaitech.bookkeeping.user.category; public class DefaultIncomeCategoryException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/ParentCategoryNotEnableException.java ================================================ package com.jiukuaitech.bookkeeping.user.category; /* 添加子分类时,父分类不可用 */ public class ParentCategoryNotEnableException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category_relation/CategoryRelation.java ================================================ package com.jiukuaitech.bookkeeping.user.category_relation; import com.jiukuaitech.bookkeeping.user.category.Category; import com.jiukuaitech.bookkeeping.user.deal.Deal; import com.jiukuaitech.bookkeeping.user.validation.AmountValidator; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.NotNull; import java.math.BigDecimal; @Entity //@Table(name = "t_category_relation", uniqueConstraints = {@UniqueConstraint(columnNames = {"deal_id", "category_id"})}) @Table(name = "t_category_relation") @Getter @Setter public class CategoryRelation extends BaseEntity { @ManyToOne(optional = false, fetch = FetchType.LAZY) @JoinColumn(name = "category_id") @NotNull private Category category; @ManyToOne(optional = false, fetch = FetchType.LAZY) @JoinColumn(name = "deal_id") @NotNull private Deal deal; @Column(nullable = false) @NotNull @AmountValidator private BigDecimal amount; // 金额 @Column(nullable = false) @NotNull @AmountValidator private BigDecimal convertedAmount; // 金额 } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category_relation/CategoryRelationAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.category_relation; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.category.Category; import com.jiukuaitech.bookkeeping.user.deal.Deal; import com.jiukuaitech.bookkeeping.user.validation.AmountValidator; import com.jiukuaitech.bookkeeping.user.balance_flow.AmountInvalidateException; import com.jiukuaitech.bookkeeping.user.deal.CategoryConflictException; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotNull; import java.math.BigDecimal; import java.util.HashSet; import java.util.List; import java.util.Set; @Getter @Setter public class CategoryRelationAddRequest { @NotNull private Integer categoryId; @NotNull @AmountValidator // @Positive ///可以负数,代表退款 private BigDecimal amount; private BigDecimal convertedAmount; public static void checkCategory(List categories) { Set set = new HashSet<>(); for (CategoryRelationAddRequest item : categories) { if (BigDecimal.ZERO.compareTo(item.getAmount()) == 0) { throw new AmountInvalidateException(); } if (item.getConvertedAmount() != null && BigDecimal.ZERO.compareTo(item.getConvertedAmount()) == 0) { throw new AmountInvalidateException(); } if (set.contains(item.getCategoryId())) { throw new CategoryConflictException(); } else { set.add(item.getCategoryId()); } } } public CategoryRelation getRelation(Deal deal, Book book) { CategoryRelation po = new CategoryRelation(); po.setAmount(amount); if (deal.getAccount().getCurrencyCode().equals(book.getDefaultCurrencyCode())) { po.setConvertedAmount(amount); } else { po.setConvertedAmount(convertedAmount); } po.setCategory(new Category(categoryId)); po.setDeal(deal); return po; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category_relation/CategoryRelationRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.category_relation; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.List; @Repository public interface CategoryRelationRepository extends BaseRepository { @Query("SELECT p1 FROM CategoryRelation p1 INNER JOIN p1.deal p2 WHERE p2.book = :book AND p2.status <> 2 AND p2.createTime BETWEEN :start AND :end") List findByBookAndCreateTimeBetween(Book book, Long start, Long end); Integer countByCategory_id(Integer id); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category_relation/CategoryRelationVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.category_relation; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @Getter @Setter public class CategoryRelationVOForList { private Integer id; private BigDecimal amount; private BigDecimal convertedAmount; private Integer categoryId; private String categoryName; public static CategoryRelationVOForList fromEntity(CategoryRelation po) { if (po == null) return null; CategoryRelationVOForList vo = new CategoryRelationVOForList(); vo.setId(po.getId()); vo.setAmount(po.getAmount()); vo.setConvertedAmount(po.getConvertedAmount()); vo.setCategoryId(po.getCategory().getId()); vo.setCategoryName(po.getCategory().getName()); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/checking_account/CheckingAccount.java ================================================ package com.jiukuaitech.bookkeeping.user.checking_account; import com.jiukuaitech.bookkeeping.user.account.Account; import javax.persistence.DiscriminatorValue; import javax.persistence.Entity; /** * 活期账户,例如银行卡的活期账户,支付宝余额,余额宝,微信零钱等,一般是可以直接用于支出,也可作为收入的入账账户。 */ @Entity @DiscriminatorValue(value = "1") public class CheckingAccount extends Account { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/checking_account/CheckingAccountController.java ================================================ package com.jiukuaitech.bookkeeping.user.checking_account; import com.jiukuaitech.bookkeeping.user.account.AccountQueryRequest; import com.jiukuaitech.bookkeeping.user.account.AccountService; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import com.jiukuaitech.bookkeeping.user.account.AccountAddRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/checking-accounts") public class CheckingAccountController extends BaseController { @Resource private CheckingAccountService checkingAccountService; @Resource private AccountService accountService; @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleQuery( @Valid AccountQueryRequest request, @PageableDefault(sort = "balance", direction = Sort.Direction.DESC) Pageable page, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(checkingAccountService.query(request, page, userSignInId)); } @RequestMapping(method = RequestMethod.POST, value = "") public BaseResponse handleAdd( @Valid @RequestBody AccountAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.add(1, request, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/sum") public BaseResponse handleSum(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(checkingAccountService.sum(userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/checking_account/CheckingAccountRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.checking_account; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import org.springframework.stereotype.Repository; @Repository public interface CheckingAccountRepository extends BaseRepository { /* @Query("SELECT COALESCE(SUM(p.balance), 0), " + "COALESCE(SUM(p.expense), 0), " + "COALESCE(SUM(p.income), 0), " + "COALESCE(SUM(p.transferFrom), 0), " + "COALESCE(SUM(p.transferTo), 0) " + "FROM CheckingAccount p WHERE p.book = :book AND p.enable = true") List findSum(Book book); @Query("SELECT COALESCE(SUM(p.balance), 0) FROM CheckingAccount p WHERE p.book = :book AND p.enable = true AND p.include = true") BigDecimal findSumBalance(Book book); */ } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/checking_account/CheckingAccountService.java ================================================ package com.jiukuaitech.bookkeeping.user.checking_account; import com.jiukuaitech.bookkeeping.user.account.*; import com.jiukuaitech.bookkeeping.user.currency.CurrencyService; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.user.UserService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.util.List; @Service public class CheckingAccountService { @Resource private UserService userService; @Resource private CheckingAccountRepository checkingAccountRepository; @Resource private CurrencyService currencyService; public Page query(AccountQueryRequest request, Pageable page, Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); Specification specification = AccountSpec.buildSpecification(request, group); Page poPage = checkingAccountRepository.findAll(specification, page); Page voPage = poPage.map(AccountVOForExtend::fromEntity); voPage.map(vo->{ vo.setConvertedBalance(currencyService.convert(vo.getBalance(), vo.getCurrencyCode(), group.getDefaultCurrencyCode())); return vo; }); return voPage; } public AccountSumVO sum(Integer userSignInId) { AccountSumVO vo = new AccountSumVO(); Group group = userService.getUser(userSignInId).getDefaultGroup(); List accounts = checkingAccountRepository.findAll(AccountSpec.inGroupAndEnable(group)); BigDecimal balance = BigDecimal.ZERO; for (int i = 0; i < accounts.size(); i++) { Account account = accounts.get(i); balance = balance.add(currencyService.convert(account.getBalance(), account.getCurrencyCode(), group.getDefaultCurrencyCode())); } vo.setBalance(balance); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/credit_account/CreditAccount.java ================================================ package com.jiukuaitech.bookkeeping.user.credit_account; import com.jiukuaitech.bookkeeping.user.account.Account; import com.jiukuaitech.bookkeeping.user.validation.BillDayValidator; import com.jiukuaitech.bookkeeping.user.validation.CreditLimitValidator; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; import javax.persistence.Column; import javax.persistence.DiscriminatorValue; import javax.persistence.Entity; import javax.validation.constraints.NotNull; /** * 信用账户,例如信用卡,花呗,白条等。一般可以直接用于支出,但不能作为收入的入账账户。 */ @Entity @DiscriminatorValue(value = "2") @Getter @Setter public class CreditAccount extends Account { // 非空 @Column(name = "credit_limit") @NotNull @CreditLimitValidator private BigDecimal limit; // 信用额度 @BillDayValidator private Integer billDay; // 每月多少号是账单日 } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/credit_account/CreditAccountAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.credit_account; import com.jiukuaitech.bookkeeping.user.account.AccountAddRequest; import com.jiukuaitech.bookkeeping.user.validation.BillDayValidator; import com.jiukuaitech.bookkeeping.user.validation.CreditLimitValidator; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotNull; import java.math.BigDecimal; @Getter @Setter public class CreditAccountAddRequest extends AccountAddRequest { @NotNull @CreditLimitValidator private BigDecimal limit; // 信用额度 @BillDayValidator private Integer billDay; // 每月多少号是账单日 public void copyPrimitive(CreditAccount po) { super.copyPrimitive(po); po.setLimit(limit); po.setBillDay(billDay); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/credit_account/CreditAccountController.java ================================================ package com.jiukuaitech.bookkeeping.user.credit_account; import com.jiukuaitech.bookkeeping.user.account.AccountService; import com.jiukuaitech.bookkeeping.user.account.AccountQueryRequest; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/credit-accounts") public class CreditAccountController extends BaseController { @Resource private CreditAccountService creditAccountService; @Resource private AccountService accountService; @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleQuery( @Valid AccountQueryRequest request, @PageableDefault(sort = "balance", direction = Sort.Direction.ASC) Pageable page, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(creditAccountService.query(request, page, userSignInId)); } @RequestMapping(method = RequestMethod.POST, value = "") public BaseResponse handleAdd( @Valid @RequestBody CreditAccountAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.add(2, request, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/sum") public BaseResponse handleSum(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(creditAccountService.sum(userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/credit_account/CreditAccountRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.credit_account; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import org.springframework.stereotype.Repository; @Repository public interface CreditAccountRepository extends BaseRepository { /* @Query("SELECT COALESCE(SUM(p.balance), 0), " + "COALESCE(SUM(p.expense), 0), " + "COALESCE(SUM(p.income), 0), " + "COALESCE(SUM(p.transferFrom), 0), " + "COALESCE(SUM(p.transferTo), 0), " + "COALESCE(SUM(p.limit), 0) " + "FROM CreditAccount p WHERE p.book = :book AND p.enable = true") List findSum(Book book); @Query("SELECT COALESCE(SUM(p.balance), 0) FROM CreditAccount p WHERE p.book = :book AND p.enable = true AND p.include = true") BigDecimal findSumBalance(Book book); */ } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/credit_account/CreditAccountService.java ================================================ package com.jiukuaitech.bookkeeping.user.credit_account; import com.jiukuaitech.bookkeeping.user.account.AccountQueryRequest; import com.jiukuaitech.bookkeeping.user.account.AccountSpec; import com.jiukuaitech.bookkeeping.user.currency.CurrencyService; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.user.UserService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.util.List; @Service public class CreditAccountService { @Resource private UserService userService; @Resource private CreditAccountRepository creditAccountRepository; @Resource private CurrencyService currencyService; public Page query(AccountQueryRequest request, Pageable page, Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); Specification specification = AccountSpec.buildSpecification(request, group); Page poPage = creditAccountRepository.findAll(specification, page); Page voPage = poPage.map(CreditAccountVOForList::fromEntity); voPage.map(vo->{ vo.setConvertedBalance(currencyService.convert(vo.getBalance(), vo.getCurrencyCode(), group.getDefaultCurrencyCode())); return vo; }); return voPage; } public CreditAccountSumVO sum(Integer userSignInId) { CreditAccountSumVO vo = new CreditAccountSumVO(); Group group = userService.getUser(userSignInId).getDefaultGroup(); List accounts = creditAccountRepository.findAll(AccountSpec.inGroupAndEnable(group)); BigDecimal balance = BigDecimal.ZERO; BigDecimal limit = BigDecimal.ZERO; for (int i = 0; i < accounts.size(); i++) { CreditAccount account = accounts.get(i); balance = balance.add(currencyService.convert(account.getBalance(), account.getCurrencyCode(), group.getDefaultCurrencyCode())); limit = limit.add(currencyService.convert(account.getLimit(), account.getCurrencyCode(), group.getDefaultCurrencyCode())); } vo.setBalance(balance); vo.setLimit(limit); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/credit_account/CreditAccountSumVO.java ================================================ package com.jiukuaitech.bookkeeping.user.credit_account; import com.jiukuaitech.bookkeeping.user.account.AccountSumVO; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @Getter @Setter public class CreditAccountSumVO extends AccountSumVO { private BigDecimal limit; public BigDecimal getRemainLimit() { return limit.add(getBalance()); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/credit_account/CreditAccountVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.credit_account; import com.jiukuaitech.bookkeeping.user.account.AccountVOForExtend; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @Getter @Setter public class CreditAccountVOForList extends AccountVOForExtend { private BigDecimal limit; private Integer billDay; public static CreditAccountVOForList fromEntity(CreditAccount po) { CreditAccountVOForList vo = new CreditAccountVOForList(); vo.setValue(po); vo.setLimit(po.getLimit()); vo.setBillDay(po.getBillDay()); return vo; } public BigDecimal getRemainLimit() { return limit.add(getBalance()); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/currency/Currency.java ================================================ package com.jiukuaitech.bookkeeping.user.currency; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import lombok.Getter; import lombok.Setter; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; import javax.validation.constraints.NotNull; import java.math.BigDecimal; @Entity @Table(name = "t_currency") @Getter @Setter public class Currency extends BaseEntity { @Column(length = 8, nullable = false, unique = true) @NotNull private String code; @Column(length = 128, nullable = false) @NotNull private String description; @Column(nullable = false) //最多9亿 @NotNull private BigDecimal rate; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/currency/CurrencyController.java ================================================ package com.jiukuaitech.bookkeeping.user.currency; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; @RestController @RequestMapping("/currency") public class CurrencyController extends BaseController { @Resource private CurrencyService currencyService; @RequestMapping(method = RequestMethod.GET, value = "/all") public BaseResponse handleAll() { return new DataResponse<>(currencyService.getAll()); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/currency/CurrencyRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.currency; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository public interface CurrencyRepository extends BaseRepository { Optional findOneByCode(String code); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/currency/CurrencyService.java ================================================ package com.jiukuaitech.bookkeeping.user.currency; import com.jiukuaitech.bookkeeping.user.exception.InputNotValidException; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.List; import java.util.stream.Collectors; @Service public class CurrencyService { @Resource private CurrencyRepository currencyRepository; public List getAll() { return currencyRepository.findAll(); } public void checkCode(String code) { if (code == null) return;; List currencyList = getAll(); List currencyCodeList = currencyList.stream().map(Currency::getCode).collect(Collectors.toList()); if (!currencyCodeList.contains(code)) { throw new InputNotValidException(); } } // TODO 定时任务,数据存入缓存 public BigDecimal convert(String fromCode, String toCode) { Currency fromCurrency = currencyRepository.findOneByCode(fromCode).orElseThrow(); Currency toCurrency = currencyRepository.findOneByCode(toCode).orElseThrow(); BigDecimal fromRate = fromCurrency.getRate(); BigDecimal toRate = toCurrency.getRate(); return toRate.divide(fromRate, 2, RoundingMode.CEILING); } public BigDecimal convert(BigDecimal amount, String fromCode, String toCode) { if (fromCode.equals(toCode)) { return amount; } return amount.multiply(convert(fromCode, toCode)).setScale(2, RoundingMode.CEILING); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/dashboard/AssetOverviewVO.java ================================================ package com.jiukuaitech.bookkeeping.user.dashboard; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @Getter @Setter public class AssetOverviewVO { private BigDecimal asset;//资产 private BigDecimal debt;//负债 public BigDecimal getNetWorth() { return asset.subtract(debt); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/dashboard/DashboardController.java ================================================ package com.jiukuaitech.bookkeeping.user.dashboard; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; @RestController @RequestMapping("/dashboard") public class DashboardController extends BaseController { @Resource private DashboardService dashboardService; @RequestMapping(method = RequestMethod.GET, value = "asset-overview") public BaseResponse handleAssetOverview(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(dashboardService.assetOverview(userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "expense-income-table") public BaseResponse handleTable(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(dashboardService.expenseIncomeTable(userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "expense-trend") public BaseResponse handleExpenseTrend(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(dashboardService.expenseTrend(userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "income-trend") public BaseResponse handleIncomeTrend(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(dashboardService.incomeTrend(userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "expense-category") public BaseResponse handleExpenseCategory(@RequestParam Long start, @RequestParam Long end, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(dashboardService.expenseCategory(start, end, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "income-category") public BaseResponse handleIncomeCategory(@RequestParam Long start, @RequestParam Long end, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(dashboardService.incomeCategory(start, end, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/dashboard/DashboardService.java ================================================ package com.jiukuaitech.bookkeeping.user.dashboard; import com.jiukuaitech.bookkeeping.user.account.Account; import com.jiukuaitech.bookkeeping.user.account.AccountRepository; import com.jiukuaitech.bookkeeping.user.account.AccountSpec; import com.jiukuaitech.bookkeeping.user.asset_account.AssetAccount; import com.jiukuaitech.bookkeeping.user.asset_account.AssetAccountRepository; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.category.Category; import com.jiukuaitech.bookkeeping.user.category.CategoryRepository; import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelation; import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelationRepository; import com.jiukuaitech.bookkeeping.user.checking_account.CheckingAccount; import com.jiukuaitech.bookkeeping.user.checking_account.CheckingAccountRepository; import com.jiukuaitech.bookkeeping.user.credit_account.CreditAccount; import com.jiukuaitech.bookkeeping.user.credit_account.CreditAccountRepository; import com.jiukuaitech.bookkeeping.user.currency.CurrencyService; import com.jiukuaitech.bookkeeping.user.debt_account.DebtAccount; import com.jiukuaitech.bookkeeping.user.debt_account.DebtAccountRepository; import com.jiukuaitech.bookkeeping.user.expense.ExpenseRepository; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.income.IncomeRepository; import com.jiukuaitech.bookkeeping.user.user.UserService; import com.jiukuaitech.bookkeeping.user.reports.ChartVO; import com.jiukuaitech.bookkeeping.user.utils.CalendarUtils; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.util.*; import java.util.stream.Collectors; @Service public class DashboardService { @Resource private UserService userService; @Resource private AccountRepository accountRepository; @Resource private CheckingAccountRepository checkingAccountRepository; @Resource private CreditAccountRepository creditAccountRepository; @Resource private DebtAccountRepository debtAccountRepository; @Resource private AssetAccountRepository assetAccountRepository; @Resource private ExpenseRepository expenseRepository; @Resource private IncomeRepository incomeRepository; @Resource private CategoryRepository categoryRepository; @Resource private CategoryRelationRepository categoryRelationRepository; @Resource private CurrencyService currencyService; public AssetOverviewVO assetOverview(Integer userSignInId) { AssetOverviewVO vo = new AssetOverviewVO(); Group group = userService.getUser(userSignInId).getDefaultGroup(); List checkingAccounts = checkingAccountRepository.findAll(AccountSpec.inGroupAndInclude(group)); List assetAccounts = assetAccountRepository.findAll(AccountSpec.inGroupAndInclude(group)); BigDecimal assetBalance = BigDecimal.ZERO; for (int i = 0; i < checkingAccounts.size(); i++) { Account account = checkingAccounts.get(i); assetBalance = assetBalance.add(currencyService.convert(account.getBalance(), account.getCurrencyCode(), group.getDefaultCurrencyCode())); } for (int i = 0; i < assetAccounts.size(); i++) { Account account = assetAccounts.get(i); assetBalance = assetBalance.add(currencyService.convert(account.getBalance(), account.getCurrencyCode(), group.getDefaultCurrencyCode())); } vo.setAsset(assetBalance); List creditAccounts = creditAccountRepository.findAll(AccountSpec.inGroupAndInclude(group)); List debtAccounts = debtAccountRepository.findAll(AccountSpec.inGroupAndInclude(group)); BigDecimal debtBalance = BigDecimal.ZERO; for (int i = 0; i < creditAccounts.size(); i++) { Account account = creditAccounts.get(i); debtBalance = debtBalance.add(currencyService.convert(account.getBalance(), account.getCurrencyCode(), group.getDefaultCurrencyCode())); } for (int i = 0; i < debtAccounts.size(); i++) { Account account = debtAccounts.get(i); debtBalance = debtBalance.add(currencyService.convert(account.getBalance(), account.getCurrencyCode(), group.getDefaultCurrencyCode())); } vo.setDebt(debtBalance.negate()); return vo; } public List> expenseIncomeTable(Integer userSignInId) { List> result = new ArrayList<>(); Book book = userService.getUser(userSignInId).getDefaultBook(); Long[] thisWeek = CalendarUtils.getThisWeek(); Long[] thisMonth = CalendarUtils.getThisMonth(); Long[] thisYear = CalendarUtils.getThisYear(); Long[] lastYear = CalendarUtils.getLastYear(); Long[] in7Days = CalendarUtils.getIn7Days(); Long[] in30Days = CalendarUtils.getIn30Days(); Long[] in1Year = CalendarUtils.getIn1Year(); { List list = new ArrayList<>(); list.add(expenseRepository.findSumAmount(book, thisWeek[0], thisWeek[1])); list.add(expenseRepository.findSumAmount(book, thisMonth[0], thisMonth[1])); list.add(expenseRepository.findSumAmount(book, thisYear[0], thisYear[1])); list.add(expenseRepository.findSumAmount(book, lastYear[0], lastYear[1])); list.add(expenseRepository.findSumAmount(book, in7Days[0], in7Days[1])); list.add(expenseRepository.findSumAmount(book, in30Days[0], in30Days[1])); list.add(expenseRepository.findSumAmount(book, in1Year[0], in1Year[1])); result.add(list); } { List list = new ArrayList<>(); list.add(incomeRepository.findSumAmount(book, thisWeek[0], thisWeek[1])); list.add(incomeRepository.findSumAmount(book, thisMonth[0], thisMonth[1])); list.add(incomeRepository.findSumAmount(book, thisYear[0], thisYear[1])); list.add(incomeRepository.findSumAmount(book, lastYear[0], lastYear[1])); list.add(incomeRepository.findSumAmount(book, in7Days[0], in7Days[1])); list.add(incomeRepository.findSumAmount(book, in30Days[0], in30Days[1])); list.add(incomeRepository.findSumAmount(book, in1Year[0], in1Year[1])); result.add(list); } { List list = new ArrayList<>(); list.add(BigDecimal.valueOf(expenseRepository.findCount(book, thisWeek[0], thisWeek[1]))); list.add(BigDecimal.valueOf(expenseRepository.findCount(book, thisMonth[0], thisMonth[1]))); list.add(BigDecimal.valueOf(expenseRepository.findCount(book, thisYear[0], thisYear[1]))); list.add(BigDecimal.valueOf(expenseRepository.findCount(book, lastYear[0], lastYear[1]))); list.add(BigDecimal.valueOf(expenseRepository.findCount(book, in7Days[0], in7Days[1]))); list.add(BigDecimal.valueOf(expenseRepository.findCount(book, in30Days[0], in30Days[1]))); list.add(BigDecimal.valueOf(expenseRepository.findCount(book, in1Year[0], in1Year[1]))); result.add(list); } { List list = new ArrayList<>(); list.add(BigDecimal.valueOf(incomeRepository.findCount(book, thisWeek[0], thisWeek[1]))); list.add(BigDecimal.valueOf(incomeRepository.findCount(book, thisMonth[0], thisMonth[1]))); list.add(BigDecimal.valueOf(incomeRepository.findCount(book, thisYear[0], thisYear[1]))); list.add(BigDecimal.valueOf(incomeRepository.findCount(book, lastYear[0], lastYear[1]))); list.add(BigDecimal.valueOf(incomeRepository.findCount(book, in7Days[0], in7Days[1]))); list.add(BigDecimal.valueOf(incomeRepository.findCount(book, in30Days[0], in30Days[1]))); list.add(BigDecimal.valueOf(incomeRepository.findCount(book, in1Year[0], in1Year[1]))); result.add(list); } return result; } public List expenseTrend(Integer userSignInId) { List result = new ArrayList<>(); Book book = userService.getUser(userSignInId).getDefaultBook(); List months = CalendarUtils.getMonths(Calendar.getInstance().get(Calendar.YEAR)); months.forEach(i -> { ChartVO vo = new ChartVO(); vo.setX(new SimpleDateFormat("yyyy.MM").format(i[0].getTime())); vo.setY(expenseRepository.findSumAmount(book, i[0].getTimeInMillis(), i[1].getTimeInMillis())); result.add(vo); }); return result; } public List incomeTrend(Integer userSignInId) { List result = new ArrayList<>(); Book book = userService.getUser(userSignInId).getDefaultBook(); List months = CalendarUtils.getMonths(Calendar.getInstance().get(Calendar.YEAR)); months.forEach(i -> { ChartVO vo = new ChartVO(); vo.setX(new SimpleDateFormat("yyyy.MM").format(i[0].getTime())); vo.setY(incomeRepository.findSumAmount(book, i[0].getTimeInMillis(), i[1].getTimeInMillis())); result.add(vo); }); return result; } public List expenseCategory(Long start, Long end, Integer userSignInId) { List result = new ArrayList<>(); Book book = userService.getUser(userSignInId).getDefaultBook(); List categories = categoryRepository.findAllByBookAndType(book, 1); List rootCategories = categories.stream().filter(i->i.getLevel() == 0).collect(Collectors.toList()); List relations = categoryRelationRepository.findByBookAndCreateTimeBetween(book, start, end); rootCategories.forEach(i -> { ChartVO vo = new ChartVO(); vo.setX(i.getName()); List offSpringCategoryIds = i.getOffspring(categories).stream().map(BaseEntity::getId).collect(Collectors.toList()); offSpringCategoryIds.add(i.getId()); vo.setY(relations.stream().filter(j -> offSpringCategoryIds.contains(j.getCategory().getId())).map(CategoryRelation::getConvertedAmount).reduce(BigDecimal.ZERO, BigDecimal::add)); if (vo.getY().compareTo(BigDecimal.ZERO) > 0) { result.add(vo); } }); return result; } public List incomeCategory(Long start, Long end, Integer userSignInId) { List result = new ArrayList<>(); Book book = userService.getUser(userSignInId).getDefaultBook(); List categories = categoryRepository.findAllByBookAndType(book, 2); List rootCategories = categories.stream().filter(i->i.getLevel() == 0).collect(Collectors.toList()); List incomeFroms = categoryRelationRepository.findByBookAndCreateTimeBetween(book, start, end); rootCategories.forEach(i -> { ChartVO vo = new ChartVO(); vo.setX(i.getName()); List offSpringCategoryIds = i.getOffspring(categories).stream().map(BaseEntity::getId).collect(Collectors.toList()); offSpringCategoryIds.add(i.getId()); vo.setY(incomeFroms.stream().filter(j -> offSpringCategoryIds.contains(j.getCategory().getId())).map(CategoryRelation::getConvertedAmount).reduce(BigDecimal.ZERO, BigDecimal::add)); if (vo.getY().compareTo(BigDecimal.ZERO) > 0) { result.add(vo); } }); return result; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/deal/CategoryConflictException.java ================================================ package com.jiukuaitech.bookkeeping.user.deal; /* 添加支出或收入时,选择的分类有重复 */ public class CategoryConflictException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/deal/Deal.java ================================================ package com.jiukuaitech.bookkeeping.user.deal; import com.jiukuaitech.bookkeeping.user.payee.Payee; import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelation; import com.jiukuaitech.bookkeeping.user.transaction.Transaction; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.HashSet; import java.util.Set; @Entity @Getter @Setter public class Deal extends Transaction { @ManyToOne(fetch = FetchType.LAZY) private Payee payee; @OneToMany(mappedBy = "deal", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) // @NotEmpty private Set categories = new HashSet<>(); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/deal/DealAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.deal; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.category.Category; import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelationAddRequest; import com.jiukuaitech.bookkeeping.user.exception.InputNotValidException; import com.jiukuaitech.bookkeeping.user.transaction.TransactionAddRequest; import lombok.Getter; import lombok.Setter; import javax.validation.Valid; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import java.util.List; import java.util.stream.Collectors; @Getter @Setter public class DealAddRequest extends TransactionAddRequest { @NotNull private Integer accountId; private Integer payeeId; @NotEmpty @Valid private List categories; public void copyCategories(Deal po, List categories, Book book) { getCategories().forEach(i-> { List categoryIds = categories.stream().map(BaseEntity::getId).collect(Collectors.toList()); if (categoryIds.contains(i.getCategoryId())) { po.getCategories().add(i.getRelation(po, book)); } else { throw new InputNotValidException(); } }); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/deal/DealController.java ================================================ package com.jiukuaitech.bookkeeping.user.deal; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.refund.RefundService; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; @RestController @RequestMapping("/deals") public class DealController extends BaseController { @Resource private RefundService refundService; @RequestMapping(method = RequestMethod.GET, value = "/{id}/refunds") public BaseResponse handleRefunds(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(refundService.getRefunds(id, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/deal/DealExceptionHandler.java ================================================ package com.jiukuaitech.bookkeeping.user.deal; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.ErrorResponse; import org.springframework.context.MessageSource; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.annotation.Resource; import java.util.Locale; @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) public class DealExceptionHandler { private static final Locale LANG = Locale.CHINA; @Resource private MessageSource messageSource; @ExceptionHandler(value = CategoryConflictException.class) @ResponseBody public BaseResponse handleException(CategoryConflictException e) { return new ErrorResponse(702, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/deal/DealQueryResultVO.java ================================================ package com.jiukuaitech.bookkeeping.user.deal; import lombok.Getter; import lombok.Setter; import org.springframework.data.domain.Page; import java.math.BigDecimal; @Getter @Setter public class DealQueryResultVO { private Page result; private BigDecimal total; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/deal/DealRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.deal; import com.jiukuaitech.bookkeeping.user.base.HasBookRepository; import org.springframework.stereotype.Repository; @Repository public interface DealRepository extends HasBookRepository { Integer countByPayee_id(Integer payeeId); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/deal/DealService.java ================================================ package com.jiukuaitech.bookkeeping.user.deal; import com.jiukuaitech.bookkeeping.user.account.Account; import com.jiukuaitech.bookkeeping.user.balance_flow.AccountInvalidateException; import com.jiukuaitech.bookkeeping.user.balance_flow.StatusNotValidateException; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.category.CategoryRepository; import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelationAddRequest; import com.jiukuaitech.bookkeeping.user.expense.Expense; import com.jiukuaitech.bookkeeping.user.flow_images.FlowImageRepository; import com.jiukuaitech.bookkeeping.user.income.Income; import com.jiukuaitech.bookkeeping.user.payee.Payee; import com.jiukuaitech.bookkeeping.user.payee.PayeeRepository; import com.jiukuaitech.bookkeeping.user.refund.Refund; import com.jiukuaitech.bookkeeping.user.refund.RefundRepository; import com.jiukuaitech.bookkeeping.user.tag.TagRepository; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.user.UserService; import com.jiukuaitech.bookkeeping.user.account.AccountRepository; import com.jiukuaitech.bookkeeping.user.balance_flow.AmountInvalidateException; import com.jiukuaitech.bookkeeping.user.exception.InputNotValidException; import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException; import com.jiukuaitech.bookkeeping.user.user_log.UserActionLog; import com.jiukuaitech.bookkeeping.user.user_log.UserActionLogRepository; import com.jiukuaitech.bookkeeping.user.user_log.UserActionLogService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.math.BigDecimal; import java.time.Instant; @Service public class DealService { @Resource private UserService userService; @Resource private AccountRepository accountRepository; @Resource private DealRepository dealRepository; @Resource private FlowImageRepository dealImageRepository; @Resource private RefundRepository refundRepository; @Resource private PayeeRepository payeeRepository; @Resource private TagRepository tagRepository; @Resource private CategoryRepository categoryRepository; @Resource private UserActionLogRepository userActionLogRepository; @Resource private UserActionLogService userActionLogService; @Transactional // 正常添加amount只能为正,退款的添加amount只能为负。 public Deal add(Integer type, DealAddRequest request, Integer userSignInId, boolean refundFlag) { //检查Category有没有重复 CategoryRelationAddRequest.checkCategory(request.getCategories()); User user = userService.getUser(userSignInId); // 不能频繁添加,防止用户恶意操作 userActionLogService.check(user); Book book = user.getDefaultBook(); Deal po = null; if (type == 1) { po = new Expense(); } else if (type == 2) { po = new Income(); } BigDecimal amount = request.getCategories().stream().map(CategoryRelationAddRequest::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); if (refundFlag && amount.compareTo(BigDecimal.ZERO) > 0) { throw new AmountInvalidateException(); } if (!refundFlag && amount.compareTo(BigDecimal.ZERO) < 0) { throw new AmountInvalidateException(); } po.setAmount(amount); Account account = accountRepository.findOneByGroupAndId(user.getDefaultGroup(), request.getAccountId()).orElseThrow(AccountInvalidateException::new); po.setAccount(account); if (account.getCurrencyCode().equals(book.getDefaultCurrencyCode())) { po.setConvertedAmount(amount); } else { BigDecimal convertedAmount = request.getCategories().stream().map(CategoryRelationAddRequest::getConvertedAmount).reduce(BigDecimal.ZERO, BigDecimal::add); if (refundFlag && convertedAmount.compareTo(BigDecimal.ZERO) > 0) { throw new AmountInvalidateException(); } if (!refundFlag && convertedAmount.compareTo(BigDecimal.ZERO) < 0) { throw new AmountInvalidateException(); } po.setConvertedAmount(convertedAmount); } po.setCreator(user); po.setBook(user.getDefaultBook()); po.setGroup(user.getDefaultGroup()); if (request.getPayeeId() != null) { Payee payee = payeeRepository.findOneByBookAndId(book, request.getPayeeId()).orElseThrow(InputNotValidException::new); po.setPayee(payee); } request.copyTags(po, tagRepository.findByBookAndEnable(book, true)); request.copyCategories(po, categoryRepository.findAllByBook(book), book); request.copyPrimitive(po); dealRepository.save(po); confirmBalance(po, type); userActionLogRepository.save(new UserActionLog(user, 1, Instant.now().toEpochMilli())); return po; } @Transactional // 已经退款的不能修改 public boolean update(Integer type, Integer id, DealUpdateRequest request, Integer userSignInId) { //检查Category有没有重复 CategoryRelationAddRequest.checkCategory(request.getCategories()); User user = userService.getUser(userSignInId); Book book = user.getDefaultBook(); Deal po = dealRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); BigDecimal newAmount = request.getCategories().stream().map(CategoryRelationAddRequest::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); //正数不能改负数,负数不能改正数 // 不能改变正负符号,也就是不能改变是否是退款的情况。 if (po.getAmount().compareTo(BigDecimal.ZERO) > 0 && newAmount.compareTo(BigDecimal.ZERO) < 0) { throw new AmountInvalidateException(); } if (po.getAmount().compareTo(BigDecimal.ZERO) < 0 && newAmount.compareTo(BigDecimal.ZERO) > 0) { throw new AmountInvalidateException(); } boolean refundFlag = false;//是否更新账户金额 if (po.getAmount().compareTo(newAmount) != 0) { refundFlag = true; // 不然更新金额,标签对应的金额不更新 // 只有更新金额才自动更新标签的金额,更新分类不更新标签的金额 po.getTags().forEach(i->i.setAmount(newAmount)); } else if (request.getAccountId() != null && !request.getAccountId().equals(po.getAccount().getId())) { refundFlag = true; } if (refundFlag) { refundBalance(po, type); } Account account = accountRepository.findOneByGroupAndId(user.getDefaultGroup(), request.getAccountId()).orElseThrow(AccountInvalidateException::new); po.setAccount(account); po.setAmount(newAmount); if (account.getCurrencyCode().equals(book.getDefaultCurrencyCode())) { po.setConvertedAmount(newAmount); } else { BigDecimal newConvertedAmount = request.getCategories().stream().map(CategoryRelationAddRequest::getConvertedAmount).reduce(BigDecimal.ZERO, BigDecimal::add); if (po.getConvertedAmount().compareTo(BigDecimal.ZERO) > 0 && newConvertedAmount.compareTo(BigDecimal.ZERO) < 0) { throw new AmountInvalidateException(); } if (po.getConvertedAmount().compareTo(BigDecimal.ZERO) < 0 && newConvertedAmount.compareTo(BigDecimal.ZERO) > 0) { throw new AmountInvalidateException(); } po.setConvertedAmount(newConvertedAmount); } if (request.getPayeeId() != null) { Payee payee = payeeRepository.findOneByBookAndId(book, request.getPayeeId()).orElseThrow(InputNotValidException::new); po.setPayee(payee); } else { po.setPayee(null); } request.updateTags(po, tagRepository.findByBookAndEnable(book, true)); request.updateCategories(po, categoryRepository.findAllByBook(book), book); request.updatePrimitive(po); dealRepository.save(po); if (refundFlag) { confirmBalance(po, type); } return true; } @Transactional // 只有状态是正常的才能退款 public boolean refund(Integer type, Integer id, DealAddRequest request, Integer userSignInId) { User user = userService.getUser(userSignInId); Book book = user.getDefaultBook(); Deal deal = dealRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); // 如果新增状态值可能有问题 if (deal.getStatus() == 2 || deal.getAmount().compareTo(BigDecimal.ZERO) < 0) throw new StatusNotValidateException(); Deal refundDeal = add(type, request, userSignInId, true); // 状态变成已退款 deal.setStatus(3); Refund refund = new Refund(deal, refundDeal); refundRepository.save(refund); return true; } // 1. 退回账户余额变更,2. 删除图片关联以及文件,3. 删除关联category relation,4. 删除关联tag relation 5. 处理关联的refund,最后删除自己。 // 已经退款的不能删除,必须先删除对应的退款记录 @Transactional public boolean remove(Integer type, Integer id, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Deal po = dealRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); if (po.getStatus() == 3) throw new StatusNotValidateException(); refundBalance(po, type); // 处理图片 po.getImages().forEach(i -> { i.setFlow(null); dealImageRepository.save(i); }); // category relation tag relation自动处理 // 处理关联的refund if (po.getAmount().compareTo(BigDecimal.ZERO) < 0) {//小于0说明是退款记录 Refund refund = refundRepository.findByRefund(po).orElseThrow(ItemNotFoundException::new); Deal deal = refund.getDeal(); deal.setStatus(1); dealRepository.save(deal); refundRepository.delete(refund); } dealRepository.delete(po); return true; } @Transactional public boolean confirm(Integer type, Integer id, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Deal po = dealRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); if (po.getStatus() != 2) { throw new StatusNotValidateException(); } po.setStatus(1); confirmBalance(po, type); dealRepository.save(po); return true; } // 余额确认,包括账户扣款,账户统计,账户日志记录。 private void confirmBalance(Deal deal, Integer type) { if (deal.getStatus() == 2) return;//未确认,不更新账户余额 Account account = deal.getAccount(); if (type == 1) { account.setBalance(account.getBalance().subtract(deal.getAmount())); } else if (type == 2) { account.setBalance(account.getBalance().add(deal.getAmount())); } accountRepository.save(account); } // 退款 private void refundBalance(Deal deal, Integer type) { if (deal.getStatus() == 2) return;//未确认,不更新账户余额 Account account = deal.getAccount(); if (type == 1) { account.setBalance(account.getBalance().add(deal.getAmount())); } else if (type == 2) { account.setBalance(account.getBalance().subtract(deal.getAmount())); } accountRepository.save(account); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/deal/DealSpec.java ================================================ package com.jiukuaitech.bookkeeping.user.deal; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowQueryRequest; import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelation; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.payee.Payee; import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelation_; import com.jiukuaitech.bookkeeping.user.transaction.TransactionSpec; import org.springframework.data.jpa.domain.Specification; import org.springframework.util.CollectionUtils; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.Join; import java.util.Set; public final class DealSpec { public static Specification payeeIn(Set payees) { return (root, query, criteriaBuilder) -> { CriteriaBuilder.In in = criteriaBuilder.in(root.get(Deal_.payee)); payees.forEach(i -> in.value(new Payee(i))); return in; }; } public static Specification categoriesIn(Set categories) { return (root, query, criteriaBuilder) -> { Join join = root.join(Deal_.categories); return join.get(CategoryRelation_.category).in(categories); }; } public static Specification buildSpecification(BalanceFlowQueryRequest request, Group group) { Specification specification = TransactionSpec.buildSpecification(request, group); if (!CollectionUtils.isEmpty(request.getPayees())) { specification = specification.and(payeeIn(request.getPayees())); } if (!CollectionUtils.isEmpty(request.getCategories())) { specification = specification.and(categoriesIn(request.getCategories())); } return specification; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/deal/DealUpdateRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.deal; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.category.Category; import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelationAddRequest; import com.jiukuaitech.bookkeeping.user.exception.InputNotValidException; import com.jiukuaitech.bookkeeping.user.transaction.TransactionUpdateRequest; import lombok.Getter; import lombok.Setter; import javax.validation.Valid; import javax.validation.constraints.NotEmpty; import java.util.List; import java.util.stream.Collectors; @Getter @Setter public class DealUpdateRequest extends TransactionUpdateRequest { private Integer accountId; private Integer payeeId; @NotEmpty @Valid private List categories; public void updateCategories(Deal po, List categories, Book book) { po.getCategories().clear(); getCategories().forEach(i-> { List categoryIds = categories.stream().map(BaseEntity::getId).collect(Collectors.toList()); if (categoryIds.contains(i.getCategoryId())) { po.getCategories().add(i.getRelation(po, book)); } else { throw new InputNotValidException(); } }); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/deal/DealVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.deal; import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelationVOForList; import com.jiukuaitech.bookkeeping.user.response.HasNameVO; import com.jiukuaitech.bookkeeping.user.transaction.TransactionVOForList; import lombok.Getter; import lombok.Setter; import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; @Getter @Setter public class DealVOForList extends TransactionVOForList { private HasNameVO payee; private String categoryName; private Set categories = new HashSet<>(); public void setValue(Deal po) { super.setValue(po); if (po.getPayee() != null) setPayee(new HasNameVO(po.getPayee().getId(), po.getPayee().getName())); setCategoryName(po.getCategories().stream().map(i -> i.getCategory().getName() + "(" + i.getAmount().stripTrailingZeros().toPlainString()+")").collect(Collectors.joining(", "))); setCategories(po.getCategories().stream().map(CategoryRelationVOForList::fromEntity).collect(Collectors.toSet())); } public static DealVOForList fromEntity(Deal po) { DealVOForList vo = new DealVOForList(); vo.setValue(po); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/debt_account/DebtAccount.java ================================================ package com.jiukuaitech.bookkeeping.user.debt_account; import com.jiukuaitech.bookkeeping.user.account.Account; import com.jiukuaitech.bookkeeping.user.validation.AprValidator; import com.jiukuaitech.bookkeeping.user.validation.CreditLimitValidator; import lombok.Getter; import lombok.Setter; import javax.persistence.Column; import javax.persistence.DiscriminatorValue; import javax.persistence.Entity; import java.math.BigDecimal; /** * 负债账户,例如房贷,车贷,应付账款,借呗等。一般不能直接用于支出和收入。 */ @Entity @DiscriminatorValue(value = "3") @Getter @Setter public class DebtAccount extends Account { @Column(name = "credit_limit") @CreditLimitValidator private BigDecimal limit; // 信用额度 @AprValidator private BigDecimal apr; // 年化利率(%) public DebtAccount() { } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/debt_account/DebtAccountAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.debt_account; import com.jiukuaitech.bookkeeping.user.account.AccountAddRequest; import com.jiukuaitech.bookkeeping.user.validation.AprValidator; import com.jiukuaitech.bookkeeping.user.validation.CreditLimitValidator; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @Getter @Setter public class DebtAccountAddRequest extends AccountAddRequest { @AprValidator private BigDecimal apr; @CreditLimitValidator private BigDecimal limit; // 信用额度 public void copyPrimitive(DebtAccount po) { super.copyPrimitive(po); po.setApr(apr); po.setLimit(limit); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/debt_account/DebtAccountController.java ================================================ package com.jiukuaitech.bookkeeping.user.debt_account; import com.jiukuaitech.bookkeeping.user.account.AccountQueryRequest; import com.jiukuaitech.bookkeeping.user.account.AccountService; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/debt-accounts") public class DebtAccountController extends BaseController { @Resource private DebtAccountService debtAccountService; @Resource private AccountService accountService; @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleQuery( @Valid AccountQueryRequest request, @PageableDefault(sort = "balance", direction = Sort.Direction.ASC) Pageable page, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(debtAccountService.query(request, page, userSignInId)); } @RequestMapping(method = RequestMethod.POST, value = "") public BaseResponse handleAdd( @Valid @RequestBody DebtAccountAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(accountService.add(3, request, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/sum") public BaseResponse handleSum(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(debtAccountService.sum(userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/debt_account/DebtAccountRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.debt_account; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import org.springframework.stereotype.Repository; @Repository public interface DebtAccountRepository extends BaseRepository { /* @Query("SELECT COALESCE(SUM(p.balance), 0), " + "COALESCE(SUM(p.expense), 0), " + "COALESCE(SUM(p.income), 0), " + "COALESCE(SUM(p.transferFrom), 0), " + "COALESCE(SUM(p.transferTo), 0), " + "COALESCE(SUM(p.limit), 0) " + "FROM DebtAccount p WHERE p.book = :book AND p.enable = true") List findSum(Book book); @Query("SELECT COALESCE(SUM(p.balance), 0) FROM DebtAccount p WHERE p.book = :book AND p.enable = true AND p.include = true") BigDecimal findSumBalance(Book book); */ } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/debt_account/DebtAccountService.java ================================================ package com.jiukuaitech.bookkeeping.user.debt_account; import com.jiukuaitech.bookkeeping.user.account.AccountQueryRequest; import com.jiukuaitech.bookkeeping.user.account.AccountSpec; import com.jiukuaitech.bookkeeping.user.currency.CurrencyService; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.user.UserService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.util.List; @Service public class DebtAccountService { @Resource private UserService userService; @Resource private DebtAccountRepository debtAccountRepository; @Resource private CurrencyService currencyService; public Page query(AccountQueryRequest request, Pageable page, Integer userSignInId) { Group group = userService.getUser(userSignInId).getDefaultGroup(); Specification specification = AccountSpec.buildSpecification(request, group); Page poPage = debtAccountRepository.findAll(specification, page); Page voPage = poPage.map(DebtAccountVOForList::fromEntity); voPage.map(vo->{ vo.setConvertedBalance(currencyService.convert(vo.getBalance(), vo.getCurrencyCode(), group.getDefaultCurrencyCode())); return vo; }); return voPage; } public DebtAccountSumVO sum(Integer userSignInId) { DebtAccountSumVO vo = new DebtAccountSumVO(); Group group = userService.getUser(userSignInId).getDefaultGroup(); List accounts = debtAccountRepository.findAll(AccountSpec.inGroupAndEnable(group)); BigDecimal balance = BigDecimal.ZERO; BigDecimal limit = BigDecimal.ZERO; BigDecimal remainLimit = BigDecimal.ZERO; for (int i = 0; i < accounts.size(); i++) { DebtAccount account = accounts.get(i); balance = balance.add(currencyService.convert(account.getBalance(), account.getCurrencyCode(), group.getDefaultCurrencyCode())); if (account.getLimit() != null) { limit = limit.add(currencyService.convert(account.getLimit(), account.getCurrencyCode(), group.getDefaultCurrencyCode())); } DebtAccountVOForList debtAccountVOForList = DebtAccountVOForList.fromEntity(account); if (debtAccountVOForList.getLimit() != null) { remainLimit = remainLimit.add(debtAccountVOForList.getRemainLimit()); } } vo.setBalance(balance); vo.setLimit(limit); vo.setRemainLimit(remainLimit); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/debt_account/DebtAccountSumVO.java ================================================ package com.jiukuaitech.bookkeeping.user.debt_account; import com.jiukuaitech.bookkeeping.user.account.AccountSumVO; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @Getter @Setter public class DebtAccountSumVO extends AccountSumVO { private BigDecimal limit; private BigDecimal remainLimit; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/debt_account/DebtAccountVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.debt_account; import com.jiukuaitech.bookkeeping.user.account.AccountVOForExtend; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @Getter @Setter public class DebtAccountVOForList extends AccountVOForExtend { private BigDecimal apr; private BigDecimal limit; public static DebtAccountVOForList fromEntity(DebtAccount po) { DebtAccountVOForList vo = new DebtAccountVOForList(); vo.setValue(po); vo.setApr(po.getApr()); vo.setLimit(po.getLimit()); return vo; } public BigDecimal getRemainLimit() { if (limit == null) return null; // if (limit.signum() == 0) return BigDecimal.valueOf(0); return limit.add(getBalance()); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/exception/GlobalExceptionHandler.java ================================================ package com.jiukuaitech.bookkeeping.user.exception; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.ErrorResponse; import org.springframework.context.MessageSource; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.BindException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.NoHandlerFoundException; import javax.annotation.Resource; import javax.validation.ConstraintViolationException; import java.util.Locale; @RestControllerAdvice public class GlobalExceptionHandler { @Resource private MessageSource messageSource; private static final Locale LANG = Locale.CHINA; @ExceptionHandler(value = Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public BaseResponse defaultExceptionHandler(Exception e) { // TODO 日志记录 System.out.println("------------------------------------------"); e.printStackTrace(); return new ErrorResponse(0, messageSource.getMessage("DefaultException", null, LANG)); } // 请求头的Content-Type不正确 @ExceptionHandler(value = HttpMediaTypeNotSupportedException.class) @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE) public BaseResponse exceptionHandler1(Exception e) { return new ErrorResponse(1, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } // 404 @ExceptionHandler(value = NoHandlerFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public BaseResponse exceptionHandler2(Exception e) { return new ErrorResponse(404, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } // 更新的数据违反了数据库约束 @ExceptionHandler(value = {DataIntegrityViolationException.class, ConstraintViolationException.class}) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public BaseResponse exceptionHandler3(Exception e) { e.printStackTrace(); return new ErrorResponse(3, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } // 前端传递的参数不合法。 @ExceptionHandler({ MethodArgumentNotValidException.class, BindException.class, InputNotValidException.class, HttpMessageNotReadableException.class }) @ResponseBody public BaseResponse exceptionHandler6(Exception e) { e.printStackTrace(); return new ErrorResponse(6, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } // @ExceptionHandler(value = HttpRequestMethodNotSupportedException.class) @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) public BaseResponse exceptionHandler7(Exception e) { return new ErrorResponse(7, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = TokenEmptyException.class) @ResponseBody public BaseResponse exceptionHandler8(Exception e) { return new ErrorResponse(8, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = TokenNotValidException.class) @ResponseBody public BaseResponse exceptionHandler10(Exception e) { return new ErrorResponse(10, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = PermissionException.class) @ResponseStatus(HttpStatus.FORBIDDEN) public BaseResponse exceptionHandler9(Exception e) { return new ErrorResponse(9, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = ItemNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public BaseResponse handleException(ItemNotFoundException e) { return new ErrorResponse(404, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = NameExistsException.class) @ResponseStatus(HttpStatus.CONFLICT) public BaseResponse handleException(NameExistsException e) { return new ErrorResponse(408, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/exception/InputNotValidException.java ================================================ package com.jiukuaitech.bookkeeping.user.exception; public class InputNotValidException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/exception/ItemNotFoundException.java ================================================ package com.jiukuaitech.bookkeeping.user.exception; public class ItemNotFoundException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/exception/NameExistsException.java ================================================ package com.jiukuaitech.bookkeeping.user.exception; public class NameExistsException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/exception/PermissionException.java ================================================ package com.jiukuaitech.bookkeeping.user.exception; public class PermissionException extends RuntimeException { private static final long serialVersionUID = -3240641871841704086L; public PermissionException() {} public PermissionException(String msg) { super(msg); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/exception/TokenEmptyException.java ================================================ package com.jiukuaitech.bookkeeping.user.exception; public class TokenEmptyException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/exception/TokenNotValidException.java ================================================ package com.jiukuaitech.bookkeeping.user.exception; public class TokenNotValidException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/expense/Expense.java ================================================ package com.jiukuaitech.bookkeeping.user.expense; import com.jiukuaitech.bookkeeping.user.deal.Deal; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Entity @DiscriminatorValue(value = "1") @Getter @Setter // n+1 //@NamedEntityGraph(name = "Expense.Graph", attributeNodes = { // @NamedAttributeNode("tags"), // @NamedAttributeNode("categories"), // @NamedAttributeNode("payee"), // @NamedAttributeNode("account"), //}) public class Expense extends Deal { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/expense/ExpenseController.java ================================================ package com.jiukuaitech.bookkeeping.user.expense; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowQueryRequest; import com.jiukuaitech.bookkeeping.user.deal.DealAddRequest; import com.jiukuaitech.bookkeeping.user.deal.DealUpdateRequest; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.deal.DealService; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/expenses") public class ExpenseController extends BaseController { @Resource private ExpenseService expenseService; @Resource private DealService dealService; @RequestMapping(method = RequestMethod.POST, value = "") public BaseResponse handleAdd( @Valid @RequestBody DealAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { dealService.add(1, request, userSignInId, false); return new BaseResponse(true); } @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleQuery( @Valid BalanceFlowQueryRequest request, @PageableDefault(sort = {"createTime", "id"}, direction = Sort.Direction.DESC) Pageable page, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(expenseService.queryWithDefaultBook(request, page, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}") public BaseResponse handleUpdate( @PathVariable("id") Integer id, @Valid @RequestBody DealUpdateRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(dealService.update(1, id, request, userSignInId)); } @RequestMapping(method = RequestMethod.POST, value = "{id}/refund") public BaseResponse handleRefund( @PathVariable("id") Integer id, @Valid @RequestBody DealAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(dealService.refund(1, id, request, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/expense/ExpenseRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.expense; import com.jiukuaitech.bookkeeping.user.base.HasBookRepository; import com.jiukuaitech.bookkeeping.user.book.Book; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.math.BigDecimal; @Repository public interface ExpenseRepository extends HasBookRepository { @Query("SELECT COALESCE(SUM(p.convertedAmount), 0) FROM Expense p WHERE p.book = :book AND p.status <> 2 AND p.createTime BETWEEN :start AND :end") BigDecimal findSumAmount(Book book, Long start, Long end); @Query("SELECT COUNT(p) FROM Expense p WHERE p.book = :book AND p.status <> 2 AND p.createTime BETWEEN :start AND :end") Integer findCount(Book book, Long start, Long end); // n+1 // @EntityGraph(value = "Expense.Graph", type = EntityGraph.EntityGraphType.FETCH) // List findAll(Specification specification); // // @EntityGraph(value = "Expense.Graph", type = EntityGraph.EntityGraphType.FETCH) // Page findAll(Specification specification, Pageable page); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/expense/ExpenseService.java ================================================ package com.jiukuaitech.bookkeeping.user.expense; import com.jiukuaitech.bookkeeping.user.deal.DealQueryResultVO; import com.jiukuaitech.bookkeeping.user.deal.DealSpec; import com.jiukuaitech.bookkeeping.user.deal.DealVOForList; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.user.UserService; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowQueryRequest; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import java.math.BigDecimal; @Service public class ExpenseService { @Resource private ExpenseRepository expenseRepository; @Resource private UserService userService; public DealQueryResultVO queryWithDefaultBook(BalanceFlowQueryRequest request, Pageable page, Integer userSignInId) { User user = userService.getUser(userSignInId); request.setBookId(user.getDefaultBook().getId()); return query(request, page, userSignInId); } public DealQueryResultVO query(BalanceFlowQueryRequest request, Pageable page, Integer userSignInId) { DealQueryResultVO result = new DealQueryResultVO(); // 缓存了,可以避免n+1 // payeeRepository.findByBookAndEnableAndExpenseable(user.getDefaultBook(), true, true); // accountRepository.findByBookAndEnableAndExpenseable(user.getDefaultBook(), true, true); // expenseCategoryRepository.findByBook(user.getDefaultBook()); // tagRepository.findByBookAndEnableAndExpenseable(user.getDefaultBook(), true, true); // Specification specification = (root, query, criteriaBuilder) -> { // List predicates = DealUtils.buildExpensePredicates(request, user, root, criteriaBuilder); // join查询,避免n+1 // root.fetch("payee"); // root.fetch("account"); // root.fetch("categories"); // root.fetch("tags"); // return criteriaBuilder.and(predicates.toArray(new Predicate[0])); // }; Group defaultGroup = userService.getUser(userSignInId).getDefaultGroup(); Specification specification = DealSpec.buildSpecification(request, defaultGroup); Page poPage = expenseRepository.findAll(specification, page); Page voPage = poPage.map(expense -> { DealVOForList vo = DealVOForList.fromEntity(expense); vo.setCurrencyCode(expense.getAccount().getCurrencyCode()); if (expense.getBook().getDefaultCurrencyCode().equals(expense.getAccount().getCurrencyCode())) { vo.setNeedConvert(false); } else { vo.setNeedConvert(true); vo.setToCurrencyCode(expense.getBook().getDefaultCurrencyCode()); } return vo; }); result.setResult(voPage); /* CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); CriteriaQuery query = criteriaBuilder.createTupleQuery(); Root root = query.from(Expense.class); query.select(criteriaBuilder.tuple(criteriaBuilder.sum(root.get(Expense_.amount)))); query.where(predicates.toArray(new Predicate[0])); TypedQuery resultQuery = em.createQuery(query); Tuple tuple = resultQuery.getSingleResult(); result.setTotal(tuple.get(0) == null ? BigDecimal.valueOf(0) : (BigDecimal) tuple.get(0)); */ // 解决join查询重复问题,应该考虑子查询 if (CollectionUtils.isEmpty(request.getTags())) { result.setTotal(expenseRepository.calcAggregate(specification, Expense_.convertedAmount, Expense.class)); } else { result.setTotal(expenseRepository.findAll(specification).stream().map(Expense::getConvertedAmount).reduce(BigDecimal.ZERO, BigDecimal::add)); } return result; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/expense_category/ExpenseCategory.java ================================================ package com.jiukuaitech.bookkeeping.user.expense_category; import javax.persistence.*; import com.jiukuaitech.bookkeeping.user.category.Category; import lombok.NoArgsConstructor; /** * 支出类别 */ @Entity @DiscriminatorValue(value = "1") @NoArgsConstructor public class ExpenseCategory extends Category { public ExpenseCategory(Integer id) { super(id); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/expense_category/ExpenseCategoryController.java ================================================ package com.jiukuaitech.bookkeeping.user.expense_category; import com.jiukuaitech.bookkeeping.user.category.CategoryAddRequest; import com.jiukuaitech.bookkeeping.user.category.CategoryService; import com.jiukuaitech.bookkeeping.user.category.CategoryUpdateRequest; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.category.CategoryQueryRequest; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/expense-categories") public class ExpenseCategoryController extends BaseController { @Resource private CategoryService categoryService; @RequestMapping(method = RequestMethod.POST, value = "") public BaseResponse handleAdd( @Valid @RequestBody CategoryAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(categoryService.add(1, request, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/enable") public BaseResponse handleGetAllEnable(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(categoryService.getAllEnable(1, userSignInId)); } // 接口没用上 @RequestMapping(method = RequestMethod.GET, value = "/simple") public BaseResponse handleQuerySimple( @PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable page, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(categoryService.query(1, page, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleQuery(@Valid CategoryQueryRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(categoryService.getAllTree(request, 1, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}") public BaseResponse handleUpdate( @PathVariable("id") Integer id, @Valid @RequestBody CategoryUpdateRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(categoryService.update(1, id, request, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/flow_images/FlowImage.java ================================================ package com.jiukuaitech.bookkeeping.user.flow_images; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlow; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import com.jiukuaitech.bookkeeping.user.validation.TimeValidator; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.NotNull; @Entity @Table(name="t_flow_image") @Getter @Setter public class FlowImage extends BaseEntity { @Column(length = 256) private String host; @Column(length = 128) private String uri; @ManyToOne(optional = false, fetch = FetchType.LAZY) @JoinColumn(name = "user_id") @NotNull private User user; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "flow_id") private BalanceFlow flow; @Column(nullable = false) @NotNull @TimeValidator private Long createTime; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/flow_images/FlowImageController.java ================================================ package com.jiukuaitech.bookkeeping.user.flow_images; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; @RestController @RequestMapping("/flow-images") public class FlowImageController { @Resource private FlowImageService flowImageService; @RequestMapping(method = RequestMethod.GET, value = "/upload-token") public BaseResponse handleUploadToken(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(flowImageService.uploadToken(userSignInId)); } @RequestMapping(method = RequestMethod.POST, value = "/upload-callback") public BaseResponse handleUploadCallBack(@RequestBody UploadCallbackRequest request) { return new DataResponse<>(flowImageService.uploadCallBack(request)); } @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") public BaseResponse handleDelete(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(flowImageService.remove(id, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/flow_images/FlowImageExceptionHandler.java ================================================ package com.jiukuaitech.bookkeeping.user.flow_images; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.ErrorResponse; import org.springframework.context.MessageSource; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.annotation.Resource; import java.util.Locale; @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) public class FlowImageExceptionHandler { private static final Locale LANG = Locale.CHINA; @Resource private MessageSource messageSource; @ExceptionHandler(value = ImageExistsException.class) @ResponseBody public BaseResponse handleException(ImageExistsException e) { return new ErrorResponse(702, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = UploadKeyEmptyException.class) @ResponseBody public BaseResponse handleException(UploadKeyEmptyException e) { return new ErrorResponse(703, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/flow_images/FlowImageRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.flow_images; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlow; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Repository public interface FlowImageRepository extends BaseRepository { Optional findByUserAndUri(User user, String uri); List findByFlow(BalanceFlow flow); Optional findByUserAndId(User user, Integer id); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/flow_images/FlowImageService.java ================================================ package com.jiukuaitech.bookkeeping.user.flow_images; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.user.UserService; import com.qiniu.util.Auth; import com.qiniu.util.StringMap; import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.time.Instant; import java.util.Optional; @Service public class FlowImageService { @Resource private FlowImageRepository flowImageRepository; @Resource private UserService userService; @Value("${upload.ACCESS_KEY}") private String uploadAK; @Value("${upload.SECRET_KEY}") private String uploadSK; @Value("${upload.FLOW_IMAGE_BUCKET}") private String uploadBucket; @Value("${upload.FLOW_IMAGE_HOST}") private String imageHost; @Value("${upload.FLOW_IMAGE_CALL_BACK_URL}") private String callBackUrl; public String uploadToken(Integer userSignInId) { if (!StringUtils.hasText(uploadAK) || !StringUtils.hasText(uploadSK) || !StringUtils.hasText(uploadBucket) || !StringUtils.hasText(imageHost) || !StringUtils.hasText(callBackUrl) ) { throw new UploadKeyEmptyException(); } Auth auth = Auth.create(uploadAK, uploadSK); StringMap putPolicy = new StringMap(); String saveKey = userSignInId.toString() + "_" + "$(year)$(mon)$(day)$(hour)$(min)$(sec)" + "$(ext)"; putPolicy.put("saveKey", saveKey); putPolicy.put("fsizeMin", 1024); //1KB putPolicy.put("fsizeLimit", 15728640); //15M 用15*1024*1024报错 putPolicy.put("mimeLimit", "image/jpeg;image/jpg;;image/png;application/pdf"); putPolicy.put("fileType", 0); //0 为标准存储(默认),1 为低频存储,2 为归档存储。 // 自定义上传回复的凭证 // putPolicy.put("returnBody", "{\"key\":\"$(key)}\""); long expireSeconds = 3600; // 带回调业务服务器的凭证 putPolicy.put("callbackUrl", callBackUrl); putPolicy.put("callbackBody", "{\"key\":\"$(key)\",\"userId\":$(x:userId)}"); putPolicy.put("callbackBodyType", "application/json"); return auth.uploadToken(uploadBucket, null, expireSeconds, putPolicy); } public FlowImageVOForList uploadCallBack(UploadCallbackRequest request) { User user = new User(request.getUserId()); FlowImage image; Optional optionalImage = flowImageRepository.findByUserAndUri(user, request.getKey()); if(optionalImage.isPresent()) { image = optionalImage.get(); if (image.getFlow() != null) throw new ImageExistsException(); } else { image = new FlowImage(); image.setHost(imageHost); image.setUri("/" + request.getKey()); image.setUser(user); image.setCreateTime(Instant.now().toEpochMilli()); flowImageRepository.save(image); } return FlowImageVOForList.fromEntity(image); } public boolean remove(Integer id, Integer userSignInId) { User user = userService.getUser(userSignInId); FlowImage image = flowImageRepository.findByUserAndId(user, id).orElseThrow(ItemNotFoundException::new); image.setFlow(null); flowImageRepository.save(image); return true; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/flow_images/FlowImageVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.flow_images; import lombok.Getter; import lombok.Setter; @Getter @Setter public class FlowImageVOForList { private Integer id; private String url; private Integer userId; private Integer flowId; public static FlowImageVOForList fromEntity(FlowImage po) { if (po == null) return null; FlowImageVOForList vo = new FlowImageVOForList(); vo.setId(po.getId()); vo.setUrl(po.getHost() + po.getUri()); vo.setUserId(po.getUser().getId()); if (po.getFlow() != null) vo.setFlowId(po.getFlow().getId()); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/flow_images/ImageExistsException.java ================================================ package com.jiukuaitech.bookkeeping.user.flow_images; public class ImageExistsException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/flow_images/UploadCallbackRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.flow_images; import lombok.Getter; import lombok.Setter; @Getter @Setter public class UploadCallbackRequest { private Integer userId; private String key; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/flow_images/UploadKeyEmptyException.java ================================================ package com.jiukuaitech.bookkeeping.user.flow_images; public class UploadKeyEmptyException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/group/Group.java ================================================ package com.jiukuaitech.bookkeeping.user.group; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.user.UserGroupRelation; import com.jiukuaitech.bookkeeping.user.base.NameNotesEnableEntity; import com.jiukuaitech.bookkeeping.user.validation.AvatarValidator; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.NotNull; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "t_group") @Getter @Setter /** * 多个人一个组一起记账。 * 组名name,可以重复。 */ public class Group extends NameNotesEnableEntity { @ManyToOne(optional = true, fetch = FetchType.LAZY) private User creator; @Column(length = 128) @AvatarValidator private String avatar; @OneToMany(mappedBy = "group", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private Set relations = new HashSet<>(); @ManyToOne(optional = true, fetch = FetchType.LAZY) private Book defaultBook; //组默认操作的账本 @Column(nullable = false, length = 8) @NotNull private String defaultCurrencyCode;//默认的币种 public Group() { } public Group(Integer id) { super.setId(id); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/group/GroupAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.group; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.NotesValidator; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotBlank; @Getter @Setter public class GroupAddRequest { @NotBlank(message="name must not be blank") @NameValidator private String name; @NotesValidator private String notes; @NotBlank private String defaultCurrencyCode; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/group/GroupController.java ================================================ package com.jiukuaitech.bookkeeping.user.group; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/groups") public class GroupController { @Resource private GroupService groupService; @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleQuery( @PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable page, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(groupService.query(page, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/enable") public BaseResponse handleEnable(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(groupService.getEnable(userSignInId)); } @RequestMapping(method = RequestMethod.POST, value = "") public BaseResponse handleAdd(@Valid @RequestBody GroupAddRequest groupAddRequest, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(groupService.add(groupAddRequest, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}") public BaseResponse handleUpdate( @PathVariable("id") Integer id, @Valid @RequestBody GroupUpdateRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(groupService.update(id, request, userSignInId)); } @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") public BaseResponse handleDelete(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(groupService.remove(id, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/group/GroupExceptionHandler.java ================================================ package com.jiukuaitech.bookkeeping.user.group; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.ErrorResponse; import org.springframework.context.MessageSource; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.annotation.Resource; import java.util.Locale; @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) public class GroupExceptionHandler { private static final Locale LANG = Locale.CHINA; @Resource private MessageSource messageSource; @ExceptionHandler(value = GroupHasBookException.class) @ResponseBody public BaseResponse handleException(GroupHasBookException e) { return new ErrorResponse(601, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = GroupMaxCountException.class) @ResponseBody public BaseResponse handleException(GroupMaxCountException e) { return new ErrorResponse(602, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/group/GroupHasBookException.java ================================================ package com.jiukuaitech.bookkeeping.user.group; public class GroupHasBookException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/group/GroupMaxCountException.java ================================================ package com.jiukuaitech.bookkeeping.user.group; public class GroupMaxCountException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/group/GroupRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.group; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import com.jiukuaitech.bookkeeping.user.user.User; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository public interface GroupRepository extends BaseRepository { long countByCreator(User creator); Optional findOneByCreatorAndId(User user, Integer id); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/group/GroupService.java ================================================ package com.jiukuaitech.bookkeeping.user.group; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.user.*; import com.jiukuaitech.bookkeeping.user.book.BookRepository; import com.jiukuaitech.bookkeeping.user.currency.CurrencyService; import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException; import com.jiukuaitech.bookkeeping.user.exception.PermissionException; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import javax.annotation.Resource; import javax.persistence.criteria.Join; import javax.persistence.criteria.Predicate; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @Service public class GroupService { @Resource private UserGroupRelationRepository userGroupRelationRepository; @Resource private GroupRepository groupRepository; @Resource private BookRepository bookRepository; @Resource private CurrencyService currencyService; @Resource private UserService userService; @Value("${group.max.count}") private Integer groupMaxCount; public Page query(Pageable page, Integer userSignInId) { Specification specification = (root, query, criteriaBuilder) -> { List predicates = new ArrayList<>(); query.distinct(true); Join join = root.join(Group_.relations); predicates.add(criteriaBuilder.equal(join.get(UserGroupRelation_.user), userSignInId)); return criteriaBuilder.and(predicates.toArray(new Predicate[0])); }; Page poPage = groupRepository.findAll(specification, page); return poPage.map(GroupVOForList::fromEntity); } public List getEnable(Integer userSignInId) { Specification specification = (root, query, criteriaBuilder) -> { List predicates = new ArrayList<>(); Join join = root.join(Group_.relations); predicates.add(criteriaBuilder.equal(join.get(UserGroupRelation_.user), userSignInId)); return criteriaBuilder.and(predicates.toArray(new Predicate[0])); }; List entityList = groupRepository.findAll(specification); return entityList.stream().map(GroupVOForList::fromEntity).collect(Collectors.toList()); } @Transactional public boolean add(GroupAddRequest request, Integer userSignInId) { User user = userService.getUser(userSignInId); if (groupRepository.countByCreator(user) >= groupMaxCount) { throw new GroupMaxCountException(); } currencyService.checkCode(request.getDefaultCurrencyCode()); Group po = new Group(); po.setName(request.getName()); po.setNotes(request.getNotes()); po.setDefaultCurrencyCode(request.getDefaultCurrencyCode()); po.setCreator(user); groupRepository.save(po); Book book = new Book(); book.setName("默认账本"); book.setDefaultCurrencyCode(po.getDefaultCurrencyCode()); book.setGroup(po); bookRepository.save(book); po.setDefaultBook(book); groupRepository.save(po); UserGroupRelation userGroupRelationToAdd = new UserGroupRelation(user, po, 1); userGroupRelationRepository.save(userGroupRelationToAdd); return true; } public boolean update(Integer id, GroupUpdateRequest request, Integer userSignInId) { Group po = groupRepository.findById(id).orElseThrow(ItemNotFoundException::new); UserGroupRelation relation = userGroupRelationRepository.findOneByUserAndGroup(new User(userSignInId), po); if (relation == null) throw new PermissionException("No Permission"); if (StringUtils.hasText(request.getName())) po.setName(request.getName()); if (request.getNotes() != null) po.setNotes(request.getNotes()); groupRepository.save(po); return true; } @Transactional public boolean remove(Integer id, Integer userSignInId) { User user = userService.getUser(userSignInId); Group po = groupRepository.findOneByCreatorAndId(user, id).orElseThrow(ItemNotFoundException::new); po.setDefaultBook(null); bookRepository.deleteByGroup_id(id); // 检查关联性 // if (bookRepository.countByGroup_id(id) > 0) { // throw new GroupHasBookException(); // } // UserGroupRelation relation = userGroupRelationRepository.findOneByUserAndGroup(new User(userSignInId), po); // if (relation == null) throw new PermissionException("No Permission"); userGroupRelationRepository.deleteByGroup(po); groupRepository.delete(po); return true; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/group/GroupUpdateRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.group; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.NotesValidator; import lombok.Getter; import lombok.Setter; @Getter @Setter public class GroupUpdateRequest { @NameValidator private String name; @NotesValidator private String notes; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/group/GroupVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.group; import lombok.Getter; import lombok.Setter; @Getter @Setter public class GroupVOForList { private Integer id; private String name; private String notes; private String defaultCurrencyCode; private Integer role; public static GroupVOForList fromEntity(Group po) { GroupVOForList vo = new GroupVOForList(); vo.setId(po.getId()); vo.setName(po.getName()); vo.setNotes(po.getNotes()); vo.setDefaultCurrencyCode(po.getDefaultCurrencyCode()); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/income/Income.java ================================================ package com.jiukuaitech.bookkeeping.user.income; import com.jiukuaitech.bookkeeping.user.deal.Deal; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Entity @DiscriminatorValue(value = "2") @Getter @Setter public class Income extends Deal { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/income/IncomeController.java ================================================ package com.jiukuaitech.bookkeeping.user.income; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowQueryRequest; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.deal.DealAddRequest; import com.jiukuaitech.bookkeeping.user.deal.DealService; import com.jiukuaitech.bookkeeping.user.deal.DealUpdateRequest; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/incomes") public class IncomeController extends BaseController { @Resource private IncomeService incomeService; @Resource private DealService dealService; @RequestMapping(method = RequestMethod.POST, value = "") public BaseResponse handleAdd( @Valid @RequestBody DealAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { dealService.add(2, request, userSignInId, false); return new BaseResponse(true); } @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleQuery( @Valid BalanceFlowQueryRequest request, @PageableDefault(sort = {"createTime", "id"}, direction = Sort.Direction.DESC) Pageable page, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(incomeService.queryWithDefaultBook(request, page, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}") public BaseResponse handleUpdate( @PathVariable("id") Integer id, @Valid @RequestBody DealUpdateRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(dealService.update(2, id, request, userSignInId)); } @RequestMapping(method = RequestMethod.POST, value = "{id}/refund") public BaseResponse handleRefund( @PathVariable("id") Integer id, @Valid @RequestBody DealAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(dealService.refund(2, id, request, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/income/IncomeRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.income; import com.jiukuaitech.bookkeeping.user.base.HasBookRepository; import com.jiukuaitech.bookkeeping.user.book.Book; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.math.BigDecimal; @Repository public interface IncomeRepository extends HasBookRepository { @Query("SELECT COALESCE(SUM(p.convertedAmount), 0) FROM Income p WHERE p.book = :book AND p.status <> 2 AND p.createTime BETWEEN :start AND :end") BigDecimal findSumAmount(Book book, Long start, Long end); @Query("SELECT COUNT(p) FROM Income p WHERE p.book = :book AND p.status <> 2 AND p.createTime BETWEEN :start AND :end") Integer findCount(Book book, Long start, Long end); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/income/IncomeService.java ================================================ package com.jiukuaitech.bookkeeping.user.income; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowQueryRequest; import com.jiukuaitech.bookkeeping.user.deal.DealQueryResultVO; import com.jiukuaitech.bookkeeping.user.deal.DealSpec; import com.jiukuaitech.bookkeeping.user.deal.DealVOForList; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.user.UserService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import java.math.BigDecimal; @Service public class IncomeService { @Resource private IncomeRepository incomeRepository; @Resource private UserService userService; public DealQueryResultVO queryWithDefaultBook(BalanceFlowQueryRequest request, Pageable page, Integer userSignInId) { User user = userService.getUser(userSignInId); request.setBookId(user.getDefaultBook().getId()); return query(request, page, userSignInId); } public DealQueryResultVO query(BalanceFlowQueryRequest request, Pageable page, Integer userSignInId) { DealQueryResultVO result = new DealQueryResultVO(); Group defaultGroup = userService.getUser(userSignInId).getDefaultGroup(); Specification specification = DealSpec.buildSpecification(request, defaultGroup); Page poPage = incomeRepository.findAll(specification, page); Page voPage = poPage.map(income -> { DealVOForList vo = DealVOForList.fromEntity(income); vo.setCurrencyCode(income.getAccount().getCurrencyCode()); if (income.getBook().getDefaultCurrencyCode().equals(income.getAccount().getCurrencyCode())) { vo.setNeedConvert(false); } else { vo.setNeedConvert(true); vo.setToCurrencyCode(income.getBook().getDefaultCurrencyCode()); } return vo; }); result.setResult(voPage); if (CollectionUtils.isEmpty(request.getTags())) { result.setTotal(incomeRepository.calcAggregate(specification, Income_.convertedAmount, Income.class)); } else { result.setTotal(incomeRepository.findAll(specification).stream().map(Income::getConvertedAmount).reduce(BigDecimal.ZERO, BigDecimal::add)); } return result; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/income_category/IncomeCategory.java ================================================ package com.jiukuaitech.bookkeeping.user.income_category; import javax.persistence.*; import com.jiukuaitech.bookkeeping.user.category.Category; import lombok.NoArgsConstructor; /** * 收入类别 */ @Entity @DiscriminatorValue(value = "2") @NoArgsConstructor public class IncomeCategory extends Category { public IncomeCategory(Integer id) { super(id); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/income_category/IncomeCategoryController.java ================================================ package com.jiukuaitech.bookkeeping.user.income_category; import com.jiukuaitech.bookkeeping.user.category.CategoryAddRequest; import com.jiukuaitech.bookkeeping.user.category.CategoryService; import com.jiukuaitech.bookkeeping.user.category.CategoryUpdateRequest; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.category.CategoryQueryRequest; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/income-categories") public class IncomeCategoryController extends BaseController { @Resource private CategoryService categoryService; @RequestMapping(method = RequestMethod.POST, value = "") public BaseResponse handleAdd( @Valid @RequestBody CategoryAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(categoryService.add(2, request, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/enable") public BaseResponse handleGetAllEnable(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(categoryService.getAllEnable(2, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleQuery(@Valid CategoryQueryRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(categoryService.getAllTree(request, 2, userSignInId)); } // 接口没用上 @RequestMapping(method = RequestMethod.GET, value = "/simple") public BaseResponse handleQuerySimple( @PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable page, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(categoryService.query(2, page, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}") public BaseResponse handleUpdate( @PathVariable("id") Integer id, @Valid @RequestBody CategoryUpdateRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(categoryService.update(2, id, request, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/interceptor/AuthInterceptor.java ================================================ package com.jiukuaitech.bookkeeping.user.interceptor; import com.jiukuaitech.bookkeeping.user.exception.TokenEmptyException; import com.jiukuaitech.bookkeeping.user.exception.TokenNotValidException; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.util.WebUtils; import javax.annotation.Resource; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component public class AuthInterceptor implements HandlerInterceptor { @Resource private ServletContext servletContext; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String userToken = request.getHeader("User-Token"); // try session if (!StringUtils.hasText(userToken)) { userToken = (String) request.getSession().getAttribute("User-Token"); } // try cookie if (!StringUtils.hasText(userToken)) { if (WebUtils.getCookie(request, "User-Token") != null) { userToken = WebUtils.getCookie(request, "User-Token").getValue(); } } if (!StringUtils.hasText(userToken)) { throw new TokenEmptyException(); } Integer userSignInId = (Integer) servletContext.getAttribute(userToken); if (userSignInId == null) throw new TokenNotValidException(); request.setAttribute("userSignInId", userSignInId); return true; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/interceptor/MvcInterceptorConfig.java ================================================ package com.jiukuaitech.bookkeeping.user.interceptor; import org.springframework.context.annotation.Configuration; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.annotation.Resource; import java.util.List; @Configuration public class MvcInterceptorConfig implements WebMvcConfigurer { @Resource private AuthInterceptor authInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor).addPathPatterns("/**") .excludePathPatterns("/signin", "/register", "/flow-images/upload-callback", "/test*", "/currency/initData", "/currency/initData2"); WebMvcConfigurer.super.addInterceptors(registry); } @Override public void addArgumentResolvers(List resolvers) { PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver(); resolver.setOneIndexedParameters(true); resolvers.add(resolver); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/interceptor/StringTrimModule.java ================================================ package com.jiukuaitech.bookkeeping.user.interceptor; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; import org.springframework.stereotype.Component; import java.io.IOException; @Component // 前端参数去前后空格 // https://stackoverflow.com/questions/6852213/can-jackson-be-configured-to-trim-leading-trailing-whitespace-from-all-string-pr/24077444 // https://stackoverflow.com/questions/2691667/can-spring-mvc-trim-all-strings-obtained-from-forms public class StringTrimModule extends SimpleModule { public StringTrimModule() { addDeserializer(String.class, new StdScalarDeserializer(String.class) { @Override public String deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException { return jsonParser.getValueAsString().trim(); } }); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/item/Item.java ================================================ package com.jiukuaitech.bookkeeping.user.item; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.NotesValidator; import com.jiukuaitech.bookkeeping.user.validation.TimeValidator; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.NotNull; @Entity @Table(name = "t_item") @Getter @Setter public class Item extends BaseEntity { @ManyToOne(optional = false, fetch = FetchType.LAZY) @NotNull @JoinColumn(name = "user_id") private User user; @Column(length = 16, nullable = false) @NotNull @NameValidator private String title; @Column(length = 1024) @NotesValidator private String notes; //备注 @Column(nullable = false) @NotNull @TimeValidator private Long startDate; //起始日期 @Column @NotNull @TimeValidator private Long endDate; //结束日期 @Column @TimeValidator private Long nextDate; //下次执行日期 @Column @NotNull private Integer repeatType; //0单次 1每天,2每月 3每年 @Column(name = "c_interval") private Integer interval; //间隔 //总执行次数 @NotNull private Integer totalCount; //已执行次数 @NotNull private Integer runCount; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/item/ItemAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.item; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.TimeValidator; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; @Getter @Setter public class ItemAddRequest { @NotNull private Integer type; @NotBlank(message="name must not be blank") @NameValidator private String title; private String notes; @TimeValidator private Long startDate; @TimeValidator private Long endDate; private Integer repeatType; private Integer interval; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/item/ItemController.java ================================================ package com.jiukuaitech.bookkeeping.user.item; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/items") public class ItemController { @Resource private ItemService itemService; @RequestMapping(method = RequestMethod.POST, value = "") public BaseResponse handleAdd( @Valid @RequestBody ItemAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(itemService.add(request, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}/run") public BaseResponse handleRun(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(itemService.run(id, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}/recall") public BaseResponse handleRecall(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(itemService.recall(id, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleQuery( @Valid ItemQueryRequest request, @PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable page, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(itemService.query(request, page, userSignInId)); } @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") public BaseResponse handleDelete(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(itemService.remove(id, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}") public BaseResponse handleUpdate( @PathVariable("id") Integer id, @Valid @RequestBody ItemUpdateRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(itemService.update(id, request, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/item/ItemCountException.java ================================================ package com.jiukuaitech.bookkeeping.user.item; public class ItemCountException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/item/ItemExceptionHandler.java ================================================ package com.jiukuaitech.bookkeeping.user.item; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.ErrorResponse; import org.springframework.context.MessageSource; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.annotation.Resource; import java.util.Locale; @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) public class ItemExceptionHandler { private static final Locale LANG = Locale.CHINA; @Resource private MessageSource messageSource; @ExceptionHandler(value = ItemCountException.class) @ResponseBody public BaseResponse handleException(ItemCountException e) { return new ErrorResponse(601, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/item/ItemQueryRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.item; public class ItemQueryRequest { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/item/ItemRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.item; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import com.jiukuaitech.bookkeeping.user.user.User; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository public interface ItemRepository extends BaseRepository { Optional findOneByUserAndTitle(User user, String title); Optional findOneByUserAndId(User user, Integer id); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/item/ItemService.java ================================================ package com.jiukuaitech.bookkeeping.user.item; import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException; import com.jiukuaitech.bookkeeping.user.exception.NameExistsException; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.user.UserService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.Calendar; @Service public class ItemService { @Resource private ItemRepository itemRepository; @Resource private UserService userService; public boolean add(ItemAddRequest request, Integer userSignInId) { User user = userService.getUser(userSignInId); if (itemRepository.findOneByUserAndTitle(user, request.getTitle()).isPresent()) { throw new NameExistsException(); } Item po = new Item(); po.setUser(user); po.setTitle(request.getTitle()); po.setNotes(request.getNotes()); po.setStartDate(request.getStartDate()); po.setNextDate(request.getStartDate()); po.setRunCount(0); if (request.getType() == 1) { po.setRepeatType(0); po.setTotalCount(1); po.setEndDate(request.getStartDate()); } else { po.setRepeatType(request.getRepeatType()); po.setEndDate(request.getEndDate()); po.setInterval(request.getInterval()); //计算总执行次数 LocalDate startDate = Instant.ofEpochMilli(po.getStartDate()).atZone(ZoneId.systemDefault()).toLocalDate(); LocalDate endDate = Instant.ofEpochMilli(po.getEndDate()).atZone(ZoneId.systemDefault()).toLocalDate(); int totalCount = 0; switch (request.getRepeatType()) { case 1: Long days = ChronoUnit.DAYS.between( LocalDate.of(startDate.getYear(), startDate.getMonth(), startDate.getDayOfMonth()), LocalDate.of(endDate.getYear(), endDate.getMonth(), endDate.getDayOfMonth()) ); totalCount = (int)(days / po.getInterval()); break; case 2: Long months = ChronoUnit.MONTHS.between( LocalDate.of(startDate.getYear(), startDate.getMonth(), startDate.getDayOfMonth()), LocalDate.of(endDate.getYear(), endDate.getMonth(), endDate.getDayOfMonth()) ); totalCount = (int)(months / po.getInterval()); break; case 3: Long years = ChronoUnit.YEARS.between( LocalDate.of(startDate.getYear(), startDate.getMonth(), startDate.getDayOfMonth()), LocalDate.of(endDate.getYear(), endDate.getMonth(), endDate.getDayOfMonth()) ); totalCount = (int)(years / po.getInterval()); break; } po.setTotalCount(totalCount + 1); po.setRunCount(0); } itemRepository.save(po); return true; } public boolean run(Integer id, Integer userSignInId) { User user = userService.getUser(userSignInId); Item po = itemRepository.findOneByUserAndId(user, id).orElseThrow(ItemNotFoundException::new); if (po.getRunCount() >= po.getTotalCount()) { throw new ItemCountException(); } Calendar nextDate = Calendar.getInstance(); nextDate.setTimeInMillis(po.getNextDate()); switch (po.getRepeatType()) { case 1: nextDate.add(Calendar.DATE, po.getInterval()); break; case 2: nextDate.add(Calendar.MONTH, po.getInterval()); break; case 3: nextDate.add(Calendar.YEAR, po.getInterval()); break; } po.setNextDate(nextDate.getTimeInMillis()); po.setRunCount(po.getRunCount() + 1); itemRepository.save(po); return true; } public boolean recall(Integer id, Integer userSignInId) { User user = userService.getUser(userSignInId); Item po = itemRepository.findOneByUserAndId(user, id).orElseThrow(ItemNotFoundException::new); if (po.getRunCount() <= 0) { throw new ItemCountException(); } Calendar nextDate = Calendar.getInstance(); nextDate.setTimeInMillis(po.getNextDate()); switch (po.getRepeatType()) { case 1: nextDate.add(Calendar.DATE, po.getInterval()*(-1)); break; case 2: nextDate.add(Calendar.MONTH, po.getInterval()*(-1)); break; case 3: nextDate.add(Calendar.YEAR, po.getInterval()*(-1)); break; } po.setNextDate(nextDate.getTimeInMillis()); po.setRunCount(po.getRunCount() - 1); itemRepository.save(po); return true; } public boolean remove(Integer id, Integer userSignInId) { User user = userService.getUser(userSignInId); Item po = itemRepository.findOneByUserAndId(user, id).orElseThrow(ItemNotFoundException::new); itemRepository.delete(po); return true; } public boolean update(Integer id, ItemUpdateRequest request, Integer userSignInId) { User user = userService.getUser(userSignInId); Item po = itemRepository.findOneByUserAndId(user, id).orElseThrow(ItemNotFoundException::new); if (StringUtils.hasText(request.getTitle())) { if (!po.getTitle().equals(request.getTitle())) { if (itemRepository.findOneByUserAndTitle(user, request.getTitle()).isPresent()) { throw new NameExistsException(); } } } if (request.getNotes() != null) po.setNotes(request.getNotes()); if (request.getEndDate() != null) po.setEndDate(request.getEndDate()); //计算总执行次数 LocalDate startDate = Instant.ofEpochMilli(po.getStartDate()).atZone(ZoneId.systemDefault()).toLocalDate(); LocalDate endDate = Instant.ofEpochMilli(po.getEndDate()).atZone(ZoneId.systemDefault()).toLocalDate(); switch (po.getRepeatType()) { case 1: Long days = ChronoUnit.DAYS.between( LocalDate.of(startDate.getYear(), startDate.getMonth(), startDate.getDayOfMonth()), LocalDate.of(endDate.getYear(), endDate.getMonth(), endDate.getDayOfMonth()) ); po.setTotalCount((int)(days / po.getInterval())); break; case 2: Long months = ChronoUnit.MONTHS.between( LocalDate.of(startDate.getYear(), startDate.getMonth(), startDate.getDayOfMonth()), LocalDate.of(endDate.getYear(), endDate.getMonth(), endDate.getDayOfMonth()) ); po.setTotalCount((int)(months / po.getInterval())); break; case 3: Long years = ChronoUnit.YEARS.between( LocalDate.of(startDate.getYear(), startDate.getMonth(), startDate.getDayOfMonth()), LocalDate.of(endDate.getYear(), endDate.getMonth(), endDate.getDayOfMonth()) ); po.setTotalCount((int)(years / po.getInterval())); break; } itemRepository.save(po); return true; } public Page query(ItemQueryRequest request, Pageable page, Integer userSignInId) { Specification specification = ItemSpec.buildSpecification(request, userService.getUser(userSignInId)); Page poPage = itemRepository.findAll(specification, page); return poPage.map(ItemVOForList::fromEntity); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/item/ItemSpec.java ================================================ package com.jiukuaitech.bookkeeping.user.item; import com.jiukuaitech.bookkeeping.user.user.User; import org.springframework.data.jpa.domain.Specification; public final class ItemSpec { public static Specification isUser(User user) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("user"), user); } public static Specification buildSpecification(ItemQueryRequest request, User user) { return isUser(user); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/item/ItemUpdateRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.item; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.TimeValidator; import lombok.Getter; import lombok.Setter; @Getter @Setter public class ItemUpdateRequest { @NameValidator private String title; private String notes; @TimeValidator private Long endDate; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/item/ItemVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.item; import com.jiukuaitech.bookkeeping.user.utils.EnumUtils; import lombok.Getter; import lombok.Setter; import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; import java.time.temporal.ChronoUnit; @Getter @Setter public class ItemVOForList { private Integer id; private String title; private String notes; private Long startDate; private Long endDate; private Integer repeatType; private Integer interval; private String repeatDescription; private Long countDown;//倒计时天数 //下次执行日期 private Long nextDate; //总执行次数 private Integer totalCount; //已执行次数 private Integer runCount; //剩余次数 private Integer remainCount; public static ItemVOForList fromEntity(Item po) { if (po == null) return null; ItemVOForList vo = new ItemVOForList(); vo.setId(po.getId()); vo.setTitle(po.getTitle()); vo.setNotes(po.getNotes()); vo.setStartDate(po.getStartDate()); vo.setEndDate(po.getEndDate()); vo.setNextDate(po.getNextDate()); vo.setRepeatType(po.getRepeatType()); vo.setInterval(po.getInterval()); vo.setTotalCount(po.getTotalCount()); vo.setRunCount(po.getRunCount()); if (po.getRepeatType() == 0) { vo.setRepeatDescription("单次执行"); } else { vo.setRepeatDescription("每" + (po.getInterval() != 1 ? po.getInterval() : "") + EnumUtils.translateItemRepeatType(po.getRepeatType()) + "执行一次"); } LocalDate now = LocalDate.now(); LocalDate nextDate = Instant.ofEpochMilli(vo.getNextDate()).atZone(ZoneId.systemDefault()).toLocalDate(); vo.setCountDown(ChronoUnit.DAYS.between( LocalDate.of(now.getYear(), now.getMonth(), now.getDayOfMonth()), LocalDate.of(nextDate.getYear(), nextDate.getMonth(), nextDate.getDayOfMonth()) )); vo.setRemainCount(po.getTotalCount() - po.getRunCount()); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/payee/Payee.java ================================================ package com.jiukuaitech.bookkeeping.user.payee; import com.jiukuaitech.bookkeeping.user.base.BookNameNotesEnableEntity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.NotNull; @Entity @Table(name = "t_payee", uniqueConstraints = {@UniqueConstraint(columnNames = {"book_id", "name"})}) @Getter @Setter /** * 交易对象 */ public class Payee extends BookNameNotesEnableEntity { @Column(nullable = false) @NotNull private Boolean expenseable = true; //是否可支出 @Column(nullable = false) @NotNull private Boolean incomeable = true; //是否可收入 public Payee() { } public Payee(Integer id) { super.setId(id); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/payee/PayeeAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.payee; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.NotesValidator; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotEmpty; @Getter @Setter public class PayeeAddRequest { @NotEmpty @NameValidator private String name; @NotesValidator private String notes; private Boolean expenseable = false; private Boolean incomeable = false; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/payee/PayeeController.java ================================================ package com.jiukuaitech.bookkeeping.user.payee; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/payees") public class PayeeController extends BaseController { @Resource private PayeeService payeeService; @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleQuery( @PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable page, @Valid PayeeQueryRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(payeeService.query(request, page, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/{id}") public BaseResponse handleGet(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(payeeService.get(id, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/enable") public BaseResponse handleEnable(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(payeeService.getEnable(userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/expenseable") public BaseResponse handleExpenseable(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(payeeService.getExpenseable(userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/incomeable") public BaseResponse handleIncomeable(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(payeeService.getIncomeable(userSignInId)); } @RequestMapping(method = RequestMethod.POST, value = "") public BaseResponse handleAdd( @Valid @RequestBody PayeeAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(payeeService.add(request, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggle") public BaseResponse handleToggle(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(payeeService.toggle(id, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}") public BaseResponse handleUpdate( @PathVariable("id") Integer id, @Valid @RequestBody PayeeUpdateRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(payeeService.update(id, request, userSignInId)); } @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") public BaseResponse handleDelete(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(payeeService.remove(id, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/payee/PayeeExceptionHandler.java ================================================ package com.jiukuaitech.bookkeeping.user.payee; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.ErrorResponse; import org.springframework.context.MessageSource; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.annotation.Resource; import java.util.Locale; @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) public class PayeeExceptionHandler { private static final Locale LANG = Locale.CHINA; @Resource private MessageSource messageSource; @ExceptionHandler(value = PayeeHasDealException.class) @ResponseBody public BaseResponse handleException(PayeeHasDealException e) { return new ErrorResponse(409, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = PayeeMaxCountException.class) @ResponseBody public BaseResponse handleException(PayeeMaxCountException e) { return new ErrorResponse(410, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/payee/PayeeHasDealException.java ================================================ package com.jiukuaitech.bookkeeping.user.payee; /* * 删除时有账单关联 */ public class PayeeHasDealException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/payee/PayeeMaxCountException.java ================================================ package com.jiukuaitech.bookkeeping.user.payee; public class PayeeMaxCountException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/payee/PayeeQueryRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.payee; import lombok.Getter; import lombok.Setter; @Getter @Setter public class PayeeQueryRequest { private String name; private Boolean enable; private Boolean expenseable; private Boolean incomeable; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/payee/PayeeRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.payee; import com.jiukuaitech.bookkeeping.user.base.HasBookRepository; import com.jiukuaitech.bookkeeping.user.book.Book; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Repository public interface PayeeRepository extends HasBookRepository { // 添加时判断名称是否重复 Optional findByBookAndName(Book book, String name); List findByBookAndEnable(Book book, Boolean enable); List findByBookAndEnableAndExpenseable(Book book, Boolean enable, Boolean expenseable); List findByBookAndEnableAndIncomeable(Book book, Boolean enable, Boolean incomeable); long countByBook(Book book); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/payee/PayeeService.java ================================================ package com.jiukuaitech.bookkeeping.user.payee; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.deal.DealRepository; import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException; import com.jiukuaitech.bookkeeping.user.exception.NameExistsException; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.user.UserService; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; import java.util.stream.Collectors; @Service public class PayeeService { @Resource private UserService userService; @Resource private PayeeRepository payeeRepository; @Resource private DealRepository dealRepository; @Value("${payee.max.count}") private Integer maxCount; public Page query(PayeeQueryRequest request, Pageable page, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Specification specification = PayeeSpec.buildSpecification(request, book); Page poPage = payeeRepository.findAll(specification, page); return poPage.map(PayeeVOForList::fromEntity); } public PayeeVOForList get(Integer id, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Payee po = payeeRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); return PayeeVOForList.fromEntity(po); } public List getEnable(Integer userSignInId) { User user = userService.getUser(userSignInId); List entityList = payeeRepository.findByBookAndEnable(user.getDefaultBook(), true); return entityList.stream().map(PayeeVOForList::fromEntity).collect(Collectors.toList()); } public List getExpenseable(Integer userSignInId) { User user = userService.getUser(userSignInId); List entityList = payeeRepository.findByBookAndEnableAndExpenseable(user.getDefaultBook(), true, true); return entityList.stream().map(PayeeVOForList::fromEntity).collect(Collectors.toList()); } public List getIncomeable(Integer userSignInId) { User user = userService.getUser(userSignInId); List entityList = payeeRepository.findByBookAndEnableAndIncomeable(user.getDefaultBook(), true, true); return entityList.stream().map(PayeeVOForList::fromEntity).collect(Collectors.toList()); } public PayeeVOForList add(PayeeAddRequest request, Integer userSignInId) { User user = userService.getUser(userSignInId); Book book = user.getDefaultBook(); // 不能重复 if (payeeRepository.findByBookAndName(book, request.getName()).isPresent()) { throw new NameExistsException(); } if (payeeRepository.countByBook(book) >= maxCount) { throw new PayeeMaxCountException(); } Payee po = new Payee(); po.setName(request.getName()); po.setNotes(request.getNotes()); po.setExpenseable(request.getExpenseable()); po.setIncomeable(request.getIncomeable()); po.setBook(book); return PayeeVOForList.fromEntity(payeeRepository.save(po)); } public PayeeVOForList remove(Integer id, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Payee po = payeeRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); if (dealRepository.countByPayee_id(id) > 0) throw new PayeeHasDealException(); payeeRepository.delete(po); return PayeeVOForList.fromEntity(po); } public PayeeVOForList update(Integer id, PayeeUpdateRequest request, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Payee po = payeeRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); if (!po.getName().equals(request.getName())) { if (payeeRepository.findByBookAndName(po.getBook(), request.getName()).isPresent()) { throw new NameExistsException(); } } if (request.getName() != null) po.setName(request.getName()); if (request.getNotes() != null) po.setNotes(request.getNotes()); if (request.getExpenseable() != null) po.setExpenseable(request.getExpenseable()); if (request.getIncomeable() != null) po.setIncomeable(request.getIncomeable()); return PayeeVOForList.fromEntity(payeeRepository.save(po)); } public PayeeVOForList toggle(Integer id, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Payee po = payeeRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); po.setEnable(!po.getEnable()); return PayeeVOForList.fromEntity(payeeRepository.save(po)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/payee/PayeeSpec.java ================================================ package com.jiukuaitech.bookkeeping.user.payee; import com.jiukuaitech.bookkeeping.user.base.BookNameNotesEnableSpec; import com.jiukuaitech.bookkeeping.user.book.Book; import org.springframework.data.jpa.domain.Specification; import org.springframework.util.StringUtils; public final class PayeeSpec { public static Specification isExpenseable(Boolean expenseable) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Payee_.EXPENSEABLE), expenseable); } public static Specification isIncomeable(Boolean incomeable) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Payee_.INCOMEABLE), incomeable); } public static Specification buildSpecification(PayeeQueryRequest request, Book book) { Specification specification = BookNameNotesEnableSpec.isBook(book); if (StringUtils.hasText(request.getName())) { specification = specification.and(BookNameNotesEnableSpec.nameLike(request.getName())); } if (request.getEnable() != null) { specification = specification.and(BookNameNotesEnableSpec.isEnable(request.getEnable())); } if (request.getExpenseable() != null) { specification = specification.and(isExpenseable(request.getExpenseable())); } if (request.getIncomeable() != null) { specification = specification.and(isIncomeable(request.getIncomeable())); } return specification; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/payee/PayeeUpdateRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.payee; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.NotesValidator; import lombok.Getter; import lombok.Setter; @Getter @Setter public class PayeeUpdateRequest { @NameValidator private String name; @NotesValidator private String notes; private Boolean expenseable; private Boolean incomeable; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/payee/PayeeVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.payee; import lombok.Getter; import lombok.Setter; @Getter @Setter public class PayeeVOForList { private Integer id; private String name; private String notes; private Boolean enable; private Boolean expenseable; private Boolean incomeable; public static PayeeVOForList fromEntity(Payee po) { if (po == null) return null; PayeeVOForList vo = new PayeeVOForList(); vo.setId(po.getId()); vo.setName(po.getName()); vo.setNotes(po.getNotes()); vo.setEnable(po.getEnable()); vo.setExpenseable(po.getExpenseable()); vo.setIncomeable(po.getIncomeable()); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/permission/PermissionCheck.java ================================================ package com.jiukuaitech.bookkeeping.user.permission; import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface PermissionCheck { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/permission/PermissionCheckAspect.java ================================================ package com.jiukuaitech.bookkeeping.user.permission; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.exception.PermissionException; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.user.UserGroupRelation; import com.jiukuaitech.bookkeeping.user.user.UserGroupRelationRepository; import com.jiukuaitech.bookkeeping.user.user.UserRepository; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; @Aspect @Component public class PermissionCheckAspect { @Resource private HttpServletRequest request; @Resource private UserGroupRelationRepository userGroupRelationRepository; @Resource private UserRepository userRepository; @Pointcut("@annotation(com.jiukuaitech.bookkeeping.user.permission.PermissionCheck)") private void permissionCheckCut() { }; @Before("permissionCheckCut()") public void before(JoinPoint joinPoint) { Integer userId = (Integer) request.getAttribute("userSignInId"); User user = userRepository.findById(userId).get(); Book book = user.getDefaultBook(); UserGroupRelation userGroupRelation = userGroupRelationRepository.findOneByUserAndGroup(user, book.getGroup()); if (userGroupRelation == null || userGroupRelation.getRole() == 3) { throw new PermissionException("No Permission"); } } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/refund/Refund.java ================================================ package com.jiukuaitech.bookkeeping.user.refund; import com.jiukuaitech.bookkeeping.user.deal.Deal; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.NotNull; @Entity @Table(name="t_refund") @Getter @Setter @NoArgsConstructor public class Refund extends BaseEntity { @ManyToOne(optional = false, fetch = FetchType.LAZY) @NotNull private Deal deal; @OneToOne(optional = false, fetch = FetchType.LAZY) @NotNull private Deal refund; public Refund(Deal deal, Deal refund) { this.deal = deal; this.refund = refund; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/refund/RefundRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.refund; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import com.jiukuaitech.bookkeeping.user.deal.Deal; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Repository public interface RefundRepository extends BaseRepository { Optional findByRefund(Deal refund); List findByDeal(Deal deal); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/refund/RefundService.java ================================================ package com.jiukuaitech.bookkeeping.user.refund; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.deal.Deal; import com.jiukuaitech.bookkeeping.user.deal.DealRepository; import com.jiukuaitech.bookkeeping.user.user.UserService; import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.util.List; import java.util.stream.Collectors; @Service public class RefundService { @Resource private UserService userService; @Resource private DealRepository dealRepository; @Resource private RefundRepository refundRepository; public List getRefunds(Integer dealId, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Deal po = dealRepository.findOneByBookAndId(book, dealId).orElseThrow(ItemNotFoundException::new); // 不需要验证,返回自己,不验证也不会报错。 // if (po.getStatus() != 3 && po.getAmount().compareTo(BigDecimal.ZERO) > 0) { // throw new StatusNotValidateException(); // } Deal deal; if (po.getAmount().compareTo(BigDecimal.ZERO) > 0) { deal = po; } else { Refund refund = refundRepository.findByRefund(po).orElseThrow(ItemNotFoundException::new); deal = refund.getDeal(); } List refunds = refundRepository.findByDeal(deal); List result = refunds.stream().map(i->i.getRefund().getId()).collect(Collectors.toList()); result.add(deal.getId()); return result; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/reports/BreakOutOfMaxException.java ================================================ package com.jiukuaitech.bookkeeping.user.reports; public class BreakOutOfMaxException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/reports/ChartVO.java ================================================ package com.jiukuaitech.bookkeeping.user.reports; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @Getter @Setter public class ChartVO { private String x; private BigDecimal y; private BigDecimal percent; public ChartVO() { } public ChartVO(String x, BigDecimal y) { this.x = x; this.y = y; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/reports/ChartVO2.java ================================================ package com.jiukuaitech.bookkeeping.user.reports; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @Getter @Setter public class ChartVO2 { private String x1; private String x2; private BigDecimal y; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/reports/ExpenseIncomeTrendQueryRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.reports; import com.jiukuaitech.bookkeeping.user.validation.TimeValidator; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import java.util.Set; @Getter @Setter public class ExpenseIncomeTrendQueryRequest extends TrendTimeQueryRequest { private Set expenseAccounts; private Set expensePayees; private Set expenseCategories; private Set expenseTags; private Set incomeAccounts; private Set incomePayees; private Set incomeCategories; private Set incomeTags; @NotNull @TimeValidator private Long minTime; @NotNull @TimeValidator private Long maxTime; @NotEmpty private String breakType; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/reports/ReportController.java ================================================ package com.jiukuaitech.bookkeeping.user.reports; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowQueryRequest; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.web.bind.annotation.RequestAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/reports") public class ReportController extends BaseController { @Resource private ReportService reportService; @RequestMapping(method = RequestMethod.GET, value = "expense-category") public BaseResponse handleExpenseCategory(@Valid BalanceFlowQueryRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(reportService.reportExpenseCategory(request, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "expense-tag") public BaseResponse handleExpenseTag(@Valid BalanceFlowQueryRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(reportService.reportExpenseTag(request, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "income-category") public BaseResponse handleIncomeCategory(@Valid BalanceFlowQueryRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(reportService.reportIncomeCategory(request, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "income-tag") public BaseResponse handleIncomeTag(@Valid BalanceFlowQueryRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(reportService.reportIncomeTag(request, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "expense-income-trend") public BaseResponse handleTrend(@Valid ExpenseIncomeTrendQueryRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(reportService.reportExpenseIncomeTrend(request, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "asset") public BaseResponse handleAsset(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(reportService.reportAsset(userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "debt") public BaseResponse handleDebt(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(reportService.reportDebt(userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "asset-debt-trend") public BaseResponse handleAssetTrend(@Valid TrendTimeQueryRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(reportService.reportAssetDebtTrend(request, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/reports/ReportService.java ================================================ package com.jiukuaitech.bookkeeping.user.reports; import com.jiukuaitech.bookkeeping.user.account.Account; import com.jiukuaitech.bookkeeping.user.asset_account.AssetAccountRepository; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowQueryRequest; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.category.Category; import com.jiukuaitech.bookkeeping.user.category.CategoryRepository; import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelation; import com.jiukuaitech.bookkeeping.user.checking_account.CheckingAccountRepository; import com.jiukuaitech.bookkeeping.user.credit_account.CreditAccountRepository; import com.jiukuaitech.bookkeeping.user.debt_account.DebtAccountRepository; import com.jiukuaitech.bookkeeping.user.expense.Expense; import com.jiukuaitech.bookkeeping.user.expense.ExpenseRepository; import com.jiukuaitech.bookkeeping.user.expense.Expense_; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.income.Income; import com.jiukuaitech.bookkeeping.user.income.IncomeRepository; import com.jiukuaitech.bookkeeping.user.income.Income_; import com.jiukuaitech.bookkeeping.user.tag.Tag; import com.jiukuaitech.bookkeeping.user.tag.TagRepository; import com.jiukuaitech.bookkeeping.user.tag_relation.TagRelation; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.user.UserService; import com.jiukuaitech.bookkeeping.user.account.AccountSpec; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowSpec; import com.jiukuaitech.bookkeeping.user.balance_log.BalanceLog; import com.jiukuaitech.bookkeeping.user.balance_log.BalanceLogRepository; import com.jiukuaitech.bookkeeping.user.currency.CurrencyService; import com.jiukuaitech.bookkeeping.user.deal.DealSpec; import com.jiukuaitech.bookkeeping.user.utils.CalendarUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.MessageSource; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.SimpleDateFormat; import java.util.*; import java.util.stream.Collectors; @Service public class ReportService { @Resource private UserService userService; @Resource private ExpenseRepository expenseRepository; @Resource private IncomeRepository incomeRepository; @Resource private TagRepository tagRepository; @Resource private CheckingAccountRepository checkingAccountRepository; @Resource private CreditAccountRepository creditAccountRepository; @Resource private DebtAccountRepository debtAccountRepository; @Resource private AssetAccountRepository assetAccountRepository; @Resource private BalanceLogRepository balanceLogRepository; @Resource private CategoryRepository categoryRepository; @Resource private CurrencyService currencyService; @Resource private MessageSource messageSource; @Value("${trend.time.max.break}") private Integer maxBreak; public List reportExpenseCategory(BalanceFlowQueryRequest request, Integer userSignInId) { List result = new ArrayList<>(); User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); Book book = user.getDefaultBook(); List categories = categoryRepository.findAllByBookAndType(book, 1); Category requestCategory = null; List rootCategories; if (request.getCategoryId() == null) { rootCategories = categories.stream().filter(i->i.getLevel() == 0).collect(Collectors.toList()); } else { requestCategory = categories.stream().filter(i-> i.getId().equals(request.getCategoryId())).collect(Collectors.toList()).get(0); rootCategories = requestCategory.getOffspring(categories); } request.setBookId(book.getId()); Specification specification = DealSpec.buildSpecification(request, group); // 不统计待确认的状态 specification = specification.and(BalanceFlowSpec.statusConfirmed()); List expenses = expenseRepository.findAll(specification); List expenseTos = new ArrayList<>(); expenses.forEach(i -> expenseTos.addAll(i.getCategories())); rootCategories.forEach(i -> { ChartVO vo = new ChartVO(); vo.setX(i.getName()); List offSpringCategoryIds = i.getOffspring(categories).stream().map(j->j.getId()).collect(Collectors.toList()); offSpringCategoryIds.add(i.getId()); vo.setY(expenseTos.stream().filter(j -> offSpringCategoryIds.contains(j.getCategory().getId())).map(CategoryRelation::getConvertedAmount).reduce(BigDecimal.ZERO, BigDecimal::add)); if (vo.getY().compareTo(BigDecimal.ZERO) > 0) { result.add(vo); } }); if (request.getCategoryId() != null) { ChartVO vo = new ChartVO(); if (rootCategories.size() > 0) { vo.setX(messageSource.getMessage("categoryOther", null, Locale.CHINA)); } else { //只有一个分类,不用其他名字 vo.setX(requestCategory.getName()); } vo.setY(expenseTos.stream().filter(i -> i.getCategory().getId().equals(request.getCategoryId())).map(CategoryRelation::getConvertedAmount).reduce(BigDecimal.ZERO, BigDecimal::add)); if (vo.getY().signum() > 0) { result.add(vo); } } result.sort(Comparator.comparing(ChartVO::getY).reversed()); BigDecimal total = result.stream().map(ChartVO::getY).reduce(BigDecimal.ZERO, BigDecimal::add); result.forEach(vo -> vo.setPercent(vo.getY().multiply(new BigDecimal(100)).divide(total, 2, RoundingMode.HALF_UP))); return result; } public List reportExpenseTag(BalanceFlowQueryRequest request, Integer userSignInId) { List result = new ArrayList<>(); User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); Book book = user.getDefaultBook(); List tags = tagRepository.findByBookAndEnableAndExpenseable(book, true, true); List rootTags = new ArrayList<>(); List requestTags = new ArrayList<>(); Tag requestTag = null; if (CollectionUtils.isEmpty(request.getTags())) { rootTags = tags.stream().filter(i->i.getLevel() == 0).collect(Collectors.toList()); } else { requestTags.addAll(request.getTags()); if (requestTags.size() == 1) { requestTag = tags.stream().filter(i-> i.getId().equals(requestTags.iterator().next())).collect(Collectors.toList()).get(0); rootTags = requestTag.getOffspring(tags); request.getTags().addAll(rootTags.stream().map(j->j.getId()).collect(Collectors.toList())); } else { // for (Integer id : request.getTags()) { // rootTags.add(tagRepository.findById(id).orElseThrow(ItemNotFoundException::new)); // } for (Integer i : requestTags) { Tag tag = tags.stream().filter(j -> j.getId().equals(i)).collect(Collectors.toList()).get(0); rootTags.add(tag); request.getTags().addAll(tag.getOffspring(tags).stream().map(j->j.getId()).collect(Collectors.toList())); } } } request.setBookId(book.getId()); Specification specification = DealSpec.buildSpecification(request, group); // 不统计待确认的状态 specification = specification.and(BalanceFlowSpec.statusConfirmed()); List expenses = expenseRepository.findAll(specification); List tagRelations = new ArrayList<>(); expenses.forEach(i->tagRelations.addAll(i.getTags())); rootTags.forEach(i -> { ChartVO vo = new ChartVO(); vo.setX(i.getName()); List offSpringIds = i.getOffspring(tags).stream().map(j->j.getId()).collect(Collectors.toList()); offSpringIds.add(i.getId()); vo.setY(tagRelations.stream().filter(j -> offSpringIds.contains(j.getTag().getId())).map(TagRelation::getConvertedAmount).reduce(BigDecimal.ZERO, BigDecimal::add)); result.add(vo); }); if (!CollectionUtils.isEmpty(requestTags) && requestTags.size() == 1) { ChartVO vo = new ChartVO(); if (rootTags.size() > 0) { vo.setX(messageSource.getMessage("categoryOther", null, Locale.CHINA)); } else { //只有一个分类,不用其他名字 vo.setX(requestTag.getName()); } Tag finalRequestTag = requestTag; vo.setY(tagRelations.stream().filter(i -> i.getTag().getId().equals(finalRequestTag.getId())).map(TagRelation::getConvertedAmount).reduce(BigDecimal.ZERO, BigDecimal::add)); if (vo.getY().signum() > 0) { result.add(vo); } } return result; } public List reportIncomeCategory(BalanceFlowQueryRequest request, Integer userSignInId) { List result = new ArrayList<>(); User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); Book book = user.getDefaultBook(); List categories = categoryRepository.findAllByBookAndType(book, 2); Category requestCategory = null; List rootCategories; if (request.getCategoryId() == null) { rootCategories = categories.stream().filter(i->i.getLevel() == 0).collect(Collectors.toList()); } else { requestCategory = categories.stream().filter(i-> i.getId().equals(request.getCategoryId())).collect(Collectors.toList()).get(0); rootCategories = requestCategory.getOffspring(categories); } request.setBookId(book.getId()); Specification specification = DealSpec.buildSpecification(request, group); // 不统计待确认的状态 specification = specification.and(BalanceFlowSpec.statusConfirmed()); List incomes = incomeRepository.findAll(specification); List incomeFroms = new ArrayList<>(); incomes.forEach(i -> incomeFroms.addAll(i.getCategories())); rootCategories.forEach(i -> { ChartVO vo = new ChartVO(); vo.setX(i.getName()); List offSpringCategoryIds = i.getOffspring(categories).stream().map(j->j.getId()).collect(Collectors.toList()); offSpringCategoryIds.add(i.getId()); vo.setY(incomeFroms.stream().filter(j -> offSpringCategoryIds.contains(j.getCategory().getId())).map(CategoryRelation::getConvertedAmount).reduce(BigDecimal.ZERO, BigDecimal::add)); if (vo.getY().compareTo(BigDecimal.ZERO) > 0) { result.add(vo); } }); if (request.getCategoryId() != null) { ChartVO vo = new ChartVO(); if (result.size() > 0) { vo.setX(messageSource.getMessage("categoryOther", null, Locale.CHINA)); } else { //只有一个分类,不用其他名字 vo.setX(requestCategory.getName()); } vo.setY(incomeFroms.stream().filter(i -> i.getCategory().getId().equals(request.getCategoryId())).map(CategoryRelation::getConvertedAmount).reduce(BigDecimal.ZERO, BigDecimal::add)); if (vo.getY().compareTo(BigDecimal.valueOf(0)) > 0) { result.add(vo); } } result.sort(Comparator.comparing(ChartVO::getY).reversed()); BigDecimal total = result.stream().map(ChartVO::getY).reduce(BigDecimal.ZERO, BigDecimal::add); result.forEach(vo -> vo.setPercent(vo.getY().multiply(new BigDecimal(100)).divide(total, 2, RoundingMode.HALF_UP))); return result; } public List reportIncomeTag(BalanceFlowQueryRequest request, Integer userSignInId) { List result = new ArrayList<>(); User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); Book book = user.getDefaultBook(); List tags = tagRepository.findByBookAndEnableAndIncomeable(book, true, true); List rootTags = new ArrayList<>(); List requestTags = new ArrayList<>(); Tag requestTag = null; if (CollectionUtils.isEmpty(request.getTags())) { rootTags = tags.stream().filter(i->i.getLevel() == 0).collect(Collectors.toList()); } else { requestTags.addAll(request.getTags()); if (requestTags.size() == 1) { requestTag = tags.stream().filter(i-> i.getId().equals(requestTags.iterator().next())).collect(Collectors.toList()).get(0); rootTags = requestTag.getOffspring(tags); request.getTags().addAll(rootTags.stream().map(j->j.getId()).collect(Collectors.toList())); } else { for (Integer i : requestTags) { Tag tag = tags.stream().filter(j -> j.getId().equals(i)).collect(Collectors.toList()).get(0); rootTags.add(tag); request.getTags().addAll(tag.getOffspring(tags).stream().map(j->j.getId()).collect(Collectors.toList())); } } } request.setBookId(book.getId()); Specification specification = DealSpec.buildSpecification(request, group); // 不统计待确认的状态 specification = specification.and(BalanceFlowSpec.statusConfirmed()); List incomes = incomeRepository.findAll(specification); List tagRelations = new ArrayList<>(); incomes.forEach(i->tagRelations.addAll(i.getTags())); rootTags.forEach(i -> { ChartVO vo = new ChartVO(); vo.setX(i.getName()); List offSpringIds = i.getOffspring(tags).stream().map(j->j.getId()).collect(Collectors.toList()); offSpringIds.add(i.getId()); vo.setY(tagRelations.stream().filter(j -> offSpringIds.contains(j.getTag().getId())).map(TagRelation::getConvertedAmount).reduce(BigDecimal.ZERO, BigDecimal::add)); result.add(vo); }); if (!CollectionUtils.isEmpty(requestTags) && requestTags.size() == 1) { ChartVO vo = new ChartVO(); if (rootTags.size() > 0) { vo.setX(messageSource.getMessage("categoryOther", null, Locale.CHINA)); } else { //只有一个分类,不用其他名字 vo.setX(requestTag.getName()); } Tag finalRequestTag = requestTag; vo.setY(tagRelations.stream().filter(i -> i.getTag().getId().equals(finalRequestTag.getId())).map(TagRelation::getConvertedAmount).reduce(BigDecimal.ZERO, BigDecimal::add)); if (vo.getY().signum() > 0) { result.add(vo); } } return result; } private String breakTypeToKey(String type, Calendar calendar) { switch (type) { case "day": return new SimpleDateFormat("yy/MM/dd").format(calendar.getTime()); case "week": // TODO 国际化 美国的周日是第一天 calendar.setFirstDayOfWeek(Calendar.MONDAY); return calendar.get(Calendar.YEAR) + "W" + calendar.get(Calendar.WEEK_OF_YEAR); case "month": return new SimpleDateFormat("yy/MM").format(calendar.getTime()); case "quarter": return calendar.get(Calendar.YEAR) + "Q" + ((calendar.get(Calendar.MONTH) / 3) + 1); case "year": return String.valueOf(calendar.get(Calendar.YEAR)); default: return ""; } } public List reportExpenseIncomeTrend(ExpenseIncomeTrendQueryRequest request, Integer userSignInId) { List result = new ArrayList<>(); User user = userService.getUser(userSignInId); Group group = user.getDefaultGroup(); Book book = user.getDefaultBook(); if (request.getMinTime() == null) { request.setMinTime(CalendarUtils.getIn1Year()[0]); } List breaks = CalendarUtils.getBreaks(request.getMinTime(), request.getMaxTime(), request.getBreakType()); if (breaks.size() > maxBreak) throw new BreakOutOfMaxException(); List expenseMap = new ArrayList<>(); BalanceFlowQueryRequest expenseQueryRequest = new BalanceFlowQueryRequest(); expenseQueryRequest.setAccounts(request.getExpenseAccounts()); expenseQueryRequest.setPayees(request.getExpensePayees()); expenseQueryRequest.setCategories(request.getExpenseCategories()); expenseQueryRequest.setTags(request.getExpenseTags()); expenseQueryRequest.setBookId(book.getId()); breaks.forEach(i -> { expenseQueryRequest.setMinTime(i[0].getTimeInMillis()); expenseQueryRequest.setMaxTime(i[1].getTimeInMillis()); Specification specification = DealSpec.buildSpecification(expenseQueryRequest, group); // 不统计待确认的状态 specification = specification.and(BalanceFlowSpec.statusConfirmed()); BigDecimal amount = expenseRepository.calcAggregate(specification, Expense_.convertedAmount, Expense.class); ChartVO vo = new ChartVO(breakTypeToKey(request.getBreakType(), i[0]), amount); expenseMap.add(vo); }); List incomeMap = new ArrayList<>(); BalanceFlowQueryRequest incomeQueryRequest = new BalanceFlowQueryRequest(); incomeQueryRequest.setAccounts(request.getIncomeAccounts()); incomeQueryRequest.setPayees(request.getIncomePayees()); incomeQueryRequest.setCategories(request.getIncomeCategories()); incomeQueryRequest.setTags(request.getIncomeTags()); incomeQueryRequest.setBookId(book.getId()); breaks.forEach(i -> { incomeQueryRequest.setMinTime(i[0].getTimeInMillis()); incomeQueryRequest.setMaxTime(i[1].getTimeInMillis()); Specification specification = DealSpec.buildSpecification(incomeQueryRequest, group); // 不统计待确认的状态 specification = specification.and(BalanceFlowSpec.statusConfirmed()); BigDecimal amount = incomeRepository.calcAggregate(specification, Income_.convertedAmount, Income.class); ChartVO vo = new ChartVO(breakTypeToKey(request.getBreakType(), i[0]), amount); incomeMap.add(vo); }); for (int i = 0; i < breaks.size(); i++) { ChartVO2 vo1 = new ChartVO2(); vo1.setX1(expenseMap.get(i).getX()); vo1.setX2(messageSource.getMessage("expense", null, Locale.CHINA)); vo1.setY(expenseMap.get(i).getY()); result.add(vo1); ChartVO2 vo2 = new ChartVO2(); vo2.setX1(incomeMap.get(i).getX()); vo2.setX2(messageSource.getMessage("income", null, Locale.CHINA)); vo2.setY(incomeMap.get(i).getY()); result.add(vo2); ChartVO2 vo3 = new ChartVO2(); vo3.setX1(incomeMap.get(i).getX()); vo3.setX2(messageSource.getMessage("remain", null, Locale.CHINA)); vo3.setY(incomeMap.get(i).getY().subtract(expenseMap.get(i).getY())); result.add(vo3); } return result; } public List reportAsset(Integer userSignInId) { List result = new ArrayList<>(); Group group = userService.getUser(userSignInId).getDefaultGroup(); List accounts = new ArrayList<>(); accounts.addAll(checkingAccountRepository.findAll(AccountSpec.inGroupAndInclude(group))); accounts.addAll(assetAccountRepository.findAll(AccountSpec.inGroupAndInclude(group))); accounts.forEach(i -> { if (i.getBalance().compareTo(BigDecimal.ZERO) > 0) { ChartVO vo = new ChartVO(); vo.setX(i.getName()); vo.setY(currencyService.convert(i.getBalance(), i.getCurrencyCode(), group.getDefaultCurrencyCode())); result.add(vo); } }); result.sort(Comparator.comparing(ChartVO::getY).reversed()); BigDecimal total = result.stream().map(ChartVO::getY).reduce(BigDecimal.ZERO, BigDecimal::add); result.forEach(vo -> vo.setPercent(vo.getY().multiply(new BigDecimal(100)).divide(total, 2, RoundingMode.HALF_UP))); return result; } public List reportDebt(Integer userSignInId) { List result = new ArrayList<>(); Group group = userService.getUser(userSignInId).getDefaultGroup(); List accounts = new ArrayList<>(); accounts.addAll(creditAccountRepository.findAll(AccountSpec.inGroupAndInclude(group))); accounts.addAll(debtAccountRepository.findAll(AccountSpec.inGroupAndInclude(group))); accounts.forEach(i -> { if (i.getBalance().compareTo(BigDecimal.ZERO) < 0) { ChartVO vo = new ChartVO(); vo.setX(i.getName()); vo.setY(currencyService.convert(i.getBalance(), i.getCurrencyCode(), group.getDefaultCurrencyCode()).negate()); result.add(vo); } }); result.sort(Comparator.comparing(ChartVO::getY).reversed()); BigDecimal total = result.stream().map(ChartVO::getY).reduce(BigDecimal.ZERO, BigDecimal::add); result.forEach(vo -> vo.setPercent(vo.getY().multiply(new BigDecimal(100)).divide(total, 2, RoundingMode.HALF_UP))); return result; } public List reportAssetDebtTrend(TrendTimeQueryRequest request, Integer userSignInId) { List result = new ArrayList<>(); Group group = userService.getUser(userSignInId).getDefaultGroup(); if (request.getMinTime() == null) { request.setMinTime(CalendarUtils.getIn1Year()[0]); } List logs = balanceLogRepository.findByGroupAndCreateTimeBetweenOrderByCreateTime(group, request.getMinTime(), request.getMaxTime()); List assetMap = new ArrayList<>(); List debtMap = new ArrayList<>(); logs.forEach(i -> { ChartVO vo1 = new ChartVO(); vo1.setX(new SimpleDateFormat("yy/MM/dd").format(i.getCreateTime())); vo1.setY(i.getAsset()); assetMap.add(vo1); ChartVO vo2 = new ChartVO(); vo2.setX(new SimpleDateFormat("yy/MM/dd").format(i.getCreateTime())); vo2.setY(i.getDebt()); debtMap.add(vo2); }); for (int i = 0; i < logs.size(); i++) { ChartVO2 vo1 = new ChartVO2(); vo1.setX1(assetMap.get(i).getX()); vo1.setX2(messageSource.getMessage("asset", null, Locale.CHINA)); vo1.setY(assetMap.get(i).getY()); result.add(vo1); ChartVO2 vo2 = new ChartVO2(); vo2.setX1(debtMap.get(i).getX()); vo2.setX2(messageSource.getMessage("debt", null, Locale.CHINA)); vo2.setY(debtMap.get(i).getY()); result.add(vo2); ChartVO2 vo3 = new ChartVO2(); vo3.setX1(assetMap.get(i).getX()); vo3.setX2(messageSource.getMessage("netWorth", null, Locale.CHINA)); vo3.setY(assetMap.get(i).getY().subtract(debtMap.get(i).getY())); result.add(vo3); } return result; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/reports/ReportsExceptionHandler.java ================================================ package com.jiukuaitech.bookkeeping.user.reports; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.ErrorResponse; import org.springframework.context.MessageSource; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.annotation.Resource; import java.util.Locale; @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) public class ReportsExceptionHandler { private static final Locale LANG = Locale.CHINA; @Resource private MessageSource messageSource; @ExceptionHandler(value = BreakOutOfMaxException.class) @ResponseBody public BaseResponse handleException(BreakOutOfMaxException e) { return new ErrorResponse(602, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/reports/TrendTimeQueryRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.reports; import lombok.Getter; import lombok.Setter; import java.time.Instant; @Getter @Setter public class TrendTimeQueryRequest { private Long minTime; private Long maxTime = Instant.now().toEpochMilli(); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/response/BaseResponse.java ================================================ package com.jiukuaitech.bookkeeping.user.response; import lombok.Getter; import lombok.Setter; @Getter @Setter public class BaseResponse { private boolean success; public BaseResponse(boolean success) { this.success = success; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/response/DataResponse.java ================================================ package com.jiukuaitech.bookkeeping.user.response; import lombok.Getter; import lombok.Setter; @Getter @Setter public class DataResponse extends BaseResponse { private T data; public DataResponse() { super(true); } public DataResponse(T data) { super(true); this.data = data; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/response/ErrorResponse.java ================================================ package com.jiukuaitech.bookkeeping.user.response; import lombok.Getter; import lombok.Setter; @Getter @Setter public class ErrorResponse extends BaseResponse { private Integer errorCode; private String errorMsg; public ErrorResponse() { super(false); } public ErrorResponse(Integer errorCode) { super(false); this.errorCode = errorCode; } public ErrorResponse(Integer errorCode, String errorMsg) { super(false); this.errorCode = errorCode; this.errorMsg = errorMsg; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/response/HasNameVO.java ================================================ package com.jiukuaitech.bookkeeping.user.response; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; @Getter @Setter @AllArgsConstructor public class HasNameVO { private Integer id; private String name; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/scheduled/Scheduled.java ================================================ package com.jiukuaitech.bookkeeping.user.scheduled; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import com.jiukuaitech.bookkeeping.user.expense.Expense; import com.jiukuaitech.bookkeeping.user.income.Income; import com.jiukuaitech.bookkeeping.user.transfer.Transfer; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.TimeValidator; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.NotNull; @Entity @Table(name="t_scheduled") @Getter @Setter public class Scheduled extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Column(length = 16, nullable = false, name = "name") @NotNull @NameValidator private String name; @Column(nullable = false) @NotNull @TimeValidator private Long startTime; @Column(nullable = false) private Integer endType; // 1 永不结束 2 设置结束日期 3 设置执行次数 @TimeValidator private Long endTime; //结束日期 private Integer loopTimes; //执行次数 @Column(nullable = false) private Integer frequencyType;// 1 每天 2 每周 3 每月 private Integer hour; private Integer minute; private Boolean autoConfirmed = true; @Column(nullable = false) private Integer type; // 1 支出 2 收入 3 转账 @OneToOne private Expense expense; @OneToOne private Income income; @OneToOne private Transfer transfer; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/scheduled/ScheduledAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.scheduled; import lombok.Getter; import lombok.Setter; @Getter @Setter public class ScheduledAddRequest { private String name; private Long startTime; private Integer endType; private Long endTime; private Integer loopTimes; private Integer frequencyType; private Integer hour; private Integer minute; private Boolean autoConfirmed; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/scheduled/ScheduledController.java ================================================ package com.jiukuaitech.bookkeeping.user.scheduled; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/scheduleds") public class ScheduledController { @Resource private ScheduledService scheduledService; @RequestMapping(method = RequestMethod.POST, value = "") public BaseResponse handleAdd(@Valid @RequestBody ScheduledExpenseAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(scheduledService.addExpense(request, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/scheduled/ScheduledExpenseAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.scheduled; import lombok.Getter; import lombok.Setter; @Getter @Setter public class ScheduledExpenseAddRequest extends ScheduledAddRequest { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/scheduled/ScheduledRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.scheduled; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import org.springframework.stereotype.Repository; @Repository public interface ScheduledRepository extends BaseRepository { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/scheduled/ScheduledService.java ================================================ package com.jiukuaitech.bookkeeping.user.scheduled; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class ScheduledService { @Resource private ScheduledRepository scheduledRepository; public boolean addExpense(ScheduledExpenseAddRequest request, Integer userSignInId) { return true; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/setting/Setting.java ================================================ package com.jiukuaitech.bookkeeping.user.setting; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.NotNull; @Entity @Table(name = "t_setting", uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "c_key"})}) @Getter @Setter public class Setting extends BaseEntity { @Column(name= "c_key", length = 16, nullable = false, unique = true) @NotNull @NameValidator private String key; @Column(length = 16, nullable = false) @NotNull @NameValidator private String value; @ManyToOne(optional = false, fetch = FetchType.LAZY) @JoinColumn(name = "user_id") @NotNull private User user; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag/Tag.java ================================================ package com.jiukuaitech.bookkeeping.user.tag; import com.jiukuaitech.bookkeeping.user.base.BookNameNotesEnableEntity; import lombok.Getter; import lombok.Setter; import org.springframework.util.CollectionUtils; import javax.persistence.*; import javax.validation.constraints.NotNull; import java.util.ArrayList; import java.util.List; @Entity @Table(name = "t_tag", uniqueConstraints = {@UniqueConstraint(columnNames = {"book_id", "name"})}) @Getter @Setter /** * 账单标签 */ public class Tag extends BookNameNotesEnableEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") private Tag parent; @Column(nullable = false) @NotNull private Integer level; @Column(nullable = false) @NotNull private Boolean expenseable = true; //是否可支出 @Column(nullable = false) @NotNull private Boolean incomeable = true; //是否可收入 @Column(nullable = false) @NotNull private Boolean transferable = true; //是否可转账 @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private List children = new ArrayList<>(); public Tag() { } public Tag(Integer id) { super.setId(id); } public List getChildren(List tags) { List result = new ArrayList<>(); for (Tag item : tags) { if (item.getParent() != null && this.getId().equals(item.getParent().getId())) { result.add(item); } } return result; } public List getOffspring(List tags) { List result = new ArrayList<>(); List children = this.getChildren(tags); if (!CollectionUtils.isEmpty(children)) { result.addAll(children); for (Tag item : children) { result.addAll(item.getOffspring(tags)); } } return result; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag/TagAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.tag; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.NotesValidator; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotEmpty; @Getter @Setter public class TagAddRequest { private Integer parentId; @NotEmpty @NameValidator private String name; @NotesValidator private String notes; private Boolean expenseable = false; private Boolean incomeable = false; private Boolean transferable = false; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag/TagController.java ================================================ package com.jiukuaitech.bookkeeping.user.tag; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/tags") public class TagController extends BaseController { @Resource private TagService tagService; @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleQuery( @Valid TagQueryRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(tagService.getAllTree(request, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/{id}") public BaseResponse handleGet(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(tagService.get(id, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/enable") public BaseResponse handleEnable(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(tagService.getAllEnable(userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/expenseable") public BaseResponse handleExpenseable(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(tagService.getExpenseable(userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/incomeable") public BaseResponse handleIncomeable(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(tagService.getIncomeable(userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "/transferable") public BaseResponse handleTransferable(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(tagService.getTransferable(userSignInId)); } @RequestMapping(method = RequestMethod.POST, value = "") public BaseResponse handleAdd( @Valid @RequestBody TagAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(tagService.add(request, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggle") public BaseResponse handleToggle(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(tagService.toggle(id, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}") public BaseResponse handleUpdate( @PathVariable("id") Integer id, @Valid @RequestBody TagUpdateRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(tagService.update(id, request, userSignInId)); } @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") public BaseResponse handleDelete(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(tagService.remove(id, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag/TagExceptionHandler.java ================================================ package com.jiukuaitech.bookkeeping.user.tag; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.ErrorResponse; import org.springframework.context.MessageSource; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.annotation.Resource; import java.util.Locale; @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) public class TagExceptionHandler { private static final Locale LANG = Locale.CHINA; @Resource private MessageSource messageSource; @ExceptionHandler(value = TagHasTransactionException.class) @ResponseBody public BaseResponse handleException(TagHasTransactionException e) { return new ErrorResponse(409, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = TagMaxCountException.class) @ResponseBody public BaseResponse handleException(TagMaxCountException e) { return new ErrorResponse(410, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag/TagHasTransactionException.java ================================================ package com.jiukuaitech.bookkeeping.user.tag; /* 删除时有账单关联 */ public class TagHasTransactionException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag/TagMaxCountException.java ================================================ package com.jiukuaitech.bookkeeping.user.tag; public class TagMaxCountException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag/TagQueryRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.tag; import lombok.Getter; import lombok.Setter; @Getter @Setter public class TagQueryRequest { private String name; private Boolean enable; private Boolean expenseable; private Boolean incomeable; private Boolean transferable; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag/TagRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.tag; import com.jiukuaitech.bookkeeping.user.base.HasBookRepository; import com.jiukuaitech.bookkeeping.user.book.Book; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Repository public interface TagRepository extends HasBookRepository { // 添加时判断名称是否重复 Optional findByBookAndName(Book book, String name); List findByBookAndEnable(Book book, Boolean enable); List findByBookAndEnableAndExpenseable(Book book, Boolean enable, Boolean expenseable); List findByBookAndEnableAndIncomeable(Book book, Boolean enable, Boolean incomeable); List findByBookAndEnableAndTransferable(Book book, Boolean enable, Boolean transferable); long countByBook(Book book); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag/TagService.java ================================================ package com.jiukuaitech.bookkeeping.user.tag; import com.jiukuaitech.bookkeeping.user.base.BookNameNotesEnableSpec; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.category.CategoryLevelException; import com.jiukuaitech.bookkeeping.user.category.ParentCategoryNotEnableException; import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException; import com.jiukuaitech.bookkeeping.user.exception.NameExistsException; import com.jiukuaitech.bookkeeping.user.tag_relation.TagRelationRepository; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.user.UserService; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; import java.util.stream.Collectors; @Service public class TagService { @Resource private UserService userService; @Resource private TagRepository tagRepository; @Resource private TagRelationRepository tagRelationRepository; @Value("${category.max.level}") private Integer maxLevel; @Value("${tag.max.count}") private Integer maxCount; public List getAllTree(TagQueryRequest request, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Specification specification = TagSpec.buildSpecification(request, book); List entityList = tagRepository.findAll(specification); return TagTreeVO.valueOfList(entityList); } public TagTreeVO get(Integer id, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Tag tag = tagRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); return TagTreeVO.valueOf(tag); } public List getAllEnable(Integer userSignInId) { User user = userService.getUser(userSignInId); List entityList = tagRepository.findByBookAndEnable(user.getDefaultBook(), true); return entityList.stream().map(TagVOForList::fromEntity).collect(Collectors.toList()); } public List getExpenseable(Integer userSignInId) { User user = userService.getUser(userSignInId); List entityList = tagRepository.findByBookAndEnableAndExpenseable(user.getDefaultBook(), true, true); return entityList.stream().map(TagVOForList::fromEntity).collect(Collectors.toList()); } public List getIncomeable(Integer userSignInId) { User user = userService.getUser(userSignInId); List entityList = tagRepository.findByBookAndEnableAndIncomeable(user.getDefaultBook(), true, true); return entityList.stream().map(TagVOForList::fromEntity).collect(Collectors.toList()); } public List getTransferable(Integer userSignInId) { User user = userService.getUser(userSignInId); List entityList = tagRepository.findByBookAndEnableAndTransferable(user.getDefaultBook(), true, true); return entityList.stream().map(TagVOForList::fromEntity).collect(Collectors.toList()); } public TagVOForList add(TagAddRequest request, Integer userSignInId) { User user = userService.getUser(userSignInId); Book book = user.getDefaultBook(); Tag parent = null; if (request.getParentId() != null) { parent = tagRepository.findOneByBookAndId(book, request.getParentId()).orElseThrow(ItemNotFoundException::new); if (!parent.getEnable()) { throw new ParentCategoryNotEnableException(); } if (parent.getLevel().equals(maxLevel-1)) { throw new CategoryLevelException(); } } if (tagRepository.countByBook(book) >= maxCount) { throw new TagMaxCountException(); } // 不能重复 if (tagRepository.findByBookAndName(book, request.getName()).isPresent()) { throw new NameExistsException(); } Tag po = new Tag(); po.setName(request.getName()); po.setNotes(request.getNotes()); if (parent == null) { po.setExpenseable(request.getExpenseable()); po.setIncomeable(request.getIncomeable()); po.setTransferable(request.getTransferable()); } else { po.setExpenseable(parent.getExpenseable()); po.setIncomeable(parent.getIncomeable()); po.setTransferable(parent.getTransferable()); } po.setBook(book); po.setParent(parent); if (parent == null) po.setLevel(0); else po.setLevel(parent.getLevel()+1); return TagVOForList.fromEntity(tagRepository.save(po)); } public TagVOForList remove(Integer id, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Tag po = tagRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); // 检查关联性,有账单关联的不能删除 if (tagRelationRepository.countByTag_id(id) > 0) throw new TagHasTransactionException(); tagRepository.delete(po); return TagVOForList.fromEntity(po); } public TagVOForList update(Integer id, TagUpdateRequest request, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Tag po = tagRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); if (!po.getName().equals(request.getName())) { if (tagRepository.findByBookAndName(po.getBook(), request.getName()).isPresent()) { throw new NameExistsException(); } } if (request.getName() != null) po.setName(request.getName()); if (request.getNotes() != null) po.setNotes(request.getNotes()); Tag parent = null; if (request.getParentId() != null) { parent = tagRepository.findOneByBookAndId(book, request.getParentId()).orElseThrow(ItemNotFoundException::new); if (!parent.getEnable()) { throw new ParentCategoryNotEnableException(); } if (parent.getLevel().equals(maxLevel-1)) { throw new CategoryLevelException(); } } po.setParent(parent); if (parent == null) po.setLevel(0); else po.setLevel(parent.getLevel()+1); if (request.getExpenseable() != null) po.setExpenseable(request.getExpenseable()); if (request.getIncomeable() != null) po.setIncomeable(request.getIncomeable()); if (request.getTransferable() != null) po.setTransferable(request.getTransferable()); // List entityList = tagRepository.findAll(BookNameNotesEnableSpec.inBook(po.getBook())); // List offSpring = po.getOffspring(entityList); // offSpring.add(po); // for (Tag item : offSpring) { // if (request.getExpenseable() != null) item.setExpenseable(request.getExpenseable()); // if (request.getIncomeable() != null) item.setIncomeable(request.getIncomeable()); // if (request.getTransferable() != null) item.setTransferable(request.getTransferable()); // } return TagVOForList.fromEntity(tagRepository.save(po)); } public boolean toggle(Integer id, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Tag po = tagRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); List entityList = tagRepository.findAll(BookNameNotesEnableSpec.isBook(po.getBook())); List offSpring = po.getOffspring(entityList); offSpring.add(po); for (Tag item : offSpring) { item.setEnable(!po.getEnable()); } tagRepository.saveAll(offSpring); return true; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag/TagSpec.java ================================================ package com.jiukuaitech.bookkeeping.user.tag; import com.jiukuaitech.bookkeeping.user.base.BookNameNotesEnableSpec; import com.jiukuaitech.bookkeeping.user.book.Book; import org.springframework.data.jpa.domain.Specification; import org.springframework.util.StringUtils; public final class TagSpec { public static Specification isExpenseable(Boolean expenseable) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Tag_.EXPENSEABLE), expenseable); } public static Specification isIncomeable(Boolean incomeable) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Tag_.INCOMEABLE), incomeable); } public static Specification isTransferable(Boolean transferable) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Tag_.TRANSFERABLE), transferable); } public static Specification buildSpecification(TagQueryRequest request, Book book) { Specification specification = BookNameNotesEnableSpec.isBook(book); if (StringUtils.hasText(request.getName())) { specification = specification.and(BookNameNotesEnableSpec.nameLike(request.getName())); } if (request.getEnable() != null) { specification = specification.and(BookNameNotesEnableSpec.isEnable(request.getEnable())); } if (request.getExpenseable() != null) { specification = specification.and(isExpenseable(request.getExpenseable())); } if (request.getIncomeable() != null) { specification = specification.and(isIncomeable(request.getIncomeable())); } if (request.getTransferable() != null) { specification = specification.and(isTransferable(request.getIncomeable())); } return specification; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag/TagTreeVO.java ================================================ package com.jiukuaitech.bookkeeping.user.tag; import lombok.Getter; import lombok.Setter; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.List; @Getter @Setter public class TagTreeVO { private Integer id; private String name; private String notes; private Boolean enable; private Boolean expenseable; private Boolean incomeable; private Boolean transferable; private List children; private Integer parentId; private String parentName; public static List valueOfList(List tags) { List treeVOList = new ArrayList<>(); for (Tag item : tags) { if (item.getParent() == null) { treeVOList.add(TagTreeVO.valueOf(item, tags)); } } return treeVOList; } public static TagTreeVO valueOf(Tag tag, List tags) { TagTreeVO treeVO = new TagTreeVO(); treeVO.setId(tag.getId()); treeVO.setName(tag.getName()); treeVO.setNotes(tag.getNotes()); treeVO.setEnable(tag.getEnable()); treeVO.setExpenseable(tag.getExpenseable()); treeVO.setIncomeable(tag.getIncomeable()); treeVO.setTransferable(tag.getTransferable()); treeVO.setParentId(tag.getParent() == null ? null : tag.getParent().getId()); treeVO.setParentName(tag.getParent() == null ? null : tag.getParent().getName()); if (!CollectionUtils.isEmpty(tag.getChildren(tags))) { for (Tag item : tag.getChildren(tags)) { if (treeVO.getChildren() == null) { treeVO.setChildren(new ArrayList<>()); } treeVO.getChildren().add(valueOf(item, tags)); } } return treeVO; } public static TagTreeVO valueOf(Tag tag) { TagTreeVO tagTreeVO = new TagTreeVO(); tagTreeVO.setId(tag.getId()); tagTreeVO.setName(tag.getName()); tagTreeVO.setNotes(tag.getNotes()); tagTreeVO.setEnable(tag.getEnable()); tagTreeVO.setExpenseable(tag.getExpenseable()); tagTreeVO.setIncomeable(tag.getIncomeable()); tagTreeVO.setTransferable(tag.getTransferable()); if (tag.getParent() != null) tagTreeVO.setParentId(tag.getParent().getId()); if (tag.getParent() != null) tagTreeVO.setParentName(tag.getParent().getName()); return tagTreeVO; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag/TagUpdateRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.tag; import com.jiukuaitech.bookkeeping.user.validation.NameValidator; import com.jiukuaitech.bookkeeping.user.validation.NotesValidator; import lombok.Getter; import lombok.Setter; @Getter @Setter public class TagUpdateRequest { private Integer parentId; @NameValidator private String name; @NotesValidator private String notes; private Boolean expenseable; private Boolean incomeable; private Boolean transferable; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag/TagVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.tag; import lombok.Getter; import lombok.Setter; @Getter @Setter public class TagVOForList { private Integer id; private String name; private String notes; private Boolean enable; private Boolean expenseable; private Boolean incomeable; private Boolean transferable; private Integer parentId; public static TagVOForList fromEntity(Tag po) { if (po == null) return null; TagVOForList vo = new TagVOForList(); vo.setId(po.getId()); vo.setName(po.getName()); vo.setNotes(po.getNotes()); vo.setEnable(po.getEnable()); vo.setExpenseable(po.getExpenseable()); vo.setIncomeable(po.getIncomeable()); vo.setTransferable(po.getTransferable()); vo.setParentId(po.getParent() != null ? po.getParent().getId() : 0); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag_relation/TagRelation.java ================================================ package com.jiukuaitech.bookkeeping.user.tag_relation; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import com.jiukuaitech.bookkeeping.user.tag.Tag; import com.jiukuaitech.bookkeeping.user.transaction.Transaction; import com.jiukuaitech.bookkeeping.user.validation.AmountValidator; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.NotNull; import java.math.BigDecimal; @Entity @Table(name = "t_tag_relation", uniqueConstraints = {@UniqueConstraint(columnNames = {"transaction_id", "tag_id"})}) @Getter @Setter public class TagRelation extends BaseEntity { @ManyToOne(optional = false, fetch = FetchType.LAZY) @NotNull @JoinColumn(name = "tag_id") private Tag tag; @ManyToOne(optional = false, fetch = FetchType.LAZY) @NotNull @JoinColumn(name = "transaction_id") private Transaction transaction; @Column(nullable = false) @NotNull @AmountValidator private BigDecimal amount; @Column(nullable = false) @NotNull @AmountValidator private BigDecimal convertedAmount; // 金额 } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag_relation/TagRelationController.java ================================================ package com.jiukuaitech.bookkeeping.user.tag_relation; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/tag-relations") public class TagRelationController { @Resource private TagRelationService tagRelationService; @RequestMapping(method = RequestMethod.PUT, value = "/{id}") public BaseResponse handleUpdate( @PathVariable("id") Integer id, @Valid @RequestBody TagRelationUpdateRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(tagRelationService.update(id, request, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag_relation/TagRelationRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.tag_relation; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import org.springframework.stereotype.Repository; @Repository public interface TagRelationRepository extends BaseRepository { Integer countByTag_id(Integer tagId); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag_relation/TagRelationService.java ================================================ package com.jiukuaitech.bookkeeping.user.tag_relation; import com.jiukuaitech.bookkeeping.user.balance_flow.AmountInvalidateException; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.user.UserService; import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; @Service public class TagRelationService { @Resource private TagRelationRepository tagRelationRepository; @Resource private UserService userService; public TagRelationVOForList update(Integer id, TagRelationUpdateRequest request, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); TagRelation po = tagRelationRepository.findById(id).orElseThrow(ItemNotFoundException::new); if (!po.getTransaction().getBook().equals(book)) throw new ItemNotFoundException(); if (request.getAmount().compareTo(BigDecimal.ZERO) == 0) throw new AmountInvalidateException(); if (request.getConvertedAmount() != null && request.getConvertedAmount().compareTo(BigDecimal.ZERO) == 0) throw new AmountInvalidateException(); po.setAmount(request.getAmount()); if (request.getConvertedAmount() != null) { po.setConvertedAmount(request.getConvertedAmount()); } else { po.setConvertedAmount(request.getAmount()); } return TagRelationVOForList.fromEntity(tagRelationRepository.save(po)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag_relation/TagRelationUpdateRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.tag_relation; import com.jiukuaitech.bookkeeping.user.validation.AmountValidator; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @Getter @Setter public class TagRelationUpdateRequest { @AmountValidator private BigDecimal amount; private BigDecimal convertedAmount; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/tag_relation/TagRelationVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.tag_relation; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @Getter @Setter public class TagRelationVOForList { private Integer id; private BigDecimal amount; private BigDecimal convertedAmount; private Integer tagId; private String tagName; public static TagRelationVOForList fromEntity(TagRelation po) { if (po == null) return null; TagRelationVOForList vo = new TagRelationVOForList(); vo.setId(po.getId()); vo.setAmount(po.getAmount()); vo.setConvertedAmount(po.getConvertedAmount()); vo.setTagId(po.getTag().getId()); vo.setTagName(po.getTag().getName()); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transaction/Transaction.java ================================================ package com.jiukuaitech.bookkeeping.user.transaction; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlow; import com.jiukuaitech.bookkeeping.user.tag_relation.TagRelation; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.HashSet; import java.util.Set; @Entity @Getter @Setter public abstract class Transaction extends BalanceFlow { @OneToMany(mappedBy = "transaction", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private Set tags = new HashSet<>(); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transaction/TransactionAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.transaction; import com.jiukuaitech.bookkeeping.user.tag.Tag; import com.jiukuaitech.bookkeeping.user.utils.CommonUtils; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowAddRequest; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import com.jiukuaitech.bookkeeping.user.exception.InputNotValidException; import lombok.Getter; import lombok.Setter; import org.springframework.util.CollectionUtils; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @Getter @Setter public class TransactionAddRequest extends BalanceFlowAddRequest { private Boolean confirmed = true; private Set tags; public void copyPrimitive(Transaction po) { super.copyPrimitive(po); po.setStatus(confirmed ? 1 : 2); } public void copyTags(Transaction po, List tags) { if (!CollectionUtils.isEmpty(getTags())) { // 自动保存关联对象 List tagIds = tags.stream().map(BaseEntity::getId).collect(Collectors.toList()); getTags().forEach(i-> { if (tagIds.contains(i)) { po.getTags().add(CommonUtils.getTagRelation(i, po)); } else { throw new InputNotValidException(); } }); } } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transaction/TransactionRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.transaction; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import org.springframework.stereotype.Repository; @Repository public interface TransactionRepository extends BaseRepository { Long countByAccount_id(Integer accountId); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transaction/TransactionSpec.java ================================================ package com.jiukuaitech.bookkeeping.user.transaction; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowQueryRequest; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.tag_relation.TagRelation; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowSpec; import com.jiukuaitech.bookkeeping.user.tag_relation.TagRelation_; import org.springframework.data.jpa.domain.Specification; import org.springframework.util.CollectionUtils; import javax.persistence.criteria.*; import java.util.Set; public final class TransactionSpec { public static Specification tagsIn(Set tags) { return (root, query, criteriaBuilder) -> { Join join = root.join(Transaction_.tags); return join.get(TagRelation_.tag).in(tags); }; } public static Specification buildSpecification(BalanceFlowQueryRequest request, Group group) { Specification specification = BalanceFlowSpec.buildSpecification(request, group); if (!CollectionUtils.isEmpty(request.getTags())) { specification = specification.and(tagsIn(request.getTags())); } return specification; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transaction/TransactionUpdateRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.transaction; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowUpdateRequest; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import com.jiukuaitech.bookkeeping.user.tag.Tag; import com.jiukuaitech.bookkeeping.user.utils.CommonUtils; import com.jiukuaitech.bookkeeping.user.exception.InputNotValidException; import lombok.Getter; import lombok.Setter; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @Getter @Setter public class TransactionUpdateRequest extends BalanceFlowUpdateRequest { private Set tags; public void updateTags(Transaction po, List tags) { if (getTags() != null) { CommonUtils.cleanTagRelation(po.getTags(), getTags()); List tagIds = tags.stream().map(BaseEntity::getId).collect(Collectors.toList()); getTags().forEach(i-> { if (tagIds.contains(i)) { po.getTags().add(CommonUtils.getTagRelation(i, po)); } else { throw new InputNotValidException(); } }); } } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transaction/TransactionVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.transaction; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowVOForExtend; import com.jiukuaitech.bookkeeping.user.tag_relation.TagRelationVOForList; import lombok.Getter; import lombok.Setter; import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; @Getter @Setter public class TransactionVOForList extends BalanceFlowVOForExtend { private Set tags = new HashSet<>(); public void setValue(Transaction po) { super.setValue(po); setTags(po.getTags().stream().map(TagRelationVOForList::fromEntity).collect(Collectors.toSet())); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transfer/Transfer.java ================================================ package com.jiukuaitech.bookkeeping.user.transfer; import com.jiukuaitech.bookkeeping.user.account.Account; import com.jiukuaitech.bookkeeping.user.transaction.Transaction; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Entity @DiscriminatorValue(value = "3") @Getter @Setter public class Transfer extends Transaction { // from 和父类的 account 共用 @ManyToOne(fetch = FetchType.LAZY) private Account to; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transfer/TransferAddRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.transfer; import com.jiukuaitech.bookkeeping.user.transaction.Transaction; import com.jiukuaitech.bookkeeping.user.transaction.TransactionAddRequest; import com.jiukuaitech.bookkeeping.user.validation.AmountValidator; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; @Getter @Setter public class TransferAddRequest extends TransactionAddRequest { @NotNull private Integer fromId; @NotNull private Integer toId; @NotNull @AmountValidator @Positive private BigDecimal amount; private BigDecimal convertedAmount; @Override public void copyPrimitive(Transaction po) { super.copyPrimitive(po); po.setAmount(amount); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transfer/TransferController.java ================================================ package com.jiukuaitech.bookkeeping.user.transfer; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowQueryRequest; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; @RestController @RequestMapping("/transfers") public class TransferController extends BaseController { @Resource private TransferService transferService; @RequestMapping(method = RequestMethod.POST, value = "") public BaseResponse handleAdd( @Valid @RequestBody TransferAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(transferService.add(request, userSignInId)); } @RequestMapping(method = RequestMethod.GET, value = "") public BaseResponse handleQuery( @Valid BalanceFlowQueryRequest request, @PageableDefault(sort = {"createTime", "id"}, direction = Sort.Direction.DESC) Pageable page, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(transferService.queryWithDefaultBook(request, page, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/{id}") public BaseResponse handleUpdate( @PathVariable("id") Integer id, @Valid @RequestBody TransferUpdateRequest request, @RequestAttribute("userSignInId") Integer userSignInId) { return new BaseResponse(transferService.update(id, request, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transfer/TransferExceptionHandler.java ================================================ package com.jiukuaitech.bookkeeping.user.transfer; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.ErrorResponse; import org.springframework.context.MessageSource; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.annotation.Resource; import java.util.Locale; @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) public class TransferExceptionHandler { private static final Locale LANG = Locale.CHINA; @Resource private MessageSource messageSource; @ExceptionHandler(value = TransferFromEqualsToException.class) @ResponseBody public BaseResponse handleException(TransferFromEqualsToException e) { return new ErrorResponse(704, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transfer/TransferFromEqualsToException.java ================================================ package com.jiukuaitech.bookkeeping.user.transfer; /** * 转出和转入的账号相同 */ public class TransferFromEqualsToException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transfer/TransferQueryResultVO.java ================================================ package com.jiukuaitech.bookkeeping.user.transfer; import lombok.Getter; import lombok.Setter; import org.springframework.data.domain.Page; import java.math.BigDecimal; @Getter @Setter public class TransferQueryResultVO { private Page result; private BigDecimal total; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transfer/TransferRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.transfer; import com.jiukuaitech.bookkeeping.user.base.HasBookRepository; import org.springframework.stereotype.Repository; @Repository public interface TransferRepository extends HasBookRepository { Long countByTo_id(Integer accountId); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transfer/TransferService.java ================================================ package com.jiukuaitech.bookkeeping.user.transfer; import com.jiukuaitech.bookkeeping.user.account.Account; import com.jiukuaitech.bookkeeping.user.account.AccountRepository; import com.jiukuaitech.bookkeeping.user.balance_flow.AccountInvalidateException; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowQueryRequest; import com.jiukuaitech.bookkeeping.user.balance_flow.StatusNotValidateException; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.tag.TagRepository; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.user.UserService; import com.jiukuaitech.bookkeeping.user.user_log.UserActionLog; import com.jiukuaitech.bookkeeping.user.user_log.UserActionLogRepository; import com.jiukuaitech.bookkeeping.user.user_log.UserActionLogService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import java.math.BigDecimal; import java.time.Instant; @Service public class TransferService { @Resource private UserService userService; @Resource private AccountRepository accountRepository; @Resource private TransferRepository transferRepository; @Resource private TagRepository tagRepository; @Resource private UserActionLogRepository userActionLogRepository; @Resource private UserActionLogService userActionLogService; @Transactional public boolean add(TransferAddRequest request, Integer userSignInId) { // 转出和转入不能相同 if (request.getFromId().equals(request.getToId())) { throw new TransferFromEqualsToException(); } User user = userService.getUser(userSignInId); // 不能频繁添加,防止用户恶意操作 userActionLogService.check(user); Group group = user.getDefaultGroup(); Book book = user.getDefaultBook(); Account fromAccount = accountRepository.findOneByGroupAndId(group, request.getFromId()).orElseThrow(AccountInvalidateException::new); Account toAccount = accountRepository.findOneByGroupAndId(group, request.getToId()).orElseThrow(AccountInvalidateException::new); Transfer po = new Transfer(); request.copyPrimitive(po); if (request.getConvertedAmount() != null) { po.setConvertedAmount(request.getConvertedAmount()); } else { po.setConvertedAmount(request.getAmount()); } po.setCreator(user); po.setGroup(user.getDefaultGroup()); po.setBook(user.getDefaultBook()); request.copyTags(po, tagRepository.findByBookAndEnable(book, true)); po.setAccount(fromAccount); po.setTo(toAccount); transferRepository.save(po); // 正常状态才改变账户余额 if (request.getConfirmed()) { confirmBalance(po); } userActionLogRepository.save(new UserActionLog(user, 1, Instant.now().toEpochMilli())); return true; } // 退款 private void refundBalance(Transfer transfer) { Account fromAccount = transfer.getAccount(); Account toAccount = transfer.getTo(); BigDecimal amount = transfer.getAmount(); fromAccount.setBalance(fromAccount.getBalance().add(amount)); toAccount.setBalance(toAccount.getBalance().subtract(amount)); accountRepository.save(fromAccount); accountRepository.save(toAccount); } // 扣款 private void confirmBalance(Transfer po) { Account fromAccount = po.getAccount(); Account toAccount = po.getTo(); BigDecimal amount = po.getAmount(); BigDecimal convertedAmount = po.getConvertedAmount(); fromAccount.setBalance(fromAccount.getBalance().subtract(amount)); toAccount.setBalance(toAccount.getBalance().add(convertedAmount)); accountRepository.save(fromAccount); accountRepository.save(toAccount); } public TransferQueryResultVO queryWithDefaultBook(BalanceFlowQueryRequest request, Pageable page, Integer userSignInId) { User user = userService.getUser(userSignInId); request.setBookId(user.getDefaultBook().getId()); return query(request, page, userSignInId); } public TransferQueryResultVO query(BalanceFlowQueryRequest request, Pageable page, Integer userSignInId) { TransferQueryResultVO result = new TransferQueryResultVO(); Specification specification = TransferSpec.buildSpecification(request, userService.getUser(userSignInId).getDefaultGroup()); Page poPage = transferRepository.findAll(specification, page); Page voPage = poPage.map(transfer -> { TransferVOForList vo = TransferVOForList.fromEntity(transfer); vo.setCurrencyCode(transfer.getAccount().getCurrencyCode()); vo.setNeedConvert(!transfer.getAccount().getCurrencyCode().equals(transfer.getTo().getCurrencyCode())); vo.setToCurrencyCode(transfer.getTo().getCurrencyCode()); return vo; }); result.setResult(voPage); // 解决join查询重复问题,应该考虑子查询 if (CollectionUtils.isEmpty(request.getTags())) { result.setTotal(transferRepository.calcAggregate(specification, Transfer_.convertedAmount, Transfer.class)); } else { result.setTotal(transferRepository.findAll(specification).stream().map(Transfer::getConvertedAmount).reduce(BigDecimal.ZERO, BigDecimal::add)); } return result; } @Transactional public boolean remove(Integer id, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Transfer po = transferRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); // 只有正常状态的需要退款,未确认和已退款不需要 if (po.getStatus() == 1) { refundBalance(po); } transferRepository.delete(po); return true; } @Transactional public boolean confirm(Integer id, Integer userSignInId) { Book book = userService.getUser(userSignInId).getDefaultBook(); Transfer po = transferRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); if (po.getStatus() != 2) { throw new StatusNotValidateException(); } confirmBalance(po); po.setStatus(1); transferRepository.save(po); return true; } @Transactional public boolean update(Integer id, TransferUpdateRequest request, Integer userSignInId) { User user = userService.getUser(userSignInId); Book book = user.getDefaultBook(); Group group = user.getDefaultGroup(); Transfer po = transferRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new); if (!po.getAccount().getId().equals(request.getFromId()) || !po.getTo().getId().equals(request.getToId()) || (request.getAmount() != null && po.getAmount().compareTo(request.getAmount()) != 0) || (request.getConvertedAmount() != null && po.getConvertedAmount().compareTo(request.getConvertedAmount()) != 0)) { if (po.getStatus() == 1) { refundBalance(po); } Account fromAccount = accountRepository.findOneByGroupAndId(group, request.getFromId()).orElseThrow(AccountInvalidateException::new); Account toAccount = accountRepository.findOneByGroupAndId(group, request.getToId()).orElseThrow(AccountInvalidateException::new); // 转出和转入不能相同 if (fromAccount.getId().equals(toAccount.getId())) throw new TransferFromEqualsToException(); po.setAccount(fromAccount); po.setTo(toAccount); po.setAmount(request.getAmount()); if (request.getConvertedAmount() != null) { if (!fromAccount.getCurrencyCode().equals(toAccount.getCurrencyCode())) { po.setConvertedAmount(request.getConvertedAmount()); } } else { po.setConvertedAmount(request.getAmount()); } if (po.getStatus() == 1) { confirmBalance(po); } } request.updatePrimitive(po); request.updateTags(po, tagRepository.findByBookAndEnable(book, true)); transferRepository.save(po); return true; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transfer/TransferSpec.java ================================================ package com.jiukuaitech.bookkeeping.user.transfer; import com.jiukuaitech.bookkeeping.user.account.Account; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowQueryRequest; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.transaction.TransactionSpec; import org.springframework.data.jpa.domain.Specification; import org.springframework.util.CollectionUtils; import javax.persistence.criteria.CriteriaBuilder; import java.util.Set; public final class TransferSpec { public static Specification fromIn(Set accounts) { return (root, query, criteriaBuilder) -> { CriteriaBuilder.In in = criteriaBuilder.in(root.get(Transfer_.account)); accounts.forEach(i -> in.value(new Account(i))); return in; }; } public static Specification toIn(Set accounts) { return (root, query, criteriaBuilder) -> { CriteriaBuilder.In in = criteriaBuilder.in(root.get(Transfer_.to)); accounts.forEach(i -> in.value(new Account(i))); return in; }; } public static Specification buildSpecification(BalanceFlowQueryRequest request, Group group) { Specification specification = TransactionSpec.buildSpecification(request, group); if (!CollectionUtils.isEmpty(request.getFromAccounts())) { specification = specification.and(fromIn(request.getFromAccounts())); } if (!CollectionUtils.isEmpty(request.getToAccounts())) { specification = specification.and(toIn(request.getToAccounts())); } return specification; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transfer/TransferUpdateRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.transfer; import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlow; import com.jiukuaitech.bookkeeping.user.transaction.TransactionUpdateRequest; import com.jiukuaitech.bookkeeping.user.validation.AmountValidator; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; import java.math.BigDecimal; @Getter @Setter public class TransferUpdateRequest extends TransactionUpdateRequest { @NotNull private Integer fromId; @NotNull private Integer toId; @NotNull @AmountValidator @Positive private BigDecimal amount; private BigDecimal convertedAmount; @Override public void updatePrimitive(BalanceFlow po) { super.updatePrimitive(po); // po.setAmount(amount); //涉及到退款问题,需要在退款之前改变 } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/transfer/TransferVOForList.java ================================================ package com.jiukuaitech.bookkeeping.user.transfer; import com.jiukuaitech.bookkeeping.user.account.AccountVOForExtend; import com.jiukuaitech.bookkeeping.user.transaction.TransactionVOForList; import lombok.Getter; import lombok.Setter; @Getter @Setter public class TransferVOForList extends TransactionVOForList { private AccountVOForExtend from; private AccountVOForExtend to; private Integer fromId; private Integer toId; private String accountName; public static TransferVOForList fromEntity(Transfer po) { TransferVOForList vo = new TransferVOForList(); vo.setValue(po); vo.setAccountName(po.getAccount().getName() + " -> " + po.getTo().getName()); vo.setFrom(AccountVOForExtend.fromEntity(po.getAccount())); vo.setTo(AccountVOForExtend.fromEntity(po.getTo())); vo.setFromId(po.getAccount().getId()); vo.setToId(po.getTo().getId()); return vo; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/InviteCodeErrorException.java ================================================ package com.jiukuaitech.bookkeeping.user.user; public class InviteCodeErrorException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/IpNotAllowedException.java ================================================ package com.jiukuaitech.bookkeeping.user.user; public class IpNotAllowedException extends Exception { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/OldPasswordErrorException.java ================================================ package com.jiukuaitech.bookkeeping.user.user; public class OldPasswordErrorException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/RegisterNameExistsException.java ================================================ package com.jiukuaitech.bookkeeping.user.user; public class RegisterNameExistsException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/SessionUserNotFoundException.java ================================================ package com.jiukuaitech.bookkeeping.user.user; public class SessionUserNotFoundException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/SessionVO.java ================================================ package com.jiukuaitech.bookkeeping.user.user; import com.jiukuaitech.bookkeeping.user.book.BookVOForList; import com.jiukuaitech.bookkeeping.user.group.GroupVOForList; import lombok.Getter; import lombok.Setter; @Getter @Setter public class SessionVO { private UserSessionVO userSessionVO; private BookVOForList defaultBook; private GroupVOForList defaultGroup; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/SigninFailedException.java ================================================ package com.jiukuaitech.bookkeeping.user.user; public class SigninFailedException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/UploadNotImageException.java ================================================ package com.jiukuaitech.bookkeeping.user.user; public class UploadNotImageException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/User.java ================================================ package com.jiukuaitech.bookkeeping.user.user; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.validation.*; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.Email; import javax.validation.constraints.NotNull; @Entity @Table(name = "t_user") @Getter @Setter public class User extends BaseEntity { @Column(length = 16, unique = true, nullable = false) @NotNull @UserNameValidator private String userName; @Column(length = 16) @NameValidator private String nickName; @Column(length = 32, nullable = false) @NotNull @PasswordValidator private String password; @Column(length = 16) @NameValidator private String telephone; @Column(length = 32) @Email private String email; @Column(length = 32, nullable = true) private String ip; @Column(length = 64) @AvatarValidator private String avatar; @ManyToOne(optional = false, fetch = FetchType.LAZY) private Group defaultGroup; //用户默认操作的组 @ManyToOne(optional = false, fetch = FetchType.LAZY) private Book defaultBook; //用户默认操作的账本 @Column(nullable = false) @NotNull @TimeValidator private Long vipTime; //会员到期日 // 上次登录ip,时间,哪个国家 设备等信息 TODO @Column(nullable = false) @NotNull private Boolean enable = true; @Column(nullable = false) @NotNull @TimeValidator private Long registerTime; //注册时间 public User() { } public User(Integer id) { super.setId(id); } public User(String userName, String password) { this.userName = userName; this.password = password; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/UserController.java ================================================ package com.jiukuaitech.bookkeeping.user.user; import com.jiukuaitech.bookkeeping.user.base.BaseController; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.DataResponse; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.validation.Valid; // 登录,注册,注销等 @RestController public class UserController extends BaseController { @Resource private UserService userService; // 登录 @RequestMapping(method = RequestMethod.POST, value = "/signin") public BaseResponse handleSignin(@Valid @RequestBody UserSignInRequest request) { return new DataResponse<>(userService.signin(request, getRequest(), getResponse())); } @RequestMapping(method = RequestMethod.POST, value = "/signout") public BaseResponse handleSignout() { return new BaseResponse(userService.signout(getRequest(), getResponse())); } // 注册 @RequestMapping(method = RequestMethod.POST, value = "/register") public BaseResponse handleRegister(@Valid @RequestBody UserRegisterRequest request) { return new BaseResponse(userService.register(request, getRequest())); } // 修改密码 @RequestMapping(method = RequestMethod.PUT, value = "/updatePassword") public BaseResponse handleUpdatePassword(@RequestAttribute("userSignInId") Integer userSignInId, @Valid @RequestBody UserUpdatePasswordRequest request) { userService.updatePassword(userSignInId, request); userService.signout(getRequest(), getResponse()); return new BaseResponse(true); } @RequestMapping(method = RequestMethod.GET, value = "/session") public BaseResponse handleSession(@RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(userService.getSession(userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/setDefaultBook/{id}") public BaseResponse handleSetDefaultBook( @PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(userService.setDefaultBook(id, userSignInId)); } @RequestMapping(method = RequestMethod.PUT, value = "/setDefaultGroup/{id}") public BaseResponse handleSetDefaultGroup( @PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) { return new DataResponse<>(userService.setDefaultGroup(id, userSignInId)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/UserDisabledException.java ================================================ package com.jiukuaitech.bookkeeping.user.user; public class UserDisabledException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/UserExceptionHandler.java ================================================ package com.jiukuaitech.bookkeeping.user.user; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.ErrorResponse; import org.springframework.context.MessageSource; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.annotation.Resource; import java.util.Locale; @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) public class UserExceptionHandler { private static final Locale LANG = Locale.CHINA; @Resource private MessageSource messageSource; @ExceptionHandler(value = RegisterNameExistsException.class) @ResponseBody public BaseResponse handleException(RegisterNameExistsException e) { return new ErrorResponse(701, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = InviteCodeErrorException.class) @ResponseBody public BaseResponse handleException(InviteCodeErrorException e) { return new ErrorResponse(702, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = SigninFailedException.class) @ResponseBody public BaseResponse handleException(SigninFailedException e) { return new ErrorResponse(703, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = UploadNotImageException.class) @ResponseBody public BaseResponse handleException(UploadNotImageException e) { return new ErrorResponse(704, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = SessionUserNotFoundException.class) @ResponseBody public BaseResponse handleException(SessionUserNotFoundException e) { return new ErrorResponse(705, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = OldPasswordErrorException.class) @ResponseBody public BaseResponse handleException(OldPasswordErrorException e) { return new ErrorResponse(706, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } @ExceptionHandler(value = UserDisabledException.class) @ResponseBody public BaseResponse handleException(UserDisabledException e) { return new ErrorResponse(706, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/UserGroupRelation.java ================================================ package com.jiukuaitech.bookkeeping.user.user; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import com.jiukuaitech.bookkeeping.user.group.Group; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import javax.validation.constraints.NotNull; @Entity @Table(name="t_user_group_relation", uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "group_id"})}) @Getter @Setter public class UserGroupRelation extends BaseEntity { @ManyToOne(optional = false, fetch = FetchType.LAZY) @JoinColumn(name = "user_id") @NotNull private User user; @ManyToOne(optional = false, fetch = FetchType.LAZY) @JoinColumn(name = "group_id") @NotNull private Group group; @Column(nullable = false) @NotNull private Integer role;//1 所有者 2维护者 3访客 public UserGroupRelation() { } public UserGroupRelation(User user, Group group, Integer role) { this.user = user; this.group = group; this.role = role; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/UserGroupRelationRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.user; import com.jiukuaitech.bookkeeping.user.group.Group; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; import java.util.List; @Repository public interface UserGroupRelationRepository extends CrudRepository { UserGroupRelation findOneByUserAndGroup(User user, Group group); List findByUser(User user); Integer deleteByGroup(Group group); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/UserRegisterRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.user; import com.jiukuaitech.bookkeeping.user.validation.PasswordValidator; import com.jiukuaitech.bookkeeping.user.validation.UserNameValidator; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.Email; @Getter @Setter public class UserRegisterRequest { @UserNameValidator private String userName; @PasswordValidator private String password; private String inviteCode; @Email private String email; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/UserRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.user; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import org.springframework.stereotype.Repository; @Repository public interface UserRepository extends BaseRepository { User findOneByUserName(String userName); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/UserService.java ================================================ package com.jiukuaitech.bookkeeping.user.user; import com.jiukuaitech.bookkeeping.user.book.Book; import com.jiukuaitech.bookkeeping.user.group.Group; import com.jiukuaitech.bookkeeping.user.utils.CommonUtils; import com.jiukuaitech.bookkeeping.user.book.BookRepository; import com.jiukuaitech.bookkeeping.user.book.BookVOForList; import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException; import com.jiukuaitech.bookkeeping.user.group.GroupRepository; import com.jiukuaitech.bookkeeping.user.group.GroupVOForList; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import org.springframework.web.util.WebUtils; import javax.annotation.Resource; import javax.servlet.ServletContext; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.time.Instant; import java.util.UUID; @Service public class UserService { @Resource private UserRepository userRepository; @Resource private GroupRepository groupRepository; @Resource private UserGroupRelationRepository userGroupRelationRepository; @Resource private BookRepository bookRepository; @Resource private ServletContext servletContext; @Value("${invite.code}") private String inviteCode; public User getUser(Integer userSignInId) { return userRepository.findById(userSignInId).orElseThrow(SessionUserNotFoundException::new); } @Transactional public boolean register(UserRegisterRequest request, HttpServletRequest httpServletRequest) { if (StringUtils.hasText(inviteCode) && !inviteCode.equals(request.getInviteCode())) { throw new InviteCodeErrorException(); } // 检查用户名是否存在 if (userRepository.findOneByUserName(request.getUserName()) != null) { throw new RegisterNameExistsException(); } // 密码MD5之后保存 User user = new User(request.getUserName(), CommonUtils.encodePassword(request.getPassword())); if (StringUtils.hasText(request.getEmail())) { user.setEmail(request.getEmail()); } user.setNickName(request.getUserName()); user.setIp(CommonUtils.getRealIP(httpServletRequest)); user.setVipTime(Instant.now().toEpochMilli()); user.setRegisterTime(Instant.now().toEpochMilli()); // 注册之后,默认一个组 Group group = new Group(); group.setName("默认组"); group.setDefaultCurrencyCode("CNY"); groupRepository.save(group); Book book = new Book(); book.setName("默认账本"); book.setDefaultCurrencyCode("CNY"); book.setGroup(group); bookRepository.save(book); user.setDefaultGroup(group); user.setDefaultBook(book); userRepository.save(user); group.setDefaultBook(book); group.setCreator(user); groupRepository.save(group); UserGroupRelation userGroupRelation = new UserGroupRelation(user, group, 1); userGroupRelationRepository.save(userGroupRelation); return true; } public boolean updatePassword(Integer userSignInId, UserUpdatePasswordRequest request) { User user = getUser(userSignInId); if (!user.getPassword().equals(CommonUtils.encodePassword(request.getOldPassword()))) { throw new OldPasswordErrorException(); } user.setPassword(CommonUtils.encodePassword(request.getNewPassword())); userRepository.save(user); return true; } public UserSignInResponse signin(UserSignInRequest request, HttpServletRequest httpServletRequest, HttpServletResponse response) { UserSignInResponse userSignInResponse = new UserSignInResponse(); User user = userRepository.findOneByUserName(request.getUserName()); if (user == null || !user.getPassword().equals(CommonUtils.encodePassword(request.getPassword()))) { throw new SigninFailedException(); } if (!user.getEnable()) throw new UserDisabledException(); // 用户名和密码都正确 String token = UUID.randomUUID().toString(); servletContext.setAttribute(token, user.getId()); userSignInResponse.setToken(token); userSignInResponse.setRemember(request.getRemember()); userSignInResponse.setSessionVO(getSession(user.getId())); // set cookie if (request.getRemember()) { Cookie cookie = new Cookie("User-Token", token); cookie.setMaxAge(30 * 24 * 60 * 60); // cookie.setSecure(true);// only https flat cookie.setHttpOnly(true); cookie.setPath("/"); response.addCookie(cookie); } else { httpServletRequest.getSession().setAttribute("User-Token", token); } return userSignInResponse; } public boolean signout(HttpServletRequest httpServletRequest, HttpServletResponse response) { if (WebUtils.getCookie(httpServletRequest, "User-Token") != null) { Cookie deleteServletCookie = new Cookie("User-Token", null); deleteServletCookie.setMaxAge(0); deleteServletCookie.setPath("/"); response.addCookie(deleteServletCookie); } httpServletRequest.getSession().removeAttribute("User-Token"); return true; } public SessionVO getSession(Integer userSignInId) { User user = getUser(userSignInId); SessionVO sessionVO = new SessionVO(); sessionVO.setUserSessionVO(UserSessionVO.fromEntity(user)); sessionVO.setDefaultBook(BookVOForList.fromEntity(user.getDefaultBook())); sessionVO.setDefaultGroup(GroupVOForList.fromEntity(user.getDefaultGroup())); return sessionVO; } public BookVOForList setDefaultBook(Integer id, Integer userSignInId) { User user = getUser(userSignInId); Group group = user.getDefaultGroup(); Book po = bookRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new); group.setDefaultBook(po); user.setDefaultBook(po); groupRepository.save(group); userRepository.save(user); return BookVOForList.fromEntity(po); } public Integer setDefaultGroup(Integer id, Integer userSignInId) { User user = getUser(userSignInId); Group po = groupRepository.findById(id).orElseThrow(ItemNotFoundException::new); UserGroupRelation userGroupRelation = userGroupRelationRepository.findOneByUserAndGroup(user, po); if (userGroupRelation == null) { throw new ItemNotFoundException(); } user.setDefaultGroup(po); user.setDefaultBook(po.getDefaultBook()); userRepository.save(user); return po.getId(); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/UserSessionVO.java ================================================ package com.jiukuaitech.bookkeeping.user.user; import lombok.Getter; import lombok.Setter; import java.util.Date; @Getter @Setter public class UserSessionVO { private Integer id; private String userName; private String nickName; private String telephone; private String email; private String avatar; private Date lastActiveTime; //上次操作的时间,利用AOP,在每次用户执行操作后更新。可实现用户在操作时,redis的token过期问题。 private Long vipTime; public static UserSessionVO fromEntity(User user) { UserSessionVO userSessionVO = new UserSessionVO(); userSessionVO.setId(user.getId()); userSessionVO.setUserName(user.getUserName()); userSessionVO.setNickName(user.getNickName()); userSessionVO.setTelephone(user.getTelephone()); userSessionVO.setEmail(user.getEmail()); userSessionVO.setAvatar(user.getAvatar()); userSessionVO.setVipTime(user.getVipTime()); return userSessionVO; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/UserSignInRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.user; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotEmpty; @Getter @Setter public class UserSignInRequest { @NotEmpty private String userName; private String password; // @Max(value = 60*24*60*60) // @Min(value = 0) // private Integer rememberTime = 60*60; //记住登录状态时间,单位,秒。默认为1小时 private Boolean remember = false; //是否记住30天 } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/UserSignInResponse.java ================================================ package com.jiukuaitech.bookkeeping.user.user; import lombok.Getter; import lombok.Setter; @Getter @Setter public class UserSignInResponse { private String token; private SessionVO sessionVO; private Boolean remember; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user/UserUpdatePasswordRequest.java ================================================ package com.jiukuaitech.bookkeeping.user.user; import com.jiukuaitech.bookkeeping.user.validation.PasswordValidator; import lombok.Getter; import lombok.Setter; @Getter @Setter public class UserUpdatePasswordRequest { @PasswordValidator private String oldPassword; @PasswordValidator private String newPassword; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user_log/FlowMaxCountException.java ================================================ package com.jiukuaitech.bookkeeping.user.user_log; public class FlowMaxCountException extends RuntimeException { } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user_log/UserActionExceptionHandler.java ================================================ package com.jiukuaitech.bookkeeping.user.user_log; import com.jiukuaitech.bookkeeping.user.response.BaseResponse; import com.jiukuaitech.bookkeeping.user.response.ErrorResponse; import org.springframework.context.MessageSource; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.annotation.Resource; import java.util.Locale; @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) public class UserActionExceptionHandler { private static final Locale LANG = Locale.CHINA; @Resource private MessageSource messageSource; @ExceptionHandler(value = FlowMaxCountException.class) @ResponseBody public BaseResponse handleException(FlowMaxCountException e) { return new ErrorResponse(601, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user_log/UserActionLog.java ================================================ package com.jiukuaitech.bookkeeping.user.user_log; import com.jiukuaitech.bookkeeping.user.base.BaseEntity; import com.jiukuaitech.bookkeeping.user.user.User; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Entity @Table(name = "t_user_action_log") @Getter @Setter public class UserActionLog extends BaseEntity { @ManyToOne(optional = false, fetch = FetchType.LAZY) private User user; private Integer type; @Column(nullable = false) private Long actionTime; public UserActionLog() { } public UserActionLog(User user, Integer type, Long actionTime) { this.user = user; this.type = type; this.actionTime = actionTime; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user_log/UserActionLogRepository.java ================================================ package com.jiukuaitech.bookkeeping.user.user_log; import com.jiukuaitech.bookkeeping.user.base.BaseRepository; import com.jiukuaitech.bookkeeping.user.user.User; import org.springframework.stereotype.Repository; @Repository public interface UserActionLogRepository extends BaseRepository { long countByUserAndTypeAndActionTimeBetween(User user, int type, long start, long end); } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/user_log/UserActionLogService.java ================================================ package com.jiukuaitech.bookkeeping.user.user_log; import com.jiukuaitech.bookkeeping.user.user.User; import com.jiukuaitech.bookkeeping.user.utils.CalendarUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class UserActionLogService { @Resource private UserActionLogRepository userActionLogRepository; @Value("${flow.max.count.daily}") private Integer maxCount; public boolean check(User user) { Long[] day = CalendarUtils.getIn1Day(); if (userActionLogRepository.countByUserAndTypeAndActionTimeBetween(user, 1, day[0], day[1]) >= maxCount) { throw new FlowMaxCountException(); } return true; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/utils/CalendarUtils.java ================================================ package com.jiukuaitech.bookkeeping.user.utils; import java.util.ArrayList; import java.util.Calendar; import java.util.List; public class CalendarUtils { public static void setToStartOfDay(Calendar c) { c.set(Calendar.HOUR_OF_DAY, 0); c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); } public static void setToEndOfDay(Calendar c) { c.set(Calendar.HOUR_OF_DAY, 23); c.set(Calendar.MINUTE, 59); c.set(Calendar.SECOND, 59); c.set(Calendar.MILLISECOND, 999); } public static void setToStartOfWeek(Calendar c) { setToStartOfDay(c); // TODO 国际化,美国的星期一是第一天,或者改为可设置 // c.setFirstDayOfWeek(Calendar.SUNDAY); c.setFirstDayOfWeek(Calendar.MONDAY); c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); } public static void setToEndOfWeek(Calendar c) { setToEndOfDay(c); // TODO 国际化,美国的星期一是第一天,或者改为可设置 c.setFirstDayOfWeek(Calendar.MONDAY); c.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); } public static void setToStartOfMonth(Calendar c) { setToStartOfDay(c); c.set(Calendar.DAY_OF_MONTH, 1); } public static void setToEndOfMonth(Calendar c) { setToEndOfDay(c); c.set(Calendar.DAY_OF_MONTH, c.getActualMaximum(Calendar.DAY_OF_MONTH)); } public static void setToStartOfQuarter(Calendar c) { setToStartOfDay(c); c.set(Calendar.DAY_OF_MONTH, 1); c.set(Calendar.MONTH, c.get(Calendar.MONTH)/3 * 3); } public static void setToEndOfQuarter(Calendar c) { setToEndOfDay(c); c.set(Calendar.DAY_OF_MONTH, 1); c.set(Calendar.MONTH, c.get(Calendar.MONTH)/3 * 3 + 2); c.set(Calendar.DAY_OF_MONTH, c.getActualMaximum(Calendar.DAY_OF_MONTH)); } public static void setToStartOfYear(Calendar c) { setToStartOfDay(c); c.set(Calendar.DAY_OF_YEAR, 1); } public static void setToEndOfYear(Calendar c) { setToEndOfDay(c); c.set(Calendar.DAY_OF_YEAR, c.getActualMaximum(Calendar.DAY_OF_YEAR)); } public static Long getStartOfDay(Long time) { Calendar c = Calendar.getInstance(); c.setTimeInMillis(time); setToStartOfDay(c); return c.getTimeInMillis(); } public static Long getEndOfDay(Long time) { Calendar c = Calendar.getInstance(); c.setTimeInMillis(time); setToEndOfDay(c); return c.getTimeInMillis(); } public static Long[] getThisWeek() { Calendar start = Calendar.getInstance(); start.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); setToStartOfDay(start); Calendar end = Calendar.getInstance(); end.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); setToEndOfDay(end); return new Long[]{ start.getTimeInMillis(), end.getTimeInMillis() }; } public static Long[] getThisMonth() { Calendar start = Calendar.getInstance(); start.set(Calendar.DAY_OF_MONTH, 1); setToStartOfDay(start); Calendar end = Calendar.getInstance(); end.set(Calendar.DAY_OF_MONTH, end.getActualMaximum(Calendar.DAY_OF_MONTH)); setToEndOfDay(end); return new Long[]{ start.getTimeInMillis(), end.getTimeInMillis() }; } public static Long[] getThisYear() { Calendar start = Calendar.getInstance(); start.set(Calendar.DAY_OF_YEAR, 1); setToStartOfDay(start); Calendar end = Calendar.getInstance(); end.set(Calendar.DAY_OF_YEAR, end.getActualMaximum(Calendar.DAY_OF_YEAR)); setToEndOfDay(end); return new Long[]{ start.getTimeInMillis(), end.getTimeInMillis() }; } public static Long[] getLastYear() { Calendar start = Calendar.getInstance(); start.set(Calendar.DAY_OF_YEAR, 1); setToStartOfDay(start); Calendar end = Calendar.getInstance(); end.set(Calendar.DAY_OF_YEAR, end.getActualMaximum(Calendar.DAY_OF_YEAR)); setToEndOfDay(end); start.add(Calendar.YEAR, -1); end.add(Calendar.YEAR, -1); return new Long[]{ start.getTimeInMillis(), end.getTimeInMillis() }; } public static Long[] getIn7Days() { Calendar start = Calendar.getInstance(); start.add(Calendar.DATE, -7); setToStartOfDay(start); Calendar end = Calendar.getInstance(); setToEndOfDay(end); return new Long[]{ start.getTimeInMillis(), end.getTimeInMillis() }; } public static Long[] getIn1Day() { Calendar start = Calendar.getInstance(); start.add(Calendar.HOUR, -24); Calendar end = Calendar.getInstance(); return new Long[]{ start.getTimeInMillis(), end.getTimeInMillis() }; } public static Long[] getIn30Days() { Calendar start = Calendar.getInstance(); start.add(Calendar.DATE, -30); setToStartOfDay(start); Calendar end = Calendar.getInstance(); setToEndOfDay(end); return new Long[]{ start.getTimeInMillis(), end.getTimeInMillis() }; } public static Long[] getIn1Year() { Calendar start = Calendar.getInstance(); start.add(Calendar.YEAR, -1); setToStartOfDay(start); Calendar end = Calendar.getInstance(); setToEndOfDay(end); return new Long[]{ start.getTimeInMillis(), end.getTimeInMillis() }; } public static List getMonths(Integer year) { List result = new ArrayList<>(); Calendar calendar = Calendar.getInstance(); if (year < 1970 || year > calendar.get(Calendar.YEAR)) { return result; } if (year == calendar.get(Calendar.YEAR)) { calendar.add(Calendar.MONTH, -11); } else { calendar.set(Calendar.YEAR, year); calendar.set(Calendar.MONTH, 0); } for (int i = 0; i < 12; i++) { Calendar start = (Calendar) calendar.clone(); start.set(Calendar.DAY_OF_MONTH, 1); setToStartOfDay(start); Calendar end = (Calendar) calendar.clone(); end.set(Calendar.DAY_OF_MONTH, end.getActualMaximum(Calendar.DAY_OF_MONTH)); setToEndOfDay(end); result.add(new Calendar[]{ start, end }); calendar.add(Calendar.MONTH, 1); } return result; } public static List getBreaks(Long start, Long end, String type) { List result = new ArrayList<>(); Calendar startCalendar = Calendar.getInstance(); startCalendar.setTimeInMillis(start); setToStartOfDay(startCalendar); Calendar endCalendar = Calendar.getInstance(); endCalendar.setTimeInMillis(end); setToEndOfDay(endCalendar); switch (type) { case "day": while (startCalendar.getTimeInMillis() < endCalendar.getTimeInMillis()) { Calendar c1 = (Calendar) startCalendar.clone(); Calendar c2 = (Calendar) startCalendar.clone(); setToEndOfDay(c2); result.add(new Calendar[]{ c1, c2 }); startCalendar.add(Calendar.DATE, 1); } break; case "week": setToStartOfWeek(startCalendar); while (startCalendar.getTimeInMillis() < endCalendar.getTimeInMillis()) { Calendar c1 = (Calendar) startCalendar.clone(); Calendar c2 = (Calendar) startCalendar.clone(); setToEndOfWeek(c2); result.add(new Calendar[]{ c1, c2 }); startCalendar.add(Calendar.WEEK_OF_YEAR, 1); } break; case "month": setToStartOfMonth(startCalendar); while (startCalendar.getTimeInMillis() < endCalendar.getTimeInMillis()) { Calendar c1 = (Calendar) startCalendar.clone(); Calendar c2 = (Calendar) startCalendar.clone(); setToEndOfMonth(c2); result.add(new Calendar[]{ c1, c2 }); startCalendar.add(Calendar.MONTH, 1); } break; case "quarter": setToStartOfQuarter(startCalendar); while (startCalendar.getTimeInMillis() < endCalendar.getTimeInMillis()) { Calendar c1 = (Calendar) startCalendar.clone(); Calendar c2 = (Calendar) startCalendar.clone(); setToEndOfQuarter(c2); result.add(new Calendar[]{ c1, c2 }); startCalendar.add(Calendar.MONTH, 3); } break; case "year": setToStartOfYear(startCalendar); while (startCalendar.getTimeInMillis() < endCalendar.getTimeInMillis()) { Calendar c1 = (Calendar) startCalendar.clone(); Calendar c2 = (Calendar) startCalendar.clone(); setToEndOfYear(c2); result.add(new Calendar[]{ c1, c2 }); startCalendar.add(Calendar.YEAR, 1); } break; } return result; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/utils/CommonUtils.java ================================================ package com.jiukuaitech.bookkeeping.user.utils; import com.jiukuaitech.bookkeeping.user.tag.Tag; import com.jiukuaitech.bookkeeping.user.tag_relation.TagRelation; import com.jiukuaitech.bookkeeping.user.transaction.Transaction; import org.springframework.util.DigestUtils; import javax.servlet.http.HttpServletRequest; import javax.xml.bind.DatatypeConverter; import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.util.*; import java.util.stream.Collectors; public class CommonUtils { public static List coalesce(BigDecimal... items) { return Arrays.stream(items).map(i -> i == null ? BigDecimal.valueOf(0) :i ).collect(Collectors.toList()); } public static void cleanTagRelation(Set tagRelations, Set requestTagIds) { Set entityTagIds = tagRelations.stream().map(i->i.getTag().getId()).collect(Collectors.toSet()); List existsIds = new ArrayList<>(); List toBeRemoved = new ArrayList<>(); requestTagIds.forEach(i -> { if (entityTagIds.contains(i)) { existsIds.add(i); } }); tagRelations.forEach(j -> { if (!requestTagIds.contains(j.getTag().getId())) { toBeRemoved.add(j); } }); tagRelations.removeAll(toBeRemoved); requestTagIds.removeAll(existsIds); } public static TagRelation getTagRelation(Integer tagId, Transaction transaction) { TagRelation po = new TagRelation(); po.setAmount(transaction.getAmount()); po.setConvertedAmount(transaction.getConvertedAmount()); po.setTransaction(transaction); po.setTag(new Tag(tagId)); return po; } /** * 获取用户真实IP地址,不使用request.getRemoteAddr()的原因是有可能用户使用了代理软件方式避免真实IP地址, * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值 * * @return ip */ public static String getRealIP(HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for"); if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) { // 多次反向代理后会有多个ip值,第一个ip才是真实ip if( ip.indexOf(",")!=-1 ){ ip = ip.split(",")[0]; } } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); System.out.println("Proxy-Client-IP ip: " + ip); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); System.out.println("WL-Proxy-Client-IP ip: " + ip); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); System.out.println("HTTP_CLIENT_IP ip: " + ip); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); System.out.println("HTTP_X_FORWARDED_FOR ip: " + ip); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("X-Real-IP"); System.out.println("X-Real-IP ip: " + ip); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); System.out.println("getRemoteAddr ip: " + ip); } return ip; } public static String encodePassword(String str) { byte[] digest = DigestUtils.md5Digest(str.getBytes()); return DatatypeConverter.printHexBinary(digest); } public static String formatDate(Long date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); return sdf.format(new Date(date)); } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/utils/EnumUtils.java ================================================ package com.jiukuaitech.bookkeeping.user.utils; public class EnumUtils { public static String translateFlowType(int value) { switch (value) { case 1: return "支出"; case 2: return "收入"; case 3: return "转账"; case 4: return "余额调整"; } return "未知"; } public static String translateFlowStatus(int value) { switch (value) { case 1: return "正常"; case 2: return "待确认"; case 3: return "已退款"; } return "未知"; } public static String translateAccountType(int value) { switch (value) { case 1: return "活期账户"; case 2: return "信用账户"; case 3: return "贷款账户"; case 4: return "资产账户"; } return "未知"; } public static String translateItemRepeatType(int type) { switch (type) { case 1: return "天"; case 2: return "月"; case 3: return "年"; } return "未知"; } } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/validation/AmountAccumulateValidator.java ================================================ package com.jiukuaitech.bookkeeping.user.validation; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.Digits; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; /* 累积统计的数额限制,比如统计流水的数额。 */ @Digits(integer = 14, fraction = 2) @Target({ FIELD }) @Retention(RUNTIME) @Constraint(validatedBy = { }) @Documented public @interface AmountAccumulateValidator { String message() default "{javax.validation.constraints.Digits.message}"; Class[] groups() default { }; Class[] payload() default { }; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/validation/AmountValidator.java ================================================ package com.jiukuaitech.bookkeeping.user.validation; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.Digits; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; /* 每一笔交易的数额限制 */ @Digits(integer = 9, fraction = 2) @Target({ FIELD }) @Retention(RUNTIME) @Constraint(validatedBy = { }) @Documented public @interface AmountValidator { String message() default "{javax.validation.constraints.Digits.message}"; Class[] groups() default { }; Class[] payload() default { }; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/validation/AprValidator.java ================================================ package com.jiukuaitech.bookkeeping.user.validation; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.Digits; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; /* 每一笔交易的数额限制 */ @Digits(integer = 3, fraction = 2) @Target({ FIELD }) @Retention(RUNTIME) @Constraint(validatedBy = { }) @Documented public @interface AprValidator { String message() default "{javax.validation.constraints.Digits.message}"; Class[] groups() default { }; Class[] payload() default { }; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/validation/AvatarValidator.java ================================================ package com.jiukuaitech.bookkeeping.user.validation; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.Size; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Size(max = 128, message = "notes size must be less than 128") @Target({ FIELD }) @Retention(RUNTIME) @Constraint(validatedBy = { }) @Documented public @interface AvatarValidator { String message() default "{javax.validation.constraints.Size.message}"; Class[] groups() default { }; Class[] payload() default { }; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/validation/BalanceValidator.java ================================================ package com.jiukuaitech.bookkeeping.user.validation; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.Digits; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; /* 账户的余额 */ @Digits(integer = 9, fraction = 2) @Target({ FIELD }) @Retention(RUNTIME) @Constraint(validatedBy = { }) @Documented public @interface BalanceValidator { String message() default "{javax.validation.constraints.Digits.message}"; Class[] groups() default { }; Class[] payload() default { }; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/validation/BillDayValidator.java ================================================ package com.jiukuaitech.bookkeeping.user.validation; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; /* 信用账户,还款日限制 */ @Min(1) @Max(31) @Target({ FIELD }) @Retention(RUNTIME) @Constraint(validatedBy = { }) @Documented public @interface BillDayValidator { String message() default "{javax.validation.constraints.Digits.message}"; Class[] groups() default { }; Class[] payload() default { }; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/validation/CreditLimitValidator.java ================================================ package com.jiukuaitech.bookkeeping.user.validation; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.Digits; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; /* 信用账户,信用额度限制 */ @Digits(integer = 9, fraction = 2) @Target({ FIELD }) @Retention(RUNTIME) @Constraint(validatedBy = { }) @Documented public @interface CreditLimitValidator { String message() default "{javax.validation.constraints.Digits.message}"; Class[] groups() default { }; Class[] payload() default { }; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/validation/DescriptionValidator.java ================================================ package com.jiukuaitech.bookkeeping.user.validation; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.Size; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Size(max = 16, message = "description size must be less than 16") @Target({ FIELD }) @Retention(RUNTIME) @Constraint(validatedBy = { }) @Documented public @interface DescriptionValidator { String message() default "{javax.validation.constraints.Size.message}"; Class[] groups() default { }; Class[] payload() default { }; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/validation/NameValidator.java ================================================ package com.jiukuaitech.bookkeeping.user.validation; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.Size; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Size(max = 16, message = "name size must be between 0 and 16") @Target({ FIELD }) @Retention(RUNTIME) @Constraint(validatedBy = { }) @Documented public @interface NameValidator { String message() default "{javax.validation.constraints.Size.message}"; Class[] groups() default { }; Class[] payload() default { }; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/validation/NoValidator.java ================================================ package com.jiukuaitech.bookkeeping.user.validation; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.Size; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Size(max = 32, message = "no size must be between 0 and 32") @Target({ FIELD }) @Retention(RUNTIME) @Constraint(validatedBy = { }) @Documented public @interface NoValidator { String message() default "{javax.validation.constraints.Size.message}"; Class[] groups() default { }; Class[] payload() default { }; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/validation/NotesValidator.java ================================================ package com.jiukuaitech.bookkeeping.user.validation; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.Size; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Size(max = 1024, message = "notes size must be less than 1024") @Target({ FIELD }) @Retention(RUNTIME) @Constraint(validatedBy = { }) @Documented public @interface NotesValidator { String message() default "{javax.validation.constraints.Size.message}"; Class[] groups() default { }; Class[] payload() default { }; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/validation/PasswordValidator.java ================================================ package com.jiukuaitech.bookkeeping.user.validation; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @NotBlank(message = "password must not be blank") @Size(min = 6, max = 32, message = "password size must be between 6 and 32") @Pattern(regexp = "^[A-Za-z0-9~`!@#\\$%\\^&\\*\\(\\)-_=\\+\\[\\]\\{\\}\\|]*$") //数字,字母,特殊字符(不包括空格) @Target({ FIELD }) @Retention(RUNTIME) @Constraint(validatedBy = { }) @Documented public @interface PasswordValidator { String message() default "{javax.validation.constraints.Pattern.message}"; Class[] groups() default { }; Class[] payload() default { }; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/validation/TimeValidator.java ================================================ package com.jiukuaitech.bookkeeping.user.validation; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.Min; import javax.validation.constraints.Max; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Min(0) @Max(99999999999999L) @Target({ FIELD }) @Retention(RUNTIME) @Constraint(validatedBy = { }) @Documented public @interface TimeValidator { String message() default "{javax.validation.constraints.Min.message}"; Class[] groups() default { }; Class[] payload() default { }; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/validation/TransactionStatusValidator.java ================================================ package com.jiukuaitech.bookkeeping.user.validation; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Min(1) @Max(4) @Target({ FIELD }) @Retention(RUNTIME) @Constraint(validatedBy = { }) @Documented public @interface TransactionStatusValidator { String message() default "{javax.validation.constraints.Digits.message}"; Class[] groups() default { }; Class[] payload() default { }; } ================================================ FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/validation/UserNameValidator.java ================================================ package com.jiukuaitech.bookkeeping.user.validation; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @NotBlank(message = "username must not be blank") @Size(min = 3, max = 20, message = "username size must be between 3 and 20") @Pattern(regexp = "^[A-Za-z0-9]*$") //用户名只允许数字加字母 @Target({ FIELD }) @Retention(RUNTIME) @Constraint(validatedBy = { }) @Documented public @interface UserNameValidator { String message() default "{javax.validation.constraints.Size.message}"; Class[] groups() default { }; Class[] payload() default { }; } ================================================ FILE: bookkeeping-user-api/src/main/resources/application.properties ================================================ spring.datasource.dialect=org.hibernate.dialect.MySQL8InnoDBDialect spring.datasource.url=jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true spring.datasource.username=${DB_USER} spring.datasource.password=${DB_PASSWORD} spring.datasource.type=com.zaxxer.hikari.HikariDataSource spring.datasource.hikari.minimumIdle=5 spring.datasource.hikari.maximumPoolSize=20 spring.datasource.hikari.idleTimeout=30000 spring.datasource.hikari.poolName=BookkeepingUserJPAHikariCP spring.datasource.hikari.maxLifetime=2000000 spring.datasource.hikari.connectionTimeout=30000 spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=false spring.jpa.properties.hibernate.generate_statistics=false spring.jpa.properties.hibernate.show_sql=false spring.jpa.properties.hibernate.format_sql=true spring.mvc.throw-exception-if-no-handler-found=true spring.jpa.properties.hibernate.jdbc.batch_size = 20 spring.jpa.properties.hibernate.order_inserts = true spring.jpa.properties.hibernate.order_updates = true spring.jpa.properties.hibernate.jdbc.batch_versioned_data = true spring.jpa.properties.hibernate.validator.apply_to_ddl = true spring.jpa.properties.hibernate.check_nullability = true server.port=${SERVER_PORT:9092} spring.mvc.servlet.path=/api/v1 spring.data.web.pageable.size-parameter=size spring.data.web.pageable.page-parameter=page spring.data.web.pageable.default-page-size=10 spring.data.web.pageable.one-indexed-parameters=true spring.data.web.pageable.max-page-size=100 spring.data.web.pageable.prefix= spring.data.web.pageable.qualifier-delimiter=_ spring.messages.encoding=UTF-8 spring.messages.basename=i18n/messages spring.messages.cache-duration=3600 spring.devtools.add-properties=false #logging.level.web=DEBUG upload.ACCESS_KEY = ${UPLOAD_ACCESS_KEY:} upload.SECRET_KEY = ${UPLOAD_SECRET_KEY:} upload.FLOW_IMAGE_BUCKET = ${UPLOAD_FLOW_IMAGE_BUCKET:} upload.FLOW_IMAGE_HOST = ${UPLOAD_FLOW_IMAGE_HOST:} upload.FLOW_IMAGE_CALL_BACK_URL = ${UPLOAD_FLOW_IMAGE_CALL_BACK_URL:} invite.code = ${INVITE_CODE:} # 自定义 # 类别的最大分层级数 category.max.level = 4 # 收支趋势时间范围的最大划分区间个数 trend.time.max.break = 370 # 单个用户添加组,最多个数 group.max.count = 5 book.max.count = 200 account.max.count = 500 category.max.count = 200 tag.max.count = 300 payee.max.count = 300 flow.max.count.daily = 400 ## MULTIPART (MultipartProperties) # Enable multipart uploads spring.servlet.multipart.enabled=true # Threshold after which files are written to disk. spring.servlet.multipart.file-size-threshold=2KB # Max file size. spring.servlet.multipart.max-file-size=10MB # Max Request Size spring.servlet.multipart.max-request-size=20MB ================================================ FILE: bookkeeping-user-api/src/main/resources/i18n/messages.properties ================================================ DefaultException=网络错误,请稍后重试 HttpRequestMethodNotSupportedException=HttpRequestMethodNotSupportedException HttpMediaTypeNotSupportedException=HttpMediaTypeNotSupportedException NoHandlerFoundException=Not Found MethodArgumentNotValidException=输入不合法 DataIntegrityViolationException=系统异常,请稍后重试。 ConstraintViolationException=ConstraintViolationException MissingServletRequestParameterException=MissingServletRequestParameterException ItemNotFoundException=记录不存在 NameExistsException=名称重复 # Account AccountNameExistsException=操作失败,账户名称重复。 AccountHasTransactionException=该账户下有账单记录,无法删除。 AccountAdjustBalanceNotValidException=调整前后余额一样 DefaultExpenseAccountException=此账户为默认支出账户,无法禁用。 DefaultIncomeAccountException=此账户为默认收入账户,无法禁用。 DefaultTransferFromAccountException=此账户为默认转出账户,无法禁用。 DefaultTransferToAccountException=此账户为默认转入账户,无法禁用。 AccountMaxCountException=最多添加500个账户 #BalanceFlow CategoryConflictException=分类重复 CategoryLevelException=分类的层级不能超过4 BalanceFlowStatusException=状态异常 AccountInvalidateException=账户不合法 AmountInvalidateException=金额输入错误 StatusNotValidateException=状态错误 FlowMaxCountException=24小时内最多添加400条账单记录 #Transfer TransferFromEqualsToException=操作失败,转入和转出的账户不能相同。 #Category CategoryHasDealException=类别下面有账单记录,无法删除。 CategoryNameExistsException=类别名称已存在 ParentCategoryNotEnableException=父级类别不可用 CategoryIsDefaultExpenseException=此分类为默认支出分类,无法操作。 CategoryIsDefaultIncomeException=此分类为默认收入分类,无法操作。 CategoryMaxCountException=最多添加200个分类 # Payee PayeeHasDealException=此交易对象存在交易记录,无法删除。 PayeeMaxCountException=最多添加300个交易对象 # Tag TagHasTransactionException=此标签存在账单记录,无法删除。 TagMaxCountException=最多添加300个标签 #User RegisterNameExistsException=注册失败,用户名已存在。 SigninFailedException=登录失败,用户名或密码错误。 InviteCodeErrorException=邀请码错误 UploadNotImageException=上传的文件类型不支持 OldPasswordErrorException=原始密码错误 UserDisabledException=用户已禁用 #Report BreakOutOfMaxException=区间划分超过50 #group GroupHasBookException=该组下存在账本,无法操作。 GroupMaxCountException=最多添加5个分组 #Book BookMaxCountException=最多添加200个账本 #DealImage ImageExistsException=图片文件已存在,不能重复上传。 UploadKeyEmptyException=请配置上传需要的环境变量 TokenEmptyException=请先登录 TokenNotValidException=登录状态已过期,请重新登录。 PermissionException=PermissionException #Item ItemCountException=执行已到最大 # common message categoryOther = 其他 expense = 支出 income = 收入 remain = 结余 asset = 资产 debt = 负债 netWorth = 净资产 ================================================ FILE: bookkeeping-user-api/start.sh ================================================ #!/bin/bash export $(grep -v '^#' .env | xargs) ./mvnw spring-boot:run ================================================ FILE: bookkeeping-user-api/startup.sh ================================================ #!/bin/bash export $(grep -v '^#' .env | xargs) nohup java -jar bookkeeping-user-0.1.jar> 1.log 2>&1 & echo $! > pid.file ================================================ FILE: bookkeeping-user-api/stop.sh ================================================ #!/bin/bash kill $(cat pid.file) #ps -ef | grep 'bookkeeping-user-0.1.jar' | grep -v grep | awk '{print $2}' | xargs kill -9 #ps -ef|grep -v grep|grep bookkeeping-user-0.1.jar | grep java |awk '{print "kill -9 "$2}'|sh ================================================ FILE: bookkeeping-user-fe/.editorconfig ================================================ # http://editorconfig.org root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false [Makefile] indent_style = tab ================================================ FILE: bookkeeping-user-fe/.eslintrc ================================================ { "extends": "eslint-config-umi" } ================================================ FILE: bookkeeping-user-fe/.prettierignore ================================================ **/*.md **/*.svg **/*.ejs **/*.html package.json .umi .umi-production ================================================ FILE: bookkeeping-user-fe/.prettierrc ================================================ { "singleQuote": true, "trailingComma": "all", "printWidth": 100, "overrides": [ { "files": ".prettierrc", "options": { "parser": "json" } } ] } ================================================ FILE: bookkeeping-user-fe/.umirc.js ================================================ // ref: https://umijs.org/config/ import { defineConfig } from 'umi'; export default defineConfig({ proxy: { '/api/v1': { 'target': 'http://127.0.0.1:9092/', // 'target': 'http://testjz.jiukuaitech.com/', 'changeOrigin': true, }, }, // publicPath: process.env.NODE_ENV === 'production' ? 'http://static.jiukuaitech.com/' : '/', // hash: true, antd: { compact: true }, dva: { hmr: true, }, locale: { default: 'zh-CN', }, // dynamicImport: { // // }, title: '九快记账', }) ================================================ FILE: bookkeeping-user-fe/Dockerfile ================================================ #FROM nginx:1.21-alpine #COPY ./docker/nginx.conf.template /etc/nginx/templates/default.conf.template #COPY ./docker/gzip.conf /etc/nginx/conf.d/gzip.conf #COPY ./dist/ /usr/share/nginx/html FROM node:16-slim as build WORKDIR /workspace/app COPY src src COPY package.json . COPY package-lock.json . RUN npm install RUN npm run build FROM nginx:1.21-alpine COPY ./docker/nginx.conf.template /etc/nginx/templates/default.conf.template COPY ./docker/gzip.conf /etc/nginx/conf.d/gzip.conf COPY --from=build /workspace/app/dist/ /usr/share/nginx/html ================================================ FILE: bookkeeping-user-fe/docker/gzip.conf ================================================ gzip on; gzip_min_length 1k; gzip_buffers 4 16k; gzip_http_version 1.1; gzip_comp_level 2; gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml; gzip_vary on; gzip_proxied expired no-cache no-store private auth; ================================================ FILE: bookkeeping-user-fe/docker/nginx.conf ================================================ server { listen 80; listen [::]:80; server_name localhost; location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; } location ~ ^/api/v1 { proxy_pass http://user-api:9092; } } ================================================ FILE: bookkeeping-user-fe/docker/nginx.conf.template ================================================ server { listen 80; listen [::]:80; server_name localhost; location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; } location ~ ^/api/v1 { proxy_pass ${API_HOST}; proxy_set_header Host $http_host; proxy_set_header Cookie $http_cookie; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ================================================ FILE: bookkeeping-user-fe/mock/.gitkeep ================================================ ================================================ FILE: bookkeeping-user-fe/package.json ================================================ { "private": true, "scripts": { "start": "umi dev", "build": "umi build" }, "dependencies": { "@antv/data-set": "^0.11.8", "bizcharts": "^3.5.3-beta.0", "dva-model-extend": "^0.1.2" }, "devDependencies": { "cross-env": "^5.0.5", "umi": "^3.5.0", "@umijs/preset-react": "^1.4.8" } } ================================================ FILE: bookkeeping-user-fe/src/app.js ================================================ export const dva = { config: { onError(err) { err.preventDefault(); console.error(err.message); }, }, }; ================================================ FILE: bookkeeping-user-fe/src/components/AccountRecordTable/index.jsx ================================================ import { useEffect, useState } from "react"; import { useResultPaginationAndData } from '@/utils/hooks'; import { tableChangeQueryFormat } from "@/utils/util"; import {query as queryFlows} from '@/services/flow' import FlowRecordTable from '@/components/FlowRecordTable'; export default (props) => { // currentTime刷新使用 const { accountId, currentTime } = props; const [queryResponse, setQueryResponse] = useState(); const [ dataAndPagination ] = useResultPaginationAndData(queryResponse); const [loading, setLoading] = useState(false); async function fetchData(id, query) { setLoading(true); let response = await queryFlows(Object.assign({accountId: id}, query)); setQueryResponse(response); setLoading(false); } function handleTableChange(pagination, _, sorter) { fetchData(accountId, tableChangeQueryFormat(pagination, sorter)); } useEffect(() => { fetchData(accountId, dataAndPagination.pagination.current, dataAndPagination.pagination.pageSize); }, [accountId, currentTime]); return ( ); } ================================================ FILE: bookkeeping-user-fe/src/components/AdjustBalanceModal/index.jsx ================================================ import { useEffect, useState } from 'react'; import { useSelector } from 'umi'; import { Form, Space, Input } from 'antd'; import FormModal from "@/components/FormModal"; import FormItemDescription from '@/components/FormItemDescription'; import FormItemCreateTime from '@/components/FormItemCreateTime'; import moment from 'moment'; import {balanceRequiredRules, notesRules} from '@/utils/rules'; import { adjustBalance } from '@/services/account'; import {formProp} from "@/utils/var"; import {getNull, refreshFlow} from "@/utils/util"; import t from "@/utils/translate"; import styles from "./index.less"; export default () => { const [form] = Form.useForm(); const { visible, currentItem } = useSelector(state => state.modal); const [initialValues, setInitialValues] = useState({}); useEffect(() => { if (visible) { setInitialValues({ ...getNull(form.getFieldsValue()), //统一将简单属性置空 'createTime': moment(), }); } }, [visible]); function successHandler(response) { refreshFlow(response.data); } return ( successHandler(response)} >
{t('account.current.balance')}: {currentItem.balance}
); } ================================================ FILE: bookkeeping-user-fe/src/components/AdjustBalanceModal/index.less ================================================ .form { :global { .ant-form-item-label { width: 70px; } } } ================================================ FILE: bookkeeping-user-fe/src/components/AlertTotalSearch/index.jsx ================================================ import { Alert } from 'antd'; export default (props) => { const { message } = props; return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/components/AmountInput/index.jsx ================================================ import { Input } from 'antd'; export default (props) => { const { amount: value, positive = 0, onBlur:blurHandler, onChange: changeHandler} = props; const onChange = e => { const { value } = e.target; const reg = /^-?\d{1,9}(\.\d{0,2})?$/; if (positive && value === '-') { changeHandler(''); return false; } if ((!isNaN(value) && reg.test(value)) || value === '' || value === '-') { changeHandler(value); } }; // '.' at the end or only '-' in the input box. const onBlur = e => { if (value) { let valueTemp = value; if (value.charAt(value.length - 1) === '.' || value === '-') { valueTemp = value.slice(0, -1); } changeHandler(valueTemp.replace(/0*(\d+)/, '$1')); } if (blurHandler) { blurHandler(); } }; return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/components/AmountInputPositive/index.jsx ================================================ import { Input } from 'antd'; export default (props) => { const onChange = e => { const { value } = e.target; const reg = /^-?\d{1,9}(\.\d{0,2})?$/; if (value !== '-') { if ((!isNaN(value) && reg.test(value)) || value === '' || value === '-') { props.onChange(value); } } }; // '.' at the end or only '-' in the input box. const onBlur = () => { const { value1:value, onBlur, onChange } = props; let valueTemp = value; if (value.charAt(value.length - 1) === '.' || value === '-') { valueTemp = value.slice(0, -1); } onChange(valueTemp.replace(/0*(\d+)/, '$1')); if (onBlur) { onBlur(); } }; return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/components/AvatarDropdown/UpdatePasswordModal.jsx ================================================ import {Form, Input} from 'antd'; import { updatePassword } from '@/services/user'; import {requiredRules} from "@/utils/rules"; import FormModal from "@/components/FormModal"; import styles from './index.less'; import t from "@/utils/translate"; export default () => { const [form] = Form.useForm(); function successHandler() { window.location.href = '/signin'; } return (
); } ================================================ FILE: bookkeeping-user-fe/src/components/AvatarDropdown/index.jsx ================================================ import {useEffect} from "react"; import {useSelector, useDispatch} from 'umi'; import {Avatar, Menu, message, Spin} from 'antd'; import moment from "moment"; import { signout } from '@/services/user'; import { LogoutOutlined, LockOutlined, UserOutlined } from '@ant-design/icons'; import HeaderDropdown from '@/components/HeaderDropdown'; import UpdatePasswordModal from './UpdatePasswordModal'; import t from '@/utils/translate'; import styles from './index.less'; export default (props) => { const dispatch = useDispatch(); const { user } = useSelector(state => state.session); useEffect(() => { if (!user) dispatch({ type: 'session/fetchSession' }); }, []); const messageOperationSuccess = t('operation.success'); const onMenuClick = async (event) => { const { key } = event; if (key === 'logout') { const response = await signout(); if (response && response.success) { message.success(messageOperationSuccess); window.location.href = "/signin"; } } if (key === 'updatePassword') { dispatch({ type: 'modal/show', payload: {component: UpdatePasswordModal }}); } }; const { currentUser = { avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png', name: user && user.userName ? user.userName : t('signin'), vipTime: user && user.vipTime ? user.vipTime : 0 }, menu, } = props; const menuHeaderDropdown = ( {menu && ({t('profile')})} {menu && ({t('update.password')})} {/*{menu && ({t('settings')})}*/} {menu && } {menu && ({t('logout')})} {menu && (会员到期时间: {moment(currentUser.vipTime).format('YYYY-MM-DD')})} ); return currentUser && currentUser.name ? ( {currentUser.name} ) : ( ); } ================================================ FILE: bookkeeping-user-fe/src/components/AvatarDropdown/index.less ================================================ .avatar { display: inline-block; height: 100%; padding: 0 20px; cursor: pointer; transition: color 0.3s; &:hover { background: rgba(0, 0, 0, 0.025); } } .form3 { :global { .ant-form-item-label { width: 75px; } } } ================================================ FILE: bookkeeping-user-fe/src/components/Breadcrumb/index.jsx ================================================ import { Breadcrumb } from 'antd'; export default (props) => { return ( { props.data.map((item, index) => {item}) } ); }; ================================================ FILE: bookkeeping-user-fe/src/components/ExpenseModal/index.jsx ================================================ import {useEffect, useState} from 'react'; import {useDispatch, useSelector} from 'umi'; import {Form, Input, Switch} from 'antd'; import moment from 'moment'; import { useCategoryTreeSelectData, useResponseData, useResponseSelectData } from "@/utils/hooks"; import {getNull, refreshFlow} from "@/utils/util"; import FormModal from "@/components/FormModal"; import FormItemAccount from '@/components/FormItemAccount'; import FormListCategory from '@/components/FormListCategory'; import FormItemTag from '@/components/FormItemTag'; import FormItemPayee from '@/components/FormItemPayee'; import FormItemDescription from '@/components/FormItemDescription'; import FormItemCreateTime from '@/components/FormItemCreateTime'; import { create, update, refund } from '@/services/expense'; import t from '@/utils/translate'; import styles from './index.less'; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { defaultBook } = useSelector(state => state.session); const { visible, type, currentItem } = useSelector(state => state.modal); const { getExpenseableResponse : tagResponse } = useSelector(state => state.tag); const [tags] = useCategoryTreeSelectData(tagResponse); const { getExpenseableResponse : payeeResponse } = useSelector(state => state.payee); const [payees, setPayees] = useResponseSelectData(payeeResponse); const { getExpenseableResponse : accountResponse } = useSelector(state => state.account); const [accounts] = useResponseData(accountResponse); const { querySimpleResponse } = useSelector(state => state.expenseCategory); const [categories] = useCategoryTreeSelectData(querySimpleResponse); // TODO 很多地方有这个只加载一次的数据,待优化 useEffect(() => { if (visible) { if (!defaultBook) dispatch({ type: 'session/fetchSession' }); if (!tagResponse) dispatch({ type: 'tag/fetchExpenseable' }); if (!payeeResponse) dispatch({ type: 'payee/fetchExpenseable' }); if (!accountResponse) dispatch({ type: 'account/fetchExpenseable' }); if (!querySimpleResponse) dispatch({ type: 'expenseCategory/fetchSimple' }); } }, [visible]); const [initialValues, setInitialValues] = useState({}); // 默认值必须为空,currentItem报错date.clone is not a function,调试了半天。 useEffect(() => { if (!visible) return; if (type === 1) { setInitialValues({ ...getNull(form.getFieldsValue()), //清空上次输入的 'createTime': moment(), 'accountId': defaultBook && defaultBook.defaultExpenseAccount && defaultBook.defaultExpenseAccount.id, 'categories': defaultBook && defaultBook.defaultExpenseCategory ? [{ categoryId: defaultBook.defaultExpenseCategory.id }] : [{ }], 'tags': [], 'confirmed': true, }); if (defaultBook && defaultBook.defaultExpenseAccount) { setAccountCurrencyCode(defaultBook.defaultExpenseAccount.currencyCode); } else { setAccountCurrencyCode(''); } } else { let currentItemCopy = JSON.parse(JSON.stringify(currentItem)); if (type === 4) { if (currentItemCopy.categories) currentItemCopy.categories.forEach(value => { value.amount = value.amount*(-1); value.convertedAmount = value.convertedAmount*(-1) }); } if (type === 3 || type === 4) { //不复制备注 currentItemCopy.notes = ""; } // 数字类型的校验存在问题, antd bug if (currentItemCopy.categories) currentItemCopy.categories.forEach(value => {value.amount = value.amount.toString();value.convertedAmount = value.convertedAmount.toString()}); currentItemCopy.createTime = type === 2 ? moment(currentItemCopy.createTime) : moment(); currentItemCopy.accountId = currentItemCopy.account ? currentItemCopy.account.id : null; currentItemCopy.payeeId = currentItemCopy.payee ? currentItemCopy.payee.id : null; currentItemCopy.tags = currentItemCopy.tags ? currentItemCopy.tags.map(item => item.tagId) : null; currentItemCopy.confirmed = type === 3 || type === 4 ? true : currentItemCopy.status === 1; /* 处理修改时,账户已禁用的情况,未完。 if (type === 3) { if (!currentItemCopy.account.enable) { currentItemCopy.accountId = null; } } else if (!currentItemCopy.account.enable) { if (!accounts.some(e => e.id === currentItemCopy.account.id)) { setAccounts([{ id: currentItemCopy.account.id, name: currentItemCopy.account.name }, ...accounts]); } } */ setInitialValues({ ...getNull(form.getFieldsValue()), ...currentItemCopy, 'categories': currentItemCopy.categories ? currentItemCopy.categories : [{}], }); setAccountCurrencyCode(currentItemCopy.currencyCode); } }, [visible, type, currentItem]); function successHandler(response) { refreshFlow(response.data); } const [accountCurrencyCode, setAccountCurrencyCode] = useState(); const accountChangeHandler = (value) => { for (let i = 0; i < accounts.length; i++) { if (accounts[i].id === value) { setAccountCurrencyCode(accounts[i].currencyCode); } } } return ( successHandler(response)} >
{(type !== 2) && }
); } ================================================ FILE: bookkeeping-user-fe/src/components/ExpenseModal/index.less ================================================ .form { :global { .ant-form-item-label { width: 70px; } } } ================================================ FILE: bookkeeping-user-fe/src/components/FlagTag/index.jsx ================================================ import { Tag } from "antd"; import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'; import t from "@/utils/translate"; export default (props) => { return ( props.value ? } color="success" style={{marginRight: 0}}>{t('yes')} : } color="error" style={{marginRight: 0}}>{t('no')} ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FlowButtons/index.jsx ================================================ import {Button, Space} from "antd"; import { PlusOutlined } from '@ant-design/icons'; import {showFlowModal} from '@/utils/flow'; import t from '@/utils/translate'; export default () => { return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FlowImageUploadModal/index.jsx ================================================ import {useEffect, useState} from "react"; import {useDispatch, useSelector} from "umi"; import {message, Modal, Upload} from "antd"; import {updateImages} from '@/services/flow'; import t from "@/utils/translate"; import {PlusOutlined} from "@ant-design/icons"; export default (props) => { const dispatch = useDispatch(); const { user } = useSelector(state => state.session); const { visible, currentItem } = useSelector(state => state.modal); const { images, uploadToken } = useSelector(state => state.flow); useEffect(() => { if (currentItem && currentItem.id) { dispatch({ type: 'flow/fetchImages', payload: {id: currentItem.id} }); } }, [currentItem]); useEffect(() => { if (visible) { dispatch({ type: 'flow/fetchUploadToken' }) } }, [visible]); function cancelHandler() { dispatch({ type: 'modal/hide' }); } useEffect(() => { if (images) { setFileList( images.map(image => { return { uid: image.id, id: image.id, status: 'done', url: image.url, } }) ); } }, [images]); const [fileList, setFileList] = useState([]); const [uploadError, setUploadError] = useState(false); useEffect(() => { if (uploadError) { let newFileList = [...fileList]; setFileList(newFileList.filter(file => { if (!file.status) return false; //beforeUpload没通过 if (file.status === 'error') return false; //七牛报错 if (file.status === 'done' && (file.response && !file.response.success)) return false; //文件已存在,回调报错 return true; })); setUploadError(false); } }, [uploadError]); const messageFileSize = t('upload.size.error'); const uploadProps = { accept: 'image/jpeg, image/png, application/pdf', multiple: true, maxCount: 3, fileList: fileList, listType: 'picture-card', action: 'http://upload-z2.qiniup.com', data: { 'token': uploadToken, 'x:userId': user && user.id }, onChange(info) { let newFileList = [...info.fileList]; newFileList = newFileList.map(file => { if (file.status === 'done' && file.response && file.response.success) { file.url = file.response.data.url; file.id = file.response.data.id; } return file; }); setFileList(newFileList); if (info.file.status === 'error') { message.error(info.file.response.error); setUploadError(true); } if (info.file.status === 'done') { if (!info.file.response.success) { message.error(info.file.response.errorMsg); setUploadError(true); } } }, beforeUpload(file) { let isOk = true; if (file.size > 1.5*1024*1024) { message.error(messageFileSize); isOk = false; } if (isOk) dispatch({ type: 'flow/fetchUploadToken' }); if (!isOk) setUploadError(true); return isOk; } } const messageOperationSuccess = t('operation.success'); const [confirmLoading, setConfirmLoading] = useState(false); async function okHandler() { setConfirmLoading(true); const response = await updateImages(currentItem.id, fileList.map(i=>i.id)); if (response && response.success) { cancelHandler(); message.success(messageOperationSuccess); } setConfirmLoading(false); } return ( { fileList.length >= 3 ? null :
Upload
}
) } ================================================ FILE: bookkeeping-user-fe/src/components/FlowRecordTable/index.jsx ================================================ import {Button, message, Modal, Space, Table, Descriptions, Dropdown, Menu} from 'antd'; import {DownOutlined} from "@ant-design/icons"; import {tableProp} from "@/utils/var"; import {amountCol, createTimeCol, flowTypeCol, statusCol} from '@/utils/columns'; import {useDescriptionEnable, useImageEnable, useTimeFormat} from "@/utils/hooks"; import {refreshFlow} from "@/utils/util"; import {imageHandler, showFlowModal} from '@/utils/flow'; import FlowTagDisplay from '@/components/FlowTagDisplay'; import { remove, confirm } from '@/services/flow'; import t from "@/utils/translate"; export default (props) => { const { queryLoading, data, pagination, tableChangeHandler, bordered, noBook, } = props; const imageEnable = useImageEnable(); const descriptionEnable = useDescriptionEnable(); const timeFormat = useTimeFormat(); const messageDeleteConfirm = t('delete.confirm', { name: '' }); const messageDeleteConfirmBalance = t('delete.confirm.balance'); const messageOperationSuccess = t('operation.success'); function deleteHandler(record) { Modal.confirm({ title: record.status === 2 ? messageDeleteConfirm : messageDeleteConfirmBalance, onOk: async () => { const response = await remove(record); if (response && response.success) { message.success(messageOperationSuccess); refreshFlow(response.data); } } }); } const messageConfirmOperation = t('confirm.operation'); function confirmHandler(record) { Modal.confirm({ title: messageConfirmOperation, onOk: async () => { const response = await confirm(record); if (response && response.success) { message.success(messageOperationSuccess); refreshFlow(response.data); } } }); } function refundHandler(record) { if (record.type === 1) { showFlowModal(1, 4, { ...record.expense }); } else if (record.type === 2) { showFlowModal(2, 4, { ...record.income }); } } function copyHandler(record) { if (record.type === 1) { showFlowModal(1, 3, { ...record.expense }); } else if (record.type === 2) { showFlowModal(2, 3, { ...record.income }); } else if (record.type === 3) { showFlowModal(3, 3, { ...record.transfer }); } } function updateHandler(record) { if (record.type === 1) { showFlowModal(1, 2, { ...record.expense }); } else if (record.type === 2) { showFlowModal(2, 2, { ...record.income }); } else if (record.type === 3) { showFlowModal(3, 2, { ...record.transfer }); } } let columns = []; if (!noBook) { columns.push({ title: t('flow.book'), dataIndex: 'bookName', }); } if (descriptionEnable) { columns.push({ title: t('description'), dataIndex: 'description', }); } columns = columns.concat( [ flowTypeCol(), amountCol(), createTimeCol(timeFormat), { title: t('account'), dataIndex: 'accountName', }, { title: t('category'), dataIndex: 'categoryName', }, { title: t('flow.tag'), dataIndex: 'tags', render: (tags, record) => <>{tags.map(i => )} }, { title: t('flow.payee'), dataIndex: 'payee', sorter: true, render: payee => <>{payee ? payee.name : null} }, statusCol(), { title: t('operation'), key: 'operation', width: 100, align: 'center', render: (_, record) => { return ( {imageEnable ? imageHandler(record)}>{t('image')} : null} updateHandler(record)}>{t('update')} confirmHandler(record)}>{t('confirm')} 0))} onClick={() => refundHandler(record)}>{t('refund')} deleteHandler(record)}>{t('delete')} }> {t('more')} ) } } ] ); function rowExpandableRecord(record) { if (record.notes) { return true; } return record.needConvert; } function descriptionsItemRecord(record) { let notesItem = null; if (record.notes) { notesItem = {record.notes}; } let currencyItem = null; if (record.needConvert) { currencyItem = {record.convertedAmount} } return <>{notesItem}{currencyItem}; } return ( {descriptionsItemRecord(record)} , rowExpandable: record => rowExpandableRecord(record), }} dataSource={data} pagination={pagination} loading={queryLoading} onChange={tableChangeHandler} /> ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FlowStatusDisplay/index.jsx ================================================ import {useMemo} from "react"; import {Tag, Tooltip} from "antd"; import { history } from 'umi'; import {flowStatusToColor} from "@/utils/util"; import {getRefunds} from '@/services/deal'; export default (props) => { const { data } = props; const clickable = useMemo(() => data.status === 3 || data.amount < 0, [data]); async function clickHandler() { const response = await getRefunds(data.id); if (response && response.success) { history.push({ pathname: history.pathname, query: { id: response.data.join(','), }, }); } } // TODO 多语言 return ( clickable ? {data.statusName} : {data.statusName} ); } ================================================ FILE: bookkeeping-user-fe/src/components/FlowTagDisplay/TagModal.jsx ================================================ import { useEffect, useState } from 'react'; import {history, useDispatch, useSelector} from 'umi'; import {Form, Input, message} from 'antd'; import {getNull, validateForm, refreshFlow} from "@/utils/util"; import {amountRequiredRules} from "@/utils/rules"; import FormModal from "@/components/FormModal"; import {update} from '@/services/tag-relation'; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { visible, currentItem } = useSelector(state => state.modal); const { defaultBook } = useSelector(state => state.session); useEffect(() => { if (!defaultBook) dispatch({ type: 'session/fetchSession' }); }, []); const [initialValues, setInitialValues] = useState({}) useEffect(() => { if (visible) { currentItem.amount = currentItem.amount.toString(); setInitialValues({...getNull(form.getFieldsValue()), ...currentItem}); } }, [visible]); function successHandler() { refreshFlow(); } return (
{ defaultBook && defaultBook.defaultCurrencyCode !== currentItem.currencyCode ? : null }
); } ================================================ FILE: bookkeeping-user-fe/src/components/FlowTagDisplay/index.jsx ================================================ import {useState} from "react"; import {Tag, Tooltip} from "antd"; import {useDispatch} from "umi"; import TagModal from './TagModal'; import t from "@/utils/translate"; export default (props) => { const { data, record } = props; const dispatch = useDispatch(); const updateTagAmountHandler = () => { dispatch({ type: 'modal/show', payload: {component: TagModal, type: 2, currentItem: {currencyCode: record.currencyCode, ...data} } }); } // TODO 多语言 return ( record.categoryName || record.type === 1 || record.type === 2 ? {data.tagName} : {data.tagName} ); } ================================================ FILE: bookkeeping-user-fe/src/components/Footer/index.jsx ================================================ import { Space } from 'antd'; import t from '@/utils/translate'; import styles from './index.less'; export default () => { return (
© {new Date().getFullYear()} {t('company.name')}版权所有  {t('footer.no')} v1.0.6
); }; ================================================ FILE: bookkeeping-user-fe/src/components/Footer/index.less ================================================ .footer { text-align: center; } .footer-link { a { color: rgba(0, 0, 0, 0.45); } } ================================================ FILE: bookkeeping-user-fe/src/components/FormItemAccount/index.jsx ================================================ import {useMemo} from "react"; import {Form, Select} from 'antd'; import {accountRequiredRules} from "@/utils/rules"; import t from '@/utils/translate'; export default (props) => { const { data, name = "accountId", label=t('account'), required=true, onSelectChange } = props; // const messageBalance = t('account.balance'); const accounts = useMemo(() => { if (data && data.length > 0) { return data.map(i => { return { value: i.id, //label: `${i.name} (${messageBalance}:${i.balance})`, label: i.name } }); } else { return 0; } }, [data]); const changeHandler = (value) => { if (onSelectChange) onSelectChange(value); } return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FormItemAmountRange/index.jsx ================================================ import {useEffect} from "react"; import {Col, Form, Input, Radio, Space} from 'antd'; import t from '@/utils/translate'; export default () => { return ( ~ ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FormItemCategories/index.jsx ================================================ import {Col, Form, TreeSelect} from 'antd'; import t from '@/utils/translate'; export default (props) => { const { data, name = "categories", label = t('category') } = props; return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FormItemCategoryReport/index.jsx ================================================ import {Col, Form, TreeSelect} from 'antd'; import t from '@/utils/translate'; export default (props) => { const { data } = props; return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FormItemCreateTime/index.jsx ================================================ import {DatePicker, Form} from 'antd'; import {timeRequiredRules} from "@/utils/rules"; import {getTimeEnable, getTimeFormat} from "@/utils/util"; import t from "@/utils/translate"; export default () => { return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FormItemDateRange/index.jsx ================================================ import {Col, DatePicker, Form, Radio, Space} from 'antd'; import {useEffect} from "react"; import {radioValueToTimeRange,getTimeFormat} from "@/utils/util"; import t from "@/utils/translate"; export default (props) => { const { form, dateRadioValue, setDateRadioValue } = props; function createTimeRadioChangeHandler(e) { setDateRadioValue(e.target.value); } useEffect(() => { form.setFieldsValue({ 'createTimeRange': radioValueToTimeRange(dateRadioValue), }); }, [dateRadioValue]); return ( {t('today')} {t('this.week')} {t('this.month')} {t('this.year')} {t('last.year')} {t('in.7.days')} {t('in.30.days')} {t('in.1.year')} ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FormItemDateRangeWithBreak/index.jsx ================================================ import {Col, DatePicker, Form, Radio, Row, Select, Space} from 'antd'; import {useEffect, useState} from "react"; import {radioValueToTimeRange,getTimeFormat,getTimeEnable} from "@/utils/util"; import t from "@/utils/translate"; export default (props) => { const { form, dateRadioValue, setDateRadioValue } = props; function createTimeRadioChangeHandler(e) { setDateRadioValue(e.target.value); } useEffect(() => { form.setFieldsValue({ 'createTimeRange': radioValueToTimeRange(dateRadioValue), 'breakType': breakType }); }, [dateRadioValue]); const [breakType, setBreakType] = useState("month"); return ( {t('this.week')} {t('this.month')} {t('this.year')} {t('last.year')} {t('in.7.days')} {t('in.30.days')} {t('in.1.year')} ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FormItemDescription/index.jsx ================================================ import {Form, Input} from 'antd'; import {getDescriptionEnable} from "@/utils/util"; import t from "@/utils/translate"; export default () => { return ( getDescriptionEnable() ? : null ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FormItemDescriptionSearch/index.jsx ================================================ import {Col, Form, Input} from 'antd'; import {getDescriptionEnable} from "@/utils/util"; import t from "@/utils/translate"; export default () => { return ( getDescriptionEnable() ? : null ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FormItemPayee/index.jsx ================================================ import {useState} from "react"; import {Divider, Form, Input, message, Select} from 'antd'; import {useDispatch} from "umi"; import {PlusOutlined} from "@ant-design/icons"; import {categoryTypeToCreateParam} from "@/utils/util"; import {create as createPayee} from "@/services/payee"; import t from "@/utils/translate"; export default (props) => { const dispatch = useDispatch(); const { form, payees, setPayees, type } = props; const [addPayeeName, setAddPayeeName] = useState(); function addPayeeInputChange(event) { setAddPayeeName(event.target.value) } const [addPayeeLoading, setAddPayeeLoading] = useState(false); async function addPayeeHandler() { if (addPayeeLoading) return; if (!addPayeeName) return; setAddPayeeLoading(true); const response = await createPayee({...{ name: addPayeeName }, ...categoryTypeToCreateParam(type)}); if (response && response.success) { setPayees([...payees, {value: response.data.id, label: response.data.name}]); setAddPayeeName(''); form.setFieldsValue({payeeId: response.data.id}); dispatch({ type: 'payee/refresh', payload: { ...response.data } }); } setAddPayeeLoading(false); } return ( {t('add')} )} > ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FormItemPayees/index.jsx ================================================ import {Col, Form, Select} from 'antd'; import t from '@/utils/translate'; export default (props) => { const { data, name = "payees", label=t('flow.payee') } = props; return ( {t('flow.status1')} {t('flow.status2')} {t('flow.status3')} ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FormItemTag/AddTagModal.jsx ================================================ import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'umi'; import {Form, Input, message, Modal, Switch, TreeSelect} from 'antd'; import { create } from '@/services/tag'; import {nameRules, notesRules} from "@/utils/rules"; import {getNull, validateForm} from "@/utils/util"; import {useCategoryTreeSelectData} from "@/utils/hooks"; import t from "@/utils/translate"; import styles from './index.less'; export default (props) => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { visible, onHide, type } = props; const { getEnableResponse : tagResponse } = useSelector(state => state.tag); const [treeData] = useCategoryTreeSelectData(tagResponse); useEffect(() => { if (visible) { if (!tagResponse) dispatch({ type: 'tag/fetchEnable' }); } }, [visible]); const [initialValues, setInitialValues] = useState({}) useEffect(() => { if (visible) { form.setFieldsValue({ ...getNull(form.getFieldsValue()), ...{ expenseable: type === 1 ? true : false, incomeable: type === 2 ? true : false, transferable: type === 3 ? true : false, parentId: null, } }); } }, [visible]); const messageOperationSuccess = t('operation.success'); const [confirmLoading, setConfirmLoading] = useState(false); async function okHandler() { setConfirmLoading(true); const values = await validateForm(form); if (values) { const response = await create(values); if (response && response.success) { onHide(); message.success(messageOperationSuccess); dispatch({ type: 'tag/refresh', payload: { ...response.data } }); } } setConfirmLoading(false); } function successHandler(response) { onHide(); } return (
); } ================================================ FILE: bookkeeping-user-fe/src/components/FormItemTag/index.jsx ================================================ import {useEffect, useState} from "react"; import {useFocus} from "@/utils/hooks"; import {useDispatch} from "umi"; import {Button, Checkbox, Col, Form, Input, message, Row, Tag, TreeSelect,Modal} from 'antd'; import {PlusOutlined} from "@ant-design/icons"; import AddTagModal from './AddTagModal'; import t from '@/utils/translate'; export default (props) => { const dispatch = useDispatch(); const { data, type } = props; const normFile = (e) => { if (Array.isArray(e)) { return e.map(i=>i.value); } }; function addHandler() { setVisible(true); } function hide() { setVisible(false); } const [visible, setVisible] = useState(false); return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FormItemTag/index.less ================================================ .form { :global { .ant-form-item-label { width: 65px; } } } ================================================ FILE: bookkeeping-user-fe/src/components/FormItemTagReport/index.jsx ================================================ import {Checkbox, Col, Form, TreeSelect} from 'antd'; import t from '@/utils/translate'; export default (props) => { const { data, name = "tags", label = t('flow.tag') } = props; const normFile = (e) => { if (Array.isArray(e)) { return e.map(i=>i.value); } }; return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FormItemTags/index.jsx ================================================ import {Checkbox, Col, Form, TreeSelect} from 'antd'; import t from '@/utils/translate'; export default (props) => { const { data, name = "tags", label = t('flow.tag') } = props; return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FormListCategory/index.jsx ================================================ import {Button, Col, Form, Input, Row, Select, TreeSelect, Space} from 'antd'; import {MinusCircleOutlined, PlusOutlined, PlusCircleOutlined} from "@ant-design/icons"; import {categoryRequiredRules, amountRequiredRules} from "@/utils/rules"; import t from '@/utils/translate'; import {useIntl} from "umi"; import styles from './index.less'; export default (props) => { const { categories, isConvert = false, currency } = props; const intl = useIntl(); const categoryRequired = categoryRequiredRules(); const amountRequired = amountRequiredRules(); return ( {(fields, { add, remove }) => { return ( <> {fields.map((field) => ( { isConvert ? : null } add()} /> {fields.length > 1 ? ( remove(field.name)} /> ) : null} ))} ); }} ); }; ================================================ FILE: bookkeeping-user-fe/src/components/FormListCategory/index.less ================================================ .row { align-items: baseline; .amount-col { :global { .ant-form-item-label { flex: 1 !important; width: auto !important; max-width: none !important; } .ant-form-item-control { flex: 2; } } } .convert-amount-col { :global { .ant-form-item-label { flex: 2 !important; width: auto !important; max-width: none !important; } .ant-form-item-control { flex: 2; } } } } ================================================ FILE: bookkeeping-user-fe/src/components/FormModal/index.jsx ================================================ import {useEffect, useState} from 'react'; import {useDispatch, useSelector} from "umi"; import {Modal, Button, message} from "antd"; import {validateForm} from "@/utils/util"; import t from '@/utils/translate'; export default (props) => { const dispatch = useDispatch(); const { title, form, initialValues, create, update, refund, onSuccess, onParseValues } = props; const { visible, type, currentItem } = useSelector(state => state.modal); const messageOperationSuccess = t('operation.success'); const [confirmLoading, setConfirmLoading] = useState(false); async function okHandler() { setConfirmLoading(true); const values = await validateForm(form); if (values) { if (values.createTime) values.createTime = values.createTime.valueOf(); if (onParseValues) onParseValues(values); // if (type === 2) { // if (values.accountId) { // if (currentItem.account.id === values.accountId) delete values.accountId; // } // if (values.categories) { // if (JSON.stringify(currentItem.categories) === JSON.stringify(values.categories)) delete values.categories; // } // if (values.tags) { // if (JSON.stringify(currentItem.tags.map(i => i.tagId)) === JSON.stringify(values.tags)) delete values.tags; // } // if (values.fromId) if (currentItem.fromId == values.fromId) delete values.fromId; // if (values.toId) if (currentItem.toId == values.toId) delete values.toId; // } const response = type === 2 ? await update(currentItem.id, values) : type === 4 ? await refund(currentItem.id, values) : await create(values); if (response && response.success) { cancelHandler(); message.success(messageOperationSuccess); onSuccess(response); } } setConfirmLoading(false); } function resetHandler() { form.setFieldsValue({...initialValues}); } useEffect(() => { if (initialValues && Object.keys(initialValues).length > 0) { form.setFieldsValue({...initialValues}); } }, [initialValues]); function cancelHandler() { dispatch({ type: 'modal/hide' }); } return ( {t('form.cancel')}, , , ]} > {props.children} ); }; ================================================ FILE: bookkeeping-user-fe/src/components/HeaderDropdown/index.jsx ================================================ import { Dropdown } from 'antd'; import React from 'react'; import classNames from 'classnames'; import styles from './index.less'; const HeaderDropdown = ({ overlayClassName: cls, ...restProps }) => ( ); export default HeaderDropdown; ================================================ FILE: bookkeeping-user-fe/src/components/HeaderDropdown/index.less ================================================ @import '~antd/es/style/themes/default.less'; .container > * { background-color: @popover-bg; border-radius: 4px; box-shadow: @shadow-1-down; } ================================================ FILE: bookkeeping-user-fe/src/components/IncomeModal/index.jsx ================================================ import { useEffect, useState } from 'react'; import {useDispatch, useSelector} from 'umi'; import {Form, Input, Switch} from 'antd'; import moment from 'moment'; import FormModal from "@/components/FormModal"; import FormListCategory from '@/components/FormListCategory'; import FormItemAccount from "@/components/FormItemAccount"; import FormItemDescription from '@/components/FormItemDescription'; import FormItemCreateTime from '@/components/FormItemCreateTime'; import FormItemPayee from '@/components/FormItemPayee'; import FormItemTag from '@/components/FormItemTag'; import { create, update, refund } from '@/services/income'; import { useCategoryTreeSelectData, useResponseData, useResponseSelectData } from "@/utils/hooks"; import {getNull, refreshFlow} from "@/utils/util"; import t from '@/utils/translate'; import styles from './index.less'; export default (props) => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { defaultBook } = useSelector(state => state.session); const { visible, type, currentItem } = useSelector(state => state.modal); const { getIncomeableResponse : tagResponse } = useSelector(state => state.tag); const [tags] = useCategoryTreeSelectData(tagResponse); const { getIncomeableResponse : payeeResponse } = useSelector(state => state.payee); const [payees, setPayees] = useResponseSelectData(payeeResponse); const { getIncomeableResponse : accountResponse } = useSelector(state => state.account); const [accounts] = useResponseData(accountResponse); const { querySimpleResponse } = useSelector(state => state.incomeCategory); const [categories] = useCategoryTreeSelectData(querySimpleResponse); useEffect(() => { if (visible) { if (!defaultBook) dispatch({ type: 'session/fetchSession' }); if (!tagResponse) dispatch({ type: 'tag/fetchIncomeable' }); if (!payeeResponse) dispatch({ type: 'payee/fetchIncomeable' }); if (!accountResponse) dispatch({ type: 'account/fetchIncomeable' }); if (!querySimpleResponse) dispatch({ type: 'incomeCategory/fetchSimple' }); } }, [visible]); const [initialValues, setInitialValues] = useState({}) useEffect(() => { if (!visible) return; if (type === 1) { setInitialValues({ ...getNull(form.getFieldsValue()), 'createTime': moment(), 'accountId': defaultBook && defaultBook.defaultIncomeAccount && defaultBook.defaultIncomeAccount.id, 'categories': defaultBook && defaultBook.defaultIncomeCategory ? [{ categoryId: defaultBook.defaultIncomeCategory.id }] : [{ }], 'tags': [], 'confirmed': true, }); if (defaultBook && defaultBook.defaultIncomeAccount) { setAccountCurrencyCode(defaultBook.defaultIncomeAccount.currencyCode); } else { setAccountCurrencyCode(''); } } else { let currentItemCopy = JSON.parse(JSON.stringify(currentItem)); if (type === 4) { if (currentItemCopy.categories) currentItemCopy.categories.forEach(value => { value.amount = value.amount*(-1); value.convertedAmount = value.convertedAmount*(-1) }); } if (type === 3 || type === 4) { //不复制备注 currentItemCopy.notes = ""; } // 数字类型的校验存在问题, antd bug if (currentItemCopy.categories) currentItemCopy.categories.forEach(value => {value.amount = value.amount.toString();value.convertedAmount = value.convertedAmount.toString()}); currentItemCopy.createTime = type === 2 ? moment(currentItemCopy.createTime) : moment(); currentItemCopy.accountId = currentItemCopy.account ? currentItemCopy.account.id : null; currentItemCopy.payeeId = currentItemCopy.payee ? currentItemCopy.payee.id : null; currentItemCopy.tags = currentItemCopy.tags ? currentItemCopy.tags.map(item => item.tagId) : null; currentItemCopy.confirmed = type === 3 ? true : currentItemCopy.status === 1; setInitialValues({ ...getNull(form.getFieldsValue()), ...currentItemCopy, 'categories': currentItemCopy.categories ? currentItemCopy.categories : [{}], }); setAccountCurrencyCode(currentItemCopy.currencyCode); } }, [visible, type, currentItem]); function successHandler(response) { refreshFlow(response.data); } const [accountCurrencyCode, setAccountCurrencyCode] = useState(); const accountChangeHandler = (value) => { for (let i = 0; i < accounts.length; i++) { if (accounts[i].id === value) { setAccountCurrencyCode(accounts[i].currencyCode); } } } return ( successHandler(response)} >
{(type !== 2) && }
); } ================================================ FILE: bookkeeping-user-fe/src/components/IncomeModal/index.less ================================================ .form { :global { .ant-form-item-label { width: 70px; } } } ================================================ FILE: bookkeeping-user-fe/src/components/Loading/index.jsx ================================================ // 暂时没用到 import { Spin } from 'antd'; import { LoadingOutlined } from '@ant-design/icons'; export default () => { const antIcon = ; return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/components/ModalRoot/index.jsx ================================================ import {useSelector} from "umi"; export default () => { const { component: Component } = useSelector(state => state.modal); return ( Component ? : null ); }; ================================================ FILE: bookkeeping-user-fe/src/components/PrimaryMenu/index.jsx ================================================ import { Menu } from 'antd'; import { NavLink } from 'umi'; import { AreaChartOutlined, BankOutlined, AccountBookOutlined, SettingOutlined, CalendarOutlined } from '@ant-design/icons'; import t from '@/utils/translate'; import styles from './index.less'; export default () => { return ( } title={t('menu.report')}> {t('menu.overview')} {t('menu.expense.category.reports')} {t('menu.expense.tag.reports')} {t('menu.income.category.reports')} {t('menu.income.tag.reports')} {t('menu.flow.trend')} {t('menu.balance.sheet')} {t('menu.asset.trend')} } title={t('menu.account.list')}> {t('menu.checking.account')} {t('menu.credit.account')} {t('menu.debt.account')} {t('menu.asset.account')} {t('menu.account.overview')} {t('balance.log')} } title={t('menu.bookkeeping')}> {t('menu.expense')} {t('menu.income')} {t('menu.transfer')} {t('menu.flow')} {t('menu.audit')} }>提醒管理 } title={t('menu.settings')}> {t('menu.category')} {t('menu.book')} {t('menu.group')} {t('menu.security')} {t('menu.budget')} {t('menu.schedule')} ); }; ================================================ FILE: bookkeeping-user-fe/src/components/PrimaryMenu/index.less ================================================ .menu { height: 100%; overflow: hidden auto; font-size: 14px; :global { .ant-menu { font-size: 14px; } .ant-menu-submenu-title { height: 40px !important; line-height: 40px !important; } .ant-menu-item { height: 40px !important; line-height: 40px !important; } } } ================================================ FILE: bookkeeping-user-fe/src/components/TransferModal/index.jsx ================================================ import { useEffect, useState } from 'react'; import {useDispatch, useSelector} from 'umi'; import { Form, Input, Switch } from 'antd'; import moment from 'moment'; import { create, update } from '@/services/transfer'; import {useCategoryTreeSelectData, useResponseData} from "@/utils/hooks"; import FormItemDescription from '@/components/FormItemDescription'; import FormItemCreateTime from '@/components/FormItemCreateTime'; import FormItemAccount from '@/components/FormItemAccount'; import FormModal from "@/components/FormModal"; import FormItemTag from "@/components/FormItemTag"; import {amountRequiredRulesPositive, requiredRules} from "@/utils/rules"; import {getNull, refreshFlow} from "@/utils/util"; import t from '@/utils/translate'; import styles from './index.less'; export default (props) => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { visible, type, currentItem } = useSelector(state => state.modal); const { defaultBook } = useSelector(state => state.session); const { getTransferableResponse : tagResponse } = useSelector(state => state.tag); const [tags] = useCategoryTreeSelectData(tagResponse); const { getTransferToAbleResponse } = useSelector(state => state.account); const [accountsTransferTo] = useResponseData(getTransferToAbleResponse); const { getTransferFromAbleResponse } = useSelector(state => state.account); const [accountsTransferFrom] = useResponseData(getTransferFromAbleResponse); useEffect(() => { if (visible) { if (!defaultBook) dispatch({ type: 'session/fetchSession' }); if (!tagResponse) dispatch({ type: 'tag/fetchTransferable' }); if (!getTransferToAbleResponse) dispatch({ type: 'account/fetchTransferToAble' }); if (!getTransferFromAbleResponse) dispatch({ type: 'account/fetchTransferFromAble' }); } }, [visible]); const [initialValues, setInitialValues] = useState({}) useEffect(() => { if (!visible) return; if (type === 1) { let fromId = null; if (defaultBook && defaultBook.defaultTransferFromAccount) { fromId = defaultBook.defaultTransferFromAccount.id; } let toId = null; if (defaultBook && defaultBook.defaultTransferToAccount) { toId = defaultBook.defaultTransferToAccount.id; } setInitialValues({ ...getNull(form.getFieldsValue()), 'createTime': moment(), 'fromId': fromId, 'toId': toId, 'tags': [], 'confirmed': true, }); if (defaultBook && defaultBook.defaultTransferFromAccount) { setFromCurrencyCode(defaultBook.defaultTransferFromAccount.currencyCode); } else { setFromCurrencyCode(''); } if (defaultBook && defaultBook.defaultTransferToAccount) { setToCurrencyCode(defaultBook.defaultTransferToAccount.currencyCode); } else { setToCurrencyCode(''); } } else { let currentItemCopy = JSON.parse(JSON.stringify(currentItem)); if (type === 3) { //不复制备注 currentItemCopy.notes = ""; } // 数字类型的校验存在问题, antd bug currentItemCopy.amount = currentItemCopy.amount.toString(); currentItemCopy.createTime = type === 3 ? moment() : moment(currentItemCopy.createTime); currentItemCopy.tags = currentItemCopy.tags ? currentItemCopy.tags.map(item => item.tagId) : null; currentItemCopy.confirmed = type === 3 ? true : currentItemCopy.status === 1; setInitialValues({ ...getNull(form.getFieldsValue()), ...currentItemCopy }); setFromCurrencyCode(currentItemCopy.currencyCode); setToCurrencyCode(currentItemCopy.toCurrencyCode); } }, [visible, type, currentItem]); function successHandler(response) { refreshFlow(response.data); } const [fromCurrencyCode, setFromCurrencyCode] = useState(); const [toCurrencyCode, setToCurrencyCode] = useState(); const fromAccountChangeHandler = (value) => { for (let i = 0; i < accountsTransferFrom.length; i++) { if (accountsTransferFrom[i].id === value) { setFromCurrencyCode(accountsTransferFrom[i].currencyCode); } } } const toAccountChangeHandler = (value) => { for (let i = 0; i < accountsTransferTo.length; i++) { if (accountsTransferTo[i].id === value) { setToCurrencyCode(accountsTransferTo[i].currencyCode); } } } return ( successHandler(response)} >
{ fromCurrencyCode && toCurrencyCode && fromCurrencyCode !== toCurrencyCode ? : null } {(type !== 2) && }
); } ================================================ FILE: bookkeeping-user-fe/src/components/TransferModal/index.less ================================================ .form { :global { .ant-form-item-label { width: 70px; } } } ================================================ FILE: bookkeeping-user-fe/src/components/charts/Bar/index.jsx ================================================ import { Axis, Chart, Geom, Tooltip } from "bizcharts"; import {Empty, Spin} from "antd"; export default (props) => { const { data, loading = false } = props; return ( {data && data.length > 0 ? {value}`} /> : } ) } ================================================ FILE: bookkeeping-user-fe/src/components/charts/Bar/index.less ================================================ ================================================ FILE: bookkeeping-user-fe/src/components/charts/Line/index.jsx ================================================ import {Axis, Chart, Geom, Legend, Tooltip} from "bizcharts"; import {Spin} from "antd"; export default (props) => { const { data, scale, height, padding, loading = false } = props; return ( ) } ================================================ FILE: bookkeeping-user-fe/src/components/charts/Line/index.less ================================================ ================================================ FILE: bookkeeping-user-fe/src/components/charts/Pie/index.jsx ================================================ import { useEffect, useRef, useState } from "react"; import { Empty, Spin, Divider } from "antd"; import { Chart, Geom, Coord, Tooltip, Label, Guide } from "bizcharts"; import { DataView } from '@antv/data-set'; import t from '@/utils/translate'; import styles from './index.less'; export default (props) => { const { data, loading = false } = props; const dv = new DataView(); dv.source(data).transform({ type: 'percent', field: 'y', dimension: 'x', as: 'percent', }); const tooltipFormat = [ 'x*y', (x, y) => ({ name: x, value: y, }), ]; const legendClickHandler = (item, i) => { const newItem = item; newItem.checked = !newItem.checked; const newLegendData = [...legendData]; newLegendData[i] = newItem; const filteredLegendData = newLegendData.filter((l) => l.checked).map((l) => l.x); if (chartRef.current) { chartRef.current.filter('x', (val) => filteredLegendData.indexOf(`${val}`) > -1); } setLegendData(newLegendData); }; const chartRef = useRef(null); const getG2Instance = (chart) => { chartRef.current = chart; }; useEffect(() => { getLegendData(); }, [data]); const [legendData, setLegendData] = useState([]); const getLegendData = () => { if (!chartRef || !chartRef.current) return; const geom = chartRef.current.getAllGeoms()[0]; // 获取所有的图形 if (!geom) return; const items = geom.get('dataArray') || []; // 获取图形对应的 const legendData = items.map((item) => { /* eslint no-underscore-dangle:0 */ const origin = item[0]._origin; origin.color = item[0].color; origin.checked = true; return origin; }); setLegendData(legendData); }; const totalMessage = t('gross.amount'); const total = data.reduce((pre, now) => now.y + pre, 0).toFixed(2); return (
{ data && data.length > 0 ? ( <> ${totalMessage}
${total}
`} alignX="middle" alignY="middle" />
    {legendData.map((item, i) => (
  • legendClickHandler(item, i)}> {item.x} {`${(Number.isNaN(item.percent) ? 0 : item.percent * 100).toFixed(2)}%`}   {item.y}
  • ))}
) : () }
) } ================================================ FILE: bookkeeping-user-fe/src/components/charts/Pie/index.less ================================================ @import '~antd/es/style/themes/default.less'; .pie { display: flex; flex-flow: row; align-items: center; .chart { flex: 1; } .legend { margin: 0; padding: 0; width: 250px; list-style: none; li { display: flex; align-items: center; margin: 8px 0; cursor: pointer; .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; } .legendTitle { flex: 1; padding-left: 8px; color: @text-color; } .percent { width: 55px; color: @text-color-secondary; } .value { width: 65px; } } } } @media (max-width: 1800px){ .pie { flex-flow: column; } } ================================================ FILE: bookkeeping-user-fe/src/components/charts/Pie2/index.jsx ================================================ import { useEffect, useRef, useState } from "react"; import { Empty, Spin, Divider } from "antd"; import { Chart, Geom, Coord, Tooltip, Label, Guide } from "bizcharts"; import { DataView } from '@antv/data-set'; import t from '@/utils/translate'; import styles from './index.less'; export default (props) => { const { data, loading = false } = props; const dv = new DataView(); dv.source(data).transform({ type: 'percent', field: 'y', dimension: 'x', as: 'percent', }); const tooltipFormat = [ 'x*y', (x, y) => ({ name: x, value: y, }), ]; const legendClickHandler = (item, i) => { const newItem = item; newItem.checked = !newItem.checked; const newLegendData = [...legendData]; newLegendData[i] = newItem; const filteredLegendData = newLegendData.filter((l) => l.checked).map((l) => l.x); if (chartRef.current) { chartRef.current.filter('x', (val) => filteredLegendData.indexOf(`${val}`) > -1); } setLegendData(newLegendData); }; const chartRef = useRef(null); const getG2Instance = (chart) => { chartRef.current = chart; }; useEffect(() => { getLegendData(); }, [data]); const [legendData, setLegendData] = useState([]); const getLegendData = () => { if (!chartRef || !chartRef.current) return; const geom = chartRef.current.getAllGeoms()[0]; // 获取所有的图形 if (!geom) return; const items = geom.get('dataArray') || []; // 获取图形对应的 const legendData = items.map((item) => { /* eslint no-underscore-dangle:0 */ const origin = item[0]._origin; origin.color = item[0].color; origin.checked = true; return origin; }); setLegendData(legendData); }; const totalMessage = t('gross.amount'); const total = data.reduce((pre, now) => now.y + pre, 0).toFixed(2); return (
{ data && data.length > 0 && total > 0 ? ( <> ${totalMessage}
${total}
`} alignX="middle" alignY="middle" />
    {legendData.map((item, i) => (
  • legendClickHandler(item, i)}> {item.x} {`${(Number.isNaN(item.percent) ? 0 : item.percent * 100).toFixed(2)}%`}   {item.y}
  • ))}
) : () }
) } ================================================ FILE: bookkeeping-user-fe/src/components/charts/Pie2/index.less ================================================ @import '~antd/es/style/themes/default.less'; .pie { display: flex; flex-flow: row; align-items: center; .chart { flex: 1; } .legend { margin: 0; padding: 0; width: 240px; min-width: 220px; list-style: none; li { display: flex; align-items: center; margin: 8px 0; cursor: pointer; .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; } .legendTitle { flex: 1; padding-left: 8px; color: @text-color; } .percent { width: 55px; color: @text-color-secondary; } .value { width: 65px; } } } } ================================================ FILE: bookkeeping-user-fe/src/global.less ================================================ @import '~antd/lib/style/themes/default.less'; html, body, #root { height: 100%; } body { margin: 0; } /* .ant-table table { text-align: center !important; } .ant-table-thead > tr > th { text-align: center !important; }*/ .table-color-default { background-color: @blue-3 !important; } .ant-table-tbody > tr.table-color-default:hover > td { background: unset; } ================================================ FILE: bookkeeping-user-fe/src/layouts/BasicLayout.jsx ================================================ import {useState} from "react"; import { Layout, Row, Col, Badge } from 'antd'; import {SelectLang} from 'umi'; import { MenuFoldOutlined, MenuUnfoldOutlined, QuestionCircleOutlined, MessageOutlined } from '@ant-design/icons'; import Avatar from '@/components/AvatarDropdown'; import PrimaryMenu from '@/components/PrimaryMenu'; import Footer from '@/components/Footer'; import ModalRoot from '@/components/ModalRoot'; import FlowButtons from '@/components/FlowButtons'; import styles from './BasicLayout.less'; export default (props) => { const [collapsed, setCollapsed] = useState(false); return (
九快计账
setCollapsed(!collapsed)}> {collapsed ? : }
{/*
*/} {/* */} {/* */} {/* */} {/*
*/} {props.children}
); } ================================================ FILE: bookkeeping-user-fe/src/layouts/BasicLayout.less ================================================ .logo { position: sticky; top: 0; z-index: 999; height: 48px; background: #1890ff; color:#fff; font-size: 25px; text-align: center; line-height: 48px; font-style: italic; font-weight: bold; } .header { position: sticky; top: 0; z-index: 999; width: 100%; height: 48px; line-height: 48px; background: #fff; padding: 0; .button { float: left; display: inline-block; height: 100%; padding: 0 20px; cursor: pointer; transition: color 0.3s; font-size: 16px; vertical-align: middle; &:hover { background: rgba(0, 0, 0, 0.025); color: #1890ff; } } } .page-layout { padding-left: 200px; } .layout-sider { position: fixed !important; top: 0; left: 0; z-index: 999; width: 200px; height: 100%; } .content { padding: 12px; } ================================================ FILE: bookkeeping-user-fe/src/layouts/UserLayout.jsx ================================================ import { Layout } from "antd"; import Footer from '@/components/Footer'; import logo from '@/assets/logo.svg'; import t from '@/utils/translate'; import styles from './UserLayout.less'; export default (props) => { return (
logo Ant Design
{t('app.title')}
{ props.children }
); } ================================================ FILE: bookkeeping-user-fe/src/layouts/UserLayout.less ================================================ @import '~antd/es/style/themes/default.less'; .container { display: flex; flex-direction: column; height: 100vh; overflow: auto; background: @layout-body-background; } .lang { width: 100%; height: 40px; line-height: 44px; text-align: right; :global(.ant-dropdown-trigger) { margin-right: 24px; } } .content { flex: 1; padding: 32px 0; } @media (min-width: @screen-md-min) { .container { background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg'); background-repeat: no-repeat; background-position: center 110px; background-size: 100%; } .content { padding: 32px 0 24px; } } .top { text-align: center; } .header { height: 44px; line-height: 44px; a { text-decoration: none; } } .logo { height: 44px; margin-right: 16px; vertical-align: top; } .title { position: relative; top: 2px; color: @heading-color; font-weight: 600; font-size: 33px; font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif; } .desc { margin-top: 12px; margin-bottom: 40px; color: @text-color-secondary; font-size: @font-size-base; } ================================================ FILE: bookkeeping-user-fe/src/layouts/index.js ================================================ import UserLayout from './UserLayout'; import BasicLayout from './BasicLayout'; export default (props) => { if (props.location.pathname === '/signin' || props.location.pathname === '/register') { return { props.children } } return { props.children } } ================================================ FILE: bookkeeping-user-fe/src/locales/en-US/account.js ================================================ export default { 'account': 'Account', 'checking.account': 'Checking Account', 'credit.limit': 'Credit Limit', 'remain.limit': 'Remain Limit', 'account.balance': 'Balance', 'account.include': 'Included', 'expenseable': 'Expense-Able', 'incomeable': 'Income-Able', 'transferFromAble': 'Transfer From - Able', 'transferToAble': 'Transfer To - Able', 'account.initial.balance': 'Initial Balance', 'account.limit': 'Limit', 'remain.limit': 'Remain Limit', 'account.billDay': 'Bill Day', 'account.apr': 'APR', 'account.asOfDate': 'As of Date', 'account.card.no': 'Card No', 'account.current.balance': 'Current Balance', 'account.log.record': 'Account Logs', 'account.new': 'Account Created', } ================================================ FILE: bookkeeping-user-fe/src/locales/en-US/book.js ================================================ export default { 'book': 'Book', 'book.name': 'Name', 'book.group': 'Group', 'book.description.eable': 'Book Description Enable', 'book.time.eable': 'Book Time Enable', 'book.default.expense.account': 'Default Expense Account', 'book.default.income.account': 'Default Income Account', 'book.default.transfer.from.account': 'Default Transfer From Account', 'book.default.transfer.to.account': 'Default Transfer To Account', 'book.default.expense.category': 'Default Expense Category', 'book.default.income.category': 'Default Income Category', 'set.default': 'Set As Default' } ================================================ FILE: bookkeeping-user-fe/src/locales/en-US/category.js ================================================ export default { 'category.expense.category': 'Expense Categories', 'category.income.category': 'Income Categories', 'category.tag': 'Tags', 'category.payee': 'Payees', 'category.add.expense.category': 'Add Expense Category', 'category.update.expense.category': 'Update Expense Category', 'category.add.income.category': 'Add Income Category', 'category.update.income.category': 'Update Income Category', 'category.add.tag': 'Add Tag', 'category.update.tag': 'Update Tag', 'category.add.payee': 'Add Payee', 'category.update.payee': 'Update Payee', 'category.parent.category': 'Parent Category', 'category.name': 'Name', 'expenseable': 'Expense-Able', 'incomeable': 'Income-Able', 'transferable': 'Transfer-Able', } ================================================ FILE: bookkeeping-user-fe/src/locales/en-US/common.js ================================================ export default { 'app.title': 'BookKeeping System', 'yes': 'Yes', 'no': 'No', 'null': 'Unknown', 'add': 'Add', 'new': 'New', 'search': 'Search', 'update': 'Update', 'disable': 'Disable', 'enable': 'Enable', 'delete': 'Delete', 'config': 'Config', 'form.cancel': 'Cancel', 'form.reset': 'Reset', 'form.submit': 'Submit', 'today': 'Today', 'this.week': 'This Week', 'this.month': 'This Month', 'this.year': 'This Year', 'last.year': 'Last Year', 'in.7.days': 'In 7 Days', 'in.30.days': 'In 30 Days', 'in.1.year': 'In 1 Year', 'operation.success': 'Your operation has already succeeded', 'delete.confirm': 'Are you sure to delete {name}?', 'name': 'Name', 'is.enable': 'Enable', 'notes': 'Notes', 'operation': 'Operation', 'pagination.showTotal': '{range0}-{range1} of {total} items', 'unit.times': 'Times', 'unit.days': 'Days', 'unit.weeks': 'Weeks', 'unit.months': 'Months', 'unit.quarters': 'Quarters', 'unit.years': 'Years', } ================================================ FILE: bookkeeping-user-fe/src/locales/en-US/dashboard.js ================================================ export default { 'asset': 'Asset', 'debt': 'Debt', 'net.worth': 'Net Worth', 'frequency.expense': 'Expense Frequency', 'frequency.income': 'Income Frequency', 'income.expense.table': 'Income Expense Table', 'expense.category': 'Expense Categories', 'expense.tag': 'Expense Tags', 'expense.trend': 'Expense Trend', 'income.category': 'Income Categories', 'income.trend': 'Income Trend', 'gross.amount': 'Total Amount', } ================================================ FILE: bookkeeping-user-fe/src/locales/en-US/flow.js ================================================ export default { 'expense': 'Expense', 'income': 'Income', 'balance': 'Balance', 'initialBalance': 'Opening Balance', 'includeAsset': 'Included in Assets', 'transfer': 'Transfer', 'transfer.from': 'Transfer From', 'transfer.to': 'Transfer To', 'adjust.balance': 'Adjust Balance', 'gross.balance': 'Balance in Gross', 'gross.expense': 'Expense in Gross', 'gross.income': 'Income in Gross', 'gross.surplus': 'Surplus in Gross', 'gross.transfer.from': 'Transfer From in Gross', 'gross.transfer.to': 'Transfer To in Gross', 'delete.confirm.balance': 'Are you sure you want to delete this transaction? It will also change your account balance.', 'flow.refund.confirm': 'Are you sure to refund?', 'confirm.operation': 'Are you sure to proceed this operation?', 'description': 'Description', 'amount': 'Amount', 'category': 'Category', 'flow.tag': 'Tag', 'flow.payee': 'Payee', 'flow.createTime': 'Time', 'flow.status': 'Status', 'flow.type': 'Type', 'flow.isConfirmed': 'is Confirmed', 'flow.adjust.balance.modal.title': 'Adjust Balance - {name}', 'flow.expense.add': 'Add Expense', 'flow.expense.copy': 'Copy Expense', 'flow.expense.update': 'Update Expense', 'flow.income.add': 'Add Income', 'flow.income.copy': 'Copy Income', 'flow.income.update': 'Update Income', 'flow.transfer.add': 'Add Income', 'flow.transfer.update': 'Update Income', 'transfer.from.account': 'From Account', 'transfer.to.account': 'To Account', 'update.tag.amount': 'Change Tag Amount', 'tag.amount': 'Tag Amount', 'flow.new.category': 'New Category', 'flow.new.tag': 'New Tag', 'flow.copy': 'Copy', 'flow.confirm': 'Confirm', 'flow.refund': 'Refund', 'amount.between': 'Amount Range', 'time.between': 'Time Range', 'flow.status1': 'Normal', 'flow.status2': 'Not Confirmed', 'flow.status3': 'Refunded', 'flow.record': 'Flow Record', } ================================================ FILE: bookkeeping-user-fe/src/locales/en-US/footer.js ================================================ export default { 'footer.download.ios': 'Download iOS', 'footer.download.andriod': 'Download Android', 'footer.download.weibo': 'Facebook', 'footer.download.zhihu': 'Twitter', 'footer.download.contact': 'Contact US', 'footer.download.about': 'About US', 'company.name': '武汉九快科技有限公司', 'footer.no': '鄂ICP备2021015799号-1', } ================================================ FILE: bookkeeping-user-fe/src/locales/en-US/group.js ================================================ export default { 'group': 'Group', 'group.name': 'Name', } ================================================ FILE: bookkeeping-user-fe/src/locales/en-US/header.js ================================================ export default { 'user.profile': 'User Profile', 'user.settings': 'Settings', 'user.logout': 'Logout', 'user.login': 'Signin', } ================================================ FILE: bookkeeping-user-fe/src/locales/en-US/menu.js ================================================ export default { 'menu.report': 'Report', 'menu.overview': 'Overview', 'menu.expense.category.reports': 'Expense Category Reports', 'menu.expense.tag.reports': 'Expense Tag Reports', 'menu.income.category.reports': 'Income Category Reports', 'menu.income.tag.reports': 'Income Tag Reports', 'menu.balance.sheet': 'Balance Sheet', 'menu.flow.trend': 'Flow Trend', 'menu.asset.trend': 'Asset Trend', 'menu.account.list': 'Account List', 'menu.checking.account': 'Checking Account', 'menu.credit.account': 'Credit Account', 'menu.debt.account': 'Debt Account', 'menu.asset.account': 'Asset Account', 'menu.account.overview': 'Account Overview', 'menu.bookkeeping': 'Bookkeeping', 'menu.expense': 'Expense', 'menu.income': 'Income', 'menu.transfer': 'Transfer', 'menu.flow': 'Flow', 'menu.settings': 'Settings', 'menu.category': 'Categories', 'menu.book': 'Books', 'menu.group': 'Groups', 'menu.security': 'Security', 'menu.budget': 'Budgets', 'menu.schedule': 'Schedules', } ================================================ FILE: bookkeeping-user-fe/src/locales/en-US/register.js ================================================ export default { 'register.success': 'Rigister Success!', 'placeholder.userName': '1-16 length', 'placeholder.password': '6-32 length', 'placeholder.invite.code': 'Invitation Code', 'placeholder.email': '', 'register': 'Register', 'register.signin': 'Has Account, Signin', } ================================================ FILE: bookkeeping-user-fe/src/locales/en-US/report.js ================================================ export default { 'report.break.down.label': 'Breakdown By', 'report.expense.condition': 'Expense Condition', 'report.income.condition': 'Income Condition', 'report.asset.category': 'Asset Category', 'report.debt.category': 'Debt Category', } ================================================ FILE: bookkeeping-user-fe/src/locales/en-US/rules.js ================================================ export default { 'rules.required': 'Required!', 'rules.userName.required': 'Please type user name.', 'rules.userName.pattern': 'Please type 1-16 length number or alphabet.', 'rules.password.required': 'Please type password.', 'rules.password.pattern': 'Please type 6-32 length', 'rules.email.pattern': 'Please type email', 'rules.name.required': 'Please type name.', 'rules.name.pattern': 'The length can\'t be more than 16.', 'rules.balance.required': 'Please type balance.', 'rules.balance.pattern': 'The length can\'t be more than 9.', 'rules.amount.required': 'Please type amount.', 'rules.amount.pattern': 'The length can\'t be more than 9.', 'rules.description.pattern': 'The length can\'t be more than 16.', 'rules.notes.pattern': 'The length can\'t be more than 128.', 'rules.time.required': 'Please select time.', 'rules.category.required': 'Please select category.', 'rules.account.required': 'Please select account.', 'rules.limit.required': 'Please type credit limit.', 'rules.limit.pattern': 'The length can\'t be more than 9.', 'rules.apr.pattern': 'The length can\'t be more than 3.', } ================================================ FILE: bookkeeping-user-fe/src/locales/en-US/schedule.js ================================================ export default { 'schedule.expense.schedule': 'Expense Schedule', 'schedule.income.schedule': 'Income Schedule', 'schedule.transfer.schedule': 'Transfer Schedule', } ================================================ FILE: bookkeeping-user-fe/src/locales/en-US/signin.js ================================================ export default { 'signin.success': 'Login Success!', 'signin': 'Login', 'placeholder.userName': '1-16 length', 'placeholder.password': '6-32 length', 'signin.keep30': 'Keep for 30 days', 'signin.forget': 'Can\'t Remember Password', 'register': 'Register New Account', } ================================================ FILE: bookkeeping-user-fe/src/locales/en-US.js ================================================ import common from './en-US/common'; import menu from './en-US/menu'; import header from './en-US/header'; import footer from './en-US/footer'; import signin from './en-US/signin'; import register from './en-US/register'; import dashboard from './en-US/dashboard'; import account from './en-US/account'; import flow from './en-US/flow'; import category from './en-US/category'; import rules from './en-US/rules'; import report from './en-US/report'; import group from './en-US/group'; import book from './en-US/book'; import schedule from './en-US/schedule'; export default { ...common, ...menu, ...header, ...footer, ...signin, ...register, ...dashboard, ...account, ...flow, ...category, ...rules, ...report, ...group, ...book, ...schedule, }; ================================================ FILE: bookkeeping-user-fe/src/locales/zh-CN/account.js ================================================ export default { 'account': '账户', 'checking.account': '活期账户', 'credit.account': '信用账户', 'debt.account': '贷款账户', 'asset.account': '资产账户', 'credit.limit': '信用额度', 'remain.limit': '剩余额度', 'debt.limit': '贷款额度', 'account.balance': '余额', 'account.include': '计入净资产', 'account.initial.balance': '初始余额', 'account.billDay': '账单日', 'account.apr': '年化利率(%)', 'account.asOfDate': '更新日期', 'account.card.no': '卡号', 'account.current.balance': '当前余额', 'balance.log': '资产日志', 'asset.balance': '资产金额', 'debt.balance': '负债金额', 'currency': '币种', 'default.currency': '默认币种', 'convertCurrency': '折合' } ================================================ FILE: bookkeeping-user-fe/src/locales/zh-CN/book.js ================================================ export default { 'book': '账本', 'book.name': '账本名', 'book.group': '所属组', 'book.description.eable': '是否展示描述', 'book.time.eable': '是否展示时间', 'book.image.eable': '是否支持图片', 'book.default.expense.account': '默认支出账户', 'book.default.income.account': '默认收入账户', 'book.default.transfer.from.account': '默认转出账户', 'book.default.transfer.to.account': '默认转入账户', 'book.default.expense.category': '默认支出类别', 'book.default.income.category': '默认收入类别', } ================================================ FILE: bookkeeping-user-fe/src/locales/zh-CN/category.js ================================================ export default { 'expense.category': '支出分类', 'income.category': '收入分类', 'tag': '交易标签', 'payee': '交易对象', 'parent.category': '所属类别', } ================================================ FILE: bookkeeping-user-fe/src/locales/zh-CN/common.js ================================================ export default { 'app.title': '记账管理系统', 'yes': '是', 'no': '否', 'null': '空', 'add': '添加', 'new': '新增', 'copy': '复制', 'search': '查询', 'update': '修改', 'delete': '删除', 'config': '设置', 'reload': '刷新', 'form.cancel': '取消', 'form.reset': '重置', 'form.submit': '提交', 'today': '今天', 'this.week': '本周', 'this.month': '本月', 'this.year': '今年', 'last.year': '去年', 'in.7.days': '7天内', 'in.30.days': '30天内', 'in.1.year': '1年内', 'operation.success': '操作成功', 'delete.confirm': '删除之后无法恢复,确定删除{name}吗?', 'name': '名称', 'is.enable': '是否可用', 'notes': '备注', 'operation': '操作', 'pagination.showTotal': '第 {range0}-{range1} 条 / 总共 {total} 条', 'unit.days': '天', 'unit.weeks': '周', 'unit.months': '月', 'unit.quarters': '季度', 'unit.years': '年', 'confirm.operation': '状态将更改为确认,确定此操作吗?', 'more': '更多' } ================================================ FILE: bookkeeping-user-fe/src/locales/zh-CN/flow.js ================================================ export default { 'expense': '支出', 'income': '收入', 'transfer': '转账', 'transfer.from': '转出', 'transfer.to': '转入', 'adjust.balance': '余额调整', 'expenseable': '可支出', 'incomeable': '可收入', 'transferable': '可转账', 'transferFromAble': '可转出', 'transferToAble': '可转入', 'delete.confirm.balance': '删除账单会撤回对应账户余额变动且无法恢复,确定删除吗?', 'description': '描述', 'amount': '金额', 'category': '类别', 'flow.book': '所属账本', 'flow.tag': '标签', 'flow.payee': '付款对象', 'flow.createTime': '时间', 'flow.status': '状态', 'flow.type': '交易类型', 'flow.image': '账单图片', 'image': '图片', 'flow.isConfirmed': '是否确认', 'transfer.from.account': '转出账户', 'transfer.to.account': '转入账户', 'update.tag.amount': '修改标签金额', 'tag.amount': '标签金额', 'confirm': '确认', 'refund': '退款', 'amount.between': '金额范围', 'time.between': '时间范围', 'flow.status1': '正常', 'flow.status2': '未确认', 'flow.status3': '已退款', 'flow.record': '流水记录', 'upload.size.error': '单个文件不能超过1.5MB', 'delete.tooltip': '已退款的账单必须先删除对应的退款记录。' } ================================================ FILE: bookkeeping-user-fe/src/locales/zh-CN/footer.js ================================================ export default { 'footer.download.ios': '下载iOS', 'footer.download.andriod': '下载安卓', 'footer.download.weibo': '关注微博', 'footer.download.zhihu': '关注知乎', 'footer.download.contact': '联系我们', 'footer.download.about': '关于我们', 'company.name': '武汉九快科技有限公司', 'footer.no': '鄂ICP备2021015799号-1', } ================================================ FILE: bookkeeping-user-fe/src/locales/zh-CN/group.js ================================================ export default { 'group': '组', 'group.name': '组名', } ================================================ FILE: bookkeeping-user-fe/src/locales/zh-CN/menu.js ================================================ export default { 'menu.report': '报表', 'menu.overview': '概览', 'menu.expense.category.reports': '支出分类', 'menu.expense.tag.reports': '支出标签', 'menu.income.category.reports': '收入分类', 'menu.income.tag.reports': '收入标签', 'menu.balance.sheet': '资产负债表', 'menu.flow.trend': '收支趋势图', 'menu.asset.trend': '资产趋势图', 'menu.account.list': '账户列表', 'menu.checking.account': '活期账户', 'menu.credit.account': '信用账户', 'menu.debt.account': '贷款账户', 'menu.asset.account': '资产账户', 'menu.account.overview': '账户管理', 'menu.bookkeeping': '记账', 'menu.expense': '支出', 'menu.income': '收入', 'menu.transfer': '转账', 'menu.flow': '流水', 'menu.audit': '对账', 'menu.settings': '设置', 'menu.category': '分类管理', 'menu.book': '账本管理', 'menu.group': '组管理', 'menu.security': '安全设置', 'menu.budget': '预算设置', 'menu.schedule': '预定设置', } ================================================ FILE: bookkeeping-user-fe/src/locales/zh-CN/report.js ================================================ export default { 'asset': '资产', 'debt': '负债', 'net.worth': '净资产', 'income.expense.table': '收支表', 'frequency.expense': '支出次数', 'frequency.income': '收入次数', 'expense.trend': '支出趋势', 'expense.tag': '支出标签', 'income.trend': '收入趋势', 'income.tag': '收入标签', 'gross.amount': '总金额', 'gross.balance': '总余额', 'gross.limit': '总额度', 'gross.remain.limit': '总剩余额度', 'gross.expense': '总支出', 'gross.income': '总收入', 'gross.surplus': '总结余', 'gross.transfer.from': '总转出', 'gross.transfer.to': '总转入', 'report.break.down.label': '划分单位', 'report.expense.condition': '支出条件', 'report.income.condition': '收入条件', 'report.asset.category': '资产分类', 'report.debt.category': '负债分类', } ================================================ FILE: bookkeeping-user-fe/src/locales/zh-CN/rules.js ================================================ export default { 'rules.required': '必填项!', 'rules.userName.required': '请输入用户名!', 'rules.userName.pattern': '请输入1-16位数字或字母。', 'rules.password.required': '请输入密码!', 'rules.password.pattern': '请输入6-32位数字,字母或特殊字符。', 'rules.email.pattern': '请输入正确的Email。', 'rules.name.required': '请输入名称!', 'rules.name.pattern': '长度不能超过16个字符', 'rules.balance.required': '请输入余额!', 'rules.balance.pattern': '余额为数字且不能超过9位数', 'rules.amount.required': '请输入金额', 'rules.amount.pattern': '金额为数字且不能超过9位数', 'rules.description.pattern': '长度不能超过16个字符', 'rules.notes.pattern': '长度不能超过1000个字符', 'rules.time.required': '请输入时间!', 'rules.category.required': '请选择类别', 'rules.account.required': '请选择账户', 'rules.limit.required': '请输入信用额度', 'rules.limit.pattern': '余额为数字且不能超过9位数', 'rules.apr.pattern': '年化利率不超过三位数', } ================================================ FILE: bookkeeping-user-fe/src/locales/zh-CN/schedule.js ================================================ export default { 'schedule.expense.schedule': '支出预定', 'schedule.income.schedule': '收入预定', 'schedule.transfer.schedule': '转账预定', } ================================================ FILE: bookkeeping-user-fe/src/locales/zh-CN/user.js ================================================ export default { 'signin': '登录', 'signin.success': '登录成功!', 'placeholder.userName': '1-16位数字或字母', 'placeholder.password': '6-32位数字,字母或特殊字符', 'signin.keep': '保存登录状态', 'signin.forget': '忘记密码', 'register': '注册账户', 'register.success': '注册成功!', 'placeholder.invite.code': '邀请码', 'placeholder.email': '邮箱可用于找回密码,选填。', 'register.signin': '已有账号,登录', 'profile': '个人中心', 'settings': '个人设置', 'logout': '退出登录', 'update.password': '修改密码', 'old.password': '原始密码', 'new.password': '新密码', 'set.default': '设为默认', } ================================================ FILE: bookkeeping-user-fe/src/locales/zh-CN.js ================================================ import common from './zh-CN/common'; import menu from './zh-CN/menu'; import footer from './zh-CN/footer'; import user from './zh-CN/user'; import account from './zh-CN/account'; import flow from './zh-CN/flow'; import category from './zh-CN/category'; import rules from './zh-CN/rules'; import report from './zh-CN/report'; import group from './zh-CN/group'; import book from './zh-CN/book'; import schedule from './zh-CN/schedule'; export default { ...common, ...menu, ...footer, ...user, ...account, ...flow, ...category, ...rules, ...report, ...group, ...book, ...schedule }; ================================================ FILE: bookkeeping-user-fe/src/models/account.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { getEnable, getExpenseable, getIncomeable, getTransferFromAble, getTransferToAble } from '@/services/account'; export default modelExtend(model, { namespace: 'account', state: { getEnableResponse: undefined, getExpenseableResponse: undefined, getIncomeableResponse: undefined, getTransferFromAbleResponse: undefined, getTransferToAbleResponse: undefined, }, effects: { *fetchEnable(_, { call, put }) { const response = yield call(getEnable); yield put({ type: 'updateState', payload: { getEnableResponse: response }, }); }, *fetchExpenseable(_, { call, put }) { const response = yield call(getExpenseable); yield put({ type: 'updateState', payload: { getExpenseableResponse: response }, }); }, *fetchIncomeable(_, { call, put }) { const response = yield call(getIncomeable); yield put({ type: 'updateState', payload: { getIncomeableResponse: response }, }); }, *fetchTransferFromAble(_, { call, put }) { const response = yield call(getTransferFromAble); yield put({ type: 'updateState', payload: { getTransferFromAbleResponse: response }, }); }, *fetchTransferToAble(_, { call, put }) { const response = yield call(getTransferToAble); yield put({ type: 'updateState', payload: { getTransferToAbleResponse: response }, }); }, *refresh(_, { __, put }) { yield put({ type: 'fetchEnable' }); //if (payload.expenseable) { yield put({ type: 'fetchExpenseable' }); //} //if (payload.incomeable) { yield put({ type: 'fetchIncomeable' }); //} //if (payload.transferFromAble) { yield put({ type: 'fetchTransferFromAble' }); //} //if (payload.transferToAble) { yield put({ type: 'fetchTransferToAble' }); //} }, }, }) ================================================ FILE: bookkeeping-user-fe/src/models/currency.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { getAll } from '@/services/currency'; export default modelExtend(model, { namespace: 'currency', state: { getAllResponse: undefined, }, effects: { *fetchAll(_, { call, put }) { const response = yield call(getAll); yield put({ type: 'updateState', payload: { getAllResponse: response }, }); } }, }) ================================================ FILE: bookkeeping-user-fe/src/models/expenseCategory.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { querySimple } from '@/services/expense-category'; export default modelExtend(model, { namespace: 'expenseCategory', state: { querySimpleResponse: undefined, }, effects: { *fetchSimple({ payload }, { call, put }) { const response = yield call(querySimple, payload); yield put({ type: 'updateState', payload: { querySimpleResponse: response }, }); }, *refresh(_, { __, put }) { yield put({ type: 'fetchSimple' }); } }, }) ================================================ FILE: bookkeeping-user-fe/src/models/flow.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import {getUploadToken} from '@/services/flow-image'; import {getImages} from '@/services/flow'; export default modelExtend(model, { namespace: 'flow', state: { uploadToken: undefined, images: undefined, }, effects: { *fetchUploadToken(_, { call, put }) { const response = yield call(getUploadToken); if (response && response.success && response.data) { yield put({ type: 'updateState', payload: { uploadToken: response.data, }, }); } }, *fetchImages({ payload }, { call, put }) { const response = yield call(getImages, payload.id); if (response && response.success && response.data) { yield put({ type: 'updateState', payload: { images: response.data, }, }); } }, } }) ================================================ FILE: bookkeeping-user-fe/src/models/incomeCategory.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { querySimple } from '@/services/income-category'; export default modelExtend(model, { namespace: 'incomeCategory', state: { querySimpleResponse: undefined, }, effects: { *fetchSimple({ payload }, { call, put }) { const response = yield call(querySimple, payload); yield put({ type: 'updateState', payload: { querySimpleResponse: response }, }); }, *refresh(_, { __, put }) { yield put({ type: 'fetchSimple' }); } }, }) ================================================ FILE: bookkeeping-user-fe/src/models/modal.js ================================================ export default { namespace: 'modal', state: { component: null, visible: false, currentItem: { }, type: 1, // 1是新增,2是更新,3是复制, 4是退款 }, reducers: { show(state, { payload }) { return { ...state, ...payload, visible: true } }, hide(state) { return { ...state, visible: false, currentItem: { } } }, }, } ================================================ FILE: bookkeeping-user-fe/src/models/payee.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { getEnable, getExpenseable, getIncomeable } from '@/services/payee'; export default modelExtend(model, { namespace: 'payee', state: { getEnableResponse: undefined, getExpenseableResponse: undefined, getIncomeableResponse: undefined, }, effects: { *fetchEnable(_, { call, put }) { const response = yield call(getEnable); yield put({ type: 'updateState', payload: { getEnableResponse: response }, }); }, *fetchExpenseable(_, { call, put }) { const response = yield call(getExpenseable); yield put({ type: 'updateState', payload: { getExpenseableResponse: response }, }); }, *fetchIncomeable(_, { call, put }) { const response = yield call(getIncomeable); yield put({ type: 'updateState', payload: { getIncomeableResponse: response }, }); }, *refresh({ payload }, { _, put }) { yield put({ type: 'fetchEnable' }); //if (payload.expenseable) { yield put({ type: 'fetchExpenseable' }); //} //if (payload.incomeable) { yield put({ type: 'fetchIncomeable' }); //} } }, }) ================================================ FILE: bookkeeping-user-fe/src/models/session.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import {getSessionUser} from '@/services/user'; export default modelExtend(model, { namespace: 'session', state: { user: undefined, defaultBook: undefined, defaultGroup: undefined, }, effects: { *fetchSession(_, { call, put }) { const response = yield call(getSessionUser); if (response && response.success && response.data) { yield put({ type: 'updateState', payload: { user: response.data.userSessionVO, defaultBook: response.data.defaultBook, defaultGroup: response.data.defaultGroup, }, }); } }, } }) ================================================ FILE: bookkeeping-user-fe/src/models/tag.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { getEnable, getExpenseable, getIncomeable, getTransferable } from '@/services/tag'; export default modelExtend(model, { namespace: 'tag', state: { getEnableResponse: undefined, getExpenseableResponse: undefined, getIncomeableResponse: undefined, getTransferableResponse: undefined, }, effects: { *fetchEnable(_, { call, put }) { const response = yield call(getEnable); yield put({ type: 'updateState', payload: { getEnableResponse: response }, }); }, *fetchExpenseable(_, { call, put }) { const response = yield call(getExpenseable); yield put({ type: 'updateState', payload: { getExpenseableResponse: response }, }); }, *fetchIncomeable(_, { call, put }) { const response = yield call(getIncomeable); yield put({ type: 'updateState', payload: { getIncomeableResponse: response }, }); }, *fetchTransferable(_, { call, put }) { const response = yield call(getTransferable); yield put({ type: 'updateState', payload: { getTransferableResponse: response }, }); }, *refresh({ payload }, { _, put }) { yield put({ type: 'fetchEnable' }); //if (payload.expenseable) { yield put({ type: 'fetchExpenseable' }); // } //if (payload.incomeable) { yield put({ type: 'fetchIncomeable' }); //} //if (payload.transferable) { yield put({ type: 'fetchTransferable' }); //} } }, }) ================================================ FILE: bookkeeping-user-fe/src/pages/accounts/AssetAccountModal.jsx ================================================ import {useDispatch, useSelector} from "umi"; import { useState, useEffect } from 'react'; import {Form, Input, Switch, Col, Row, Select} from 'antd'; import FormModal from "@/components/FormModal"; import {nameRules, notesRules, balanceRequiredRules, requiredRules} from '@/utils/rules'; import { formProp } from '@/utils/var'; import {getNull, tableChangeQueryFormat} from '@/utils/util'; import { create } from '@/services/asset-account'; import { update } from '@/services/account'; import {useCurrencyResponseSelectData} from "@/utils/hooks"; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { visible, type, currentItem } = useSelector(state => state.modal); const { assetAccountPagination : pagination, assetAccountSorter : sorter } = useSelector(state => state.accounts); const { defaultGroup } = useSelector(state => state.session); const { getAllResponse : currencyResponse } = useSelector(state => state.currency); const [currencyList] = useCurrencyResponseSelectData(currencyResponse); useEffect(() => { if (visible) { if (!currencyResponse) dispatch({ type: 'currency/fetchAll' }); if (!defaultGroup) dispatch({ type: 'session/fetchSession' }); } }, [visible]); const [initialValues, setInitialValues] = useState({}); useEffect(() => { if (!visible) return; if (type === 1) { setInitialValues({ ...getNull(form.getFieldsValue()), include: true, expenseable: false, incomeable: false, transferToAble: true, transferFromAble: true, currencyCode: defaultGroup.defaultCurrencyCode }); } else { // 数字类型的校验存在问题, antd bug currentItem.balance = currentItem.balance.toString(); setInitialValues({...getNull(form.getFieldsValue()), ...currentItem}); } }, [visible, type, currentItem]); function successHandler(response) { dispatch({ type: 'accounts/queryAssetAccount', payload: tableChangeQueryFormat(pagination, sorter) }); dispatch({ type: 'account/refresh', payload: { ...response.data } }); } return ( successHandler(response)} >
); } ================================================ FILE: bookkeeping-user-fe/src/pages/accounts/AssetAccountTable.jsx ================================================ import { useEffect } from 'react'; import {useDispatch, useIntl, useSelector} from 'umi'; import {Button, Descriptions, message, Modal, Space, Switch, Table} from "antd"; import { usePaginationAndData } from "@/utils/hooks"; import { remove, toggle, toggleExpenseable, toggleInclude, toggleIncomeable, toggleTransferFromAble, toggleTransferToAble } from '@/services/account'; import {getTimeFormat, tableChangeQueryFormat} from "@/utils/util"; import {showFlowModal} from '@/utils/flow'; import { balanceCol, accountIncludeCol, accountExpenseableCol, accountIncomeableCol, accountTransferToCol, accountTransferFromCol, accountEnableCol } from '@/utils/columns'; import {tableProp} from "@/utils/var"; import moment from "moment"; import AssetAccountModal from "@/pages/accounts/AssetAccountModal"; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch({ type: 'accounts/queryAssetAccount' }); }, []); const { queryAssetAccountResponse: queryResponse } = useSelector(state => state.accounts); const queryLoading = useSelector(state => state.loading.effects['accounts/queryAssetAccount']); const [ dataAndPagination ] = usePaginationAndData(queryResponse); const { assetAccountPagination : pagination, assetAccountSorter : sorter } = useSelector(state => state.accounts); const { defaultGroup } = useSelector(state => state.session); useEffect(() => { if (!defaultGroup) dispatch({ type: 'session/fetchSession' }); }, []); function tableChangeHandler(pagination, _, sorter) { dispatch({ type: 'accounts/updateState', payload: {assetAccountPagination: pagination, assetAccountSorter: sorter} }); dispatch({ type: 'accounts/queryAssetAccount', payload: tableChangeQueryFormat(pagination, sorter) }); } function refresh() { dispatch({ type: 'accounts/queryAssetAccount', payload: tableChangeQueryFormat(pagination, sorter) }); dispatch({ type: 'account/refresh' }); } const messageOperationSuccess = t('operation.success'); const toggleHandler = async (record) => { const response = await toggle(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleIncludeHandler = async (record) => { const response = await toggleInclude(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleExpenseableHandler = async (record) => { const response = await toggleExpenseable(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleIncomeableHandler = async (record) => { const response = await toggleIncomeable(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleTransferFromAbleHandler = async (record) => { const response = await toggleTransferFromAble(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleTransferToAbleHandler = async (record) => { const response = await toggleTransferToAble(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const updateHandler = (record) => { dispatch({ type: 'modal/show', payload: {component: AssetAccountModal, type: 2, currentItem: record }}); } const intl = useIntl(); const deleteHandler = async (record) => { Modal.confirm({ title: intl.formatMessage({id:'delete.confirm'}, { name: record.name }), onOk: async () => { const response = await remove(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } }); } const adjustBalanceHandler = (record) => { showFlowModal(4, 2, {...record}); } const columns = [ { title: t('name'), dataIndex: 'name', }, balanceCol(), { title: t('currency'), dataIndex: 'currencyCode', sorter: true, width: 70, }, { title: t('convertCurrency') + (defaultGroup ? defaultGroup.defaultCurrencyCode : ''), dataIndex: 'convertedBalance', width: 80, }, { title: t('account.asOfDate'), dataIndex: 'asOfDate', sorter: true, width: 90, render: value => moment(value).format('YYYY-MM-DD') }, accountIncludeCol(toggleIncludeHandler), accountExpenseableCol(toggleExpenseableHandler), accountIncomeableCol(toggleIncomeableHandler), accountTransferToCol(toggleTransferToAbleHandler), accountTransferFromCol(toggleTransferFromAbleHandler), accountEnableCol(toggleHandler), { title: t('operation'), key: 'operation', align: 'center', width: 150, render: (_, record) => { return ( ) } } ]; return (
{record.no ? record.no : t('null')} {record.initialBalance} {record.notes ? record.notes : t('null')} , }} dataSource={dataAndPagination.data} pagination={dataAndPagination.pagination} loading={queryLoading} onChange={tableChangeHandler} /> ); } ================================================ FILE: bookkeeping-user-fe/src/pages/accounts/CheckingAccountModal.jsx ================================================ import { useState, useEffect } from 'react'; import {Form, Input, Switch, Col, Row, Select} from 'antd'; import {useDispatch, useSelector} from "umi"; import FormModal from "@/components/FormModal"; import {nameRules, notesRules, balanceRequiredRules, requiredRules} from '@/utils/rules'; import { formProp } from '@/utils/var'; import {getNull, tableChangeQueryFormat} from '@/utils/util'; import {useCurrencyResponseSelectData} from "@/utils/hooks"; import { create } from '@/services/checking-account'; import { update } from '@/services/account'; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { visible, type, currentItem } = useSelector(state => state.modal); const { checkingAccountPagination : pagination, checkingAccountSorter : sorter } = useSelector(state => state.accounts); const { defaultGroup } = useSelector(state => state.session); const { getAllResponse : currencyResponse } = useSelector(state => state.currency); const [currencyList] = useCurrencyResponseSelectData(currencyResponse); useEffect(() => { if (visible) { if (!currencyResponse) dispatch({ type: 'currency/fetchAll' }); if (!defaultGroup) dispatch({ type: 'session/fetchSession' }); } }, [visible]); const [initialValues, setInitialValues] = useState({}); useEffect(() => { if (!visible) return; if (type === 1) { setInitialValues({ ...getNull(form.getFieldsValue()), ...{ include: true, expenseable: true, incomeable: true, transferToAble: true, transferFromAble: true, currencyCode: defaultGroup.defaultCurrencyCode } }); } else { // 数字类型的校验存在问题, antd bug currentItem.balance = currentItem.balance + ''; setInitialValues({...getNull(form.getFieldsValue()), ...currentItem}); } }, [visible, type, currentItem]); function successHandler(response) { dispatch({ type: 'accounts/queryCheckingAccount', payload: tableChangeQueryFormat(pagination, sorter) }); dispatch({ type: 'account/refresh', payload: { ...response.data } }); } return ( successHandler(response)} >
); } ================================================ FILE: bookkeeping-user-fe/src/pages/accounts/CheckingAccountTable.jsx ================================================ import { useEffect } from 'react'; import {useDispatch, useIntl, useSelector} from 'umi'; import {Button, Descriptions, message, Modal, Space, Table} from "antd"; import { usePaginationAndData } from "@/utils/hooks"; import { remove, toggle, toggleInclude, toggleExpenseable, toggleIncomeable, toggleTransferFromAble, toggleTransferToAble } from '@/services/account'; import { tableChangeQueryFormat } from "@/utils/util"; import {showFlowModal} from '@/utils/flow'; import CheckingAccountModal from "./CheckingAccountModal"; import t from "@/utils/translate"; import { accountEnableCol, accountExpenseableCol, accountIncludeCol, accountIncomeableCol, accountTransferFromCol, accountTransferToCol, balanceCol } from "@/utils/columns"; import {tableProp} from "@/utils/var"; export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch({ type: 'accounts/queryCheckingAccount' }); }, []); const { queryCheckingAccountResponse: queryResponse } = useSelector(state => state.accounts); const queryLoading = useSelector(state => state.loading.effects['accounts/queryCheckingAccount']); const [ dataAndPagination ] = usePaginationAndData(queryResponse); const { checkingAccountPagination : pagination, checkingAccountSorter : sorter } = useSelector(state => state.accounts); const { defaultGroup } = useSelector(state => state.session); useEffect(() => { if (!defaultGroup) dispatch({ type: 'session/fetchSession' }); }, []); function tableChangeHandler(pagination, _, sorter) { dispatch({ type: 'accounts/updateState', payload: {checkingAccountPagination: pagination, checkingAccountSorter: sorter} }); dispatch({ type: 'accounts/queryCheckingAccount', payload: tableChangeQueryFormat(pagination, sorter) }); } function refresh() { dispatch({ type: 'accounts/queryCheckingAccount', payload: tableChangeQueryFormat(pagination, sorter) }); dispatch({ type: 'account/refresh'}); } const messageOperationSuccess = t('operation.success'); const toggleHandler = async (record) => { const response = await toggle(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleIncludeHandler = async (record) => { const response = await toggleInclude(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleExpenseableHandler = async (record) => { const response = await toggleExpenseable(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleIncomeableHandler = async (record) => { const response = await toggleIncomeable(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleTransferFromAbleHandler = async (record) => { const response = await toggleTransferFromAble(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleTransferToAbleHandler = async (record) => { const response = await toggleTransferToAble(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const updateHandler = (record) => { dispatch({ type: 'modal/show', payload: {component: CheckingAccountModal, type: 2, currentItem: record }}); } const adjustBalanceHandler = (record) => { showFlowModal(4, 2, {...record}); } const intl = useIntl(); const deleteHandler = async (record) => { Modal.confirm({ title: intl.formatMessage({id:'delete.confirm'}, { name: record.name }), onOk: async () => { const response = await remove(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } }); } const columns = [ { title: t('name'), dataIndex: 'name', }, balanceCol(), { title: t('currency'), dataIndex: 'currencyCode', sorter: true, width: 70, }, { title: t('convertCurrency') + (defaultGroup ? defaultGroup.defaultCurrencyCode : ''), dataIndex: 'convertedBalance', width: 80, }, accountIncludeCol(toggleIncludeHandler), accountExpenseableCol(toggleExpenseableHandler), accountIncomeableCol(toggleIncomeableHandler), accountTransferToCol(toggleTransferToAbleHandler), accountTransferFromCol(toggleTransferFromAbleHandler), accountEnableCol(toggleHandler), { title: t('operation'), key: 'operation', align: 'center', width: 170, render: (_, record) => { return ( ) } } ]; return (
{record.no ? record.no : t('null')} {record.initialBalance} {record.notes ? record.notes : t('null')} , }} dataSource={dataAndPagination.data} pagination={dataAndPagination.pagination} loading={queryLoading} onChange={tableChangeHandler} /> ); } ================================================ FILE: bookkeeping-user-fe/src/pages/accounts/CreditAccountModal.jsx ================================================ import {useDispatch, useSelector} from "umi"; import { useState, useEffect } from 'react'; import {Form, Input, Switch, Col, Row, Select} from 'antd'; import {nameRules, notesRules, balanceRequiredRules, limitRequiredRules, requiredRules} from '@/utils/rules'; import { formProp } from '@/utils/var'; import { getNull, tableChangeQueryFormat } from '@/utils/util'; import { create } from '@/services/credit-account'; import { update } from '@/services/account'; import FormModal from "@/components/FormModal"; import t from "@/utils/translate"; import {useCurrencyResponseSelectData} from "@/utils/hooks"; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { visible, type, currentItem } = useSelector(state => state.modal); const { creditAccountPagination : pagination, creditAccountSorter : sorter } = useSelector(state => state.accounts); const { defaultGroup } = useSelector(state => state.session); const { getAllResponse : currencyResponse } = useSelector(state => state.currency); const [currencyList] = useCurrencyResponseSelectData(currencyResponse); useEffect(() => { if (visible) { if (!currencyResponse) dispatch({ type: 'currency/fetchAll' }); if (!defaultGroup) dispatch({ type: 'session/fetchSession' }); } }, [visible]); const [initialValues, setInitialValues] = useState({}); useEffect(() => { if (!visible) return; if (type === 1) { setInitialValues({ ...getNull(form.getFieldsValue()), include: true, expenseable: true, incomeable: false, transferToAble: true, transferFromAble: false, currencyCode: defaultGroup.defaultCurrencyCode }); } else { // 数字类型的校验存在问题, antd bug currentItem.balance = currentItem.balance.toString(); currentItem.limit = currentItem.limit.toString(); setInitialValues({...getNull(form.getFieldsValue()), ...currentItem}); } }, [visible, type, currentItem]); function successHandler(response) { dispatch({ type: 'accounts/queryCreditAccount', payload: tableChangeQueryFormat(pagination, sorter) }); dispatch({ type: 'account/refresh', payload: { ...response.data } }); } return ( successHandler(response)} >
); } ================================================ FILE: bookkeeping-user-fe/src/pages/accounts/CreditAccountTable.jsx ================================================ import { useEffect } from 'react'; import {useDispatch, useIntl, useSelector} from 'umi'; import {Button, Descriptions, message, Modal, Space, Table} from "antd"; import { usePaginationAndData } from "@/utils/hooks"; import { remove, toggle, toggleExpenseable, toggleInclude, toggleIncomeable, toggleTransferFromAble, toggleTransferToAble } from '@/services/account'; import { tableChangeQueryFormat } from "@/utils/util"; import {showFlowModal} from '@/utils/flow'; import { balanceCol, accountIncludeCol, accountExpenseableCol, accountIncomeableCol, accountTransferToCol, accountTransferFromCol, accountEnableCol, } from '@/utils/columns'; import {tableProp} from "@/utils/var"; import t from "@/utils/translate"; import CreditAccountModal from "@/pages/accounts/CreditAccountModal"; export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch({ type: 'accounts/queryCreditAccount' }); }, []); const { queryCreditAccountResponse: queryResponse } = useSelector(state => state.accounts); const queryLoading = useSelector(state => state.loading.effects['accounts/queryCreditAccount']); const [ dataAndPagination ] = usePaginationAndData(queryResponse); const { creditAccountPagination : pagination, creditAccountSorter : sorter } = useSelector(state => state.accounts); const { defaultGroup } = useSelector(state => state.session); useEffect(() => { if (!defaultGroup) dispatch({ type: 'session/fetchSession' }); }, []); function tableChangeHandler(pagination, _, sorter) { dispatch({ type: 'accounts/updateState', payload: {creditAccountPagination: pagination, creditAccountSorter: sorter} }); dispatch({ type: 'accounts/queryCreditAccount', payload: tableChangeQueryFormat(pagination, sorter) }); } function refresh() { dispatch({ type: 'accounts/queryCreditAccount', payload: tableChangeQueryFormat(pagination, sorter) }); dispatch({ type: 'account/refresh' }); } const messageOperationSuccess = t('operation.success'); const toggleHandler = async (record) => { const response = await toggle(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleIncludeHandler = async (record) => { const response = await toggleInclude(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleExpenseableHandler = async (record) => { const response = await toggleExpenseable(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleIncomeableHandler = async (record) => { const response = await toggleIncomeable(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleTransferFromAbleHandler = async (record) => { const response = await toggleTransferFromAble(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleTransferToAbleHandler = async (record) => { const response = await toggleTransferToAble(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const updateHandler = (record) => { dispatch({ type: 'modal/show', payload: {component: CreditAccountModal, type: 2, currentItem: record }}); } const intl = useIntl(); const deleteHandler = async (record) => { Modal.confirm({ title: intl.formatMessage({id:'delete.confirm'}, { name: record.name }), onOk: async () => { const response = await remove(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } }); } const adjustBalanceHandler = (record) => { showFlowModal(4, 2, {...record}); } const columns = [ { title: t('name'), dataIndex: 'name', }, balanceCol(), { title: t('currency'), dataIndex: 'currencyCode', sorter: true, width: 70, }, { title: t('convertCurrency') + (defaultGroup ? defaultGroup.defaultCurrencyCode : ''), dataIndex: 'convertedBalance', width: 80, }, { title: t('credit.limit'), dataIndex: 'limit', sorter: true, width: 85, }, { title: t('remain.limit'), dataIndex: 'remainLimit', sorter: true, width: 85, }, accountIncludeCol(toggleIncludeHandler), accountExpenseableCol(toggleExpenseableHandler), accountIncomeableCol(toggleIncomeableHandler), accountTransferToCol(toggleTransferToAbleHandler), accountTransferFromCol(toggleTransferFromAbleHandler), accountEnableCol(toggleHandler), { title: t('operation'), key: 'operation', align: 'center', width: 150, render: (_, record) => { return ( ) } } ]; return (
{record.billDay ? record.billDay : t('null')} {record.no ? record.no : t('null')} {record.initialBalance} {record.notes ? record.notes : t('null')} , }} dataSource={dataAndPagination.data} pagination={dataAndPagination.pagination} loading={queryLoading} onChange={tableChangeHandler} /> ); } ================================================ FILE: bookkeeping-user-fe/src/pages/accounts/DebtAccountModal.jsx ================================================ import {useDispatch, useSelector} from "umi"; import { useState, useEffect } from 'react'; import {Form, Input, Switch, message, Col, Row, Select} from 'antd'; import {nameRules, notesRules, balanceRequiredRules, aprRules, requiredRules} from '@/utils/rules'; import { formProp } from '@/utils/var'; import {getNull, validateForm, tableChangeQueryFormat} from '@/utils/util'; import { create } from '@/services/debt-account'; import { update } from '@/services/account'; import FormModal from "@/components/FormModal"; import t from "@/utils/translate"; import {useCurrencyResponseSelectData} from "@/utils/hooks"; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { visible, type, currentItem } = useSelector(state => state.modal); const { debtAccountPagination : pagination, debtAccountSorter : sorter } = useSelector(state => state.accounts); const { defaultGroup } = useSelector(state => state.session); const { getAllResponse : currencyResponse } = useSelector(state => state.currency); const [currencyList] = useCurrencyResponseSelectData(currencyResponse); useEffect(() => { if (visible) { if (!currencyResponse) dispatch({ type: 'currency/fetchAll' }); if (!defaultGroup) dispatch({ type: 'session/fetchSession' }); } }, [visible]); const [initialValues, setInitialValues] = useState({}); useEffect(() => { if (!visible) return; if (type === 1) { setInitialValues({ ...getNull(form.getFieldsValue()), include: true, expenseable: false, incomeable: false, transferToAble: true, transferFromAble: true, currencyCode: defaultGroup.defaultCurrencyCode }); } else { // 数字类型的校验存在问题, antd bug currentItem.balance = currentItem.balance.toString(); if (currentItem.limit != null) currentItem.limit = currentItem.limit.toString(); if (currentItem.apr != null) currentItem.apr = currentItem.apr.toString(); setInitialValues({...getNull(form.getFieldsValue()), ...currentItem}); } }, [visible, type, currentItem]); function successHandler(response) { dispatch({ type: 'accounts/queryDebtAccount', payload: tableChangeQueryFormat(pagination, sorter) }); dispatch({ type: 'account/refresh', payload: { ...response.data } }); } return ( successHandler(response)} >
); } ================================================ FILE: bookkeeping-user-fe/src/pages/accounts/DebtAccountTable.jsx ================================================ import { useEffect } from 'react'; import {useDispatch, useIntl, useSelector} from 'umi'; import {Button, Descriptions, message, Modal, Space, Switch, Table} from "antd"; import { usePaginationAndData } from "@/utils/hooks"; import { remove, toggle, toggleExpenseable, toggleInclude, toggleIncomeable, toggleTransferFromAble, toggleTransferToAble } from '@/services/account'; import { tableChangeQueryFormat } from "@/utils/util"; import {showFlowModal} from '@/utils/flow'; import { balanceCol, accountIncludeCol, accountExpenseableCol, accountIncomeableCol, accountTransferToCol, accountTransferFromCol, accountEnableCol } from '@/utils/columns'; import FlagTag from "@/components/FlagTag"; import DebtAccountModal from "./DebtAccountModal"; import {tableProp} from "@/utils/var"; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch({ type: 'accounts/queryDebtAccount' }); }, []); const { queryDebtAccountResponse : queryResponse } = useSelector(state => state.accounts); const queryLoading = useSelector(state => state.loading.effects['accounts/queryDebtAccount']); const [ dataAndPagination ] = usePaginationAndData(queryResponse); const { debtAccountPagination : pagination, debtAccountSorter : sorter } = useSelector(state => state.accounts); const { defaultGroup } = useSelector(state => state.session); useEffect(() => { if (!defaultGroup) dispatch({ type: 'session/fetchSession' }); }, []); function tableChangeHandler(pagination, _, sorter) { dispatch({ type: 'accounts/updateState', payload: {debtAccountPagination: pagination, debtAccountSorter: sorter} }); dispatch({ type: 'accounts/queryDebtAccount', payload: tableChangeQueryFormat(pagination, sorter) }); } function refresh() { dispatch({ type: 'accounts/queryDebtAccount', payload: tableChangeQueryFormat(pagination, sorter) }); dispatch({ type: 'account/refresh' }); } const messageOperationSuccess = t('operation.success'); const toggleHandler = async (record) => { const response = await toggle(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleIncludeHandler = async (record) => { const response = await toggleInclude(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleExpenseableHandler = async (record) => { const response = await toggleExpenseable(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleIncomeableHandler = async (record) => { const response = await toggleIncomeable(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleTransferFromAbleHandler = async (record) => { const response = await toggleTransferFromAble(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const toggleTransferToAbleHandler = async (record) => { const response = await toggleTransferToAble(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const updateHandler = (record) => { dispatch({ type: 'modal/show', payload: {component: DebtAccountModal, type: 2, currentItem: record }}); } const intl = useIntl(); const deleteHandler = async (record) => { Modal.confirm({ title: intl.formatMessage({id:'delete.confirm'}, { name: record.name }), onOk: async () => { const response = await remove(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } }); } const adjustBalanceHandler = (record) => { showFlowModal(4, 2, {...record}); } const columns = [ { title: t('name'), dataIndex: 'name', }, balanceCol(), { title: t('currency'), dataIndex: 'currencyCode', sorter: true, width: 70, }, { title: t('convertCurrency') + (defaultGroup ? defaultGroup.defaultCurrencyCode : ''), dataIndex: 'convertedBalance', width: 80, }, { title: t('debt.limit'), dataIndex: 'limit', sorter: true, width: 85, }, { title: t('remain.limit'), dataIndex: 'remainLimit', sorter: true, width: 85, }, { title: t('account.apr'), dataIndex: 'apr', sorter: true, width: 100, }, accountIncludeCol(toggleIncludeHandler), accountExpenseableCol(toggleExpenseableHandler), accountIncomeableCol(toggleIncomeableHandler), accountTransferToCol(toggleTransferToAbleHandler), accountTransferFromCol(toggleTransferFromAbleHandler), accountEnableCol(toggleHandler), { title: t('operation'), key: 'operation', align: 'center', width: 150, render: (_, record) => { return ( ) } } ]; return (
{record.no ? record.no : t('null')} {record.initialBalance} {record.notes ? record.notes : t('null')} , }} dataSource={dataAndPagination.data} pagination={dataAndPagination.pagination} loading={queryLoading} onChange={tableChangeHandler} /> ); } ================================================ FILE: bookkeeping-user-fe/src/pages/accounts/OperationBar.jsx ================================================ import { useDispatch, useSelector } from 'umi'; import { Radio, Button, Space } from 'antd'; import {PlusOutlined, ReloadOutlined} from "@ant-design/icons"; import CheckingAccountModal from './CheckingAccountModal'; import CreditAccountModal from './CreditAccountModal'; import DebtAccountModal from './DebtAccountModal'; import AssetAccountModal from './AssetAccountModal'; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); const { currentAccountType } = useSelector(state => state.accounts); const options = [ { label: t('menu.checking.account'), value: 1 }, { label: t('menu.credit.account'), value: 2 }, { label: t('menu.debt.account'), value: 3 }, { label: t('menu.asset.account'), value: 4 }, ]; const typeChangeHandler = (e) => { dispatch({ type: 'accounts/updateState', payload: { currentAccountType: e.target.value }}) } const addAccountHandler = () => { switch (currentAccountType) { case 1: dispatch({ type: 'modal/show', payload: {component: CheckingAccountModal, type: 1, currentItem: { } }}); break; case 2: dispatch({ type: 'modal/show', payload: {component: CreditAccountModal, type: 1, currentItem: { } }}); break; case 3: dispatch({ type: 'modal/show', payload: {component: DebtAccountModal, type: 1, currentItem: { } }}); break; case 4: dispatch({ type: 'modal/show', payload: {component: AssetAccountModal, type: 1, currentItem: { } }}); break; } } const refreshHandler = () => { dispatch({ type: 'accounts/refresh' }); } return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/accounts/index.jsx ================================================ import { useEffect } from "react"; import { Space } from 'antd'; import { useDispatch, useSelector } from 'umi'; import Breadcrumb from '@/components/Breadcrumb'; import { spaceVProp } from '@/utils/var'; import OperationBar from './OperationBar'; import CheckingAccountTable from './CheckingAccountTable'; import CreditAccountTable from './CreditAccountTable'; import DebtAccountTable from './DebtAccountTable'; import AssetAccountTable from './AssetAccountTable'; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); const { currentAccountType } = useSelector(state => state.accounts); const tableSwitch = (type) => { switch(type) { case 1: return ; case 2: return ; case 3: return ; case 4: return ; } } return ( { tableSwitch(currentAccountType) } ); } ================================================ FILE: bookkeeping-user-fe/src/pages/accounts/model.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { query as queryCheckingAccount } from '@/services/checking-account'; import { query as queryCreditAccount } from '@/services/credit-account'; import { query as queryDebtAccount } from '@/services/debt-account'; import { query as queryAssetAccount } from '@/services/asset-account'; import {tableChangeQueryFormat} from '@/utils/util'; export default modelExtend(model, { namespace: 'accounts', state: { currentAccountType: 1, checkingAccountPagination: { }, checkingAccountSorter: { }, queryCheckingAccountResponse: undefined, creditAccountPagination: { }, creditAccountSorter: { }, queryCreditAccountResponse: undefined, debtAccountPagination: { }, debtAccountSorter: { }, queryDebtAccountResponse: undefined, assetAccountPagination: { }, assetAccountSorter: { }, queryAssetAccountResponse: undefined, }, effects: { *queryCheckingAccount({ payload }, { call, put }) { const response = yield call(queryCheckingAccount, payload); yield put({ type: 'updateState', payload: { queryCheckingAccountResponse: response }, }); }, *queryCreditAccount({ payload }, { call, put }) { const response = yield call(queryCreditAccount, payload); yield put({ type: 'updateState', payload: { queryCreditAccountResponse: response }, }); }, *queryDebtAccount({ payload }, { call, put }) { const response = yield call(queryDebtAccount, payload); yield put({ type: 'updateState', payload: { queryDebtAccountResponse: response }, }); }, *queryAssetAccount({ payload }, { call, put }) { const response = yield call(queryAssetAccount, payload); yield put({ type: 'updateState', payload: { queryAssetAccountResponse: response }, }); }, *refresh(_, { __, put, select }) { const { currentAccountType, checkingAccountPagination, checkingAccountSorter, creditAccountPagination, creditAccountSorter, debtAccountPagination, debtAccountSorter, assetAccountPagination, assetAccountSorter } = yield select(state => state.accounts); switch (currentAccountType) { case 1: yield put({ type: 'queryCheckingAccount', payload: tableChangeQueryFormat(checkingAccountPagination, checkingAccountSorter) }); break; case 2: yield put({ type: 'queryCreditAccount', payload: tableChangeQueryFormat(creditAccountPagination, creditAccountSorter) }); break; case 3: yield put({ type: 'queryDebtAccount', payload: tableChangeQueryFormat(debtAccountPagination, debtAccountSorter) }); break; case 4: yield put({ type: 'queryAssetAccount', payload: tableChangeQueryFormat(assetAccountPagination, assetAccountSorter) }); break; } } }, }) ================================================ FILE: bookkeeping-user-fe/src/pages/asset-accounts/GeneralBar.jsx ================================================ import { useEffect } from 'react'; import { useDispatch, useSelector } from 'umi'; import {ReloadOutlined} from '@ant-design/icons'; import { useResponseData } from '@/utils/hooks'; import {Button, Card, Col, Row, Statistic} from "antd"; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch({ type: 'assetAccounts/sum' }); }, []); const loading = useSelector(state => state.loading.effects['assetAccounts/sum']); const queryLoading = useSelector(state => state.loading.effects['assetAccounts/query']); const { sumResponse } = useSelector(state => state.assetAccounts); const [sum] = useResponseData(sumResponse); function refreshHandler() { dispatch({ type: 'assetAccounts/refresh' }); } return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/asset-accounts/ItemCard.jsx ================================================ import {useEffect} from "react"; import {useDispatch, useSelector} from "umi"; import {Card, Row, Col, Button, Space, Descriptions, Collapse} from 'antd'; import { spaceVProp } from '@/utils/var'; import AccountRecordTable from "@/components/AccountRecordTable"; import {showFlowModal} from "@/utils/flow"; import styles from './index.less'; import t from "@/utils/translate"; export default (props) => { const dispatch = useDispatch(); const { account } = props; const { currentTime } = useSelector(state => state.assetAccounts); const { defaultGroup } = useSelector(state => state.session); useEffect(() => { if (!defaultGroup) dispatch({ type: 'session/fetchSession' }); }, []); function expenseHandler() { showFlowModal(1, 1, {accountId: account.id}); } function incomeHandler() { showFlowModal(2, 1, {accountId: account.id}); } function transferToHandler() { showFlowModal(3, 1, { toId: account.id }); } function transferFromHandler() { showFlowModal(3, 1, { fromId: account.id }); } function adjustBalanceHandler() { showFlowModal(4, 2, { ...account }); } return ( {account.balance} {account.currencyCode} {account.include ? t('yes') : t('no')} ); } ================================================ FILE: bookkeeping-user-fe/src/pages/asset-accounts/List.jsx ================================================ import { useEffect } from "react"; import {Empty, Pagination, Space, Spin} from "antd"; import { useDispatch, useSelector } from "umi"; import {spaceVProp} from "@/utils/var"; import { usePaginationAndData } from "@/utils/hooks"; import { paginationChange } from "@/utils/util"; import ItemCard from "./ItemCard"; export default () => { const dispatch = useDispatch(); const { queryResponse } = useSelector(state => state.assetAccounts); const queryLoading = useSelector(state => state.loading.effects['assetAccounts/query']); const [ dataAndPagination ] = usePaginationAndData(queryResponse); return ( { dataAndPagination.data && dataAndPagination.data.length > 0 ? ( { dataAndPagination.data.map(item => ) } ) : () } ) } ================================================ FILE: bookkeeping-user-fe/src/pages/asset-accounts/index.jsx ================================================ import { Space } from 'antd'; import { spaceVProp } from '@/utils/var'; import Breadcrumb from "@/components/Breadcrumb"; import GeneralBar from './GeneralBar'; import List from './List'; import t from '@/utils/translate'; export default () => { return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/asset-accounts/index.less ================================================ .item-card-collapse { :global { .ant-collapse-content-box { padding: 0 !important; } } } ================================================ FILE: bookkeeping-user-fe/src/pages/asset-accounts/model.js ================================================ import {history} from "umi"; import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import {query, sum} from '@/services/asset-account'; export default modelExtend(model, { namespace: 'assetAccounts', state: { queryResponse: undefined, sumResponse: undefined, currentTime: Date.now() }, effects: { *sum(_, { call, put }) { const response = yield call(sum); yield put({ type: 'updateState', payload: { sumResponse: response }, }); }, *query({ payload }, { call, put }) { const response = yield call(query, { ...payload, ...{ enable: true } }); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, *refresh(_, { call, put, select }) { yield put({ type: 'query', payload: history.location.query }); yield put({ type: 'sum' }); // 更新账户的流水记录 yield put({ type: 'updateState', payload: {currentTime: new Date()} }); } }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/asset-accounts') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/audit/OperationBar.jsx ================================================ import { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'umi'; import { Form, Row, Col, Button, Select, Space } from 'antd'; import { filterFormProp } from "@/utils/var"; import {searchHandler} from "@/utils/util"; import {useResponseSelectData, useCategoryTreeSelectData} from "@/utils/hooks"; import FormItemDateRange from "@/components/FormItemDateRange"; import FormItemAmountRange from "@/components/FormItemAmountRange"; import FormItemAccounts from "@/components/FormItemAccounts"; import FormItemPayees from "@/components/FormItemPayees"; import FormItemTags from "@/components/FormItemTags"; import FormItemStatus from "@/components/FormItemStatus"; import FormItemDescriptionSearch from "@/components/FormItemDescriptionSearch"; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const loading = useSelector(state => state.loading.effects['audit/query']); useEffect(() => { if (!accountResponse) dispatch({ type: 'account/fetchEnable' }); }, []); const { getEnableResponse : accountResponse } = useSelector(state => state.account); const [accounts] = useResponseSelectData(accountResponse); const [dateRadioValue, setDateRadioValue] = useState(); function resetHandler() { setDateRadioValue(); form.resetFields(); } return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/pages/audit/RecordTable.jsx ================================================ import { useSelector } from 'umi'; import {useResultPaginationAndData} from '@/utils/hooks'; import {handleTableChange} from "@/utils/util"; import FlowRecordTable from '@/components/FlowRecordTable'; export default () => { const queryLoading = useSelector(state => state.loading.effects['audit/query']); const { queryResponse } = useSelector(state => state.audit); const [ dataAndPagination ] = useResultPaginationAndData(queryResponse); return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/audit/TotalAlert.jsx ================================================ import {useMemo} from "react"; import { useSelector } from 'umi'; import { Space } from 'antd'; import {useResponseData} from "@/utils/hooks"; import AlertTotalSearch from "@/components/AlertTotalSearch"; import t from '@/utils/translate'; export default () => { const { queryResponse } = useSelector(state => state.audit); const [ responseData ] = useResponseData(queryResponse); const total = useMemo(() => { if (responseData.result) { return [responseData.expense, responseData.income, responseData.surplus] } else { return [0, 0, 0] } }, [responseData]); return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/pages/audit/index.jsx ================================================ import { Space } from 'antd'; import {spaceVProp} from '@/utils/var'; import Breadcrumb from '@/components/Breadcrumb'; import OperationBar from './OperationBar'; import TotalAlert from './TotalAlert'; import RecordTable from './RecordTable'; import t from '@/utils/translate'; export default () => { return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/audit/model.js ================================================ import modelExtend from 'dva-model-extend'; import { model } from '@/utils/model'; import { audit } from '@/services/flow'; export default modelExtend(model, { namespace: 'audit', state: { queryResponse: undefined, }, effects: { *query({ payload }, { call, put }) { const response = yield call(audit, payload); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/audit') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/balance-logs/OperationBar.jsx ================================================ import { useDispatch } from 'umi'; import { Button } from 'antd'; import OperationModal from './OperationModal'; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); const addHandler = () => { dispatch({ type: 'modal/show', payload: {component: OperationModal, type: 1, currentItem: {} }}); } return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/balance-logs/OperationModal.jsx ================================================ import { useEffect, useState } from 'react'; import {history, useDispatch, useSelector} from 'umi'; import {DatePicker, Form, Input} from 'antd'; import moment from "moment"; import { create } from '@/services/log'; import {getNull} from "@/utils/util"; import {balanceRequiredRules, timeRequiredRules} from "@/utils/rules"; import FormModal from "@/components/FormModal"; import styles from './index.less'; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { visible } = useSelector(state => state.modal); const [initialValues, setInitialValues] = useState({}); useEffect(() => { if (!visible) return; setInitialValues({ ...getNull(form.getFieldsValue()), 'createTime': moment(), }); }, [visible]); function successHandler() { dispatch({ type: 'logs/query', payload: history.location.query }); } return (
); } ================================================ FILE: bookkeeping-user-fe/src/pages/balance-logs/RecordTable.jsx ================================================ import {useEffect} from "react"; import {history, useDispatch, useIntl, useSelector} from 'umi'; import { Button, message, Table, Space, Modal } from "antd"; import {tableProp} from "@/utils/var"; import { remove } from '@/services/log'; import {getTimeFormat, handleTableChange} from "@/utils/util"; import {usePaginationAndData} from "@/utils/hooks"; import t from "@/utils/translate"; import moment from "moment"; export default () => { const dispatch = useDispatch(); const { queryResponse } = useSelector(state => state.logs); const queryLoading = useSelector(state => state.loading.effects['logs/query']); const [dataAndPagination] = usePaginationAndData(queryResponse); const intl = useIntl(); const messageOperationSuccess = t('operation.success'); const deleteHandler = async (record) => { Modal.confirm({ title: intl.formatMessage({id:'delete.confirm'}), onOk: async () => { const response = await remove(record.id); if (response && response.success) { message.success(messageOperationSuccess); dispatch({ type: 'logs/query', payload: history.location.query }); } } }); } const columns = [ { title: t('flow.createTime'), dataIndex: 'createTime', sorter: true, width: 120, render: time => moment(time).format('YYYY-MM-DD') }, { title: t('asset.balance'), dataIndex: 'asset', }, { title: t('debt.balance'), dataIndex: 'debt', }, { title: t('operation'), key: 'operation', align: 'center', width: 150, render: (_, record) => { return ( ) } } ]; return (
); } ================================================ FILE: bookkeeping-user-fe/src/pages/balance-logs/index.jsx ================================================ import {Space} from "antd"; import {spaceVProp} from "@/utils/var"; import Breadcrumb from "@/components/Breadcrumb"; import RecordTable from './RecordTable'; import OperationBar from './OperationBar'; import t from "@/utils/translate"; export default () => { return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/balance-logs/index.less ================================================ .form3 { :global { .ant-form-item-label { width: 70px; } } } ================================================ FILE: bookkeeping-user-fe/src/pages/balance-logs/model.js ================================================ import modelExtend from 'dva-model-extend'; import { model } from '@/utils/model'; import { query } from '@/services/log'; export default modelExtend(model, { namespace: 'logs', state: { queryResponse: undefined, }, effects: { *query({ payload }, { call, put }) { const response = yield call(query, payload); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/balance-logs') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/books/ConfigModal.jsx ================================================ import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'umi'; import {Form, Select, TreeSelect, Switch} from 'antd'; import {config} from '@/services/book'; import {useCategoryTreeSelectData, useResponseSelectData} from "@/utils/hooks"; import {getNull} from "@/utils/util"; import FormModal from "@/components/FormModal"; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { visible, currentItem } = useSelector(state => state.modal); const { getExpenseableResponse } = useSelector(state => state.account); const [expenseAccounts] = useResponseSelectData(getExpenseableResponse); const { getIncomeableResponse } = useSelector(state => state.account); const [incomeAccounts] = useResponseSelectData(getIncomeableResponse); const { getTransferFromAbleResponse } = useSelector(state => state.account); const [transferFromAccounts] = useResponseSelectData(getTransferFromAbleResponse); const { getTransferToAbleResponse } = useSelector(state => state.account); const [transferToAccounts] = useResponseSelectData(getTransferToAbleResponse); const { querySimpleResponse : expenseCategoryResponse } = useSelector(state => state.expenseCategory); const [expenseCategories] = useCategoryTreeSelectData(expenseCategoryResponse); const { querySimpleResponse : incomeCategoryResponse } = useSelector(state => state.incomeCategory); const [incomeCategories] = useCategoryTreeSelectData(incomeCategoryResponse); useEffect(() => { if (visible) { if (!getExpenseableResponse) dispatch({ type: 'account/fetchExpenseable' }); if (!getIncomeableResponse) dispatch({ type: 'account/fetchIncomeable' }); if (!getTransferFromAbleResponse) dispatch({ type: 'account/fetchTransferFromAble' }); if (!getTransferToAbleResponse) dispatch({ type: 'account/fetchTransferToAble' }); if (!expenseCategoryResponse) dispatch({ type: 'expenseCategory/fetchSimple' }); if (!incomeCategoryResponse) dispatch({ type: 'incomeCategory/fetchSimple' }); } }, [visible]); const [initialValues, setInitialValues] = useState({}) useEffect(() => { if (visible) { let currentItemCopy = {...currentItem}; currentItemCopy.defaultExpenseAccountId = currentItemCopy.defaultExpenseAccount ? currentItemCopy.defaultExpenseAccount.id : null; currentItemCopy.defaultIncomeAccountId = currentItemCopy.defaultIncomeAccount ? currentItemCopy.defaultIncomeAccount.id : null; currentItemCopy.defaultTransferFromAccountId = currentItemCopy.defaultTransferFromAccount ? currentItemCopy.defaultTransferFromAccount.id : null; currentItemCopy.defaultTransferToAccountId = currentItemCopy.defaultTransferToAccount ? currentItemCopy.defaultTransferToAccount.id : null; currentItemCopy.defaultExpenseCategoryId = currentItemCopy.defaultExpenseCategory ? currentItemCopy.defaultExpenseCategory.id : null; currentItemCopy.defaultIncomeCategoryId = currentItemCopy.defaultIncomeCategory ? currentItemCopy.defaultIncomeCategory.id : null; setInitialValues({...getNull(form.getFieldsValue()), ...currentItemCopy}); } }, [visible]); function successHandler(response) { dispatch({ type: 'books/query' }); dispatch({ type: 'session/updateState', payload: { defaultBook: response.data } }); } return ( successHandler(response)} >
); } ================================================ FILE: bookkeeping-user-fe/src/pages/books/OperationBar.jsx ================================================ import { useDispatch } from 'umi'; import { Button } from 'antd'; import OperationModal from './OperationModal'; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); const addHandler = () => { dispatch({ type: 'modal/show', payload: {component: OperationModal, type: 1, currentItem: null }}); } return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/books/OperationModal.jsx ================================================ import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'umi'; import {Form, Input, Select, Switch} from 'antd'; import { create, update } from '@/services/book'; import {getNull} from "@/utils/util"; import {nameRules, notesRules, requiredRules} from "@/utils/rules"; import FormModal from "@/components/FormModal"; import styles from './index.less'; import t from "@/utils/translate"; import {useCurrencyResponseSelectData} from "@/utils/hooks"; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { visible, type, currentItem } = useSelector(state => state.modal); const [initialValues, setInitialValues] = useState(currentItem) useEffect(() => { if (!visible) return; if (type === 1) { setInitialValues({ ...getNull(form.getFieldsValue()), ...{ defaultCurrencyCode: defaultGroup.defaultCurrencyCode, descriptionEnable: true, timeEnable: false, imageEnable: false, } }); } else { setInitialValues({...getNull(form.getFieldsValue()), ...currentItem}); } }, [visible]); useEffect(() => { if (!currencyResponse) dispatch({ type: 'currency/fetchAll' }); }, []); const { getAllResponse : currencyResponse } = useSelector(state => state.currency); const [currencyList] = useCurrencyResponseSelectData(currencyResponse); const { defaultGroup } = useSelector(state => state.session); useEffect(() => { if (!defaultGroup) dispatch({ type: 'session/fetchSession' }); }, []); function successHandler() { dispatch({ type: 'books/query' }); } return (
{ if (defaultBook && record.id === defaultBook.id) return 'table-color-default'; }} size="small" columns={columns} dataSource={dataAndPagination.data} loading={queryLoading} pagination={dataAndPagination.pagination} onChange={handleTableChange} /> ); } ================================================ FILE: bookkeeping-user-fe/src/pages/books/index.jsx ================================================ import {Space} from "antd"; import {spaceVProp} from "@/utils/var"; import Breadcrumb from "@/components/Breadcrumb"; import OperationBar from './OperationBar'; import RecordTable from './RecordTable'; import t from "@/utils/translate"; export default () => { return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/books/index.less ================================================ .form3 { :global { .ant-form-item-label { width: 100px; } } } ================================================ FILE: bookkeeping-user-fe/src/pages/books/model.js ================================================ import modelExtend from 'dva-model-extend'; import { model } from '@/utils/model'; import { query } from '@/services/book'; export default modelExtend(model, { namespace: 'books', state: { queryResponse: undefined, }, effects: { *query({ payload }, { call, put }) { const response = yield call(query, payload); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/books') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/categories/ExpenseCategoryFilterBar.jsx ================================================ import {Button, Col, Form, Input, Row, Select, Space} from "antd"; import {useDispatch} from "umi"; import {filterFormProp} from "@/utils/var"; import t from "@/utils/translate"; export default () => { const [form] = Form.useForm(); const dispatch = useDispatch(); async function resetHandler() { form.resetFields(); const values = await form.validateFields(); dispatch({ type: 'categories/updateState', payload: {expenseCategoryQueryData: values} }); } async function searchHandler(form) { const values = await form.validateFields(); dispatch({ type: 'categories/queryExpenseCategory', payload: values }); dispatch({ type: 'categories/updateState', payload: {expenseCategoryQueryData: values} }); } return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/categories/ExpenseCategoryModal.jsx ================================================ import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'umi'; import { Form, Input, TreeSelect, message } from 'antd'; import { create, update } from '@/services/expense-category'; import { useCategoryTreeSelectData } from "@/utils/hooks"; import {getNull, validateForm} from "@/utils/util"; import FormModal from "@/components/FormModal"; import {nameRules, notesRules} from "@/utils/rules"; import t from "@/utils/translate"; import styles from './index.less'; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { visible, type, currentItem } = useSelector(state => state.modal); const { querySimpleResponse : categoryResponse } = useSelector(state => state.expenseCategory); const [treeData] = useCategoryTreeSelectData(categoryResponse); useEffect(() => { if (visible) { if (!categoryResponse) dispatch({ type: 'expenseCategory/fetchSimple' }); } }, [visible]); const [initialValues, setInitialValues] = useState({}) useEffect(() => { if (visible) { if (type === 1) { setInitialValues({ ...getNull(form.getFieldsValue()), ...{ parentId: currentItem && currentItem.id ? currentItem.id : null, } }); } else { setInitialValues({...getNull(form.getFieldsValue()), ...currentItem}); } } }, [visible]); function successHandler() { dispatch({ type: 'categories/queryExpenseCategory' }); dispatch({ type: 'expenseCategory/refresh' }); } return (
); } ================================================ FILE: bookkeeping-user-fe/src/pages/categories/ExpenseCategoryTable.jsx ================================================ import {useDispatch, useIntl, useSelector} from 'umi'; import {useEffect} from "react"; import {Button, message, Table, Space, Modal, Switch} from "antd"; import { remove, toggle } from '@/services/category'; import { useResponseData } from '@/utils/hooks'; import {searchTreeArray, tableChangeQueryFormat} from '@/utils/util'; import {tableProp} from '@/utils/var'; import ExpenseCategoryModal from './ExpenseCategoryModal'; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch({ type: 'categories/queryExpenseCategory' }); }, []); const { queryExpenseCategoryResponse : queryResponse } = useSelector(state => state.categories); const queryLoading = useSelector(state => state.loading.effects['categories/queryExpenseCategory']); const [categoryTreeData, setCategoryTreeData] = useResponseData(queryResponse); const addHandler = (record, event) => { dispatch({ type: 'modal/show', payload: {component: ExpenseCategoryModal, type: 1, currentItem: record }}); event.stopPropagation(); } function refresh() { dispatch({ type: 'categories/queryExpenseCategory' }); dispatch({ type: 'expenseCategory/refresh' }); } const messageOperationSuccess = t('operation.success'); const toggleHandler = async (record) => { // 乐观更新 let newCategoryTreeData = JSON.parse(JSON.stringify(categoryTreeData)); let node = searchTreeArray(newCategoryTreeData, record.id); if (node) node.enable = !node.enable; setCategoryTreeData(newCategoryTreeData); const response = await toggle(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const updateHandler = (record, event) => { dispatch({ type: 'modal/show', payload: {component: ExpenseCategoryModal, type: 2, currentItem: record }}); event.stopPropagation(); } const intl = useIntl(); const deleteHandler = async (record, event) => { Modal.confirm({ title: intl.formatMessage({id:'delete.confirm'}, { name: record.name }), onOk: async () => { const response = await remove(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } }); event.stopPropagation(); } const columns = [ { title: t('name'), dataIndex: 'name', }, { title: t('notes'), dataIndex: 'notes', }, { title: t('is.enable'), dataIndex: 'enable', width: 150, render: (value, record) => event.stopPropagation() } onChange={ () => toggleHandler(record) } checked={value} /> }, { title: t('operation'), key: 'operation', width: 150, align: 'center', render: (_, record) => { return ( ) } } ]; return ( categoryTreeData && categoryTreeData.length > 0 &&
); } ================================================ FILE: bookkeeping-user-fe/src/pages/categories/IncomeCategoryFilterBar.jsx ================================================ import {Button, Col, Form, Input, Row, Select, Space} from "antd"; import {useDispatch} from "umi"; import {filterFormProp} from "@/utils/var"; import t from "@/utils/translate"; export default () => { const [form] = Form.useForm(); const dispatch = useDispatch(); async function resetHandler() { form.resetFields(); const values = await form.validateFields(); dispatch({ type: 'categories/updateState', payload: {incomeCategoryQueryData: values} }); } async function searchHandler(form) { const values = await form.validateFields(); dispatch({ type: 'categories/queryIncomeCategory', payload: values }); dispatch({ type: 'categories/updateState', payload: {incomeCategoryQueryData: values} }); } return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/categories/IncomeCategoryModal.jsx ================================================ import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'umi'; import { Form, Input, TreeSelect, message } from 'antd'; import { create, update } from '@/services/income-category'; import { useCategoryTreeSelectData } from "@/utils/hooks"; import {getNull, validateForm} from "@/utils/util"; import FormModal from "@/components/FormModal"; import {nameRules, notesRules} from "@/utils/rules"; import t from "@/utils/translate"; import styles from './index.less'; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { visible, type, currentItem } = useSelector(state => state.modal); const { querySimpleResponse : categoryResponse } = useSelector(state => state.incomeCategory); const [treeData] = useCategoryTreeSelectData(categoryResponse); useEffect(() => { if (visible) { if (!categoryResponse) dispatch({ type: 'incomeCategory/fetchSimple' }); } }, [visible]); const [initialValues, setInitialValues] = useState(currentItem) useEffect(() => { if (visible) { if (type === 1) { setInitialValues({ ...getNull(form.getFieldsValue()), ...{ parentId: currentItem && currentItem.id ? currentItem.id : null, } }); } else { setInitialValues({...getNull(form.getFieldsValue()), ...currentItem}); } } }, [visible]); function successHandler() { dispatch({ type: 'categories/queryIncomeCategory' }); dispatch({ type: 'incomeCategory/refresh' }); } return (
); } ================================================ FILE: bookkeeping-user-fe/src/pages/categories/IncomeCategoryTable.jsx ================================================ import {useDispatch, useIntl, useSelector} from 'umi'; import { Button, message, Table, Space, Modal, Switch } from "antd"; import { remove, toggle } from '@/services/category'; import { useResponseData } from "@/utils/hooks"; import {useEffect} from "react"; import {tableProp} from "@/utils/var"; import {searchTreeArray} from "@/utils/util"; import IncomeCategoryModal from './IncomeCategoryModal'; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch({ type: 'categories/queryIncomeCategory' }); }, []); const { queryIncomeCategoryResponse : queryResponse } = useSelector(state => state.categories); const queryLoading = useSelector(state => state.loading.effects['categories/queryIncomeCategory']); const [categoryTreeData, setCategoryTreeData] = useResponseData(queryResponse); const addHandler = (record, event) => { dispatch({ type: 'modal/show', payload: {component: IncomeCategoryModal, type: 1, currentItem: record }}); event.stopPropagation(); } const messageOperationSuccess = t('operation.success'); const toggleHandler = async (record) => { // 乐观更新 let newCategoryTreeData = JSON.parse(JSON.stringify(categoryTreeData)); let node = searchTreeArray(newCategoryTreeData, record.id); if (node) node.enable = !node.enable; setCategoryTreeData(newCategoryTreeData); const response = await toggle(record.id); if (response) { if (response.success) { message.success(messageOperationSuccess); dispatch({ type: 'categories/queryIncomeCategory' }); dispatch({ type: 'incomeCategory/refresh' }); } } } const updateHandler = (record, event) => { dispatch({ type: 'modal/show', payload: {component: IncomeCategoryModal, type: 2, currentItem: record }}); event.stopPropagation(); } const intl = useIntl(); const deleteHandler = async (record, event) => { Modal.confirm({ title: intl.formatMessage({id:'delete.confirm'}, { name: record.name }), onOk: async () => { const response = await remove(record.id); if (response && response.success) { message.success(messageOperationSuccess); dispatch({ type: 'categories/queryIncomeCategory' }); dispatch({ type: 'incomeCategory/refresh' }); } } }); event.stopPropagation(); } const columns = [ { title: t('name'), dataIndex: 'name', }, { title: t('notes'), dataIndex: 'notes', }, { title: t('is.enable'), dataIndex: 'enable', width: 150, render: (value, record) => event.stopPropagation() } onChange={ () => toggleHandler(record) } checked={value} /> }, { title: t('operation'), key: 'operation', width: 150, align: 'center', render: (_, record) => { return ( ) } } ]; return ( categoryTreeData && categoryTreeData.length > 0 &&
); } ================================================ FILE: bookkeeping-user-fe/src/pages/categories/OperationBar.jsx ================================================ import { useDispatch, useSelector } from 'umi'; import { Radio, Button, Space } from 'antd'; import { PlusOutlined } from "@ant-design/icons"; import ExpenseCategoryModal from './ExpenseCategoryModal'; import IncomeCategoryModal from './IncomeCategoryModal'; import TagModal from './TagModal'; import PayeeModal from './PayeeModal'; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); const { currentCategoryType } = useSelector(state => state.categories); const options = [ { label: t('expense.category'), value: 1 }, { label: t('income.category'), value: 2 }, { label: t('tag'), value: 3 }, { label: t('payee'), value: 4 }, ]; const typeChangeHandler = (e) => { dispatch({ type: 'categories/updateState', payload: { currentCategoryType: e.target.value }}) } const addHandler = () => { switch (currentCategoryType) { case 1: dispatch({ type: 'modal/show', payload: {component: ExpenseCategoryModal, type: 1, currentItem: {} }}); break; case 2: dispatch({ type: 'modal/show', payload: {component: IncomeCategoryModal, type: 1, currentItem: {} }}); break; case 3: dispatch({ type: 'modal/show', payload: {component: TagModal, type: 1, currentItem: {} }}); break; case 4: dispatch({ type: 'modal/show', payload: {component: PayeeModal, type: 1, currentItem: {} }}); break; } } return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/categories/PayeeFilterBar.jsx ================================================ import {Button, Col, Form, Input, Row, Select, Space} from "antd"; import {useDispatch} from "umi"; import {filterFormProp} from "@/utils/var"; import t from "@/utils/translate"; export default () => { const [form] = Form.useForm(); const dispatch = useDispatch(); async function resetHandler() { form.resetFields(); const values = await form.validateFields(); dispatch({ type: 'categories/updateState', payload: {payeeQueryData: values} }); } async function searchHandler(form) { const values = await form.validateFields(); dispatch({ type: 'categories/queryPayee', payload: values }); dispatch({ type: 'categories/updateState', payload: {payeeQueryData: values} }); } return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/categories/PayeeModal.jsx ================================================ import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'umi'; import {Form, Input, message, Switch} from 'antd'; import { create, update } from '@/services/payee'; import FormModal from "@/components/FormModal"; import {nameRules, notesRules} from "@/utils/rules"; import {getNull, tableChangeQueryFormat, validateForm} from "@/utils/util"; import t from "@/utils/translate"; import styles from './index.less'; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { visible, type, currentItem } = useSelector(state => state.modal); const { payeePagination : pagination, payeeSorter : sorter } = useSelector(state => state.categories); const [initialValues, setInitialValues] = useState({}) useEffect(() => { if (visible) { if (type === 1) { setInitialValues({ ...getNull(form.getFieldsValue()), expenseable: true, incomeable: false, }); } else { setInitialValues({...getNull(form.getFieldsValue()), ...currentItem}); } } }, [visible]); function successHandler(response) { dispatch({ type: 'categories/queryPayee', payload: tableChangeQueryFormat(pagination, sorter) }); dispatch({ type: 'payee/refresh', payload: { ...response.data } }); } return ( successHandler(response)} >
); } ================================================ FILE: bookkeeping-user-fe/src/pages/categories/PayeeTable.jsx ================================================ import {useEffect} from "react"; import {useDispatch, useIntl, useSelector} from 'umi'; import {Button, message, Table, Space, Modal, Switch} from "antd"; import {tableProp} from "@/utils/var"; import { tableChangeQueryFormat } from "@/utils/util"; import { categoryExpenseableCol, categoryIncomeableCol } from '@/utils/columns'; import { remove, toggle } from '@/services/payee'; import { usePaginationAndData } from '@/utils/hooks'; import PayeeModal from './PayeeModal'; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); const { queryPayeeResponse : queryResponse } = useSelector(state => state.categories); const queryLoading = useSelector(state => state.loading.effects['categories/queryPayee']); const [ dataAndPagination ] = usePaginationAndData(queryResponse); const { payeePagination : pagination, payeeSorter : sorter, payeeQueryData: query } = useSelector(state => state.categories); useEffect(() => { dispatch({ type: 'categories/queryPayee' }); }, []); function tableChangeHandler(pagination, _, sorter) { dispatch({ type: 'categories/updateState', payload: {payeePagination: pagination, payeeSorter: sorter} }); dispatch({ type: 'categories/queryPayee', payload: { ...tableChangeQueryFormat(pagination, sorter), ...query }}); } function refresh(response) { dispatch({ type: 'categories/queryPayee', payload: {...tableChangeQueryFormat(pagination, sorter), ...query }}); dispatch({ type: 'payee/refresh', payload: { ...response.data } }); } const messageOperationSuccess = t('operation.success'); const toggleHandler = async (record) => { const response = await toggle(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(response); } } const updateHandler = (record) => { dispatch({ type: 'modal/show', payload: {component: PayeeModal, type: 2, currentItem: record }}); } const intl = useIntl(); const deleteHandler = async (record) => { Modal.confirm({ title: intl.formatMessage({id:'delete.confirm'}, { name: record.name }), onOk: async () => { const response = await remove(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(response); } } }); } const columns = [ { title: t('name'), dataIndex: 'name', }, { title: t('notes'), dataIndex: 'notes', }, { title: t('is.enable'), dataIndex: 'enable', width: 150, render: (value, record) => toggleHandler(record)} checked={value} /> }, categoryExpenseableCol(), categoryIncomeableCol(), { title: t('operation'), key: 'operation', width: 120, align: 'center', render: (_, record) => { return ( ) } } ]; return (
record.id} loading={queryLoading} pagination={dataAndPagination.pagination} onChange={tableChangeHandler} /> ); } ================================================ FILE: bookkeeping-user-fe/src/pages/categories/TagFilterBar.jsx ================================================ import {Button, Col, Form, Input, Row, Select, Space} from "antd"; import {useDispatch} from "umi"; import {filterFormProp} from "@/utils/var"; import t from "@/utils/translate"; export default () => { const [form] = Form.useForm(); const dispatch = useDispatch(); async function resetHandler() { form.resetFields(); const values = await form.validateFields(); dispatch({ type: 'categories/updateState', payload: {tagQueryData: values} }); } async function searchHandler(form) { const values = await form.validateFields(); dispatch({ type: 'categories/queryTag', payload: values }); dispatch({ type: 'categories/updateState', payload: {tagQueryData: values} }); } return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/categories/TagModal.jsx ================================================ import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'umi'; import {Form, Input, message, Switch, TreeSelect} from 'antd'; import { create, update } from '@/services/tag'; import FormModal from "@/components/FormModal"; import {nameRules, notesRules} from "@/utils/rules"; import {getNull, tableChangeQueryFormat, validateForm} from "@/utils/util"; import {useCategoryTreeSelectData} from "@/utils/hooks"; import t from "@/utils/translate"; import styles from './index.less'; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { visible, type, currentItem } = useSelector(state => state.modal); const { getEnableResponse : tagResponse } = useSelector(state => state.tag); const [treeData] = useCategoryTreeSelectData(tagResponse); useEffect(() => { if (visible) { if (!tagResponse) dispatch({ type: 'tag/fetchEnable' }); } }, [visible]); const [initialValues, setInitialValues] = useState({}) useEffect(() => { if (visible) { if (type === 1) { setInitialValues({ ...getNull(form.getFieldsValue()), ...{ expenseable: currentItem && currentItem.expenseable ? currentItem.expenseable : true, incomeable: currentItem && currentItem.incomeable ? currentItem.incomeable : false, transferable: currentItem && currentItem.transferable ? currentItem.transferable :false, parentId: currentItem && currentItem.id ? currentItem.id : null, } }); } else { setInitialValues({...getNull(form.getFieldsValue()), ...currentItem}); } } }, [visible]); function successHandler(response) { dispatch({ type: 'categories/queryTag' }); dispatch({ type: 'tag/refresh', payload: { ...response.data } }); } return ( successHandler(response)} >
); } ================================================ FILE: bookkeeping-user-fe/src/pages/categories/TagTable.jsx ================================================ import {useEffect} from "react"; import {useDispatch, useIntl, useSelector} from 'umi'; import {Button, message, Table, Space, Modal, Switch} from "antd"; import {tableProp} from "@/utils/var"; import {searchTreeArray, tableChangeQueryFormat} from "@/utils/util"; import { categoryExpenseableCol, categoryIncomeableCol, categoryTransferableCol } from '@/utils/columns'; import { remove, toggle } from '@/services/tag'; import {useResponseData} from '@/utils/hooks'; import TagModal from './TagModal'; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); const { queryTagResponse : queryResponse } = useSelector(state => state.categories); const queryLoading = useSelector(state => state.loading.effects['categories/queryTag']); const [tagTreeData, setTagTreeData] = useResponseData(queryResponse); useEffect(() => { dispatch({ type: 'categories/queryTag' }); }, []); function refresh() { dispatch({ type: 'categories/queryTag' }); dispatch({ type: 'tag/refresh'}); } const messageOperationSuccess = t('operation.success'); const toggleHandler = async (record) => { // 乐观更新 let newTreeData = JSON.parse(JSON.stringify(tagTreeData)); let node = searchTreeArray(newTreeData, record.id); if (node) node.enable = !node.enable; setTagTreeData(newTreeData); const response = await toggle(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } const addHandler = (record, event) => { dispatch({ type: 'modal/show', payload: {component: TagModal, type: 1, currentItem: record }}); event.stopPropagation(); } const updateHandler = (record, event) => { dispatch({ type: 'modal/show', payload: {component: TagModal, type: 2, currentItem: record }}); event.stopPropagation(); } const intl = useIntl(); const deleteHandler = async (record, event) => { Modal.confirm({ title: intl.formatMessage({id:'delete.confirm'}, { name: record.name }), onOk: async () => { const response = await remove(record.id); if (response && response.success) { message.success(messageOperationSuccess); refresh(); } } }); event.stopPropagation(); } const columns = [ { title: t('name'), dataIndex: 'name', }, { title: t('notes'), dataIndex: 'notes', }, { title: t('is.enable'), dataIndex: 'enable', width: 150, sorter: true, render: (value, record) => e.stopPropagation() } onChange={() => toggleHandler(record)} checked={value} /> }, categoryExpenseableCol(), categoryIncomeableCol(), categoryTransferableCol(), { title: t('operation'), key: 'operation', width: 160, align: 'center', render: (_, record) => { return ( ) } } ]; return ( tagTreeData && tagTreeData.length > 0 &&
); } ================================================ FILE: bookkeeping-user-fe/src/pages/categories/index.jsx ================================================ import { useSelector } from 'umi'; import { Space } from "antd"; import {spaceVProp} from "@/utils/var"; import Breadcrumb from '@/components/Breadcrumb'; import OperationBar from './OperationBar'; import ExpenseCategoryTable from './ExpenseCategoryTable'; import IncomeCategoryTable from './IncomeCategoryTable'; import TagTable from './TagTable'; import PayeeTable from './PayeeTable'; import PayeeFilterBar from "./PayeeFilterBar"; import TagFilterBar from "./TagFilterBar"; import ExpenseCategoryFilterBar from "./ExpenseCategoryFilterBar"; import IncomeCategoryFilterBar from "./IncomeCategoryFilterBar"; import t from "@/utils/translate"; export default () => { const { currentCategoryType } = useSelector(state => state.categories); const tableSwitch = (type) => { switch(type) { case 1: return ; case 2: return ; case 3: return ; case 4: return ; } } const filterSwitch = (type) => { switch(type) { case 1: return ; case 2: return ; case 3: return ; case 4: return ; } } return ( { filterSwitch(currentCategoryType) } { tableSwitch(currentCategoryType) } ); } ================================================ FILE: bookkeeping-user-fe/src/pages/categories/index.less ================================================ .form { :global { .ant-form-item-label { width: 65px; } } } .form2 { :global { .ant-form-item-label { width: 45px; } } } .form3 { :global { .ant-form-item-label { width: 50px; } } } ================================================ FILE: bookkeeping-user-fe/src/pages/categories/model.js ================================================ import modelExtend from 'dva-model-extend'; import { model } from '@/utils/model'; import { query as queryExpenseCategory } from '@/services/expense-category'; import { query as queryIncomeCategory } from '@/services/income-category'; import { query as queryTag } from '@/services/tag'; import { query as queryPayee } from '@/services/payee'; export default modelExtend(model, { namespace: 'categories', state: { currentCategoryType: 1, queryExpenseCategoryResponse: undefined, queryIncomeCategoryResponse: undefined, queryTagResponse: undefined, queryPayeeResponse: undefined, payeePagination: { }, payeeSorter: { }, payeeQueryData: { }, expenseCategoryQueryData: { }, incomeCategoryQueryData: { }, tagQueryData: { }, }, effects: { *queryExpenseCategory({ payload }, { call, put }) { const response = yield call(queryExpenseCategory, payload); yield put({ type: 'updateState', payload: { queryExpenseCategoryResponse: response }, }); }, *queryIncomeCategory({ payload }, { call, put }) { const response = yield call(queryIncomeCategory, payload); yield put({ type: 'updateState', payload: { queryIncomeCategoryResponse: response }, }); }, *queryTag({ payload }, { call, put }) { const response = yield call(queryTag, payload); yield put({ type: 'updateState', payload: { queryTagResponse: response }, }); }, *queryPayee({ payload }, { call, put }) { const response = yield call(queryPayee, payload); yield put({ type: 'updateState', payload: { queryPayeeResponse: response }, }); }, }, }) ================================================ FILE: bookkeeping-user-fe/src/pages/checking-accounts/GeneralBar.jsx ================================================ import { useEffect } from 'react'; import {Button, Card, Col, Row, Statistic} from "antd"; import {ReloadOutlined} from "@ant-design/icons"; import { useDispatch, useSelector } from 'umi'; import { useResponseData } from '@/utils/hooks'; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch({ type: 'checkingAccounts/sum' }); }, []); const loading = useSelector(state => state.loading.effects['checkingAccounts/sum']); const queryLoading = useSelector(state => state.loading.effects['checkingAccounts/query']); const { sumResponse } = useSelector(state => state.checkingAccounts); const [sum] = useResponseData(sumResponse); function refreshHandler() { dispatch({ type: 'checkingAccounts/refresh' }); } return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/checking-accounts/ItemCard.jsx ================================================ import {useSelector} from "umi"; import {Card, Row, Col, Button, Space, Descriptions, Collapse} from 'antd'; import { spaceVProp } from '@/utils/var'; import AccountRecordTable from '@/components/AccountRecordTable'; import {showFlowModal} from '@/utils/flow'; import styles from './index.less'; import t from '@/utils/translate'; export default (props) => { const { account } = props; const { currentTime } = useSelector(state => state.checkingAccounts); function expenseHandler() { showFlowModal(1, 1, {accountId: account.id}); } function incomeHandler() { showFlowModal(2, 1, {accountId: account.id}); } function transferToHandler() { showFlowModal(3, 1, { toId: account.id }); } function transferFromHandler() { showFlowModal(3, 1, { fromId: account.id }); } function adjustBalanceHandler() { showFlowModal(4, 2, { ...account }); } return ( {account.balance} {account.currencyCode} {account.include ? t('yes') : t('no')} ); } ================================================ FILE: bookkeeping-user-fe/src/pages/checking-accounts/List.jsx ================================================ import { useEffect } from "react"; import {Empty, Pagination, Space, Spin} from "antd"; import { useDispatch, useSelector } from "umi"; import {spaceVProp} from "@/utils/var"; import { usePaginationAndData } from "@/utils/hooks"; import { paginationChange } from "@/utils/util"; import ItemCard from "./ItemCard"; export default () => { const dispatch = useDispatch(); const { queryResponse } = useSelector(state => state.checkingAccounts); const queryLoading = useSelector(state => state.loading.effects['checkingAccounts/query']); const [ dataAndPagination ] = usePaginationAndData(queryResponse); return ( { dataAndPagination.data && dataAndPagination.data.length > 0 ? ( { dataAndPagination.data.map(item => ) } ) : () } ) } ================================================ FILE: bookkeeping-user-fe/src/pages/checking-accounts/index.jsx ================================================ import { Space } from 'antd'; import { spaceVProp } from '@/utils/var'; import Breadcrumb from "@/components/Breadcrumb"; import GeneralBar from './GeneralBar'; import List from './List'; import t from '@/utils/translate'; export default () => { return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/checking-accounts/index.less ================================================ .item-card-collapse { :global { .ant-collapse-content-box { padding: 0 !important; } } } ================================================ FILE: bookkeeping-user-fe/src/pages/checking-accounts/model.js ================================================ import {history} from "umi"; import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import {query, sum} from "@/services/checking-account"; export default modelExtend(model, { namespace: 'checkingAccounts', state: { queryResponse: undefined, sumResponse: undefined, currentTime: Date.now() }, effects: { *sum(_, { call, put }) { const response = yield call(sum); yield put({ type: 'updateState', payload: { sumResponse: response }, }); }, *query({ payload }, { call, put }) { const response = yield call(query, { ...payload, ...{ enable: true } }); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, *refresh(_, { call, put, select }) { yield put({ type: 'query', payload: history.location.query }); yield put({ type: 'sum' }); // 更新账户的流水记录 yield put({ type: 'updateState', payload: {currentTime: new Date()} }); } }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/checking-accounts') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/credit-accounts/GeneralBar.jsx ================================================ import { useEffect } from 'react'; import {Button, Card, Col, Row, Statistic} from "antd"; import {ReloadOutlined} from "@ant-design/icons"; import { useDispatch, useSelector } from 'umi'; import { useResponseData } from '@/utils/hooks'; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch({ type: 'creditAccounts/sum' }); }, []); const loading = useSelector(state => state.loading.effects['creditAccounts/sum']); const queryLoading = useSelector(state => state.loading.effects['creditAccounts/query']); const { sumResponse } = useSelector(state => state.creditAccounts); const [sum] = useResponseData(sumResponse); function refreshHandler() { dispatch({ type: 'creditAccounts/refresh' }); } return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/credit-accounts/ItemCard.jsx ================================================ import {useSelector} from "umi"; import {Card, Row, Col, Button, Space, Descriptions, Collapse} from 'antd'; import { spaceVProp } from '@/utils/var'; import AccountRecordTable from '@/components/AccountRecordTable'; import {showFlowModal} from "@/utils/flow"; import styles from './index.less'; import t from '@/utils/translate'; export default (props) => { const { account } = props; const { currentTime } = useSelector(state => state.creditAccounts); function expenseHandler() { showFlowModal(1, 1, {accountId: account.id}); } function incomeHandler() { showFlowModal(2, 1, {accountId: account.id}); } function transferToHandler() { showFlowModal(3, 1, { toId: account.id }); } function transferFromHandler() { showFlowModal(3, 1, { fromId: account.id }); } function adjustBalanceHandler() { showFlowModal(4, 2, { ...account }); } return ( {account.balance} {account.currencyCode} {account.limit} {account.remainLimit} {account.include ? t('yes') : t('no')} ); } ================================================ FILE: bookkeeping-user-fe/src/pages/credit-accounts/List.jsx ================================================ import { useEffect } from "react"; import {Empty, Pagination, Space, Spin} from "antd"; import { useDispatch, useSelector } from "umi"; import { usePaginationAndData } from "@/utils/hooks"; import {spaceVProp} from "@/utils/var"; import {paginationChange} from "@/utils/util"; import ItemCard from "./ItemCard"; export default () => { const dispatch = useDispatch(); const { queryResponse } = useSelector(state => state.creditAccounts); const queryLoading = useSelector(state => state.loading.effects['creditAccounts/query']); const [ dataAndPagination ] = usePaginationAndData(queryResponse); return ( { dataAndPagination.data && dataAndPagination.data.length > 0 ? ( { dataAndPagination.data.map(item => ) } ) : () } ) } ================================================ FILE: bookkeeping-user-fe/src/pages/credit-accounts/index.jsx ================================================ import { Space } from 'antd'; import { spaceVProp } from '@/utils/var'; import Breadcrumb from "@/components/Breadcrumb"; import GeneralBar from './GeneralBar'; import List from './List'; import t from '@/utils/translate'; export default () => { return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/credit-accounts/index.less ================================================ .item-card-collapse { :global { .ant-collapse-content-box { padding: 0 !important; } } } ================================================ FILE: bookkeeping-user-fe/src/pages/credit-accounts/model.js ================================================ import {history} from "umi"; import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import {query, sum} from "@/services/credit-account"; export default modelExtend(model, { namespace: 'creditAccounts', state: { queryResponse: undefined, sumResponse: undefined, currentTime: Date.now() }, effects: { *sum(_, { call, put }) { const response = yield call(sum); yield put({ type: 'updateState', payload: { sumResponse: response }, }); }, *query({ payload }, { call, put }) { const response = yield call(query, { ...payload, ...{ enable: true } }); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, *refresh(_, { call, put, select }) { yield put({ type: 'query', payload: history.location.query }); yield put({ type: 'sum' }); // 更新账户的流水记录 yield put({ type: 'updateState', payload: {currentTime: new Date()} }); } }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/credit-accounts') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/dashboard/AssetBar.jsx ================================================ import {useEffect} from "react"; import {Button, Card, Col, Row, Statistic} from "antd"; import {useDispatch, useSelector} from "umi"; import t from '@/utils/translate'; import {useResponseData} from "@/utils/hooks"; export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch({ type: 'dashboard/fetchAssetOverview' }); }, []); const loading = useSelector(state => state.loading.effects['dashboard/fetchAssetOverview']); const { getAssetOverviewResponse } = useSelector(state => state.dashboard); const [assetOverview] = useResponseData(getAssetOverviewResponse); return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/dashboard/CardExtra.jsx ================================================ import {useEffect, useState} from "react"; import {DatePicker, Radio, Space} from "antd"; import {radioValueToTimeRange} from '@/utils/util'; import t from '@/utils/translate'; export default (props) => { const { value, onChange } = props; const [ rangePickerValue, setRangePickerValue ] = useState([]); useEffect(() => { setRangePickerValue(radioValueToTimeRange(value)); }, [value]); function radioChangeHandler(e) { onChange(e.target.value); } return {t('in.30.days')} {t('in.1.year')} {t('this.month')} {t('this.year')} {t('last.year')} } ================================================ FILE: bookkeeping-user-fe/src/pages/dashboard/ExpenseCategory.jsx ================================================ import {useEffect, useState} from "react"; import {useDispatch, useSelector} from "umi"; import { Card } from "antd"; import CardExtra from './CardExtra'; import {radioValueToTimeRange} from "@/utils/util"; import {useResponseData} from "@/utils/hooks"; import Pie from '@/components/charts/Pie'; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); const [timeRange, setTimeRange] = useState(7); function timeRangeChangeHanlder(value) { setTimeRange(value); } useEffect(() => { const rangeValues = radioValueToTimeRange(timeRange); dispatch({ type: 'dashboard/fetchExpenseCategory', payload: { start: rangeValues[0].valueOf(), end: rangeValues[1].valueOf() } }); }, [timeRange]); const loading = useSelector(state => state.loading.effects['dashboard/fetchExpenseCategory']); const { getExpenseCategoryResponse } = useSelector(state => state.dashboard); const [data] = useResponseData(getExpenseCategoryResponse); return ( }> ) } ================================================ FILE: bookkeeping-user-fe/src/pages/dashboard/ExpenseTrend.jsx ================================================ import { useEffect } from "react"; import { useDispatch, useSelector } from "umi"; import { useResponseData } from "@/utils/hooks"; import { Card, Tabs } from "antd"; import Bar from '@/components/charts/Bar'; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch({ type: 'dashboard/fetchExpenseTrend' }); }, []); const loading = useSelector(state => state.loading.effects['dashboard/fetchExpenseTrend']); const { getExpenseTrendResponse } = useSelector(state => state.dashboard); const [data] = useResponseData(getExpenseTrendResponse); return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/dashboard/IncomeCategory.jsx ================================================ import {useEffect, useState} from "react"; import {useDispatch, useSelector} from "umi"; import { Card } from "antd"; import CardExtra from './CardExtra'; import {radioValueToTimeRange} from "@/utils/util"; import {useResponseData} from "@/utils/hooks"; import Pie from '@/components/charts/Pie'; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); const [timeRange, setTimeRange] = useState(7); function timeRangeChangeHanlder(value) { setTimeRange(value); } useEffect(() => { const rangeValues = radioValueToTimeRange(timeRange); dispatch({ type: 'dashboard/fetchIncomeCategory', payload: { start: rangeValues[0].valueOf(), end: rangeValues[1].valueOf() } }); }, [timeRange]); const loading = useSelector(state => state.loading.effects['dashboard/fetchIncomeCategory']); const { getIncomeCategoryResponse } = useSelector(state => state.dashboard); const [data] = useResponseData(getIncomeCategoryResponse); return ( }> ) } ================================================ FILE: bookkeeping-user-fe/src/pages/dashboard/IncomeTrend.jsx ================================================ import { useEffect } from "react"; import { useDispatch, useSelector } from "umi"; import { useResponseData } from "@/utils/hooks"; import { Card, Tabs } from "antd"; import Bar from '@/components/charts/Bar'; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch({ type: 'dashboard/fetchIncomeTrend' }); }, []); const loading = useSelector(state => state.loading.effects['dashboard/fetchIncomeTrend']); const { getIncomeTrendResponse } = useSelector(state => state.dashboard); const [data] = useResponseData(getIncomeTrendResponse); return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/dashboard/TransactionTable.jsx ================================================ import {useEffect, useState} from "react"; import {useDispatch, useSelector} from "umi"; import { Card, Table } from 'antd'; import styles from './TransactionTable.less'; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch({ type: 'dashboard/fetchExpenseIncomeTable' }); }, []); const loading = useSelector(state => state.loading.effects['dashboard/fetchExpenseIncomeTable']); const { getExpenseIncomeTableResponse } = useSelector(state => state.dashboard); const [data, setData] = useState([]); const expenseMessage = t('expense') + t('amount'); const incomeMessage = t('income') + t('amount'); const frequencyExpenseMessage = t('frequency.expense'); const frequencyIncomeMessage = t('frequency.income'); useEffect(() => { if (!getExpenseIncomeTableResponse) return; if (getExpenseIncomeTableResponse.success) { const responseData = getExpenseIncomeTableResponse.data; setData( [ { key: '1', name: expenseMessage, week: responseData[0][0], month: responseData[0][1], year: responseData[0][2], year2: responseData[0][3], week2: responseData[0][4], month2: responseData[0][5], year3: responseData[0][6], }, { key: '2', name: incomeMessage, week: responseData[1][0], month: responseData[1][1], year: responseData[1][2], year2: responseData[1][3], week2: responseData[1][4], month2: responseData[1][5], year3: responseData[1][6], }, { key: '3', name: frequencyExpenseMessage, week: responseData[2][0], month: responseData[2][1], year: responseData[2][2], year2: responseData[2][3], week2: responseData[2][4], month2: responseData[2][5], year3: responseData[2][6], }, { key: '4', name: frequencyIncomeMessage, week: responseData[3][0], month: responseData[3][1], year: responseData[3][2], year2: responseData[3][3], week2: responseData[3][4], month2: responseData[3][5], year3: responseData[3][6], }, ] ); } }, [getExpenseIncomeTableResponse]); const columns = [ { title: '', dataIndex: 'name', width: 150, className: 'name-col' }, { title: t('this.week'), dataIndex: 'week', }, { title: t('this.month'), dataIndex: 'month', }, { title: t('this.year'), dataIndex: 'year', }, { title: t('last.year'), dataIndex: 'year2', }, { title: t('in.7.days'), dataIndex: 'week2', }, { title: t('in.30.days'), dataIndex: 'month2', }, { title: t('in.1.year'), dataIndex: 'year3', } ]; return (
); } ================================================ FILE: bookkeeping-user-fe/src/pages/dashboard/TransactionTable.less ================================================ .table { :global { .name-col { background: #fafafa; } tr:hover > td { background-color: transparent !important; } tr:hover > td.name-col { background-color: #fafafa !important; } } } ================================================ FILE: bookkeeping-user-fe/src/pages/dashboard/index.jsx ================================================ import {Col, Row, Space} from "antd"; import { spaceVProp } from "@/utils/var"; import Breadcrumb from "@/components/Breadcrumb"; import AssetBar from './AssetBar'; import TransactionTable from './TransactionTable'; import ExpenseTrend from './ExpenseTrend'; import IncomeTrend from './IncomeTrend'; import ExpenseCategory from './ExpenseCategory'; import IncomeCategory from './IncomeCategory'; import styles from './index.less'; import t from '@/utils/translate'; export default () => { return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/dashboard/index.less ================================================ .pie-row { :global(.ant-card) { height: 100%; } } .trend { display: flex; flex-flow: row; align-items: center; >div { flex: 1; } } ================================================ FILE: bookkeeping-user-fe/src/pages/dashboard/model.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { getAssetOverview, getExpenseIncomeTable, getExpenseTrend, getIncomeTrend, getExpenseCategory, getIncomeCategory } from '@/services/dashboard'; export default modelExtend(model, { namespace: 'dashboard', state: { getAssetOverviewResponse: undefined, getExpenseIncomeTableResponse: undefined, getExpenseTrendResponse: undefined, getIncomeTrendResponse: undefined, getExpenseCategoryResponse: undefined, getIncomeCategoryResponse: undefined, }, effects: { *fetchAssetOverview(_, { call, put }) { const response = yield call(getAssetOverview); yield put({ type: 'updateState', payload: { getAssetOverviewResponse: response }, }); }, *fetchExpenseIncomeTable(_, { call, put }) { const response = yield call(getExpenseIncomeTable); yield put({ type: 'updateState', payload: { getExpenseIncomeTableResponse: response }, }); }, *fetchExpenseTrend(_, { call, put }) { const response = yield call(getExpenseTrend); yield put({ type: 'updateState', payload: { getExpenseTrendResponse: response }, }); }, *fetchIncomeTrend(_, { call, put }) { const response = yield call(getIncomeTrend); yield put({ type: 'updateState', payload: { getIncomeTrendResponse: response }, }); }, *fetchExpenseCategory({ payload }, { call, put }) { const response = yield call(getExpenseCategory, payload); yield put({ type: 'updateState', payload: { getExpenseCategoryResponse: response }, }); }, *fetchIncomeCategory({ payload }, { call, put }) { const response = yield call(getIncomeCategory, payload); yield put({ type: 'updateState', payload: { getIncomeCategoryResponse: response }, }); }, }, }) ================================================ FILE: bookkeeping-user-fe/src/pages/debt-accounts/GeneralBar.jsx ================================================ import { useEffect } from 'react'; import { useDispatch, useSelector } from 'umi'; import {Button, Card, Col, Row, Statistic} from "antd"; import {ReloadOutlined} from "@ant-design/icons"; import { useResponseData } from '@/utils/hooks'; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch({ type: 'debtAccounts/sum' }); }, []); const loading = useSelector(state => state.loading.effects['debtAccounts/sum']); const queryLoading = useSelector(state => state.loading.effects['debtAccounts/query']); const { sumResponse } = useSelector(state => state.debtAccounts); const [sum] = useResponseData(sumResponse); function refreshHandler() { dispatch({ type: 'debtAccounts/refresh' }); } return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/debt-accounts/ItemCard.jsx ================================================ import {useSelector} from "umi"; import {Card, Row, Col, Button, Space, Descriptions, Collapse} from 'antd'; import { spaceVProp } from '@/utils/var'; import {showFlowModal} from "@/utils/flow"; import AccountRecordTable from "@/components/AccountRecordTable"; import styles from './index.less'; import t from "@/utils/translate"; export default (props) => { const { account } = props; const { currentTime } = useSelector(state => state.debtAccounts); function expenseHandler() { showFlowModal(1, 1, {accountId: account.id}); } function incomeHandler() { showFlowModal(2, 1, {accountId: account.id}); } function transferToHandler() { showFlowModal(3, 1, { toId: account.id }); } function transferFromHandler() { showFlowModal(3, 1, { fromId: account.id }); } function adjustBalanceHandler() { showFlowModal(4, 2, { ...account }); } return ( {account.balance} {account.currencyCode} {account.limit ? account.limit : ''} {account.remainLimit ? account.remainLimit : ''} {account.include ? t('yes') : t('no')} ); } ================================================ FILE: bookkeeping-user-fe/src/pages/debt-accounts/List.jsx ================================================ import { useEffect } from "react"; import {Empty, Pagination, Space, Spin} from "antd"; import { useDispatch, useSelector } from "umi"; import { usePaginationAndData } from "@/utils/hooks"; import {spaceVProp} from "@/utils/var"; import {paginationChange} from "@/utils/util"; import ItemCard from "./ItemCard"; export default () => { const dispatch = useDispatch(); const { queryResponse } = useSelector(state => state.debtAccounts); const queryLoading = useSelector(state => state.loading.effects['debtAccounts/query']); const [ dataAndPagination ] = usePaginationAndData(queryResponse); return ( { dataAndPagination.data && dataAndPagination.data.length > 0 ? ( { dataAndPagination.data.map(item => ) } ) : () } ) } ================================================ FILE: bookkeeping-user-fe/src/pages/debt-accounts/index.jsx ================================================ import { Space } from 'antd'; import { spaceVProp } from '@/utils/var'; import Breadcrumb from "@/components/Breadcrumb"; import GeneralBar from './GeneralBar'; import List from './List'; import t from '@/utils/translate'; export default () => { return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/debt-accounts/index.less ================================================ .item-card-collapse { :global { .ant-collapse-content-box { padding: 0 !important; } } } ================================================ FILE: bookkeeping-user-fe/src/pages/debt-accounts/model.js ================================================ import {history} from "umi"; import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import {query, sum} from "@/services/debt-account"; export default modelExtend(model, { namespace: 'debtAccounts', state: { queryResponse: undefined, sumResponse: undefined, currentTime: Date.now() }, effects: { *sum(_, { call, put }) { const response = yield call(sum); yield put({ type: 'updateState', payload: { sumResponse: response }, }); }, *query({ payload }, { call, put }) { const response = yield call(query, { ...payload, ...{ enable: true } }); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, *refresh(_, { call, put, select }) { yield put({ type: 'query', payload: history.location.query }); yield put({ type: 'sum' }); // 更新账户的流水记录 yield put({ type: 'updateState', payload: {currentTime: new Date()} }); } }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/debt-accounts') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/document.ejs ================================================ 九快记账
================================================ FILE: bookkeeping-user-fe/src/pages/expenses/OperationBar.jsx ================================================ import { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'umi'; import { Form, Row, Col, Button, Space} from 'antd'; import { useResponseSelectData, useCategoryTreeSelectData, } from "@/utils/hooks"; import { filterFormProp } from "@/utils/var"; import { searchHandlerWithCategory } from "@/utils/util"; import {showFlowModal} from '@/utils/flow'; import FormItemDateRange from "@/components/FormItemDateRange"; import FormItemAmountRange from "@/components/FormItemAmountRange"; import FormItemAccounts from "@/components/FormItemAccounts"; import FormItemPayees from "@/components/FormItemPayees"; import FormItemTags from "@/components/FormItemTags"; import FormItemCategories from "@/components/FormItemCategories"; import FormItemDescriptionSearch from "@/components/FormItemDescriptionSearch"; import FormItemStatus from "@/components/FormItemStatus"; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const loading = useSelector(state => state.loading.effects['expenses/query']); useEffect(() => { if (!tagResponse) dispatch({ type: 'tag/fetchExpenseable' }); if (!payeeResponse) dispatch({ type: 'payee/fetchExpenseable' }); if (!accountResponse) dispatch({ type: 'account/fetchExpenseable' }); if (!querySimpleResponse) dispatch({ type: 'expenseCategory/fetchSimple' }); }, []); const { getExpenseableResponse : tagResponse } = useSelector(state => state.tag); const [tags] = useCategoryTreeSelectData(tagResponse); const { getExpenseableResponse : payeeResponse } = useSelector(state => state.payee); const [payees] = useResponseSelectData(payeeResponse); const { getExpenseableResponse : accountResponse } = useSelector(state => state.account); const [accounts] = useResponseSelectData(accountResponse); const { querySimpleResponse } = useSelector(state => state.expenseCategory); const [categories] = useCategoryTreeSelectData(querySimpleResponse); function addHandler() { showFlowModal(1, 1, {}); } const [dateRadioValue, setDateRadioValue] = useState(); function resetHandler() { setDateRadioValue(); form.resetFields(); } return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/pages/expenses/RecordTable.jsx ================================================ import {history, useDispatch, useSelector} from 'umi'; import {Table, Space, message, Modal, Descriptions, Dropdown, Menu, Tooltip} from 'antd'; import { DownOutlined } from '@ant-design/icons'; import { remove, confirm } from '@/services/flow'; import {useResultPaginationAndData, useImageEnable, useDescriptionEnable, useTimeFormat} from '@/utils/hooks'; import { handleTableChange } from "@/utils/util"; import {showFlowModal, imageHandler} from '@/utils/flow'; import { tableProp } from '@/utils/var'; import {createTimeCol, amountCol, statusCol} from '@/utils/columns'; import FlowTagDisplay from '@/components/FlowTagDisplay'; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); const { queryResponse } = useSelector(state => state.expenses); const queryLoading = useSelector(state => state.loading.effects['expenses/query']); const [ dataAndPagination ] = useResultPaginationAndData(queryResponse); const imageEnable = useImageEnable(); const descriptionEnable = useDescriptionEnable(); const timeFormat = useTimeFormat(); let columns = []; if (descriptionEnable) { columns.push({ title: t('description'), dataIndex: 'description', }); } columns = columns.concat( [ amountCol(), createTimeCol(timeFormat), { title: t('category'), dataIndex: 'categoryName', }, { title: t('account'), dataIndex: 'accountName', }, { title: t('flow.tag'), dataIndex: 'tags', render: (tags, record) => <>{tags.map(i => )} }, { title: t('flow.payee'), dataIndex: 'payee', sorter: true, render: payee => <>{payee ? payee.name : null} }, statusCol(), { title: t('operation'), key: 'operation', width: 100, align: 'center', render: (_, record) => { return ( copyHandler(record)}>{t('copy')} {imageEnable ? imageHandler(record)}>{t('image')} : null} updateHandler(record)}>{t('update')} confirmHandler(record)}>{t('confirm')} refundHandler(record)}>{t('refund')} deleteHandler(record)}> { record.status == 3 ? {t('delete')}: t('delete') } }> {t('more')} ) } } ] ); function copyHandler(record) { showFlowModal(1, 3, {...record}); } const updateHandler = (record) => { showFlowModal(1, 2, {...record}); } const messageDeleteConfirm = t('delete.confirm', { name: '' }); const messageDeleteConfirmBalance = t('delete.confirm.balance'); const messageOperationSuccess = t('operation.success'); function deleteHandler(record) { Modal.confirm({ title: record.status === 2 ? messageDeleteConfirm : messageDeleteConfirmBalance, onOk: async () => { const response = await remove(record); if (response && response.success) { message.success(messageOperationSuccess); dispatch({ type: 'expenses/query', payload: history.location.query }); } } }); } function refundHandler(record) { showFlowModal(1, 4, {...record}); } const messageConfirmOperation = t('confirm.operation'); function confirmHandler(record) { Modal.confirm({ title: messageConfirmOperation, onOk: async () => { const response = await confirm(record); if (response && response.success) { message.success(messageOperationSuccess); dispatch({ type: 'expenses/query', payload: history.location.query }); } } }); } return (
{record.needConvert ? {record.convertedAmount} : null} {record.notes ? {record.notes} : null} , rowExpandable: record => record.notes || record.needConvert, }} dataSource={dataAndPagination.data} pagination={dataAndPagination.pagination} loading={queryLoading} onChange={handleTableChange} /> ); } ================================================ FILE: bookkeeping-user-fe/src/pages/expenses/TotalAlert.jsx ================================================ import {useMemo} from "react"; import { useSelector } from 'umi'; import AlertTotalSearch from "@/components/AlertTotalSearch"; import {useResponseData} from "@/utils/hooks"; import t from '@/utils/translate'; export default () => { const { queryResponse } = useSelector(state => state.expenses); const [ responseData ] = useResponseData(queryResponse); const total = useMemo(() => { if (responseData.result) { return responseData.total; } else { return 0; } }, [responseData]); return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/pages/expenses/index.jsx ================================================ import { Space } from 'antd'; import Breadcrumb from '@/components/Breadcrumb'; import {spaceVProp} from "@/utils/var"; import OperationBar from './OperationBar'; import TotalAlert from './TotalAlert'; import RecordTable from './RecordTable'; import t from "@/utils/translate"; export default () => { return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/expenses/model.js ================================================ import modelExtend from 'dva-model-extend'; import { model } from '@/utils/model'; import { query } from '@/services/expense'; export default modelExtend(model, { namespace: 'expenses', state: { queryResponse: undefined, }, effects: { *query({ payload }, { call, put }) { const response = yield call(query, payload); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/expenses') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/flows/OperationBar.jsx ================================================ import { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'umi'; import { Form, Row, Col, Button, Select, Space } from 'antd'; import { filterFormProp } from "@/utils/var"; import {searchHandler} from "@/utils/util"; import {useResponseSelectData, useCategoryTreeSelectData} from "@/utils/hooks"; import FormItemDateRange from "@/components/FormItemDateRange"; import FormItemAmountRange from "@/components/FormItemAmountRange"; import FormItemAccounts from "@/components/FormItemAccounts"; import FormItemPayees from "@/components/FormItemPayees"; import FormItemTags from "@/components/FormItemTags"; import FormItemStatus from "@/components/FormItemStatus"; import FormItemDescriptionSearch from "@/components/FormItemDescriptionSearch"; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const loading = useSelector(state => state.loading.effects['flows/query']); useEffect(() => { if (!accountResponse) dispatch({ type: 'account/fetchEnable' }); if (!payeeResponse) dispatch({ type: 'payee/fetchEnable' }); if (!tagResponse) dispatch({ type: 'tag/fetchEnable' }); }, []); const { getEnableResponse : accountResponse } = useSelector(state => state.account); const [accounts] = useResponseSelectData(accountResponse); const { getEnableResponse : payeeResponse } = useSelector(state => state.payee); const [payees] = useResponseSelectData(payeeResponse); const { getEnableResponse : tagResponse } = useSelector(state => state.tag); const [tags] = useCategoryTreeSelectData(tagResponse); const [dateRadioValue, setDateRadioValue] = useState(); function resetHandler() { setDateRadioValue(); form.resetFields(); } return (
); }; ================================================ FILE: bookkeeping-user-fe/src/pages/flows/RecordTable.jsx ================================================ import { useSelector } from 'umi'; import {useResultPaginationAndData} from '@/utils/hooks'; import {handleTableChange} from "@/utils/util"; import FlowRecordTable from '@/components/FlowRecordTable'; export default () => { const queryLoading = useSelector(state => state.loading.effects['flows/query']); const { queryResponse } = useSelector(state => state.flows); const [ dataAndPagination ] = useResultPaginationAndData(queryResponse); return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/flows/TotalAlert.jsx ================================================ import {useMemo} from "react"; import { useSelector } from 'umi'; import { Space } from 'antd'; import {useResponseData} from "@/utils/hooks"; import AlertTotalSearch from "@/components/AlertTotalSearch"; import t from '@/utils/translate'; export default () => { const { queryResponse } = useSelector(state => state.flows); const [ responseData ] = useResponseData(queryResponse); const total = useMemo(() => { if (responseData.result) { return [responseData.expense, responseData.income, responseData.surplus] } else { return [0, 0, 0] } }, [responseData]); return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/pages/flows/index.jsx ================================================ import { Space } from 'antd'; import {spaceVProp} from '@/utils/var'; import Breadcrumb from '@/components/Breadcrumb'; import OperationBar from './OperationBar'; import TotalAlert from './TotalAlert'; import RecordTable from './RecordTable'; import t from '@/utils/translate'; export default () => { return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/flows/model.js ================================================ import modelExtend from 'dva-model-extend'; import { model } from '@/utils/model'; import { query } from '@/services/flow'; export default modelExtend(model, { namespace: 'flows', state: { queryResponse: undefined, }, effects: { *query({ payload }, { call, put }) { const response = yield call(query, payload); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/flows') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/groups/OperationBar.jsx ================================================ import { useDispatch } from 'umi'; import { Button } from 'antd'; import OperationModal from './OperationModal'; import t from "@/utils/translate"; import request from "@/utils/request"; export default () => { const dispatch = useDispatch(); const addHandler = () => { dispatch({ type: 'modal/show', payload: {component: OperationModal, type: 1, currentItem: null }}); } return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/groups/OperationModal.jsx ================================================ import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'umi'; import { Form, Input, Select} from 'antd'; import { create, update } from '@/services/group'; import {getNull} from "@/utils/util"; import t from "@/utils/translate"; import FormModal from "@/components/FormModal"; import {nameRules, notesRules, requiredRules} from "@/utils/rules"; import {useCurrencyResponseSelectData} from "@/utils/hooks"; import styles from './index.less'; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { visible, type, currentItem } = useSelector(state => state.modal); const [initialValues, setInitialValues] = useState(currentItem) useEffect(() => { if (!visible) return; if (type === 1) { setInitialValues({ ...getNull(form.getFieldsValue()), ...{ defaultCurrencyCode: 'CNY' } }); } else { setInitialValues({...getNull(form.getFieldsValue()), ...currentItem}); } }, [visible]); useEffect(() => { if (!currencyResponse) dispatch({ type: 'currency/fetchAll' }); }, []); const { getAllResponse : currencyResponse } = useSelector(state => state.currency); const [currencyList] = useCurrencyResponseSelectData(currencyResponse); function successHandler() { dispatch({ type: 'groups/query' }); dispatch({ type: 'session/fetchSession' }) } return (
{ if (defaultGroup && record.id === defaultGroup.id) return 'table-color-default'; }} size="middle" columns={columns} dataSource={dataAndPagination.data} loading={queryLoading} pagination={dataAndPagination.pagination} onChange={handleTableChange} /> ); } ================================================ FILE: bookkeeping-user-fe/src/pages/groups/index.jsx ================================================ import {Space} from "antd"; import {spaceVProp} from "@/utils/var"; import Breadcrumb from "@/components/Breadcrumb"; import OperationBar from './OperationBar'; import RecordTable from './RecordTable'; import t from "@/utils/translate"; export default () => { return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/groups/index.less ================================================ .form2 { :global { .ant-form-item-label { width: 45px; } } } ================================================ FILE: bookkeeping-user-fe/src/pages/groups/model.js ================================================ import modelExtend from 'dva-model-extend'; import { model } from '@/utils/model'; import { query } from '@/services/group'; export default modelExtend(model, { namespace: 'groups', state: { queryResponse: undefined, }, effects: { *query({ payload }, { call, put }) { const response = yield call(query, payload); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/groups') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/incomes/OperationBar.jsx ================================================ import { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'umi'; import {Form, Row, Col, Button, Space} from 'antd'; import { useResponseSelectData, useCategoryTreeSelectData } from "@/utils/hooks"; import { filterFormProp } from "@/utils/var"; import { searchHandlerWithCategory } from "@/utils/util"; import {showFlowModal} from '@/utils/flow'; import FormItemDateRange from "@/components/FormItemDateRange"; import FormItemAmountRange from "@/components/FormItemAmountRange"; import FormItemAccounts from "@/components/FormItemAccounts"; import FormItemPayees from "@/components/FormItemPayees"; import FormItemTags from "@/components/FormItemTags"; import FormItemCategories from "@/components/FormItemCategories"; import FormItemDescriptionSearch from "@/components/FormItemDescriptionSearch"; import FormItemStatus from "@/components/FormItemStatus"; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const loading = useSelector(state => state.loading.effects['incomes/query']); useEffect(() => { if (!tagResponse) dispatch({ type: 'tag/fetchIncomeable' }); if (!payeeResponse) dispatch({ type: 'payee/fetchIncomeable' }); if (!accountResponse) dispatch({ type: 'account/fetchIncomeable' }); if (!querySimpleResponse) dispatch({ type: 'incomeCategory/fetchSimple' }); }, []); const { getIncomeableResponse : tagResponse } = useSelector(state => state.tag); const [tags] = useCategoryTreeSelectData(tagResponse); const { getIncomeableResponse : payeeResponse } = useSelector(state => state.payee); const [payees] = useResponseSelectData(payeeResponse); const { getIncomeableResponse : accountResponse } = useSelector(state => state.account); const [accounts] = useResponseSelectData(accountResponse); const { querySimpleResponse } = useSelector(state => state.incomeCategory); const [categories] = useCategoryTreeSelectData(querySimpleResponse); function addHandler() { showFlowModal(2, 1, {}); } const [dateRadioValue, setDateRadioValue] = useState(); function resetHandler() { setDateRadioValue(); form.resetFields(); } return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/pages/incomes/RecordTable.jsx ================================================ import { history, useDispatch, useSelector } from 'umi'; import {Table, Space, message, Modal, Descriptions, Dropdown, Menu, Tooltip} from 'antd'; import {DownOutlined} from "@ant-design/icons"; import { remove, confirm } from '@/services/flow'; import {useDescriptionEnable, useImageEnable, useResultPaginationAndData, useTimeFormat} from '@/utils/hooks'; import {handleTableChange } from "@/utils/util"; import {showFlowModal, imageHandler} from '@/utils/flow'; import { tableProp } from '@/utils/var'; import {createTimeCol, amountCol, statusCol} from '@/utils/columns'; import FlowTagDisplay from "@/components/FlowTagDisplay"; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); const { queryResponse } = useSelector(state => state.incomes); const queryLoading = useSelector(state => state.loading.effects['incomes/query']); const [ dataAndPagination ] = useResultPaginationAndData(queryResponse); const imageEnable = useImageEnable(); const descriptionEnable = useDescriptionEnable(); const timeFormat = useTimeFormat(); let columns = []; if (descriptionEnable) { columns.push({ title: t('description'), dataIndex: 'description', }); } columns = columns.concat( amountCol(), createTimeCol(timeFormat), { title: t('category'), dataIndex: 'categoryName', }, { title: t('account'), dataIndex: 'accountName', }, { title: t('flow.tag'), dataIndex: 'tags', render: (tags, record) => <>{tags.map(i => )} }, { title: t('flow.payee'), dataIndex: 'payee', sorter: true, render: payee => <>{payee ? payee.name : null} }, statusCol(), { title: t('operation'), key: 'operation', width: 100, align: 'center', render: (_, record) => { return ( copyHandler(record)}>{t('copy')} {imageEnable ? imageHandler(record)}>{t('image')} : null} updateHandler(record)}>{t('update')} confirmHandler(record)}>{t('confirm')} refundHandler(record)}>{t('refund')} deleteHandler(record)}> { record.status == 3 ? {t('delete')}: t('delete') } }> {t('more')} ) } } ); function copyHandler(record) { showFlowModal(2, 3, {...record}); } const updateHandler = (record) => { showFlowModal(2, 2, {...record}); } const messageDeleteConfirm = t('delete.confirm', { name: '' }); const messageDeleteConfirmBalance = t('delete.confirm.balance'); const messageOperationSuccess = t('operation.success'); function deleteHandler(record) { Modal.confirm({ title: record.status === 2 ? messageDeleteConfirm : messageDeleteConfirmBalance, onOk: async () => { const response = await remove(record); if (response && response.success) { message.success(messageOperationSuccess); dispatch({ type: 'incomes/query', payload: history.location.query }); } } }); } function refundHandler(record) { showFlowModal(2, 4, {...record}); } const messageConfirmOperation = t('confirm.operation'); function confirmHandler(record) { Modal.confirm({ title: messageConfirmOperation, onOk: async () => { const response = await confirm(record); if (response && response.success) { message.success(messageOperationSuccess); dispatch({ type: 'incomes/query', payload: history.location.query }); } } }); } return (
{record.needConvert ? {record.convertedAmount} : null} {record.notes ? {record.notes} : null} , rowExpandable: record => record.notes || record.needConvert, }} dataSource={dataAndPagination.data} pagination={dataAndPagination.pagination} loading={queryLoading} onChange={handleTableChange} /> ); } ================================================ FILE: bookkeeping-user-fe/src/pages/incomes/TotalAlert.jsx ================================================ import {useMemo} from "react"; import { useSelector } from 'umi'; import AlertTotalSearch from "@/components/AlertTotalSearch"; import {useResponseData} from "@/utils/hooks"; import t from '@/utils/translate'; export default () => { const { queryResponse } = useSelector(state => state.incomes); const [ responseData ] = useResponseData(queryResponse); const total = useMemo(() => { if (responseData.result) { return responseData.total; } else { return 0; } }, [responseData]); return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/pages/incomes/index.jsx ================================================ import { Space } from 'antd'; import {spaceVProp} from "@/utils/var"; import Breadcrumb from '@/components/Breadcrumb'; import OperationBar from './OperationBar'; import RecordTable from './RecordTable'; import TotalAlert from "./TotalAlert"; import t from '@/utils/translate'; export default () => { return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/incomes/model.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { query } from '@/services/income'; export default modelExtend(model, { namespace: 'incomes', state: { queryResponse: undefined, }, effects: { *query({ payload }, { call, put }) { const response = yield call(query, payload); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/incomes') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/index.jsx ================================================ import { Redirect } from 'umi' export default function() { return } ================================================ FILE: bookkeeping-user-fe/src/pages/items/OperationBar.jsx ================================================ import { useDispatch } from 'umi'; import { Button } from 'antd'; import OperationModal from './OperationModal'; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); const addHandler = () => { dispatch({ type: 'modal/show', payload: {component: OperationModal, type: 1, currentItem: null }}); } return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/items/OperationModal.jsx ================================================ import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'umi'; import {Modal, Form, Input, message, DatePicker, Select} from 'antd'; import { create, update } from '@/services/item'; import {getNull, getTimeEnable, getTimeFormat, validateForm} from "@/utils/util"; import t from "@/utils/translate"; import FormModal from "@/components/FormModal"; import {nameRules, notesRules, timeRequiredRules, timeRangeRequiredRules, requiredRules} from "@/utils/rules"; import styles from './index.less'; import moment from "moment"; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const { visible, type, currentItem } = useSelector(state => state.modal); const [initialValues, setInitialValues] = useState(currentItem); useEffect(() => { if (!visible) return; if (type === 1) { setInitialValues({ ...getNull(form.getFieldsValue()), 'type': 1, 'repeatType': 2, 'interval': 1 }); setRepeatFlag(1); } else { let currentItemCopy = JSON.parse(JSON.stringify(currentItem)); currentItemCopy.startDate = moment(currentItemCopy.startDate); currentItemCopy.endDate = moment(currentItemCopy.endDate); currentItemCopy.dateRange = [currentItemCopy.startDate, currentItemCopy.endDate]; const repeatFlag = currentItemCopy.repeatType == 0 ? 1 : 2; setInitialValues({ ...getNull(form.getFieldsValue()), ...currentItemCopy, 'type': repeatFlag, }); setRepeatFlag(repeatFlag); } }, [visible, type, currentItem]); const [repeatFlag, setRepeatFlag] = useState(1); function successHandler() { dispatch({ type: 'items/query' }); } function parseValues(values) { if (values.startDate) values.startDate = values.startDate.valueOf(); if (values.dateRange && values.dateRange[0]) { values.startDate = values.dateRange[0].valueOf(); } if (values.dateRange && values.dateRange[1]) { values.endDate = values.dateRange[1].valueOf(); } delete values.dateRange; } return (
{repeatFlag == 1 ? :
}
); } ================================================ FILE: bookkeeping-user-fe/src/pages/items/RecordTable.jsx ================================================ import { useEffect } from 'react'; import {useDispatch, useIntl, useSelector} from 'umi'; import { Button, message, Table, Space, Modal } from "antd"; import { remove, run, recall } from '@/services/item'; import {tableProp} from "@/utils/var"; import {handleTableChange} from "@/utils/util"; import {usePaginationAndData} from "@/utils/hooks"; import OperationModal from "./OperationModal"; import t from "@/utils/translate"; import moment from "moment"; export default () => { const dispatch = useDispatch(); const { queryResponse } = useSelector(state => state.items); const queryLoading = useSelector(state => state.loading.effects['items/query']); const [dataAndPagination] = usePaginationAndData(queryResponse); const updateHandler = (record) => { dispatch({ type: 'modal/show', payload: {component: OperationModal, type: 2, currentItem: record } }); } const messageOperationSuccess = t('operation.success'); const runHandler = async (record) => { const response = await run(record.id); if (response) { if (response.success) { message.success(messageOperationSuccess); dispatch({ type: 'items/query' }); } } } const recallHandler = async (record) => { const response = await recall(record.id); if (response) { if (response.success) { message.success(messageOperationSuccess); dispatch({ type: 'items/query' }); } } } const intl = useIntl(); const deleteHandler = async (record) => { Modal.confirm({ title: intl.formatMessage({id:'delete.confirm'}, { name: record.title }), onOk: async () => { const response = await remove(record.id); if (response && response.success) { message.success(messageOperationSuccess); dispatch({ type: 'items/query' }); } } }); } const columns = [ { title: '标题', dataIndex: 'title', }, { title: '开始日期', dataIndex: 'startDate', sorter: true, width: 85, render: time => moment(time).format('YYYY-MM-DD') }, { title: '结束日期', dataIndex: 'endDate', sorter: true, width: 85, render: time => time ? moment(time).format('YYYY-MM-DD') : '' }, { title: '执行频率', dataIndex: 'repeatDescription', width: 95, }, { title: '下次执行', dataIndex: 'nextDate', sorter: true, width: 85, render: time => time ? moment(time).format('YYYY-MM-DD') : '' }, { title: '倒计时(天)', dataIndex: 'countDown', width: 85, }, { title: '总次数', dataIndex: 'totalCount', sorter: true, width: 65, }, { title: '已执行次数', dataIndex: 'runCount', sorter: true, width: 85, }, { title: '剩余次数', dataIndex: 'remainCount', width: 65, }, { title: t('notes'), dataIndex: 'notes', }, { title: t('operation'), key: 'operation', width: 220, align: 'center', render: (_, record) => { return ( ) } } ]; return (
); } ================================================ FILE: bookkeeping-user-fe/src/pages/items/index.jsx ================================================ import {Space} from "antd"; import {spaceVProp} from "@/utils/var"; import Breadcrumb from "@/components/Breadcrumb"; import OperationBar from './OperationBar'; import RecordTable from './RecordTable'; import t from "@/utils/translate"; export default () => { return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/items/index.less ================================================ .form2 { :global { .ant-form-item-label { width: 90px; } } } ================================================ FILE: bookkeeping-user-fe/src/pages/items/model.js ================================================ import modelExtend from 'dva-model-extend'; import { model } from '@/utils/model'; import { query } from '@/services/item'; export default modelExtend(model, { namespace: 'items', state: { queryResponse: undefined, }, effects: { *query({ payload }, { call, put }) { const response = yield call(query, payload); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/items') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/register/index.jsx ================================================ import { useEffect } from 'react'; import { Form, Input, Button, message } from 'antd'; import { Link, useDispatch, useSelector } from 'umi'; import { UserOutlined, LockOutlined, VerifiedOutlined, MailOutlined } from '@ant-design/icons'; import { userNameRules, passwordRules, emailRules, requiredRules } from '@/utils/rules'; import t from '@/utils/translate'; import styles from './index.less'; export default () => { const dispatch = useDispatch(); const { registerResponse } = useSelector(state => state.userRegister); const submitting = useSelector(state => state.loading.effects['userRegister/submit']); const messageRegisterSuccess = t('register.success'); useEffect(() => { if (registerResponse && registerResponse.success) { message.success(messageRegisterSuccess); } return () => { dispatch({ type: 'userRegister/clearRegisterResponse' }); } }, [registerResponse]); const handleSubmit = (values) => { dispatch({ type: 'userRegister/submit', payload: { ...values }, }); }; return (
} placeholder={t('placeholder.userName')} /> } type="password" placeholder={t('placeholder.password')} /> } placeholder={t('placeholder.invite.code')} /> } type="eamil" placeholder={t('placeholder.email')} />
{t('register.signin')}
); }; ================================================ FILE: bookkeeping-user-fe/src/pages/register/index.less ================================================ @import '~antd/es/style/themes/default.less'; .main { margin: 0 auto; width: 368px; @media screen and (max-width: @screen-sm) { width: 95%; } } .form_row { margin-bottom: 15px; } ================================================ FILE: bookkeeping-user-fe/src/pages/register/model.js ================================================ import { register } from '@/services/user'; export default { namespace: 'userRegister', state: { registerResponse: undefined, }, effects: { *submit({ payload }, { call, put }) { const response = yield call(register, payload); yield put({ type: 'registerHandler', payload: response, }); } }, reducers: { registerHandler(state, { payload }) { return { ...state, registerResponse: payload }; }, clearRegisterResponse(state) { return { ...state, registerResponse: undefined }; } } } ================================================ FILE: bookkeeping-user-fe/src/pages/reports/asset-debt-trend/Chart.jsx ================================================ import {useSelector} from "umi"; import {Card} from "antd"; import Line from "@/components/charts/Line"; import {useResponseData} from "@/utils/hooks"; import t from "@/utils/translate"; export default () => { const loading = useSelector(state => state.loading.effects['assetDebtTrendReports/query']); const { queryResponse } = useSelector(state => state.assetDebtTrendReports); const [data] = useResponseData(queryResponse); const cols = { month: { range: [0, 1] } }; return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/reports/asset-debt-trend/OperationBar.jsx ================================================ import {Form, Row, Col, Button, Space} from 'antd'; import { filterFormProp } from "@/utils/var"; import { searchHandler } from "@/utils/util"; import FormItemDateRange from "@/components/FormItemDateRange"; import t from '@/utils/translate'; import {useState} from "react"; import {useSelector} from "umi"; export default () => { const [form] = Form.useForm(); const loading = useSelector(state => state.loading.effects['assetDebtTrendReports/query']); const [dateRadioValue, setDateRadioValue] = useState(8); function resetHandler() { setDateRadioValue(); form.resetFields(); } return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/pages/reports/asset-debt-trend/index.jsx ================================================ import { Space } from "antd"; import {spaceVProp} from "@/utils/var"; import Breadcrumb from "@/components/Breadcrumb"; import OperationBar from './OperationBar'; import Chart from './Chart'; import t from "@/utils/translate"; export default () => { return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/reports/asset-debt-trend/model.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { getAssetDebtTrend } from "@/services/report"; export default modelExtend(model, { namespace: 'assetDebtTrendReports', state: { queryResponse: undefined, }, effects: { *query({ payload }, { call, put }) { const response = yield call(getAssetDebtTrend, payload); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/reports/asset-debt-trend') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/reports/balance-sheet/AssetCategory.jsx ================================================ import {useEffect, useState} from "react"; import {useDispatch, useSelector} from "umi"; import { Card } from "antd"; import {useResponseData} from "@/utils/hooks"; import Pie from '@/components/charts/Pie'; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch({ type: 'balanceSheetReports/getAsset' }); }, []); const loading = useSelector(state => state.loading.effects['balanceSheetReports/getAsset']); const { getAssetResponse } = useSelector(state => state.balanceSheetReports); const [data] = useResponseData(getAssetResponse); return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/reports/balance-sheet/DebtCategory.jsx ================================================ import {useEffect, useState} from "react"; import {useDispatch, useSelector} from "umi"; import { Card } from "antd"; import {useResponseData} from "@/utils/hooks"; import Pie from '@/components/charts/Pie'; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch({ type: 'balanceSheetReports/getDebt' }); }, []); const loading = useSelector(state => state.loading.effects['balanceSheetReports/getDebt']); const { getDebtResponse } = useSelector(state => state.balanceSheetReports); const [data] = useResponseData(getDebtResponse); return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/reports/balance-sheet/index.jsx ================================================ import {Col, Row, Space} from "antd"; import {spaceVProp} from "@/utils/var"; import Breadcrumb from "@/components/Breadcrumb"; import AssetCategory from './AssetCategory' import DebtCategory from './DebtCategory' import t from "@/utils/translate"; export default () => { return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/reports/balance-sheet/model.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { getAsset, getDebt } from '@/services/report' import {query as queryCategory} from "@/services/expense-category"; export default modelExtend(model, { namespace: 'balanceSheetReports', state: { getAssetResponse: undefined, getDebtResponse: undefined, }, effects: { *getAsset(_, { call, put }) { const response = yield call(getAsset); yield put({ type: 'updateState', payload: { getAssetResponse: response }, }); }, *getDebt(_, { call, put }) { const response = yield call(getDebt); yield put({ type: 'updateState', payload: { getDebtResponse: response }, }); }, }, }) ================================================ FILE: bookkeeping-user-fe/src/pages/reports/expense-category/CategoryPie.jsx ================================================ import {useEffect, useState} from "react"; import {useDispatch, useSelector} from "umi"; import { Card } from "antd"; import {radioValueToTimeRange} from "@/utils/util"; import {useResponseData} from "@/utils/hooks"; import Pie from '@/components/charts/Pie2'; import t from '@/utils/translate'; export default () => { const loading = useSelector(state => state.loading.effects['expenseCategoryReports/query']); const { queryResponse } = useSelector(state => state.expenseCategoryReports); const [data] = useResponseData(queryResponse); return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/reports/expense-category/OperationBar.jsx ================================================ import { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'umi'; import {Form, Row, Col, Button, Space, TreeSelect} from 'antd'; import {useCategoryTreeSelectData, useResponseData, useResponseSelectData} from "@/utils/hooks"; import { filterFormProp } from "@/utils/var"; import { searchHandler } from "@/utils/util"; import FormItemDateRange from "@/components/FormItemDateRange"; import FormItemAccounts from "@/components/FormItemAccounts"; import FormItemCategoryReport from "@/components/FormItemCategoryReport"; import FormItemPayees from "@/components/FormItemPayees"; import FormItemTags from "@/components/FormItemTags"; import FormItemDescriptionSearch from "@/components/FormItemDescriptionSearch"; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const loading = useSelector(state => state.loading.effects['expenseCategoryReports/query']); useEffect(() => { if (!tagResponse) dispatch({ type: 'tag/fetchExpenseable' }); if (!payeeResponse) dispatch({ type: 'payee/fetchExpenseable' }); if (!accountResponse) dispatch({ type: 'account/fetchExpenseable' }); if (!querySimpleResponse) dispatch({ type: 'expenseCategory/fetchSimple' }); }, []); const { getExpenseableResponse : tagResponse } = useSelector(state => state.tag); const [tags] = useCategoryTreeSelectData(tagResponse); const { getExpenseableResponse : payeeResponse } = useSelector(state => state.payee); const [payees] = useResponseSelectData(payeeResponse); const { getExpenseableResponse : accountResponse } = useSelector(state => state.account); const [accounts] = useResponseSelectData(accountResponse); const { querySimpleResponse } = useSelector(state => state.expenseCategory); const [categories] = useCategoryTreeSelectData(querySimpleResponse); const [dateRadioValue, setDateRadioValue] = useState(8); function resetHandler() { setDateRadioValue(); form.resetFields(); } return (
); }; ================================================ FILE: bookkeeping-user-fe/src/pages/reports/expense-category/index.jsx ================================================ import { Space } from "antd"; import {spaceVProp} from "@/utils/var"; import Breadcrumb from "@/components/Breadcrumb"; import OperationBar from './OperationBar'; import CategoryPie from "./CategoryPie"; import t from "@/utils/translate"; export default () => { return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/reports/expense-category/model.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { query as queryCategory } from '@/services/expense-category'; import { getExpenseCategory as getCategoryReport } from '@/services/report'; export default modelExtend(model, { namespace: 'expenseCategoryReports', state: { getCategoryResponse: undefined, queryResponse: undefined, }, effects: { *getCategory({ payload }, { call, put }) { const response = yield call(queryCategory, payload); yield put({ type: 'updateState', payload: { getCategoryResponse: response }, }); }, *query({ payload }, { call, put }) { const response = yield call(getCategoryReport, payload); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/reports/expense-category') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/reports/expense-income-trend/Chart.jsx ================================================ import {useSelector} from "umi"; import {Card} from "antd"; import Line from '@/components/charts/Line'; import {useResponseData} from "@/utils/hooks"; import t from "@/utils/translate"; export default () => { const loading = useSelector(state => state.loading.effects['expenseIncomeTrendReports/query']); const { queryResponse } = useSelector(state => state.expenseIncomeTrendReports); const [data] = useResponseData(queryResponse); const cols = { month: { range: [0, 1] } }; return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/reports/expense-income-trend/OperationBar.jsx ================================================ import { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'umi'; import {Form, Row, Col, Button, Space, Divider} from 'antd'; import {useCategoryTreeSelectData, useResponseData, useResponseSelectData} from "@/utils/hooks"; import { filterFormProp } from "@/utils/var"; import {getNull, radioValueToTimeRange, searchHandler} from "@/utils/util"; import FormItemDateRangeWithBreak from "@/components/FormItemDateRangeWithBreak"; import FormItemAccounts from "@/components/FormItemAccounts"; import FormItemTags from "@/components/FormItemTags"; import FormItemPayees from "@/components/FormItemPayees"; import FormItemCategories from "@/components/FormItemCategories"; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const loading = useSelector(state => state.loading.effects['expenseIncomeTrendReports/query']); useEffect(() => { if (!expenseTagResponse) dispatch({ type: 'tag/fetchExpenseable' }); if (!expensePayeeResponse) dispatch({ type: 'payee/fetchExpenseable' }); if (!expenseAccountResponse) dispatch({ type: 'account/fetchExpenseable' }); if (!expenseCategoryResponse) dispatch({ type: 'incomeCategory/fetchSimple' }); if (!incomeTagResponse) dispatch({ type: 'tag/fetchIncomeable' }); if (!incomePayeeResponse) dispatch({ type: 'payee/fetchIncomeable' }); if (!incomeAccountResponse) dispatch({ type: 'account/fetchIncomeable' }); if (!incomeCategoryResponse) dispatch({ type: 'expenseCategory/fetchSimple' }); }, []); const { getExpenseableResponse : expenseTagResponse } = useSelector(state => state.tag); const [expenseTags] = useCategoryTreeSelectData(expenseTagResponse); const { getExpenseableResponse : expensePayeeResponse } = useSelector(state => state.payee); const [expensePayees] = useResponseSelectData(expensePayeeResponse); const { getExpenseableResponse : expenseAccountResponse } = useSelector(state => state.account); const [expenseAccounts] = useResponseSelectData(expenseAccountResponse); const { querySimpleResponse: expenseCategoryResponse } = useSelector(state => state.expenseCategory); const [expenseCategories] = useCategoryTreeSelectData(expenseCategoryResponse); const { getIncomeableResponse : incomeTagResponse } = useSelector(state => state.tag); const [incomeTags] = useCategoryTreeSelectData(incomeTagResponse); const { getIncomeableResponse : incomePayeeResponse } = useSelector(state => state.payee); const [incomePayees] = useResponseSelectData(incomePayeeResponse); const { getIncomeableResponse : incomeAccountResponse } = useSelector(state => state.account); const [incomeAccounts] = useResponseSelectData(incomeAccountResponse); const { querySimpleResponse: incomeCategoryResponse } = useSelector(state => state.incomeCategory); const [incomeCategories] = useCategoryTreeSelectData(incomeCategoryResponse); const [dateRadioValue, setDateRadioValue] = useState(8); function resetHandler() { setDateRadioValue(8); form.setFieldsValue({...initialValues}); } const [initialValues, setInitialValues] = useState({}); useEffect(() => { setInitialValues({ 'createTimeRange': radioValueToTimeRange(dateRadioValue), 'breakType': "month" }); searchHandler(form); }, []); return ( {t('report.expense.condition')} {t('report.income.condition')} ); }; ================================================ FILE: bookkeeping-user-fe/src/pages/reports/expense-income-trend/index.jsx ================================================ import { Space } from "antd"; import {spaceVProp} from "@/utils/var"; import Breadcrumb from "@/components/Breadcrumb"; import OperationBar from './OperationBar'; import Chart from './Chart'; import t from "@/utils/translate"; export default () => { return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/reports/expense-income-trend/model.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { getExpenseIncomeTrend } from "@/services/report"; export default modelExtend(model, { namespace: 'expenseIncomeTrendReports', state: { queryResponse: undefined, }, effects: { *query({ payload }, { call, put }) { const response = yield call(getExpenseIncomeTrend, payload); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/reports/expense-income-trend') { console.log(query); if (query && query.breakType) { dispatch({ type: 'query', payload: query }); } } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/reports/expense-tag/OperationBar.jsx ================================================ import { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'umi'; import {Form, Row, Col, Button, Space} from 'antd'; import {useCategoryTreeSelectData, useResponseData, useResponseSelectData} from "@/utils/hooks"; import { filterFormProp } from "@/utils/var"; import { searchHandlerWithCategory } from "@/utils/util"; import FormItemDateRange from "@/components/FormItemDateRange"; import FormItemAccounts from "@/components/FormItemAccounts"; import FormItemCategories from "@/components/FormItemCategories"; import FormItemDescriptionSearch from "@/components/FormItemDescriptionSearch"; import FormItemPayees from "@/components/FormItemPayees"; import FormItemTagReport from "@/components/FormItemTagReport"; import t from '@/utils/translate'; import Pie from "@/components/charts/Pie2"; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const loading = useSelector(state => state.loading.effects['expenseTagReports/query']); useEffect(() => { if (!tagResponse) dispatch({ type: 'tag/fetchExpenseable' }); if (!payeeResponse) dispatch({ type: 'payee/fetchExpenseable' }); if (!accountResponse) dispatch({ type: 'account/fetchExpenseable' }); if (!querySimpleResponse) dispatch({ type: 'expenseCategory/fetchSimple' }); }, []); const { getExpenseableResponse : tagResponse } = useSelector(state => state.tag); const [tags] = useCategoryTreeSelectData(tagResponse); const { getExpenseableResponse : payeeResponse } = useSelector(state => state.payee); const [payees] = useResponseSelectData(payeeResponse); const { getExpenseableResponse : accountResponse } = useSelector(state => state.account); const [accounts] = useResponseSelectData(accountResponse); const { querySimpleResponse } = useSelector(state => state.expenseCategory); const [categories] = useCategoryTreeSelectData(querySimpleResponse); const [dateRadioValue, setDateRadioValue] = useState(8); function resetHandler() { setDateRadioValue(); form.resetFields(); } return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/pages/reports/expense-tag/TagPie.jsx ================================================ import {useEffect, useState} from "react"; import {useDispatch, useSelector} from "umi"; import { Card } from "antd"; import {radioValueToTimeRange} from "@/utils/util"; import {useResponseData} from "@/utils/hooks"; import Pie from '@/components/charts/Pie2'; import t from '@/utils/translate'; export default () => { const loading = useSelector(state => state.loading.effects['expenseTagReports/query']); const { queryResponse } = useSelector(state => state.expenseTagReports); const [data] = useResponseData(queryResponse); return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/reports/expense-tag/index.jsx ================================================ import { Space } from "antd"; import {spaceVProp} from "@/utils/var"; import Breadcrumb from "@/components/Breadcrumb"; import OperationBar from './OperationBar'; import TagPie from "./TagPie"; import t from "@/utils/translate"; export default () => { return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/reports/expense-tag/model.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { getExpenseTag as getTagReport } from '@/services/report'; export default modelExtend(model, { namespace: 'expenseTagReports', state: { queryResponse: undefined, }, effects: { *query({ payload }, { call, put }) { const response = yield call(getTagReport, payload); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/reports/expense-tag') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/reports/income-category/CategoryPie.jsx ================================================ import {useEffect, useState} from "react"; import {useDispatch, useSelector} from "umi"; import { Card } from "antd"; import {radioValueToTimeRange} from "@/utils/util"; import {useResponseData} from "@/utils/hooks"; import Pie from '@/components/charts/Pie2'; import t from '@/utils/translate'; export default () => { const loading = useSelector(state => state.loading.effects['incomeCategoryReports/query']); const { queryResponse } = useSelector(state => state.incomeCategoryReports); const [data] = useResponseData(queryResponse); return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/reports/income-category/OperationBar.jsx ================================================ import { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'umi'; import {Form, Row, Col, Button, Select, Space, Checkbox, Input} from 'antd'; import {useCategoryTreeSelectData, useResponseData, useResponseSelectData} from "@/utils/hooks"; import { filterFormProp } from "@/utils/var"; import { searchHandler } from "@/utils/util"; import FormItemDateRange from "@/components/FormItemDateRange"; import FormItemAccounts from "@/components/FormItemAccounts"; import FormItemCategoryReport from "@/components/FormItemCategoryReport"; import FormItemDescriptionSearch from "@/components/FormItemDescriptionSearch"; import FormItemPayees from "@/components/FormItemPayees"; import FormItemTags from "@/components/FormItemTags"; import t from '@/utils/translate'; import Pie from "@/components/charts/Pie2"; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const loading = useSelector(state => state.loading.effects['incomeCategoryReports/query']); useEffect(() => { if (!tagResponse) dispatch({ type: 'tag/fetchIncomeable' }); if (!payeeResponse) dispatch({ type: 'payee/fetchIncomeable' }); if (!accountResponse) dispatch({ type: 'account/fetchIncomeable' }); if (!querySimpleResponse) dispatch({ type: 'incomeCategory/fetchSimple' }); }, []); const { getIncomeableResponse : tagResponse } = useSelector(state => state.tag); const [tags] = useCategoryTreeSelectData(tagResponse); const { getIncomeableResponse : payeeResponse } = useSelector(state => state.payee); const [payees] = useResponseSelectData(payeeResponse); const { getIncomeableResponse : accountResponse } = useSelector(state => state.account); const [accounts] = useResponseSelectData(accountResponse); const { querySimpleResponse } = useSelector(state => state.incomeCategory); const [categories] = useCategoryTreeSelectData(querySimpleResponse); const [dateRadioValue, setDateRadioValue] = useState(8); function resetHandler() { setDateRadioValue(); form.resetFields(); } return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/pages/reports/income-category/index.jsx ================================================ import {Col, Row, Space} from "antd"; import {spaceVProp} from "@/utils/var"; import Breadcrumb from "@/components/Breadcrumb"; import OperationBar from './OperationBar'; import CategoryPie from "./CategoryPie"; import t from "@/utils/translate"; export default () => { return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/reports/income-category/model.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { query as queryCategory } from '@/services/income-category'; import { getIncomeCategory as getCategoryReport } from '@/services/report'; export default modelExtend(model, { namespace: 'incomeCategoryReports', state: { getCategoryResponse: undefined, queryResponse: undefined, }, effects: { *getCategory({ payload }, { call, put }) { const response = yield call(queryCategory, payload); yield put({ type: 'updateState', payload: { getCategoryResponse: response }, }); }, *query({ payload }, { call, put }) { const response = yield call(getCategoryReport, payload); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/reports/income-category') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/reports/income-tag/OperationBar.jsx ================================================ import { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'umi'; import {Form, Row, Col, Button, Space} from 'antd'; import {useCategoryTreeSelectData, useResponseData, useResponseSelectData} from "@/utils/hooks"; import { filterFormProp } from "@/utils/var"; import { searchHandlerWithCategory } from "@/utils/util"; import FormItemDateRange from "@/components/FormItemDateRange"; import FormItemAccounts from "@/components/FormItemAccounts"; import FormItemCategories from "@/components/FormItemCategories"; import FormItemDescriptionSearch from "@/components/FormItemDescriptionSearch"; import FormItemPayees from "@/components/FormItemPayees"; import FormItemTags from "@/components/FormItemTags"; import t from '@/utils/translate'; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const loading = useSelector(state => state.loading.effects['incomeTagReports/query']); useEffect(() => { if (!tagResponse) dispatch({ type: 'tag/fetchIncomeable' }); if (!payeeResponse) dispatch({ type: 'payee/fetchIncomeable' }); if (!accountResponse) dispatch({ type: 'account/fetchIncomeable' }); if (!querySimpleResponse) dispatch({ type: 'incomeCategory/fetchSimple' }); }, []); const { getIncomeableResponse : tagResponse } = useSelector(state => state.tag); const [tags] = useCategoryTreeSelectData(tagResponse); const { getIncomeableResponse : payeeResponse } = useSelector(state => state.payee); const [payees] = useResponseSelectData(payeeResponse); const { getIncomeableResponse : accountResponse } = useSelector(state => state.account); const [accounts] = useResponseSelectData(accountResponse); const { querySimpleResponse } = useSelector(state => state.incomeCategory); const [categories] = useCategoryTreeSelectData(querySimpleResponse); const [dateRadioValue, setDateRadioValue] = useState(8); function resetHandler() { setDateRadioValue(); form.resetFields(); } return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/pages/reports/income-tag/TagPie.jsx ================================================ import {useEffect, useState} from "react"; import {useDispatch, useSelector} from "umi"; import { Card } from "antd"; import {radioValueToTimeRange} from "@/utils/util"; import {useResponseData} from "@/utils/hooks"; import Pie from '@/components/charts/Pie2'; import t from '@/utils/translate'; export default () => { const loading = useSelector(state => state.loading.effects['incomeTagReports/query']); const { queryResponse } = useSelector(state => state.incomeTagReports); const [data] = useResponseData(queryResponse); return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/reports/income-tag/index.jsx ================================================ import { Space } from "antd"; import {spaceVProp} from "@/utils/var"; import Breadcrumb from "@/components/Breadcrumb"; import OperationBar from './OperationBar'; import TagPie from "./TagPie"; import t from "@/utils/translate"; export default () => { return ( ) } ================================================ FILE: bookkeeping-user-fe/src/pages/reports/income-tag/model.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { getIncomeTag as getTagReport } from '@/services/report'; export default modelExtend(model, { namespace: 'incomeTagReports', state: { queryResponse: undefined, }, effects: { *query({ payload }, { call, put }) { const response = yield call(getTagReport, payload); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/reports/income-tag') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/pages/scheduled/index.jsx ================================================ export default () => { return (
scheduled
); } ================================================ FILE: bookkeeping-user-fe/src/pages/settings/index.jsx ================================================ export default () => { return (
settings
); } ================================================ FILE: bookkeeping-user-fe/src/pages/signin/RememberDropDown.js ================================================ // not used import { Menu, Dropdown } from 'antd'; import { DownOutlined } from '@ant-design/icons'; const menu = ( 不记住登录状态 记住30分钟 记住1小时 记住1天 记住3天 记住7天 记住30天 ); const RememberDropDown = () => { return ( 不记住登录状态 ); }; export default RememberDropDown; ================================================ FILE: bookkeeping-user-fe/src/pages/signin/index.jsx ================================================ import { useEffect } from 'react'; import { Form, Input, Checkbox, Button, message } from 'antd'; import { Link, history, useDispatch, useSelector } from 'umi'; import { UserOutlined, LockOutlined } from '@ant-design/icons'; import { userNameRules, passwordRules } from '@/utils/rules'; import t from '@/utils/translate'; import styles from './index.less'; export default () => { const dispatch = useDispatch(); const { signInResponse } = useSelector(state => state.userSignIn); const submitting = useSelector(state => state.loading.effects['userSignIn/submit']); const messageLoginSuccess = t('signin.success'); useEffect(() => { if (signInResponse && signInResponse.success) { message.success(messageLoginSuccess); history.push({ pathname: '/dashboard' }); } return () => { dispatch({ type: 'userSignIn/clearSignInResponse' }); } }, [signInResponse]); const handleSubmit = (values) => { dispatch({ type: "userSignIn/submit", payload: { ...values } }); }; return (
} placeholder={t('placeholder.userName')} /> } type="password" placeholder={t('placeholder.password')} />
{t('signin.keep')} {/*{t('signin.forget')}*/}
{t('register')}
); } ================================================ FILE: bookkeeping-user-fe/src/pages/signin/index.less ================================================ @import '~antd/es/style/themes/default.less'; .main { margin: 0 auto; width: 368px; @media screen and (max-width: @screen-sm) { width: 95%; } } .form_row { margin-bottom: 15px; } ================================================ FILE: bookkeeping-user-fe/src/pages/signin/model.js ================================================ import { signin } from '@/services/user'; export default { namespace: 'userSignIn', state: { signInResponse: undefined, }, effects: { *submit({ payload }, { put, call }) { const response = yield call(signin, payload); yield put({ type: 'signInHandler', payload: response, }); } }, reducers: { signInHandler(state, { payload }) { return { ...state, signInResponse: payload }; }, clearSignInResponse(state) { return { ...state, signInResponse: undefined }; } }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/signin') { // token有可能失效,导致死循环跳转。 // if (localStorage.getItem("userToken")) { // history.push({ pathname: '/dashboard' }); // } } }); } } } ================================================ FILE: bookkeeping-user-fe/src/pages/test/index.jsx ================================================ import request from 'umi-request'; export default class extends React.Component { render() { return ( <> 123 ) } } ================================================ FILE: bookkeeping-user-fe/src/pages/transfers/OperationBar.jsx ================================================ import { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'umi'; import {Form, Row, Col, Button, Space} from 'antd'; import {useCategoryTreeSelectData, useResponseSelectData} from "@/utils/hooks"; import { filterFormProp } from "@/utils/var"; import {searchHandlerWithCategory} from "@/utils/util"; import {showFlowModal} from '@/utils/flow'; import FormItemDateRange from "@/components/FormItemDateRange"; import FormItemAmountRange from "@/components/FormItemAmountRange"; import FormItemDescriptionSearch from "@/components/FormItemDescriptionSearch"; import FormItemAccounts from "@/components/FormItemAccounts"; import FormItemTags from "@/components/FormItemTags"; import FormItemStatus from "@/components/FormItemStatus"; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); const [form] = Form.useForm(); const loading = useSelector(state => state.loading.effects['transfers/query']); useEffect(() => { if (!getTransferToAbleResponse) dispatch({ type: 'account/fetchTransferToAble' }); if (!getTransferFromAbleResponse) dispatch({ type: 'account/fetchTransferFromAble' }); if (!tagResponse) dispatch({ type: 'tag/fetchTransferable' }); }, []); const { getTransferToAbleResponse } = useSelector(state => state.account); const [accountsTransferTo] = useResponseSelectData(getTransferToAbleResponse); const { getTransferFromAbleResponse } = useSelector(state => state.account); const [accountsTransferFrom] = useResponseSelectData(getTransferFromAbleResponse); const { getTransferableResponse : tagResponse } = useSelector(state => state.tag); const [tags] = useCategoryTreeSelectData(tagResponse); function addHandler() { showFlowModal(3, 1, {}); } const [dateRadioValue, setDateRadioValue] = useState(); function resetHandler() { setDateRadioValue(); form.resetFields(); } return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/pages/transfers/RecordTable.jsx ================================================ import { history, useDispatch, useSelector } from 'umi'; import {Table, Space, message, Modal, Tag, Descriptions, Dropdown, Menu } from 'antd'; import {DownOutlined} from "@ant-design/icons"; import { remove, confirm } from '@/services/flow'; import {useDescriptionEnable, useImageEnable, useResultPaginationAndData, useTimeFormat} from '@/utils/hooks'; import { handleTableChange } from "@/utils/util"; import {imageHandler, showFlowModal} from '@/utils/flow'; import { tableProp } from '@/utils/var'; import {createTimeCol, amountCol, statusCol} from '@/utils/columns'; import t from "@/utils/translate"; export default () => { const dispatch = useDispatch(); const { queryResponse } = useSelector(state => state.transfers); const queryLoading = useSelector(state => state.loading.effects['transfers/query']); const [ dataAndPagination ] = useResultPaginationAndData(queryResponse); const imageEnable = useImageEnable(); const descriptionEnable = useDescriptionEnable(); const timeFormat = useTimeFormat(); let columns = []; if (descriptionEnable) { columns.push({ title: t('description'), dataIndex: 'description', }); } columns = columns.concat( [ amountCol(), createTimeCol(timeFormat), { title: t('account'), dataIndex: 'accountName', }, { title: t('flow.tag'), dataIndex: 'tags', render: tags => <>{tags.map(i => {i.tagName})} }, statusCol(), { title: t('operation'), key: 'operation', width: 100, align: 'center', render: (_, record) => { return ( copyHandler(record)}>{t('copy')} {imageEnable ? imageHandler(record)}>{t('image')} : null} updateHandler(record)}>{t('update')} confirmHandler(record)}>{t('confirm')} deleteHandler(record)}>{t('delete')} }> {t('more')} ) } } ] ); function copyHandler(record) { showFlowModal(3, 3, {...record}); } const updateHandler = (record) => { showFlowModal(3, 2, {...record}); } const messageDeleteConfirm = t('delete.confirm', { name: '' }); const messageDeleteConfirmBalance = t('delete.confirm.balance'); const messageOperationSuccess = t('operation.success'); function deleteHandler(record) { Modal.confirm({ title: record.status === 2 ? messageDeleteConfirm : messageDeleteConfirmBalance, onOk: async () => { const response = await remove(record); if (response && response.success) { message.success(messageOperationSuccess); dispatch({ type: 'transfers/query', payload: history.location.query }); } } }); } const messageConfirmOperation = t('confirm.operation'); function confirmHandler(record) { Modal.confirm({ title: messageConfirmOperation, onOk: async () => { const response = await confirm(record); if (response && response.success) { message.success(messageOperationSuccess); dispatch({ type: 'transfers/query', payload: history.location.query }); } } }); } return (
{record.needConvert ? {record.convertedAmount} : null} {record.notes ? {record.notes} : null} , rowExpandable: record => record.notes || record.needConvert, }} dataSource={dataAndPagination.data} pagination={dataAndPagination.pagination} loading={queryLoading} onChange={handleTableChange} /> ); } ================================================ FILE: bookkeeping-user-fe/src/pages/transfers/TotalAlert.jsx ================================================ import {useMemo} from "react"; import { useSelector } from 'umi'; import AlertTotalSearch from "@/components/AlertTotalSearch"; import {useResponseData} from "@/utils/hooks"; import t from '@/utils/translate'; export default () => { const { queryResponse } = useSelector(state => state.transfers); const [ responseData ] = useResponseData(queryResponse); const total = useMemo(() => { if (responseData.result) { return responseData.total; } else { return 0; } }, [responseData]); return ( ); }; ================================================ FILE: bookkeeping-user-fe/src/pages/transfers/index.jsx ================================================ import { Space } from "antd"; import {spaceVProp} from "@/utils/var"; import Breadcrumb from '@/components/Breadcrumb'; import OperationBar from './OperationBar'; import RecordTable from './RecordTable'; import TotalAlert from './TotalAlert'; import t from "@/utils/translate"; export default () => { return ( ); } ================================================ FILE: bookkeeping-user-fe/src/pages/transfers/model.js ================================================ import modelExtend from 'dva-model-extend'; import {model} from '@/utils/model'; import { query } from '@/services/transfer'; export default modelExtend(model, { namespace: 'transfers', state: { queryResponse: undefined, }, effects: { *query({ payload }, { call, put }) { const response = yield call(query, payload); yield put({ type: 'updateState', payload: { queryResponse: response }, }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/transfers') { dispatch({ type: 'query', payload: query }); } }); } } }) ================================================ FILE: bookkeeping-user-fe/src/services/account.js ================================================ import request from "@/utils/request"; const prefix = 'accounts'; // 搜索流水,按账户查找的下拉框 export async function getEnable() { return request(prefix + '/enable', { method: 'GET', }); } export async function getExpenseable() { return request(prefix + '/expenseable', { method: 'GET', }); } export async function getIncomeable() { return request(prefix + '/incomeable', { method: 'GET', }); } export async function getTransferFromAble() { return request(prefix + '/transfer-from-able', { method: 'GET', }); } export async function getTransferToAble() { return request(prefix + '/transfer-to-able', { method: 'GET', }); } export async function adjustBalance(id, json) { return request(prefix + '/' + id + '/adjust-balance', { method: 'POST', data: json, }); } export async function remove(id) { return request(prefix + '/' + id, { method: 'DELETE', }); } export async function toggle(id) { return request(prefix + '/' + id + '/toggle', { method: 'PUT', }); } export async function toggleInclude(id) { return request(prefix + '/' + id + '/toggleInclude', { method: 'PUT', }); } export async function toggleExpenseable(id) { return request(prefix + '/' + id + '/toggleExpenseable', { method: 'PUT', }); } export async function toggleIncomeable(id) { return request(prefix + '/' + id + '/toggleIncomeable', { method: 'PUT', }); } export async function toggleTransferFromAble(id) { return request(prefix + '/' + id + '/toggleTransferFromAble', { method: 'PUT', }); } export async function toggleTransferToAble(id) { return request(prefix + '/' + id + '/toggleTransferToAble', { method: 'PUT', }); } export async function update(id, json) { return request(prefix+'/'+id, { method: 'PUT', data: json, }); } ================================================ FILE: bookkeeping-user-fe/src/services/adjust-balance.js ================================================ import request from '@/utils/request'; const prefix = 'adjust-balances'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function remove(id) { return request(prefix + '/'+id, { method: 'DELETE', }); } ================================================ FILE: bookkeeping-user-fe/src/services/asset-account.js ================================================ import request from '@/utils/request'; const prefix = 'asset-accounts'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function create(json) { return request(prefix, { method: 'POST', data: json, }); } export async function sum() { return request(prefix+'/sum', { method: 'GET', }); } ================================================ FILE: bookkeeping-user-fe/src/services/book.js ================================================ import request from "@/utils/request"; const prefix = 'books'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function create(json) { return request(prefix, { method: 'POST', data: json, }); } export async function update(id, json) { return request(prefix + '/' + id, { method: 'PUT', data: json, }); } export async function config(_, json) { return request(prefix + '/config', { method: 'PUT', data: json, }); } export async function remove(id) { return request(prefix + '/' + id, { method: 'DELETE', }); } export async function toggle(id) { return request(prefix + '/' + id + '/toggle', { method: 'PUT', }); } ================================================ FILE: bookkeeping-user-fe/src/services/category.js ================================================ import request from "@/utils/request"; const prefix = 'categories'; export async function remove(id) { return request(prefix + '/' + id, { method: 'DELETE', }); } export async function toggle(id) { return request(prefix + '/' + id + '/toggle', { method: 'PUT', }); } ================================================ FILE: bookkeeping-user-fe/src/services/checking-account.js ================================================ import request from '@/utils/request'; const prefix = 'checking-accounts'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function create(json) { return request(prefix, { method: 'POST', data: json, }); } export async function sum() { return request(prefix+'/sum', { method: 'GET', }); } ================================================ FILE: bookkeeping-user-fe/src/services/credit-account.js ================================================ import request from '@/utils/request'; const prefix = 'credit-accounts'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function create(json) { return request(prefix, { method: 'POST', data: json, }); } export async function sum() { return request(prefix+'/sum', { method: 'GET', }); } ================================================ FILE: bookkeeping-user-fe/src/services/currency.js ================================================ import request from '@/utils/request'; const prefix = 'currency'; export async function getAll() { return request(prefix + '/all', { method: 'GET' }); } ================================================ FILE: bookkeeping-user-fe/src/services/dashboard.js ================================================ import request from "@/utils/request"; const prefix = 'dashboard'; export async function getAssetOverview() { return request(prefix + '/asset-overview', { method: 'GET', }); } export async function getExpenseIncomeTable() { return request(prefix + '/expense-income-table', { method: 'GET', }); } export async function getExpenseTrend() { return request(prefix + '/expense-trend', { method: 'GET', }); } export async function getIncomeTrend() { return request(prefix + '/income-trend', { method: 'GET', }); } export async function getExpenseCategory(params) { return request(prefix + '/expense-category', { method: 'GET', params: params, }); } export async function getIncomeCategory(params) { return request(prefix + '/income-category', { method: 'GET', params: params, }); } ================================================ FILE: bookkeeping-user-fe/src/services/deal.js ================================================ import request from "@/utils/request"; const prefix = 'deals'; export async function getRefunds(id) { return request(prefix + '/' + id + '/refunds', { method: 'GET', }); } ================================================ FILE: bookkeeping-user-fe/src/services/debt-account.js ================================================ import request from '@/utils/request'; const prefix = 'debt-accounts'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function create(json) { return request(prefix, { method: 'POST', data: json, }); } export async function sum() { return request(prefix+'/sum', { method: 'GET', }); } ================================================ FILE: bookkeeping-user-fe/src/services/expense-category.js ================================================ import request from "@/utils/request"; const prefix = 'expense-categories'; // 给分类管理的table用 export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } // 给类别下拉选择框使用 export async function querySimple(params) { return request(prefix + '/enable', { method: 'GET', params: params, }); } export async function create(json) { return request(prefix, { method: 'POST', data: json, }); } export async function update(id, json) { return request(prefix + '/' + id, { method: 'PUT', data: json, }); } ================================================ FILE: bookkeeping-user-fe/src/services/expense.js ================================================ import request from "@/utils/request"; const prefix = 'expenses'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function create(json) { return request(prefix, { method: 'POST', data: json, }); } export async function update(id, json) { return request(prefix+'/'+id, { method: 'PUT', data: json, }); } export async function refund(id, json) { return request(prefix + '/' + id + '/refund', { method: 'POST', data: json, }); } ================================================ FILE: bookkeeping-user-fe/src/services/flow-image.js ================================================ import request from "@/utils/request"; const prefix = 'flow-images'; export async function getUploadToken() { return request(prefix + '/upload-token', { method: 'GET', }); } ================================================ FILE: bookkeeping-user-fe/src/services/flow.js ================================================ import request from "@/utils/request"; const prefix = 'flows'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function remove(record) { return request(prefix + '/' + record.id, { method: 'DELETE', }); } export async function confirm(record) { return request(prefix + '/' + record.id + '/confirm', { method: 'PUT', }); } export async function audit(params) { return request(prefix + '/audit', { method: 'GET', params: params, }); } export async function getImages(id) { return request(prefix + '/' + id + '/images', { method: 'GET', }); } export async function updateImages(id, images) { return request(prefix + '/' + id + '/images', { method: 'POST', data: images, }); } ================================================ FILE: bookkeeping-user-fe/src/services/group.js ================================================ import request from "@/utils/request"; const prefix = 'groups'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function getEnable() { return request(prefix + '/enable', { method: 'GET' }); } export async function create(json) { return request(prefix, { method: 'POST', data: json, }); } export async function update(id, json) { return request(prefix + '/' + id, { method: 'PUT', data: json, }); } export async function remove(id) { return request(prefix + '/' + id, { method: 'DELETE', }); } ================================================ FILE: bookkeeping-user-fe/src/services/income-category.js ================================================ import request from "@/utils/request"; const prefix = 'income-categories'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function querySimple(params) { return request(prefix + '/enable', { method: 'GET', params: params, }); } export async function create(json) { return request(prefix, { method: 'POST', data: json, }); } export async function update(id, json) { return request(prefix + '/' + id, { method: 'PUT', data: json, }); } ================================================ FILE: bookkeeping-user-fe/src/services/income.js ================================================ import request from "@/utils/request"; const prefix = 'incomes'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function create(json) { return request(prefix, { method: 'POST', data: json, }); } export async function update(id, json) { return request(prefix+'/'+id, { method: 'PUT', data: json, }); } export async function refund(id, json) { return request(prefix + '/' + id + '/refund', { method: 'POST', data: json, }); } ================================================ FILE: bookkeeping-user-fe/src/services/item.js ================================================ import request from "@/utils/request"; const prefix = 'items'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function create(json) { return request(prefix, { method: 'POST', data: json, }); } export async function update(id, json) { return request(prefix + '/' + id, { method: 'PUT', data: json, }); } export async function run(id, json) { return request(prefix + '/' + id + '/run', { method: 'PUT', data: json, }); } export async function recall(id, json) { return request(prefix + '/' + id + '/recall', { method: 'PUT', data: json, }); } export async function remove(id) { return request(prefix + '/' + id, { method: 'DELETE', }); } ================================================ FILE: bookkeeping-user-fe/src/services/log.js ================================================ import request from "@/utils/request"; const prefix = 'logs'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function create(json) { return request(prefix, { method: 'POST', data: json, }); } export async function remove(id) { return request(prefix + '/' + id, { method: 'DELETE', }); } ================================================ FILE: bookkeeping-user-fe/src/services/payee.js ================================================ import request from '@/utils/request'; const prefix = 'payees'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function getEnable() { return request(prefix + '/enable', { method: 'GET' }); } export async function getExpenseable() { return request(prefix + '/expenseable', { method: 'GET' }); } export async function getIncomeable() { return request(prefix + '/incomeable', { method: 'GET' }); } export async function create(json) { return request(prefix, { method: 'POST', data: json, }); } export async function update(id, json) { return request(prefix + '/' + id, { method: 'PUT', data: json, }); } export async function remove(id) { return request(prefix + '/' + id, { method: 'DELETE', }); } export async function toggle(id) { return request(prefix + '/' + id + '/toggle', { method: 'PUT', }); } ================================================ FILE: bookkeeping-user-fe/src/services/report.js ================================================ import request from "@/utils/request"; const prefix = 'reports'; export async function getExpenseCategory(params) { return request(prefix + '/expense-category', { method: 'GET', params: params, }); } export async function getExpenseTag(params) { return request(prefix + '/expense-tag', { method: 'GET', params: params, }); } export async function getIncomeCategory(params) { return request(prefix + '/income-category', { method: 'GET', params: params, }); } export async function getIncomeTag(params) { return request(prefix + '/income-tag', { method: 'GET', params: params, }); } export async function getExpenseIncomeTrend(params) { return request(prefix + '/expense-income-trend', { method: 'GET', params: params, }); } export async function getAssetDebtTrend(params) { return request(prefix + '/asset-debt-trend', { method: 'GET', params: params, }); } export async function getAsset() { return request(prefix + '/asset', { method: 'GET', }); } export async function getDebt() { return request(prefix + '/debt', { method: 'GET', }); } ================================================ FILE: bookkeeping-user-fe/src/services/schedule.js ================================================ import request from "@/utils/request"; const prefix = 'schedules'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function create(json) { return request(prefix, { method: 'POST', data: json, }); } export async function update(id, json) { return request(prefix + '/' + id, { method: 'PUT', data: json, }); } export async function remove(id) { return request(prefix + '/' + id, { method: 'DELETE', }); } ================================================ FILE: bookkeeping-user-fe/src/services/tag-relation.js ================================================ import request from "@/utils/request"; const prefix = 'tag-relations'; export async function update(id, json) { return request(prefix + '/' + id, { method: 'PUT', data: json, }); } ================================================ FILE: bookkeeping-user-fe/src/services/tag.js ================================================ import request from "@/utils/request"; const prefix = 'tags'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function getEnable() { return request(prefix + '/enable', { method: 'GET' }); } export async function getExpenseable() { return request(prefix + '/expenseable', { method: 'GET' }); } export async function getIncomeable() { return request(prefix + '/incomeable', { method: 'GET' }); } export async function getTransferable() { return request(prefix + '/transferable', { method: 'GET' }); } export async function create(json) { return request(prefix, { method: 'POST', data: json, }); } export async function update(id, json) { return request(prefix + '/' + id, { method: 'PUT', data: json, }); } export async function remove(id) { return request(prefix + '/' + id, { method: 'DELETE', }); } export async function toggle(id) { return request(prefix + '/' + id +'/toggle', { method: 'PUT', }); } ================================================ FILE: bookkeeping-user-fe/src/services/transfer.js ================================================ import request from "@/utils/request"; const prefix = 'transfers'; export async function query(params) { return request(prefix, { method: 'GET', params: params, }); } export async function create(json) { return request(prefix, { method: 'POST', data: json, }); } export async function update(id, json) { return request(prefix+'/'+id, { method: 'PUT', data: json, }); } ================================================ FILE: bookkeeping-user-fe/src/services/user.js ================================================ import request from "@/utils/request"; export async function register(params) { return request('register', { method: 'POST', data: params, }); } export async function signin(params) { return request('signin', { method: 'POST', data: params, }); } export async function signout() { return request('signout', { method: 'POST', }); } export async function updatePassword(params) { return request('updatePassword', { method: 'PUT', data: params, }); } export async function getSessionUser() { return request('session', { method: 'GET', }); } export async function setDefaultBook(id) { return request('setDefaultBook/' + id, { method: 'PUT', }); } export async function setDefaultGroup(id) { return request('setDefaultGroup/' + id, { method: 'PUT', }); } ================================================ FILE: bookkeeping-user-fe/src/utils/columns.js ================================================ import {Switch} from "antd"; import moment from 'moment'; import FlagTag from '@/components/FlagTag'; import FlowStatusDisplay from '@/components/FlowStatusDisplay'; import t from '@/utils/translate'; export const createTimeCol = (timeFormat) => { return { title: t('flow.createTime'), dataIndex: 'createTime', sorter: true, width: 125, align: 'center', render: time => moment(time).format(timeFormat ? timeFormat : 'YYYY-MM-DD') } } export const amountCol = () => { return { title: t('amount'), dataIndex: 'amount', sorter: true, width: 70, } } export const balanceCol = () => { return { title: t('account.balance'), dataIndex: 'balance', sorter: true, width: 80, } } export const statusCol = () => { return { title: t('flow.status'), dataIndex: 'status', sorter: true, width: 70, align: 'center', render: (_, record) => } } export const flowTypeCol = () => { return { title: t('flow.type'), dataIndex: 'typeName', // sorter: true, width: 58, } } export const accountIncludeCol = (toggleIncludeHandler) => { return { title: t('account.include'), dataIndex: 'include', sorter: true, width: 90, render: (value, record) => toggleIncludeHandler(record)} checked={value} /> } } export const accountExpenseableCol = (toggleExpenseableHandler) => { return { title: t('expenseable'), dataIndex: 'expenseable', sorter: true, width: 70, render: (value, record) => toggleExpenseableHandler(record)} checked={value} /> } } export const accountIncomeableCol = (toggleIncomeableHandler) => { return { title: t('incomeable'), dataIndex: 'incomeable', sorter: true, width: 70, render: (value, record) => toggleIncomeableHandler(record)} checked={value} /> } } export const accountTransferToCol = (toggleTransferFromAbleHandler) => { return { title: t('transferToAble'), dataIndex: 'transferToAble', sorter: true, width: 70, render: (value, record) => toggleTransferFromAbleHandler(record)} checked={value} /> } } export const accountTransferFromCol = (toggleTransferToAbleHandler) => { return { title: t('transferFromAble'), dataIndex: 'transferFromAble', sorter: true, width: 70, render: (value, record) => toggleTransferToAbleHandler(record)} checked={value} /> } } export const accountEnableCol = (toggleHandler) => { return { title: t('is.enable'), dataIndex: 'enable', sorter: true, width: 80, render: (value, record) => toggleHandler(record)} checked={value} /> } } export const categoryExpenseableCol = () => { return { title: t('expenseable'), dataIndex: 'expenseable', sorter: true, width: 120, render: value => } } export const categoryIncomeableCol = () => { return { title: t('incomeable'), dataIndex: 'incomeable', sorter: true, width: 120, render: value => } } export const categoryTransferableCol = () => { return { title: t('transferable'), dataIndex: 'transferable', sorter: true, width: 120, render: value => } } ================================================ FILE: bookkeeping-user-fe/src/utils/flow.js ================================================ import {getDvaApp, useDispatch} from "umi"; import ExpenseModal from '@/components/ExpenseModal'; import IncomeModal from '@/components/IncomeModal'; import TransferModal from '@/components/TransferModal'; import AdjustBalanceModal from '@/components/AdjustBalanceModal'; import t from "@/utils/translate"; import FlowImageUploadModal from "@/components/FlowImageUploadModal"; export function showFlowModal(flowType, modalType, currentItem) { const dispatch = getDvaApp()._store.dispatch; switch (flowType) { case 1: dispatch({ type: 'modal/show', payload: {component: ExpenseModal, type: modalType, currentItem: currentItem }}); break; case 2: dispatch({ type: 'modal/show', payload: {component: IncomeModal, type: modalType, currentItem: currentItem }}); break; case 3: dispatch({ type: 'modal/show', payload: {component: TransferModal, type: modalType, currentItem: currentItem }}); break; case 4: dispatch({ type: 'modal/show', payload: {component: AdjustBalanceModal, type: modalType, currentItem: currentItem }}); break; } } export function imageHandler(record) { const dispatch = getDvaApp()._store.dispatch; dispatch({ type: 'modal/show', payload: {component: FlowImageUploadModal, type: 2, currentItem: record } }); } ================================================ FILE: bookkeeping-user-fe/src/utils/hooks.js ================================================ import { useEffect, useState, useRef } from 'react'; import {useIntl, useSelector} from 'umi'; import { message } from 'antd'; export function useFocus() { const htmlElRef = useRef(null); const setFocus = () => {htmlElRef.current && htmlElRef.current.focus()}; return [ htmlElRef, setFocus ]; } export function usePaginationAndData(queryResponse) { const intl = useIntl(); const [dataAndPagination, setDataAndPagination] = useState({ data: [], pagination: { showQuickJumper: true, current: 1, pageSize: 10, showTotal: (total, range) => intl.formatMessage({id:'pagination.showTotal'}, {range0:range[0], range1:range[1], total:total}), } }); useEffect(() => { if (queryResponse && queryResponse.success) { setDataAndPagination({ data: queryResponse.data.content, pagination: { ...dataAndPagination.pagination, current: queryResponse.data.number+1, pageSize: queryResponse.data.size, total: queryResponse.data.totalElements } }); } }, [queryResponse]); return [dataAndPagination, setDataAndPagination] } export function useResultPaginationAndData(queryResponse) { const intl = useIntl(); const [dataAndPagination, setDataAndPagination] = useState({ data: [], pagination: { showQuickJumper: true, current: 1, pageSize: 10, showTotal: (total, range) => intl.formatMessage({id:'pagination.showTotal'}, {range0:range[0], range1:range[1], total:total}), } }); useEffect(() => { if (queryResponse && queryResponse.success) { setDataAndPagination({ data: queryResponse.data.result.content, pagination: { ...dataAndPagination.pagination, current: queryResponse.data.result.number+1, pageSize: queryResponse.data.result.size, total: queryResponse.data.result.totalElements } }); } }, [queryResponse]); return [dataAndPagination, setDataAndPagination] } export function useResponseData(queryResponse) { const [responseData, setResponseData] = useState([]); useEffect(() => { if (queryResponse && queryResponse.success) { setResponseData(queryResponse.data); } }, [queryResponse]); return [responseData, setResponseData]; } export function useResponseSelectData(queryResponse) { const [responseSelectData, setResponseSelectData] = useState([]); useEffect(() => { if (queryResponse && queryResponse.success) { setResponseSelectData(queryResponse.data.map(item => { return { value: item.id, title: item.name, label: item.name } })); } }, [queryResponse]); return [responseSelectData, setResponseSelectData]; } export function useCurrencyResponseSelectData(queryResponse) { const [responseSelectData, setResponseSelectData] = useState([]); useEffect(() => { if (queryResponse && queryResponse.success) { setResponseSelectData(queryResponse.data.map(item => { return { value: item.code, title: item.code, label: item.code } })); } }, [queryResponse]); return [responseSelectData, setResponseSelectData]; } export function useCategoryTreeSelectData(categoriesResponse) { const [categories, setCategories] = useState([]); useEffect(() => { if (categoriesResponse && categoriesResponse.success) { setCategories(categoriesResponse.data.map(item => { return { id: item.id, pId: item.parentId, value: item.id, title: item.name } })); } }, [categoriesResponse]); return [categories, setCategories]; } export function useImageEnable() { const [enable, setEnable] = useState(false); const { defaultBook } = useSelector(state => state.session); useEffect(() => { if (defaultBook && defaultBook.imageEnable) { setEnable(true); } else { setEnable(false); } }, [defaultBook]); return enable; } export function useDescriptionEnable() { const [enable, setEnable] = useState(false); const { defaultBook } = useSelector(state => state.session); useEffect(() => { if (defaultBook && defaultBook.descriptionEnable) { setEnable(true); } else { setEnable(false); } }, [defaultBook]); return enable; } export function useTimeFormat() { const [format, setFormat] = useState('YYYY-MM-DD'); const { defaultBook } = useSelector(state => state.session); useEffect(() => { if (defaultBook && defaultBook.timeEnable) { setFormat('YYYY-MM-DD HH:mm'); } else { setFormat('YYYY-MM-DD'); } }, [defaultBook]); return format; } ================================================ FILE: bookkeeping-user-fe/src/utils/model.js ================================================ export const model = { reducers: { updateState(state, { payload }) { return { ...state, ...payload } }, }, } ================================================ FILE: bookkeeping-user-fe/src/utils/request.js ================================================ import { extend } from "umi-request"; import { message } from 'antd'; const request = extend({ prefix: "/api/v1/", // prefix: "http://jz.jiukuaitech.com/api/v1/", timeout: 60000, headers: { "Content-Type": "application/json", }, errorHandler: function(error) { // error.response.status if (error.data && error.data.errorMsg) { message.error(error.data.errorMsg); } else { console.log(error); message.error("系统异常,请稍后重试。"); } } }); // 自动跳转到登陆页 request.interceptors.response.use(async (response, options) => { const data = await response.clone().json(); if (data.errorCode === 8 || data.errorCode === 10) { window.location.href = '/signin'; } return response; }); // 全局处理异常 request.interceptors.response.use(async (response, options) => { if (response.status === 200) { const data = await response.clone().json(); if (!data.success) { message.error(data.errorMsg); } } return response; }); export default request; ================================================ FILE: bookkeeping-user-fe/src/utils/rules.js ================================================ import t from '@/utils/translate' export const requiredRules = () => [ { required: true, message: t('rules.required') } ]; export const userNameRules = () => [ { required: true, message: t('rules.userName.required'), }, { pattern: "^[A-Za-z0-9]{1,16}$", message: t('rules.userName.pattern'), } ]; export const passwordRules = () => [ { required: true, message: t('rules.password.required'), }, { pattern: "^[A-Za-z0-9~`!@#\\$%\\^&\\*\\(\\)-_=\\+\\[\\]\\{\\}\\|]{6,32}$", message: t('rules.password.pattern'), } ]; export const emailRules = () => [ { type: "email", message: t('rules.email.pattern'), } ]; // 账户名,类别名,标签名等。 export const nameRules = () => [ { required: true, message: t('rules.name.required'), }, { max: 16, message: t('rules.name.pattern'), } ]; export const balanceRequiredRules = () => [ { required: true, message: t('rules.balance.required'), }, { pattern: "^-?\\d{1,9}(\\.\\d{0,2})?$", message: t('rules.balance.pattern'), } ]; export const amountRequiredRules = () => [ { required: true, message: t('rules.amount.required'), }, { pattern: "^-?\\d{1,9}(\\.\\d{0,2})?$", //可以负数,代表退款 message: t('rules.amount.pattern'), } ]; export const amountRequiredRulesPositive = () => [ { required: true, message: t('rules.amount.required'), }, { pattern: "^\\d{1,9}(\\.\\d{0,2})?$", message: t('rules.amount.pattern'), } ]; export const balanceRules = () => [ { pattern: "^-?\\d{1,9}(\\.\\d{0,2})?$", message: t('rules.balance.pattern'), } ]; export const descriptionRules = () => [ { max: 16, message: t('rules.description.pattern'), } ]; export const notesRules = () => [ { max: 1024, message: t('rules.notes.pattern'), } ]; export const timeRequiredRules = () => [ { type: 'object', required: true, message: t('rules.time.required') } ]; export const timeRangeRequiredRules = () => [ { type: 'array', required: true, message: t('rules.time.required') } ]; export const categoryRequiredRules = () => [ { required: true, message: t('rules.category.required') } ]; export const accountRequiredRules = () => [ { required: true, message: t('rules.account.required') } ]; export const limitRequiredRules = () => [ { required: true, message: t('rules.limit.required'), }, { pattern: "^\\d{1,9}(\\.\\d{0,2})?$", message: t('rules.limit.pattern'), } ]; export const aprRules = () => [ { pattern: "^\\d{1,3}(\\.\\d{0,2})?$", message: t('rules.apr.pattern'), } ]; ================================================ FILE: bookkeeping-user-fe/src/utils/translate.js ================================================ import { useIntl, setLocale } from 'umi'; export default (id, args) => { const intl = useIntl(); // setLocale('en-US', true); // setLocale('zh-CN', true); return intl.formatMessage({id:id}, {...args}); } ================================================ FILE: bookkeeping-user-fe/src/utils/util.js ================================================ import {history, useIntl, getDvaApp} from 'umi'; import moment from 'moment'; export function tableSortFormat(sorter) { let orderDir = ''; if (sorter && sorter.field && sorter.order) { orderDir = sorter.field; if (sorter.order === 'ascend') { orderDir += ',asc' } else if (sorter.order === 'descend') { orderDir += ',desc' } } return orderDir; } export function handleTableChange(pagination, _, sorter) { const { query, pathname } = history.location; const newQuery = Object.assign({}, query, { page: pagination.current, size: pagination.pageSize, sort: tableSortFormat(sorter), }); history.push({ pathname: pathname, query: newQuery }); } export function paginationChange(page, pageSize) { const { query, pathname } = history.location; const newQuery = Object.assign({}, query, { page: page, size: pageSize, }); history.push({ pathname: pathname, query: newQuery }); } export function tableChangeQueryFormat(pagination, sorter) { return { page: pagination.current, size: pagination.pageSize, sort: tableSortFormat(sorter), } } function offSpring(categories, category) { return categories.reduce((r, item) => { if (item.pId === category.value) { r.push(item, ...offSpring(categories, item)); } return r; }, []); } function parseCreateTimeRange(values) { if (values.createTimeRange && values.createTimeRange[0]) { values.minTime = values.createTimeRange[0].valueOf(); } if (values.createTimeRange && values.createTimeRange[1]) { values.maxTime = values.createTimeRange[1].valueOf(); } delete values.createTimeRange; } export async function searchHandler(form) { const values = await form.validateFields(); parseCreateTimeRange(values); history.push({ pathname: location.pathname, query: values }); } export async function searchHandlerWithCategory(form) { const values = await form.validateFields(); parseCreateTimeRange(values); // if (values.categories) { // let offSpringCategories = values.categories.map(item => offSpring(categories, item)); // offSpringCategories = offSpringCategories.flat(); // values.categories = [...(values.categories.map(item => item.value)), ...(offSpringCategories.map(item => item.value))]; // values.categories = [...new Set(values.categories)]; // } history.push({ pathname: location.pathname, query: values }); } export function flowStatusToColor(code) { switch (code) { case 1: return 'blue'; case 2: return 'yellow'; case 3: return 'red'; default: break; } } export function getNull(obj) { let newObj = { ...obj }; Object.keys(newObj).forEach(k => newObj[k] = null); return newObj; } export async function validateForm(form) { try { return await form.validateFields(); } catch (e) { //console.log(e); } } // 1-今天 2-本周 3-本月 4-今年 5-去年 6-7天内 7-30天内 8-1年内 export function radioValueToTimeRange(value) { switch (value) { case 1: return [moment().startOf('day'), moment().endOf('day')]; case 2: return [moment().startOf('week'), moment().endOf('week')] case 3: return [moment().startOf('month'), moment().endOf('month')]; case 4: return [moment().startOf('year'), moment().endOf('year')]; case 5: return [moment().subtract(1, 'years').startOf('year'), moment().subtract(1, 'years').endOf('year')]; case 6: return [moment().subtract(7, 'days'), moment()]; case 7: return [moment().subtract(30, 'days'), moment()]; case 8: return [moment().subtract(1, 'years'), moment()]; } } export function initPagination() { const intl = useIntl(); return { showQuickJumper: true, current: 1, pageSize: 15, showTotal: (total, range) => intl.formatMessage({id:'pagination.showTotal'}, {range0:range[0], range1:range[1], total:total}), } } export function getPagination(data, pagination) { return { ...pagination, current: data.number+1, pageSize: data.size, total: data.totalElements }; } export function getTimeFormat() { let timeFormat = 'YYYY-MM-DD'; if (getTimeEnable()) { timeFormat = 'YYYY-MM-DD HH:mm'; } return timeFormat; } export function getTimeEnable() { let defaultBook = getDvaApp()._store.getState().session.defaultBook; if (defaultBook && defaultBook.timeEnable) return true; else return false; } export function getDescriptionEnable() { let defaultBook = getDvaApp()._store.getState().session.defaultBook; if (defaultBook && defaultBook.descriptionEnable) return true; else return false; } export function getImageEnable() { let defaultBook = getDvaApp()._store.getState().session.defaultBook; if (defaultBook && defaultBook.imageEnable) return true; else return false; } export function categoryTypeToCreateParam(type) { switch (type) { case 1: return { expenseable: true }; case 2: return { incomeable: true }; case 3: return { transferable: true }; } } export function refreshFlow(data) { const dispatch = getDvaApp()._store.dispatch; if (history.location.pathname === '/expenses') { dispatch({ type: 'expenses/query', payload: history.location.query }); } if (history.location.pathname === '/incomes') { dispatch({ type: 'incomes/query', payload: history.location.query }); } if (history.location.pathname === '/transfers') { dispatch({ type: 'transfers/query', payload: history.location.query }); } if (history.location.pathname === '/flows') { dispatch({ type: 'flows/query', payload: history.location.query }); } if (history.location.pathname === '/audit') { dispatch({ type: 'audit/query', payload: history.location.query }); } // if (history.location.pathname === '/accounts') { // dispatch({ type: 'accounts/refresh' }); // } // if (history.location.pathname === '/checking-accounts') { // dispatch({ type: 'checkingAccounts/refresh', payload: data }); // } // if (history.location.pathname === '/credit-accounts') { // dispatch({ type: 'creditAccounts/refresh', payload: data }); // } // if (history.location.pathname === '/debt-accounts') { // dispatch({ type: 'debtAccounts/refresh', payload: data }); // } // if (history.location.pathname === '/asset-accounts') { // dispatch({ type: 'assetAccounts/refresh', payload: data }); // } } export function searchTreeArray(treeArray, value, key = 'id', reverse = true) { for (var i = 0; i < treeArray.length; i++) { const stack = [ treeArray[i] ]; while (stack.length) { const node = stack[reverse ? 'pop' : 'shift'](); if (node[key] === value) return node; node.children && stack.push(...node.children); } } return null; } ================================================ FILE: bookkeeping-user-fe/src/utils/var.js ================================================ export const tableProp = { size: 'small', bordered: true, rowKey: 'id', pagination: false, showSorterTooltip: false, //sortDirections: ['asc', 'desc'] } export const spaceVProp = { direction: 'vertical', size: 'small', style: { width:'100%' } } export const inputNumberProp = { style: { width:'100%' }, min: 0, step: 1 } export const formProp = { validateTrigger: ['onBlur'] } export const filterFormProp = { size: 'small', style: { padding:'10px', background: 'rgba(255,255,255,1)'} } export const formItemLayout = { labelCol: { span: 3 }, wrapperCol: { span: 21 }, } ================================================ FILE: bookkeeping-user-fe/webpack.config.js ================================================ /** * 不是真实的 webpack 配置,仅为兼容 webstorm 和 intellij idea 代码跳转 * ref: https://github.com/umijs/umi/issues/1109#issuecomment-423380125 */ module.exports = { resolve: { alias: { '@': require('path').resolve(__dirname, 'src'), }, }, }; ================================================ FILE: bookkeeping_user_flutter/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ /build/ # Web related lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: bookkeeping_user_flutter/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled. version: revision: f1875d570e39de09040c8f79aa13cc56baab8db1 channel: stable project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 - platform: linux create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 - platform: macos create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 - platform: windows create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: bookkeeping_user_flutter/README.md ================================================ # bookkeeping_user_flutter A new Flutter project. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: bookkeeping_user_flutter/analysis_options.yaml ================================================ # This file configures the analyzer, which statically analyzes Dart code to # check for errors, warnings, and lints. # # The issues identified by the analyzer are surfaced in the UI of Dart-enabled # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be # invoked from the command line by running `flutter analyze`. # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints # and their documentation is published at # https://dart-lang.github.io/linter/lints/index.html. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code # or a specific dart file by using the `// ignore: name_of_lint` and # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: bookkeeping_user_flutter/android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties ================================================ FILE: bookkeeping_user_flutter/android/app/build.gradle ================================================ def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion 32 sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.jiukuaitech.bookkeeping_user_flutter" minSdkVersion 19 targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug lintOptions { disable 'InvalidPackage' disable "Instantiatable" checkReleaseBuilds false abortOnError false } } } } flutter { source '../..' } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } ================================================ FILE: bookkeeping_user_flutter/android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: bookkeeping_user_flutter/android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: bookkeeping_user_flutter/android/app/src/main/kotlin/com/whlcsj/bookkeeping_user_flutter/MainActivity.kt ================================================ package com.jiukuaitech.bookkeeping_user_flutter import io.flutter.embedding.android.FlutterActivity class MainActivity: FlutterActivity() { } ================================================ FILE: bookkeeping_user_flutter/android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: bookkeeping_user_flutter/android/app/src/main/res/drawable-v21/launch_background.xml ================================================ ================================================ FILE: bookkeeping_user_flutter/android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: bookkeeping_user_flutter/android/app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: bookkeeping_user_flutter/android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: bookkeeping_user_flutter/android/build.gradle ================================================ buildscript { ext.kotlin_version = '1.6.21' repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() jcenter() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" project.evaluationDependsOn(':app') } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: bookkeeping_user_flutter/android/gradle/wrapper/gradle-wrapper.properties ================================================ #Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip ================================================ FILE: bookkeeping_user_flutter/android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true ================================================ FILE: bookkeeping_user_flutter/android/settings.gradle ================================================ include ':app' def localPropertiesFile = new File(rootProject.projectDir, "local.properties") def properties = new Properties() assert localPropertiesFile.exists() localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" ================================================ FILE: bookkeeping_user_flutter/ios/.gitignore ================================================ *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: bookkeeping_user_flutter/ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 11.0 ================================================ FILE: bookkeeping_user_flutter/ios/Flutter/Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: bookkeeping_user_flutter/ios/Flutter/Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: bookkeeping_user_flutter/ios/Podfile ================================================ # Uncomment this line to define a global platform for your project # platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end end ================================================ FILE: bookkeeping_user_flutter/ios/Runner/AppDelegate.swift ================================================ import UIKit import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } ================================================ FILE: bookkeeping_user_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "Icon-App-1024x1024@1x.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: bookkeeping_user_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "LaunchImage.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "LaunchImage@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "LaunchImage@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: bookkeeping_user_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # Launch Screen Assets You can customize the launch screen with your own desired assets by replacing the image files in this directory. You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. ================================================ FILE: bookkeeping_user_flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: bookkeeping_user_flutter/ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: bookkeeping_user_flutter/ios/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName 九快记账 CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName bookkeeping_user_flutter CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents ================================================ FILE: bookkeeping_user_flutter/ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: bookkeeping_user_flutter/ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 317A2C53BB5FF676168ECE72 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9DA02CB907CE0F6FF8FB20B4 /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 96F37FC13C924C3FF0E5F18A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9DA02CB907CE0F6FF8FB20B4 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9DCD06E0BA9999D42EED641A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; DCA4C3B4092AC92C0938E696 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 317A2C53BB5FF676168ECE72 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, DA0E3430B2CF1B346EC449CC /* Pods */, B1414E7CCCC57CC8C5D302AD /* Frameworks */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; B1414E7CCCC57CC8C5D302AD /* Frameworks */ = { isa = PBXGroup; children = ( 9DA02CB907CE0F6FF8FB20B4 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; DA0E3430B2CF1B346EC449CC /* Pods */ = { isa = PBXGroup; children = ( DCA4C3B4092AC92C0938E696 /* Pods-Runner.debug.xcconfig */, 96F37FC13C924C3FF0E5F18A /* Pods-Runner.release.xcconfig */, 9DCD06E0BA9999D42EED641A /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 330760EC9F725B8A7FB98CB4 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 4E2C97FAD6B63ABA3AB947A8 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 330760EC9F725B8A7FB98CB4 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 4E2C97FAD6B63ABA3AB947A8 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = PB6FXZT6M2; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.jiukuaitech.bookkeepingUserFlutter; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = PB6FXZT6M2; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.jiukuaitech.bookkeepingUserFlutter; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = PB6FXZT6M2; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.jiukuaitech.bookkeepingUserFlutter; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: bookkeeping_user_flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: bookkeeping_user_flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: bookkeeping_user_flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: bookkeeping_user_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: bookkeeping_user_flutter/ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: bookkeeping_user_flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: bookkeeping_user_flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: bookkeeping_user_flutter/lib/accounts/accounts.dart ================================================ export 'bloc/account_expenseable/account_expenseable_bloc.dart'; export 'bloc/account_incomeable/account_incomeable_bloc.dart'; export 'bloc/account_transfer_from_able/account_transfer_from_able_bloc.dart'; export 'bloc/account_transfer_to_able/account_transfer_to_able_bloc.dart'; export 'bloc/account_enable/account_enable_bloc.dart'; export 'bloc/accounts/accounts_bloc.dart'; export 'bloc/account_fetch/account_fetch_bloc.dart'; export 'bloc/account_adjust_balance/account_adjust_balance_bloc.dart'; export 'bloc/account_form/account_form_bloc.dart'; export 'data/account_repository.dart'; export 'data/models/account.dart'; export 'data/models/account_query_request.dart'; export 'data/models/account_form_request.dart'; export 'data/models/adjust_balance_request.dart'; export 'ui/account_detail_page.dart'; export 'ui/account_form_page.dart'; export 'ui/checking_account_form_page.dart'; export 'ui/credit_account_form_page.dart'; export 'ui/debt_account_form_page.dart'; export 'ui/asset_account_form_page.dart'; export 'ui/account_adjust_balance.dart'; export 'ui/widgets/order_button.dart'; export 'ui/accounts_page.dart'; export 'ui/widgets/adjust_balance/description_input.dart'; export 'ui/widgets/adjust_balance/date_time_input.dart'; export 'ui/widgets/adjust_balance/balance_input.dart'; export 'ui/widgets/adjust_balance/notes_input.dart'; export 'ui/widgets/adjust_balance/adjust_balance_form.dart'; export 'ui/widgets/account_form/account_balance_input.dart'; export 'ui/widgets/account_form/name_input.dart'; export 'ui/widgets/account_form/currency_input.dart'; export 'ui/widgets/account_form/account_limit_input.dart'; export 'ui/widgets/account_form/account_billday_input.dart'; export 'ui/widgets/account_form/account_apr_input.dart'; export 'ui/widgets/account_form/no_input.dart'; export 'ui/widgets/account_form/expenseable_input.dart'; export 'ui/widgets/account_form/incomeable_input.dart'; export 'ui/widgets/account_form/transfer_from_able_input.dart'; export 'ui/widgets/account_form/transfer_to_able_input.dart'; export 'ui/widgets/account_form/inclue_input.dart'; export 'ui/widgets/account_form/notes_input.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_adjust_balance/account_adjust_balance_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:formz/formz.dart'; import 'package:meta/meta.dart'; import '/accounts/accounts.dart'; import '/flows/flows.dart'; part 'account_adjust_balance_event.dart'; part 'account_adjust_balance_state.dart'; class AccountAdjustBalanceBloc extends Bloc { final AccountRepository accountRepository; AccountAdjustBalanceBloc({ required this.accountRepository, }) : super(AccountAdjustBalanceState()) { on(_onDescriptionChanged); on(_onNotesChanged); on(_onCreateDateChanged); on(_onCreateTimeChanged); on(_onBalanceChanged); on(_onDefaultLoaded); on(_onSubmitted); } void _onDescriptionChanged(AccountAdjustBalanceDescriptionChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(description: event.description), )); } void _onNotesChanged(AccountAdjustBalanceNotesChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(notes: event.notes), )); } void _onCreateDateChanged(AccountAdjustBalanceCreateDateChanged event, Emitter emit) { DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(state.request.createTime ?? DateTime.now().millisecondsSinceEpoch); dateTime = DateTime(event.dateTime.year, event.dateTime.month, event.dateTime.day, dateTime.hour, dateTime.minute); emit(state.copyWith( request: state.request.copyWith(createTime: dateTime.millisecondsSinceEpoch), )); } void _onCreateTimeChanged(AccountAdjustBalanceCreateTimeChanged event, Emitter emit) { DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(state.request.createTime ?? DateTime.now().millisecondsSinceEpoch); dateTime = DateTime(dateTime.year, dateTime.month, dateTime.day, event.time.hour, event.time.minute); emit(state.copyWith( request: state.request.copyWith(createTime: dateTime.millisecondsSinceEpoch), )); } void _onDefaultLoaded(AccountAdjustBalanceDefaultLoaded event, Emitter emit) { emit(state.copyWith( status: FormzStatus.pure, request: state.request.copyWith( description: event.adjustBalance?.description ?? '', createTime: event.type != 2 ? DateTime.now().millisecondsSinceEpoch : event.adjustBalance!.createTime, notes: event.adjustBalance?.notes ?? '', ) )); } void _onBalanceChanged(AccountAdjustBalanceBalanceChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(balance: event.balance), )); } void _onSubmitted(AccountAdjustBalanceSubmitted event, Emitter emit) async { try { bool result = false; switch (event.type) { case 1: result = await accountRepository.adjustBalance(event.accountId!, state.request); break; case 2: result = await accountRepository.updateAdjustBalance(event.adjustBalance!.id, state.request); break; default: break; } if (result) { emit(state.copyWith(status: FormzStatus.submissionSuccess)); } else { emit(state.copyWith(status: FormzStatus.submissionFailure)); } } catch (_) { emit(state.copyWith(status: FormzStatus.submissionFailure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_adjust_balance/account_adjust_balance_event.dart ================================================ part of 'account_adjust_balance_bloc.dart'; @immutable abstract class AccountAdjustBalanceEvent extends Equatable { const AccountAdjustBalanceEvent(); @override List get props => []; } class AccountAdjustBalanceCreateTimeChanged extends AccountAdjustBalanceEvent { const AccountAdjustBalanceCreateTimeChanged(this.time); final TimeOfDay time; @override List get props => [time]; } class AccountAdjustBalanceCreateDateChanged extends AccountAdjustBalanceEvent { const AccountAdjustBalanceCreateDateChanged(this.dateTime); final DateTime dateTime; @override List get props => [dateTime]; } class AccountAdjustBalanceBalanceChanged extends AccountAdjustBalanceEvent { const AccountAdjustBalanceBalanceChanged(this.balance); final String balance; @override List get props => [balance]; } class AccountAdjustBalanceDescriptionChanged extends AccountAdjustBalanceEvent { const AccountAdjustBalanceDescriptionChanged(this.description); final String description; @override List get props => [description]; } class AccountAdjustBalanceNotesChanged extends AccountAdjustBalanceEvent { const AccountAdjustBalanceNotesChanged(this.notes); final String notes; @override List get props => [notes]; } class AccountAdjustBalanceDefaultLoaded extends AccountAdjustBalanceEvent { final int type; final AdjustBalance? adjustBalance; const AccountAdjustBalanceDefaultLoaded(this.type, this.adjustBalance); @override List get props => [type, adjustBalance]; } class AccountAdjustBalanceSubmitted extends AccountAdjustBalanceEvent { final int? accountId; final int type; final AdjustBalance? adjustBalance; const AccountAdjustBalanceSubmitted(this.type, this.accountId, this.adjustBalance); @override List get props => [type, accountId, adjustBalance]; } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_adjust_balance/account_adjust_balance_state.dart ================================================ part of 'account_adjust_balance_bloc.dart'; @immutable class AccountAdjustBalanceState extends Equatable { final FormzStatus status; final AdjustBalanceRequest request; const AccountAdjustBalanceState({ this.status = FormzStatus.pure, this.request = const AdjustBalanceRequest(), }); AccountAdjustBalanceState copyWith({ FormzStatus? status, AdjustBalanceRequest? request, }) { return AccountAdjustBalanceState( status: status ?? this.status, request: request ?? this.request, ); } @override List get props => [status, request]; } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_enable/account_enable_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; part 'account_enable_event.dart'; part 'account_enable_state.dart'; class AccountEnableBloc extends Bloc { final AccountRepository accountRepository; AccountEnableBloc({ required this.accountRepository }) : super(AccountEnableStateLoadInProgress()) { on(_onAccountEnableLoaded); } void _onAccountEnableLoaded(_, Emitter emit) async { // 只加载一次数据 if (state is AccountEnableStateLoadSuccess) return; emit(AccountEnableStateLoadInProgress()); final accounts = await accountRepository.getEnable(); emit(AccountEnableStateLoadSuccess(accounts)); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_enable/account_enable_event.dart ================================================ part of 'account_enable_bloc.dart'; abstract class AccountEnableEvent extends Equatable { const AccountEnableEvent(); @override List get props => []; } class AccountEnableLoaded extends AccountEnableEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_enable/account_enable_state.dart ================================================ part of 'account_enable_bloc.dart'; abstract class AccountEnableState extends Equatable { const AccountEnableState(); @override List get props => []; } class AccountEnableStateLoadInProgress extends AccountEnableState { } class AccountEnableStateLoadSuccess extends AccountEnableState { final List accounts; const AccountEnableStateLoadSuccess(this.accounts); @override List get props => [accounts]; } class AccountEnableStateLoadFailure extends AccountEnableState { } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_expenseable/account_expenseable_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; part 'account_expenseable_event.dart'; part 'account_expenseable_state.dart'; class AccountExpenseableBloc extends Bloc { final AccountRepository accountRepository; AccountExpenseableBloc({ required this.accountRepository }) : super(AccountExpenseableStateLoadInProgress()) { on(_onAccountExpenseableLoaded); } void _onAccountExpenseableLoaded(_, Emitter emit) async { // 只加载一次数据 if (state is AccountExpenseableStateLoadSuccess) return; try { emit(AccountExpenseableStateLoadInProgress()); final accounts = await accountRepository.getExpenseable(); emit(AccountExpenseableStateLoadSuccess(accounts)); } catch (_) { print(_); emit(AccountExpenseableStateLoadFailure()); } } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_expenseable/account_expenseable_event.dart ================================================ part of 'account_expenseable_bloc.dart'; abstract class AccountExpenseableEvent extends Equatable { const AccountExpenseableEvent(); @override List get props => []; } class AccountExpenseableLoaded extends AccountExpenseableEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_expenseable/account_expenseable_state.dart ================================================ part of 'account_expenseable_bloc.dart'; abstract class AccountExpenseableState extends Equatable { const AccountExpenseableState(); @override List get props => []; } class AccountExpenseableStateLoadInProgress extends AccountExpenseableState { } class AccountExpenseableStateLoadSuccess extends AccountExpenseableState { final List accounts; const AccountExpenseableStateLoadSuccess(this.accounts); @override List get props => [accounts]; } class AccountExpenseableStateLoadFailure extends AccountExpenseableState { } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_fetch/account_fetch_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import '/accounts/accounts.dart'; import '/commons/commons.dart'; part 'account_fetch_event.dart'; part 'account_fetch_state.dart'; class AccountFetchBloc extends Bloc { final AccountRepository accountRepository; AccountFetchBloc({ required this.accountRepository, }) : super(AccountFetchState()) { on(_onFetched); on(_onDefault); } void _onDefault(AccountLoadDefault event, Emitter emit) { emit(state.copyWith( status: LoadDataStatus.success, account: event.account )); } void _onFetched(_, Emitter emit) async { try { emit(state.copyWith(status: LoadDataStatus.progress)); final account = await accountRepository.get(state.account!.id); emit(state.copyWith( status: LoadDataStatus.success, account: account, )); } catch (_) { emit(state.copyWith(status: LoadDataStatus.failure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_fetch/account_fetch_event.dart ================================================ part of 'account_fetch_bloc.dart'; @immutable class AccountFetchEvent extends Equatable { const AccountFetchEvent(); @override List get props => []; } class AccountFetched extends AccountFetchEvent {} class AccountLoadDefault extends AccountFetchEvent { final Account account; const AccountLoadDefault({ required this.account, }); List get props => [account]; } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_fetch/account_fetch_state.dart ================================================ part of 'account_fetch_bloc.dart'; @immutable class AccountFetchState extends Equatable { final LoadDataStatus status; final Account? account; const AccountFetchState({ this.status = LoadDataStatus.initial, this.account, }); AccountFetchState copyWith({ LoadDataStatus? status, Account? account, }) { return AccountFetchState( status: status ?? this.status, account: account ?? this.account, ); } @override List get props => [status, account]; } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_form/account_form_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:formz/formz.dart'; import 'package:meta/meta.dart'; import '/commons/commons.dart'; import '/login/login.dart'; import '/accounts/accounts.dart'; part 'account_form_event.dart'; part 'account_form_state.dart'; class AccountFormBloc extends Bloc { final AccountRepository accountRepository; final AuthBloc authBloc; AccountFormBloc({ required this.accountRepository, required this.authBloc, }) : super(const AccountFormState()) { on(_onCurrencyCodeChanged); on(_onNameChanged); on(_onBalanceChanged); on(_onNoChanged); on(_onExpenseableChanged); on(_onIncomeableChanged); on(_onTransferFromAbleChanged); on(_onTransferToAbleChanged); on(_onIncludeChanged); on(_onNotesChanged); on(_onLimitChanged); on(_onBillDayChanged); on(_onAprChanged); on(_onSubmitted); on(_onDefaultLoaded); } void _onCurrencyCodeChanged(AccountFormCurrencyCodeChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(currencyCode: event.currencyCode), )); } void _onNameChanged(AccountFormNameChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(name: event.name), )); } void _onBalanceChanged(AccountFormBalanceChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(balance: event.balance), )); } void _onNoChanged(AccountFormNoChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(no: event.no), )); } void _onExpenseableChanged(AccountFormExpenseableChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(expenseable: event.expenseable), )); } void _onIncomeableChanged(AccountFormIncomeableChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(incomeable: event.incomeable), )); } void _onTransferFromAbleChanged(AccountFormTransferFromAbleChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(transferFromAble: event.transferFromAble), )); } void _onTransferToAbleChanged(AccountFormTransferToAbleChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(transferToAble: event.transferToAble), )); } void _onIncludeChanged(AccountFormIncludeChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(include: event.include), )); } void _onNotesChanged(AccountFormNotesChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(notes: event.notes), )); } void _onLimitChanged(AccountFormLimitChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(limit: event.limit), )); } void _onBillDayChanged(AccountFormBillDayChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(billDay: event.billDay), )); } void _onAprChanged(AccountFormAprChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(apr: event.apr), )); } void _onDefaultLoaded(AccountFormDefaultLoaded event, Emitter emit) { emit(state.copyWith( status: FormzStatus.pure, request: state.request.copyWith( currencyCode: event.account?.currencyCode ?? authBloc.state.session!.defaultGroup.defaultCurrencyCode, name: event.account?.name ?? '', balance: event.account != null ? removeDecimalZero(event.account!.balance) : null, no: event.account?.no ?? '', expenseable: event.account?.expenseable ?? event.accountType == 1 || event.accountType == 2 ? true : false, incomeable: event.account?.incomeable ?? event.accountType == 1 ? true : false, transferFromAble: event.account?.transferFromAble ?? true, transferToAble: event.account?.transferToAble ?? event.accountType != 2 ? true : false, include: event.account?.include ?? true, notes: event.account?.notes ?? '', limit: event.account != null && event.account!.limit != null ? removeDecimalZero(event.account!.limit!) : null, billDay: event.account != null && event.account!.billDay != null ? removeDecimalZero(event.account!.billDay!) : null, apr: event.account != null && event.account!.apr != null ? removeDecimalZero(event.account!.apr!) : null, ) )); } void _onSubmitted(AccountFormSubmitted event, Emitter emit) async { try { bool result = false; switch (event.type) { case 1: result = await accountRepository.add(event.accountType, state.request); break; case 2: result = await accountRepository.update(event.account!.id, state.request); break; default: break; } if (result) { emit(state.copyWith(status: FormzStatus.submissionSuccess)); } else { emit(state.copyWith(status: FormzStatus.submissionFailure)); } } catch (_) { emit(state.copyWith(status: FormzStatus.submissionFailure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_form/account_form_event.dart ================================================ part of 'account_form_bloc.dart'; @immutable abstract class AccountFormEvent extends Equatable { const AccountFormEvent(); @override List get props => []; } class AccountFormCurrencyCodeChanged extends AccountFormEvent { const AccountFormCurrencyCodeChanged(this.currencyCode); final String currencyCode; @override List get props => [currencyCode]; } class AccountFormNameChanged extends AccountFormEvent { const AccountFormNameChanged(this.name); final String name; @override List get props => [name]; } class AccountFormBalanceChanged extends AccountFormEvent { const AccountFormBalanceChanged(this.balance); final String balance; @override List get props => [balance]; } class AccountFormNoChanged extends AccountFormEvent { const AccountFormNoChanged(this.no); final String no; @override List get props => [no]; } class AccountFormExpenseableChanged extends AccountFormEvent { const AccountFormExpenseableChanged(this.expenseable); final bool expenseable; @override List get props => [expenseable]; } class AccountFormIncomeableChanged extends AccountFormEvent { const AccountFormIncomeableChanged(this.incomeable); final bool incomeable; @override List get props => [incomeable]; } class AccountFormTransferFromAbleChanged extends AccountFormEvent { const AccountFormTransferFromAbleChanged(this.transferFromAble); final bool transferFromAble; @override List get props => [transferFromAble]; } class AccountFormTransferToAbleChanged extends AccountFormEvent { const AccountFormTransferToAbleChanged(this.transferToAble); final bool transferToAble; @override List get props => [transferToAble]; } class AccountFormIncludeChanged extends AccountFormEvent { const AccountFormIncludeChanged(this.include); final bool include; @override List get props => [include]; } class AccountFormNotesChanged extends AccountFormEvent { const AccountFormNotesChanged(this.notes); final String notes; @override List get props => [notes]; } class AccountFormLimitChanged extends AccountFormEvent { const AccountFormLimitChanged(this.limit); final String limit; @override List get props => [limit]; } class AccountFormBillDayChanged extends AccountFormEvent { const AccountFormBillDayChanged(this.billDay); final String billDay; @override List get props => [billDay]; } class AccountFormAprChanged extends AccountFormEvent { const AccountFormAprChanged(this.apr); final String apr; @override List get props => [apr]; } class AccountFormDefaultLoaded extends AccountFormEvent { final int type; final int accountType; final Account? account; const AccountFormDefaultLoaded(this.type, this.accountType, this.account); @override List get props => [type, accountType, account]; } class AccountFormSubmitted extends AccountFormEvent { final int type; final int accountType; final Account? account; const AccountFormSubmitted(this.type, this.accountType, this.account); @override List get props => [type, accountType, account]; } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_form/account_form_state.dart ================================================ part of 'account_form_bloc.dart'; @immutable class AccountFormState extends Equatable { final FormzStatus status; final AccountFormRequest request; const AccountFormState({ this.status = FormzStatus.pure, this.request = const AccountFormRequest(), }); AccountFormState copyWith({ FormzStatus? status, AccountFormRequest? request, }) { return AccountFormState( status: status ?? this.status, request: request ?? this.request, ); } @override List get props => [status, request]; } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_incomeable/account_incomeable_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; part 'account_incomeable_event.dart'; part 'account_incomeable_state.dart'; class AccountIncomeableBloc extends Bloc { final AccountRepository accountRepository; AccountIncomeableBloc({ required this.accountRepository }) : super(AccountIncomeableStateLoadInProgress()) { on(_onAccountIncomeableLoaded); } void _onAccountIncomeableLoaded(_, Emitter emit) async { // 只加载一次数据 if (state is AccountIncomeableStateLoadSuccess) return; try { emit(AccountIncomeableStateLoadInProgress()); final accounts = await accountRepository.getIncomeable(); emit(AccountIncomeableStateLoadSuccess(accounts)); } catch (_) { emit(AccountIncomeableStateLoadFailure()); } } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_incomeable/account_incomeable_event.dart ================================================ part of 'account_incomeable_bloc.dart'; abstract class AccountIncomeableEvent extends Equatable { const AccountIncomeableEvent(); @override List get props => []; } class AccountIncomeableLoaded extends AccountIncomeableEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_incomeable/account_incomeable_state.dart ================================================ part of 'account_incomeable_bloc.dart'; abstract class AccountIncomeableState extends Equatable { const AccountIncomeableState(); @override List get props => []; } class AccountIncomeableStateLoadInProgress extends AccountIncomeableState { } class AccountIncomeableStateLoadSuccess extends AccountIncomeableState { final List accounts; const AccountIncomeableStateLoadSuccess(this.accounts); @override List get props => [accounts]; } class AccountIncomeableStateLoadFailure extends AccountIncomeableState { } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_transfer_from_able/account_transfer_from_able_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; part 'account_transfer_from_able_event.dart'; part 'account_transfer_from_able_state.dart'; class AccountTransferFromAbleBloc extends Bloc { final AccountRepository accountRepository; AccountTransferFromAbleBloc({ required this.accountRepository }) : super(AccountTransferFromAbleStateLoadInProgress()) { on(_onAccountTransferFromAbleLoaded); } void _onAccountTransferFromAbleLoaded(_, Emitter emit) async { // 只加载一次数据 if (state is AccountTransferFromAbleStateLoadSuccess) return; try { emit(AccountTransferFromAbleStateLoadInProgress()); final accounts = await accountRepository.getTransferFromAble(); emit(AccountTransferFromAbleStateLoadSuccess(accounts)); } catch (_) { emit(AccountTransferFromAbleStateLoadFailure()); } } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_transfer_from_able/account_transfer_from_able_event.dart ================================================ part of 'account_transfer_from_able_bloc.dart'; abstract class AccountTransferFromAbleEvent extends Equatable { const AccountTransferFromAbleEvent(); @override List get props => []; } class AccountTransferFromAbleLoaded extends AccountTransferFromAbleEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_transfer_from_able/account_transfer_from_able_state.dart ================================================ part of 'account_transfer_from_able_bloc.dart'; abstract class AccountTransferFromAbleState extends Equatable { const AccountTransferFromAbleState(); @override List get props => []; } class AccountTransferFromAbleStateLoadInProgress extends AccountTransferFromAbleState { } class AccountTransferFromAbleStateLoadSuccess extends AccountTransferFromAbleState { final List accounts; const AccountTransferFromAbleStateLoadSuccess(this.accounts); @override List get props => [accounts]; } class AccountTransferFromAbleStateLoadFailure extends AccountTransferFromAbleState { } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_transfer_to_able/account_transfer_to_able_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; part 'account_transfer_to_able_event.dart'; part 'account_transfer_to_able_state.dart'; class AccountTransferToAbleBloc extends Bloc { final AccountRepository accountRepository; AccountTransferToAbleBloc({ required this.accountRepository }) : super(AccountTransferToAbleStateLoadInProgress()) { on(_onAccountTransferToAbleLoaded); } void _onAccountTransferToAbleLoaded(_, Emitter emit) async { // 只加载一次数据 if (state is AccountTransferToAbleStateLoadSuccess) return; try { emit(AccountTransferToAbleStateLoadInProgress()); final accounts = await accountRepository.getTransferToAble(); emit(AccountTransferToAbleStateLoadSuccess(accounts)); } catch (_) { emit(AccountTransferToAbleStateLoadFailure()); } } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_transfer_to_able/account_transfer_to_able_event.dart ================================================ part of 'account_transfer_to_able_bloc.dart'; abstract class AccountTransferToAbleEvent extends Equatable { const AccountTransferToAbleEvent(); @override List get props => []; } class AccountTransferToAbleLoaded extends AccountTransferToAbleEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/account_transfer_to_able/account_transfer_to_able_state.dart ================================================ part of 'account_transfer_to_able_bloc.dart'; abstract class AccountTransferToAbleState extends Equatable { const AccountTransferToAbleState(); @override List get props => []; } class AccountTransferToAbleStateLoadInProgress extends AccountTransferToAbleState { } class AccountTransferToAbleStateLoadSuccess extends AccountTransferToAbleState { final List accounts; const AccountTransferToAbleStateLoadSuccess(this.accounts); @override List get props => [accounts]; } class AccountTransferToAbleStateLoadFailure extends AccountTransferToAbleState { } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/accounts/accounts_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; part 'accounts_event.dart'; part 'accounts_state.dart'; class AccountsBloc extends Bloc { final AccountRepository accountRepository; AccountsBloc({ required this.accountRepository, }) : super(AccountsState()) { on(_onRefreshed); on(_onLoadMore); on(_onTabChanged); on(_onSortChanged); on(_onDeleted); on(_onToggled); } void _onRefreshed(_, Emitter emit) async { try { emit(state.copyWith( status: LoadDataStatus.progress, request: state.request.copyWith(page: 1), )); final accounts = await accountRepository.query(state.request); emit(state.copyWith( status: LoadDataStatus.success, accounts: accounts, request: state.request.copyWith(page: state.request.page + 1), )); } catch (_) { emit(state.copyWith(status: LoadDataStatus.failure)); } } void _onLoadMore(_, Emitter emit) async { try { emit(state.copyWith(loadMoreStatus: LoadDataStatus.progress)); final accounts = await accountRepository.query(state.request); if (accounts.isNotEmpty) { emit(state.copyWith( request: state.request.copyWith(page: state.request.page + 1), accounts: List.of(state.accounts)..addAll(accounts), loadMoreStatus: LoadDataStatus.success, )); } else { emit(state.copyWith(loadMoreStatus: LoadDataStatus.empty)); } } catch (_) { emit(state.copyWith(loadMoreStatus: LoadDataStatus.failure)); } } void _onTabChanged(AccountsTabChanged event, Emitter emit) async { emit(state.copyWith( request: state.request.copyWith(type: event.tab+1), )); } void _onSortChanged(AccountsSortChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(sort: event.sort), )); } void _onDeleted(AccountDeleted event, Emitter emit) async { try { emit(state.copyWith(deleteStatus: LoadDataStatus.progress)); final result = await accountRepository.delete(event.id); if (result) { emit(state.copyWith(deleteStatus: LoadDataStatus.success)); } else { emit(state.copyWith(deleteStatus: LoadDataStatus.failure)); } } catch (_) { emit(state.copyWith(deleteStatus: LoadDataStatus.failure)); } } void _onToggled(AccountToggled event, Emitter emit) async { try { emit(state.copyWith(toggleStatus: LoadDataStatus.progress)); final result = await accountRepository.toggle(event.id); if (result) { emit(state.copyWith(toggleStatus: LoadDataStatus.success)); } else { emit(state.copyWith(toggleStatus: LoadDataStatus.failure)); } } catch (_) { emit(state.copyWith(toggleStatus: LoadDataStatus.failure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/accounts/accounts_event.dart ================================================ part of 'accounts_bloc.dart'; @immutable abstract class AccountsEvent extends Equatable { const AccountsEvent(); @override List get props => []; } class AccountsRefreshed extends AccountsEvent {} class AccountsLoadMore extends AccountsEvent {} class AccountDeleted extends AccountsEvent { final String id; const AccountDeleted(this.id); @override List get props => [id]; } class AccountToggled extends AccountsEvent { final String id; const AccountToggled(this.id); @override List get props => [id]; } class AccountsTabChanged extends AccountsEvent { const AccountsTabChanged(this.tab); final int tab; @override List get props => [tab]; } class AccountsSortChanged extends AccountsEvent { const AccountsSortChanged(this.sort); final String sort; @override List get props => [sort]; } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/bloc/accounts/accounts_state.dart ================================================ part of 'accounts_bloc.dart'; @immutable class AccountsState extends Equatable { final LoadDataStatus status; final List accounts; final AccountQueryRequest request; final LoadDataStatus loadMoreStatus; final LoadDataStatus deleteStatus; final LoadDataStatus toggleStatus; const AccountsState({ this.status = LoadDataStatus.initial, this.accounts = const [], this.request = const AccountQueryRequest(), this.loadMoreStatus = LoadDataStatus.initial, this.deleteStatus = LoadDataStatus.initial, this.toggleStatus = LoadDataStatus.initial, }); AccountsState copyWith({ LoadDataStatus? status, List? accounts, AccountQueryRequest? request, LoadDataStatus? loadMoreStatus, LoadDataStatus? deleteStatus, LoadDataStatus? toggleStatus, }) { return AccountsState( status: status ?? this.status, accounts: accounts ?? this.accounts, request: request ?? this.request, loadMoreStatus: loadMoreStatus ?? this.loadMoreStatus, deleteStatus: deleteStatus ?? this.deleteStatus, toggleStatus: toggleStatus ?? this.toggleStatus ); } @override List get props => [status, request, loadMoreStatus, deleteStatus, toggleStatus]; } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/data/account_repository.dart ================================================ import 'dart:convert'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; class AccountRepository { List _responseToList(String response) { return (json.decode(response)['data']).map((i) => Account.fromJson(i)).toList(); } Account _responseAccount(String response) { return Account.fromJson(json.decode(response)['data']); } List _responseToList2(String response) { return (json.decode(response)['data']['content']).map((i) => Account.fromJson(i)).toList(); } Future> getExpenseable() async { String response = await HttpClient().get('accounts/expenseable'); return _responseToList(response); } Future> getIncomeable() async { String response = await HttpClient().get('accounts/incomeable'); return _responseToList(response); } Future> getTransferFromAble() async { String response = await HttpClient().get('accounts/transfer-from-able'); return _responseToList(response); } Future> getTransferToAble() async { String response = await HttpClient().get('accounts/transfer-to-able'); return _responseToList(response); } Future> getEnable() async { String response = await HttpClient().get('accounts/enable'); return _responseToList(response); } Future> query(AccountQueryRequest request) async { switch(request.type) { case 1: return _responseToList2(await HttpClient().get('checking-accounts', params: request.toJson())); case 2: return _responseToList2(await HttpClient().get('credit-accounts', params: request.toJson())); case 3: return _responseToList2(await HttpClient().get('debt-accounts', params: request.toJson())); case 4: return _responseToList2(await HttpClient().get('asset-accounts', params: request.toJson())); default: throw('type error'); } } Future get(int id) async { return _responseAccount(await HttpClient().get('accounts/$id')); } Future delete(String id) async { String response = await HttpClient().delete('accounts/$id'); return parseResponse(response); } Future toggle(String id) async { String response = await HttpClient().put('accounts/$id/toggle'); return parseResponse(response); } Future add(int type, AccountFormRequest request) async { String response = ''; switch(type) { case 1: response = await HttpClient().post('checking-accounts', data: request.toJson()); return parseResponse(response); case 2: response = await HttpClient().post('credit-accounts', data: request.toJson()); return parseResponse(response); case 3: response = await HttpClient().post('debt-accounts', data: request.toJson()); return parseResponse(response); case 4: response = await HttpClient().post('asset-accounts', data: request.toJson()); return parseResponse(response); default: throw('type error'); } } Future update(int id, AccountFormRequest request) async { String response = await HttpClient().put('accounts/$id', data: request.toJson()); return parseResponse(response); } Future adjustBalance(int id, AdjustBalanceRequest request) async { String response = await HttpClient().post('accounts/$id/adjust-balance', data: request.toJson()); return parseResponse(response); } Future updateAdjustBalance(int id, AdjustBalanceRequest request) async { String response = await HttpClient().put('adjust-balances/$id', data: request.toJson()); return parseResponse(response); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/data/models/account.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'account.g.dart'; @JsonSerializable() class Account extends Equatable { final int id; final int type; final String typeName; final String name; final String? no; final double balance; final String currencyCode; final double? convertedBalance; final bool enable; final bool include; final bool expenseable; final bool incomeable; final bool transferFromAble; final bool transferToAble; final double initialBalance; final String? notes; //credit final double? limit; final int? billDay; final double? remainLimit; final double? apr; final int? asOfDate; Account({ required this.id, required this.type, required this.typeName, required this.name, required this.balance, required this.currencyCode, required this.convertedBalance, required this.enable, required this.include, required this.expenseable, required this.incomeable, required this.transferFromAble, required this.transferToAble, required this.initialBalance, this.no, this.notes, this.limit, this.billDay, this.remainLimit, this.apr, this.asOfDate }); factory Account.fromJson(Map json) => _$AccountFromJson(json); Map toJson() => _$AccountToJson(this); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/data/models/account.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'account.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Account _$AccountFromJson(Map json) => Account( id: json['id'] as int, type: json['type'] as int, typeName: json['typeName'] as String, name: json['name'] as String, balance: (json['balance'] as num).toDouble(), currencyCode: json['currencyCode'] as String, convertedBalance: (json['convertedBalance'] as num?)?.toDouble(), enable: json['enable'] as bool, include: json['include'] as bool, expenseable: json['expenseable'] as bool, incomeable: json['incomeable'] as bool, transferFromAble: json['transferFromAble'] as bool, transferToAble: json['transferToAble'] as bool, initialBalance: (json['initialBalance'] as num).toDouble(), no: json['no'] as String?, notes: json['notes'] as String?, limit: (json['limit'] as num?)?.toDouble(), billDay: json['billDay'] as int?, remainLimit: (json['remainLimit'] as num?)?.toDouble(), apr: (json['apr'] as num?)?.toDouble(), asOfDate: json['asOfDate'] as int?, ); Map _$AccountToJson(Account instance) => { 'id': instance.id, 'type': instance.type, 'typeName': instance.typeName, 'name': instance.name, 'no': instance.no, 'balance': instance.balance, 'currencyCode': instance.currencyCode, 'convertedBalance': instance.convertedBalance, 'enable': instance.enable, 'include': instance.include, 'expenseable': instance.expenseable, 'incomeable': instance.incomeable, 'transferFromAble': instance.transferFromAble, 'transferToAble': instance.transferToAble, 'initialBalance': instance.initialBalance, 'notes': instance.notes, 'limit': instance.limit, 'billDay': instance.billDay, 'remainLimit': instance.remainLimit, 'apr': instance.apr, 'asOfDate': instance.asOfDate, }; ================================================ FILE: bookkeeping_user_flutter/lib/accounts/data/models/account_form_request.dart ================================================ import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; part 'account_form_request.g.dart'; @JsonSerializable() class AccountFormRequest extends Equatable { final String? currencyCode; final String? name; final String? no; // 卡号 final String? balance; final bool? include; final bool? transferFromAble; final bool? transferToAble; final bool? expenseable; final bool? incomeable; final String? notes; final String? limit; final String? billDay; final String? apr; const AccountFormRequest({ this.currencyCode, this.name, this.no, this.balance, this.include, this.transferFromAble, this.transferToAble, this.expenseable, this.incomeable, this.notes, this.limit, this.billDay, this.apr, }); Map toJson() => _$AccountFormRequestToJson(this); AccountFormRequest copyWith({ String? currencyCode, String? name, String? no, String? balance, bool? include, bool? transferFromAble, bool? transferToAble, bool? expenseable, bool? incomeable, String? notes, String? limit, String? billDay, String? apr, }) { return AccountFormRequest( currencyCode: currencyCode ?? this.currencyCode, name: name ?? this.name, no: no ?? this.no, balance: balance ?? this.balance, include: include ?? this.include, transferFromAble: transferFromAble ?? this.transferFromAble, transferToAble: transferToAble ?? this.transferToAble, expenseable: expenseable ?? this.expenseable, incomeable: incomeable ?? this.incomeable, notes: notes ?? this.notes, limit: limit ?? this.limit, billDay: billDay ?? this.billDay, apr: apr ?? this.apr, ); } @override List get props => [currencyCode, name, no, balance, expenseable, incomeable, transferFromAble, transferToAble, include, notes, limit, billDay, apr]; } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/data/models/account_form_request.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'account_form_request.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** AccountFormRequest _$AccountFormRequestFromJson(Map json) => AccountFormRequest( currencyCode: json['currencyCode'] as String?, name: json['name'] as String?, no: json['no'] as String?, balance: json['balance'] as String?, include: json['include'] as bool?, transferFromAble: json['transferFromAble'] as bool?, transferToAble: json['transferToAble'] as bool?, expenseable: json['expenseable'] as bool?, incomeable: json['incomeable'] as bool?, notes: json['notes'] as String?, limit: json['limit'] as String?, billDay: json['billDay'] as String?, apr: json['apr'] as String?, ); Map _$AccountFormRequestToJson(AccountFormRequest instance) => { 'currencyCode': instance.currencyCode, 'name': instance.name, 'no': instance.no, 'balance': instance.balance, 'include': instance.include, 'transferFromAble': instance.transferFromAble, 'transferToAble': instance.transferToAble, 'expenseable': instance.expenseable, 'incomeable': instance.incomeable, 'notes': instance.notes, 'limit': instance.limit, 'billDay': instance.billDay, 'apr': instance.apr, }; ================================================ FILE: bookkeeping_user_flutter/lib/accounts/data/models/account_query_request.dart ================================================ import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; part 'account_query_request.g.dart'; @JsonSerializable() class AccountQueryRequest extends Equatable { final int type; final int page; final int size; final String sort; const AccountQueryRequest({ this.type = 1, this.page = 1, this.size = 10, this.sort = '' }); factory AccountQueryRequest.fromJson(Map json) => _$AccountQueryRequestFromJson(json); Map toJson() => _$AccountQueryRequestToJson(this); @override List get props => [type, page, sort]; AccountQueryRequest copyWith({ int? type, int? page, int? size, String? sort, }) { return AccountQueryRequest( type: type ?? this.type, page: page ?? this.page, size: size ?? this.size, sort: sort ?? this.sort, ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/data/models/account_query_request.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'account_query_request.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** AccountQueryRequest _$AccountQueryRequestFromJson(Map json) => AccountQueryRequest( type: json['type'] as int? ?? 1, page: json['page'] as int? ?? 1, size: json['size'] as int? ?? 10, sort: json['sort'] as String? ?? '', ); Map _$AccountQueryRequestToJson( AccountQueryRequest instance) => { 'type': instance.type, 'page': instance.page, 'size': instance.size, 'sort': instance.sort, }; ================================================ FILE: bookkeeping_user_flutter/lib/accounts/data/models/adjust_balance_request.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'adjust_balance_request.g.dart'; @JsonSerializable() class AdjustBalanceRequest extends Equatable { final String? description; final int? createTime; final String? balance; final String? convertedBalance; final String? notes; const AdjustBalanceRequest({ this.description, this.createTime, this.balance, this.convertedBalance, this.notes, }); AdjustBalanceRequest copyWith({ String? description, int? createTime, String? balance, String? convertedBalance, String? notes, }) { return AdjustBalanceRequest( description: description ?? this.description, createTime: createTime ?? this.createTime, balance: balance ?? this.balance, convertedBalance: convertedBalance ?? this.convertedBalance, notes: notes ?? this.notes, ); } factory AdjustBalanceRequest.fromJson(Map json) => _$AdjustBalanceRequestFromJson(json); Map toJson() => _$AdjustBalanceRequestToJson(this); @override List get props => [description, createTime, balance, convertedBalance, notes]; } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/data/models/adjust_balance_request.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'adjust_balance_request.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** AdjustBalanceRequest _$AdjustBalanceRequestFromJson( Map json) => AdjustBalanceRequest( description: json['description'] as String?, createTime: json['createTime'] as int?, balance: json['balance'] as String?, convertedBalance: json['convertedBalance'] as String?, notes: json['notes'] as String?, ); Map _$AdjustBalanceRequestToJson( AdjustBalanceRequest instance) => { 'description': instance.description, 'createTime': instance.createTime, 'balance': instance.balance, 'convertedBalance': instance.convertedBalance, 'notes': instance.notes, }; ================================================ FILE: bookkeeping_user_flutter/lib/accounts/data/models/credit_account.dart ================================================ // import 'package:json_annotation/json_annotation.dart'; // import 'account.dart'; // // part 'credit_account.g.dart'; // // @JsonSerializable() // // // class CreditAccount extends Account { // // final double limit; // final int billDay; // } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/account_adjust_balance.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; class AccountAdjustBalance extends StatelessWidget { final Account account; AccountAdjustBalance({ required this.account, }); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('调整账户余额'), actions: [ Builder(builder: (context) { return IconButton( icon: Icon(Icons.done), onPressed: () { BlocProvider.of(context).add(AccountAdjustBalanceSubmitted(1, account.id, null)); } ); }) ] ), body: AdjustBalanceForm(type: 1, account: account), ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/account_detail_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/components/components.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; import '/login/login.dart'; class AccountDetailPage extends StatefulWidget { final Account account; AccountDetailPage({ required this.account }); @override State createState() => _AccountDetailPageState(); } class _AccountDetailPageState extends State { @override void initState() { BlocProvider.of(context).add(AccountLoadDefault(account: widget.account)); super.initState(); } @override Widget build(BuildContext context) { return MultiBlocListener( listeners: [ BlocListener( listenWhen: (previous, current) => previous.deleteStatus != current.deleteStatus, listener: (context, state) { if (state.deleteStatus == LoadDataStatus.success) { Message.success('操作成功!'); // Navigator.pop(context); if (Navigator.canPop(context)) { Navigator.of(context).pop(); } else { SystemNavigator.pop(); } } } ), BlocListener( listenWhen: (previous, current) => previous.toggleStatus != current.toggleStatus, listener: (context, state) { if (state.toggleStatus == LoadDataStatus.success) { Message.success('操作成功!'); BlocProvider.of(context).add(AccountFetched()); } }, ) ], child: BlocBuilder( builder: (context, state) { return Scaffold( appBar: AppBar( centerTitle: true, title: const Text('账户详情'), actions: _buildActions(context, state.account ?? widget.account) ), body: Builder( builder: (context) { switch (state.status) { case LoadDataStatus.progress: case LoadDataStatus.initial: return const PageLoading(); case LoadDataStatus.success: return _buildBody(context, state.account ?? widget.account); default: return PageError(onTap: () { BlocProvider.of(context).add(AccountFetched()); }); } }, ) ); } ) ); } List _buildActions(BuildContext context, Account account) { return [ IconButton( icon: Icon(Icons.edit), onPressed: () { fullDialog(context, AccountFormPage(type: 2, accountType: account.type, account: account)); } ), IconButton( icon: Icon(Icons.delete), onPressed: () async { if (await confirm( context, content: Text("确定删除${account.name}吗?"), textOK: Text("确定"), textCancel: Text("取消"), )) { BlocProvider.of(context).add(AccountDeleted(account.id.toString())); } } ) ]; } Widget _buildBody(BuildContext context, Account account) { final theme = Theme.of(context); TextStyle? style1 = theme.textTheme.bodyText2; TextStyle? style2 = theme.textTheme.bodyText1; final state = context.watch().state; return SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ SizedBox(height: 20), Row(children: [Text("账户类型:", style: style1), Text(account.typeName, style: style2)]), SizedBox(height: 15), Row(children: [Text("账户名称:", style: style1), Text(account.name, style: style2)]), Row(children: [ Text("余额:", style: style1), Text(removeDecimalZero(account.balance), style: style2), SizedBox(width: 10), TextButton( child: const Text('余额调整'), onPressed: () { fullDialog(context, AccountAdjustBalance(account: account)); } ), ]), Row(children: [Text("币种:", style: style1), Text(account.currencyCode, style: style2)]), SizedBox(height: 15), if (account.currencyCode != state.session?.defaultGroup.defaultCurrencyCode) Row(children: [Text("折合${state.session?.defaultGroup.defaultCurrencyCode}:", style: style1), Text(removeDecimalZero(account.convertedBalance!), style: style2)]), if (account.currencyCode != state.session?.defaultGroup.defaultCurrencyCode) SizedBox(height: 15), if (account.limit != null) Row(children: [Text("信用额度:", style: style1), Text(removeDecimalZero(account.limit!), style: style2)]), if (account.limit != null) SizedBox(height: 15), if (account.remainLimit != null) Row(children: [Text("剩余额度:", style: style1), Text(removeDecimalZero(account.remainLimit!), style: style2)]), if (account.remainLimit != null) SizedBox(height: 15), if (account.type == 2) Row(children: [Text("账单日:", style: style1), Text(account.billDay?.toString() ?? '', style: style2)]), if (account.type == 2) SizedBox(height: 15), if (account.type == 3) Row(children: [Text("年化利率(%):", style: style1), Text(account.apr?.toString() ?? '', style: style2)]), if (account.type == 3) SizedBox(height: 15), if (account.type == 4) Row(children: [Text("更新日期:", style: style1), Text(dateFormat(account.asOfDate), style: style2)]), if (account.type == 4) SizedBox(height: 15), Row(children: [Text("是否可用:", style: style1), Text(boolToString(account.enable), style: style2)]), SizedBox(height: 15), Row(children: [Text("是否计入净资产:", style: style1), Text(boolToString(account.include), style: style2)]), SizedBox(height: 15), Row(children: [Text("是否可支出:", style: style1), Text(boolToString(account.expenseable), style: style2)]), SizedBox(height: 15), Row(children: [Text("是否可收入:", style: style1), Text(boolToString(account.incomeable), style: style2)]), SizedBox(height: 15), Row(children: [Text("是否可转入:", style: style1), Text(boolToString(account.transferToAble), style: style2)]), SizedBox(height: 15), Row(children: [Text("是否可转出:", style: style1), Text(boolToString(account.transferFromAble), style: style2)]), SizedBox(height: 15), Row(children: [Text("初始余额:", style: style1), Text(removeDecimalZero(account.initialBalance), style: style2)]), SizedBox(height: 15), Row(children: [Text("备注:", style: style1), Flexible(child: Text(account.notes ?? '', style: style2))]), account.no != null && account.no!.isNotEmpty ? SizedBox(height: 0) : SizedBox(height: 15), Row(children: [ Text("卡号:", style: style1), Flexible(child: Text(account.no ?? '', style: style2)), SizedBox(width: 10), account.no != null && account.no!.isNotEmpty ? TextButton( child: const Text('复制卡号'), onPressed: () { Clipboard.setData(ClipboardData(text: account.no)).then((_) => Message.success('卡号复制成功')); } ) : Text(''), ]), account.no != null && account.no!.isNotEmpty ? SizedBox(height: 0) : SizedBox(height: 15), SizedBox(height: 5), SizedBox( width: double.infinity, child: ElevatedButton( child: Text(account.enable ? '禁用' : '启用'), onPressed: () { BlocProvider.of(context).add(AccountToggled(account.id.toString())); } ), ), SizedBox(height: 15), ], ) ), ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/account_form_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import '/components/components.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; import '/login/login.dart'; class AccountFormPage extends StatelessWidget { final int type; // 1-新增,2-修改 final int accountType; final Account? account; const AccountFormPage({ required this.type, required this.accountType, this.account, }); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => AccountFormBloc( accountRepository: RepositoryProvider.of(context), authBloc: BlocProvider.of(context), )..add(AccountFormDefaultLoaded(type, accountType, account) ), child: BlocListener( listener: (context, state) { if (state.status == FormzStatus.submissionSuccess) { Message.success('操作成功'); Navigator.of(context).pop(); BlocProvider.of(context).add(AccountsRefreshed()); if (type == 2) { BlocProvider.of(context).add(AccountFetched()); } } }, child: Builder( builder: (context) { switch (accountType) { case 1: return CheckingAccountFormPage(type: type, accountType: accountType, account: account); case 2: return CreditAccountFormPage(type: type, accountType: accountType, account: account); case 3: return DebtAccountFormPage(type: type, accountType: accountType, account: account); case 4: return AssetAccountFormPage(type: type, accountType: accountType, account: account); default: return PageError(msg: '账户类型错误'); } }, ) ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/accounts_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import '/routes.dart'; import '/commons/commons.dart'; import '/components/components.dart'; import '/accounts/accounts.dart'; class AccountsPage extends StatefulWidget { @override State createState() => _AccountsPageState(); } // https://stackoverflow.com/questions/68013459/how-to-use-multiple-tab-for-single-page-in-flutter class _AccountsPageState extends State with TickerProviderStateMixin { late TabController tabController; RefreshController _refreshController = RefreshController(initialRefresh: false); @override void initState() { BlocProvider.of(context).add(AccountsRefreshed()); tabController = TabController(length: 4, vsync: this); tabController.addListener(() { if(!tabController.indexIsChanging) { BlocProvider.of(context).add(AccountsTabChanged(tabController.index)); BlocProvider.of(context).add(AccountsRefreshed()); } }); super.initState(); } @override void dispose() { tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return MultiBlocListener( listeners: [ BlocListener( listenWhen: (previous, current) => previous.loadMoreStatus != current.loadMoreStatus, listener: (context, state) { if (state.loadMoreStatus == LoadDataStatus.success) { _refreshController.loadComplete(); } else if (state.loadMoreStatus == LoadDataStatus.failure) { _refreshController.loadFailed(); } else if (state.loadMoreStatus == LoadDataStatus.empty) { _refreshController.loadNoData(); } }, ), BlocListener( listenWhen: (previous, current) => previous.deleteStatus != current.deleteStatus, listener: (context, state) { if (state.deleteStatus == LoadDataStatus.success) { BlocProvider.of(context).add(AccountsRefreshed()); } }, ), BlocListener( listenWhen: (previous, current) => previous.toggleStatus != current.toggleStatus, listener: (context, state) { if (state.toggleStatus == LoadDataStatus.success) { BlocProvider.of(context).add(AccountsRefreshed()); } }, ), ], child: Scaffold( appBar: AppBar( leading: IconButton( onPressed: () { BlocProvider.of(context).add(AccountsRefreshed()); }, icon: const Icon(Icons.refresh) ), centerTitle: true, title: TabBar( controller: tabController, labelPadding: EdgeInsets.all(0), tabs: [ Tab(child: Text('活期', softWrap: false)), Tab(child: Text('信用', softWrap: false)), Tab(child: Text('贷款', softWrap: false)), Tab(child: Text('资产', softWrap: false)), ], ), actions: [ OrderButton(), IconButton( icon: const Icon(Icons.add), onPressed: () { fullDialog(context, AccountFormPage(type: 1, accountType: tabController.index + 1)); } ) ] ), body: BlocBuilder( buildWhen: (previous, current) => previous.status != current.status || previous.accounts != current.accounts, builder: (context, state) { switch (state.status) { case LoadDataStatus.progress: case LoadDataStatus.initial: return const PageLoading(); case LoadDataStatus.success: if (state.accounts.isEmpty) return Empty(); return SmartRefresher( enablePullDown: true, enablePullUp: true, controller: _refreshController, child: _buildList(context, state.accounts), onRefresh: () async { BlocProvider.of(context).add(AccountsRefreshed()); _refreshController.refreshCompleted(); }, onLoading: () async { BlocProvider.of(context).add(AccountsLoadMore()); }, ); default: return PageError(onTap: () { BlocProvider.of(context).add(AccountsRefreshed()); }); } } ) ) ); } Widget _buildList(BuildContext context, List accounts) { final theme = Theme.of(context); return ListView.separated( itemCount: accounts.length, itemBuilder: (context, index) { Account account = accounts[index]; return ListTile( dense: false, title: Text(account.name, style: theme.textTheme.bodyText1), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Text(account.balance.toStringAsFixed(2), style: TextStyle(fontWeight: FontWeight.w500, fontSize: 17)), Icon(Icons.keyboard_arrow_right) ], ), onTap: () { Navigator.pushNamed(context, '/account-detail', arguments: AccountDetailArguments(account: account)); }, onLongPress: () { fullDialog(context, AccountAdjustBalance(account: account)); }, ); }, separatorBuilder: (context, index) { return Divider(); }, ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/asset_account_form_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; import '/login/login.dart'; class AssetAccountFormPage extends StatelessWidget { final int type; // 1-新增,2-修改 final int accountType; final Account? account; const AssetAccountFormPage({ required this.type, required this.accountType, this.account }); @override Widget build(BuildContext context) { final authState = context.watch().state; final state = context.watch().state; return Scaffold( appBar: AppBar( centerTitle: true, title: Text(_buildTitle(type)), actions: [ IconButton( icon: Icon(Icons.done), onPressed: () { BlocProvider.of(context).add(AccountFormSubmitted(type, accountType, account)); } ) ] ), body: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ if (type == 1) CurrencyInput(), NameInput(), if (type == 1) SizedBox(height: 10), if (type == 1) AccountBalanceInput(), if (type == 1) SizedBox(height: 10), ExpenseableSwitch(), SizedBox(height: 10), IncomeableSwitch(), SizedBox(height: 10), TransferFromAbleSwitch(), SizedBox(height: 10), TransferToAbleSwitch(), SizedBox(height: 10), IncludeSwitch(), SizedBox(height: 10), NotesInput() ], ), ), ) ); } String _buildTitle(int type) { switch (type) { case 1: return '新增资产账户'; case 2: return '修改资产账户'; default: return '账户操作类型异常'; } } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/checking_account_form_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; import '/login/login.dart'; class CheckingAccountFormPage extends StatelessWidget { final int type; // 1-新增,2-修改 final int accountType; final Account? account; const CheckingAccountFormPage({ required this.type, required this.accountType, this.account }); @override Widget build(BuildContext context) { final authState = context.watch().state; final state = context.watch().state; return Scaffold( appBar: AppBar( centerTitle: true, title: Text(_buildTitle(type)), actions: [ IconButton( icon: Icon(Icons.done), onPressed: () { BlocProvider.of(context).add(AccountFormSubmitted(type, accountType, account)); } ) ] ), body: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ if (type == 1) CurrencyInput(), NameInput(), if (type == 1) SizedBox(height: 10), if (type == 1) AccountBalanceInput(), if (type == 1) SizedBox(height: 10), NoInput(), SizedBox(height: 10), ExpenseableSwitch(), SizedBox(height: 10), IncomeableSwitch(), SizedBox(height: 10), TransferFromAbleSwitch(), SizedBox(height: 10), TransferToAbleSwitch(), SizedBox(height: 10), IncludeSwitch(), SizedBox(height: 10), NotesInput() ], ), ), ) ); } String _buildTitle(int type) { switch (type) { case 1: return '新增活期账户'; case 2: return '修改活期账户'; default: return '账户操作类型异常'; } } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/credit_account_form_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; import '/login/login.dart'; class CreditAccountFormPage extends StatelessWidget { final int type; // 1-新增,2-修改 final int accountType; final Account? account; const CreditAccountFormPage({ required this.type, required this.accountType, this.account }); @override Widget build(BuildContext context) { final authState = context.watch().state; final state = context.watch().state; return Scaffold( appBar: AppBar( centerTitle: true, title: Text(_buildTitle(type)), actions: [ IconButton( icon: Icon(Icons.done), onPressed: () { BlocProvider.of(context).add(AccountFormSubmitted(type, accountType, account)); } ) ] ), body: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ if (type == 1) CurrencyInput(), NameInput(), if (type == 1) SizedBox(height: 10), if (type == 1) AccountBalanceInput(), if (type == 1) SizedBox(height: 10), AccountLimitInput(), SizedBox(height: 10), AccountBillDayInput(), SizedBox(height: 10), NoInput(), SizedBox(height: 10), ExpenseableSwitch(), SizedBox(height: 10), IncomeableSwitch(), SizedBox(height: 10), TransferFromAbleSwitch(), SizedBox(height: 10), TransferToAbleSwitch(), SizedBox(height: 10), IncludeSwitch(), SizedBox(height: 10), NotesInput() ], ), ), ) ); } String _buildTitle(int type) { switch (type) { case 1: return '新增信用账户'; case 2: return '修改信用账户'; default: return '账户操作类型异常'; } } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/debt_account_form_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; import '/login/login.dart'; class DebtAccountFormPage extends StatelessWidget { final int type; // 1-新增,2-修改 final int accountType; final Account? account; const DebtAccountFormPage({ required this.type, required this.accountType, this.account }); @override Widget build(BuildContext context) { final authState = context.watch().state; final state = context.watch().state; return Scaffold( appBar: AppBar( centerTitle: true, title: Text(_buildTitle(type)), actions: [ IconButton( icon: Icon(Icons.done), onPressed: () { BlocProvider.of(context).add(AccountFormSubmitted(type, accountType, account)); } ) ] ), body: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ if (type == 1) CurrencyInput(), NameInput(), if (type == 1) SizedBox(height: 10), if (type == 1) AccountBalanceInput(), if (type == 1) SizedBox(height: 10), AccountLimitInput(), SizedBox(height: 10), AccountAprInput(), SizedBox(height: 10), NoInput(), SizedBox(height: 10), ExpenseableSwitch(), SizedBox(height: 10), IncomeableSwitch(), SizedBox(height: 10), TransferFromAbleSwitch(), SizedBox(height: 10), TransferToAbleSwitch(), SizedBox(height: 10), IncludeSwitch(), SizedBox(height: 10), NotesInput() ], ), ), ) ); } String _buildTitle(int type) { switch (type) { case 1: return '新增贷款账户'; case 2: return '修改贷款账户'; default: return '账户操作类型异常'; } } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/account_form/account_apr_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; class AccountAprInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '年化利率', BlocSelector( selector: (state) => state.request.apr, builder: (context, state) { controller.value = TextEditingValue( text: state ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(AccountFormAprChanged(value)), keyboardType: TextInputType.number, decoration: InputDecoration( contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0), ), ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/account_form/account_balance_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; class AccountBalanceInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '余额', BlocSelector( selector: (state) => state.request.balance, builder: (context, state) { controller.value = TextEditingValue( text: state ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(AccountFormBalanceChanged(value)), keyboardType: TextInputType.number, decoration: InputDecoration( contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0), ), ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/account_form/account_billday_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; class AccountBillDayInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '账单日', BlocSelector( selector: (state) => state.request.billDay, builder: (context, state) { controller.value = TextEditingValue( text: state ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(AccountFormBillDayChanged(value)), keyboardType: TextInputType.number, decoration: InputDecoration( contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0), ), ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/account_form/account_limit_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; class AccountLimitInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '额度', BlocSelector( selector: (state) => state.request.limit, builder: (context, state) { controller.value = TextEditingValue( text: state ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(AccountFormLimitChanged(value)), keyboardType: TextInputType.number, decoration: InputDecoration( contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0), ), ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/account_form/currency_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; import '/currency/currency.dart'; class CurrencyInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector( selector: (state) => state.request.currencyCode, builder: (context, state) { return SmartSelect.single( key: Key(new DateTime.now().millisecondsSinceEpoch.toString()), title: '币种', selectedValue: state ?? '', onChange: (selected) { context.read().add(AccountFormCurrencyCodeChanged(selected.value ?? '')); }, choiceItems: state1 is CurrencyAllStateLoadSuccess ? modelToChoiceCode(state1.currencies) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto: true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is CurrencyAllStateLoadInProgress, padding: EdgeInsets.zero, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/account_form/expenseable_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; import '/commons/commons.dart'; class ExpenseableSwitch extends StatelessWidget { @override Widget build(BuildContext context) { return buildFormItem( "是否可支出", BlocSelector( selector: (state) => state.request.expenseable, builder: (context, state) { return Align( alignment: Alignment.centerLeft, child: Switch( value: state ?? true, onChanged: (value) => context.read().add(AccountFormExpenseableChanged(value)), ), ); } ), context); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/account_form/inclue_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; import '/commons/commons.dart'; class IncludeSwitch extends StatelessWidget { @override Widget build(BuildContext context) { return buildFormItem( "是否计入净资产", BlocSelector( selector: (state) => state.request.include, builder: (context, state) { return Align( alignment: Alignment.centerLeft, child: Switch( value: state ?? true, onChanged: (value) => context.read().add(AccountFormIncludeChanged(value)), ), ); } ), context); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/account_form/incomeable_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; import '/commons/commons.dart'; class IncomeableSwitch extends StatelessWidget { @override Widget build(BuildContext context) { return buildFormItem( "是否可收入", BlocSelector( selector: (state) => state.request.incomeable, builder: (context, state) { return Align( alignment: Alignment.centerLeft, child: Switch( value: state ?? true, onChanged: (value) => context.read().add(AccountFormIncomeableChanged(value)), ), ); } ), context); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/account_form/name_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; class NameInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '名称', BlocBuilder( buildWhen: (previous, current) => previous.request.name != current.request.name, builder: (context, state) { controller.value = TextEditingValue( text: state.request.name ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state.request.name?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(AccountFormNameChanged(value)), decoration: InputDecoration( contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0), ), ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/account_form/no_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; import '/commons/commons.dart'; class NoInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '卡号', BlocBuilder( buildWhen: (previous, current) => previous.request.no != current.request.no, builder: (context, state) { controller.value = TextEditingValue( text: state.request.no ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state.request.no?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(AccountFormNoChanged(value)), decoration: InputDecoration(), ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/account_form/notes_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; import '/commons/commons.dart'; class NotesInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '备注', BlocBuilder( buildWhen: (previous, current) => previous.request.notes != current.request.notes, builder: (context, state) { controller.value = TextEditingValue( text: state.request.notes ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state.request.notes?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(AccountFormNotesChanged(value)), decoration: InputDecoration(), ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/account_form/transfer_from_able_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; import '/commons/commons.dart'; class TransferFromAbleSwitch extends StatelessWidget { @override Widget build(BuildContext context) { return buildFormItem( "是否可转入", BlocSelector( selector: (state) => state.request.transferFromAble, builder: (context, state) { return Align( alignment: Alignment.centerLeft, child: Switch( value: state ?? true, onChanged: (value) => context.read().add(AccountFormTransferFromAbleChanged(value)), ), ); } ), context); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/account_form/transfer_to_able_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/accounts/accounts.dart'; import '/commons/commons.dart'; class TransferToAbleSwitch extends StatelessWidget { @override Widget build(BuildContext context) { return buildFormItem( "是否可转出", BlocSelector( selector: (state) => state.request.transferToAble, builder: (context, state) { return Align( alignment: Alignment.centerLeft, child: Switch( value: state ?? true, onChanged: (value) => context.read().add(AccountFormTransferToAbleChanged(value)), ), ); } ), context); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/adjust_balance/adjust_balance_form.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import '/accounts/accounts.dart'; import '/flows/flows.dart'; import '/commons/commons.dart'; class AdjustBalanceForm extends StatefulWidget { final int type; // 1-新增,2-修改,3-复制,4-退款 final Account? account; final AdjustBalance? adjustBalance; AdjustBalanceForm({ required this.type, this.account, this.adjustBalance }); @override State createState() => _AdjustBalanceFormState(); } class _AdjustBalanceFormState extends State { @override void initState() { BlocProvider.of(context).add(AccountAdjustBalanceDefaultLoaded(widget.type, widget.adjustBalance)); super.initState(); } @override Widget build(BuildContext context) { return BlocListener( listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { if (state.status == FormzStatus.submissionSuccess) { Message.success('操作成功'); Navigator.of(context).pop(); BlocProvider.of(context).add(FlowsRefreshed()); if (widget.type == 1) { BlocProvider.of(context).add(AccountFetched()); BlocProvider.of(context).add(AccountsRefreshed()); } // 如果是修改和退款,要刷新账单详情页 if (widget.type == 2) { BlocProvider.of(context).add(FlowFetched()); } } }, child: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ DescriptionInput(), SizedBox(height: 10), AdjustBlanceDateTimeInput(), SizedBox(height: 10), if (widget.type == 1) BalanceInput(currentBalance: widget.account!.balance), SizedBox(height: 10), NoteInput() ], ), ) ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/adjust_balance/balance_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; class BalanceInput extends StatelessWidget { final num currentBalance; BalanceInput({ required this.currentBalance, }); @override Widget build(BuildContext context) { var controller = TextEditingController(); return Container( height: 45, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('余额', style: Theme.of(context).textTheme.bodyText1), SizedBox(width: 10), Expanded(child: BlocSelector( selector: (state) => state.request.balance, builder: (context, state) { controller.value = TextEditingValue( text: state ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state?.length ?? 0), ), ); return TextField( controller: controller, keyboardType: TextInputType.number, onChanged: (value) => context.read().add(AccountAdjustBalanceBalanceChanged(value)), decoration: InputDecoration( hintText: '必填项', ) ); } )), Text('当前余额:${removeDecimalZero(currentBalance)}', style: Theme.of(context).textTheme.bodyText2), ] ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/adjust_balance/date_time_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/components/components.dart'; import '/accounts/accounts.dart'; class AdjustBlanceDateTimeInput extends StatelessWidget { @override Widget build(BuildContext context) { return BlocSelector( selector: (state) => state.request.createTime, builder: (context, state) { return DateTimeInput( initialTime: state, onDateChange: (value) { BlocProvider.of(context).add(AccountAdjustBalanceCreateDateChanged(value)); }, onTimeChange: (value) { BlocProvider.of(context).add(AccountAdjustBalanceCreateTimeChanged(value)); }, ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/adjust_balance/description_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; class DescriptionInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '描述', BlocSelector( selector: (state) => state.request.description, builder: (context, state) { controller.value = TextEditingValue( text: state ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(AccountAdjustBalanceDescriptionChanged(value)), decoration: InputDecoration() ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/adjust_balance/notes_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; class NoteInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '备注', BlocSelector( selector: (state) => state.request.notes, builder: (context, state) { controller.value = TextEditingValue( text: state ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(AccountAdjustBalanceNotesChanged(value)), decoration: InputDecoration() ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/accounts/ui/widgets/order_button.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/components/components.dart'; import '/accounts/accounts.dart'; class OrderButton extends StatelessWidget { @override Widget build(BuildContext context) { return BlocSelector( selector: (state) => state.request.sort, builder: (context, state) { return PopupMenu( onSelected: (selected) { if (selected != state) { context.read().add(AccountsSortChanged(selected)); context.read().add(AccountsRefreshed()); } }, items: { 'balance,desc': '按余额排序', 'enable,desc': '可用优先', 'expenseable,desc': '支出优先', 'incomeable,desc': '收入优先', }, selected: state, ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/add_flow.dart ================================================ export 'bloc/add_expense/add_expense_bloc.dart'; export 'bloc/add_income/add_income_bloc.dart'; export 'bloc/add_transfer/add_transfer_bloc.dart'; export 'bloc/models/deal_add_request.dart'; export 'bloc/models/transfer_add_request.dart'; export 'ui/add_flow_page.dart'; export 'ui/tab_page.dart'; export 'ui/no_tab_page.dart'; export 'ui/widgets/expense/add_expense_form.dart'; export 'ui/widgets/income/add_income_form.dart'; export 'ui/widgets/transfer/add_transfer_form.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/bloc/add_expense/add_expense_bloc.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:formz/formz.dart'; import '/login/login.dart'; import '/flows/flows.dart'; import '/add_flow/add_flow.dart'; import '/accounts/accounts.dart'; part 'add_expense_event.dart'; part 'add_expense_state.dart'; class AddExpenseBloc extends Bloc { AddExpenseBloc({ required this.flowRepository, required this.authBloc, required this.accountExpenseableBloc, }) : super(const AddExpenseState()) { on(_onDefaultLoaded); on(_onAccountChanged); on(_onPayeeChanged); on(_onTagChanged); on(_onDescriptionChanged); on(_onNotesChanged); on(_onCreateTimeChanged); on(_onCreateDateChanged); on(_onCategoryChanged); on(_onAmountChanged); on(_onConvertedAmountChanged); on(_onConfirmedChanged); on(_onSubmitted); } final FlowRepository flowRepository; final AuthBloc authBloc; final AccountExpenseableBloc accountExpenseableBloc; void _onAccountChanged(AddExpenseAccountChanged event, Emitter emit) { String? currencyCode = null; if (accountExpenseableBloc.state is AccountExpenseableStateLoadSuccess) { AccountExpenseableStateLoadSuccess state = accountExpenseableBloc.state as AccountExpenseableStateLoadSuccess; Account account = state.accounts.firstWhere((account) => account.id.toString() == event.accountId); currencyCode = account.currencyCode; } emit(state.copyWith( request: state.request.copyWith(accountId: event.accountId), currencyCode: currencyCode )); } void _onTagChanged(AddExpenseTagChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(tags: event.tags), )); } void _onDescriptionChanged(AddExpenseDescriptionChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(description: event.description), )); } void _onNotesChanged(AddExpenseNotesChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(notes: event.notes), )); } void _onConfirmedChanged(AddExpenseConfirmedChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(confirmed: event.confirmed), )); } void _onPayeeChanged(AddExpensePayeeChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(payeeId: event.payeeId), )); } void _onCreateTimeChanged(AddExpenseCreateTimeChanged event, Emitter emit) { DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(state.request.createTime ?? DateTime.now().millisecondsSinceEpoch); dateTime = DateTime(dateTime.year, dateTime.month, dateTime.day, event.time.hour, event.time.minute); emit(state.copyWith( request: state.request.copyWith(createTime: dateTime.millisecondsSinceEpoch), )); } void _onCreateDateChanged(AddExpenseCreateDateChanged event, Emitter emit) { DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(state.request.createTime ?? DateTime.now().millisecondsSinceEpoch); dateTime = DateTime(event.dateTime.year, event.dateTime.month, event.dateTime.day, dateTime.hour, dateTime.minute); emit(state.copyWith( request: state.request.copyWith(createTime: dateTime.millisecondsSinceEpoch), )); } void _onCategoryChanged(AddExpenseCategoryChanged event, Emitter emit) { final List fixedList = Iterable.generate(event.categoryIds.length).toList(); emit(state.copyWith( request: state.request.copyWith( categories: fixedList.map((i) { return new CategoryIdAmountRequest(amount: 0, convertedAmount: null, categoryId: event.categoryIds[i], categoryName: event.categoryNames[i]); }).toList(), ), )); } void _onAmountChanged(AddExpenseAmountChanged event, Emitter emit) { state.request.categories?.firstWhere((item) => item.categoryId == event.categoryId).amount = num.tryParse(event.amount) ?? 0; } void _onConvertedAmountChanged(AddExpenseConvertedAmountChanged event, Emitter emit) { state.request.categories?.firstWhere((item) => item.categoryId == event.categoryId).convertedAmount = num.tryParse(event.convertedAmount) ?? null; } void _onDefaultLoaded(AddExpenseDefaultLoaded event, Emitter emit) { List defaultCategories = []; if (event.deal != null) { defaultCategories = event.deal!.categories.map((e) => new CategoryIdAmountRequest( categoryId: e.categoryId.toString(), categoryName: e.categoryName, amount: event.type == 4 ? e.amount*-1 : e.amount, convertedAmount: event.type == 4 ? e.convertedAmount*-1 : e.convertedAmount ) ).toList(); } else if (authBloc.state.session!.defaultBook.defaultExpenseCategory != null) { defaultCategories = [ new CategoryIdAmountRequest( categoryId: authBloc.state.session!.defaultBook.defaultExpenseCategory!.id.toString(), categoryName: authBloc.state.session!.defaultBook.defaultExpenseCategory!.name, amount: 0, convertedAmount: null ) ]; } String? accountId = event.deal?.account?.id.toString() ?? authBloc.state.session!.defaultBook.defaultExpenseAccount?.id.toString() ?? null; String? currencyCode = null; if (accountExpenseableBloc.state is AccountExpenseableStateLoadSuccess && accountId != null) { AccountExpenseableStateLoadSuccess state = accountExpenseableBloc.state as AccountExpenseableStateLoadSuccess; Account account = state.accounts.firstWhere((account) => account.id.toString() == accountId); currencyCode = account.currencyCode; } emit(state.copyWith( status: FormzStatus.pure, request: state.request.copyWith( description: event.deal?.description ?? '', accountId: accountId, payeeId: event.deal?.payee?.id.toString() ?? '', categories: defaultCategories, tags: event.deal?.tags?.map((e) => e.tagId.toString()).toList() ?? [], createTime: event.type != 2 ? DateTime.now().millisecondsSinceEpoch : event.deal!.createTime, notes: event.type == 2 ? event.deal?.notes ?? '' : '', ), currencyCode: currencyCode )); } void _onSubmitted(AddExpenseSubmitted event, Emitter emit) async { try { bool result = false; // 1-新增,2-修改,3-复制,4-退款 switch (event.type) { case 1: case 3: result = await flowRepository.saveExpense(state.request); break; case 2: result = await flowRepository.updateExpense(event.deal!.id, state.request); break; case 4: result = await flowRepository.refundExpense(event.deal!.id, state.request); break; default: break; } if (result) { emit(state.copyWith(status: FormzStatus.submissionSuccess)); } else { emit(state.copyWith(status: FormzStatus.submissionFailure)); } } catch (_) { print(_); emit(state.copyWith(status: FormzStatus.submissionFailure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/bloc/add_expense/add_expense_event.dart ================================================ part of 'add_expense_bloc.dart'; abstract class AddExpenseEvent extends Equatable { const AddExpenseEvent(); @override List get props => []; } class AddExpenseCreateTimeChanged extends AddExpenseEvent { const AddExpenseCreateTimeChanged(this.time); final TimeOfDay time; @override List get props => [time]; } class AddExpenseCreateDateChanged extends AddExpenseEvent { const AddExpenseCreateDateChanged(this.dateTime); final DateTime dateTime; @override List get props => [dateTime]; } class AddExpenseAccountChanged extends AddExpenseEvent { const AddExpenseAccountChanged(this.accountId); final String accountId; @override List get props => [accountId]; } class AddExpensePayeeChanged extends AddExpenseEvent { const AddExpensePayeeChanged(this.payeeId); final String payeeId; @override List get props => [payeeId]; } class AddExpenseCategoryChanged extends AddExpenseEvent { const AddExpenseCategoryChanged(this.categoryIds, this.categoryNames); final List categoryIds; final List categoryNames; @override List get props => [categoryIds, categoryNames]; } class AddExpenseTagChanged extends AddExpenseEvent { const AddExpenseTagChanged(this.tags); final List tags; @override List get props => [tags]; } class AddExpenseAmountChanged extends AddExpenseEvent { const AddExpenseAmountChanged(this.categoryId, this.amount); final String categoryId; final String amount; @override List get props => [categoryId, amount]; } class AddExpenseConvertedAmountChanged extends AddExpenseEvent { const AddExpenseConvertedAmountChanged(this.categoryId, this.convertedAmount); final String categoryId; final String convertedAmount; @override List get props => [categoryId, convertedAmount]; } class AddExpenseDescriptionChanged extends AddExpenseEvent { const AddExpenseDescriptionChanged(this.description); final String description; @override List get props => [description]; } class AddExpenseNotesChanged extends AddExpenseEvent { const AddExpenseNotesChanged(this.notes); final String notes; @override List get props => [notes]; } class AddExpenseConfirmedChanged extends AddExpenseEvent { const AddExpenseConfirmedChanged(this.confirmed); final bool confirmed; @override List get props => [confirmed]; } class AddExpenseDefaultLoaded extends AddExpenseEvent { final int type; final Deal? deal; const AddExpenseDefaultLoaded(this.type, this.deal); @override List get props => [type, deal]; } class AddExpenseSubmitted extends AddExpenseEvent { final int type; final Deal? deal; const AddExpenseSubmitted(this.type, this.deal); @override List get props => [type, deal]; } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/bloc/add_expense/add_expense_state.dart ================================================ part of 'add_expense_bloc.dart'; class AddExpenseState extends Equatable { final FormzStatus status; final DealAddRequest request; final String? currencyCode; const AddExpenseState({ this.status = FormzStatus.pure, this.request = const DealAddRequest(), this.currencyCode }); AddExpenseState copyWith({ FormzStatus? status, DealAddRequest? request, String? currencyCode, }) { return AddExpenseState( status: status ?? this.status, request: request ?? this.request, currencyCode: currencyCode ?? this.currencyCode ); } @override List get props => [status, request, currencyCode]; } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/bloc/add_income/add_income_bloc.dart ================================================ import 'package:bookkeeping_user_flutter/accounts/accounts.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:formz/formz.dart'; import '/login/login.dart'; import '/flows/flows.dart'; import '../models/deal_add_request.dart'; part 'add_income_event.dart'; part 'add_income_state.dart'; class AddIncomeBloc extends Bloc { AddIncomeBloc({ required this.flowRepository, required this.authBloc, required this.accountIncomeableBloc }) : super(const AddIncomeState()) { on(_onDefaultLoaded); on(_onAccountChanged); on(_onPayeeChanged); on(_onTagChanged); on(_onDescriptionChanged); on(_onNotesChanged); on(_onCreateTimeChanged); on(_onCreateDateChanged); on(_onCategoryChanged); on(_onAmountChanged); on(_onConvertedAmountChanged); on(_onConfirmedChanged); on(_onSubmitted); } final FlowRepository flowRepository; final AuthBloc authBloc; final AccountIncomeableBloc accountIncomeableBloc; void _onAccountChanged(AddIncomeAccountChanged event, Emitter emit) { String? currencyCode = null; if (accountIncomeableBloc.state is AccountIncomeableStateLoadSuccess) { AccountIncomeableStateLoadSuccess state = accountIncomeableBloc.state as AccountIncomeableStateLoadSuccess; Account account = state.accounts.firstWhere((account) => account.id.toString() == event.accountId); currencyCode = account.currencyCode; } emit(state.copyWith( request: state.request.copyWith(accountId: event.accountId), currencyCode: currencyCode )); } void _onTagChanged(AddIncomeTagChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(tags: event.tags), )); } void _onDescriptionChanged(AddIncomeDescriptionChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(description: event.description), )); } void _onNotesChanged(AddIncomeNotesChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(notes: event.notes), )); } void _onConfirmedChanged(AddIncomeConfirmedChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(confirmed: event.confirmed), )); } void _onPayeeChanged(AddIncomePayeeChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(payeeId: event.payeeId), )); } void _onCreateTimeChanged(AddIncomeCreateTimeChanged event, Emitter emit) { DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(state.request.createTime ?? DateTime.now().millisecondsSinceEpoch); dateTime = new DateTime(dateTime.year, dateTime.month, dateTime.day, event.time.hour, event.time.minute); emit(state.copyWith( request: state.request.copyWith(createTime: dateTime.millisecondsSinceEpoch), )); } void _onCreateDateChanged(AddIncomeCreateDateChanged event, Emitter emit) { DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(state.request.createTime ?? DateTime.now().millisecondsSinceEpoch); dateTime = new DateTime(event.dateTime.year, event.dateTime.month, event.dateTime.day, dateTime.hour, dateTime.minute); emit(state.copyWith( request: state.request.copyWith(createTime: dateTime.millisecondsSinceEpoch), )); } void _onCategoryChanged(AddIncomeCategoryChanged event, Emitter emit) { final List fixedList = Iterable.generate(event.categoryIds.length).toList(); emit(state.copyWith( request: state.request.copyWith( categories: fixedList.map((i) { return new CategoryIdAmountRequest(amount: 0, convertedAmount: null, categoryId: event.categoryIds[i], categoryName: event.categoryNames[i]); }).toList(), ), )); } void _onAmountChanged(AddIncomeAmountChanged event, Emitter emit) { state.request.categories?.firstWhere((item) => item.categoryId == event.categoryId).amount = num.tryParse(event.amount) ?? 0; } void _onConvertedAmountChanged(AddIncomeConvertedAmountChanged event, Emitter emit) { state.request.categories?.firstWhere((item) => item.categoryId == event.categoryId).convertedAmount = num.tryParse(event.convertedAmount) ?? null; } void _onDefaultLoaded(AddIncomeDefaultLoaded event, Emitter emit) { List defaultCategories = []; if (event.deal != null) { defaultCategories = event.deal!.categories.map((e) => new CategoryIdAmountRequest( categoryId: e.categoryId.toString(), categoryName: e.categoryName, amount: event.type == 4 ? e.amount*-1 : e.amount, convertedAmount: event.type == 4 ? e.convertedAmount*-1 : e.convertedAmount ) ).toList(); } else if (authBloc.state.session!.defaultBook.defaultIncomeCategory != null) { defaultCategories = [ new CategoryIdAmountRequest( categoryId: authBloc.state.session!.defaultBook.defaultIncomeCategory!.id.toString(), categoryName: authBloc.state.session!.defaultBook.defaultIncomeCategory!.name, amount: 0, convertedAmount: null ) ]; } String? accountId = event.deal?.account?.id.toString() ?? authBloc.state.session!.defaultBook.defaultIncomeAccount?.id.toString() ?? null; String? currencyCode = null; if (accountIncomeableBloc.state is AccountIncomeableStateLoadSuccess && accountId != null) { AccountIncomeableStateLoadSuccess state = accountIncomeableBloc.state as AccountIncomeableStateLoadSuccess; Account account = state.accounts.firstWhere((account) => account.id.toString() == accountId); currencyCode = account.currencyCode; } emit(state.copyWith( status: FormzStatus.pure, request: state.request.copyWith( description: event.deal?.description ?? '', accountId: event.deal?.account?.id.toString() ?? authBloc.state.session!.defaultBook.defaultIncomeAccount?.id.toString() ?? '', payeeId: event.deal?.payee?.id.toString() ?? '', categories: defaultCategories, tags: event.deal?.tags?.map((e) => e.tagId.toString()).toList() ?? [], createTime: event.type != 2 ? DateTime.now().millisecondsSinceEpoch : event.deal!.createTime, notes: event.type == 2 ? event.deal?.notes ?? '' : '', ), currencyCode: currencyCode )); } void _onSubmitted(AddIncomeSubmitted event, Emitter emit) async { try { bool result = false; // 1-新增,2-修改,3-复制,4-退款 switch (event.type) { case 1: case 3: result = await flowRepository.saveIncome(state.request); break; case 2: result = await flowRepository.updateIncome(event.deal!.id, state.request); break; case 4: result = await flowRepository.refundIncome(event.deal!.id, state.request); break; default: break; } if (result) { emit(state.copyWith(status: FormzStatus.submissionSuccess)); } else { emit(state.copyWith(status: FormzStatus.submissionFailure)); } } catch (_) { print(_); emit(state.copyWith(status: FormzStatus.submissionFailure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/bloc/add_income/add_income_event.dart ================================================ part of 'add_income_bloc.dart'; abstract class AddIncomeEvent extends Equatable { const AddIncomeEvent(); @override List get props => []; } class AddIncomeCreateTimeChanged extends AddIncomeEvent { const AddIncomeCreateTimeChanged(this.time); final TimeOfDay time; @override List get props => [time]; } class AddIncomeCreateDateChanged extends AddIncomeEvent { const AddIncomeCreateDateChanged(this.dateTime); final DateTime dateTime; @override List get props => [dateTime]; } class AddIncomeAccountChanged extends AddIncomeEvent { const AddIncomeAccountChanged(this.accountId); final String accountId; @override List get props => [accountId]; } class AddIncomePayeeChanged extends AddIncomeEvent { const AddIncomePayeeChanged(this.payeeId); final String payeeId; @override List get props => [payeeId]; } class AddIncomeCategoryChanged extends AddIncomeEvent { const AddIncomeCategoryChanged(this.categoryIds, this.categoryNames); final List categoryIds; final List categoryNames; @override List get props => [categoryIds, categoryNames]; } class AddIncomeTagChanged extends AddIncomeEvent { const AddIncomeTagChanged(this.tags); final List tags; @override List get props => [tags]; } class AddIncomeAmountChanged extends AddIncomeEvent { const AddIncomeAmountChanged(this.categoryId, this.amount); final String categoryId; final String amount; @override List get props => [categoryId, amount]; } class AddIncomeConvertedAmountChanged extends AddIncomeEvent { const AddIncomeConvertedAmountChanged(this.categoryId, this.convertedAmount); final String categoryId; final String convertedAmount; @override List get props => [categoryId, convertedAmount]; } class AddIncomeDescriptionChanged extends AddIncomeEvent { const AddIncomeDescriptionChanged(this.description); final String description; @override List get props => [description]; } class AddIncomeNotesChanged extends AddIncomeEvent { const AddIncomeNotesChanged(this.notes); final String notes; @override List get props => [notes]; } class AddIncomeConfirmedChanged extends AddIncomeEvent { const AddIncomeConfirmedChanged(this.confirmed); final bool confirmed; @override List get props => [confirmed]; } class AddIncomeDefaultLoaded extends AddIncomeEvent { final int type; final Deal? deal; const AddIncomeDefaultLoaded(this.type, this.deal); @override List get props => [type, deal]; } class AddIncomeSubmitted extends AddIncomeEvent { final int type; final Deal? deal; const AddIncomeSubmitted(this.type, this.deal); @override List get props => [type, deal]; } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/bloc/add_income/add_income_state.dart ================================================ part of 'add_income_bloc.dart'; class AddIncomeState extends Equatable { final FormzStatus status; final DealAddRequest request; final String? currencyCode; const AddIncomeState({ this.status = FormzStatus.pure, this.request = const DealAddRequest(), this.currencyCode }); AddIncomeState copyWith({ FormzStatus? status, DealAddRequest? request, String? currencyCode, }) { return AddIncomeState( status: status ?? this.status, request: request ?? this.request, currencyCode: currencyCode ?? this.currencyCode ); } @override List get props => [status, request, currencyCode]; } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/bloc/add_transfer/add_transfer_bloc.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:formz/formz.dart'; import '/login/login.dart'; import '/flows/flows.dart'; import '/add_flow/add_flow.dart'; import '/accounts/accounts.dart'; part 'add_transfer_event.dart'; part 'add_transfer_state.dart'; class AddTransferBloc extends Bloc { AddTransferBloc({ required this.flowRepository, required this.authBloc, required this.accountTransferFromAbleBloc, required this.accountTransferToAbleBloc, }) : super(const AddTransferState()) { on(_onDefaultLoaded); on(_onFromChanged); on(_onToChanged); on(_onAmountChanged); on(_onConvertedAmountChanged); on(_onTagChanged); on(_onDescriptionChanged); on(_onNotesChanged); on(_onCreateTimeChanged); on(_onCreateDateChanged); on(_onConfirmedChanged); on(_onSubmitted); } final FlowRepository flowRepository; final AuthBloc authBloc; final AccountTransferFromAbleBloc accountTransferFromAbleBloc; final AccountTransferToAbleBloc accountTransferToAbleBloc; void _onFromChanged(AddTransferFromChanged event, Emitter emit) { String? fromCurrencyCode = null; if (accountTransferFromAbleBloc.state is AccountTransferFromAbleStateLoadSuccess) { AccountTransferFromAbleStateLoadSuccess state = accountTransferFromAbleBloc.state as AccountTransferFromAbleStateLoadSuccess; Account account = state.accounts.firstWhere((account) => account.id.toString() == event.fromId); fromCurrencyCode = account.currencyCode; } emit(state.copyWith( request: state.request.copyWith(fromId: event.fromId), fromCurrencyCode: fromCurrencyCode )); } void _onToChanged(AddTransferToChanged event, Emitter emit) { String? toCurrencyCode = null; if (accountTransferToAbleBloc.state is AccountTransferToAbleStateLoadSuccess) { AccountTransferToAbleStateLoadSuccess state = accountTransferToAbleBloc.state as AccountTransferToAbleStateLoadSuccess; Account account = state.accounts.firstWhere((account) => account.id.toString() == event.toId); toCurrencyCode = account.currencyCode; } emit(state.copyWith( request: state.request.copyWith(toId: event.toId), toCurrencyCode: toCurrencyCode )); } void _onTagChanged(AddTransferTagChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(tags: event.tags), )); } void _onDescriptionChanged(AddTransferDescriptionChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(description: event.description), )); } void _onNotesChanged(AddTransferNotesChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(notes: event.notes), )); } void _onConfirmedChanged(AddTransferConfirmedChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(confirmed: event.confirmed), )); } void _onCreateTimeChanged(AddTransferCreateTimeChanged event, Emitter emit) { DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(state.request.createTime ?? DateTime.now().millisecondsSinceEpoch); dateTime = new DateTime(dateTime.year, dateTime.month, dateTime.day, event.time.hour, event.time.minute); emit(state.copyWith( request: state.request.copyWith(createTime: dateTime.millisecondsSinceEpoch), )); } void _onCreateDateChanged(AddTransferCreateDateChanged event, Emitter emit) { DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(state.request.createTime ?? DateTime.now().millisecondsSinceEpoch); dateTime = new DateTime(event.dateTime.year, event.dateTime.month, event.dateTime.day, dateTime.hour, dateTime.minute); emit(state.copyWith( request: state.request.copyWith(createTime: dateTime.millisecondsSinceEpoch), )); } void _onAmountChanged(AddTransferAmountChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(amount: num.tryParse(event.amount) ?? 0), )); } void _onConvertedAmountChanged(AddTransferConvertedAmountChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(convertedAmount: num.tryParse(event.convertedAmount) ?? null), )); } void _onDefaultLoaded(AddTransferDefaultLoaded event, Emitter emit) { String? fromId = event.transfer?.from.id.toString() ?? authBloc.state.session!.defaultBook.defaultTransferFromAccount?.id.toString() ?? null; String? fromCurrencyCode = null; if (accountTransferFromAbleBloc.state is AccountTransferFromAbleStateLoadSuccess && fromId != null) { AccountTransferFromAbleStateLoadSuccess state = accountTransferFromAbleBloc.state as AccountTransferFromAbleStateLoadSuccess; Account account = state.accounts.firstWhere((account) => account.id.toString() == fromId); fromCurrencyCode = account.currencyCode; } String? toId = event.transfer?.to.id.toString() ?? authBloc.state.session!.defaultBook.defaultTransferToAccount?.id.toString() ?? null; String? toCurrencyCode = null; if (accountTransferToAbleBloc.state is AccountTransferToAbleStateLoadSuccess && toId != null) { AccountTransferToAbleStateLoadSuccess state = accountTransferToAbleBloc.state as AccountTransferToAbleStateLoadSuccess; Account account = state.accounts.firstWhere((account) => account.id.toString() == toId); toCurrencyCode = account.currencyCode; } emit(state.copyWith( status: FormzStatus.pure, request: state.request.copyWith( description: event.transfer?.description ?? '', fromId: event.transfer?.from.id.toString() ?? authBloc.state.session!.defaultBook.defaultTransferFromAccount?.id.toString() ?? '', toId: event.transfer?.to.id.toString() ?? authBloc.state.session!.defaultBook.defaultTransferToAccount?.id.toString() ?? '', amount: event.transfer?.amount, convertedAmount: event.transfer?.convertedAmount, tags: event.transfer?.tags?.map((e) => e.tagId.toString()).toList() ?? [], createTime: event.type != 2 ? DateTime.now().millisecondsSinceEpoch : event.transfer!.createTime, notes: event.type == 2 ? event.transfer?.notes ?? '' : '', ), fromCurrencyCode: fromCurrencyCode, toCurrencyCode: toCurrencyCode )); } void _onSubmitted(AddTransferSubmitted event, Emitter emit) async { try { bool result = false; // 1-新增,2-修改,3-复制,4-退款 switch (event.type) { case 1: case 3: result = await flowRepository.saveTransfer(state.request); break; case 2: result = await flowRepository.updateTransfer(event.transfer!.id, state.request); break; default: break; } if (result) { emit(state.copyWith(status: FormzStatus.submissionSuccess)); } else { emit(state.copyWith(status: FormzStatus.submissionFailure)); } } catch (_) { print(_); emit(state.copyWith(status: FormzStatus.submissionFailure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/bloc/add_transfer/add_transfer_event.dart ================================================ part of 'add_transfer_bloc.dart'; abstract class AddTransferEvent extends Equatable { const AddTransferEvent(); @override List get props => []; } class AddTransferCreateTimeChanged extends AddTransferEvent { const AddTransferCreateTimeChanged(this.time); final TimeOfDay time; @override List get props => [time]; } class AddTransferCreateDateChanged extends AddTransferEvent { const AddTransferCreateDateChanged(this.dateTime); final DateTime dateTime; @override List get props => [dateTime]; } class AddTransferFromChanged extends AddTransferEvent { const AddTransferFromChanged(this.fromId); final String fromId; @override List get props => [fromId]; } class AddTransferToChanged extends AddTransferEvent { const AddTransferToChanged(this.toId); final String toId; @override List get props => [toId]; } class AddTransferTagChanged extends AddTransferEvent { const AddTransferTagChanged(this.tags); final List tags; @override List get props => [tags]; } class AddTransferAmountChanged extends AddTransferEvent { const AddTransferAmountChanged(this.amount); final String amount; @override List get props => [amount]; } class AddTransferConvertedAmountChanged extends AddTransferEvent { const AddTransferConvertedAmountChanged(this.convertedAmount); final String convertedAmount; @override List get props => [convertedAmount]; } class AddTransferDescriptionChanged extends AddTransferEvent { const AddTransferDescriptionChanged(this.description); final String description; @override List get props => [description]; } class AddTransferNotesChanged extends AddTransferEvent { const AddTransferNotesChanged(this.notes); final String notes; @override List get props => [notes]; } class AddTransferConfirmedChanged extends AddTransferEvent { const AddTransferConfirmedChanged(this.confirmed); final bool confirmed; @override List get props => [confirmed]; } class AddTransferDefaultLoaded extends AddTransferEvent { final int type; final Transfer? transfer; const AddTransferDefaultLoaded(this.type, this.transfer); @override List get props => [type, transfer]; } class AddTransferSubmitted extends AddTransferEvent { final int type; final Transfer? transfer; const AddTransferSubmitted(this.type, this.transfer); @override List get props => [type, transfer]; } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/bloc/add_transfer/add_transfer_state.dart ================================================ part of 'add_transfer_bloc.dart'; class AddTransferState extends Equatable { final FormzStatus status; final TransferAddRequest request; final String? fromCurrencyCode; final String? toCurrencyCode; const AddTransferState({ this.status = FormzStatus.pure, this.request = const TransferAddRequest(), this.fromCurrencyCode, this.toCurrencyCode }); AddTransferState copyWith({ FormzStatus? status, TransferAddRequest? request, String? fromCurrencyCode, String? toCurrencyCode }) { return AddTransferState( status: status ?? this.status, request: request ?? this.request, fromCurrencyCode: fromCurrencyCode ?? this.fromCurrencyCode, toCurrencyCode: toCurrencyCode ?? this.toCurrencyCode ); } @override List get props => [status, request, fromCurrencyCode, toCurrencyCode]; } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/bloc/models/deal_add_request.dart ================================================ import 'package:json_annotation/json_annotation.dart'; part 'deal_add_request.g.dart'; @JsonSerializable() class DealAddRequest { final String? description; final int? createTime; final String? accountId; final List? categories; final String? payeeId; final List? tags; final bool? confirmed; final String? notes; const DealAddRequest({ this.description, this.createTime, this.accountId, this.categories, this.payeeId, this.tags, this.confirmed, this.notes }); DealAddRequest copyWith({ String? description, int? createTime, String? accountId, String? payeeId, List? categories, List? tags, bool? confirmed = true, String? notes }) { return DealAddRequest( description: description ?? this.description, createTime: createTime ?? this.createTime, accountId: accountId ?? this.accountId, payeeId: payeeId ?? this.payeeId, categories: categories ?? this.categories, tags: tags ?? this.tags, confirmed: confirmed ?? this.confirmed, notes: notes ?? this.notes ); } //factory ExpenseAddRequest.fromJson(Map json) => _$ExpenseAddRequestFromJson(json); Map toJson() => _$DealAddRequestToJson(this); List get categoryIds { if (categories != null) { return categories!.map((i) {return i.categoryId;}).toList(); } else { return []; } } } @JsonSerializable() class CategoryIdAmountRequest { String categoryId; String categoryName; num amount; num? convertedAmount; CategoryIdAmountRequest({ required this.categoryId, required this.categoryName, required this.amount, this.convertedAmount, }); factory CategoryIdAmountRequest.fromJson(Map json) => _$CategoryIdAmountRequestFromJson(json); Map toJson() => _$CategoryIdAmountRequestToJson(this); } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/bloc/models/deal_add_request.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'deal_add_request.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** DealAddRequest _$DealAddRequestFromJson(Map json) => DealAddRequest( description: json['description'] as String?, createTime: json['createTime'] as int?, accountId: json['accountId'] as String?, categories: (json['categories'] as List?) ?.map((e) => CategoryIdAmountRequest.fromJson(e as Map)) .toList(), payeeId: json['payeeId'] as String?, tags: (json['tags'] as List?)?.map((e) => e as String).toList(), confirmed: json['confirmed'] as bool?, notes: json['notes'] as String?, ); Map _$DealAddRequestToJson(DealAddRequest instance) => { 'description': instance.description, 'createTime': instance.createTime, 'accountId': instance.accountId, 'categories': instance.categories, 'payeeId': instance.payeeId, 'tags': instance.tags, 'confirmed': instance.confirmed, 'notes': instance.notes, }; CategoryIdAmountRequest _$CategoryIdAmountRequestFromJson( Map json) => CategoryIdAmountRequest( categoryId: json['categoryId'] as String, categoryName: json['categoryName'] as String, amount: json['amount'] as num, convertedAmount: json['convertedAmount'] as num?, ); Map _$CategoryIdAmountRequestToJson( CategoryIdAmountRequest instance) => { 'categoryId': instance.categoryId, 'categoryName': instance.categoryName, 'amount': instance.amount, 'convertedAmount': instance.convertedAmount, }; ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/bloc/models/transfer_add_request.dart ================================================ import 'package:json_annotation/json_annotation.dart'; part 'transfer_add_request.g.dart'; @JsonSerializable() class TransferAddRequest { final String? description; final int? createTime; final String? fromId; final String? toId; final num? amount; final num? convertedAmount; final List? tags; final bool? confirmed; final String? notes; const TransferAddRequest({ this.description, this.createTime, this.fromId, this.toId, this.amount, this.convertedAmount, this.tags, this.confirmed, this.notes }); TransferAddRequest copyWith({ String? description, int? createTime, String? fromId, String? toId, num? amount, num? convertedAmount, List? tags, bool? confirmed = true, String? notes }) { return TransferAddRequest( description: description ?? this.description, createTime: createTime ?? this.createTime, fromId: fromId ?? this.fromId, toId: toId ?? this.toId, amount: amount ?? this.amount, convertedAmount: convertedAmount ?? this.convertedAmount, tags: tags ?? this.tags, confirmed: confirmed ?? this.confirmed, notes: notes ?? this.notes ); } Map toJson() => _$TransferAddRequestToJson(this); } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/bloc/models/transfer_add_request.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'transfer_add_request.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** TransferAddRequest _$TransferAddRequestFromJson(Map json) => TransferAddRequest( description: json['description'] as String?, createTime: json['createTime'] as int?, fromId: json['fromId'] as String?, toId: json['toId'] as String?, amount: json['amount'] as num?, convertedAmount: json['convertedAmount'] as num?, tags: (json['tags'] as List?)?.map((e) => e as String).toList(), confirmed: json['confirmed'] as bool?, notes: json['notes'] as String?, ); Map _$TransferAddRequestToJson(TransferAddRequest instance) => { 'description': instance.description, 'createTime': instance.createTime, 'fromId': instance.fromId, 'toId': instance.toId, 'amount': instance.amount, 'convertedAmount': instance.convertedAmount, 'tags': instance.tags, 'confirmed': instance.confirmed, 'notes': instance.notes, }; ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/add_flow_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/login/login.dart'; import '/add_flow/add_flow.dart'; import '/flows/flows.dart'; import '/accounts/accounts.dart'; class AddFlowPage extends StatelessWidget { final int type; // 1-新增,2-修改,3-复制,4-退款 final FlowModel? flow; AddFlowPage({ required this.type, this.flow, }); @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider( create: (_) => AddExpenseBloc( flowRepository: RepositoryProvider.of(context), authBloc: BlocProvider.of(context), accountExpenseableBloc: BlocProvider.of(context), )..add(AddExpenseDefaultLoaded(type, flow?.expense)), ), BlocProvider( create: (_) => AddIncomeBloc( flowRepository: RepositoryProvider.of(context), authBloc: BlocProvider.of(context), accountIncomeableBloc: BlocProvider.of(context), )..add(AddIncomeDefaultLoaded(type, flow?.income)), ), BlocProvider( create: (_) => AddTransferBloc( flowRepository: RepositoryProvider.of(context), authBloc: BlocProvider.of(context), accountTransferFromAbleBloc: BlocProvider.of(context), accountTransferToAbleBloc: BlocProvider.of(context), )..add(AddTransferDefaultLoaded(type, flow?.transfer)), ), ], child: type == 1 ? TabPage() : NoTabPage(type: type, flow: flow!) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/no_tab_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/components/components.dart'; import '/accounts/accounts.dart'; import '/flows/flows.dart'; import '/add_flow/add_flow.dart'; class NoTabPage extends StatelessWidget { final int type; // 1-新增,2-修改,3-复制,4-退款 final FlowModel flow; NoTabPage({ required this.type, required this.flow }); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text(_buildTitle(type, flow)), actions: [ IconButton( icon: Icon(Icons.done), onPressed: () { switch (flow.type) { case 1: BlocProvider.of(context).add(AddExpenseSubmitted(type, flow.expense)); break; case 2: BlocProvider.of(context).add(AddIncomeSubmitted(type, flow.income)); break; case 3: BlocProvider.of(context).add(AddTransferSubmitted(type, flow.transfer)); break; case 4: BlocProvider.of(context).add(AccountAdjustBalanceSubmitted(2, null, flow.adjustBalance)); break; default: break; } }, ) ] ), body: _buildBody(flow) ); } Widget _buildBody(FlowModel flow) { switch (flow.type) { case 1: return AddExpenseForm(type: type); case 2: return AddIncomeForm(type: type); case 3: return AddTransferForm(type: type); case 4: return AdjustBalanceForm(type: type, adjustBalance: flow.adjustBalance); default: return PageError(msg: '账单类型异常'); } } String _buildTitle(int type, FlowModel flow) { switch (type) { case 2: return '修改${flow.typeName}'; case 3: return '复制${flow.typeName}'; case 4: return '${flow.typeName}退款'; default: return '账单操作类型异常'; } } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/tab_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/add_flow/add_flow.dart'; class TabPage extends StatelessWidget { @override Widget build(BuildContext context) { return DefaultTabController( length: 3, initialIndex: 0, child: Scaffold( appBar: AppBar( title: TabBar( labelPadding: EdgeInsets.all(0), tabs: [ Tab(child: Text('支出')), Tab(child: Text('收入')), Tab(child: Text('转账')), ], ), actions: [ // 必须加Builder包装,否则DefaultTabController.of(context)找不到。 Builder(builder: (context) { return IconButton( icon: Icon(Icons.done), onPressed: () { switch (DefaultTabController.of(context)!.index) { case 0: BlocProvider.of(context).add(AddExpenseSubmitted(1, null)); break; case 1: BlocProvider.of(context).add(AddIncomeSubmitted(1, null)); break; case 2: BlocProvider.of(context).add(AddTransferSubmitted(1, null)); break; default: break; } }, ); }) ] ), body: TabBarView( children: [ AddExpenseForm(type: 1), AddIncomeForm(type: 1), AddTransferForm(type: 1), ], ), ), ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/expense/account_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; import '/add_flow/add_flow.dart'; class AccountInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector( selector: (state) => state.request.accountId, builder: (context, state) { return SmartSelect.single( key: Key(new DateTime.now().millisecondsSinceEpoch.toString()), title: '账户', selectedValue: state ?? '', onChange: (selected) { context.read().add(AddExpenseAccountChanged(selected.value ?? '')); }, choiceItems: state1 is AccountExpenseableStateLoadSuccess ? modelToChoice(state1.accounts) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto: true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is AccountExpenseableStateLoadInProgress, padding: EdgeInsets.zero, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/expense/add_expense_form.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import '/add_flow/add_flow.dart'; import '/flows/flows.dart'; import '/commons/commons.dart'; import 'widgets.dart'; class AddExpenseForm extends StatelessWidget { final int type; // 1-新增,2-修改,3-复制,4-退款 AddExpenseForm({ required this.type, }); @override Widget build(BuildContext context) { return BlocListener( listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { if (state.status == FormzStatus.submissionSuccess) { Message.success('操作成功'); Navigator.of(context).pop(); BlocProvider.of(context).add(FlowsRefreshed()); // 如果是修改和退款,要刷新账单详情页 if (type == 2 || type == 4) { BlocProvider.of(context).add(FlowFetched()); } } }, child: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ DescriptionInput(), SizedBox(height: 10), AddExpenseDateTimeInput(), AccountInput(), CategoryInput(), PayeeInput(), TagInput(), IsConfirmSwitch(), NotesInput(), ], ), ), ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/expense/category_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; import '/categories/categories.dart'; import '/login/login.dart'; class CategoryInput extends StatelessWidget { @override Widget build(BuildContext context) { final categoryState = context.watch().state; final authState = context.watch().state; final defaultCurrencyCode = authState.session!.defaultGroup.defaultCurrencyCode; return BlocBuilder( buildWhen: (previous, current) => previous.request.categories != current.request.categories || previous.currencyCode != current.currencyCode, builder: (context, state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SmartSelect.multiple ( key: Key(new DateTime.now().millisecondsSinceEpoch.toString()), title: '分类', selectedValue: state.request.categoryIds, onChange: (selected) { context.read().add(AddExpenseCategoryChanged(selected!.value ?? [], selected.title ?? [])); }, choiceItems: categoryState is ExpenseCategorySelectStateLoadSuccess ? modelToChoice(categoryState.categories) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto: true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: categoryState is ExpenseCategorySelectStateLoadInProgress, padding: EdgeInsets.zero, ); } ), for ( var i in state.request.categories ?? []) buildAddCategoryItem(i, defaultCurrencyCode, state.currencyCode, context) ], ); } ); } Widget buildAddCategoryItem(CategoryIdAmountRequest item, String defaultCurrencyCode, String? currencyCode, BuildContext context) { return Container( child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text(item.categoryName, style: Theme.of(context).textTheme.bodyText1), SizedBox(width: 10), Expanded( child: TextField( controller: TextEditingController(text: item.amount != 0 ? removeDecimalZero(item.amount) : ''), keyboardType: TextInputType.number, onChanged: (amount) => context.read().add(AddExpenseAmountChanged(item.categoryId, amount)), decoration: InputDecoration(), ) ) ] ), if (currencyCode != null && defaultCurrencyCode != currencyCode) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('折合${defaultCurrencyCode}', style: Theme.of(context).textTheme.bodyText1), SizedBox(width: 10), Expanded( child: TextField( controller: TextEditingController(text: item.convertedAmount != null ? removeDecimalZero(item.convertedAmount!) : null), keyboardType: TextInputType.number, onChanged: (amount) => context.read().add(AddExpenseConvertedAmountChanged(item.categoryId, amount)), decoration: InputDecoration(), ) ) ] ) ], ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/expense/date_time_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/components/components.dart'; import '/add_flow/add_flow.dart'; class AddExpenseDateTimeInput extends StatelessWidget { @override Widget build(BuildContext context) { return BlocSelector( selector: (state) => state.request.createTime, builder: (context, state) { return DateTimeInput( initialTime: state, onDateChange: (value) { BlocProvider.of(context).add(AddExpenseCreateDateChanged(value)); }, onTimeChange: (value) { BlocProvider.of(context).add(AddExpenseCreateTimeChanged(value)); }, ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/expense/description_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; class DescriptionInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '描述', BlocSelector( selector: (state) => state.request.description, builder: (context, state) { controller.value = TextEditingValue( text: state ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(AddExpenseDescriptionChanged(value)), decoration: InputDecoration() ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/expense/is_confirm_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; class IsConfirmSwitch extends StatelessWidget { @override Widget build(BuildContext context) { return buildFormItem( "是否确认", BlocSelector( selector: (state) => state.request.confirmed, builder: (context, state) { return Align( alignment: Alignment.centerLeft, child: Switch( value: state ?? true, onChanged: (value) => context.read().add(AddExpenseConfirmedChanged(value)), ), ); } ), context); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/expense/notes_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; class NotesInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '备注', BlocSelector( selector: (state) => state.request.notes, builder: (context, state) { controller.value = TextEditingValue( text: state ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(AddExpenseNotesChanged(value)), decoration: InputDecoration() ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/expense/payee_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; import '/payees/payees.dart'; class PayeeInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector( selector: (state) => state.request.payeeId, builder: (context, state) { return SmartSelect.single ( key: Key(new DateTime.now().millisecondsSinceEpoch.toString()), title: '对象', selectedValue: state ?? '', onChange: (selected) { context.read().add(AddExpensePayeeChanged(selected.value ?? '')); }, choiceItems: state1 is PayeeExpenseableStateLoadSuccess ? modelToChoice(state1.payees) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto:true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is PayeeExpenseableStateLoadInProgress, padding: EdgeInsets.zero, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/expense/tag_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; import '/tags/tags.dart'; class TagInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector?>( selector: (state) => state.request.tags, builder: (context, state) { return SmartSelect.multiple( key: Key(new DateTime.now().millisecondsSinceEpoch.toString()), title: '标签', selectedValue: state ?? [], onChange: (selected) { context.read().add(AddExpenseTagChanged(selected!.value ?? [])); }, choiceItems: state1 is TagExpenseableStateLoadSuccess ? modelToChoice(state1.tags) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto: true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is TagExpenseableStateLoadInProgress, padding: EdgeInsets.zero, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/expense/widgets.dart ================================================ export 'account_input.dart'; export 'description_input.dart'; export 'date_time_input.dart'; export 'category_input.dart'; export 'payee_input.dart'; export 'tag_input.dart'; export 'is_confirm_input.dart'; export 'notes_input.dart'; export 'add_expense_form.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/income/account_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; import '/add_flow/add_flow.dart'; class AccountInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector( selector: (state) => state.request.accountId, builder: (context, state) { return SmartSelect.single( key: Key(new DateTime.now().millisecondsSinceEpoch.toString()), title: '账户', selectedValue: state ?? '', onChange: (selected) { context.read().add(AddIncomeAccountChanged(selected.value ?? '')); }, choiceItems: state1 is AccountIncomeableStateLoadSuccess ? modelToChoice(state1.accounts) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto: true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is AccountIncomeableStateLoadInProgress, padding: EdgeInsets.zero, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/income/add_income_form.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import '/add_flow/add_flow.dart'; import '/flows/flows.dart'; import '/commons/commons.dart'; import 'widgets.dart'; class AddIncomeForm extends StatelessWidget { final int type; // 1-新增,2-修改,3-复制,4-退款 AddIncomeForm({ required this.type, }); @override Widget build(BuildContext context) { return BlocListener( listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { if (state.status == FormzStatus.submissionSuccess) { Message.success('操作成功'); Navigator.of(context).pop(); BlocProvider.of(context).add(FlowsRefreshed()); // 如果是修改,要刷新账单详情页 if (type == 2 || type == 4) { BlocProvider.of(context).add(FlowFetched()); } } }, child: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ DescriptionInput(), SizedBox(height: 10), AddIncomeDateTimeInput(), AccountInput(), CategoryInput(), PayeeInput(), TagInput(), IsConfirmSwitch(), NotesInput(), ], ), ), ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/income/category_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; import '/categories/categories.dart'; import '/login/login.dart'; class CategoryInput extends StatelessWidget { @override Widget build(BuildContext context) { final categoryState = context.watch().state; final authState = context.watch().state; final defaultCurrencyCode = authState.session!.defaultGroup.defaultCurrencyCode; return BlocBuilder( buildWhen: (previous, current) => previous.request.categories != current.request.categories || previous.currencyCode != current.currencyCode, builder: (context, state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SmartSelect.multiple ( key: Key(new DateTime.now().millisecondsSinceEpoch.toString()), title: '分类', selectedValue: state.request.categoryIds, onChange: (selected) { context.read().add(AddIncomeCategoryChanged(selected!.value ?? [], selected.title ?? [])); }, choiceItems: categoryState is IncomeCategorySelectStateLoadSuccess ? modelToChoice(categoryState.categories) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto: true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: categoryState is IncomeCategorySelectStateLoadInProgress, padding: EdgeInsets.zero, ); } ), for ( var i in state.request.categories ?? []) buildAddCategoryItem(i, defaultCurrencyCode, state.currencyCode, context) ], ); } ); } Widget buildAddCategoryItem(CategoryIdAmountRequest item, String defaultCurrencyCode, String? currencyCode, BuildContext context) { return Container( child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text(item.categoryName, style: Theme.of(context).textTheme.bodyText1), SizedBox(width: 10), Expanded( child: TextField( controller: TextEditingController(text: item.amount != 0 ? removeDecimalZero(item.amount) : ''), keyboardType: TextInputType.number, onChanged: (amount) => context.read().add(AddIncomeAmountChanged(item.categoryId, amount)), decoration: InputDecoration(), ) ) ] ), if (currencyCode != null && defaultCurrencyCode != currencyCode) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('折合${defaultCurrencyCode}', style: Theme.of(context).textTheme.bodyText1), SizedBox(width: 10), Expanded( child: TextField( controller: TextEditingController(text: item.convertedAmount != null ? removeDecimalZero(item.convertedAmount!) : null), keyboardType: TextInputType.number, onChanged: (amount) => context.read().add(AddIncomeConvertedAmountChanged(item.categoryId, amount)), decoration: InputDecoration(), ) ) ] ) ], ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/income/date_time_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/components/components.dart'; import '/add_flow/add_flow.dart'; class AddIncomeDateTimeInput extends StatelessWidget { @override Widget build(BuildContext context) { return BlocSelector( selector: (state) => state.request.createTime, builder: (context, state) { return DateTimeInput( initialTime: state, onDateChange: (value) { BlocProvider.of(context).add(AddIncomeCreateDateChanged(value)); }, onTimeChange: (value) { BlocProvider.of(context).add(AddIncomeCreateTimeChanged(value)); }, ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/income/description_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; class DescriptionInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '描述', BlocSelector( selector: (state) => state.request.description, builder: (context, state) { controller.value = TextEditingValue( text: state ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(AddIncomeDescriptionChanged(value)), decoration: InputDecoration() ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/income/is_confirm_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; class IsConfirmSwitch extends StatelessWidget { @override Widget build(BuildContext context) { return buildFormItem( "是否确认", BlocSelector( selector: (state) => state.request.confirmed, builder: (context, state) { return Align( alignment: Alignment.centerLeft, child: Switch( value: state ?? true, onChanged: (value) => context.read().add(AddIncomeConfirmedChanged(value)), ), ); } ), context); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/income/notes_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; class NotesInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '备注', BlocSelector( selector: (state) => state.request.notes, builder: (context, state) { controller.value = TextEditingValue( text: state ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(AddIncomeNotesChanged(value)), decoration: InputDecoration() ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/income/payee_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; import '/payees/payees.dart'; class PayeeInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector( selector: (state) => state.request.payeeId, builder: (context, state) { return SmartSelect.single ( key: Key(new DateTime.now().millisecondsSinceEpoch.toString()), title: '对象', selectedValue: state ?? '', onChange: (selected) { context.read().add(AddIncomePayeeChanged(selected.value ?? '')); }, choiceItems: state1 is PayeeIncomeableStateLoadSuccess ? modelToChoice(state1.payees) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto:true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is PayeeIncomeableStateLoadInProgress, padding: EdgeInsets.zero, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/income/tag_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; import '/tags/tags.dart'; class TagInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector?>( selector: (state) => state.request.tags, builder: (context, state) { return SmartSelect.multiple( key: Key(new DateTime.now().millisecondsSinceEpoch.toString()), title: '标签', selectedValue: state ?? [], onChange: (selected) { context.read().add(AddIncomeTagChanged(selected!.value ?? [])); }, choiceItems: state1 is TagIncomeableStateLoadSuccess ? modelToChoice(state1.tags) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto: true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is TagIncomeableStateLoadInProgress, padding: EdgeInsets.zero, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/income/widgets.dart ================================================ export 'account_input.dart'; export 'description_input.dart'; export 'date_time_input.dart'; export 'category_input.dart'; export 'payee_input.dart'; export 'tag_input.dart'; export 'is_confirm_input.dart'; export 'notes_input.dart'; export 'add_income_form.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/transfer/add_transfer_form.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import '/add_flow/add_flow.dart'; import '/flows/flows.dart'; import '/commons/commons.dart'; import 'widgets.dart'; class AddTransferForm extends StatelessWidget { final int type; // 1-新增,2-修改,3-复制,4-退款 AddTransferForm({ required this.type, }); @override Widget build(BuildContext context) { final state = context.watch().state; return BlocListener( listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { if (state.status == FormzStatus.submissionSuccess) { Message.success('操作成功'); Navigator.of(context).pop(); BlocProvider.of(context).add(FlowsRefreshed()); // 如果是修改,要刷新账单详情页 if (type == 2 || type == 4) { BlocProvider.of(context).add(FlowFetched()); } } }, child: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ DescriptionInput(), SizedBox(height: 10), AddTransferDateTimeInput(), FromInput(), ToInput(), AmountInput(), if (state.fromCurrencyCode != null && state.toCurrencyCode != null && state.fromCurrencyCode != state.toCurrencyCode) SizedBox(height: 10), if (state.fromCurrencyCode != null && state.toCurrencyCode != null && state.fromCurrencyCode != state.toCurrencyCode) ConvertedAmountInput(), SizedBox(height: 10), TagInput(), IsConfirmSwitch(), NotesInput(), ], ), ), ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/transfer/amount_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; class AmountInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '金额', BlocSelector( selector: (state) => state.request.amount, builder: (context, state) { controller.value = TextEditingValue( text: state != null && state != 0 ? removeDecimalZero(state) : '', selection: TextSelection.fromPosition( TextPosition(offset: state.toString().length), ), ); return TextField( controller: controller, keyboardType: TextInputType.number, onChanged: (value) => context.read().add(AddTransferAmountChanged(value)), decoration: InputDecoration() ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/transfer/converted_amount_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; class ConvertedAmountInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); final state = context.watch().state; return buildFormItem( '折合${state.toCurrencyCode}', BlocSelector( selector: (state) => state.request.convertedAmount, builder: (context, state) { controller.value = TextEditingValue( text: state != null && state != 0 ? removeDecimalZero(state) : '', selection: TextSelection.fromPosition( TextPosition(offset: state.toString().length), ), ); return TextField( controller: controller, keyboardType: TextInputType.number, onChanged: (value) => context.read().add(AddTransferConvertedAmountChanged(value)), decoration: InputDecoration() ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/transfer/date_time_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/components/components.dart'; import '/add_flow/add_flow.dart'; class AddTransferDateTimeInput extends StatelessWidget { @override Widget build(BuildContext context) { return BlocSelector( selector: (state) => state.request.createTime, builder: (context, state) { return DateTimeInput( initialTime: state, onDateChange: (value) { BlocProvider.of(context).add(AddTransferCreateDateChanged(value)); }, onTimeChange: (value) { BlocProvider.of(context).add(AddTransferCreateTimeChanged(value)); }, ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/transfer/description_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; class DescriptionInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '描述', BlocSelector( selector: (state) => state.request.description, builder: (context, state) { controller.value = TextEditingValue( text: state ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(AddTransferDescriptionChanged(value)), decoration: InputDecoration() ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/transfer/from_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; import '/add_flow/add_flow.dart'; class FromInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector( selector: (state) => state.request.fromId, builder: (context, state) { return SmartSelect.single( key: Key(new DateTime.now().millisecondsSinceEpoch.toString()), title: '转出账户', selectedValue: state ?? '', onChange: (selected) { context.read().add(AddTransferFromChanged(selected.value ?? '')); }, choiceItems: state1 is AccountTransferFromAbleStateLoadSuccess ? modelToChoice(state1.accounts) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto: true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is AccountTransferFromAbleStateLoadInProgress, padding: EdgeInsets.zero, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/transfer/is_confirm_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; class IsConfirmSwitch extends StatelessWidget { @override Widget build(BuildContext context) { return buildFormItem( "是否确认", BlocSelector( selector: (state) => state.request.confirmed, builder: (context, state) { return Align( alignment: Alignment.centerLeft, child: Switch( value: state ?? true, onChanged: (value) => context.read().add(AddTransferConfirmedChanged(value)), ), ); } ), context); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/transfer/notes_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; class NotesInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '备注', BlocSelector( selector: (state) => state.request.notes, builder: (context, state) { controller.value = TextEditingValue( text: state ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(AddTransferNotesChanged(value)), decoration: InputDecoration() ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/transfer/tag_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; import '/tags/tags.dart'; class TagInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector?>( selector: (state) => state.request.tags, builder: (context, state) { return SmartSelect.multiple( key: Key(new DateTime.now().millisecondsSinceEpoch.toString()), title: '标签', selectedValue: state ?? [], onChange: (selected) { context.read().add(AddTransferTagChanged(selected!.value ?? [])); }, choiceItems: state1 is TagTransferableStateLoadSuccess ? modelToChoice(state1.tags) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto: true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is TagTransferableStateLoadInProgress, padding: EdgeInsets.zero, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/transfer/to_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; import '/add_flow/add_flow.dart'; class ToInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector( selector: (state) => state.request.toId, builder: (context, state) { return SmartSelect.single( key: Key(new DateTime.now().millisecondsSinceEpoch.toString()), title: '转入账户', selectedValue: state ?? '', onChange: (selected) { context.read().add(AddTransferToChanged(selected.value ?? '')); }, choiceItems: state1 is AccountTransferToAbleStateLoadSuccess ? modelToChoice(state1.accounts) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto: true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is AccountTransferToAbleStateLoadInProgress, padding: EdgeInsets.zero, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/add_flow/ui/widgets/transfer/widgets.dart ================================================ export 'description_input.dart'; export 'date_time_input.dart'; export 'from_input.dart'; export 'to_input.dart'; export 'amount_input.dart'; export 'converted_amount_input.dart'; export 'tag_input.dart'; export 'is_confirm_input.dart'; export 'notes_input.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/app.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/categories/categories.dart'; import '/flows/flows.dart'; import '/payees/payees.dart'; import '/tags/tags.dart'; import '/accounts/accounts.dart'; import '/login/login.dart'; import '/books/books.dart'; import '/charts/charts.dart'; import '/items/items.dart'; import '/currency/currency.dart'; import 'themes.dart'; import 'routes.dart'; class App extends StatelessWidget { const App({ Key? key, required this.loginRepository, required this.accountRepository, required this.payeeRepository, required this.tagRepository, required this.categoryRepository, required this.flowRepository, required this.bookRepository, required this.reportRepository, required this.itemRepository, required this.currencyRepository, }) : super(key: key); final LoginRepository loginRepository; final AccountRepository accountRepository; final PayeeRepository payeeRepository; final TagRepository tagRepository; final CategoryRepository categoryRepository; final FlowRepository flowRepository; final BookRepository bookRepository; final ReportRepository reportRepository; final ItemRepository itemRepository; final CurrencyRepository currencyRepository; @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider.value(value: loginRepository), RepositoryProvider.value(value: accountRepository), RepositoryProvider.value(value: categoryRepository), RepositoryProvider.value(value: payeeRepository), RepositoryProvider.value(value: tagRepository), RepositoryProvider.value(value: flowRepository), RepositoryProvider.value(value: bookRepository), RepositoryProvider.value(value: reportRepository), RepositoryProvider.value(value: itemRepository), RepositoryProvider.value(value: currencyRepository), ], child: MultiBlocProvider( providers: [ BlocProvider( create: (_) => AuthBloc(loginRepository: loginRepository)..add(AppStarted()) ), BlocProvider( create: (_) => FlowsBloc(flowRepository: flowRepository) ), BlocProvider( create: (_) => PayeeExpenseableBloc(payeeRepository: payeeRepository)..add(PayeeExpenseableLoaded()) ), BlocProvider( create: (_) => PayeeIncomeableBloc(payeeRepository: payeeRepository)..add(PayeeIncomeableLoaded()) ), BlocProvider( create: (_) => PayeeEnableBloc(payeeRepository: payeeRepository)..add(PayeeEnableLoaded()) ), BlocProvider( create: (_) => TagExpenseableBloc(tagRepository: tagRepository)..add(TagExpenseableLoaded()) ), BlocProvider( create: (_) => TagIncomeableBloc(tagRepository: tagRepository)..add(TagIncomeableLoaded()) ), BlocProvider( create: (_) => TagTransferableBloc(tagRepository: tagRepository)..add(TagTransferableLoaded()) ), BlocProvider( create: (_) => TagEnableBloc(tagRepository: tagRepository)..add(TagEnableLoaded()) ), BlocProvider( create: (_) => ExpenseCategorySelectBloc(categoryRepository: categoryRepository)..add(ExpenseCategorySelectLoaded()) ), BlocProvider( create: (_) => IncomeCategorySelectBloc(categoryRepository: categoryRepository)..add(IncomeCategorySelectLoaded()) ), ], child: Builder(builder: (context) { return MultiBlocProvider( providers: [ BlocProvider( create: (_) => AccountExpenseableBloc(accountRepository: accountRepository)..add(AccountExpenseableLoaded()) ), BlocProvider( create: (_) => AccountIncomeableBloc(accountRepository: accountRepository)..add(AccountIncomeableLoaded()) ), BlocProvider( create: (_) => AccountTransferFromAbleBloc(accountRepository: accountRepository)..add(AccountTransferFromAbleLoaded()) ), BlocProvider( create: (_) => AccountTransferToAbleBloc(accountRepository: accountRepository)..add(AccountTransferToAbleLoaded()) ), BlocProvider( create: (_) => AccountEnableBloc(accountRepository: accountRepository)..add(AccountEnableLoaded()) ), BlocProvider( create: (_) => CategoryTreeBloc( categoryRepository: categoryRepository, expenseCategorySelectBloc: BlocProvider.of(context), incomeCategorySelectBloc: BlocProvider.of(context), ) ), BlocProvider( create: (_) => CategoryFetchBloc(categoryRepository: categoryRepository) ), BlocProvider( create: (_) => TagTreeBloc( tagRepository: tagRepository, tagExpenseableBloc: BlocProvider.of(context), tagIncomeableBloc: BlocProvider.of(context), tagTransferableBloc: BlocProvider.of(context), tagEnableBloc: BlocProvider.of(context), ) ), BlocProvider( create: (_) => TagFetchBloc(tagRepository: tagRepository) ), BlocProvider( create: (_) => BookFetchBloc(bookRepository: bookRepository) ), BlocProvider( create: (_) => FlowFetchBloc(flowRepository: flowRepository) ), BlocProvider( create: (_) => AccountsBloc(accountRepository: accountRepository) ), BlocProvider( create: (_) => AccountAdjustBalanceBloc(accountRepository: accountRepository) ), BlocProvider( create: (_) => AccountFetchBloc(accountRepository: accountRepository) ), BlocProvider( create: (_) => PayeesBloc(payeeRepository: payeeRepository) ), BlocProvider( create: (_) => PayeeFetchBloc(payeeRepository: payeeRepository) ), BlocProvider( create: (_) => BooksBloc( bookRepository: bookRepository, authBloc: BlocProvider.of(context), flowsBloc: BlocProvider.of(context), expenseCategorySelectBloc: BlocProvider.of(context), incomeCategorySelectBloc: BlocProvider.of(context), tagExpenseableBloc: BlocProvider.of(context), tagIncomeableBloc: BlocProvider.of(context), tagTransferableBloc: BlocProvider.of(context), tagEnableBloc: BlocProvider.of(context), payeeExpenseableBloc: BlocProvider.of(context), payeeIncomeableBloc: BlocProvider.of(context), payeeEnableBloc: BlocProvider.of(context), ) ), BlocProvider( create: (_) => BookFetchBloc(bookRepository: bookRepository) ), BlocProvider( create: (_) => ReportAssetBloc(reportRepository: reportRepository)..add(ReportAssetLoaded()) ), BlocProvider( create: (_) => ReportDebtBloc(reportRepository: reportRepository)..add(ReportDebtLoaded()) ), BlocProvider( create: (_) => ReportExpenseCategoryBloc(reportRepository: reportRepository)..add(ReportExpenseCategoryRefreshed()), ), BlocProvider( create: (_) => ReportIncomeCategoryBloc(reportRepository: reportRepository)..add(ReportIncomeCategoryRefreshed()), ), BlocProvider( create: (_) => ItemsBloc(itemRepository: itemRepository) ), BlocProvider( create: (_) => CurrencyAllBloc(currencyRepository: currencyRepository)..add(CurrencyAllLoaded()), ) ], child: AppView() ); }) ) ); } } class AppView extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( localizationsDelegates: [ GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ], supportedLocales: [ const Locale('zh'), ], locale: const Locale('zh'), title: '九快记账', theme: lightTheme, darkTheme: darkTheme, themeMode: ThemeMode.system, routes: AppRouter.routes, initialRoute: AppRouter.initialRoute, onGenerateRoute: AppRouter.generateRoute, onUnknownRoute: AppRouter.unknownRoute, ); } } ================================================ FILE: bookkeeping_user_flutter/lib/books/bloc/book_fetch/book_fetch_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:bookkeeping_user_flutter/books/books.dart'; import 'package:bookkeeping_user_flutter/books/data/models/book.dart'; import 'package:bookkeeping_user_flutter/commons/enums.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; part 'book_fetch_event.dart'; part 'book_fetch_state.dart'; class BookFetchBloc extends Bloc { final BookRepository bookRepository; BookFetchBloc({ required this.bookRepository, }) : super(BookFetchState()) { on(_onFetched); on(_onDefault); } void _onDefault(BookLoadDefault event, Emitter emit) { emit(state.copyWith( status: LoadDataStatus.success, book: event.book )); } void _onFetched(_, Emitter emit) async { try { emit(state.copyWith(status: LoadDataStatus.progress)); final book = await bookRepository.get(state.book!.id); emit(state.copyWith( status: LoadDataStatus.success, book: book, )); } catch (_) { emit(state.copyWith(status: LoadDataStatus.failure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/books/bloc/book_fetch/book_fetch_event.dart ================================================ part of 'book_fetch_bloc.dart'; @immutable class BookFetchEvent extends Equatable { const BookFetchEvent(); @override List get props => []; } class BookFetched extends BookFetchEvent {} class BookLoadDefault extends BookFetchEvent { final Book book; const BookLoadDefault({ required this.book, }); List get props => [book]; } ================================================ FILE: bookkeeping_user_flutter/lib/books/bloc/book_fetch/book_fetch_state.dart ================================================ part of 'book_fetch_bloc.dart'; @immutable class BookFetchState extends Equatable { final LoadDataStatus status; final Book? book; const BookFetchState({ this.status = LoadDataStatus.initial, this.book, }); BookFetchState copyWith({ LoadDataStatus? status, Book? book, }) { return BookFetchState( status: status ?? this.status, book: book ?? this.book, ); } @override List get props => [status, book]; } ================================================ FILE: bookkeeping_user_flutter/lib/books/bloc/books/books_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import '/login/login.dart'; import '/books/books.dart'; import '/commons/commons.dart'; import '/flows/flows.dart'; import '/categories/categories.dart'; import '/payees/payees.dart'; import '/tags/tags.dart'; part 'books_event.dart'; part 'books_state.dart'; class BooksBloc extends Bloc { final BookRepository bookRepository; final AuthBloc authBloc; final FlowsBloc flowsBloc; final ExpenseCategorySelectBloc expenseCategorySelectBloc; final IncomeCategorySelectBloc incomeCategorySelectBloc; final TagExpenseableBloc tagExpenseableBloc; final TagIncomeableBloc tagIncomeableBloc; final TagTransferableBloc tagTransferableBloc; final TagEnableBloc tagEnableBloc; final PayeeExpenseableBloc payeeExpenseableBloc; final PayeeIncomeableBloc payeeIncomeableBloc; final PayeeEnableBloc payeeEnableBloc; BooksBloc({ required this.bookRepository, required this.authBloc, required this.flowsBloc, required this.expenseCategorySelectBloc, required this.incomeCategorySelectBloc, required this.tagExpenseableBloc, required this.tagIncomeableBloc, required this.tagTransferableBloc, required this.tagEnableBloc, required this.payeeExpenseableBloc, required this.payeeIncomeableBloc, required this.payeeEnableBloc, }) : super(BooksState()) { on(_onRefreshed); on(_onLoadMore); on(_onDefault); } void _onRefreshed(_, Emitter emit) async { try { emit(state.copyWith( status: LoadDataStatus.progress, request: state.request.copyWith(page: 1), )); final books = await bookRepository.query(state.request); emit(state.copyWith( status: LoadDataStatus.success, books: books, request: state.request.copyWith(page: state.request.page + 1), )); } catch (_) { emit(state.copyWith(status: LoadDataStatus.failure)); } } void _onLoadMore(_, Emitter emit) async { try { emit(state.copyWith(loadMoreStatus: LoadDataStatus.progress)); final books = await bookRepository.query(state.request); if (books.isNotEmpty) { emit(state.copyWith( request: state.request.copyWith(page: state.request.page + 1), books: List.of(state.books)..addAll(books), loadMoreStatus: LoadDataStatus.success, )); } else { emit(state.copyWith(loadMoreStatus: LoadDataStatus.empty)); } } catch (_) { emit(state.copyWith(loadMoreStatus: LoadDataStatus.failure)); } } void _onDefault(BookSetDefault event, Emitter emit) async { try { emit(state.copyWith(defaultStatus: LoadDataStatus.progress)); final result = await bookRepository.setDefault(event.book.id.toString()); if (result) { emit(state.copyWith(defaultStatus: LoadDataStatus.success)); authBloc.add(DefaultBookChanged(event.book)); flowsBloc.add(FlowsRefreshed()); expenseCategorySelectBloc.add(ExpenseCategorySelectLoaded()); incomeCategorySelectBloc.add(IncomeCategorySelectLoaded()); tagExpenseableBloc.add(TagExpenseableLoaded()); tagIncomeableBloc.add(TagIncomeableLoaded()); tagTransferableBloc.add(TagTransferableLoaded()); tagEnableBloc.add(TagEnableLoaded()); payeeExpenseableBloc.add(PayeeExpenseableLoaded()); payeeIncomeableBloc.add(PayeeIncomeableLoaded()); payeeEnableBloc.add(PayeeEnableLoaded()); } else { emit(state.copyWith(defaultStatus: LoadDataStatus.failure)); } } catch (_) { emit(state.copyWith(defaultStatus: LoadDataStatus.failure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/books/bloc/books/books_event.dart ================================================ part of 'books_bloc.dart'; @immutable abstract class BooksEvent extends Equatable { const BooksEvent(); @override List get props => []; } class BooksRefreshed extends BooksEvent { } class BooksLoadMore extends BooksEvent { } class BookSetDefault extends BooksEvent { final Book book; const BookSetDefault(this.book); @override List get props => [book]; } ================================================ FILE: bookkeeping_user_flutter/lib/books/bloc/books/books_state.dart ================================================ part of 'books_bloc.dart'; @immutable class BooksState extends Equatable { final LoadDataStatus status; final List books; final BookQueryRequest request; final LoadDataStatus loadMoreStatus; final LoadDataStatus deleteStatus; final LoadDataStatus defaultStatus; const BooksState({ this.status = LoadDataStatus.initial, this.books = const [], this.request = const BookQueryRequest(), this.loadMoreStatus = LoadDataStatus.initial, this.deleteStatus = LoadDataStatus.initial, this.defaultStatus = LoadDataStatus.initial, }); BooksState copyWith({ LoadDataStatus? status, List? books, BookQueryRequest? request, LoadDataStatus? loadMoreStatus, LoadDataStatus? deleteStatus, LoadDataStatus? defaultStatus, }) { return BooksState( status: status ?? this.status, books: books ?? this.books, request: request ?? this.request, loadMoreStatus: loadMoreStatus ?? this.loadMoreStatus, deleteStatus: deleteStatus ?? this.deleteStatus, defaultStatus: defaultStatus ?? this.defaultStatus ); } @override List get props => [status, books, request, loadMoreStatus, deleteStatus, defaultStatus]; } ================================================ FILE: bookkeeping_user_flutter/lib/books/books.dart ================================================ export 'bloc/books/books_bloc.dart'; export 'bloc/book_fetch/book_fetch_bloc.dart'; export 'data/models/book.dart'; export 'data/models/book_query_request.dart'; export 'data/book_repository.dart'; export 'ui/books_page.dart'; export 'ui/book_detail_page.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/books/data/book_repository.dart ================================================ import 'dart:convert'; import '/commons/commons.dart'; import '/books/books.dart'; class BookRepository { List _responseToList(String response) { return (json.decode(response)['data']['content']).map((i) => Book.fromJson(i)).toList(); } Book _responseBook(String response) { return Book.fromJson(json.decode(response)['data']); } Future> query(BookQueryRequest request) async { return _responseToList(await HttpClient().get('books', params: request.toJson())); } Future get(int id) async { return _responseBook(await HttpClient().get('accounts/$id')); } Future setDefault(String id) async { String response = await HttpClient().put('setDefaultBook/$id'); return parseResponse(response); } } ================================================ FILE: bookkeeping_user_flutter/lib/books/data/models/book.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import '/commons/commons.dart'; part 'book.g.dart'; @JsonSerializable() class Book extends Equatable { final int id; final String name; final IdNameModel group; final String? notes; final bool descriptionEnable; final bool timeEnable; final bool imageEnable; final IdNameModel? defaultExpenseAccount; final IdNameModel? defaultIncomeAccount; final IdNameModel? defaultTransferFromAccount; final IdNameModel? defaultTransferToAccount; final IdNameModel? defaultExpenseCategory; final IdNameModel? defaultIncomeCategory; const Book({ required this.id, required this.name, required this.group, this.notes, required this.descriptionEnable, required this.timeEnable, required this.imageEnable, this.defaultExpenseAccount, this.defaultIncomeAccount, this.defaultTransferFromAccount, this.defaultTransferToAccount, this.defaultExpenseCategory, this.defaultIncomeCategory, }); factory Book.fromJson(Map json) => _$BookFromJson(json); Map toJson() => _$BookToJson(this); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/books/data/models/book.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'book.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Book _$BookFromJson(Map json) => Book( id: json['id'] as int, name: json['name'] as String, group: IdNameModel.fromJson(json['group'] as Map), notes: json['notes'] as String?, descriptionEnable: json['descriptionEnable'] as bool, timeEnable: json['timeEnable'] as bool, imageEnable: json['imageEnable'] as bool, defaultExpenseAccount: json['defaultExpenseAccount'] == null ? null : IdNameModel.fromJson( json['defaultExpenseAccount'] as Map), defaultIncomeAccount: json['defaultIncomeAccount'] == null ? null : IdNameModel.fromJson( json['defaultIncomeAccount'] as Map), defaultTransferFromAccount: json['defaultTransferFromAccount'] == null ? null : IdNameModel.fromJson( json['defaultTransferFromAccount'] as Map), defaultTransferToAccount: json['defaultTransferToAccount'] == null ? null : IdNameModel.fromJson( json['defaultTransferToAccount'] as Map), defaultExpenseCategory: json['defaultExpenseCategory'] == null ? null : IdNameModel.fromJson( json['defaultExpenseCategory'] as Map), defaultIncomeCategory: json['defaultIncomeCategory'] == null ? null : IdNameModel.fromJson( json['defaultIncomeCategory'] as Map), ); Map _$BookToJson(Book instance) => { 'id': instance.id, 'name': instance.name, 'group': instance.group, 'notes': instance.notes, 'descriptionEnable': instance.descriptionEnable, 'timeEnable': instance.timeEnable, 'imageEnable': instance.imageEnable, 'defaultExpenseAccount': instance.defaultExpenseAccount, 'defaultIncomeAccount': instance.defaultIncomeAccount, 'defaultTransferFromAccount': instance.defaultTransferFromAccount, 'defaultTransferToAccount': instance.defaultTransferToAccount, 'defaultExpenseCategory': instance.defaultExpenseCategory, 'defaultIncomeCategory': instance.defaultIncomeCategory, }; ================================================ FILE: bookkeeping_user_flutter/lib/books/data/models/book_query_request.dart ================================================ import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; part 'book_query_request.g.dart'; @JsonSerializable() class BookQueryRequest extends Equatable { final int page; final int size; const BookQueryRequest({ this.page = 1, this.size = 10, }); BookQueryRequest copyWith({ int? page, int? size, }) { return BookQueryRequest( page: page ?? this.page, size: size ?? this.size, ); } factory BookQueryRequest.fromJson(Map json) => _$BookQueryRequestFromJson(json); Map toJson() => _$BookQueryRequestToJson(this); @override List get props => [page]; } ================================================ FILE: bookkeeping_user_flutter/lib/books/data/models/book_query_request.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'book_query_request.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** BookQueryRequest _$BookQueryRequestFromJson(Map json) => BookQueryRequest( page: json['page'] as int? ?? 1, size: json['size'] as int? ?? 10, ); Map _$BookQueryRequestToJson(BookQueryRequest instance) => { 'page': instance.page, 'size': instance.size, }; ================================================ FILE: bookkeeping_user_flutter/lib/books/ui/book_detail_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/login/bloc/auth/auth_bloc.dart'; import '/components/components.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; import '/books/books.dart'; class BookDetailPage extends StatefulWidget { final Book book; BookDetailPage({ required this.book }); @override State createState() => _BookDetailPageState(); } class _BookDetailPageState extends State { @override void initState() { BlocProvider.of(context).add(BookLoadDefault(book: widget.book)); super.initState(); } @override Widget build(BuildContext context) { return MultiBlocListener( listeners: [ BlocListener( listenWhen: (previous, current) => previous.deleteStatus != current.deleteStatus, listener: (context, state) { if (state.deleteStatus == LoadDataStatus.success) { Message.success('操作成功!'); // Navigator.pop(context); if (Navigator.canPop(context)) { Navigator.of(context).pop(); } else { SystemNavigator.pop(); } } } ), BlocListener( listenWhen: (previous, current) => previous.defaultStatus != current.defaultStatus, listener: (context, state) { if (state.defaultStatus == LoadDataStatus.success) { Message.success('操作成功!'); } }, ) ], child: BlocBuilder( builder: (context, state) { return Scaffold( appBar: AppBar( centerTitle: true, title: const Text('账本详情'), actions: _buildActions(context, state.book ?? widget.book) ), body: Builder( builder: (context) { switch (state.status) { case LoadDataStatus.progress: case LoadDataStatus.initial: return const PageLoading(); case LoadDataStatus.success: return _buildBody(context, state.book ?? widget.book); default: return PageError(onTap: () { BlocProvider.of(context).add(AccountFetched()); }); } }, ) ); } ) ); } List _buildActions(BuildContext context, Book book) { return [ IconButton( icon: Icon(Icons.edit), onPressed: () { //fullDialog(context, AccountFormPage(type: 2, accountType: account.type, account: account)); } ), IconButton( icon: Icon(Icons.delete), onPressed: () async { if (await confirm( context, content: Text("确定删除${book.name}吗?"), textOK: Text("确定"), textCancel: Text("取消"), )) { //BlocProvider.of(context).add(BookDeleted(book.id.toString())); } } ) ]; } Widget _buildBody(BuildContext context, Book book) { final theme = Theme.of(context); TextStyle? style1 = theme.textTheme.bodyText2; TextStyle? style2 = theme.textTheme.bodyText1; final state = context.watch().state; return SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ OverflowBar( overflowAlignment: OverflowBarAlignment.center, spacing: 20, children: [ ElevatedButton( child: const Text('设置'), onPressed: () { } ), ElevatedButton( child: const Text('设为默认'), onPressed: state.session?.defaultBook.id != book.id ? () { BlocProvider.of(context).add(BookSetDefault(book)); } : null ) ] ), SizedBox(height: 15), Row(children: [Text("账本名称:", style: style1), Text(book.name, style: style2)]), SizedBox(height: 15), Row(children: [Text("所属组:", style: style1), Text(book.group.name, style: style2)]), SizedBox(height: 15), Row(children: [Text("是否展示描述:", style: style1), Text(boolToString(book.descriptionEnable), style: style2)]), SizedBox(height: 15), Row(children: [Text("是否展示时间:", style: style1), Text(boolToString(book.timeEnable), style: style2)]), SizedBox(height: 15), Row(children: [Text("是否支出图片:", style: style1), Text(boolToString(book.imageEnable), style: style2)]), SizedBox(height: 15), Row(children: [Text("默认支出账户:", style: style1), Text(book.defaultExpenseAccount?.name ?? '', style: style2)]), SizedBox(height: 15), Row(children: [Text("默认收入账户:", style: style1), Text(book.defaultIncomeAccount?.name ?? '', style: style2)]), SizedBox(height: 15), Row(children: [Text("默认支出类别:", style: style1), Text(book.defaultExpenseCategory?.name ?? '', style: style2)]), SizedBox(height: 15), Row(children: [Text("默认收入类别:", style: style1), Text(book.defaultIncomeCategory?.name ?? '', style: style2)]), SizedBox(height: 15), Row(children: [Text("默认转出账户:", style: style1), Text(book.defaultTransferFromAccount?.name ?? '', style: style2)]), SizedBox(height: 15), Row(children: [Text("默认转入账户:", style: style1), Text(book.defaultTransferToAccount?.name ?? '', style: style2)]), ], ) ), ); } } ================================================ FILE: bookkeeping_user_flutter/lib/books/ui/books_page.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import '/routes.dart'; import '/commons/commons.dart'; import '/books/books.dart'; import '/components/components.dart'; class BooksPage extends StatefulWidget { @override State createState() => _BooksPageState(); } class _BooksPageState extends State { RefreshController _refreshController = RefreshController(initialRefresh: false); @override void initState() { BlocProvider.of(context).add(BooksRefreshed()); super.initState(); } @override Widget build(BuildContext context) { return MultiBlocListener( listeners: [ BlocListener( listenWhen: (previous, current) => previous.loadMoreStatus != current.loadMoreStatus, listener: (context, state) { if (state.loadMoreStatus == LoadDataStatus.success) { _refreshController.loadComplete(); } else if (state.loadMoreStatus == LoadDataStatus.failure) { _refreshController.loadFailed(); } else if (state.loadMoreStatus == LoadDataStatus.empty) { _refreshController.loadNoData(); } }, ) ], child: Scaffold( appBar: AppBar( centerTitle: true, title: const Text('账本'), actions: [ IconButton( icon: const Icon(Icons.add), onPressed: () { } ) ] ), body: BlocBuilder( buildWhen: (previous, current) => previous.status != current.status || previous.books != current.books, builder: (context, state) { switch (state.status) { case LoadDataStatus.progress: case LoadDataStatus.initial: return const PageLoading(); case LoadDataStatus.success: if (state.books.isEmpty) return Empty(); return SmartRefresher( enablePullDown: true, enablePullUp: true, controller: _refreshController, child: _buildList(context, state.books), onRefresh: () async { BlocProvider.of(context).add(BooksRefreshed()); _refreshController.refreshCompleted(); }, onLoading: () async { BlocProvider.of(context).add(BooksLoadMore()); }, ); default: return PageError(onTap: () { BlocProvider.of(context).add(BooksRefreshed()); }); } } ), ) ); } Widget _buildList(BuildContext context, List books) { final theme = Theme.of(context); return ListView.separated( itemCount: books.length, itemBuilder: (context, index) { Book book = books[index]; return ListTile( dense: false, title: Text(book.name, style: theme.textTheme.bodyText1), subtitle: book.notes != null && book.notes!.isNotEmpty ? Text(book.notes!, style: theme.textTheme.caption) : null, trailing: Icon(Icons.keyboard_arrow_right), onTap: () { Navigator.pushNamed(context, '/book-detail', arguments: BookDetailArguments(book: book)); }, ); }, separatorBuilder: (context, index) { return Divider(); }, ); } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/bloc/category_fetch/category_fetch_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import '/categories/categories.dart'; import '/commons/commons.dart'; part 'category_fetch_event.dart'; part 'category_fetch_state.dart'; class CategoryFetchBloc extends Bloc { final CategoryRepository categoryRepository; CategoryFetchBloc({ required this.categoryRepository, }) : super(CategoryFetchState()) { on(_onFetched); on(_onDefault); } void _onDefault(CategoryLoadDefault event, Emitter emit) { emit(state.copyWith( status: LoadDataStatus.success, category: event.category )); } void _onFetched(_, Emitter emit) async { try { emit(state.copyWith(status: LoadDataStatus.progress)); final category = await categoryRepository.get(state.category!.id); emit(state.copyWith( status: LoadDataStatus.success, category: category, )); } catch (_) { emit(state.copyWith(status: LoadDataStatus.failure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/bloc/category_fetch/category_fetch_event.dart ================================================ part of 'category_fetch_bloc.dart'; @immutable class CategoryFetchEvent extends Equatable { const CategoryFetchEvent(); @override List get props => []; } class CategoryFetched extends CategoryFetchEvent {} class CategoryLoadDefault extends CategoryFetchEvent { final Category category; const CategoryLoadDefault({ required this.category, }); List get props => [category]; } ================================================ FILE: bookkeeping_user_flutter/lib/categories/bloc/category_fetch/category_fetch_state.dart ================================================ part of 'category_fetch_bloc.dart'; @immutable class CategoryFetchState extends Equatable { final LoadDataStatus status; final Category? category; const CategoryFetchState({ this.status = LoadDataStatus.initial, this.category, }); CategoryFetchState copyWith({ LoadDataStatus? status, Category? category, }) { return CategoryFetchState( status: status ?? this.status, category: category ?? this.category, ); } @override List get props => [status, category]; } ================================================ FILE: bookkeeping_user_flutter/lib/categories/bloc/category_form/category_form_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:formz/formz.dart'; import 'package:meta/meta.dart'; import '/categories/categories.dart'; part 'category_form_event.dart'; part 'category_form_state.dart'; class CategoryFormBloc extends Bloc { final CategoryRepository categoryRepository; final ExpenseCategorySelectBloc expenseCategorySelectBloc; final IncomeCategorySelectBloc incomeCategorySelectBloc; CategoryFormBloc({ required this.categoryRepository, required this.expenseCategorySelectBloc, required this.incomeCategorySelectBloc }) : super(const CategoryFormState()) { on(_onNameChanged); on(_onNotesChanged); on(_onParentChanged); on(_onDefaultLoaded); on(_onSubmitted); } void _onNameChanged(CategoryFormNameChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(name: event.name), )); } void _onNotesChanged(CategoryFormNotesChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(notes: event.notes), )); } void _onParentChanged(CategoryFormParentChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(parentId: event.parentId), )); } void _onDefaultLoaded(CategoryFormDefaultLoaded event, Emitter emit) { emit(state.copyWith( status: FormzStatus.pure, request: state.request.copyWith( name: event.type == 2 ? event.category?.name ?? '' : '', notes: event.type == 2 ? event.category?.notes ?? '' : '', parentId: event.type == 2 ? event.category?.parentId.toString() : event.category?.id.toString() ) )); } void _onSubmitted(CategoryFormSubmitted event, Emitter emit) async { try { bool result = false; switch (event.type) { case 1: result = await categoryRepository.add(event.categoryType, state.request); break; case 2: result = await categoryRepository.update(event.categoryType, event.category!.id, state.request); break; default: break; } if (result) { emit(state.copyWith(status: FormzStatus.submissionSuccess)); if (event.categoryType == 1) { expenseCategorySelectBloc.add(ExpenseCategorySelectLoaded()); } else if (event.categoryType == 2) { incomeCategorySelectBloc.add(IncomeCategorySelectLoaded()); } } else { emit(state.copyWith(status: FormzStatus.submissionFailure)); } } catch (_) { print(_); emit(state.copyWith(status: FormzStatus.submissionFailure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/bloc/category_form/category_form_event.dart ================================================ part of 'category_form_bloc.dart'; @immutable abstract class CategoryFormEvent extends Equatable { const CategoryFormEvent(); @override List get props => []; } class CategoryFormNameChanged extends CategoryFormEvent { const CategoryFormNameChanged(this.name); final String name; @override List get props => [name]; } class CategoryFormNotesChanged extends CategoryFormEvent { const CategoryFormNotesChanged(this.notes); final String notes; @override List get props => [notes]; } class CategoryFormParentChanged extends CategoryFormEvent { const CategoryFormParentChanged(this.parentId); final String parentId; @override List get props => [parentId]; } class CategoryFormDefaultLoaded extends CategoryFormEvent { final int type; final int categoryType; final Category? category; const CategoryFormDefaultLoaded(this.type, this.categoryType, this.category); @override List get props => [type, categoryType, category]; } class CategoryFormSubmitted extends CategoryFormEvent { final int type; final int categoryType; final Category? category; const CategoryFormSubmitted(this.type, this.categoryType, this.category); @override List get props => [type, categoryType, category]; } ================================================ FILE: bookkeeping_user_flutter/lib/categories/bloc/category_form/category_form_state.dart ================================================ part of 'category_form_bloc.dart'; @immutable class CategoryFormState extends Equatable { final FormzStatus status; final CategoryFormRequest request; const CategoryFormState({ this.status = FormzStatus.pure, this.request = const CategoryFormRequest(), }); CategoryFormState copyWith({ FormzStatus? status, CategoryFormRequest? request, }) { return CategoryFormState( status: status ?? this.status, request: request ?? this.request, ); } @override List get props => [status, request]; } ================================================ FILE: bookkeeping_user_flutter/lib/categories/bloc/category_tree/category_tree_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import '/categories/categories.dart'; import '/commons/commons.dart'; part 'category_tree_event.dart'; part 'category_tree_state.dart'; class CategoryTreeBloc extends Bloc { final CategoryRepository categoryRepository; final ExpenseCategorySelectBloc expenseCategorySelectBloc; final IncomeCategorySelectBloc incomeCategorySelectBloc; CategoryTreeBloc({ required this.categoryRepository, required this.expenseCategorySelectBloc, required this.incomeCategorySelectBloc }) : super(CategoryTreeState()) { on(_onExpenseCategoryRefreshed); on(_onIncomeCategoryRefreshed); on(_onItemClicked); on(_onBackClicked); on(_onToggled); on(_onDeleted); } void _onExpenseCategoryRefreshed(_, Emitter emit) async { try { emit(state.copyWith( status: LoadDataStatus.progress, )); final categories = await categoryRepository.queryExpense(); emit(state.copyWith( status: LoadDataStatus.success, categories: categories, currentCategories: categories )); } catch (_) { print(_); emit(state.copyWith(status: LoadDataStatus.failure)); } } void _onIncomeCategoryRefreshed(_, Emitter emit) async { try { emit(state.copyWith( status: LoadDataStatus.progress, )); final categories = await categoryRepository.queryIncome(); emit(state.copyWith( status: LoadDataStatus.success, categories: categories, currentCategories: categories )); } catch (_) { print(_); emit(state.copyWith(status: LoadDataStatus.failure)); } } void _onItemClicked(CategoryItemClicked event, Emitter emit) async { List> newHistory = List.from(state.history); newHistory.add(state.currentCategories); emit(state.copyWith( currentLevel: state.currentLevel + 1, currentCategories: event.categoryTree.children, history: newHistory, )); } void _onBackClicked(CategoryBackClicked event, Emitter emit) async { emit(state.copyWith( currentLevel: state.currentLevel - 1, currentCategories: state.history.last )); } void _onToggled(CategoryToggled event, Emitter emit) async { try { emit(state.copyWith(toggleStatus: LoadDataStatus.progress)); final result = await categoryRepository.toggle(event.id); if (result) { emit(state.copyWith(toggleStatus: LoadDataStatus.success)); expenseCategorySelectBloc.add(ExpenseCategorySelectLoaded()); incomeCategorySelectBloc.add(IncomeCategorySelectLoaded()); } else { emit(state.copyWith(toggleStatus: LoadDataStatus.failure)); } } catch (_) { emit(state.copyWith(toggleStatus: LoadDataStatus.failure)); } } void _onDeleted(CategoryDeleted event, Emitter emit) async { try { emit(state.copyWith(deleteStatus: LoadDataStatus.progress)); final result = await categoryRepository.delete(event.id); if (result) { emit(state.copyWith(deleteStatus: LoadDataStatus.success)); expenseCategorySelectBloc.add(ExpenseCategorySelectLoaded()); incomeCategorySelectBloc.add(IncomeCategorySelectLoaded()); } else { emit(state.copyWith(deleteStatus: LoadDataStatus.failure)); } } catch (_) { emit(state.copyWith(deleteStatus: LoadDataStatus.failure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/bloc/category_tree/category_tree_event.dart ================================================ part of 'category_tree_bloc.dart'; @immutable abstract class CategoryTreeEvent extends Equatable { const CategoryTreeEvent(); @override List get props => []; } class ExpenseCategoryTreeRefreshed extends CategoryTreeEvent { } class IncomeCategoryTreeRefreshed extends CategoryTreeEvent { } class CategoryItemClicked extends CategoryTreeEvent { final CategoryTree categoryTree; const CategoryItemClicked({ required this.categoryTree, }); } class CategoryBackClicked extends CategoryTreeEvent { } class CategoryDeleted extends CategoryTreeEvent { final String id; const CategoryDeleted(this.id); @override List get props => [id]; } class CategoryToggled extends CategoryTreeEvent { final String id; const CategoryToggled(this.id); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/categories/bloc/category_tree/category_tree_state.dart ================================================ part of 'category_tree_bloc.dart'; @immutable class CategoryTreeState extends Equatable { final LoadDataStatus status; final List categories; final LoadDataStatus deleteStatus; final LoadDataStatus toggleStatus; final List currentCategories; final int currentLevel; final List> history; @override List get props => [status, categories, deleteStatus, toggleStatus, currentCategories, currentLevel, history]; const CategoryTreeState({ this.status = LoadDataStatus.initial, this.categories = const [], this.deleteStatus = LoadDataStatus.initial, this.toggleStatus = LoadDataStatus.initial, this.currentCategories = const [], this.currentLevel = 1, this.history = const >[], }); CategoryTreeState copyWith({ LoadDataStatus? status, List? categories, LoadDataStatus? deleteStatus, LoadDataStatus? toggleStatus, List? currentCategories, int? currentLevel, List>? history }) { return CategoryTreeState( status: status ?? this.status, categories: categories ?? this.categories, deleteStatus: deleteStatus ?? this.deleteStatus, toggleStatus: toggleStatus ?? this.toggleStatus, currentCategories: currentCategories ?? this.currentCategories, currentLevel: currentLevel ?? this.currentLevel, history: history ?? this.history ); } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/bloc/expense_category_select/expense_category_select_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/categories/categories.dart'; part 'expense_category_select_event.dart'; part 'expense_category_select_state.dart'; class ExpenseCategorySelectBloc extends Bloc { final CategoryRepository categoryRepository; ExpenseCategorySelectBloc({ required this.categoryRepository }) : super(ExpenseCategorySelectStateLoadInProgress()) { on(_onExpenseCategorySelectLoaded); } void _onExpenseCategorySelectLoaded(_, Emitter emit) async { // 只加载一次数据 // if (state is ExpenseCategorySelectStateLoadSuccess) return; try { emit(ExpenseCategorySelectStateLoadInProgress()); final categories = await categoryRepository.getExpenseable(); emit(ExpenseCategorySelectStateLoadSuccess(categories)); } catch (_) { emit(ExpenseCategorySelectStateLoadFailure()); } } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/bloc/expense_category_select/expense_category_select_event.dart ================================================ part of 'expense_category_select_bloc.dart'; abstract class ExpenseCategorySelectEvent extends Equatable { const ExpenseCategorySelectEvent(); @override List get props => []; } class ExpenseCategorySelectLoaded extends ExpenseCategorySelectEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/categories/bloc/expense_category_select/expense_category_select_state.dart ================================================ part of 'expense_category_select_bloc.dart'; abstract class ExpenseCategorySelectState extends Equatable { const ExpenseCategorySelectState(); @override List get props => []; } class ExpenseCategorySelectStateLoadInProgress extends ExpenseCategorySelectState { } class ExpenseCategorySelectStateLoadSuccess extends ExpenseCategorySelectState { final List categories; const ExpenseCategorySelectStateLoadSuccess(this.categories); @override List get props => [categories]; } class ExpenseCategorySelectStateLoadFailure extends ExpenseCategorySelectState { } ================================================ FILE: bookkeeping_user_flutter/lib/categories/bloc/income_category_select/income_category_select_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/categories/categories.dart'; part 'income_category_select_event.dart'; part 'income_category_select_state.dart'; class IncomeCategorySelectBloc extends Bloc { final CategoryRepository categoryRepository; IncomeCategorySelectBloc({ required this.categoryRepository }) : super(IncomeCategorySelectStateLoadInProgress()) { on(_onIncomeCategorySelectLoaded); } void _onIncomeCategorySelectLoaded(_, Emitter emit) async { // 只加载一次数据 // if (state is IncomeCategorySelectStateLoadSuccess) return; try { emit(IncomeCategorySelectStateLoadInProgress()); final categories = await categoryRepository.getIncomeable(); emit(IncomeCategorySelectStateLoadSuccess(categories)); } catch (_) { emit(IncomeCategorySelectStateLoadFailure()); } } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/bloc/income_category_select/income_category_select_event.dart ================================================ part of 'income_category_select_bloc.dart'; abstract class IncomeCategorySelectEvent extends Equatable { const IncomeCategorySelectEvent(); @override List get props => []; } class IncomeCategorySelectLoaded extends IncomeCategorySelectEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/categories/bloc/income_category_select/income_category_select_state.dart ================================================ part of 'income_category_select_bloc.dart'; abstract class IncomeCategorySelectState extends Equatable { const IncomeCategorySelectState(); @override List get props => []; } class IncomeCategorySelectStateLoadInProgress extends IncomeCategorySelectState { } class IncomeCategorySelectStateLoadSuccess extends IncomeCategorySelectState { final List categories; const IncomeCategorySelectStateLoadSuccess(this.categories); @override List get props => [categories]; } class IncomeCategorySelectStateLoadFailure extends IncomeCategorySelectState { } ================================================ FILE: bookkeeping_user_flutter/lib/categories/categories.dart ================================================ export 'bloc/expense_category_select/expense_category_select_bloc.dart'; export 'bloc/income_category_select/income_category_select_bloc.dart'; export 'bloc/category_tree/category_tree_bloc.dart'; export 'bloc/category_fetch/category_fetch_bloc.dart'; export 'bloc/category_form/category_form_bloc.dart'; export 'data/category_repository.dart'; export 'data/models/category.dart'; export 'data/models/category_tree.dart'; export 'data/models/category_form_request.dart'; export 'ui/expense_categories_page.dart'; export 'ui/income_categories_page.dart'; export 'ui/category_detail_page.dart'; export 'ui/category_form_page.dart'; export 'ui/expense_category_form_page.dart'; export 'ui/income_category_form_page.dart'; export 'ui/widgets/category_form/name_input.dart'; export 'ui/widgets/category_form/notes_input.dart'; export 'ui/widgets/category_form/expense_category_input.dart'; export 'ui/widgets/category_form/income_category_input.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/categories/data/category_repository.dart ================================================ import 'dart:convert'; import 'models/category.dart'; import '/commons/commons.dart'; import '/categories/categories.dart'; class CategoryRepository { List _responseToList(String response) { return (json.decode(response)['data']).map((i) => Category.fromJson(i)).toList(); } Future> getExpenseable() async { String response = await HttpClient().get('expense-categories/enable'); return _responseToList(response); } Future> getIncomeable() async { String response = await HttpClient().get('income-categories/enable'); return _responseToList(response); } Future> queryExpense() async { String response = await HttpClient().get('expense-categories'); return (json.decode(response)['data']).map((i) => CategoryTree.fromJson(i)).toList(); } Future> queryIncome() async { String response = await HttpClient().get('income-categories'); return (json.decode(response)['data']).map((i) => CategoryTree.fromJson(i)).toList(); } Future get(int id) async { String response = await HttpClient().get('categories/$id'); return Category.fromJson(json.decode(response)['data']); } Future toggle(String id) async { String response = await HttpClient().put('categories/$id/toggle'); return parseResponse(response); } Future delete(String id) async { String response = await HttpClient().delete('categories/$id'); return parseResponse(response); } Future add(int type, CategoryFormRequest request) async { String response = ''; switch(type) { case 1: response = await HttpClient().post('expense-categories', data: request.toJson()); return parseResponse(response); case 2: response = await HttpClient().post('income-categories', data: request.toJson()); return parseResponse(response); default: throw('type error'); } } Future update(int type, int id, CategoryFormRequest request) async { String response = ''; switch(type) { case 1: response = await HttpClient().put('expense-categories/$id', data: request.toJson()); return parseResponse(response); case 2: response = await HttpClient().put('income-categories/$id', data: request.toJson()); return parseResponse(response); default: throw('type error'); } } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/data/models/category.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import '/categories/categories.dart'; part 'category.g.dart'; @JsonSerializable() class Category extends Equatable { final int id; final String name; final String? notes; final bool enable; final int? parentId; final String? parentName; Category({ required this.id, required this.name, this.notes, required this.enable, this.parentId, this.parentName }); factory Category.fromJson(Map json) => _$CategoryFromJson(json); Map toJson() => _$CategoryToJson(this); factory Category.fromTree(CategoryTree categoryTree) => Category( id: categoryTree.id, name: categoryTree.name, notes: categoryTree.notes, enable: categoryTree.enable, parentId: categoryTree.parentId, parentName: categoryTree.parentName ); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/categories/data/models/category.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'category.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Category _$CategoryFromJson(Map json) => Category( id: json['id'] as int, name: json['name'] as String, notes: json['notes'] as String?, enable: json['enable'] as bool, parentId: json['parentId'] as int?, parentName: json['parentName'] as String?, ); Map _$CategoryToJson(Category instance) => { 'id': instance.id, 'name': instance.name, 'notes': instance.notes, 'enable': instance.enable, 'parentId': instance.parentId, 'parentName': instance.parentName, }; ================================================ FILE: bookkeeping_user_flutter/lib/categories/data/models/category_form_request.dart ================================================ import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; part 'category_form_request.g.dart'; @JsonSerializable() class CategoryFormRequest extends Equatable { final String? name; final String? notes; final String? parentId; const CategoryFormRequest({ this.name, this.notes, this.parentId, }); Map toJson() => _$CategoryFormRequestToJson(this); CategoryFormRequest copyWith({ String? name, String? notes, String? parentId }) { return CategoryFormRequest( name: name ?? this.name, notes: notes ?? this.notes, parentId: parentId ?? this.parentId ); } @override List get props => [name, notes, parentId]; } ================================================ FILE: bookkeeping_user_flutter/lib/categories/data/models/category_form_request.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'category_form_request.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** CategoryFormRequest _$CategoryFormRequestFromJson(Map json) => CategoryFormRequest( name: json['name'] as String?, notes: json['notes'] as String?, parentId: json['parentId'] as String?, ); Map _$CategoryFormRequestToJson( CategoryFormRequest instance) => { 'name': instance.name, 'notes': instance.notes, 'parentId': instance.parentId, }; ================================================ FILE: bookkeeping_user_flutter/lib/categories/data/models/category_tree.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'category_tree.g.dart'; @JsonSerializable() class CategoryTree extends Equatable { final int id; final String name; final String? notes; final bool enable; final int? parentId; final String? parentName; final List? children; CategoryTree({ required this.id, required this.name, this.notes, required this.enable, this.parentId, this.parentName, this.children }); factory CategoryTree.fromJson(Map json) => _$CategoryTreeFromJson(json); Map toJson() => _$CategoryTreeToJson(this); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/categories/data/models/category_tree.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'category_tree.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** CategoryTree _$CategoryTreeFromJson(Map json) => CategoryTree( id: json['id'] as int, name: json['name'] as String, notes: json['notes'] as String?, enable: json['enable'] as bool, parentId: json['parentId'] as int?, parentName: json['parentName'] as String?, children: (json['children'] as List?) ?.map((e) => CategoryTree.fromJson(e as Map)) .toList(), ); Map _$CategoryTreeToJson(CategoryTree instance) => { 'id': instance.id, 'name': instance.name, 'notes': instance.notes, 'enable': instance.enable, 'parentId': instance.parentId, 'parentName': instance.parentName, 'children': instance.children, }; ================================================ FILE: bookkeeping_user_flutter/lib/categories/ui/category_detail_page.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/categories/categories.dart'; import '/commons/commons.dart'; import '/components/components.dart'; class CategoryDetailPage extends StatefulWidget { final int categoryType; final Category category; CategoryDetailPage({ required this.category, required this.categoryType }); @override State createState() => _CategoryDetailPageState(); } class _CategoryDetailPageState extends State { @override void initState() { BlocProvider.of(context).add(CategoryLoadDefault(category: widget.category)); super.initState(); } @override Widget build(BuildContext context) { return MultiBlocListener( listeners: [ BlocListener( listenWhen: (previous, current) => previous.deleteStatus != current.deleteStatus, listener: (context, state) { if (state.deleteStatus == LoadDataStatus.success) { Message.success('操作成功!'); if (Navigator.canPop(context)) { Navigator.of(context).pop(); } else { SystemNavigator.pop(); } } } ), BlocListener( listenWhen: (previous, current) => previous.toggleStatus != current.toggleStatus, listener: (context, state) { if (state.toggleStatus == LoadDataStatus.success) { Message.success('操作成功!'); BlocProvider.of(context).add(CategoryFetched()); } }, ) ], child: BlocBuilder( builder: (context, state) { return Scaffold( appBar: AppBar( centerTitle: true, title: const Text('类别详情'), actions: _buildActions(context, state.category ?? widget.category) ), body: Builder( builder: (context) { switch (state.status) { case LoadDataStatus.progress: case LoadDataStatus.initial: return const PageLoading(); case LoadDataStatus.success: return _buildBody(context, state.category ?? widget.category); default: return PageError(onTap: () { BlocProvider.of(context).add(CategoryFetched()); }); } }, ) ); } ) ); } List _buildActions(BuildContext context, Category category) { return [ IconButton( icon: const Icon(Icons.add), onPressed: () { fullDialog(context, CategoryFormPage(type: 1, categoryType: widget.categoryType, category: category)); } ), IconButton( icon: Icon(Icons.edit), onPressed: () { fullDialog(context, CategoryFormPage(type: 2, categoryType: widget.categoryType, category: category)); } ), IconButton( icon: Icon(Icons.delete), onPressed: () async { if (await confirm( context, content: Text("确定删除${category.name}吗?"), textOK: Text("确定"), textCancel: Text("取消"), )) { BlocProvider.of(context).add(CategoryDeleted(category.id.toString())); } } ) ]; } Widget _buildBody(BuildContext context, Category category) { final theme = Theme.of(context); TextStyle? style1 = theme.textTheme.bodyText2; TextStyle? style2 = theme.textTheme.bodyText1; return SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ SizedBox(height: 20), Row(children: [Text("名称:", style: style1), Text(category.name, style: style2)]), SizedBox(height: 15), Row(children: [Text("父级名称:", style: style1), Text(category.parentName ?? '', style: style2)]), SizedBox(height: 15), Row(children: [Text("是否可用:", style: style1), Text(boolToString(category.enable), style: style2)]), SizedBox(height: 15), Row(children: [Text("备注:", style: style1), Flexible(child: Text(category.notes ?? '', style: style2))]), SizedBox(height: 15), SizedBox( width: double.infinity, child: ElevatedButton( child: Text(category.enable ? '禁用' : '启用'), onPressed: () { BlocProvider.of(context).add(CategoryToggled(category.id.toString())); } ), ) ], ) ), ); } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/ui/category_form_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import '/components/components.dart'; import '/commons/commons.dart'; import '/categories/categories.dart'; class CategoryFormPage extends StatelessWidget { final int type; // 1-新增,2-修改 final int categoryType; final Category? category; const CategoryFormPage({ required this.type, required this.categoryType, this.category, }); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => CategoryFormBloc( categoryRepository: RepositoryProvider.of(context), expenseCategorySelectBloc: BlocProvider.of(context), incomeCategorySelectBloc: BlocProvider.of(context) )..add(CategoryFormDefaultLoaded(type, categoryType, category)), child: BlocListener( listener: (context, state) { if (state.status == FormzStatus.submissionSuccess) { Message.success('操作成功'); Navigator.of(context).pop(); if (categoryType == 1) { BlocProvider.of(context).add(ExpenseCategoryTreeRefreshed()); } else if (categoryType == 2) { BlocProvider.of(context).add(IncomeCategoryTreeRefreshed()); } if (type == 2) { BlocProvider.of(context).add(CategoryFetched()); } } }, child: Builder( builder: (context) { switch (categoryType) { case 1: return ExpenseCategoryFormPage(type: type, category: category); case 2: return IncomeCategoryFormPage(type: type, category: category); default: return PageError(msg: '分类类型错误'); } }, ) ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/ui/expense_categories_page.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/routes.dart'; import '/components/components.dart'; import '/commons/commons.dart'; import '/categories/categories.dart'; class ExpenseCategoryPage extends StatefulWidget { @override State createState() => _ExpenseCategoryPageState(); } class _ExpenseCategoryPageState extends State { @override void initState() { BlocProvider.of(context).add(ExpenseCategoryTreeRefreshed()); super.initState(); } @override Widget build(BuildContext context) { final state = context.watch().state; return MultiBlocListener( listeners: [ BlocListener( listenWhen: (previous, current) => previous.deleteStatus != current.deleteStatus, listener: (context, state) { if (state.deleteStatus == LoadDataStatus.success) { BlocProvider.of(context).add(ExpenseCategoryTreeRefreshed()); } }, ), BlocListener( listenWhen: (previous, current) => previous.toggleStatus != current.toggleStatus, listener: (context, state) { if (state.toggleStatus == LoadDataStatus.success) { BlocProvider.of(context).add(ExpenseCategoryTreeRefreshed()); } }, ), ], child: WillPopScope( onWillPop: () => _onWillPop(state), child: BlocBuilder( builder: (context, state) { return Scaffold( appBar: AppBar( title: const Text('支出类别'), centerTitle: true, actions: [ IconButton( icon: const Icon(Icons.add), onPressed: () { fullDialog(context, CategoryFormPage(type: 1, categoryType: 1)); } ) ] ), body: Builder( builder: (BuildContext context) { switch (state.status) { case LoadDataStatus.progress: case LoadDataStatus.initial: return const PageLoading(); case LoadDataStatus.success: if (state.currentCategories.isEmpty) return Empty(); return _buildList(context, state.currentCategories); default: return PageError(onTap: () { BlocProvider.of(context).add(ExpenseCategoryTreeRefreshed()); }); } }, ), ); }, ) ), ); } Widget _buildList(BuildContext context, List categories) { final theme = Theme.of(context); return ListView.separated( itemCount: categories.length, itemBuilder: (context, index) { CategoryTree categoryTree = categories[index]; return ListTile( dense: true, title: Text(categoryTree.name, style: theme.textTheme.bodyText1), subtitle: categoryTree.notes != null && categoryTree.notes!.isNotEmpty ? Text(categoryTree.notes!, style: theme.textTheme.caption) : null, trailing: categoryTree.children != null && categoryTree.children!.isNotEmpty ? Icon(Icons.keyboard_arrow_right) : null, onTap: () { if (categoryTree.children != null && categoryTree.children!.isNotEmpty) { BlocProvider.of(context).add(CategoryItemClicked(categoryTree: categoryTree)); } else { Navigator.pushNamed(context, '/category-detail', arguments: CategoryDetailArguments(category: Category.fromTree(categoryTree), categoryType: 1)); } }, onLongPress: () { Navigator.pushNamed(context, '/category-detail', arguments: CategoryDetailArguments(category: Category.fromTree(categoryTree), categoryType: 1)); }, ); }, separatorBuilder: (context, index) { return Divider(); }, ); } Future _onWillPop(state) async { if (state.currentLevel == 1) return true; BlocProvider.of(context).add(CategoryBackClicked()); return false; } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/ui/expense_category_form_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/categories/categories.dart'; class ExpenseCategoryFormPage extends StatelessWidget { final int type; // 1-新增,2-修改 final Category? category; const ExpenseCategoryFormPage({ required this.type, this.category }); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text(_buildTitle(type)), actions: [ IconButton( icon: Icon(Icons.done), onPressed: () { BlocProvider.of(context).add(CategoryFormSubmitted(type, 1, category)); } ) ] ), body: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ ExpenseCategoryInput(), NameInput(), SizedBox(height: 10), NotesInput(), SizedBox(height: 10), ], ), ), ) ); } String _buildTitle(int type) { switch (type) { case 1: return '新增支出分类'; case 2: return '修改支出分类'; default: return '操作类型异常'; } } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/ui/income_categories_page.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/routes.dart'; import '/components/components.dart'; import '/commons/commons.dart'; import '/categories/categories.dart'; class IncomeCategoryPage extends StatefulWidget { @override State createState() => _IncomeCategoryPageState(); } class _IncomeCategoryPageState extends State { @override void initState() { BlocProvider.of(context).add(IncomeCategoryTreeRefreshed()); super.initState(); } @override Widget build(BuildContext context) { final state = context.watch().state; return MultiBlocListener( listeners: [ BlocListener( listenWhen: (previous, current) => previous.deleteStatus != current.deleteStatus, listener: (context, state) { if (state.deleteStatus == LoadDataStatus.success) { BlocProvider.of(context).add(IncomeCategoryTreeRefreshed()); } }, ), BlocListener( listenWhen: (previous, current) => previous.toggleStatus != current.toggleStatus, listener: (context, state) { if (state.toggleStatus == LoadDataStatus.success) { BlocProvider.of(context).add(IncomeCategoryTreeRefreshed()); } }, ), ], child: WillPopScope( onWillPop: () => _onWillPop(state), child: BlocBuilder( builder: (context, state) { return Scaffold( appBar: AppBar( title: const Text('收入类别'), centerTitle: true, actions: [ IconButton( icon: const Icon(Icons.add), onPressed: () { fullDialog(context, CategoryFormPage(type: 1, categoryType: 2)); } ) ] ), body: Builder( builder: (BuildContext context) { switch (state.status) { case LoadDataStatus.progress: case LoadDataStatus.initial: return const PageLoading(); case LoadDataStatus.success: if (state.currentCategories.isEmpty) return Empty(); return _buildList(context, state.currentCategories); default: return PageError(onTap: () { BlocProvider.of(context).add(IncomeCategoryTreeRefreshed()); }); } }, ), ); }, ) ), ); } Widget _buildList(BuildContext context, List categories) { final theme = Theme.of(context); return ListView.separated( itemCount: categories.length, itemBuilder: (context, index) { CategoryTree categoryTree = categories[index]; return ListTile( dense: true, title: Text(categoryTree.name, style: theme.textTheme.bodyText1), subtitle: categoryTree.notes != null && categoryTree.notes!.isNotEmpty ? Text(categoryTree.notes!, style: theme.textTheme.caption) : null, trailing: categoryTree.children != null && categoryTree.children!.isNotEmpty ? Icon(Icons.keyboard_arrow_right) : null, onTap: () { if (categoryTree.children != null && categoryTree.children!.isNotEmpty) { BlocProvider.of(context).add(CategoryItemClicked(categoryTree: categoryTree)); } else { Navigator.pushNamed(context, '/category-detail', arguments: CategoryDetailArguments(category: Category.fromTree(categoryTree), categoryType: 2)); } }, onLongPress: () { Navigator.pushNamed(context, '/category-detail', arguments: CategoryDetailArguments(category: Category.fromTree(categoryTree), categoryType: 2)); }, ); }, separatorBuilder: (context, index) { return Divider(); }, ); } Future _onWillPop(state) async { if (state.currentLevel == 1) return true; BlocProvider.of(context).add(CategoryBackClicked()); return false; } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/ui/income_category_form_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/categories/categories.dart'; class IncomeCategoryFormPage extends StatelessWidget { final int type; // 1-新增,2-修改 final Category? category; const IncomeCategoryFormPage({ required this.type, this.category }); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text(_buildTitle(type)), actions: [ IconButton( icon: Icon(Icons.done), onPressed: () { BlocProvider.of(context).add(CategoryFormSubmitted(type, 2, category)); } ) ] ), body: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ IncomeCategoryInput(), NameInput(), SizedBox(height: 10), NotesInput(), SizedBox(height: 10), ], ), ), ) ); } String _buildTitle(int type) { switch (type) { case 1: return '新增收入分类'; case 2: return '修改收入分类'; default: return '操作类型异常'; } } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/ui/widgets/category_form/expense_category_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/categories/categories.dart'; class ExpenseCategoryInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocBuilder( buildWhen: (previous, current) => previous.request.parentId != current.request.parentId, builder: (context, state) { return SmartSelect.single ( title: '父级分类', selectedValue: state.request.parentId ?? '', onChange: (selected) { context.read().add(CategoryFormParentChanged(selected.value ?? '')); }, choiceItems: state1 is ExpenseCategorySelectStateLoadSuccess ? modelToChoice(state1.categories) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto: true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is ExpenseCategorySelectStateLoadInProgress, padding: EdgeInsets.zero, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/ui/widgets/category_form/income_category_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/categories/categories.dart'; class IncomeCategoryInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocBuilder( buildWhen: (previous, current) => previous.request.parentId != current.request.parentId, builder: (context, state) { return SmartSelect.single ( title: '父级分类', selectedValue: state.request.parentId ?? '', onChange: (selected) { context.read().add(CategoryFormParentChanged(selected.value ?? '')); }, choiceItems: state1 is IncomeCategorySelectStateLoadSuccess ? modelToChoice(state1.categories) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto: true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is IncomeCategorySelectStateLoadInProgress, padding: EdgeInsets.zero, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/ui/widgets/category_form/name_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/categories/categories.dart'; class NameInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '名称', BlocBuilder( buildWhen: (previous, current) => previous.request.name != current.request.name, builder: (context, state) { controller.value = TextEditingValue( text: state.request.name ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state.request.name?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(CategoryFormNameChanged(value)), decoration: InputDecoration( contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0), ), ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/categories/ui/widgets/category_form/notes_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/categories/categories.dart'; import '/commons/commons.dart'; class NotesInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '备注', BlocBuilder( buildWhen: (previous, current) => previous.request.notes != current.request.notes, builder: (context, state) { controller.value = TextEditingValue( text: state.request.notes ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state.request.notes?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(CategoryFormNotesChanged(value)), decoration: InputDecoration(), ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/bloc/report_asset/report_asset_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/charts/charts.dart'; part 'report_asset_state.dart'; part 'report_asset_event.dart'; class ReportAssetBloc extends Bloc { final ReportRepository reportRepository; ReportAssetBloc({ required this.reportRepository }) : super(ReportAssetStateLoadInProgress()) { on(_onReportAssetLoaded); } void _onReportAssetLoaded(_, Emitter emit) async { try { emit(ReportAssetStateLoadInProgress()); final xys = await reportRepository.getAsset(); emit(ReportAssetStateLoadSuccess(xys)); } catch (_) { emit(ReportAssetStateLoadFailure()); } } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/bloc/report_asset/report_asset_event.dart ================================================ part of 'report_asset_bloc.dart'; abstract class ReportAssetEvent extends Equatable { const ReportAssetEvent(); @override List get props => []; } class ReportAssetLoaded extends ReportAssetEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/charts/bloc/report_asset/report_asset_state.dart ================================================ part of 'report_asset_bloc.dart'; abstract class ReportAssetState extends Equatable { const ReportAssetState(); @override List get props => []; } class ReportAssetStateLoadInProgress extends ReportAssetState { } class ReportAssetStateLoadSuccess extends ReportAssetState { final List xys; const ReportAssetStateLoadSuccess(this.xys); @override List get props => [xys]; num get total { num total = xys.fold(0, (previousValue, element) => previousValue + element.y*100); return total / 100; } } class ReportAssetStateLoadFailure extends ReportAssetState { } ================================================ FILE: bookkeeping_user_flutter/lib/charts/bloc/report_debt/report_debt_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/charts/charts.dart'; part 'report_debt_state.dart'; part 'report_debt_event.dart'; class ReportDebtBloc extends Bloc { final ReportRepository reportRepository; ReportDebtBloc({ required this.reportRepository }) : super(ReportDebtStateLoadInProgress()) { on(_onReportDebtLoaded); } void _onReportDebtLoaded(_, Emitter emit) async { try { emit(ReportDebtStateLoadInProgress()); final xys = await reportRepository.getDebt(); emit(ReportDebtStateLoadSuccess(xys)); } catch (_) { emit(ReportDebtStateLoadFailure()); } } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/bloc/report_debt/report_debt_event.dart ================================================ part of 'report_debt_bloc.dart'; abstract class ReportDebtEvent extends Equatable { const ReportDebtEvent(); @override List get props => []; } class ReportDebtLoaded extends ReportDebtEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/charts/bloc/report_debt/report_debt_state.dart ================================================ part of 'report_debt_bloc.dart'; abstract class ReportDebtState extends Equatable { const ReportDebtState(); @override List get props => []; } class ReportDebtStateLoadInProgress extends ReportDebtState { } class ReportDebtStateLoadSuccess extends ReportDebtState { final List xys; const ReportDebtStateLoadSuccess(this.xys); @override List get props => [xys]; num get total { num total = xys.fold(0, (previousValue, element) => previousValue + element.y*100); // 解决精度问题 return total / 100; } } class ReportDebtStateLoadFailure extends ReportDebtState { } ================================================ FILE: bookkeeping_user_flutter/lib/charts/bloc/report_expense_category/report_expense_category_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import '/commons/commons.dart'; import '/charts/charts.dart'; part 'report_expense_category_event.dart'; part 'report_expense_category_state.dart'; class ReportExpenseCategoryBloc extends Bloc { final ReportRepository reportRepository; ReportExpenseCategoryBloc({ required this.reportRepository }) : super(ReportExpenseCategoryState()) { on(_onRefreshed); on(_onReset); on(_onMinTimeChanged); on(_onMaxTimeChanged); on(_onCategoryChanged); } void _onRefreshed(_, Emitter emit) async { try { emit(state.copyWith( status: LoadDataStatus.progress, request: state.request.copyWith( minTime: state.request.minTime ?? DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch, maxTime: state.request.maxTime ?? DateTime.now().millisecondsSinceEpoch, ) )); final xys = await reportRepository.queryExpenseCategory(state.request); emit(state.copyWith( status: LoadDataStatus.success, xys: xys, )); } catch (_) { print(_); emit(state.copyWith(status: LoadDataStatus.failure)); } } void _onMinTimeChanged(ReportExpenseCategoryMinTimeChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(minTime: event.minTime), )); } void _onMaxTimeChanged(ReportExpenseCategoryMaxTimeChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(maxTime: event.maxTime), )); } void _onCategoryChanged(ReportExpenseCategoryCategoryChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(categoryId: event.categoryId), )); } void _onReset(_, Emitter emit) async { emit(state.copyWith(request: CategoryQueryRequest( minTime: DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch, maxTime: DateTime.now().millisecondsSinceEpoch ))); this.add(ReportExpenseCategoryRefreshed()); } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/bloc/report_expense_category/report_expense_category_event.dart ================================================ part of 'report_expense_category_bloc.dart'; @immutable abstract class ReportExpenseCategoryEvent extends Equatable { const ReportExpenseCategoryEvent(); @override List get props => []; } class ReportExpenseCategoryRefreshed extends ReportExpenseCategoryEvent { } class ReportExpenseCategoryReset extends ReportExpenseCategoryEvent { } class ReportExpenseCategoryMinTimeChanged extends ReportExpenseCategoryEvent { const ReportExpenseCategoryMinTimeChanged(this.minTime); final int minTime; @override List get props => [minTime]; } class ReportExpenseCategoryMaxTimeChanged extends ReportExpenseCategoryEvent { const ReportExpenseCategoryMaxTimeChanged(this.maxTime); final int maxTime; @override List get props => [maxTime]; } class ReportExpenseCategoryCategoryChanged extends ReportExpenseCategoryEvent { const ReportExpenseCategoryCategoryChanged(this.categoryId); final String categoryId; @override List get props => [categoryId]; } ================================================ FILE: bookkeeping_user_flutter/lib/charts/bloc/report_expense_category/report_expense_category_state.dart ================================================ part of 'report_expense_category_bloc.dart'; class ReportExpenseCategoryState extends Equatable { final LoadDataStatus status; final List xys; final CategoryQueryRequest request; const ReportExpenseCategoryState({ this.status = LoadDataStatus.initial, this.xys = const [], this.request = const CategoryQueryRequest(), }); ReportExpenseCategoryState copyWith({ LoadDataStatus? status, List? xys, CategoryQueryRequest? request, }) { return ReportExpenseCategoryState( status: status ?? this.status, xys: xys ?? this.xys, request: request ?? this.request, ); } num get total { if (status == LoadDataStatus.success) { num total = xys.fold(0, (previousValue, element) => previousValue + element.y); return num.parse(total.toStringAsFixed(2)); } else { return 0; } } @override List get props => [status, xys, request]; } ================================================ FILE: bookkeeping_user_flutter/lib/charts/bloc/report_income_category/report_income_category_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import '/commons/commons.dart'; import '/charts/charts.dart'; part 'report_income_category_event.dart'; part 'report_income_category_state.dart'; class ReportIncomeCategoryBloc extends Bloc { final ReportRepository reportRepository; ReportIncomeCategoryBloc({ required this.reportRepository }) : super(ReportIncomeCategoryState()) { on(_onRefreshed); on(_onReset); on(_onMinTimeChanged); on(_onMaxTimeChanged); on(_onCategoryChanged); } void _onRefreshed(_, Emitter emit) async { try { emit(state.copyWith( status: LoadDataStatus.progress, request: state.request.copyWith( minTime: state.request.minTime ?? DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch, maxTime: state.request.maxTime ?? DateTime.now().millisecondsSinceEpoch, ) )); final xys = await reportRepository.queryIncomeCategory(state.request); emit(state.copyWith( status: LoadDataStatus.success, xys: xys, )); } catch (_) { print(_); emit(state.copyWith(status: LoadDataStatus.failure)); } } void _onMinTimeChanged(ReportIncomeCategoryMinTimeChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(minTime: event.minTime), )); } void _onMaxTimeChanged(ReportIncomeCategoryMaxTimeChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(maxTime: event.maxTime), )); } void _onCategoryChanged(ReportIncomeCategoryCategoryChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(categoryId: event.categoryId), )); } void _onReset(_, Emitter emit) async { emit(state.copyWith(request: CategoryQueryRequest( minTime: DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch, maxTime: DateTime.now().millisecondsSinceEpoch ))); this.add(ReportIncomeCategoryRefreshed()); } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/bloc/report_income_category/report_income_category_event.dart ================================================ part of 'report_income_category_bloc.dart'; @immutable abstract class ReportIncomeCategoryEvent extends Equatable { const ReportIncomeCategoryEvent(); @override List get props => []; } class ReportIncomeCategoryRefreshed extends ReportIncomeCategoryEvent { } class ReportIncomeCategoryReset extends ReportIncomeCategoryEvent { } class ReportIncomeCategoryMinTimeChanged extends ReportIncomeCategoryEvent { const ReportIncomeCategoryMinTimeChanged(this.minTime); final int minTime; @override List get props => [minTime]; } class ReportIncomeCategoryMaxTimeChanged extends ReportIncomeCategoryEvent { const ReportIncomeCategoryMaxTimeChanged(this.maxTime); final int maxTime; @override List get props => [maxTime]; } class ReportIncomeCategoryCategoryChanged extends ReportIncomeCategoryEvent { const ReportIncomeCategoryCategoryChanged(this.categoryId); final String categoryId; @override List get props => [categoryId]; } ================================================ FILE: bookkeeping_user_flutter/lib/charts/bloc/report_income_category/report_income_category_state.dart ================================================ part of 'report_income_category_bloc.dart'; class ReportIncomeCategoryState extends Equatable { final LoadDataStatus status; final List xys; final CategoryQueryRequest request; const ReportIncomeCategoryState({ this.status = LoadDataStatus.initial, this.xys = const [], this.request = const CategoryQueryRequest(), }); ReportIncomeCategoryState copyWith({ LoadDataStatus? status, List? xys, CategoryQueryRequest? request, }) { return ReportIncomeCategoryState( status: status ?? this.status, xys: xys ?? this.xys, request: request ?? this.request, ); } num get total { if (status == LoadDataStatus.success) { num total = xys.fold(0, (previousValue, element) => previousValue + element.y); return num.parse(total.toStringAsFixed(2)); } else { return 0; } } @override List get props => [status, xys, request]; } ================================================ FILE: bookkeeping_user_flutter/lib/charts/charts.dart ================================================ export 'data/report_repository.dart'; export 'data/models/x_y.dart'; export 'data/models/category_query_request.dart'; export 'bloc/report_asset/report_asset_bloc.dart'; export 'bloc/report_debt/report_debt_bloc.dart'; export 'bloc/report_expense_category/report_expense_category_bloc.dart'; export 'bloc/report_income_category/report_income_category_bloc.dart'; export 'ui/charts_page.dart'; export 'ui/charts_expense_category_filter_page.dart'; export 'ui/charts_income_category_filter_page.dart'; export 'ui/widgets/asset_sheet.dart'; export 'ui/widgets/debt_sheet.dart'; export 'ui/widgets/expense_category.dart'; export 'ui/widgets/income_category.dart'; export 'ui/widgets/circular_legend.dart'; export 'ui/widgets/date_input_expense.dart'; export 'ui/widgets/date_input_income.dart'; export 'ui/widgets/expense_category_input.dart'; export 'ui/widgets/income_category_input.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/charts/data/models/category_query_request.dart ================================================ import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; part 'category_query_request.g.dart'; @JsonSerializable() class CategoryQueryRequest extends Equatable { final int? minTime; final int? maxTime; final String? categoryId; const CategoryQueryRequest({ this.minTime, this.maxTime, this.categoryId }); factory CategoryQueryRequest.fromJson(Map json) => _$CategoryQueryRequestFromJson(json); Map toJson() => _$CategoryQueryRequestToJson(this); @override List get props => [minTime, maxTime, categoryId]; CategoryQueryRequest copyWith({ int? minTime, int? maxTime, String? categoryId }) { return CategoryQueryRequest( minTime: minTime ?? this.minTime, maxTime: maxTime ?? this.maxTime, categoryId: categoryId ?? this.categoryId ); } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/data/models/category_query_request.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'category_query_request.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** CategoryQueryRequest _$CategoryQueryRequestFromJson( Map json) => CategoryQueryRequest( minTime: json['minTime'] as int?, maxTime: json['maxTime'] as int?, categoryId: json['categoryId'] as String?, ); Map _$CategoryQueryRequestToJson( CategoryQueryRequest instance) => { 'minTime': instance.minTime, 'maxTime': instance.maxTime, 'categoryId': instance.categoryId, }; ================================================ FILE: bookkeeping_user_flutter/lib/charts/data/models/x_y.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'x_y.g.dart'; @JsonSerializable() class XY extends Equatable { final String x; final num y; final num percent; XY({ required this.x, required this.y, required this.percent }); factory XY.fromJson(Map json) => _$XYFromJson(json); Map toJson() => _$XYToJson(this); @override List get props => [x, y]; } ================================================ FILE: bookkeeping_user_flutter/lib/charts/data/models/x_y.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'x_y.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** XY _$XYFromJson(Map json) => XY( x: json['x'] as String, y: json['y'] as num, percent: json['percent'] as num, ); Map _$XYToJson(XY instance) => { 'x': instance.x, 'y': instance.y, 'percent': instance.percent, }; ================================================ FILE: bookkeeping_user_flutter/lib/charts/data/report_repository.dart ================================================ import 'dart:convert'; import '/commons/commons.dart'; import '/charts/charts.dart'; class ReportRepository { List _responseToList(String response) { return (json.decode(response)['data']).map((i) => XY.fromJson(i)).toList(); } Future> getAsset() async { String response = await HttpClient().get('reports/asset'); return _responseToList(response); } Future> getDebt() async { String response = await HttpClient().get('reports/debt'); return _responseToList(response); } Future> queryExpenseCategory(CategoryQueryRequest request) async { String response = await HttpClient().get('reports/expense-category', params: request.toJson()); return _responseToList(response); } Future> queryIncomeCategory(CategoryQueryRequest request) async { String response = await HttpClient().get('reports/income-category', params: request.toJson()); return _responseToList(response); } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/ui/charts_expense_category_filter_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/charts/charts.dart'; class ChartsExpenseCategoryFilterPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('搜索'), actions: [ IconButton( icon: Icon(Icons.done), onPressed: () { BlocProvider.of(context).add(ReportExpenseCategoryRefreshed()); Navigator.pop(context); }, ), ] ), body: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ DateInputExpense(), ExpenseCategoryInput() ], ), ) ), ); } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/ui/charts_income_category_filter_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/charts/charts.dart'; class ChartsIncomeCategoryFilterPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('搜索'), actions: [ IconButton( icon: Icon(Icons.done), onPressed: () { BlocProvider.of(context).add(ReportIncomeCategoryRefreshed()); Navigator.pop(context); }, ), ] ), body: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ DateInputIncome(), IncomeCategoryInput() ], ), ) ), ); } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/ui/charts_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/charts/charts.dart'; class ChartsPage extends StatefulWidget { @override State createState() => _ChartsPageState(); } class _ChartsPageState extends State with TickerProviderStateMixin { int tabIndex = 0; @override Widget build(BuildContext context) { return DefaultTabController( length: 4, initialIndex: 0, child: Builder( builder: (context) { final tabController = DefaultTabController.of(context)!; tabController.addListener(() { setState(() { tabIndex = tabController.index; }); }); return Scaffold( appBar: AppBar( leading: IconButton( onPressed: () { switch (tabIndex) { case 0: BlocProvider.of(context).add(ReportExpenseCategoryReset()); break; case 1: BlocProvider.of(context).add(ReportIncomeCategoryReset()); break; case 2: BlocProvider.of(context).add(ReportAssetLoaded()); break; case 3: BlocProvider.of(context).add(ReportDebtLoaded()); break; } }, icon: const Icon(Icons.refresh) ), title: TabBar( labelPadding: EdgeInsets.all(0), tabs: [ Tab(child: Text('支出')), Tab(child: Text('收入')), Tab(child: Text('资产')), Tab(child: Text('负债')), ], ), actions: [ IconButton( onPressed: (tabIndex == 2 || tabIndex == 3) ? null : () { if (tabIndex == 0) { Navigator.pushNamed(context, '/charts-expense-category-filter'); } else if (tabIndex == 1) { Navigator.pushNamed(context, '/charts-income-category-filter'); } }, icon: const Icon(Icons.search) ) ], ), body: TabBarView( children: [ ExpenseCategory(), IncomeCategory(), AssetSheet(), DebtSheet(), ] ) ); }, ), ); } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/ui/widgets/asset_sheet.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import '/charts/charts.dart'; import '/components/components.dart'; import '/commons/commons.dart'; class AssetSheet extends StatelessWidget { @override Widget build(BuildContext context) { return SingleChildScrollView( child: Column( children: [ BlocBuilder( builder: (context, state) { if (state is ReportAssetStateLoadSuccess) { return Column( children: [ SfCircularChart( title: ChartTitle(text: '资产分类'), legend: Legend(isVisible: false), tooltipBehavior: TooltipBehavior(enable: true), series: getDefaultDoughnutSeries(state.xys), annotations: [ CircularChartAnnotation( widget: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('总金额', style: TextStyle(color: Colors.grey, fontSize: 12)), Text(removeDecimalZero(state.total), style: TextStyle(color: Colors.black, fontSize: 18)) ], ) ) ], ), Divider(), CircularLegend(xys: state.xys), Divider(), ], ); } return Container( height: 200, child: const PageLoading(), ); }, ), ], ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/ui/widgets/circular_legend.dart ================================================ import 'package:flutter/material.dart'; import '/commons/commons.dart'; import '/charts/charts.dart'; class CircularLegend extends StatelessWidget { final List xys; CircularLegend({ required this.xys }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return ListView.separated( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), itemCount: xys.length, separatorBuilder: (_, __) => const Divider(), itemBuilder: (context, index) { XY xy = xys[index]; return ListTile( dense: true, visualDensity: VisualDensity(horizontal: 0, vertical: -4), title: Text(xy.x, style: theme.textTheme.titleSmall), subtitle: Text(removeDecimalZero(xy.y), style: theme.textTheme.caption), trailing: Text(removeDecimalZero(xy.percent)+"%", style: theme.textTheme.titleMedium), ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/ui/widgets/date_input_expense.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncfusion_flutter_datepicker/datepicker.dart'; import 'package:intl/intl.dart'; import '/components/components.dart'; import '/charts/charts.dart'; class DateInputExpense extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (previous, current) => previous.request.minTime != current.request.minTime || previous.request.maxTime != current.request.maxTime, builder: (context, state) { return Row( children: [ ElevatedButton( onPressed: () async { final PickerDateRange? range = await showDialog( context: context, builder: (BuildContext context) { return DateRangePicker( PickerDateRange(DateTime.fromMillisecondsSinceEpoch(state.request.minTime!), DateTime.fromMillisecondsSinceEpoch(state.request.maxTime!)) ); } ); if (range != null) { BlocProvider.of(context).add(ReportExpenseCategoryMinTimeChanged(range.startDate!.millisecondsSinceEpoch)); BlocProvider.of(context).add(ReportExpenseCategoryMaxTimeChanged(range.endDate!.millisecondsSinceEpoch)); } }, child: Text('选择日期范围') ), SizedBox(width: 10), Text( DateFormat('yyyy.MM.dd').format(DateTime.fromMillisecondsSinceEpoch(state.request.minTime!))+ ' - '+ DateFormat('yyyy.MM.dd').format(DateTime.fromMillisecondsSinceEpoch(state.request.maxTime!)) ) ], ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/ui/widgets/date_input_income.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncfusion_flutter_datepicker/datepicker.dart'; import 'package:intl/intl.dart'; import '/components/components.dart'; import '/charts/charts.dart'; class DateInputIncome extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (previous, current) => previous.request.minTime != current.request.minTime || previous.request.maxTime != current.request.maxTime, builder: (context, state) { return Row( children: [ ElevatedButton( onPressed: () async { final PickerDateRange? range = await showDialog( context: context, builder: (BuildContext context) { return DateRangePicker( PickerDateRange(DateTime.fromMillisecondsSinceEpoch(state.request.minTime!), DateTime.fromMillisecondsSinceEpoch(state.request.maxTime!)) ); } ); if (range != null) { BlocProvider.of(context).add(ReportIncomeCategoryMinTimeChanged(range.startDate!.millisecondsSinceEpoch)); BlocProvider.of(context).add(ReportIncomeCategoryMaxTimeChanged(range.endDate!.millisecondsSinceEpoch)); } }, child: Text('选择日期范围') ), SizedBox(width: 10), Text( DateFormat('yyyy.MM.dd').format(DateTime.fromMillisecondsSinceEpoch(state.request.minTime!))+ ' - '+ DateFormat('yyyy.MM.dd').format(DateTime.fromMillisecondsSinceEpoch(state.request.maxTime!)) ) ], ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/ui/widgets/debt_sheet.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import '/charts/charts.dart'; import '/components/components.dart'; import '/commons/commons.dart'; class DebtSheet extends StatelessWidget { @override Widget build(BuildContext context) { return SingleChildScrollView( child: Column( children: [ BlocBuilder( builder: (context, state) { if (state is ReportDebtStateLoadSuccess) { return Column( children: [ SfCircularChart( title: ChartTitle(text: '负债分类'), legend: Legend(isVisible: false), tooltipBehavior: TooltipBehavior(enable: true), series: getDefaultDoughnutSeries(state.xys), annotations: [ CircularChartAnnotation( widget: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('总金额', style: TextStyle(color: Colors.grey, fontSize: 12)), Text(removeDecimalZero(state.total), style: TextStyle(color: Colors.black, fontSize: 18)) ], ) ) ], ), Divider(), CircularLegend(xys: state.xys), Divider(), ], ); } return Container( height: 200, child: const PageLoading(), ); }, ), ], ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/ui/widgets/expense_category.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import '/charts/charts.dart'; import '/components/components.dart'; import '/commons/commons.dart'; class ExpenseCategory extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); return SingleChildScrollView( child: BlocBuilder( builder: (context, state) { if (state.status == LoadDataStatus.success) { return Column( children: [ SizedBox(height: 30), Text( DateFormat('yyyy.MM.dd').format(DateTime.fromMillisecondsSinceEpoch(state.request.minTime!))+ ' - '+ DateFormat('yyyy.MM.dd').format(DateTime.fromMillisecondsSinceEpoch(state.request.maxTime!)), style: theme.textTheme.headline6, ), SfCircularChart( title: ChartTitle(text: '支出分类'), legend: Legend(isVisible: false), tooltipBehavior: TooltipBehavior(enable: true), series: getDefaultDoughnutSeries(state.xys), annotations: [ CircularChartAnnotation( widget: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('总金额', style: TextStyle(color: Colors.grey, fontSize: 12)), Text(removeDecimalZero(state.total), style: TextStyle(color: Colors.black, fontSize: 18)) ], ) ) ], ), Divider(), CircularLegend(xys: state.xys), ], ); } return Container( height: 200, child: const PageLoading(), ); }, ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/ui/widgets/expense_category_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/charts/charts.dart'; import '/categories/categories.dart'; class ExpenseCategoryInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector( selector: (state) => state.request.categoryId, builder: (context, state) { return SmartSelect.single ( title: '支出分类', selectedValue: state ?? '', onChange: (selected) { context.read().add(ReportExpenseCategoryCategoryChanged(selected.value ?? '')); }, choiceItems: state1 is ExpenseCategorySelectStateLoadSuccess ? modelToChoice(state1.categories) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto:true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is ExpenseCategorySelectStateLoadInProgress, padding: EdgeInsets.symmetric(horizontal: 0), ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/ui/widgets/income_category.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import '/charts/charts.dart'; import '/components/components.dart'; import '/commons/commons.dart'; class IncomeCategory extends StatelessWidget { @override Widget build(BuildContext context) { return SingleChildScrollView( child: BlocBuilder( builder: (context, state) { if (state.status == LoadDataStatus.success) { return Column( children: [ SizedBox(height: 30), Text( DateFormat('yyyy.MM.dd').format(DateTime.fromMillisecondsSinceEpoch(state.request.minTime!))+ ' - '+ DateFormat('yyyy.MM.dd').format(DateTime.fromMillisecondsSinceEpoch(state.request.maxTime!)), style: Theme.of(context).textTheme.headline6, ), SfCircularChart( title: ChartTitle(text: '收入分类'), legend: Legend(isVisible: false), tooltipBehavior: TooltipBehavior(enable: true), series: getDefaultDoughnutSeries(state.xys), annotations: [ CircularChartAnnotation( widget: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('总金额', style: TextStyle(color: Colors.grey, fontSize: 12)), Text(removeDecimalZero(state.total), style: TextStyle(color: Colors.black, fontSize: 18)) ], ) ) ], ), Divider(), CircularLegend(xys: state.xys), ], ); } return Container( height: 200, child: const PageLoading(), ); }, ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/charts/ui/widgets/income_category_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/charts/charts.dart'; import '/categories/categories.dart'; class IncomeCategoryInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector( selector: (state) => state.request.categoryId, builder: (context, state) { return SmartSelect.single ( title: '收入分类', selectedValue: state ?? '', onChange: (selected) { context.read().add(ReportIncomeCategoryCategoryChanged(selected.value ?? '')); }, choiceItems: state1 is IncomeCategorySelectStateLoadSuccess ? modelToChoice(state1.categories) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto:true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is IncomeCategorySelectStateLoadInProgress, padding: EdgeInsets.symmetric(horizontal: 0), ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/commons/common_util.dart ================================================ import 'dart:convert'; import 'package:intl/intl.dart'; String removeDecimalZero(num n) { RegExp regex = RegExp(r"([.]*0)(?!.*\d)"); return n.toString().replaceAll(regex, ""); } String boolToString(bool val) { if (val) return '是'; else return '否'; } String dateFormat(int? timestamp) { if (timestamp == null) return ''; return DateFormat('yyyy-MM-dd').format(DateTime.fromMillisecondsSinceEpoch(timestamp)); } bool parseResponse(String response) { return json.decode(response)['success']; } ================================================ FILE: bookkeeping_user_flutter/lib/commons/commons.dart ================================================ export 'common_util.dart'; export 'http_client.dart'; export 'session.dart'; export 'widget_util.dart'; export 'id_name_model.dart'; export 'enums.dart'; export 'web_view_page.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/commons/enums.dart ================================================ enum LoadDataStatus { initial, progress, success, empty, failure } ================================================ FILE: bookkeeping_user_flutter/lib/commons/http_client.dart ================================================ import 'package:dio/dio.dart'; import '/commons/commons.dart'; class HttpClient { // 单例模式 static final HttpClient _instance = HttpClient._internal(); factory HttpClient() => _instance; HttpClient._internal() { init(); } late Dio _dio; init(){ print('init'); print(session['apiUrl']); BaseOptions baseOptions = BaseOptions( // baseUrl: 'http://127.0.0.1:9092/api/v1/', baseUrl: session['apiUrl'], contentType: 'application/json', // responseType: ResponseType.plain, connectTimeout: 30000, receiveTimeout: 30000 ); _dio = Dio(baseOptions); _dio.interceptors.add(TokenInterceptor()); _dio.interceptors.add(ExceptionInterceptor()); } Future get(String uri, {Map? params}) async { var response = await _dio.get(uri, queryParameters: params); return response.toString(); } Future post(String uri, {data}) async { var response = await _dio.post(uri, data: data); return response.toString(); } Future delete(String uri) async { var response = await _dio.delete(uri); return response.toString(); } Future put(String uri, {data}) async { var response = await _dio.put(uri, data: data); return response.toString(); } Future upload(data) async { var response = await _dio.post('http://upload-z2.qiniup.com', data: FormData.fromMap(data)); return response.toString(); } } class TokenInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { if (session["userToken"] != null) { options.headers['User-Token'] = session["userToken"]; } super.onRequest(options, handler); } } // 全局错误处理 class ExceptionInterceptor extends Interceptor { @override void onResponse(Response response, ResponseInterceptorHandler handler) async { if (!response.data['success']) { if (response.data['errorCode'] == 8 || response.data['errorCode'] == 10) { Message.error("登录状态已过期,请退出之后重新登录。"); } else { Message.error(response.data['errorMsg']); } } super.onResponse(response, handler); } @override void onError(DioError e, handler) { // if (e.response != null) { // // The request was made and the server responded with a status code // // that falls out of the range of 2xx and is also not 304. // Message.error(e.response!.data['errorMsg']); // } else { // // Something happened in setting up or sending the request that triggered an Error // Message.error('网络错误,请稍后重试'); // } print(e); String errorMsg = e.response?.data['errorMsg'] ?? '网络错误,请稍后重试'; Message.error(errorMsg); super.onError(e, handler); } } ================================================ FILE: bookkeeping_user_flutter/lib/commons/id_name_model.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'id_name_model.g.dart'; @JsonSerializable() class IdNameModel extends Equatable { final int id; final String name; const IdNameModel({ required this.id, required this.name, }); factory IdNameModel.fromJson(Map json) => _$IdNameModelFromJson(json); Map toJson() => _$IdNameModelToJson(this); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/commons/id_name_model.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'id_name_model.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** IdNameModel _$IdNameModelFromJson(Map json) => IdNameModel( id: json['id'] as int, name: json['name'] as String, ); Map _$IdNameModelToJson(IdNameModel instance) => { 'id': instance.id, 'name': instance.name, }; ================================================ FILE: bookkeeping_user_flutter/lib/commons/session.dart ================================================ Map session = { 'userToken': null, 'user': null, 'apiUrl': 'http://jz.jiukuaitech.com/api/v1/', }; ================================================ FILE: bookkeeping_user_flutter/lib/commons/web_view_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; class WebViewPage extends StatefulWidget { final String title; final String url; const WebViewPage({ required this.title, required this.url }); @override _WebViewPageState createState() => _WebViewPageState(); } class _WebViewPageState extends State { int loadingPercentage = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text(widget.title), ), body: Stack( children: [ WebView( initialUrl: widget.url, javascriptMode: JavascriptMode.unrestricted, onPageStarted: (url) { setState(() { loadingPercentage = 0; }); }, onProgress: (progress) { setState(() { loadingPercentage = progress; }); }, onPageFinished: (url) { setState(() { loadingPercentage = 100; }); }, ), if (loadingPercentage < 100) LinearProgressIndicator(value: loadingPercentage / 100.0), ], ), ); } } ================================================ FILE: bookkeeping_user_flutter/lib/commons/widget_util.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:awesome_select/awesome_select.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import '/charts/charts.dart'; Widget buildFormItem(String label, Widget field, BuildContext context) { return Container( // height: 45, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text(label, style: Theme.of(context).textTheme.bodyText1), SizedBox(width: 10), Expanded(child: field) ] ) ); } List> modelToChoice(models) { return S2Choice.listFrom( source: models, value: (index, item) => item.id.toString(), title: (index, item) => item.name, ); } List> modelToChoiceCode(models) { return S2Choice.listFrom( source: models, value: (index, item) => item.code, title: (index, item) => item.code, ); } Future confirm( BuildContext context, { Widget? title, Widget? content, Widget? textOK, Widget? textCancel, }) async { final bool? isConfirm = await showDialog( context: context, builder: (_) => WillPopScope( child: AlertDialog( title: title, content: (content != null) ? content : Text('Are you sure continue?'), actions: [ TextButton( child: textCancel != null ? textCancel : Text('Cancel'), onPressed: () => Navigator.pop(context, false), ), TextButton( child: textOK != null ? textOK : Text('OK'), onPressed: () => Navigator.pop(context, true), ), ], ), onWillPop: () async { Navigator.pop(context, false); return true; }, ), ); return (isConfirm != null) ? isConfirm : false; } class Message { static success(String msg) { Fluttertoast.showToast( msg: msg, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.TOP, timeInSecForIosWeb: 1, backgroundColor: Colors.black, textColor: Colors.white, fontSize: 16.0 ); } static error(String msg) { Fluttertoast.showToast( msg: msg, toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.TOP, timeInSecForIosWeb: 1, backgroundColor: Colors.red, textColor: Colors.white, fontSize: 16.0 ); } } void fullDialog(BuildContext context, Widget widget) { Navigator.of(context, rootNavigator: false).push( // ensures fullscreen CupertinoPageRoute( fullscreenDialog: true, builder: (context) => widget ) ); } List> getDefaultDoughnutSeries(xys) { return [ DoughnutSeries( radius: '80%', innerRadius: '70%', dataSource: xys, xValueMapper: (XY data, _) => data.x, yValueMapper: (XY data, _) => data.y, dataLabelMapper: (XY data, _) => data.x + ": " + data.percent.toString() + "%", dataLabelSettings: const DataLabelSettings( isVisible: true, textStyle: const TextStyle(fontSize: 6), labelPosition: ChartDataLabelPosition.outside ), legendIconType: LegendIconType.circle, ) ]; } ================================================ FILE: bookkeeping_user_flutter/lib/components/components.dart ================================================ export 'lazy_indexed_stack.dart'; export 'page_loading.dart'; export 'page_error.dart'; export 'empty.dart'; export 'popup_menu.dart'; export 'date_time_input.dart'; export 'date_range_picker.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/components/date_range_picker.dart ================================================ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:syncfusion_flutter_datepicker/datepicker.dart'; class DateRangePicker extends StatefulWidget { final dynamic range; const DateRangePicker(this.range); @override _DateRangePickerState createState() => _DateRangePickerState(); } class _DateRangePickerState extends State { dynamic _controller; dynamic _range; @override void initState() { _range = widget.range; _controller = DateRangePickerController(); super.initState(); } @override Widget build(BuildContext context) { final Widget selectedDateWidget = Container( color: Colors.transparent, padding: const EdgeInsets.symmetric(vertical: 16.0), child: Container( height: 30, padding: const EdgeInsets.symmetric(horizontal: 4.0), child: _range == null || _range.startDate == null || _range.endDate == null || _range.startDate == _range.endDate ? Text( DateFormat('yyyy-MM-dd').format(_range.startDate ?? _range.endDate), textAlign: TextAlign.center, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.black ) ) : Row( children: [ Expanded( flex: 5, child: Text( DateFormat('yyyy-MM-dd').format( _range.startDate.isAfter(_range.endDate) == true ? _range.endDate : _range.startDate ), maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.black), ) ), const VerticalDivider( thickness: 1, ), Expanded( flex: 5, child: Text( DateFormat('yyyy-MM-dd').format( _range.startDate.isAfter(_range.endDate) == true ? _range.startDate : _range.endDate ), maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.black), ) ) ], ), ), ); _controller.selectedRange = _range; Widget pickerWidget = SfDateRangePicker( controller: _controller, selectionMode: DateRangePickerSelectionMode.range, showNavigationArrow: true, showActionButtons: true, confirmText: '确定', cancelText: '取消', onCancel: () => Navigator.pop(context, null), headerStyle: DateRangePickerHeaderStyle( textAlign: TextAlign.center, textStyle: TextStyle(color: Colors.black, fontSize: 15) ), onSubmit: (Object? value) { Navigator.pop(context, _range); }, onSelectionChanged: (DateRangePickerSelectionChangedArgs details) { setState(() { _range = details.value; }); }, ); return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)), child: Container( width: 300, height: 400, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ selectedDateWidget, Flexible( child: Padding( padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 5), child: pickerWidget ) ) ], ), ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/components/date_time_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class DateTimeInput extends StatelessWidget { final int? initialTime; final Function(DateTime) onDateChange; final Function(TimeOfDay) onTimeChange; DateTimeInput({ this.initialTime, required this.onDateChange, required this.onTimeChange, }); @override Widget build(BuildContext context) { return Row( children: [ ElevatedButton( onPressed: () { showDatePicker( context: context, initialDate: DateTime.fromMillisecondsSinceEpoch(initialTime ?? DateTime.now().millisecondsSinceEpoch), firstDate: DateTime(2015), lastDate: DateTime(2025) ).then((value) => { if (value != null) onDateChange(value) }); }, child: Text('选择日期') ), SizedBox(width: 5), ElevatedButton( onPressed: () { showTimePicker( context: context, initialTime: TimeOfDay.now(), ).then((value) => { if (value != null) onTimeChange(value) }); }, child: Text('选择时间') ), SizedBox(width: 10), Expanded( child: Text(DateFormat('yyyy-MM-dd kk:mm').format(DateTime.fromMillisecondsSinceEpoch(initialTime ?? DateTime.now().millisecondsSinceEpoch))), ) ], ); } } ================================================ FILE: bookkeeping_user_flutter/lib/components/empty.dart ================================================ import 'package:flutter/material.dart'; class Empty extends StatelessWidget { const Empty({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Icons.feedback_outlined, size: 70), Text('无数据', style: theme.textTheme.headline4), ], ), ); } } ================================================ FILE: bookkeeping_user_flutter/lib/components/lazy_indexed_stack.dart ================================================ import 'package:flutter/widgets.dart'; class LazyIndexedStack extends StatefulWidget { final AlignmentGeometry alignment; final TextDirection? textDirection; final StackFit sizing; final int? index; //reuse the created view final bool reuse; final int itemCount; final IndexedWidgetBuilder itemBuilder; LazyIndexedStack( {Key? key, this.alignment = AlignmentDirectional.topStart, this.textDirection, this.sizing = StackFit.loose, this.index, this.reuse = true, required this.itemBuilder, this.itemCount = 0}) : super(key: key); @override _LazyIndexedStackState createState() => _LazyIndexedStackState(); } class _LazyIndexedStackState extends State { late List _children; late List _loaded; @override void initState() { _loaded = []; _children = []; for (int i = 0; i < widget.itemCount; ++i) { if (i == widget.index) { _children.add(widget.itemBuilder(context, i)); _loaded.add(true); } else { _children.add(new Container()); _loaded.add(false); } } super.initState(); } @override void didUpdateWidget(LazyIndexedStack oldWidget) { for (int i = 0; i < widget.itemCount; ++i) { if (i == widget.index) { if (!_loaded[i]) { _children[i] = widget.itemBuilder(context, i); _loaded[i] = true; } else { if (widget.reuse) { return; } _children[i] = widget.itemBuilder(context, i); } } } super.didUpdateWidget(oldWidget); } @override Widget build(BuildContext context) { return new IndexedStack( index: widget.index, alignment: widget.alignment, textDirection: widget.textDirection, sizing: widget.sizing, children: _children, ); } } ================================================ FILE: bookkeeping_user_flutter/lib/components/page_error.dart ================================================ import 'package:flutter/material.dart'; class PageError extends StatelessWidget { final String? msg; final Function()? onTap; const PageError({ this.msg, this.onTap, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return GestureDetector( onTap: onTap, child: Container( color: Colors.white, child: Center( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text('🙈', style: TextStyle(fontSize: 42)), Text( msg ?? '加载异常,点击屏幕重新加载。', style: theme.textTheme.headline6, ), ], ), ), ), ); } } ================================================ FILE: bookkeeping_user_flutter/lib/components/page_loading.dart ================================================ import 'package:flutter/material.dart'; class PageLoading extends StatelessWidget { const PageLoading({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Center( child: CircularProgressIndicator(), ); } } ================================================ FILE: bookkeeping_user_flutter/lib/components/popup_menu.dart ================================================ import 'package:flutter/material.dart'; class PopupMenu extends StatelessWidget { final Function(String) onSelected; final String selected; final Map items; PopupMenu({ required this.onSelected, required this.selected, required this.items, }); @override Widget build(BuildContext context) { final defaultStyle = Theme.of(context).textTheme.bodyText2; final activeStyle = defaultStyle!.copyWith(color: Theme.of(context).colorScheme.secondary); return PopupMenuButton( onSelected: onSelected, icon: Icon(Icons.swap_vert), itemBuilder: (context) => items.entries.map((i) => PopupMenuItem( value: i.key, child: Text(i.value, style: selected == i.key ? activeStyle : defaultStyle), )).toList() ); } } ================================================ FILE: bookkeeping_user_flutter/lib/currency/bloc/currency_all/currency_all_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/currency/currency.dart'; part 'currency_all_event.dart'; part 'currency_all_state.dart'; class CurrencyAllBloc extends Bloc { final CurrencyRepository currencyRepository; CurrencyAllBloc({ required this.currencyRepository }) : super(CurrencyAllStateLoadInProgress()) { on(_onCurrencyAllLoaded); } void _onCurrencyAllLoaded(_, Emitter emit) async { // 只加载一次数据 if (state is CurrencyAllStateLoadSuccess) return; emit(CurrencyAllStateLoadInProgress()); final currencies = await currencyRepository.getAll(); emit(CurrencyAllStateLoadSuccess(currencies)); } } ================================================ FILE: bookkeeping_user_flutter/lib/currency/bloc/currency_all/currency_all_event.dart ================================================ part of 'currency_all_bloc.dart'; abstract class CurrencyAllEvent extends Equatable { const CurrencyAllEvent(); @override List get props => []; } class CurrencyAllLoaded extends CurrencyAllEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/currency/bloc/currency_all/currency_all_state.dart ================================================ part of 'currency_all_bloc.dart'; abstract class CurrencyAllState extends Equatable { const CurrencyAllState(); @override List get props => []; } class CurrencyAllStateLoadInProgress extends CurrencyAllState { } class CurrencyAllStateLoadSuccess extends CurrencyAllState { final List currencies; const CurrencyAllStateLoadSuccess(this.currencies); @override List get props => [currencies]; } class CurrencyAllStateLoadFailure extends CurrencyAllState { } ================================================ FILE: bookkeeping_user_flutter/lib/currency/currency.dart ================================================ export 'data/currency_repository.dart'; export 'data/models/currency.dart'; export 'bloc/currency_all/currency_all_bloc.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/currency/data/currency_repository.dart ================================================ import 'dart:convert'; import '/commons/commons.dart'; import '/currency/currency.dart'; class CurrencyRepository { Future> getAll() async { String response = await HttpClient().get('currency/all'); return (json.decode(response)['data']).map((i) => Currency.fromJson(i)).toList(); } } ================================================ FILE: bookkeeping_user_flutter/lib/currency/data/models/currency.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'currency.g.dart'; @JsonSerializable() class Currency extends Equatable { final String code; final double rate; Currency({ required this.code, required this.rate, }); factory Currency.fromJson(Map json) => _$CurrencyFromJson(json); Map toJson() => _$CurrencyToJson(this); @override List get props => [code]; } ================================================ FILE: bookkeeping_user_flutter/lib/currency/data/models/currency.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'currency.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Currency _$CurrencyFromJson(Map json) => Currency( code: json['code'] as String, rate: (json['rate'] as num).toDouble(), ); Map _$CurrencyToJson(Currency instance) => { 'code': instance.code, 'rate': instance.rate, }; ================================================ FILE: bookkeeping_user_flutter/lib/flows/bloc/flow_fetch/flow_fetch_bloc.dart ================================================ import 'dart:io'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import '/commons/commons.dart'; import '/flows/flows.dart'; part 'flow_fetch_event.dart'; part 'flow_fetch_state.dart'; class FlowFetchBloc extends Bloc { final FlowRepository flowRepository; FlowFetchBloc({ required this.flowRepository, }) : super(FlowFetchState()) { on(_onFetched); on(_onImagesFetched); on(_onDefault); on(_onImageDeleted); on(_onImageUploaded); } void _onDefault(FlowLoadDefault event, Emitter emit) { emit(state.copyWith( status: LoadDataStatus.success, flow: event.flow )); } void _onFetched(_, Emitter emit) async { try { emit(state.copyWith(status: LoadDataStatus.progress)); final flow = await flowRepository.get(state.flow!.id); emit(state.copyWith( status: LoadDataStatus.success, flow: flow, )); } catch (_) { emit(state.copyWith(status: LoadDataStatus.failure)); } } void _onImagesFetched(_, Emitter emit) async { try { final images = await flowRepository.getImages(state.flow!.id); emit(state.copyWith( images: images, )); } catch (_) { print(_); } } void _onImageDeleted(FlowImageDeleted event, Emitter emit) async { try { emit(state.copyWith(deleteImageStatus: LoadDataStatus.progress)); final result = await flowRepository.deleteImage(event.id); if (result) { emit(state.copyWith(deleteImageStatus: LoadDataStatus.success)); } else { emit(state.copyWith(deleteImageStatus: LoadDataStatus.failure)); } } catch (_) { emit(state.copyWith(deleteImageStatus: LoadDataStatus.failure)); } } void _onImageUploaded(FlowImageUploaded event, Emitter emit) async { try { emit(state.copyWith(uploadImageStatus: LoadDataStatus.progress)); final result = await flowRepository.uploadImage(event.filePath, event.userId, event.flowId); if (result) { emit(state.copyWith(uploadImageStatus: LoadDataStatus.success)); } else { emit(state.copyWith(uploadImageStatus: LoadDataStatus.failure)); } } catch (_) { emit(state.copyWith(uploadImageStatus: LoadDataStatus.failure)); print(_); } } } ================================================ FILE: bookkeeping_user_flutter/lib/flows/bloc/flow_fetch/flow_fetch_event.dart ================================================ part of 'flow_fetch_bloc.dart'; @immutable class FlowFetchEvent extends Equatable { const FlowFetchEvent(); @override List get props => []; } class FlowFetched extends FlowFetchEvent { } class FlowImagesFetched extends FlowFetchEvent { } class FlowLoadDefault extends FlowFetchEvent { final FlowModel flow; const FlowLoadDefault({ required this.flow, }); List get props => [flow]; } class FlowImageDeleted extends FlowFetchEvent { final String id; const FlowImageDeleted(this.id); @override List get props => [id]; } class FlowImageUploaded extends FlowFetchEvent { final String filePath; final String flowId; final String userId; const FlowImageUploaded(this.filePath, this.flowId, this.userId); @override List get props => [filePath, flowId, userId]; } ================================================ FILE: bookkeeping_user_flutter/lib/flows/bloc/flow_fetch/flow_fetch_state.dart ================================================ part of 'flow_fetch_bloc.dart'; @immutable class FlowFetchState extends Equatable { final LoadDataStatus status; final FlowModel? flow; final List images; final LoadDataStatus deleteImageStatus; final LoadDataStatus uploadImageStatus; const FlowFetchState({ this.status = LoadDataStatus.initial, this.flow, this.images = const [], this.deleteImageStatus = LoadDataStatus.initial, this.uploadImageStatus = LoadDataStatus.initial, }); FlowFetchState copyWith({ LoadDataStatus? status, FlowModel? flow, List? images, LoadDataStatus? deleteImageStatus, LoadDataStatus? uploadImageStatus, }) { return FlowFetchState( status: status ?? this.status, flow: flow ?? this.flow, images: images ?? this.images, deleteImageStatus: deleteImageStatus ?? this.deleteImageStatus, uploadImageStatus: uploadImageStatus ?? this.uploadImageStatus, ); } @override List get props => [status, flow, images, deleteImageStatus, uploadImageStatus]; } ================================================ FILE: bookkeeping_user_flutter/lib/flows/bloc/flows/flows_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/flows/flows.dart'; part 'flows_event.dart'; part 'flows_state.dart'; class FlowsBloc extends Bloc { final FlowRepository flowRepository; FlowsBloc({ required this.flowRepository }) : super(FlowsState()) { on(_onLoadMore); on(_onRefreshed); on(_onReset); on(_onDeleted); on(_onConfirmed); on(_onPayeeChanged); on(_onCategoryChanged); on(_onTagChanged); on(_onTypeChanged); on(_onStatusChanged); on(_onAccountChanged); on(_onSortChanged); on(_onMinTimeChanged); on(_onMaxTimeChanged); } void _onRefreshed(_, Emitter emit) async { try { var now = DateTime.now(); emit(state.copyWith( status: LoadDataStatus.progress, request: state.request.copyWith( minTime: state.request.minTime ?? DateTime(now.year-1, now.month, now.day).millisecondsSinceEpoch, maxTime: state.request.maxTime ?? DateTime(now.year, now.month+1, now.day).millisecondsSinceEpoch, page: 1 ), )); final flows = await flowRepository.query(state.request); emit(state.copyWith( status: LoadDataStatus.success, flows: flows, request: state.request.copyWith(page: state.request.page + 1), )); } catch (_) { print(_); emit(state.copyWith(status: LoadDataStatus.failure)); } } void _onReset(_, Emitter emit) async { emit(state.copyWith(request: const FlowQueryRequest())); this.add(FlowsRefreshed()); } void _onLoadMore(_, Emitter emit) async { try { emit(state.copyWith(loadMoreStatus: LoadDataStatus.progress)); final flows = await flowRepository.query(state.request); if (flows.isNotEmpty) { emit(state.copyWith( request: state.request.copyWith(page: state.request.page + 1), flows: List.of(state.flows)..addAll(flows), loadMoreStatus: LoadDataStatus.success, )); } else { emit(state.copyWith(loadMoreStatus: LoadDataStatus.empty)); } } catch (_) { print(_); emit(state.copyWith(loadMoreStatus: LoadDataStatus.failure)); } } void _onDeleted(FlowsDeleted event, Emitter emit) async { try { emit(state.copyWith(deleteStatus: LoadDataStatus.progress)); final result = await flowRepository.delete(event.id); if (result) { emit(state.copyWith(deleteStatus: LoadDataStatus.success)); } else { emit(state.copyWith(deleteStatus: LoadDataStatus.failure)); } } catch (_) { emit(state.copyWith(deleteStatus: LoadDataStatus.failure)); } } void _onConfirmed(FlowsConfirmed event, Emitter emit) async { try { emit(state.copyWith(confirmStatus: LoadDataStatus.progress)); final result = await flowRepository.confirm(event.flow.id); if (result) { emit(state.copyWith(confirmStatus: LoadDataStatus.success)); } else { emit(state.copyWith(confirmStatus: LoadDataStatus.failure)); } } catch (_) { emit(state.copyWith(confirmStatus: LoadDataStatus.failure)); } } void _onPayeeChanged(FlowsFilterPayeeChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(payees: [event.payeeId]), )); } void _onCategoryChanged(FlowsFilterCategoryChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(categories: [event.categoryId]), )); } void _onTagChanged(FlowsFilterTagChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(tags: [event.tagId]), )); } void _onTypeChanged(FlowsFilterTypeChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(type: event.type), )); } void _onStatusChanged(FlowsFilterStatusChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(status: event.status), )); } void _onAccountChanged(FlowsFilterAccountIdChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(accountId: event.accountId), )); } void _onSortChanged(FlowsSortChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(sort: event.sort), )); } void _onMinTimeChanged(FlowsFilterMinTimeChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(minTime: event.minTime), )); } void _onMaxTimeChanged(FlowsFilterMaxTimeChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(maxTime: event.maxTime), )); } } ================================================ FILE: bookkeeping_user_flutter/lib/flows/bloc/flows/flows_event.dart ================================================ part of 'flows_bloc.dart'; abstract class FlowsEvent extends Equatable { const FlowsEvent(); @override List get props => []; } class FlowsLoadMore extends FlowsEvent {} class FlowsRefreshed extends FlowsEvent {} class FlowsReset extends FlowsEvent {} class FlowsDeleted extends FlowsEvent { final String id; const FlowsDeleted(this.id); @override List get props => [id]; } class FlowsConfirmed extends FlowsEvent { final FlowModel flow; const FlowsConfirmed(this.flow); @override List get props => [flow]; } class FlowsFilterPayeeChanged extends FlowsEvent { const FlowsFilterPayeeChanged(this.payeeId); final String payeeId; @override List get props => [payeeId]; } class FlowsFilterCategoryChanged extends FlowsEvent { const FlowsFilterCategoryChanged(this.categoryId); final String categoryId; @override List get props => [categoryId]; } class FlowsFilterTagChanged extends FlowsEvent { const FlowsFilterTagChanged(this.tagId); final String tagId; @override List get props => [tagId]; } class FlowsFilterTypeChanged extends FlowsEvent { const FlowsFilterTypeChanged(this.type); final String type; @override List get props => [type]; } class FlowsFilterStatusChanged extends FlowsEvent { const FlowsFilterStatusChanged(this.status); final String status; @override List get props => [status]; } class FlowsFilterAccountIdChanged extends FlowsEvent { const FlowsFilterAccountIdChanged(this.accountId); final String accountId; @override List get props => [accountId]; } class FlowsFilterMinTimeChanged extends FlowsEvent { const FlowsFilterMinTimeChanged(this.minTime); final int minTime; @override List get props => [minTime]; } class FlowsFilterMaxTimeChanged extends FlowsEvent { const FlowsFilterMaxTimeChanged(this.maxTime); final int maxTime; @override List get props => [maxTime]; } class FlowsSortChanged extends FlowsEvent { const FlowsSortChanged(this.sort); final String sort; @override List get props => [sort]; } ================================================ FILE: bookkeeping_user_flutter/lib/flows/bloc/flows/flows_state.dart ================================================ part of 'flows_bloc.dart'; class FlowsState extends Equatable { const FlowsState({ this.status = LoadDataStatus.initial, this.flows = const [], this.request = const FlowQueryRequest(), this.loadMoreStatus = LoadDataStatus.initial, this.deleteStatus = LoadDataStatus.initial, this.confirmStatus = LoadDataStatus.initial, }); final LoadDataStatus status; final List flows; final FlowQueryRequest request; final LoadDataStatus loadMoreStatus; final LoadDataStatus deleteStatus; final LoadDataStatus confirmStatus; FlowsState copyWith({ LoadDataStatus? status, List? flows, FlowQueryRequest? request, LoadDataStatus? loadMoreStatus, LoadDataStatus? deleteStatus, LoadDataStatus? confirmStatus }) { return FlowsState( status: status ?? this.status, flows: flows ?? this.flows, request: request ?? this.request, loadMoreStatus: loadMoreStatus ?? this.loadMoreStatus, deleteStatus: deleteStatus ?? this.deleteStatus, confirmStatus: confirmStatus ?? this.confirmStatus ); } @override List get props => [status, flows, request, loadMoreStatus, deleteStatus, confirmStatus]; } ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/flow_repository.dart ================================================ import 'dart:convert'; import 'package:dio/dio.dart'; import '/commons/commons.dart'; import '/add_flow/add_flow.dart'; import '/flows/flows.dart'; class FlowRepository { Future saveExpense(DealAddRequest request) async { String response = await HttpClient().post('expenses', data: request.toJson()); return json.decode(response)['success']; } Future refundExpense(int id, DealAddRequest request) async { String response = await HttpClient().post('expenses/$id/refund', data: request.toJson()); return json.decode(response)['success']; } Future updateExpense(int id, DealAddRequest request) async { String response = await HttpClient().put('expenses/$id', data: request.toJson()); return json.decode(response)['success']; } Future saveIncome(DealAddRequest request) async { String response = await HttpClient().post('incomes', data: request.toJson()); return json.decode(response)['success']; } Future refundIncome(int id, DealAddRequest request) async { String response = await HttpClient().post('incomes/$id/refund', data: request.toJson()); return json.decode(response)['success']; } Future updateIncome(int id, DealAddRequest request) async { String response = await HttpClient().put('incomes/$id', data: request.toJson()); return json.decode(response)['success']; } Future saveTransfer(TransferAddRequest request) async { String response = await HttpClient().post('transfers', data: request.toJson()); return json.decode(response)['success']; } Future updateTransfer(int id, TransferAddRequest request) async { String response = await HttpClient().put('transfers/$id', data: request.toJson()); return json.decode(response)['success']; } Future> query(FlowQueryRequest request) async { String response = await HttpClient().get('flows', params: request.toJson()); var responseDecoded = json.decode(response); return (responseDecoded['data']['result']['content']).map((i) => FlowModel.fromJson(i)).toList(); } Future get(int id) async { String response = await HttpClient().get('flows/$id'); return FlowModel.fromJson(json.decode(response)['data']); } Future confirm(int id) async { String response = await HttpClient().put('flows/$id/confirm'); return json.decode(response)['success']; } Future delete(String id) async { String response = await HttpClient().delete('flows/$id'); return json.decode(response)['success']; } Future> getImages(int id) async { String response = await HttpClient().get('flows/$id/images'); return json.decode(response)['data'].map((i) => FlowImage.fromJson(i)).toList(); } Future deleteImage(String id) async { String response = await HttpClient().delete('flow-images/$id'); return json.decode(response)['success']; } Future getUploadToken() async { String response = await HttpClient().get('flow-images/upload-token'); return json.decode(response)['data']; } Future uploadImage(String filePath, String userId, String flowId) async { String token = await getUploadToken(); MultipartFile file = await MultipartFile.fromFile(filePath); String response = await HttpClient().upload({ 'file': file, 'token': token, 'x:userId': userId }); await HttpClient().post('flows/$flowId/image', data: json.decode(response)['data']['id']); return true; } } ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/models/adjust_balance.dart ================================================ import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; import '/commons/commons.dart'; part 'adjust_balance.g.dart'; @JsonSerializable() class AdjustBalance extends Equatable { final int id; final num amount; final IdNameModel? account; final String accountName; final int createTime; final String? description; final String? notes; final int status; AdjustBalance({ required this.id, required this.amount, required this.account, required this.accountName, required this.createTime, this.description, this.notes, required this.status }); factory AdjustBalance.fromJson(Map json) => _$AdjustBalanceFromJson(json); Map toJson() => _$AdjustBalanceToJson(this); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/models/adjust_balance.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'adjust_balance.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** AdjustBalance _$AdjustBalanceFromJson(Map json) => AdjustBalance( id: json['id'] as int, amount: json['amount'] as num, account: json['account'] == null ? null : IdNameModel.fromJson(json['account'] as Map), accountName: json['accountName'] as String, createTime: json['createTime'] as int, description: json['description'] as String?, notes: json['notes'] as String?, status: json['status'] as int, ); Map _$AdjustBalanceToJson(AdjustBalance instance) => { 'id': instance.id, 'amount': instance.amount, 'account': instance.account, 'accountName': instance.accountName, 'createTime': instance.createTime, 'description': instance.description, 'notes': instance.notes, 'status': instance.status, }; ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/models/category.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'category.g.dart'; @JsonSerializable() class Category extends Equatable { final int id; final num amount; final num convertedAmount; final int categoryId; final String categoryName; Category({ required this.id, required this.amount, required this.convertedAmount, required this.categoryId, required this.categoryName, }); factory Category.fromJson(Map json) => _$CategoryFromJson(json); Map toJson() => _$CategoryToJson(this); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/models/category.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'category.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Category _$CategoryFromJson(Map json) => Category( id: json['id'] as int, amount: json['amount'] as num, convertedAmount: json['convertedAmount'] as num, categoryId: json['categoryId'] as int, categoryName: json['categoryName'] as String, ); Map _$CategoryToJson(Category instance) => { 'id': instance.id, 'amount': instance.amount, 'convertedAmount': instance.convertedAmount, 'categoryId': instance.categoryId, 'categoryName': instance.categoryName, }; ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/models/deal.dart ================================================ import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; import '/commons/commons.dart'; import 'category.dart'; import 'tag.dart'; part 'deal.g.dart'; @JsonSerializable() class Deal extends Equatable { final int id; final num amount; final num convertedAmount; final IdNameModel? account; final String? accountName; final int createTime; final String? description; final String? notes; final int status; final List categories; final String categoryName; final List? tags; final IdNameModel? payee; Deal({ required this.id, required this.amount, required this.convertedAmount, this.account, this.accountName, required this.createTime, this.description, this.notes, required this.status, required this.categories, required this.categoryName, this.tags, this.payee }); factory Deal.fromJson(Map json) => _$DealFromJson(json); Map toJson() => _$DealToJson(this); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/models/deal.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'deal.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Deal _$DealFromJson(Map json) => Deal( id: json['id'] as int, amount: json['amount'] as num, convertedAmount: json['convertedAmount'] as num, account: json['account'] == null ? null : IdNameModel.fromJson(json['account'] as Map), accountName: json['accountName'] as String?, createTime: json['createTime'] as int, description: json['description'] as String?, notes: json['notes'] as String?, status: json['status'] as int, categories: (json['categories'] as List) .map((e) => Category.fromJson(e as Map)) .toList(), categoryName: json['categoryName'] as String, tags: (json['tags'] as List?) ?.map((e) => Tag.fromJson(e as Map)) .toList(), payee: json['payee'] == null ? null : IdNameModel.fromJson(json['payee'] as Map), ); Map _$DealToJson(Deal instance) => { 'id': instance.id, 'amount': instance.amount, 'convertedAmount': instance.convertedAmount, 'account': instance.account, 'accountName': instance.accountName, 'createTime': instance.createTime, 'description': instance.description, 'notes': instance.notes, 'status': instance.status, 'categories': instance.categories, 'categoryName': instance.categoryName, 'tags': instance.tags, 'payee': instance.payee, }; ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/models/flow.dart ================================================ import 'package:bookkeeping_user_flutter/flows/data/models/adjust_balance.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; import 'deal.dart'; import 'tag.dart'; import 'transfer.dart'; import '/commons/commons.dart'; part 'flow.g.dart'; @JsonSerializable() class FlowModel extends Equatable { final int id; final double amount; final String amountFormatted; final double? convertedAmount; final bool needConvert; final String? toCurrencyCode; final String? accountName; final String title; final String subTitle; final int createTime; final String createTimeFormatted; final String? description; final String? notes; final String? categoryName; final IdNameModel? payee; final int status; final String statusName; final List? tags; final String? tagsName; final int type; final String typeName; final Deal? expense; final Deal? income; final Transfer? transfer; final AdjustBalance? adjustBalance; FlowModel({ required this.id, required this.amount, required this.amountFormatted, this.convertedAmount, required this.needConvert, this.toCurrencyCode, this.accountName, required this.title, required this.subTitle, required this.createTime, required this.createTimeFormatted, this.description, this.notes, this.categoryName, this.payee, required this.status, required this.statusName, this.tags, this.tagsName, required this.type, required this.typeName, this.expense, this.income, this.transfer, this.adjustBalance }); factory FlowModel.fromJson(Map json) => _$FlowModelFromJson(json); Map toJson() => _$FlowModelToJson(this); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/models/flow.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'flow.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** FlowModel _$FlowModelFromJson(Map json) => FlowModel( id: json['id'] as int, amount: (json['amount'] as num).toDouble(), amountFormatted: json['amountFormatted'] as String, convertedAmount: (json['convertedAmount'] as num?)?.toDouble(), needConvert: json['needConvert'] as bool, toCurrencyCode: json['toCurrencyCode'] as String?, accountName: json['accountName'] as String?, title: json['title'] as String, subTitle: json['subTitle'] as String, createTime: json['createTime'] as int, createTimeFormatted: json['createTimeFormatted'] as String, description: json['description'] as String?, notes: json['notes'] as String?, categoryName: json['categoryName'] as String?, payee: json['payee'] == null ? null : IdNameModel.fromJson(json['payee'] as Map), status: json['status'] as int, statusName: json['statusName'] as String, tags: (json['tags'] as List?) ?.map((e) => Tag.fromJson(e as Map)) .toList(), tagsName: json['tagsName'] as String?, type: json['type'] as int, typeName: json['typeName'] as String, expense: json['expense'] == null ? null : Deal.fromJson(json['expense'] as Map), income: json['income'] == null ? null : Deal.fromJson(json['income'] as Map), transfer: json['transfer'] == null ? null : Transfer.fromJson(json['transfer'] as Map), adjustBalance: json['adjustBalance'] == null ? null : AdjustBalance.fromJson( json['adjustBalance'] as Map), ); Map _$FlowModelToJson(FlowModel instance) => { 'id': instance.id, 'amount': instance.amount, 'amountFormatted': instance.amountFormatted, 'convertedAmount': instance.convertedAmount, 'needConvert': instance.needConvert, 'toCurrencyCode': instance.toCurrencyCode, 'accountName': instance.accountName, 'title': instance.title, 'subTitle': instance.subTitle, 'createTime': instance.createTime, 'createTimeFormatted': instance.createTimeFormatted, 'description': instance.description, 'notes': instance.notes, 'categoryName': instance.categoryName, 'payee': instance.payee, 'status': instance.status, 'statusName': instance.statusName, 'tags': instance.tags, 'tagsName': instance.tagsName, 'type': instance.type, 'typeName': instance.typeName, 'expense': instance.expense, 'income': instance.income, 'transfer': instance.transfer, 'adjustBalance': instance.adjustBalance, }; ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/models/flow_image.dart ================================================ import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; part 'flow_image.g.dart'; @JsonSerializable() class FlowImage extends Equatable { final int id; final String url; final int userId; final int? dealId; FlowImage({ required this.id, required this.url, required this.userId, this.dealId, }); factory FlowImage.fromJson(Map json) => _$FlowImageFromJson(json); Map toJson() => _$FlowImageToJson(this); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/models/flow_image.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'flow_image.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** FlowImage _$FlowImageFromJson(Map json) => FlowImage( id: json['id'] as int, url: json['url'] as String, userId: json['userId'] as int, dealId: json['dealId'] as int?, ); Map _$FlowImageToJson(FlowImage instance) => { 'id': instance.id, 'url': instance.url, 'userId': instance.userId, 'dealId': instance.dealId, }; ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/models/flow_query_request.dart ================================================ import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; part 'flow_query_request.g.dart'; @JsonSerializable() class FlowQueryRequest extends Equatable { final List? payees; final String? type;//1-支出 2-收入 3-转账 4-余额调整 final String? accountId; final String? status;// final List? tags; final List? categories; final int? minTime; final int? maxTime; final int page; final int size; final String sort; const FlowQueryRequest({ this.payees, this.type, this.accountId, this.status, this.tags, this.categories, this.minTime, this.maxTime, this.page = 1, this.size = 10, this.sort = 'createTime,desc' }); factory FlowQueryRequest.fromJson(Map json) => _$FlowQueryRequestFromJson(json); Map toJson() => _$FlowQueryRequestToJson(this); @override List get props => [payees, type, accountId, status, tags, categories, minTime, maxTime, page, sort]; FlowQueryRequest copyWith({ List? payees, String? type, String? accountId, String? status, List? tags, List? categories, int? minTime, int? maxTime, int? page, int? size, String? sort, }) { return FlowQueryRequest( payees: payees ?? this.payees, type: type ?? this.type, accountId: accountId ?? this.accountId, status: status ?? this.status, tags: tags ?? this.tags, categories: categories ?? this.categories, minTime: minTime ?? this.minTime, maxTime: maxTime ?? this.maxTime, page: page ?? this.page, size: size ?? this.size, sort: sort ?? this.sort, ); } } ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/models/flow_query_request.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'flow_query_request.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** FlowQueryRequest _$FlowQueryRequestFromJson(Map json) => FlowQueryRequest( payees: (json['payees'] as List?)?.map((e) => e as String).toList(), type: json['type'] as String?, accountId: json['accountId'] as String?, status: json['status'] as String?, tags: (json['tags'] as List?)?.map((e) => e as String).toList(), categories: (json['categories'] as List?) ?.map((e) => e as String) .toList(), minTime: json['minTime'] as int?, maxTime: json['maxTime'] as int?, page: json['page'] as int? ?? 1, size: json['size'] as int? ?? 10, sort: json['sort'] as String? ?? 'createTime,desc', ); Map _$FlowQueryRequestToJson(FlowQueryRequest instance) => { 'payees': instance.payees, 'type': instance.type, 'accountId': instance.accountId, 'status': instance.status, 'tags': instance.tags, 'categories': instance.categories, 'minTime': instance.minTime, 'maxTime': instance.maxTime, 'page': instance.page, 'size': instance.size, 'sort': instance.sort, }; ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/models/tag.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'tag.g.dart'; @JsonSerializable() class Tag extends Equatable { final int id; final double amount; final double convertedAmount; final int tagId; final String tagName; Tag({ required this.id, required this.amount, required this.convertedAmount, required this.tagId, required this.tagName, }); factory Tag.fromJson(Map json) => _$TagFromJson(json); Map toJson() => _$TagToJson(this); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/models/tag.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'tag.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Tag _$TagFromJson(Map json) => Tag( id: json['id'] as int, amount: (json['amount'] as num).toDouble(), convertedAmount: (json['convertedAmount'] as num).toDouble(), tagId: json['tagId'] as int, tagName: json['tagName'] as String, ); Map _$TagToJson(Tag instance) => { 'id': instance.id, 'amount': instance.amount, 'convertedAmount': instance.convertedAmount, 'tagId': instance.tagId, 'tagName': instance.tagName, }; ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/models/transfer.dart ================================================ import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; import 'tag.dart'; import '/commons/commons.dart'; part 'transfer.g.dart'; @JsonSerializable() class Transfer extends Equatable { final int id; final num amount; final num convertedAmount; final IdNameModel from; final IdNameModel to; final String accountName; final int createTime; final String? description; final String? notes; final int status; final List? tags; Transfer({ required this.id, required this.amount, required this.convertedAmount, required this.from, required this.to, required this.accountName, required this.createTime, this.description, this.notes, required this.status, this.tags, }); factory Transfer.fromJson(Map json) => _$TransferFromJson(json); Map toJson() => _$TransferToJson(this); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/flows/data/models/transfer.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'transfer.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Transfer _$TransferFromJson(Map json) => Transfer( id: json['id'] as int, amount: json['amount'] as num, convertedAmount: json['convertedAmount'] as num, from: IdNameModel.fromJson(json['from'] as Map), to: IdNameModel.fromJson(json['to'] as Map), accountName: json['accountName'] as String, createTime: json['createTime'] as int, description: json['description'] as String?, notes: json['notes'] as String?, status: json['status'] as int, tags: (json['tags'] as List?) ?.map((e) => Tag.fromJson(e as Map)) .toList(), ); Map _$TransferToJson(Transfer instance) => { 'id': instance.id, 'amount': instance.amount, 'convertedAmount': instance.convertedAmount, 'from': instance.from, 'to': instance.to, 'accountName': instance.accountName, 'createTime': instance.createTime, 'description': instance.description, 'notes': instance.notes, 'status': instance.status, 'tags': instance.tags, }; ================================================ FILE: bookkeeping_user_flutter/lib/flows/flows.dart ================================================ export 'bloc/flows/flows_bloc.dart'; export 'bloc/flow_fetch/flow_fetch_bloc.dart'; export 'data/flow_repository.dart'; export 'data/models/flow.dart'; export 'data/models/deal.dart'; export 'data/models/transfer.dart'; export 'data/models/adjust_balance.dart'; export 'data/models/flow_query_request.dart'; export 'data/models/flow_image.dart'; export 'ui/flows_page.dart'; export 'ui/flow_detail.dart'; export 'ui/flows_filter.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/flows/ui/flow_detail.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; import '/components/components.dart'; import '/commons/commons.dart'; import '/flows/flows.dart'; import '/add_flow/add_flow.dart'; import '/login/login.dart'; class FlowDetailPage extends StatefulWidget { final FlowModel flow; FlowDetailPage({ required this.flow }); @override State createState() => _FlowDetailPageState(); } class _FlowDetailPageState extends State { @override void initState() { BlocProvider.of(context).add(FlowLoadDefault(flow: widget.flow)); BlocProvider.of(context).add(FlowImagesFetched()); super.initState(); } @override Widget build(BuildContext context) { return MultiBlocListener( listeners: [ BlocListener( listenWhen: (previous, current) => previous.deleteStatus != current.deleteStatus, listener: (context, state) { if (state.deleteStatus == LoadDataStatus.success) { Message.success('操作成功!'); // Navigator.of(context).pop(); // 下面这个可以解决黑屏问题,原因未知。TODO if(Navigator.canPop(context)){ Navigator.of(context).pop(); }else{ SystemNavigator.pop(); } } } ), BlocListener( listenWhen: (previous, current) => previous.confirmStatus != current.confirmStatus, listener: (context, state) { if (state.confirmStatus == LoadDataStatus.success) { Message.success('操作成功!'); BlocProvider.of(context).add(FlowFetched()); // Navigator.of(context).pop(); } } ), BlocListener( listenWhen: (previous, current) => previous.deleteImageStatus != current.deleteImageStatus, listener: (context, state) { if (state.deleteImageStatus == LoadDataStatus.success) { Message.success('图片删除成功!'); BlocProvider.of(context).add(FlowImagesFetched()); } } ), BlocListener( listenWhen: (previous, current) => previous.uploadImageStatus != current.uploadImageStatus, listener: (context, state) { if (state.uploadImageStatus == LoadDataStatus.success) { Message.success('图片上传成功!'); BlocProvider.of(context).add(FlowImagesFetched()); } } ) ], child: BlocBuilder( builder: (context, state) { return Scaffold( appBar: AppBar( centerTitle: true, title: const Text('账单详情'), actions: _buildActions(context, state) ), body: Builder( builder: (context) { switch (state.status) { case LoadDataStatus.progress: case LoadDataStatus.initial: return const PageLoading(); case LoadDataStatus.success: return _buildBody(context, state); default: return PageError(onTap: () { BlocProvider.of(context).add(FlowFetched()); }); } } ) ); } ) ); } List _buildActions(BuildContext context, FlowFetchState state) { FlowModel? flow = state.flow; return [ IconButton( icon: Icon(Icons.edit), onPressed: state.status == LoadDataStatus.success && flow != null ? () { fullDialog(context, AddFlowPage(type: 2, flow: flow)); } : null ), IconButton( icon: Icon(Icons.delete), onPressed: state.status == LoadDataStatus.success && flow != null ? () async { if (widget.flow.status == 3 ) { Message.error('已退款的账单必须先删除对应的退款记录。'); return; } if (await confirm( context, content: Text(widget.flow.status != 2 ? '删除账单会撤回对应账户余额变动且无法恢复' : '删除之后无法恢复' + ',确定删除吗?'), textOK: Text("确定"), textCancel: Text("取消"), )) { BlocProvider.of(context).add(FlowsDeleted(widget.flow.id.toString())); } } : null, ) ]; } Widget _buildActionBar(BuildContext context, FlowModel flow) { final state = context.watch().state; return OverflowBar( overflowAlignment: OverflowBarAlignment.center, spacing: 20, children: [ ElevatedButton( child: const Text('复制'), onPressed: flow.type != 4 ? () { fullDialog(context, AddFlowPage(type: 3, flow: flow)); } : null ), ElevatedButton( child: const Text('退款'), // 支出和收入,而且状态正常,不是退款的情况才能退款 onPressed: ((flow.type == 1 || flow.type == 2) && (flow.status == 1 && flow.amount > 0)) ? () { fullDialog(context, AddFlowPage(type: 4, flow: flow)); } : null ), ElevatedButton( child: const Text('确认'), onPressed: flow.status == 2 ? () async { if (await confirm( context, content: Text("状态将更改为确认,确定此操作吗?"), textOK: Text("确定"), textCancel: Text("取消"), )) { BlocProvider.of(context).add(FlowsConfirmed(flow)); } } : null ), ElevatedButton( child: const Text('图片'), onPressed: () { showModalBottomSheet( context: context, builder: (context) { return Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: new Icon(Icons.camera_alt), title: const Text('拍照'), onTap: () async { final ImagePicker _picker = ImagePicker(); final XFile? photo = await _picker.pickImage(source: ImageSource.camera); if (photo != null) { BlocProvider.of(context).add(FlowImageUploaded(photo.path, flow.id.toString(), state.session?.userSessionVO.id.toString() ?? '')); } Navigator.pop(context); }, ), ListTile( leading: new Icon(Icons.photo_library), title: const Text('图片库'), onTap: () async { final ImagePicker _picker = ImagePicker(); final XFile? photo = await _picker.pickImage(source: ImageSource.gallery); if (photo != null) { BlocProvider.of(context).add(FlowImageUploaded(photo.path, flow.id.toString(), state.session?.userSessionVO.id.toString() ?? '')); } Navigator.pop(context); }, ), SizedBox(height: 10), ListTile( leading: new Icon(Icons.cancel), title: const Text('取消'), onTap: () { Navigator.pop(context); }, ), ], ); } ); } ), ], ); } Widget _buildBody(BuildContext context, FlowFetchState state) { TextStyle? style1 = Theme.of(context).textTheme.bodyText2; TextStyle? style2 = Theme.of(context).textTheme.bodyText1; if (state.uploadImageStatus == LoadDataStatus.progress) { return const PageLoading(); } FlowModel flow = state.flow!; List images = state.images; return SingleChildScrollView( child: Column( children: [ // 放不下变两行可以用Wrap https://github.com/flutter/flutter/issues/53642 if (flow.type != 4) _buildActionBar(context, flow), SizedBox(height: 15), Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ Row(children: [Text("描述:", style: style1), Text(flow.description ?? "", style: style2)]), SizedBox(height: 15), Row(children: [Text("交易类型:", style: style1), Text(flow.typeName, style: style2)]), SizedBox(height: 15), Row(children: [Text("时间:", style: style1), Text(flow.createTimeFormatted, style: style2)]), SizedBox(height: 15), Row(children: [Text("金额:", style: style1), Text(flow.amount.toStringAsFixed(2), style: style2)]), SizedBox(height: 15), if (flow.needConvert) Row(children: [Text("折合${flow.toCurrencyCode}:", style: style1), Text(flow.convertedAmount != null ? flow.convertedAmount.toString() : '', style: style2)]), if (flow.needConvert) SizedBox(height: 15), Row(children: [Text("账户:", style: style1), Text(flow.accountName ?? "", style: style2)]), SizedBox(height: 15), if(flow.type == 1 || flow.type == 2) Row(children: [Text("类别:", style: style1), Text(flow.categoryName ?? "", style: style2)]), if(flow.type == 1 || flow.type == 2) SizedBox(height: 15), if(flow.type != 4) Row(children: [Text("标签:", style: style1), Text(flow.tagsName ?? "", style: style2)]), if(flow.type != 4) SizedBox(height: 15), if(flow.type == 1 || flow.type == 2) Row(children: [Text("交易对象:", style: style1), Text(flow.payee?.name ?? "", style: style2)]), if(flow.type == 1 || flow.type == 2) SizedBox(height: 15), Row(children: [Text("状态:", style: style1), Text(flow.statusName, style: style2)]), SizedBox(height: 15), Row(children: [Text("备注:", style: style1), Flexible(child: Text(flow.notes ?? "", style: style2))]), ], ) ), Column( children: images.map((e) => ( Column( children: [ SizedBox(height: 15), GestureDetector( onTap: () async { if (await confirm( context, content: const Text('确定删除此账单图片吗?'), textOK: Text("确定"), textCancel: Text("取消"), )) { BlocProvider.of(context).add(FlowImageDeleted(e.id.toString())); } }, child: Image.network(e.url), ) ], ) )).toList(), ), ], ), ); } } ================================================ FILE: bookkeeping_user_flutter/lib/flows/ui/flows_filter.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/flows/flows.dart'; import 'widgets/widgets.dart'; class FlowsFilterPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('搜索流水'), actions: [ IconButton( icon: Icon(Icons.done), onPressed: () { BlocProvider.of(context).add(FlowsRefreshed()); Navigator.pop(context); }, ), ] ), body: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 0), child: Column( children: [ DateInput(), PayeeInput(), TagInput(), ExpenseCategoryInput(), IncomeCategoryInput(), TypeInput(), AccountInput(), StatusInput(), ], ) ) ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/flows/ui/flows_page.dart ================================================ import 'package:bookkeeping_user_flutter/routes.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import '/commons/commons.dart'; import '/components/components.dart'; import '/flows/flows.dart'; import '/add_flow/add_flow.dart'; import 'widgets/widgets.dart'; class FlowsPage extends StatefulWidget { @override State createState() => _FlowsPageState(); } class _FlowsPageState extends State { RefreshController _refreshController = RefreshController(initialRefresh: false); @override void initState() { BlocProvider.of(context).add(FlowsRefreshed()); super.initState(); } @override Widget build(BuildContext context) { return MultiBlocListener( listeners: [ BlocListener( listenWhen: (previous, current) => previous.loadMoreStatus != current.loadMoreStatus, listener: (context, state) { if (state.loadMoreStatus == LoadDataStatus.success) { _refreshController.loadComplete(); } else if (state.loadMoreStatus == LoadDataStatus.failure) { _refreshController.loadFailed(); } else if (state.loadMoreStatus == LoadDataStatus.empty) { _refreshController.loadNoData(); } } ), BlocListener( listenWhen: (previous, current) => previous.deleteStatus != current.deleteStatus, listener: (context, state) { if (state.deleteStatus == LoadDataStatus.success) { // 不加延迟刷新会黑屏,原因未知。 TODO // await Future.delayed(const Duration(milliseconds: 500)); BlocProvider.of(context).add(FlowsRefreshed()); } } ), BlocListener( listenWhen: (previous, current) => previous.confirmStatus != current.confirmStatus, listener: (context, state) { if (state.confirmStatus == LoadDataStatus.success) { BlocProvider.of(context).add(FlowsRefreshed()); } } ) ], child: Scaffold( appBar: AppBar( leading: IconButton( onPressed: () { BlocProvider.of(context).add(FlowsReset()); }, icon: const Icon(Icons.refresh) ), title: const Text("流水"), centerTitle: true, actions: [ OrderButton(), IconButton( onPressed: () { Navigator.pushNamed(context, '/flows-filter'); }, icon: const Icon(Icons.search) ) ], ), body: BlocBuilder( buildWhen: (previous, current) => previous.status != current.status || previous.flows != current.flows, builder: (context, state) { switch (state.status) { case LoadDataStatus.progress: case LoadDataStatus.initial: return const PageLoading(); case LoadDataStatus.success: if (state.flows.isEmpty) return Empty(); return SmartRefresher( enablePullDown: true, enablePullUp: true, controller: _refreshController, child: _buildList(context, state.flows), onRefresh: () async { BlocProvider.of(context).add(FlowsRefreshed()); _refreshController.refreshCompleted(); }, onLoading: () async { BlocProvider.of(context).add(FlowsLoadMore()); }, ); default: return PageError(onTap: () { BlocProvider.of(context).add(FlowsRefreshed()); }); } } ) ) ); } Widget _buildList(BuildContext context, List flows) { final theme = Theme.of(context); return ListView.separated( itemCount: flows.length, itemBuilder: (context, index) { FlowModel flow = flows[index]; TextStyle amountStyle = theme.textTheme.headline6 ?? TextStyle(fontWeight: FontWeight.w500, fontSize: 20); if (flow.type == 1) amountStyle = amountStyle.copyWith(color: Colors.green); if (flow.type == 2) amountStyle = amountStyle.copyWith(color: Colors.red); return ListTile( dense: true, title: Text(flow.title, style: theme.textTheme.bodyText1), subtitle: Text(flow.subTitle, style: theme.textTheme.caption), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Text(flow.amountFormatted, style: amountStyle), Icon(Icons.keyboard_arrow_right) ], ), onTap: () { Navigator.pushNamed(context, '/flow-detail', arguments: FlowDetailArguments(flow: flow)); }, onLongPress: flow.type != 4 ? () { fullDialog(context, AddFlowPage(type: 3, flow: flow)); } : null, ); }, separatorBuilder: (context, index) { return Divider(); }, ); } } ================================================ FILE: bookkeeping_user_flutter/lib/flows/ui/widgets/account_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/flows/flows.dart'; import '/accounts/accounts.dart'; class AccountInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector( selector: (state) => state.request.accountId, builder: (context, state) { return SmartSelect.single( title: '交易账户', selectedValue: state ?? '', onChange: (selected) { context.read().add(FlowsFilterAccountIdChanged(selected.value ?? '')); }, choiceItems: state1 is AccountEnableStateLoadSuccess ? modelToChoice(state1.accounts) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto: true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is AccountExpenseableStateLoadInProgress, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/flows/ui/widgets/date_input.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncfusion_flutter_datepicker/datepicker.dart'; import 'package:intl/intl.dart'; import '/components/components.dart'; import '/flows/flows.dart'; class DateInput extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (previous, current) => previous.request.minTime != current.request.minTime || previous.request.maxTime != current.request.maxTime, builder: (context, state) { String dateText = ''; DateTime? startDate; DateTime? endDate; if (state.request.minTime != null) { dateText = DateFormat('yyyy.MM.dd').format(DateTime.fromMillisecondsSinceEpoch(state.request.minTime!)); dateText = dateText + ' - '; startDate = DateTime.fromMillisecondsSinceEpoch(state.request.minTime!); } if (state.request.maxTime != null) { if (state.request.minTime == null) { dateText = dateText + ' - '; } else { dateText = dateText + DateFormat('yyyy.MM.dd').format(DateTime.fromMillisecondsSinceEpoch(state.request.maxTime!)); } endDate = DateTime.fromMillisecondsSinceEpoch(state.request.maxTime!); } return Row( children: [ SizedBox(width: 15), ElevatedButton( onPressed: () async { final PickerDateRange? range = await showDialog( context: context, builder: (BuildContext context) { return DateRangePicker(PickerDateRange(startDate, endDate)); } ); if (range != null) { BlocProvider.of(context).add(FlowsFilterMinTimeChanged(range.startDate!.millisecondsSinceEpoch)); BlocProvider.of(context).add(FlowsFilterMaxTimeChanged(range.endDate!.millisecondsSinceEpoch)); } }, child: Text('选择日期范围') ), SizedBox(width: 10), Text(dateText), SizedBox(width: 15), ], ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/flows/ui/widgets/expense_category_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/flows/flows.dart'; import '/categories/categories.dart'; class ExpenseCategoryInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector?>( selector: (state) => state.request.categories, builder: (context, state) { return SmartSelect.single ( title: '支出分类', selectedValue: state?.first ?? '', onChange: (selected) { context.read().add(FlowsFilterCategoryChanged(selected.value ?? '')); }, choiceItems: state1 is ExpenseCategorySelectStateLoadSuccess ? modelToChoice(state1.categories) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto:true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is ExpenseCategorySelectStateLoadInProgress, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/flows/ui/widgets/income_category_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/flows/flows.dart'; import '/categories/categories.dart'; class IncomeCategoryInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector?>( selector: (state) => state.request.categories, builder: (context, state) { return SmartSelect.single ( title: '收入分类', selectedValue: state?.first ?? '', onChange: (selected) { context.read().add(FlowsFilterCategoryChanged(selected.value ?? '')); }, choiceItems: state1 is IncomeCategorySelectStateLoadSuccess ? modelToChoice(state1.categories) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto:true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is IncomeCategorySelectStateLoadInProgress, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/flows/ui/widgets/order_button.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/components/components.dart'; import '/flows/flows.dart'; class OrderButton extends StatelessWidget { @override Widget build(BuildContext context) { return BlocSelector( selector: (state) => state.request.sort, builder: (context, state) { return PopupMenu( onSelected: (selected) { if (selected != state) { context.read().add(FlowsSortChanged(selected)); context.read().add(FlowsRefreshed()); } }, items: { 'createTime,desc': '按时间排序', 'amount,desc': '按金额排序', }, selected: state, ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/flows/ui/widgets/payee_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/flows/flows.dart'; import '/payees/payees.dart'; class PayeeInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector?>( selector: (state) => state.request.payees, builder: (context, state) { return SmartSelect.single ( title: '交易对象', selectedValue: state?.first ?? '', onChange: (selected) { context.read().add(FlowsFilterPayeeChanged(selected.value ?? '')); }, choiceItems: state1 is PayeeEnableStateLoadSuccess ? modelToChoice(state1.payees) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto:true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is PayeeEnableStateLoadInProgress, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/flows/ui/widgets/status_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/flows/flows.dart'; class StatusInput extends StatelessWidget { @override Widget build(BuildContext context) { List> options = [ S2Choice(value: '1', title: '正常'), S2Choice(value: '2', title: '待确认'), S2Choice(value: '3', title: '已退款'), ]; return BlocSelector( selector: (state) => state.request.status, builder: (context, state) { return SmartSelect.single ( title: '交易状态', selectedValue: state ?? '', onChange: (selected) { context.read().add(FlowsFilterStatusChanged(selected.value ?? '')); }, choiceItems: options, choiceType: S2ChoiceType.radios, ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/flows/ui/widgets/tag_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/flows/flows.dart'; import '/tags/tags.dart'; class TagInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocSelector?>( selector: (state) => state.request.tags, builder: (context, state) { return SmartSelect.single ( title: '交易标签', selectedValue: state?.first ?? '', onChange: (selected) { context.read().add(FlowsFilterTagChanged(selected.value ?? '')); }, choiceItems: state1 is TagEnableStateLoadSuccess ? modelToChoice(state1.tags) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto:true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is TagEnableStateLoadInProgress, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/flows/ui/widgets/type_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/flows/flows.dart'; class TypeInput extends StatelessWidget { @override Widget build(BuildContext context) { List> options = [ S2Choice(value: '1', title: '支出'), S2Choice(value: '2', title: '收入'), S2Choice(value: '3', title: '转账'), S2Choice(value: '4', title: '余额调整'), ]; return BlocSelector( selector: (state) => state.request.type, builder: (context, state) { return SmartSelect.single ( title: '交易类型', selectedValue: state ?? '', onChange: (selected) { context.read().add(FlowsFilterTypeChanged(selected.value ?? '')); }, choiceItems: options, choiceType: S2ChoiceType.radios, ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/flows/ui/widgets/widgets.dart ================================================ export 'payee_input.dart'; export 'tag_input.dart'; export 'expense_category_input.dart'; export 'income_category_input.dart'; export 'type_input.dart'; export 'account_input.dart'; export 'status_input.dart'; export 'order_button.dart'; export 'date_input.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/groups/data/models/group.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'group.g.dart'; @JsonSerializable() class Group extends Equatable { final int id; final String name; final String? notes; final String defaultCurrencyCode; const Group({ required this.id, required this.name, this.notes, required this.defaultCurrencyCode, }); factory Group.fromJson(Map json) => _$GroupFromJson(json); Map toJson() => _$GroupToJson(this); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/groups/data/models/group.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'group.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Group _$GroupFromJson(Map json) => Group( id: json['id'] as int, name: json['name'] as String, notes: json['notes'] as String?, defaultCurrencyCode: json['defaultCurrencyCode'] as String, ); Map _$GroupToJson(Group instance) => { 'id': instance.id, 'name': instance.name, 'notes': instance.notes, 'defaultCurrencyCode': instance.defaultCurrencyCode, }; ================================================ FILE: bookkeeping_user_flutter/lib/groups/groups.dart ================================================ export 'data/models/group.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/index.dart ================================================ import 'package:flutter/material.dart'; import '/components/components.dart'; import '/commons/commons.dart'; import '/accounts/accounts.dart'; import '/add_flow/add_flow.dart'; import '/flows/flows.dart'; import '/charts/charts.dart'; import '/my/my.dart'; class IndexPage extends StatefulWidget { final int initialIndex; const IndexPage({ this.initialIndex = 1, }); @override State createState() => new _IndexPageState(); } class _IndexPageState extends State { late int _selectedIndex; @override void initState() { this._selectedIndex = widget.initialIndex; super.initState(); } Widget buildBottomItem(int index, IconData iconData, String label) { final theme = Theme.of(context); Color color = _selectedIndex == index ? theme.primaryColor : theme.unselectedWidgetColor; return GestureDetector( onTap: () { setState(() { _selectedIndex = index; }); }, child: Container( width: 66, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(iconData, color: color), Text(label, style: TextStyle(color: color)) ], ), ) ); } static final List _pages = [AccountsPage(), FlowsPage(), ChartsPage(), MyPage()]; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( body: LazyIndexedStack( reuse: false, index: _selectedIndex, itemBuilder: (c, i) { return _pages[i]; }, itemCount: 4, ), bottomNavigationBar: Container( height: 56, margin: EdgeInsets.symmetric(vertical: 0), decoration: BoxDecoration( border: Border( top: BorderSide(width: 1, color: theme.unselectedWidgetColor), ), ), child: Row(children: [ buildBottomItem(0, Icons.account_balance_outlined, '账户'), buildBottomItem(1, Icons.table_rows_outlined, '流水'), Expanded( child: GestureDetector( onTap: () { fullDialog(context, AddFlowPage(type: 1)); }, child: Container( height: 44, alignment: Alignment.center, decoration: BoxDecoration( color: theme.primaryColor, borderRadius: BorderRadius.circular(50), ), child: Text('记账', style: theme.textTheme.headline6!.copyWith(color: theme.colorScheme.onPrimary)), ), ), ), buildBottomItem(2, Icons.pie_chart_outline, '图表'), buildBottomItem(3, Icons.person_outline_outlined, '我的'), ])) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/items/bloc/item_form/item_form_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:formz/formz.dart'; import 'models/models.dart'; import '/items/items.dart'; part 'item_form_event.dart'; part 'item_form_state.dart'; class ItemFormBloc extends Bloc { final ItemRepository itemRepository; ItemFormBloc({ required this.itemRepository }) : super(const ItemFormState()) { on(_onTitleChanged); on(_onNotesChanged); on(_onDefaultLoaded); on(_onAddSubmitted); } void _onTitleChanged(ItemFormTitleChanged event, Emitter emit) { final title = Title.dirty(event.title); emit(state.copyWith( title: title, status: Formz.validate([title]), )); } void _onNotesChanged(ItemFormNotesChanged event, Emitter emit) { emit(state.copyWith( notes: event.notes, )); } void _onDefaultLoaded(ItemFormDefaultLoaded event, Emitter emit) { var now = DateTime.now(); emit(state.copyWith( status: FormzStatus.pure, startDate: now.millisecondsSinceEpoch, endDate: DateTime(now.year+100, now.month, now.day).millisecondsSinceEpoch )); } void _onAddSubmitted(ItemFormAddSubmitted event, Emitter emit) async { if (state.status.isValidated) { emit(state.copyWith(status: FormzStatus.submissionInProgress)); try { await itemRepository.save( ItemAddRequest( title: state.title.value, notes: state.notes, startDate: 1232, endDate: 124142, repeatType: 1, interval: 2 ) ); emit(state.copyWith(status: FormzStatus.submissionSuccess)); } catch (_) { emit(state.copyWith(status: FormzStatus.submissionFailure)); } } } } ================================================ FILE: bookkeeping_user_flutter/lib/items/bloc/item_form/item_form_event.dart ================================================ part of 'item_form_bloc.dart'; abstract class ItemFormEvent extends Equatable { const ItemFormEvent(); @override List get props => []; } class ItemFormTitleChanged extends ItemFormEvent { const ItemFormTitleChanged(this.title); final String title; @override List get props => [title]; } class ItemFormNotesChanged extends ItemFormEvent { const ItemFormNotesChanged(this.notes); final String notes; @override List get props => [notes]; } class ItemFormStartDateChanged extends ItemFormEvent { const ItemFormStartDateChanged(this.startDate); final int startDate; @override List get props => [startDate]; } class ItemFormEndDateChanged extends ItemFormEvent { const ItemFormEndDateChanged(this.endDate); final int endDate; @override List get props => [endDate]; } class ItemFormRepeatTypeChanged extends ItemFormEvent { const ItemFormRepeatTypeChanged(this.repeatType); final int repeatType; @override List get props => [repeatType]; } class ItemFormIntervalChanged extends ItemFormEvent { const ItemFormIntervalChanged(this.interval); final int interval; @override List get props => [interval]; } class ItemFormDefaultLoaded extends ItemFormEvent { final int type; final Item? item; const ItemFormDefaultLoaded(this.type, this.item); @override List get props => [type, item]; } class ItemFormAddSubmitted extends ItemFormEvent { const ItemFormAddSubmitted(); } ================================================ FILE: bookkeeping_user_flutter/lib/items/bloc/item_form/item_form_state.dart ================================================ part of 'item_form_bloc.dart'; class ItemFormState extends Equatable { final FormzStatus status; final Title title; final String? notes; final int? startDate; final int? endDate; final int? repeatType; final int? interval; const ItemFormState({ this.status = FormzStatus.pure, this.title = const Title.pure(), this.notes, this.startDate = 1, this.endDate = 2, this.repeatType, this.interval }); @override List get props => [status, title, notes, startDate, endDate, repeatType, interval]; ItemFormState copyWith({ FormzStatus? status, Title? title, String? notes, int? startDate, int? endDate, int? repeatType, int? interval, }) { return ItemFormState( status: status ?? this.status, title: title ?? this.title, notes: notes ?? this.notes, startDate: startDate ?? this.startDate, endDate: endDate ?? this.endDate, repeatType: repeatType ?? this.repeatType, interval: interval ?? this.interval, ); } } ================================================ FILE: bookkeeping_user_flutter/lib/items/bloc/item_form/models/models.dart ================================================ export 'title.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/items/bloc/item_form/models/title.dart ================================================ import 'package:formz/formz.dart'; enum TitleValidationError { empty } class Title extends FormzInput { const Title.pure() : super.pure(''); const Title.dirty([String value = '']) : super.dirty(value); @override TitleValidationError? validator(String? value) { return value?.isNotEmpty == true ? null : TitleValidationError.empty; } } ================================================ FILE: bookkeeping_user_flutter/lib/items/bloc/items/items_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import '/commons/commons.dart'; import '/items/items.dart'; part 'items_event.dart'; part 'items_state.dart'; class ItemsBloc extends Bloc { final ItemRepository itemRepository; ItemsBloc({ required this.itemRepository, }) : super(ItemsState()) { on(_onRefreshed); on(_onLoadMore); on(_onSortChanged); on(_onDeleted); } void _onRefreshed(_, Emitter emit) async { try { emit(state.copyWith( status: LoadDataStatus.progress, request: state.request.copyWith(page: 1), )); final items = await itemRepository.query(state.request); emit(state.copyWith( status: LoadDataStatus.success, items: items, request: state.request.copyWith(page: state.request.page + 1), )); } catch (_) { emit(state.copyWith(status: LoadDataStatus.failure)); } } void _onLoadMore(_, Emitter emit) async { try { emit(state.copyWith(loadMoreStatus: LoadDataStatus.progress)); final items = await itemRepository.query(state.request); if (items.isNotEmpty) { emit(state.copyWith( request: state.request.copyWith(page: state.request.page + 1), items: List.of(state.items)..addAll(items), loadMoreStatus: LoadDataStatus.success, )); } else { emit(state.copyWith(loadMoreStatus: LoadDataStatus.empty)); } } catch (_) { emit(state.copyWith(loadMoreStatus: LoadDataStatus.failure)); } } void _onSortChanged(ItemsSortChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(sort: event.sort), )); } void _onDeleted(ItemDeleted event, Emitter emit) async { try { emit(state.copyWith(deleteStatus: LoadDataStatus.progress)); final result = await itemRepository.delete(event.id); if (result) { emit(state.copyWith(deleteStatus: LoadDataStatus.success)); } else { emit(state.copyWith(deleteStatus: LoadDataStatus.failure)); } } catch (_) { emit(state.copyWith(deleteStatus: LoadDataStatus.failure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/items/bloc/items/items_event.dart ================================================ part of 'items_bloc.dart'; @immutable abstract class ItemsEvent extends Equatable { const ItemsEvent(); @override List get props => []; } class ItemsRefreshed extends ItemsEvent {} class ItemsLoadMore extends ItemsEvent {} class ItemDeleted extends ItemsEvent { final String id; const ItemDeleted(this.id); @override List get props => [id]; } class ItemsSortChanged extends ItemsEvent { const ItemsSortChanged(this.sort); final String sort; @override List get props => [sort]; } ================================================ FILE: bookkeeping_user_flutter/lib/items/bloc/items/items_state.dart ================================================ part of 'items_bloc.dart'; @immutable class ItemsState extends Equatable { final LoadDataStatus status; final List items; final ItemQueryRequest request; final LoadDataStatus loadMoreStatus; final LoadDataStatus deleteStatus; const ItemsState({ this.status = LoadDataStatus.initial, this.items = const [], this.request = const ItemQueryRequest(), this.loadMoreStatus = LoadDataStatus.initial, this.deleteStatus = LoadDataStatus.initial, }); ItemsState copyWith({ LoadDataStatus? status, List? items, ItemQueryRequest? request, LoadDataStatus? loadMoreStatus, LoadDataStatus? deleteStatus, }) { return ItemsState( status: status ?? this.status, items: items ?? this.items, request: request ?? this.request, loadMoreStatus: loadMoreStatus ?? this.loadMoreStatus, deleteStatus: deleteStatus ?? this.deleteStatus, ); } @override List get props => [status, request, loadMoreStatus, deleteStatus]; } ================================================ FILE: bookkeeping_user_flutter/lib/items/data/item_repository.dart ================================================ import 'dart:convert'; import '/commons/commons.dart'; import '/items/items.dart'; class ItemRepository { Future save(ItemAddRequest request) async { String response = await HttpClient().post('items', data: request.toJson()); return json.decode(response)['success']; } Future update(int id, ItemAddRequest request) async { String response = await HttpClient().put('items/$id', data: request.toJson()); return parseResponse(response); } Future> query(ItemQueryRequest request) async { String response = await HttpClient().get('items', params: request.toJson()); return json.decode(response)['data']['content'].map((i) => Item.fromJson(i)).toList(); } Future delete(String id) async { String response = await HttpClient().delete('items/$id'); return parseResponse(response); } } ================================================ FILE: bookkeeping_user_flutter/lib/items/data/models/item.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'item.g.dart'; @JsonSerializable() class Item extends Equatable { final int id; final String title; final String? notes; final int nextDate; final int countDown; Item({ required this.id, required this.title, this.notes, required this.nextDate, required this.countDown, }); factory Item.fromJson(Map json) => _$ItemFromJson(json); Map toJson() => _$ItemToJson(this); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/items/data/models/item.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'item.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Item _$ItemFromJson(Map json) => Item( id: json['id'] as int, title: json['title'] as String, notes: json['notes'] as String?, nextDate: json['nextDate'] as int, countDown: json['countDown'] as int, ); Map _$ItemToJson(Item instance) => { 'id': instance.id, 'title': instance.title, 'notes': instance.notes, 'nextDate': instance.nextDate, 'countDown': instance.countDown, }; ================================================ FILE: bookkeeping_user_flutter/lib/items/data/models/item_add_request.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'item_add_request.g.dart'; @JsonSerializable() class ItemAddRequest { final String title; final String? notes; final int startDate; final int endDate; final int repeatType; final int interval; const ItemAddRequest({ required this.title, this.notes, required this.startDate, required this.endDate, required this.repeatType, required this.interval }); ItemAddRequest copyWith({ String? title, String? notes, int? startDate, int? endDate, int? repeatType, int? interval, }) { return ItemAddRequest( title: title ?? this.title, notes: notes ?? this.notes, startDate: startDate ?? this.startDate, endDate: endDate ?? this.endDate, repeatType: repeatType ?? this.repeatType, interval: interval ?? this.interval, ); } factory ItemAddRequest.fromJson(Map json) => _$ItemAddRequestFromJson(json); Map toJson() => _$ItemAddRequestToJson(this); } ================================================ FILE: bookkeeping_user_flutter/lib/items/data/models/item_add_request.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'item_add_request.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** ItemAddRequest _$ItemAddRequestFromJson(Map json) => ItemAddRequest( title: json['title'] as String, notes: json['notes'] as String?, startDate: json['startDate'] as int, endDate: json['endDate'] as int, repeatType: json['repeatType'] as int, interval: json['interval'] as int, ); Map _$ItemAddRequestToJson(ItemAddRequest instance) => { 'title': instance.title, 'notes': instance.notes, 'startDate': instance.startDate, 'endDate': instance.endDate, 'repeatType': instance.repeatType, 'interval': instance.interval, }; ================================================ FILE: bookkeeping_user_flutter/lib/items/data/models/item_query_request.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'item_query_request.g.dart'; @JsonSerializable() class ItemQueryRequest extends Equatable { final int page; final int size; final String sort; const ItemQueryRequest({ this.page = 1, this.size = 10, this.sort = '' }); factory ItemQueryRequest.fromJson(Map json) => _$ItemQueryRequestFromJson(json); Map toJson() => _$ItemQueryRequestToJson(this); @override List get props => [page, sort]; ItemQueryRequest copyWith({ int? page, int? size, String? sort, }) { return ItemQueryRequest( page: page ?? this.page, size: size ?? this.size, sort: sort ?? this.sort, ); } } ================================================ FILE: bookkeeping_user_flutter/lib/items/data/models/item_query_request.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'item_query_request.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** ItemQueryRequest _$ItemQueryRequestFromJson(Map json) => ItemQueryRequest( page: json['page'] as int? ?? 1, size: json['size'] as int? ?? 10, sort: json['sort'] as String? ?? '', ); Map _$ItemQueryRequestToJson(ItemQueryRequest instance) => { 'page': instance.page, 'size': instance.size, 'sort': instance.sort, }; ================================================ FILE: bookkeeping_user_flutter/lib/items/items.dart ================================================ export 'data/models/item.dart'; export 'data/models/item_add_request.dart'; export 'data/models/item_query_request.dart'; export 'data/item_repository.dart'; export 'bloc/items/items_bloc.dart'; export 'bloc/item_form/item_form_bloc.dart'; export 'ui/items_page.dart'; export 'ui/item_form_page.dart'; export 'ui/items_index.dart'; export 'ui/item_form/title_input.dart'; export 'ui/item_form/date_input.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/items/ui/item_form/date_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncfusion_flutter_datepicker/datepicker.dart'; import 'package:intl/intl.dart'; import '/components/components.dart'; import '/items/items.dart'; class DateInput extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (previous, current) => previous.startDate != current.startDate || previous.endDate != current.endDate, builder: (context, state) { return Row( children: [ ElevatedButton( onPressed: () async { final PickerDateRange? range = await showDialog( context: context, builder: (BuildContext context) { return DateRangePicker( PickerDateRange(DateTime.fromMillisecondsSinceEpoch(state.startDate!), DateTime.fromMillisecondsSinceEpoch(state.endDate!)) ); } ); if (range != null) { BlocProvider.of(context).add(ItemFormStartDateChanged(range.startDate!.millisecondsSinceEpoch)); BlocProvider.of(context).add(ItemFormEndDateChanged(range.endDate!.millisecondsSinceEpoch)); } }, child: Text('选择日期范围') ), SizedBox(width: 10), Text( DateFormat('yyyy.MM.dd').format(DateTime.fromMillisecondsSinceEpoch(state.startDate!))+ ' - '+ DateFormat('yyyy.MM.dd').format(DateTime.fromMillisecondsSinceEpoch(state.endDate!)) ), ], ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/items/ui/item_form/title_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/items/items.dart'; class TitleInput extends StatelessWidget { @override Widget build(BuildContext context) { return buildFormItem( '标题', BlocBuilder( buildWhen: (previous, current) => previous.title != current.title, builder: (context, state) { return TextField( onChanged: (value) => context.read().add(ItemFormTitleChanged(value)), decoration: InputDecoration( contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0), errorText: state.title.invalid ? '请输入标题' : null, ), ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/items/ui/item_form_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import '/commons/commons.dart'; import '/items/items.dart'; class ItemFormPage extends StatelessWidget { final int type; // 1-新增,2-修改 final Item? item; const ItemFormPage({ required this.type, this.item }); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => ItemFormBloc(itemRepository: RepositoryProvider.of(context))..add(ItemFormDefaultLoaded(type, item)), child: BlocListener( listener: (context, state) { if (state.status == FormzStatus.submissionSuccess) { Message.success('操作成功'); Navigator.of(context).pop(); BlocProvider.of(context).add(ItemsRefreshed()); if (type == 2) { //BlocProvider.of(context).add(PayeeFetched()); } } }, child: Builder( builder: (context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text(_buildTitle(type)), actions: [ IconButton( icon: Icon(Icons.done), onPressed: () { if (type == 1) { BlocProvider.of(context).add(ItemFormAddSubmitted()); } else if (type == 2) { // BlocProvider.of(context).add(PayeeFormSubmitted(type, payee)); } } ) ], ), body: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ TitleInput(), SizedBox(height: 10), DateInput() ], ), ), ), ); }, ), ), ); } String _buildTitle(int type) { switch (type) { case 1: return '新增项目'; case 2: return '修改项目'; default: return '类型异常'; } } } ================================================ FILE: bookkeeping_user_flutter/lib/items/ui/items_index.dart ================================================ import 'package:flutter/material.dart'; import '/commons/commons.dart'; import '/items/items.dart'; class ItemsIndexPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('提醒事项'), ), floatingActionButton: FloatingActionButton( onPressed: () { fullDialog(context, ItemFormPage(type: 1)); }, child: const Icon(Icons.add), ), body: ItemsPage(), ); } } ================================================ FILE: bookkeeping_user_flutter/lib/items/ui/items_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/components/components.dart'; import '/items/items.dart'; class ItemsPage extends StatefulWidget { @override _ItemsPageState createState() => _ItemsPageState(); } class _ItemsPageState extends State { RefreshController _refreshController = RefreshController(initialRefresh: false); @override void initState() { BlocProvider.of(context).add(ItemsRefreshed()); super.initState(); } @override Widget build(BuildContext context) { return MultiBlocListener( listeners: [ BlocListener( listenWhen: (previous, current) => previous.loadMoreStatus != current.loadMoreStatus, listener: (context, state) { if (state.loadMoreStatus == LoadDataStatus.success) { _refreshController.loadComplete(); } else if (state.loadMoreStatus == LoadDataStatus.failure) { _refreshController.loadFailed(); } else if (state.loadMoreStatus == LoadDataStatus.empty) { _refreshController.loadNoData(); } } ), BlocListener( listenWhen: (previous, current) => previous.deleteStatus != current.deleteStatus, listener: (context, state) { if (state.deleteStatus == LoadDataStatus.success) { BlocProvider.of(context).add(ItemsRefreshed()); } } ), ], child: BlocBuilder( buildWhen: (previous, current) => previous.status != current.status || previous.items != current.items, builder: (context, state) { switch (state.status) { case LoadDataStatus.progress: case LoadDataStatus.initial: return const PageLoading(); case LoadDataStatus.success: if (state.items.isEmpty) return Empty(); return SmartRefresher( enablePullDown: true, enablePullUp: true, controller: _refreshController, child: _buildList(context, state.items), onRefresh: () async { BlocProvider.of(context).add(ItemsRefreshed()); _refreshController.refreshCompleted(); }, onLoading: () async { BlocProvider.of(context).add(ItemsLoadMore()); }, ); default: return PageError(onTap: () { BlocProvider.of(context).add(ItemsRefreshed()); }); } }, ) ); } Widget _buildList(BuildContext context, List items) { final theme = Theme.of(context); return ListView.separated( itemCount: items.length, itemBuilder: (context, index) { Item item = items[index]; return ListTile( dense: true, title: Text(item.title, style: theme.textTheme.bodyText1,), subtitle: Text(dateFormat(item.nextDate), style: theme.textTheme.caption), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Text('还有'+item.countDown.toString()+'天', style: theme.textTheme.bodyText2), Icon(Icons.keyboard_arrow_right) ], ), onTap: () { // Navigator.pushNamed(context, '/flow-detail', arguments: FlowDetailArguments(flow: flow)); }, ); }, separatorBuilder: (context, index) { return Divider(); }, ); } } ================================================ FILE: bookkeeping_user_flutter/lib/login/bloc/auth/auth_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/login/login.dart'; import '/books/books.dart'; part 'auth_event.dart'; part 'auth_state.dart'; class AuthBloc extends Bloc { final LoginRepository loginRepository; AuthBloc({ required this.loginRepository, }) : super(AuthState()) { on(_onAppStarted); on(_onLoggedOut); on(_onLoggedIn); on(_onDefaultBookChanged); } void _onAppStarted(_, Emitter emit) async { await loginRepository.getApiUrl(); final String token = await loginRepository.getToken(); if (token.isNotEmpty) { try { Session sessionData = await loginRepository.getSession(); emit(state.copyWith( status: AuthStatus.authenticated, session: sessionData, )); } catch (_) { print(_); emit(state.copyWith( status: AuthStatus.unauthenticated, )); } } else { emit(state.copyWith( status: AuthStatus.unauthenticated, )); } } void _onLoggedOut(_, Emitter emit) async { emit(state.copyWith( status: AuthStatus.loading, )); await loginRepository.deleteToken(); emit(state.copyWith( status: AuthStatus.unauthenticated, )); } void _onLoggedIn(LoggedIn event, Emitter emit) async { emit(state.copyWith( status: AuthStatus.loading, )); await loginRepository.saveToken(event.token); emit(state.copyWith( status: AuthStatus.authenticated, session: await loginRepository.getSession(), )); } void _onDefaultBookChanged(DefaultBookChanged event, Emitter emit) async { emit(state.copyWith( session: state.session!.copyWith(defaultBook: event.book), )); } } ================================================ FILE: bookkeeping_user_flutter/lib/login/bloc/auth/auth_event.dart ================================================ part of 'auth_bloc.dart'; abstract class AuthEvent extends Equatable { const AuthEvent(); @override List get props => []; } /// AppStarted will be dispatched when the Flutter application first loads. /// It will notify bloc that it needs to determine whether or not there is an existing user. class AppStarted extends AuthEvent { @override String toString() => 'AppStarted'; } /// LoggedIn will be dispatched on a successful login. /// It will notify the bloc that the user has successfully logged in. class LoggedIn extends AuthEvent { const LoggedIn(this.token); final String token; @override String toString() => 'LoggedIn { token: $token }'; } /// LoggedOut will be dispatched on a successful logout. /// It will notify the bloc that the user has successfully logged out. class LoggedOut extends AuthEvent { @override String toString() => 'LoggedOut'; } class DefaultBookChanged extends AuthEvent { final Book book; const DefaultBookChanged(this.book); @override List get props => [book]; } ================================================ FILE: bookkeeping_user_flutter/lib/login/bloc/auth/auth_state.dart ================================================ part of 'auth_bloc.dart'; enum AuthStatus { uninitialized, authenticated, unauthenticated, loading } class AuthState extends Equatable { final AuthStatus status; final Session? session; const AuthState({ this.status = AuthStatus.uninitialized, this.session }); AuthState copyWith({ AuthStatus? status, Session? session, }) { return AuthState( status: status ?? this.status, session: session ?? this.session, ); } @override List get props => [status, session]; } ================================================ FILE: bookkeeping_user_flutter/lib/login/bloc/login/login_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:formz/formz.dart'; import '/login/login.dart'; part 'login_event.dart'; part 'login_state.dart'; class LoginBloc extends Bloc { LoginBloc({ required this.loginRepository, required this.authBloc }) : super(LoginState()) { on(_onUsernameChanged); on(_onPasswordChanged); on(_onApiUrlChanged); on(_onLoginButtonPressed); } final LoginRepository loginRepository; final AuthBloc authBloc; void _onUsernameChanged(LoginUsernameChanged event, Emitter emit) { final username = Username.dirty(event.username); emit(state.copyWith( username: username, status: Formz.validate([username, state.password, state.apiUrl]), )); } void _onPasswordChanged(LoginPasswordChanged event, Emitter emit) { final password = Password.dirty(event.password); emit(state.copyWith( password: password, status: Formz.validate([password, state.username, state.apiUrl]), )); } void _onApiUrlChanged(LoginApiUrlChanged event, Emitter emit) { final url = ApiUrl.dirty(event.url); emit(state.copyWith( apiUrl: url, status: Formz.validate([url, state.username, state.password]), )); } void _onLoginButtonPressed(_, Emitter emit) async { if (state.status.isValidated) { emit(state.copyWith(status: FormzStatus.submissionInProgress)); try { await loginRepository.saveApiUrl(state.apiUrl.value); String token = await loginRepository.logIn(username: state.username.value, password: state.password.value); authBloc.add(LoggedIn(token)); emit(state.copyWith(status: FormzStatus.submissionSuccess)); } catch (_) { emit(state.copyWith(status: FormzStatus.submissionFailure)); } } } } ================================================ FILE: bookkeeping_user_flutter/lib/login/bloc/login/login_event.dart ================================================ part of 'login_bloc.dart'; abstract class LoginEvent extends Equatable { const LoginEvent(); @override List get props => []; } class LoginUsernameChanged extends LoginEvent { const LoginUsernameChanged(this.username); final String username; @override List get props => [username]; } class LoginPasswordChanged extends LoginEvent { const LoginPasswordChanged(this.password); final String password; @override List get props => [password]; } class LoginApiUrlChanged extends LoginEvent { const LoginApiUrlChanged(this.url); final String url; @override List get props => [url]; } class LoginButtonPressed extends LoginEvent { const LoginButtonPressed(); } ================================================ FILE: bookkeeping_user_flutter/lib/login/bloc/login/login_state.dart ================================================ part of 'login_bloc.dart'; class LoginState extends Equatable { final FormzStatus status; final Username username; final Password password; ApiUrl apiUrl; LoginState({ this.status = FormzStatus.pure, this.username = const Username.pure(), this.password = const Password.pure(), this.apiUrl = const ApiUrl.dirty('http://testjz.jiukuaitech.com/api/v1/'), }); LoginState copyWith({ FormzStatus? status, Username? username, Password? password, ApiUrl? apiUrl, }) { return LoginState( status: status ?? this.status, username: username ?? this.username, password: password ?? this.password, apiUrl: apiUrl ?? this.apiUrl ); } @override List get props => [status, username, password, apiUrl]; } ================================================ FILE: bookkeeping_user_flutter/lib/login/bloc/login/models/ApiUrl.dart ================================================ import 'package:formz/formz.dart'; enum ApiUrlValidationError { empty } class ApiUrl extends FormzInput { const ApiUrl.pure() : super.pure(''); const ApiUrl.dirty([String value = '']) : super.dirty(value); @override ApiUrlValidationError? validator(String? value) { return value?.isNotEmpty == true ? null : ApiUrlValidationError.empty; } } ================================================ FILE: bookkeeping_user_flutter/lib/login/bloc/login/models/password.dart ================================================ import 'package:formz/formz.dart'; enum PasswordValidationError { empty } class Password extends FormzInput { const Password.pure() : super.pure(''); const Password.dirty([String value = '']) : super.dirty(value); @override PasswordValidationError? validator(String? value) { return value?.isNotEmpty == true ? null : PasswordValidationError.empty; } } ================================================ FILE: bookkeeping_user_flutter/lib/login/bloc/login/models/username.dart ================================================ import 'package:formz/formz.dart'; enum UsernameValidationError { empty } class Username extends FormzInput { const Username.pure() : super.pure(''); const Username.dirty([String value = '']) : super.dirty(value); @override UsernameValidationError? validator(String? value) { return value?.isNotEmpty == true ? null : UsernameValidationError.empty; } } ================================================ FILE: bookkeeping_user_flutter/lib/login/data/login_repository.dart ================================================ import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; import '/login/login.dart'; import '/commons/commons.dart'; class LogInFailure implements Exception { } class LoginRepository { Future logIn({ required String username, required String password }) async { String response = await HttpClient().post('signin', data: {'userName': username, 'password': password}); var responseDecoded = json.decode(response); if (responseDecoded['success']) { return responseDecoded['data']['token']; } else { throw LogInFailure(); } } Future getSession() async { String response = await HttpClient().get('session'); // TODO response 可能失败 return Session.fromJson(json.decode(response)['data']); } Future saveToken(String token) async { session["userToken"] = token; final SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.setString('User-Token', token); } Future saveApiUrl(String url) async { session["apiUrl"] = url; final SharedPreferences prefs = await SharedPreferences.getInstance(); HttpClient().init(); return prefs.setString('Api-Url', url); } Future deleteToken() async { session["userToken"] = null; final SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.remove('User-Token'); } Future getToken() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); final String userToken = prefs.getString('User-Token') ?? ''; session['userToken'] = userToken; return userToken; } Future getApiUrl() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); final String apiUrl = prefs.getString('Api-Url') ?? ''; session['apiUrl'] = apiUrl; return apiUrl; } } ================================================ FILE: bookkeeping_user_flutter/lib/login/data/models/session.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import '/books/books.dart'; import '/groups/groups.dart'; import 'user.dart'; part 'session.g.dart'; @JsonSerializable() class Session extends Equatable { final User userSessionVO; final Book defaultBook; final Group defaultGroup; const Session({ required this.userSessionVO, required this.defaultBook, required this.defaultGroup }); Session copyWith({ User? userSessionVO, Book? defaultBook, Group? defaultGroup }) { return Session( userSessionVO: userSessionVO ?? this.userSessionVO, defaultBook: defaultBook ?? this.defaultBook, defaultGroup: defaultGroup ?? this.defaultGroup ); } factory Session.fromJson(Map json) => _$SessionFromJson(json); Map toJson() => _$SessionToJson(this); @override List get props => [userSessionVO, defaultBook, defaultGroup]; } ================================================ FILE: bookkeeping_user_flutter/lib/login/data/models/session.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'session.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Session _$SessionFromJson(Map json) => Session( userSessionVO: User.fromJson(json['userSessionVO'] as Map), defaultBook: Book.fromJson(json['defaultBook'] as Map), defaultGroup: Group.fromJson(json['defaultGroup'] as Map), ); Map _$SessionToJson(Session instance) => { 'userSessionVO': instance.userSessionVO, 'defaultBook': instance.defaultBook, 'defaultGroup': instance.defaultGroup, }; ================================================ FILE: bookkeeping_user_flutter/lib/login/data/models/user.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'user.g.dart'; @JsonSerializable() class User extends Equatable { final int id; final String userName; final int vipTime; const User({ required this.id, required this.userName, required this.vipTime, }); factory User.fromJson(Map json) => _$UserFromJson(json); Map toJson() => _$UserToJson(this); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/login/data/models/user.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'user.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** User _$UserFromJson(Map json) => User( id: json['id'] as int, userName: json['userName'] as String, vipTime: json['vipTime'] as int, ); Map _$UserToJson(User instance) => { 'id': instance.id, 'userName': instance.userName, 'vipTime': instance.vipTime, }; ================================================ FILE: bookkeeping_user_flutter/lib/login/login.dart ================================================ export 'bloc/login/login_bloc.dart'; export 'bloc/login/models/password.dart'; export 'bloc/login/models/username.dart'; export 'bloc/login/models/ApiUrl.dart'; export 'bloc/auth/auth_bloc.dart'; export 'data/login_repository.dart'; export 'data/models/user.dart'; export 'data/models/session.dart'; export 'ui/login_page.dart'; export 'ui/widgets/login_form.dart'; export 'ui/widgets/user_name_input.dart'; export 'ui/widgets/password_input.dart'; export 'ui/widgets/api_url_input.dart'; export 'ui/widgets/submit_btn.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/login/ui/login_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/login/login.dart'; class LoginPage extends StatelessWidget { @override Widget build(BuildContext context) { return Material( child: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ Container( width: 200, padding: EdgeInsets.only(top: 50.0, bottom: 60.0), child: Image.asset('assets/images/logo.png') ), BlocProvider( create: (_) => LoginBloc( loginRepository: RepositoryProvider.of(context), authBloc: BlocProvider.of(context) ), child: LoginForm(), ), SizedBox(height: 50), ], ) ), ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/login/ui/widgets/api_url_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/login/login.dart'; class ApiUrlInput extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (previous, current) => previous.apiUrl != current.apiUrl, builder: (context, state) { return TextFormField( initialValue: state.apiUrl.value, onChanged: (url) => context.read().add(LoginApiUrlChanged(url)), decoration: InputDecoration( hintText: 'API地址', contentPadding: EdgeInsets.symmetric(horizontal: 10), errorText: state.apiUrl.invalid ? '请输入API地址' : null, ), ); }, ); } } ================================================ FILE: bookkeeping_user_flutter/lib/login/ui/widgets/login_form.dart ================================================ import 'package:bookkeeping_user_flutter/login/ui/widgets/api_url_input.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import '/commons/commons.dart'; import '/login/login.dart'; class LoginForm extends StatelessWidget { Widget build(BuildContext context) { return BlocListener( listener: (context, state) { if (state.status == FormzStatus.submissionSuccess) { Message.success('登录成功'); } }, child: Column( children: [ UsernameInput(), SizedBox(height: 10), PasswordInput(), SizedBox(height: 10), ApiUrlInput(), SizedBox(height: 30), SizedBox( width: double.infinity, height: 50, child: SubmitBtn(), ) ], ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/login/ui/widgets/password_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/login/login.dart'; class PasswordInput extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (previous, current) => previous.password != current.password, builder: (context, state) { return TextField( key: const Key('loginForm_passwordInput_textField'), onChanged: (password) => context.read().add(LoginPasswordChanged(password)), obscureText: true, decoration: InputDecoration( hintText: '密码', contentPadding: EdgeInsets.symmetric(horizontal: 10), errorText: state.password.invalid ? '请输入密码' : null, ), ); }, ); } } ================================================ FILE: bookkeeping_user_flutter/lib/login/ui/widgets/submit_btn.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import '/login/login.dart'; class SubmitBtn extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (previous, current) => previous.status != current.status, builder: (context, state) { return ElevatedButton( onPressed: state.status.isValidated && !state.status.isSubmissionInProgress ? () {context.read().add(LoginButtonPressed());} : null, child: state.status.isSubmissionInProgress ? CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Colors.white), strokeWidth: 1.5, ) : Text('登录') ); }, ); } } ================================================ FILE: bookkeeping_user_flutter/lib/login/ui/widgets/user_name_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/login/login.dart'; class UsernameInput extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (previous, current) => previous.username != current.username, builder: (context, state) { return TextField( onChanged: (username) => context.read().add(LoginUsernameChanged(username)), decoration: InputDecoration( hintText: '用户名', contentPadding: EdgeInsets.symmetric(horizontal: 10), errorText: state.username.invalid ? '请输入用户名' : null, ), ); }, ); } } ================================================ FILE: bookkeeping_user_flutter/lib/main.dart ================================================ import 'package:flutter/cupertino.dart'; import '/categories/categories.dart'; import '/flows/flows.dart'; import '/payees/payees.dart'; import '/tags/tags.dart'; import '/accounts/accounts.dart'; import '/login/login.dart'; import '/books/books.dart'; import '/charts/charts.dart'; import '/currency/currency.dart'; import '/items/items.dart'; import 'app.dart'; void main() { // BlocOverrides.runZoned( // () { // runApp(App( // loginRepository: LoginRepository(), // )); // }, // blocObserver: AppBlocObserver(), // ); runApp(App( loginRepository: LoginRepository(), accountRepository: AccountRepository(), categoryRepository: CategoryRepository(), payeeRepository: PayeeRepository(), tagRepository: TagRepository(), flowRepository: FlowRepository(), bookRepository: BookRepository(), reportRepository: ReportRepository(), itemRepository: ItemRepository(), currencyRepository: CurrencyRepository(), )); // runApp(new MaterialApp(home: PositionedTiles())); } ================================================ FILE: bookkeeping_user_flutter/lib/my/my.dart ================================================ export 'my_page.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/my/my_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import '/commons/commons.dart'; import '/login/login.dart'; class MyPage extends StatelessWidget { @override Widget build(BuildContext context) { final state = context.watch().state; return Scaffold( appBar: AppBar( title: const Text('我的'), centerTitle: true, ), body: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ ListTile( title: Text('登录用户名:'), trailing: Text(state.session?.userSessionVO.userName ?? '') ), ListTile( title: Text('会员到期日:'), trailing: Text(DateFormat('yyyy-MM-dd').format(DateTime.fromMillisecondsSinceEpoch(state.session?.userSessionVO.vipTime ?? 0))) ), ListTile( title: Text('当前组:'), trailing: Text(state.session?.defaultGroup.name ?? '') ), ListTile( title: Text('当前账本:'), trailing: Text(state.session?.defaultBook.name ?? '') ), Divider(), ListTile( title: Text('账本管理'), trailing: Icon(Icons.keyboard_arrow_right), onTap: () { Navigator.pushNamed(context, '/books'); }, ), Divider(), ListTile( title: const Text('支出类别'), trailing: Icon(Icons.keyboard_arrow_right), onTap: () { Navigator.pushNamed(context, '/expense-categories'); } ), ListTile( title: const Text('收入类别'), trailing: Icon(Icons.keyboard_arrow_right), onTap: () { Navigator.pushNamed(context, '/income-categories'); } ), ListTile( title: const Text('交易标签'), trailing: Icon(Icons.keyboard_arrow_right), onTap: () { Navigator.pushNamed(context, '/tags'); } ), ListTile( title: const Text('交易对象'), trailing: Icon(Icons.keyboard_arrow_right), onTap: () { Navigator.pushNamed(context, '/payees'); } ), Divider(), ListTile( title: const Text('提醒事项'), trailing: Icon(Icons.keyboard_arrow_right), onTap: () { Navigator.pushNamed(context, '/items-index'); } ), Divider(), ListTile( title: const Text('api地址'), trailing: Text(session['apiUrl']), onTap: () { //Navigator.pushNamed(context, ); } ), Divider(), ListTile( title: const Text('使用指南'), trailing: Icon(Icons.keyboard_arrow_right), onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (context) => const WebViewPage(title: '使用指南', url: 'https://docs.jz.jiukuaitech.com/'), ), ); } ), Divider(), ListTile( title: const Text('当前版本号:'), trailing: const Text('1.0.2') ), Divider(), Container( width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 15), child: ElevatedButton( child: const Text('退出登录'), onPressed: () async { if (await confirm( context, content: Text('确定退出吗?'), textOK: Text('确定'), textCancel: Text('取消'), )) { BlocProvider.of(context).add(LoggedOut()); } } ) ) ], ), ) ); } } ================================================ FILE: bookkeeping_user_flutter/lib/observer.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:flutter/cupertino.dart'; class AppBlocObserver extends BlocObserver { @override void onEvent(Bloc bloc, Object? event) { super.onEvent(bloc, event); print(event); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { print(error); super.onError(bloc, error, stackTrace); } @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); print(change); } @override void onTransition(Bloc bloc, Transition transition) { super.onTransition(bloc, transition); print(transition); } } class AppNavigatorObserver extends NavigatorObserver { @override void didPush(Route route, Route? previousRoute) { print("${route.settings.name} pushed"); } @override void didPop(Route route, Route? previousRoute) { print("${route.settings.name} popped"); } @override void didReplace({Route? newRoute, Route? oldRoute}) { print("${oldRoute!.settings.name} is replaced by ${newRoute!.settings.name}"); } @override void didRemove(Route route, Route? previousRoute) { print("${route.settings.name} removed"); } } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payee_enable/payee_enable_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/payees/payees.dart'; part 'payee_enable_event.dart'; part 'payee_enable_state.dart'; class PayeeEnableBloc extends Bloc { final PayeeRepository payeeRepository; PayeeEnableBloc({ required this.payeeRepository }) : super(PayeeEnableStateLoadInProgress()) { on(_onPayeeEnableLoaded); } void _onPayeeEnableLoaded(_, Emitter emit) async { // 只加载一次数据 // if (state is PayeeEnableStateLoadSuccess) return; try { emit(PayeeEnableStateLoadInProgress()); final payees = await payeeRepository.getEnable(); emit(PayeeEnableStateLoadSuccess(payees)); } catch (_) { emit(PayeeEnableStateLoadFailure()); } } } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payee_enable/payee_enable_event.dart ================================================ part of 'payee_enable_bloc.dart'; abstract class PayeeEnableEvent extends Equatable { const PayeeEnableEvent(); @override List get props => []; } class PayeeEnableLoaded extends PayeeEnableEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payee_enable/payee_enable_state.dart ================================================ part of 'payee_enable_bloc.dart'; abstract class PayeeEnableState extends Equatable { const PayeeEnableState(); @override List get props => []; } class PayeeEnableStateLoadInProgress extends PayeeEnableState { } class PayeeEnableStateLoadSuccess extends PayeeEnableState { final List payees; const PayeeEnableStateLoadSuccess(this.payees); @override List get props => [payees]; } class PayeeEnableStateLoadFailure extends PayeeEnableState { } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payee_expenseable/payee_expenseable_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/payees/payees.dart'; part 'payee_expenseable_event.dart'; part 'payee_expenseable_state.dart'; class PayeeExpenseableBloc extends Bloc { final PayeeRepository payeeRepository; PayeeExpenseableBloc({ required this.payeeRepository }) : super(PayeeExpenseableStateLoadInProgress()) { on(_onPayeeExpenseableLoaded); } void _onPayeeExpenseableLoaded(_, Emitter emit) async { // 只加载一次数据 // if (state is PayeeExpenseableStateLoadSuccess) return; try { emit(PayeeExpenseableStateLoadInProgress()); final payees = await payeeRepository.getExpenseable(); emit(PayeeExpenseableStateLoadSuccess(payees)); } catch (_) { emit(PayeeExpenseableStateLoadFailure()); } } } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payee_expenseable/payee_expenseable_event.dart ================================================ part of 'payee_expenseable_bloc.dart'; abstract class PayeeExpenseableEvent extends Equatable { const PayeeExpenseableEvent(); @override List get props => []; } class PayeeExpenseableLoaded extends PayeeExpenseableEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payee_expenseable/payee_expenseable_state.dart ================================================ part of 'payee_expenseable_bloc.dart'; abstract class PayeeExpenseableState extends Equatable { const PayeeExpenseableState(); @override List get props => []; } class PayeeExpenseableStateLoadInProgress extends PayeeExpenseableState { } class PayeeExpenseableStateLoadSuccess extends PayeeExpenseableState { final List payees; const PayeeExpenseableStateLoadSuccess(this.payees); @override List get props => [payees]; } class PayeeExpenseableStateLoadFailure extends PayeeExpenseableState { } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payee_fetch/payee_fetch_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import '/payees/payees.dart'; import '/commons/commons.dart'; part 'payee_fetch_event.dart'; part 'payee_fetch_state.dart'; class PayeeFetchBloc extends Bloc { final PayeeRepository payeeRepository; PayeeFetchBloc({ required this.payeeRepository, }) : super(PayeeFetchState()) { on(_onFetched); on(_onDefault); } void _onDefault(PayeeLoadDefault event, Emitter emit) { emit(state.copyWith( status: LoadDataStatus.success, payee: event.payee )); } void _onFetched(_, Emitter emit) async { try { emit(state.copyWith(status: LoadDataStatus.progress)); final payee = await payeeRepository.get(state.payee!.id); emit(state.copyWith( status: LoadDataStatus.success, payee: payee, )); } catch (_) { emit(state.copyWith(status: LoadDataStatus.failure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payee_fetch/payee_fetch_event.dart ================================================ part of 'payee_fetch_bloc.dart'; @immutable class PayeeFetchEvent extends Equatable { const PayeeFetchEvent(); @override List get props => []; } class PayeeFetched extends PayeeFetchEvent {} class PayeeLoadDefault extends PayeeFetchEvent { final Payee payee; const PayeeLoadDefault({ required this.payee, }); List get props => [payee]; } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payee_fetch/payee_fetch_state.dart ================================================ part of 'payee_fetch_bloc.dart'; @immutable class PayeeFetchState extends Equatable { final LoadDataStatus status; final Payee? payee; const PayeeFetchState({ this.status = LoadDataStatus.initial, this.payee, }); PayeeFetchState copyWith({ LoadDataStatus? status, Payee? payee, }) { return PayeeFetchState( status: status ?? this.status, payee: payee ?? this.payee, ); } @override List get props => [status, payee]; } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payee_form/payee_form_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:bookkeeping_user_flutter/payees/payees.dart'; import 'package:bookkeeping_user_flutter/payees/data/models/payee_form_request.dart'; import 'package:bookkeeping_user_flutter/commons/commons.dart'; import 'package:equatable/equatable.dart'; import 'package:formz/formz.dart'; import 'package:meta/meta.dart'; part 'payee_form_event.dart'; part 'payee_form_state.dart'; class PayeeFormBloc extends Bloc { final PayeeRepository payeeRepository; PayeeFormBloc({ required this.payeeRepository, }) : super(const PayeeFormState()) { on(_onNameChanged); on(_onExpenseableChanged); on(_onIncomeableChanged); on(_onNotesChanged); on(_onSubmitted); on(_onDefaultLoaded); } void _onNameChanged(PayeeFormNameChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(name: event.name), )); } void _onExpenseableChanged(PayeeFormExpenseableChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(expenseable: event.expenseable), )); } void _onIncomeableChanged(PayeeFormIncomeableChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(incomeable: event.incomeable), )); } void _onNotesChanged(PayeeFormNotesChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(notes: event.notes), )); } void _onDefaultLoaded(PayeeFormDefaultLoaded event, Emitter emit) { emit(state.copyWith( status: FormzStatus.pure, request: state.request.copyWith( name: event.payee?.name ?? '', expenseable: event.payee?.expenseable ?? true, incomeable: event.payee?.incomeable ?? false, notes: event.payee?.notes ?? '', ) )); } void _onSubmitted(PayeeFormSubmitted event, Emitter emit) async { try { bool result = false; switch (event.type) { case 1: result = await payeeRepository.add(state.request); break; case 2: result = await payeeRepository.update(event.payee!.id, state.request); break; default: break; } if (result) { emit(state.copyWith(status: FormzStatus.submissionSuccess)); } else { emit(state.copyWith(status: FormzStatus.submissionFailure)); } } catch (_) { emit(state.copyWith(status: FormzStatus.submissionFailure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payee_form/payee_form_event.dart ================================================ part of 'payee_form_bloc.dart'; @immutable abstract class PayeeFormEvent extends Equatable { const PayeeFormEvent(); @override List get props => []; } class PayeeFormNameChanged extends PayeeFormEvent { const PayeeFormNameChanged(this.name); final String name; @override List get props => [name]; } class PayeeFormExpenseableChanged extends PayeeFormEvent { const PayeeFormExpenseableChanged(this.expenseable); final bool expenseable; @override List get props => [expenseable]; } class PayeeFormIncomeableChanged extends PayeeFormEvent { const PayeeFormIncomeableChanged(this.incomeable); final bool incomeable; @override List get props => [incomeable]; } class PayeeFormNotesChanged extends PayeeFormEvent { const PayeeFormNotesChanged(this.notes); final String notes; @override List get props => [notes]; } class PayeeFormDefaultLoaded extends PayeeFormEvent { final int type; final Payee? payee; const PayeeFormDefaultLoaded(this.type, this.payee); @override List get props => [type, payee]; } class PayeeFormSubmitted extends PayeeFormEvent { final int type; final Payee? payee; const PayeeFormSubmitted(this.type, this.payee); @override List get props => [type, payee]; } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payee_form/payee_form_state.dart ================================================ part of 'payee_form_bloc.dart'; @immutable class PayeeFormState extends Equatable { final FormzStatus status; final PayeeFormRequest request; const PayeeFormState({ this.status = FormzStatus.pure, this.request = const PayeeFormRequest(), }); PayeeFormState copyWith({ FormzStatus? status, PayeeFormRequest? request, }) { return PayeeFormState( status: status ?? this.status, request: request ?? this.request, ); } @override List get props => [status, request]; } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payee_incomeable/payee_incomeable_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/payees/payees.dart'; part 'payee_incomeable_event.dart'; part 'payee_incomeable_state.dart'; class PayeeIncomeableBloc extends Bloc { final PayeeRepository payeeRepository; PayeeIncomeableBloc({ required this.payeeRepository }) : super(PayeeIncomeableStateLoadInProgress()) { on(_onPayeeIncomeableLoaded); } void _onPayeeIncomeableLoaded(_, Emitter emit) async { // 只加载一次数据 // if (state is PayeeIncomeableStateLoadSuccess) return; try { emit(PayeeIncomeableStateLoadInProgress()); final payees = await payeeRepository.getIncomeable(); emit(PayeeIncomeableStateLoadSuccess(payees)); } catch (_) { emit(PayeeIncomeableStateLoadFailure()); } } } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payee_incomeable/payee_incomeable_event.dart ================================================ part of 'payee_incomeable_bloc.dart'; abstract class PayeeIncomeableEvent extends Equatable { const PayeeIncomeableEvent(); @override List get props => []; } class PayeeIncomeableLoaded extends PayeeIncomeableEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payee_incomeable/payee_incomeable_state.dart ================================================ part of 'payee_incomeable_bloc.dart'; abstract class PayeeIncomeableState extends Equatable { const PayeeIncomeableState(); @override List get props => []; } class PayeeIncomeableStateLoadInProgress extends PayeeIncomeableState { } class PayeeIncomeableStateLoadSuccess extends PayeeIncomeableState { final List payees; const PayeeIncomeableStateLoadSuccess(this.payees); @override List get props => [payees]; } class PayeeIncomeableStateLoadFailure extends PayeeIncomeableState { } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payees/payees_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import '/commons/commons.dart'; import '/payees/payees.dart'; part 'payees_event.dart'; part 'payees_state.dart'; class PayeesBloc extends Bloc { final PayeeRepository payeeRepository; PayeesBloc({ required this.payeeRepository, }) : super(PayeesState()) { on(_onRefreshed); on(_onLoadMore); on(_onSortChanged); on(_onDeleted); on(_onToggled); } void _onRefreshed(_, Emitter emit) async { try { emit(state.copyWith( status: LoadDataStatus.progress, request: state.request.copyWith(page: 1), )); final payees = await payeeRepository.query(state.request); emit(state.copyWith( status: LoadDataStatus.success, payees: payees, request: state.request.copyWith(page: state.request.page + 1), )); } catch (_) { print(_); emit(state.copyWith(status: LoadDataStatus.failure)); } } void _onLoadMore(_, Emitter emit) async { try { emit(state.copyWith(loadMoreStatus: LoadDataStatus.progress)); final payees = await payeeRepository.query(state.request); if (payees.isNotEmpty) { emit(state.copyWith( request: state.request.copyWith(page: state.request.page + 1), payees: List.of(state.payees)..addAll(payees), loadMoreStatus: LoadDataStatus.success, )); } else { emit(state.copyWith(loadMoreStatus: LoadDataStatus.empty)); } } catch (_) { emit(state.copyWith(loadMoreStatus: LoadDataStatus.failure)); } } void _onSortChanged(PayeesSortChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(sort: event.sort), )); } void _onDeleted(PayeeDeleted event, Emitter emit) async { try { emit(state.copyWith(deleteStatus: LoadDataStatus.progress)); final result = await payeeRepository.delete(event.id); if (result) { emit(state.copyWith(deleteStatus: LoadDataStatus.success)); } else { emit(state.copyWith(deleteStatus: LoadDataStatus.failure)); } } catch (_) { emit(state.copyWith(deleteStatus: LoadDataStatus.failure)); } } void _onToggled(PayeeToggled event, Emitter emit) async { try { emit(state.copyWith(toggleStatus: LoadDataStatus.progress)); final result = await payeeRepository.toggle(event.id); if (result) { emit(state.copyWith(toggleStatus: LoadDataStatus.success)); } else { emit(state.copyWith(toggleStatus: LoadDataStatus.failure)); } } catch (_) { emit(state.copyWith(toggleStatus: LoadDataStatus.failure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payees/payees_event.dart ================================================ part of 'payees_bloc.dart'; @immutable abstract class PayeesEvent extends Equatable { const PayeesEvent(); @override List get props => []; } class PayeesRefreshed extends PayeesEvent {} class PayeesLoadMore extends PayeesEvent {} class PayeeDeleted extends PayeesEvent { final String id; const PayeeDeleted(this.id); @override List get props => [id]; } class PayeeToggled extends PayeesEvent { final String id; const PayeeToggled(this.id); @override List get props => [id]; } class PayeesSortChanged extends PayeesEvent { const PayeesSortChanged(this.sort); final String sort; @override List get props => [sort]; } ================================================ FILE: bookkeeping_user_flutter/lib/payees/bloc/payees/payees_state.dart ================================================ part of 'payees_bloc.dart'; @immutable class PayeesState extends Equatable { final LoadDataStatus status; final List payees; final PayeeQueryRequest request; final LoadDataStatus loadMoreStatus; final LoadDataStatus deleteStatus; final LoadDataStatus toggleStatus; const PayeesState({ this.status = LoadDataStatus.initial, this.payees = const [], this.request = const PayeeQueryRequest(), this.loadMoreStatus = LoadDataStatus.initial, this.deleteStatus = LoadDataStatus.initial, this.toggleStatus = LoadDataStatus.initial, }); @override List get props => [status, payees, request, loadMoreStatus, deleteStatus, toggleStatus]; PayeesState copyWith({ LoadDataStatus? status, List? payees, PayeeQueryRequest? request, LoadDataStatus? loadMoreStatus, LoadDataStatus? deleteStatus, LoadDataStatus? toggleStatus, }) { return PayeesState( status: status ?? this.status, payees: payees ?? this.payees, request: request ?? this.request, loadMoreStatus: loadMoreStatus ?? this.loadMoreStatus, deleteStatus: deleteStatus ?? this.deleteStatus, toggleStatus: toggleStatus ?? this.toggleStatus, ); } } ================================================ FILE: bookkeeping_user_flutter/lib/payees/data/models/payee.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'payee.g.dart'; @JsonSerializable() class Payee extends Equatable { final int id; final String name; final bool enable; final bool expenseable; final bool incomeable; final String? notes; Payee({ required this.id, required this.name, required this.enable, required this.expenseable, required this.incomeable, this.notes }); factory Payee.fromJson(Map json) => _$PayeeFromJson(json); Map toJson() => _$PayeeToJson(this); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/payees/data/models/payee.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'payee.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Payee _$PayeeFromJson(Map json) => Payee( id: json['id'] as int, name: json['name'] as String, enable: json['enable'] as bool, expenseable: json['expenseable'] as bool, incomeable: json['incomeable'] as bool, notes: json['notes'] as String?, ); Map _$PayeeToJson(Payee instance) => { 'id': instance.id, 'name': instance.name, 'enable': instance.enable, 'expenseable': instance.expenseable, 'incomeable': instance.incomeable, 'notes': instance.notes, }; ================================================ FILE: bookkeeping_user_flutter/lib/payees/data/models/payee_form_request.dart ================================================ import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; part 'payee_form_request.g.dart'; @JsonSerializable() class PayeeFormRequest extends Equatable { final String? name; final bool? expenseable; final bool? incomeable; final String? notes; const PayeeFormRequest({ this.name, this.expenseable, this.incomeable, this.notes, }); PayeeFormRequest copyWith({ String? name, bool? expenseable, bool? incomeable, String? notes, }) { return PayeeFormRequest( name: name ?? this.name, expenseable: expenseable ?? this.expenseable, incomeable: incomeable ?? this.incomeable, notes: notes ?? this.notes, ); } Map toJson() => _$PayeeFormRequestToJson(this); @override List get props => [name, expenseable, incomeable, notes]; } ================================================ FILE: bookkeeping_user_flutter/lib/payees/data/models/payee_form_request.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'payee_form_request.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** PayeeFormRequest _$PayeeFormRequestFromJson(Map json) => PayeeFormRequest( name: json['name'] as String?, expenseable: json['expenseable'] as bool?, incomeable: json['incomeable'] as bool?, notes: json['notes'] as String?, ); Map _$PayeeFormRequestToJson(PayeeFormRequest instance) => { 'name': instance.name, 'expenseable': instance.expenseable, 'incomeable': instance.incomeable, 'notes': instance.notes, }; ================================================ FILE: bookkeeping_user_flutter/lib/payees/data/models/payee_query_request.dart ================================================ import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; part 'payee_query_request.g.dart'; @JsonSerializable() class PayeeQueryRequest extends Equatable { final int page; final int size; final String sort; const PayeeQueryRequest({ this.page = 1, this.size = 15, this.sort = 'id,desc' }); factory PayeeQueryRequest.fromJson(Map json) => _$PayeeQueryRequestFromJson(json); Map toJson() => _$PayeeQueryRequestToJson(this); PayeeQueryRequest copyWith({ int? page, int? size, String? sort, }) { return PayeeQueryRequest( page: page ?? this.page, size: size ?? this.size, sort: sort ?? this.sort, ); } @override List get props => [page, sort]; } ================================================ FILE: bookkeeping_user_flutter/lib/payees/data/models/payee_query_request.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'payee_query_request.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** PayeeQueryRequest _$PayeeQueryRequestFromJson(Map json) => PayeeQueryRequest( page: json['page'] as int? ?? 1, size: json['size'] as int? ?? 15, sort: json['sort'] as String? ?? 'id,desc', ); Map _$PayeeQueryRequestToJson(PayeeQueryRequest instance) => { 'page': instance.page, 'size': instance.size, 'sort': instance.sort, }; ================================================ FILE: bookkeeping_user_flutter/lib/payees/data/payee_repository.dart ================================================ import 'dart:convert'; import '/payees/payees.dart'; import '/commons/commons.dart'; class PayeeRepository { List _responseToList(String response) { return (json.decode(response)['data']).map((i) => Payee.fromJson(i)).toList(); } Payee _responsePayee(String response) { return Payee.fromJson(json.decode(response)['data']); } Future> getExpenseable() async { String response = await HttpClient().get('payees/expenseable'); return _responseToList(response); } Future> getIncomeable() async { String response = await HttpClient().get('payees/incomeable'); return _responseToList(response); } Future> getEnable() async { String response = await HttpClient().get('payees/enable'); return _responseToList(response); } Future> query(PayeeQueryRequest request) async { String response = await HttpClient().get('payees', params: request.toJson()); return (json.decode(response)['data']['content']).map((i) => Payee.fromJson(i)).toList(); } Future get(int id) async { return _responsePayee(await HttpClient().get('payees/$id')); } Future delete(String id) async { String response = await HttpClient().delete('payees/$id'); return parseResponse(response); } Future toggle(String id) async { String response = await HttpClient().put('payees/$id/toggle'); return parseResponse(response); } Future add(PayeeFormRequest request) async { String response = await HttpClient().post('payees', data: request.toJson()); return parseResponse(response); } Future update(int id, PayeeFormRequest request) async { String response = await HttpClient().put('payees/$id', data: request.toJson()); return parseResponse(response); } } ================================================ FILE: bookkeeping_user_flutter/lib/payees/payees.dart ================================================ export 'bloc/payee_enable/payee_enable_bloc.dart'; export 'bloc/payee_expenseable/payee_expenseable_bloc.dart'; export 'bloc/payee_incomeable/payee_incomeable_bloc.dart'; export 'bloc/payees/payees_bloc.dart'; export 'bloc/payee_fetch/payee_fetch_bloc.dart'; export 'bloc/payee_form/payee_form_bloc.dart'; export 'data/payee_repository.dart'; export 'data/models/payee.dart'; export 'data/models/payee_query_request.dart'; export 'data/models/payee_form_request.dart'; export 'ui/payees_page.dart'; export 'ui/payee_form_page.dart'; export 'ui/payee_detail_page.dart'; export 'ui/payee_form/name_input.dart'; export 'ui/payee_form/expenseable_input.dart'; export 'ui/payee_form/incomeable_input.dart'; export 'ui/payee_form/notes_input.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/payees/ui/payee_detail_page.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/components/components.dart'; import '/commons/commons.dart'; import '/payees/payees.dart'; class PayeeDetailPage extends StatefulWidget { final Payee payee; PayeeDetailPage({ required this.payee }); @override State createState() => _PayeeDetailPageState(); } class _PayeeDetailPageState extends State { @override void initState() { BlocProvider.of(context).add(PayeeLoadDefault(payee: widget.payee)); super.initState(); } @override Widget build(BuildContext context) { return MultiBlocListener( listeners: [ BlocListener( listenWhen: (previous, current) => previous.deleteStatus != current.deleteStatus, listener: (context, state) { if (state.deleteStatus == LoadDataStatus.success) { Message.success('操作成功!'); // Navigator.pop(context); if (Navigator.canPop(context)) { Navigator.of(context).pop(); } else { SystemNavigator.pop(); } } } ), BlocListener( listenWhen: (previous, current) => previous.toggleStatus != current.toggleStatus, listener: (context, state) { if (state.toggleStatus == LoadDataStatus.success) { Message.success('操作成功!'); BlocProvider.of(context).add(PayeeFetched()); } }, ) ], child: BlocBuilder( builder: (context, state) { return Scaffold( appBar: AppBar( centerTitle: true, title: const Text('交易对象详情'), actions: _buildActions(context, state.payee ?? widget.payee) ), body: Builder( builder: (context) { switch (state.status) { case LoadDataStatus.progress: case LoadDataStatus.initial: return const PageLoading(); case LoadDataStatus.success: return _buildBody(context, state.payee ?? widget.payee); default: return PageError(onTap: () { BlocProvider.of(context).add(PayeeFetched()); }); } }, ) ); } ) ); } List _buildActions(BuildContext context, Payee payee) { return [ IconButton( icon: Icon(Icons.edit), onPressed: () { fullDialog(context, PayeeFormPage(type: 2, payee: payee)); } ), IconButton( icon: Icon(Icons.delete), onPressed: () async { if (await confirm( context, content: Text("确定删除${payee.name}吗?"), textOK: Text("确定"), textCancel: Text("取消"), )) { BlocProvider.of(context).add(PayeeDeleted(payee.id.toString())); } } ) ]; } Widget _buildBody(BuildContext context, Payee payee) { final theme = Theme.of(context); TextStyle? style1 = theme.textTheme.bodyText2; TextStyle? style2 = theme.textTheme.bodyText1; return SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ SizedBox(height: 20), Row(children: [Text("名称:", style: style1), Text(payee.name, style: style2)]), SizedBox(height: 15), Row(children: [Text("是否可支出:", style: style1), Text(boolToString(payee.expenseable), style: style2)]), SizedBox(height: 15), Row(children: [Text("是否可收入:", style: style1), Text(boolToString(payee.incomeable), style: style2)]), SizedBox(height: 15), Row(children: [Text("备注:", style: style1), Flexible(child: Text(payee.notes ?? '', style: style2))]), SizedBox(height: 15), Row(children: [Text("是否可用:", style: style1), Text(boolToString(payee.enable), style: style2)]), SizedBox(height: 20), SizedBox( width: double.infinity, child: ElevatedButton( child: Text(payee.enable ? '禁用' : '启用'), onPressed: () { BlocProvider.of(context).add(PayeeToggled(payee.id.toString())); } ), ) ], ) ), ); } } ================================================ FILE: bookkeeping_user_flutter/lib/payees/ui/payee_form/expenseable_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/payees/payees.dart'; import '/commons/commons.dart'; class ExpenseableSwitch extends StatelessWidget { @override Widget build(BuildContext context) { return buildFormItem( "是否可支出", BlocSelector( selector: (state) => state.request.expenseable, builder: (context, state) { return Align( alignment: Alignment.centerLeft, child: Switch( value: state ?? true, onChanged: (value) => context.read().add(PayeeFormExpenseableChanged(value)), ), ); } ), context); } } ================================================ FILE: bookkeeping_user_flutter/lib/payees/ui/payee_form/incomeable_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/payees/payees.dart'; import '/commons/commons.dart'; class IncomeableSwitch extends StatelessWidget { @override Widget build(BuildContext context) { return buildFormItem( "是否可支出", BlocSelector( selector: (state) => state.request.incomeable, builder: (context, state) { return Align( alignment: Alignment.centerLeft, child: Switch( value: state ?? true, onChanged: (value) => context.read().add(PayeeFormIncomeableChanged(value)), ), ); } ), context); } } ================================================ FILE: bookkeeping_user_flutter/lib/payees/ui/payee_form/name_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/payees/payees.dart'; class NameInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '名称', BlocBuilder( buildWhen: (previous, current) => previous.request.name != current.request.name, builder: (context, state) { controller.value = TextEditingValue( text: state.request.name ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state.request.name?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(PayeeFormNameChanged(value)), decoration: InputDecoration( contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0), ), ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/payees/ui/payee_form/notes_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/payees/payees.dart'; import '/commons/commons.dart'; class NotesInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '备注', BlocBuilder( buildWhen: (previous, current) => previous.request.notes != current.request.notes, builder: (context, state) { controller.value = TextEditingValue( text: state.request.notes ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state.request.notes?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(PayeeFormNotesChanged(value)), decoration: InputDecoration(), ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/payees/ui/payee_form_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import '/payees/payees.dart'; import '/commons/commons.dart'; class PayeeFormPage extends StatelessWidget { final int type; // 1-新增,2-修改 final Payee? payee; const PayeeFormPage({ required this.type, this.payee, }); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => PayeeFormBloc(payeeRepository: RepositoryProvider.of(context))..add(PayeeFormDefaultLoaded(type, payee)), child: BlocListener( listener: (context, state) { if (state.status == FormzStatus.submissionSuccess) { Message.success('操作成功'); Navigator.of(context).pop(); BlocProvider.of(context).add(PayeesRefreshed()); if (type == 2) { BlocProvider.of(context).add(PayeeFetched()); } } }, child: Builder( builder: (context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text(_buildTitle(type)), actions: [ IconButton( icon: Icon(Icons.done), onPressed: () { BlocProvider.of(context).add(PayeeFormSubmitted(type, payee)); } ) ] ), body: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ NameInput(), SizedBox(height: 10), ExpenseableSwitch(), IncomeableSwitch(), NotesInput() ], ) ) ), ); } ) ) ); } String _buildTitle(int type) { switch (type) { case 1: return '新增交易对象'; case 2: return '修改交易对象'; default: return '账户操作类型异常'; } } } ================================================ FILE: bookkeeping_user_flutter/lib/payees/ui/payees_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import '/routes.dart'; import '/commons/commons.dart'; import '/payees/payees.dart'; import '/components/components.dart'; class PayeesPage extends StatefulWidget { @override State createState() => _PayeesPageState(); } class _PayeesPageState extends State { RefreshController _refreshController = RefreshController(initialRefresh: false); @override void initState() { BlocProvider.of(context).add(PayeesRefreshed()); super.initState(); } @override Widget build(BuildContext context) { return MultiBlocListener( listeners: [ BlocListener( listenWhen: (previous, current) => previous.loadMoreStatus != current.loadMoreStatus, listener: (context, state) { if (state.loadMoreStatus == LoadDataStatus.success) { _refreshController.loadComplete(); } else if (state.loadMoreStatus == LoadDataStatus.failure) { _refreshController.loadFailed(); } else if (state.loadMoreStatus == LoadDataStatus.empty) { _refreshController.loadNoData(); } }, ), BlocListener( listenWhen: (previous, current) => previous.deleteStatus != current.deleteStatus, listener: (context, state) { if (state.deleteStatus == LoadDataStatus.success) { BlocProvider.of(context).add(PayeesRefreshed()); } }, ), BlocListener( listenWhen: (previous, current) => previous.toggleStatus != current.toggleStatus, listener: (context, state) { if (state.toggleStatus == LoadDataStatus.success) { BlocProvider.of(context).add(PayeesRefreshed()); } }, ), ], child: Scaffold( appBar: AppBar( title: const Text("交易对象"), centerTitle: true, actions: [ IconButton( icon: const Icon(Icons.add), onPressed: () { fullDialog(context, PayeeFormPage(type: 1)); } ) ] ), body: BlocBuilder( buildWhen: (previous, current) => previous.status != current.status || previous.payees != current.payees, builder: (context, state) { switch (state.status) { case LoadDataStatus.progress: case LoadDataStatus.initial: return const PageLoading(); case LoadDataStatus.success: if (state.payees.isEmpty) return Empty(); return SmartRefresher( enablePullDown: true, enablePullUp: true, controller: _refreshController, child: _buildList(context, state.payees), onRefresh: () async { BlocProvider.of(context).add(PayeesRefreshed()); _refreshController.refreshCompleted(); }, onLoading: () async { BlocProvider.of(context).add(PayeesLoadMore()); }, ); default: return PageError(onTap: () { BlocProvider.of(context).add(PayeesRefreshed()); }); } } ), ) ); } Widget _buildList(BuildContext context, List payees) { final theme = Theme.of(context); return ListView.separated( itemCount: payees.length, itemBuilder: (context, index) { Payee payee = payees[index]; return ListTile( dense: true, title: Text(payee.name, style: theme.textTheme.bodyText1,), subtitle: payee.notes != null && payee.notes!.isNotEmpty ? Text(payee.notes!, style: theme.textTheme.caption) : null, trailing: Icon(Icons.keyboard_arrow_right), onTap: () { Navigator.pushNamed(context, '/payee-detail', arguments: PayeeDetailArguments(payee: payee)); }, ); }, separatorBuilder: (context, index) { return Divider(); }, ); } } ================================================ FILE: bookkeeping_user_flutter/lib/routes.dart ================================================ import 'package:bookkeeping_user_flutter/charts/charts.dart'; import 'package:bookkeeping_user_flutter/items/items.dart'; import 'package:flutter/material.dart'; import '/components/components.dart'; import 'start_page.dart'; import 'index.dart'; import '/accounts/accounts.dart'; import '/categories/categories.dart'; import '/flows/flows.dart'; import '/payees/payees.dart'; import '/books/books.dart'; import '/tags/tags.dart'; class AppRouter { static final Map routes = { '/': (context) => StartPage(), '/index': (context) => IndexPage(), '/flows-filter': (context) => FlowsFilterPage(), '/expense-categories': (context) => ExpenseCategoryPage(), '/income-categories': (context) => IncomeCategoryPage(), '/payees': (context) => PayeesPage(), '/books': (context) => BooksPage(), '/tags': (context) => TagsPage(), '/charts-expense-category-filter': (context) => ChartsExpenseCategoryFilterPage(), '/charts-income-category-filter': (context) => ChartsIncomeCategoryFilterPage(), '/items-index': (context) => ItemsIndexPage() }; static final String initialRoute = '/'; static final RouteFactory generateRoute = (settings) { if (settings.name == '/flow-detail') { final args = settings.arguments as FlowDetailArguments; return MaterialPageRoute( builder: (_) => FlowDetailPage(flow: args.flow) ); } if (settings.name == '/account-detail') { final args = settings.arguments as AccountDetailArguments; return MaterialPageRoute( builder: (_) => AccountDetailPage(account: args.account) ); } if (settings.name == '/payee-detail') { final args = settings.arguments as PayeeDetailArguments; return MaterialPageRoute( builder: (_) => PayeeDetailPage(payee: args.payee) ); } if (settings.name == '/book-detail') { final args = settings.arguments as BookDetailArguments; return MaterialPageRoute( builder: (_) => BookDetailPage(book: args.book) ); } if (settings.name == '/category-detail') { final args = settings.arguments as CategoryDetailArguments; return MaterialPageRoute( builder: (_) => CategoryDetailPage(category: args.category, categoryType: args.categoryType) ); } if (settings.name == '/tag-detail') { final args = settings.arguments as TagDetailArguments; return MaterialPageRoute( builder: (_) => TagDetailPage(tag: args.tag) ); } return null; }; static final RouteFactory unknownRoute = (settings) { return MaterialPageRoute( builder: (_) => const PageError(msg: '路由找不到') ); }; } class FlowDetailArguments { final FlowModel flow; FlowDetailArguments({ required this.flow, }); } class AccountDetailArguments { final Account account; AccountDetailArguments({ required this.account, }); } class PayeeDetailArguments { final Payee payee; PayeeDetailArguments({ required this.payee, }); } class BookDetailArguments { final Book book; BookDetailArguments({ required this.book, }); } class CategoryDetailArguments { final int categoryType; final Category category; CategoryDetailArguments({ required this.category, required this.categoryType }); } class TagDetailArguments { final Tag tag; TagDetailArguments({ required this.tag, }); } ================================================ FILE: bookkeeping_user_flutter/lib/start_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/components/components.dart'; import '/login/login.dart'; import 'index.dart'; class StartPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, AuthState state) { if (state.status == AuthStatus.uninitialized) { return PageLoading(); } if (state.status == AuthStatus.authenticated) { return IndexPage(); } if (state.status == AuthStatus.unauthenticated) { return LoginPage(); } if (state.status == AuthStatus.loading) { return PageLoading(); } return Text('error'); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_enable/tag_enable_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/tags/tags.dart'; part 'tag_enable_event.dart'; part 'tag_enable_state.dart'; class TagEnableBloc extends Bloc { final TagRepository tagRepository; TagEnableBloc({ required this.tagRepository }) : super(TagEnableStateLoadInProgress()) { on(_onTagEnableLoaded); } void _onTagEnableLoaded(_, Emitter emit) async { // 只加载一次数据 // if (state is TagEnableStateLoadSuccess) return; try { emit(TagEnableStateLoadInProgress()); final tags = await tagRepository.getEnable(); emit(TagEnableStateLoadSuccess(tags)); } catch (_) { emit(TagEnableStateLoadFailure()); } } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_enable/tag_enable_event.dart ================================================ part of 'tag_enable_bloc.dart'; abstract class TagEnableEvent extends Equatable { const TagEnableEvent(); @override List get props => []; } class TagEnableLoaded extends TagEnableEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_enable/tag_enable_state.dart ================================================ part of 'tag_enable_bloc.dart'; abstract class TagEnableState extends Equatable { const TagEnableState(); @override List get props => []; } class TagEnableStateLoadInProgress extends TagEnableState { } class TagEnableStateLoadSuccess extends TagEnableState { final List tags; const TagEnableStateLoadSuccess(this.tags); @override List get props => [tags]; } class TagEnableStateLoadFailure extends TagEnableState { } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_expenseable/tag_expenseable_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/tags/tags.dart'; part 'tag_expenseable_event.dart'; part 'tag_expenseable_state.dart'; class TagExpenseableBloc extends Bloc { final TagRepository tagRepository; TagExpenseableBloc({ required this.tagRepository }) : super(TagExpenseableStateLoadInProgress()) { on(_onTagExpenseableLoaded); } void _onTagExpenseableLoaded(_, Emitter emit) async { // 只加载一次数据 // if (state is TagExpenseableStateLoadSuccess) return; try { emit(TagExpenseableStateLoadInProgress()); final tags = await tagRepository.getExpenseable(); emit(TagExpenseableStateLoadSuccess(tags)); } catch (_) { emit(TagExpenseableStateLoadFailure()); } } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_expenseable/tag_expenseable_event.dart ================================================ part of 'tag_expenseable_bloc.dart'; abstract class TagExpenseableEvent extends Equatable { const TagExpenseableEvent(); @override List get props => []; } class TagExpenseableLoaded extends TagExpenseableEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_expenseable/tag_expenseable_state.dart ================================================ part of 'tag_expenseable_bloc.dart'; abstract class TagExpenseableState extends Equatable { const TagExpenseableState(); @override List get props => []; } class TagExpenseableStateLoadInProgress extends TagExpenseableState { } class TagExpenseableStateLoadSuccess extends TagExpenseableState { final List tags; const TagExpenseableStateLoadSuccess(this.tags); @override List get props => [tags]; } class TagExpenseableStateLoadFailure extends TagExpenseableState { } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_fetch/tag_fetch_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import '/tags/tags.dart'; import '/commons/commons.dart'; part 'tag_fetch_event.dart'; part 'tag_fetch_state.dart'; class TagFetchBloc extends Bloc { final TagRepository tagRepository; TagFetchBloc({ required this.tagRepository, }) : super(TagFetchState()) { on(_onFetched); on(_onDefault); } void _onDefault(TagLoadDefault event, Emitter emit) { emit(state.copyWith( status: LoadDataStatus.success, tag: event.tag )); } void _onFetched(_, Emitter emit) async { try { emit(state.copyWith(status: LoadDataStatus.progress)); final tag = await tagRepository.get(state.tag!.id); emit(state.copyWith( status: LoadDataStatus.success, tag: tag, )); } catch (_) { emit(state.copyWith(status: LoadDataStatus.failure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_fetch/tag_fetch_event.dart ================================================ part of 'tag_fetch_bloc.dart'; @immutable class TagFetchEvent extends Equatable { const TagFetchEvent(); @override List get props => []; } class TagFetched extends TagFetchEvent {} class TagLoadDefault extends TagFetchEvent { final Tag tag; const TagLoadDefault({ required this.tag, }); List get props => [tag]; } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_fetch/tag_fetch_state.dart ================================================ part of 'tag_fetch_bloc.dart'; @immutable class TagFetchState extends Equatable { final LoadDataStatus status; final Tag? tag; const TagFetchState({ this.status = LoadDataStatus.initial, this.tag, }); TagFetchState copyWith({ LoadDataStatus? status, Tag? tag, }) { return TagFetchState( status: status ?? this.status, tag: tag ?? this.tag, ); } @override List get props => [status, tag]; } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_form/tag_form_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:formz/formz.dart'; import 'package:meta/meta.dart'; import '/tags/tags.dart'; part 'tag_form_event.dart'; part 'tag_form_state.dart'; class TagFormBloc extends Bloc { final TagRepository tagRepository; final TagExpenseableBloc tagExpenseableBloc; final TagIncomeableBloc tagIncomeableBloc; final TagTransferableBloc tagTransferableBloc; final TagEnableBloc tagEnableBloc; TagFormBloc({ required this.tagRepository, required this.tagExpenseableBloc, required this.tagIncomeableBloc, required this.tagTransferableBloc, required this.tagEnableBloc }) : super(const TagFormState()) { on(_onNameChanged); on(_onNotesChanged); on(_onParentChanged); on(_onExpenseableChanged); on(_onIncomeableChanged); on(_onTransferableChanged); on(_onDefaultLoaded); on(_onSubmitted); } void _onNameChanged(TagFormNameChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(name: event.name), )); } void _onNotesChanged(TagFormNotesChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(notes: event.notes), )); } void _onParentChanged(TagFormParentChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(parentId: event.parentId), )); } void _onExpenseableChanged(TagFormExpenseableChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(expenseable: event.expenseable), )); } void _onIncomeableChanged(TagFormIncomeableChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(incomeable: event.incomeable), )); } void _onTransferableChanged(TagFormTransferableChanged event, Emitter emit) { emit(state.copyWith( request: state.request.copyWith(transferable: event.transferable), )); } void _onDefaultLoaded(TagFormDefaultLoaded event, Emitter emit) { emit(state.copyWith( status: FormzStatus.pure, request: state.request.copyWith( name: event.type == 2 ? event.tag?.name ?? '' : '', notes: event.type == 2 ? event.tag?.notes ?? '' : '', parentId: event.type == 2 ? event.tag?.parentId.toString() : event.tag?.id.toString(), expenseable: event.tag?.expenseable ?? true, incomeable: event.tag?.incomeable ?? false, transferable: event.tag?.transferable ?? false ) )); } void _onSubmitted(TagFormSubmitted event, Emitter emit) async { try { bool result = false; switch (event.type) { case 1: result = await tagRepository.add(state.request); break; case 2: result = await tagRepository.update(event.tag!.id, state.request); break; default: break; } if (result) { emit(state.copyWith(status: FormzStatus.submissionSuccess)); tagExpenseableBloc.add(TagExpenseableLoaded()); tagIncomeableBloc.add(TagIncomeableLoaded()); tagTransferableBloc.add(TagTransferableLoaded()); tagEnableBloc.add(TagEnableLoaded()); } else { emit(state.copyWith(status: FormzStatus.submissionFailure)); } } catch (_) { print(_); emit(state.copyWith(status: FormzStatus.submissionFailure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_form/tag_form_event.dart ================================================ part of 'tag_form_bloc.dart'; @immutable abstract class TagFormEvent extends Equatable { const TagFormEvent(); @override List get props => []; } class TagFormNameChanged extends TagFormEvent { const TagFormNameChanged(this.name); final String name; @override List get props => [name]; } class TagFormNotesChanged extends TagFormEvent { const TagFormNotesChanged(this.notes); final String notes; @override List get props => [notes]; } class TagFormParentChanged extends TagFormEvent { const TagFormParentChanged(this.parentId); final String parentId; @override List get props => [parentId]; } class TagFormExpenseableChanged extends TagFormEvent { const TagFormExpenseableChanged(this.expenseable); final bool expenseable; @override List get props => [expenseable]; } class TagFormIncomeableChanged extends TagFormEvent { const TagFormIncomeableChanged(this.incomeable); final bool incomeable; @override List get props => [incomeable]; } class TagFormTransferableChanged extends TagFormEvent { const TagFormTransferableChanged(this.transferable); final bool transferable; @override List get props => [transferable]; } class TagFormDefaultLoaded extends TagFormEvent { final int type; final Tag? tag; const TagFormDefaultLoaded(this.type, this.tag); @override List get props => [type, tag]; } class TagFormSubmitted extends TagFormEvent { final int type; final Tag? tag; const TagFormSubmitted(this.type, this.tag); @override List get props => [type, tag]; } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_form/tag_form_state.dart ================================================ part of 'tag_form_bloc.dart'; @immutable class TagFormState extends Equatable { final FormzStatus status; final TagFormRequest request; const TagFormState({ this.status = FormzStatus.pure, this.request = const TagFormRequest(), }); TagFormState copyWith({ FormzStatus? status, TagFormRequest? request, }) { return TagFormState( status: status ?? this.status, request: request ?? this.request, ); } @override List get props => [status, request]; } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_incomeable/tag_incomeable_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/tags/tags.dart'; part 'tag_incomeable_event.dart'; part 'tag_incomeable_state.dart'; class TagIncomeableBloc extends Bloc { final TagRepository tagRepository; TagIncomeableBloc({ required this.tagRepository }) : super(TagIncomeableStateLoadInProgress()) { on(_onTagIncomeableLoaded); } void _onTagIncomeableLoaded(_, Emitter emit) async { // 只加载一次数据 // if (state is TagIncomeableStateLoadSuccess) return; try { emit(TagIncomeableStateLoadInProgress()); final tags = await tagRepository.getIncomeable(); emit(TagIncomeableStateLoadSuccess(tags)); } catch (_) { emit(TagIncomeableStateLoadFailure()); } } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_incomeable/tag_incomeable_event.dart ================================================ part of 'tag_incomeable_bloc.dart'; abstract class TagIncomeableEvent extends Equatable { const TagIncomeableEvent(); @override List get props => []; } class TagIncomeableLoaded extends TagIncomeableEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_incomeable/tag_incomeable_state.dart ================================================ part of 'tag_incomeable_bloc.dart'; abstract class TagIncomeableState extends Equatable { const TagIncomeableState(); @override List get props => []; } class TagIncomeableStateLoadInProgress extends TagIncomeableState { } class TagIncomeableStateLoadSuccess extends TagIncomeableState { final List tags; const TagIncomeableStateLoadSuccess(this.tags); @override List get props => [tags]; } class TagIncomeableStateLoadFailure extends TagIncomeableState { } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_transferable/tag_transferable_bloc.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/tags/tags.dart'; part 'tag_transferable_event.dart'; part 'tag_transferable_state.dart'; class TagTransferableBloc extends Bloc { final TagRepository tagRepository; TagTransferableBloc({ required this.tagRepository }) : super(TagTransferableStateLoadInProgress()) { on(_onTagTransferableLoaded); } void _onTagTransferableLoaded(_, Emitter emit) async { // 只加载一次数据 // if (state is TagTransferableStateLoadSuccess) return; try { emit(TagTransferableStateLoadInProgress()); final tags = await tagRepository.getTransferable(); emit(TagTransferableStateLoadSuccess(tags)); } catch (_) { emit(TagTransferableStateLoadFailure()); } } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_transferable/tag_transferable_event.dart ================================================ part of 'tag_transferable_bloc.dart'; abstract class TagTransferableEvent extends Equatable { const TagTransferableEvent(); @override List get props => []; } class TagTransferableLoaded extends TagTransferableEvent { } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_transferable/tag_transferable_state.dart ================================================ part of 'tag_transferable_bloc.dart'; abstract class TagTransferableState extends Equatable { const TagTransferableState(); @override List get props => []; } class TagTransferableStateLoadInProgress extends TagTransferableState { } class TagTransferableStateLoadSuccess extends TagTransferableState { final List tags; const TagTransferableStateLoadSuccess(this.tags); @override List get props => [tags]; } class TagTransferableStateLoadFailure extends TagTransferableState { } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_tree/tag_tree_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import '/tags/tags.dart'; import '/commons/commons.dart'; part 'tag_tree_event.dart'; part 'tag_tree_state.dart'; class TagTreeBloc extends Bloc { final TagRepository tagRepository; final TagExpenseableBloc tagExpenseableBloc; final TagIncomeableBloc tagIncomeableBloc; final TagTransferableBloc tagTransferableBloc; final TagEnableBloc tagEnableBloc; TagTreeBloc({ required this.tagRepository, required this.tagExpenseableBloc, required this.tagIncomeableBloc, required this.tagTransferableBloc, required this.tagEnableBloc, }) : super(TagTreeState()) { on(_onRefreshed); on(_onItemClicked); on(_onBackClicked); on(_onToggled); on(_onDeleted); } void _onRefreshed(_, Emitter emit) async { try { emit(state.copyWith( status: LoadDataStatus.progress, )); final tags = await tagRepository.query(); emit(state.copyWith( status: LoadDataStatus.success, tags: tags, currentTags: tags )); } catch (_) { print(_); emit(state.copyWith(status: LoadDataStatus.failure)); } } void _onItemClicked(TagItemClicked event, Emitter emit) async { List> newHistory = List.from(state.history); newHistory.add(state.currentTags); emit(state.copyWith( currentLevel: state.currentLevel + 1, currentTags: event.tagTree.children, history: newHistory, )); } void _onBackClicked(TagBackClicked event, Emitter emit) async { emit(state.copyWith( currentLevel: state.currentLevel - 1, currentTags: state.history.last )); } void _onToggled(TagToggled event, Emitter emit) async { try { emit(state.copyWith(toggleStatus: LoadDataStatus.progress)); final result = await tagRepository.toggle(event.id); if (result) { emit(state.copyWith(toggleStatus: LoadDataStatus.success)); tagExpenseableBloc.add(TagExpenseableLoaded()); tagIncomeableBloc.add(TagIncomeableLoaded()); tagTransferableBloc.add(TagTransferableLoaded()); tagEnableBloc.add(TagEnableLoaded()); } else { emit(state.copyWith(toggleStatus: LoadDataStatus.failure)); } } catch (_) { emit(state.copyWith(toggleStatus: LoadDataStatus.failure)); } } void _onDeleted(TagDeleted event, Emitter emit) async { try { emit(state.copyWith(deleteStatus: LoadDataStatus.progress)); final result = await tagRepository.delete(event.id); if (result) { emit(state.copyWith(deleteStatus: LoadDataStatus.success)); tagExpenseableBloc.add(TagExpenseableLoaded()); tagIncomeableBloc.add(TagIncomeableLoaded()); tagTransferableBloc.add(TagTransferableLoaded()); tagEnableBloc.add(TagEnableLoaded()); } else { emit(state.copyWith(deleteStatus: LoadDataStatus.failure)); } } catch (_) { emit(state.copyWith(deleteStatus: LoadDataStatus.failure)); } } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_tree/tag_tree_event.dart ================================================ part of 'tag_tree_bloc.dart'; @immutable abstract class TagTreeEvent extends Equatable { const TagTreeEvent(); @override List get props => []; } class TagTreeRefreshed extends TagTreeEvent { } class TagItemClicked extends TagTreeEvent { final TagTree tagTree; const TagItemClicked({ required this.tagTree, }); } class TagBackClicked extends TagTreeEvent { } class TagDeleted extends TagTreeEvent { final String id; const TagDeleted(this.id); @override List get props => [id]; } class TagToggled extends TagTreeEvent { final String id; const TagToggled(this.id); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/tags/bloc/tag_tree/tag_tree_state.dart ================================================ part of 'tag_tree_bloc.dart'; @immutable class TagTreeState extends Equatable { final LoadDataStatus status; final List tags; final LoadDataStatus deleteStatus; final LoadDataStatus toggleStatus; final List currentTags; final int currentLevel; final List> history; @override List get props => [status, tags, deleteStatus, toggleStatus, currentTags, currentLevel, history]; const TagTreeState({ this.status = LoadDataStatus.initial, this.tags = const [], this.deleteStatus = LoadDataStatus.initial, this.toggleStatus = LoadDataStatus.initial, this.currentTags = const [], this.currentLevel = 1, this.history = const [], }); TagTreeState copyWith({ LoadDataStatus? status, List? tags, LoadDataStatus? deleteStatus, LoadDataStatus? toggleStatus, List? currentTags, String? title, int? currentLevel, List>? history }) { return TagTreeState( status: status ?? this.status, tags: tags ?? this.tags, deleteStatus: deleteStatus ?? this.deleteStatus, toggleStatus: toggleStatus ?? this.toggleStatus, currentTags: currentTags ?? this.currentTags, currentLevel: currentLevel ?? this.currentLevel, history: history ?? this.history ); } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/data/models/tag.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import '/tags/data/models/tag_tree.dart'; part 'tag.g.dart'; @JsonSerializable() class Tag extends Equatable { final int id; final String name; final String? notes; final bool enable; final int? parentId; final String? parentName; final bool expenseable; final bool incomeable; final bool transferable; Tag({ required this.id, required this.name, required this.enable, this.parentId, this.parentName, required this.expenseable, required this.incomeable, required this.transferable, this.notes }); factory Tag.fromJson(Map json) => _$TagFromJson(json); Map toJson() => _$TagToJson(this); factory Tag.fromTree(TagTree tagTree) => Tag( id: tagTree.id, name: tagTree.name, notes: tagTree.notes, enable: tagTree.enable, parentId: tagTree.parentId, parentName: tagTree.parentName, expenseable: tagTree.expenseable, incomeable: tagTree.incomeable, transferable: tagTree.transferable ); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/tags/data/models/tag.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'tag.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Tag _$TagFromJson(Map json) => Tag( id: json['id'] as int, name: json['name'] as String, enable: json['enable'] as bool, parentId: json['parentId'] as int?, parentName: json['parentName'] as String?, expenseable: json['expenseable'] as bool, incomeable: json['incomeable'] as bool, transferable: json['transferable'] as bool, notes: json['notes'] as String?, ); Map _$TagToJson(Tag instance) => { 'id': instance.id, 'name': instance.name, 'notes': instance.notes, 'enable': instance.enable, 'parentId': instance.parentId, 'parentName': instance.parentName, 'expenseable': instance.expenseable, 'incomeable': instance.incomeable, 'transferable': instance.transferable, }; ================================================ FILE: bookkeeping_user_flutter/lib/tags/data/models/tag_form_request.dart ================================================ import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; part 'tag_form_request.g.dart'; @JsonSerializable() class TagFormRequest extends Equatable { final String? name; final String? notes; final String? parentId; final bool? expenseable; final bool? incomeable; final bool? transferable; const TagFormRequest({ this.name, this.notes, this.parentId, this.expenseable, this.incomeable, this.transferable }); Map toJson() => _$TagFormRequestToJson(this); TagFormRequest copyWith({ String? name, String? notes, String? parentId, bool? expenseable, bool? incomeable, bool? transferable, }) { return TagFormRequest( name: name ?? this.name, notes: notes ?? this.notes, parentId: parentId ?? this.parentId, expenseable: expenseable ?? this.expenseable, incomeable: incomeable ?? this.incomeable, transferable: transferable ?? this.transferable, ); } @override List get props => [name, notes, parentId, expenseable, incomeable, transferable]; } ================================================ FILE: bookkeeping_user_flutter/lib/tags/data/models/tag_form_request.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'tag_form_request.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** TagFormRequest _$TagFormRequestFromJson(Map json) => TagFormRequest( name: json['name'] as String?, notes: json['notes'] as String?, parentId: json['parentId'] as String?, expenseable: json['expenseable'] as bool?, incomeable: json['incomeable'] as bool?, transferable: json['transferable'] as bool?, ); Map _$TagFormRequestToJson(TagFormRequest instance) => { 'name': instance.name, 'notes': instance.notes, 'parentId': instance.parentId, 'expenseable': instance.expenseable, 'incomeable': instance.incomeable, 'transferable': instance.transferable, }; ================================================ FILE: bookkeeping_user_flutter/lib/tags/data/models/tag_tree.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'tag_tree.g.dart'; @JsonSerializable() class TagTree extends Equatable { final int id; final String name; final String? notes; final bool enable; final int? parentId; final String? parentName; final List? children; final bool expenseable; final bool incomeable; final bool transferable; TagTree({ required this.id, required this.name, this.notes, required this.enable, this.parentId, this.parentName, this.children, required this.expenseable, required this.incomeable, required this.transferable, }); factory TagTree.fromJson(Map json) => _$TagTreeFromJson(json); Map toJson() => _$TagTreeToJson(this); @override List get props => [id]; } ================================================ FILE: bookkeeping_user_flutter/lib/tags/data/models/tag_tree.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'tag_tree.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** TagTree _$TagTreeFromJson(Map json) => TagTree( id: json['id'] as int, name: json['name'] as String, notes: json['notes'] as String?, enable: json['enable'] as bool, parentId: json['parentId'] as int?, parentName: json['parentName'] as String?, children: (json['children'] as List?) ?.map((e) => TagTree.fromJson(e as Map)) .toList(), expenseable: json['expenseable'] as bool, incomeable: json['incomeable'] as bool, transferable: json['transferable'] as bool, ); Map _$TagTreeToJson(TagTree instance) => { 'id': instance.id, 'name': instance.name, 'notes': instance.notes, 'enable': instance.enable, 'parentId': instance.parentId, 'parentName': instance.parentName, 'children': instance.children, 'expenseable': instance.expenseable, 'incomeable': instance.incomeable, 'transferable': instance.transferable, }; ================================================ FILE: bookkeeping_user_flutter/lib/tags/data/tag_repository.dart ================================================ import 'dart:convert'; import '/commons/commons.dart'; import '/tags/tags.dart'; class TagRepository { List _responseToList(String response) { return (json.decode(response)['data']).map((i) => Tag.fromJson(i)).toList(); } Future> getExpenseable() async { String response = await HttpClient().get('tags/expenseable'); return _responseToList(response); } Future> getIncomeable() async { String response = await HttpClient().get('tags/incomeable'); return _responseToList(response); } Future> getTransferable() async { String response = await HttpClient().get('tags/transferable'); return _responseToList(response); } Future> getEnable() async { String response = await HttpClient().get('tags/enable'); return _responseToList(response); } Future get(int id) async { String response = await HttpClient().get('tags/$id'); return Tag.fromJson(json.decode(response)['data']); } Future> query() async { String response = await HttpClient().get('tags'); return (json.decode(response)['data']).map((i) => TagTree.fromJson(i)).toList(); } Future toggle(String id) async { String response = await HttpClient().put('tags/$id/toggle'); return parseResponse(response); } Future delete(String id) async { String response = await HttpClient().delete('tags/$id'); return parseResponse(response); } Future add(TagFormRequest request) async { String response = await HttpClient().post('tags', data: request.toJson()); return parseResponse(response); } Future update(int id, TagFormRequest request) async { String response = await HttpClient().put('tags/$id', data: request.toJson()); return parseResponse(response); } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/tags.dart ================================================ export 'bloc/tag_expenseable/tag_expenseable_bloc.dart'; export 'bloc/tag_incomeable/tag_incomeable_bloc.dart'; export 'bloc/tag_transferable/tag_transferable_bloc.dart'; export 'bloc/tag_enable/tag_enable_bloc.dart'; export 'bloc/tag_tree/tag_tree_bloc.dart'; export 'bloc/tag_fetch/tag_fetch_bloc.dart'; export 'bloc/tag_form/tag_form_bloc.dart'; export 'data/tag_repository.dart'; export 'data/models/tag.dart'; export 'data/models/tag_tree.dart'; export 'data/models/tag_form_request.dart'; export 'ui/tags_page.dart'; export 'ui/tag_detail_page.dart'; export 'ui/tag_form_page.dart'; export 'ui/widgets/tag_form/name_input.dart'; export 'ui/widgets/tag_form/notes_input.dart'; export 'ui/widgets/tag_form/expenseable_input.dart'; export 'ui/widgets/tag_form/incomeable_input.dart'; export 'ui/widgets/tag_form/transferable_input.dart'; export 'ui/widgets/tag_form/parent_input.dart'; ================================================ FILE: bookkeeping_user_flutter/lib/tags/ui/tag_detail_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/components/components.dart'; import '/commons/commons.dart'; import '/tags/tags.dart'; class TagDetailPage extends StatefulWidget { final Tag tag; TagDetailPage({ required this.tag }); @override State createState() => _TagDetailPageState(); } class _TagDetailPageState extends State { @override void initState() { BlocProvider.of(context).add(TagLoadDefault(tag: widget.tag)); super.initState(); } @override Widget build(BuildContext context) { return MultiBlocListener( listeners: [ BlocListener( listenWhen: (previous, current) => previous.deleteStatus != current.deleteStatus, listener: (context, state) { if (state.deleteStatus == LoadDataStatus.success) { Message.success('操作成功!'); // Navigator.pop(context); if (Navigator.canPop(context)) { Navigator.of(context).pop(); } else { SystemNavigator.pop(); } } } ), BlocListener( listenWhen: (previous, current) => previous.toggleStatus != current.toggleStatus, listener: (context, state) { if (state.toggleStatus == LoadDataStatus.success) { Message.success('操作成功!'); BlocProvider.of(context).add(TagFetched()); } }, ) ], child: BlocBuilder( builder: (context, state) { return Scaffold( appBar: AppBar( centerTitle: true, title: const Text('标签详情'), actions: _buildActions(context, state.tag ?? widget.tag) ), body: Builder( builder: (context) { switch (state.status) { case LoadDataStatus.progress: case LoadDataStatus.initial: return const PageLoading(); case LoadDataStatus.success: return _buildBody(context, state.tag ?? widget.tag); default: return PageError(onTap: () { BlocProvider.of(context).add(TagFetched()); }); } }, ) ); } ) ); } List _buildActions(BuildContext context, Tag tag) { return [ IconButton( icon: const Icon(Icons.add), onPressed: () { fullDialog(context, TagFormPage(type: 1, tag: tag)); } ), IconButton( icon: Icon(Icons.edit), onPressed: () { fullDialog(context, TagFormPage(type: 2, tag: tag)); } ), IconButton( icon: Icon(Icons.delete), onPressed: () async { if (await confirm( context, content: Text("确定删除${tag.name}吗?"), textOK: Text("确定"), textCancel: Text("取消"), )) { BlocProvider.of(context).add(TagDeleted(tag.id.toString())); } } ) ]; } Widget _buildBody(BuildContext context, Tag tag) { final theme = Theme.of(context); TextStyle? style1 = theme.textTheme.bodyText2; TextStyle? style2 = theme.textTheme.bodyText1; return SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ SizedBox(height: 20), Row(children: [Text("父级名称:", style: style1), Text(tag.parentName ?? '', style: style2)]), SizedBox(height: 15), Row(children: [Text("标签名称:", style: style1), Text(tag.name, style: style2)]), SizedBox(height: 15), Row(children: [Text("备注:", style: style1), Flexible(child: Text(tag.notes ?? '', style: style2))]), SizedBox(height: 15), Row(children: [Text("是否可支出:", style: style1), Text(boolToString(tag.expenseable), style: style2)]), SizedBox(height: 15), Row(children: [Text("是否可收入:", style: style1), Text(boolToString(tag.incomeable), style: style2)]), SizedBox(height: 15), Row(children: [Text("是否可转账:", style: style1), Text(boolToString(tag.transferable), style: style2)]), SizedBox(height: 15), Row(children: [Text("是否可用:", style: style1), Text(boolToString(tag.enable), style: style2)]), SizedBox(height: 20), SizedBox( width: double.infinity, child: ElevatedButton( child: Text(tag.enable ? '禁用' : '启用'), onPressed: () { BlocProvider.of(context).add(TagToggled(tag.id.toString())); } ), ) ], ) ), ); } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/ui/tag_form_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import '/components/components.dart'; import '/commons/commons.dart'; import '/tags/tags.dart'; class TagFormPage extends StatelessWidget { final int type; // 1-新增,2-修改 final Tag? tag; const TagFormPage({ required this.type, this.tag, }); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => TagFormBloc( tagRepository: RepositoryProvider.of(context), tagExpenseableBloc: BlocProvider.of(context), tagIncomeableBloc: BlocProvider.of(context), tagTransferableBloc: BlocProvider.of(context), tagEnableBloc: BlocProvider.of(context), )..add(TagFormDefaultLoaded(type, tag)), child: BlocListener( listener: (context, state) { if (state.status == FormzStatus.submissionSuccess) { Message.success('操作成功'); Navigator.of(context).pop(); BlocProvider.of(context).add(TagTreeRefreshed()); if (type == 2) { BlocProvider.of(context).add(TagFetched()); } } }, child: Builder( builder: (context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text(_buildTitle(type)), actions: [ IconButton( icon: Icon(Icons.done), onPressed: () { BlocProvider.of(context).add(TagFormSubmitted(type, tag)); } ) ] ), body: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ ParentInput(), SizedBox(height: 10), NameInput(), SizedBox(height: 10), ExpenseableSwitch(), SizedBox(height: 10), IncomeableSwitch(), SizedBox(height: 10), TransferableSwitch(), SizedBox(height: 10), NotesInput() ], ), ), ) ); }, ) ) ); } String _buildTitle(int type) { switch (type) { case 1: return '新增标签'; case 2: return '修改标签'; default: return '操作类型异常'; } } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/ui/tags_page.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/routes.dart'; import '/components/components.dart'; import '/commons/commons.dart'; import '/tags/tags.dart'; class TagsPage extends StatefulWidget { @override State createState() => _TagPagesState(); } class _TagPagesState extends State { @override void initState() { BlocProvider.of(context).add(TagTreeRefreshed()); super.initState(); } @override Widget build(BuildContext context) { final state = context.watch().state; return MultiBlocListener( listeners: [ BlocListener( listenWhen: (previous, current) => previous.deleteStatus != current.deleteStatus, listener: (context, state) { if (state.deleteStatus == LoadDataStatus.success) { BlocProvider.of(context).add(TagTreeRefreshed()); } }, ), BlocListener( listenWhen: (previous, current) => previous.toggleStatus != current.toggleStatus, listener: (context, state) { if (state.toggleStatus == LoadDataStatus.success) { BlocProvider.of(context).add(TagTreeRefreshed()); } }, ), ], child: WillPopScope( onWillPop: () => _onWillPop(state), child: BlocBuilder( builder: (context, state) { return Scaffold( appBar: AppBar( title: const Text('标签'), centerTitle: true, actions: [ IconButton( icon: const Icon(Icons.add), onPressed: () { fullDialog(context, TagFormPage(type: 1)); } ) ] ), body: Builder( builder: (BuildContext context) { switch (state.status) { case LoadDataStatus.progress: case LoadDataStatus.initial: return const PageLoading(); case LoadDataStatus.success: if (state.currentTags.isEmpty) return Empty(); return _buildList(context, state.currentTags); default: return PageError(onTap: () { BlocProvider.of(context).add(TagTreeRefreshed()); }); } }, ), ); }, ) ), ); } Widget _buildList(BuildContext context, List tags) { final theme = Theme.of(context); return ListView.separated( itemCount: tags.length, itemBuilder: (context, index) { TagTree tagTree = tags[index]; return ListTile( dense: true, title: Text(tagTree.name, style: theme.textTheme.bodyText1), subtitle: tagTree.notes != null && tagTree.notes!.isNotEmpty ? Text(tagTree.notes!, style: theme.textTheme.caption) : null, trailing: tagTree.children != null && tagTree.children!.isNotEmpty ? Icon(Icons.keyboard_arrow_right) : null, onTap: () { if (tagTree.children != null && tagTree.children!.isNotEmpty) { BlocProvider.of(context).add(TagItemClicked(tagTree: tagTree)); } else { Navigator.pushNamed(context, '/tag-detail', arguments: TagDetailArguments(tag: Tag.fromTree(tagTree))); } }, onLongPress: () { Navigator.pushNamed(context, '/tag-detail', arguments: TagDetailArguments(tag: Tag.fromTree(tagTree))); }, ); }, separatorBuilder: (context, index) { return Divider(); }, ); } Future _onWillPop(state) async { if (state.currentLevel == 1) return true; BlocProvider.of(context).add(TagBackClicked()); return false; } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/ui/widgets/tag_form/expenseable_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/tags/tags.dart'; import '/commons/commons.dart'; class ExpenseableSwitch extends StatelessWidget { @override Widget build(BuildContext context) { return buildFormItem( "是否可支出", BlocSelector( selector: (state) => state.request.expenseable, builder: (context, state) { return Align( alignment: Alignment.centerLeft, child: Switch( value: state ?? true, onChanged: (value) => context.read().add(TagFormExpenseableChanged(value)), ), ); } ), context); } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/ui/widgets/tag_form/incomeable_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/tags/tags.dart'; import '/commons/commons.dart'; class IncomeableSwitch extends StatelessWidget { @override Widget build(BuildContext context) { return buildFormItem( "是否可收入", BlocSelector( selector: (state) => state.request.incomeable, builder: (context, state) { return Align( alignment: Alignment.centerLeft, child: Switch( value: state ?? true, onChanged: (value) => context.read().add(TagFormIncomeableChanged(value)), ), ); } ), context); } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/ui/widgets/tag_form/name_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/commons/commons.dart'; import '/tags/tags.dart'; class NameInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '名称', BlocBuilder( buildWhen: (previous, current) => previous.request.name != current.request.name, builder: (context, state) { controller.value = TextEditingValue( text: state.request.name ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state.request.name?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(TagFormNameChanged(value)), decoration: InputDecoration( contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0), ), ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/ui/widgets/tag_form/notes_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/tags/tags.dart'; import '/commons/commons.dart'; class NotesInput extends StatelessWidget { @override Widget build(BuildContext context) { var controller = TextEditingController(); return buildFormItem( '备注', BlocBuilder( buildWhen: (previous, current) => previous.request.notes != current.request.notes, builder: (context, state) { controller.value = TextEditingValue( text: state.request.notes ?? '', selection: TextSelection.fromPosition( TextPosition(offset: state.request.notes?.length ?? 0), ), ); return TextField( controller: controller, onChanged: (value) => context.read().add(TagFormNotesChanged(value)), decoration: InputDecoration(), ); } ), context ); } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/ui/widgets/tag_form/parent_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awesome_select/awesome_select.dart'; import '/commons/commons.dart'; import '/tags/tags.dart'; class ParentInput extends StatelessWidget { @override Widget build(BuildContext context) { final state1 = context.watch().state; return BlocBuilder( buildWhen: (previous, current) => previous.request.parentId != current.request.parentId, builder: (context, state) { return SmartSelect.single ( title: '父级标签', selectedValue: state.request.parentId ?? '', onChange: (selected) { context.read().add(TagFormParentChanged(selected.value ?? '')); }, choiceItems: state1 is TagEnableStateLoadSuccess ? modelToChoice(state1.tags) : [], choiceType: S2ChoiceType.chips, modalFilter: true, modalFilterAuto: true, tileBuilder: (context, state) { return S2Tile.fromState( state, isLoading: state1 is TagEnableStateLoadInProgress, padding: EdgeInsets.zero, ); } ); } ); } } ================================================ FILE: bookkeeping_user_flutter/lib/tags/ui/widgets/tag_form/transferable_input.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '/tags/tags.dart'; import '/commons/commons.dart'; class TransferableSwitch extends StatelessWidget { @override Widget build(BuildContext context) { return buildFormItem( "是否可转账", BlocSelector( selector: (state) => state.request.transferable, builder: (context, state) { return Align( alignment: Alignment.centerLeft, child: Switch( value: state ?? true, onChanged: (value) => context.read().add(TagFormTransferableChanged(value)), ), ); } ), context); } } ================================================ FILE: bookkeeping_user_flutter/lib/themes.dart ================================================ import 'package:flutter/material.dart'; final ThemeData lightTheme = _buildLightTheme(); final ThemeData darkTheme = _buildDarkTheme(); ThemeData _buildLightTheme() { final base = ThemeData.light(); return base.copyWith( scaffoldBackgroundColor: Colors.white, chipTheme: ChipThemeData.fromDefaults( secondaryColor: Colors.red, brightness: Brightness.light, labelStyle: TextStyle(fontSize: 16), ) ); } ThemeData _buildDarkTheme() { final base = ThemeData.dark(); return base.copyWith( ); } ================================================ FILE: bookkeeping_user_flutter/linux/.gitignore ================================================ flutter/ephemeral ================================================ FILE: bookkeeping_user_flutter/linux/CMakeLists.txt ================================================ # Project-level configuration. cmake_minimum_required(VERSION 3.10) project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. set(BINARY_NAME "bookkeeping_user_flutter") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID set(APPLICATION_ID "com.jiukuaitech.bookkeeping_user_flutter") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(SET CMP0063 NEW) # Load bundled libraries from the lib/ directory relative to the binary. set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") # Root filesystem for cross-building. if(FLUTTER_TARGET_PLATFORM_SYSROOT) set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) endif() # Define build configuration options. if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() # Compilation settings that should be applied to most targets. # # Be cautious about adding new options here, as plugins use this function by # default. In most cases, you should add new options to specific targets instead # of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_14) target_compile_options(${TARGET} PRIVATE -Wall -Werror) target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") endfunction() # Flutter library and tool build rules. set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") # Define the application target. To change its name, change BINARY_NAME above, # not the value here, or `flutter run` will no longer work. # # Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} "main.cc" "my_application.cc" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" ) # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) # Add dependency libraries. Add any application-specific dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) # Only the install-generated bundle's copy of the executable will launch # correctly, since the resources must in the right relative locations. To avoid # people trying to run the unbundled copy, put it in a subdirectory instead of # the default top-level location. set_target_properties(${BINARY_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" ) # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # By default, "installing" just makes a relocatable bundle in the build # directory. set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() # Start with a clean build bundle directory every time. install(CODE " file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") " COMPONENT Runtime) set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) install(FILES "${bundled_library}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endforeach(bundled_library) # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() ================================================ FILE: bookkeeping_user_flutter/linux/flutter/CMakeLists.txt ================================================ # This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.10) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. # Serves the same purpose as list(TRANSFORM ... PREPEND ...), # which isn't available in 3.10. function(list_prepend LIST_NAME PREFIX) set(NEW_LIST "") foreach(element ${${LIST_NAME}}) list(APPEND NEW_LIST "${PREFIX}${element}") endforeach(element) set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) endfunction() # === Flutter Library === # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "fl_basic_message_channel.h" "fl_binary_codec.h" "fl_binary_messenger.h" "fl_dart_project.h" "fl_engine.h" "fl_json_message_codec.h" "fl_json_method_codec.h" "fl_message_codec.h" "fl_method_call.h" "fl_method_channel.h" "fl_method_codec.h" "fl_method_response.h" "fl_plugin_registrar.h" "fl_plugin_registry.h" "fl_standard_message_codec.h" "fl_standard_method_codec.h" "fl_string_codec.h" "fl_value.h" "fl_view.h" "flutter_linux.h" ) list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") target_link_libraries(flutter INTERFACE PkgConfig::GTK PkgConfig::GLIB PkgConfig::GIO ) add_dependencies(flutter flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/_phony_ COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ) ================================================ FILE: bookkeeping_user_flutter/linux/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" void fl_register_plugins(FlPluginRegistry* registry) { } ================================================ FILE: bookkeeping_user_flutter/linux/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void fl_register_plugins(FlPluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: bookkeeping_user_flutter/linux/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: bookkeeping_user_flutter/linux/main.cc ================================================ #include "my_application.h" int main(int argc, char** argv) { g_autoptr(MyApplication) app = my_application_new(); return g_application_run(G_APPLICATION(app), argc, argv); } ================================================ FILE: bookkeeping_user_flutter/linux/my_application.cc ================================================ #include "my_application.h" #include #ifdef GDK_WINDOWING_X11 #include #endif #include "flutter/generated_plugin_registrant.h" struct _MyApplication { GtkApplication parent_instance; char** dart_entrypoint_arguments; }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); // Use a header bar when running in GNOME as this is the common style used // by applications and is the setup most users will be using (e.g. Ubuntu // desktop). // If running on X and not using GNOME then just use a traditional title bar // in case the window manager does more exotic layout, e.g. tiling. // If running on Wayland assume the header bar will work (may need changing // if future cases occur). gboolean use_header_bar = TRUE; #ifdef GDK_WINDOWING_X11 GdkScreen* screen = gtk_window_get_screen(window); if (GDK_IS_X11_SCREEN(screen)) { const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); if (g_strcmp0(wm_name, "GNOME Shell") != 0) { use_header_bar = FALSE; } } #endif if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); gtk_header_bar_set_title(header_bar, "bookkeeping_user_flutter"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { gtk_window_set_title(window, "bookkeeping_user_flutter"); } gtk_window_set_default_size(window, 1280, 720); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); FlView* view = fl_view_new(project); gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); gtk_widget_grab_focus(GTK_WIDGET(view)); } // Implements GApplication::local_command_line. static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { MyApplication* self = MY_APPLICATION(application); // Strip out the first argument as it is the binary name. self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); g_autoptr(GError) error = nullptr; if (!g_application_register(application, nullptr, &error)) { g_warning("Failed to register: %s", error->message); *exit_status = 1; return TRUE; } g_application_activate(application); *exit_status = 0; return TRUE; } // Implements GObject::dispose. static void my_application_dispose(GObject* object) { MyApplication* self = MY_APPLICATION(object); g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } static void my_application_class_init(MyApplicationClass* klass) { G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } static void my_application_init(MyApplication* self) {} MyApplication* my_application_new() { return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, "flags", G_APPLICATION_NON_UNIQUE, nullptr)); } ================================================ FILE: bookkeeping_user_flutter/linux/my_application.h ================================================ #ifndef FLUTTER_MY_APPLICATION_H_ #define FLUTTER_MY_APPLICATION_H_ #include G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, GtkApplication) /** * my_application_new: * * Creates a new Flutter-based application. * * Returns: a new #MyApplication. */ MyApplication* my_application_new(); #endif // FLUTTER_MY_APPLICATION_H_ ================================================ FILE: bookkeeping_user_flutter/macos/.gitignore ================================================ # Flutter-related **/Flutter/ephemeral/ **/Pods/ # Xcode-related **/dgph **/xcuserdata/ ================================================ FILE: bookkeeping_user_flutter/macos/Flutter/Flutter-Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: bookkeeping_user_flutter/macos/Flutter/Flutter-Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: bookkeeping_user_flutter/macos/Flutter/GeneratedPluginRegistrant.swift ================================================ // // Generated file. Do not edit. // import FlutterMacOS import Foundation import shared_preferences_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } ================================================ FILE: bookkeeping_user_flutter/macos/Podfile ================================================ platform :osx, '10.11' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_macos_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) end end ================================================ FILE: bookkeeping_user_flutter/macos/Runner/AppDelegate.swift ================================================ import Cocoa import FlutterMacOS @NSApplicationMain class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } } ================================================ FILE: bookkeeping_user_flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "16x16", "idiom" : "mac", "filename" : "app_icon_16.png", "scale" : "1x" }, { "size" : "16x16", "idiom" : "mac", "filename" : "app_icon_32.png", "scale" : "2x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "app_icon_32.png", "scale" : "1x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "app_icon_64.png", "scale" : "2x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "app_icon_128.png", "scale" : "1x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "app_icon_256.png", "scale" : "2x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "app_icon_256.png", "scale" : "1x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "app_icon_512.png", "scale" : "2x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "app_icon_512.png", "scale" : "1x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "app_icon_1024.png", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: bookkeeping_user_flutter/macos/Runner/Base.lproj/MainMenu.xib ================================================ ================================================ FILE: bookkeeping_user_flutter/macos/Runner/Configs/AppInfo.xcconfig ================================================ // Application-level settings for the Runner target. // // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the // future. If not, the values below would default to using the project name when this becomes a // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. PRODUCT_NAME = bookkeeping_user_flutter // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = com.jiukuaitech.bookkeepingUserFlutter // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2022 com.jiukuaitech. All rights reserved. ================================================ FILE: bookkeeping_user_flutter/macos/Runner/Configs/Debug.xcconfig ================================================ #include "../../Flutter/Flutter-Debug.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: bookkeeping_user_flutter/macos/Runner/Configs/Release.xcconfig ================================================ #include "../../Flutter/Flutter-Release.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: bookkeeping_user_flutter/macos/Runner/Configs/Warnings.xcconfig ================================================ WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings GCC_WARN_UNDECLARED_SELECTOR = YES CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE CLANG_WARN__DUPLICATE_METHOD_MATCH = YES CLANG_WARN_PRAGMA_PACK = YES CLANG_WARN_STRICT_PROTOTYPES = YES CLANG_WARN_COMMA = YES GCC_WARN_STRICT_SELECTOR_MATCH = YES CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES GCC_WARN_SHADOW = YES CLANG_WARN_UNREACHABLE_CODE = YES ================================================ FILE: bookkeeping_user_flutter/macos/Runner/DebugProfile.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.cs.allow-jit com.apple.security.network.server ================================================ FILE: bookkeeping_user_flutter/macos/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu NSPrincipalClass NSApplication ================================================ FILE: bookkeeping_user_flutter/macos/Runner/MainFlutterWindow.swift ================================================ import Cocoa import FlutterMacOS class MainFlutterWindow: NSWindow { override func awakeFromNib() { let flutterViewController = FlutterViewController.init() let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) RegisterGeneratedPlugins(registry: flutterViewController) super.awakeFromNib() } } ================================================ FILE: bookkeeping_user_flutter/macos/Runner/Release.entitlements ================================================ com.apple.security.app-sandbox ================================================ FILE: bookkeeping_user_flutter/macos/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 51; objects = { /* Begin PBXAggregateTarget section */ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { isa = PBXAggregateTarget; buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; buildPhases = ( 33CC111E2044C6BF0003C045 /* ShellScript */, ); dependencies = ( ); name = "Flutter Assemble"; productName = FLX; }; /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ 27FF91265827600A1F59F5C9 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4019E5EB853DB35F883A3375 /* Pods_Runner.framework */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; proxyType = 1; remoteGlobalIDString = 33CC111A2044C6BA0003C045; remoteInfo = FLX; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 33CC110E2044A8840003C045 /* Bundle Framework */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* bookkeeping_user_flutter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bookkeeping_user_flutter.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 4019E5EB853DB35F883A3375 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; E43660232EF8CA53192A9F75 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; EDB9382B6BEDC46ABCFCD6F2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; F28E0351EDEB7C94CB824AC0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 33CC10EA2044A3C60003C045 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 27FF91265827600A1F59F5C9 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 33BA886A226E78AF003329D5 /* Configs */ = { isa = PBXGroup; children = ( 33E5194F232828860026EE4D /* AppInfo.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, ); path = Configs; sourceTree = ""; }; 33CC10E42044A3C60003C045 = { isa = PBXGroup; children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, 6013DB3183603D80E6B01304 /* Pods */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( 33CC10ED2044A3C60003C045 /* bookkeeping_user_flutter.app */, ); name = Products; sourceTree = ""; }; 33CC11242044D66E0003C045 /* Resources */ = { isa = PBXGroup; children = ( 33CC10F22044A3C60003C045 /* Assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, 33CC10F72044A3C60003C045 /* Info.plist */, ); name = Resources; path = ..; sourceTree = ""; }; 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, ); path = Flutter; sourceTree = ""; }; 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */, 33CC11242044D66E0003C045 /* Resources */, 33BA886A226E78AF003329D5 /* Configs */, ); path = Runner; sourceTree = ""; }; 6013DB3183603D80E6B01304 /* Pods */ = { isa = PBXGroup; children = ( E43660232EF8CA53192A9F75 /* Pods-Runner.debug.xcconfig */, EDB9382B6BEDC46ABCFCD6F2 /* Pods-Runner.release.xcconfig */, F28E0351EDEB7C94CB824AC0 /* Pods-Runner.profile.xcconfig */, ); name = Pods; path = Pods; sourceTree = ""; }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( 4019E5EB853DB35F883A3375 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 33CC10EC2044A3C60003C045 /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( A4CD32F08EE403CC072F2229 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, 0AF52EB48178C9F30463C578 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* bookkeeping_user_flutter.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { enabled = 1; }; }; }; 33CC111A2044C6BA0003C045 = { CreatedOnToolsVersion = 9.2; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 33CC10E42044A3C60003C045; productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 33CC10EC2044A3C60003C045 /* Runner */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 33CC10EB2044A3C60003C045 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 0AF52EB48178C9F30463C578 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( Flutter/ephemeral/FlutterInputs.xcfilelist, ); inputPaths = ( Flutter/ephemeral/tripwire, ); outputFileListPaths = ( Flutter/ephemeral/FlutterOutputs.xcfilelist, ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; A4CD32F08EE403CC072F2229 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 33CC10E92044A3C60003C045 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( 33CC10F52044A3C60003C045 /* Base */, ); name = MainMenu.xib; path = Runner; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 338D0CE9231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Profile; }; 338D0CEA231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Profile; }; 338D0CEB231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Profile; }; 33CC10F92044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 33CC10FA2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; 33CC10FC2044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; 33CC10FD2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Release; }; 33CC111C2044C6BA0003C045 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 33CC111D2044C6BA0003C045 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10F92044A3C60003C045 /* Debug */, 33CC10FA2044A3C60003C045 /* Release */, 338D0CE9231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10FC2044A3C60003C045 /* Debug */, 33CC10FD2044A3C60003C045 /* Release */, 338D0CEA231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC111C2044C6BA0003C045 /* Debug */, 33CC111D2044C6BA0003C045 /* Release */, 338D0CEB231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } ================================================ FILE: bookkeeping_user_flutter/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: bookkeeping_user_flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: bookkeeping_user_flutter/macos/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: bookkeeping_user_flutter/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: bookkeeping_user_flutter/pubspec.yaml ================================================ name: bookkeeping_user_flutter description: A new Flutter project. publish_to: 'none' version: 1.0.0+1 environment: sdk: ">=2.14.4 <3.0.0" dependencies: flutter: sdk: flutter intl: ^0.17.0 #日期格式化 dio: ^4.0.6 formz: ^0.4.1 shared_preferences: ^2.0.15 equatable: ^2.0.5 flutter_bloc: ^8.1.1 json_annotation: ^4.7.0 awesome_select: ^6.0.0 pull_to_refresh: ^2.0.0 fluttertoast: ^8.1.1 syncfusion_flutter_charts: ^20.3.52 syncfusion_flutter_datepicker: ^20.3.52 image_picker: ^0.8.6 flutter_localizations: sdk: flutter webview_flutter: ^3.0.4 dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.3.2 json_serializable: ^6.5.4 flutter: uses-material-design: true assets: - assets/images/ generate: false ================================================ FILE: bookkeeping_user_flutter/test/widget_test.dart ================================================ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility in the flutter_test package. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:bookkeeping_user_flutter/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. // await tester.pumpWidget(const MyApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); // Tap the '+' icon and trigger a frame. await tester.tap(find.byIcon(Icons.add)); await tester.pump(); // Verify that our counter has incremented. expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); }); } ================================================ FILE: bookkeeping_user_flutter/windows/.gitignore ================================================ flutter/ephemeral/ # Visual Studio user-specific files. *.suo *.user *.userosscache *.sln.docstates # Visual Studio build-related files. x64/ x86/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ ================================================ FILE: bookkeeping_user_flutter/windows/CMakeLists.txt ================================================ # Project-level configuration. cmake_minimum_required(VERSION 3.14) project(bookkeeping_user_flutter LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. set(BINARY_NAME "bookkeeping_user_flutter") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(SET CMP0063 NEW) # Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" CACHE STRING "" FORCE) else() if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() endif() # Define settings for the Profile build mode. set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") # Use Unicode for all projects. add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. # # Be cautious about adding new options here, as plugins use this function by # default. In most cases, you should add new options to specific targets instead # of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() # Flutter library and tool build rules. set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build; see runner/CMakeLists.txt. add_subdirectory("runner") # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # Support files are copied into place next to the executable, so that it can # run in place. This is done instead of making a separate bundle (as on Linux) # so that building and running from within Visual Studio will work. set(BUILD_BUNDLE_DIR "$") # Make the "install" step default, as it's required to run. set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) if(PLUGIN_BUNDLED_LIBRARIES) install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) ================================================ FILE: bookkeeping_user_flutter/windows/flutter/CMakeLists.txt ================================================ # This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "flutter_export.h" "flutter_windows.h" "flutter_messenger.h" "flutter_plugin_registrar.h" "flutter_texture_registrar.h" ) list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") add_dependencies(flutter flutter_assemble) # === Wrapper === list(APPEND CPP_WRAPPER_SOURCES_CORE "core_implementations.cc" "standard_codec.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_PLUGIN "plugin_registrar.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_APP "flutter_engine.cc" "flutter_view_controller.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") # Wrapper sources needed for a plugin. add_library(flutter_wrapper_plugin STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ) apply_standard_settings(flutter_wrapper_plugin) set_target_properties(flutter_wrapper_plugin PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(flutter_wrapper_plugin PROPERTIES CXX_VISIBILITY_PRESET hidden) target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) target_include_directories(flutter_wrapper_plugin PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_plugin flutter_assemble) # Wrapper sources needed for the runner. add_library(flutter_wrapper_app STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_APP} ) apply_standard_settings(flutter_wrapper_app) target_link_libraries(flutter_wrapper_app PUBLIC flutter) target_include_directories(flutter_wrapper_app PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_app flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ${PHONY_OUTPUT} COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" windows-x64 $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ) ================================================ FILE: bookkeeping_user_flutter/windows/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" void RegisterPlugins(flutter::PluginRegistry* registry) { } ================================================ FILE: bookkeeping_user_flutter/windows/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void RegisterPlugins(flutter::PluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: bookkeeping_user_flutter/windows/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: bookkeeping_user_flutter/windows/runner/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.14) project(runner LANGUAGES CXX) # Define the application target. To change its name, change BINARY_NAME in the # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer # work. # # Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" "utils.cpp" "win32_window.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" ) # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) # Disable Windows macros that collide with C++ standard library functions. target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") # Add dependency libraries and include directories. Add any application-specific # dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) ================================================ FILE: bookkeeping_user_flutter/windows/runner/Runner.rc ================================================ // Microsoft Visual C++ generated resource script. // #pragma code_page(65001) #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (United States) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APP_ICON ICON "resources\\app_icon.ico" ///////////////////////////////////////////////////////////////////////////// // // Version // #ifdef FLUTTER_BUILD_NUMBER #define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER #else #define VERSION_AS_NUMBER 1,0,0 #endif #ifdef FLUTTER_BUILD_NAME #define VERSION_AS_STRING #FLUTTER_BUILD_NAME #else #define VERSION_AS_STRING "1.0.0" #endif VS_VERSION_INFO VERSIONINFO FILEVERSION VERSION_AS_NUMBER PRODUCTVERSION VERSION_AS_NUMBER FILEFLAGSMASK VS_FFI_FILEFLAGSMASK #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG #else FILEFLAGS 0x0L #endif FILEOS VOS__WINDOWS32 FILETYPE VFT_APP FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.jiukuaitech" "\0" VALUE "FileDescription", "bookkeeping_user_flutter" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "bookkeeping_user_flutter" "\0" VALUE "LegalCopyright", "Copyright (C) 2022 com.jiukuaitech. All rights reserved." "\0" VALUE "OriginalFilename", "bookkeeping_user_flutter.exe" "\0" VALUE "ProductName", "bookkeeping_user_flutter" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1252 END END #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED ================================================ FILE: bookkeeping_user_flutter/windows/runner/flutter_window.cpp ================================================ #include "flutter_window.h" #include #include "flutter/generated_plugin_registrant.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} FlutterWindow::~FlutterWindow() {} bool FlutterWindow::OnCreate() { if (!Win32Window::OnCreate()) { return false; } RECT frame = GetClientArea(); // The size here must match the window dimensions to avoid unnecessary surface // creation / destruction in the startup path. flutter_controller_ = std::make_unique( frame.right - frame.left, frame.bottom - frame.top, project_); // Ensure that basic setup of the controller was successful. if (!flutter_controller_->engine() || !flutter_controller_->view()) { return false; } RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); return true; } void FlutterWindow::OnDestroy() { if (flutter_controller_) { flutter_controller_ = nullptr; } Win32Window::OnDestroy(); } LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { // Give Flutter, including plugins, an opportunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, lparam); if (result) { return *result; } } switch (message) { case WM_FONTCHANGE: flutter_controller_->engine()->ReloadSystemFonts(); break; } return Win32Window::MessageHandler(hwnd, message, wparam, lparam); } ================================================ FILE: bookkeeping_user_flutter/windows/runner/flutter_window.h ================================================ #ifndef RUNNER_FLUTTER_WINDOW_H_ #define RUNNER_FLUTTER_WINDOW_H_ #include #include #include #include "win32_window.h" // A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: // Creates a new FlutterWindow hosting a Flutter view running |project|. explicit FlutterWindow(const flutter::DartProject& project); virtual ~FlutterWindow(); protected: // Win32Window: bool OnCreate() override; void OnDestroy() override; LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; private: // The project to run. flutter::DartProject project_; // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; }; #endif // RUNNER_FLUTTER_WINDOW_H_ ================================================ FILE: bookkeeping_user_flutter/windows/runner/main.cpp ================================================ #include #include #include #include "flutter_window.h" #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { CreateAndAttachConsole(); } // Initialize COM, so that it is available for use in the library and/or // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); flutter::DartProject project(L"data"); std::vector command_line_arguments = GetCommandLineArguments(); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); if (!window.CreateAndShow(L"bookkeeping_user_flutter", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); ::MSG msg; while (::GetMessage(&msg, nullptr, 0, 0)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } ::CoUninitialize(); return EXIT_SUCCESS; } ================================================ FILE: bookkeeping_user_flutter/windows/runner/resource.h ================================================ //{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by Runner.rc // #define IDI_APP_ICON 101 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif ================================================ FILE: bookkeeping_user_flutter/windows/runner/runner.exe.manifest ================================================ PerMonitorV2 ================================================ FILE: bookkeeping_user_flutter/windows/runner/utils.cpp ================================================ #include "utils.h" #include #include #include #include #include void CreateAndAttachConsole() { if (::AllocConsole()) { FILE *unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } if (freopen_s(&unused, "CONOUT$", "w", stderr)) { _dup2(_fileno(stdout), 2); } std::ios::sync_with_stdio(); FlutterDesktopResyncOutputStreams(); } } std::vector GetCommandLineArguments() { // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. int argc; wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); if (argv == nullptr) { return std::vector(); } std::vector command_line_arguments; // Skip the first argument as it's the binary name. for (int i = 1; i < argc; i++) { command_line_arguments.push_back(Utf8FromUtf16(argv[i])); } ::LocalFree(argv); return command_line_arguments; } std::string Utf8FromUtf16(const wchar_t* utf16_string) { if (utf16_string == nullptr) { return std::string(); } int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, nullptr, 0, nullptr, nullptr); std::string utf8_string; if (target_length == 0 || target_length > utf8_string.max_size()) { return utf8_string; } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } return utf8_string; } ================================================ FILE: bookkeeping_user_flutter/windows/runner/utils.h ================================================ #ifndef RUNNER_UTILS_H_ #define RUNNER_UTILS_H_ #include #include // Creates a console for the process, and redirects stdout and stderr to // it for both the runner and the Flutter library. void CreateAndAttachConsole(); // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string // encoded in UTF-8. Returns an empty std::string on failure. std::string Utf8FromUtf16(const wchar_t* utf16_string); // Gets the command line arguments passed in as a std::vector, // encoded in UTF-8. Returns an empty std::vector on failure. std::vector GetCommandLineArguments(); #endif // RUNNER_UTILS_H_ ================================================ FILE: bookkeeping_user_flutter/windows/runner/win32_window.cpp ================================================ #include "win32_window.h" #include #include "resource.h" namespace { constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); // Scale helper to convert logical scaler values to physical using passed in // scale factor int Scale(int source, double scale_factor) { return static_cast(source * scale_factor); } // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. // This API is only needed for PerMonitor V1 awareness mode. void EnableFullDpiSupportIfAvailable(HWND hwnd) { HMODULE user32_module = LoadLibraryA("User32.dll"); if (!user32_module) { return; } auto enable_non_client_dpi_scaling = reinterpret_cast( GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); FreeLibrary(user32_module); } } } // namespace // Manages the Win32Window's window class registration. class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; // Returns the singleton registar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); } return instance_; } // Returns the name of the window class, registering the class if it hasn't // previously been registered. const wchar_t* GetWindowClass(); // Unregisters the window class. Should only be called if there are no // instances of the window. void UnregisterWindowClass(); private: WindowClassRegistrar() = default; static WindowClassRegistrar* instance_; bool class_registered_ = false; }; WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; const wchar_t* WindowClassRegistrar::GetWindowClass() { if (!class_registered_) { WNDCLASS window_class{}; window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); window_class.lpszClassName = kWindowClassName; window_class.style = CS_HREDRAW | CS_VREDRAW; window_class.cbClsExtra = 0; window_class.cbWndExtra = 0; window_class.hInstance = GetModuleHandle(nullptr); window_class.hIcon = LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); window_class.hbrBackground = 0; window_class.lpszMenuName = nullptr; window_class.lpfnWndProc = Win32Window::WndProc; RegisterClass(&window_class); class_registered_ = true; } return kWindowClassName; } void WindowClassRegistrar::UnregisterWindowClass() { UnregisterClass(kWindowClassName, nullptr); class_registered_ = false; } Win32Window::Win32Window() { ++g_active_window_count; } Win32Window::~Win32Window() { --g_active_window_count; Destroy(); } bool Win32Window::CreateAndShow(const std::wstring& title, const Point& origin, const Size& size) { Destroy(); const wchar_t* window_class = WindowClassRegistrar::GetInstance()->GetWindowClass(); const POINT target_point = {static_cast(origin.x), static_cast(origin.y)}; HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); double scale_factor = dpi / 96.0; HWND window = CreateWindow( window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); if (!window) { return false; } return OnCreate(); } // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { if (message == WM_NCCREATE) { auto window_struct = reinterpret_cast(lparam); SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(window_struct->lpCreateParams)); auto that = static_cast(window_struct->lpCreateParams); EnableFullDpiSupportIfAvailable(window); that->window_handle_ = window; } else if (Win32Window* that = GetThisFromHandle(window)) { return that->MessageHandler(window, message, wparam, lparam); } return DefWindowProc(window, message, wparam, lparam); } LRESULT Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { switch (message) { case WM_DESTROY: window_handle_ = nullptr; Destroy(); if (quit_on_close_) { PostQuitMessage(0); } return 0; case WM_DPICHANGED: { auto newRectSize = reinterpret_cast(lparam); LONG newWidth = newRectSize->right - newRectSize->left; LONG newHeight = newRectSize->bottom - newRectSize->top; SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, newHeight, SWP_NOZORDER | SWP_NOACTIVATE); return 0; } case WM_SIZE: { RECT rect = GetClientArea(); if (child_content_ != nullptr) { // Size and position the child window. MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE); } return 0; } case WM_ACTIVATE: if (child_content_ != nullptr) { SetFocus(child_content_); } return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); } void Win32Window::Destroy() { OnDestroy(); if (window_handle_) { DestroyWindow(window_handle_); window_handle_ = nullptr; } if (g_active_window_count == 0) { WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); } } Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { return reinterpret_cast( GetWindowLongPtr(window, GWLP_USERDATA)); } void Win32Window::SetChildContent(HWND content) { child_content_ = content; SetParent(content, window_handle_); RECT frame = GetClientArea(); MoveWindow(content, frame.left, frame.top, frame.right - frame.left, frame.bottom - frame.top, true); SetFocus(child_content_); } RECT Win32Window::GetClientArea() { RECT frame; GetClientRect(window_handle_, &frame); return frame; } HWND Win32Window::GetHandle() { return window_handle_; } void Win32Window::SetQuitOnClose(bool quit_on_close) { quit_on_close_ = quit_on_close; } bool Win32Window::OnCreate() { // No-op; provided for subclasses. return true; } void Win32Window::OnDestroy() { // No-op; provided for subclasses. } ================================================ FILE: bookkeeping_user_flutter/windows/runner/win32_window.h ================================================ #ifndef RUNNER_WIN32_WINDOW_H_ #define RUNNER_WIN32_WINDOW_H_ #include #include #include #include // A class abstraction for a high DPI-aware Win32 Window. Intended to be // inherited from by classes that wish to specialize with custom // rendering and input handling class Win32Window { public: struct Point { unsigned int x; unsigned int y; Point(unsigned int x, unsigned int y) : x(x), y(y) {} }; struct Size { unsigned int width; unsigned int height; Size(unsigned int width, unsigned int height) : width(width), height(height) {} }; Win32Window(); virtual ~Win32Window(); // Creates and shows a win32 window with |title| and position and size using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a // consistent size to will treat the width height passed in to this function // as logical pixels and scale to appropriate for the default monitor. Returns // true if the window was created successfully. bool CreateAndShow(const std::wstring& title, const Point& origin, const Size& size); // Release OS resources associated with window. void Destroy(); // Inserts |content| into the window tree. void SetChildContent(HWND content); // Returns the backing Window handle to enable clients to set icon and other // window properties. Returns nullptr if the window has been destroyed. HWND GetHandle(); // If true, closing this window will quit the application. void SetQuitOnClose(bool quit_on_close); // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that // inheriting classes can handle. virtual LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Called when CreateAndShow is called, allowing subclass window-related // setup. Subclasses should return false if setup fails. virtual bool OnCreate(); // Called when Destroy is called. virtual void OnDestroy(); private: friend class WindowClassRegistrar; // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically // responsponds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Retrieves a class instance pointer for |window| static Win32Window* GetThisFromHandle(HWND const window) noexcept; bool quit_on_close_ = false; // window handle for top level window. HWND window_handle_ = nullptr; // window handle for hosted content. HWND child_content_ = nullptr; }; #endif // RUNNER_WIN32_WINDOW_H_ ================================================ FILE: bookkeeping_user_uniapp/App.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/components/accounts/accounts.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/components/expense-form/expense-form.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/components/flows/flows.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/components/income-form/income-form.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/components/tab-bar/tab-bar.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/components/transfer-form/transfer-form.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/config/api.js ================================================ const http = uni.$u.http export const signin = (params) => http.post('signin', params) export const getSession = () => http.get('session') export const queryFlows = (params) => http.get('flows', { params: params }) export const getFlowById = (id) => http.get('flows/' + id); export const deleteFlowById = (id) => http.delete('flows/' + id) export const confirmFlow = (id) => http.put('flows/' + id + '/confirm') export const saveExpense = (params) => http.post('expenses', params) export const updateExpense = (id, params) => http.put('expenses/' + id, params) export const refundExpense = (id, params) => http.post('expenses/' + id + '/refund' , params) export const saveIncome = (params) => http.post('incomes', params) export const updateIncome = (id, params) => http.put('incomes/' + id, params) export const refundIncome = (id, params) => http.post('incomes/' + id + '/refund' , params) export const saveTransfer = (params) => http.post('transfers', params) export const updateTransfer = (id, params) => http.put('transfers/' + id, params) export const getEnableAccounts = () => http.get('accounts/enable') export const getExpenseableAccounts = () => http.get('accounts/expenseable') export const getIncomeableAccounts = () => http.get('accounts/incomeable') export const getTransferFromAbleAccounts = () => http.get('accounts/transfer-from-able') export const getTransferToAbleAccounts = () => http.get('accounts/transfer-to-able') export const getExpenseCategories = () => http.get('expense-categories/enable'); export const getIncomeCategories = () => http.get('income-categories/enable'); export const getExpenseablePayees = () => http.get('payees/expenseable') export const getIncomeablePayees = () => http.get('payees/incomeable') export const getEnablePayees = () => http.get('payees/enable') export const getExpenseableTags = () => http.get('tags/expenseable') export const getIncomeableTags = () => http.get('tags/incomeable') export const getTransferableTags = () => http.get('tags/transferable') export const getEnableTags = () => http.get('tags/enable') export const queryAccounts = (type, params) => { if (type === 1) { return http.get('checking-accounts', { params: params }); } else if (type === 2) { return http.get('credit-accounts', { params: params }); } else if (type === 3) { return http.get('debt-accounts', { params: params }); } else if (type === 4) { return http.get('asset-accounts', { params: params }); } } export const getAccountById = (id) => http.get('accounts/' + id); export const deleteAccountById = (id) => http.delete('accounts/' + id); export const queryBooks = (params) => http.get('books', { params: params }); export const setDefaultBook = (id) => http.put('setDefaultBook/' + id); export const reportExpenseCategory = (params) => http.get('reports/expense-category', { params: params }); export const reportIncomeCategory = (params) => http.get('reports/income-category', { params: params }); export const reportDebt = (params) => http.get('reports/debt', { params: params }); export const reportAsset = (params) => http.get('reports/asset', { params: params }); ================================================ FILE: bookkeeping_user_uniapp/config/request.js ================================================ // 此vm参数为页面的实例,可以通过它引用vuex中的变量 module.exports = (vm) => { // 初始化请求配置 uni.$u.http.setConfig((config) => { /* config 为默认全局配置*/ config.baseURL = 'https://testjz.jiukuaitech.com/api/v1/'; /* 根域名 */ // config.baseURL = '/api/v1/'; /* 根域名 */ return config; }) // 请求拦截 uni.$u.http.interceptors.request.use((config) => { // 可使用async await 做异步操作 // 初始化请求拦截器时,会执行此方法,此时data为undefined,赋予默认{} config.data = config.data || {} // 根据custom参数中配置的是否需要token,添加对应的请求头 // if(config?.custom?.auth) { // // 可以在此通过vm引用vuex中的变量,具体值在vm.$store.state中 // config.header.token = vm.$store.state.userInfo.token // } config.header['user-token'] = vm.$store.getters.userToken; return config }, config => { // 可使用async await 做异步操作 return Promise.reject(config) }) // 响应拦截 uni.$u.http.interceptors.response.use((response) => { /* 对响应成功做点什么 可使用async await 做异步操作*/ const data = response.data // 自定义参数 const custom = response.config?.custom if (response.statusCode !== 200) { // 如果没有显式定义custom的toast参数为false的话,默认对报错进行toast弹出提示 if (custom.toast !== false) { uni.$u.toast(data.message) } // 如果需要catch返回,则进行reject if (custom?.catch) { return Promise.reject(data) } else { // 否则返回一个pending中的promise,请求不会进入catch中 return new Promise(() => { }) } } else { if (data.success) { return data.data; } else { uni.$u.toast(data.errorMsg) return Promise.reject(data) } } return data.data === undefined ? {} : data.data }, (response) => { /* 对响应错误做点什么 (statusCode !== 200)*/ // 对响应错误做点什么 (statusCode !== 200 uni.$u.toast(response.errMsg); // uni.$u.toast('网络错误,稍后重试') return Promise.reject(response) }) } ================================================ FILE: bookkeeping_user_uniapp/index.html ================================================
================================================ FILE: bookkeeping_user_uniapp/main.js ================================================ import Vue from 'vue' import uView from '@/uni_modules/uview-ui' import App from './App' import store from './store' Vue.prototype.$store = store var EventBus = new Vue() Vue.prototype.$eventBus = EventBus Vue.config.productionTip = false App.mpType = 'app' Vue.use(uView) //uni.$u.config.unit = 'rpx' const app = new Vue({ ...App, store }) require('@/config/request.js')(app) app.$mount() ================================================ FILE: bookkeeping_user_uniapp/manifest.json ================================================ { "name" : "bookkeeping_user_uniapp", "appid" : "__UNI__ED9932E", "description" : "", "versionName" : "1.0.0", "versionCode" : "100", "transformPx" : false, /* 5+App特有相关 */ "app-plus" : { "usingComponents" : true, "nvueStyleCompiler" : "uni-app", "compilerVersion" : 3, "splashscreen" : { "alwaysShowBeforeRender" : true, "waiting" : true, "autoclose" : true, "delay" : 0 }, /* 模块配置 */ "modules" : {}, /* 应用发布信息 */ "distribute" : { /* android打包配置 */ "android" : { "permissions" : [ "", "", "", "", "", "", "", "", "", "", "", "", "", "", "" ] }, /* ios打包配置 */ "ios" : {}, /* SDK配置 */ "sdkConfigs" : {} } }, /* 快应用特有相关 */ "quickapp" : {}, /* 小程序特有相关 */ "mp-weixin" : { "appid" : "wxc0f1ef35c3bfcae4", "setting" : { "urlCheck" : false }, "usingComponents" : true }, "mp-alipay" : { "usingComponents" : true }, "mp-baidu" : { "usingComponents" : true }, "mp-toutiao" : { "usingComponents" : true }, "h5" : { "devServer" : { // "port" : 80, //端口 "disableHostCheck" : true, "proxy" : { //使用代理 "/api/v1" : { // "target": "http://47.115.83.135/api/v2", "target" : "https://testjz.jiukuaitech.com", "changeOrigin" : true, "pathRewrite" : {} } } }, //"^/api/v1" : "" "title" : "九快记账" }, "uniStatistics" : { "enable" : false }, "vueVersion" : "2" } ================================================ FILE: bookkeeping_user_uniapp/package.json ================================================ { "id": "uview-ui", "scripts": { "test": "eslint . --fix" }, "dependencies": {}, "devDependencies": { "eslint": "^8.2.0", "eslint-config-airbnb": "^19.0.0" } } ================================================ FILE: bookkeeping_user_uniapp/pages/account-detail/account-detail.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/pages/accounts/accounts.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/pages/books/books.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/pages/charts/charts.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/pages/flow-detail/flow-detail.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/pages/flow-form/flow-form.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/pages/flows/flows.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/pages/index/index.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/pages/login/login.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/pages/my/my.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/pages/select/select.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/pages/test/test.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/pages.json ================================================ { "easycom": { "autoscan": true, //是否开启自动扫描,开启后将会自动扫描符合components/组件名称/组件名称.vue目录结构的组件 "custom": { "^u-(.*)": "@/uni_modules/uview-ui/components/u-$1/u-$1.vue" } }, "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages { "path": "pages/index/index", "style": { "navigationBarTitleText": "", "navigationStyle": "custom" } }, { "path": "pages/login/login", "style": { "navigationBarTitleText": "登录", "titleAlign": "center", "enablePullDownRefresh": false } }, { "path": "pages/flow-form/flow-form", "style": { "navigationBarTitleText": "", "enablePullDownRefresh": false } }, { "path": "pages/select/select", "style": { "navigationBarTitleText": "", "enablePullDownRefresh": false } }, { "path": "pages/flow-detail/flow-detail", "style": { "navigationBarTitleText": "账单详情", "enablePullDownRefresh": false } }, { "path" : "pages/account-detail/account-detail", "style" : { "navigationBarTitleText": "账户详情", "enablePullDownRefresh": false } } ,{ "path" : "pages/accounts/accounts", "style" : { "navigationBarTitleText": "", "enablePullDownRefresh": true, "navigationStyle": "custom" } } ,{ "path" : "pages/flows/flows", "style" : { "navigationBarTitleText": "", "enablePullDownRefresh": true, "navigationStyle": "custom" } } ,{ "path" : "pages/charts/charts", "style" : { "navigationBarTitleText": "", "enablePullDownRefresh": false, "navigationStyle": "custom" } } ,{ "path" : "pages/my/my", "style" : { "navigationBarTitleText": "我的", "enablePullDownRefresh": false, "navigationStyle": "custom" } } ,{ "path" : "pages/books/books", "style" : { "navigationBarTitleText": "", "enablePullDownRefresh": false } } ], "globalStyle": { "navigationBarTextStyle": "black", "navigationBarTitleText": "uni-app", "navigationBarBackgroundColor": "#F8F8F8", "backgroundColor": "#F8F8F8" }, "uniIdRouter": {} } ================================================ FILE: bookkeeping_user_uniapp/store/index.js ================================================ import Vue from 'vue' import Vuex from 'vuex' import session from './modules/session' import account from './modules/account' import payee from './modules/payee' import tag from './modules/tag' import select from './modules/select' import expenseCategory from './modules/expenseCategory' import incomeCategory from './modules/incomeCategory' import modelForm from './modules/modelForm' Vue.use(Vuex) // vue的插件机制 export default new Vuex.Store({ modules: { session, account, select, expenseCategory, incomeCategory, payee, tag, modelForm } }) ================================================ FILE: bookkeeping_user_uniapp/store/modules/account.js ================================================ import { getExpenseableAccounts, getIncomeableAccounts, getTransferFromAbleAccounts, getTransferToAbleAccounts, getEnableAccounts } from '@/config/api.js'; export default { state: () => { return { expenseableAccounts: undefined, incomeableAccounts: undefined, transferFromAbleAccounts: undefined, transferToAbleAccounts: undefined, enableAccounts: undefined, } }, mutations: { setExpenseableAccounts(state, data) { state.expenseableAccounts = data; }, setIncomeableAccounts(state, data) { state.incomeableAccounts = data; }, setTransferFromAbleAccounts(state, data) { state.transferFromAbleAccounts = data; }, setTransferToAbleAccounts(state, data) { state.transferToAbleAccounts = data; }, setEnableAccounts(state, data) { state.enableAccounts = data; } }, actions: { async getExpenseableAccounts({commit}) { commit('setExpenseableAccounts', await getExpenseableAccounts()); }, async getIncomeableAccounts({commit}) { commit('setIncomeableAccounts', await getIncomeableAccounts()); }, async getTransferFromAbleAccounts({commit}) { commit('setTransferFromAbleAccounts', await getTransferFromAbleAccounts()); }, async getTransferToAbleAccounts({commit}) { commit('setTransferToAbleAccounts', await getTransferToAbleAccounts()); }, async getEnableAccounts({commit}) { commit('setEnableAccounts', await getEnableAccounts()); } }, getters: { expenseableAccounts (state) { return state.expenseableAccounts; }, incomeableAccounts (state) { return state.incomeableAccounts; }, transferFromAbleAccounts (state) { return state.transferFromAbleAccounts; }, transferToAbleAccounts (state) { return state.transferToAbleAccounts; }, enableAccounts (state) { return state.enableAccounts; } }, } ================================================ FILE: bookkeeping_user_uniapp/store/modules/expenseCategory.js ================================================ import { getExpenseCategories } from '@/config/api.js'; export default { state: () => { return { expenseCategories: undefined, } }, mutations: { setExpenseCategories(state, data) { state.expenseCategories = data; } }, actions: { async getExpenseCategories({commit}) { commit('setExpenseCategories', await getExpenseCategories()); } }, getters: { expenseCategories (state) { return state.expenseCategories; } }, } ================================================ FILE: bookkeeping_user_uniapp/store/modules/incomeCategory.js ================================================ import { getIncomeCategories } from '@/config/api.js'; export default { state: () => { return { incomeCategories: undefined, } }, mutations: { setIncomeCategories(state, data) { state.incomeCategories = data; } }, actions: { async getIncomeCategories({commit}) { commit('setIncomeCategories', await getIncomeCategories()); } }, getters: { incomeCategories (state) { return state.incomeCategories; } }, } ================================================ FILE: bookkeeping_user_uniapp/store/modules/modelForm.js ================================================ export default { namespaced: true, state: () => { return { type: 1, //1-新增,2-修改,3-复制,4-退款 model: undefined, } }, mutations: { setModel(state, model) { state.model = model; }, setType(state, type) { state.type = type; } }, getters: { model (state) { return state.model; }, type (state) { return state.type } }, } ================================================ FILE: bookkeeping_user_uniapp/store/modules/payee.js ================================================ import { getExpenseablePayees, getIncomeablePayees, getEnablePayees } from '@/config/api.js'; export default { state: () => { return { expenseablePayees: undefined, incomeablePayees: undefined, enablePayees: undefined, } }, mutations: { setExpenseablePayees(state, data) { state.expenseablePayees = data; }, setIncomeablePayees(state, data) { state.incomeablePayees = data; }, setEnablePayees(state, data) { state.enablePayees = data; } }, actions: { async getExpenseablePayees({commit}) { commit('setExpenseablePayees', await getExpenseablePayees()); }, async getIncomeablePayees({commit}) { commit('setIncomeablePayees', await getIncomeablePayees()); }, async getEnablePayees({commit}) { commit('setEnablePayees', await getEnablePayees()); } }, getters: { expenseablePayees (state) { return state.expenseablePayees; }, incomeablePayees (state) { return state.incomeablePayees; }, enablePayees (state) { return state.enablePayees; } }, } ================================================ FILE: bookkeeping_user_uniapp/store/modules/select.js ================================================ export default { state: () => { return { selectList: undefined, } }, mutations: { setSelectList(state, list) { state.selectList = list; } }, actions: { }, getters: { selectList (state) { return state.selectList; } }, } ================================================ FILE: bookkeeping_user_uniapp/store/modules/session.js ================================================ import { getSession } from '@/config/api.js'; export default { state: () => { return { userToken: undefined, user: undefined, defaultBook: undefined, defaultGroup: undefined, apiUrl: undefined, } }, getters: { userToken(state) { if (state.userToken) { return state.userToken; } else { const userToken = uni.getStorageSync('userToken'); state.userToken = userToken; return userToken; } }, user(state) { return state.user; }, defaultBook(state) { return state.defaultBook; }, defaultGroup(state) { return state.defaultGroup; }, defaultCurrencyCode(state) { return state.defaultGroup.defaultCurrencyCode }, apiUrl(state) { if (state.apiUrl) { return state.apiUrl; } else { const apiUrl = uni.getStorageSync('apiUrl'); state.apiUrl = apiUrl; return apiUrl; } }, }, actions: { async getSession({ commit }) { const data = await getSession(); commit('setUser', data.userSessionVO); commit('setDefaultBook', data.defaultBook); commit('setDefaultGroup', data.defaultGroup); } }, mutations: { updateToken: (state, userToken) => { state.userToken = userToken; uni.setStorageSync('userToken', userToken); }, setUser: (state, user) => { state.user = user; }, setDefaultBook: (state, book) => { state.defaultBook = book; }, setDefaultGroup: (state, group) => { state.defaultGroup = group; }, updateApiUrl: (state, apiUrl) => { state.apiUrl = apiUrl; uni.setStorageSync('apiUrl', apiUrl); }, } } ================================================ FILE: bookkeeping_user_uniapp/store/modules/tag.js ================================================ import { getExpenseableTags, getIncomeableTags, getTransferableTags, getEnableTags } from '@/config/api.js'; export default { state: () => { return { expenseableTags: undefined, incomeableTags: undefined, transferableTags: undefined, enableTags: undefined, } }, mutations: { setExpenseableTags(state, data) { state.expenseableTags = data; }, setIncomeableTags(state, data) { state.incomeableTags = data; }, setTransferableTags(state, data) { state.transferableTags = data; }, setEnableTags(state, data) { state.enableTags = data; }, }, actions: { async getExpenseableTags({commit}) { commit('setExpenseableTags', await getExpenseableTags()); }, async getIncomeableTags({commit}) { commit('setIncomeableTags', await getIncomeableTags()); }, async getTransferableTags({commit}) { commit('setTransferableTags', await getTransferableTags()); }, async getEnableTags({commit}) { commit('setEnableTags', await getEnableTags()); } }, getters: { expenseableTags (state) { return state.expenseableTags; }, incomeableTags (state) { return state.incomeableTags; }, transferableTags (state) { return state.transferableTags; }, enableTags (state) { return state.enableTags; } }, } ================================================ FILE: bookkeeping_user_uniapp/uni.scss ================================================ /** * 这里是uni-app内置的常用样式变量 * * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App * */ /** * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 * * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 */ @import '@/uni_modules/uview-ui/theme.scss'; /* 颜色变量 */ /* 行为相关颜色 */ $uni-color-primary: #007aff; $uni-color-success: #4cd964; $uni-color-warning: #f0ad4e; $uni-color-error: #dd524d; /* 文字基本颜色 */ $uni-text-color:#333;//基本色 $uni-text-color-inverse:#fff;//反色 $uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息 $uni-text-color-placeholder: #808080; $uni-text-color-disable:#c0c0c0; /* 背景颜色 */ $uni-bg-color:#ffffff; $uni-bg-color-grey:#f8f8f8; $uni-bg-color-hover:#f1f1f1;//点击状态颜色 $uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色 /* 边框颜色 */ $uni-border-color:#c8c7cc; /* 尺寸变量 */ /* 文字尺寸 */ $uni-font-size-sm:12px; $uni-font-size-base:14px; $uni-font-size-lg:16; /* 图片尺寸 */ $uni-img-size-sm:20px; $uni-img-size-base:26px; $uni-img-size-lg:40px; /* Border Radius */ $uni-border-radius-sm: 2px; $uni-border-radius-base: 3px; $uni-border-radius-lg: 6px; $uni-border-radius-circle: 50%; /* 水平间距 */ $uni-spacing-row-sm: 5px; $uni-spacing-row-base: 10px; $uni-spacing-row-lg: 15px; /* 垂直间距 */ $uni-spacing-col-sm: 4px; $uni-spacing-col-base: 8px; $uni-spacing-col-lg: 12px; /* 透明度 */ $uni-opacity-disabled: 0.3; // 组件禁用态的透明度 /* 文章场景相关 */ $uni-color-title: #2C405A; // 文章标题颜色 $uni-font-size-title:20px; $uni-color-subtitle: #555555; // 二级标题颜色 $uni-font-size-subtitle:26px; $uni-color-paragraph: #3F536E; // 文章段落颜色 $uni-font-size-paragraph:15px; ================================================ FILE: bookkeeping_user_uniapp/uni_modules/qiun-data-charts/changelog.md ================================================ ## 2.4.3-20220505(2022-05-05) - 秋云图表组件 修复开启canvas2d后将series赋值为空数组显示加载图标时,再次赋值后画布闪动的bug - 秋云图表组件 修复升级hbx最新版后ECharts的highlight方法报错的bug - uCharts.js 雷达图新增参数opts.extra.radar.gridEval,数据点位网格抽希,默认1 - uCharts.js 雷达图新增参数opts.extra.radar.axisLabel, 是否显示刻度点值,默认false - uCharts.js 雷达图新增参数opts.extra.radar.axisLabelTofix,刻度点值小数位数,默认0 - uCharts.js 雷达图新增参数opts.extra.radar.labelPointShow,是否显示末端刻度圆点,默认false - uCharts.js 雷达图新增参数opts.extra.radar.labelPointRadius,刻度圆点的半径,默认3 - uCharts.js 雷达图新增参数opts.extra.radar.labelPointColor,刻度圆点的颜色,默认#cccccc - uCharts.js 雷达图新增参数opts.extra.radar.linearType,渐变色类型,可选值"none"关闭渐变,"custom"开启渐变 - uCharts.js 雷达图新增参数opts.extra.radar.customColor,自定义渐变颜色,数组类型对应series的数组长度以匹配不同series颜色的不同配色方案,例如["#FA7D8D", "#EB88E2"] - uCharts.js 雷达图优化支持series.textColor、series.textSize属性 - uCharts.js 柱状图中温度计式图标,优化支持全圆角类型,修复边框有缝隙的bug,详见官网【演示】中的温度计图表 - uCharts.js 柱状图新增参数opts.extra.column.activeWidth,当前点击柱状图的背景宽度,默认一个单元格单位 - uCharts.js 混合图增加opts.extra.mix.area.gradient 区域图是否开启渐变色 - uCharts.js 混合图增加opts.extra.mix.area.opacity 区域图透明度,默认0.2 - uCharts.js 饼图、圆环图、玫瑰图、漏斗图,增加opts.series[0].data[i].labelText,自定义标签文字,避免formatter格式化的繁琐,详见官网【演示】中的饼图 - uCharts.js 饼图、圆环图、玫瑰图、漏斗图,增加opts.series[0].data[i].labelShow,自定义是否显示某一个指示标签,避免因饼图类别太多导致标签重复或者居多导致图形变形的问题,详见官网【演示】中的饼图 - uCharts.js 增加opts.series[i].legendText/opts.series[0].data[i].legendText(与series.name同级)自定义图例显示文字的方法 - uCharts.js 优化X轴、Y轴formatter格式化方法增加形参,统一为fromatter:function(value,index,opts){} - uCharts.js 修复横屏模式下无法使用双指缩放方法的bug - uCharts.js 修复当只有一条数据或者多条数据值相等的时候Y轴自动计算的最大值错误的bug - 【官网模板】增加外部自定义图例与图表交互的例子,[点击跳转](https://www.ucharts.cn/v2/#/layout/info?id=2) ## 注意:非unimodules 版本如因更新 hbx 至 3.4.7 导致报错如下,请到码云更新非 unimodules 版本组件,[点击跳转](https://gitee.com/uCharts/uCharts/tree/master/uni-app/uCharts-%E7%BB%84%E4%BB%B6) > Error in callback for immediate watcher "uchartsOpts": "SyntaxError: Unexpected token u in JSON at position 0" ## 2.4.2-20220421(2022-04-21) - 秋云图表组件 修复HBX升级3.4.6.20220420版本后echarts报错的问题 ## 2.4.2-20220420(2022-04-20) ## 重要!此版本uCharts新增了很多功能,修复了诸多已知问题 - 秋云图表组件 新增onzoom开启双指缩放功能(仅uCharts),前提需要直角坐标系类图表类型,并且ontouch为true、opts.enableScroll为true,详见实例项目K线图 - 秋云图表组件 新增optsWatch是否监听opts变化,关闭optsWatch后,动态修改opts不会触发图表重绘 - 秋云图表组件 修复开启canvas2d功能后,动态更新数据后画布闪动的bug - 秋云图表组件 去除directory属性,改为自动获取echarts.min.js路径(升级不受影响) - 秋云图表组件 增加getImage()方法及@getImage事件,通过ref调用getImage()方法获,触发@getImage事件获取当前画布的base64图片文件流。 - 秋云图表组件 支付宝、字节跳动、飞书、快手小程序支持开启canvas2d同层渲染设置。 - 秋云图表组件 新增加【非uniCloud】版本组件,避免有些不需要uniCloud的使用组件发布至小程序需要提交隐私声明问题,请到码云[【非uniCloud版本】](https://gitee.com/uCharts/uCharts/tree/master/uni-app/uCharts-%E7%BB%84%E4%BB%B6),或npm[【非uniCloud版本】](https://www.npmjs.com/package/@qiun/uni-ucharts)下载使用。 - uCharts.js 新增dobuleZoom双指缩放功能 - uCharts.js 新增山峰图type="mount",数据格式为饼图类格式,不需要传入categories,具体详见新版官网在线演示 - uCharts.js 修复折线图当数据中存在null时tooltip报错的bug - uCharts.js 修复饼图类当画布比较小时自动计算的半径是负数报错的bug - uCharts.js 统一各图表类型的series.formatter格式化方法的形参为(val, index, series, opts),方便格式化时有更多参数可用 - uCharts.js 标记线功能增加labelText自定义显示文字,增加labelAlign标签显示位置(左侧或右侧),增加标签显示位置微调labelOffsetX、labelOffsetY - uCharts.js 修复条状图当数值很小时开启圆角后样式错误的bug - uCharts.js 修复X轴开启disabled后,X轴仍占用空间的bug - uCharts.js 修复X轴开启滚动条并且开启rotateLabel后,X轴文字与滚动条重叠的bug - uCharts.js 增加X轴rotateAngle文字旋转自定义角度,取值范围(-90至90) - uCharts.js 修复地图文字标签层级显示不正确的bug - uCharts.js 修复饼图、圆环图、玫瑰图当数据全部为0的时候不显示数据标签的bug - uCharts.js 修复当opts.padding上边距为0时,Y轴顶部刻度标签位置不正确的bug ## 另外我们还开发了各大原生小程序组件,已发布至码云和npm [https://gitee.com/uCharts/uCharts](https://gitee.com/uCharts/uCharts) [https://www.npmjs.com/~qiun](https://www.npmjs.com/~qiun) ## 对于原生uCharts文档我们已上线新版官方网站,详情点击下面链接进入官网 [https://www.uCharts.cn/v2/](https://www.ucharts.cn/v2/) ## 2.3.7-20220122(2022-01-22) ## 重要!使用vue3编译,请使用cli模式并升级至最新依赖,HbuilderX编译需要使用3.3.8以上版本 - uCharts.js 修复uni-app平台组件模式使用vue3编译到小程序报错的bug。 ## 2.3.7-20220118(2022-01-18) ## 注意,使用vue3的前提是需要3.3.8.20220114-alpha版本的HBuilder! ## 2.3.67-20220118(2022-01-18) - 秋云图表组件 组件初步支持vue3,全端编译会有些问题,具体详见下面修改: 1. 小程序端运行时,在uni_modules文件夹的qiun-data-charts.js中搜索 new uni_modules_qiunDataCharts_js_sdk_uCharts_uCharts.uCharts,将.uCharts去掉。 2. 小程序端发行时,在uni_modules文件夹的qiun-data-charts.js中搜索 new e.uCharts,将.uCharts去掉,变为 new e。 3. 如果觉得上述步骤比较麻烦,如果您的项目只编译到小程序端,可以修改u-charts.js最后一行导出方式,将 export default uCharts;变更为 export default { uCharts: uCharts }; 这样变更后,H5和App端的renderjs会有问题,请开发者自行选择。(此问题非组件问题,请等待DC官方修复Vue3的小程序端) ## 2.3.6-20220111(2022-01-11) - 秋云图表组件 修改组件 props 属性中的 background 默认值为 rgba(0,0,0,0) ## 2.3.6-20211201(2021-12-01) - uCharts.js 修复bar条状图开启圆角模式时,值很小时圆角渲染错误的bug ## 2.3.5-20211014(2021-10-15) - uCharts.js 增加vue3的编译支持(仅原生uCharts,qiun-data-charts组件后续会支持,请关注更新) ## 2.3.4-20211012(2021-10-12) - 秋云图表组件 修复 mac os x 系统 mouseover 事件丢失的 bug ## 2.3.3-20210706(2021-07-06) - uCharts.js 增加雷达图开启数据点值(opts.dataLabel)的显示 ## 2.3.2-20210627(2021-06-27) - 秋云图表组件 修复tooltipCustom个别情况下传值不正确报错TypeError: Cannot read property 'name' of undefined的bug ## 2.3.1-20210616(2021-06-16) - uCharts.js 修复圆角柱状图使用4角圆角时,当数值过大时不正确的bug ## 2.3.0-20210612(2021-06-12) - uCharts.js 【重要】uCharts增加nvue兼容,可在nvue项目中使用gcanvas组件渲染uCharts,[详见码云uCharts-demo-nvue](https://gitee.com/uCharts/uCharts) - 秋云图表组件 增加tapLegend属性,是否开启图例点击交互事件 - 秋云图表组件 getIndex事件中增加返回uCharts实例中的opts参数,以便在页面中调用参数 - 示例项目 pages/other/other.vue增加app端自定义tooltip的方法,详见showOptsTooltip方法 ## 2.2.1-20210603(2021-06-03) - uCharts.js 修复饼图、圆环图、玫瑰图,当起始角度不为0时,tooltip位置不准确的bug - uCharts.js 增加温度计式柱状图开启顶部半圆形的配置 ## 2.2.0-20210529(2021-05-29) - uCharts.js 增加条状图type="bar" - 示例项目 pages/ucharts/ucharts.vue增加条状图的demo ## 2.1.7-20210524(2021-05-24) - uCharts.js 修复大数据量模式下曲线图不平滑的bug ## 2.1.6-20210523(2021-05-23) - 秋云图表组件 修复小程序端开启滚动条更新数据后滚动条位置不符合预期的bug ## 2.1.5-2021051702(2021-05-17) - uCharts.js 修复自定义Y轴min和max值为0时不能正确显示的bug ## 2.1.5-20210517(2021-05-17) - uCharts.js 修复Y轴自定义min和max时,未按指定的最大值最小值显示坐标轴刻度的bug ## 2.1.4-20210516(2021-05-16) - 秋云图表组件 优化onWindowResize防抖方法 - 秋云图表组件 修复APP端uCharts更新数据时,清空series显示loading图标后再显示图表,图表抖动的bug - uCharts.js 修复开启canvas2d后,x轴、y轴、series自定义字体大小未按比例缩放的bug - 示例项目 修复format-e.vue拼写错误导致app端使用uCharts渲染图表 ## 2.1.3-20210513(2021-05-13) - 秋云图表组件 修改uCharts变更chartData数据为updateData方法,支持带滚动条的数据动态打点 - 秋云图表组件 增加onWindowResize防抖方法 fix by ど誓言,如尘般染指流年づ - 秋云图表组件 H5或者APP变更chartData数据显示loading图表时,原数据闪现的bug - 秋云图表组件 props增加errorReload禁用错误点击重新加载的方法 - uCharts.js 增加tooltip显示category(x轴对应点位)标题的功能,opts.extra.tooltip.showCategory,默认为false - uCharts.js 修复mix混合图只有柱状图时,tooltip的分割线显示位置不正确的bug - uCharts.js 修复开启滚动条,图表在拖动中动态打点,滚动条位置不正确的bug - uCharts.js 修复饼图类数据格式为echarts数据格式,series为空数组报错的bug - 示例项目 修改uCharts.js更新到v2.1.2版本后,@getIndex方法获取索引值变更为e.currentIndex.index - 示例项目 pages/updata/updata.vue增加滚动条拖动更新(数据动态打点)的demo - 示例项目 pages/other/other.vue增加errorReload禁用错误点击重新加载的demo ## 2.1.2-20210509(2021-05-09) 秋云图表组件 修复APP端初始化时就传入chartData或lacaldata不显示图表的bug ## 2.1.1-20210509(2021-05-09) - 秋云图表组件 变更ECharts的eopts配置在renderjs内执行,支持在config-echarts.js配置文件内写function配置。 - 秋云图表组件 修复APP端报错Prop being mutated: "onmouse"错误的bug。 - 秋云图表组件 修复APP端报错Error: Not Found:Page[6][-1,27] at view.umd.min.js:1的bug。 ## 2.1.0-20210507(2021-05-07) - 秋云图表组件 修复初始化时就有数据或者数据更新的时候loading加载动画闪动的bug - uCharts.js 修复x轴format方法categories为字符串类型时返回NaN的bug - uCharts.js 修复series.textColor、legend.fontColor未执行全局默认颜色的bug ## 2.1.0-20210506(2021-05-06) - 秋云图表组件 修复极个别情况下报错item.properties undefined的bug - 秋云图表组件 修复极个别情况下关闭加载动画reshow不起作用,无法显示图表的bug - 示例项目 pages/ucharts/ucharts.vue 增加时间轴折线图(type="tline")、时间轴区域图(type="tarea")、散点图(type="scatter")、气泡图demo(type="bubble")、倒三角形漏斗图(opts.extra.funnel.type="triangle")、金字塔形漏斗图(opts.extra.funnel.type="pyramid") - 示例项目 pages/format-u/format-u.vue 增加X轴format格式化示例 - uCharts.js 升级至v2.1.0版本 - uCharts.js 修复 玫瑰图面积模式点击tooltip位置不正确的bug - uCharts.js 修复 玫瑰图点击图例,只剩一个类别显示空白的bug - uCharts.js 修复 饼图类图点击图例,其他图表tooltip位置某些情况下不准的bug - uCharts.js 修复 x轴为矢量轴(时间轴)情况下,点击tooltip位置不正确的bug - uCharts.js 修复 词云图获取点击索引偶尔不准的bug - uCharts.js 增加 直角坐标系图表X轴format格式化方法(原生uCharts.js用法请使用formatter) - uCharts.js 增加 漏斗图扩展配置,倒三角形(opts.extra.funnel.type="triangle"),金字塔形(opts.extra.funnel.type="pyramid") - uCharts.js 增加 散点图(opts.type="scatter")、气泡图(opts.type="bubble") - 后期计划 完善散点图、气泡图,增加markPoints标记点,增加横向条状图。 ## 2.0.0-20210502(2021-05-02) - uCharts.js 修复词云图获取点击索引不正确的bug ## 2.0.0-20210501(2021-05-01) - 秋云图表组件 修复QQ小程序、百度小程序在关闭动画效果情况下,v-for循环使用图表,显示不正确的bug ## 2.0.0-20210426(2021-04-26) - 秋云图表组件 修复QQ小程序不支持canvas2d的bug - 秋云图表组件 修复钉钉小程序某些情况点击坐标计算错误的bug - uCharts.js 增加 extra.column.categoryGap 参数,柱状图类每个category点位(X轴点)柱子组之间的间距 - uCharts.js 增加 yAxis.data[i].titleOffsetY 参数,标题纵向偏移距离,负数为向上偏移,正数向下偏移 - uCharts.js 增加 yAxis.data[i].titleOffsetX 参数,标题横向偏移距离,负数为向左偏移,正数向右偏移 - uCharts.js 增加 extra.gauge.labelOffset 参数,仪表盘标签文字径向便宜距离,默认13px ## 2.0.0-20210422-2(2021-04-22) 秋云图表组件 修复 formatterAssign 未判断 args[key] == null 的情况导致栈溢出的 bug ## 2.0.0-20210422(2021-04-22) - 秋云图表组件 修复H5、APP、支付宝小程序、微信小程序canvas2d模式下横屏模式的bug ## 2.0.0-20210421(2021-04-21) - uCharts.js 修复多行图例的情况下,图例在上方或者下方时,图例float为左侧或者右侧时,第二行及以后的图例对齐方式不正确的bug ## 2.0.0-20210420(2021-04-20) - 秋云图表组件 修复微信小程序开启canvas2d模式后,windows版微信小程序不支持canvas2d模式的bug - 秋云图表组件 修改非uni_modules版本为v2.0版本qiun-data-charts组件 ## 2.0.0-20210419(2021-04-19) ## v1.0版本已停更,建议转uni_modules版本组件方式调用,点击右侧绿色【使用HBuilderX导入插件】即可使用,示例项目请点击右侧蓝色按钮【使用HBuilderX导入示例项目】。 ## 初次使用如果提示未注册<qiun-data-charts>组件,请重启HBuilderX,如仍不好用,请重启电脑; ## 如果是cli项目,请尝试清理node_modules,重新install,还不行就删除项目,再重新install。 ## 此问题已于DCloud官方确认,HBuilderX下个版本会修复。 ## 其他图表不显示问题详见[常见问题选项卡](https://demo.ucharts.cn) ## 新手请先完整阅读帮助文档及常见问题3遍,右侧蓝色按钮示例项目请看2遍! ## [DEMO演示及在线生成工具(v2.0文档)https://demo.ucharts.cn](https://demo.ucharts.cn) ## [图表组件在项目中的应用参见 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651) - uCharts.js 修复混合图中柱状图单独设置颜色不生效的bug - uCharts.js 修复多Y轴单独设置fontSize时,开启canvas2d后,未对应放大字体的bug ## 2.0.0-20210418(2021-04-18) - 秋云图表组件 增加directory配置,修复H5端history模式下如果发布到二级目录无法正确加载echarts.min.js的bug ## 2.0.0-20210416(2021-04-16) ## v1.0版本已停更,建议转uni_modules版本组件方式调用,点击右侧绿色【使用HBuilderX导入插件】即可使用,示例项目请点击右侧蓝色按钮【使用HBuilderX导入示例项目】。 ## 初次使用如果提示未注册<qiun-data-charts>组件,请重启HBuilderX,如仍不好用,请重启电脑; ## 如果是cli项目,请尝试清理node_modules,重新install,还不行就删除项目,再重新install。 ## 此问题已于DCloud官方确认,HBuilderX下个版本会修复。 ## 其他图表不显示问题详见[常见问题选项卡](https://demo.ucharts.cn) ## 新手请先完整阅读帮助文档及常见问题3遍,右侧蓝色按钮示例项目请看2遍! ## [DEMO演示及在线生成工具(v2.0文档)https://demo.ucharts.cn](https://demo.ucharts.cn) ## [图表组件在项目中的应用参见 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651) - 秋云图表组件 修复APP端某些情况下报错`Not Found Page`的bug,fix by 高级bug开发技术员 - 示例项目 修复APP端v-for循环某些情况下报错`Not Found Page`的bug,fix by 高级bug开发技术员 - uCharts.js 修复非直角坐标系tooltip提示窗右侧超出未变换方向显示的bug ## 2.0.0-20210415(2021-04-15) - 秋云图表组件 修复H5端发布到二级目录下echarts无法加载的bug - 秋云图表组件 修复某些情况下echarts.off('finished')移除监听事件报错的bug ## 2.0.0-20210414(2021-04-14) ## v1.0版本已停更,建议转uni_modules版本组件方式调用,点击右侧绿色【使用HBuilderX导入插件】即可使用,示例项目请点击右侧蓝色按钮【使用HBuilderX导入示例项目】。 ## 初次使用如果提示未注册<qiun-data-charts>组件,请重启HBuilderX,如仍不好用,请重启电脑; ## 如果是cli项目,请尝试清理node_modules,重新install,还不行就删除项目,再重新install。 ## 此问题已于DCloud官方确认,HBuilderX下个版本会修复。 ## 其他图表不显示问题详见[常见问题选项卡](https://demo.ucharts.cn) ## 新手请先完整阅读帮助文档及常见问题3遍,右侧蓝色按钮示例项目请看2遍! ## [DEMO演示及在线生成工具(v2.0文档)https://demo.ucharts.cn](https://demo.ucharts.cn) ## [图表组件在项目中的应用参见 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651) - 秋云图表组件 修复H5端在cli项目下ECharts引用地址错误的bug - 示例项目 增加ECharts的formatter用法的示例(详见示例项目format-e.vue) - uCharts.js 增加圆环图中心背景色的配置extra.ring.centerColor - uCharts.js 修复微信小程序安卓端柱状图开启透明色后显示不正确的bug ## 2.0.0-20210413(2021-04-13) - 秋云图表组件 修复百度小程序多个图表真机未能正确获取根元素dom尺寸的bug - 秋云图表组件 修复百度小程序横屏模式方向不正确的bug - 秋云图表组件 修改ontouch时,@getTouchStart@getTouchMove@getTouchEnd的触发条件 - uCharts.js 修复饼图类数据格式series属性不生效的bug - uCharts.js 增加时序区域图 详见示例项目中ucharts.vue ## 2.0.0-20210412-2(2021-04-12) ## v1.0版本已停更,建议转uni_modules版本组件方式调用,点击右侧绿色【使用HBuilderX导入插件】即可使用,示例项目请点击右侧蓝色按钮【使用HBuilderX导入示例项目】。 ## 初次使用如果提示未注册<qiun-data-charts>组件,请重启HBuilderX。如仍不好用,请重启电脑,此问题已于DCloud官方确认,HBuilderX下个版本会修复。 ## [DEMO演示及在线生成工具(v2.0文档)https://demo.ucharts.cn](https://demo.ucharts.cn) ## [图表组件在uniCloudAdmin中的应用 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651) - 秋云图表组件 修复uCharts在APP端横屏模式下不能正确渲染的bug - 示例项目 增加ECharts柱状图渐变色、圆角柱状图、横向柱状图(条状图)的示例 ## 2.0.0-20210412(2021-04-12) - 秋云图表组件 修复created中判断echarts导致APP端无法识别,改回mounted中判断echarts初始化 - uCharts.js 修复2d模式下series.textOffset未乘像素比的bug ## 2.0.0-20210411(2021-04-11) ## v1.0版本已停更,建议转uni_modules版本组件方式调用,点击右侧绿色【使用HBuilderX导入插件】即可使用,示例项目请点击右侧蓝色按钮【使用HBuilderX导入示例项目】。 ## 初次使用如果提示未注册组件,请重启HBuilderX,并清空小程序开发者工具缓存。 ## [DEMO演示及在线生成工具(v2.0文档)https://demo.ucharts.cn](https://demo.ucharts.cn) ## [图表组件在uniCloudAdmin中的应用 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651) - uCharts.js 折线图区域图增加connectNulls断点续连的功能,详见示例项目中ucharts.vue - 秋云图表组件 变更初始化方法为created,变更type2d默认值为true,优化2d模式下组件初始化后dom获取不到的bug - 秋云图表组件 修复左右布局时,右侧图表点击坐标错误的bug,修复tooltip柱状图自定义颜色显示object的bug ## 2.0.0-20210410(2021-04-10) - 修复左右布局时,右侧图表点击坐标错误的bug,修复柱状图自定义颜色tooltip显示object的bug - 增加标记线及柱状图自定义颜色的demo ## 2.0.0-20210409(2021-04-08) ## v1.0版本已停更,建议转uni_modules版本组件方式调用,点击右侧【使用HBuilderX导入插件】即可体验,DEMO演示及在线生成工具(v2.0文档)[https://demo.ucharts.cn](https://demo.ucharts.cn) ## 图表组件在uniCloudAdmin中的应用 [UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651) - uCharts.js 修复钉钉小程序百度小程序measureText不准确的bug,修复2d模式下饼图类activeRadius为按比例放大的bug - 修复组件在支付宝小程序端点击位置不准确的bug ## 2.0.0-20210408(2021-04-07) - 修复组件在支付宝小程序端不能显示的bug(目前支付宝小程不能点击交互,后续修复) - uCharts.js 修复高分屏下柱状图类,圆弧进度条 自定义宽度不能按比例放大的bug ## 2.0.0-20210407(2021-04-06) ## v1.0版本已停更,建议转uni_modules版本组件方式调用,点击右侧【使用HBuilderX导入插件】即可体验,DEMO演示及在线生成工具(v2.0文档)[https://demo.ucharts.cn](https://demo.ucharts.cn) ## 增加 通过tofix和unit快速格式化y轴的demo add by `howcode` ## 增加 图表组件在uniCloudAdmin中的应用 [UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651) ## 2.0.0-20210406(2021-04-05) # 秋云图表组件+uCharts v2.0版本同步上线,使用方法详见https://demo.ucharts.cn帮助页 ## 2.0.0(2021-04-05) # 秋云图表组件+uCharts v2.0版本同步上线,使用方法详见https://demo.ucharts.cn帮助页 ================================================ FILE: bookkeeping_user_uniapp/uni_modules/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/qiun-data-charts/components/qiun-error/qiun-error.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/qiun-data-charts/components/qiun-loading/loading1.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/qiun-data-charts/components/qiun-loading/loading2.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/qiun-data-charts/components/qiun-loading/loading3.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/qiun-data-charts/components/qiun-loading/loading4.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/qiun-data-charts/components/qiun-loading/loading5.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/qiun-data-charts/components/qiun-loading/qiun-loading.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/qiun-data-charts/js_sdk/u-charts/config-echarts.js ================================================ /* * uCharts® * 高性能跨平台图表库,支持H5、APP、小程序(微信/支付宝/百度/头条/QQ/360)、Vue、Taro等支持canvas的框架平台 * Copyright (c) 2021 QIUN®秋云 https://www.ucharts.cn All rights reserved. * Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) * 复制使用请保留本段注释,感谢支持开源! * * uCharts®官方网站 * https://www.uCharts.cn * * 开源地址: * https://gitee.com/uCharts/uCharts * * uni-app插件市场地址: * http://ext.dcloud.net.cn/plugin?id=271 * */ // 通用配置项 // 主题颜色配置:如每个图表类型需要不同主题,请在对应图表类型上更改color属性 const color = ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc']; const cfe = { //demotype为自定义图表类型 "type": ["pie", "ring", "rose", "funnel", "line", "column", "area", "radar", "gauge","candle","demotype"], //增加自定义图表类型,如果需要categories,请在这里加入您的图表类型例如最后的"demotype" "categories": ["line", "column", "area", "radar", "gauge", "candle","demotype"], //instance为实例变量承载属性,option为eopts承载属性,不要删除 "instance": {}, "option": {}, //下面是自定义format配置,因除H5端外的其他端无法通过props传递函数,只能通过此属性对应下标的方式来替换 "formatter":{ "tooltipDemo1":function(res){ let result = '' for (let i in res) { if (i == 0) { result += res[i].axisValueLabel + '年销售额' } let value = '--' if (res[i].data !== null) { value = res[i].data } // #ifdef H5 result += '\n' + res[i].seriesName + ':' + value + ' 万元' // #endif // #ifdef APP-PLUS result += '
' + res[i].marker + res[i].seriesName + ':' + value + ' 万元' // #endif } return result; }, legendFormat:function(name){ return "自定义图例+"+name; }, yAxisFormatDemo:function (value, index) { return value + '元'; }, seriesFormatDemo:function(res){ return res.name + '年' + res.value + '元'; } }, //这里演示了自定义您的图表类型的option,可以随意命名,之后在组件上 type="demotype" 后,组件会调用这个花括号里的option,如果组件上还存在eopts参数,会将demotype与eopts中option合并后渲染图表。 "demotype":{ "color": color, //在这里填写echarts的option即可 }, //下面是自定义配置,请添加项目所需的通用配置 "column": { "color": color, "title": { "text": '' }, "tooltip": { "trigger": 'axis' }, "grid": { "top": 30, "bottom": 50, "right": 15, "left": 40 }, "legend": { "bottom": 'left', }, "toolbox": { "show": false, }, "xAxis": { "type": 'category', "axisLabel": { "color": '#666666' }, "axisLine": { "lineStyle": { "color": '#CCCCCC' } }, "boundaryGap": true, "data": [] }, "yAxis": { "type": 'value', "axisTick": { "show": false, }, "axisLabel": { "color": '#666666' }, "axisLine": { "lineStyle": { "color": '#CCCCCC' } }, }, "seriesTemplate": { "name": '', "type": 'bar', "data": [], "barwidth": 20, "label": { "show": true, "color": "#666666", "position": 'top', }, }, }, "line": { "color": color, "title": { "text": '' }, "tooltip": { "trigger": 'axis' }, "grid": { "top": 30, "bottom": 50, "right": 15, "left": 40 }, "legend": { "bottom": 'left', }, "toolbox": { "show": false, }, "xAxis": { "type": 'category', "axisLabel": { "color": '#666666' }, "axisLine": { "lineStyle": { "color": '#CCCCCC' } }, "boundaryGap": true, "data": [] }, "yAxis": { "type": 'value', "axisTick": { "show": false, }, "axisLabel": { "color": '#666666' }, "axisLine": { "lineStyle": { "color": '#CCCCCC' } }, }, "seriesTemplate": { "name": '', "type": 'line', "data": [], "barwidth": 20, "label": { "show": true, "color": "#666666", "position": 'top', }, }, }, "area": { "color": color, "title": { "text": '' }, "tooltip": { "trigger": 'axis' }, "grid": { "top": 30, "bottom": 50, "right": 15, "left": 40 }, "legend": { "bottom": 'left', }, "toolbox": { "show": false, }, "xAxis": { "type": 'category', "axisLabel": { "color": '#666666' }, "axisLine": { "lineStyle": { "color": '#CCCCCC' } }, "boundaryGap": true, "data": [] }, "yAxis": { "type": 'value', "axisTick": { "show": false, }, "axisLabel": { "color": '#666666' }, "axisLine": { "lineStyle": { "color": '#CCCCCC' } }, }, "seriesTemplate": { "name": '', "type": 'line', "data": [], "areaStyle": {}, "label": { "show": true, "color": "#666666", "position": 'top', }, }, }, "pie": { "color": color, "title": { "text": '' }, "tooltip": { "trigger": 'item' }, "grid": { "top": 40, "bottom": 30, "right": 15, "left": 15 }, "legend": { "bottom": 'left', }, "seriesTemplate": { "name": '', "type": 'pie', "data": [], "radius": '50%', "label": { "show": true, "color": "#666666", "position": 'top', }, }, }, "ring": { "color": color, "title": { "text": '' }, "tooltip": { "trigger": 'item' }, "grid": { "top": 40, "bottom": 30, "right": 15, "left": 15 }, "legend": { "bottom": 'left', }, "seriesTemplate": { "name": '', "type": 'pie', "data": [], "radius": ['40%', '70%'], "avoidLabelOverlap": false, "label": { "show": true, "color": "#666666", "position": 'top', }, "labelLine": { "show": true }, }, }, "rose": { "color": color, "title": { "text": '' }, "tooltip": { "trigger": 'item' }, "legend": { "top": 'bottom' }, "seriesTemplate": { "name": '', "type": 'pie', "data": [], "radius": "55%", "center": ['50%', '50%'], "roseType": 'area', }, }, "funnel": { "color": color, "title": { "text": '' }, "tooltip": { "trigger": 'item', "formatter": "{b} : {c}%" }, "legend": { "top": 'bottom' }, "seriesTemplate": { "name": '', "type": 'funnel', "left": '10%', "top": 60, "bottom": 60, "width": '80%', "min": 0, "max": 100, "minSize": '0%', "maxSize": '100%', "sort": 'descending', "gap": 2, "label": { "show": true, "position": 'inside' }, "labelLine": { "length": 10, "lineStyle": { "width": 1, "type": 'solid' } }, "itemStyle": { "bordercolor": '#fff', "borderwidth": 1 }, "emphasis": { "label": { "fontSize": 20 } }, "data": [], }, }, "gauge": { "color": color, "tooltip": { "formatter": '{a}
{b} : {c}%' }, "seriesTemplate": { "name": '业务指标', "type": 'gauge', "detail": {"formatter": '{value}%'}, "data": [{"value": 50, "name": '完成率'}] }, }, "candle": { "xAxis": { "data": [] }, "yAxis": {}, "color": color, "title": { "text": '' }, "dataZoom": [{ "type": 'inside', "xAxisIndex": [0, 1], "start": 10, "end": 100 }, { "show": true, "xAxisIndex": [0, 1], "type": 'slider', "bottom": 10, "start": 10, "end": 100 } ], "seriesTemplate": { "name": '', "type": 'k', "data": [], }, } } export default cfe; ================================================ FILE: bookkeeping_user_uniapp/uni_modules/qiun-data-charts/js_sdk/u-charts/config-ucharts.js ================================================ /* * uCharts® * 高性能跨平台图表库,支持H5、APP、小程序(微信/支付宝/百度/头条/QQ/360)、Vue、Taro等支持canvas的框架平台 * Copyright (c) 2021 QIUN®秋云 https://www.ucharts.cn All rights reserved. * Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) * 复制使用请保留本段注释,感谢支持开源! * * uCharts®官方网站 * https://www.uCharts.cn * * 开源地址: * https://gitee.com/uCharts/uCharts * * uni-app插件市场地址: * http://ext.dcloud.net.cn/plugin?id=271 * */ // 主题颜色配置:如每个图表类型需要不同主题,请在对应图表类型上更改color属性 const color = ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc']; //事件转换函数,主要用作格式化x轴为时间轴,根据需求自行修改 const formatDateTime = (timeStamp, returnType)=>{ var date = new Date(); date.setTime(timeStamp * 1000); var y = date.getFullYear(); var m = date.getMonth() + 1; m = m < 10 ? ('0' + m) : m; var d = date.getDate(); d = d < 10 ? ('0' + d) : d; var h = date.getHours(); h = h < 10 ? ('0' + h) : h; var minute = date.getMinutes(); var second = date.getSeconds(); minute = minute < 10 ? ('0' + minute) : minute; second = second < 10 ? ('0' + second) : second; if(returnType == 'full'){return y + '-' + m + '-' + d + ' '+ h +':' + minute + ':' + second;} if(returnType == 'y-m-d'){return y + '-' + m + '-' + d;} if(returnType == 'h:m'){return h +':' + minute;} if(returnType == 'h:m:s'){return h +':' + minute +':' + second;} return [y, m, d, h, minute, second]; } const cfu = { //demotype为自定义图表类型,一般不需要自定义图表类型,只需要改根节点上对应的类型即可 "type":["pie","ring","rose","word","funnel","map","arcbar","line","column","mount","bar","area","radar","gauge","candle","mix","tline","tarea","scatter","bubble","demotype"], "range":["饼状图","圆环图","玫瑰图","词云图","漏斗图","地图","圆弧进度条","折线图","柱状图","山峰图","条状图","区域图","雷达图","仪表盘","K线图","混合图","时间轴折线","时间轴区域","散点图","气泡图","自定义类型"], //增加自定义图表类型,如果需要categories,请在这里加入您的图表类型,例如最后的"demotype" //自定义类型时需要注意"tline","tarea","scatter","bubble"等时间轴(矢量x轴)类图表,没有categories,不需要加入categories "categories":["line","column","mount","bar","area","radar","gauge","candle","mix","demotype"], //instance为实例变量承载属性,不要删除 "instance":{}, //option为opts及eopts承载属性,不要删除 "option":{}, //下面是自定义format配置,因除H5端外的其他端无法通过props传递函数,只能通过此属性对应下标的方式来替换 "formatter":{ "yAxisDemo1":function(val, index, opts){return val+'元'}, "yAxisDemo2":function(val, index, opts){return val.toFixed(2)}, "xAxisDemo1":function(val, index, opts){return val+'年';}, "xAxisDemo2":function(val, index, opts){return formatDateTime(val,'h:m')}, "seriesDemo1":function(val, index, series, opts){return val+'元'}, "tooltipDemo1":function(item, category, index, opts){ if(index==0){ return '随便用'+item.data+'年' }else{ return '其他我没改'+item.data+'天' } }, "pieDemo":function(val, index, series, opts){ if(index !== undefined){ return series[index].name+':'+series[index].data+'元' } }, }, //这里演示了自定义您的图表类型的option,可以随意命名,之后在组件上 type="demotype" 后,组件会调用这个花括号里的option,如果组件上还存在opts参数,会将demotype与opts中option合并后渲染图表。 "demotype":{ //我这里把曲线图当做了自定义图表类型,您可以根据需要随意指定类型或配置 "type": "line", "color": color, "padding": [15,10,0,15], "xAxis": { "disableGrid": true, }, "yAxis": { "gridType": "dash", "dashLength": 2, }, "legend": { }, "extra": { "line": { "type": "curve", "width": 2 }, } }, //下面是自定义配置,请添加项目所需的通用配置 "pie":{ "type": "pie", "color": color, "padding": [5,5,5,5], "extra": { "pie": { "activeOpacity": 0.5, "activeRadius": 10, "offsetAngle": 0, "labelWidth": 15, "border": true, "borderWidth": 3, "borderColor": "#FFFFFF" }, } }, "ring":{ "type": "ring", "color": color, "padding": [5,5,5,5], "rotate": false, "dataLabel": true, "legend": { "show": true, "position": "right", "lineHeight": 25, }, "title": { "name": "收益率", "fontSize": 15, "color": "#666666" }, "subtitle": { "name": "70%", "fontSize": 25, "color": "#7cb5ec" }, "extra": { "ring": { "ringWidth":30, "activeOpacity": 0.5, "activeRadius": 10, "offsetAngle": 0, "labelWidth": 15, "border": true, "borderWidth": 3, "borderColor": "#FFFFFF" }, }, }, "rose":{ "type": "rose", "color": color, "padding": [5,5,5,5], "legend": { "show": true, "position": "left", "lineHeight": 25, }, "extra": { "rose": { "type": "area", "minRadius": 50, "activeOpacity": 0.5, "activeRadius": 10, "offsetAngle": 0, "labelWidth": 15, "border": false, "borderWidth": 2, "borderColor": "#FFFFFF" }, } }, "word":{ "type": "word", "color": color, "extra": { "word": { "type": "normal", "autoColors": false } } }, "funnel":{ "type": "funnel", "color": color, "padding": [15,15,0,15], "extra": { "funnel": { "activeOpacity": 0.3, "activeWidth": 10, "border": true, "borderWidth": 2, "borderColor": "#FFFFFF", "fillOpacity": 1, "labelAlign": "right" }, } }, "map":{ "type": "map", "color": color, "padding": [0,0,0,0], "dataLabel": true, "extra": { "map": { "border": true, "borderWidth": 1, "borderColor": "#666666", "fillOpacity": 0.6, "activeBorderColor": "#F04864", "activeFillColor": "#FACC14", "activeFillOpacity": 1 }, } }, "arcbar":{ "type": "arcbar", "color": color, "title": { "name": "百分比", "fontSize": 25, "color": "#00FF00" }, "subtitle": { "name": "默认标题", "fontSize": 15, "color": "#666666" }, "extra": { "arcbar": { "type": "default", "width": 12, "backgroundColor": "#E9E9E9", "startAngle": 0.75, "endAngle": 0.25, "gap": 2 } } }, "line":{ "type": "line", "color": color, "padding": [15,10,0,15], "xAxis": { "disableGrid": true, }, "yAxis": { "gridType": "dash", "dashLength": 2, }, "legend": { }, "extra": { "line": { "type": "straight", "width": 2 }, } }, "tline":{ "type": "line", "color": color, "padding": [15,10,0,15], "xAxis": { "disableGrid": false, "boundaryGap":"justify", }, "yAxis": { "gridType": "dash", "dashLength": 2, "data":[ { "min":0, "max":80 } ] }, "legend": { }, "extra": { "line": { "type": "curve", "width": 2 }, } }, "tarea":{ "type": "area", "color": color, "padding": [15,10,0,15], "xAxis": { "disableGrid": true, "boundaryGap":"justify", }, "yAxis": { "gridType": "dash", "dashLength": 2, "data":[ { "min":0, "max":80 } ] }, "legend": { }, "extra": { "area": { "type": "curve", "opacity": 0.2, "addLine": true, "width": 2, "gradient": true }, } }, "column":{ "type": "column", "color": color, "padding": [15,15,0,5], "xAxis": { "disableGrid": true, }, "yAxis": { "data":[{"min":0}] }, "legend": { }, "extra": { "column": { "type": "group", "width": 30, "activeBgColor": "#000000", "activeBgOpacity": 0.08 }, } }, "mount":{ "type": "mount", "color": color, "padding": [15,15,0,5], "xAxis": { "disableGrid": true, }, "yAxis": { "data":[{"min":0}] }, "legend": { }, "extra": { "mount": { "type": "mount", "widthRatio": 1.5, }, } }, "bar":{ "type": "bar", "color": color, "padding": [15,30,0,5], "xAxis": { "boundaryGap":"justify", "disableGrid":false, "min":0, "axisLine":false }, "yAxis": { }, "legend": { }, "extra": { "bar": { "type": "group", "width": 30, "meterBorde": 1, "meterFillColor": "#FFFFFF", "activeBgColor": "#000000", "activeBgOpacity": 0.08 }, } }, "area":{ "type": "area", "color": color, "padding": [15,15,0,15], "xAxis": { "disableGrid": true, }, "yAxis": { "gridType": "dash", "dashLength": 2, }, "legend": { }, "extra": { "area": { "type": "straight", "opacity": 0.2, "addLine": true, "width": 2, "gradient": false }, } }, "radar":{ "type": "radar", "color": color, "padding": [5,5,5,5], "dataLabel": false, "legend": { "show": true, "position": "right", "lineHeight": 25, }, "extra": { "radar": { "gridType": "radar", "gridColor": "#CCCCCC", "gridCount": 3, "opacity": 0.2, "max": 200 }, } }, "gauge":{ "type": "gauge", "color": color, "title": { "name": "66Km/H", "fontSize": 25, "color": "#2fc25b", "offsetY": 50 }, "subtitle": { "name": "实时速度", "fontSize": 15, "color": "#1890ff", "offsetY": -50 }, "extra": { "gauge": { "type": "default", "width": 30, "labelColor": "#666666", "startAngle": 0.75, "endAngle": 0.25, "startNumber": 0, "endNumber": 100, "labelFormat": "", "splitLine": { "fixRadius": 0, "splitNumber": 10, "width": 30, "color": "#FFFFFF", "childNumber": 5, "childWidth": 12 }, "pointer": { "width": 24, "color": "auto" } } } }, "candle":{ "type": "candle", "color": color, "padding": [15,15,0,15], "enableScroll": true, "enableMarkLine": true, "dataLabel": false, "xAxis": { "labelCount": 4, "itemCount": 40, "disableGrid": true, "gridColor": "#CCCCCC", "gridType": "solid", "dashLength": 4, "scrollShow": true, "scrollAlign": "left", "scrollColor": "#A6A6A6", "scrollBackgroundColor": "#EFEBEF" }, "yAxis": { }, "legend": { }, "extra": { "candle": { "color": { "upLine": "#f04864", "upFill": "#f04864", "downLine": "#2fc25b", "downFill": "#2fc25b" }, "average": { "show": true, "name": ["MA5","MA10","MA30"], "day": [5,10,20], "color": ["#1890ff","#2fc25b","#facc14"] } }, "markLine": { "type": "dash", "dashLength": 5, "data": [ { "value": 2150, "lineColor": "#f04864", "showLabel": true }, { "value": 2350, "lineColor": "#f04864", "showLabel": true } ] } } }, "mix":{ "type": "mix", "color": color, "padding": [15,15,0,15], "xAxis": { "disableGrid": true, }, "yAxis": { "disabled": false, "disableGrid": false, "splitNumber": 5, "gridType": "dash", "dashLength": 4, "gridColor": "#CCCCCC", "padding": 10, "showTitle": true, "data": [] }, "legend": { }, "extra": { "mix": { "column": { "width": 20 } }, } }, "scatter":{ "type": "scatter", "color":color, "padding":[15,15,0,15], "dataLabel":false, "xAxis": { "disableGrid": false, "gridType":"dash", "splitNumber":5, "boundaryGap":"justify", "min":0 }, "yAxis": { "disableGrid": false, "gridType":"dash", }, "legend": { }, "extra": { "scatter": { }, } }, "bubble":{ "type": "bubble", "color":color, "padding":[15,15,0,15], "xAxis": { "disableGrid": false, "gridType":"dash", "splitNumber":5, "boundaryGap":"justify", "min":0, "max":250 }, "yAxis": { "disableGrid": false, "gridType":"dash", "data":[{ "min":0, "max":150 }] }, "legend": { }, "extra": { "bubble": { "border":2, "opacity": 0.5, }, } } } export default cfu; ================================================ FILE: bookkeeping_user_uniapp/uni_modules/qiun-data-charts/js_sdk/u-charts/readme.md ================================================ # uCharts JSSDK说明 1、如不使用uCharts组件,可直接引用u-charts.js,打包编译后会`自动压缩`,压缩后体积约为`120kb`。 2、如果120kb的体积仍需压缩,请手到uCharts官网通过在线定制选择您需要的图表。 3、config-ucharts.js为uCharts组件的用户配置文件,升级前请`自行备份config-ucharts.js`文件,以免被强制覆盖。 4、config-echarts.js为ECharts组件的用户配置文件,升级前请`自行备份config-echarts.js`文件,以免被强制覆盖。 ================================================ FILE: bookkeeping_user_uniapp/uni_modules/qiun-data-charts/js_sdk/u-charts/u-charts.js ================================================ /* * uCharts (R) * 高性能跨平台图表库,支持H5、APP、小程序(微信/支付宝/百度/头条/QQ/360/快手)、Vue、Taro等支持canvas的框架平台 * Copyright (C) 2018-2022 QIUN (R) 秋云 https://www.ucharts.cn All rights reserved. * Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) * 复制使用请保留本段注释,感谢支持开源! * * uCharts (R) 官方网站 * https://www.uCharts.cn * * 开源地址: * https://gitee.com/uCharts/uCharts * * uni-app插件市场地址: * http://ext.dcloud.net.cn/plugin?id=271 * */ 'use strict'; var config = { version: 'v2.4.3-20220505', yAxisWidth: 15, xAxisHeight: 22, xAxisTextPadding: 3, padding: [10, 10, 10, 10], pixelRatio: 1, rotate: false, fontSize: 13, fontColor: '#666666', dataPointShape: ['circle', 'circle', 'circle', 'circle'], color: ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc'], linearColor: ['#0EE2F8', '#2BDCA8', '#FA7D8D', '#EB88E2', '#2AE3A0', '#0EE2F8', '#EB88E2', '#6773E3', '#F78A85'], pieChartLinePadding: 15, pieChartTextPadding: 5, titleFontSize: 20, subtitleFontSize: 15, toolTipPadding: 3, toolTipBackground: '#000000', toolTipOpacity: 0.7, toolTipLineHeight: 20, radarLabelTextMargin: 13, }; var assign = function(target, ...varArgs) { if (target == null) { throw new TypeError('[uCharts] Cannot convert undefined or null to object'); } if (!varArgs || varArgs.length <= 0) { return target; } // 深度合并对象 function deepAssign(obj1, obj2) { for (let key in obj2) { obj1[key] = obj1[key] && obj1[key].toString() === "[object Object]" ? deepAssign(obj1[key], obj2[key]) : obj1[key] = obj2[key]; } return obj1; } varArgs.forEach(val => { target = deepAssign(target, val); }); return target; }; var util = { toFixed: function toFixed(num, limit) { limit = limit || 2; if (this.isFloat(num)) { num = num.toFixed(limit); } return num; }, isFloat: function isFloat(num) { return num % 1 !== 0; }, approximatelyEqual: function approximatelyEqual(num1, num2) { return Math.abs(num1 - num2) < 1e-10; }, isSameSign: function isSameSign(num1, num2) { return Math.abs(num1) === num1 && Math.abs(num2) === num2 || Math.abs(num1) !== num1 && Math.abs(num2) !== num2; }, isSameXCoordinateArea: function isSameXCoordinateArea(p1, p2) { return this.isSameSign(p1.x, p2.x); }, isCollision: function isCollision(obj1, obj2) { obj1.end = {}; obj1.end.x = obj1.start.x + obj1.width; obj1.end.y = obj1.start.y - obj1.height; obj2.end = {}; obj2.end.x = obj2.start.x + obj2.width; obj2.end.y = obj2.start.y - obj2.height; var flag = obj2.start.x > obj1.end.x || obj2.end.x < obj1.start.x || obj2.end.y > obj1.start.y || obj2.start.y < obj1.end.y; return !flag; } }; //兼容H5点击事件 function getH5Offset(e) { e.mp = { changedTouches: [] }; e.mp.changedTouches.push({ x: e.offsetX, y: e.offsetY }); return e; } // hex 转 rgba function hexToRgb(hexValue, opc) { var rgx = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; var hex = hexValue.replace(rgx, function(m, r, g, b) { return r + r + g + g + b + b; }); var rgb = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); var r = parseInt(rgb[1], 16); var g = parseInt(rgb[2], 16); var b = parseInt(rgb[3], 16); return 'rgba(' + r + ',' + g + ',' + b + ',' + opc + ')'; } function findRange(num, type, limit) { if (isNaN(num)) { throw new Error('[uCharts] series数据需为Number格式'); } limit = limit || 10; type = type ? type : 'upper'; var multiple = 1; while (limit < 1) { limit *= 10; multiple *= 10; } if (type === 'upper') { num = Math.ceil(num * multiple); } else { num = Math.floor(num * multiple); } while (num % limit !== 0) { if (type === 'upper') { if (num == num + 1) { //修复数据值过大num++无效的bug by 向日葵 @xrk_jy break; } num++; } else { num--; } } return num / multiple; } function calCandleMA(dayArr, nameArr, colorArr, kdata) { let seriesTemp = []; for (let k = 0; k < dayArr.length; k++) { let seriesItem = { data: [], name: nameArr[k], color: colorArr[k] }; for (let i = 0, len = kdata.length; i < len; i++) { if (i < dayArr[k]) { seriesItem.data.push(null); continue; } let sum = 0; for (let j = 0; j < dayArr[k]; j++) { sum += kdata[i - j][1]; } seriesItem.data.push(+(sum / dayArr[k]).toFixed(3)); } seriesTemp.push(seriesItem); } return seriesTemp; } function calValidDistance(self, distance, chartData, config, opts) { var dataChartAreaWidth = opts.width - opts.area[1] - opts.area[3]; var dataChartWidth = chartData.eachSpacing * (opts.chartData.xAxisData.xAxisPoints.length - 1); if(opts.type == 'mount' && opts.extra && opts.extra.mount && opts.extra.mount.widthRatio && opts.extra.mount.widthRatio > 1){ if(opts.extra.mount.widthRatio>2) opts.extra.mount.widthRatio = 2 dataChartWidth += (opts.extra.mount.widthRatio - 1)*chartData.eachSpacing; } var validDistance = distance; if (distance >= 0) { validDistance = 0; self.uevent.trigger('scrollLeft'); self.scrollOption.position = 'left' opts.xAxis.scrollPosition = 'left'; } else if (Math.abs(distance) >= dataChartWidth - dataChartAreaWidth) { validDistance = dataChartAreaWidth - dataChartWidth; self.uevent.trigger('scrollRight'); self.scrollOption.position = 'right' opts.xAxis.scrollPosition = 'right'; } else { self.scrollOption.position = distance opts.xAxis.scrollPosition = distance; } return validDistance; } function isInAngleRange(angle, startAngle, endAngle) { function adjust(angle) { while (angle < 0) { angle += 2 * Math.PI; } while (angle > 2 * Math.PI) { angle -= 2 * Math.PI; } return angle; } angle = adjust(angle); startAngle = adjust(startAngle); endAngle = adjust(endAngle); if (startAngle > endAngle) { endAngle += 2 * Math.PI; if (angle < startAngle) { angle += 2 * Math.PI; } } return angle >= startAngle && angle <= endAngle; } function createCurveControlPoints(points, i) { function isNotMiddlePoint(points, i) { if (points[i - 1] && points[i + 1]) { return points[i].y >= Math.max(points[i - 1].y, points[i + 1].y) || points[i].y <= Math.min(points[i - 1].y, points[i + 1].y); } else { return false; } } function isNotMiddlePointX(points, i) { if (points[i - 1] && points[i + 1]) { return points[i].x >= Math.max(points[i - 1].x, points[i + 1].x) || points[i].x <= Math.min(points[i - 1].x, points[i + 1].x); } else { return false; } } var a = 0.2; var b = 0.2; var pAx = null; var pAy = null; var pBx = null; var pBy = null; if (i < 1) { pAx = points[0].x + (points[1].x - points[0].x) * a; pAy = points[0].y + (points[1].y - points[0].y) * a; } else { pAx = points[i].x + (points[i + 1].x - points[i - 1].x) * a; pAy = points[i].y + (points[i + 1].y - points[i - 1].y) * a; } if (i > points.length - 3) { var last = points.length - 1; pBx = points[last].x - (points[last].x - points[last - 1].x) * b; pBy = points[last].y - (points[last].y - points[last - 1].y) * b; } else { pBx = points[i + 1].x - (points[i + 2].x - points[i].x) * b; pBy = points[i + 1].y - (points[i + 2].y - points[i].y) * b; } if (isNotMiddlePoint(points, i + 1)) { pBy = points[i + 1].y; } if (isNotMiddlePoint(points, i)) { pAy = points[i].y; } if (isNotMiddlePointX(points, i + 1)) { pBx = points[i + 1].x; } if (isNotMiddlePointX(points, i)) { pAx = points[i].x; } if (pAy >= Math.max(points[i].y, points[i + 1].y) || pAy <= Math.min(points[i].y, points[i + 1].y)) { pAy = points[i].y; } if (pBy >= Math.max(points[i].y, points[i + 1].y) || pBy <= Math.min(points[i].y, points[i + 1].y)) { pBy = points[i + 1].y; } if (pAx >= Math.max(points[i].x, points[i + 1].x) || pAx <= Math.min(points[i].x, points[i + 1].x)) { pAx = points[i].x; } if (pBx >= Math.max(points[i].x, points[i + 1].x) || pBx <= Math.min(points[i].x, points[i + 1].x)) { pBx = points[i + 1].x; } return { ctrA: { x: pAx, y: pAy }, ctrB: { x: pBx, y: pBy } }; } function convertCoordinateOrigin(x, y, center) { return { x: center.x + x, y: center.y - y }; } function avoidCollision(obj, target) { if (target) { // is collision test while (util.isCollision(obj, target)) { if (obj.start.x > 0) { obj.start.y--; } else if (obj.start.x < 0) { obj.start.y++; } else { if (obj.start.y > 0) { obj.start.y++; } else { obj.start.y--; } } } } return obj; } function fixPieSeries(series, opts, config){ let pieSeriesArr = []; if(series.length>0 && series[0].data.constructor.toString().indexOf('Array') > -1){ opts._pieSeries_ = series; let oldseries = series[0].data; for (var i = 0; i < oldseries.length; i++) { oldseries[i].formatter = series[0].formatter; oldseries[i].data = oldseries[i].value; pieSeriesArr.push(oldseries[i]); } opts.series = pieSeriesArr; }else{ pieSeriesArr = series; } return pieSeriesArr; } function fillSeries(series, opts, config) { var index = 0; for (var i = 0; i < series.length; i++) { let item = series[i]; if (!item.color) { item.color = config.color[index]; index = (index + 1) % config.color.length; } if (!item.linearIndex) { item.linearIndex = i; } if (!item.index) { item.index = 0; } if (!item.type) { item.type = opts.type; } if (typeof item.show == "undefined") { item.show = true; } if (!item.type) { item.type = opts.type; } if (!item.pointShape) { item.pointShape = "circle"; } if (!item.legendShape) { switch (item.type) { case 'line': item.legendShape = "line"; break; case 'column': case 'bar': item.legendShape = "rect"; break; case 'area': case 'mount': item.legendShape = "triangle"; break; default: item.legendShape = "circle"; } } } return series; } function fillCustomColor(linearType, customColor, series, config) { var newcolor = customColor || []; if (linearType == 'custom' && newcolor.length == 0 ) { newcolor = config.linearColor; } if (linearType == 'custom' && newcolor.length < series.length) { let chazhi = series.length - newcolor.length; for (var i = 0; i < chazhi; i++) { newcolor.push(config.linearColor[(i + 1) % config.linearColor.length]); } } return newcolor; } function getDataRange(minData, maxData) { var limit = 0; var range = maxData - minData; if (range >= 10000) { limit = 1000; } else if (range >= 1000) { limit = 100; } else if (range >= 100) { limit = 10; } else if (range >= 10) { limit = 5; } else if (range >= 1) { limit = 1; } else if (range >= 0.1) { limit = 0.1; } else if (range >= 0.01) { limit = 0.01; } else if (range >= 0.001) { limit = 0.001; } else if (range >= 0.0001) { limit = 0.0001; } else if (range >= 0.00001) { limit = 0.00001; } else { limit = 0.000001; } return { minRange: findRange(minData, 'lower', limit), maxRange: findRange(maxData, 'upper', limit) }; } function measureText(text, fontSize, context) { var width = 0; text = String(text); // #ifdef MP-ALIPAY || MP-BAIDU || APP-NVUE context = false; // #endif if (context !== false && context !== undefined && context.setFontSize && context.measureText) { context.setFontSize(fontSize); return context.measureText(text).width; } else { var text = text.split(''); for (let i = 0; i < text.length; i++) { let item = text[i]; if (/[a-zA-Z]/.test(item)) { width += 7; } else if (/[0-9]/.test(item)) { width += 5.5; } else if (/\./.test(item)) { width += 2.7; } else if (/-/.test(item)) { width += 3.25; } else if (/:/.test(item)) { width += 2.5; } else if (/[\u4e00-\u9fa5]/.test(item)) { width += 10; } else if (/\(|\)/.test(item)) { width += 3.73; } else if (/\s/.test(item)) { width += 2.5; } else if (/%/.test(item)) { width += 8; } else { width += 10; } } return width * fontSize / 10; } } function dataCombine(series) { return series.reduce(function(a, b) { return (a.data ? a.data : a).concat(b.data); }, []); } function dataCombineStack(series, len) { var sum = new Array(len); for (var j = 0; j < sum.length; j++) { sum[j] = 0; } for (var i = 0; i < series.length; i++) { for (var j = 0; j < sum.length; j++) { sum[j] += series[i].data[j]; } } return series.reduce(function(a, b) { return (a.data ? a.data : a).concat(b.data).concat(sum); }, []); } function getTouches(touches, opts, e) { let x, y; if (touches.clientX) { if (opts.rotate) { y = opts.height - touches.clientX * opts.pix; x = (touches.pageY - e.currentTarget.offsetTop - (opts.height / opts.pix / 2) * (opts.pix - 1)) * opts.pix; } else { x = touches.clientX * opts.pix; y = (touches.pageY - e.currentTarget.offsetTop - (opts.height / opts.pix / 2) * (opts.pix - 1)) * opts.pix; } } else { if (opts.rotate) { y = opts.height - touches.x * opts.pix; x = touches.y * opts.pix; } else { x = touches.x * opts.pix; y = touches.y * opts.pix; } } return { x: x, y: y } } function getSeriesDataItem(series, index, group) { var data = []; var newSeries = []; var indexIsArr = index.constructor.toString().indexOf('Array') > -1; if(indexIsArr){ let tempSeries = filterSeries(series); for (var i = 0; i < group.length; i++) { newSeries.push(tempSeries[group[i]]); } }else{ newSeries = series; }; for (let i = 0; i < newSeries.length; i++) { let item = newSeries[i]; let tmpindex = -1; if(indexIsArr){ tmpindex = index[i]; }else{ tmpindex = index; } if (item.data[tmpindex] !== null && typeof item.data[tmpindex] !== 'undefined' && item.show) { let seriesItem = {}; seriesItem.color = item.color; seriesItem.type = item.type; seriesItem.style = item.style; seriesItem.pointShape = item.pointShape; seriesItem.disableLegend = item.disableLegend; seriesItem.name = item.name; seriesItem.show = item.show; seriesItem.data = item.formatter ? item.formatter(item.data[tmpindex]) : item.data[tmpindex]; data.push(seriesItem); } } return data; } function getMaxTextListLength(list, fontSize, context) { var lengthList = list.map(function(item) { return measureText(item, fontSize, context); }); return Math.max.apply(null, lengthList); } function getRadarCoordinateSeries(length) { var eachAngle = 2 * Math.PI / length; var CoordinateSeries = []; for (var i = 0; i < length; i++) { CoordinateSeries.push(eachAngle * i); } return CoordinateSeries.map(function(item) { return -1 * item + Math.PI / 2; }); } function getToolTipData(seriesData, opts, index, group, categories) { var option = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : {}; var calPoints = opts.chartData.calPoints?opts.chartData.calPoints:[]; let points = {}; if(group.length > 0){ let filterPoints = []; for (let i = 0; i < group.length; i++) { filterPoints.push(calPoints[group[i]]) } points = filterPoints[0][index[0]]; }else{ for (let i = 0; i < calPoints.length; i++) { if(calPoints[i][index]){ points = calPoints[i][index]; break; } } }; var textList = seriesData.map(function(item) { let titleText = null; if (opts.categories && opts.categories.length>0) { titleText = categories[index]; }; return { text: option.formatter ? option.formatter(item, titleText, index, opts) : item.name + ': ' + item.data, color: item.color }; }); var offset = { x: Math.round(points.x), y: Math.round(points.y) }; return { textList: textList, offset: offset }; } function getMixToolTipData(seriesData, opts, index, categories) { var option = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}; var points = opts.chartData.xAxisPoints[index] + opts.chartData.eachSpacing / 2; var textList = seriesData.map(function(item) { return { text: option.formatter ? option.formatter(item, categories[index], index, opts) : item.name + ': ' + item.data, color: item.color, disableLegend: item.disableLegend ? true : false }; }); textList = textList.filter(function(item) { if (item.disableLegend !== true) { return item; } }); var offset = { x: Math.round(points), y: 0 }; return { textList: textList, offset: offset }; } function getCandleToolTipData(series, seriesData, opts, index, categories, extra) { var option = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : {}; var calPoints = opts.chartData.calPoints; let upColor = extra.color.upFill; let downColor = extra.color.downFill; //颜色顺序为开盘,收盘,最低,最高 let color = [upColor, upColor, downColor, upColor]; var textList = []; seriesData.map(function(item) { if (index == 0) { if (item.data[1] - item.data[0] < 0) { color[1] = downColor; } else { color[1] = upColor; } } else { if (item.data[0] < series[index - 1][1]) { color[0] = downColor; } if (item.data[1] < item.data[0]) { color[1] = downColor; } if (item.data[2] > series[index - 1][1]) { color[2] = upColor; } if (item.data[3] < series[index - 1][1]) { color[3] = downColor; } } let text1 = { text: '开盘:' + item.data[0], color: color[0] }; let text2 = { text: '收盘:' + item.data[1], color: color[1] }; let text3 = { text: '最低:' + item.data[2], color: color[2] }; let text4 = { text: '最高:' + item.data[3], color: color[3] }; textList.push(text1, text2, text3, text4); }); var validCalPoints = []; var offset = { x: 0, y: 0 }; for (let i = 0; i < calPoints.length; i++) { let points = calPoints[i]; if (typeof points[index] !== 'undefined' && points[index] !== null) { validCalPoints.push(points[index]); } } offset.x = Math.round(validCalPoints[0][0].x); return { textList: textList, offset: offset }; } function filterSeries(series) { let tempSeries = []; for (let i = 0; i < series.length; i++) { if (series[i].show == true) { tempSeries.push(series[i]) } } return tempSeries; } function findCurrentIndex(currentPoints, calPoints, opts, config) { var offset = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 0; var current={ index:-1, group:[] }; var spacing = opts.chartData.eachSpacing / 2; let xAxisPoints = []; if (calPoints && calPoints.length > 0) { if (!opts.categories) { spacing = 0; }else{ for (let i = 1; i < opts.chartData.xAxisPoints.length; i++) { xAxisPoints.push(opts.chartData.xAxisPoints[i] - spacing); } if ((opts.type == 'line' || opts.type == 'area') && opts.xAxis.boundaryGap == 'justify') { xAxisPoints = opts.chartData.xAxisPoints; } } if (isInExactChartArea(currentPoints, opts, config)) { if (!opts.categories) { let timePoints = Array(calPoints.length); for (let i = 0; i < calPoints.length; i++) { timePoints[i] = Array(calPoints[i].length) for (let j = 0; j < calPoints[i].length; j++) { timePoints[i][j] = (Math.abs(calPoints[i][j].x - currentPoints.x)); } }; let pointValue = Array(timePoints.length); let pointIndex = Array(timePoints.length); for (let i = 0; i < timePoints.length; i++) { pointValue[i] = Math.min.apply(null, timePoints[i]); pointIndex[i] = timePoints[i].indexOf(pointValue[i]); } let minValue = Math.min.apply(null, pointValue); current.index = []; for (let i = 0; i < pointValue.length; i++) { if(pointValue[i] == minValue){ current.group.push(i); current.index.push(pointIndex[i]); } }; }else{ xAxisPoints.forEach(function(item, index) { if (currentPoints.x + offset + spacing > item) { current.index = index; } }); } } } return current; } function findBarChartCurrentIndex(currentPoints, calPoints, opts, config) { var offset = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 0; var current={ index:-1, group:[] }; var spacing = opts.chartData.eachSpacing / 2; let yAxisPoints = opts.chartData.yAxisPoints; if (calPoints && calPoints.length > 0) { if (isInExactChartArea(currentPoints, opts, config)) { yAxisPoints.forEach(function(item, index) { if (currentPoints.y + offset + spacing > item) { current.index = index; } }); } } return current; } function findLegendIndex(currentPoints, legendData, opts) { let currentIndex = -1; let gap = 0; if (isInExactLegendArea(currentPoints, legendData.area)) { let points = legendData.points; let index = -1; for (let i = 0, len = points.length; i < len; i++) { let item = points[i]; for (let j = 0; j < item.length; j++) { index += 1; let area = item[j]['area']; if (area && currentPoints.x > area[0] - gap && currentPoints.x < area[2] + gap && currentPoints.y > area[1] - gap && currentPoints.y < area[3] + gap) { currentIndex = index; break; } } } return currentIndex; } return currentIndex; } function isInExactLegendArea(currentPoints, area) { return currentPoints.x > area.start.x && currentPoints.x < area.end.x && currentPoints.y > area.start.y && currentPoints.y < area.end.y; } function isInExactChartArea(currentPoints, opts, config) { return currentPoints.x <= opts.width - opts.area[1] + 10 && currentPoints.x >= opts.area[3] - 10 && currentPoints.y >= opts.area[0] && currentPoints.y <= opts.height - opts.area[2]; } function findRadarChartCurrentIndex(currentPoints, radarData, count) { var eachAngleArea = 2 * Math.PI / count; var currentIndex = -1; if (isInExactPieChartArea(currentPoints, radarData.center, radarData.radius)) { var fixAngle = function fixAngle(angle) { if (angle < 0) { angle += 2 * Math.PI; } if (angle > 2 * Math.PI) { angle -= 2 * Math.PI; } return angle; }; var angle = Math.atan2(radarData.center.y - currentPoints.y, currentPoints.x - radarData.center.x); angle = -1 * angle; if (angle < 0) { angle += 2 * Math.PI; } var angleList = radarData.angleList.map(function(item) { item = fixAngle(-1 * item); return item; }); angleList.forEach(function(item, index) { var rangeStart = fixAngle(item - eachAngleArea / 2); var rangeEnd = fixAngle(item + eachAngleArea / 2); if (rangeEnd < rangeStart) { rangeEnd += 2 * Math.PI; } if (angle >= rangeStart && angle <= rangeEnd || angle + 2 * Math.PI >= rangeStart && angle + 2 * Math.PI <= rangeEnd) { currentIndex = index; } }); } return currentIndex; } function findFunnelChartCurrentIndex(currentPoints, funnelData) { var currentIndex = -1; for (var i = 0, len = funnelData.series.length; i < len; i++) { var item = funnelData.series[i]; if (currentPoints.x > item.funnelArea[0] && currentPoints.x < item.funnelArea[2] && currentPoints.y > item.funnelArea[1] && currentPoints.y < item.funnelArea[3]) { currentIndex = i; break; } } return currentIndex; } function findWordChartCurrentIndex(currentPoints, wordData) { var currentIndex = -1; for (var i = 0, len = wordData.length; i < len; i++) { var item = wordData[i]; if (currentPoints.x > item.area[0] && currentPoints.x < item.area[2] && currentPoints.y > item.area[1] && currentPoints.y < item.area[3]) { currentIndex = i; break; } } return currentIndex; } function findMapChartCurrentIndex(currentPoints, opts) { var currentIndex = -1; var cData = opts.chartData.mapData; var data = opts.series; var tmp = pointToCoordinate(currentPoints.y, currentPoints.x, cData.bounds, cData.scale, cData.xoffset, cData.yoffset); var poi = [tmp.x, tmp.y]; for (var i = 0, len = data.length; i < len; i++) { var item = data[i].geometry.coordinates; if (isPoiWithinPoly(poi, item, opts.chartData.mapData.mercator)) { currentIndex = i; break; } } return currentIndex; } function findRoseChartCurrentIndex(currentPoints, pieData, opts) { var currentIndex = -1; var series = getRoseDataPoints(opts._series_, opts.extra.rose.type, pieData.radius, pieData.radius); if (pieData && pieData.center && isInExactPieChartArea(currentPoints, pieData.center, pieData.radius)) { var angle = Math.atan2(pieData.center.y - currentPoints.y, currentPoints.x - pieData.center.x); angle = -angle; if(opts.extra.rose && opts.extra.rose.offsetAngle){ angle = angle - opts.extra.rose.offsetAngle * Math.PI / 180; } for (var i = 0, len = series.length; i < len; i++) { if (isInAngleRange(angle, series[i]._start_, series[i]._start_ + series[i]._rose_proportion_ * 2 * Math.PI)) { currentIndex = i; break; } } } return currentIndex; } function findPieChartCurrentIndex(currentPoints, pieData, opts) { var currentIndex = -1; var series = getPieDataPoints(pieData.series); if (pieData && pieData.center && isInExactPieChartArea(currentPoints, pieData.center, pieData.radius)) { var angle = Math.atan2(pieData.center.y - currentPoints.y, currentPoints.x - pieData.center.x); angle = -angle; if(opts.extra.pie && opts.extra.pie.offsetAngle){ angle = angle - opts.extra.pie.offsetAngle * Math.PI / 180; } if(opts.extra.ring && opts.extra.ring.offsetAngle){ angle = angle - opts.extra.ring.offsetAngle * Math.PI / 180; } for (var i = 0, len = series.length; i < len; i++) { if (isInAngleRange(angle, series[i]._start_, series[i]._start_ + series[i]._proportion_ * 2 * Math.PI)) { currentIndex = i; break; } } } return currentIndex; } function isInExactPieChartArea(currentPoints, center, radius) { return Math.pow(currentPoints.x - center.x, 2) + Math.pow(currentPoints.y - center.y, 2) <= Math.pow(radius, 2); } function splitPoints(points,eachSeries) { var newPoints = []; var items = []; points.forEach(function(item, index) { if(eachSeries.connectNulls){ if (item !== null) { items.push(item); } }else{ if (item !== null) { items.push(item); } else { if (items.length) { newPoints.push(items); } items = []; } } }); if (items.length) { newPoints.push(items); } return newPoints; } function calLegendData(series, opts, config, chartData, context) { let legendData = { area: { start: { x: 0, y: 0 }, end: { x: 0, y: 0 }, width: 0, height: 0, wholeWidth: 0, wholeHeight: 0 }, points: [], widthArr: [], heightArr: [] }; if (opts.legend.show === false) { chartData.legendData = legendData; return legendData; } let padding = opts.legend.padding * opts.pix; let margin = opts.legend.margin * opts.pix; let fontSize = opts.legend.fontSize ? opts.legend.fontSize * opts.pix : config.fontSize; let shapeWidth = 15 * opts.pix; let shapeRight = 5 * opts.pix; let lineHeight = Math.max(opts.legend.lineHeight * opts.pix, fontSize); if (opts.legend.position == 'top' || opts.legend.position == 'bottom') { let legendList = []; let widthCount = 0; let widthCountArr = []; let currentRow = []; for (let i = 0; i < series.length; i++) { let item = series[i]; const legendText = item.legendText ? item.legendText : item.name; let itemWidth = shapeWidth + shapeRight + measureText(legendText || 'undefined', fontSize, context) + opts.legend.itemGap * opts.pix; if (widthCount + itemWidth > opts.width - opts.area[1] - opts.area[3]) { legendList.push(currentRow); widthCountArr.push(widthCount - opts.legend.itemGap * opts.pix); widthCount = itemWidth; currentRow = [item]; } else { widthCount += itemWidth; currentRow.push(item); } } if (currentRow.length) { legendList.push(currentRow); widthCountArr.push(widthCount - opts.legend.itemGap * opts.pix); legendData.widthArr = widthCountArr; let legendWidth = Math.max.apply(null, widthCountArr); switch (opts.legend.float) { case 'left': legendData.area.start.x = opts.area[3]; legendData.area.end.x = opts.area[3] + legendWidth + 2 * padding; break; case 'right': legendData.area.start.x = opts.width - opts.area[1] - legendWidth - 2 * padding; legendData.area.end.x = opts.width - opts.area[1]; break; default: legendData.area.start.x = (opts.width - legendWidth) / 2 - padding; legendData.area.end.x = (opts.width + legendWidth) / 2 + padding; } legendData.area.width = legendWidth + 2 * padding; legendData.area.wholeWidth = legendWidth + 2 * padding; legendData.area.height = legendList.length * lineHeight + 2 * padding; legendData.area.wholeHeight = legendList.length * lineHeight + 2 * padding + 2 * margin; legendData.points = legendList; } } else { let len = series.length; let maxHeight = opts.height - opts.area[0] - opts.area[2] - 2 * margin - 2 * padding; let maxLength = Math.min(Math.floor(maxHeight / lineHeight), len); legendData.area.height = maxLength * lineHeight + padding * 2; legendData.area.wholeHeight = maxLength * lineHeight + padding * 2; switch (opts.legend.float) { case 'top': legendData.area.start.y = opts.area[0] + margin; legendData.area.end.y = opts.area[0] + margin + legendData.area.height; break; case 'bottom': legendData.area.start.y = opts.height - opts.area[2] - margin - legendData.area.height; legendData.area.end.y = opts.height - opts.area[2] - margin; break; default: legendData.area.start.y = (opts.height - legendData.area.height) / 2; legendData.area.end.y = (opts.height + legendData.area.height) / 2; } let lineNum = len % maxLength === 0 ? len / maxLength : Math.floor((len / maxLength) + 1); let currentRow = []; for (let i = 0; i < lineNum; i++) { let temp = series.slice(i * maxLength, i * maxLength + maxLength); currentRow.push(temp); } legendData.points = currentRow; if (currentRow.length) { for (let i = 0; i < currentRow.length; i++) { let item = currentRow[i]; let maxWidth = 0; for (let j = 0; j < item.length; j++) { let itemWidth = shapeWidth + shapeRight + measureText(item[j].name || 'undefined', fontSize, context) + opts.legend.itemGap * opts.pix; if (itemWidth > maxWidth) { maxWidth = itemWidth; } } legendData.widthArr.push(maxWidth); legendData.heightArr.push(item.length * lineHeight + padding * 2); } let legendWidth = 0 for (let i = 0; i < legendData.widthArr.length; i++) { legendWidth += legendData.widthArr[i]; } legendData.area.width = legendWidth - opts.legend.itemGap * opts.pix + 2 * padding; legendData.area.wholeWidth = legendData.area.width + padding; } } switch (opts.legend.position) { case 'top': legendData.area.start.y = opts.area[0] + margin; legendData.area.end.y = opts.area[0] + margin + legendData.area.height; break; case 'bottom': legendData.area.start.y = opts.height - opts.area[2] - legendData.area.height - margin; legendData.area.end.y = opts.height - opts.area[2] - margin; break; case 'left': legendData.area.start.x = opts.area[3]; legendData.area.end.x = opts.area[3] + legendData.area.width; break; case 'right': legendData.area.start.x = opts.width - opts.area[1] - legendData.area.width; legendData.area.end.x = opts.width - opts.area[1]; break; } chartData.legendData = legendData; return legendData; } function calCategoriesData(categories, opts, config, eachSpacing, context) { var result = { angle: 0, xAxisHeight: config.xAxisHeight }; var fontSize = opts.xAxis.fontSize * opts.pix || config.fontSize; var categoriesTextLenth = categories.map(function(item,index) { var xitem = opts.xAxis.formatter ? opts.xAxis.formatter(item,index,opts) : item; return measureText(String(xitem), fontSize, context); }); var maxTextLength = Math.max.apply(this, categoriesTextLenth); if (opts.xAxis.rotateLabel == true) { result.angle = opts.xAxis.rotateAngle * Math.PI / 180; let tempHeight = 2 * config.xAxisTextPadding + Math.abs(maxTextLength * Math.sin(result.angle)) tempHeight = tempHeight < fontSize + 2 * config.xAxisTextPadding ? tempHeight + 2 * config.xAxisTextPadding : tempHeight; if(opts.enableScroll == true && opts.xAxis.scrollShow == true){ tempHeight += 12 * opts.pix; } result.xAxisHeight = tempHeight; } if (opts.xAxis.disabled){ result.xAxisHeight = 0; } return result; } function getXAxisTextList(series, opts, config, stack) { var index = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : -1; var data; if (stack == 'stack') { data = dataCombineStack(series, opts.categories.length); } else { data = dataCombine(series); } var sorted = []; // remove null from data data = data.filter(function(item) { //return item !== null; if (typeof item === 'object' && item !== null) { if (item.constructor.toString().indexOf('Array') > -1) { return item !== null; } else { return item.value !== null; } } else { return item !== null; } }); data.map(function(item) { if (typeof item === 'object') { if (item.constructor.toString().indexOf('Array') > -1) { if (opts.type == 'candle') { item.map(function(subitem) { sorted.push(subitem); }) } else { sorted.push(item[0]); } } else { sorted.push(item.value); } } else { sorted.push(item); } }) var minData = 0; var maxData = 0; if (sorted.length > 0) { minData = Math.min.apply(this, sorted); maxData = Math.max.apply(this, sorted); } //为了兼容v1.9.0之前的项目 if (index > -1) { if (typeof opts.xAxis.data[index].min === 'number') { minData = Math.min(opts.xAxis.data[index].min, minData); } if (typeof opts.xAxis.data[index].max === 'number') { maxData = Math.max(opts.xAxis.data[index].max, maxData); } } else { if (typeof opts.xAxis.min === 'number') { minData = Math.min(opts.xAxis.min, minData); } if (typeof opts.xAxis.max === 'number') { maxData = Math.max(opts.xAxis.max, maxData); } } if (minData === maxData) { var rangeSpan = maxData || 10; maxData += rangeSpan; } //var dataRange = getDataRange(minData, maxData); var minRange = minData; var maxRange = maxData; var range = []; var eachRange = (maxRange - minRange) / opts.xAxis.splitNumber; for (var i = 0; i <= opts.xAxis.splitNumber; i++) { range.push(minRange + eachRange * i); } return range; } function calXAxisData(series, opts, config, context) { //堆叠图重算Y轴 var columnstyle = assign({}, { type: "" }, opts.extra.bar); var result = { angle: 0, xAxisHeight: config.xAxisHeight }; result.ranges = getXAxisTextList(series, opts, config, columnstyle.type); result.rangesFormat = result.ranges.map(function(item) { //item = opts.xAxis.formatter ? opts.xAxis.formatter(item) : util.toFixed(item, 2); item = util.toFixed(item, 2); return item; }); var xAxisScaleValues = result.ranges.map(function(item) { // 如果刻度值是浮点数,则保留两位小数 item = util.toFixed(item, 2); // 若有自定义格式则调用自定义的格式化函数 //item = opts.xAxis.formatter ? opts.xAxis.formatter(Number(item)) : item; return item; }); result = Object.assign(result, getXAxisPoints(xAxisScaleValues, opts, config)); // 计算X轴刻度的属性譬如每个刻度的间隔,刻度的起始点\结束点以及总长 var eachSpacing = result.eachSpacing; var textLength = xAxisScaleValues.map(function(item) { return measureText(item, opts.xAxis.fontSize * opts.pix || config.fontSize, context); }); // get max length of categories text var maxTextLength = Math.max.apply(this, textLength); // 如果刻度值文本内容过长,则将其逆时针旋转45° if (maxTextLength + 2 * config.xAxisTextPadding > eachSpacing) { result.angle = 45 * Math.PI / 180; result.xAxisHeight = 2 * config.xAxisTextPadding + maxTextLength * Math.sin(result.angle); } if (opts.xAxis.disabled === true) { result.xAxisHeight = 0; } return result; } function getRadarDataPoints(angleList, center, radius, series, opts) { var process = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 1; var radarOption = opts.extra.radar || {}; radarOption.max = radarOption.max || 0; var maxData = Math.max(radarOption.max, Math.max.apply(null, dataCombine(series))); var data = []; for (let i = 0; i < series.length; i++) { let each = series[i]; let listItem = {}; listItem.color = each.color; listItem.legendShape = each.legendShape; listItem.pointShape = each.pointShape; listItem.data = []; each.data.forEach(function(item, index) { let tmp = {}; tmp.angle = angleList[index]; tmp.proportion = item / maxData; tmp.value = item; tmp.position = convertCoordinateOrigin(radius * tmp.proportion * process * Math.cos(tmp.angle), radius * tmp.proportion * process * Math.sin(tmp.angle), center); listItem.data.push(tmp); }); data.push(listItem); } return data; } function getPieDataPoints(series, radius) { var process = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1; var count = 0; var _start_ = 0; for (let i = 0; i < series.length; i++) { let item = series[i]; item.data = item.data === null ? 0 : item.data; count += item.data; } for (let i = 0; i < series.length; i++) { let item = series[i]; item.data = item.data === null ? 0 : item.data; if (count === 0) { item._proportion_ = 1 / series.length * process; } else { item._proportion_ = item.data / count * process; } item._radius_ = radius; } for (let i = 0; i < series.length; i++) { let item = series[i]; item._start_ = _start_; _start_ += 2 * item._proportion_ * Math.PI; } return series; } function getFunnelDataPoints(series, radius, type, eachSpacing) { var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; series = series.sort(function(a, b) { return parseInt(b.data) - parseInt(a.data); }); for (let i = 0; i < series.length; i++) { if(type == 'funnel'){ series[i].radius = series[i].data / series[0].data * radius * process; }else{ series[i].radius = (eachSpacing * (series.length - i)) / (eachSpacing * series.length) * radius * process; } series[i]._proportion_ = series[i].data / series[0].data; } if(type !== 'pyramid'){ series.reverse(); } return series; } function getRoseDataPoints(series, type, minRadius, radius) { var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; var count = 0; var _start_ = 0; var dataArr = []; for (let i = 0; i < series.length; i++) { let item = series[i]; item.data = item.data === null ? 0 : item.data; count += item.data; dataArr.push(item.data); } var minData = Math.min.apply(null, dataArr); var maxData = Math.max.apply(null, dataArr); var radiusLength = radius - minRadius; for (let i = 0; i < series.length; i++) { let item = series[i]; item.data = item.data === null ? 0 : item.data; if (count === 0) { item._proportion_ = 1 / series.length * process; item._rose_proportion_ = 1 / series.length * process; } else { item._proportion_ = item.data / count * process; if(type == 'area'){ item._rose_proportion_ = 1 / series.length * process; }else{ item._rose_proportion_ = item.data / count * process; } } item._radius_ = minRadius + radiusLength * ((item.data - minData) / (maxData - minData)) || radius; } for (let i = 0; i < series.length; i++) { let item = series[i]; item._start_ = _start_; _start_ += 2 * item._rose_proportion_ * Math.PI; } return series; } function getArcbarDataPoints(series, arcbarOption) { var process = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1; if (process == 1) { process = 0.999999; } for (let i = 0; i < series.length; i++) { let item = series[i]; item.data = item.data === null ? 0 : item.data; let totalAngle; if (arcbarOption.type == 'circle') { totalAngle = 2; } else { if (arcbarOption.endAngle < arcbarOption.startAngle) { totalAngle = 2 + arcbarOption.endAngle - arcbarOption.startAngle; } else { totalAngle = arcbarOption.startAngle - arcbarOption.endAngle; } } item._proportion_ = totalAngle * item.data * process + arcbarOption.startAngle; if (item._proportion_ >= 2) { item._proportion_ = item._proportion_ % 2; } } return series; } function getGaugeArcbarDataPoints(series, arcbarOption) { var process = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1; if (process == 1) { process = 0.999999; } for (let i = 0; i < series.length; i++) { let item = series[i]; item.data = item.data === null ? 0 : item.data; let totalAngle; if (arcbarOption.type == 'circle') { totalAngle = 2; } else { if (arcbarOption.endAngle < arcbarOption.startAngle) { totalAngle = 2 + arcbarOption.endAngle - arcbarOption.startAngle; } else { totalAngle = arcbarOption.startAngle - arcbarOption.endAngle; } } item._proportion_ = totalAngle * item.data * process + arcbarOption.startAngle; if (item._proportion_ >= 2) { item._proportion_ = item._proportion_ % 2; } } return series; } function getGaugeAxisPoints(categories, startAngle, endAngle) { let totalAngle = startAngle - endAngle + 1; let tempStartAngle = startAngle; for (let i = 0; i < categories.length; i++) { categories[i].value = categories[i].value === null ? 0 : categories[i].value; categories[i]._startAngle_ = tempStartAngle; categories[i]._endAngle_ = totalAngle * categories[i].value + startAngle; if (categories[i]._endAngle_ >= 2) { categories[i]._endAngle_ = categories[i]._endAngle_ % 2; } tempStartAngle = categories[i]._endAngle_; } return categories; } function getGaugeDataPoints(series, categories, gaugeOption) { let process = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1; for (let i = 0; i < series.length; i++) { let item = series[i]; item.data = item.data === null ? 0 : item.data; if (gaugeOption.pointer.color == 'auto') { for (let i = 0; i < categories.length; i++) { if (item.data <= categories[i].value) { item.color = categories[i].color; break; } } } else { item.color = gaugeOption.pointer.color; } let totalAngle = gaugeOption.startAngle - gaugeOption.endAngle + 1; item._endAngle_ = totalAngle * item.data + gaugeOption.startAngle; item._oldAngle_ = gaugeOption.oldAngle; if (gaugeOption.oldAngle < gaugeOption.endAngle) { item._oldAngle_ += 2; } if (item.data >= gaugeOption.oldData) { item._proportion_ = (item._endAngle_ - item._oldAngle_) * process + gaugeOption.oldAngle; } else { item._proportion_ = item._oldAngle_ - (item._oldAngle_ - item._endAngle_) * process; } if (item._proportion_ >= 2) { item._proportion_ = item._proportion_ % 2; } } return series; } function getPieTextMaxLength(series, config, context, opts) { series = getPieDataPoints(series); let maxLength = 0; for (let i = 0; i < series.length; i++) { let item = series[i]; let text = item.formatter ? item.formatter(+item._proportion_.toFixed(2)) : util.toFixed(item._proportion_ * 100) + '%'; maxLength = Math.max(maxLength, measureText(text, item.textSize * opts.pix || config.fontSize, context)); } return maxLength; } function fixColumeData(points, eachSpacing, columnLen, index, config, opts) { return points.map(function(item) { if (item === null) { return null; } var seriesGap = 0; var categoryGap = 0; if (opts.type == 'mix') { seriesGap = opts.extra.mix.column.seriesGap * opts.pix || 0; categoryGap = opts.extra.mix.column.categoryGap * opts.pix || 0; } else { seriesGap = opts.extra.column.seriesGap * opts.pix || 0; categoryGap = opts.extra.column.categoryGap * opts.pix || 0; } seriesGap = Math.min(seriesGap, eachSpacing / columnLen) categoryGap = Math.min(categoryGap, eachSpacing / columnLen) item.width = Math.ceil((eachSpacing - 2 * categoryGap - seriesGap * (columnLen - 1)) / columnLen); if (opts.extra.mix && opts.extra.mix.column.width && +opts.extra.mix.column.width > 0) { item.width = Math.min(item.width, +opts.extra.mix.column.width * opts.pix); } if (opts.extra.column && opts.extra.column.width && +opts.extra.column.width > 0) { item.width = Math.min(item.width, +opts.extra.column.width * opts.pix); } if (item.width <= 0) { item.width = 1; } item.x += (index + 0.5 - columnLen / 2) * (item.width + seriesGap); return item; }); } function fixBarData(points, eachSpacing, columnLen, index, config, opts) { return points.map(function(item) { if (item === null) { return null; } var seriesGap = 0; var categoryGap = 0; seriesGap = opts.extra.bar.seriesGap * opts.pix || 0; categoryGap = opts.extra.bar.categoryGap * opts.pix || 0; seriesGap = Math.min(seriesGap, eachSpacing / columnLen) categoryGap = Math.min(categoryGap, eachSpacing / columnLen) item.width = Math.ceil((eachSpacing - 2 * categoryGap - seriesGap * (columnLen - 1)) / columnLen); if (opts.extra.bar && opts.extra.bar.width && +opts.extra.bar.width > 0) { item.width = Math.min(item.width, +opts.extra.bar.width * opts.pix); } if (item.width <= 0) { item.width = 1; } item.y += (index + 0.5 - columnLen / 2) * (item.width + seriesGap); return item; }); } function fixColumeMeterData(points, eachSpacing, columnLen, index, config, opts, border) { var categoryGap = opts.extra.column.categoryGap * opts.pix || 0; return points.map(function(item) { if (item === null) { return null; } item.width = eachSpacing - 2 * categoryGap; if (opts.extra.column && opts.extra.column.width && +opts.extra.column.width > 0) { item.width = Math.min(item.width, +opts.extra.column.width * opts.pix); } if (index > 0) { item.width -= border; } return item; }); } function fixColumeStackData(points, eachSpacing, columnLen, index, config, opts, series) { var categoryGap = opts.extra.column.categoryGap * opts.pix || 0; return points.map(function(item, indexn) { if (item === null) { return null; } item.width = Math.ceil(eachSpacing - 2 * categoryGap); if (opts.extra.column && opts.extra.column.width && +opts.extra.column.width > 0) { item.width = Math.min(item.width, +opts.extra.column.width * opts.pix); } if (item.width <= 0) { item.width = 1; } return item; }); } function fixBarStackData(points, eachSpacing, columnLen, index, config, opts, series) { var categoryGap = opts.extra.bar.categoryGap * opts.pix || 0; return points.map(function(item, indexn) { if (item === null) { return null; } item.width = Math.ceil(eachSpacing - 2 * categoryGap); if (opts.extra.bar && opts.extra.bar.width && +opts.extra.bar.width > 0) { item.width = Math.min(item.width, +opts.extra.bar.width * opts.pix); } if (item.width <= 0) { item.width = 1; } return item; }); } function getXAxisPoints(categories, opts, config) { var spacingValid = opts.width - opts.area[1] - opts.area[3]; var dataCount = opts.enableScroll ? Math.min(opts.xAxis.itemCount, categories.length) : categories.length; if ((opts.type == 'line' || opts.type == 'area' || opts.type == 'scatter' || opts.type == 'bubble' || opts.type == 'bar') && dataCount > 1 && opts.xAxis.boundaryGap == 'justify') { dataCount -= 1; } var widthRatio = 0; if(opts.type == 'mount' && opts.extra && opts.extra.mount && opts.extra.mount.widthRatio && opts.extra.mount.widthRatio > 1){ if(opts.extra.mount.widthRatio>2) opts.extra.mount.widthRatio = 2 widthRatio = opts.extra.mount.widthRatio - 1; dataCount += widthRatio; } var eachSpacing = spacingValid / dataCount; var xAxisPoints = []; var startX = opts.area[3]; var endX = opts.width - opts.area[1]; categories.forEach(function(item, index) { xAxisPoints.push(startX + widthRatio / 2 * eachSpacing + index * eachSpacing); }); if (opts.xAxis.boundaryGap !== 'justify') { if (opts.enableScroll === true) { xAxisPoints.push(startX + widthRatio * eachSpacing + categories.length * eachSpacing); } else { xAxisPoints.push(endX); } } return { xAxisPoints: xAxisPoints, startX: startX, endX: endX, eachSpacing: eachSpacing }; } function getCandleDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config) { var process = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : 1; var points = []; var validHeight = opts.height - opts.area[0] - opts.area[2]; data.forEach(function(item, index) { if (item === null) { points.push(null); } else { var cPoints = []; item.forEach(function(items, indexs) { var point = {}; point.x = xAxisPoints[index] + Math.round(eachSpacing / 2); var value = items.value || items; var height = validHeight * (value - minRange) / (maxRange - minRange); height *= process; point.y = opts.height - Math.round(height) - opts.area[2]; cPoints.push(point); }); points.push(cPoints); } }); return points; } function getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config) { var process = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : 1; var boundaryGap = 'center'; if (opts.type == 'line' || opts.type == 'area' || opts.type == 'scatter' || opts.type == 'bubble' ) { boundaryGap = opts.xAxis.boundaryGap; } var points = []; var validHeight = opts.height - opts.area[0] - opts.area[2]; var validWidth = opts.width - opts.area[1] - opts.area[3]; data.forEach(function(item, index) { if (item === null) { points.push(null); } else { var point = {}; point.color = item.color; point.x = xAxisPoints[index]; var value = item; if (typeof item === 'object' && item !== null) { if (item.constructor.toString().indexOf('Array') > -1) { let xranges, xminRange, xmaxRange; xranges = [].concat(opts.chartData.xAxisData.ranges); xminRange = xranges.shift(); xmaxRange = xranges.pop(); value = item[1]; point.x = opts.area[3] + validWidth * (item[0] - xminRange) / (xmaxRange - xminRange); if(opts.type == 'bubble'){ point.r = item[2]; point.t = item[3]; } } else { value = item.value; } } if (boundaryGap == 'center') { point.x += eachSpacing / 2; } var height = validHeight * (value - minRange) / (maxRange - minRange); height *= process; point.y = opts.height - height - opts.area[2]; points.push(point); } }); return points; } function getMountDataPoints(series, minRange, maxRange, xAxisPoints, eachSpacing, opts, mountOption) { var process = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : 1; var points = []; var validHeight = opts.height - opts.area[0] - opts.area[2]; var validWidth = opts.width - opts.area[1] - opts.area[3]; var mountWidth = eachSpacing * mountOption.widthRatio; series.forEach(function(item, index) { if (item === null) { points.push(null); } else { var point = {}; point.color = item.color; point.x = xAxisPoints[index]; point.x += eachSpacing / 2; var value = item.data; var height = validHeight * (value - minRange) / (maxRange - minRange); height *= process; point.y = opts.height - height - opts.area[2]; point.value = value; point.width = mountWidth; points.push(point); } }); return points; } function getBarDataPoints(data, minRange, maxRange, yAxisPoints, eachSpacing, opts, config) { var process = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : 1; var points = []; var validHeight = opts.height - opts.area[0] - opts.area[2]; var validWidth = opts.width - opts.area[1] - opts.area[3]; data.forEach(function(item, index) { if (item === null) { points.push(null); } else { var point = {}; point.color = item.color; point.y = yAxisPoints[index]; var value = item; if (typeof item === 'object' && item !== null) { value = item.value; } var height = validWidth * (value - minRange) / (maxRange - minRange); height *= process; point.height = height; point.value = value; point.x = height + opts.area[3]; points.push(point); } }); return points; } function getStackDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, seriesIndex, stackSeries) { var process = arguments.length > 9 && arguments[9] !== undefined ? arguments[9] : 1; var points = []; var validHeight = opts.height - opts.area[0] - opts.area[2]; data.forEach(function(item, index) { if (item === null) { points.push(null); } else { var point = {}; point.color = item.color; point.x = xAxisPoints[index] + Math.round(eachSpacing / 2); if (seriesIndex > 0) { var value = 0; for (let i = 0; i <= seriesIndex; i++) { value += stackSeries[i].data[index]; } var value0 = value - item; var height = validHeight * (value - minRange) / (maxRange - minRange); var height0 = validHeight * (value0 - minRange) / (maxRange - minRange); } else { var value = item; var height = validHeight * (value - minRange) / (maxRange - minRange); var height0 = 0; } var heightc = height0; height *= process; heightc *= process; point.y = opts.height - Math.round(height) - opts.area[2]; point.y0 = opts.height - Math.round(heightc) - opts.area[2]; points.push(point); } }); return points; } function getBarStackDataPoints(data, minRange, maxRange, yAxisPoints, eachSpacing, opts, config, seriesIndex, stackSeries) { var process = arguments.length > 9 && arguments[9] !== undefined ? arguments[9] : 1; var points = []; var validHeight = opts.width - opts.area[1] - opts.area[3]; data.forEach(function(item, index) { if (item === null) { points.push(null); } else { var point = {}; point.color = item.color; point.y = yAxisPoints[index]; if (seriesIndex > 0) { var value = 0; for (let i = 0; i <= seriesIndex; i++) { value += stackSeries[i].data[index]; } var value0 = value - item; var height = validHeight * (value - minRange) / (maxRange - minRange); var height0 = validHeight * (value0 - minRange) / (maxRange - minRange); } else { var value = item; var height = validHeight * (value - minRange) / (maxRange - minRange); var height0 = 0; } var heightc = height0; height *= process; heightc *= process; point.height = height - heightc; point.x = opts.area[3] + height; point.x0 = opts.area[3] + heightc; points.push(point); } }); return points; } function getYAxisTextList(series, opts, config, stack, yData) { var index = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : -1; var data; if (stack == 'stack') { data = dataCombineStack(series, opts.categories.length); } else { data = dataCombine(series); } var sorted = []; // remove null from data data = data.filter(function(item) { //return item !== null; if (typeof item === 'object' && item !== null) { if (item.constructor.toString().indexOf('Array') > -1) { return item !== null; } else { return item.value !== null; } } else { return item !== null; } }); data.map(function(item) { if (typeof item === 'object') { if (item.constructor.toString().indexOf('Array') > -1) { if (opts.type == 'candle') { item.map(function(subitem) { sorted.push(subitem); }) } else { sorted.push(item[1]); } } else { sorted.push(item.value); } } else { sorted.push(item); } }) var minData = yData.min || 0; var maxData = yData.max || 0; if (sorted.length > 0) { minData = Math.min.apply(this, sorted); maxData = Math.max.apply(this, sorted); } if (minData === maxData) { // var rangeSpan = maxData || 10; // maxData += rangeSpan; if(maxData == 0){ maxData = 10; }else{ minData = 0; } } var dataRange = getDataRange(minData, maxData); var minRange = (yData.min === undefined || yData.min === null) ? dataRange.minRange : yData.min; var maxRange = (yData.max === undefined || yData.max === null) ? dataRange.maxRange : yData.max; var range = []; var eachRange = (maxRange - minRange) / opts.yAxis.splitNumber; for (var i = 0; i <= opts.yAxis.splitNumber; i++) { range.push(minRange + eachRange * i); } return range.reverse(); } function calYAxisData(series, opts, config, context) { //堆叠图重算Y轴 var columnstyle = assign({}, { type: "" }, opts.extra.column); //如果是多Y轴,重新计算 var YLength = opts.yAxis.data.length; var newSeries = new Array(YLength); if (YLength > 0) { for (let i = 0; i < YLength; i++) { newSeries[i] = []; for (let j = 0; j < series.length; j++) { if (series[j].index == i) { newSeries[i].push(series[j]); } } } var rangesArr = new Array(YLength); var rangesFormatArr = new Array(YLength); var yAxisWidthArr = new Array(YLength); for (let i = 0; i < YLength; i++) { let yData = opts.yAxis.data[i]; //如果总开关不显示,强制每个Y轴为不显示 if (opts.yAxis.disabled == true) { yData.disabled = true; } if(yData.type === 'categories'){ if(!yData.formatter){ yData.formatter = (val,index,opts) => {return val + (yData.unit || '')}; } yData.categories = yData.categories || opts.categories; rangesArr[i] = yData.categories; }else{ if(!yData.formatter){ yData.formatter = (val,index,opts) => {return val.toFixed(yData.tofix) + (yData.unit || '')}; } rangesArr[i] = getYAxisTextList(newSeries[i], opts, config, columnstyle.type, yData, i); } let yAxisFontSizes = yData.fontSize * opts.pix || config.fontSize; yAxisWidthArr[i] = { position: yData.position ? yData.position : 'left', width: 0 }; rangesFormatArr[i] = rangesArr[i].map(function(items,index) { items = yData.formatter(items,index,opts); yAxisWidthArr[i].width = Math.max(yAxisWidthArr[i].width, measureText(items, yAxisFontSizes, context) + 5); return items; }); let calibration = yData.calibration ? 4 * opts.pix : 0; yAxisWidthArr[i].width += calibration + 3 * opts.pix; if (yData.disabled === true) { yAxisWidthArr[i].width = 0; } } } else { var rangesArr = new Array(1); var rangesFormatArr = new Array(1); var yAxisWidthArr = new Array(1); if(opts.type === 'bar'){ rangesArr[0] = opts.categories; if(!opts.yAxis.formatter){ opts.yAxis.formatter = (val,index,opts) => {return val + (opts.yAxis.unit || '')} } }else{ if(!opts.yAxis.formatter){ opts.yAxis.formatter = (val,index,opts) => {return val.toFixed(opts.yAxis.tofix ) + (opts.yAxis.unit || '')} } rangesArr[0] = getYAxisTextList(series, opts, config, columnstyle.type, {}); } yAxisWidthArr[0] = { position: 'left', width: 0 }; var yAxisFontSize = opts.yAxis.fontSize * opts.pix || config.fontSize; rangesFormatArr[0] = rangesArr[0].map(function(item,index) { item = opts.yAxis.formatter(item,index,opts); yAxisWidthArr[0].width = Math.max(yAxisWidthArr[0].width, measureText(item, yAxisFontSize, context) + 5); return item; }); yAxisWidthArr[0].width += 3 * opts.pix; if (opts.yAxis.disabled === true) { yAxisWidthArr[0] = { position: 'left', width: 0 }; opts.yAxis.data[0] = { disabled: true }; } else { opts.yAxis.data[0] = { disabled: false, position: 'left', max: opts.yAxis.max, min: opts.yAxis.min, formatter: opts.yAxis.formatter }; if(opts.type === 'bar'){ opts.yAxis.data[0].categories = opts.categories; opts.yAxis.data[0].type = 'categories'; } } } return { rangesFormat: rangesFormatArr, ranges: rangesArr, yAxisWidth: yAxisWidthArr }; } function calTooltipYAxisData(point, series, opts, config, eachSpacing) { let ranges = [].concat(opts.chartData.yAxisData.ranges); let spacingValid = opts.height - opts.area[0] - opts.area[2]; let minAxis = opts.area[0]; let items = []; for (let i = 0; i < ranges.length; i++) { let maxVal = ranges[i].shift(); let minVal = ranges[i].pop(); let item = maxVal - (maxVal - minVal) * (point - minAxis) / spacingValid; item = opts.yAxis.data[i].formatter ? opts.yAxis.data[i].formatter(item) : item.toFixed(0); items.push(String(item)) } return items; } function calMarkLineData(points, opts) { let minRange, maxRange; let spacingValid = opts.height - opts.area[0] - opts.area[2]; for (let i = 0; i < points.length; i++) { points[i].yAxisIndex = points[i].yAxisIndex ? points[i].yAxisIndex : 0; let range = [].concat(opts.chartData.yAxisData.ranges[points[i].yAxisIndex]); minRange = range.pop(); maxRange = range.shift(); let height = spacingValid * (points[i].value - minRange) / (maxRange - minRange); points[i].y = opts.height - Math.round(height) - opts.area[2]; } return points; } function contextRotate(context, opts) { if (opts.rotateLock !== true) { context.translate(opts.height, 0); context.rotate(90 * Math.PI / 180); } else if (opts._rotate_ !== true) { context.translate(opts.height, 0); context.rotate(90 * Math.PI / 180); opts._rotate_ = true; } } function drawPointShape(points, color, shape, context, opts) { context.beginPath(); if (opts.dataPointShapeType == 'hollow') { context.setStrokeStyle(color); context.setFillStyle(opts.background); context.setLineWidth(2 * opts.pix); } else { context.setStrokeStyle("#ffffff"); context.setFillStyle(color); context.setLineWidth(1 * opts.pix); } if (shape === 'diamond') { points.forEach(function(item, index) { if (item !== null) { context.moveTo(item.x, item.y - 4.5); context.lineTo(item.x - 4.5, item.y); context.lineTo(item.x, item.y + 4.5); context.lineTo(item.x + 4.5, item.y); context.lineTo(item.x, item.y - 4.5); } }); } else if (shape === 'circle') { points.forEach(function(item, index) { if (item !== null) { context.moveTo(item.x + 2.5 * opts.pix, item.y); context.arc(item.x, item.y, 3 * opts.pix, 0, 2 * Math.PI, false); } }); } else if (shape === 'square') { points.forEach(function(item, index) { if (item !== null) { context.moveTo(item.x - 3.5, item.y - 3.5); context.rect(item.x - 3.5, item.y - 3.5, 7, 7); } }); } else if (shape === 'triangle') { points.forEach(function(item, index) { if (item !== null) { context.moveTo(item.x, item.y - 4.5); context.lineTo(item.x - 4.5, item.y + 4.5); context.lineTo(item.x + 4.5, item.y + 4.5); context.lineTo(item.x, item.y - 4.5); } }); } else if (shape === 'triangle') { return; } context.closePath(); context.fill(); context.stroke(); } function drawRingTitle(opts, config, context, center) { var titlefontSize = opts.title.fontSize || config.titleFontSize; var subtitlefontSize = opts.subtitle.fontSize || config.subtitleFontSize; var title = opts.title.name || ''; var subtitle = opts.subtitle.name || ''; var titleFontColor = opts.title.color || opts.fontColor; var subtitleFontColor = opts.subtitle.color || opts.fontColor; var titleHeight = title ? titlefontSize : 0; var subtitleHeight = subtitle ? subtitlefontSize : 0; var margin = 5; if (subtitle) { var textWidth = measureText(subtitle, subtitlefontSize * opts.pix, context); var startX = center.x - textWidth / 2 + (opts.subtitle.offsetX|| 0) * opts.pix ; var startY = center.y + subtitlefontSize * opts.pix / 2 + (opts.subtitle.offsetY || 0) * opts.pix; if (title) { startY += (titleHeight * opts.pix + margin) / 2; } context.beginPath(); context.setFontSize(subtitlefontSize * opts.pix); context.setFillStyle(subtitleFontColor); context.fillText(subtitle, startX, startY); context.closePath(); context.stroke(); } if (title) { var _textWidth = measureText(title, titlefontSize * opts.pix, context); var _startX = center.x - _textWidth / 2 + (opts.title.offsetX || 0); var _startY = center.y + titlefontSize * opts.pix / 2 + (opts.title.offsetY || 0) * opts.pix; if (subtitle) { _startY -= (subtitleHeight * opts.pix + margin) / 2; } context.beginPath(); context.setFontSize(titlefontSize * opts.pix); context.setFillStyle(titleFontColor); context.fillText(title, _startX, _startY); context.closePath(); context.stroke(); } } function drawPointText(points, series, config, context, opts) { // 绘制数据文案 var data = series.data; var textOffset = series.textOffset ? series.textOffset : 0; points.forEach(function(item, index) { if (item !== null) { context.beginPath(); var fontSize = series.textSize ? series.textSize * opts.pix : config.fontSize; context.setFontSize(fontSize); context.setFillStyle(series.textColor || opts.fontColor); var value = data[index] if (typeof data[index] === 'object' && data[index] !== null) { if (data[index].constructor.toString().indexOf('Array')>-1) { value = data[index][1]; } else { value = data[index].value } } var formatVal = series.formatter ? series.formatter(value,index,series,opts) : value; context.setTextAlign('center'); context.fillText(String(formatVal), item.x, item.y - 4 + textOffset * opts.pix); context.closePath(); context.stroke(); context.setTextAlign('left'); } }); } function drawMountPointText(points, series, config, context, opts) { // 绘制数据文案 var data = series.data; var textOffset = series.textOffset ? series.textOffset : 0; points.forEach(function(item, index) { if (item !== null) { context.beginPath(); var fontSize = series[index].textSize ? series[index].textSize * opts.pix : config.fontSize; context.setFontSize(fontSize); context.setFillStyle(series[index].textColor || opts.fontColor); var value = item.value var formatVal = series[index].formatter ? series[index].formatter(value,index,series,opts) : value; context.setTextAlign('center'); context.fillText(String(formatVal), item.x, item.y - 4 + textOffset * opts.pix); context.closePath(); context.stroke(); context.setTextAlign('left'); } }); } function drawBarPointText(points, series, config, context, opts) { // 绘制数据文案 var data = series.data; var textOffset = series.textOffset ? series.textOffset : 0; points.forEach(function(item, index) { if (item !== null) { context.beginPath(); var fontSize = series.textSize ? series.textSize * opts.pix : config.fontSize; context.setFontSize(fontSize); context.setFillStyle(series.textColor || opts.fontColor); var value = data[index] if (typeof data[index] === 'object' && data[index] !== null) { value = data[index].value ; } var formatVal = series.formatter ? series.formatter(value,index,series,opts) : value; context.setTextAlign('left'); context.fillText(String(formatVal), item.x + 4 * opts.pix , item.y + fontSize / 2 - 3 ); context.closePath(); context.stroke(); } }); } function drawGaugeLabel(gaugeOption, radius, centerPosition, opts, config, context) { radius -= gaugeOption.width / 2 + gaugeOption.labelOffset * opts.pix; radius = radius < 10 ? 10 : radius; let totalAngle = gaugeOption.startAngle - gaugeOption.endAngle + 1; let splitAngle = totalAngle / gaugeOption.splitLine.splitNumber; let totalNumber = gaugeOption.endNumber - gaugeOption.startNumber; let splitNumber = totalNumber / gaugeOption.splitLine.splitNumber; let nowAngle = gaugeOption.startAngle; let nowNumber = gaugeOption.startNumber; for (let i = 0; i < gaugeOption.splitLine.splitNumber + 1; i++) { var pos = { x: radius * Math.cos(nowAngle * Math.PI), y: radius * Math.sin(nowAngle * Math.PI) }; var labelText = gaugeOption.formatter ? gaugeOption.formatter(nowNumber,i,opts) : nowNumber; pos.x += centerPosition.x - measureText(labelText, config.fontSize, context) / 2; pos.y += centerPosition.y; var startX = pos.x; var startY = pos.y; context.beginPath(); context.setFontSize(config.fontSize); context.setFillStyle(gaugeOption.labelColor || opts.fontColor); context.fillText(labelText, startX, startY + config.fontSize / 2); context.closePath(); context.stroke(); nowAngle += splitAngle; if (nowAngle >= 2) { nowAngle = nowAngle % 2; } nowNumber += splitNumber; } } function drawRadarLabel(angleList, radius, centerPosition, opts, config, context) { var radarOption = opts.extra.radar || {}; angleList.forEach(function(angle, index) { if(radarOption.labelPointShow === true && opts.categories[index] !== ''){ var posPoint = { x: radius * Math.cos(angle), y: radius * Math.sin(angle) }; var posPointAxis = convertCoordinateOrigin(posPoint.x, posPoint.y, centerPosition); context.setFillStyle(radarOption.labelPointColor); context.beginPath(); context.arc(posPointAxis.x, posPointAxis.y, radarOption.labelPointRadius * opts.pix, 0, 2 * Math.PI, false); context.closePath(); context.fill(); } var pos = { x: (radius + config.radarLabelTextMargin * opts.pix) * Math.cos(angle), y: (radius + config.radarLabelTextMargin * opts.pix) * Math.sin(angle) }; var posRelativeCanvas = convertCoordinateOrigin(pos.x, pos.y, centerPosition); var startX = posRelativeCanvas.x; var startY = posRelativeCanvas.y; if (util.approximatelyEqual(pos.x, 0)) { startX -= measureText(opts.categories[index] || '', config.fontSize, context) / 2; } else if (pos.x < 0) { startX -= measureText(opts.categories[index] || '', config.fontSize, context); } context.beginPath(); context.setFontSize(config.fontSize); context.setFillStyle(radarOption.labelColor || opts.fontColor); context.fillText(opts.categories[index] || '', startX, startY + config.fontSize / 2); context.closePath(); context.stroke(); }); } function drawPieText(series, opts, config, context, radius, center) { var lineRadius = config.pieChartLinePadding; var textObjectCollection = []; var lastTextObject = null; var seriesConvert = series.map(function(item,index) { var text = item.formatter ? item.formatter(item,index,series,opts) : util.toFixed(item._proportion_.toFixed(4) * 100) + '%'; text = item.labelText ? item.labelText : text; var arc = 2 * Math.PI - (item._start_ + 2 * Math.PI * item._proportion_ / 2); if (item._rose_proportion_) { arc = 2 * Math.PI - (item._start_ + 2 * Math.PI * item._rose_proportion_ / 2); } var color = item.color; var radius = item._radius_; return { arc: arc, text: text, color: color, radius: radius, textColor: item.textColor, textSize: item.textSize, labelShow: item.labelShow }; }); for (let i = 0; i < seriesConvert.length; i++) { let item = seriesConvert[i]; // line end let orginX1 = Math.cos(item.arc) * (item.radius + lineRadius); let orginY1 = Math.sin(item.arc) * (item.radius + lineRadius); // line start let orginX2 = Math.cos(item.arc) * item.radius; let orginY2 = Math.sin(item.arc) * item.radius; // text start let orginX3 = orginX1 >= 0 ? orginX1 + config.pieChartTextPadding : orginX1 - config.pieChartTextPadding; let orginY3 = orginY1; let textWidth = measureText(item.text, item.textSize * opts.pix || config.fontSize, context); let startY = orginY3; if (lastTextObject && util.isSameXCoordinateArea(lastTextObject.start, { x: orginX3 })) { if (orginX3 > 0) { startY = Math.min(orginY3, lastTextObject.start.y); } else if (orginX1 < 0) { startY = Math.max(orginY3, lastTextObject.start.y); } else { if (orginY3 > 0) { startY = Math.max(orginY3, lastTextObject.start.y); } else { startY = Math.min(orginY3, lastTextObject.start.y); } } } if (orginX3 < 0) { orginX3 -= textWidth; } let textObject = { lineStart: { x: orginX2, y: orginY2 }, lineEnd: { x: orginX1, y: orginY1 }, start: { x: orginX3, y: startY }, width: textWidth, height: config.fontSize, text: item.text, color: item.color, textColor: item.textColor, textSize: item.textSize }; lastTextObject = avoidCollision(textObject, lastTextObject); textObjectCollection.push(lastTextObject); } for (let i = 0; i < textObjectCollection.length; i++) { if(seriesConvert[i].labelShow === false){ continue; } let item = textObjectCollection[i]; let lineStartPoistion = convertCoordinateOrigin(item.lineStart.x, item.lineStart.y, center); let lineEndPoistion = convertCoordinateOrigin(item.lineEnd.x, item.lineEnd.y, center); let textPosition = convertCoordinateOrigin(item.start.x, item.start.y, center); context.setLineWidth(1 * opts.pix); context.setFontSize(item.textSize * opts.pix || config.fontSize); context.beginPath(); context.setStrokeStyle(item.color); context.setFillStyle(item.color); context.moveTo(lineStartPoistion.x, lineStartPoistion.y); let curveStartX = item.start.x < 0 ? textPosition.x + item.width : textPosition.x; let textStartX = item.start.x < 0 ? textPosition.x - 5 : textPosition.x + 5; context.quadraticCurveTo(lineEndPoistion.x, lineEndPoistion.y, curveStartX, textPosition.y); context.moveTo(lineStartPoistion.x, lineStartPoistion.y); context.stroke(); context.closePath(); context.beginPath(); context.moveTo(textPosition.x + item.width, textPosition.y); context.arc(curveStartX, textPosition.y, 2 * opts.pix, 0, 2 * Math.PI); context.closePath(); context.fill(); context.beginPath(); context.setFontSize(item.textSize * opts.pix || config.fontSize); context.setFillStyle(item.textColor || opts.fontColor); context.fillText(item.text, textStartX, textPosition.y + 3); context.closePath(); context.stroke(); context.closePath(); } } function drawToolTipSplitLine(offsetX, opts, config, context) { var toolTipOption = opts.extra.tooltip || {}; toolTipOption.gridType = toolTipOption.gridType == undefined ? 'solid' : toolTipOption.gridType; toolTipOption.dashLength = toolTipOption.dashLength == undefined ? 4 : toolTipOption.dashLength; var startY = opts.area[0]; var endY = opts.height - opts.area[2]; if (toolTipOption.gridType == 'dash') { context.setLineDash([toolTipOption.dashLength, toolTipOption.dashLength]); } context.setStrokeStyle(toolTipOption.gridColor || '#cccccc'); context.setLineWidth(1 * opts.pix); context.beginPath(); context.moveTo(offsetX, startY); context.lineTo(offsetX, endY); context.stroke(); context.setLineDash([]); if (toolTipOption.xAxisLabel) { let labelText = opts.categories[opts.tooltip.index]; context.setFontSize(config.fontSize); let textWidth = measureText(labelText, config.fontSize, context); let textX = offsetX - 0.5 * textWidth; let textY = endY; context.beginPath(); context.setFillStyle(hexToRgb(toolTipOption.labelBgColor || config.toolTipBackground, toolTipOption.labelBgOpacity || config.toolTipOpacity)); context.setStrokeStyle(toolTipOption.labelBgColor || config.toolTipBackground); context.setLineWidth(1 * opts.pix); context.rect(textX - config.toolTipPadding, textY, textWidth + 2 * config.toolTipPadding, config.fontSize + 2 * config.toolTipPadding); context.closePath(); context.stroke(); context.fill(); context.beginPath(); context.setFontSize(config.fontSize); context.setFillStyle(toolTipOption.labelFontColor || opts.fontColor); context.fillText(String(labelText), textX, textY + config.toolTipPadding + config.fontSize); context.closePath(); context.stroke(); } } function drawMarkLine(opts, config, context) { let markLineOption = assign({}, { type: 'solid', dashLength: 4, data: [] }, opts.extra.markLine); let startX = opts.area[3]; let endX = opts.width - opts.area[1]; let points = calMarkLineData(markLineOption.data, opts); for (let i = 0; i < points.length; i++) { let item = assign({}, { lineColor: '#DE4A42', showLabel: false, labelFontColor: '#666666', labelBgColor: '#DFE8FF', labelBgOpacity: 0.8, labelAlign: 'left', labelOffsetX: 0, labelOffsetY: 0, }, points[i]); if (markLineOption.type == 'dash') { context.setLineDash([markLineOption.dashLength, markLineOption.dashLength]); } context.setStrokeStyle(item.lineColor); context.setLineWidth(1 * opts.pix); context.beginPath(); context.moveTo(startX, item.y); context.lineTo(endX, item.y); context.stroke(); context.setLineDash([]); if (item.showLabel) { let labelText = item.labelText ? item.labelText : item.value; context.setFontSize(config.fontSize); let textWidth = measureText(labelText, config.fontSize, context); let bgWidth = textWidth + config.toolTipPadding * 2; let bgStartX = item.labelAlign == 'left' ? opts.area[3] - bgWidth : opts.width - opts.area[1]; bgStartX += item.labelOffsetX; let bgStartY = item.y - 0.5 * config.fontSize - config.toolTipPadding; bgStartY += item.labelOffsetY; let textX = bgStartX + config.toolTipPadding; let textY = item.y; context.setFillStyle(hexToRgb(item.labelBgColor, item.labelBgOpacity)); context.setStrokeStyle(item.labelBgColor); context.setLineWidth(1 * opts.pix); context.beginPath(); context.rect(bgStartX, bgStartY, bgWidth, config.fontSize + 2 * config.toolTipPadding); context.closePath(); context.stroke(); context.fill(); context.setFontSize(config.fontSize); context.setTextAlign('left'); context.setFillStyle(item.labelFontColor); context.fillText(String(labelText), textX, bgStartY + config.fontSize + config.toolTipPadding/2); context.stroke(); context.setTextAlign('left'); } } } function drawToolTipHorizentalLine(opts, config, context, eachSpacing, xAxisPoints) { var toolTipOption = assign({}, { gridType: 'solid', dashLength: 4 }, opts.extra.tooltip); var startX = opts.area[3]; var endX = opts.width - opts.area[1]; if (toolTipOption.gridType == 'dash') { context.setLineDash([toolTipOption.dashLength, toolTipOption.dashLength]); } context.setStrokeStyle(toolTipOption.gridColor || '#cccccc'); context.setLineWidth(1 * opts.pix); context.beginPath(); context.moveTo(startX, opts.tooltip.offset.y); context.lineTo(endX, opts.tooltip.offset.y); context.stroke(); context.setLineDash([]); if (toolTipOption.yAxisLabel) { let labelText = calTooltipYAxisData(opts.tooltip.offset.y, opts.series, opts, config, eachSpacing); let widthArr = opts.chartData.yAxisData.yAxisWidth; let tStartLeft = opts.area[3]; let tStartRight = opts.width - opts.area[1]; for (let i = 0; i < labelText.length; i++) { context.setFontSize(config.fontSize); let textWidth = measureText(labelText[i], config.fontSize, context); let bgStartX, bgEndX, bgWidth; if (widthArr[i].position == 'left') { bgStartX = tStartLeft - widthArr[i].width; bgEndX = Math.max(bgStartX, bgStartX + textWidth + config.toolTipPadding * 2); } else { bgStartX = tStartRight; bgEndX = Math.max(bgStartX + widthArr[i].width, bgStartX + textWidth + config.toolTipPadding * 2); } bgWidth = bgEndX - bgStartX; let textX = bgStartX + (bgWidth - textWidth) / 2; let textY = opts.tooltip.offset.y; context.beginPath(); context.setFillStyle(hexToRgb(toolTipOption.labelBgColor || config.toolTipBackground, toolTipOption.labelBgOpacity || config.toolTipOpacity)); context.setStrokeStyle(toolTipOption.labelBgColor || config.toolTipBackground); context.setLineWidth(1 * opts.pix); context.rect(bgStartX, textY - 0.5 * config.fontSize - config.toolTipPadding, bgWidth, config.fontSize + 2 * config.toolTipPadding); context.closePath(); context.stroke(); context.fill(); context.beginPath(); context.setFontSize(config.fontSize); context.setFillStyle(toolTipOption.labelFontColor || opts.fontColor); context.fillText(labelText[i], textX, textY + 0.5 * config.fontSize); context.closePath(); context.stroke(); if (widthArr[i].position == 'left') { tStartLeft -= (widthArr[i].width + opts.yAxis.padding * opts.pix); } else { tStartRight += widthArr[i].width + opts.yAxis.padding * opts.pix; } } } } function drawToolTipSplitArea(offsetX, opts, config, context, eachSpacing) { var toolTipOption = assign({}, { activeBgColor: '#000000', activeBgOpacity: 0.08, activeWidth: eachSpacing }, opts.extra.column); toolTipOption.activeWidth = toolTipOption.activeWidth > eachSpacing ? eachSpacing : toolTipOption.activeWidth; var startY = opts.area[0]; var endY = opts.height - opts.area[2]; context.beginPath(); context.setFillStyle(hexToRgb(toolTipOption.activeBgColor, toolTipOption.activeBgOpacity)); context.rect(offsetX - toolTipOption.activeWidth / 2, startY, toolTipOption.activeWidth, endY - startY); context.closePath(); context.fill(); context.setFillStyle("#FFFFFF"); } function drawBarToolTipSplitArea(offsetX, opts, config, context, eachSpacing) { var toolTipOption = assign({}, { activeBgColor: '#000000', activeBgOpacity: 0.08 }, opts.extra.bar); var startX = opts.area[3]; var endX = opts.width - opts.area[1]; context.beginPath(); context.setFillStyle(hexToRgb(toolTipOption.activeBgColor, toolTipOption.activeBgOpacity)); context.rect( startX ,offsetX - eachSpacing / 2 , endX - startX,eachSpacing); context.closePath(); context.fill(); context.setFillStyle("#FFFFFF"); } function drawToolTip(textList, offset, opts, config, context, eachSpacing, xAxisPoints) { var toolTipOption = assign({}, { showBox: true, showArrow: true, showCategory: false, bgColor: '#000000', bgOpacity: 0.7, borderColor: '#000000', borderWidth: 0, borderRadius: 0, borderOpacity: 0.7, fontColor: '#FFFFFF', splitLine: true, }, opts.extra.tooltip); if(toolTipOption.showCategory==true && opts.categories){ textList.unshift({text:opts.categories[opts.tooltip.index],color:null}) } var legendWidth = 4 * opts.pix; var legendMarginRight = 5 * opts.pix; var arrowWidth = toolTipOption.showArrow ? 8 * opts.pix : 0; var isOverRightBorder = false; if (opts.type == 'line' || opts.type == 'mount' || opts.type == 'area' || opts.type == 'candle' || opts.type == 'mix') { if (toolTipOption.splitLine == true) { drawToolTipSplitLine(opts.tooltip.offset.x, opts, config, context); } } offset = assign({ x: 0, y: 0 }, offset); offset.y -= 8 * opts.pix; var textWidth = textList.map(function(item) { return measureText(item.text, config.fontSize, context); }); var toolTipWidth = legendWidth + legendMarginRight + 4 * config.toolTipPadding + Math.max.apply(null, textWidth); var toolTipHeight = 2 * config.toolTipPadding + textList.length * config.toolTipLineHeight; if (toolTipOption.showBox == false) { return } // if beyond the right border if (offset.x - Math.abs(opts._scrollDistance_ || 0) + arrowWidth + toolTipWidth > opts.width) { isOverRightBorder = true; } if (toolTipHeight + offset.y > opts.height) { offset.y = opts.height - toolTipHeight; } // draw background rect context.beginPath(); context.setFillStyle(hexToRgb(toolTipOption.bgColor || config.toolTipBackground, toolTipOption.bgOpacity || config.toolTipOpacity)); context.setLineWidth(toolTipOption.borderWidth * opts.pix); context.setStrokeStyle(hexToRgb(toolTipOption.borderColor, toolTipOption.borderOpacity)); var radius = toolTipOption.borderRadius; if (isOverRightBorder) { if (toolTipOption.showArrow) { context.moveTo(offset.x, offset.y + 10 * opts.pix); context.lineTo(offset.x - arrowWidth, offset.y + 10 * opts.pix + 5 * opts.pix); } context.arc(offset.x - arrowWidth - radius, offset.y + toolTipHeight - radius, radius, 0, Math.PI / 2, false); context.arc(offset.x - arrowWidth - Math.round(toolTipWidth) + radius, offset.y + toolTipHeight - radius, radius, Math.PI / 2, Math.PI, false); context.arc(offset.x - arrowWidth - Math.round(toolTipWidth) + radius, offset.y + radius, radius, -Math.PI, -Math.PI / 2, false); context.arc(offset.x - arrowWidth - radius, offset.y + radius, radius, -Math.PI / 2, 0, false); if (toolTipOption.showArrow) { context.lineTo(offset.x - arrowWidth, offset.y + 10 * opts.pix - 5 * opts.pix); context.lineTo(offset.x, offset.y + 10 * opts.pix); } } else { if (toolTipOption.showArrow) { context.moveTo(offset.x, offset.y + 10 * opts.pix); context.lineTo(offset.x + arrowWidth, offset.y + 10 * opts.pix - 5 * opts.pix); } context.arc(offset.x + arrowWidth + radius, offset.y + radius, radius, -Math.PI, -Math.PI / 2, false); context.arc(offset.x + arrowWidth + Math.round(toolTipWidth) - radius, offset.y + radius, radius, -Math.PI / 2, 0, false); context.arc(offset.x + arrowWidth + Math.round(toolTipWidth) - radius, offset.y + toolTipHeight - radius, radius, 0, Math.PI / 2, false); context.arc(offset.x + arrowWidth + radius, offset.y + toolTipHeight - radius, radius, Math.PI / 2, Math.PI, false); if (toolTipOption.showArrow) { context.lineTo(offset.x + arrowWidth, offset.y + 10 * opts.pix + 5 * opts.pix); context.lineTo(offset.x, offset.y + 10 * opts.pix); } } context.closePath(); context.fill(); if (toolTipOption.borderWidth > 0) { context.stroke(); } // draw legend textList.forEach(function(item, index) { if (item.color !== null) { context.beginPath(); context.setFillStyle(item.color); var startX = offset.x + arrowWidth + 2 * config.toolTipPadding; var startY = offset.y + (config.toolTipLineHeight - config.fontSize) / 2 + config.toolTipLineHeight * index + config.toolTipPadding + 1; if (isOverRightBorder) { startX = offset.x - toolTipWidth - arrowWidth + 2 * config.toolTipPadding; } context.fillRect(startX, startY, legendWidth, config.fontSize); context.closePath(); } }); // draw text list textList.forEach(function(item, index) { var startX = offset.x + arrowWidth + 2 * config.toolTipPadding + legendWidth + legendMarginRight; if (isOverRightBorder) { startX = offset.x - toolTipWidth - arrowWidth + 2 * config.toolTipPadding + +legendWidth + legendMarginRight; } var startY = offset.y + (config.toolTipLineHeight - config.fontSize) / 2 + config.toolTipLineHeight * index + config.toolTipPadding; context.beginPath(); context.setFontSize(config.fontSize); context.setFillStyle(toolTipOption.fontColor); context.fillText(item.text, startX, startY + config.fontSize); context.closePath(); context.stroke(); }); } function drawColumnDataPoints(series, opts, config, context) { let process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; let xAxisData = opts.chartData.xAxisData, xAxisPoints = xAxisData.xAxisPoints, eachSpacing = xAxisData.eachSpacing; let columnOption = assign({}, { type: 'group', width: eachSpacing / 2, meterBorder: 4, meterFillColor: '#FFFFFF', barBorderCircle: false, barBorderRadius: [], seriesGap: 2, linearType: 'none', linearOpacity: 1, customColor: [], colorStop: 0, }, opts.extra.column); let calPoints = []; context.save(); let leftNum = -2; let rightNum = xAxisPoints.length + 2; if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { context.translate(opts._scrollDistance_, 0); leftNum = Math.floor(-opts._scrollDistance_ / eachSpacing) - 2; rightNum = leftNum + opts.xAxis.itemCount + 4; } if (opts.tooltip && opts.tooltip.textList && opts.tooltip.textList.length && process === 1) { drawToolTipSplitArea(opts.tooltip.offset.x, opts, config, context, eachSpacing); } columnOption.customColor = fillCustomColor(columnOption.linearType, columnOption.customColor, series, config); series.forEach(function(eachSeries, seriesIndex) { let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.yAxisData.ranges[eachSeries.index]); minRange = ranges.pop(); maxRange = ranges.shift(); var data = eachSeries.data; switch (columnOption.type) { case 'group': var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); var tooltipPoints = getStackDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, seriesIndex, series, process); calPoints.push(tooltipPoints); points = fixColumeData(points, eachSpacing, series.length, seriesIndex, config, opts); for (let i = 0; i < points.length; i++) { let item = points[i]; //fix issues/I27B1N yyoinge & Joeshu if (item !== null && i > leftNum && i < rightNum) { var startX = item.x - item.width / 2; var height = opts.height - item.y - opts.area[2]; context.beginPath(); var fillColor = item.color || eachSeries.color var strokeColor = item.color || eachSeries.color if (columnOption.linearType !== 'none') { var grd = context.createLinearGradient(startX, item.y, startX, opts.height - opts.area[2]); //透明渐变 if (columnOption.linearType == 'opacity') { grd.addColorStop(0, hexToRgb(fillColor, columnOption.linearOpacity)); grd.addColorStop(1, hexToRgb(fillColor, 1)); } else { grd.addColorStop(0, hexToRgb(columnOption.customColor[eachSeries.linearIndex], columnOption.linearOpacity)); grd.addColorStop(columnOption.colorStop, hexToRgb(columnOption.customColor[eachSeries.linearIndex],columnOption.linearOpacity)); grd.addColorStop(1, hexToRgb(fillColor, 1)); } fillColor = grd } // 圆角边框 if ((columnOption.barBorderRadius && columnOption.barBorderRadius.length === 4) || columnOption.barBorderCircle === true) { const left = startX; const top = item.y; const width = item.width; const height = opts.height - opts.area[2] - item.y; if (columnOption.barBorderCircle) { columnOption.barBorderRadius = [width / 2, width / 2, 0, 0]; } let [r0, r1, r2, r3] = columnOption.barBorderRadius; let minRadius = Math.min(width/2,height/2); r0 = r0 > minRadius ? minRadius : r0; r1 = r1 > minRadius ? minRadius : r1; r2 = r2 > minRadius ? minRadius : r2; r3 = r3 > minRadius ? minRadius : r3; r0 = r0 < 0 ? 0 : r0; r1 = r1 < 0 ? 0 : r1; r2 = r2 < 0 ? 0 : r2; r3 = r3 < 0 ? 0 : r3; context.arc(left + r0, top + r0, r0, -Math.PI, -Math.PI / 2); context.arc(left + width - r1, top + r1, r1, -Math.PI / 2, 0); context.arc(left + width - r2, top + height - r2, r2, 0, Math.PI / 2); context.arc(left + r3, top + height - r3, r3, Math.PI / 2, Math.PI); } else { context.moveTo(startX, item.y); context.lineTo(startX + item.width, item.y); context.lineTo(startX + item.width, opts.height - opts.area[2]); context.lineTo(startX, opts.height - opts.area[2]); context.lineTo(startX, item.y); context.setLineWidth(1) context.setStrokeStyle(strokeColor); } context.setFillStyle(fillColor); context.closePath(); //context.stroke(); context.fill(); } }; break; case 'stack': // 绘制堆叠数据图 var points = getStackDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, seriesIndex, series, process); calPoints.push(points); points = fixColumeStackData(points, eachSpacing, series.length, seriesIndex, config, opts, series); for (let i = 0; i < points.length; i++) { let item = points[i]; if (item !== null && i > leftNum && i < rightNum) { context.beginPath(); var fillColor = item.color || eachSeries.color; var startX = item.x - item.width / 2 + 1; var height = opts.height - item.y - opts.area[2]; var height0 = opts.height - item.y0 - opts.area[2]; if (seriesIndex > 0) { height -= height0; } context.setFillStyle(fillColor); context.moveTo(startX, item.y); context.fillRect(startX, item.y, item.width, height); context.closePath(); context.fill(); } }; break; case 'meter': // 绘制温度计数据图 var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); calPoints.push(points); points = fixColumeMeterData(points, eachSpacing, series.length, seriesIndex, config, opts, columnOption.meterBorder); for (let i = 0; i < points.length; i++) { let item = points[i]; if (item !== null && i > leftNum && i < rightNum) { //画背景颜色 context.beginPath(); if (seriesIndex == 0 && columnOption.meterBorder > 0) { context.setStrokeStyle(eachSeries.color); context.setLineWidth(columnOption.meterBorder * opts.pix); } if(seriesIndex == 0){ context.setFillStyle(columnOption.meterFillColor); }else{ context.setFillStyle(item.color || eachSeries.color); } var startX = item.x - item.width / 2; var height = opts.height - item.y - opts.area[2]; if ((columnOption.barBorderRadius && columnOption.barBorderRadius.length === 4) || columnOption.barBorderCircle === true) { const left = startX; const top = item.y; const width = item.width; const height = opts.height - opts.area[2] - item.y; if (columnOption.barBorderCircle) { columnOption.barBorderRadius = [width / 2, width / 2, 0, 0]; } let [r0, r1, r2, r3] = columnOption.barBorderRadius; let minRadius = Math.min(width/2,height/2); r0 = r0 > minRadius ? minRadius : r0; r1 = r1 > minRadius ? minRadius : r1; r2 = r2 > minRadius ? minRadius : r2; r3 = r3 > minRadius ? minRadius : r3; r0 = r0 < 0 ? 0 : r0; r1 = r1 < 0 ? 0 : r1; r2 = r2 < 0 ? 0 : r2; r3 = r3 < 0 ? 0 : r3; context.arc(left + r0, top + r0, r0, -Math.PI, -Math.PI / 2); context.arc(left + width - r1, top + r1, r1, -Math.PI / 2, 0); context.arc(left + width - r2, top + height - r2, r2, 0, Math.PI / 2); context.arc(left + r3, top + height - r3, r3, Math.PI / 2, Math.PI); context.fill(); }else{ context.moveTo(startX, item.y); context.lineTo(startX + item.width, item.y); context.lineTo(startX + item.width, opts.height - opts.area[2]); context.lineTo(startX, opts.height - opts.area[2]); context.lineTo(startX, item.y); context.fill(); } if (seriesIndex == 0 && columnOption.meterBorder > 0) { context.closePath(); context.stroke(); } } } break; } }); if (opts.dataLabel !== false && process === 1) { series.forEach(function(eachSeries, seriesIndex) { let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.yAxisData.ranges[eachSeries.index]); minRange = ranges.pop(); maxRange = ranges.shift(); var data = eachSeries.data; switch (columnOption.type) { case 'group': var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); points = fixColumeData(points, eachSpacing, series.length, seriesIndex, config, opts); drawPointText(points, eachSeries, config, context, opts); break; case 'stack': var points = getStackDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, seriesIndex, series, process); drawPointText(points, eachSeries, config, context, opts); break; case 'meter': var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); drawPointText(points, eachSeries, config, context, opts); break; } }); } context.restore(); return { xAxisPoints: xAxisPoints, calPoints: calPoints, eachSpacing: eachSpacing }; } function drawMountDataPoints(series, opts, config, context) { let process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; let xAxisData = opts.chartData.xAxisData, xAxisPoints = xAxisData.xAxisPoints, eachSpacing = xAxisData.eachSpacing; let mountOption = assign({}, { type: 'mount', widthRatio: 1, borderWidth: 1, barBorderCircle: false, barBorderRadius: [], linearType: 'none', linearOpacity: 1, customColor: [], colorStop: 0, }, opts.extra.mount); mountOption.widthRatio = mountOption.widthRatio <= 0 ? 0 : mountOption.widthRatio; mountOption.widthRatio = mountOption.widthRatio >= 2 ? 2 : mountOption.widthRatio; let calPoints = []; context.save(); let leftNum = -2; let rightNum = xAxisPoints.length + 2; if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { context.translate(opts._scrollDistance_, 0); leftNum = Math.floor(-opts._scrollDistance_ / eachSpacing) - 2; rightNum = leftNum + opts.xAxis.itemCount + 4; } mountOption.customColor = fillCustomColor(mountOption.linearType, mountOption.customColor, series, config); let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.yAxisData.ranges[0]); minRange = ranges.pop(); maxRange = ranges.shift(); var points = getMountDataPoints(series, minRange, maxRange, xAxisPoints, eachSpacing, opts, mountOption, process); switch (mountOption.type) { case 'bar': for (let i = 0; i < points.length; i++) { let item = points[i]; if (item !== null && i > leftNum && i < rightNum) { var startX = item.x - eachSpacing*mountOption.widthRatio/2; var height = opts.height - item.y - opts.area[2]; context.beginPath(); var fillColor = item.color || series[i].color var strokeColor = item.color || series[i].color if (mountOption.linearType !== 'none') { var grd = context.createLinearGradient(startX, item.y, startX, opts.height - opts.area[2]); //透明渐变 if (mountOption.linearType == 'opacity') { grd.addColorStop(0, hexToRgb(fillColor, mountOption.linearOpacity)); grd.addColorStop(1, hexToRgb(fillColor, 1)); } else { grd.addColorStop(0, hexToRgb(mountOption.customColor[series[i].linearIndex], mountOption.linearOpacity)); grd.addColorStop(mountOption.colorStop, hexToRgb(mountOption.customColor[series[i].linearIndex],mountOption.linearOpacity)); grd.addColorStop(1, hexToRgb(fillColor, 1)); } fillColor = grd } // 圆角边框 if ((mountOption.barBorderRadius && mountOption.barBorderRadius.length === 4) || mountOption.barBorderCircle === true) { const left = startX; const top = item.y; const width = item.width; const height = opts.height - opts.area[2] - item.y - mountOption.borderWidth * opts.pix / 2; if (mountOption.barBorderCircle) { mountOption.barBorderRadius = [width / 2, width / 2, 0, 0]; } let [r0, r1, r2, r3] = mountOption.barBorderRadius; let minRadius = Math.min(width/2,height/2); r0 = r0 > minRadius ? minRadius : r0; r1 = r1 > minRadius ? minRadius : r1; r2 = r2 > minRadius ? minRadius : r2; r3 = r3 > minRadius ? minRadius : r3; r0 = r0 < 0 ? 0 : r0; r1 = r1 < 0 ? 0 : r1; r2 = r2 < 0 ? 0 : r2; r3 = r3 < 0 ? 0 : r3; context.arc(left + r0, top + r0, r0, -Math.PI, -Math.PI / 2); context.arc(left + width - r1, top + r1, r1, -Math.PI / 2, 0); context.arc(left + width - r2, top + height - r2, r2, 0, Math.PI / 2); context.arc(left + r3, top + height - r3, r3, Math.PI / 2, Math.PI); } else { context.moveTo(startX, item.y); context.lineTo(startX + item.width, item.y); context.lineTo(startX + item.width, opts.height - opts.area[2]); context.lineTo(startX, opts.height - opts.area[2]); context.lineTo(startX, item.y); } context.setStrokeStyle(strokeColor); context.setFillStyle(fillColor); if(mountOption.borderWidth > 0){ context.setLineWidth(mountOption.borderWidth * opts.pix); context.closePath(); context.stroke(); } context.fill(); } }; break; case 'triangle': for (let i = 0; i < points.length; i++) { let item = points[i]; if (item !== null && i > leftNum && i < rightNum) { var startX = item.x - eachSpacing*mountOption.widthRatio/2; var height = opts.height - item.y - opts.area[2]; context.beginPath(); var fillColor = item.color || series[i].color var strokeColor = item.color || series[i].color if (mountOption.linearType !== 'none') { var grd = context.createLinearGradient(startX, item.y, startX, opts.height - opts.area[2]); //透明渐变 if (mountOption.linearType == 'opacity') { grd.addColorStop(0, hexToRgb(fillColor, mountOption.linearOpacity)); grd.addColorStop(1, hexToRgb(fillColor, 1)); } else { grd.addColorStop(0, hexToRgb(mountOption.customColor[series[i].linearIndex], mountOption.linearOpacity)); grd.addColorStop(mountOption.colorStop, hexToRgb(mountOption.customColor[series[i].linearIndex],mountOption.linearOpacity)); grd.addColorStop(1, hexToRgb(fillColor, 1)); } fillColor = grd } context.moveTo(startX, opts.height - opts.area[2]); context.lineTo(item.x, item.y); context.lineTo(startX + item.width, opts.height - opts.area[2]); context.setStrokeStyle(strokeColor); context.setFillStyle(fillColor); if(mountOption.borderWidth > 0){ context.setLineWidth(mountOption.borderWidth * opts.pix); context.stroke(); } context.fill(); } }; break; case 'mount': for (let i = 0; i < points.length; i++) { let item = points[i]; if (item !== null && i > leftNum && i < rightNum) { var startX = item.x - eachSpacing*mountOption.widthRatio/2; var height = opts.height - item.y - opts.area[2]; context.beginPath(); var fillColor = item.color || series[i].color var strokeColor = item.color || series[i].color if (mountOption.linearType !== 'none') { var grd = context.createLinearGradient(startX, item.y, startX, opts.height - opts.area[2]); //透明渐变 if (mountOption.linearType == 'opacity') { grd.addColorStop(0, hexToRgb(fillColor, mountOption.linearOpacity)); grd.addColorStop(1, hexToRgb(fillColor, 1)); } else { grd.addColorStop(0, hexToRgb(mountOption.customColor[series[i].linearIndex], mountOption.linearOpacity)); grd.addColorStop(mountOption.colorStop, hexToRgb(mountOption.customColor[series[i].linearIndex],mountOption.linearOpacity)); grd.addColorStop(1, hexToRgb(fillColor, 1)); } fillColor = grd } context.moveTo(startX, opts.height - opts.area[2]); context.bezierCurveTo(item.x - item.width/4, opts.height - opts.area[2], item.x - item.width/4, item.y, item.x, item.y); context.bezierCurveTo(item.x + item.width/4, item.y, item.x + item.width/4, opts.height - opts.area[2], startX + item.width, opts.height - opts.area[2]); context.setStrokeStyle(strokeColor); context.setFillStyle(fillColor); if(mountOption.borderWidth > 0){ context.setLineWidth(mountOption.borderWidth * opts.pix); context.stroke(); } context.fill(); } }; break; case 'sharp': for (let i = 0; i < points.length; i++) { let item = points[i]; if (item !== null && i > leftNum && i < rightNum) { var startX = item.x - eachSpacing*mountOption.widthRatio/2; var height = opts.height - item.y - opts.area[2]; context.beginPath(); var fillColor = item.color || series[i].color var strokeColor = item.color || series[i].color if (mountOption.linearType !== 'none') { var grd = context.createLinearGradient(startX, item.y, startX, opts.height - opts.area[2]); //透明渐变 if (mountOption.linearType == 'opacity') { grd.addColorStop(0, hexToRgb(fillColor, mountOption.linearOpacity)); grd.addColorStop(1, hexToRgb(fillColor, 1)); } else { grd.addColorStop(0, hexToRgb(mountOption.customColor[series[i].linearIndex], mountOption.linearOpacity)); grd.addColorStop(mountOption.colorStop, hexToRgb(mountOption.customColor[series[i].linearIndex],mountOption.linearOpacity)); grd.addColorStop(1, hexToRgb(fillColor, 1)); } fillColor = grd } context.moveTo(startX, opts.height - opts.area[2]); context.quadraticCurveTo(item.x - 0, opts.height - opts.area[2] - height/4, item.x, item.y); context.quadraticCurveTo(item.x + 0, opts.height - opts.area[2] - height/4, startX + item.width, opts.height - opts.area[2]) context.setStrokeStyle(strokeColor); context.setFillStyle(fillColor); if(mountOption.borderWidth > 0){ context.setLineWidth(mountOption.borderWidth * opts.pix); context.stroke(); } context.fill(); } }; break; } if (opts.dataLabel !== false && process === 1) { let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.yAxisData.ranges[0]); minRange = ranges.pop(); maxRange = ranges.shift(); var points = getMountDataPoints(series, minRange, maxRange, xAxisPoints, eachSpacing, opts, mountOption, process); drawMountPointText(points, series, config, context, opts); } context.restore(); return { xAxisPoints: xAxisPoints, calPoints: points, eachSpacing: eachSpacing }; } function drawBarDataPoints(series, opts, config, context) { let process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; let yAxisPoints = []; let eachSpacing = (opts.height - opts.area[0] - opts.area[2])/opts.categories.length; for (let i = 0; i < opts.categories.length; i++) { yAxisPoints.push(opts.area[0] + eachSpacing / 2 + eachSpacing * i); } let columnOption = assign({}, { type: 'group', width: eachSpacing / 2, meterBorder: 4, meterFillColor: '#FFFFFF', barBorderCircle: false, barBorderRadius: [], seriesGap: 2, linearType: 'none', linearOpacity: 1, customColor: [], colorStop: 0, }, opts.extra.bar); let calPoints = []; context.save(); let leftNum = -2; let rightNum = yAxisPoints.length + 2; if (opts.tooltip && opts.tooltip.textList && opts.tooltip.textList.length && process === 1) { drawBarToolTipSplitArea(opts.tooltip.offset.y, opts, config, context, eachSpacing); } columnOption.customColor = fillCustomColor(columnOption.linearType, columnOption.customColor, series, config); series.forEach(function(eachSeries, seriesIndex) { let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.xAxisData.ranges); maxRange = ranges.pop(); minRange = ranges.shift(); var data = eachSeries.data; switch (columnOption.type) { case 'group': var points = getBarDataPoints(data, minRange, maxRange, yAxisPoints, eachSpacing, opts, config, process); var tooltipPoints = getBarStackDataPoints(data, minRange, maxRange, yAxisPoints, eachSpacing, opts, config, seriesIndex, series, process); calPoints.push(tooltipPoints); points = fixBarData(points, eachSpacing, series.length, seriesIndex, config, opts); for (let i = 0; i < points.length; i++) { let item = points[i]; //fix issues/I27B1N yyoinge & Joeshu if (item !== null && i > leftNum && i < rightNum) { //var startX = item.x - item.width / 2; var startX = opts.area[3]; var startY = item.y - item.width / 2; var height = item.height; context.beginPath(); var fillColor = item.color || eachSeries.color var strokeColor = item.color || eachSeries.color if (columnOption.linearType !== 'none') { var grd = context.createLinearGradient(startX, item.y, item.x, item.y); //透明渐变 if (columnOption.linearType == 'opacity') { grd.addColorStop(0, hexToRgb(fillColor, columnOption.linearOpacity)); grd.addColorStop(1, hexToRgb(fillColor, 1)); } else { grd.addColorStop(0, hexToRgb(columnOption.customColor[eachSeries.linearIndex], columnOption.linearOpacity)); grd.addColorStop(columnOption.colorStop, hexToRgb(columnOption.customColor[eachSeries.linearIndex],columnOption.linearOpacity)); grd.addColorStop(1, hexToRgb(fillColor, 1)); } fillColor = grd } // 圆角边框 if ((columnOption.barBorderRadius && columnOption.barBorderRadius.length === 4) || columnOption.barBorderCircle === true) { const left = startX; const width = item.width; const top = item.y - item.width / 2; const height = item.height; if (columnOption.barBorderCircle) { columnOption.barBorderRadius = [width / 2, width / 2, 0, 0]; } let [r0, r1, r2, r3] = columnOption.barBorderRadius; let minRadius = Math.min(width/2,height/2); r0 = r0 > minRadius ? minRadius : r0; r1 = r1 > minRadius ? minRadius : r1; r2 = r2 > minRadius ? minRadius : r2; r3 = r3 > minRadius ? minRadius : r3; r0 = r0 < 0 ? 0 : r0; r1 = r1 < 0 ? 0 : r1; r2 = r2 < 0 ? 0 : r2; r3 = r3 < 0 ? 0 : r3; context.arc(left + r3, top + r3, r3, -Math.PI, -Math.PI / 2); context.arc(item.x - r0, top + r0, r0, -Math.PI / 2, 0); context.arc(item.x - r1, top + width - r1, r1, 0, Math.PI / 2); context.arc(left + r2, top + width - r2, r2, Math.PI / 2, Math.PI); } else { context.moveTo(startX, startY); context.lineTo(item.x, startY); context.lineTo(item.x, startY + item.width); context.lineTo(startX, startY + item.width); context.lineTo(startX, startY); context.setLineWidth(1) context.setStrokeStyle(strokeColor); } context.setFillStyle(fillColor); context.closePath(); //context.stroke(); context.fill(); } }; break; case 'stack': // 绘制堆叠数据图 var points = getBarStackDataPoints(data, minRange, maxRange, yAxisPoints, eachSpacing, opts, config, seriesIndex, series, process); calPoints.push(points); points = fixBarStackData(points, eachSpacing, series.length, seriesIndex, config, opts, series); for (let i = 0; i < points.length; i++) { let item = points[i]; if (item !== null && i > leftNum && i < rightNum) { context.beginPath(); var fillColor = item.color || eachSeries.color; var startX = item.x0; context.setFillStyle(fillColor); context.moveTo(startX, item.y - item.width/2); context.fillRect(startX, item.y - item.width/2, item.height , item.width); context.closePath(); context.fill(); } }; break; } }); if (opts.dataLabel !== false && process === 1) { series.forEach(function(eachSeries, seriesIndex) { let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.xAxisData.ranges); maxRange = ranges.pop(); minRange = ranges.shift(); var data = eachSeries.data; switch (columnOption.type) { case 'group': var points = getBarDataPoints(data, minRange, maxRange, yAxisPoints, eachSpacing, opts, config, process); points = fixBarData(points, eachSpacing, series.length, seriesIndex, config, opts); drawBarPointText(points, eachSeries, config, context, opts); break; case 'stack': var points = getBarStackDataPoints(data, minRange, maxRange, yAxisPoints, eachSpacing, opts, config, seriesIndex, series, process); drawBarPointText(points, eachSeries, config, context, opts); break; } }); } return { yAxisPoints: yAxisPoints, calPoints: calPoints, eachSpacing: eachSpacing }; } function drawCandleDataPoints(series, seriesMA, opts, config, context) { var process = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 1; var candleOption = assign({}, { color: {}, average: {} }, opts.extra.candle); candleOption.color = assign({}, { upLine: '#f04864', upFill: '#f04864', downLine: '#2fc25b', downFill: '#2fc25b' }, candleOption.color); candleOption.average = assign({}, { show: false, name: [], day: [], color: config.color }, candleOption.average); opts.extra.candle = candleOption; let xAxisData = opts.chartData.xAxisData, xAxisPoints = xAxisData.xAxisPoints, eachSpacing = xAxisData.eachSpacing; let calPoints = []; context.save(); let leftNum = -2; let rightNum = xAxisPoints.length + 2; let leftSpace = 0; let rightSpace = opts.width + eachSpacing; if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { context.translate(opts._scrollDistance_, 0); leftNum = Math.floor(-opts._scrollDistance_ / eachSpacing) - 2; rightNum = leftNum + opts.xAxis.itemCount + 4; leftSpace = -opts._scrollDistance_ - eachSpacing * 2 + opts.area[3]; rightSpace = leftSpace + (opts.xAxis.itemCount + 4) * eachSpacing; } //画均线 if (candleOption.average.show || seriesMA) { //Merge pull request !12 from 邱贵翔 seriesMA.forEach(function(eachSeries, seriesIndex) { let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.yAxisData.ranges[eachSeries.index]); minRange = ranges.pop(); maxRange = ranges.shift(); var data = eachSeries.data; var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); var splitPointList = splitPoints(points,eachSeries); for (let i = 0; i < splitPointList.length; i++) { let points = splitPointList[i]; context.beginPath(); context.setStrokeStyle(eachSeries.color); context.setLineWidth(1); if (points.length === 1) { context.moveTo(points[0].x, points[0].y); context.arc(points[0].x, points[0].y, 1, 0, 2 * Math.PI); } else { context.moveTo(points[0].x, points[0].y); let startPoint = 0; for (let j = 0; j < points.length; j++) { let item = points[j]; if (startPoint == 0 && item.x > leftSpace) { context.moveTo(item.x, item.y); startPoint = 1; } if (j > 0 && item.x > leftSpace && item.x < rightSpace) { var ctrlPoint = createCurveControlPoints(points, j - 1); context.bezierCurveTo(ctrlPoint.ctrA.x, ctrlPoint.ctrA.y, ctrlPoint.ctrB.x, ctrlPoint.ctrB.y, item.x, item.y); } } context.moveTo(points[0].x, points[0].y); } context.closePath(); context.stroke(); } }); } //画K线 series.forEach(function(eachSeries, seriesIndex) { let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.yAxisData.ranges[eachSeries.index]); minRange = ranges.pop(); maxRange = ranges.shift(); var data = eachSeries.data; var points = getCandleDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); calPoints.push(points); var splitPointList = splitPoints(points,eachSeries); for (let i = 0; i < splitPointList[0].length; i++) { if (i > leftNum && i < rightNum) { let item = splitPointList[0][i]; context.beginPath(); //如果上涨 if (data[i][1] - data[i][0] > 0) { context.setStrokeStyle(candleOption.color.upLine); context.setFillStyle(candleOption.color.upFill); context.setLineWidth(1 * opts.pix); context.moveTo(item[3].x, item[3].y); //顶点 context.lineTo(item[1].x, item[1].y); //收盘中间点 context.lineTo(item[1].x - eachSpacing / 4, item[1].y); //收盘左侧点 context.lineTo(item[0].x - eachSpacing / 4, item[0].y); //开盘左侧点 context.lineTo(item[0].x, item[0].y); //开盘中间点 context.lineTo(item[2].x, item[2].y); //底点 context.lineTo(item[0].x, item[0].y); //开盘中间点 context.lineTo(item[0].x + eachSpacing / 4, item[0].y); //开盘右侧点 context.lineTo(item[1].x + eachSpacing / 4, item[1].y); //收盘右侧点 context.lineTo(item[1].x, item[1].y); //收盘中间点 context.moveTo(item[3].x, item[3].y); //顶点 } else { context.setStrokeStyle(candleOption.color.downLine); context.setFillStyle(candleOption.color.downFill); context.setLineWidth(1 * opts.pix); context.moveTo(item[3].x, item[3].y); //顶点 context.lineTo(item[0].x, item[0].y); //开盘中间点 context.lineTo(item[0].x - eachSpacing / 4, item[0].y); //开盘左侧点 context.lineTo(item[1].x - eachSpacing / 4, item[1].y); //收盘左侧点 context.lineTo(item[1].x, item[1].y); //收盘中间点 context.lineTo(item[2].x, item[2].y); //底点 context.lineTo(item[1].x, item[1].y); //收盘中间点 context.lineTo(item[1].x + eachSpacing / 4, item[1].y); //收盘右侧点 context.lineTo(item[0].x + eachSpacing / 4, item[0].y); //开盘右侧点 context.lineTo(item[0].x, item[0].y); //开盘中间点 context.moveTo(item[3].x, item[3].y); //顶点 } context.closePath(); context.fill(); context.stroke(); } } }); context.restore(); return { xAxisPoints: xAxisPoints, calPoints: calPoints, eachSpacing: eachSpacing }; } function drawAreaDataPoints(series, opts, config, context) { var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; var areaOption = assign({}, { type: 'straight', opacity: 0.2, addLine: false, width: 2, gradient: false }, opts.extra.area); let xAxisData = opts.chartData.xAxisData, xAxisPoints = xAxisData.xAxisPoints, eachSpacing = xAxisData.eachSpacing; let endY = opts.height - opts.area[2]; let calPoints = []; context.save(); let leftSpace = 0; let rightSpace = opts.width + eachSpacing; if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { context.translate(opts._scrollDistance_, 0); leftSpace = -opts._scrollDistance_ - eachSpacing * 2 + opts.area[3]; rightSpace = leftSpace + (opts.xAxis.itemCount + 4) * eachSpacing; } series.forEach(function(eachSeries, seriesIndex) { let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.yAxisData.ranges[eachSeries.index]); minRange = ranges.pop(); maxRange = ranges.shift(); let data = eachSeries.data; let points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); calPoints.push(points); let splitPointList = splitPoints(points,eachSeries); for (let i = 0; i < splitPointList.length; i++) { let points = splitPointList[i]; // 绘制区域数 context.beginPath(); context.setStrokeStyle(hexToRgb(eachSeries.color, areaOption.opacity)); if (areaOption.gradient) { let gradient = context.createLinearGradient(0, opts.area[0], 0, opts.height - opts.area[2]); gradient.addColorStop('0', hexToRgb(eachSeries.color, areaOption.opacity)); gradient.addColorStop('1.0', hexToRgb("#FFFFFF", 0.1)); context.setFillStyle(gradient); } else { context.setFillStyle(hexToRgb(eachSeries.color, areaOption.opacity)); } context.setLineWidth(areaOption.width * opts.pix); if (points.length > 1) { let firstPoint = points[0]; let lastPoint = points[points.length - 1]; context.moveTo(firstPoint.x, firstPoint.y); let startPoint = 0; if (areaOption.type === 'curve') { for (let j = 0; j < points.length; j++) { let item = points[j]; if (startPoint == 0 && item.x > leftSpace) { context.moveTo(item.x, item.y); startPoint = 1; } if (j > 0 && item.x > leftSpace && item.x < rightSpace) { let ctrlPoint = createCurveControlPoints(points, j - 1); context.bezierCurveTo(ctrlPoint.ctrA.x, ctrlPoint.ctrA.y, ctrlPoint.ctrB.x, ctrlPoint.ctrB.y, item.x, item.y); } }; } if (areaOption.type === 'straight') { for (let j = 0; j < points.length; j++) { let item = points[j]; if (startPoint == 0 && item.x > leftSpace) { context.moveTo(item.x, item.y); startPoint = 1; } if (j > 0 && item.x > leftSpace && item.x < rightSpace) { context.lineTo(item.x, item.y); } }; } if (areaOption.type === 'step') { for (let j = 0; j < points.length; j++) { let item = points[j]; if (startPoint == 0 && item.x > leftSpace) { context.moveTo(item.x, item.y); startPoint = 1; } if (j > 0 && item.x > leftSpace && item.x < rightSpace) { context.lineTo(item.x, points[j - 1].y); context.lineTo(item.x, item.y); } }; } context.lineTo(lastPoint.x, endY); context.lineTo(firstPoint.x, endY); context.lineTo(firstPoint.x, firstPoint.y); } else { let item = points[0]; context.moveTo(item.x - eachSpacing / 2, item.y); context.lineTo(item.x + eachSpacing / 2, item.y); context.lineTo(item.x + eachSpacing / 2, endY); context.lineTo(item.x - eachSpacing / 2, endY); context.moveTo(item.x - eachSpacing / 2, item.y); } context.closePath(); context.fill(); //画连线 if (areaOption.addLine) { if (eachSeries.lineType == 'dash') { let dashLength = eachSeries.dashLength ? eachSeries.dashLength : 8; dashLength *= opts.pix; context.setLineDash([dashLength, dashLength]); } context.beginPath(); context.setStrokeStyle(eachSeries.color); context.setLineWidth(areaOption.width * opts.pix); if (points.length === 1) { context.moveTo(points[0].x, points[0].y); context.arc(points[0].x, points[0].y, 1, 0, 2 * Math.PI); } else { context.moveTo(points[0].x, points[0].y); let startPoint = 0; if (areaOption.type === 'curve') { for (let j = 0; j < points.length; j++) { let item = points[j]; if (startPoint == 0 && item.x > leftSpace) { context.moveTo(item.x, item.y); startPoint = 1; } if (j > 0 && item.x > leftSpace && item.x < rightSpace) { let ctrlPoint = createCurveControlPoints(points, j - 1); context.bezierCurveTo(ctrlPoint.ctrA.x, ctrlPoint.ctrA.y, ctrlPoint.ctrB.x, ctrlPoint.ctrB.y, item.x, item.y); } }; } if (areaOption.type === 'straight') { for (let j = 0; j < points.length; j++) { let item = points[j]; if (startPoint == 0 && item.x > leftSpace) { context.moveTo(item.x, item.y); startPoint = 1; } if (j > 0 && item.x > leftSpace && item.x < rightSpace) { context.lineTo(item.x, item.y); } }; } if (areaOption.type === 'step') { for (let j = 0; j < points.length; j++) { let item = points[j]; if (startPoint == 0 && item.x > leftSpace) { context.moveTo(item.x, item.y); startPoint = 1; } if (j > 0 && item.x > leftSpace && item.x < rightSpace) { context.lineTo(item.x, points[j - 1].y); context.lineTo(item.x, item.y); } }; } context.moveTo(points[0].x, points[0].y); } context.stroke(); context.setLineDash([]); } } //画点 if (opts.dataPointShape !== false) { drawPointShape(points, eachSeries.color, eachSeries.pointShape, context, opts); } }); if (opts.dataLabel !== false && process === 1) { series.forEach(function(eachSeries, seriesIndex) { let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.yAxisData.ranges[eachSeries.index]); minRange = ranges.pop(); maxRange = ranges.shift(); var data = eachSeries.data; var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); drawPointText(points, eachSeries, config, context, opts); }); } context.restore(); return { xAxisPoints: xAxisPoints, calPoints: calPoints, eachSpacing: eachSpacing }; } function drawScatterDataPoints(series, opts, config, context) { var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; var scatterOption = assign({}, { type: 'circle' }, opts.extra.scatter); let xAxisData = opts.chartData.xAxisData, xAxisPoints = xAxisData.xAxisPoints, eachSpacing = xAxisData.eachSpacing; var calPoints = []; context.save(); let leftSpace = 0; let rightSpace = opts.width + eachSpacing; if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { context.translate(opts._scrollDistance_, 0); leftSpace = -opts._scrollDistance_ - eachSpacing * 2 + opts.area[3]; rightSpace = leftSpace + (opts.xAxis.itemCount + 4) * eachSpacing; } series.forEach(function(eachSeries, seriesIndex) { let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.yAxisData.ranges[eachSeries.index]); minRange = ranges.pop(); maxRange = ranges.shift(); var data = eachSeries.data; var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); context.beginPath(); context.setStrokeStyle(eachSeries.color); context.setFillStyle(eachSeries.color); context.setLineWidth(1 * opts.pix); var shape = eachSeries.pointShape; if (shape === 'diamond') { points.forEach(function(item, index) { if (item !== null) { context.moveTo(item.x, item.y - 4.5); context.lineTo(item.x - 4.5, item.y); context.lineTo(item.x, item.y + 4.5); context.lineTo(item.x + 4.5, item.y); context.lineTo(item.x, item.y - 4.5); } }); } else if (shape === 'circle') { points.forEach(function(item, index) { if (item !== null) { context.moveTo(item.x + 2.5 * opts.pix, item.y); context.arc(item.x, item.y, 3 * opts.pix, 0, 2 * Math.PI, false); } }); } else if (shape === 'square') { points.forEach(function(item, index) { if (item !== null) { context.moveTo(item.x - 3.5, item.y - 3.5); context.rect(item.x - 3.5, item.y - 3.5, 7, 7); } }); } else if (shape === 'triangle') { points.forEach(function(item, index) { if (item !== null) { context.moveTo(item.x, item.y - 4.5); context.lineTo(item.x - 4.5, item.y + 4.5); context.lineTo(item.x + 4.5, item.y + 4.5); context.lineTo(item.x, item.y - 4.5); } }); } else if (shape === 'triangle') { return; } context.closePath(); context.fill(); context.stroke(); }); if (opts.dataLabel !== false && process === 1) { series.forEach(function(eachSeries, seriesIndex) { let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.yAxisData.ranges[eachSeries.index]); minRange = ranges.pop(); maxRange = ranges.shift(); var data = eachSeries.data; var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); drawPointText(points, eachSeries, config, context, opts); }); } context.restore(); return { xAxisPoints: xAxisPoints, calPoints: calPoints, eachSpacing: eachSpacing }; } function drawBubbleDataPoints(series, opts, config, context) { var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; var bubbleOption = assign({}, { opacity: 1, border:2 }, opts.extra.bubble); let xAxisData = opts.chartData.xAxisData, xAxisPoints = xAxisData.xAxisPoints, eachSpacing = xAxisData.eachSpacing; var calPoints = []; context.save(); let leftSpace = 0; let rightSpace = opts.width + eachSpacing; if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { context.translate(opts._scrollDistance_, 0); leftSpace = -opts._scrollDistance_ - eachSpacing * 2 + opts.area[3]; rightSpace = leftSpace + (opts.xAxis.itemCount + 4) * eachSpacing; } series.forEach(function(eachSeries, seriesIndex) { let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.yAxisData.ranges[eachSeries.index]); minRange = ranges.pop(); maxRange = ranges.shift(); var data = eachSeries.data; var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); context.beginPath(); context.setStrokeStyle(eachSeries.color); context.setLineWidth(bubbleOption.border * opts.pix); context.setFillStyle(hexToRgb(eachSeries.color, bubbleOption.opacity)); points.forEach(function(item, index) { context.moveTo(item.x + item.r, item.y); context.arc(item.x, item.y, item.r * opts.pix, 0, 2 * Math.PI, false); }); context.closePath(); context.fill(); context.stroke(); if (opts.dataLabel !== false && process === 1) { points.forEach(function(item, index) { context.beginPath(); var fontSize = series.textSize * opts.pix || config.fontSize; context.setFontSize(fontSize); context.setFillStyle(series.textColor || "#FFFFFF"); context.setTextAlign('center'); context.fillText(String(item.t), item.x, item.y + fontSize/2); context.closePath(); context.stroke(); context.setTextAlign('left'); }); } }); context.restore(); return { xAxisPoints: xAxisPoints, calPoints: calPoints, eachSpacing: eachSpacing }; } function drawLineDataPoints(series, opts, config, context) { var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; var lineOption = assign({}, { type: 'straight', width: 2 }, opts.extra.line); lineOption.width *= opts.pix; let xAxisData = opts.chartData.xAxisData, xAxisPoints = xAxisData.xAxisPoints, eachSpacing = xAxisData.eachSpacing; var calPoints = []; context.save(); let leftSpace = 0; let rightSpace = opts.width + eachSpacing; if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { context.translate(opts._scrollDistance_, 0); leftSpace = -opts._scrollDistance_ - eachSpacing * 2 + opts.area[3]; rightSpace = leftSpace + (opts.xAxis.itemCount + 4) * eachSpacing; } series.forEach(function(eachSeries, seriesIndex) { let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.yAxisData.ranges[eachSeries.index]); minRange = ranges.pop(); maxRange = ranges.shift(); var data = eachSeries.data; var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); calPoints.push(points); var splitPointList = splitPoints(points,eachSeries); if (eachSeries.lineType == 'dash') { let dashLength = eachSeries.dashLength ? eachSeries.dashLength : 8; dashLength *= opts.pix; context.setLineDash([dashLength, dashLength]); } context.beginPath(); context.setStrokeStyle(eachSeries.color); context.setLineWidth(lineOption.width); splitPointList.forEach(function(points, index) { if (points.length === 1) { context.moveTo(points[0].x, points[0].y); context.arc(points[0].x, points[0].y, 1, 0, 2 * Math.PI); } else { context.moveTo(points[0].x, points[0].y); let startPoint = 0; if (lineOption.type === 'curve') { for (let j = 0; j < points.length; j++) { let item = points[j]; if (startPoint == 0 && item.x > leftSpace) { context.moveTo(item.x, item.y); startPoint = 1; } if (j > 0 && item.x > leftSpace && item.x < rightSpace) { var ctrlPoint = createCurveControlPoints(points, j - 1); context.bezierCurveTo(ctrlPoint.ctrA.x, ctrlPoint.ctrA.y, ctrlPoint.ctrB.x, ctrlPoint.ctrB.y, item.x, item.y); } }; } if (lineOption.type === 'straight') { for (let j = 0; j < points.length; j++) { let item = points[j]; if (startPoint == 0 && item.x > leftSpace) { context.moveTo(item.x, item.y); startPoint = 1; } if (j > 0 && item.x > leftSpace && item.x < rightSpace) { context.lineTo(item.x, item.y); } }; } if (lineOption.type === 'step') { for (let j = 0; j < points.length; j++) { let item = points[j]; if (startPoint == 0 && item.x > leftSpace) { context.moveTo(item.x, item.y); startPoint = 1; } if (j > 0 && item.x > leftSpace && item.x < rightSpace) { context.lineTo(item.x, points[j - 1].y); context.lineTo(item.x, item.y); } }; } context.moveTo(points[0].x, points[0].y); } }); context.stroke(); context.setLineDash([]); if (opts.dataPointShape !== false) { drawPointShape(points, eachSeries.color, eachSeries.pointShape, context, opts); } }); if (opts.dataLabel !== false && process === 1) { series.forEach(function(eachSeries, seriesIndex) { let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.yAxisData.ranges[eachSeries.index]); minRange = ranges.pop(); maxRange = ranges.shift(); var data = eachSeries.data; var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); drawPointText(points, eachSeries, config, context, opts); }); } context.restore(); return { xAxisPoints: xAxisPoints, calPoints: calPoints, eachSpacing: eachSpacing }; } function drawMixDataPoints(series, opts, config, context) { let process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; let xAxisData = opts.chartData.xAxisData, xAxisPoints = xAxisData.xAxisPoints, eachSpacing = xAxisData.eachSpacing; let columnOption = assign({}, { width: eachSpacing / 2, barBorderCircle: false, barBorderRadius: [], seriesGap: 2, linearType: 'none', linearOpacity: 1, customColor: [], colorStop: 0, }, opts.extra.mix.column); let areaOption = assign({}, { opacity: 0.2, gradient: false }, opts.extra.mix.area); let endY = opts.height - opts.area[2]; let calPoints = []; var columnIndex = 0; var columnLength = 0; series.forEach(function(eachSeries, seriesIndex) { if (eachSeries.type == 'column') { columnLength += 1; } }); context.save(); let leftNum = -2; let rightNum = xAxisPoints.length + 2; let leftSpace = 0; let rightSpace = opts.width + eachSpacing; if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { context.translate(opts._scrollDistance_, 0); leftNum = Math.floor(-opts._scrollDistance_ / eachSpacing) - 2; rightNum = leftNum + opts.xAxis.itemCount + 4; leftSpace = -opts._scrollDistance_ - eachSpacing * 2 + opts.area[3]; rightSpace = leftSpace + (opts.xAxis.itemCount + 4) * eachSpacing; } columnOption.customColor = fillCustomColor(columnOption.linearType, columnOption.customColor, series, config); series.forEach(function(eachSeries, seriesIndex) { let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.yAxisData.ranges[eachSeries.index]); minRange = ranges.pop(); maxRange = ranges.shift(); var data = eachSeries.data; var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); calPoints.push(points); // 绘制柱状数据图 if (eachSeries.type == 'column') { points = fixColumeData(points, eachSpacing, columnLength, columnIndex, config, opts); for (let i = 0; i < points.length; i++) { let item = points[i]; if (item !== null && i > leftNum && i < rightNum) { var startX = item.x - item.width / 2; var height = opts.height - item.y - opts.area[2]; context.beginPath(); var fillColor = item.color || eachSeries.color var strokeColor = item.color || eachSeries.color if (columnOption.linearType !== 'none') { var grd = context.createLinearGradient(startX, item.y, startX, opts.height - opts.area[2]); //透明渐变 if (columnOption.linearType == 'opacity') { grd.addColorStop(0, hexToRgb(fillColor, columnOption.linearOpacity)); grd.addColorStop(1, hexToRgb(fillColor, 1)); } else { grd.addColorStop(0, hexToRgb(columnOption.customColor[eachSeries.linearIndex], columnOption.linearOpacity)); grd.addColorStop(columnOption.colorStop, hexToRgb(columnOption.customColor[eachSeries.linearIndex], columnOption.linearOpacity)); grd.addColorStop(1, hexToRgb(fillColor, 1)); } fillColor = grd } // 圆角边框 if ((columnOption.barBorderRadius && columnOption.barBorderRadius.length === 4) || columnOption.barBorderCircle) { const left = startX; const top = item.y; const width = item.width; const height = opts.height - opts.area[2] - item.y; if (columnOption.barBorderCircle) { columnOption.barBorderRadius = [width / 2, width / 2, 0, 0]; } let [r0, r1, r2, r3] = columnOption.barBorderRadius; let minRadius = Math.min(width/2,height/2); r0 = r0 > minRadius ? minRadius : r0; r1 = r1 > minRadius ? minRadius : r1; r2 = r2 > minRadius ? minRadius : r2; r3 = r3 > minRadius ? minRadius : r3; r0 = r0 < 0 ? 0 : r0; r1 = r1 < 0 ? 0 : r1; r2 = r2 < 0 ? 0 : r2; r3 = r3 < 0 ? 0 : r3; context.arc(left + r0, top + r0, r0, -Math.PI, -Math.PI / 2); context.arc(left + width - r1, top + r1, r1, -Math.PI / 2, 0); context.arc(left + width - r2, top + height - r2, r2, 0, Math.PI / 2); context.arc(left + r3, top + height - r3, r3, Math.PI / 2, Math.PI); } else { context.moveTo(startX, item.y); context.lineTo(startX + item.width, item.y); context.lineTo(startX + item.width, opts.height - opts.area[2]); context.lineTo(startX, opts.height - opts.area[2]); context.lineTo(startX, item.y); context.setLineWidth(1) context.setStrokeStyle(strokeColor); } context.setFillStyle(fillColor); context.closePath(); context.fill(); } } columnIndex += 1; } //绘制区域图数据 if (eachSeries.type == 'area') { let splitPointList = splitPoints(points,eachSeries); for (let i = 0; i < splitPointList.length; i++) { let points = splitPointList[i]; // 绘制区域数据 context.beginPath(); context.setStrokeStyle(eachSeries.color); context.setStrokeStyle(hexToRgb(eachSeries.color, areaOption.opacity)); if (areaOption.gradient) { let gradient = context.createLinearGradient(0, opts.area[0], 0, opts.height - opts.area[2]); gradient.addColorStop('0', hexToRgb(eachSeries.color, areaOption.opacity)); gradient.addColorStop('1.0', hexToRgb("#FFFFFF", 0.1)); context.setFillStyle(gradient); } else { context.setFillStyle(hexToRgb(eachSeries.color, areaOption.opacity)); } context.setLineWidth(2 * opts.pix); if (points.length > 1) { var firstPoint = points[0]; let lastPoint = points[points.length - 1]; context.moveTo(firstPoint.x, firstPoint.y); let startPoint = 0; if (eachSeries.style === 'curve') { for (let j = 0; j < points.length; j++) { let item = points[j]; if (startPoint == 0 && item.x > leftSpace) { context.moveTo(item.x, item.y); startPoint = 1; } if (j > 0 && item.x > leftSpace && item.x < rightSpace) { var ctrlPoint = createCurveControlPoints(points, j - 1); context.bezierCurveTo(ctrlPoint.ctrA.x, ctrlPoint.ctrA.y, ctrlPoint.ctrB.x, ctrlPoint.ctrB.y, item.x, item.y); } }; } else { for (let j = 0; j < points.length; j++) { let item = points[j]; if (startPoint == 0 && item.x > leftSpace) { context.moveTo(item.x, item.y); startPoint = 1; } if (j > 0 && item.x > leftSpace && item.x < rightSpace) { context.lineTo(item.x, item.y); } }; } context.lineTo(lastPoint.x, endY); context.lineTo(firstPoint.x, endY); context.lineTo(firstPoint.x, firstPoint.y); } else { let item = points[0]; context.moveTo(item.x - eachSpacing / 2, item.y); context.lineTo(item.x + eachSpacing / 2, item.y); context.lineTo(item.x + eachSpacing / 2, endY); context.lineTo(item.x - eachSpacing / 2, endY); context.moveTo(item.x - eachSpacing / 2, item.y); } context.closePath(); context.fill(); } } // 绘制折线数据图 if (eachSeries.type == 'line') { var splitPointList = splitPoints(points,eachSeries); splitPointList.forEach(function(points, index) { if (eachSeries.lineType == 'dash') { let dashLength = eachSeries.dashLength ? eachSeries.dashLength : 8; dashLength *= opts.pix; context.setLineDash([dashLength, dashLength]); } context.beginPath(); context.setStrokeStyle(eachSeries.color); context.setLineWidth(2 * opts.pix); if (points.length === 1) { context.moveTo(points[0].x, points[0].y); context.arc(points[0].x, points[0].y, 1, 0, 2 * Math.PI); } else { context.moveTo(points[0].x, points[0].y); let startPoint = 0; if (eachSeries.style == 'curve') { for (let j = 0; j < points.length; j++) { let item = points[j]; if (startPoint == 0 && item.x > leftSpace) { context.moveTo(item.x, item.y); startPoint = 1; } if (j > 0 && item.x > leftSpace && item.x < rightSpace) { var ctrlPoint = createCurveControlPoints(points, j - 1); context.bezierCurveTo(ctrlPoint.ctrA.x, ctrlPoint.ctrA.y, ctrlPoint.ctrB.x, ctrlPoint.ctrB.y, item.x, item.y); } } } else { for (let j = 0; j < points.length; j++) { let item = points[j]; if (startPoint == 0 && item.x > leftSpace) { context.moveTo(item.x, item.y); startPoint = 1; } if (j > 0 && item.x > leftSpace && item.x < rightSpace) { context.lineTo(item.x, item.y); } } } context.moveTo(points[0].x, points[0].y); } context.stroke(); context.setLineDash([]); }); } // 绘制点数据图 if (eachSeries.type == 'point') { eachSeries.addPoint = true; } if (eachSeries.addPoint == true && eachSeries.type !== 'column') { drawPointShape(points, eachSeries.color, eachSeries.pointShape, context, opts); } }); if (opts.dataLabel !== false && process === 1) { var columnIndex = 0; series.forEach(function(eachSeries, seriesIndex) { let ranges, minRange, maxRange; ranges = [].concat(opts.chartData.yAxisData.ranges[eachSeries.index]); minRange = ranges.pop(); maxRange = ranges.shift(); var data = eachSeries.data; var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process); if (eachSeries.type !== 'column') { drawPointText(points, eachSeries, config, context, opts); } else { points = fixColumeData(points, eachSpacing, columnLength, columnIndex, config, opts); drawPointText(points, eachSeries, config, context, opts); columnIndex += 1; } }); } context.restore(); return { xAxisPoints: xAxisPoints, calPoints: calPoints, eachSpacing: eachSpacing, } } function drawToolTipBridge(opts, config, context, process, eachSpacing, xAxisPoints) { var toolTipOption = opts.extra.tooltip || {}; if (toolTipOption.horizentalLine && opts.tooltip && process === 1 && (opts.type == 'line' || opts.type == 'area' || opts.type == 'column' || opts.type == 'mount' || opts.type == 'candle' || opts.type == 'mix')) { drawToolTipHorizentalLine(opts, config, context, eachSpacing, xAxisPoints) } context.save(); if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) { context.translate(opts._scrollDistance_, 0); } if (opts.tooltip && opts.tooltip.textList && opts.tooltip.textList.length && process === 1) { drawToolTip(opts.tooltip.textList, opts.tooltip.offset, opts, config, context, eachSpacing, xAxisPoints); } context.restore(); } function drawXAxis(categories, opts, config, context) { let xAxisData = opts.chartData.xAxisData, xAxisPoints = xAxisData.xAxisPoints, startX = xAxisData.startX, endX = xAxisData.endX, eachSpacing = xAxisData.eachSpacing; var boundaryGap = 'center'; if (opts.type == 'bar' || opts.type == 'line' || opts.type == 'area'|| opts.type == 'scatter' || opts.type == 'bubble') { boundaryGap = opts.xAxis.boundaryGap; } var startY = opts.height - opts.area[2]; var endY = opts.area[0]; //绘制滚动条 if (opts.enableScroll && opts.xAxis.scrollShow) { var scrollY = opts.height - opts.area[2] + config.xAxisHeight; var scrollScreenWidth = endX - startX; var scrollTotalWidth = eachSpacing * (xAxisPoints.length - 1); if(opts.type == 'mount' && opts.extra && opts.extra.mount && opts.extra.mount.widthRatio && opts.extra.mount.widthRatio > 1){ if(opts.extra.mount.widthRatio>2) opts.extra.mount.widthRatio = 2 scrollTotalWidth += (opts.extra.mount.widthRatio - 1)*eachSpacing; } var scrollWidth = scrollScreenWidth * scrollScreenWidth / scrollTotalWidth; var scrollLeft = 0; if (opts._scrollDistance_) { scrollLeft = -opts._scrollDistance_ * (scrollScreenWidth) / scrollTotalWidth; } context.beginPath(); context.setLineCap('round'); context.setLineWidth(6 * opts.pix); context.setStrokeStyle(opts.xAxis.scrollBackgroundColor || "#EFEBEF"); context.moveTo(startX, scrollY); context.lineTo(endX, scrollY); context.stroke(); context.closePath(); context.beginPath(); context.setLineCap('round'); context.setLineWidth(6 * opts.pix); context.setStrokeStyle(opts.xAxis.scrollColor || "#A6A6A6"); context.moveTo(startX + scrollLeft, scrollY); context.lineTo(startX + scrollLeft + scrollWidth, scrollY); context.stroke(); context.closePath(); context.setLineCap('butt'); } context.save(); if (opts._scrollDistance_ && opts._scrollDistance_ !== 0) { context.translate(opts._scrollDistance_, 0); } //绘制X轴刻度线 if (opts.xAxis.calibration === true) { context.setStrokeStyle(opts.xAxis.gridColor || "#cccccc"); context.setLineCap('butt'); context.setLineWidth(1 * opts.pix); xAxisPoints.forEach(function(item, index) { if (index > 0) { context.beginPath(); context.moveTo(item - eachSpacing / 2, startY); context.lineTo(item - eachSpacing / 2, startY + 3 * opts.pix); context.closePath(); context.stroke(); } }); } //绘制X轴网格 if (opts.xAxis.disableGrid !== true) { context.setStrokeStyle(opts.xAxis.gridColor || "#cccccc"); context.setLineCap('butt'); context.setLineWidth(1 * opts.pix); if (opts.xAxis.gridType == 'dash') { context.setLineDash([opts.xAxis.dashLength * opts.pix, opts.xAxis.dashLength * opts.pix]); } opts.xAxis.gridEval = opts.xAxis.gridEval || 1; xAxisPoints.forEach(function(item, index) { if (index % opts.xAxis.gridEval == 0) { context.beginPath(); context.moveTo(item, startY); context.lineTo(item, endY); context.stroke(); } }); context.setLineDash([]); } //绘制X轴文案 if (opts.xAxis.disabled !== true) { // 对X轴列表做抽稀处理 //默认全部显示X轴标签 let maxXAxisListLength = categories.length; //如果设置了X轴单屏数量 if (opts.xAxis.labelCount) { //如果设置X轴密度 if (opts.xAxis.itemCount) { maxXAxisListLength = Math.ceil(categories.length / opts.xAxis.itemCount * opts.xAxis.labelCount); } else { maxXAxisListLength = opts.xAxis.labelCount; } maxXAxisListLength -= 1; } let ratio = Math.ceil(categories.length / maxXAxisListLength); let newCategories = []; let cgLength = categories.length; for (let i = 0; i < cgLength; i++) { if (i % ratio !== 0) { newCategories.push(""); } else { newCategories.push(categories[i]); } } newCategories[cgLength - 1] = categories[cgLength - 1]; var xAxisFontSize = opts.xAxis.fontSize * opts.pix || config.fontSize; if (config._xAxisTextAngle_ === 0) { newCategories.forEach(function(item, index) { var xitem = opts.xAxis.formatter ? opts.xAxis.formatter(item,index,opts) : item; var offset = -measureText(String(xitem), xAxisFontSize, context) / 2; if (boundaryGap == 'center') { offset += eachSpacing / 2; } var scrollHeight = 0; if (opts.xAxis.scrollShow) { scrollHeight = 6 * opts.pix; } context.beginPath(); context.setFontSize(xAxisFontSize); context.setFillStyle(opts.xAxis.fontColor || opts.fontColor); context.fillText(String(xitem), xAxisPoints[index] + offset, startY + xAxisFontSize + (config.xAxisHeight - scrollHeight - xAxisFontSize) / 2); context.closePath(); context.stroke(); }); } else { newCategories.forEach(function(item, index) { var xitem = opts.xAxis.formatter ? opts.xAxis.formatter(item) : item; context.save(); context.beginPath(); context.setFontSize(xAxisFontSize); context.setFillStyle(opts.xAxis.fontColor || opts.fontColor); var textWidth = measureText(String(xitem), xAxisFontSize, context); var offsetX = xAxisPoints[index]; if (boundaryGap == 'center') { offsetX = xAxisPoints[index] + eachSpacing / 2; } var scrollHeight = 0; if (opts.xAxis.scrollShow) { scrollHeight = 6 * opts.pix; } var offsetY = startY + 6 * opts.pix + xAxisFontSize - xAxisFontSize * Math.abs(Math.sin(config._xAxisTextAngle_)); if(opts.xAxis.rotateAngle < 0){ offsetX -= xAxisFontSize / 2; textWidth = 0; }else{ offsetX += xAxisFontSize / 2; textWidth = -textWidth; } context.translate(offsetX, offsetY); context.rotate(-1 * config._xAxisTextAngle_); context.fillText(String(xitem), textWidth , 0 ); context.closePath(); context.stroke(); context.restore(); }); } } context.restore(); //绘制X轴轴线 if (opts.xAxis.axisLine) { context.beginPath(); context.setStrokeStyle(opts.xAxis.axisLineColor); context.setLineWidth(1 * opts.pix); context.moveTo(startX, opts.height - opts.area[2]); context.lineTo(endX, opts.height - opts.area[2]); context.stroke(); } } function drawYAxisGrid(categories, opts, config, context) { if (opts.yAxis.disableGrid === true) { return; } let spacingValid = opts.height - opts.area[0] - opts.area[2]; let eachSpacing = spacingValid / opts.yAxis.splitNumber; let startX = opts.area[3]; let xAxisPoints = opts.chartData.xAxisData.xAxisPoints, xAxiseachSpacing = opts.chartData.xAxisData.eachSpacing; let TotalWidth = xAxiseachSpacing * (xAxisPoints.length - 1); if(opts.type == 'mount' && opts.extra && opts.extra.mount && opts.extra.mount.widthRatio && opts.extra.mount.widthRatio > 1 ){ if(opts.extra.mount.widthRatio>2) opts.extra.mount.widthRatio = 2 TotalWidth += (opts.extra.mount.widthRatio - 1)*xAxiseachSpacing; } let endX = startX + TotalWidth; let points = []; let startY = 1 if (opts.xAxis.axisLine === false) { startY = 0 } for (let i = startY; i < opts.yAxis.splitNumber + 1; i++) { points.push(opts.height - opts.area[2] - eachSpacing * i); } context.save(); if (opts._scrollDistance_ && opts._scrollDistance_ !== 0) { context.translate(opts._scrollDistance_, 0); } if (opts.yAxis.gridType == 'dash') { context.setLineDash([opts.yAxis.dashLength * opts.pix, opts.yAxis.dashLength * opts.pix]); } context.setStrokeStyle(opts.yAxis.gridColor); context.setLineWidth(1 * opts.pix); points.forEach(function(item, index) { context.beginPath(); context.moveTo(startX, item); context.lineTo(endX, item); context.stroke(); }); context.setLineDash([]); context.restore(); } function drawYAxis(series, opts, config, context) { if (opts.yAxis.disabled === true) { return; } var spacingValid = opts.height - opts.area[0] - opts.area[2]; var eachSpacing = spacingValid / opts.yAxis.splitNumber; var startX = opts.area[3]; var endX = opts.width - opts.area[1]; var endY = opts.height - opts.area[2]; var fillEndY = endY + config.xAxisHeight; if (opts.xAxis.scrollShow) { fillEndY -= 3 * opts.pix; } if (opts.xAxis.rotateLabel) { fillEndY = opts.height - opts.area[2] + opts.fontSize * opts.pix / 2; } // set YAxis background context.beginPath(); context.setFillStyle(opts.background); if (opts.enableScroll == true && opts.xAxis.scrollPosition && opts.xAxis.scrollPosition !== 'left') { context.fillRect(0, 0, startX, fillEndY); } if (opts.enableScroll == true && opts.xAxis.scrollPosition && opts.xAxis.scrollPosition !== 'right') { context.fillRect(endX, 0, opts.width, fillEndY); } context.closePath(); context.stroke(); let tStartLeft = opts.area[3]; let tStartRight = opts.width - opts.area[1]; let tStartCenter = opts.area[3] + (opts.width - opts.area[1] - opts.area[3]) / 2; if (opts.yAxis.data) { for (let i = 0; i < opts.yAxis.data.length; i++) { let yData = opts.yAxis.data[i]; var points = []; if(yData.type === 'categories'){ for (let i = 0; i <= yData.categories.length; i++) { points.push(opts.area[0] + spacingValid / yData.categories.length / 2 + spacingValid / yData.categories.length * i); } }else{ for (let i = 0; i <= opts.yAxis.splitNumber; i++) { points.push(opts.area[0] + eachSpacing * i); } } if (yData.disabled !== true) { let rangesFormat = opts.chartData.yAxisData.rangesFormat[i]; let yAxisFontSize = yData.fontSize ? yData.fontSize * opts.pix : config.fontSize; let yAxisWidth = opts.chartData.yAxisData.yAxisWidth[i]; let textAlign = yData.textAlign || "right"; //画Y轴刻度及文案 rangesFormat.forEach(function(item, index) { var pos = points[index]; context.beginPath(); context.setFontSize(yAxisFontSize); context.setLineWidth(1 * opts.pix); context.setStrokeStyle(yData.axisLineColor || '#cccccc'); context.setFillStyle(yData.fontColor || opts.fontColor); let tmpstrat = 0; let gapwidth = 4 * opts.pix; if (yAxisWidth.position == 'left') { //画刻度线 if (yData.calibration == true) { context.moveTo(tStartLeft, pos); context.lineTo(tStartLeft - 3 * opts.pix, pos); gapwidth += 3 * opts.pix; } //画文字 switch (textAlign) { case "left": context.setTextAlign('left'); tmpstrat = tStartLeft - yAxisWidth.width break; case "right": context.setTextAlign('right'); tmpstrat = tStartLeft - gapwidth break; default: context.setTextAlign('center'); tmpstrat = tStartLeft - yAxisWidth.width / 2 } context.fillText(String(item), tmpstrat, pos + yAxisFontSize / 2 - 3 * opts.pix); } else if (yAxisWidth.position == 'right') { //画刻度线 if (yData.calibration == true) { context.moveTo(tStartRight, pos); context.lineTo(tStartRight + 3 * opts.pix, pos); gapwidth += 3 * opts.pix; } switch (textAlign) { case "left": context.setTextAlign('left'); tmpstrat = tStartRight + gapwidth break; case "right": context.setTextAlign('right'); tmpstrat = tStartRight + yAxisWidth.width break; default: context.setTextAlign('center'); tmpstrat = tStartRight + yAxisWidth.width / 2 } context.fillText(String(item), tmpstrat, pos + yAxisFontSize / 2 - 3 * opts.pix); } else if (yAxisWidth.position == 'center') { //画刻度线 if (yData.calibration == true) { context.moveTo(tStartCenter, pos); context.lineTo(tStartCenter - 3 * opts.pix, pos); gapwidth += 3 * opts.pix; } //画文字 switch (textAlign) { case "left": context.setTextAlign('left'); tmpstrat = tStartCenter - yAxisWidth.width break; case "right": context.setTextAlign('right'); tmpstrat = tStartCenter - gapwidth break; default: context.setTextAlign('center'); tmpstrat = tStartCenter - yAxisWidth.width / 2 } context.fillText(String(item), tmpstrat, pos + yAxisFontSize / 2 - 3 * opts.pix); } context.closePath(); context.stroke(); context.setTextAlign('left'); }); //画Y轴轴线 if (yData.axisLine !== false) { context.beginPath(); context.setStrokeStyle(yData.axisLineColor || '#cccccc'); context.setLineWidth(1 * opts.pix); if (yAxisWidth.position == 'left') { context.moveTo(tStartLeft, opts.height - opts.area[2]); context.lineTo(tStartLeft, opts.area[0]); } else if (yAxisWidth.position == 'right') { context.moveTo(tStartRight, opts.height - opts.area[2]); context.lineTo(tStartRight, opts.area[0]); } else if (yAxisWidth.position == 'center') { context.moveTo(tStartCenter, opts.height - opts.area[2]); context.lineTo(tStartCenter, opts.area[0]); } context.stroke(); } //画Y轴标题 if (opts.yAxis.showTitle) { let titleFontSize = yData.titleFontSize * opts.pix || config.fontSize; let title = yData.title; context.beginPath(); context.setFontSize(titleFontSize); context.setFillStyle(yData.titleFontColor || opts.fontColor); if (yAxisWidth.position == 'left') { context.fillText(title, tStartLeft - measureText(title, titleFontSize, context) / 2 + (yData.titleOffsetX || 0), opts.area[0] - (10 - (yData.titleOffsetY || 0)) * opts.pix); } else if (yAxisWidth.position == 'right') { context.fillText(title, tStartRight - measureText(title, titleFontSize, context) / 2 + (yData.titleOffsetX || 0), opts.area[0] - (10 - (yData.titleOffsetY || 0)) * opts.pix); } else if (yAxisWidth.position == 'center') { context.fillText(title, tStartCenter - measureText(title, titleFontSize, context) / 2 + (yData.titleOffsetX || 0), opts.area[0] - (10 - (yData.titleOffsetY || 0)) * opts.pix); } context.closePath(); context.stroke(); } if (yAxisWidth.position == 'left') { tStartLeft -= (yAxisWidth.width + opts.yAxis.padding * opts.pix); } else { tStartRight += yAxisWidth.width + opts.yAxis.padding * opts.pix; } } } } } function drawLegend(series, opts, config, context, chartData) { if (opts.legend.show === false) { return; } let legendData = chartData.legendData; let legendList = legendData.points; let legendArea = legendData.area; let padding = opts.legend.padding * opts.pix; let fontSize = opts.legend.fontSize * opts.pix; let shapeWidth = 15 * opts.pix; let shapeRight = 5 * opts.pix; let itemGap = opts.legend.itemGap * opts.pix; let lineHeight = Math.max(opts.legend.lineHeight * opts.pix, fontSize); //画背景及边框 context.beginPath(); context.setLineWidth(opts.legend.borderWidth * opts.pix); context.setStrokeStyle(opts.legend.borderColor); context.setFillStyle(opts.legend.backgroundColor); context.moveTo(legendArea.start.x, legendArea.start.y); context.rect(legendArea.start.x, legendArea.start.y, legendArea.width, legendArea.height); context.closePath(); context.fill(); context.stroke(); legendList.forEach(function(itemList, listIndex) { let width = 0; let height = 0; width = legendData.widthArr[listIndex]; height = legendData.heightArr[listIndex]; let startX = 0; let startY = 0; if (opts.legend.position == 'top' || opts.legend.position == 'bottom') { switch (opts.legend.float) { case 'left': startX = legendArea.start.x + padding; break; case 'right': startX = legendArea.start.x + legendArea.width - width; break; default: startX = legendArea.start.x + (legendArea.width - width) / 2; } startY = legendArea.start.y + padding + listIndex * lineHeight; } else { if (listIndex == 0) { width = 0; } else { width = legendData.widthArr[listIndex - 1]; } startX = legendArea.start.x + padding + width; startY = legendArea.start.y + padding + (legendArea.height - height) / 2; } context.setFontSize(config.fontSize); for (let i = 0; i < itemList.length; i++) { let item = itemList[i]; item.area = [0, 0, 0, 0]; item.area[0] = startX; item.area[1] = startY; item.area[3] = startY + lineHeight; context.beginPath(); context.setLineWidth(1 * opts.pix); context.setStrokeStyle(item.show ? item.color : opts.legend.hiddenColor); context.setFillStyle(item.show ? item.color : opts.legend.hiddenColor); switch (item.legendShape) { case 'line': context.moveTo(startX, startY + 0.5 * lineHeight - 2 * opts.pix); context.fillRect(startX, startY + 0.5 * lineHeight - 2 * opts.pix, 15 * opts.pix, 4 * opts.pix); break; case 'triangle': context.moveTo(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight - 5 * opts.pix); context.lineTo(startX + 2.5 * opts.pix, startY + 0.5 * lineHeight + 5 * opts.pix); context.lineTo(startX + 12.5 * opts.pix, startY + 0.5 * lineHeight + 5 * opts.pix); context.lineTo(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight - 5 * opts.pix); break; case 'diamond': context.moveTo(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight - 5 * opts.pix); context.lineTo(startX + 2.5 * opts.pix, startY + 0.5 * lineHeight); context.lineTo(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight + 5 * opts.pix); context.lineTo(startX + 12.5 * opts.pix, startY + 0.5 * lineHeight); context.lineTo(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight - 5 * opts.pix); break; case 'circle': context.moveTo(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight); context.arc(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight, 5 * opts.pix, 0, 2 * Math.PI); break; case 'rect': context.moveTo(startX, startY + 0.5 * lineHeight - 5 * opts.pix); context.fillRect(startX, startY + 0.5 * lineHeight - 5 * opts.pix, 15 * opts.pix, 10 * opts.pix); break; case 'square': context.moveTo(startX + 5 * opts.pix, startY + 0.5 * lineHeight - 5 * opts.pix); context.fillRect(startX + 5 * opts.pix, startY + 0.5 * lineHeight - 5 * opts.pix, 10 * opts.pix, 10 * opts.pix); break; case 'none': break; default: context.moveTo(startX, startY + 0.5 * lineHeight - 5 * opts.pix); context.fillRect(startX, startY + 0.5 * lineHeight - 5 * opts.pix, 15 * opts.pix, 10 * opts.pix); } context.closePath(); context.fill(); context.stroke(); startX += shapeWidth + shapeRight; let fontTrans = 0.5 * lineHeight + 0.5 * fontSize - 2; const legendText = item.legendText ? item.legendText : item.name; context.beginPath(); context.setFontSize(fontSize); context.setFillStyle(item.show ? opts.legend.fontColor : opts.legend.hiddenColor); context.fillText(legendText, startX, startY + fontTrans); context.closePath(); context.stroke(); if (opts.legend.position == 'top' || opts.legend.position == 'bottom') { startX += measureText(legendText, fontSize, context) + itemGap; item.area[2] = startX; } else { item.area[2] = startX + measureText(legendText, fontSize, context) + itemGap;; startX -= shapeWidth + shapeRight; startY += lineHeight; } } }); } function drawPieDataPoints(series, opts, config, context) { var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; var pieOption = assign({}, { activeOpacity: 0.5, activeRadius: 10, offsetAngle: 0, labelWidth: 15, ringWidth: 30, customRadius: 0, border: false, borderWidth: 2, borderColor: '#FFFFFF', centerColor: '#FFFFFF', linearType: 'none', customColor: [], }, opts.type == "pie" ? opts.extra.pie : opts.extra.ring); var centerPosition = { x: opts.area[3] + (opts.width - opts.area[1] - opts.area[3]) / 2, y: opts.area[0] + (opts.height - opts.area[0] - opts.area[2]) / 2 }; if (config.pieChartLinePadding == 0) { config.pieChartLinePadding = pieOption.activeRadius * opts.pix; } var radius = Math.min((opts.width - opts.area[1] - opts.area[3]) / 2 - config.pieChartLinePadding - config.pieChartTextPadding - config._pieTextMaxLength_, (opts.height - opts.area[0] - opts.area[2]) / 2 - config.pieChartLinePadding - config.pieChartTextPadding); radius = radius < 10 ? 10 : radius; if (pieOption.customRadius > 0) { radius = pieOption.customRadius * opts.pix; } series = getPieDataPoints(series, radius, process); var activeRadius = pieOption.activeRadius * opts.pix; pieOption.customColor = fillCustomColor(pieOption.linearType, pieOption.customColor, series, config); series = series.map(function(eachSeries) { eachSeries._start_ += (pieOption.offsetAngle) * Math.PI / 180; return eachSeries; }); series.forEach(function(eachSeries, seriesIndex) { if (opts.tooltip) { if (opts.tooltip.index == seriesIndex) { context.beginPath(); context.setFillStyle(hexToRgb(eachSeries.color, pieOption.activeOpacity || 0.5)); context.moveTo(centerPosition.x, centerPosition.y); context.arc(centerPosition.x, centerPosition.y, eachSeries._radius_ + activeRadius, eachSeries._start_, eachSeries._start_ + 2 * eachSeries._proportion_ * Math.PI); context.closePath(); context.fill(); } } context.beginPath(); context.setLineWidth(pieOption.borderWidth * opts.pix); context.lineJoin = "round"; context.setStrokeStyle(pieOption.borderColor); var fillcolor = eachSeries.color; if (pieOption.linearType == 'custom') { var grd; if(context.createCircularGradient){ grd = context.createCircularGradient(centerPosition.x, centerPosition.y, eachSeries._radius_) }else{ grd = context.createRadialGradient(centerPosition.x, centerPosition.y, 0,centerPosition.x, centerPosition.y, eachSeries._radius_) } grd.addColorStop(0, hexToRgb(pieOption.customColor[eachSeries.linearIndex], 1)) grd.addColorStop(1, hexToRgb(eachSeries.color, 1)) fillcolor = grd } context.setFillStyle(fillcolor); context.moveTo(centerPosition.x, centerPosition.y); context.arc(centerPosition.x, centerPosition.y, eachSeries._radius_, eachSeries._start_, eachSeries._start_ + 2 * eachSeries._proportion_ * Math.PI); context.closePath(); context.fill(); if (pieOption.border == true) { context.stroke(); } }); if (opts.type === 'ring') { var innerPieWidth = radius * 0.6; if (typeof pieOption.ringWidth === 'number' && pieOption.ringWidth > 0) { innerPieWidth = Math.max(0, radius - pieOption.ringWidth * opts.pix); } context.beginPath(); context.setFillStyle(pieOption.centerColor); context.moveTo(centerPosition.x, centerPosition.y); context.arc(centerPosition.x, centerPosition.y, innerPieWidth, 0, 2 * Math.PI); context.closePath(); context.fill(); } if (opts.dataLabel !== false && process === 1) { drawPieText(series, opts, config, context, radius, centerPosition); } if (process === 1 && opts.type === 'ring') { drawRingTitle(opts, config, context, centerPosition); } return { center: centerPosition, radius: radius, series: series }; } function drawRoseDataPoints(series, opts, config, context) { var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; var roseOption = assign({}, { type: 'area', activeOpacity: 0.5, activeRadius: 10, offsetAngle: 0, labelWidth: 15, border: false, borderWidth: 2, borderColor: '#FFFFFF', linearType: 'none', customColor: [], }, opts.extra.rose); if (config.pieChartLinePadding == 0) { config.pieChartLinePadding = roseOption.activeRadius * opts.pix; } var centerPosition = { x: opts.area[3] + (opts.width - opts.area[1] - opts.area[3]) / 2, y: opts.area[0] + (opts.height - opts.area[0] - opts.area[2]) / 2 }; var radius = Math.min((opts.width - opts.area[1] - opts.area[3]) / 2 - config.pieChartLinePadding - config.pieChartTextPadding - config._pieTextMaxLength_, (opts.height - opts.area[0] - opts.area[2]) / 2 - config.pieChartLinePadding - config.pieChartTextPadding); radius = radius < 10 ? 10 : radius; var minRadius = roseOption.minRadius || radius * 0.5; series = getRoseDataPoints(series, roseOption.type, minRadius, radius, process); var activeRadius = roseOption.activeRadius * opts.pix; roseOption.customColor = fillCustomColor(roseOption.linearType, roseOption.customColor, series, config); series = series.map(function(eachSeries) { eachSeries._start_ += (roseOption.offsetAngle || 0) * Math.PI / 180; return eachSeries; }); series.forEach(function(eachSeries, seriesIndex) { if (opts.tooltip) { if (opts.tooltip.index == seriesIndex) { context.beginPath(); context.setFillStyle(hexToRgb(eachSeries.color, roseOption.activeOpacity || 0.5)); context.moveTo(centerPosition.x, centerPosition.y); context.arc(centerPosition.x, centerPosition.y, activeRadius + eachSeries._radius_, eachSeries._start_, eachSeries._start_ + 2 * eachSeries._rose_proportion_ * Math.PI); context.closePath(); context.fill(); } } context.beginPath(); context.setLineWidth(roseOption.borderWidth * opts.pix); context.lineJoin = "round"; context.setStrokeStyle(roseOption.borderColor); var fillcolor = eachSeries.color; if (roseOption.linearType == 'custom') { var grd; if(context.createCircularGradient){ grd = context.createCircularGradient(centerPosition.x, centerPosition.y, eachSeries._radius_) }else{ grd = context.createRadialGradient(centerPosition.x, centerPosition.y, 0,centerPosition.x, centerPosition.y, eachSeries._radius_) } grd.addColorStop(0, hexToRgb(roseOption.customColor[eachSeries.linearIndex], 1)) grd.addColorStop(1, hexToRgb(eachSeries.color, 1)) fillcolor = grd } context.setFillStyle(fillcolor); context.moveTo(centerPosition.x, centerPosition.y); context.arc(centerPosition.x, centerPosition.y, eachSeries._radius_, eachSeries._start_, eachSeries._start_ + 2 * eachSeries._rose_proportion_ * Math.PI); context.closePath(); context.fill(); if (roseOption.border == true) { context.stroke(); } }); if (opts.dataLabel !== false && process === 1) { drawPieText(series, opts, config, context, radius, centerPosition); } return { center: centerPosition, radius: radius, series: series }; } function drawArcbarDataPoints(series, opts, config, context) { var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; var arcbarOption = assign({}, { startAngle: 0.75, endAngle: 0.25, type: 'default', lineCap: 'round', width: 12 , gap: 2 , linearType: 'none', customColor: [], }, opts.extra.arcbar); series = getArcbarDataPoints(series, arcbarOption, process); var centerPosition; if (arcbarOption.centerX || arcbarOption.centerY) { centerPosition = { x: arcbarOption.centerX ? arcbarOption.centerX : opts.width / 2, y: arcbarOption.centerY ? arcbarOption.centerY : opts.height / 2 }; } else { centerPosition = { x: opts.width / 2, y: opts.height / 2 }; } var radius; if (arcbarOption.radius) { radius = arcbarOption.radius; } else { radius = Math.min(centerPosition.x, centerPosition.y); radius -= 5 * opts.pix; radius -= arcbarOption.width / 2; } radius = radius < 10 ? 10 : radius; arcbarOption.customColor = fillCustomColor(arcbarOption.linearType, arcbarOption.customColor, series, config); for (let i = 0; i < series.length; i++) { let eachSeries = series[i]; //背景颜色 context.setLineWidth(arcbarOption.width * opts.pix); context.setStrokeStyle(arcbarOption.backgroundColor || '#E9E9E9'); context.setLineCap(arcbarOption.lineCap); context.beginPath(); if (arcbarOption.type == 'default') { context.arc(centerPosition.x, centerPosition.y, radius - (arcbarOption.width * opts.pix + arcbarOption.gap * opts.pix) * i, arcbarOption.startAngle * Math.PI, arcbarOption.endAngle * Math.PI, false); } else { context.arc(centerPosition.x, centerPosition.y, radius - (arcbarOption.width * opts.pix + arcbarOption.gap * opts.pix) * i, 0, 2 * Math.PI, false); } context.stroke(); //进度条 var fillColor = eachSeries.color if(arcbarOption.linearType == 'custom'){ var grd = context.createLinearGradient(centerPosition.x - radius, centerPosition.y, centerPosition.x + radius, centerPosition.y); grd.addColorStop(1, hexToRgb(arcbarOption.customColor[eachSeries.linearIndex], 1)) grd.addColorStop(0, hexToRgb(eachSeries.color, 1)) fillColor = grd; } context.setLineWidth(arcbarOption.width * opts.pix); context.setStrokeStyle(fillColor); context.setLineCap(arcbarOption.lineCap); context.beginPath(); context.arc(centerPosition.x, centerPosition.y, radius - (arcbarOption.width * opts.pix + arcbarOption.gap * opts.pix) * i, arcbarOption.startAngle * Math.PI, eachSeries._proportion_ * Math.PI, false); context.stroke(); } drawRingTitle(opts, config, context, centerPosition); return { center: centerPosition, radius: radius, series: series }; } function drawGaugeDataPoints(categories, series, opts, config, context) { var process = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 1; var gaugeOption = assign({}, { type: 'default', startAngle: 0.75, endAngle: 0.25, width: 15, labelOffset:13, splitLine: { fixRadius: 0, splitNumber: 10, width: 15, color: '#FFFFFF', childNumber: 5, childWidth: 5 }, pointer: { width: 15, color: 'auto' } }, opts.extra.gauge); if (gaugeOption.oldAngle == undefined) { gaugeOption.oldAngle = gaugeOption.startAngle; } if (gaugeOption.oldData == undefined) { gaugeOption.oldData = 0; } categories = getGaugeAxisPoints(categories, gaugeOption.startAngle, gaugeOption.endAngle); var centerPosition = { x: opts.width / 2, y: opts.height / 2 }; var radius = Math.min(centerPosition.x, centerPosition.y); radius -= 5 * opts.pix; radius -= gaugeOption.width / 2; radius = radius < 10 ? 10 : radius; var innerRadius = radius - gaugeOption.width; var totalAngle = 0; //判断仪表盘的样式:default百度样式,progress新样式 if (gaugeOption.type == 'progress') { //## 第一步画中心圆形背景和进度条背景 //中心圆形背景 var pieRadius = radius - gaugeOption.width * 3; context.beginPath(); let gradient = context.createLinearGradient(centerPosition.x, centerPosition.y - pieRadius, centerPosition.x, centerPosition.y + pieRadius); //配置渐变填充(起点:中心点向上减半径;结束点中心点向下加半径) gradient.addColorStop('0', hexToRgb(series[0].color, 0.3)); gradient.addColorStop('1.0', hexToRgb("#FFFFFF", 0.1)); context.setFillStyle(gradient); context.arc(centerPosition.x, centerPosition.y, pieRadius, 0, 2 * Math.PI, false); context.fill(); //画进度条背景 context.setLineWidth(gaugeOption.width); context.setStrokeStyle(hexToRgb(series[0].color, 0.3)); context.setLineCap('round'); context.beginPath(); context.arc(centerPosition.x, centerPosition.y, innerRadius, gaugeOption.startAngle * Math.PI, gaugeOption.endAngle * Math.PI, false); context.stroke(); //## 第二步画刻度线 totalAngle = gaugeOption.startAngle - gaugeOption.endAngle + 1; let splitAngle = totalAngle / gaugeOption.splitLine.splitNumber; let childAngle = totalAngle / gaugeOption.splitLine.splitNumber / gaugeOption.splitLine.childNumber; let startX = -radius - gaugeOption.width * 0.5 - gaugeOption.splitLine.fixRadius; let endX = -radius - gaugeOption.width - gaugeOption.splitLine.fixRadius + gaugeOption.splitLine.width; context.save(); context.translate(centerPosition.x, centerPosition.y); context.rotate((gaugeOption.startAngle - 1) * Math.PI); let len = gaugeOption.splitLine.splitNumber * gaugeOption.splitLine.childNumber + 1; let proc = series[0].data * process; for (let i = 0; i < len; i++) { context.beginPath(); //刻度线随进度变色 if (proc > (i / len)) { context.setStrokeStyle(hexToRgb(series[0].color, 1)); } else { context.setStrokeStyle(hexToRgb(series[0].color, 0.3)); } context.setLineWidth(3 * opts.pix); context.moveTo(startX, 0); context.lineTo(endX, 0); context.stroke(); context.rotate(childAngle * Math.PI); } context.restore(); //## 第三步画进度条 series = getGaugeArcbarDataPoints(series, gaugeOption, process); context.setLineWidth(gaugeOption.width); context.setStrokeStyle(series[0].color); context.setLineCap('round'); context.beginPath(); context.arc(centerPosition.x, centerPosition.y, innerRadius, gaugeOption.startAngle * Math.PI, series[0]._proportion_ * Math.PI, false); context.stroke(); //## 第四步画指针 let pointerRadius = radius - gaugeOption.width * 2.5; context.save(); context.translate(centerPosition.x, centerPosition.y); context.rotate((series[0]._proportion_ - 1) * Math.PI); context.beginPath(); context.setLineWidth(gaugeOption.width / 3); let gradient3 = context.createLinearGradient(0, -pointerRadius * 0.6, 0, pointerRadius * 0.6); gradient3.addColorStop('0', hexToRgb('#FFFFFF', 0)); gradient3.addColorStop('0.5', hexToRgb(series[0].color, 1)); gradient3.addColorStop('1.0', hexToRgb('#FFFFFF', 0)); context.setStrokeStyle(gradient3); context.arc(0, 0, pointerRadius, 0.85 * Math.PI, 1.15 * Math.PI, false); context.stroke(); context.beginPath(); context.setLineWidth(1); context.setStrokeStyle(series[0].color); context.setFillStyle(series[0].color); context.moveTo(-pointerRadius - gaugeOption.width / 3 / 2, -4); context.lineTo(-pointerRadius - gaugeOption.width / 3 / 2 - 4, 0); context.lineTo(-pointerRadius - gaugeOption.width / 3 / 2, 4); context.lineTo(-pointerRadius - gaugeOption.width / 3 / 2, -4); context.stroke(); context.fill(); context.restore(); //default百度样式 } else { //画背景 context.setLineWidth(gaugeOption.width); context.setLineCap('butt'); for (let i = 0; i < categories.length; i++) { let eachCategories = categories[i]; context.beginPath(); context.setStrokeStyle(eachCategories.color); context.arc(centerPosition.x, centerPosition.y, radius, eachCategories._startAngle_ * Math.PI, eachCategories._endAngle_ * Math.PI, false); context.stroke(); } context.save(); //画刻度线 totalAngle = gaugeOption.startAngle - gaugeOption.endAngle + 1; let splitAngle = totalAngle / gaugeOption.splitLine.splitNumber; let childAngle = totalAngle / gaugeOption.splitLine.splitNumber / gaugeOption.splitLine.childNumber; let startX = -radius - gaugeOption.width * 0.5 - gaugeOption.splitLine.fixRadius; let endX = -radius - gaugeOption.width * 0.5 - gaugeOption.splitLine.fixRadius + gaugeOption.splitLine.width; let childendX = -radius - gaugeOption.width * 0.5 - gaugeOption.splitLine.fixRadius + gaugeOption.splitLine.childWidth; context.translate(centerPosition.x, centerPosition.y); context.rotate((gaugeOption.startAngle - 1) * Math.PI); for (let i = 0; i < gaugeOption.splitLine.splitNumber + 1; i++) { context.beginPath(); context.setStrokeStyle(gaugeOption.splitLine.color); context.setLineWidth(2 * opts.pix); context.moveTo(startX, 0); context.lineTo(endX, 0); context.stroke(); context.rotate(splitAngle * Math.PI); } context.restore(); context.save(); context.translate(centerPosition.x, centerPosition.y); context.rotate((gaugeOption.startAngle - 1) * Math.PI); for (let i = 0; i < gaugeOption.splitLine.splitNumber * gaugeOption.splitLine.childNumber + 1; i++) { context.beginPath(); context.setStrokeStyle(gaugeOption.splitLine.color); context.setLineWidth(1 * opts.pix); context.moveTo(startX, 0); context.lineTo(childendX, 0); context.stroke(); context.rotate(childAngle * Math.PI); } context.restore(); //画指针 series = getGaugeDataPoints(series, categories, gaugeOption, process); for (let i = 0; i < series.length; i++) { let eachSeries = series[i]; context.save(); context.translate(centerPosition.x, centerPosition.y); context.rotate((eachSeries._proportion_ - 1) * Math.PI); context.beginPath(); context.setFillStyle(eachSeries.color); context.moveTo(gaugeOption.pointer.width, 0); context.lineTo(0, -gaugeOption.pointer.width / 2); context.lineTo(-innerRadius, 0); context.lineTo(0, gaugeOption.pointer.width / 2); context.lineTo(gaugeOption.pointer.width, 0); context.closePath(); context.fill(); context.beginPath(); context.setFillStyle('#FFFFFF'); context.arc(0, 0, gaugeOption.pointer.width / 6, 0, 2 * Math.PI, false); context.fill(); context.restore(); } if (opts.dataLabel !== false) { drawGaugeLabel(gaugeOption, radius, centerPosition, opts, config, context); } } //画仪表盘标题,副标题 drawRingTitle(opts, config, context, centerPosition); if (process === 1 && opts.type === 'gauge') { opts.extra.gauge.oldAngle = series[0]._proportion_; opts.extra.gauge.oldData = series[0].data; } return { center: centerPosition, radius: radius, innerRadius: innerRadius, categories: categories, totalAngle: totalAngle }; } function drawRadarDataPoints(series, opts, config, context) { var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; var radarOption = assign({}, { gridColor: '#cccccc', gridType: 'radar', gridEval:1, axisLabel:false, axisLabelTofix:0, labelColor:'#666666', labelPointShow:false, labelPointRadius:3, labelPointColor:'#cccccc', opacity: 0.2, gridCount: 3, border:false, borderWidth:2, linearType: 'none', customColor: [], }, opts.extra.radar); var coordinateAngle = getRadarCoordinateSeries(opts.categories.length); var centerPosition = { x: opts.area[3] + (opts.width - opts.area[1] - opts.area[3]) / 2, y: opts.area[0] + (opts.height - opts.area[0] - opts.area[2]) / 2 }; var xr = (opts.width - opts.area[1] - opts.area[3]) / 2 var yr = (opts.height - opts.area[0] - opts.area[2]) / 2 var radius = Math.min(xr - (getMaxTextListLength(opts.categories, config.fontSize, context) + config.radarLabelTextMargin), yr - config.radarLabelTextMargin); radius -= config.radarLabelTextMargin * opts.pix; radius = radius < 10 ? 10 : radius; // 画分割线 context.beginPath(); context.setLineWidth(1 * opts.pix); context.setStrokeStyle(radarOption.gridColor); coordinateAngle.forEach(function(angle,index) { var pos = convertCoordinateOrigin(radius * Math.cos(angle), radius * Math.sin(angle), centerPosition); context.moveTo(centerPosition.x, centerPosition.y); if (index % radarOption.gridEval == 0) { context.lineTo(pos.x, pos.y); } }); context.stroke(); context.closePath(); // 画背景网格 var _loop = function _loop(i) { var startPos = {}; context.beginPath(); context.setLineWidth(1 * opts.pix); context.setStrokeStyle(radarOption.gridColor); if (radarOption.gridType == 'radar') { coordinateAngle.forEach(function(angle, index) { var pos = convertCoordinateOrigin(radius / radarOption.gridCount * i * Math.cos(angle), radius / radarOption.gridCount * i * Math.sin(angle), centerPosition); if (index === 0) { startPos = pos; context.moveTo(pos.x, pos.y); } else { context.lineTo(pos.x, pos.y); } }); context.lineTo(startPos.x, startPos.y); } else { var pos = convertCoordinateOrigin(radius / radarOption.gridCount * i * Math.cos(1.5), radius / radarOption.gridCount * i * Math.sin(1.5), centerPosition); context.arc(centerPosition.x, centerPosition.y, centerPosition.y - pos.y, 0, 2 * Math.PI, false); } context.stroke(); context.closePath(); }; for (var i = 1; i <= radarOption.gridCount; i++) { _loop(i); } radarOption.customColor = fillCustomColor(radarOption.linearType, radarOption.customColor, series, config); var radarDataPoints = getRadarDataPoints(coordinateAngle, centerPosition, radius, series, opts, process); radarDataPoints.forEach(function(eachSeries, seriesIndex) { // 绘制区域数据 context.beginPath(); context.setLineWidth(radarOption.borderWidth * opts.pix); context.setStrokeStyle(eachSeries.color); var fillcolor = hexToRgb(eachSeries.color, radarOption.opacity); if (radarOption.linearType == 'custom') { var grd; if(context.createCircularGradient){ grd = context.createCircularGradient(centerPosition.x, centerPosition.y, radius) }else{ grd = context.createRadialGradient(centerPosition.x, centerPosition.y, 0,centerPosition.x, centerPosition.y, radius) } grd.addColorStop(0, hexToRgb(radarOption.customColor[series[seriesIndex].linearIndex], radarOption.opacity)) grd.addColorStop(1, hexToRgb(eachSeries.color, radarOption.opacity)) fillcolor = grd } context.setFillStyle(fillcolor); eachSeries.data.forEach(function(item, index) { if (index === 0) { context.moveTo(item.position.x, item.position.y); } else { context.lineTo(item.position.x, item.position.y); } }); context.closePath(); context.fill(); if(radarOption.border === true){ context.stroke(); } context.closePath(); if (opts.dataPointShape !== false) { var points = eachSeries.data.map(function(item) { return item.position; }); drawPointShape(points, eachSeries.color, eachSeries.pointShape, context, opts); } }); // 画刻度值 if(radarOption.axisLabel === true){ const maxData = Math.max(radarOption.max, Math.max.apply(null, dataCombine(series))); const stepLength = radius / radarOption.gridCount; const fontSize = opts.fontSize * opts.pix; context.setFontSize(fontSize); context.setFillStyle(opts.fontColor); context.setTextAlign('left'); for (var i = 0; i < radarOption.gridCount + 1; i++) { let label = i * maxData / radarOption.gridCount; label = label.toFixed(radarOption.axisLabelTofix); context.fillText(String(label), centerPosition.x + 3 * opts.pix, centerPosition.y - i * stepLength + fontSize / 2); } } // draw label text drawRadarLabel(coordinateAngle, radius, centerPosition, opts, config, context); // draw dataLabel if (opts.dataLabel !== false && process === 1) { radarDataPoints.forEach(function(eachSeries, seriesIndex) { context.beginPath(); var fontSize = eachSeries.textSize * opts.pix || config.fontSize; context.setFontSize(fontSize); context.setFillStyle(eachSeries.textColor || opts.fontColor); eachSeries.data.forEach(function(item, index) { //如果是中心点垂直的上下点位 if(Math.abs(item.position.x - centerPosition.x)<2){ //如果在上面 if(item.position.y < centerPosition.y){ context.setTextAlign('center'); context.fillText(item.value, item.position.x, item.position.y - 4); }else{ context.setTextAlign('center'); context.fillText(item.value, item.position.x, item.position.y + fontSize + 2); } }else{ //如果在左侧 if(item.position.x < centerPosition.x){ context.setTextAlign('right'); context.fillText(item.value, item.position.x - 4, item.position.y + fontSize / 2 - 2); }else{ context.setTextAlign('left'); context.fillText(item.value, item.position.x + 4, item.position.y + fontSize / 2 - 2); } } }); context.closePath(); context.stroke(); }); context.setTextAlign('left'); } return { center: centerPosition, radius: radius, angleList: coordinateAngle }; } // 经纬度转墨卡托 function lonlat2mercator(longitude, latitude) { var mercator = Array(2); var x = longitude * 20037508.34 / 180; var y = Math.log(Math.tan((90 + latitude) * Math.PI / 360)) / (Math.PI / 180); y = y * 20037508.34 / 180; mercator[0] = x; mercator[1] = y; return mercator; } // 墨卡托转经纬度 function mercator2lonlat(longitude, latitude) { var lonlat = Array(2) var x = longitude / 20037508.34 * 180; var y = latitude / 20037508.34 * 180; y = 180 / Math.PI * (2 * Math.atan(Math.exp(y * Math.PI / 180)) - Math.PI / 2); lonlat[0] = x; lonlat[1] = y; return lonlat; } function getBoundingBox(data) { var bounds = {},coords; bounds.xMin = 180; bounds.xMax = 0; bounds.yMin = 90; bounds.yMax = 0 for (var i = 0; i < data.length; i++) { var coorda = data[i].geometry.coordinates for (var k = 0; k < coorda.length; k++) { coords = coorda[k]; if (coords.length == 1) { coords = coords[0] } for (var j = 0; j < coords.length; j++) { var longitude = coords[j][0]; var latitude = coords[j][1]; var point = { x: longitude, y: latitude } bounds.xMin = bounds.xMin < point.x ? bounds.xMin : point.x; bounds.xMax = bounds.xMax > point.x ? bounds.xMax : point.x; bounds.yMin = bounds.yMin < point.y ? bounds.yMin : point.y; bounds.yMax = bounds.yMax > point.y ? bounds.yMax : point.y; } } } return bounds; } function coordinateToPoint(latitude, longitude, bounds, scale, xoffset, yoffset) { return { x: (longitude - bounds.xMin) * scale + xoffset, y: (bounds.yMax - latitude) * scale + yoffset }; } function pointToCoordinate(pointY, pointX, bounds, scale, xoffset, yoffset) { return { x: (pointX - xoffset) / scale + bounds.xMin, y: bounds.yMax - (pointY - yoffset) / scale }; } function isRayIntersectsSegment(poi, s_poi, e_poi) { if (s_poi[1] == e_poi[1]) { return false; } if (s_poi[1] > poi[1] && e_poi[1] > poi[1]) { return false; } if (s_poi[1] < poi[1] && e_poi[1] < poi[1]) { return false; } if (s_poi[1] == poi[1] && e_poi[1] > poi[1]) { return false; } if (e_poi[1] == poi[1] && s_poi[1] > poi[1]) { return false; } if (s_poi[0] < poi[0] && e_poi[1] < poi[1]) { return false; } let xseg = e_poi[0] - (e_poi[0] - s_poi[0]) * (e_poi[1] - poi[1]) / (e_poi[1] - s_poi[1]); if (xseg < poi[0]) { return false; } else { return true; } } function isPoiWithinPoly(poi, poly, mercator) { let sinsc = 0; for (let i = 0; i < poly.length; i++) { let epoly = poly[i][0]; if (poly.length == 1) { epoly = poly[i][0] } for (let j = 0; j < epoly.length - 1; j++) { let s_poi = epoly[j]; let e_poi = epoly[j + 1]; if (mercator) { s_poi = lonlat2mercator(epoly[j][0], epoly[j][1]); e_poi = lonlat2mercator(epoly[j + 1][0], epoly[j + 1][1]); } if (isRayIntersectsSegment(poi, s_poi, e_poi)) { sinsc += 1; } } } if (sinsc % 2 == 1) { return true; } else { return false; } } function drawMapDataPoints(series, opts, config, context) { var mapOption = assign({}, { border: true, mercator: false, borderWidth: 1, borderColor: '#666666', fillOpacity: 0.6, activeBorderColor: '#f04864', activeFillColor: '#facc14', activeFillOpacity: 1 }, opts.extra.map); var coords, point; var data = series; var bounds = getBoundingBox(data); if (mapOption.mercator) { var max = lonlat2mercator(bounds.xMax, bounds.yMax) var min = lonlat2mercator(bounds.xMin, bounds.yMin) bounds.xMax = max[0] bounds.yMax = max[1] bounds.xMin = min[0] bounds.yMin = min[1] } var xScale = opts.width / Math.abs(bounds.xMax - bounds.xMin); var yScale = opts.height / Math.abs(bounds.yMax - bounds.yMin); var scale = xScale < yScale ? xScale : yScale; var xoffset = opts.width / 2 - Math.abs(bounds.xMax - bounds.xMin) / 2 * scale; var yoffset = opts.height / 2 - Math.abs(bounds.yMax - bounds.yMin) / 2 * scale; for (var i = 0; i < data.length; i++) { context.beginPath(); context.setLineWidth(mapOption.borderWidth * opts.pix); context.setStrokeStyle(mapOption.borderColor); context.setFillStyle(hexToRgb(series[i].color, mapOption.fillOpacity)); if (opts.tooltip) { if (opts.tooltip.index == i) { context.setStrokeStyle(mapOption.activeBorderColor); context.setFillStyle(hexToRgb(mapOption.activeFillColor, mapOption.activeFillOpacity)); } } var coorda = data[i].geometry.coordinates for (var k = 0; k < coorda.length; k++) { coords = coorda[k]; if (coords.length == 1) { coords = coords[0] } for (var j = 0; j < coords.length; j++) { var gaosi = Array(2); if (mapOption.mercator) { gaosi = lonlat2mercator(coords[j][0], coords[j][1]) } else { gaosi = coords[j] } point = coordinateToPoint(gaosi[1], gaosi[0], bounds, scale, xoffset, yoffset) if (j === 0) { context.beginPath(); context.moveTo(point.x, point.y); } else { context.lineTo(point.x, point.y); } } context.fill(); if (mapOption.border == true) { context.stroke(); } } } if (opts.dataLabel == true) { for (var i = 0; i < data.length; i++) { var centerPoint = data[i].properties.centroid; if (centerPoint) { if (mapOption.mercator) { centerPoint = lonlat2mercator(data[i].properties.centroid[0], data[i].properties.centroid[1]) } point = coordinateToPoint(centerPoint[1], centerPoint[0], bounds, scale, xoffset, yoffset); let fontSize = data[i].textSize * opts.pix || config.fontSize; let text = data[i].properties.name; context.beginPath(); context.setFontSize(fontSize) context.setFillStyle(data[i].textColor || opts.fontColor) context.fillText(text, point.x - measureText(text, fontSize, context) / 2, point.y + fontSize / 2); context.closePath(); context.stroke(); } } } opts.chartData.mapData = { bounds: bounds, scale: scale, xoffset: xoffset, yoffset: yoffset, mercator: mapOption.mercator } drawToolTipBridge(opts, config, context, 1); context.draw(); } function normalInt(min, max, iter) { iter = iter == 0 ? 1 : iter; var arr = []; for (var i = 0; i < iter; i++) { arr[i] = Math.random(); }; return Math.floor(arr.reduce(function(i, j) { return i + j }) / iter * (max - min)) + min; }; function collisionNew(area, points, width, height) { var isIn = false; for (let i = 0; i < points.length; i++) { if (points[i].area) { if (area[3] < points[i].area[1] || area[0] > points[i].area[2] || area[1] > points[i].area[3] || area[2] < points[i].area[0]) { if (area[0] < 0 || area[1] < 0 || area[2] > width || area[3] > height) { isIn = true; break; } else { isIn = false; } } else { isIn = true; break; } } } return isIn; }; function getWordCloudPoint(opts, type, context) { let points = opts.series; switch (type) { case 'normal': for (let i = 0; i < points.length; i++) { let text = points[i].name; let tHeight = points[i].textSize * opts.pix; let tWidth = measureText(text, tHeight, context); let x, y; let area; let breaknum = 0; while (true) { breaknum++; x = normalInt(-opts.width / 2, opts.width / 2, 5) - tWidth / 2; y = normalInt(-opts.height / 2, opts.height / 2, 5) + tHeight / 2; area = [x - 5 + opts.width / 2, y - 5 - tHeight + opts.height / 2, x + tWidth + 5 + opts.width / 2, y + 5 + opts.height / 2 ]; let isCollision = collisionNew(area, points, opts.width, opts.height); if (!isCollision) break; if (breaknum == 1000) { area = [-100, -100, -100, -100]; break; } }; points[i].area = area; } break; case 'vertical': function Spin() { //获取均匀随机值,是否旋转,旋转的概率为(1-0.5) if (Math.random() > 0.7) { return true; } else { return false }; }; for (let i = 0; i < points.length; i++) { let text = points[i].name; let tHeight = points[i].textSize * opts.pix; let tWidth = measureText(text, tHeight, context); let isSpin = Spin(); let x, y, area, areav; let breaknum = 0; while (true) { breaknum++; let isCollision; if (isSpin) { x = normalInt(-opts.width / 2, opts.width / 2, 5) - tWidth / 2; y = normalInt(-opts.height / 2, opts.height / 2, 5) + tHeight / 2; area = [y - 5 - tWidth + opts.width / 2, (-x - 5 + opts.height / 2), y + 5 + opts.width / 2, (-x + tHeight + 5 + opts.height / 2)]; areav = [opts.width - (opts.width / 2 - opts.height / 2) - (-x + tHeight + 5 + opts.height / 2) - 5, (opts.height / 2 - opts.width / 2) + (y - 5 - tWidth + opts.width / 2) - 5, opts.width - (opts.width / 2 - opts.height / 2) - (-x + tHeight + 5 + opts.height / 2) + tHeight, (opts.height / 2 - opts.width / 2) + (y - 5 - tWidth + opts.width / 2) + tWidth + 5]; isCollision = collisionNew(areav, points, opts.height, opts.width); } else { x = normalInt(-opts.width / 2, opts.width / 2, 5) - tWidth / 2; y = normalInt(-opts.height / 2, opts.height / 2, 5) + tHeight / 2; area = [x - 5 + opts.width / 2, y - 5 - tHeight + opts.height / 2, x + tWidth + 5 + opts.width / 2, y + 5 + opts.height / 2]; isCollision = collisionNew(area, points, opts.width, opts.height); } if (!isCollision) break; if (breaknum == 1000) { area = [-1000, -1000, -1000, -1000]; break; } }; if (isSpin) { points[i].area = areav; points[i].areav = area; } else { points[i].area = area; } points[i].rotate = isSpin; }; break; } return points; } function drawWordCloudDataPoints(series, opts, config, context) { let process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; let wordOption = assign({}, { type: 'normal', autoColors: true }, opts.extra.word); if (!opts.chartData.wordCloudData) { opts.chartData.wordCloudData = getWordCloudPoint(opts, wordOption.type, context); } context.beginPath(); context.setFillStyle(opts.background); context.rect(0, 0, opts.width, opts.height); context.fill(); context.save(); let points = opts.chartData.wordCloudData; context.translate(opts.width / 2, opts.height / 2); for (let i = 0; i < points.length; i++) { context.save(); if (points[i].rotate) { context.rotate(90 * Math.PI / 180); } let text = points[i].name; let tHeight = points[i].textSize * opts.pix; let tWidth = measureText(text, tHeight, context); context.beginPath(); context.setStrokeStyle(points[i].color); context.setFillStyle(points[i].color); context.setFontSize(tHeight); if (points[i].rotate) { if (points[i].areav[0] > 0) { if (opts.tooltip) { if (opts.tooltip.index == i) { context.strokeText(text, (points[i].areav[0] + 5 - opts.width / 2) * process - tWidth * (1 - process) / 2, (points[i].areav[1] + 5 + tHeight - opts.height / 2) * process); } else { context.fillText(text, (points[i].areav[0] + 5 - opts.width / 2) * process - tWidth * (1 - process) / 2, (points[i].areav[1] + 5 + tHeight - opts.height / 2) * process); } } else { context.fillText(text, (points[i].areav[0] + 5 - opts.width / 2) * process - tWidth * (1 - process) / 2, (points[i].areav[1] + 5 + tHeight - opts.height / 2) * process); } } } else { if (points[i].area[0] > 0) { if (opts.tooltip) { if (opts.tooltip.index == i) { context.strokeText(text, (points[i].area[0] + 5 - opts.width / 2) * process - tWidth * (1 - process) / 2, (points[i].area[1] + 5 + tHeight - opts.height / 2) * process); } else { context.fillText(text, (points[i].area[0] + 5 - opts.width / 2) * process - tWidth * (1 - process) / 2, (points[i].area[1] + 5 + tHeight - opts.height / 2) * process); } } else { context.fillText(text, (points[i].area[0] + 5 - opts.width / 2) * process - tWidth * (1 - process) / 2, (points[i].area[1] + 5 + tHeight - opts.height / 2) * process); } } } context.stroke(); context.restore(); } context.restore(); } function drawFunnelDataPoints(series, opts, config, context) { let process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; let funnelOption = assign({}, { type:'funnel', activeWidth: 10, activeOpacity: 0.3, border: false, borderWidth: 2, borderColor: '#FFFFFF', fillOpacity: 1, labelAlign: 'right', linearType: 'none', customColor: [], }, opts.extra.funnel); let eachSpacing = (opts.height - opts.area[0] - opts.area[2]) / series.length; let centerPosition = { x: opts.area[3] + (opts.width - opts.area[1] - opts.area[3]) / 2, y: opts.height - opts.area[2] }; let activeWidth = funnelOption.activeWidth * opts.pix; let radius = Math.min((opts.width - opts.area[1] - opts.area[3]) / 2 - activeWidth, (opts.height - opts.area[0] - opts.area[2]) / 2 - activeWidth); series = getFunnelDataPoints(series, radius, funnelOption.type, eachSpacing, process); context.save(); context.translate(centerPosition.x, centerPosition.y); funnelOption.customColor = fillCustomColor(funnelOption.linearType, funnelOption.customColor, series, config); if(funnelOption.type == 'pyramid'){ for (let i = 0; i < series.length; i++) { if (i == series.length -1) { if (opts.tooltip) { if (opts.tooltip.index == i) { context.beginPath(); context.setFillStyle(hexToRgb(series[i].color, funnelOption.activeOpacity)); context.moveTo(-activeWidth, -eachSpacing); context.lineTo(-series[i].radius - activeWidth, 0); context.lineTo(series[i].radius + activeWidth, 0); context.lineTo(activeWidth, -eachSpacing); context.lineTo(-activeWidth, -eachSpacing); context.closePath(); context.fill(); } } series[i].funnelArea = [centerPosition.x - series[i].radius, centerPosition.y - eachSpacing * (i + 1), centerPosition.x + series[i].radius, centerPosition.y - eachSpacing * i]; context.beginPath(); context.setLineWidth(funnelOption.borderWidth * opts.pix); context.setStrokeStyle(funnelOption.borderColor); var fillColor = hexToRgb(series[i].color, funnelOption.fillOpacity); if (funnelOption.linearType == 'custom') { var grd = context.createLinearGradient(series[i].radius, -eachSpacing, -series[i].radius, -eachSpacing); grd.addColorStop(0, hexToRgb(series[i].color, funnelOption.fillOpacity)); grd.addColorStop(0.5, hexToRgb(funnelOption.customColor[series[i].linearIndex], funnelOption.fillOpacity)); grd.addColorStop(1, hexToRgb(series[i].color, funnelOption.fillOpacity)); fillColor = grd } context.setFillStyle(fillColor); context.moveTo(0, -eachSpacing); context.lineTo(-series[i].radius, 0); context.lineTo(series[i].radius, 0); context.lineTo(0, -eachSpacing); context.closePath(); context.fill(); if (funnelOption.border == true) { context.stroke(); } } else { if (opts.tooltip) { if (opts.tooltip.index == i) { context.beginPath(); context.setFillStyle(hexToRgb(series[i].color, funnelOption.activeOpacity)); context.moveTo(0, 0); context.lineTo(-series[i].radius - activeWidth, 0); context.lineTo(-series[i + 1].radius - activeWidth, -eachSpacing); context.lineTo(series[i + 1].radius + activeWidth, -eachSpacing); context.lineTo(series[i].radius + activeWidth, 0); context.lineTo(0, 0); context.closePath(); context.fill(); } } series[i].funnelArea = [centerPosition.x - series[i].radius, centerPosition.y - eachSpacing * (i + 1), centerPosition.x + series[i].radius, centerPosition.y - eachSpacing * i]; context.beginPath(); context.setLineWidth(funnelOption.borderWidth * opts.pix); context.setStrokeStyle(funnelOption.borderColor); var fillColor = hexToRgb(series[i].color, funnelOption.fillOpacity); if (funnelOption.linearType == 'custom') { var grd = context.createLinearGradient(series[i].radius, -eachSpacing, -series[i].radius, -eachSpacing); grd.addColorStop(0, hexToRgb(series[i].color, funnelOption.fillOpacity)); grd.addColorStop(0.5, hexToRgb(funnelOption.customColor[series[i].linearIndex], funnelOption.fillOpacity)); grd.addColorStop(1, hexToRgb(series[i].color, funnelOption.fillOpacity)); fillColor = grd } context.setFillStyle(fillColor); context.moveTo(0, 0); context.lineTo(-series[i].radius, 0); context.lineTo(-series[i + 1].radius, -eachSpacing); context.lineTo(series[i + 1].radius, -eachSpacing); context.lineTo(series[i].radius, 0); context.lineTo(0, 0); context.closePath(); context.fill(); if (funnelOption.border == true) { context.stroke(); } } context.translate(0, -eachSpacing) } }else{ for (let i = 0; i < series.length; i++) { if (i == 0) { if (opts.tooltip) { if (opts.tooltip.index == i) { context.beginPath(); context.setFillStyle(hexToRgb(series[i].color, funnelOption.activeOpacity)); context.moveTo(-activeWidth, 0); context.lineTo(-series[i].radius - activeWidth, -eachSpacing); context.lineTo(series[i].radius + activeWidth, -eachSpacing); context.lineTo(activeWidth, 0); context.lineTo(-activeWidth, 0); context.closePath(); context.fill(); } } series[i].funnelArea = [centerPosition.x - series[i].radius, centerPosition.y - eachSpacing, centerPosition.x + series[i].radius, centerPosition.y]; context.beginPath(); context.setLineWidth(funnelOption.borderWidth * opts.pix); context.setStrokeStyle(funnelOption.borderColor); var fillColor = hexToRgb(series[i].color, funnelOption.fillOpacity); if (funnelOption.linearType == 'custom') { var grd = context.createLinearGradient(series[i].radius, -eachSpacing, -series[i].radius, -eachSpacing); grd.addColorStop(0, hexToRgb(series[i].color, funnelOption.fillOpacity)); grd.addColorStop(0.5, hexToRgb(funnelOption.customColor[series[i].linearIndex], funnelOption.fillOpacity)); grd.addColorStop(1, hexToRgb(series[i].color, funnelOption.fillOpacity)); fillColor = grd } context.setFillStyle(fillColor); context.moveTo(0, 0); context.lineTo(-series[i].radius, -eachSpacing); context.lineTo(series[i].radius, -eachSpacing); context.lineTo(0, 0); context.closePath(); context.fill(); if (funnelOption.border == true) { context.stroke(); } } else { if (opts.tooltip) { if (opts.tooltip.index == i) { context.beginPath(); context.setFillStyle(hexToRgb(series[i].color, funnelOption.activeOpacity)); context.moveTo(0, 0); context.lineTo(-series[i - 1].radius - activeWidth, 0); context.lineTo(-series[i].radius - activeWidth, -eachSpacing); context.lineTo(series[i].radius + activeWidth, -eachSpacing); context.lineTo(series[i - 1].radius + activeWidth, 0); context.lineTo(0, 0); context.closePath(); context.fill(); } } series[i].funnelArea = [centerPosition.x - series[i].radius, centerPosition.y - eachSpacing * (i + 1), centerPosition.x + series[i].radius, centerPosition.y - eachSpacing * i]; context.beginPath(); context.setLineWidth(funnelOption.borderWidth * opts.pix); context.setStrokeStyle(funnelOption.borderColor); var fillColor = hexToRgb(series[i].color, funnelOption.fillOpacity); if (funnelOption.linearType == 'custom') { var grd = context.createLinearGradient(series[i].radius, -eachSpacing, -series[i].radius, -eachSpacing); grd.addColorStop(0, hexToRgb(series[i].color, funnelOption.fillOpacity)); grd.addColorStop(0.5, hexToRgb(funnelOption.customColor[series[i].linearIndex], funnelOption.fillOpacity)); grd.addColorStop(1, hexToRgb(series[i].color, funnelOption.fillOpacity)); fillColor = grd } context.setFillStyle(fillColor); context.moveTo(0, 0); context.lineTo(-series[i - 1].radius, 0); context.lineTo(-series[i].radius, -eachSpacing); context.lineTo(series[i].radius, -eachSpacing); context.lineTo(series[i - 1].radius, 0); context.lineTo(0, 0); context.closePath(); context.fill(); if (funnelOption.border == true) { context.stroke(); } } context.translate(0, -eachSpacing) } } context.restore(); if (opts.dataLabel !== false && process === 1) { drawFunnelText(series, opts, context, eachSpacing, funnelOption.labelAlign, activeWidth, centerPosition); } return { center: centerPosition, radius: radius, series: series }; } function drawFunnelText(series, opts, context, eachSpacing, labelAlign, activeWidth, centerPosition) { for (let i = 0; i < series.length; i++) { let item = series[i]; if(item.labelShow === false){ continue; } let startX, endX, startY, fontSize; let text = item.formatter ? item.formatter(item,i,series,opts) : util.toFixed(item._proportion_ * 100) + '%'; text = item.labelText ? item.labelText : text; if (labelAlign == 'right') { if(opts.extra.funnel.type === 'pyramid'){ if (i == series.length -1) { startX = (item.funnelArea[2] + centerPosition.x) / 2; } else { startX = (item.funnelArea[2] + series[i + 1].funnelArea[2]) / 2; } }else{ if (i == 0) { startX = (item.funnelArea[2] + centerPosition.x) / 2; } else { startX = (item.funnelArea[2] + series[i - 1].funnelArea[2]) / 2; } } endX = startX + activeWidth * 2; startY = item.funnelArea[1] + eachSpacing / 2; fontSize = item.textSize * opts.pix || opts.fontSize * opts.pix; context.setLineWidth(1 * opts.pix); context.setStrokeStyle(item.color); context.setFillStyle(item.color); context.beginPath(); context.moveTo(startX, startY); context.lineTo(endX, startY); context.stroke(); context.closePath(); context.beginPath(); context.moveTo(endX, startY); context.arc(endX, startY, 2 * opts.pix, 0, 2 * Math.PI); context.closePath(); context.fill(); context.beginPath(); context.setFontSize(fontSize); context.setFillStyle(item.textColor || opts.fontColor); context.fillText(text, endX + 5, startY + fontSize / 2 - 2); context.closePath(); context.stroke(); context.closePath(); } else { if(opts.extra.funnel.type === 'pyramid'){ if (i == series.length -1) { startX = (item.funnelArea[0] + centerPosition.x) / 2; } else { startX = (item.funnelArea[0] + series[i + 1].funnelArea[0]) / 2; } }else{ if (i == 0) { startX = (item.funnelArea[0] + centerPosition.x) / 2; } else { startX = (item.funnelArea[0] + series[i - 1].funnelArea[0]) / 2; } } endX = startX - activeWidth * 2; startY = item.funnelArea[1] + eachSpacing / 2; fontSize = item.textSize * opts.pix || opts.fontSize * opts.pix; context.setLineWidth(1 * opts.pix); context.setStrokeStyle(item.color); context.setFillStyle(item.color); context.beginPath(); context.moveTo(startX, startY); context.lineTo(endX, startY); context.stroke(); context.closePath(); context.beginPath(); context.moveTo(endX, startY); context.arc(endX, startY, 2, 0, 2 * Math.PI); context.closePath(); context.fill(); context.beginPath(); context.setFontSize(fontSize); context.setFillStyle(item.textColor || opts.fontColor); context.fillText(text, endX - 5 - measureText(text, fontSize, context), startY + fontSize / 2 - 2); context.closePath(); context.stroke(); context.closePath(); } } } function drawCanvas(opts, context) { context.draw(); } var Timing = { easeIn: function easeIn(pos) { return Math.pow(pos, 3); }, easeOut: function easeOut(pos) { return Math.pow(pos - 1, 3) + 1; }, easeInOut: function easeInOut(pos) { if ((pos /= 0.5) < 1) { return 0.5 * Math.pow(pos, 3); } else { return 0.5 * (Math.pow(pos - 2, 3) + 2); } }, linear: function linear(pos) { return pos; } }; function Animation(opts) { this.isStop = false; opts.duration = typeof opts.duration === 'undefined' ? 1000 : opts.duration; opts.timing = opts.timing || 'easeInOut'; var delay = 17; function createAnimationFrame() { if (typeof setTimeout !== 'undefined') { return function(step, delay) { setTimeout(function() { var timeStamp = +new Date(); step(timeStamp); }, delay); }; } else if (typeof requestAnimationFrame !== 'undefined') { return requestAnimationFrame; } else { return function(step) { step(null); }; } }; var animationFrame = createAnimationFrame(); var startTimeStamp = null; var _step = function step(timestamp) { if (timestamp === null || this.isStop === true) { opts.onProcess && opts.onProcess(1); opts.onAnimationFinish && opts.onAnimationFinish(); return; } if (startTimeStamp === null) { startTimeStamp = timestamp; } if (timestamp - startTimeStamp < opts.duration) { var process = (timestamp - startTimeStamp) / opts.duration; var timingFunction = Timing[opts.timing]; process = timingFunction(process); opts.onProcess && opts.onProcess(process); animationFrame(_step, delay); } else { opts.onProcess && opts.onProcess(1); opts.onAnimationFinish && opts.onAnimationFinish(); } }; _step = _step.bind(this); animationFrame(_step, delay); } Animation.prototype.stop = function() { this.isStop = true; }; function drawCharts(type, opts, config, context) { var _this = this; var series = opts.series; //兼容ECharts饼图类数据格式 if (type === 'pie' || type === 'ring' || type === 'mount' || type === 'rose' || type === 'funnel') { series = fixPieSeries(series, opts, config); } var categories = opts.categories; if (type === 'mount') { categories = []; for (let j = 0; j < series.length; j++) { if(series[j].show !== false) categories.push(series[j].name) } opts.categories = categories; } series = fillSeries(series, opts, config); var duration = opts.animation ? opts.duration : 0; _this.animationInstance && _this.animationInstance.stop(); var seriesMA = null; if (type == 'candle') { let average = assign({}, opts.extra.candle.average); if (average.show) { seriesMA = calCandleMA(average.day, average.name, average.color, series[0].data); seriesMA = fillSeries(seriesMA, opts, config); opts.seriesMA = seriesMA; } else if (opts.seriesMA) { seriesMA = opts.seriesMA = fillSeries(opts.seriesMA, opts, config); } else { seriesMA = series; } } else { seriesMA = series; } /* 过滤掉show=false的series */ opts._series_ = series = filterSeries(series); //重新计算图表区域 opts.area = new Array(4); //复位绘图区域 for (let j = 0; j < 4; j++) { opts.area[j] = opts.padding[j] * opts.pix; } //通过计算三大区域:图例、X轴、Y轴的大小,确定绘图区域 var _calLegendData = calLegendData(seriesMA, opts, config, opts.chartData, context), legendHeight = _calLegendData.area.wholeHeight, legendWidth = _calLegendData.area.wholeWidth; switch (opts.legend.position) { case 'top': opts.area[0] += legendHeight; break; case 'bottom': opts.area[2] += legendHeight; break; case 'left': opts.area[3] += legendWidth; break; case 'right': opts.area[1] += legendWidth; break; } let _calYAxisData = {}, yAxisWidth = 0; if (opts.type === 'line' || opts.type === 'column'|| opts.type === 'mount' || opts.type === 'area' || opts.type === 'mix' || opts.type === 'candle' || opts.type === 'scatter' || opts.type === 'bubble' || opts.type === 'bar') { _calYAxisData = calYAxisData(series, opts, config, context); yAxisWidth = _calYAxisData.yAxisWidth; //如果显示Y轴标题 if (opts.yAxis.showTitle) { let maxTitleHeight = 0; for (let i = 0; i < opts.yAxis.data.length; i++) { maxTitleHeight = Math.max(maxTitleHeight, opts.yAxis.data[i].titleFontSize ? opts.yAxis.data[i].titleFontSize * opts.pix : config.fontSize) } opts.area[0] += maxTitleHeight; } let rightIndex = 0, leftIndex = 0; //计算主绘图区域左右位置 for (let i = 0; i < yAxisWidth.length; i++) { if (yAxisWidth[i].position == 'left') { if (leftIndex > 0) { opts.area[3] += yAxisWidth[i].width + opts.yAxis.padding * opts.pix; } else { opts.area[3] += yAxisWidth[i].width; } leftIndex += 1; } else if (yAxisWidth[i].position == 'right') { if (rightIndex > 0) { opts.area[1] += yAxisWidth[i].width + opts.yAxis.padding * opts.pix; } else { opts.area[1] += yAxisWidth[i].width; } rightIndex += 1; } } } else { config.yAxisWidth = yAxisWidth; } opts.chartData.yAxisData = _calYAxisData; if (opts.categories && opts.categories.length && opts.type !== 'radar' && opts.type !== 'gauge' && opts.type !== 'bar') { opts.chartData.xAxisData = getXAxisPoints(opts.categories, opts, config); let _calCategoriesData = calCategoriesData(opts.categories, opts, config, opts.chartData.xAxisData.eachSpacing, context), xAxisHeight = _calCategoriesData.xAxisHeight, angle = _calCategoriesData.angle; config.xAxisHeight = xAxisHeight; config._xAxisTextAngle_ = angle; opts.area[2] += xAxisHeight; opts.chartData.categoriesData = _calCategoriesData; } else { if (opts.type === 'line' || opts.type === 'area' || opts.type === 'scatter' || opts.type === 'bubble' || opts.type === 'bar') { opts.chartData.xAxisData = calXAxisData(series, opts, config, context); categories = opts.chartData.xAxisData.rangesFormat; let _calCategoriesData = calCategoriesData(categories, opts, config, opts.chartData.xAxisData.eachSpacing, context), xAxisHeight = _calCategoriesData.xAxisHeight, angle = _calCategoriesData.angle; config.xAxisHeight = xAxisHeight; config._xAxisTextAngle_ = angle; opts.area[2] += xAxisHeight; opts.chartData.categoriesData = _calCategoriesData; } else { opts.chartData.xAxisData = { xAxisPoints: [] }; } } //计算右对齐偏移距离 if (opts.enableScroll && opts.xAxis.scrollAlign == 'right' && opts._scrollDistance_ === undefined) { let offsetLeft = 0, xAxisPoints = opts.chartData.xAxisData.xAxisPoints, startX = opts.chartData.xAxisData.startX, endX = opts.chartData.xAxisData.endX, eachSpacing = opts.chartData.xAxisData.eachSpacing; let totalWidth = eachSpacing * (xAxisPoints.length - 1); let screenWidth = endX - startX; offsetLeft = screenWidth - totalWidth; _this.scrollOption.currentOffset = offsetLeft; _this.scrollOption.startTouchX = offsetLeft; _this.scrollOption.distance = 0; _this.scrollOption.lastMoveTime = 0; opts._scrollDistance_ = offsetLeft; } if (type === 'pie' || type === 'ring' || type === 'rose') { config._pieTextMaxLength_ = opts.dataLabel === false ? 0 : getPieTextMaxLength(seriesMA, config, context, opts); } switch (type) { case 'word': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } drawWordCloudDataPoints(series, opts, config, context, process); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; case 'map': context.clearRect(0, 0, opts.width, opts.height); drawMapDataPoints(series, opts, config, context); break; case 'funnel': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } opts.chartData.funnelData = drawFunnelDataPoints(series, opts, config, context, process); drawLegend(opts.series, opts, config, context, opts.chartData); drawToolTipBridge(opts, config, context, process); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; case 'line': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function onProcess(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } drawYAxisGrid(categories, opts, config, context); drawXAxis(categories, opts, config, context); var _drawLineDataPoints = drawLineDataPoints(series, opts, config, context, process), xAxisPoints = _drawLineDataPoints.xAxisPoints, calPoints = _drawLineDataPoints.calPoints, eachSpacing = _drawLineDataPoints.eachSpacing; opts.chartData.xAxisPoints = xAxisPoints; opts.chartData.calPoints = calPoints; opts.chartData.eachSpacing = eachSpacing; drawYAxis(series, opts, config, context); if (opts.enableMarkLine !== false && process === 1) { drawMarkLine(opts, config, context); } drawLegend(opts.series, opts, config, context, opts.chartData); drawToolTipBridge(opts, config, context, process, eachSpacing, xAxisPoints); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; case 'scatter': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function onProcess(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } drawYAxisGrid(categories, opts, config, context); drawXAxis(categories, opts, config, context); var _drawScatterDataPoints = drawScatterDataPoints(series, opts, config, context, process), xAxisPoints = _drawScatterDataPoints.xAxisPoints, calPoints = _drawScatterDataPoints.calPoints, eachSpacing = _drawScatterDataPoints.eachSpacing; opts.chartData.xAxisPoints = xAxisPoints; opts.chartData.calPoints = calPoints; opts.chartData.eachSpacing = eachSpacing; drawYAxis(series, opts, config, context); if (opts.enableMarkLine !== false && process === 1) { drawMarkLine(opts, config, context); } drawLegend(opts.series, opts, config, context, opts.chartData); drawToolTipBridge(opts, config, context, process, eachSpacing, xAxisPoints); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; case 'bubble': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function onProcess(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } drawYAxisGrid(categories, opts, config, context); drawXAxis(categories, opts, config, context); var _drawBubbleDataPoints = drawBubbleDataPoints(series, opts, config, context, process), xAxisPoints = _drawBubbleDataPoints.xAxisPoints, calPoints = _drawBubbleDataPoints.calPoints, eachSpacing = _drawBubbleDataPoints.eachSpacing; opts.chartData.xAxisPoints = xAxisPoints; opts.chartData.calPoints = calPoints; opts.chartData.eachSpacing = eachSpacing; drawYAxis(series, opts, config, context); if (opts.enableMarkLine !== false && process === 1) { drawMarkLine(opts, config, context); } drawLegend(opts.series, opts, config, context, opts.chartData); drawToolTipBridge(opts, config, context, process, eachSpacing, xAxisPoints); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; case 'mix': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function onProcess(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } drawYAxisGrid(categories, opts, config, context); drawXAxis(categories, opts, config, context); var _drawMixDataPoints = drawMixDataPoints(series, opts, config, context, process), xAxisPoints = _drawMixDataPoints.xAxisPoints, calPoints = _drawMixDataPoints.calPoints, eachSpacing = _drawMixDataPoints.eachSpacing; opts.chartData.xAxisPoints = xAxisPoints; opts.chartData.calPoints = calPoints; opts.chartData.eachSpacing = eachSpacing; drawYAxis(series, opts, config, context); if (opts.enableMarkLine !== false && process === 1) { drawMarkLine(opts, config, context); } drawLegend(opts.series, opts, config, context, opts.chartData); drawToolTipBridge(opts, config, context, process, eachSpacing, xAxisPoints); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; case 'column': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function onProcess(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } drawYAxisGrid(categories, opts, config, context); drawXAxis(categories, opts, config, context); var _drawColumnDataPoints = drawColumnDataPoints(series, opts, config, context, process), xAxisPoints = _drawColumnDataPoints.xAxisPoints, calPoints = _drawColumnDataPoints.calPoints, eachSpacing = _drawColumnDataPoints.eachSpacing; opts.chartData.xAxisPoints = xAxisPoints; opts.chartData.calPoints = calPoints; opts.chartData.eachSpacing = eachSpacing; drawYAxis(series, opts, config, context); if (opts.enableMarkLine !== false && process === 1) { drawMarkLine(opts, config, context); } drawLegend(opts.series, opts, config, context, opts.chartData); drawToolTipBridge(opts, config, context, process, eachSpacing, xAxisPoints); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; case 'mount': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function onProcess(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } drawYAxisGrid(categories, opts, config, context); drawXAxis(categories, opts, config, context); var _drawMountDataPoints = drawMountDataPoints(series, opts, config, context, process), xAxisPoints = _drawMountDataPoints.xAxisPoints, calPoints = _drawMountDataPoints.calPoints, eachSpacing = _drawMountDataPoints.eachSpacing; opts.chartData.xAxisPoints = xAxisPoints; opts.chartData.calPoints = calPoints; opts.chartData.eachSpacing = eachSpacing; drawYAxis(series, opts, config, context); if (opts.enableMarkLine !== false && process === 1) { drawMarkLine(opts, config, context); } drawLegend(opts.series, opts, config, context, opts.chartData); drawToolTipBridge(opts, config, context, process, eachSpacing, xAxisPoints); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; case 'bar': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function onProcess(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } drawXAxis(categories, opts, config, context); var _drawBarDataPoints = drawBarDataPoints(series, opts, config, context, process), yAxisPoints = _drawBarDataPoints.yAxisPoints, calPoints = _drawBarDataPoints.calPoints, eachSpacing = _drawBarDataPoints.eachSpacing; opts.chartData.yAxisPoints = yAxisPoints; opts.chartData.xAxisPoints = opts.chartData.xAxisData.xAxisPoints; opts.chartData.calPoints = calPoints; opts.chartData.eachSpacing = eachSpacing; drawYAxis(series, opts, config, context); if (opts.enableMarkLine !== false && process === 1) { drawMarkLine(opts, config, context); } drawLegend(opts.series, opts, config, context, opts.chartData); drawToolTipBridge(opts, config, context, process, eachSpacing, yAxisPoints); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; case 'area': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function onProcess(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } drawYAxisGrid(categories, opts, config, context); drawXAxis(categories, opts, config, context); var _drawAreaDataPoints = drawAreaDataPoints(series, opts, config, context, process), xAxisPoints = _drawAreaDataPoints.xAxisPoints, calPoints = _drawAreaDataPoints.calPoints, eachSpacing = _drawAreaDataPoints.eachSpacing; opts.chartData.xAxisPoints = xAxisPoints; opts.chartData.calPoints = calPoints; opts.chartData.eachSpacing = eachSpacing; drawYAxis(series, opts, config, context); if (opts.enableMarkLine !== false && process === 1) { drawMarkLine(opts, config, context); } drawLegend(opts.series, opts, config, context, opts.chartData); drawToolTipBridge(opts, config, context, process, eachSpacing, xAxisPoints); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; case 'ring': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function onProcess(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } opts.chartData.pieData = drawPieDataPoints(series, opts, config, context, process); drawLegend(opts.series, opts, config, context, opts.chartData); drawToolTipBridge(opts, config, context, process); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; case 'pie': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function onProcess(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } opts.chartData.pieData = drawPieDataPoints(series, opts, config, context, process); drawLegend(opts.series, opts, config, context, opts.chartData); drawToolTipBridge(opts, config, context, process); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; case 'rose': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function onProcess(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } opts.chartData.pieData = drawRoseDataPoints(series, opts, config, context, process); drawLegend(opts.series, opts, config, context, opts.chartData); drawToolTipBridge(opts, config, context, process); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; case 'radar': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function onProcess(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } opts.chartData.radarData = drawRadarDataPoints(series, opts, config, context, process); drawLegend(opts.series, opts, config, context, opts.chartData); drawToolTipBridge(opts, config, context, process); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; case 'arcbar': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function onProcess(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } opts.chartData.arcbarData = drawArcbarDataPoints(series, opts, config, context, process); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; case 'gauge': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function onProcess(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } opts.chartData.gaugeData = drawGaugeDataPoints(categories, series, opts, config, context, process); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; case 'candle': this.animationInstance = new Animation({ timing: opts.timing, duration: duration, onProcess: function onProcess(process) { context.clearRect(0, 0, opts.width, opts.height); if (opts.rotate) { contextRotate(context, opts); } drawYAxisGrid(categories, opts, config, context); drawXAxis(categories, opts, config, context); var _drawCandleDataPoints = drawCandleDataPoints(series, seriesMA, opts, config, context, process), xAxisPoints = _drawCandleDataPoints.xAxisPoints, calPoints = _drawCandleDataPoints.calPoints, eachSpacing = _drawCandleDataPoints.eachSpacing; opts.chartData.xAxisPoints = xAxisPoints; opts.chartData.calPoints = calPoints; opts.chartData.eachSpacing = eachSpacing; drawYAxis(series, opts, config, context); if (opts.enableMarkLine !== false && process === 1) { drawMarkLine(opts, config, context); } if (seriesMA) { drawLegend(seriesMA, opts, config, context, opts.chartData); } else { drawLegend(opts.series, opts, config, context, opts.chartData); } drawToolTipBridge(opts, config, context, process, eachSpacing, xAxisPoints); drawCanvas(opts, context); }, onAnimationFinish: function onAnimationFinish() { _this.uevent.trigger('renderComplete'); } }); break; } } function uChartsEvent() { this.events = {}; } uChartsEvent.prototype.addEventListener = function(type, listener) { this.events[type] = this.events[type] || []; this.events[type].push(listener); }; uChartsEvent.prototype.delEventListener = function(type) { this.events[type] = []; }; uChartsEvent.prototype.trigger = function() { for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } var type = args[0]; var params = args.slice(1); if (!!this.events[type]) { this.events[type].forEach(function(listener) { try { listener.apply(null, params); } catch (e) { //console.log('[uCharts] '+e); } }); } }; var uCharts = function uCharts(opts) { opts.pix = opts.pixelRatio ? opts.pixelRatio : 1; opts.fontSize = opts.fontSize ? opts.fontSize : 13; opts.fontColor = opts.fontColor ? opts.fontColor : config.fontColor; if (opts.background == "" || opts.background == "none") { opts.background = "#FFFFFF" } opts.title = assign({}, opts.title); opts.subtitle = assign({}, opts.subtitle); opts.duration = opts.duration ? opts.duration : 1000; opts.yAxis = assign({}, { data: [], showTitle: false, disabled: false, disableGrid: false, splitNumber: 5, gridType: 'solid', dashLength: 4 * opts.pix, gridColor: '#cccccc', padding: 10, fontColor: '#666666' }, opts.yAxis); opts.xAxis = assign({}, { rotateLabel: false, rotateAngle:45, disabled: false, disableGrid: false, splitNumber: 5, calibration:false, gridType: 'solid', dashLength: 4, scrollAlign: 'left', boundaryGap: 'center', axisLine: true, axisLineColor: '#cccccc' }, opts.xAxis); opts.xAxis.scrollPosition = opts.xAxis.scrollAlign; opts.legend = assign({}, { show: true, position: 'bottom', float: 'center', backgroundColor: 'rgba(0,0,0,0)', borderColor: 'rgba(0,0,0,0)', borderWidth: 0, padding: 5, margin: 5, itemGap: 10, fontSize: opts.fontSize, lineHeight: opts.fontSize, fontColor: opts.fontColor, formatter: {}, hiddenColor: '#CECECE' }, opts.legend); opts.extra = assign({}, opts.extra); opts.rotate = opts.rotate ? true : false; opts.animation = opts.animation ? true : false; opts.rotate = opts.rotate ? true : false; opts.canvas2d = opts.canvas2d ? true : false; let config$$1 = assign({}, config); config$$1.color = opts.color ? opts.color : config$$1.color; if (opts.type == 'pie') { config$$1.pieChartLinePadding = opts.dataLabel === false ? 0 : opts.extra.pie.labelWidth * opts.pix || config$$1.pieChartLinePadding * opts.pix; } if (opts.type == 'ring') { config$$1.pieChartLinePadding = opts.dataLabel === false ? 0 : opts.extra.ring.labelWidth * opts.pix || config$$1.pieChartLinePadding * opts.pix; } if (opts.type == 'rose') { config$$1.pieChartLinePadding = opts.dataLabel === false ? 0 : opts.extra.rose.labelWidth * opts.pix || config$$1.pieChartLinePadding * opts.pix; } config$$1.pieChartTextPadding = opts.dataLabel === false ? 0 : config$$1.pieChartTextPadding * opts.pix; //屏幕旋转 config$$1.rotate = opts.rotate; if (opts.rotate) { let tempWidth = opts.width; let tempHeight = opts.height; opts.width = tempHeight; opts.height = tempWidth; } //适配高分屏 opts.padding = opts.padding ? opts.padding : config$$1.padding; config$$1.yAxisWidth = config.yAxisWidth * opts.pix; config$$1.xAxisHeight = config.xAxisHeight * opts.pix; if (opts.enableScroll && opts.xAxis.scrollShow) { config$$1.xAxisHeight += 6 * opts.pix; } config$$1.fontSize = opts.fontSize * opts.pix; config$$1.titleFontSize = config.titleFontSize * opts.pix; config$$1.subtitleFontSize = config.subtitleFontSize * opts.pix; config$$1.toolTipPadding = config.toolTipPadding * opts.pix; config$$1.toolTipLineHeight = config.toolTipLineHeight * opts.pix; if(!opts.context){ throw new Error('[uCharts] 未获取到context!注意:v2.0版本后,需要自行获取canvas的绘图上下文并传入opts.context!'); } this.context = opts.context; if (!this.context.setTextAlign) { this.context.setStrokeStyle = function(e) { return this.strokeStyle = e; } this.context.setLineWidth = function(e) { return this.lineWidth = e; } this.context.setLineCap = function(e) { return this.lineCap = e; } this.context.setFontSize = function(e) { return this.font = e + "px sans-serif"; } this.context.setFillStyle = function(e) { return this.fillStyle = e; } this.context.setTextAlign = function(e) { return this.textAlign = e; } this.context.draw = function() {} } //兼容NVUEsetLineDash if(!this.context.setLineDash){ this.context.setLineDash = function(e) {} } opts.chartData = {}; this.uevent = new uChartsEvent(); this.scrollOption = { currentOffset: 0, startTouchX: 0, distance: 0, lastMoveTime: 0 }; this.opts = opts; this.config = config$$1; drawCharts.call(this, opts.type, opts, config$$1, this.context); }; uCharts.prototype.updateData = function() { let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; this.opts = assign({}, this.opts, data); this.opts.updateData = true; let scrollPosition = data.scrollPosition || 'current'; switch (scrollPosition) { case 'current': this.opts._scrollDistance_ = this.scrollOption.currentOffset; break; case 'left': this.opts._scrollDistance_ = 0; this.scrollOption = { currentOffset: 0, startTouchX: 0, distance: 0, lastMoveTime: 0 }; break; case 'right': let _calYAxisData = calYAxisData(this.opts.series, this.opts, this.config, this.context), yAxisWidth = _calYAxisData.yAxisWidth; this.config.yAxisWidth = yAxisWidth; let offsetLeft = 0; let _getXAxisPoints0 = getXAxisPoints(this.opts.categories, this.opts, this.config), xAxisPoints = _getXAxisPoints0.xAxisPoints, startX = _getXAxisPoints0.startX, endX = _getXAxisPoints0.endX, eachSpacing = _getXAxisPoints0.eachSpacing; let totalWidth = eachSpacing * (xAxisPoints.length - 1); let screenWidth = endX - startX; offsetLeft = screenWidth - totalWidth; this.scrollOption = { currentOffset: offsetLeft, startTouchX: offsetLeft, distance: 0, lastMoveTime: 0 }; this.opts._scrollDistance_ = offsetLeft; break; } drawCharts.call(this, this.opts.type, this.opts, this.config, this.context); }; uCharts.prototype.zoom = function() { var val = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.opts.xAxis.itemCount; if (this.opts.enableScroll !== true) { console.log('[uCharts] 请启用滚动条后使用') return; } //当前屏幕中间点 let centerPoint = Math.round(Math.abs(this.scrollOption.currentOffset) / this.opts.chartData.eachSpacing) + Math.round(this.opts.xAxis.itemCount / 2); this.opts.animation = false; this.opts.xAxis.itemCount = val.itemCount; //重新计算x轴偏移距离 let _calYAxisData = calYAxisData(this.opts.series, this.opts, this.config, this.context), yAxisWidth = _calYAxisData.yAxisWidth; this.config.yAxisWidth = yAxisWidth; let offsetLeft = 0; let _getXAxisPoints0 = getXAxisPoints(this.opts.categories, this.opts, this.config), xAxisPoints = _getXAxisPoints0.xAxisPoints, startX = _getXAxisPoints0.startX, endX = _getXAxisPoints0.endX, eachSpacing = _getXAxisPoints0.eachSpacing; let centerLeft = eachSpacing * centerPoint; let screenWidth = endX - startX; let MaxLeft = screenWidth - eachSpacing * (xAxisPoints.length - 1); offsetLeft = screenWidth / 2 - centerLeft; if (offsetLeft > 0) { offsetLeft = 0; } if (offsetLeft < MaxLeft) { offsetLeft = MaxLeft; } this.scrollOption = { currentOffset: offsetLeft, startTouchX: 0, distance: 0, lastMoveTime: 0 }; calValidDistance(this, offsetLeft, this.opts.chartData, this.config, this.opts); this.opts._scrollDistance_ = offsetLeft; drawCharts.call(this, this.opts.type, this.opts, this.config, this.context); }; uCharts.prototype.dobuleZoom = function(e) { if (this.opts.enableScroll !== true) { console.log('[uCharts] 请启用滚动条后使用') return; } const tcs = e.changedTouches; if (tcs.length < 2) { return; } for (var i = 0; i < tcs.length; i++) { tcs[i].x = tcs[i].x ? tcs[i].x : tcs[i].clientX; tcs[i].y = tcs[i].y ? tcs[i].y : tcs[i].clientY; } const ntcs = [getTouches(tcs[0], this.opts, e),getTouches(tcs[1], this.opts, e)]; const xlength = Math.abs(ntcs[0].x - ntcs[1].x); // 记录初始的两指之间的数据 if(!this.scrollOption.moveCount){ let cts0 = {changedTouches:[{x:tcs[0].x,y:this.opts.area[0] / this.opts.pix + 2}]}; let cts1 = {changedTouches:[{x:tcs[1].x,y:this.opts.area[0] / this.opts.pix + 2}]}; if(this.opts.rotate){ cts0 = {changedTouches:[{x:this.opts.height / this.opts.pix - this.opts.area[0] / this.opts.pix - 2,y:tcs[0].y}]}; cts1 = {changedTouches:[{x:this.opts.height / this.opts.pix - this.opts.area[0] / this.opts.pix - 2,y:tcs[1].y}]}; } const moveCurrent1 = this.getCurrentDataIndex(cts0).index; const moveCurrent2 = this.getCurrentDataIndex(cts1).index; const moveCount = Math.abs(moveCurrent1 - moveCurrent2); this.scrollOption.moveCount = moveCount; this.scrollOption.moveCurrent1 = Math.min(moveCurrent1, moveCurrent2); this.scrollOption.moveCurrent2 = Math.max(moveCurrent1, moveCurrent2); return; } let currentEachSpacing = xlength / this.scrollOption.moveCount; let itemCount = (this.opts.width - this.opts.area[1] - this.opts.area[3]) / currentEachSpacing; itemCount = itemCount <= 2 ? 2 : itemCount; itemCount = itemCount >= this.opts.categories.length ? this.opts.categories.length : itemCount; this.opts.animation = false; this.opts.xAxis.itemCount = itemCount; // 重新计算滚动条偏移距离 let offsetLeft = 0; let _getXAxisPoints0 = getXAxisPoints(this.opts.categories, this.opts, this.config), xAxisPoints = _getXAxisPoints0.xAxisPoints, startX = _getXAxisPoints0.startX, endX = _getXAxisPoints0.endX, eachSpacing = _getXAxisPoints0.eachSpacing; let currentLeft = eachSpacing * this.scrollOption.moveCurrent1; let screenWidth = endX - startX; let MaxLeft = screenWidth - eachSpacing * (xAxisPoints.length - 1); offsetLeft = -currentLeft+Math.min(ntcs[0].x,ntcs[1].x)-this.opts.area[3]-eachSpacing; if (offsetLeft > 0) { offsetLeft = 0; } if (offsetLeft < MaxLeft) { offsetLeft = MaxLeft; } this.scrollOption.currentOffset= offsetLeft; this.scrollOption.startTouchX= 0; this.scrollOption.distance=0; calValidDistance(this, offsetLeft, this.opts.chartData, this.config, this.opts); this.opts._scrollDistance_ = offsetLeft; drawCharts.call(this, this.opts.type, this.opts, this.config, this.context); } uCharts.prototype.stopAnimation = function() { this.animationInstance && this.animationInstance.stop(); }; uCharts.prototype.addEventListener = function(type, listener) { this.uevent.addEventListener(type, listener); }; uCharts.prototype.delEventListener = function(type) { this.uevent.delEventListener(type); }; uCharts.prototype.getCurrentDataIndex = function(e) { var touches = null; if (e.changedTouches) { touches = e.changedTouches[0]; } else { touches = e.mp.changedTouches[0]; } if (touches) { let _touches$ = getTouches(touches, this.opts, e); if (this.opts.type === 'pie' || this.opts.type === 'ring') { return findPieChartCurrentIndex({ x: _touches$.x, y: _touches$.y }, this.opts.chartData.pieData, this.opts); } else if (this.opts.type === 'rose') { return findRoseChartCurrentIndex({ x: _touches$.x, y: _touches$.y }, this.opts.chartData.pieData, this.opts); } else if (this.opts.type === 'radar') { return findRadarChartCurrentIndex({ x: _touches$.x, y: _touches$.y }, this.opts.chartData.radarData, this.opts.categories.length); } else if (this.opts.type === 'funnel') { return findFunnelChartCurrentIndex({ x: _touches$.x, y: _touches$.y }, this.opts.chartData.funnelData); } else if (this.opts.type === 'map') { return findMapChartCurrentIndex({ x: _touches$.x, y: _touches$.y }, this.opts); } else if (this.opts.type === 'word') { return findWordChartCurrentIndex({ x: _touches$.x, y: _touches$.y }, this.opts.chartData.wordCloudData); } else if (this.opts.type === 'bar') { return findBarChartCurrentIndex({ x: _touches$.x, y: _touches$.y }, this.opts.chartData.calPoints, this.opts, this.config, Math.abs(this.scrollOption.currentOffset)); } else { return findCurrentIndex({ x: _touches$.x, y: _touches$.y }, this.opts.chartData.calPoints, this.opts, this.config, Math.abs(this.scrollOption.currentOffset)); } } return -1; }; uCharts.prototype.getLegendDataIndex = function(e) { var touches = null; if (e.changedTouches) { touches = e.changedTouches[0]; } else { touches = e.mp.changedTouches[0]; } if (touches) { let _touches$ = getTouches(touches, this.opts, e); return findLegendIndex({ x: _touches$.x, y: _touches$.y }, this.opts.chartData.legendData); } return -1; }; uCharts.prototype.touchLegend = function(e) { var option = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var touches = null; if (e.changedTouches) { touches = e.changedTouches[0]; } else { touches = e.mp.changedTouches[0]; } if (touches) { var _touches$ = getTouches(touches, this.opts, e); var index = this.getLegendDataIndex(e); if (index >= 0) { if (this.opts.type == 'candle') { this.opts.seriesMA[index].show = !this.opts.seriesMA[index].show; } else { this.opts.series[index].show = !this.opts.series[index].show; } this.opts.animation = option.animation ? true : false; this.opts._scrollDistance_ = this.scrollOption.currentOffset; drawCharts.call(this, this.opts.type, this.opts, this.config, this.context); } } }; uCharts.prototype.showToolTip = function(e) { var option = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var touches = null; if (e.changedTouches) { touches = e.changedTouches[0]; } else { touches = e.mp.changedTouches[0]; } if (!touches) { console.log("[uCharts] 未获取到event坐标信息"); } var _touches$ = getTouches(touches, this.opts, e); var currentOffset = this.scrollOption.currentOffset; var opts = assign({}, this.opts, { _scrollDistance_: currentOffset, animation: false }); if (this.opts.type === 'line' || this.opts.type === 'area' || this.opts.type === 'column' || this.opts.type === 'scatter' || this.opts.type === 'bubble') { var current = this.getCurrentDataIndex(e); var index = option.index == undefined ? current.index : option.index; if (index > -1 || index.length>0) { var seriesData = getSeriesDataItem(this.opts.series, index, current.group); if (seriesData.length !== 0) { var _getToolTipData = getToolTipData(seriesData, this.opts, index, current.group, this.opts.categories, option), textList = _getToolTipData.textList, offset = _getToolTipData.offset; offset.y = _touches$.y; opts.tooltip = { textList: option.textList !== undefined ? option.textList : textList, offset: option.offset !== undefined ? option.offset : offset, option: option, index: index }; } } drawCharts.call(this, opts.type, opts, this.config, this.context); } if (this.opts.type === 'mount') { var index = option.index == undefined ? this.getCurrentDataIndex(e).index : option.index; if (index > -1) { var opts = assign({}, this.opts, {animation: false}); var seriesData = assign({}, opts._series_[index]); var textList = [{ text: option.formatter ? option.formatter(seriesData, undefined, index, opts) : seriesData.name + ': ' + seriesData.data, color: seriesData.color }]; var offset = { x: opts.chartData.calPoints[index].x, y: _touches$.y }; opts.tooltip = { textList: option.textList ? option.textList : textList, offset: option.offset !== undefined ? option.offset : offset, option: option, index: index }; } drawCharts.call(this, opts.type, opts, this.config, this.context); } if (this.opts.type === 'bar') { var current = this.getCurrentDataIndex(e); var index = option.index == undefined ? current.index : option.index; if (index > -1 || index.length>0) { var seriesData = getSeriesDataItem(this.opts.series, index, current.group); if (seriesData.length !== 0) { var _getToolTipData = getToolTipData(seriesData, this.opts, index, current.group, this.opts.categories, option), textList = _getToolTipData.textList, offset = _getToolTipData.offset; offset.x = _touches$.x; opts.tooltip = { textList: option.textList !== undefined ? option.textList : textList, offset: option.offset !== undefined ? option.offset : offset, option: option, index: index }; } } drawCharts.call(this, opts.type, opts, this.config, this.context); } if (this.opts.type === 'mix') { var current = this.getCurrentDataIndex(e); var index = option.index == undefined ? current.index : option.index; if (index > -1) { var currentOffset = this.scrollOption.currentOffset; var opts = assign({}, this.opts, { _scrollDistance_: currentOffset, animation: false }); var seriesData = getSeriesDataItem(this.opts.series, index); if (seriesData.length !== 0) { var _getMixToolTipData = getMixToolTipData(seriesData, this.opts, index, this.opts.categories, option), textList = _getMixToolTipData.textList, offset = _getMixToolTipData.offset; offset.y = _touches$.y; opts.tooltip = { textList: option.textList ? option.textList : textList, offset: option.offset !== undefined ? option.offset : offset, option: option, index: index }; } } drawCharts.call(this, opts.type, opts, this.config, this.context); } if (this.opts.type === 'candle') { var current = this.getCurrentDataIndex(e); var index = option.index == undefined ? current.index : option.index; if (index > -1) { var currentOffset = this.scrollOption.currentOffset; var opts = assign({}, this.opts, { _scrollDistance_: currentOffset, animation: false }); var seriesData = getSeriesDataItem(this.opts.series, index); if (seriesData.length !== 0) { var _getToolTipData = getCandleToolTipData(this.opts.series[0].data, seriesData, this.opts, index, this.opts.categories, this.opts.extra.candle, option), textList = _getToolTipData.textList, offset = _getToolTipData.offset; offset.y = _touches$.y; opts.tooltip = { textList: option.textList ? option.textList : textList, offset: option.offset !== undefined ? option.offset : offset, option: option, index: index }; } } drawCharts.call(this, opts.type, opts, this.config, this.context); } if (this.opts.type === 'pie' || this.opts.type === 'ring' || this.opts.type === 'rose' || this.opts.type === 'funnel') { var index = option.index == undefined ? this.getCurrentDataIndex(e) : option.index; if (index > -1) { var opts = assign({}, this.opts, {animation: false}); var seriesData = assign({}, opts._series_[index]); var textList = [{ text: option.formatter ? option.formatter(seriesData, undefined, index, opts) : seriesData.name + ': ' + seriesData.data, color: seriesData.color }]; var offset = { x: _touches$.x, y: _touches$.y }; opts.tooltip = { textList: option.textList ? option.textList : textList, offset: option.offset !== undefined ? option.offset : offset, option: option, index: index }; } drawCharts.call(this, opts.type, opts, this.config, this.context); } if (this.opts.type === 'map') { var index = option.index == undefined ? this.getCurrentDataIndex(e) : option.index; if (index > -1) { var opts = assign({}, this.opts, {animation: false}); var seriesData = assign({}, this.opts.series[index]); seriesData.name = seriesData.properties.name var textList = [{ text: option.formatter ? option.formatter(seriesData, undefined, index, this.opts) : seriesData.name, color: seriesData.color }]; var offset = { x: _touches$.x, y: _touches$.y }; opts.tooltip = { textList: option.textList ? option.textList : textList, offset: option.offset !== undefined ? option.offset : offset, option: option, index: index }; } opts.updateData = false; drawCharts.call(this, opts.type, opts, this.config, this.context); } if (this.opts.type === 'word') { var index = option.index == undefined ? this.getCurrentDataIndex(e) : option.index; if (index > -1) { var opts = assign({}, this.opts, {animation: false}); var seriesData = assign({}, this.opts.series[index]); var textList = [{ text: option.formatter ? option.formatter(seriesData, undefined, index, this.opts) : seriesData.name, color: seriesData.color }]; var offset = { x: _touches$.x, y: _touches$.y }; opts.tooltip = { textList: option.textList ? option.textList : textList, offset: option.offset !== undefined ? option.offset : offset, option: option, index: index }; } opts.updateData = false; drawCharts.call(this, opts.type, opts, this.config, this.context); } if (this.opts.type === 'radar') { var index = option.index == undefined ? this.getCurrentDataIndex(e) : option.index; if (index > -1) { var opts = assign({}, this.opts, {animation: false}); var seriesData = getSeriesDataItem(this.opts.series, index); if (seriesData.length !== 0) { var textList = seriesData.map((item) => { return { text: option.formatter ? option.formatter(item, this.opts.categories[index], index, this.opts) : item.name + ': ' + item.data, color: item.color }; }); var offset = { x: _touches$.x, y: _touches$.y }; opts.tooltip = { textList: option.textList ? option.textList : textList, offset: option.offset !== undefined ? option.offset : offset, option: option, index: index }; } } drawCharts.call(this, opts.type, opts, this.config, this.context); } }; uCharts.prototype.translate = function(distance) { this.scrollOption = { currentOffset: distance, startTouchX: distance, distance: 0, lastMoveTime: 0 }; let opts = assign({}, this.opts, { _scrollDistance_: distance, animation: false }); drawCharts.call(this, this.opts.type, opts, this.config, this.context); }; uCharts.prototype.scrollStart = function(e) { var touches = null; if (e.changedTouches) { touches = e.changedTouches[0]; } else { touches = e.mp.changedTouches[0]; } var _touches$ = getTouches(touches, this.opts, e); if (touches && this.opts.enableScroll === true) { this.scrollOption.startTouchX = _touches$.x; } }; uCharts.prototype.scroll = function(e) { if (this.scrollOption.lastMoveTime === 0) { this.scrollOption.lastMoveTime = Date.now(); } let Limit = this.opts.touchMoveLimit || 60; let currMoveTime = Date.now(); let duration = currMoveTime - this.scrollOption.lastMoveTime; if (duration < Math.floor(1000 / Limit)) return; if (this.scrollOption.startTouchX == 0) return; this.scrollOption.lastMoveTime = currMoveTime; var touches = null; if (e.changedTouches) { touches = e.changedTouches[0]; } else { touches = e.mp.changedTouches[0]; } if (touches && this.opts.enableScroll === true) { var _touches$ = getTouches(touches, this.opts, e); var _distance; _distance = _touches$.x - this.scrollOption.startTouchX; var currentOffset = this.scrollOption.currentOffset; var validDistance = calValidDistance(this, currentOffset + _distance, this.opts.chartData, this.config, this.opts); this.scrollOption.distance = _distance = validDistance - currentOffset; var opts = assign({}, this.opts, { _scrollDistance_: currentOffset + _distance, animation: false }); this.opts = opts; drawCharts.call(this, opts.type, opts, this.config, this.context); return currentOffset + _distance; } }; uCharts.prototype.scrollEnd = function(e) { if (this.opts.enableScroll === true) { var _scrollOption = this.scrollOption, currentOffset = _scrollOption.currentOffset, distance = _scrollOption.distance; this.scrollOption.currentOffset = currentOffset + distance; this.scrollOption.distance = 0; this.scrollOption.moveCount = 0; } }; export default uCharts; ================================================ FILE: bookkeeping_user_uniapp/uni_modules/qiun-data-charts/license.md ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: bookkeeping_user_uniapp/uni_modules/qiun-data-charts/package.json ================================================ { "id": "qiun-data-charts", "displayName": "秋云 ucharts echarts 高性能跨全端图表组件", "version": "2.4.3-20220505", "description": "uCharts 新增双指缩放、新增山峰图!支持H5及APP用 ucharts echarts 渲染图表,uniapp可视化首选组件", "keywords": [ "ucharts", "echarts", "f2", "图表", "可视化" ], "repository": "https://gitee.com/uCharts/uCharts", "engines": { "HBuilderX": "^3.3.8" }, "dcloudext": { "category": [ "前端组件", "通用组件" ], "sale": { "regular": { "price": "0.00" }, "sourcecode": { "price": "0.00" } }, "contact": { "qq": "474119" }, "declaration": { "ads": "无", "data": "插件不采集任何数据", "permissions": "无" }, "npmurl": "https://www.npmjs.com/~qiun" }, "uni_modules": { "dependencies": [], "encrypt": [], "platforms": { "cloud": { "tcb": "y", "aliyun": "y" }, "client": { "App": { "app-vue": "y", "app-nvue": "y" }, "H5-mobile": { "Safari": "y", "Android Browser": "y", "微信浏览器(Android)": "y", "QQ浏览器(Android)": "y" }, "H5-pc": { "Chrome": "y", "IE": "y", "Edge": "y", "Firefox": "y", "Safari": "y" }, "小程序": { "微信": "y", "阿里": "y", "百度": "y", "字节跳动": "y", "QQ": "y" }, "快应用": { "华为": "y", "联盟": "y" }, "Vue": { "vue2": "y", "vue3": "y" } } } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/qiun-data-charts/readme.md ================================================ ## 写给uCharts使用者的一封信 亲爱的用户: - 由于最近上线的官网中实行了部分收费体验,收到了许多用户的使用反馈,大致反馈的问题都指向同一矛头:为何新官网的在线工具也要收费?对于这件事,我们深表歉意。由于新官网本身未提供技术文档,使得用户误以为我们对文档实行了收费。经我们连夜整改,新官网目前已经将技术文档开放出来供大家阅读使用,并免费对外开放了【演示】中的查看全端全平台的代码的功能,为此再次向所受影响的用户们致以诚恳的歉意。 - 其次,我们须澄清几点,如下: 1. uCharts的插件本身遵循开源原则,并不收费,用户可自行到DCloud市场与Gitee码云上获取源码 2. uCharts的技术文档永久对用户开放 3. 收费内容仅针对原生工具、组件工具、定制功能以及模板市场的部分收费模板 - uCharts为什么实行收费原则? 1. 服务器的费用支撑 2. 团队的运营支出;正如你所见,我们的群里有大量的用户在请教图表配置与反馈问题,群里的每一位管理员都在花费不少精力在积极解决用户的问题,然而遇到巨大的咨询量时,我们无法及时、精准解答回复,因此,我们推出了会员优先服务 3. 与其说模板市场是收费,倒不如说给野生用户提供了创造价值的机会,用户既可以在上面发布模板赚取费用,遇到心动的模板也能免费/付费使用 - 收费不是目的,正如你们所见,用户可以申请成为[【开发者】](https://www.ucharts.cn/v2/#/agreement/developer),开发者不限制任何官网功能,并享有官方指导、开发、改造uCharts的权力,并且活动期间【返还超级会员费用】!我们想说的是,我们新版官网上线旨在希望更多的用户加入到开发者的队伍,我们共同去维护uCharts! 我们相信:星星之火可以燎原! uCharts技术团队 2022.4.23 ![logo](https://img-blog.csdnimg.cn/4a276226973841468c1be356f8d9438b.png) [![star](https://gitee.com/uCharts/uCharts/badge/star.svg?theme=gvp)](https://gitee.com/uCharts/uCharts/stargazers) [![fork](https://gitee.com/uCharts/uCharts/badge/fork.svg?theme=gvp)](https://gitee.com/uCharts/uCharts/members) [![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) [![npm package](https://img.shields.io/npm/v/@qiun/ucharts.svg?style=flat-square)](https://www.npmjs.com/~qiun) ## uCharts简介 `uCharts`是一款基于`canvas API`开发的适用于所有前端应用的图表库,开发者编写一套代码,可运行到 Web、iOS、Android(基于 uni-app / taro )、以及各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等更多支持 canvas API 的平台。 ## 官方网站 ## [https://www.ucharts.cn](https://www.ucharts.cn) ## 快速体验 一套代码编到多个平台,依次扫描二维码,亲自体验uCharts图表跨平台效果!其他平台请自行编译。 ![](https://www.ucharts.cn/images/web/guide/qrcode20220224.png) ## 致开发者 感谢各位开发者`四年`来对秋云及uCharts的支持,uCharts的进步离不开各位开发者的鼓励与贡献。为更好的帮助各位开发者使用图表工具,我们推出了新版官网,增加了在线定制、问答社区、在线配置等一些增值服务,为确保您能更好的应用图表组件,建议您先`仔细阅读本页指南`以及`常见问题`,而不是下载下来`直接使用`。如仍然不能解决,请到`官网社区`或开通会员后加入`专属VIP会员群`提问将会很快得到回答。 ## 社群支持 uCharts官方拥有4个2000人的QQ群及专属VIP会员群支持,庞大的用户量证明我们一直在努力,请各位放心使用!uCharts的开源图表组件的开发,团队付出了大量的时间与精力,经过四来的考验,不会有比较明显的bug,请各位放心使用。如果您有更好的想法,可以在`码云提交Pull Requests`以帮助更多开发者完成需求,再次感谢各位对uCharts的鼓励与支持! #### 官方交流群 - 交流群1:371774600(已满) - 交流群2:619841586(已满) - 交流群3:955340127(已满) - 交流群4:641669795 - 口令`uniapp` #### 专属VIP会员群 - 开通会员后详见【账号详情】页面中顶部的滚动通知 - 口令`您的用户ID` ## 版权信息 uCharts始终坚持开源,遵循 [Apache Licence 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) 开源协议,意味着您无需支付任何费用,即可将uCharts应用到您的产品中。 注意:这并不意味着您可以将uCharts应用到非法的领域,比如涉及赌博,暴力等方面。如因此产生纠纷或法律问题,uCharts相关方及秋云科技不承担任何责任。 ## 合作伙伴 [![DIY官网](https://www.ucharts.cn/images/web/guide/links/diy-gw.png)](https://www.diygw.com/) [![HasChat](https://www.ucharts.cn/images/web/guide/links/haschat.png)](https://gitee.com/howcode/has-chat) [![uViewUI](https://www.ucharts.cn/images/web/guide/links/uView.png)](https://www.uviewui.com/) [![图鸟UI](https://www.ucharts.cn/images/web/guide/links/tuniao.png)](https://ext.dcloud.net.cn/plugin?id=7088) [![thorui](https://www.ucharts.cn/images/web/guide/links/thorui.png)](https://ext.dcloud.net.cn/publisher?id=202) [![FirstUI](https://www.ucharts.cn/images/web/guide/links/first.png)](https://www.firstui.cn/) [![nProUI](https://www.ucharts.cn/images/web/guide/links/nPro.png)](https://ext.dcloud.net.cn/plugin?id=5169) [![GraceUI](https://www.ucharts.cn/images/web/guide/links/grace.png)](https://www.graceui.com/) ## 更新记录 详见官网指南中说明,[点击此处查看](https://www.ucharts.cn/v2/#/guide/index?id=100) ## 相关链接 - [uCharts官网](https://www.ucharts.cn) - [DCloud插件市场地址](https://ext.dcloud.net.cn/plugin?id=271) - [uCharts码云开源托管地址](https://gitee.com/uCharts/uCharts) [![star](https://gitee.com/uCharts/uCharts/badge/star.svg?theme=gvp)](https://gitee.com/uCharts/uCharts/stargazers) - [uCharts npm开源地址](https://www.ucharts.cn) - [ECharts官网](https://echarts.apache.org/zh/index.html) - [ECharts配置手册](https://echarts.apache.org/zh/option.html) - [图表组件在项目中的应用 ReportPlus数据报表](https://www.ucharts.cn/v2/#/layout/info?id=1) ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-datetime-picker/changelog.md ================================================ ## 2.2.10(2022-09-19) - 修复,反向选择日期范围,日期显示异常,[详情](https://ask.dcloud.net.cn/question/153401?item_id=212892&rf=false) ## 2.2.9(2022-09-16) - 可以使用 uni-scss 控制主题色 ## 2.2.8(2022-09-08) - 修复 close事件无效的 bug ## 2.2.7(2022-09-05) - 修复 移动端 maskClick 无效的 bug,详见:[https://ask.dcloud.net.cn/question/140824?item_id=209458&rf=false](https://ask.dcloud.net.cn/question/140824?item_id=209458&rf=false) ## 2.2.6(2022-06-30) - 优化 组件样式,调整了组件图标大小、高度、颜色等,与uni-ui风格保持一致 ## 2.2.5(2022-06-24) - 修复 日历顶部年月及底部确认未国际化 bug ## 2.2.4(2022-03-31) - 修复 Vue3 下动态赋值,单选类型未响应的 bug ## 2.2.3(2022-03-28) - 修复 Vue3 下动态赋值未响应的 bug ## 2.2.2(2021-12-10) - 修复 clear-icon 属性在小程序平台不生效的 bug ## 2.2.1(2021-12-10) - 修复 日期范围选在小程序平台,必须多点击一次才能取消选中状态的 bug ## 2.2.0(2021-11-19) - 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) - 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-datetime-picker](https://uniapp.dcloud.io/component/uniui/uni-datetime-picker) ## 2.1.5(2021-11-09) - 新增 提供组件设计资源,组件样式调整 ## 2.1.4(2021-09-10) - 修复 hide-second 在移动端的 bug - 修复 单选赋默认值时,赋值日期未高亮的 bug - 修复 赋默认值时,移动端未正确显示时间的 bug ## 2.1.3(2021-09-09) - 新增 hide-second 属性,支持只使用时分,隐藏秒 ## 2.1.2(2021-09-03) - 优化 取消选中时(范围选)直接开始下一次选择, 避免多点一次 - 优化 移动端支持清除按钮,同时支持通过 ref 调用组件的 clear 方法 - 优化 调整字号大小,美化日历界面 - 修复 因国际化导致的 placeholder 失效的 bug ## 2.1.1(2021-08-24) - 新增 支持国际化 - 优化 范围选择器在 pc 端过宽的问题 ## 2.1.0(2021-08-09) - 新增 适配 vue3 ## 2.0.19(2021-08-09) - 新增 支持作为 uni-forms 子组件相关功能 - 修复 在 uni-forms 中使用时,选择时间报 NAN 错误的 bug ## 2.0.18(2021-08-05) - 修复 type 属性动态赋值无效的 bug - 修复 ‘确认’按钮被 tabbar 遮盖 bug - 修复 组件未赋值时范围选左、右日历相同的 bug ## 2.0.17(2021-08-04) - 修复 范围选未正确显示当前值的 bug - 修复 h5 平台(移动端)报错 'cale' of undefined 的 bug ## 2.0.16(2021-07-21) - 新增 return-type 属性支持返回 date 日期对象 ## 2.0.15(2021-07-14) - 修复 单选日期类型,初始赋值后不在当前日历的 bug - 新增 clearIcon 属性,显示框的清空按钮可配置显示隐藏(仅 pc 有效) - 优化 移动端移除显示框的清空按钮,无实际用途 ## 2.0.14(2021-07-14) - 修复 组件赋值为空,界面未更新的 bug - 修复 start 和 end 不能动态赋值的 bug - 修复 范围选类型,用户选择后再次选择右侧日历(结束日期)显示不正确的 bug ## 2.0.13(2021-07-08) - 修复 范围选择不能动态赋值的 bug ## 2.0.12(2021-07-08) - 修复 范围选择的初始时间在一个月内时,造成无法选择的bug ## 2.0.11(2021-07-08) - 优化 弹出层在超出视窗边缘定位不准确的问题 ## 2.0.10(2021-07-08) - 修复 范围起始点样式的背景色与今日样式的字体前景色融合,导致日期字体看不清的 bug - 优化 弹出层在超出视窗边缘被遮盖的问题 ## 2.0.9(2021-07-07) - 新增 maskClick 事件 - 修复 特殊情况日历 rpx 布局错误的 bug,rpx -> px - 修复 范围选择时清空返回值不合理的bug,['', ''] -> [] ## 2.0.8(2021-07-07) - 新增 日期时间显示框支持插槽 ## 2.0.7(2021-07-01) - 优化 添加 uni-icons 依赖 ## 2.0.6(2021-05-22) - 修复 图标在小程序上不显示的 bug - 优化 重命名引用组件,避免潜在组件命名冲突 ## 2.0.5(2021-05-20) - 优化 代码目录扁平化 ## 2.0.4(2021-05-12) - 新增 组件示例地址 ## 2.0.3(2021-05-10) - 修复 ios 下不识别 '-' 日期格式的 bug - 优化 pc 下弹出层添加边框和阴影 ## 2.0.2(2021-05-08) - 修复 在 admin 中获取弹出层定位错误的bug ## 2.0.1(2021-05-08) - 修复 type 属性向下兼容,默认值从 date 变更为 datetime ## 2.0.0(2021-04-30) - 支持日历形式的日期+时间的范围选择 > 注意:此版本不向后兼容,不再支持单独时间选择(type=time)及相关的 hide-second 属性(时间选可使用内置组件 picker) ## 1.0.6(2021-03-18) - 新增 hide-second 属性,时间支持仅选择时、分 - 修复 选择跟显示的日期不一样的 bug - 修复 chang事件触发2次的 bug - 修复 分、秒 end 范围错误的 bug - 优化 更好的 nvue 适配 ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar-item.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/en.json ================================================ { "uni-datetime-picker.selectDate": "select date", "uni-datetime-picker.selectTime": "select time", "uni-datetime-picker.selectDateTime": "select datetime", "uni-datetime-picker.startDate": "start date", "uni-datetime-picker.endDate": "end date", "uni-datetime-picker.startTime": "start time", "uni-datetime-picker.endTime": "end time", "uni-datetime-picker.ok": "ok", "uni-datetime-picker.clear": "clear", "uni-datetime-picker.cancel": "cancel", "uni-datetime-picker.year": "-", "uni-datetime-picker.month": "", "uni-calender.MON": "MON", "uni-calender.TUE": "TUE", "uni-calender.WED": "WED", "uni-calender.THU": "THU", "uni-calender.FRI": "FRI", "uni-calender.SAT": "SAT", "uni-calender.SUN": "SUN", "uni-calender.confirm": "confirm" } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/index.js ================================================ import en from './en.json' import zhHans from './zh-Hans.json' import zhHant from './zh-Hant.json' export default { en, 'zh-Hans': zhHans, 'zh-Hant': zhHant } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hans.json ================================================ { "uni-datetime-picker.selectDate": "选择日期", "uni-datetime-picker.selectTime": "选择时间", "uni-datetime-picker.selectDateTime": "选择日期时间", "uni-datetime-picker.startDate": "开始日期", "uni-datetime-picker.endDate": "结束日期", "uni-datetime-picker.startTime": "开始时间", "uni-datetime-picker.endTime": "结束时间", "uni-datetime-picker.ok": "确定", "uni-datetime-picker.clear": "清除", "uni-datetime-picker.cancel": "取消", "uni-datetime-picker.year": "年", "uni-datetime-picker.month": "月", "uni-calender.SUN": "日", "uni-calender.MON": "一", "uni-calender.TUE": "二", "uni-calender.WED": "三", "uni-calender.THU": "四", "uni-calender.FRI": "五", "uni-calender.SAT": "六", "uni-calender.confirm": "确认" } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hant.json ================================================ { "uni-datetime-picker.selectDate": "選擇日期", "uni-datetime-picker.selectTime": "選擇時間", "uni-datetime-picker.selectDateTime": "選擇日期時間", "uni-datetime-picker.startDate": "開始日期", "uni-datetime-picker.endDate": "結束日期", "uni-datetime-picker.startTime": "開始时间", "uni-datetime-picker.endTime": "結束时间", "uni-datetime-picker.ok": "確定", "uni-datetime-picker.clear": "清除", "uni-datetime-picker.cancel": "取消", "uni-datetime-picker.year": "年", "uni-datetime-picker.month": "月", "uni-calender.SUN": "日", "uni-calender.MON": "一", "uni-calender.TUE": "二", "uni-calender.WED": "三", "uni-calender.THU": "四", "uni-calender.FRI": "五", "uni-calender.SAT": "六", "uni-calender.confirm": "確認" } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/keypress.js ================================================ // #ifdef H5 export default { name: 'Keypress', props: { disable: { type: Boolean, default: false } }, mounted () { const keyNames = { esc: ['Esc', 'Escape'], tab: 'Tab', enter: 'Enter', space: [' ', 'Spacebar'], up: ['Up', 'ArrowUp'], left: ['Left', 'ArrowLeft'], right: ['Right', 'ArrowRight'], down: ['Down', 'ArrowDown'], delete: ['Backspace', 'Delete', 'Del'] } const listener = ($event) => { if (this.disable) { return } const keyName = Object.keys(keyNames).find(key => { const keyName = $event.key const value = keyNames[key] return value === keyName || (Array.isArray(value) && value.includes(keyName)) }) if (keyName) { // 避免和其他按键事件冲突 setTimeout(() => { this.$emit(keyName, {}) }, 0) } } document.addEventListener('keyup', listener) this.$once('hook:beforeDestroy', () => { document.removeEventListener('keyup', listener) }) }, render: () => {} } // #endif ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/time-picker.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/util.js ================================================ class Calendar { constructor({ date, selected, startDate, endDate, range, // multipleStatus } = {}) { // 当前日期 this.date = this.getDate(new Date()) // 当前初入日期 // 打点信息 this.selected = selected || []; // 范围开始 this.startDate = startDate // 范围结束 this.endDate = endDate this.range = range // 多选状态 this.cleanMultipleStatus() // 每周日期 this.weeks = {} // this._getWeek(this.date.fullDate) // this.multipleStatus = multipleStatus this.lastHover = false } /** * 设置日期 * @param {Object} date */ setDate(date) { this.selectDate = this.getDate(date) this._getWeek(this.selectDate.fullDate) } /** * 清理多选状态 */ cleanMultipleStatus() { this.multipleStatus = { before: '', after: '', data: [] } } /** * 重置开始日期 */ resetSatrtDate(startDate) { // 范围开始 this.startDate = startDate } /** * 重置结束日期 */ resetEndDate(endDate) { // 范围结束 this.endDate = endDate } /** * 获取任意时间 */ getDate(date, AddDayCount = 0, str = 'day') { if (!date) { date = new Date() } if (typeof date !== 'object') { date = date.replace(/-/g, '/') } const dd = new Date(date) switch (str) { case 'day': dd.setDate(dd.getDate() + AddDayCount) // 获取AddDayCount天后的日期 break case 'month': if (dd.getDate() === 31) { dd.setDate(dd.getDate() + AddDayCount) } else { dd.setMonth(dd.getMonth() + AddDayCount) // 获取AddDayCount天后的日期 } break case 'year': dd.setFullYear(dd.getFullYear() + AddDayCount) // 获取AddDayCount天后的日期 break } const y = dd.getFullYear() const m = dd.getMonth() + 1 < 10 ? '0' + (dd.getMonth() + 1) : dd.getMonth() + 1 // 获取当前月份的日期,不足10补0 const d = dd.getDate() < 10 ? '0' + dd.getDate() : dd.getDate() // 获取当前几号,不足10补0 return { fullDate: y + '-' + m + '-' + d, year: y, month: m, date: d, day: dd.getDay() } } /** * 获取上月剩余天数 */ _getLastMonthDays(firstDay, full) { let dateArr = [] for (let i = firstDay; i > 0; i--) { const beforeDate = new Date(full.year, full.month - 1, -i + 1).getDate() dateArr.push({ date: beforeDate, month: full.month - 1, disable: true }) } return dateArr } /** * 获取本月天数 */ _currentMonthDys(dateData, full) { let dateArr = [] let fullDate = this.date.fullDate for (let i = 1; i <= dateData; i++) { let isinfo = false let nowDate = full.year + '-' + (full.month < 10 ? full.month : full.month) + '-' + (i < 10 ? '0' + i : i) // 是否今天 let isDay = fullDate === nowDate // 获取打点信息 let info = this.selected && this.selected.find((item) => { if (this.dateEqual(nowDate, item.date)) { return item } }) // 日期禁用 let disableBefore = true let disableAfter = true if (this.startDate) { // let dateCompBefore = this.dateCompare(this.startDate, fullDate) // disableBefore = this.dateCompare(dateCompBefore ? this.startDate : fullDate, nowDate) disableBefore = this.dateCompare(this.startDate, nowDate) } if (this.endDate) { // let dateCompAfter = this.dateCompare(fullDate, this.endDate) // disableAfter = this.dateCompare(nowDate, dateCompAfter ? this.endDate : fullDate) disableAfter = this.dateCompare(nowDate, this.endDate) } let multiples = this.multipleStatus.data let checked = false let multiplesStatus = -1 if (this.range) { if (multiples) { multiplesStatus = multiples.findIndex((item) => { return this.dateEqual(item, nowDate) }) } if (multiplesStatus !== -1) { checked = true } } let data = { fullDate: nowDate, year: full.year, date: i, multiple: this.range ? checked : false, beforeMultiple: this.isLogicBefore(nowDate, this.multipleStatus.before, this.multipleStatus.after), afterMultiple: this.isLogicAfter(nowDate, this.multipleStatus.before, this.multipleStatus.after), month: full.month, disable: !(disableBefore && disableAfter), isDay, userChecked: false } if (info) { data.extraInfo = info } dateArr.push(data) } return dateArr } /** * 获取下月天数 */ _getNextMonthDays(surplus, full) { let dateArr = [] for (let i = 1; i < surplus + 1; i++) { dateArr.push({ date: i, month: Number(full.month) + 1, disable: true }) } return dateArr } /** * 获取当前日期详情 * @param {Object} date */ getInfo(date) { if (!date) { date = new Date() } const dateInfo = this.canlender.find(item => item.fullDate === this.getDate(date).fullDate) return dateInfo } /** * 比较时间大小 */ dateCompare(startDate, endDate) { // 计算截止时间 startDate = new Date(startDate.replace('-', '/').replace('-', '/')) // 计算详细项的截止时间 endDate = new Date(endDate.replace('-', '/').replace('-', '/')) if (startDate <= endDate) { return true } else { return false } } /** * 比较时间是否相等 */ dateEqual(before, after) { // 计算截止时间 before = new Date(before.replace('-', '/').replace('-', '/')) // 计算详细项的截止时间 after = new Date(after.replace('-', '/').replace('-', '/')) if (before.getTime() - after.getTime() === 0) { return true } else { return false } } /** * 比较真实起始日期 */ isLogicBefore(currentDay, before, after) { let logicBefore = before if (before && after) { logicBefore = this.dateCompare(before, after) ? before : after } return this.dateEqual(logicBefore, currentDay) } isLogicAfter(currentDay, before, after) { let logicAfter = after if (before && after) { logicAfter = this.dateCompare(before, after) ? after : before } return this.dateEqual(logicAfter, currentDay) } /** * 获取日期范围内所有日期 * @param {Object} begin * @param {Object} end */ geDateAll(begin, end) { var arr = [] var ab = begin.split('-') var ae = end.split('-') var db = new Date() db.setFullYear(ab[0], ab[1] - 1, ab[2]) var de = new Date() de.setFullYear(ae[0], ae[1] - 1, ae[2]) var unixDb = db.getTime() - 24 * 60 * 60 * 1000 var unixDe = de.getTime() - 24 * 60 * 60 * 1000 for (var k = unixDb; k <= unixDe;) { k = k + 24 * 60 * 60 * 1000 arr.push(this.getDate(new Date(parseInt(k))).fullDate) } return arr } /** * 获取多选状态 */ setMultiple(fullDate) { let { before, after } = this.multipleStatus if (!this.range) return if (before && after) { if (!this.lastHover) { this.lastHover = true return } this.multipleStatus.before = fullDate this.multipleStatus.after = '' this.multipleStatus.data = [] this.multipleStatus.fulldate = '' this.lastHover = false } else { if (!before) { this.multipleStatus.before = fullDate this.lastHover = false } else { this.multipleStatus.after = fullDate if (this.dateCompare(this.multipleStatus.before, this.multipleStatus.after)) { this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus .after); } else { this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus .before); } this.lastHover = true } } this._getWeek(fullDate) } /** * 鼠标 hover 更新多选状态 */ setHoverMultiple(fullDate) { let { before, after } = this.multipleStatus if (!this.range) return if (this.lastHover) return if (!before) { this.multipleStatus.before = fullDate } else { this.multipleStatus.after = fullDate if (this.dateCompare(this.multipleStatus.before, this.multipleStatus.after)) { this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus.after); } else { this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus.before); } } this._getWeek(fullDate) } /** * 更新默认值多选状态 */ setDefaultMultiple(before, after) { this.multipleStatus.before = before this.multipleStatus.after = after if (before && after) { if (this.dateCompare(before, after)) { this.multipleStatus.data = this.geDateAll(before, after); this._getWeek(after) } else { this.multipleStatus.data = this.geDateAll(after, before); this._getWeek(before) } } } /** * 获取每周数据 * @param {Object} dateData */ _getWeek(dateData) { const { fullDate, year, month, date, day } = this.getDate(dateData) let firstDay = new Date(year, month - 1, 1).getDay() let currentDay = new Date(year, month, 0).getDate() let dates = { lastMonthDays: this._getLastMonthDays(firstDay, this.getDate(dateData)), // 上个月末尾几天 currentMonthDys: this._currentMonthDys(currentDay, this.getDate(dateData)), // 本月天数 nextMonthDays: [], // 下个月开始几天 weeks: [] } let canlender = [] const surplus = 42 - (dates.lastMonthDays.length + dates.currentMonthDys.length) dates.nextMonthDays = this._getNextMonthDays(surplus, this.getDate(dateData)) canlender = canlender.concat(dates.lastMonthDays, dates.currentMonthDys, dates.nextMonthDays) let weeks = {} // 拼接数组 上个月开始几天 + 本月天数+ 下个月开始几天 for (let i = 0; i < canlender.length; i++) { if (i % 7 === 0) { weeks[parseInt(i / 7)] = new Array(7) } weeks[parseInt(i / 7)][i % 7] = canlender[i] } this.canlender = canlender this.weeks = weeks } //静态方法 // static init(date) { // if (!this.instance) { // this.instance = new Calendar(date); // } // return this.instance; // } } export default Calendar ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-datetime-picker/package.json ================================================ { "id": "uni-datetime-picker", "displayName": "uni-datetime-picker 日期选择器", "version": "2.2.10", "description": "uni-datetime-picker 日期时间选择器,支持日历,支持范围选择", "keywords": [ "uni-datetime-picker", "uni-ui", "uniui", "日期时间选择器", "日期时间" ], "repository": "https://github.com/dcloudio/uni-ui", "engines": { "HBuilderX": "" }, "directories": { "example": "../../temps/example_temps" }, "dcloudext": { "sale": { "regular": { "price": "0.00" }, "sourcecode": { "price": "0.00" } }, "contact": { "qq": "" }, "declaration": { "ads": "无", "data": "无", "permissions": "无" }, "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", "type": "component-vue" }, "uni_modules": { "dependencies": [ "uni-scss", "uni-icons" ], "encrypt": [], "platforms": { "cloud": { "tcb": "y", "aliyun": "y" }, "client": { "App": { "app-vue": "y", "app-nvue": "n" }, "H5-mobile": { "Safari": "y", "Android Browser": "y", "微信浏览器(Android)": "y", "QQ浏览器(Android)": "y" }, "H5-pc": { "Chrome": "y", "IE": "y", "Edge": "y", "Firefox": "y", "Safari": "y" }, "小程序": { "微信": "y", "阿里": "y", "百度": "y", "字节跳动": "y", "QQ": "y" }, "快应用": { "华为": "u", "联盟": "u" }, "Vue": { "vue2": "y", "vue3": "y" } } } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-datetime-picker/readme.md ================================================ > `重要通知:组件升级更新 2.0.0 后,支持日期+时间范围选择,组件 ui 将使用日历选择日期,ui 变化较大,同时支持 PC 和 移动端。此版本不向后兼容,不再支持单独的时间选择(type=time)及相关的 hide-second 属性(时间选可使用内置组件 picker)。若仍需使用旧版本,可在插件市场下载*非uni_modules版本*,旧版本将不再维护` ## DatetimePicker 时间选择器 > **组件名:uni-datetime-picker** > 代码块: `uDatetimePicker` 该组件的优势是,支持**时间戳**输入和输出(起始时间、终止时间也支持时间戳),可**同时选择**日期和时间。 若只是需要单独选择日期和时间,不需要时间戳输入和输出,可使用原生的 picker 组件。 **_点击 picker 默认值规则:_** - 若设置初始值 value, 会显示在 picker 显示框中 - 若无初始值 value,则初始值 value 为当前本地时间 Date.now(), 但不会显示在 picker 显示框中 ### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-datetime-picker) #### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-icons/changelog.md ================================================ ## 1.3.5(2022-01-24) - 优化 size 属性可以传入不带单位的字符串数值 ## 1.3.4(2022-01-24) - 优化 size 支持其他单位 ## 1.3.3(2022-01-17) - 修复 nvue 有些图标不显示的bug,兼容老版本图标 ## 1.3.2(2021-12-01) - 优化 示例可复制图标名称 ## 1.3.1(2021-11-23) - 优化 兼容旧组件 type 值 ## 1.3.0(2021-11-19) - 新增 更多图标 - 优化 自定义图标使用方式 - 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) - 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-icons](https://uniapp.dcloud.io/component/uniui/uni-icons) ## 1.1.7(2021-11-08) ## 1.2.0(2021-07-30) - 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) ## 1.1.5(2021-05-12) - 新增 组件示例地址 ## 1.1.4(2021-02-05) - 调整为uni_modules目录规范 ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-icons/components/uni-icons/icons.js ================================================ export default { "id": "2852637", "name": "uniui图标库", "font_family": "uniicons", "css_prefix_text": "uniui-", "description": "", "glyphs": [ { "icon_id": "25027049", "name": "yanse", "font_class": "color", "unicode": "e6cf", "unicode_decimal": 59087 }, { "icon_id": "25027048", "name": "wallet", "font_class": "wallet", "unicode": "e6b1", "unicode_decimal": 59057 }, { "icon_id": "25015720", "name": "settings-filled", "font_class": "settings-filled", "unicode": "e6ce", "unicode_decimal": 59086 }, { "icon_id": "25015434", "name": "shimingrenzheng-filled", "font_class": "auth-filled", "unicode": "e6cc", "unicode_decimal": 59084 }, { "icon_id": "24934246", "name": "shop-filled", "font_class": "shop-filled", "unicode": "e6cd", "unicode_decimal": 59085 }, { "icon_id": "24934159", "name": "staff-filled-01", "font_class": "staff-filled", "unicode": "e6cb", "unicode_decimal": 59083 }, { "icon_id": "24932461", "name": "VIP-filled", "font_class": "vip-filled", "unicode": "e6c6", "unicode_decimal": 59078 }, { "icon_id": "24932462", "name": "plus_circle_fill", "font_class": "plus-filled", "unicode": "e6c7", "unicode_decimal": 59079 }, { "icon_id": "24932463", "name": "folder_add-filled", "font_class": "folder-add-filled", "unicode": "e6c8", "unicode_decimal": 59080 }, { "icon_id": "24932464", "name": "yanse-filled", "font_class": "color-filled", "unicode": "e6c9", "unicode_decimal": 59081 }, { "icon_id": "24932465", "name": "tune-filled", "font_class": "tune-filled", "unicode": "e6ca", "unicode_decimal": 59082 }, { "icon_id": "24932455", "name": "a-rilidaka-filled", "font_class": "calendar-filled", "unicode": "e6c0", "unicode_decimal": 59072 }, { "icon_id": "24932456", "name": "notification-filled", "font_class": "notification-filled", "unicode": "e6c1", "unicode_decimal": 59073 }, { "icon_id": "24932457", "name": "wallet-filled", "font_class": "wallet-filled", "unicode": "e6c2", "unicode_decimal": 59074 }, { "icon_id": "24932458", "name": "paihangbang-filled", "font_class": "medal-filled", "unicode": "e6c3", "unicode_decimal": 59075 }, { "icon_id": "24932459", "name": "gift-filled", "font_class": "gift-filled", "unicode": "e6c4", "unicode_decimal": 59076 }, { "icon_id": "24932460", "name": "fire-filled", "font_class": "fire-filled", "unicode": "e6c5", "unicode_decimal": 59077 }, { "icon_id": "24928001", "name": "refreshempty", "font_class": "refreshempty", "unicode": "e6bf", "unicode_decimal": 59071 }, { "icon_id": "24926853", "name": "location-ellipse", "font_class": "location-filled", "unicode": "e6af", "unicode_decimal": 59055 }, { "icon_id": "24926735", "name": "person-filled", "font_class": "person-filled", "unicode": "e69d", "unicode_decimal": 59037 }, { "icon_id": "24926703", "name": "personadd-filled", "font_class": "personadd-filled", "unicode": "e698", "unicode_decimal": 59032 }, { "icon_id": "24923351", "name": "back", "font_class": "back", "unicode": "e6b9", "unicode_decimal": 59065 }, { "icon_id": "24923352", "name": "forward", "font_class": "forward", "unicode": "e6ba", "unicode_decimal": 59066 }, { "icon_id": "24923353", "name": "arrowthinright", "font_class": "arrow-right", "unicode": "e6bb", "unicode_decimal": 59067 }, { "icon_id": "24923353", "name": "arrowthinright", "font_class": "arrowthinright", "unicode": "e6bb", "unicode_decimal": 59067 }, { "icon_id": "24923354", "name": "arrowthinleft", "font_class": "arrow-left", "unicode": "e6bc", "unicode_decimal": 59068 }, { "icon_id": "24923354", "name": "arrowthinleft", "font_class": "arrowthinleft", "unicode": "e6bc", "unicode_decimal": 59068 }, { "icon_id": "24923355", "name": "arrowthinup", "font_class": "arrow-up", "unicode": "e6bd", "unicode_decimal": 59069 }, { "icon_id": "24923355", "name": "arrowthinup", "font_class": "arrowthinup", "unicode": "e6bd", "unicode_decimal": 59069 }, { "icon_id": "24923356", "name": "arrowthindown", "font_class": "arrow-down", "unicode": "e6be", "unicode_decimal": 59070 },{ "icon_id": "24923356", "name": "arrowthindown", "font_class": "arrowthindown", "unicode": "e6be", "unicode_decimal": 59070 }, { "icon_id": "24923349", "name": "arrowdown", "font_class": "bottom", "unicode": "e6b8", "unicode_decimal": 59064 },{ "icon_id": "24923349", "name": "arrowdown", "font_class": "arrowdown", "unicode": "e6b8", "unicode_decimal": 59064 }, { "icon_id": "24923346", "name": "arrowright", "font_class": "right", "unicode": "e6b5", "unicode_decimal": 59061 }, { "icon_id": "24923346", "name": "arrowright", "font_class": "arrowright", "unicode": "e6b5", "unicode_decimal": 59061 }, { "icon_id": "24923347", "name": "arrowup", "font_class": "top", "unicode": "e6b6", "unicode_decimal": 59062 }, { "icon_id": "24923347", "name": "arrowup", "font_class": "arrowup", "unicode": "e6b6", "unicode_decimal": 59062 }, { "icon_id": "24923348", "name": "arrowleft", "font_class": "left", "unicode": "e6b7", "unicode_decimal": 59063 }, { "icon_id": "24923348", "name": "arrowleft", "font_class": "arrowleft", "unicode": "e6b7", "unicode_decimal": 59063 }, { "icon_id": "24923334", "name": "eye", "font_class": "eye", "unicode": "e651", "unicode_decimal": 58961 }, { "icon_id": "24923335", "name": "eye-filled", "font_class": "eye-filled", "unicode": "e66a", "unicode_decimal": 58986 }, { "icon_id": "24923336", "name": "eye-slash", "font_class": "eye-slash", "unicode": "e6b3", "unicode_decimal": 59059 }, { "icon_id": "24923337", "name": "eye-slash-filled", "font_class": "eye-slash-filled", "unicode": "e6b4", "unicode_decimal": 59060 }, { "icon_id": "24923305", "name": "info-filled", "font_class": "info-filled", "unicode": "e649", "unicode_decimal": 58953 }, { "icon_id": "24923299", "name": "reload-01", "font_class": "reload", "unicode": "e6b2", "unicode_decimal": 59058 }, { "icon_id": "24923195", "name": "mic_slash_fill", "font_class": "micoff-filled", "unicode": "e6b0", "unicode_decimal": 59056 }, { "icon_id": "24923165", "name": "map-pin-ellipse", "font_class": "map-pin-ellipse", "unicode": "e6ac", "unicode_decimal": 59052 }, { "icon_id": "24923166", "name": "map-pin", "font_class": "map-pin", "unicode": "e6ad", "unicode_decimal": 59053 }, { "icon_id": "24923167", "name": "location", "font_class": "location", "unicode": "e6ae", "unicode_decimal": 59054 }, { "icon_id": "24923064", "name": "starhalf", "font_class": "starhalf", "unicode": "e683", "unicode_decimal": 59011 }, { "icon_id": "24923065", "name": "star", "font_class": "star", "unicode": "e688", "unicode_decimal": 59016 }, { "icon_id": "24923066", "name": "star-filled", "font_class": "star-filled", "unicode": "e68f", "unicode_decimal": 59023 }, { "icon_id": "24899646", "name": "a-rilidaka", "font_class": "calendar", "unicode": "e6a0", "unicode_decimal": 59040 }, { "icon_id": "24899647", "name": "fire", "font_class": "fire", "unicode": "e6a1", "unicode_decimal": 59041 }, { "icon_id": "24899648", "name": "paihangbang", "font_class": "medal", "unicode": "e6a2", "unicode_decimal": 59042 }, { "icon_id": "24899649", "name": "font", "font_class": "font", "unicode": "e6a3", "unicode_decimal": 59043 }, { "icon_id": "24899650", "name": "gift", "font_class": "gift", "unicode": "e6a4", "unicode_decimal": 59044 }, { "icon_id": "24899651", "name": "link", "font_class": "link", "unicode": "e6a5", "unicode_decimal": 59045 }, { "icon_id": "24899652", "name": "notification", "font_class": "notification", "unicode": "e6a6", "unicode_decimal": 59046 }, { "icon_id": "24899653", "name": "staff", "font_class": "staff", "unicode": "e6a7", "unicode_decimal": 59047 }, { "icon_id": "24899654", "name": "VIP", "font_class": "vip", "unicode": "e6a8", "unicode_decimal": 59048 }, { "icon_id": "24899655", "name": "folder_add", "font_class": "folder-add", "unicode": "e6a9", "unicode_decimal": 59049 }, { "icon_id": "24899656", "name": "tune", "font_class": "tune", "unicode": "e6aa", "unicode_decimal": 59050 }, { "icon_id": "24899657", "name": "shimingrenzheng", "font_class": "auth", "unicode": "e6ab", "unicode_decimal": 59051 }, { "icon_id": "24899565", "name": "person", "font_class": "person", "unicode": "e699", "unicode_decimal": 59033 }, { "icon_id": "24899566", "name": "email-filled", "font_class": "email-filled", "unicode": "e69a", "unicode_decimal": 59034 }, { "icon_id": "24899567", "name": "phone-filled", "font_class": "phone-filled", "unicode": "e69b", "unicode_decimal": 59035 }, { "icon_id": "24899568", "name": "phone", "font_class": "phone", "unicode": "e69c", "unicode_decimal": 59036 }, { "icon_id": "24899570", "name": "email", "font_class": "email", "unicode": "e69e", "unicode_decimal": 59038 }, { "icon_id": "24899571", "name": "personadd", "font_class": "personadd", "unicode": "e69f", "unicode_decimal": 59039 }, { "icon_id": "24899558", "name": "chatboxes-filled", "font_class": "chatboxes-filled", "unicode": "e692", "unicode_decimal": 59026 }, { "icon_id": "24899559", "name": "contact", "font_class": "contact", "unicode": "e693", "unicode_decimal": 59027 }, { "icon_id": "24899560", "name": "chatbubble-filled", "font_class": "chatbubble-filled", "unicode": "e694", "unicode_decimal": 59028 }, { "icon_id": "24899561", "name": "contact-filled", "font_class": "contact-filled", "unicode": "e695", "unicode_decimal": 59029 }, { "icon_id": "24899562", "name": "chatboxes", "font_class": "chatboxes", "unicode": "e696", "unicode_decimal": 59030 }, { "icon_id": "24899563", "name": "chatbubble", "font_class": "chatbubble", "unicode": "e697", "unicode_decimal": 59031 }, { "icon_id": "24881290", "name": "upload-filled", "font_class": "upload-filled", "unicode": "e68e", "unicode_decimal": 59022 }, { "icon_id": "24881292", "name": "upload", "font_class": "upload", "unicode": "e690", "unicode_decimal": 59024 }, { "icon_id": "24881293", "name": "weixin", "font_class": "weixin", "unicode": "e691", "unicode_decimal": 59025 }, { "icon_id": "24881274", "name": "compose", "font_class": "compose", "unicode": "e67f", "unicode_decimal": 59007 }, { "icon_id": "24881275", "name": "qq", "font_class": "qq", "unicode": "e680", "unicode_decimal": 59008 }, { "icon_id": "24881276", "name": "download-filled", "font_class": "download-filled", "unicode": "e681", "unicode_decimal": 59009 }, { "icon_id": "24881277", "name": "pengyouquan", "font_class": "pyq", "unicode": "e682", "unicode_decimal": 59010 }, { "icon_id": "24881279", "name": "sound", "font_class": "sound", "unicode": "e684", "unicode_decimal": 59012 }, { "icon_id": "24881280", "name": "trash-filled", "font_class": "trash-filled", "unicode": "e685", "unicode_decimal": 59013 }, { "icon_id": "24881281", "name": "sound-filled", "font_class": "sound-filled", "unicode": "e686", "unicode_decimal": 59014 }, { "icon_id": "24881282", "name": "trash", "font_class": "trash", "unicode": "e687", "unicode_decimal": 59015 }, { "icon_id": "24881284", "name": "videocam-filled", "font_class": "videocam-filled", "unicode": "e689", "unicode_decimal": 59017 }, { "icon_id": "24881285", "name": "spinner-cycle", "font_class": "spinner-cycle", "unicode": "e68a", "unicode_decimal": 59018 }, { "icon_id": "24881286", "name": "weibo", "font_class": "weibo", "unicode": "e68b", "unicode_decimal": 59019 }, { "icon_id": "24881288", "name": "videocam", "font_class": "videocam", "unicode": "e68c", "unicode_decimal": 59020 }, { "icon_id": "24881289", "name": "download", "font_class": "download", "unicode": "e68d", "unicode_decimal": 59021 }, { "icon_id": "24879601", "name": "help", "font_class": "help", "unicode": "e679", "unicode_decimal": 59001 }, { "icon_id": "24879602", "name": "navigate-filled", "font_class": "navigate-filled", "unicode": "e67a", "unicode_decimal": 59002 }, { "icon_id": "24879603", "name": "plusempty", "font_class": "plusempty", "unicode": "e67b", "unicode_decimal": 59003 }, { "icon_id": "24879604", "name": "smallcircle", "font_class": "smallcircle", "unicode": "e67c", "unicode_decimal": 59004 }, { "icon_id": "24879605", "name": "minus-filled", "font_class": "minus-filled", "unicode": "e67d", "unicode_decimal": 59005 }, { "icon_id": "24879606", "name": "micoff", "font_class": "micoff", "unicode": "e67e", "unicode_decimal": 59006 }, { "icon_id": "24879588", "name": "closeempty", "font_class": "closeempty", "unicode": "e66c", "unicode_decimal": 58988 }, { "icon_id": "24879589", "name": "clear", "font_class": "clear", "unicode": "e66d", "unicode_decimal": 58989 }, { "icon_id": "24879590", "name": "navigate", "font_class": "navigate", "unicode": "e66e", "unicode_decimal": 58990 }, { "icon_id": "24879591", "name": "minus", "font_class": "minus", "unicode": "e66f", "unicode_decimal": 58991 }, { "icon_id": "24879592", "name": "image", "font_class": "image", "unicode": "e670", "unicode_decimal": 58992 }, { "icon_id": "24879593", "name": "mic", "font_class": "mic", "unicode": "e671", "unicode_decimal": 58993 }, { "icon_id": "24879594", "name": "paperplane", "font_class": "paperplane", "unicode": "e672", "unicode_decimal": 58994 }, { "icon_id": "24879595", "name": "close", "font_class": "close", "unicode": "e673", "unicode_decimal": 58995 }, { "icon_id": "24879596", "name": "help-filled", "font_class": "help-filled", "unicode": "e674", "unicode_decimal": 58996 }, { "icon_id": "24879597", "name": "plus-filled", "font_class": "paperplane-filled", "unicode": "e675", "unicode_decimal": 58997 }, { "icon_id": "24879598", "name": "plus", "font_class": "plus", "unicode": "e676", "unicode_decimal": 58998 }, { "icon_id": "24879599", "name": "mic-filled", "font_class": "mic-filled", "unicode": "e677", "unicode_decimal": 58999 }, { "icon_id": "24879600", "name": "image-filled", "font_class": "image-filled", "unicode": "e678", "unicode_decimal": 59000 }, { "icon_id": "24855900", "name": "locked-filled", "font_class": "locked-filled", "unicode": "e668", "unicode_decimal": 58984 }, { "icon_id": "24855901", "name": "info", "font_class": "info", "unicode": "e669", "unicode_decimal": 58985 }, { "icon_id": "24855903", "name": "locked", "font_class": "locked", "unicode": "e66b", "unicode_decimal": 58987 }, { "icon_id": "24855884", "name": "camera-filled", "font_class": "camera-filled", "unicode": "e658", "unicode_decimal": 58968 }, { "icon_id": "24855885", "name": "chat-filled", "font_class": "chat-filled", "unicode": "e659", "unicode_decimal": 58969 }, { "icon_id": "24855886", "name": "camera", "font_class": "camera", "unicode": "e65a", "unicode_decimal": 58970 }, { "icon_id": "24855887", "name": "circle", "font_class": "circle", "unicode": "e65b", "unicode_decimal": 58971 }, { "icon_id": "24855888", "name": "checkmarkempty", "font_class": "checkmarkempty", "unicode": "e65c", "unicode_decimal": 58972 }, { "icon_id": "24855889", "name": "chat", "font_class": "chat", "unicode": "e65d", "unicode_decimal": 58973 }, { "icon_id": "24855890", "name": "circle-filled", "font_class": "circle-filled", "unicode": "e65e", "unicode_decimal": 58974 }, { "icon_id": "24855891", "name": "flag", "font_class": "flag", "unicode": "e65f", "unicode_decimal": 58975 }, { "icon_id": "24855892", "name": "flag-filled", "font_class": "flag-filled", "unicode": "e660", "unicode_decimal": 58976 }, { "icon_id": "24855893", "name": "gear-filled", "font_class": "gear-filled", "unicode": "e661", "unicode_decimal": 58977 }, { "icon_id": "24855894", "name": "home", "font_class": "home", "unicode": "e662", "unicode_decimal": 58978 }, { "icon_id": "24855895", "name": "home-filled", "font_class": "home-filled", "unicode": "e663", "unicode_decimal": 58979 }, { "icon_id": "24855896", "name": "gear", "font_class": "gear", "unicode": "e664", "unicode_decimal": 58980 }, { "icon_id": "24855897", "name": "smallcircle-filled", "font_class": "smallcircle-filled", "unicode": "e665", "unicode_decimal": 58981 }, { "icon_id": "24855898", "name": "map-filled", "font_class": "map-filled", "unicode": "e666", "unicode_decimal": 58982 }, { "icon_id": "24855899", "name": "map", "font_class": "map", "unicode": "e667", "unicode_decimal": 58983 }, { "icon_id": "24855825", "name": "refresh-filled", "font_class": "refresh-filled", "unicode": "e656", "unicode_decimal": 58966 }, { "icon_id": "24855826", "name": "refresh", "font_class": "refresh", "unicode": "e657", "unicode_decimal": 58967 }, { "icon_id": "24855808", "name": "cloud-upload", "font_class": "cloud-upload", "unicode": "e645", "unicode_decimal": 58949 }, { "icon_id": "24855809", "name": "cloud-download-filled", "font_class": "cloud-download-filled", "unicode": "e646", "unicode_decimal": 58950 }, { "icon_id": "24855810", "name": "cloud-download", "font_class": "cloud-download", "unicode": "e647", "unicode_decimal": 58951 }, { "icon_id": "24855811", "name": "cloud-upload-filled", "font_class": "cloud-upload-filled", "unicode": "e648", "unicode_decimal": 58952 }, { "icon_id": "24855813", "name": "redo", "font_class": "redo", "unicode": "e64a", "unicode_decimal": 58954 }, { "icon_id": "24855814", "name": "images-filled", "font_class": "images-filled", "unicode": "e64b", "unicode_decimal": 58955 }, { "icon_id": "24855815", "name": "undo-filled", "font_class": "undo-filled", "unicode": "e64c", "unicode_decimal": 58956 }, { "icon_id": "24855816", "name": "more", "font_class": "more", "unicode": "e64d", "unicode_decimal": 58957 }, { "icon_id": "24855817", "name": "more-filled", "font_class": "more-filled", "unicode": "e64e", "unicode_decimal": 58958 }, { "icon_id": "24855818", "name": "undo", "font_class": "undo", "unicode": "e64f", "unicode_decimal": 58959 }, { "icon_id": "24855819", "name": "images", "font_class": "images", "unicode": "e650", "unicode_decimal": 58960 }, { "icon_id": "24855821", "name": "paperclip", "font_class": "paperclip", "unicode": "e652", "unicode_decimal": 58962 }, { "icon_id": "24855822", "name": "settings", "font_class": "settings", "unicode": "e653", "unicode_decimal": 58963 }, { "icon_id": "24855823", "name": "search", "font_class": "search", "unicode": "e654", "unicode_decimal": 58964 }, { "icon_id": "24855824", "name": "redo-filled", "font_class": "redo-filled", "unicode": "e655", "unicode_decimal": 58965 }, { "icon_id": "24841702", "name": "list", "font_class": "list", "unicode": "e644", "unicode_decimal": 58948 }, { "icon_id": "24841489", "name": "mail-open-filled", "font_class": "mail-open-filled", "unicode": "e63a", "unicode_decimal": 58938 }, { "icon_id": "24841491", "name": "hand-thumbsdown-filled", "font_class": "hand-down-filled", "unicode": "e63c", "unicode_decimal": 58940 }, { "icon_id": "24841492", "name": "hand-thumbsdown", "font_class": "hand-down", "unicode": "e63d", "unicode_decimal": 58941 }, { "icon_id": "24841493", "name": "hand-thumbsup-filled", "font_class": "hand-up-filled", "unicode": "e63e", "unicode_decimal": 58942 }, { "icon_id": "24841494", "name": "hand-thumbsup", "font_class": "hand-up", "unicode": "e63f", "unicode_decimal": 58943 }, { "icon_id": "24841496", "name": "heart-filled", "font_class": "heart-filled", "unicode": "e641", "unicode_decimal": 58945 }, { "icon_id": "24841498", "name": "mail-open", "font_class": "mail-open", "unicode": "e643", "unicode_decimal": 58947 }, { "icon_id": "24841488", "name": "heart", "font_class": "heart", "unicode": "e639", "unicode_decimal": 58937 }, { "icon_id": "24839963", "name": "loop", "font_class": "loop", "unicode": "e633", "unicode_decimal": 58931 }, { "icon_id": "24839866", "name": "pulldown", "font_class": "pulldown", "unicode": "e632", "unicode_decimal": 58930 }, { "icon_id": "24813798", "name": "scan", "font_class": "scan", "unicode": "e62a", "unicode_decimal": 58922 }, { "icon_id": "24813786", "name": "bars", "font_class": "bars", "unicode": "e627", "unicode_decimal": 58919 }, { "icon_id": "24813788", "name": "cart-filled", "font_class": "cart-filled", "unicode": "e629", "unicode_decimal": 58921 }, { "icon_id": "24813790", "name": "checkbox", "font_class": "checkbox", "unicode": "e62b", "unicode_decimal": 58923 }, { "icon_id": "24813791", "name": "checkbox-filled", "font_class": "checkbox-filled", "unicode": "e62c", "unicode_decimal": 58924 }, { "icon_id": "24813794", "name": "shop", "font_class": "shop", "unicode": "e62f", "unicode_decimal": 58927 }, { "icon_id": "24813795", "name": "headphones", "font_class": "headphones", "unicode": "e630", "unicode_decimal": 58928 }, { "icon_id": "24813796", "name": "cart", "font_class": "cart", "unicode": "e631", "unicode_decimal": 58929 } ] } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-icons/components/uni-icons/uni-icons.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.css ================================================ .uniui-color:before { content: "\e6cf"; } .uniui-wallet:before { content: "\e6b1"; } .uniui-settings-filled:before { content: "\e6ce"; } .uniui-auth-filled:before { content: "\e6cc"; } .uniui-shop-filled:before { content: "\e6cd"; } .uniui-staff-filled:before { content: "\e6cb"; } .uniui-vip-filled:before { content: "\e6c6"; } .uniui-plus-filled:before { content: "\e6c7"; } .uniui-folder-add-filled:before { content: "\e6c8"; } .uniui-color-filled:before { content: "\e6c9"; } .uniui-tune-filled:before { content: "\e6ca"; } .uniui-calendar-filled:before { content: "\e6c0"; } .uniui-notification-filled:before { content: "\e6c1"; } .uniui-wallet-filled:before { content: "\e6c2"; } .uniui-medal-filled:before { content: "\e6c3"; } .uniui-gift-filled:before { content: "\e6c4"; } .uniui-fire-filled:before { content: "\e6c5"; } .uniui-refreshempty:before { content: "\e6bf"; } .uniui-location-filled:before { content: "\e6af"; } .uniui-person-filled:before { content: "\e69d"; } .uniui-personadd-filled:before { content: "\e698"; } .uniui-back:before { content: "\e6b9"; } .uniui-forward:before { content: "\e6ba"; } .uniui-arrow-right:before { content: "\e6bb"; } .uniui-arrowthinright:before { content: "\e6bb"; } .uniui-arrow-left:before { content: "\e6bc"; } .uniui-arrowthinleft:before { content: "\e6bc"; } .uniui-arrow-up:before { content: "\e6bd"; } .uniui-arrowthinup:before { content: "\e6bd"; } .uniui-arrow-down:before { content: "\e6be"; } .uniui-arrowthindown:before { content: "\e6be"; } .uniui-bottom:before { content: "\e6b8"; } .uniui-arrowdown:before { content: "\e6b8"; } .uniui-right:before { content: "\e6b5"; } .uniui-arrowright:before { content: "\e6b5"; } .uniui-top:before { content: "\e6b6"; } .uniui-arrowup:before { content: "\e6b6"; } .uniui-left:before { content: "\e6b7"; } .uniui-arrowleft:before { content: "\e6b7"; } .uniui-eye:before { content: "\e651"; } .uniui-eye-filled:before { content: "\e66a"; } .uniui-eye-slash:before { content: "\e6b3"; } .uniui-eye-slash-filled:before { content: "\e6b4"; } .uniui-info-filled:before { content: "\e649"; } .uniui-reload:before { content: "\e6b2"; } .uniui-micoff-filled:before { content: "\e6b0"; } .uniui-map-pin-ellipse:before { content: "\e6ac"; } .uniui-map-pin:before { content: "\e6ad"; } .uniui-location:before { content: "\e6ae"; } .uniui-starhalf:before { content: "\e683"; } .uniui-star:before { content: "\e688"; } .uniui-star-filled:before { content: "\e68f"; } .uniui-calendar:before { content: "\e6a0"; } .uniui-fire:before { content: "\e6a1"; } .uniui-medal:before { content: "\e6a2"; } .uniui-font:before { content: "\e6a3"; } .uniui-gift:before { content: "\e6a4"; } .uniui-link:before { content: "\e6a5"; } .uniui-notification:before { content: "\e6a6"; } .uniui-staff:before { content: "\e6a7"; } .uniui-vip:before { content: "\e6a8"; } .uniui-folder-add:before { content: "\e6a9"; } .uniui-tune:before { content: "\e6aa"; } .uniui-auth:before { content: "\e6ab"; } .uniui-person:before { content: "\e699"; } .uniui-email-filled:before { content: "\e69a"; } .uniui-phone-filled:before { content: "\e69b"; } .uniui-phone:before { content: "\e69c"; } .uniui-email:before { content: "\e69e"; } .uniui-personadd:before { content: "\e69f"; } .uniui-chatboxes-filled:before { content: "\e692"; } .uniui-contact:before { content: "\e693"; } .uniui-chatbubble-filled:before { content: "\e694"; } .uniui-contact-filled:before { content: "\e695"; } .uniui-chatboxes:before { content: "\e696"; } .uniui-chatbubble:before { content: "\e697"; } .uniui-upload-filled:before { content: "\e68e"; } .uniui-upload:before { content: "\e690"; } .uniui-weixin:before { content: "\e691"; } .uniui-compose:before { content: "\e67f"; } .uniui-qq:before { content: "\e680"; } .uniui-download-filled:before { content: "\e681"; } .uniui-pyq:before { content: "\e682"; } .uniui-sound:before { content: "\e684"; } .uniui-trash-filled:before { content: "\e685"; } .uniui-sound-filled:before { content: "\e686"; } .uniui-trash:before { content: "\e687"; } .uniui-videocam-filled:before { content: "\e689"; } .uniui-spinner-cycle:before { content: "\e68a"; } .uniui-weibo:before { content: "\e68b"; } .uniui-videocam:before { content: "\e68c"; } .uniui-download:before { content: "\e68d"; } .uniui-help:before { content: "\e679"; } .uniui-navigate-filled:before { content: "\e67a"; } .uniui-plusempty:before { content: "\e67b"; } .uniui-smallcircle:before { content: "\e67c"; } .uniui-minus-filled:before { content: "\e67d"; } .uniui-micoff:before { content: "\e67e"; } .uniui-closeempty:before { content: "\e66c"; } .uniui-clear:before { content: "\e66d"; } .uniui-navigate:before { content: "\e66e"; } .uniui-minus:before { content: "\e66f"; } .uniui-image:before { content: "\e670"; } .uniui-mic:before { content: "\e671"; } .uniui-paperplane:before { content: "\e672"; } .uniui-close:before { content: "\e673"; } .uniui-help-filled:before { content: "\e674"; } .uniui-paperplane-filled:before { content: "\e675"; } .uniui-plus:before { content: "\e676"; } .uniui-mic-filled:before { content: "\e677"; } .uniui-image-filled:before { content: "\e678"; } .uniui-locked-filled:before { content: "\e668"; } .uniui-info:before { content: "\e669"; } .uniui-locked:before { content: "\e66b"; } .uniui-camera-filled:before { content: "\e658"; } .uniui-chat-filled:before { content: "\e659"; } .uniui-camera:before { content: "\e65a"; } .uniui-circle:before { content: "\e65b"; } .uniui-checkmarkempty:before { content: "\e65c"; } .uniui-chat:before { content: "\e65d"; } .uniui-circle-filled:before { content: "\e65e"; } .uniui-flag:before { content: "\e65f"; } .uniui-flag-filled:before { content: "\e660"; } .uniui-gear-filled:before { content: "\e661"; } .uniui-home:before { content: "\e662"; } .uniui-home-filled:before { content: "\e663"; } .uniui-gear:before { content: "\e664"; } .uniui-smallcircle-filled:before { content: "\e665"; } .uniui-map-filled:before { content: "\e666"; } .uniui-map:before { content: "\e667"; } .uniui-refresh-filled:before { content: "\e656"; } .uniui-refresh:before { content: "\e657"; } .uniui-cloud-upload:before { content: "\e645"; } .uniui-cloud-download-filled:before { content: "\e646"; } .uniui-cloud-download:before { content: "\e647"; } .uniui-cloud-upload-filled:before { content: "\e648"; } .uniui-redo:before { content: "\e64a"; } .uniui-images-filled:before { content: "\e64b"; } .uniui-undo-filled:before { content: "\e64c"; } .uniui-more:before { content: "\e64d"; } .uniui-more-filled:before { content: "\e64e"; } .uniui-undo:before { content: "\e64f"; } .uniui-images:before { content: "\e650"; } .uniui-paperclip:before { content: "\e652"; } .uniui-settings:before { content: "\e653"; } .uniui-search:before { content: "\e654"; } .uniui-redo-filled:before { content: "\e655"; } .uniui-list:before { content: "\e644"; } .uniui-mail-open-filled:before { content: "\e63a"; } .uniui-hand-down-filled:before { content: "\e63c"; } .uniui-hand-down:before { content: "\e63d"; } .uniui-hand-up-filled:before { content: "\e63e"; } .uniui-hand-up:before { content: "\e63f"; } .uniui-heart-filled:before { content: "\e641"; } .uniui-mail-open:before { content: "\e643"; } .uniui-heart:before { content: "\e639"; } .uniui-loop:before { content: "\e633"; } .uniui-pulldown:before { content: "\e632"; } .uniui-scan:before { content: "\e62a"; } .uniui-bars:before { content: "\e627"; } .uniui-cart-filled:before { content: "\e629"; } .uniui-checkbox:before { content: "\e62b"; } .uniui-checkbox-filled:before { content: "\e62c"; } .uniui-shop:before { content: "\e62f"; } .uniui-headphones:before { content: "\e630"; } .uniui-cart:before { content: "\e631"; } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-icons/package.json ================================================ { "id": "uni-icons", "displayName": "uni-icons 图标", "version": "1.3.5", "description": "图标组件,用于展示移动端常见的图标,可自定义颜色、大小。", "keywords": [ "uni-ui", "uniui", "icon", "图标" ], "repository": "https://github.com/dcloudio/uni-ui", "engines": { "HBuilderX": "^3.2.14" }, "directories": { "example": "../../temps/example_temps" }, "dcloudext": { "category": [ "前端组件", "通用组件" ], "sale": { "regular": { "price": "0.00" }, "sourcecode": { "price": "0.00" } }, "contact": { "qq": "" }, "declaration": { "ads": "无", "data": "无", "permissions": "无" }, "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" }, "uni_modules": { "dependencies": ["uni-scss"], "encrypt": [], "platforms": { "cloud": { "tcb": "y", "aliyun": "y" }, "client": { "App": { "app-vue": "y", "app-nvue": "y" }, "H5-mobile": { "Safari": "y", "Android Browser": "y", "微信浏览器(Android)": "y", "QQ浏览器(Android)": "y" }, "H5-pc": { "Chrome": "y", "IE": "y", "Edge": "y", "Firefox": "y", "Safari": "y" }, "小程序": { "微信": "y", "阿里": "y", "百度": "y", "字节跳动": "y", "QQ": "y" }, "快应用": { "华为": "u", "联盟": "u" }, "Vue": { "vue2": "y", "vue3": "y" } } } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-icons/readme.md ================================================ ## Icons 图标 > **组件名:uni-icons** > 代码块: `uIcons` 用于展示 icons 图标 。 ### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-icons) #### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-list/changelog.md ================================================ ## 1.2.1(2022-03-30) - 删除无用文件 ## 1.2.0(2021-11-23) - 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) - 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-list](https://uniapp.dcloud.io/component/uniui/uni-list) ## 1.1.3(2021-08-30) - 修复 在vue3中to属性在发行应用的时候报错的bug ## 1.1.2(2021-07-30) - 优化 vue3下事件警告的问题 ## 1.1.1(2021-07-21) - 修复 与其他组件嵌套使用时,点击失效的Bug ## 1.1.0(2021-07-13) - 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) ## 1.0.17(2021-05-12) - 新增 组件示例地址 ## 1.0.16(2021-02-05) - 优化 组件引用关系,通过uni_modules引用组件 ## 1.0.15(2021-02-05) - 调整为uni_modules目录规范 - 修复 uni-list-chat 角标显示不正常的问题 ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-list/components/uni-list/uni-list.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-list/components/uni-list/uni-refresh.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-list/components/uni-list/uni-refresh.wxs ================================================ var pullDown = { threshold: 95, maxHeight: 200, callRefresh: 'onrefresh', callPullingDown: 'onpullingdown', refreshSelector: '.uni-refresh' }; function ready(newValue, oldValue, ownerInstance, instance) { var state = instance.getState() state.canPullDown = newValue; // console.log(newValue); } function touchStart(e, instance) { var state = instance.getState(); state.refreshInstance = instance.selectComponent(pullDown.refreshSelector); state.canPullDown = (state.refreshInstance != null && state.refreshInstance != undefined); if (!state.canPullDown) { return } // console.log("touchStart"); state.height = 0; state.touchStartY = e.touches[0].pageY || e.changedTouches[0].pageY; state.refreshInstance.setStyle({ 'height': 0 }); state.refreshInstance.callMethod("onchange", true); } function touchMove(e, ownerInstance) { var instance = e.instance; var state = instance.getState(); if (!state.canPullDown) { return } var oldHeight = state.height; var endY = e.touches[0].pageY || e.changedTouches[0].pageY; var height = endY - state.touchStartY; if (height > pullDown.maxHeight) { return; } var refreshInstance = state.refreshInstance; refreshInstance.setStyle({ 'height': height + 'px' }); height = height < pullDown.maxHeight ? height : pullDown.maxHeight; state.height = height; refreshInstance.callMethod(pullDown.callPullingDown, { height: height }); } function touchEnd(e, ownerInstance) { var state = e.instance.getState(); if (!state.canPullDown) { return } state.refreshInstance.callMethod("onchange", false); var refreshInstance = state.refreshInstance; if (state.height > pullDown.threshold) { refreshInstance.callMethod(pullDown.callRefresh); return; } refreshInstance.setStyle({ 'height': 0 }); } function propObserver(newValue, oldValue, instance) { pullDown = newValue; } module.exports = { touchmove: touchMove, touchstart: touchStart, touchend: touchEnd, propObserver: propObserver } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-list/components/uni-list-ad/uni-list-ad.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-list/components/uni-list-chat/uni-list-chat.scss ================================================ /** * 这里是 uni-list 组件内置的常用样式变量 * 如果需要覆盖样式,这里提供了基本的组件样式变量,您可以尝试修改这里的变量,去完成样式替换,而不用去修改源码 * */ // 背景色 $background-color : #fff; // 分割线颜色 $divide-line-color : #e5e5e5; // 默认头像大小,如需要修改此值,注意同步修改 js 中的值 const avatarWidth = xx ,目前只支持方形头像 // nvue 页面不支持修改头像大小 $avatar-width : 45px ; // 头像边框 $avatar-border-radius: 5px; $avatar-border-color: #eee; $avatar-border-width: 1px; // 标题文字样式 $title-size : 16px; $title-color : #3b4144; $title-weight : normal; // 描述文字样式 $note-size : 12px; $note-color : #999; $note-weight : normal; // 右侧额外内容默认样式 $right-text-size : 12px; $right-text-color : #999; $right-text-weight : normal; // 角标样式 // nvue 页面不支持修改圆点位置以及大小 // 角标在左侧时,角标的位置,默认为 0 ,负数左/下移动,正数右/上移动 $badge-left: 0px; $badge-top: 0px; // 显示圆点时,圆点大小 $dot-width: 10px; $dot-height: 10px; // 显示角标时,角标大小和字体大小 $badge-size : 18px; $badge-font : 12px; // 显示角标时,角标前景色 $badge-color : #fff; // 显示角标时,角标背景色 $badge-background-color : #ff5a5f; // 显示角标时,角标左右间距 $badge-space : 6px; // 状态样式 // 选中颜色 $hover : #f5f5f5; ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-list/components/uni-list-chat/uni-list-chat.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-list/components/uni-list-item/uni-list-item.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-list/package.json ================================================ { "id": "uni-list", "displayName": "uni-list 列表", "version": "1.2.1", "description": "List 组件 ,帮助使用者快速构建列表。", "keywords": [ "", "uni-ui", "uniui", "列表", "", "list" ], "repository": "https://github.com/dcloudio/uni-ui", "engines": { "HBuilderX": "" }, "directories": { "example": "../../temps/example_temps" }, "dcloudext": { "category": [ "前端组件", "通用组件" ], "sale": { "regular": { "price": "0.00" }, "sourcecode": { "price": "0.00" } }, "contact": { "qq": "" }, "declaration": { "ads": "无", "data": "无", "permissions": "无" }, "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" }, "uni_modules": { "dependencies": [ "uni-badge", "uni-icons" ], "encrypt": [], "platforms": { "cloud": { "tcb": "y", "aliyun": "y" }, "client": { "App": { "app-vue": "y", "app-nvue": "y" }, "H5-mobile": { "Safari": "y", "Android Browser": "y", "微信浏览器(Android)": "y", "QQ浏览器(Android)": "y" }, "H5-pc": { "Chrome": "y", "IE": "y", "Edge": "y", "Firefox": "y", "Safari": "y" }, "小程序": { "微信": "y", "阿里": "y", "百度": "y", "字节跳动": "y", "QQ": "y" }, "快应用": { "华为": "u", "联盟": "u" }, "Vue": { "vue2": "y", "vue3": "y" } } } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uni-list/readme.md ================================================ ## List 列表 > **组件名:uni-list** > 代码块: `uList`、`uListItem` > 关联组件:`uni-list-item`、`uni-badge`、`uni-icons`、`uni-list-chat`、`uni-list-ad` List 列表组件,包含基本列表样式、可扩展插槽机制、长列表性能优化、多端兼容。 在vue页面里,它默认使用页面级滚动。在app-nvue页面里,它默认使用原生list组件滚动。这样的长列表,在滚动出屏幕外后,系统会回收不可见区域的渲染内存资源,不会造成滚动越长手机越卡的问题。 uni-list组件是父容器,里面的核心是uni-list-item子组件,它代表列表中的一个可重复行,子组件可以无限循环。 uni-list-item有很多风格,uni-list-item组件通过内置的属性,满足一些常用的场景。当内置属性不满足需求时,可以通过扩展插槽来自定义列表内容。 内置属性可以覆盖的场景包括:导航列表、设置列表、小图标列表、通信录列表、聊天记录列表。 涉及很多大图或丰富内容的列表,比如类今日头条的新闻列表、类淘宝的电商列表,需要通过扩展插槽实现。 下文均有样例给出。 uni-list不包含下拉刷新和上拉翻页。上拉翻页另见组件:[uni-load-more](https://ext.dcloud.net.cn/plugin?id=29) ### 安装方式 本组件符合[easycom](https://uniapp.dcloud.io/collocation/pages?id=easycom)规范,`HBuilderX 2.5.5`起,只需将本组件导入项目,在页面`template`中即可直接使用,无需在页面中`import`和注册`components`。 如需通过`npm`方式使用`uni-ui`组件,另见文档:[https://ext.dcloud.net.cn/plugin?id=55](https://ext.dcloud.net.cn/plugin?id=55) > **注意事项** > 为了避免错误使用,给大家带来不好的开发体验,请在使用组件前仔细阅读下面的注意事项,可以帮你避免一些错误。 > - 组件需要依赖 `sass` 插件 ,请自行手动安装 > - 组件内部依赖 `'uni-icons'` 、`uni-badge` 组件 > - `uni-list` 和 `uni-list-item` 需要配套使用,暂不支持单独使用 `uni-list-item` > - 只有开启点击反馈后,会有点击选中效果 > - 使用插槽时,可以完全自定义内容 > - note 、rightText 属性暂时没做限制,不支持文字溢出隐藏,使用时应该控制长度显示或通过默认插槽自行扩展 > - 支付宝小程序平台需要在支付宝小程序开发者工具里开启 component2 编译模式,开启方式: 详情 --> 项目配置 --> 启用 component2 编译 > - 如果需要修改 `switch`、`badge` 样式,请使用插槽自定义 > - 在 `HBuilderX` 低版本中,可能会出现组件显示 `undefined` 的问题,请升级最新的 `HBuilderX` 或者 `cli` > - 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 ### 基本用法 - 设置 `title` 属性,可以显示列表标题 - 设置 `disabled` 属性,可以禁用当前项 ```html ``` ### 多行内容显示 - 设置 `note` 属性 ,可以在第二行显示描述文本信息 ```html ``` ### 右侧显示角标、switch - 设置 `show-badge` 属性 ,可以显示角标内容 - 设置 `show-switch` 属性,可以显示 switch 开关 ```html ``` ### 左侧显示略缩图、图标 - 设置 `thumb` 属性 ,可以在列表左侧显示略缩图 - 设置 `show-extra-icon` 属性,并指定 `extra-icon` 可以在左侧显示图标 ```html ``` ### 开启点击反馈和右侧箭头 - 设置 `clickable` 为 `true` ,则表示这是一个可点击的列表,会默认给一个点击效果,并可以监听 `click` 事件 - 设置 `link` 属性,会自动开启点击反馈,并给列表右侧添加一个箭头 - 设置 `to` 属性,可以跳转页面,`link` 的值表示跳转方式,如果不指定,默认为 `navigateTo` ```html ``` ### 聊天列表示例 - 设置 `clickable` 为 `true` ,则表示这是一个可点击的列表,会默认给一个点击效果,并可以监听 `click` 事件 - 设置 `link` 属性,会自动开启点击反馈,`link` 的值表示跳转方式,如果不指定,默认为 `navigateTo` - 设置 `to` 属性,可以跳转页面 - `time` 属性,通常会设置成时间显示,但是这个属性不仅仅可以设置时间,你可以传入任何文本,注意文本长度可能会影响显示 - `avatar` 和 `avatarList` 属性同时只会有一个生效,同时设置的话,`avatarList` 属性的长度大于1 ,`avatar` 属性将失效 - 可以通过默认插槽自定义列表右侧内容 ```html 刚刚 ``` ```javascript export default { components: {}, data() { return { avatarList: [{ url: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/460d46d0-4fcc-11eb-8ff1-d5dcf8779628.png' }, { url: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/460d46d0-4fcc-11eb-8ff1-d5dcf8779628.png' }, { url: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/460d46d0-4fcc-11eb-8ff1-d5dcf8779628.png' }] } } } ``` ```css .chat-custom-right { flex: 1; /* #ifndef APP-NVUE */ display: flex; /* #endif */ flex-direction: column; justify-content: space-between; align-items: flex-end; } .chat-custom-text { font-size: 12px; color: #999; } ``` ## API ### List Props 属性名 |类型 |默认值 | 说明 :-: |:-: |:-: | :-: border |Boolean |true | 是否显示边框 ### ListItem Props 属性名 |类型 |默认值 | 说明 :-: |:-: |:-: | :-: title |String |- | 标题 note |String |- | 描述 ellipsis |Number |0 | title 是否溢出隐藏,可选值,0:默认; 1:显示一行; 2:显示两行;【nvue 暂不支持】 thumb |String |- | 左侧缩略图,若thumb有值,则不会显示扩展图标 thumbSize |String |medium | 略缩图尺寸,可选值,lg:大图; medium:一般; sm:小图; showBadge |Boolean |false | 是否显示数字角标 badgeText |String |- | 数字角标内容 badgeType |String |- | 数字角标类型,参考[uni-icons](https://ext.dcloud.net.cn/plugin?id=21) badgeStyle |Object |- | 数字角标样式,使用uni-badge的custom-style参数 rightText |String |- | 右侧文字内容 disabled |Boolean |false | 是否禁用 showArrow |Boolean |true | 是否显示箭头图标 link |String |navigateTo | 新页面跳转方式,可选值见下表 to |String |- | 新页面跳转地址,如填写此属性,click 会返回页面是否跳转成功 clickable |Boolean |false | 是否开启点击反馈 showSwitch |Boolean |false | 是否显示Switch switchChecked |Boolean |false | Switch是否被选中 showExtraIcon |Boolean |false | 左侧是否显示扩展图标 extraIcon |Object |- | 扩展图标参数,格式为 ``{color: '#4cd964',size: '22',type: 'spinner'}``,参考 [uni-icons](https://ext.dcloud.net.cn/plugin?id=28) direction | String |row | 排版方向,可选值,row:水平排列; column:垂直排列; 3个插槽是水平排还是垂直排,也受此属性控制 #### Link Options 属性名 | 说明 :-: | :-: navigateTo | 同 uni.navigateTo() redirectTo | 同 uni.reLaunch() reLaunch | 同 uni.reLaunch() switchTab | 同 uni.switchTab() ### ListItem Events 事件称名 |说明 |返回参数 :-: |:-: |:-: click |点击 uniListItem 触发事件,需开启点击反馈 |- switchChange |点击切换 Switch 时触发,需显示 switch |e={value:checked} ### ListItem Slots 名称 | 说明 :-: | :-: header | 左/上内容插槽,可完全自定义默认显示 body | 中间内容插槽,可完全自定义中间内容 footer | 右/下内容插槽,可完全自定义右侧内容 > **通过插槽扩展** > 需要注意的是当使用插槽时,内置样式将会失效,只保留排版样式,此时的样式需要开发者自己实现 > 如果 `uni-list-item` 组件内置属性样式无法满足需求,可以使用插槽来自定义uni-list-item里的内容。 > uni-list-item提供了3个可扩展的插槽:`header`、`body`、`footer` > - 当 `direction` 属性为 `row` 时表示水平排列,此时 `header` 表示列表的左边部分,`body` 表示列表的中间部分,`footer` 表示列表的右边部分 > - 当 `direction` 属性为 `column` 时表示垂直排列,此时 `header` 表示列表的上边部分,`body` 表示列表的中间部分,`footer` 表示列表的下边部分 > 开发者可以只用1个插槽,也可以3个一起使用。在插槽中可自主编写view标签,实现自己所需的效果。 **示例** ```html 自定义插槽 ``` ### ListItemChat Props 属性名 |类型 |默认值 | 说明 :-: |:-: |:-: | :-: title |String |- | 标题 note |String |- | 描述 clickable |Boolean |false | 是否开启点击反馈 badgeText |String |- | 数字角标内容,设置为 `dot` 将显示圆点 badgePositon |String |right | 角标位置 link |String |navigateTo | 是否展示右侧箭头并开启点击反馈,可选值见下表 clickable |Boolean |false | 是否开启点击反馈 to |String |- | 跳转页面地址,如填写此属性,click 会返回页面是否跳转成功 time |String |- | 右侧时间显示 avatarCircle |Boolean |false | 是否显示圆形头像 avatar |String |- | 头像地址,avatarCircle 不填时生效 avatarList |Array |- | 头像组,格式为 [{url:''}] #### Link Options 属性名 | 说明 :-: | :-: navigateTo | 同 uni.navigateTo() redirectTo | 同 uni.reLaunch() reLaunch | 同 uni.reLaunch() switchTab | 同 uni.switchTab() ### ListItemChat Slots 名称 | 说明 :- | :- default | 自定义列表右侧内容(包括时间和角标显示) ### ListItemChat Events 事件称名 | 说明 | 返回参数 :-: | :-: | :-: @click | 点击 uniListChat 触发事件 | {data:{}} ,如有 to 属性,会返回页面跳转信息 ## 基于uni-list扩展的页面模板 通过扩展插槽,可实现多种常见样式的列表 **新闻列表类** 1. 云端一体混合布局:[https://ext.dcloud.net.cn/plugin?id=2546](https://ext.dcloud.net.cn/plugin?id=2546) 2. 云端一体垂直布局,大图模式:[https://ext.dcloud.net.cn/plugin?id=2583](https://ext.dcloud.net.cn/plugin?id=2583) 3. 云端一体垂直布局,多行图文混排:[https://ext.dcloud.net.cn/plugin?id=2584](https://ext.dcloud.net.cn/plugin?id=2584) 4. 云端一体垂直布局,多图模式:[https://ext.dcloud.net.cn/plugin?id=2585](https://ext.dcloud.net.cn/plugin?id=2585) 5. 云端一体水平布局,左图右文:[https://ext.dcloud.net.cn/plugin?id=2586](https://ext.dcloud.net.cn/plugin?id=2586) 6. 云端一体水平布局,左文右图:[https://ext.dcloud.net.cn/plugin?id=2587](https://ext.dcloud.net.cn/plugin?id=2587) 7. 云端一体垂直布局,无图模式,主标题+副标题:[https://ext.dcloud.net.cn/plugin?id=2588](https://ext.dcloud.net.cn/plugin?id=2588) **商品列表类** 1. 云端一体列表/宫格视图互切:[https://ext.dcloud.net.cn/plugin?id=2651](https://ext.dcloud.net.cn/plugin?id=2651) 2. 云端一体列表(宫格模式):[https://ext.dcloud.net.cn/plugin?id=2671](https://ext.dcloud.net.cn/plugin?id=2671) 3. 云端一体列表(列表模式):[https://ext.dcloud.net.cn/plugin?id=2672](https://ext.dcloud.net.cn/plugin?id=2672) ## 组件示例 点击查看:[https://hellouniapp.dcloud.net.cn/pages/extUI/list/list](https://hellouniapp.dcloud.net.cn/pages/extUI/list/list) ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/LICENSE ================================================ MIT License Copyright (c) 2020 www.uviewui.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/README.md ================================================

logo

uView 2.0

多平台快速开发的UI框架

[![stars](https://img.shields.io/github/stars/umicro/uView2.0?style=flat-square&logo=GitHub)](https://github.com/umicro/uView2.0) [![forks](https://img.shields.io/github/forks/umicro/uView2.0?style=flat-square&logo=GitHub)](https://github.com/umicro/uView2.0) [![issues](https://img.shields.io/github/issues/umicro/uView2.0?style=flat-square&logo=GitHub)](https://github.com/umicro/uView2.0/issues) [![Website](https://img.shields.io/badge/uView-up-blue?style=flat-square)](https://uviewui.com) [![release](https://img.shields.io/github/v/release/umicro/uView2.0?style=flat-square)](https://gitee.com/umicro/uView2.0/releases) [![license](https://img.shields.io/github/license/umicro/uView2.0?style=flat-square)](https://en.wikipedia.org/wiki/MIT_License) ## 说明 uView UI,是[uni-app](https://uniapp.dcloud.io/)全面兼容nvue的uni-app生态框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水 ## [官方文档:https://uviewui.com](https://uviewui.com) ## 预览 您可以通过**微信**扫码,查看最佳的演示效果。

## 链接 - [官方文档](https://www.uviewui.com/) - [更新日志](https://www.uviewui.com/components/changelog.html) - [升级指南](https://www.uviewui.com/components/changeGuide.html) - [关于我们](https://www.uviewui.com/cooperation/about.html) ## 交流反馈 欢迎加入我们的QQ群交流反馈:[点此跳转](https://www.uviewui.com/components/addQQGroup.html) ## 关于PR > 我们非常乐意接受各位的优质PR,但在此之前我希望您了解uView2.0是一个需要兼容多个平台的(小程序、h5、ios app、android app)包括nvue页面、vue页面。 > 所以希望在您修复bug并提交之前尽可能的去这些平台测试一下兼容性。最好能携带测试截图以方便审核。非常感谢! ## 安装 #### **uni-app插件市场链接** —— [https://ext.dcloud.net.cn/plugin?id=1593](https://ext.dcloud.net.cn/plugin?id=1593) 请通过[官网安装文档](https://www.uviewui.com/components/install.html)了解更详细的内容 ## 快速上手 请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容 ## 使用方法 配置easycom规则后,自动按需引入,无需`import`组件,直接引用即可。 ```html ``` ## 版权信息 uView遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uView应用到您的产品中。 ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/changelog.md ================================================ ## 2.0.34(2022-09-25) # uView2.0重磅发布,利剑出鞘,一统江湖 1. `u-input`、`u-textarea`增加`ignoreCompositionEvent`属性 2. 修复`route`方法调用可能报错的问题 3. 修复`u-no-network`组件`z-index`无效的问题 4. 修复`textarea`组件在h5上confirmType=""报错的问题 5. `u-rate`适配`nvue` 6. 优化验证手机号码的正则表达式(根据工信部发布的《电信网编号计划(2017年版)》进行修改。) 7. `form-item`添加`labelPosition`属性 8. `u-calendar`修复`maxDate`设置为当前日期,并且当前时间大于08:00时无法显示日期列表的问题 (#724) 9. `u-radio`增加一个默认插槽用于自定义修改label内容 (#680) 10. 修复`timeFormat`函数在safari重的兼容性问题 (#664) ## 2.0.33(2022-06-17) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 修复`loadmore`组件`lineColor`类型错误问题 2. 修复`u-parse`组件`imgtap`、`linktap`不生效问题 ## 2.0.32(2022-06-16) # uView2.0重磅发布,利剑出鞘,一统江湖 1. `u-loadmore`新增自定义颜色、虚/实线 2. 修复`u-swiper-action`组件部分平台不能上下滑动的问题 3. 修复`u-list`回弹问题 4. 修复`notice-bar`组件动画在低端安卓机可能会抖动的问题 5. `u-loading-page`添加控制图标大小的属性`iconSize` 6. 修复`u-tooltip`组件`color`参数不生效的问题 7. 修复`u--input`组件使用`blur`事件输出为`undefined`的bug 8. `u-code-input`组件新增键盘弹起时,是否自动上推页面参数`adjustPosition` 9. 修复`image`组件`load`事件无回调对象问题 10. 修复`button`组件`loadingSize`设置无效问题 10. 其他修复 ## 2.0.31(2022-04-19) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 修复`upload`在`vue`页面上传成功后没有成功标志的问题 2. 解决演示项目中微信小程序模拟上传图片一直出于上传中问题 3. 修复`u-code-input`组件在`nvue`页面编译到`app`平台上光标异常问题(`app`去除此功能) 4. 修复`actionSheet`组件标题关闭按钮点击事件名称错误的问题 5. 其他修复 ## 2.0.30(2022-04-04) # uView2.0重磅发布,利剑出鞘,一统江湖 1. `u-rate`增加`readonly`属性 2. `tabs`滑块支持设置背景图片 3. 修复`u-subsection` `mode`为`subsection`时,滑块样式不正确的问题 4. `u-code-input`添加光标效果动画 5. 修复`popup`的`open`事件不触发 6. 修复`u-flex-column`无效的问题 7. 修复`u-datetime-picker`索引在特定场合异常问题 8. 修复`u-datetime-picker`最小时间字符串模板错误问题 9. `u-swiper`添加`m3u8`验证 10. `u-swiper`修改判断image和video逻辑 11. 修复`swiper`无法使用本地图片问题,增加`type`参数 12. 修复`u-row-notice`格式错误问题 13. 修复`u-switch`组件当`unit`为`rpx`时,`nodeStyle`消失的问题 14. 修复`datetime-picker`组件`showToolbar`与`visibleItemCount`属性无效的问题 15. 修复`upload`组件条件编译位置判断错误,导致`previewImage`属性设置为`false`时,整个组件都会被隐藏的问题 16. 修复`u-checkbox-group`设置`shape`属性无效的问题 17. 修复`u-upload`的`capture`传入字符串的时候不生效的问题 18. 修复`u-action-sheet`组件,关闭事件逻辑错误的问题 19. 修复`u-list`触顶事件的触发错误的问题 20. 修复`u-text`只有手机号可拨打的问题 21. 修复`u-textarea`不能换行的问题 22. 其他修复 ## 2.0.29(2022-03-13) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 修复`u--text`组件设置`decoration`属性未生效的问题 2. 修复`u-datetime-picker`使用`formatter`后返回值不正确 3. 修复`u-datetime-picker` `intercept` 可能为undefined 4. 修复已设置单位 uni..config.unit = 'rpx'时,线型指示器 `transform` 的位置翻倍,导致指示器超出宽度 5. 修复mixin中bem方法生成的类名在支付宝和字节小程序中失效 6. 修复默认值传值为空的时候,打开`u-datetime-picker`报错,不能选中第一列时间的bug 7. 修复`u-datetime-picker`使用`formatter`后返回值不正确 8. 修复`u-image`组件`loading`无效果的问题 9. 修复`config.unit`属性设为`rpx`时,导航栏占用高度不足导致塌陷的问题 10. 修复`u-datetime-picker`组件`itemHeight`无效问题 11. 其他修复 ## 2.0.28(2022-02-22) # uView2.0重磅发布,利剑出鞘,一统江湖 1. search组件新增searchIconSize属性 2. 兼容Safari/Webkit中传入时间格式如2022-02-17 12:00:56 3. 修复text value.js 判断日期出format错误问题 4. priceFormat格式化金额出现精度错误 5. priceFormat在部分情况下出现精度损失问题 6. 优化表单rules提示 7. 修复avatar组件src为空时,展示状态不对 8. 其他修复 ## 2.0.27(2022-01-28) # uView2.0重磅发布,利剑出鞘,一统江湖 1.样式修复 ## 2.0.26(2022-01-28) # uView2.0重磅发布,利剑出鞘,一统江湖 1.样式修复 ## 2.0.25(2022-01-27) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 修复text组件mode=price时,可能会导致精度错误的问题 2. 添加$u.setConfig()方法,可设置uView内置的config, props, zIndex, color属性,详见:[修改uView内置配置方案](https://uviewui.com/components/setting.html#%E9%BB%98%E8%AE%A4%E5%8D%95%E4%BD%8D%E9%85%8D%E7%BD%AE) 3. 优化form组件在errorType=toast时,如果输入错误页面会有抖动的问题 4. 修复$u.addUnit()对配置默认单位可能无效的问题 ## 2.0.24(2022-01-25) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 修复swiper在current指定非0时缩放有误 2. 修复u-icon添加stop属性的时候报错 3. 优化遗留的通过正则判断rpx单位的问题 4. 优化Layout布局 vue使用gutter时,会超出固定区域 5. 优化search组件高度单位问题(rpx -> px) 6. 修复u-image slot 加载和错误的图片失去了高度 7. 修复u-index-list中footer插槽与header插槽存在性判断错误 8. 修复部分机型下u-popup关闭时会闪烁 9. 修复u-image在nvue-app下失去宽高 10. 修复u-popup运行报错 11. 修复u-tooltip报错 12. 修复box-sizing在app下的警告 13. 修复u-navbar在小程序中报运行时错误 14. 其他修复 ## 2.0.23(2022-01-24) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 修复image组件在hx3.3.9的nvue下可能会显示异常的问题 2. 修复col组件gutter参数带rpx单位处理不正确的问题 3. 修复text组件单行时无法显示省略号的问题 4. navbar添加titleStyle参数 5. 升级到hx3.3.9可消除nvue下控制台样式警告的问题 ## 2.0.22(2022-01-19) # uView2.0重磅发布,利剑出鞘,一统江湖 1. $u.page()方法优化,避免在特殊场景可能报错的问题 2. picker组件添加immediateChange参数 3. 新增$u.pages()方法 ## 2.0.21(2022-01-19) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 优化:form组件在用户设置rules的时候提示用户model必传 2. 优化遗留的通过正则判断rpx单位的问题 3. 修复微信小程序环境中tabbar组件开启safeAreaInsetBottom属性后,placeholder高度填充不正确 4. 修复swiper在current指定非0时缩放有误 5. 修复u-icon添加stop属性的时候报错 6. 修复upload组件在accept=all的时候没有作用 7. 修复在text组件mode为phone时call属性无效的问题 8. 处理u-form clearValidate方法 9. 其他修复 ## 2.0.20(2022-01-14) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 修复calendar默认会选择一个日期,如果直接点确定的话,无法取到值的问题 2. 修复Slider缺少disabled props 还有注释 3. 修复u-notice-bar点击事件无法拿到index索引值的问题 4. 修复u-collapse-item在vue文件下,app端自定义插槽不生效的问题 5. 优化头像为空时显示默认头像 6. 修复图片地址赋值后判断加载状态为完成问题 7. 修复日历滚动到默认日期月份区域 8. search组件暴露点击左边icon事件 9. 修复u-form clearValidate方法不生效 10. upload h5端增加返回文件参数(文件的name参数) 11. 处理upload选择文件后url为blob类型无法预览的问题 12. u-code-input 修复输入框没有往左移出一半屏幕 13. 修复Upload上传 disabled为true时,控制台报hoverClass类型错误 14. 临时处理ios app下grid点击坍塌问题 15. 其他修复 ## 2.0.19(2021-12-29) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 优化微信小程序包体积可在微信中预览,请升级HbuilderX3.3.4,同时在“运行->运行到小程序模拟器”中勾选“运行时是否压缩代码” 2. 优化微信小程序setData性能,处理某些方法如$u.route()无法在模板中使用的问题 3. navbar添加autoBack参数 4. 允许avatar组件的事件冒泡 5. 修复cell组件报错问题 6. 其他修复 ## 2.0.18(2021-12-28) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 修复app端编译报错问题 2. 重新处理微信小程序端setData过大的性能问题 3. 修复边框问题 4. 修复最大最小月份不大于0则没有数据出现的问题 5. 修复SwipeAction微信小程序端无法上下滑动问题 6. 修复input的placeholder在小程序端默认显示为true问题 7. 修复divider组件click事件无效问题 8. 修复u-code-input maxlength 属性值为 String 类型时显示异常 9. 修复当 grid只有 1到2时 在小程序端algin设置无效的问题 10. 处理form-item的label为top时,取消错误提示的左边距 11. 其他修复 ## 2.0.17(2021-12-26) ## uView正在参与开源中国的“年度最佳项目”评选,之前投过票的现在也可以投票,恳请同学们投一票,[点此帮助uView](https://www.oschina.net/project/top_cn_2021/?id=583) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 解决HBuilderX3.3.3.20211225版本导致的样式问题 2. calendar日历添加monthNum参数 3. navbar添加center slot ## 2.0.16(2021-12-25) ## uView正在参与开源中国的“年度最佳项目”评选,之前投过票的现在也可以投票,恳请同学们投一票,[点此帮助uView](https://www.oschina.net/project/top_cn_2021/?id=583) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 解决微信小程序setData性能问题 2. 修复count-down组件change事件不触发问题 ## 2.0.15(2021-12-21) ## uView正在参与开源中国的“年度最佳项目”评选,之前投过票的现在也可以投票,恳请同学们投一票,[点此帮助uView](https://www.oschina.net/project/top_cn_2021/?id=583) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 修复Cell单元格titleWidth无效 2. 修复cheakbox组件ischecked不更新 3. 修复keyboard是否显示"."按键默认值问题 4. 修复number-keyboard是否显示键盘的"."符号问题 5. 修复Input输入框 readonly无效 6. 修复u-avatar 导致打包app、H5时候报错问题 7. 修复Upload上传deletable无效 8. 修复upload当设置maxSize时无效的问题 9. 修复tabs lineWidth传入带单位的字符串的时候偏移量计算错误问题 10. 修复rate组件在有padding的view内,显示的星星位置和可触摸区域不匹配,无法正常选中星星 ## 2.0.13(2021-12-14) ## [点击加群交流反馈:364463526](https://jq.qq.com/?_chanwv=1027&k=mCxS3TGY) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 修复配置默认单位为rpx可能会导致自定义导航栏高度异常的问题 ## 2.0.12(2021-12-14) ## [点击加群交流反馈:364463526](https://jq.qq.com/?_chanwv=1027&k=mCxS3TGY) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 修复tabs组件在vue环境下划线消失的问题 2. 修复upload组件在安卓小程序无法选择视频的问题 3. 添加uni.$u.config.unit配置,用于配置参数默认单位,详见:[默认单位配置](https://www.uviewui.com/components/setting.html#%E9%BB%98%E8%AE%A4%E5%8D%95%E4%BD%8D%E9%85%8D%E7%BD%AE) 4. 修复textarea组件在没绑定v-model时,字符统计不生效问题 5. 修复nvue下控制是否出现滚动条失效问题 ## 2.0.11(2021-12-13) ## [点击加群交流反馈:364463526](https://jq.qq.com/?_chanwv=1027&k=mCxS3TGY) # uView2.0重磅发布,利剑出鞘,一统江湖 1. text组件align参数无效的问题 2. subsection组件添加keyName参数 3. upload组件无法判断[Object file]类型的问题 4. 处理notify层级过低问题 5. codeInput组件添加disabledDot参数 6. 处理actionSheet组件round参数无效的问题 7. calendar组件添加round参数用于控制圆角值 8. 处理swipeAction组件在vue环境下默认被打开的问题 9. button组件的throttleTime节流参数无效的问题 10. 解决u-notify手动关闭方法close()无效的问题 11. input组件readonly不生效问题 12. tag组件type参数为info不生效问题 ## 2.0.10(2021-12-08) ## [点击加群交流反馈:364463526](https://jq.qq.com/?_chanwv=1027&k=mCxS3TGY) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 修复button sendMessagePath属性不生效 2. 修复DatetimePicker选择器title无效 3. 修复u-toast设置loading=true不生效 4. 修复u-text金额模式传0报错 5. 修复u-toast组件的icon属性配置不生效 6. button的icon在特殊场景下的颜色优化 7. IndexList优化,增加# ## 2.0.9(2021-12-01) ## [点击加群交流反馈:232041042](https://jq.qq.com/?_wv=1027&k=KnbeceDU) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 优化swiper的height支持100%值(仅vue有效),修复嵌入视频时click事件无法触发的问题 2. 优化tabs组件对list值为空的判断,或者动态变化list时重新计算相关尺寸的问题 3. 优化datetime-picker组件逻辑,让其后续打开的默认值为上一次的选中值,需要通过v-model绑定值才有效 4. 修复upload内嵌在其他组件中,选择图片可能不会换行的问题 ## 2.0.8(2021-12-01) ## [点击加群交流反馈:232041042](https://jq.qq.com/?_wv=1027&k=KnbeceDU) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 修复toast的position参数无效问题 2. 处理input在ios nvue上无法获得焦点的问题 3. avatar-group组件添加extraValue参数,让剩余展示数量可手动控制 4. tabs组件添加keyName参数用于配置从对象中读取的键名 5. 处理text组件名字脱敏默认配置无效的问题 6. 处理picker组件item文本太长换行问题 ## 2.0.7(2021-11-30) ## [点击加群交流反馈:232041042](https://jq.qq.com/?_wv=1027&k=KnbeceDU) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 修复radio和checkbox动态改变v-model无效的问题。 2. 优化form规则validator在微信小程序用法 3. 修复backtop组件mode参数在微信小程序无效的问题 4. 处理Album的previewFullImage属性无效的问题 5. 处理u-datetime-picker组件mode='time'在选择改变时间时,控制台报错的问题 ## 2.0.6(2021-11-27) ## [点击加群交流反馈:232041042](https://jq.qq.com/?_wv=1027&k=KnbeceDU) # uView2.0重磅发布,利剑出鞘,一统江湖 1. 处理tag组件在vue下边框无效的问题。 2. 处理popup组件圆角参数可能无效的问题。 3. 处理tabs组件lineColor参数可能无效的问题。 4. propgress组件在值很小时,显示异常的问题。 ## 2.0.5(2021-11-25) ## [点击加群交流反馈:232041042](https://jq.qq.com/?_wv=1027&k=KnbeceDU) # uView2.0重磅发布,利剑出鞘,一统江湖 1. calendar在vue下显示异常问题。 2. form组件labelPosition和errorType参数无效的问题 3. input组件inputAlign无效的问题 4. 其他一些修复 ## 2.0.4(2021-11-23) ## [点击加群交流反馈:232041042](https://jq.qq.com/?_wv=1027&k=KnbeceDU) # uView2.0重磅发布,利剑出鞘,一统江湖 0. input组件缺失@confirm事件,以及subfix和prefix无效问题 1. component.scss文件样式在vue下干扰全局布局问题 2. 修复subsection在vue环境下表现异常的问题 3. tag组件的bgColor等参数无效的问题 4. upload组件不换行的问题 5. 其他的一些修复处理 ## 2.0.3(2021-11-16) ## [点击加群交流反馈:1129077272](https://jq.qq.com/?_wv=1027&k=KnbeceDU) # uView2.0重磅发布,利剑出鞘,一统江湖 1. uView2.0已实现全面兼容nvue 2. uView2.0对1.x进行了架构重构,细节和性能都有极大提升 3. 目前uView2.0为公测阶段,相关细节可能会有变动 4. 我们写了一份与1.x的对比指南,详见[对比1.x](https://www.uviewui.com/components/diff1.x.html) 5. 处理modal的confirm回调事件拼写错误问题 6. 处理input组件@input事件参数错误问题 7. 其他一些修复 ## 2.0.2(2021-11-16) ## [点击加群交流反馈:1129077272](https://jq.qq.com/?_wv=1027&k=KnbeceDU) # uView2.0重磅发布,利剑出鞘,一统江湖 1. uView2.0已实现全面兼容nvue 2. uView2.0对1.x进行了架构重构,细节和性能都有极大提升 3. 目前uView2.0为公测阶段,相关细节可能会有变动 4. 我们写了一份与1.x的对比指南,详见[对比1.x](https://www.uviewui.com/components/diff1.x.html) 5. 修复input组件formatter参数缺失问题 6. 优化loading-icon组件的scss写法问题,防止不兼容新版本scss ## 2.0.0(2020-11-15) ## [点击加群交流反馈:1129077272](https://jq.qq.com/?_wv=1027&k=KnbeceDU) # uView2.0重磅发布,利剑出鞘,一统江湖 1. uView2.0已实现全面兼容nvue 2. uView2.0对1.x进行了架构重构,细节和性能都有极大提升 3. 目前uView2.0为公测阶段,相关细节可能会有变动 4. 我们写了一份与1.x的对比指南,详见[对比1.x](https://www.uviewui.com/components/diff1.x.html) 5. 修复input组件formatter参数缺失问题 ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u--form/u--form.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u--image/u--image.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u--input/u--input.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u--text/u--text.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u--textarea/u--textarea.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-action-sheet/props.js ================================================ export default { props: { // 操作菜单是否展示 (默认false) show: { type: Boolean, default: uni.$u.props.actionSheet.show }, // 标题 title: { type: String, default: uni.$u.props.actionSheet.title }, // 选项上方的描述信息 description: { type: String, default: uni.$u.props.actionSheet.description }, // 数据 actions: { type: Array, default: uni.$u.props.actionSheet.actions }, // 取消按钮的文字,不为空时显示按钮 cancelText: { type: String, default: uni.$u.props.actionSheet.cancelText }, // 点击某个菜单项时是否关闭弹窗 closeOnClickAction: { type: Boolean, default: uni.$u.props.actionSheet.closeOnClickAction }, // 处理底部安全区(默认true) safeAreaInsetBottom: { type: Boolean, default: uni.$u.props.actionSheet.safeAreaInsetBottom }, // 小程序的打开方式 openType: { type: String, default: uni.$u.props.actionSheet.openType }, // 点击遮罩是否允许关闭 (默认true) closeOnClickOverlay: { type: Boolean, default: uni.$u.props.actionSheet.closeOnClickOverlay }, // 圆角值 round: { type: [Boolean, String, Number], default: uni.$u.props.actionSheet.round } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-action-sheet/u-action-sheet.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-album/props.js ================================================ export default { props: { // 图片地址,Array|Array形式 urls: { type: Array, default: uni.$u.props.album.urls }, // 指定从数组的对象元素中读取哪个属性作为图片地址 keyName: { type: String, default: uni.$u.props.album.keyName }, // 单图时,图片长边的长度 singleSize: { type: [String, Number], default: uni.$u.props.album.singleSize }, // 多图时,图片边长 multipleSize: { type: [String, Number], default: uni.$u.props.album.multipleSize }, // 多图时,图片水平和垂直之间的间隔 space: { type: [String, Number], default: uni.$u.props.album.space }, // 单图时,图片缩放裁剪的模式 singleMode: { type: String, default: uni.$u.props.album.singleMode }, // 多图时,图片缩放裁剪的模式 multipleMode: { type: String, default: uni.$u.props.album.multipleMode }, // 最多展示的图片数量,超出时最后一个位置将会显示剩余图片数量 maxCount: { type: [String, Number], default: uni.$u.props.album.maxCount }, // 是否可以预览图片 previewFullImage: { type: Boolean, default: uni.$u.props.album.previewFullImage }, // 每行展示图片数量,如设置,singleSize和multipleSize将会无效 rowCount: { type: [String, Number], default: uni.$u.props.album.rowCount }, // 超出maxCount时是否显示查看更多的提示 showMore: { type: Boolean, default: uni.$u.props.album.showMore } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-album/u-album.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-alert/props.js ================================================ export default { props: { // 显示文字 title: { type: String, default: uni.$u.props.alert.title }, // 主题,success/warning/info/error type: { type: String, default: uni.$u.props.alert.type }, // 辅助性文字 description: { type: String, default: uni.$u.props.alert.description }, // 是否可关闭 closable: { type: Boolean, default: uni.$u.props.alert.closable }, // 是否显示图标 showIcon: { type: Boolean, default: uni.$u.props.alert.showIcon }, // 浅或深色调,light-浅色,dark-深色 effect: { type: String, default: uni.$u.props.alert.effect }, // 文字是否居中 center: { type: Boolean, default: uni.$u.props.alert.center }, // 字体大小 fontSize: { type: [String, Number], default: uni.$u.props.alert.fontSize } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-alert/u-alert.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-avatar/props.js ================================================ export default { props: { // 头像图片路径(不能为相对路径) src: { type: String, default: uni.$u.props.avatar.src }, // 头像形状,circle-圆形,square-方形 shape: { type: String, default: uni.$u.props.avatar.shape }, // 头像尺寸 size: { type: [String, Number], default: uni.$u.props.avatar.size }, // 裁剪模式 mode: { type: String, default: uni.$u.props.avatar.mode }, // 显示的文字 text: { type: String, default: uni.$u.props.avatar.text }, // 背景色 bgColor: { type: String, default: uni.$u.props.avatar.bgColor }, // 文字颜色 color: { type: String, default: uni.$u.props.avatar.color }, // 文字大小 fontSize: { type: [String, Number], default: uni.$u.props.avatar.fontSize }, // 显示的图标 icon: { type: String, default: uni.$u.props.avatar.icon }, // 显示小程序头像,只对百度,微信,QQ小程序有效 mpAvatar: { type: Boolean, default: uni.$u.props.avatar.mpAvatar }, // 是否使用随机背景色 randomBgColor: { type: Boolean, default: uni.$u.props.avatar.randomBgColor }, // 加载失败的默认头像(组件有内置默认图片) defaultUrl: { type: String, default: uni.$u.props.avatar.defaultUrl }, // 如果配置了randomBgColor为true,且配置了此值,则从默认的背景色数组中取出对应索引的颜色值,取值0-19之间 colorIndex: { type: [String, Number], // 校验参数规则,索引在0-19之间 validator(n) { return uni.$u.test.range(n, [0, 19]) || n === '' }, default: uni.$u.props.avatar.colorIndex }, // 组件标识符 name: { type: String, default: uni.$u.props.avatar.name } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-avatar/u-avatar.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-avatar-group/props.js ================================================ export default { props: { // 头像图片组 urls: { type: Array, default: uni.$u.props.avatarGroup.urls }, // 最多展示的头像数量 maxCount: { type: [String, Number], default: uni.$u.props.avatarGroup.maxCount }, // 头像形状 shape: { type: String, default: uni.$u.props.avatarGroup.shape }, // 图片裁剪模式 mode: { type: String, default: uni.$u.props.avatarGroup.mode }, // 超出maxCount时是否显示查看更多的提示 showMore: { type: Boolean, default: uni.$u.props.avatarGroup.showMore }, // 头像大小 size: { type: [String, Number], default: uni.$u.props.avatarGroup.size }, // 指定从数组的对象元素中读取哪个属性作为图片地址 keyName: { type: String, default: uni.$u.props.avatarGroup.keyName }, // 头像之间的遮挡比例 gap: { type: [String, Number], validator(value) { return value >= 0 && value <= 1 }, default: uni.$u.props.avatarGroup.gap }, // 需额外显示的值 extraValue: { type: [Number, String], default: uni.$u.props.avatarGroup.extraValue } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-avatar-group/u-avatar-group.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-back-top/props.js ================================================ export default { props: { // 返回顶部的形状,circle-圆形,square-方形 mode: { type: String, default: uni.$u.props.backtop.mode }, // 自定义图标 icon: { type: String, default: uni.$u.props.backtop.icon }, // 提示文字 text: { type: String, default: uni.$u.props.backtop.text }, // 返回顶部滚动时间 duration: { type: [String, Number], default: uni.$u.props.backtop.duration }, // 滚动距离 scrollTop: { type: [String, Number], default: uni.$u.props.backtop.scrollTop }, // 距离顶部多少距离显示,单位px top: { type: [String, Number], default: uni.$u.props.backtop.top }, // 返回顶部按钮到底部的距离,单位px bottom: { type: [String, Number], default: uni.$u.props.backtop.bottom }, // 返回顶部按钮到右边的距离,单位px right: { type: [String, Number], default: uni.$u.props.backtop.right }, // 层级 zIndex: { type: [String, Number], default: uni.$u.props.backtop.zIndex }, // 图标的样式,对象形式 iconStyle: { type: Object, default: uni.$u.props.backtop.iconStyle } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-back-top/u-back-top.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-badge/props.js ================================================ export default { props: { // 是否显示圆点 isDot: { type: Boolean, default: uni.$u.props.badge.isDot }, // 显示的内容 value: { type: [Number, String], default: uni.$u.props.badge.value }, // 是否显示 show: { type: Boolean, default: uni.$u.props.badge.show }, // 最大值,超过最大值会显示 '{max}+' max: { type: [Number, String], default: uni.$u.props.badge.max }, // 主题类型,error|warning|success|primary type: { type: String, default: uni.$u.props.badge.type }, // 当数值为 0 时,是否展示 Badge showZero: { type: Boolean, default: uni.$u.props.badge.showZero }, // 背景颜色,优先级比type高,如设置,type参数会失效 bgColor: { type: [String, null], default: uni.$u.props.badge.bgColor }, // 字体颜色 color: { type: [String, null], default: uni.$u.props.badge.color }, // 徽标形状,circle-四角均为圆角,horn-左下角为直角 shape: { type: String, default: uni.$u.props.badge.shape }, // 设置数字的显示方式,overflow|ellipsis|limit // overflow会根据max字段判断,超出显示`${max}+` // ellipsis会根据max判断,超出显示`${max}...` // limit会依据1000作为判断条件,超出1000,显示`${value/1000}K`,比如2.2k、3.34w,最多保留2位小数 numberType: { type: String, default: uni.$u.props.badge.numberType }, // 设置badge的位置偏移,格式为 [x, y],也即设置的为top和right的值,absolute为true时有效 offset: { type: Array, default: uni.$u.props.badge.offset }, // 是否反转背景和字体颜色 inverted: { type: Boolean, default: uni.$u.props.badge.inverted }, // 是否绝对定位 absolute: { type: Boolean, default: uni.$u.props.badge.absolute } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-badge/u-badge.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-button/nvue.scss ================================================ $u-button-active-opacity:0.75 !default; $u-button-loading-text-margin-left:4px !default; $u-button-text-color: #FFFFFF !default; $u-button-text-plain-error-color:$u-error !default; $u-button-text-plain-warning-color:$u-warning !default; $u-button-text-plain-success-color:$u-success !default; $u-button-text-plain-info-color:$u-info !default; $u-button-text-plain-primary-color:$u-primary !default; .u-button { &--active { opacity: $u-button-active-opacity; } &--active--plain { background-color: rgb(217, 217, 217); } &__loading-text { margin-left:$u-button-loading-text-margin-left; } &__text, &__loading-text { color:$u-button-text-color; } &__text--plain--error { color:$u-button-text-plain-error-color; } &__text--plain--warning { color:$u-button-text-plain-warning-color; } &__text--plain--success{ color:$u-button-text-plain-success-color; } &__text--plain--info { color:$u-button-text-plain-info-color; } &__text--plain--primary { color:$u-button-text-plain-primary-color; } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-button/props.js ================================================ /* * @Author : LQ * @Description : * @version : 1.0 * @Date : 2021-08-16 10:04:04 * @LastAuthor : LQ * @lastTime : 2021-08-16 10:04:24 * @FilePath : /u-view2.0/uview-ui/components/u-button/props.js */ export default { props: { // 是否细边框 hairline: { type: Boolean, default: uni.$u.props.button.hairline }, // 按钮的预置样式,info,primary,error,warning,success type: { type: String, default: uni.$u.props.button.type }, // 按钮尺寸,large,normal,small,mini size: { type: String, default: uni.$u.props.button.size }, // 按钮形状,circle(两边为半圆),square(带圆角) shape: { type: String, default: uni.$u.props.button.shape }, // 按钮是否镂空 plain: { type: Boolean, default: uni.$u.props.button.plain }, // 是否禁止状态 disabled: { type: Boolean, default: uni.$u.props.button.disabled }, // 是否加载中 loading: { type: Boolean, default: uni.$u.props.button.loading }, // 加载中提示文字 loadingText: { type: [String, Number], default: uni.$u.props.button.loadingText }, // 加载状态图标类型 loadingMode: { type: String, default: uni.$u.props.button.loadingMode }, // 加载图标大小 loadingSize: { type: [String, Number], default: uni.$u.props.button.loadingSize }, // 开放能力,具体请看uniapp稳定关于button组件部分说明 // https://uniapp.dcloud.io/component/button openType: { type: String, default: uni.$u.props.button.openType }, // 用于
组件,点击分别会触发 组件的 submit/reset 事件 // 取值为submit(提交表单),reset(重置表单) formType: { type: String, default: uni.$u.props.button.formType }, // 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效 // 只微信小程序、QQ小程序有效 appParameter: { type: String, default: uni.$u.props.button.appParameter }, // 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效 hoverStopPropagation: { type: Boolean, default: uni.$u.props.button.hoverStopPropagation }, // 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文。只微信小程序有效 lang: { type: String, default: uni.$u.props.button.lang }, // 会话来源,open-type="contact"时有效。只微信小程序有效 sessionFrom: { type: String, default: uni.$u.props.button.sessionFrom }, // 会话内消息卡片标题,open-type="contact"时有效 // 默认当前标题,只微信小程序有效 sendMessageTitle: { type: String, default: uni.$u.props.button.sendMessageTitle }, // 会话内消息卡片点击跳转小程序路径,open-type="contact"时有效 // 默认当前分享路径,只微信小程序有效 sendMessagePath: { type: String, default: uni.$u.props.button.sendMessagePath }, // 会话内消息卡片图片,open-type="contact"时有效 // 默认当前页面截图,只微信小程序有效 sendMessageImg: { type: String, default: uni.$u.props.button.sendMessageImg }, // 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示, // 用户点击后可以快速发送小程序消息,open-type="contact"时有效 showMessageCard: { type: Boolean, default: uni.$u.props.button.showMessageCard }, // 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取 dataName: { type: String, default: uni.$u.props.button.dataName }, // 节流,一定时间内只能触发一次 throttleTime: { type: [String, Number], default: uni.$u.props.button.throttleTime }, // 按住后多久出现点击态,单位毫秒 hoverStartTime: { type: [String, Number], default: uni.$u.props.button.hoverStartTime }, // 手指松开后点击态保留时间,单位毫秒 hoverStayTime: { type: [String, Number], default: uni.$u.props.button.hoverStayTime }, // 按钮文字,之所以通过props传入,是因为slot传入的话 // nvue中无法控制文字的样式 text: { type: [String, Number], default: uni.$u.props.button.text }, // 按钮图标 icon: { type: String, default: uni.$u.props.button.icon }, // 按钮图标 iconColor: { type: String, default: uni.$u.props.button.icon }, // 按钮颜色,支持传入linear-gradient渐变色 color: { type: String, default: uni.$u.props.button.color } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-button/u-button.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-button/vue.scss ================================================ // nvue下hover-class无效 $u-button-before-top:50% !default; $u-button-before-left:50% !default; $u-button-before-width:100% !default; $u-button-before-height:100% !default; $u-button-before-transform:translate(-50%, -50%) !default; $u-button-before-opacity:0 !default; $u-button-before-background-color:#000 !default; $u-button-before-border-color:#000 !default; $u-button-active-before-opacity:.15 !default; $u-button-icon-margin-left:4px !default; $u-button-plain-u-button-info-color:$u-info; $u-button-plain-u-button-success-color:$u-success; $u-button-plain-u-button-error-color:$u-error; $u-button-plain-u-button-warning-color:$u-error; .u-button { width: 100%; &__text { white-space: nowrap; line-height: 1; } &:before { position: absolute; top:$u-button-before-top; left:$u-button-before-left; width:$u-button-before-width; height:$u-button-before-height; border: inherit; border-radius: inherit; transform:$u-button-before-transform; opacity:$u-button-before-opacity; content: " "; background-color:$u-button-before-background-color; border-color:$u-button-before-border-color; } &--active { &:before { opacity: .15 } } &__icon+&__text:not(:empty), &__loading-text { margin-left:$u-button-icon-margin-left; } &--plain { &.u-button--primary { color: $u-primary; } } &--plain { &.u-button--info { color:$u-button-plain-u-button-info-color; } } &--plain { &.u-button--success { color:$u-button-plain-u-button-success-color; } } &--plain { &.u-button--error { color:$u-button-plain-u-button-error-color; } } &--plain { &.u-button--warning { color:$u-button-plain-u-button-warning-color; } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-calendar/header.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-calendar/month.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-calendar/props.js ================================================ export default { props: { // 日历顶部标题 title: { type: String, default: uni.$u.props.calendar.title }, // 是否显示标题 showTitle: { type: Boolean, default: uni.$u.props.calendar.showTitle }, // 是否显示副标题 showSubtitle: { type: Boolean, default: uni.$u.props.calendar.showSubtitle }, // 日期类型选择,single-选择单个日期,multiple-可以选择多个日期,range-选择日期范围 mode: { type: String, default: uni.$u.props.calendar.mode }, // mode=range时,第一个日期底部的提示文字 startText: { type: String, default: uni.$u.props.calendar.startText }, // mode=range时,最后一个日期底部的提示文字 endText: { type: String, default: uni.$u.props.calendar.endText }, // 自定义列表 customList: { type: Array, default: uni.$u.props.calendar.customList }, // 主题色,对底部按钮和选中日期有效 color: { type: String, default: uni.$u.props.calendar.color }, // 最小的可选日期 minDate: { type: [String, Number], default: uni.$u.props.calendar.minDate }, // 最大可选日期 maxDate: { type: [String, Number], default: uni.$u.props.calendar.maxDate }, // 默认选中的日期,mode为multiple或range是必须为数组格式 defaultDate: { type: [Array, String, Date, null], default: uni.$u.props.calendar.defaultDate }, // mode=multiple时,最多可选多少个日期 maxCount: { type: [String, Number], default: uni.$u.props.calendar.maxCount }, // 日期行高 rowHeight: { type: [String, Number], default: uni.$u.props.calendar.rowHeight }, // 日期格式化函数 formatter: { type: [Function, null], default: uni.$u.props.calendar.formatter }, // 是否显示农历 showLunar: { type: Boolean, default: uni.$u.props.calendar.showLunar }, // 是否显示月份背景色 showMark: { type: Boolean, default: uni.$u.props.calendar.showMark }, // 确定按钮的文字 confirmText: { type: String, default: uni.$u.props.calendar.confirmText }, // 确认按钮处于禁用状态时的文字 confirmDisabledText: { type: String, default: uni.$u.props.calendar.confirmDisabledText }, // 是否显示日历弹窗 show: { type: Boolean, default: uni.$u.props.calendar.show }, // 是否允许点击遮罩关闭日历 closeOnClickOverlay: { type: Boolean, default: uni.$u.props.calendar.closeOnClickOverlay }, // 是否为只读状态,只读状态下禁止选择日期 readonly: { type: Boolean, default: uni.$u.props.calendar.readonly }, // 是否展示确认按钮 showConfirm: { type: Boolean, default: uni.$u.props.calendar.showConfirm }, // 日期区间最多可选天数,默认无限制,mode = range时有效 maxRange: { type: [Number, String], default: uni.$u.props.calendar.maxRange }, // 范围选择超过最多可选天数时的提示文案,mode = range时有效 rangePrompt: { type: String, default: uni.$u.props.calendar.rangePrompt }, // 范围选择超过最多可选天数时,是否展示提示文案,mode = range时有效 showRangePrompt: { type: Boolean, default: uni.$u.props.calendar.showRangePrompt }, // 是否允许日期范围的起止时间为同一天,mode = range时有效 allowSameDay: { type: Boolean, default: uni.$u.props.calendar.allowSameDay }, // 圆角值 round: { type: [Boolean, String, Number], default: uni.$u.props.calendar.round }, // 最多展示月份数量 monthNum: { type: [Number, String], default: 3 } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-calendar/u-calendar.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-calendar/util.js ================================================ export default { methods: { // 设置月份数据 setMonth() { // 月初是周几 const day = dayjs(this.date).date(1).day() const start = day == 0 ? 6 : day - 1 // 本月天数 const days = dayjs(this.date).endOf('month').format('D') // 上个月天数 const prevDays = dayjs(this.date).endOf('month').subtract(1, 'month').format('D') // 日期数据 const arr = [] // 清空表格 this.month = [] // 添加上月数据 arr.push( ...new Array(start).fill(1).map((e, i) => { const day = prevDays - start + i + 1 return { value: day, disabled: true, date: dayjs(this.date).subtract(1, 'month').date(day).format('YYYY-MM-DD') } }) ) // 添加本月数据 arr.push( ...new Array(days - 0).fill(1).map((e, i) => { const day = i + 1 return { value: day, date: dayjs(this.date).date(day).format('YYYY-MM-DD') } }) ) // 添加下个月 arr.push( ...new Array(42 - days - start).fill(1).map((e, i) => { const day = i + 1 return { value: day, disabled: true, date: dayjs(this.date).add(1, 'month').date(day).format('YYYY-MM-DD') } }) ) // 分割数组 for (let n = 0; n < arr.length; n += 7) { this.month.push( arr.slice(n, n + 7).map((e, i) => { e.index = i + n // 自定义信息 const custom = this.customList.find((c) => c.date == e.date) // 农历 if (this.lunar) { const { IDayCn, IMonthCn } = this.getLunar(e.date) e.lunar = IDayCn == '初一' ? IMonthCn : IDayCn } return { ...e, ...custom } }) ) } } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-car-keyboard/props.js ================================================ export default { props: { // 是否打乱键盘按键的顺序 random: { type: Boolean, default: false }, // 输入一个中文后,是否自动切换到英文 autoChange: { type: Boolean, default: false } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-car-keyboard/u-car-keyboard.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-cell/props.js ================================================ export default { props: { // 标题 title: { type: [String, Number], default: uni.$u.props.cell.title }, // 标题下方的描述信息 label: { type: [String, Number], default: uni.$u.props.cell.label }, // 右侧的内容 value: { type: [String, Number], default: uni.$u.props.cell.value }, // 左侧图标名称,或者图片链接(本地文件建议使用绝对地址) icon: { type: String, default: uni.$u.props.cell.icon }, // 是否禁用cell disabled: { type: Boolean, default: uni.$u.props.cell.disabled }, // 是否显示下边框 border: { type: Boolean, default: uni.$u.props.cell.border }, // 内容是否垂直居中(主要是针对右侧的value部分) center: { type: Boolean, default: uni.$u.props.cell.center }, // 点击后跳转的URL地址 url: { type: String, default: uni.$u.props.cell.url }, // 链接跳转的方式,内部使用的是uView封装的route方法,可能会进行拦截操作 linkType: { type: String, default: uni.$u.props.cell.linkType }, // 是否开启点击反馈(表现为点击时加上灰色背景) clickable: { type: Boolean, default: uni.$u.props.cell.clickable }, // 是否展示右侧箭头并开启点击反馈 isLink: { type: Boolean, default: uni.$u.props.cell.isLink }, // 是否显示表单状态下的必填星号(此组件可能会内嵌入input组件) required: { type: Boolean, default: uni.$u.props.cell.required }, // 右侧的图标箭头 rightIcon: { type: String, default: uni.$u.props.cell.rightIcon }, // 右侧箭头的方向,可选值为:left,up,down arrowDirection: { type: String, default: uni.$u.props.cell.arrowDirection }, // 左侧图标样式 iconStyle: { type: [Object, String], default: () => { return uni.$u.props.cell.iconStyle } }, // 右侧箭头图标的样式 rightIconStyle: { type: [Object, String], default: () => { return uni.$u.props.cell.rightIconStyle } }, // 标题的样式 titleStyle: { type: [Object, String], default: () => { return uni.$u.props.cell.titleStyle } }, // 单位元的大小,可选值为large size: { type: String, default: uni.$u.props.cell.size }, // 点击cell是否阻止事件传播 stop: { type: Boolean, default: uni.$u.props.cell.stop }, // 标识符,cell被点击时返回 name: { type: [Number, String], default: uni.$u.props.cell.name } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-cell/u-cell.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-cell-group/props.js ================================================ export default { props: { // 分组标题 title: { type: String, default: uni.$u.props.cellGroup.title }, // 是否显示外边框 border: { type: Boolean, default: uni.$u.props.cellGroup.border } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-cell-group/u-cell-group.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-checkbox/props.js ================================================ export default { props: { // checkbox的名称 name: { type: [String, Number, Boolean], default: uni.$u.props.checkbox.name }, // 形状,square为方形,circle为圆型 shape: { type: String, default: uni.$u.props.checkbox.shape }, // 整体的大小 size: { type: [String, Number], default: uni.$u.props.checkbox.size }, // 是否默认选中 checked: { type: Boolean, default: uni.$u.props.checkbox.checked }, // 是否禁用 disabled: { type: [String, Boolean], default: uni.$u.props.checkbox.disabled }, // 选中状态下的颜色,如设置此值,将会覆盖parent的activeColor值 activeColor: { type: String, default: uni.$u.props.checkbox.activeColor }, // 未选中的颜色 inactiveColor: { type: String, default: uni.$u.props.checkbox.inactiveColor }, // 图标的大小,单位px iconSize: { type: [String, Number], default: uni.$u.props.checkbox.iconSize }, // 图标颜色 iconColor: { type: String, default: uni.$u.props.checkbox.iconColor }, // label提示文字,因为nvue下,直接slot进来的文字,由于特殊的结构,无法修改样式 label: { type: [String, Number], default: uni.$u.props.checkbox.label }, // label的字体大小,px单位 labelSize: { type: [String, Number], default: uni.$u.props.checkbox.labelSize }, // label的颜色 labelColor: { type: String, default: uni.$u.props.checkbox.labelColor }, // 是否禁止点击提示语选中复选框 labelDisabled: { type: [String, Boolean], default: uni.$u.props.checkbox.labelDisabled } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-checkbox/u-checkbox.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-checkbox-group/props.js ================================================ export default { props: { // 标识符 name: { type: String, default: uni.$u.props.checkboxGroup.name }, // 绑定的值 value: { type: Array, default: uni.$u.props.checkboxGroup.value }, // 形状,circle-圆形,square-方形 shape: { type: String, default: uni.$u.props.checkboxGroup.shape }, // 是否禁用全部checkbox disabled: { type: Boolean, default: uni.$u.props.checkboxGroup.disabled }, // 选中状态下的颜色,如设置此值,将会覆盖parent的activeColor值 activeColor: { type: String, default: uni.$u.props.checkboxGroup.activeColor }, // 未选中的颜色 inactiveColor: { type: String, default: uni.$u.props.checkboxGroup.inactiveColor }, // 整个组件的尺寸,默认px size: { type: [String, Number], default: uni.$u.props.checkboxGroup.size }, // 布局方式,row-横向,column-纵向 placement: { type: String, default: uni.$u.props.checkboxGroup.placement }, // label的字体大小,px单位 labelSize: { type: [String, Number], default: uni.$u.props.checkboxGroup.labelSize }, // label的字体颜色 labelColor: { type: [String], default: uni.$u.props.checkboxGroup.labelColor }, // 是否禁止点击文本操作 labelDisabled: { type: Boolean, default: uni.$u.props.checkboxGroup.labelDisabled }, // 图标颜色 iconColor: { type: String, default: uni.$u.props.checkboxGroup.iconColor }, // 图标的大小,单位px iconSize: { type: [String, Number], default: uni.$u.props.checkboxGroup.iconSize }, // 勾选图标的对齐方式,left-左边,right-右边 iconPlacement: { type: String, default: uni.$u.props.checkboxGroup.iconPlacement }, // 竖向配列时,是否显示下划线 borderBottom: { type: Boolean, default: uni.$u.props.checkboxGroup.borderBottom } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-checkbox-group/u-checkbox-group.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-circle-progress/props.js ================================================ export default { props: { percentage: { type: [String, Number], default: uni.$u.props.circleProgress.percentage } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-circle-progress/u-circle-progress.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-code/props.js ================================================ export default { props: { // 倒计时总秒数 seconds: { type: [String, Number], default: uni.$u.props.code.seconds }, // 尚未开始时提示 startText: { type: String, default: uni.$u.props.code.startText }, // 正在倒计时中的提示 changeText: { type: String, default: uni.$u.props.code.changeText }, // 倒计时结束时的提示 endText: { type: String, default: uni.$u.props.code.endText }, // 是否在H5刷新或各端返回再进入时继续倒计时 keepRunning: { type: Boolean, default: uni.$u.props.code.keepRunning }, // 为了区分多个页面,或者一个页面多个倒计时组件本地存储的继续倒计时变了 uniqueKey: { type: String, default: uni.$u.props.code.uniqueKey } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-code/u-code.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-code-input/props.js ================================================ export default { props: { // 键盘弹起时,是否自动上推页面 adjustPosition: { type: Boolean, default: uni.$u.props.codeInput.adjustPosition }, // 最大输入长度 maxlength: { type: [String, Number], default: uni.$u.props.codeInput.maxlength }, // 是否用圆点填充 dot: { type: Boolean, default: uni.$u.props.codeInput.dot }, // 显示模式,box-盒子模式,line-底部横线模式 mode: { type: String, default: uni.$u.props.codeInput.mode }, // 是否细边框 hairline: { type: Boolean, default: uni.$u.props.codeInput.hairline }, // 字符间的距离 space: { type: [String, Number], default: uni.$u.props.codeInput.space }, // 预置值 value: { type: [String, Number], default: uni.$u.props.codeInput.value }, // 是否自动获取焦点 focus: { type: Boolean, default: uni.$u.props.codeInput.focus }, // 字体是否加粗 bold: { type: Boolean, default: uni.$u.props.codeInput.bold }, // 字体颜色 color: { type: String, default: uni.$u.props.codeInput.color }, // 字体大小 fontSize: { type: [String, Number], default: uni.$u.props.codeInput.fontSize }, // 输入框的大小,宽等于高 size: { type: [String, Number], default: uni.$u.props.codeInput.size }, // 是否隐藏原生键盘,如果想用自定义键盘的话,需设置此参数为true disabledKeyboard: { type: Boolean, default: uni.$u.props.codeInput.disabledKeyboard }, // 边框和线条颜色 borderColor: { type: String, default: uni.$u.props.codeInput.borderColor }, // 是否禁止输入"."符号 disabledDot: { type: Boolean, default: uni.$u.props.codeInput.disabledDot } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-code-input/u-code-input.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-col/props.js ================================================ export default { props: { // 占父容器宽度的多少等分,总分为12份 span: { type: [String, Number], default: uni.$u.props.col.span }, // 指定栅格左侧的间隔数(总12栏) offset: { type: [String, Number], default: uni.$u.props.col.offset }, // 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`) justify: { type: String, default: uni.$u.props.col.justify }, // 垂直对齐方式,可选值为top、center、bottom、stretch align: { type: String, default: uni.$u.props.col.align }, // 文字对齐方式 textAlign: { type: String, default: uni.$u.props.col.textAlign } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-col/u-col.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-collapse/props.js ================================================ export default { props: { // 当前展开面板的name,非手风琴模式:[],手风琴模式:string | number value: { type: [String, Number, Array, null], default: uni.$u.props.collapse.value }, // 是否手风琴模式 accordion: { type: Boolean, default: uni.$u.props.collapse.accordion }, // 是否显示外边框 border: { type: Boolean, default: uni.$u.props.collapse.border } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-collapse/u-collapse.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-collapse-item/props.js ================================================ export default { props: { // 标题 title: { type: String, default: uni.$u.props.collapseItem.title }, // 标题右侧内容 value: { type: String, default: uni.$u.props.collapseItem.value }, // 标题下方的描述信息 label: { type: String, default: uni.$u.props.collapseItem.label }, // 是否禁用折叠面板 disabled: { type: Boolean, default: uni.$u.props.collapseItem.disabled }, // 是否展示右侧箭头并开启点击反馈 isLink: { type: Boolean, default: uni.$u.props.collapseItem.isLink }, // 是否开启点击反馈 clickable: { type: Boolean, default: uni.$u.props.collapseItem.clickable }, // 是否显示内边框 border: { type: Boolean, default: uni.$u.props.collapseItem.border }, // 标题的对齐方式 align: { type: String, default: uni.$u.props.collapseItem.align }, // 唯一标识符 name: { type: [String, Number], default: uni.$u.props.collapseItem.name }, // 标题左侧图片,可为绝对路径的图片或内置图标 icon: { type: String, default: uni.$u.props.collapseItem.icon }, // 面板展开收起的过渡时间,单位ms duration: { type: Number, default: uni.$u.props.collapseItem.duration } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-collapse-item/u-collapse-item.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-column-notice/props.js ================================================ export default { props: { // 显示的内容,字符串 text: { type: [Array], default: uni.$u.props.columnNotice.text }, // 是否显示左侧的音量图标 icon: { type: String, default: uni.$u.props.columnNotice.icon }, // 通告模式,link-显示右箭头,closable-显示右侧关闭图标 mode: { type: String, default: uni.$u.props.columnNotice.mode }, // 文字颜色,各图标也会使用文字颜色 color: { type: String, default: uni.$u.props.columnNotice.color }, // 背景颜色 bgColor: { type: String, default: uni.$u.props.columnNotice.bgColor }, // 字体大小,单位px fontSize: { type: [String, Number], default: uni.$u.props.columnNotice.fontSize }, // 水平滚动时的滚动速度,即每秒滚动多少px(px),这有利于控制文字无论多少时,都能有一个恒定的速度 speed: { type: [String, Number], default: uni.$u.props.columnNotice.speed }, // direction = row时,是否使用步进形式滚动 step: { type: Boolean, default: uni.$u.props.columnNotice.step }, // 滚动一个周期的时间长,单位ms duration: { type: [String, Number], default: uni.$u.props.columnNotice.duration }, // 是否禁止用手滑动切换 // 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序 disableTouch: { type: Boolean, default: uni.$u.props.columnNotice.disableTouch } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-column-notice/u-column-notice.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-count-down/props.js ================================================ export default { props: { // 倒计时时长,单位ms time: { type: [String, Number], default: uni.$u.props.countDown.time }, // 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒 format: { type: String, default: uni.$u.props.countDown.format }, // 是否自动开始倒计时 autoStart: { type: Boolean, default: uni.$u.props.countDown.autoStart }, // 是否展示毫秒倒计时 millisecond: { type: Boolean, default: uni.$u.props.countDown.millisecond } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-count-down/u-count-down.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-count-down/utils.js ================================================ // 补0,如1 -> 01 function padZero(num, targetLength = 2) { let str = `${num}` while (str.length < targetLength) { str = `0${str}` } return str } const SECOND = 1000 const MINUTE = 60 * SECOND const HOUR = 60 * MINUTE const DAY = 24 * HOUR export function parseTimeData(time) { const days = Math.floor(time / DAY) const hours = Math.floor((time % DAY) / HOUR) const minutes = Math.floor((time % HOUR) / MINUTE) const seconds = Math.floor((time % MINUTE) / SECOND) const milliseconds = Math.floor(time % SECOND) return { days, hours, minutes, seconds, milliseconds } } export function parseFormat(format, timeData) { let { days, hours, minutes, seconds, milliseconds } = timeData // 如果格式化字符串中不存在DD(天),则将天的时间转为小时中去 if (format.indexOf('DD') === -1) { hours += days * 24 } else { // 对天补0 format = format.replace('DD', padZero(days)) } // 其他同理于DD的格式化处理方式 if (format.indexOf('HH') === -1) { minutes += hours * 60 } else { format = format.replace('HH', padZero(hours)) } if (format.indexOf('mm') === -1) { seconds += minutes * 60 } else { format = format.replace('mm', padZero(minutes)) } if (format.indexOf('ss') === -1) { milliseconds += seconds * 1000 } else { format = format.replace('ss', padZero(seconds)) } return format.replace('SSS', padZero(milliseconds, 3)) } export function isSameSecond(time1, time2) { return Math.floor(time1 / 1000) === Math.floor(time2 / 1000) } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-count-to/props.js ================================================ export default { props: { // 开始的数值,默认从0增长到某一个数 startVal: { type: [String, Number], default: uni.$u.props.countTo.startVal }, // 要滚动的目标数值,必须 endVal: { type: [String, Number], default: uni.$u.props.countTo.endVal }, // 滚动到目标数值的动画持续时间,单位为毫秒(ms) duration: { type: [String, Number], default: uni.$u.props.countTo.duration }, // 设置数值后是否自动开始滚动 autoplay: { type: Boolean, default: uni.$u.props.countTo.autoplay }, // 要显示的小数位数 decimals: { type: [String, Number], default: uni.$u.props.countTo.decimals }, // 是否在即将到达目标数值的时候,使用缓慢滚动的效果 useEasing: { type: Boolean, default: uni.$u.props.countTo.useEasing }, // 十进制分割 decimal: { type: [String, Number], default: uni.$u.props.countTo.decimal }, // 字体颜色 color: { type: String, default: uni.$u.props.countTo.color }, // 字体大小 fontSize: { type: [String, Number], default: uni.$u.props.countTo.fontSize }, // 是否加粗字体 bold: { type: Boolean, default: uni.$u.props.countTo.bold }, // 千位分隔符,类似金额的分割(¥23,321.05中的",") separator: { type: String, default: uni.$u.props.countTo.separator } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-count-to/u-count-to.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-datetime-picker/props.js ================================================ export default { props: { // 是否打开组件 show: { type: Boolean, default: uni.$u.props.datetimePicker.show }, // 是否展示顶部的操作栏 showToolbar: { type: Boolean, default: uni.$u.props.datetimePicker.showToolbar }, // 绑定值 value: { type: [String, Number], default: uni.$u.props.datetimePicker.value }, // 顶部标题 title: { type: String, default: uni.$u.props.datetimePicker.title }, // 展示格式,mode=date为日期选择,mode=time为时间选择,mode=year-month为年月选择,mode=datetime为日期时间选择 mode: { type: String, default: uni.$u.props.datetimePicker.mode }, // 可选的最大时间 maxDate: { type: Number, // 最大默认值为后10年 default: uni.$u.props.datetimePicker.maxDate }, // 可选的最小时间 minDate: { type: Number, // 最小默认值为前10年 default: uni.$u.props.datetimePicker.minDate }, // 可选的最小小时,仅mode=time有效 minHour: { type: Number, default: uni.$u.props.datetimePicker.minHour }, // 可选的最大小时,仅mode=time有效 maxHour: { type: Number, default: uni.$u.props.datetimePicker.maxHour }, // 可选的最小分钟,仅mode=time有效 minMinute: { type: Number, default: uni.$u.props.datetimePicker.minMinute }, // 可选的最大分钟,仅mode=time有效 maxMinute: { type: Number, default: uni.$u.props.datetimePicker.maxMinute }, // 选项过滤函数 filter: { type: [Function, null], default: uni.$u.props.datetimePicker.filter }, // 选项格式化函数 formatter: { type: [Function, null], default: uni.$u.props.datetimePicker.formatter }, // 是否显示加载中状态 loading: { type: Boolean, default: uni.$u.props.datetimePicker.loading }, // 各列中,单个选项的高度 itemHeight: { type: [String, Number], default: uni.$u.props.datetimePicker.itemHeight }, // 取消按钮的文字 cancelText: { type: String, default: uni.$u.props.datetimePicker.cancelText }, // 确认按钮的文字 confirmText: { type: String, default: uni.$u.props.datetimePicker.confirmText }, // 取消按钮的颜色 cancelColor: { type: String, default: uni.$u.props.datetimePicker.cancelColor }, // 确认按钮的颜色 confirmColor: { type: String, default: uni.$u.props.datetimePicker.confirmColor }, // 每列中可见选项的数量 visibleItemCount: { type: [String, Number], default: uni.$u.props.datetimePicker.visibleItemCount }, // 是否允许点击遮罩关闭选择器 closeOnClickOverlay: { type: Boolean, default: uni.$u.props.datetimePicker.closeOnClickOverlay }, // 各列的默认索引 defaultIndex: { type: Array, default: uni.$u.props.datetimePicker.defaultIndex } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-datetime-picker/u-datetime-picker.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-divider/props.js ================================================ export default { props: { // 是否虚线 dashed: { type: Boolean, default: uni.$u.props.divider.dashed }, // 是否细线 hairline: { type: Boolean, default: uni.$u.props.divider.hairline }, // 是否以点替代文字,优先于text字段起作用 dot: { type: Boolean, default: uni.$u.props.divider.dot }, // 内容文本的位置,left-左边,center-中间,right-右边 textPosition: { type: String, default: uni.$u.props.divider.textPosition }, // 文本内容 text: { type: [String, Number], default: uni.$u.props.divider.text }, // 文本大小 textSize: { type: [String, Number], default: uni.$u.props.divider.textSize }, // 文本颜色 textColor: { type: String, default: uni.$u.props.divider.textColor }, // 线条颜色 lineColor: { type: String, default: uni.$u.props.divider.lineColor } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-divider/u-divider.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-dropdown/props.js ================================================ export default { props: { // 标题选中时的样式 activeStyle: { type: [String, Object], default: () => ({ color: '#2979ff', fontSize: '14px' }) }, // 标题未选中时的样式 inactiveStyle: { type: [String, Object], default: () => ({ color: '#606266', fontSize: '14px' }) }, // 点击遮罩是否关闭菜单 closeOnClickMask: { type: Boolean, default: true }, // 点击当前激活项标题是否关闭菜单 closeOnClickSelf: { type: Boolean, default: true }, // 过渡时间 duration: { type: [Number, String], default: 300 }, // 标题菜单的高度 height: { type: [Number, String], default: 40 }, // 是否显示下边框 borderBottom: { type: Boolean, default: false }, // 标题的字体大小 titleSize: { type: [Number, String], default: 14 }, // 下拉出来的内容部分的圆角值 borderRadius: { type: [Number, String], default: 0 }, // 菜单右侧的icon图标 menuIcon: { type: String, default: 'arrow-down' }, // 菜单右侧图标的大小 menuIconSize: { type: [Number, String], default: 14 } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-dropdown/u-dropdown.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-dropdown-item/props.js ================================================ export default { props: { // 当前选中项的value值 value: { type: [Number, String, Array], default: '' }, // 菜单项标题 title: { type: [String, Number], default: '' }, // 选项数据,如果传入了默认slot,此参数无效 options: { type: Array, default() { return [] } }, // 是否禁用此菜单项 disabled: { type: Boolean, default: false }, // 下拉弹窗的高度 height: { type: [Number, String], default: 'auto' }, // 点击遮罩是否可以收起弹窗 closeOnClickOverlay: { type: Boolean, default: true } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-dropdown-item/u-dropdown-item.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-empty/props.js ================================================ export default { props: { // 内置图标名称,或图片路径,建议绝对路径 icon: { type: String, default: uni.$u.props.empty.icon }, // 提示文字 text: { type: String, default: uni.$u.props.empty.text }, // 文字颜色 textColor: { type: String, default: uni.$u.props.empty.textColor }, // 文字大小 textSize: { type: [String, Number], default: uni.$u.props.empty.textSize }, // 图标的颜色 iconColor: { type: String, default: uni.$u.props.empty.iconColor }, // 图标的大小 iconSize: { type: [String, Number], default: uni.$u.props.empty.iconSize }, // 选择预置的图标类型 mode: { type: String, default: uni.$u.props.empty.mode }, // 图标宽度,单位px width: { type: [String, Number], default: uni.$u.props.empty.width }, // 图标高度,单位px height: { type: [String, Number], default: uni.$u.props.empty.height }, // 是否显示组件 show: { type: Boolean, default: uni.$u.props.empty.show }, // 组件距离上一个元素之间的距离,默认px单位 marginTop: { type: [String, Number], default: uni.$u.props.empty.marginTop } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-empty/u-empty.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-form/props.js ================================================ export default { props: { // 当前form的需要验证字段的集合 model: { type: Object, default: uni.$u.props.form.model }, // 验证规则 rules: { type: [Object, Function, Array], default: uni.$u.props.form.rules }, // 有错误时的提示方式,message-提示信息,toast-进行toast提示 // border-bottom-下边框呈现红色,none-无提示 errorType: { type: String, default: uni.$u.props.form.errorType }, // 是否显示表单域的下划线边框 borderBottom: { type: Boolean, default: uni.$u.props.form.borderBottom }, // label的位置,left-左边,top-上边 labelPosition: { type: String, default: uni.$u.props.form.labelPosition }, // label的宽度,单位px labelWidth: { type: [String, Number], default: uni.$u.props.form.labelWidth }, // lable字体的对齐方式 labelAlign: { type: String, default: uni.$u.props.form.labelAlign }, // lable的样式,对象形式 labelStyle: { type: Object, default: uni.$u.props.form.labelStyle } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-form/u-form.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-form-item/props.js ================================================ export default { props: { // input的label提示语 label: { type: String, default: uni.$u.props.formItem.label }, // 绑定的值 prop: { type: String, default: uni.$u.props.formItem.prop }, // 是否显示表单域的下划线边框 borderBottom: { type: [String, Boolean], default: uni.$u.props.formItem.borderBottom }, // label的位置,left-左边,top-上边 labelPosition: { type: String, default: uni.$u.props.formItem.labelPosition }, // label的宽度,单位px labelWidth: { type: [String, Number], default: uni.$u.props.formItem.labelWidth }, // 右侧图标 rightIcon: { type: String, default: uni.$u.props.formItem.rightIcon }, // 左侧图标 leftIcon: { type: String, default: uni.$u.props.formItem.leftIcon }, // 是否显示左边的必填星号,只作显示用,具体校验必填的逻辑,请在rules中配置 required: { type: Boolean, default: uni.$u.props.formItem.required }, leftIconStyle: { type: [String, Object], default: uni.$u.props.formItem.leftIconStyle, } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-form-item/u-form-item.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-gap/props.js ================================================ export default { props: { // 背景颜色(默认transparent) bgColor: { type: String, default: uni.$u.props.gap.bgColor }, // 分割槽高度,单位px(默认30) height: { type: [String, Number], default: uni.$u.props.gap.height }, // 与上一个组件的距离 marginTop: { type: [String, Number], default: uni.$u.props.gap.marginTop }, // 与下一个组件的距离 marginBottom: { type: [String, Number], default: uni.$u.props.gap.marginBottom } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-gap/u-gap.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-grid/props.js ================================================ export default { props: { // 分成几列 col: { type: [String, Number], default: uni.$u.props.grid.col }, // 是否显示边框 border: { type: Boolean, default: uni.$u.props.grid.border }, // 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右 align: { type: String, default: uni.$u.props.grid.align } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-grid/u-grid.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-grid-item/props.js ================================================ export default { props: { // 宫格的name name: { type: [String, Number, null], default: uni.$u.props.gridItem.name }, // 背景颜色 bgColor: { type: String, default: uni.$u.props.gridItem.bgColor } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-grid-item/u-grid-item.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-icon/icons.js ================================================ export default { 'uicon-level': '\ue693', 'uicon-column-line': '\ue68e', 'uicon-checkbox-mark': '\ue807', 'uicon-folder': '\ue7f5', 'uicon-movie': '\ue7f6', 'uicon-star-fill': '\ue669', 'uicon-star': '\ue65f', 'uicon-phone-fill': '\ue64f', 'uicon-phone': '\ue622', 'uicon-apple-fill': '\ue881', 'uicon-chrome-circle-fill': '\ue885', 'uicon-backspace': '\ue67b', 'uicon-attach': '\ue632', 'uicon-cut': '\ue948', 'uicon-empty-car': '\ue602', 'uicon-empty-coupon': '\ue682', 'uicon-empty-address': '\ue646', 'uicon-empty-favor': '\ue67c', 'uicon-empty-permission': '\ue686', 'uicon-empty-news': '\ue687', 'uicon-empty-search': '\ue664', 'uicon-github-circle-fill': '\ue887', 'uicon-rmb': '\ue608', 'uicon-person-delete-fill': '\ue66a', 'uicon-reload': '\ue788', 'uicon-order': '\ue68f', 'uicon-server-man': '\ue6bc', 'uicon-search': '\ue62a', 'uicon-fingerprint': '\ue955', 'uicon-more-dot-fill': '\ue630', 'uicon-scan': '\ue662', 'uicon-share-square': '\ue60b', 'uicon-map': '\ue61d', 'uicon-map-fill': '\ue64e', 'uicon-tags': '\ue629', 'uicon-tags-fill': '\ue651', 'uicon-bookmark-fill': '\ue63b', 'uicon-bookmark': '\ue60a', 'uicon-eye': '\ue613', 'uicon-eye-fill': '\ue641', 'uicon-mic': '\ue64a', 'uicon-mic-off': '\ue649', 'uicon-calendar': '\ue66e', 'uicon-calendar-fill': '\ue634', 'uicon-trash': '\ue623', 'uicon-trash-fill': '\ue658', 'uicon-play-left': '\ue66d', 'uicon-play-right': '\ue610', 'uicon-minus': '\ue618', 'uicon-plus': '\ue62d', 'uicon-info': '\ue653', 'uicon-info-circle': '\ue7d2', 'uicon-info-circle-fill': '\ue64b', 'uicon-question': '\ue715', 'uicon-error': '\ue6d3', 'uicon-close': '\ue685', 'uicon-checkmark': '\ue6a8', 'uicon-android-circle-fill': '\ue67e', 'uicon-android-fill': '\ue67d', 'uicon-ie': '\ue87b', 'uicon-IE-circle-fill': '\ue889', 'uicon-google': '\ue87a', 'uicon-google-circle-fill': '\ue88a', 'uicon-setting-fill': '\ue872', 'uicon-setting': '\ue61f', 'uicon-minus-square-fill': '\ue855', 'uicon-plus-square-fill': '\ue856', 'uicon-heart': '\ue7df', 'uicon-heart-fill': '\ue851', 'uicon-camera': '\ue7d7', 'uicon-camera-fill': '\ue870', 'uicon-more-circle': '\ue63e', 'uicon-more-circle-fill': '\ue645', 'uicon-chat': '\ue620', 'uicon-chat-fill': '\ue61e', 'uicon-bag-fill': '\ue617', 'uicon-bag': '\ue619', 'uicon-error-circle-fill': '\ue62c', 'uicon-error-circle': '\ue624', 'uicon-close-circle': '\ue63f', 'uicon-close-circle-fill': '\ue637', 'uicon-checkmark-circle': '\ue63d', 'uicon-checkmark-circle-fill': '\ue635', 'uicon-question-circle-fill': '\ue666', 'uicon-question-circle': '\ue625', 'uicon-share': '\ue631', 'uicon-share-fill': '\ue65e', 'uicon-shopping-cart': '\ue621', 'uicon-shopping-cart-fill': '\ue65d', 'uicon-bell': '\ue609', 'uicon-bell-fill': '\ue640', 'uicon-list': '\ue650', 'uicon-list-dot': '\ue616', 'uicon-zhihu': '\ue6ba', 'uicon-zhihu-circle-fill': '\ue709', 'uicon-zhifubao': '\ue6b9', 'uicon-zhifubao-circle-fill': '\ue6b8', 'uicon-weixin-circle-fill': '\ue6b1', 'uicon-weixin-fill': '\ue6b2', 'uicon-twitter-circle-fill': '\ue6ab', 'uicon-twitter': '\ue6aa', 'uicon-taobao-circle-fill': '\ue6a7', 'uicon-taobao': '\ue6a6', 'uicon-weibo-circle-fill': '\ue6a5', 'uicon-weibo': '\ue6a4', 'uicon-qq-fill': '\ue6a1', 'uicon-qq-circle-fill': '\ue6a0', 'uicon-moments-circel-fill': '\ue69a', 'uicon-moments': '\ue69b', 'uicon-qzone': '\ue695', 'uicon-qzone-circle-fill': '\ue696', 'uicon-baidu-circle-fill': '\ue680', 'uicon-baidu': '\ue681', 'uicon-facebook-circle-fill': '\ue68a', 'uicon-facebook': '\ue689', 'uicon-car': '\ue60c', 'uicon-car-fill': '\ue636', 'uicon-warning-fill': '\ue64d', 'uicon-warning': '\ue694', 'uicon-clock-fill': '\ue638', 'uicon-clock': '\ue60f', 'uicon-edit-pen': '\ue612', 'uicon-edit-pen-fill': '\ue66b', 'uicon-email': '\ue611', 'uicon-email-fill': '\ue642', 'uicon-minus-circle': '\ue61b', 'uicon-minus-circle-fill': '\ue652', 'uicon-plus-circle': '\ue62e', 'uicon-plus-circle-fill': '\ue661', 'uicon-file-text': '\ue663', 'uicon-file-text-fill': '\ue665', 'uicon-pushpin': '\ue7e3', 'uicon-pushpin-fill': '\ue86e', 'uicon-grid': '\ue673', 'uicon-grid-fill': '\ue678', 'uicon-play-circle': '\ue647', 'uicon-play-circle-fill': '\ue655', 'uicon-pause-circle-fill': '\ue654', 'uicon-pause': '\ue8fa', 'uicon-pause-circle': '\ue643', 'uicon-eye-off': '\ue648', 'uicon-eye-off-outline': '\ue62b', 'uicon-gift-fill': '\ue65c', 'uicon-gift': '\ue65b', 'uicon-rmb-circle-fill': '\ue657', 'uicon-rmb-circle': '\ue677', 'uicon-kefu-ermai': '\ue656', 'uicon-server-fill': '\ue751', 'uicon-coupon-fill': '\ue8c4', 'uicon-coupon': '\ue8ae', 'uicon-integral': '\ue704', 'uicon-integral-fill': '\ue703', 'uicon-home-fill': '\ue964', 'uicon-home': '\ue965', 'uicon-hourglass-half-fill': '\ue966', 'uicon-hourglass': '\ue967', 'uicon-account': '\ue628', 'uicon-plus-people-fill': '\ue626', 'uicon-minus-people-fill': '\ue615', 'uicon-account-fill': '\ue614', 'uicon-thumb-down-fill': '\ue726', 'uicon-thumb-down': '\ue727', 'uicon-thumb-up': '\ue733', 'uicon-thumb-up-fill': '\ue72f', 'uicon-lock-fill': '\ue979', 'uicon-lock-open': '\ue973', 'uicon-lock-opened-fill': '\ue974', 'uicon-lock': '\ue97a', 'uicon-red-packet-fill': '\ue690', 'uicon-photo-fill': '\ue98b', 'uicon-photo': '\ue98d', 'uicon-volume-off-fill': '\ue659', 'uicon-volume-off': '\ue644', 'uicon-volume-fill': '\ue670', 'uicon-volume': '\ue633', 'uicon-red-packet': '\ue691', 'uicon-download': '\ue63c', 'uicon-arrow-up-fill': '\ue6b0', 'uicon-arrow-down-fill': '\ue600', 'uicon-play-left-fill': '\ue675', 'uicon-play-right-fill': '\ue676', 'uicon-rewind-left-fill': '\ue679', 'uicon-rewind-right-fill': '\ue67a', 'uicon-arrow-downward': '\ue604', 'uicon-arrow-leftward': '\ue601', 'uicon-arrow-rightward': '\ue603', 'uicon-arrow-upward': '\ue607', 'uicon-arrow-down': '\ue60d', 'uicon-arrow-right': '\ue605', 'uicon-arrow-left': '\ue60e', 'uicon-arrow-up': '\ue606', 'uicon-skip-back-left': '\ue674', 'uicon-skip-forward-right': '\ue672', 'uicon-rewind-right': '\ue66f', 'uicon-rewind-left': '\ue671', 'uicon-arrow-right-double': '\ue68d', 'uicon-arrow-left-double': '\ue68c', 'uicon-wifi-off': '\ue668', 'uicon-wifi': '\ue667', 'uicon-empty-data': '\ue62f', 'uicon-empty-history': '\ue684', 'uicon-empty-list': '\ue68b', 'uicon-empty-page': '\ue627', 'uicon-empty-order': '\ue639', 'uicon-man': '\ue697', 'uicon-woman': '\ue69c', 'uicon-man-add': '\ue61c', 'uicon-man-add-fill': '\ue64c', 'uicon-man-delete': '\ue61a', 'uicon-man-delete-fill': '\ue66a', 'uicon-zh': '\ue70a', 'uicon-en': '\ue692' } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-icon/props.js ================================================ export default { props: { // 图标类名 name: { type: String, default: uni.$u.props.icon.name }, // 图标颜色,可接受主题色 color: { type: String, default: uni.$u.props.icon.color }, // 字体大小,单位px size: { type: [String, Number], default: uni.$u.props.icon.size }, // 是否显示粗体 bold: { type: Boolean, default: uni.$u.props.icon.bold }, // 点击图标的时候传递事件出去的index(用于区分点击了哪一个) index: { type: [String, Number], default: uni.$u.props.icon.index }, // 触摸图标时的类名 hoverClass: { type: String, default: uni.$u.props.icon.hoverClass }, // 自定义扩展前缀,方便用户扩展自己的图标库 customPrefix: { type: String, default: uni.$u.props.icon.customPrefix }, // 图标右边或者下面的文字 label: { type: [String, Number], default: uni.$u.props.icon.label }, // label的位置,只能右边或者下边 labelPos: { type: String, default: uni.$u.props.icon.labelPos }, // label的大小 labelSize: { type: [String, Number], default: uni.$u.props.icon.labelSize }, // label的颜色 labelColor: { type: String, default: uni.$u.props.icon.labelColor }, // label与图标的距离 space: { type: [String, Number], default: uni.$u.props.icon.space }, // 图片的mode imgMode: { type: String, default: uni.$u.props.icon.imgMode }, // 用于显示图片小图标时,图片的宽度 width: { type: [String, Number], default: uni.$u.props.icon.width }, // 用于显示图片小图标时,图片的高度 height: { type: [String, Number], default: uni.$u.props.icon.height }, // 用于解决某些情况下,让图标垂直居中的用途 top: { type: [String, Number], default: uni.$u.props.icon.top }, // 是否阻止事件传播 stop: { type: Boolean, default: uni.$u.props.icon.stop } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-icon/u-icon.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-image/props.js ================================================ export default { props: { // 图片地址 src: { type: String, default: uni.$u.props.image.src }, // 裁剪模式 mode: { type: String, default: uni.$u.props.image.mode }, // 宽度,单位任意 width: { type: [String, Number], default: uni.$u.props.image.width }, // 高度,单位任意 height: { type: [String, Number], default: uni.$u.props.image.height }, // 图片形状,circle-圆形,square-方形 shape: { type: String, default: uni.$u.props.image.shape }, // 圆角,单位任意 radius: { type: [String, Number], default: uni.$u.props.image.radius }, // 是否懒加载,微信小程序、App、百度小程序、字节跳动小程序 lazyLoad: { type: Boolean, default: uni.$u.props.image.lazyLoad }, // 开启长按图片显示识别微信小程序码菜单 showMenuByLongpress: { type: Boolean, default: uni.$u.props.image.showMenuByLongpress }, // 加载中的图标,或者小图片 loadingIcon: { type: String, default: uni.$u.props.image.loadingIcon }, // 加载失败的图标,或者小图片 errorIcon: { type: String, default: uni.$u.props.image.errorIcon }, // 是否显示加载中的图标或者自定义的slot showLoading: { type: Boolean, default: uni.$u.props.image.showLoading }, // 是否显示加载错误的图标或者自定义的slot showError: { type: Boolean, default: uni.$u.props.image.showError }, // 是否需要淡入效果 fade: { type: Boolean, default: uni.$u.props.image.fade }, // 只支持网络资源,只对微信小程序有效 webp: { type: Boolean, default: uni.$u.props.image.webp }, // 过渡时间,单位ms duration: { type: [String, Number], default: uni.$u.props.image.duration }, // 背景颜色,用于深色页面加载图片时,为了和背景色融合 bgColor: { type: String, default: uni.$u.props.image.bgColor } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-image/u-image.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-index-anchor/props.js ================================================ export default { props: { // 列表锚点文本内容 text: { type: [String, Number], default: uni.$u.props.indexAnchor.text }, // 列表锚点文字颜色 color: { type: String, default: uni.$u.props.indexAnchor.color }, // 列表锚点文字大小,单位默认px size: { type: [String, Number], default: uni.$u.props.indexAnchor.size }, // 列表锚点背景颜色 bgColor: { type: String, default: uni.$u.props.indexAnchor.bgColor }, // 列表锚点高度,单位默认px height: { type: [String, Number], default: uni.$u.props.indexAnchor.height } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-index-anchor/u-index-anchor.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-index-item/props.js ================================================ export default { props: { } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-index-item/u-index-item.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-index-list/props.js ================================================ export default { props: { // 右边锚点非激活的颜色 inactiveColor: { type: String, default: uni.$u.props.indexList.inactiveColor }, // 右边锚点激活的颜色 activeColor: { type: String, default: uni.$u.props.indexList.activeColor }, // 索引字符列表,数组形式 indexList: { type: Array, default: uni.$u.props.indexList.indexList }, // 是否开启锚点自动吸顶 sticky: { type: Boolean, default: uni.$u.props.indexList.sticky }, // 自定义导航栏的高度 customNavHeight: { type: [String, Number], default: uni.$u.props.indexList.customNavHeight } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-index-list/u-index-list.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-input/props.js ================================================ export default { props: { // 输入的值 value: { type: [String, Number], default: uni.$u.props.input.value }, // 输入框类型 // number-数字输入键盘,app-vue下可以输入浮点数,app-nvue和小程序平台下只能输入整数 // idcard-身份证输入键盘,微信、支付宝、百度、QQ小程序 // digit-带小数点的数字键盘,App的nvue页面、微信、支付宝、百度、头条、QQ小程序 // text-文本输入键盘 type: { type: String, default: uni.$u.props.input.type }, // 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true, // 兼容性:微信小程序、百度小程序、字节跳动小程序、QQ小程序 fixed: { type: Boolean, default: uni.$u.props.input.fixed }, // 是否禁用输入框 disabled: { type: Boolean, default: uni.$u.props.input.disabled }, // 禁用状态时的背景色 disabledColor: { type: String, default: uni.$u.props.input.disabledColor }, // 是否显示清除控件 clearable: { type: Boolean, default: uni.$u.props.input.clearable }, // 是否密码类型 password: { type: Boolean, default: uni.$u.props.input.password }, // 最大输入长度,设置为 -1 的时候不限制最大长度 maxlength: { type: [String, Number], default: uni.$u.props.input.maxlength }, // 输入框为空时的占位符 placeholder: { type: String, default: uni.$u.props.input.placeholder }, // 指定placeholder的样式类,注意页面或组件的style中写了scoped时,需要在类名前写/deep/ placeholderClass: { type: String, default: uni.$u.props.input.placeholderClass }, // 指定placeholder的样式 placeholderStyle: { type: [String, Object], default: uni.$u.props.input.placeholderStyle }, // 是否显示输入字数统计,只在 type ="text"或type ="textarea"时有效 showWordLimit: { type: Boolean, default: uni.$u.props.input.showWordLimit }, // 设置右下角按钮的文字,有效值:send|search|next|go|done,兼容性详见uni-app文档 // https://uniapp.dcloud.io/component/input // https://uniapp.dcloud.io/component/textarea confirmType: { type: String, default: uni.$u.props.input.confirmType }, // 点击键盘右下角按钮时是否保持键盘不收起,H5无效 confirmHold: { type: Boolean, default: uni.$u.props.input.confirmHold }, // focus时,点击页面的时候不收起键盘,微信小程序有效 holdKeyboard: { type: Boolean, default: uni.$u.props.input.holdKeyboard }, // 自动获取焦点 // 在 H5 平台能否聚焦以及软键盘是否跟随弹出,取决于当前浏览器本身的实现。nvue 页面不支持,需使用组件的 focus()、blur() 方法控制焦点 focus: { type: Boolean, default: uni.$u.props.input.focus }, // 键盘收起时,是否自动失去焦点,目前仅App3.0.0+有效 autoBlur: { type: Boolean, default: uni.$u.props.input.autoBlur }, // 是否去掉 iOS 下的默认内边距,仅微信小程序,且type=textarea时有效 disableDefaultPadding: { type: Boolean, default: uni.$u.props.input.disableDefaultPadding }, // 指定focus时光标的位置 cursor: { type: [String, Number], default: uni.$u.props.input.cursor }, // 输入框聚焦时底部与键盘的距离 cursorSpacing: { type: [String, Number], default: uni.$u.props.input.cursorSpacing }, // 光标起始位置,自动聚集时有效,需与selection-end搭配使用 selectionStart: { type: [String, Number], default: uni.$u.props.input.selectionStart }, // 光标结束位置,自动聚集时有效,需与selection-start搭配使用 selectionEnd: { type: [String, Number], default: uni.$u.props.input.selectionEnd }, // 键盘弹起时,是否自动上推页面 adjustPosition: { type: Boolean, default: uni.$u.props.input.adjustPosition }, // 输入框内容对齐方式,可选值为:left|center|right inputAlign: { type: String, default: uni.$u.props.input.inputAlign }, // 输入框字体的大小 fontSize: { type: [String, Number], default: uni.$u.props.input.fontSize }, // 输入框字体颜色 color: { type: String, default: uni.$u.props.input.color }, // 输入框前置图标 prefixIcon: { type: String, default: uni.$u.props.input.prefixIcon }, // 前置图标样式,对象或字符串 prefixIconStyle: { type: [String, Object], default: uni.$u.props.input.prefixIconStyle }, // 输入框后置图标 suffixIcon: { type: String, default: uni.$u.props.input.suffixIcon }, // 后置图标样式,对象或字符串 suffixIconStyle: { type: [String, Object], default: uni.$u.props.input.suffixIconStyle }, // 边框类型,surround-四周边框,bottom-底部边框,none-无边框 border: { type: String, default: uni.$u.props.input.border }, // 是否只读,与disabled不同之处在于disabled会置灰组件,而readonly则不会 readonly: { type: Boolean, default: uni.$u.props.input.readonly }, // 输入框形状,circle-圆形,square-方形 shape: { type: String, default: uni.$u.props.input.shape }, // 用于处理或者过滤输入框内容的方法 formatter: { type: [Function, null], default: uni.$u.props.input.formatter }, // 是否忽略组件内对文本合成系统事件的处理 ignoreCompositionEvent: { type: Boolean, default: true } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-input/u-input.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-keyboard/props.js ================================================ export default { props: { // 键盘的类型,number-数字键盘,card-身份证键盘,car-车牌号键盘 mode: { type: String, default: uni.$u.props.keyboard.mode }, // 是否显示键盘的"."符号 dotDisabled: { type: Boolean, default: uni.$u.props.keyboard.dotDisabled }, // 是否显示顶部工具条 tooltip: { type: Boolean, default: uni.$u.props.keyboard.tooltip }, // 是否显示工具条中间的提示 showTips: { type: Boolean, default: uni.$u.props.keyboard.showTips }, // 工具条中间的提示文字 tips: { type: String, default: uni.$u.props.keyboard.tips }, // 是否显示工具条左边的"取消"按钮 showCancel: { type: Boolean, default: uni.$u.props.keyboard.showCancel }, // 是否显示工具条右边的"完成"按钮 showConfirm: { type: Boolean, default: uni.$u.props.keyboard.showConfirm }, // 是否打乱键盘按键的顺序 random: { type: Boolean, default: uni.$u.props.keyboard.random }, // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距 safeAreaInsetBottom: { type: Boolean, default: uni.$u.props.keyboard.safeAreaInsetBottom }, // 是否允许通过点击遮罩关闭键盘 closeOnClickOverlay: { type: Boolean, default: uni.$u.props.keyboard.closeOnClickOverlay }, // 控制键盘的弹出与收起 show: { type: Boolean, default: uni.$u.props.keyboard.show }, // 是否显示遮罩,某些时候数字键盘时,用户希望看到自己的数值,所以可能不想要遮罩 overlay: { type: Boolean, default: uni.$u.props.keyboard.overlay }, // z-index值 zIndex: { type: [String, Number], default: uni.$u.props.keyboard.zIndex }, // 取消按钮的文字 cancelText: { type: String, default: uni.$u.props.keyboard.cancelText }, // 确认按钮的文字 confirmText: { type: String, default: uni.$u.props.keyboard.confirmText }, // 输入一个中文后,是否自动切换到英文 autoChange: { type: Boolean, default: uni.$u.props.keyboard.autoChange } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-keyboard/u-keyboard.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-line/props.js ================================================ export default { props: { color: { type: String, default: uni.$u.props.line.color }, // 长度,竖向时表现为高度,横向时表现为长度,可以为百分比,带px单位的值等 length: { type: [String, Number], default: uni.$u.props.line.length }, // 线条方向,col-竖向,row-横向 direction: { type: String, default: uni.$u.props.line.direction }, // 是否显示细边框 hairline: { type: Boolean, default: uni.$u.props.line.hairline }, // 线条与上下左右元素的间距,字符串形式,如"30px"、"20px 30px" margin: { type: [String, Number], default: uni.$u.props.line.margin }, // 是否虚线,true-虚线,false-实线 dashed: { type: Boolean, default: uni.$u.props.line.dashed } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-line/u-line.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-line-progress/props.js ================================================ export default { props: { // 激活部分的颜色 activeColor: { type: String, default: uni.$u.props.lineProgress.activeColor }, inactiveColor: { type: String, default: uni.$u.props.lineProgress.color }, // 进度百分比,数值 percentage: { type: [String, Number], default: uni.$u.props.lineProgress.inactiveColor }, // 是否在进度条内部显示百分比的值 showText: { type: Boolean, default: uni.$u.props.lineProgress.showText }, // 进度条的高度,单位px height: { type: [String, Number], default: uni.$u.props.lineProgress.height } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-line-progress/u-line-progress.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-link/props.js ================================================ export default { props: { // 文字颜色 color: { type: String, default: uni.$u.props.link.color }, // 字体大小,单位px fontSize: { type: [String, Number], default: uni.$u.props.link.fontSize }, // 是否显示下划线 underLine: { type: Boolean, default: uni.$u.props.link.underLine }, // 要跳转的链接 href: { type: String, default: uni.$u.props.link.href }, // 小程序中复制到粘贴板的提示语 mpTips: { type: String, default: uni.$u.props.link.mpTips }, // 下划线颜色 lineColor: { type: String, default: uni.$u.props.link.lineColor }, // 超链接的问题,不使用slot形式传入,是因为nvue下无法修改颜色 text: { type: String, default: uni.$u.props.link.text } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-link/u-link.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-list/props.js ================================================ export default { props: { // 控制是否出现滚动条,仅nvue有效 showScrollbar: { type: Boolean, default: uni.$u.props.list.showScrollbar }, // 距底部多少时触发scrolltolower事件 lowerThreshold: { type: [String, Number], default: uni.$u.props.list.lowerThreshold }, // 距顶部多少时触发scrolltoupper事件,非nvue有效 upperThreshold: { type: [String, Number], default: uni.$u.props.list.upperThreshold }, // 设置竖向滚动条位置 scrollTop: { type: [String, Number], default: uni.$u.props.list.scrollTop }, // 控制 onscroll 事件触发的频率,仅nvue有效 offsetAccuracy: { type: [String, Number], default: uni.$u.props.list.offsetAccuracy }, // 启用 flexbox 布局。开启后,当前节点声明了display: flex就会成为flex container,并作用于其孩子节点,仅微信小程序有效 enableFlex: { type: Boolean, default: uni.$u.props.list.enableFlex }, // 是否按分页模式显示List,默认值false pagingEnabled: { type: Boolean, default: uni.$u.props.list.pagingEnabled }, // 是否允许List滚动 scrollable: { type: Boolean, default: uni.$u.props.list.scrollable }, // 值应为某子元素id(id不能以数字开头) scrollIntoView: { type: String, default: uni.$u.props.list.scrollIntoView }, // 在设置滚动条位置时使用动画过渡 scrollWithAnimation: { type: Boolean, default: uni.$u.props.list.scrollWithAnimation }, // iOS点击顶部状态栏、安卓双击标题栏时,滚动条返回顶部,只对微信小程序有效 enableBackToTop: { type: Boolean, default: uni.$u.props.list.enableBackToTop }, // 列表的高度 height: { type: [String, Number], default: uni.$u.props.list.height }, // 列表宽度 width: { type: [String, Number], default: uni.$u.props.list.width }, // 列表前后预渲染的屏数,1代表一个屏幕的高度,1.5代表1个半屏幕高度 preLoadScreen: { type: [String, Number], default: uni.$u.props.list.preLoadScreen } // vue下,是否开启虚拟列表 } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-list/u-list.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-list-item/props.js ================================================ export default { props: { // 用于滚动到指定item anchor: { type: [String, Number], default: uni.$u.props.listItem.anchor } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-list-item/u-list-item.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-loading-icon/props.js ================================================ export default { props: { // 是否显示组件 show: { type: Boolean, default: uni.$u.props.loadingIcon.show }, // 颜色 color: { type: String, default: uni.$u.props.loadingIcon.color }, // 提示文字颜色 textColor: { type: String, default: uni.$u.props.loadingIcon.textColor }, // 文字和图标是否垂直排列 vertical: { type: Boolean, default: uni.$u.props.loadingIcon.vertical }, // 模式选择,circle-圆形,spinner-花朵形,semicircle-半圆形 mode: { type: String, default: uni.$u.props.loadingIcon.mode }, // 图标大小,单位默认px size: { type: [String, Number], default: uni.$u.props.loadingIcon.size }, // 文字大小 textSize: { type: [String, Number], default: uni.$u.props.loadingIcon.textSize }, // 文字内容 text: { type: [String, Number], default: uni.$u.props.loadingIcon.text }, // 动画模式 timingFunction: { type: String, default: uni.$u.props.loadingIcon.timingFunction }, // 动画执行周期时间 duration: { type: [String, Number], default: uni.$u.props.loadingIcon.duration }, // mode=circle时的暗边颜色 inactiveColor: { type: String, default: uni.$u.props.loadingIcon.inactiveColor } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-loading-icon/u-loading-icon.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-loading-page/props.js ================================================ export default { props: { // 提示内容 loadingText: { type: [String, Number], default: uni.$u.props.loadingPage.loadingText }, // 文字上方用于替换loading动画的图片 image: { type: String, default: uni.$u.props.loadingPage.image }, // 加载动画的模式,circle-圆形,spinner-花朵形,semicircle-半圆形 loadingMode: { type: String, default: uni.$u.props.loadingPage.loadingMode }, // 是否加载中 loading: { type: Boolean, default: uni.$u.props.loadingPage.loading }, // 背景色 bgColor: { type: String, default: uni.$u.props.loadingPage.bgColor }, // 文字颜色 color: { type: String, default: uni.$u.props.loadingPage.color }, // 文字大小 fontSize: { type: [String, Number], default: uni.$u.props.loadingPage.fontSize }, // 图标大小 iconSize: { type: [String, Number], default: uni.$u.props.loadingPage.fontSize }, // 加载中图标的颜色,只能rgb或者十六进制颜色值 loadingColor: { type: String, default: uni.$u.props.loadingPage.loadingColor } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-loading-page/u-loading-page.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-loadmore/props.js ================================================ export default { props: { // 组件状态,loadmore-加载前的状态,loading-加载中的状态,nomore-没有更多的状态 status: { type: String, default: uni.$u.props.loadmore.status }, // 组件背景色 bgColor: { type: String, default: uni.$u.props.loadmore.bgColor }, // 是否显示加载中的图标 icon: { type: Boolean, default: uni.$u.props.loadmore.icon }, // 字体大小 fontSize: { type: [String, Number], default: uni.$u.props.loadmore.fontSize }, // 图标大小 iconSize: { type: [String, Number], default: uni.$u.props.loadmore.iconSize }, // 字体颜色 color: { type: String, default: uni.$u.props.loadmore.color }, // 加载中状态的图标,spinner-花朵状图标,circle-圆圈状,semicircle-半圆 loadingIcon: { type: String, default: uni.$u.props.loadmore.loadingIcon }, // 加载前的提示语 loadmoreText: { type: String, default: uni.$u.props.loadmore.loadmoreText }, // 加载中提示语 loadingText: { type: String, default: uni.$u.props.loadmore.loadingText }, // 没有更多的提示语 nomoreText: { type: String, default: uni.$u.props.loadmore.nomoreText }, // 在“没有更多”状态下,是否显示粗点 isDot: { type: Boolean, default: uni.$u.props.loadmore.isDot }, // 加载中图标的颜色 iconColor: { type: String, default: uni.$u.props.loadmore.iconColor }, // 上边距 marginTop: { type: [String, Number], default: uni.$u.props.loadmore.marginTop }, // 下边距 marginBottom: { type: [String, Number], default: uni.$u.props.loadmore.marginBottom }, // 高度,单位px height: { type: [String, Number], default: uni.$u.props.loadmore.height }, // 是否显示左边分割线 line: { type: Boolean, default: uni.$u.props.loadmore.line }, // 线条颜色 lineColor: { type: String, default: uni.$u.props.loadmore.lineColor }, // 是否虚线,true-虚线,false-实线 dashed: { type: Boolean, default: uni.$u.props.loadmore.dashed } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-loadmore/u-loadmore.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-modal/props.js ================================================ export default { props: { // 是否展示modal show: { type: Boolean, default: uni.$u.props.modal.show }, // 标题 title: { type: [String], default: uni.$u.props.modal.title }, // 弹窗内容 content: { type: String, default: uni.$u.props.modal.content }, // 确认文案 confirmText: { type: String, default: uni.$u.props.modal.confirmText }, // 取消文案 cancelText: { type: String, default: uni.$u.props.modal.cancelText }, // 是否显示确认按钮 showConfirmButton: { type: Boolean, default: uni.$u.props.modal.showConfirmButton }, // 是否显示取消按钮 showCancelButton: { type: Boolean, default: uni.$u.props.modal.showCancelButton }, // 确认按钮颜色 confirmColor: { type: String, default: uni.$u.props.modal.confirmColor }, // 取消文字颜色 cancelColor: { type: String, default: uni.$u.props.modal.cancelColor }, // 对调确认和取消的位置 buttonReverse: { type: Boolean, default: uni.$u.props.modal.buttonReverse }, // 是否开启缩放效果 zoom: { type: Boolean, default: uni.$u.props.modal.zoom }, // 是否异步关闭,只对确定按钮有效 asyncClose: { type: Boolean, default: uni.$u.props.modal.asyncClose }, // 是否允许点击遮罩关闭modal closeOnClickOverlay: { type: Boolean, default: uni.$u.props.modal.closeOnClickOverlay }, // 给一个负的margin-top,往上偏移,避免和键盘重合的情况 negativeTop: { type: [String, Number], default: uni.$u.props.modal.negativeTop }, // modal宽度,不支持百分比,可以数值,px,rpx单位 width: { type: [String, Number], default: uni.$u.props.modal.width }, // 确认按钮的样式,circle-圆形,square-方形,如设置,将不会显示取消按钮 confirmButtonShape: { type: String, default: uni.$u.props.modal.confirmButtonShape } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-modal/u-modal.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-navbar/props.js ================================================ export default { props: { // 是否开启顶部安全区适配 safeAreaInsetTop: { type: Boolean, default: uni.$u.props.navbar.safeAreaInsetTop }, // 固定在顶部时,是否生成一个等高元素,以防止塌陷 placeholder: { type: Boolean, default: uni.$u.props.navbar.placeholder }, // 是否固定在顶部 fixed: { type: Boolean, default: uni.$u.props.navbar.fixed }, // 是否显示下边框 border: { type: Boolean, default: uni.$u.props.navbar.border }, // 左边的图标 leftIcon: { type: String, default: uni.$u.props.navbar.leftIcon }, // 左边的提示文字 leftText: { type: String, default: uni.$u.props.navbar.leftText }, // 左右的提示文字 rightText: { type: String, default: uni.$u.props.navbar.rightText }, // 右边的图标 rightIcon: { type: String, default: uni.$u.props.navbar.rightIcon }, // 标题 title: { type: [String, Number], default: uni.$u.props.navbar.title }, // 背景颜色 bgColor: { type: String, default: uni.$u.props.navbar.bgColor }, // 标题的宽度 titleWidth: { type: [String, Number], default: uni.$u.props.navbar.titleWidth }, // 导航栏高度 height: { type: [String, Number], default: uni.$u.props.navbar.height }, // 左侧返回图标的大小 leftIconSize: { type: [String, Number], default: uni.$u.props.navbar.leftIconSize }, // 左侧返回图标的颜色 leftIconColor: { type: String, default: uni.$u.props.navbar.leftIconColor }, // 点击左侧区域(返回图标),是否自动返回上一页 autoBack: { type: Boolean, default: uni.$u.props.navbar.autoBack }, // 标题的样式,对象或字符串 titleStyle: { type: [String, Object], default: uni.$u.props.navbar.titleStyle } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-navbar/u-navbar.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-no-network/props.js ================================================ export default { props: { // 页面文字提示 tips: { type: String, default: uni.$u.props.noNetwork.tips }, // 一个z-index值,用于设置没有网络这个组件的层次,因为页面可能会有其他定位的元素层级过高,导致此组件被覆盖 zIndex: { type: [String, Number], default: uni.$u.props.noNetwork.zIndex }, // image 没有网络的图片提示 image: { type: String, default: uni.$u.props.noNetwork.image } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-no-network/u-no-network.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-notice-bar/props.js ================================================ export default { props: { // 显示的内容,数组 text: { type: [Array, String], default: uni.$u.props.noticeBar.text }, // 通告滚动模式,row-横向滚动,column-竖向滚动 direction: { type: String, default: uni.$u.props.noticeBar.direction }, // direction = row时,是否使用步进形式滚动 step: { type: Boolean, default: uni.$u.props.noticeBar.step }, // 是否显示左侧的音量图标 icon: { type: String, default: uni.$u.props.noticeBar.icon }, // 通告模式,link-显示右箭头,closable-显示右侧关闭图标 mode: { type: String, default: uni.$u.props.noticeBar.mode }, // 文字颜色,各图标也会使用文字颜色 color: { type: String, default: uni.$u.props.noticeBar.color }, // 背景颜色 bgColor: { type: String, default: uni.$u.props.noticeBar.bgColor }, // 水平滚动时的滚动速度,即每秒滚动多少px(px),这有利于控制文字无论多少时,都能有一个恒定的速度 speed: { type: [String, Number], default: uni.$u.props.noticeBar.speed }, // 字体大小 fontSize: { type: [String, Number], default: uni.$u.props.noticeBar.fontSize }, // 滚动一个周期的时间长,单位ms duration: { type: [String, Number], default: uni.$u.props.noticeBar.duration }, // 是否禁止用手滑动切换 // 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序 disableTouch: { type: Boolean, default: uni.$u.props.noticeBar.disableTouch }, // 跳转的页面路径 url: { type: String, default: uni.$u.props.noticeBar.url }, // 页面跳转的类型 linkType: { type: String, default: uni.$u.props.noticeBar.linkType } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-notice-bar/u-notice-bar.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-notify/props.js ================================================ export default { props: { // 到顶部的距离 top: { type: [String, Number], default: uni.$u.props.notify.top }, // 是否展示组件 // show: { // type: Boolean, // default: uni.$u.props.notify.show // }, // type主题,primary,success,warning,error type: { type: String, default: uni.$u.props.notify.type }, // 字体颜色 color: { type: String, default: uni.$u.props.notify.color }, // 背景颜色 bgColor: { type: String, default: uni.$u.props.notify.bgColor }, // 展示的文字内容 message: { type: String, default: uni.$u.props.notify.message }, // 展示时长,为0时不消失,单位ms duration: { type: [String, Number], default: uni.$u.props.notify.duration }, // 字体大小 fontSize: { type: [String, Number], default: uni.$u.props.notify.fontSize }, // 是否留出顶部安全距离(状态栏高度) safeAreaInsetTop: { type: Boolean, default: uni.$u.props.notify.safeAreaInsetTop } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-notify/u-notify.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-number-box/props.js ================================================ export default { props: { // 步进器标识符,在change回调返回 name: { type: [String, Number], default: uni.$u.props.numberBox.name }, // 用于双向绑定的值,初始化时设置设为默认min值(最小值) value: { type: [String, Number], default: uni.$u.props.numberBox.value }, // 最小值 min: { type: [String, Number], default: uni.$u.props.numberBox.min }, // 最大值 max: { type: [String, Number], default: uni.$u.props.numberBox.max }, // 加减的步长,可为小数 step: { type: [String, Number], default: uni.$u.props.numberBox.step }, // 是否只允许输入整数 integer: { type: Boolean, default: uni.$u.props.numberBox.integer }, // 是否禁用,包括输入框,加减按钮 disabled: { type: Boolean, default: uni.$u.props.numberBox.disabled }, // 是否禁用输入框 disabledInput: { type: Boolean, default: uni.$u.props.numberBox.disabledInput }, // 是否开启异步变更,开启后需要手动控制输入值 asyncChange: { type: Boolean, default: uni.$u.props.numberBox.asyncChange }, // 输入框宽度,单位为px inputWidth: { type: [String, Number], default: uni.$u.props.numberBox.inputWidth }, // 是否显示减少按钮 showMinus: { type: Boolean, default: uni.$u.props.numberBox.showMinus }, // 是否显示增加按钮 showPlus: { type: Boolean, default: uni.$u.props.numberBox.showPlus }, // 显示的小数位数 decimalLength: { type: [String, Number, null], default: uni.$u.props.numberBox.decimalLength }, // 是否开启长按加减手势 longPress: { type: Boolean, default: uni.$u.props.numberBox.longPress }, // 输入框文字和加减按钮图标的颜色 color: { type: String, default: uni.$u.props.numberBox.color }, // 按钮大小,宽高等于此值,单位px,输入框高度和此值保持一致 buttonSize: { type: [String, Number], default: uni.$u.props.numberBox.buttonSize }, // 输入框和按钮的背景颜色 bgColor: { type: String, default: uni.$u.props.numberBox.bgColor }, // 指定光标于键盘的距离,避免键盘遮挡输入框,单位px cursorSpacing: { type: [String, Number], default: uni.$u.props.numberBox.cursorSpacing }, // 是否禁用增加按钮 disablePlus: { type: Boolean, default: uni.$u.props.numberBox.disablePlus }, // 是否禁用减少按钮 disableMinus: { type: Boolean, default: uni.$u.props.numberBox.disableMinus }, // 加减按钮图标的样式 iconStyle: { type: [Object, String], default: uni.$u.props.numberBox.iconStyle } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-number-box/u-number-box.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-number-keyboard/props.js ================================================ export default { props: { // 键盘的类型,number-数字键盘,card-身份证键盘 mode: { type: String, default: uni.$u.props.numberKeyboard.value }, // 是否显示键盘的"."符号 dotDisabled: { type: Boolean, default: uni.$u.props.numberKeyboard.dotDisabled }, // 是否打乱键盘按键的顺序 random: { type: Boolean, default: uni.$u.props.numberKeyboard.random } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-number-keyboard/u-number-keyboard.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-overlay/props.js ================================================ export default { props: { // 是否显示遮罩 show: { type: Boolean, default: uni.$u.props.overlay.show }, // 层级z-index zIndex: { type: [String, Number], default: uni.$u.props.overlay.zIndex }, // 遮罩的过渡时间,单位为ms duration: { type: [String, Number], default: uni.$u.props.overlay.duration }, // 不透明度值,当做rgba的第四个参数 opacity: { type: [String, Number], default: uni.$u.props.overlay.opacity } } } ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-overlay/u-overlay.vue ================================================ ================================================ FILE: bookkeeping_user_uniapp/uni_modules/uview-ui/components/u-parse/node/node.vue ================================================