├── server
├── migrations
│ └── .gitkeep
├── .husky
│ └── pre-commit
├── src
│ ├── constants
│ │ └── pagination.ts
│ ├── utils
│ │ ├── validatorUtils.ts
│ │ ├── crypto.ts
│ │ ├── transformerUtils.ts
│ │ ├── cronUtils.ts
│ │ └── markdown.ts
│ ├── modules
│ │ ├── company
│ │ │ ├── dto
│ │ │ │ ├── get-company.dto.ts
│ │ │ │ ├── update-company.dto.ts
│ │ │ │ └── create-company.dto.ts
│ │ │ ├── company.module.ts
│ │ │ ├── entities
│ │ │ │ └── company.entity.ts
│ │ │ ├── company.controller.ts
│ │ │ └── company.service.ts
│ │ ├── common
│ │ │ ├── common.service.ts
│ │ │ ├── common.module.ts
│ │ │ └── common.controller.ts
│ │ ├── memorandums
│ │ │ ├── dto
│ │ │ │ ├── get-memorandum.dto.ts
│ │ │ │ ├── update-memorandum.dto.ts
│ │ │ │ └── create-memorandum.dto.ts
│ │ │ ├── entities
│ │ │ │ └── memorandum.entity.ts
│ │ │ ├── memorandums.module.ts
│ │ │ ├── memorandums.controller.ts
│ │ │ └── memorandums.service.ts
│ │ ├── auth
│ │ │ ├── dto
│ │ │ │ ├── github-login.dto.ts
│ │ │ │ └── login.dto.ts
│ │ │ ├── auth.module.ts
│ │ │ └── auth.controller.ts
│ │ ├── users
│ │ │ ├── dto
│ │ │ │ ├── update-user.dto.ts
│ │ │ │ └── create-user.dto.ts
│ │ │ ├── users.module.ts
│ │ │ ├── users.controller.ts
│ │ │ ├── entities
│ │ │ │ └── user.entity.ts
│ │ │ ├── guards
│ │ │ │ └── user-controller-auth.guard.ts
│ │ │ └── users.service.ts
│ │ ├── inner-messages
│ │ │ ├── dto
│ │ │ │ ├── update-inner-message.dto.ts
│ │ │ │ └── create-inner-message.dto.ts
│ │ │ ├── inner-messages.module.ts
│ │ │ ├── entities
│ │ │ │ └── inner-message.entity.ts
│ │ │ ├── inner-messages.controller.ts
│ │ │ └── inner-messages.service.ts
│ │ ├── user-configures
│ │ │ ├── dto
│ │ │ │ ├── update-user-configure.dto.ts
│ │ │ │ └── create-user-configure.dto.ts
│ │ │ ├── user-configures.module.ts
│ │ │ ├── entities
│ │ │ │ └── user-configure.entity.ts
│ │ │ ├── user-configures.controller.ts
│ │ │ └── user-configures.service.ts
│ │ ├── tasks
│ │ │ ├── dto
│ │ │ │ ├── update-task.dto.ts
│ │ │ │ ├── get-task.dto.ts
│ │ │ │ └── create-task.dto.ts
│ │ │ ├── tasks.module.ts
│ │ │ ├── task-schedule.service.ts
│ │ │ ├── entities
│ │ │ │ └── task.entity.ts
│ │ │ ├── tasks.controller.ts
│ │ │ └── tasks.service.ts
│ │ ├── reminders
│ │ │ ├── dto
│ │ │ │ ├── update-reminder.dto.ts
│ │ │ │ ├── get-reminder.dto.ts
│ │ │ │ └── create-reminder.dto.ts
│ │ │ ├── reminders.module.ts
│ │ │ ├── entities
│ │ │ │ └── reminder.entity.ts
│ │ │ └── reminders.controller.ts
│ │ ├── todo-lists
│ │ │ ├── dto
│ │ │ │ ├── create-todo-list.dto.ts
│ │ │ │ ├── update-todo-list.dto.ts
│ │ │ │ └── get-todo-list.dto.ts
│ │ │ ├── todo-lists.module.ts
│ │ │ ├── entities
│ │ │ │ └── todo-list.entity.ts
│ │ │ ├── todo-lists.controller.ts
│ │ │ └── todo-lists.service.ts
│ │ ├── logs
│ │ │ ├── dto
│ │ │ │ ├── update-log.dto.ts
│ │ │ │ ├── get-log.dto.ts
│ │ │ │ └── create-log.dto.ts
│ │ │ ├── logs.module.ts
│ │ │ ├── entities
│ │ │ │ └── log.entity.ts
│ │ │ └── logs.controller.ts
│ │ ├── bills
│ │ │ ├── dto
│ │ │ │ ├── update-bill.dto.ts
│ │ │ │ ├── create-bill.dto.ts
│ │ │ │ └── get-bill.dto.ts
│ │ │ ├── bills.module.ts
│ │ │ ├── entities
│ │ │ │ └── bill.entity.ts
│ │ │ └── bills.controller.ts
│ │ ├── system
│ │ │ ├── system.module.ts
│ │ │ ├── system.controller.ts
│ │ │ └── system.service.ts
│ │ ├── bill-types
│ │ │ ├── dto
│ │ │ │ ├── update-bill-type.dto.ts
│ │ │ │ └── create-bill-type.dto.ts
│ │ │ ├── bill-types.module.ts
│ │ │ ├── entities
│ │ │ │ └── bill-type.entity.ts
│ │ │ ├── bill-types.controller.ts
│ │ │ └── bill-types.service.ts
│ │ └── mail
│ │ │ ├── mail.module.ts
│ │ │ └── mail-schedule.service.ts
│ ├── app.service.ts
│ ├── guards
│ │ ├── guards.module.ts
│ │ └── user-auth.guard.ts
│ ├── app.controller.ts
│ ├── dtos
│ │ └── pagination.dto.ts
│ ├── global-modules
│ │ └── global-modules.module.ts
│ ├── decorators
│ │ └── user.decorator.ts
│ ├── entities
│ │ └── date.entity.ts
│ └── main.ts
├── .prettierrc
├── tsconfig.build.json
├── nest-cli.json
├── netlify.toml
├── ecosystem.config.cjs
├── .env.development
├── tsconfig.json
├── scripts
│ └── init.ts
├── netlify
│ └── functions
│ │ └── api.js
├── .gitignore
├── LICENSE
├── README.md
└── typeorm.config.ts
├── src
├── assets
│ ├── styles
│ │ ├── tailwindcss.css
│ │ ├── variables.scss
│ │ ├── antd
│ │ │ ├── drawer.scss
│ │ │ ├── menu.scss
│ │ │ ├── card.scss
│ │ │ ├── index.scss
│ │ │ ├── modal.scss
│ │ │ └── table.scss
│ │ ├── global.scss
│ │ ├── style.scss
│ │ ├── query-panel.scss
│ │ ├── mixins.scss
│ │ └── nprogress.css
│ └── img
│ │ └── common
│ │ └── user-poster.png
├── views
│ ├── memorandum
│ │ ├── constants.ts
│ │ ├── DetailPage.tsx
│ │ ├── style.scss
│ │ └── CreatePage.tsx
│ ├── todo-list
│ │ ├── constants.ts
│ │ └── CreateTodoModal.tsx
│ ├── setting
│ │ ├── index
│ │ │ ├── style.scss
│ │ │ └── index.tsx
│ │ ├── base
│ │ │ ├── style.scss
│ │ │ └── index.tsx
│ │ ├── notification
│ │ │ ├── style.scss
│ │ │ └── index.tsx
│ │ └── inner-message
│ │ │ ├── style.scss
│ │ │ └── index.tsx
│ ├── exception
│ │ ├── 404.tsx
│ │ └── style.scss
│ ├── bill
│ │ ├── enum.tsx
│ │ ├── style.scss
│ │ └── CreateTypeModal.tsx
│ ├── log
│ │ ├── style.scss
│ │ ├── constants.ts
│ │ └── DetailDrawer.tsx
│ ├── main
│ │ ├── style.scss
│ │ └── index.tsx
│ ├── index
│ │ ├── index.tsx
│ │ ├── PenelGroup.tsx
│ │ └── style.scss
│ ├── today-task
│ │ ├── style.scss
│ │ ├── TaskItem.tsx
│ │ └── CreateTaskModal.tsx
│ ├── login
│ │ └── style.scss
│ └── reminder
│ │ └── CreateReminder.tsx
├── constants
│ ├── index.ts
│ ├── storage.ts
│ └── menu.tsx
├── components
│ ├── no-data
│ │ ├── style.scss
│ │ └── index.tsx
│ ├── exception
│ │ ├── style.scss
│ │ └── index.tsx
│ ├── footer
│ │ ├── style.scss
│ │ └── index.tsx
│ ├── table
│ │ ├── style.scss
│ │ └── Toolbar.tsx
│ ├── avatar
│ │ └── index.tsx
│ ├── sidebar
│ │ └── style.scss
│ ├── private-route
│ │ └── index.tsx
│ └── header
│ │ └── style.scss
├── services
│ ├── system.ts
│ ├── common.ts
│ ├── index.ts
│ ├── innerMessage.ts
│ ├── todayTask.ts
│ ├── todoList.ts
│ ├── reminder.ts
│ ├── memorandum.ts
│ ├── user.ts
│ ├── log.ts
│ ├── company.ts
│ └── bill.ts
├── registerSW.ts
├── hooks
│ ├── index.ts
│ ├── useCreation.ts
│ └── useDebounceFn.ts
├── router
│ └── index.tsx
├── vite-env.d.ts
├── store
│ ├── middlewares.ts
│ ├── index.ts
│ ├── companySlice.ts
│ ├── systemSlice.ts
│ └── userSlice.ts
├── config
│ └── index.ts
├── main.tsx
└── utils
│ ├── helper.ts
│ ├── date.ts
│ ├── index.ts
│ └── http.ts
├── .env.prod
├── public
├── robots.txt
├── pwa-192x192.png
├── pwa-512x512.png
├── apple-touch-icon.png
└── logo.svg
├── .prettierrc.js
├── .husky
└── pre-commit
├── media
└── screenshot.png
├── .env
├── vercel.json
├── .oxlintrc.json
├── netlify.toml
├── .gitignore
├── tsconfig.json
├── LICENSE
├── README.md
├── package.json
└── vite.config.mts
/server/migrations/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/styles/tailwindcss.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 |
--------------------------------------------------------------------------------
/src/views/memorandum/constants.ts:
--------------------------------------------------------------------------------
1 | export const defaultTitle = '无标题'
2 |
--------------------------------------------------------------------------------
/.env.prod:
--------------------------------------------------------------------------------
1 | VITE_HTTP_BASE=https://work-api.xiejiahe.com/api
2 | VITE_DEV=false
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | export * from './storage'
2 | export * from './menu'
3 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | semi: false,
3 | singleQuote: true,
4 | }
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/server/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/media/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xjh22222228/tomato-work/HEAD/media/screenshot.png
--------------------------------------------------------------------------------
/public/pwa-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xjh22222228/tomato-work/HEAD/public/pwa-192x192.png
--------------------------------------------------------------------------------
/public/pwa-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xjh22222228/tomato-work/HEAD/public/pwa-512x512.png
--------------------------------------------------------------------------------
/server/src/constants/pagination.ts:
--------------------------------------------------------------------------------
1 | export const PAGE_NO = 0
2 |
3 | export const PAGE_SIZE = 50
4 |
--------------------------------------------------------------------------------
/server/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "trailingComma": "all"
5 | }
6 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xjh22222228/tomato-work/HEAD/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # VITE_HTTP_BASE=http://localhost:3000/api
2 | VITE_HTTP_BASE=https://work-api.xiejiahe.com/api
3 | VITE_DEV=true
--------------------------------------------------------------------------------
/src/assets/styles/variables.scss:
--------------------------------------------------------------------------------
1 | // 主题颜色
2 | $theme-color: #20abfe;
3 |
4 | // 边框灰色
5 | $border-gray-color: #ebedf0;
6 |
--------------------------------------------------------------------------------
/src/components/no-data/style.scss:
--------------------------------------------------------------------------------
1 | .no-data {
2 | img {
3 | width: 200px;
4 | height: 200px;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/assets/styles/antd/drawer.scss:
--------------------------------------------------------------------------------
1 | .ant-drawer-content .ant-drawer-body {
2 | padding: 0;
3 | padding-top: 20px;
4 | }
5 |
--------------------------------------------------------------------------------
/src/assets/img/common/user-poster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xjh22222228/tomato-work/HEAD/src/assets/img/common/user-poster.png
--------------------------------------------------------------------------------
/src/assets/styles/antd/menu.scss:
--------------------------------------------------------------------------------
1 | // 菜单每一列的字体颜色
2 | .ant-menu-dark .ant-menu-item {
3 | color: rgba(255, 255, 255, 0.85);
4 | }
5 |
--------------------------------------------------------------------------------
/server/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {
4 | "source": "/:path*",
5 | "destination": "/index.html"
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/src/assets/styles/antd/card.scss:
--------------------------------------------------------------------------------
1 | .ant-card-head {
2 | padding: 0 15px;
3 |
4 | .ant-card-head-title {
5 | padding: 10px 0;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/server/src/utils/validatorUtils.ts:
--------------------------------------------------------------------------------
1 | export const dateValidator = {
2 | REGEXP: /^\d{4}-\d{2}-\d{2}$/,
3 | MESSAGE: '日期格式必须为 YYYY-MM-DD',
4 | } as const
5 |
--------------------------------------------------------------------------------
/src/views/todo-list/constants.ts:
--------------------------------------------------------------------------------
1 | export const STATUS: any = {
2 | 1: { text: '进行中', color: 'volcano' },
3 | 2: { text: '已完成', color: '#2db7f5' },
4 | }
5 |
--------------------------------------------------------------------------------
/server/src/modules/company/dto/get-company.dto.ts:
--------------------------------------------------------------------------------
1 | import { PaginationDto } from '@/dtos/pagination.dto'
2 |
3 | export class GetCompanyDto extends PaginationDto {}
4 |
--------------------------------------------------------------------------------
/src/services/system.ts:
--------------------------------------------------------------------------------
1 | import http from '@/utils/http'
2 |
3 | // 获取系统信息
4 | export function serviceGetSystemInfo() {
5 | return http.post('/system/info')
6 | }
7 |
--------------------------------------------------------------------------------
/server/src/modules/common/common.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common'
2 |
3 | @Injectable()
4 | export class CommonService {
5 | // 可以添加公共服务方法
6 | }
7 |
--------------------------------------------------------------------------------
/server/src/modules/memorandums/dto/get-memorandum.dto.ts:
--------------------------------------------------------------------------------
1 | import { PaginationDto } from '@/dtos/pagination.dto'
2 |
3 | export class GetMemorandumDto extends PaginationDto {}
4 |
--------------------------------------------------------------------------------
/.oxlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "no-unused-vars": [
4 | "error",
5 | {
6 | "ignoreRestSiblings": true
7 | }
8 | ]
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/server/src/utils/crypto.ts:
--------------------------------------------------------------------------------
1 | import * as crypto from 'crypto'
2 |
3 | export function md5(str: string): string {
4 | return crypto.createHash('md5').update(str).digest('hex')
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/styles/antd/index.scss:
--------------------------------------------------------------------------------
1 | // 全局覆盖antd默认样式
2 | @use 'menu.scss' as *;
3 | @use 'table.scss' as *;
4 | @use 'modal.scss' as *;
5 | @use 'card.scss' as *;
6 | @use 'drawer.scss' as *;
7 |
--------------------------------------------------------------------------------
/src/services/common.ts:
--------------------------------------------------------------------------------
1 | import http from '@/utils/http'
2 |
3 | // 获取后台首页面板数据
4 | export function serviceGetPanelData(params?: object) {
5 | return http.post('/panel', { ...params })
6 | }
7 |
--------------------------------------------------------------------------------
/src/views/setting/index/style.scss:
--------------------------------------------------------------------------------
1 | .setting-page {
2 | background: #fff;
3 | flex-direction: row !important;
4 |
5 | .ant-layout-content {
6 | padding: 15px 30px !important;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/server/src/modules/auth/dto/github-login.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsNotEmpty, IsString } from 'class-validator'
2 |
3 | export class GithubLoginDto {
4 | @IsNotEmpty()
5 | @IsString()
6 | code: string
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/exception/style.scss:
--------------------------------------------------------------------------------
1 | @use '@/assets/styles/mixins.scss' as *;
2 |
3 | .ant-result-404 {
4 | @include flex-center;
5 |
6 | .ant-result-icon {
7 | margin: 0 auto;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/server/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/nest-cli",
3 | "collection": "@nestjs/schematics",
4 | "sourceRoot": "src",
5 | "compilerOptions": {
6 | "deleteOutDir": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/server/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | command = "npm run build"
3 | functions = "netlify/functions"
4 |
5 | [[redirects]]
6 | from = "/api/*"
7 | to = "/.netlify/functions/api/:splat"
8 | status = 200
9 | force = true
--------------------------------------------------------------------------------
/server/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common'
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(): string {
6 | return 'https://github.com/xjh22222228/tomato-work'
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/assets/styles/global.scss:
--------------------------------------------------------------------------------
1 | @use 'style.scss' as *;
2 | @use 'antd/index.scss' as *;
3 | @use 'query-panel.scss' as *;
4 | @use 'nprogress.css' as *;
5 | // 编辑器
6 | @use '@toast-ui/editor/dist/toastui-editor.css' as *;
7 |
--------------------------------------------------------------------------------
/server/src/modules/users/dto/update-user.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types'
2 | import { CreateUserDto } from './create-user.dto'
3 |
4 | export class UpdateUserDto extends PartialType(CreateUserDto) {}
5 |
--------------------------------------------------------------------------------
/src/components/footer/style.scss:
--------------------------------------------------------------------------------
1 | @use '@/assets/styles/variables.scss' as *;
2 | .global-footer {
3 | align-self: center;
4 | padding: 15px 5px;
5 | font-size: 14px;
6 | a {
7 | color: $theme-color;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/constants/storage.ts:
--------------------------------------------------------------------------------
1 | export const LOCAL_STORAGE = {
2 | USER: 'USER',
3 | SIDEBAR_COLLAPSED: 'SIDEBAR_COLLAPSED',
4 | LOGIN_NAME: 'LOGIN_NAME',
5 | LOCK_SCREEN: 'LOCK_SCREEN',
6 | COMPANY_ID: 'COMPANY_ID',
7 | TOKEN: 'TOKEN',
8 | }
9 |
--------------------------------------------------------------------------------
/server/src/guards/guards.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { UserAuthGuard } from './user-auth.guard'
3 |
4 | @Module({
5 | providers: [UserAuthGuard],
6 | exports: [UserAuthGuard],
7 | })
8 | export class GuardsModule {}
9 |
--------------------------------------------------------------------------------
/src/views/exception/404.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './style.scss'
3 | import Exception from '@/components/exception'
4 |
5 | export default () => (
6 |
7 |
8 |
9 | )
10 |
--------------------------------------------------------------------------------
/src/views/exception/style.scss:
--------------------------------------------------------------------------------
1 | @use '@/assets/styles/mixins.scss' as *;
2 |
3 | .exception-wrapper {
4 | height: 100vh;
5 | @include flex-center;
6 |
7 | .ant-result {
8 | margin-top: -100px;
9 | flex-direction: column;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [[redirects]]
2 | from = "/*"
3 | to = "/index.html"
4 | status = 200
5 | [build.environment]
6 | NODE_VERSION = "22"
7 | [[headers]]
8 | for = "/manifest.webmanifest"
9 | [headers.values]
10 | Content-Type = "application/manifest+json"
--------------------------------------------------------------------------------
/server/src/modules/inner-messages/dto/update-inner-message.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types'
2 | import { CreateInnerMessageDto } from './create-inner-message.dto'
3 |
4 | export class UpdateInnerMessageDto extends PartialType(CreateInnerMessageDto) {}
5 |
--------------------------------------------------------------------------------
/server/src/modules/user-configures/dto/update-user-configure.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types'
2 | import { CreateUserConfigureDto } from './create-user-configure.dto'
3 |
4 | export class UpdateUserConfigureDto extends PartialType(
5 | CreateUserConfigureDto,
6 | ) {}
7 |
--------------------------------------------------------------------------------
/src/registerSW.ts:
--------------------------------------------------------------------------------
1 | import { registerSW } from 'virtual:pwa-register'
2 |
3 | const updateSW = registerSW({
4 | onNeedRefresh() {
5 | if (confirm('发现新版本,是否立即更新?')) {
6 | updateSW()
7 | }
8 | },
9 | onOfflineReady() {
10 | console.log('应用已准备就绪,可以离线使用')
11 | },
12 | })
13 |
--------------------------------------------------------------------------------
/server/src/modules/tasks/dto/update-task.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types'
2 | import { CreateTaskDto } from './create-task.dto'
3 | import { IsString } from 'class-validator'
4 |
5 | export class UpdateTaskDto extends PartialType(CreateTaskDto) {
6 | @IsString()
7 | id: string
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/table/style.scss:
--------------------------------------------------------------------------------
1 | .table-action-panel {
2 | background: #fff;
3 | padding: 10px 15px;
4 | margin-bottom: 10px;
5 | display: flex;
6 | justify-content: space-between;
7 | padding-right: 20px;
8 | align-items: center;
9 |
10 | .ant-btn {
11 | margin-right: 15px;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './user'
2 | export * from './system'
3 | export * from './reminder'
4 | export * from './todayTask'
5 | export * from './bill'
6 | export * from './memorandum'
7 | export * from './innerMessage'
8 | export * from './todoList'
9 | export * from './common'
10 | export * from './company'
11 |
--------------------------------------------------------------------------------
/server/src/modules/reminders/dto/update-reminder.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types'
2 | import { CreateReminderDto } from './create-reminder.dto'
3 | import { IsString } from 'class-validator'
4 |
5 | export class UpdateReminderDto extends PartialType(CreateReminderDto) {
6 | @IsString()
7 | id: string
8 | }
9 |
--------------------------------------------------------------------------------
/server/src/modules/todo-lists/dto/create-todo-list.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsNumber, IsOptional, IsString, IsIn } from 'class-validator'
2 |
3 | export class CreateTodoListDto {
4 | @IsOptional()
5 | @IsString()
6 | content?: string
7 |
8 | @IsOptional()
9 | @IsNumber()
10 | @IsIn([1, 2])
11 | status?: number
12 | }
13 |
--------------------------------------------------------------------------------
/server/src/modules/todo-lists/dto/update-todo-list.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types'
2 | import { CreateTodoListDto } from './create-todo-list.dto'
3 | import { IsString } from 'class-validator'
4 |
5 | export class UpdateTodoListDto extends PartialType(CreateTodoListDto) {
6 | @IsString()
7 | id: string
8 | }
9 |
--------------------------------------------------------------------------------
/server/src/modules/logs/dto/update-log.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types'
2 | import { CreateLogDto } from './create-log.dto'
3 | import { IsString, IsNotEmpty } from 'class-validator'
4 |
5 | export class UpdateLogDto extends PartialType(CreateLogDto) {
6 | @IsString()
7 | @IsNotEmpty()
8 | id: string
9 | }
10 |
--------------------------------------------------------------------------------
/src/assets/styles/style.scss:
--------------------------------------------------------------------------------
1 | body,
2 | html {
3 | overscroll-behavior-y: contain;
4 | }
5 |
6 | ol,
7 | ul,
8 | li {
9 | list-style: none;
10 | }
11 |
12 | a {
13 | text-decoration: none;
14 | }
15 |
16 | pre {
17 | white-space: pre-wrap;
18 | word-break: break-all;
19 | }
20 |
21 | em {
22 | font-style: normal;
23 | }
24 |
--------------------------------------------------------------------------------
/server/src/modules/auth/dto/login.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsNotEmpty, IsString, IsOptional } from 'class-validator'
2 |
3 | export class LoginDto {
4 | @IsOptional()
5 | @IsString()
6 | code?: string
7 |
8 | @IsNotEmpty()
9 | @IsString()
10 | loginName: string
11 |
12 | @IsNotEmpty()
13 | @IsString()
14 | password: string
15 | }
16 |
--------------------------------------------------------------------------------
/server/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get } from '@nestjs/common'
2 | import { AppService } from './app.service'
3 |
4 | @Controller()
5 | export class AppController {
6 | constructor(private readonly appService: AppService) {}
7 |
8 | @Get()
9 | getHello(): string {
10 | return this.appService.getHello()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/server/src/dtos/pagination.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsNumber, IsOptional } from 'class-validator'
2 | import { PAGE_SIZE, PAGE_NO } from '@/constants/pagination'
3 |
4 | export class PaginationDto {
5 | @IsNumber()
6 | @IsOptional()
7 | pageNo?: number = PAGE_NO
8 |
9 | @IsNumber()
10 | @IsOptional()
11 | pageSize?: number = PAGE_SIZE
12 | }
13 |
--------------------------------------------------------------------------------
/server/src/modules/bills/dto/update-bill.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types'
2 | import { CreateBillDto } from './create-bill.dto'
3 | import { IsNotEmpty, IsString } from 'class-validator'
4 |
5 | export class UpdateBillDto extends PartialType(CreateBillDto) {
6 | @IsNotEmpty()
7 | @IsString()
8 | id: string
9 | }
10 |
--------------------------------------------------------------------------------
/server/src/modules/system/system.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { SystemController } from './system.controller'
3 | import { SystemService } from './system.service'
4 |
5 | @Module({
6 | controllers: [SystemController],
7 | providers: [SystemService],
8 | exports: [SystemService],
9 | })
10 | export class SystemModule {}
11 |
--------------------------------------------------------------------------------
/server/src/modules/company/dto/update-company.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types'
2 | import { CreateCompanyDto } from './create-company.dto'
3 | import { IsString, IsNotEmpty } from 'class-validator'
4 |
5 | export class UpdateCompanyDto extends PartialType(CreateCompanyDto) {
6 | @IsString()
7 | @IsNotEmpty()
8 | id: string
9 | }
10 |
--------------------------------------------------------------------------------
/server/ecosystem.config.cjs:
--------------------------------------------------------------------------------
1 | // pm2 start ecosystem.config.cjs
2 |
3 | module.exports = {
4 | apps: [
5 | {
6 | name: 'tomato-work-server',
7 | port: '7003',
8 | exec_mode: 'cluster',
9 | instances: 'max',
10 | script: './dist/src/main.js',
11 | env: {
12 | NODE_ENV: 'production',
13 | },
14 | },
15 | ],
16 | }
17 |
--------------------------------------------------------------------------------
/server/src/modules/bill-types/dto/update-bill-type.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types'
2 | import { CreateBillTypeDto } from './create-bill-type.dto'
3 | import { IsNotEmpty, IsString } from 'class-validator'
4 |
5 | export class UpdateBillTypeDto extends PartialType(CreateBillTypeDto) {
6 | @IsNotEmpty()
7 | @IsString()
8 | id: string
9 | }
10 |
--------------------------------------------------------------------------------
/server/src/modules/memorandums/dto/update-memorandum.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types'
2 | import { CreateMemorandumDto } from './create-memorandum.dto'
3 | import { IsNotEmpty, IsString } from 'class-validator'
4 |
5 | export class UpdateMemorandumDto extends PartialType(CreateMemorandumDto) {
6 | @IsString()
7 | @IsNotEmpty()
8 | id: string
9 | }
10 |
--------------------------------------------------------------------------------
/server/src/modules/memorandums/dto/create-memorandum.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'
2 |
3 | export class CreateMemorandumDto {
4 | @IsNotEmpty()
5 | @IsString()
6 | title: string
7 |
8 | @IsOptional()
9 | @IsString()
10 | markdown?: string
11 |
12 | @IsOptional()
13 | @IsNumber()
14 | sortIndex?: number
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/avatar/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Avatar as AvatarComponent } from 'antd'
3 | import { AvatarProps } from 'antd/lib/avatar'
4 |
5 | function handleError() {
6 | return true
7 | }
8 |
9 | const Avatar: React.FC = (props) => {
10 | return
11 | }
12 |
13 | export default Avatar
14 |
--------------------------------------------------------------------------------
/server/src/global-modules/global-modules.module.ts:
--------------------------------------------------------------------------------
1 | import { Module, Global } from '@nestjs/common'
2 | import { UsersModule } from '../modules/users/users.module'
3 | import { GuardsModule } from '../guards/guards.module'
4 |
5 | @Global()
6 | @Module({
7 | imports: [UsersModule, GuardsModule],
8 | exports: [UsersModule, GuardsModule],
9 | })
10 | export class GlobalModulesModule {}
11 |
--------------------------------------------------------------------------------
/src/assets/styles/query-panel.scss:
--------------------------------------------------------------------------------
1 | // 面板查询
2 |
3 | .query-panel {
4 | padding: 15px 10px;
5 | margin-bottom: 15px;
6 | background: #fff;
7 | white-space: nowrap;
8 | overflow-x: auto;
9 |
10 | .ant-select {
11 | width: 120px;
12 | }
13 |
14 | & > [class*='ant-'] {
15 | margin-right: 20px;
16 | }
17 |
18 | .ant-btn {
19 | margin-right: 10px;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
2 | import type { RootState, AppDispatch } from '@/store'
3 | import { createSelector } from '@reduxjs/toolkit'
4 |
5 | export const useAppDispatch = () => useDispatch()
6 | export const useAppSelector: TypedUseSelectorHook = useSelector
7 | export const useCreateSelector = createSelector
8 |
--------------------------------------------------------------------------------
/src/views/bill/enum.tsx:
--------------------------------------------------------------------------------
1 | export enum TypeNames {
2 | 收入 = 1,
3 | 支出 = 2,
4 | }
5 |
6 | export enum TypeColors {
7 | '#108ee9' = 1,
8 | '#f50' = 2,
9 | }
10 |
11 | export const TYPES = [
12 | { label: '收入', value: 1, symbol: '+', color: '#666' },
13 | { label: '支出', value: 2, symbol: '-', color: '#f50' },
14 | ]
15 |
16 | export const OPTION_TYPES = [{ label: '全部', value: 0 }, ...TYPES]
17 |
--------------------------------------------------------------------------------
/server/src/modules/system/system.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post } from '@nestjs/common'
2 | import { SystemService } from './system.service'
3 |
4 | @Controller('system')
5 | export class SystemController {
6 | constructor(private readonly systemService: SystemService) {}
7 |
8 | @Post('info')
9 | async getSystemInfo() {
10 | return this.systemService.getSystemInfo()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/server/src/modules/user-configures/dto/create-user-configure.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsBoolean, IsOptional, IsString } from 'class-validator'
2 |
3 | export class CreateUserConfigureDto {
4 | @IsOptional()
5 | @IsBoolean()
6 | isTaskNotify?: boolean
7 |
8 | @IsOptional()
9 | @IsBoolean()
10 | isMatterNotify?: boolean
11 |
12 | @IsOptional()
13 | @IsString()
14 | serverChanSckey?: string
15 | }
16 |
--------------------------------------------------------------------------------
/src/views/setting/base/style.scss:
--------------------------------------------------------------------------------
1 | .setting-base {
2 | .ant-card-meta-title {
3 | margin-bottom: 0 !important;
4 | }
5 |
6 | .meta-desc {
7 | line-height: 1.7;
8 | .loginname {
9 | margin-bottom: 8px;
10 | }
11 | }
12 |
13 | .poster {
14 | display: block;
15 | width: 370px;
16 | height: 305px;
17 | background: #f7f7f7;
18 | object-fit: cover;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/server/src/modules/tasks/dto/get-task.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, Matches } from 'class-validator'
2 | import { dateValidator } from '@/utils/validatorUtils'
3 |
4 | export class GetTaskDto {
5 | @IsString()
6 | @Matches(dateValidator.REGEXP, { message: dateValidator.MESSAGE })
7 | startDate: string
8 |
9 | @IsString()
10 | @Matches(dateValidator.REGEXP, { message: dateValidator.MESSAGE })
11 | endDate: string
12 | }
13 |
--------------------------------------------------------------------------------
/server/src/decorators/user.decorator.ts:
--------------------------------------------------------------------------------
1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'
2 |
3 | /**
4 | * 获取当前登录用户的装饰器
5 | * 使用示例: @User() user: any
6 | */
7 | export const User = createParamDecorator(
8 | (data: string, ctx: ExecutionContext) => {
9 | const request = ctx.switchToHttp().getRequest()
10 | const user = request.user
11 |
12 | return data ? user?.[data] : user
13 | },
14 | )
15 |
--------------------------------------------------------------------------------
/server/src/modules/inner-messages/dto/create-inner-message.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsBoolean,
3 | IsNotEmpty,
4 | IsNumber,
5 | IsOptional,
6 | IsString,
7 | } from 'class-validator'
8 |
9 | export class CreateInnerMessageDto {
10 | @IsNotEmpty()
11 | @IsString()
12 | content: string
13 |
14 | @IsOptional()
15 | @IsNumber()
16 | type?: number
17 |
18 | @IsOptional()
19 | @IsBoolean()
20 | hasRead?: boolean
21 | }
22 |
--------------------------------------------------------------------------------
/server/src/entities/date.entity.ts:
--------------------------------------------------------------------------------
1 | import { CreateDateColumn, UpdateDateColumn } from 'typeorm'
2 | import { dateTransformer } from '@/utils/transformerUtils'
3 |
4 | export class DateEntity {
5 | // mysql 默认值需要设置 CURRENT_TIMESTAMP
6 | @CreateDateColumn({ name: 'created_at', transformer: dateTransformer() })
7 | createdAt: Date
8 |
9 | @UpdateDateColumn({ name: 'updated_at', transformer: dateTransformer() })
10 | updatedAt: Date
11 | }
12 |
--------------------------------------------------------------------------------
/src/views/setting/notification/style.scss:
--------------------------------------------------------------------------------
1 | .notification {
2 | .list {
3 | margin-bottom: 10px;
4 | display: flex;
5 | border-bottom: 1px solid #e6e6e6;
6 |
7 | .left {
8 | flex: 1;
9 | }
10 |
11 | .title {
12 | color: rgba(0, 0, 0, 0.65);
13 | }
14 |
15 | .description {
16 | color: rgba(0, 0, 0, 0.45);
17 | }
18 |
19 | .ant-switch {
20 | align-self: center;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/assets/styles/mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin flex-center($x: center, $y: center) {
2 | display: flex;
3 | justify-content: $x;
4 | align-items: $y;
5 | }
6 |
7 | @mixin lineClamp($line: 3) {
8 | display: -webkit-box;
9 | overflow: hidden;
10 | -webkit-line-clamp: $line;
11 | /* autoprefixer: off */
12 | -webkit-box-orient: vertical;
13 | }
14 |
15 | @mixin ellipsis() {
16 | white-space: nowrap;
17 | overflow: hidden;
18 | text-overflow: ellipsis;
19 | }
20 |
--------------------------------------------------------------------------------
/src/assets/styles/antd/modal.scss:
--------------------------------------------------------------------------------
1 | .ant-modal-wrap {
2 | .ant-form-item {
3 | &:not(:nth-last-child(1)) {
4 | margin-bottom: 10px;
5 | }
6 |
7 | .ant-calendar-picker {
8 | width: 100%;
9 | }
10 |
11 | .ant-form-item-control-wrapper {
12 | flex: 1;
13 | }
14 |
15 | textarea {
16 | resize: auto;
17 | }
18 | }
19 | }
20 |
21 | @media (max-width: 768px) {
22 | .ant-modal {
23 | top: 0 !important;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/router/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import CONFIG from '@/config'
3 | import { MainRoutes } from './routes'
4 | import { BrowserRouter as Router } from 'react-router'
5 |
6 | export default function () {
7 | const [mounted, setMounted] = React.useState(false)
8 |
9 | React.useEffect(() => {
10 | setMounted(true)
11 | }, [])
12 |
13 | return mounted ? (
14 |
15 |
16 |
17 | ) : null
18 | }
19 |
--------------------------------------------------------------------------------
/server/src/modules/mail/mail.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { MailService } from './mail.service'
3 | import { MailScheduleService } from './mail-schedule.service'
4 | import { ConfigModule } from '@nestjs/config'
5 | import { RemindersModule } from '../reminders/reminders.module'
6 |
7 | @Module({
8 | imports: [ConfigModule, RemindersModule],
9 | providers: [MailService, MailScheduleService],
10 | exports: [MailService],
11 | })
12 | export class MailModule {}
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 | /dist
14 |
15 | ./config/**
16 | /dev-dist
17 | # misc
18 | .DS_Store
19 | .env.local
20 | .env.development.local
21 | .env.test.local
22 | .env.production.local
23 | .idea/
24 |
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 | .eslintcache
29 | package-lock.json
30 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module 'virtual:pwa-register' {
4 | export interface RegisterSWOptions {
5 | immediate?: boolean
6 | onNeedRefresh?: () => void
7 | onOfflineReady?: () => void
8 | onRegistered?: (registration: ServiceWorkerRegistration | undefined) => void
9 | onRegisterError?: (error: any) => void
10 | }
11 |
12 | export function registerSW(
13 | options?: RegisterSWOptions,
14 | ): (reloadPage?: boolean) => Promise
15 | }
16 |
--------------------------------------------------------------------------------
/server/src/modules/tasks/dto/create-task.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsNotEmpty,
3 | IsNumber,
4 | IsOptional,
5 | IsString,
6 | Min,
7 | Max,
8 | } from 'class-validator'
9 |
10 | export class CreateTaskDto {
11 | @IsNotEmpty()
12 | @IsString()
13 | content: string
14 |
15 | @IsNotEmpty()
16 | @IsNumber()
17 | date: number
18 |
19 | @IsOptional()
20 | @IsNumber()
21 | @Min(1)
22 | @Max(4)
23 | type?: number
24 |
25 | @IsOptional()
26 | @IsNumber()
27 | @Min(0)
28 | count?: number
29 | }
30 |
--------------------------------------------------------------------------------
/src/services/innerMessage.ts:
--------------------------------------------------------------------------------
1 | import http from '@/utils/http'
2 |
3 | // 查询
4 | export function serviceGetInnerMessage(params?: object) {
5 | return http.post(
6 | '/inner-messages/get',
7 | {
8 | ...params,
9 | },
10 | {
11 | headers: {
12 | errorAlert: 'false',
13 | },
14 | },
15 | )
16 | }
17 |
18 | // 标志已读
19 | export function serviceUpdateInnerMessageHasRead(id: unknown) {
20 | return http.put(`/inner-messages/${id}`, null, {
21 | headers: { successAlert: 'true' },
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/server/src/modules/auth/auth.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { ConfigModule } from '@nestjs/config'
3 | import { HttpModule } from '@nestjs/axios'
4 | import { AuthService } from './auth.service'
5 | import { AuthController } from './auth.controller'
6 | import { UsersModule } from '../users/users.module'
7 |
8 | @Module({
9 | imports: [UsersModule, ConfigModule, HttpModule],
10 | controllers: [AuthController],
11 | providers: [AuthService],
12 | exports: [AuthService],
13 | })
14 | export class AuthModule {}
15 |
--------------------------------------------------------------------------------
/server/src/modules/bill-types/bill-types.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { TypeOrmModule } from '@nestjs/typeorm'
3 | import { BillTypesService } from './bill-types.service'
4 | import { BillTypesController } from './bill-types.controller'
5 | import { BillType } from './entities/bill-type.entity'
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature([BillType])],
9 | controllers: [BillTypesController],
10 | providers: [BillTypesService],
11 | exports: [BillTypesService],
12 | })
13 | export class BillTypesModule {}
14 |
--------------------------------------------------------------------------------
/server/src/modules/todo-lists/todo-lists.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { TypeOrmModule } from '@nestjs/typeorm'
3 | import { TodoListsService } from './todo-lists.service'
4 | import { TodoListsController } from './todo-lists.controller'
5 | import { TodoList } from './entities/todo-list.entity'
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature([TodoList])],
9 | controllers: [TodoListsController],
10 | providers: [TodoListsService],
11 | exports: [TodoListsService],
12 | })
13 | export class TodoListsModule {}
14 |
--------------------------------------------------------------------------------
/server/src/modules/logs/logs.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { TypeOrmModule } from '@nestjs/typeorm'
3 | import { LogsService } from './logs.service'
4 | import { LogsController } from './logs.controller'
5 | import { Log } from './entities/log.entity'
6 | import { Company } from '../company/entities/company.entity'
7 |
8 | @Module({
9 | imports: [TypeOrmModule.forFeature([Log, Company])],
10 | controllers: [LogsController],
11 | providers: [LogsService],
12 | exports: [LogsService],
13 | })
14 | export class LogsModule {}
15 |
--------------------------------------------------------------------------------
/server/.env.development:
--------------------------------------------------------------------------------
1 | PORT=7003
2 | NODE_ENV=development
3 |
4 | MYSQL_HOST=localhost
5 | MYSQL_PORT=3306
6 | MYSQL_USERNAME=root
7 | MYSQL_PASSWORD=123456
8 | MYSQL_DATABASE=tomato_work
9 | MYSQL_SYNCHRONIZE=true
10 |
11 | GITHUB_CLIENT_ID=489b39e1f91d934128c8
12 | GITHUB_CLIENT_SECRET=9ec2cf95bee7f1451792ce8124075cce7b66450d
13 |
14 | MAIL_HOST=smtp.mxhichina.com
15 | MAIL_PORT=465
16 | MAIL_SECURE=true
17 | MAIL_USER=system@example.com
18 | MAIL_PASS=123123
19 | # 通知邮箱
20 | ADMIN_EMAIL=xjh22222228@gmail.com
21 |
22 | CORS_ORIGIN=*
23 | APP_TITLE=Tomato Work
24 |
--------------------------------------------------------------------------------
/server/src/modules/memorandums/entities/memorandum.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
2 | import { DateEntity } from '@/entities/date.entity'
3 |
4 | @Entity('memorandums')
5 | export class Memorandum extends DateEntity {
6 | @PrimaryGeneratedColumn('uuid')
7 | id: string
8 |
9 | @Column()
10 | uid: number
11 |
12 | @Column({ name: 'sort_index', default: 0 })
13 | sortIndex: number
14 |
15 | @Column()
16 | title: string
17 |
18 | @Column({ type: 'longtext', nullable: true })
19 | markdown: string
20 | }
21 |
--------------------------------------------------------------------------------
/server/src/modules/memorandums/memorandums.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { TypeOrmModule } from '@nestjs/typeorm'
3 | import { MemorandumsService } from './memorandums.service'
4 | import { MemorandumsController } from './memorandums.controller'
5 | import { Memorandum } from './entities/memorandum.entity'
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature([Memorandum])],
9 | controllers: [MemorandumsController],
10 | providers: [MemorandumsService],
11 | exports: [MemorandumsService],
12 | })
13 | export class MemorandumsModule {}
14 |
--------------------------------------------------------------------------------
/server/src/modules/tasks/tasks.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { TypeOrmModule } from '@nestjs/typeorm'
3 | import { TasksService } from './tasks.service'
4 | import { TasksController } from './tasks.controller'
5 | import { Task } from './entities/task.entity'
6 | import { TaskScheduleService } from './task-schedule.service'
7 |
8 | @Module({
9 | imports: [TypeOrmModule.forFeature([Task])],
10 | controllers: [TasksController],
11 | providers: [TasksService, TaskScheduleService],
12 | exports: [TasksService],
13 | })
14 | export class TasksModule {}
15 |
--------------------------------------------------------------------------------
/src/views/setting/inner-message/style.scss:
--------------------------------------------------------------------------------
1 | .inner-message {
2 | position: relative;
3 | height: 100%;
4 | font-size: 12px;
5 |
6 | .ant-table {
7 | font-size: 12px;
8 | }
9 |
10 | .unread-dot {
11 | display: block;
12 | color: #f50;
13 | transform: scale(0.9);
14 | }
15 |
16 | .unread-row {
17 | padding-left: 0 !important;
18 | padding-right: 0 !important;
19 | }
20 |
21 | .action-group {
22 | position: absolute;
23 | bottom: 18px;
24 | left: 20px;
25 |
26 | .ant-btn {
27 | margin-right: 15px;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/server/src/modules/bill-types/dto/create-bill-type.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsEnum,
3 | IsNotEmpty,
4 | IsOptional,
5 | IsString,
6 | MaxLength,
7 | Min,
8 | } from 'class-validator'
9 |
10 | export enum BillType {
11 | INCOME = 1,
12 | EXPENSE = 2,
13 | }
14 |
15 | export class CreateBillTypeDto {
16 | @IsNotEmpty()
17 | @IsString()
18 | @MaxLength(20, { message: '类型名称不能超过20个字符' })
19 | name: string
20 |
21 | @IsNotEmpty()
22 | @IsEnum(BillType, { message: '类型必须是1(收入)或2(支出)' })
23 | type: number
24 |
25 | @IsOptional()
26 | @Min(0)
27 | sortIndex?: number
28 | }
29 |
--------------------------------------------------------------------------------
/server/src/modules/company/company.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { TypeOrmModule } from '@nestjs/typeorm'
3 | import { CompanyService } from './company.service'
4 | import { CompanyController } from './company.controller'
5 | import { Company } from './entities/company.entity'
6 | import { LogsModule } from '../logs/logs.module'
7 |
8 | @Module({
9 | imports: [TypeOrmModule.forFeature([Company]), LogsModule],
10 | controllers: [CompanyController],
11 | providers: [CompanyService],
12 | exports: [CompanyService],
13 | })
14 | export class CompanyModule {}
15 |
--------------------------------------------------------------------------------
/server/src/modules/mail/mail-schedule.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger } from '@nestjs/common'
2 | import { Cron } from '@nestjs/schedule'
3 | import { MailService } from './mail.service'
4 |
5 | @Injectable()
6 | export class MailScheduleService {
7 | private readonly logger = new Logger(MailScheduleService.name)
8 |
9 | constructor(private mailService: MailService) {}
10 |
11 | /**
12 | * 每分钟检查一次是否有提醒需要发送
13 | */
14 | @Cron('0 * * * * *')
15 | async handleCron() {
16 | this.logger.debug('定时任务 - 检查提醒事项')
17 | await this.mailService.sendReminder()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/views/log/style.scss:
--------------------------------------------------------------------------------
1 | .createlog-page {
2 | height: 100%;
3 | background-color: #fff;
4 | padding: 15px;
5 | overflow: auto;
6 |
7 | .icon-arrow {
8 | font-size: 22px;
9 | cursor: pointer;
10 | }
11 | .top {
12 | display: flex;
13 | align-items: center;
14 | justify-content: space-between;
15 | margin-bottom: 30px;
16 | .title {
17 | font-size: 26px;
18 | align-self: center;
19 | margin-bottom: 0;
20 | }
21 | }
22 | .footbar {
23 | display: flex;
24 | justify-content: center;
25 | column-gap: 20px;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/server/src/modules/bill-types/entities/bill-type.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
2 | import { DateEntity } from '@/entities/date.entity'
3 |
4 | @Entity('bill_types')
5 | export class BillType extends DateEntity {
6 | @PrimaryGeneratedColumn('uuid', { name: 'id' })
7 | id: string
8 |
9 | @Column()
10 | uid: number
11 |
12 | @Column({ name: 'sort_index', default: 0 })
13 | sortIndex: number
14 |
15 | @Column({ length: 20, default: '' })
16 | name: string
17 |
18 | @Column({ default: 1, comment: '1=收入, 2=支出' })
19 | type: number
20 | }
21 |
--------------------------------------------------------------------------------
/server/src/modules/inner-messages/inner-messages.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { TypeOrmModule } from '@nestjs/typeorm'
3 | import { InnerMessagesService } from './inner-messages.service'
4 | import { InnerMessagesController } from './inner-messages.controller'
5 | import { InnerMessage } from './entities/inner-message.entity'
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature([InnerMessage])],
9 | controllers: [InnerMessagesController],
10 | providers: [InnerMessagesService],
11 | exports: [InnerMessagesService],
12 | })
13 | export class InnerMessagesModule {}
14 |
--------------------------------------------------------------------------------
/server/src/modules/users/users.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { TypeOrmModule } from '@nestjs/typeorm'
3 | import { UsersService } from './users.service'
4 | import { UsersController } from './users.controller'
5 | import { User } from './entities/user.entity'
6 | import { UserControllerAuthGuard } from './guards/user-controller-auth.guard'
7 |
8 | @Module({
9 | imports: [TypeOrmModule.forFeature([User])],
10 | controllers: [UsersController],
11 | providers: [UsersService, UserControllerAuthGuard],
12 | exports: [UsersService],
13 | })
14 | export class UsersModule {}
15 |
--------------------------------------------------------------------------------
/src/components/footer/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './style.scss'
3 | import CONFIG from '@/config'
4 |
5 | const currentYear = new Date().getFullYear()
6 |
7 | export default () => {
8 | return (
9 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/server/src/modules/reminders/reminders.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { TypeOrmModule } from '@nestjs/typeorm'
3 | import { RemindersService } from './reminders.service'
4 | import { RemindersController } from './reminders.controller'
5 | import { Reminder } from './entities/reminder.entity'
6 | import { User } from '../users/entities/user.entity'
7 |
8 | @Module({
9 | imports: [TypeOrmModule.forFeature([Reminder, User])],
10 | controllers: [RemindersController],
11 | providers: [RemindersService],
12 | exports: [RemindersService],
13 | })
14 | export class RemindersModule {}
15 |
--------------------------------------------------------------------------------
/server/src/modules/user-configures/user-configures.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { TypeOrmModule } from '@nestjs/typeorm'
3 | import { UserConfiguresService } from './user-configures.service'
4 | import { UserConfiguresController } from './user-configures.controller'
5 | import { UserConfigure } from './entities/user-configure.entity'
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature([UserConfigure])],
9 | controllers: [UserConfiguresController],
10 | providers: [UserConfiguresService],
11 | exports: [UserConfiguresService],
12 | })
13 | export class UserConfiguresModule {}
14 |
--------------------------------------------------------------------------------
/src/services/todayTask.ts:
--------------------------------------------------------------------------------
1 | import http from '@/utils/http'
2 |
3 | // 创建
4 | export function serviceCreateTask(data: object) {
5 | return http.post('/task/add', data, {
6 | headers: { successAlert: 'true' },
7 | })
8 | }
9 |
10 | // 查询
11 | export function serviceGetTask(params?: object) {
12 | return http.post('/task/getAll', { ...params })
13 | }
14 |
15 | // 删除
16 | export function serviceDeleteTask(id: unknown) {
17 | return http.post(`/task/delete`, { id })
18 | }
19 |
20 | // 更新
21 | export function serviceUpdateTask(id: unknown, data?: object) {
22 | return http.post(`/task/update`, { id, ...data })
23 | }
24 |
--------------------------------------------------------------------------------
/server/src/modules/company/dto/create-company.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsDateString,
3 | IsNotEmpty,
4 | IsNumber,
5 | IsOptional,
6 | IsString,
7 | } from 'class-validator'
8 |
9 | export class CreateCompanyDto {
10 | @IsNotEmpty()
11 | @IsString()
12 | companyName: string
13 |
14 | @IsNotEmpty()
15 | @IsDateString()
16 | startDate: string
17 |
18 | @IsOptional()
19 | @IsDateString()
20 | endDate?: string
21 |
22 | @IsNotEmpty()
23 | @IsString()
24 | remark: string
25 |
26 | @IsNotEmpty()
27 | @IsNumber()
28 | amount: number
29 |
30 | @IsOptional()
31 | @IsDateString()
32 | expectLeaveDate?: string
33 | }
34 |
--------------------------------------------------------------------------------
/server/src/modules/tasks/task-schedule.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger } from '@nestjs/common'
2 | import { Cron } from '@nestjs/schedule'
3 | import { TasksService } from './tasks.service'
4 |
5 | @Injectable()
6 | export class TaskScheduleService {
7 | private readonly logger = new Logger(TaskScheduleService.name)
8 |
9 | constructor(private tasksService: TasksService) {}
10 |
11 | /**
12 | * 每天凌晨 0 点 0 分 1 秒执行
13 | */
14 | @Cron('1 0 0 * * *')
15 | // @Cron('*/5 * * * * *') // 每 5 秒执行一次,用于调试
16 | async handleCron() {
17 | this.logger.debug('定时任务 - 今日待办未完成设置')
18 | this.tasksService.updateBeforeToDay()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/server/src/modules/todo-lists/dto/get-todo-list.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsNumber, IsOptional, IsString, Matches } from 'class-validator'
2 | import { dateValidator } from '@/utils/validatorUtils'
3 | import { PaginationDto } from '@/dtos/pagination.dto'
4 |
5 | export class GetTodoListDto extends PaginationDto {
6 | @IsString()
7 | @Matches(dateValidator.REGEXP, { message: dateValidator.MESSAGE })
8 | @IsOptional()
9 | startDate?: string
10 |
11 | @IsString()
12 | @Matches(dateValidator.REGEXP, { message: dateValidator.MESSAGE })
13 | @IsOptional()
14 | endDate?: string
15 |
16 | @IsNumber()
17 | @IsOptional()
18 | status?: number
19 | }
20 |
--------------------------------------------------------------------------------
/server/src/modules/auth/auth.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, Body } from '@nestjs/common'
2 | import { AuthService } from './auth.service'
3 | import { LoginDto } from './dto/login.dto'
4 | import { GithubLoginDto } from './dto/github-login.dto'
5 |
6 | @Controller('passport')
7 | export class AuthController {
8 | constructor(private readonly authService: AuthService) {}
9 |
10 | @Post('login')
11 | login(@Body() loginDto: LoginDto) {
12 | return this.authService.login(loginDto)
13 | }
14 |
15 | @Post('code')
16 | githubLogin(@Body() githubLoginDto: GithubLoginDto) {
17 | return this.authService.githubLogin(githubLoginDto)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/server/src/modules/bills/bills.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { TypeOrmModule } from '@nestjs/typeorm'
3 | import { BillsService } from './bills.service'
4 | import { BillsController } from './bills.controller'
5 | import { Bill } from './entities/bill.entity'
6 | import { BillTypesModule } from '../bill-types/bill-types.module'
7 | import { BillType } from '../bill-types/entities/bill-type.entity'
8 |
9 | @Module({
10 | imports: [TypeOrmModule.forFeature([Bill, BillType]), BillTypesModule],
11 | controllers: [BillsController],
12 | providers: [BillsService],
13 | exports: [BillsService],
14 | })
15 | export class BillsModule {}
16 |
--------------------------------------------------------------------------------
/src/views/main/style.scss:
--------------------------------------------------------------------------------
1 | .home-main {
2 | min-height: 100vh;
3 |
4 | .ant-layout {
5 | min-height: 100vh;
6 | height: 100%;
7 | }
8 |
9 | .home-layout {
10 | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
11 | border-radius: 5px;
12 |
13 | #container {
14 | position: relative;
15 | min-height: auto;
16 | padding: 15px 0 15px 15px;
17 | overflow: hidden;
18 | flex: 1;
19 | display: flex;
20 | flex-direction: column;
21 |
22 | & > div {
23 | flex: 1;
24 | display: flex;
25 | flex-direction: column;
26 | overflow: hidden;
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/server/src/modules/users/users.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, Body, UseGuards } from '@nestjs/common'
2 | import { UsersService } from './users.service'
3 | import { UpdateUserDto } from './dto/update-user.dto'
4 | import { UserAuthGuard } from '@/guards/user-auth.guard'
5 | import { User } from '@/decorators/user.decorator'
6 |
7 | @Controller('user')
8 | @UseGuards(UserAuthGuard)
9 | export class UsersController {
10 | constructor(private readonly usersService: UsersService) {}
11 |
12 | @Post('update')
13 | update(@User('uid') uid: number, @Body() updateUserDto: UpdateUserDto) {
14 | return this.usersService.update(uid, updateUserDto)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/server/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core'
2 | import { ValidationPipe } from '@nestjs/common'
3 | import { AppModule } from './app.module'
4 |
5 | async function bootstrap() {
6 | const app = await NestFactory.create(AppModule)
7 |
8 | // 设置全局前缀
9 | app.setGlobalPrefix('api')
10 |
11 | // 启用CORS
12 | app.enableCors()
13 |
14 | // 配置全局验证管道
15 | app.useGlobalPipes(
16 | new ValidationPipe({
17 | whitelist: true,
18 | transform: true,
19 | }),
20 | )
21 |
22 | const port = process.env.PORT || 7003
23 | await app.listen(port, '0.0.0.0')
24 | console.log(`Server running on http://localhost:${port}`)
25 | }
26 | bootstrap()
27 |
--------------------------------------------------------------------------------
/src/store/middlewares.ts:
--------------------------------------------------------------------------------
1 | import { Middleware } from '@reduxjs/toolkit'
2 | import { SET_USER_INFO } from '@/store/userSlice'
3 | import { LOCAL_STORAGE } from '@/constants'
4 | import { RootState } from '@/store/index'
5 |
6 | export const authMiddleware: Middleware<{}, RootState> =
7 | () => (next) => (action: unknown) => {
8 | if (SET_USER_INFO.match(action) && !!action.payload.token) {
9 | localStorage.setItem(LOCAL_STORAGE.USER, JSON.stringify(action.payload))
10 | localStorage.setItem(LOCAL_STORAGE.TOKEN, action.payload.token)
11 | localStorage.setItem(LOCAL_STORAGE.LOGIN_NAME, action.payload.loginName)
12 | }
13 |
14 | return next(action)
15 | }
16 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "ES2023",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true,
14 | "skipLibCheck": true,
15 | "strictNullChecks": true,
16 | "forceConsistentCasingInFileNames": true,
17 | "noImplicitAny": false,
18 | "strictBindCallApply": false,
19 | "noFallthroughCasesInSwitch": false,
20 | "paths": {
21 | "@/*": ["src/*"]
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/views/log/constants.ts:
--------------------------------------------------------------------------------
1 | // 日报
2 | const LOG_DAILY = 1
3 | // 周报
4 | const LOG_WEEK = 2
5 | // 月报
6 | const LOG_MONTH = 3
7 |
8 | export const LOG_LIST = [
9 | {
10 | key: LOG_DAILY,
11 | name: '日报',
12 | doneTitle: '今日完成工作',
13 | undoneTitle: '今日未完成工作',
14 | planTitle: '明天工作计划',
15 | summaryTitle: '工作总结',
16 | },
17 | {
18 | key: LOG_WEEK,
19 | name: '周报',
20 | doneTitle: '本周完成工作',
21 | undoneTitle: '本周未完成工作',
22 | planTitle: '下周工作计划',
23 | summaryTitle: '工作总结',
24 | },
25 | {
26 | key: LOG_MONTH,
27 | name: '月报',
28 | doneTitle: '本月完成工作',
29 | undoneTitle: '本月未完成工作',
30 | planTitle: '下月工作计划',
31 | summaryTitle: '工作总结',
32 | },
33 | ]
34 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "paths": {
5 | "@/*": ["*"]
6 | },
7 | "target": "ESNext",
8 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
9 | "types": ["vite/client"],
10 | "allowJs": false,
11 | "skipLibCheck": true,
12 | "esModuleInterop": false,
13 | "allowSyntheticDefaultImports": true,
14 | "strict": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "experimentalDecorators": true,
17 | "module": "ESNext",
18 | "moduleResolution": "Node",
19 | "resolveJsonModule": true,
20 | "isolatedModules": true,
21 | "noEmit": true,
22 | "jsx": "react-jsx"
23 | },
24 | "include": [
25 | "src"
26 | ]
27 | }
--------------------------------------------------------------------------------
/server/src/modules/bills/dto/create-bill.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsDate,
3 | IsNotEmpty,
4 | IsNumber,
5 | IsOptional,
6 | IsString,
7 | IsUUID,
8 | MaxLength,
9 | } from 'class-validator'
10 | import { Type } from 'class-transformer'
11 |
12 | export class CreateBillDto {
13 | @IsNotEmpty()
14 | @IsDate()
15 | @Type(() => Date)
16 | date: Date
17 |
18 | @IsNotEmpty()
19 | @IsUUID()
20 | typeId: string
21 |
22 | @IsNotEmpty()
23 | @IsNumber()
24 | price: number
25 |
26 | @IsOptional()
27 | @IsNumber()
28 | originalAmount: number
29 |
30 | @IsOptional()
31 | @IsString()
32 | @MaxLength(250)
33 | remark?: string
34 |
35 | @IsOptional()
36 | @IsString()
37 | imgs?: string
38 | }
39 |
--------------------------------------------------------------------------------
/server/src/modules/inner-messages/entities/inner-message.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | Column,
4 | PrimaryGeneratedColumn,
5 | CreateDateColumn,
6 | UpdateDateColumn,
7 | } from 'typeorm'
8 |
9 | @Entity('inner_messages')
10 | export class InnerMessage {
11 | @PrimaryGeneratedColumn('uuid')
12 | id: string
13 |
14 | @Column()
15 | uid: number
16 |
17 | @Column()
18 | content: string
19 |
20 | @Column({ default: 0, comment: '消息类型, 0=系统消息' })
21 | type: number
22 |
23 | @Column({ name: 'has_read', default: false })
24 | hasRead: boolean
25 |
26 | @CreateDateColumn({ name: 'created_at' })
27 | createdAt: Date
28 |
29 | @UpdateDateColumn({ name: 'updated_at' })
30 | updatedAt: Date
31 | }
32 |
--------------------------------------------------------------------------------
/src/hooks/useCreation.ts:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react'
2 |
3 | export default function useCreation(factory: () => T, deps: any[]) {
4 | const { current } = useRef({
5 | deps,
6 | obj: undefined as undefined | T,
7 | initialized: false,
8 | })
9 | if (current.initialized === false || !depsAreSame(current.deps, deps)) {
10 | current.deps = deps
11 | current.obj = factory()
12 | current.initialized = true
13 | }
14 | return current.obj as T
15 | }
16 |
17 | function depsAreSame(oldDeps: any[], deps: any[]): boolean {
18 | if (oldDeps === deps) return true
19 | for (let i = 0; i < oldDeps.length; i++) {
20 | if (oldDeps[i] !== deps[i]) return false
21 | }
22 |
23 | return true
24 | }
25 |
--------------------------------------------------------------------------------
/src/views/bill/style.scss:
--------------------------------------------------------------------------------
1 | // Index
2 | .capital-flow {
3 | .poly {
4 | display: flex;
5 | margin-top: 10px;
6 | font-size: 14px;
7 |
8 | .item-amount {
9 | display: flex;
10 | align-items: center;
11 | margin-right: 30px;
12 | }
13 |
14 | .ant-statistic-content,
15 | .ant-statistic-content-value-decimal {
16 | font-size: 14px;
17 | color: #666;
18 | }
19 | }
20 | }
21 |
22 | // Type
23 | .capital-flow-type {
24 | .button-group {
25 | padding: 10px 20px;
26 | margin-bottom: 10px;
27 | background: #fff;
28 |
29 | .ant-btn {
30 | margin-right: 10px;
31 | }
32 | }
33 |
34 | .ant-table-wrapper {
35 | overflow-y: auto;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/sidebar/style.scss:
--------------------------------------------------------------------------------
1 | $bg-color: #304156;
2 | .sidebar {
3 | background: $bg-color !important;
4 | user-select: none;
5 | position: sticky;
6 | top: 0;
7 | height: 100vh;
8 |
9 | .ant-menu-dark {
10 | background: $bg-color;
11 | }
12 |
13 | .ant-menu-sub {
14 | background: $bg-color !important;
15 | box-shadow: none !important;
16 | }
17 |
18 | .sider-menu-logo {
19 | overflow: hidden;
20 | height: 64px;
21 | line-height: 64px;
22 | background: #2b2f3a;
23 | color: #fff;
24 | font-size: 20px;
25 | font-weight: 600;
26 | text-align: center;
27 | img {
28 | width: 30px;
29 | height: 30px;
30 | pointer-events: none;
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/server/scripts/init.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'node:fs'
2 | import * as path from 'node:path'
3 |
4 | const join = (p) => path.resolve(p)
5 |
6 | try {
7 | const DEVELOPMENT_PATH = '.env.development'
8 | const LOCAL_PATH = '.env.local'
9 | const PRODUCTION_PATH = '.env.production'
10 | const envDevelopment = fs.readFileSync(join(DEVELOPMENT_PATH), 'utf8')
11 | const existsLocal = fs.existsSync(join(LOCAL_PATH))
12 | const existsProduction = fs.existsSync(join(PRODUCTION_PATH))
13 | if (!existsLocal) {
14 | fs.writeFileSync(join(LOCAL_PATH), envDevelopment)
15 | }
16 | if (!existsProduction) {
17 | fs.writeFileSync(join(PRODUCTION_PATH), envDevelopment)
18 | }
19 | } catch (error) {
20 | console.error(error)
21 | }
22 |
--------------------------------------------------------------------------------
/server/src/utils/transformerUtils.ts:
--------------------------------------------------------------------------------
1 | import * as dayjs from 'dayjs'
2 |
3 | export function dateTransformer(format: string = 'YYYY-MM-DD HH:mm:ss') {
4 | return {
5 | from: (value: any) => {
6 | if (!value) {
7 | return value
8 | }
9 | const numberValue = Number(value)
10 | if (numberValue) {
11 | value = numberValue
12 | }
13 | return dayjs(value).format(format)
14 | },
15 | to: (value: any) => value,
16 | } as const
17 | }
18 |
19 | export function numberTransformer() {
20 | return {
21 | from: (value: any) => {
22 | if (!value) {
23 | return value
24 | }
25 | return Number(value)
26 | },
27 | to: (value: any) => value,
28 | } as const
29 | }
30 |
--------------------------------------------------------------------------------
/server/src/modules/reminders/dto/get-reminder.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsNumber,
3 | IsOptional,
4 | IsString,
5 | Matches,
6 | IsBoolean,
7 | } from 'class-validator'
8 | import { dateValidator } from '@/utils/validatorUtils'
9 | import { PaginationDto } from '@/dtos/pagination.dto'
10 |
11 | export class GetReminderDto extends PaginationDto {
12 | @IsString()
13 | @Matches(dateValidator.REGEXP, { message: dateValidator.MESSAGE })
14 | @IsOptional()
15 | startDate?: string
16 |
17 | @IsString()
18 | @Matches(dateValidator.REGEXP, { message: dateValidator.MESSAGE })
19 | @IsOptional()
20 | endDate?: string
21 |
22 | @IsNumber()
23 | @IsOptional()
24 | type?: number
25 |
26 | @IsBoolean()
27 | @IsOptional()
28 | open?: boolean
29 | }
30 |
--------------------------------------------------------------------------------
/server/src/modules/logs/dto/get-log.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsNumber, IsOptional, IsString, Matches } from 'class-validator'
2 | import { dateValidator } from '@/utils/validatorUtils'
3 | import { PaginationDto } from '@/dtos/pagination.dto'
4 |
5 | export class GetLogDto extends PaginationDto {
6 | @IsString()
7 | @IsOptional()
8 | id?: string
9 |
10 | @IsString()
11 | @Matches(dateValidator.REGEXP, { message: dateValidator.MESSAGE })
12 | @IsOptional()
13 | startDate?: string
14 |
15 | @IsString()
16 | @Matches(dateValidator.REGEXP, { message: dateValidator.MESSAGE })
17 | @IsOptional()
18 | endDate?: string
19 |
20 | @IsString()
21 | @IsOptional()
22 | companyId?: string
23 |
24 | @IsNumber()
25 | @IsOptional()
26 | logType?: number
27 | }
28 |
--------------------------------------------------------------------------------
/server/src/modules/user-configures/entities/user-configure.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
2 | import { DateEntity } from '@/entities/date.entity'
3 |
4 | @Entity('user_configures')
5 | export class UserConfigure extends DateEntity {
6 | @PrimaryGeneratedColumn('uuid', { name: 'id' })
7 | id: string
8 |
9 | @Column({ unique: true })
10 | uid: number
11 |
12 | @Column({ name: 'is_task_notify', default: true, comment: '待办任务通知' })
13 | isTaskNotify: boolean
14 |
15 | @Column({ name: 'is_matter_notify', default: true, comment: '提醒事项通知' })
16 | isMatterNotify: boolean
17 |
18 | @Column({
19 | name: 'server_chan_sckey',
20 | default: '',
21 | comment: '企业微信API KEY',
22 | })
23 | serverChanSckey: string
24 | }
25 |
--------------------------------------------------------------------------------
/src/services/todoList.ts:
--------------------------------------------------------------------------------
1 | import http from '@/utils/http'
2 |
3 | // 创建
4 | export function serviceCreateTodoList(data: object) {
5 | return http.post('/todo-list/add', data, {
6 | headers: {
7 | successAlert: 'true',
8 | },
9 | })
10 | }
11 |
12 | // 查询
13 | export function serviceGetTodoList(params?: object) {
14 | return http.post('/todo-list/getAll', { ...params })
15 | }
16 |
17 | // 删除
18 | export function serviceDeleteTodoList(id: unknown) {
19 | return http.post(
20 | `/todo-list/delete`,
21 | { id },
22 | {
23 | headers: { successAlert: 'true' },
24 | },
25 | )
26 | }
27 |
28 | // 更新
29 | export function serviceUpdateTodoList(id: unknown, data?: object) {
30 | return http.post(`/todo-list/update`, { id, ...data })
31 | }
32 |
--------------------------------------------------------------------------------
/server/src/modules/todo-lists/entities/todo-list.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | Column,
4 | PrimaryGeneratedColumn,
5 | CreateDateColumn,
6 | UpdateDateColumn,
7 | } from 'typeorm'
8 | import { dateTransformer } from '@/utils/transformerUtils'
9 |
10 | @Entity('todo_lists')
11 | export class TodoList {
12 | @PrimaryGeneratedColumn('uuid')
13 | id: string
14 |
15 | @Column()
16 | uid: number
17 |
18 | @Column('text')
19 | content: string
20 |
21 | @Column({ type: 'tinyint', default: 1, comment: '状态, 1=进行中, 2=完成' })
22 | status: number
23 |
24 | @CreateDateColumn({ name: 'created_at', transformer: dateTransformer() })
25 | createdAt: Date
26 |
27 | @UpdateDateColumn({ name: 'updated_at', transformer: dateTransformer() })
28 | updatedAt: Date
29 | }
30 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | const { VITE_HTTP_BASE, VITE_DEV } = import.meta.env
2 | const PROD = VITE_DEV === 'false'
3 | const isDevelopment = !PROD
4 |
5 | const CONFIG = {
6 | isProduction: PROD,
7 | isDevelopment,
8 | // 路由 basename
9 | baseURL: '/',
10 | // 网页标题
11 | title: 'Tomato Work',
12 | http: {
13 | baseURL: VITE_HTTP_BASE as string,
14 | },
15 |
16 | // 申请地址:https://github.com/settings/developers
17 | github: {
18 | clientId: PROD ? '789d87c19dd5ed1dc42e' : '489b39e1f91d934128c8',
19 | // 授权成功重定向页面
20 | redirectUri: location.origin,
21 |
22 | // 可忽略,只是用于页面展示
23 | repositoryUrl: 'https://github.com/xjh22222228/tomato-work',
24 | bug: 'https://github.com/xjh22222228/tomato-work/issues',
25 | },
26 | }
27 |
28 | export default CONFIG
29 |
--------------------------------------------------------------------------------
/src/views/index/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import './style.scss'
3 | import PenelGroup from './PenelGroup'
4 | import SystemInfo from './SystemInfo'
5 | import AmountChart from './AmountChart'
6 | import { getSystemInfo } from '@/store/systemSlice'
7 | import { useAppSelector, useAppDispatch } from '@/hooks'
8 |
9 | const HomeIndexPage: React.FC = function () {
10 | const systemInfo = useAppSelector((state) => state.system.info)
11 | const dispatch = useAppDispatch()
12 |
13 | useEffect(() => {
14 | dispatch(getSystemInfo())
15 | }, [])
16 |
17 | return (
18 |
23 | )
24 | }
25 |
26 | export default HomeIndexPage
27 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2018-present the xiejiahe. All rights reserved. MIT license.
2 | import React from 'react'
3 | import ReactDOM from 'react-dom/client'
4 | import './assets/styles/global.scss'
5 | import AppRoute from './router'
6 | import 'antd/dist/reset.css'
7 | import { Provider } from 'react-redux'
8 | import { ConfigProvider } from 'antd'
9 | import zhCN from 'antd/locale/zh_CN'
10 | import dayjs from 'dayjs'
11 | import zh from 'dayjs/locale/zh-cn'
12 | import store from '@/store'
13 | import './registerSW'
14 |
15 | dayjs.locale(zh)
16 |
17 | const root = ReactDOM.createRoot(
18 | document.getElementById('tomato-work-root') as HTMLElement,
19 | )
20 |
21 | root.render(
22 |
23 |
24 |
25 |
26 | ,
27 | )
28 |
--------------------------------------------------------------------------------
/server/src/modules/bills/dto/get-bill.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, Matches, IsOptional, IsNumber } from 'class-validator'
2 | import { dateValidator } from '@/utils/validatorUtils'
3 | import { PaginationDto } from '@/dtos/pagination.dto'
4 |
5 | export class GetBillDto extends PaginationDto {
6 | @Matches(dateValidator.REGEXP, { message: dateValidator.MESSAGE })
7 | @IsString()
8 | @IsOptional()
9 | startDate?: string
10 |
11 | @Matches(dateValidator.REGEXP, { message: dateValidator.MESSAGE })
12 | @IsString()
13 | @IsOptional()
14 | endDate?: string
15 |
16 | @IsString()
17 | @IsOptional()
18 | typeId?: string
19 |
20 | @IsNumber()
21 | @IsOptional()
22 | type?: number
23 |
24 | @IsString()
25 | @IsOptional()
26 | keyword?: string
27 |
28 | @IsString()
29 | @IsOptional()
30 | sort?: string
31 | }
32 |
--------------------------------------------------------------------------------
/server/src/modules/logs/dto/create-log.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsNotEmpty,
3 | IsNumber,
4 | IsString,
5 | Min,
6 | Max,
7 | IsOptional,
8 | IsDate,
9 | } from 'class-validator'
10 | import { Type } from 'class-transformer'
11 |
12 | export class CreateLogDto {
13 | @IsNotEmpty()
14 | @IsString()
15 | companyId: string
16 |
17 | @IsNotEmpty()
18 | @IsDate()
19 | @Type(() => Date)
20 | createdAt: Date
21 |
22 | @IsNotEmpty()
23 | @IsNumber()
24 | @Min(1)
25 | @Max(3)
26 | logType: number
27 |
28 | @IsString()
29 | @IsOptional()
30 | doneContent: string = ''
31 |
32 | @IsString()
33 | @IsOptional()
34 | undoneContent: string = ''
35 |
36 | @IsString()
37 | @IsOptional()
38 | planContent: string = ''
39 |
40 | @IsOptional()
41 | @IsString()
42 | summaryContent: string = ''
43 | }
44 |
--------------------------------------------------------------------------------
/server/src/modules/logs/entities/log.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
2 | import { DateEntity } from '@/entities/date.entity'
3 |
4 | @Entity('logs')
5 | export class Log extends DateEntity {
6 | @PrimaryGeneratedColumn('uuid')
7 | id: string
8 |
9 | @Column()
10 | uid: number
11 |
12 | @Column({ name: 'company_id' })
13 | companyId: string
14 |
15 | @Column({ name: 'log_type', comment: '日志类型, 1=日报、2=周报、3=月报' })
16 | logType: number
17 |
18 | @Column({ name: 'done_content', type: 'text' })
19 | doneContent: string
20 |
21 | @Column({ name: 'undone_content', type: 'text' })
22 | undoneContent: string
23 |
24 | @Column({ name: 'plan_content', type: 'text' })
25 | planContent: string
26 |
27 | @Column({ name: 'summary_content', type: 'text' })
28 | summaryContent: string
29 | }
30 |
--------------------------------------------------------------------------------
/server/netlify/functions/api.js:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core'
2 | import { AppModule } from '../../dist/src/app.module.js'
3 | import serverless from 'serverless-http'
4 | import { ValidationPipe } from '@nestjs/common'
5 |
6 | let cachedServer
7 |
8 | async function bootstrap() {
9 | if (!cachedServer) {
10 | const app = await NestFactory.create(AppModule)
11 | app.setGlobalPrefix('api')
12 | app.enableCors()
13 | app.useGlobalPipes(
14 | new ValidationPipe({
15 | whitelist: true,
16 | transform: true,
17 | }),
18 | )
19 | await app.init()
20 | cachedServer = serverless(app.getHttpAdapter().getInstance())
21 | }
22 | return cachedServer
23 | }
24 |
25 | export const handler = async (event, context) => {
26 | const server = await bootstrap()
27 | return server(event, context)
28 | }
29 |
--------------------------------------------------------------------------------
/src/services/reminder.ts:
--------------------------------------------------------------------------------
1 | import http from '@/utils/http'
2 |
3 | // 新增
4 | export function serviceCreateReminder(data: object) {
5 | return http.post('/reminder/add', data, {
6 | headers: { successAlert: 'true' },
7 | })
8 | }
9 |
10 | // 查询
11 | export function serviceGetReminder(params?: object) {
12 | return http.post('/reminder/getAll', { ...params })
13 | }
14 |
15 | // 删除
16 | export function serviceDeleteReminder(id: unknown) {
17 | return http.post(
18 | `/reminder/delete`,
19 | { id },
20 | {
21 | headers: { successAlert: 'true' },
22 | },
23 | )
24 | }
25 |
26 | // 更新
27 | export function serviceUpdateReminder(id: unknown, data: object) {
28 | return http.post(
29 | `/reminder/update`,
30 | { id, ...data },
31 | {
32 | headers: {
33 | successAlert: 'true',
34 | },
35 | },
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/no-data/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './style.scss'
3 | import { Button, Result } from 'antd'
4 | import NoDataSvg from '@/assets/img/common/no-data.svg'
5 |
6 | interface Props {
7 | onClick(e: React.MouseEvent): void
8 | message?: string
9 | }
10 |
11 | const NoData: React.FC = ({ onClick, message = '暂无数据' }) => {
12 | return (
13 |
20 | }
21 | title={message}
22 | extra={
23 |
26 | }
27 | status="info"
28 | style={{ marginTop: '50px' }}
29 | />
30 | )
31 | }
32 |
33 | export default React.memo(NoData)
34 |
--------------------------------------------------------------------------------
/server/src/modules/common/common.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { CommonController } from './common.controller'
3 | import { CommonService } from './common.service'
4 | import { BillsModule } from '../bills/bills.module'
5 | import { TasksModule } from '../tasks/tasks.module'
6 | import { TodoListsModule } from '../todo-lists/todo-lists.module'
7 | import { RemindersModule } from '../reminders/reminders.module'
8 | import { GuardsModule } from '@/guards/guards.module'
9 | import { UsersModule } from '../users/users.module'
10 |
11 | @Module({
12 | imports: [
13 | BillsModule,
14 | TasksModule,
15 | TodoListsModule,
16 | RemindersModule,
17 | GuardsModule,
18 | UsersModule,
19 | ],
20 | controllers: [CommonController],
21 | providers: [CommonService],
22 | exports: [CommonService],
23 | })
24 | export class CommonModule {}
25 |
--------------------------------------------------------------------------------
/server/src/modules/users/dto/create-user.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsEmail,
3 | IsNotEmpty,
4 | IsOptional,
5 | IsString,
6 | IsNumber,
7 | } from 'class-validator'
8 |
9 | export class CreateUserDto {
10 | @IsOptional()
11 | @IsNumber()
12 | uid?: number
13 |
14 | @IsOptional()
15 | @IsString()
16 | provider?: string
17 |
18 | @IsNotEmpty()
19 | @IsString()
20 | username: string
21 |
22 | @IsNotEmpty()
23 | @IsString()
24 | password: string
25 |
26 | @IsOptional()
27 | @IsEmail()
28 | email?: string
29 |
30 | @IsOptional()
31 | @IsString()
32 | avatarUrl?: string
33 |
34 | @IsOptional()
35 | @IsString()
36 | location?: string
37 |
38 | @IsOptional()
39 | @IsString()
40 | bio?: string
41 |
42 | @IsOptional()
43 | @IsString()
44 | ipAddr?: string
45 |
46 | @IsOptional()
47 | @IsString()
48 | token?: string
49 | }
50 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2018-present the xiejiahe. All rights reserved. MIT license.
2 | import { combineReducers, configureStore } from '@reduxjs/toolkit'
3 | import userReducer from './userSlice'
4 | import systemReducer from './systemSlice'
5 | import companyReducer from './companySlice'
6 | import { authMiddleware } from '@/store/middlewares'
7 |
8 | const rootReducer = combineReducers({
9 | user: userReducer,
10 | system: systemReducer,
11 | company: companyReducer,
12 | })
13 |
14 | const store = configureStore({
15 | reducer: rootReducer,
16 | middleware: (getDefaultMiddleware) =>
17 | getDefaultMiddleware().concat(authMiddleware),
18 | })
19 |
20 | export default store
21 |
22 | export type IStore = typeof store
23 | export type RootState = ReturnType
24 | export type AppDispatch = typeof store.dispatch
25 | export type GetState = () => RootState
26 |
--------------------------------------------------------------------------------
/server/src/modules/user-configures/user-configures.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, Body, UseGuards } from '@nestjs/common'
2 | import { UserAuthGuard } from '@/guards/user-auth.guard'
3 | import { UserConfiguresService } from './user-configures.service'
4 | import { UpdateUserConfigureDto } from './dto/update-user-configure.dto'
5 | import { User } from '@/decorators/user.decorator'
6 |
7 | @Controller('user-configure')
8 | @UseGuards(UserAuthGuard)
9 | export class UserConfiguresController {
10 | constructor(private readonly userConfiguresService: UserConfiguresService) {}
11 |
12 | @Post('get')
13 | findOne(@User() user) {
14 | return this.userConfiguresService.findOrCreate(user.uid)
15 | }
16 |
17 | @Post('update')
18 | update(@User() user, @Body() updateUserConfigureDto: UpdateUserConfigureDto) {
19 | return this.userConfiguresService.update(user.uid, updateUserConfigureDto)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/hooks/useDebounceFn.ts:
--------------------------------------------------------------------------------
1 | import { debounce } from 'lodash'
2 | import { useRef } from 'react'
3 | import useCreation from './useCreation'
4 |
5 | interface DebounceOptions {
6 | wait?: number
7 | leading?: boolean
8 | trailing?: boolean
9 | }
10 |
11 | type Fn = (...args: any) => any
12 |
13 | function useDebounceFn(fn: T, options?: DebounceOptions) {
14 | const fnRef = useRef(fn)
15 | fnRef.current = fn
16 |
17 | const wait = options?.wait ?? 1000
18 |
19 | const debounced = useCreation(
20 | () =>
21 | debounce(
22 | ((...args: any[]) => {
23 | return fnRef.current(...args)
24 | }) as T,
25 | wait,
26 | options,
27 | ),
28 | [],
29 | )
30 |
31 | return {
32 | run: debounced as unknown as T,
33 | cancel: debounced.cancel,
34 | flush: debounced.flush,
35 | }
36 | }
37 |
38 | export default useDebounceFn
39 |
--------------------------------------------------------------------------------
/src/services/memorandum.ts:
--------------------------------------------------------------------------------
1 | import http from '@/utils/http'
2 |
3 | // 新增
4 | export function serviceCreateMemorandum(data: object) {
5 | return http.post('/memorandum/add', data, {
6 | headers: { successAlert: 'true' },
7 | })
8 | }
9 |
10 | // 查询所有
11 | export function serviceGetMemorandum(params?: object) {
12 | return http.post('/memorandum/getAll', {
13 | ...params,
14 | })
15 | }
16 |
17 | // 通过id查询
18 | export function serviceGetMemorandumById(id: unknown) {
19 | return http.post(`/memorandum/get`, { id })
20 | }
21 |
22 | // 删除
23 | export function serviceDeleteMemorandum(id: unknown) {
24 | return http.post(`/memorandum/delete`, { id })
25 | }
26 |
27 | // 更新
28 | export function serviceUpdateMemorandum(id: unknown, data: object) {
29 | return http.post(
30 | `/memorandum/update`,
31 | { id, ...data },
32 | {
33 | headers: { successAlert: 'true' },
34 | },
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/server/src/modules/tasks/entities/task.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
2 | import { DateEntity } from '@/entities/date.entity'
3 | import { dateTransformer } from '@/utils/transformerUtils'
4 |
5 | export const enum TaskType {
6 | PENDING = 1,
7 | IN_PROGRESS = 2,
8 | COMPLETED = 3,
9 | UNCOMPLETED = 4,
10 | }
11 |
12 | @Entity('tasks')
13 | export class Task extends DateEntity {
14 | @PrimaryGeneratedColumn('uuid')
15 | id: string
16 |
17 | @Column()
18 | uid: number
19 |
20 | @Column()
21 | content: string
22 |
23 | @Column({ type: 'bigint', transformer: dateTransformer() })
24 | date: number
25 |
26 | @Column({
27 | type: 'tinyint',
28 | default: TaskType.PENDING,
29 | comment: '进度类型: 1=待作业, 2=作业中, 3=已完成, 4=未完成',
30 | })
31 | type: number
32 |
33 | @Column({ type: 'tinyint', default: 0, comment: '待办优先级, 0-5' })
34 | count: number
35 | }
36 |
--------------------------------------------------------------------------------
/server/src/modules/system/system.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common'
2 | import { InjectDataSource } from '@nestjs/typeorm'
3 | import { DataSource } from 'typeorm'
4 | import * as os from 'os'
5 |
6 | @Injectable()
7 | export class SystemService {
8 | constructor(
9 | @InjectDataSource()
10 | private dataSource: DataSource,
11 | ) {}
12 |
13 | async getSystemInfo() {
14 | // 获取 MySQL 版本信息
15 | const mysqlResult = await this.dataSource.query(
16 | 'SELECT VERSION() as mysqlVersion',
17 | )
18 | const mysqlVersion = mysqlResult[0]?.mysqlVersion || ''
19 |
20 | return {
21 | mysqlVersion,
22 | currentSystemTime: Date.now(),
23 | freemem: os.freemem(),
24 | totalmem: os.totalmem(),
25 | platform: os.platform(),
26 | type: os.type(),
27 | hostname: os.hostname(),
28 | arch: os.arch(),
29 | nodeVersion: process.version,
30 | cpus: os.cpus(),
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/server/src/modules/reminders/entities/reminder.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
2 | import { dateTransformer } from '@/utils/transformerUtils'
3 | import { DateEntity } from '@/entities/date.entity'
4 |
5 | @Entity('reminders')
6 | export class Reminder extends DateEntity {
7 | @PrimaryGeneratedColumn('uuid')
8 | id: string
9 |
10 | @Column()
11 | uid: number
12 |
13 | @Column()
14 | content: string
15 |
16 | @Column({
17 | length: 20,
18 | comment: 'cron表达式',
19 | default: null,
20 | })
21 | cron: string
22 |
23 | @Column({
24 | type: 'bigint',
25 | transformer: dateTransformer(),
26 | comment: '提醒时间',
27 | })
28 | date: number
29 |
30 | @Column({
31 | type: 'tinyint',
32 | default: 1,
33 | comment: '事项类型, 1=待提醒, 2=已提醒',
34 | })
35 | type: number
36 |
37 | @Column({
38 | type: 'boolean',
39 | default: true,
40 | comment: '是否开启提醒',
41 | })
42 | open: boolean
43 | }
44 |
--------------------------------------------------------------------------------
/src/assets/styles/antd/table.scss:
--------------------------------------------------------------------------------
1 | .ant-table-wrapper {
2 | flex: 1;
3 | height: 100%;
4 | overflow: hidden;
5 | background: #fff;
6 | border: 1px solid #e1e6eb;
7 | .ant-table-container {
8 | height: 100%;
9 | min-height: 100%;
10 | }
11 |
12 | .ant-spin-nested-loading {
13 | // loading 设为居中对齐
14 | & > div > .ant-spin {
15 | max-height: none;
16 | }
17 | }
18 |
19 | .ant-btn:not(:nth-child(1)) {
20 | margin-left: 10px;
21 | }
22 |
23 | .ant-spin-nested-loading,
24 | .ant-spin-container {
25 | min-height: 100%;
26 | display: flex;
27 | flex-direction: column;
28 | flex: 1;
29 | }
30 |
31 | .ant-table {
32 | font-size: 12px;
33 | flex: 1;
34 | }
35 |
36 | .ant-pagination {
37 | text-align: right;
38 | margin: 10px 0;
39 | }
40 |
41 | // 暂无数据居中对齐
42 | // .ant-table-placeholder {
43 | // position: absolute;
44 | // top: 50%;
45 | // left: 50%;
46 | // transform: translate(-50%, -50%);
47 | // }
48 | }
49 |
--------------------------------------------------------------------------------
/src/views/today-task/style.scss:
--------------------------------------------------------------------------------
1 | .today-task {
2 | .ant-tag {
3 | margin-bottom: 10px;
4 | }
5 |
6 | .task-component {
7 | &:not(:nth-last-child(1)) {
8 | margin-bottom: 15px;
9 | }
10 | }
11 |
12 | .wrapper {
13 | flex: 1;
14 | overflow: hidden;
15 | overflow-y: auto;
16 | .ant-row-flex {
17 | height: 100%;
18 | .ant-col:not(:nth-last-child(1)) {
19 | border-right: 1px dashed #dcd5d5;
20 | }
21 | }
22 | }
23 | }
24 |
25 | // TaskItem
26 | .task-component {
27 | $padding: 15px;
28 | .ant-card-body {
29 | padding: $padding 0;
30 | }
31 |
32 | .content {
33 | padding: 0 24px $padding $padding;
34 | border-bottom: 1px dashed #ccc;
35 | white-space: pre-wrap;
36 | }
37 |
38 | .level {
39 | padding-left: $padding;
40 | }
41 |
42 | .button-wrapper {
43 | padding: 0 $padding;
44 | display: flex;
45 | justify-content: flex-end;
46 |
47 | .ant-btn:not(:nth-last-child(1)) {
48 | margin-right: 7px;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/services/user.ts:
--------------------------------------------------------------------------------
1 | import http from '@/utils/http'
2 |
3 | // 通过账号密码登录
4 | export function serviceLogin(data: object) {
5 | return http.post('/passport/login', data)
6 | }
7 |
8 | // 通过token登录
9 | export function serviceLoginByToken(token: string) {
10 | return http.post('/passport/token', {
11 | token,
12 | })
13 | }
14 |
15 | export function serviceLoginByCode(code: string) {
16 | return http.post('/passport/code', {
17 | code,
18 | })
19 | }
20 |
21 | // 退出登录
22 | export function serviceLogout() {
23 | // return http.get('/logout')
24 | }
25 |
26 | // 更新用户信息
27 | export function serviceUpdateUser(data: object) {
28 | return http.post('/user/update', data, {
29 | headers: { successAlert: 'true' },
30 | })
31 | }
32 |
33 | // 获取用户配置信息
34 | export function serviceGetUserConfig() {
35 | return http.post('/user-configure/get')
36 | }
37 |
38 | // 更新用户配置信息
39 | export function serviceUpdateUserConfig(data: object) {
40 | return http.post('/user-configure/update', data, {
41 | headers: { successAlert: 'true' },
42 | })
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/exception/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './style.scss'
3 | import { Result, Button } from 'antd'
4 | import { useNavigate } from 'react-router'
5 | import { ExceptionStatusType } from 'antd/lib/result'
6 |
7 | interface Props {
8 | status?: ExceptionStatusType
9 | }
10 |
11 | const statusMap = {
12 | 403: {
13 | title: '403',
14 | subTitle: 'Sorry, you are not authorized to access this page.',
15 | },
16 | 404: {
17 | title: '404',
18 | subTitle: 'Sorry, the page you visited does not exist.',
19 | },
20 | 500: {
21 | title: '500',
22 | subTitle: 'Sorry, the server is wrong.',
23 | },
24 | }
25 |
26 | const NoMatch: React.FC = function ({ status = '404' }) {
27 | const navigate = useNavigate()
28 |
29 | function goBack() {
30 | navigate(-1)
31 | }
32 |
33 | return (
34 |
38 | Back
39 |
40 | }
41 | {...statusMap[status]}
42 | />
43 | )
44 | }
45 |
46 | export default NoMatch
47 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | # /dist/*
4 | # !/dist/src/
5 | # !/dist/src/**
6 | /build
7 | node_modules
8 |
9 | # Logs
10 | ./logs
11 | *.log
12 | npm-debug.log*
13 | pnpm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 | lerna-debug.log*
17 |
18 | # OS
19 | .DS_Store
20 |
21 | # Tests
22 | /coverage
23 | /.nyc_output
24 |
25 | # IDEs and editors
26 | /.idea
27 | .project
28 | .classpath
29 | .c9/
30 | *.launch
31 | .settings/
32 | *.sublime-workspace
33 |
34 | # IDE - VSCode
35 | .vscode/*
36 | !.vscode/settings.json
37 | !.vscode/tasks.json
38 | !.vscode/launch.json
39 | !.vscode/extensions.json
40 |
41 | # dotenv environment variable files
42 | .env
43 | .env.production
44 | .env.development.local
45 | .env.test.local
46 | .env.production.local
47 | .env.local
48 |
49 | # temp directory
50 | .temp
51 | .tmp
52 |
53 | # Runtime data
54 | pids
55 | *.pid
56 | *.seed
57 | *.pid.lock
58 |
59 | # Diagnostic reports (https://nodejs.org/api/report.html)
60 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
61 |
62 |
63 | # typeorm
64 | migrations/*.ts
65 |
66 | # Local Netlify folder
67 | .netlify
68 | gin-server
--------------------------------------------------------------------------------
/server/src/modules/company/entities/company.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
2 | import { DateEntity } from '@/entities/date.entity'
3 | import { dateTransformer } from '@/utils/transformerUtils'
4 |
5 | @Entity('company')
6 | export class Company extends DateEntity {
7 | @PrimaryGeneratedColumn('uuid')
8 | id: string
9 |
10 | @Column()
11 | uid: number
12 |
13 | @Column({ name: 'company_name' })
14 | companyName: string
15 |
16 | @Column({
17 | name: 'start_date',
18 | type: 'datetime',
19 | transformer: dateTransformer(),
20 | })
21 | startDate: Date
22 |
23 | @Column({
24 | name: 'end_date',
25 | type: 'datetime',
26 | nullable: true,
27 | transformer: dateTransformer(),
28 | })
29 | endDate: Date
30 |
31 | @Column({ type: 'text' })
32 | remark: string
33 |
34 | @Column({ type: 'decimal', precision: 19, scale: 2 })
35 | amount: number
36 |
37 | @Column({
38 | name: 'expect_leave_date',
39 | type: 'datetime',
40 | nullable: true,
41 | transformer: dateTransformer(),
42 | })
43 | expectLeaveDate: Date
44 | }
45 |
--------------------------------------------------------------------------------
/server/src/modules/users/entities/user.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | Column,
4 | PrimaryColumn,
5 | CreateDateColumn,
6 | UpdateDateColumn,
7 | } from 'typeorm'
8 |
9 | @Entity('users')
10 | export class User {
11 | @PrimaryColumn()
12 | uid: number
13 |
14 | @Column({ default: 'github' })
15 | provider: string
16 |
17 | @Column({ name: 'login_name', default: '' })
18 | loginName: string
19 |
20 | @Column({ default: '' })
21 | username: string
22 |
23 | @Column({ default: '' })
24 | password: string
25 |
26 | @Column({ default: '' })
27 | token: string
28 |
29 | @Column({ name: 'avatar_url', nullable: true })
30 | avatarUrl: string
31 |
32 | @Column({ nullable: true })
33 | location: string
34 |
35 | @Column({ nullable: true })
36 | bio: string
37 |
38 | @Column({ nullable: true })
39 | email: string
40 |
41 | @Column({ name: 'ip_addr', nullable: true })
42 | ipAddr: string
43 |
44 | @Column({ default: 1 })
45 | role: number
46 |
47 | @CreateDateColumn({ name: 'created_at' })
48 | createdAt: Date
49 |
50 | @UpdateDateColumn({ name: 'updated_at' })
51 | updatedAt: Date
52 | }
53 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019-present xiejiahe
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/server/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019-present xiejiahe
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/server/src/utils/cronUtils.ts:
--------------------------------------------------------------------------------
1 | import * as cronParser from 'cron-parser'
2 |
3 | /**
4 | * 验证 cron 表达式是否有效
5 | * @param cronExpression cron 表达式
6 | * @returns boolean
7 | */
8 | export function isValidCronExpression(cronExpression: string): boolean {
9 | if (!cronExpression) return false
10 | try {
11 | cronParser.CronExpressionParser.parse(cronExpression)
12 | return true
13 | } catch {
14 | return false
15 | }
16 | }
17 |
18 | /**
19 | * 检查 cron 表达式是否满足当前时间
20 | * @param cronExpression cron 表达式
21 | * @returns boolean
22 | */
23 | export function isCronExpressionMatch(cronExpression: string): boolean {
24 | if (!isValidCronExpression(cronExpression)) {
25 | return false
26 | }
27 |
28 | try {
29 | const interval = cronParser.CronExpressionParser.parse(cronExpression)
30 | const nextDate = interval.next()
31 | return nextDate.getTime() <= Date.now()
32 | } catch {
33 | return false
34 | }
35 | }
36 |
37 | export function getNextCronExecution(cronExpression: string): number | null {
38 | try {
39 | const interval = cronParser.CronExpressionParser.parse(cronExpression)
40 | const nextDate = interval.next()
41 | return nextDate.getTime()
42 | } catch {
43 | return null
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Tomato Work 个人事务管理系统
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | 本系统不提供测试账号密码,如果想预览请使用 `GitHub` 登录。
15 |
16 | ## Screenshot
17 |
18 | 
19 |
20 | ## 主要功能
21 |
22 | - [x] github 登录
23 | - [x] 提醒事项 - 支持日期和Cron定时,通过企业微信+邮箱推送
24 | - [x] 活动清单
25 | - [x] 今日待办
26 | - [x] 日志管理
27 | - [x] 公司单位
28 | - [x] 账单管理
29 | - [x] 个人中心
30 | - [x] 我的备忘 - 支持 Markdown & WYSIWYG
31 |
32 | ## 开发
33 |
34 | **Node.js >= 22**
35 |
36 | ```bash
37 | $ git clone --depth=1 https://github.com/xjh22222228/tomato-work.git
38 |
39 | $ pnpm i
40 |
41 | $ npm start # 启动本地环境
42 | $ npm run start:prod # 连接作者生产环境,可以用于测试
43 |
44 | # 打包
45 | npm run build
46 | ```
47 |
48 | ---
49 |
50 | ## License
51 |
52 | [MIT](https://opensource.org/licenses/MIT)
53 |
--------------------------------------------------------------------------------
/server/src/modules/bills/entities/bill.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | Column,
4 | PrimaryGeneratedColumn,
5 | ManyToOne,
6 | JoinColumn,
7 | } from 'typeorm'
8 | import { BillType } from '../..//bill-types/entities/bill-type.entity'
9 | import { DateEntity } from '@/entities/date.entity'
10 | import { numberTransformer } from '@/utils/transformerUtils'
11 |
12 | @Entity('bills')
13 | export class Bill extends DateEntity {
14 | @PrimaryGeneratedColumn('uuid')
15 | id: string
16 |
17 | @Column()
18 | uid: number
19 |
20 | @Column({
21 | type: 'decimal',
22 | precision: 19,
23 | scale: 2,
24 | default: 0,
25 | transformer: numberTransformer(),
26 | })
27 | price: number
28 |
29 | @Column({
30 | type: 'decimal',
31 | precision: 19,
32 | scale: 2,
33 | default: null,
34 | transformer: numberTransformer(),
35 | nullable: true,
36 | })
37 | originalAmount: number
38 |
39 | @Column({ type: 'bigint', transformer: numberTransformer() })
40 | date: number
41 |
42 | @Column({ name: 'type_id' })
43 | typeId: string
44 |
45 | @Column({ length: 250, default: '' })
46 | remark: string
47 |
48 | @Column({ type: 'text', nullable: true })
49 | imgs: string
50 |
51 | @ManyToOne(() => BillType)
52 | @JoinColumn({ name: 'type_id' })
53 | billType: BillType
54 | }
55 |
--------------------------------------------------------------------------------
/src/views/setting/base/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 个人中心
3 | */
4 | import React from 'react'
5 | import './style.scss'
6 | import Avatar from '@/components/avatar'
7 | import userPoster from '@/assets/img/common/user-poster.png'
8 | import { Card, Divider } from 'antd'
9 | import { useAppSelector } from '@/hooks'
10 |
11 | const { Meta } = Card
12 |
13 | const BasePage: React.FC = function () {
14 | const userInfo = useAppSelector((state) => state.user.userInfo)
15 |
16 | const MetaDesc = (
17 |
18 |
{userInfo.loginName}
19 |
UID:{userInfo.uid}
20 |
简介:{userInfo.bio}
21 |
邮箱:{userInfo.email}
22 |
地区:{userInfo.location}
23 |
注册时间:{userInfo.createdAt}
24 |
25 | )
26 |
27 | return (
28 |
29 |
30 | 个人中心
31 |
32 |
}
35 | >
36 |
}
38 | title={userInfo.username}
39 | description={MetaDesc}
40 | />
41 |
42 |
43 | )
44 | }
45 |
46 | export default BasePage
47 |
--------------------------------------------------------------------------------
/src/services/log.ts:
--------------------------------------------------------------------------------
1 | import http from '@/utils/http'
2 | import { formatDate, getWeek } from '@/utils'
3 | import { LOG_LIST } from '@/views/log/constants'
4 |
5 | // 创建日志
6 | export function serviceCreateLog(data: object) {
7 | return http.post('/log/add', data, {
8 | headers: { successAlert: 'true' },
9 | })
10 | }
11 |
12 | // 更新日志
13 | export function serviceUpdateLog(data: Record) {
14 | return http.post(`/log/update`, data, {
15 | headers: { successAlert: 'true' },
16 | })
17 | }
18 |
19 | // 查询日志列表
20 | export async function serviceGetLogList(params?: object) {
21 | const res = await http.post('/log/getAll', { ...params })
22 | res.rows = res.rows.map((item: Record) => {
23 | item.__createdAt__ = `${formatDate(item.createdAt)} ${getWeek(
24 | item.createdAt,
25 | )}`
26 | const lType = LOG_LIST.find((el) => Number(el.key) === Number(item.logType))
27 | item.__logType__ = lType?.name
28 | item.companyName ||= '无'
29 | return item
30 | })
31 | return res
32 | }
33 |
34 | // 删除日志
35 | export function serviceDeleteLog(id: string) {
36 | return http.post(
37 | `/log/delete`,
38 | { id },
39 | {
40 | headers: { successAlert: 'true' },
41 | },
42 | )
43 | }
44 |
45 | // 查询日志
46 | export function serviceGetLogById(id: string) {
47 | return http.post(`/log/get`, { id })
48 | }
49 |
--------------------------------------------------------------------------------
/server/src/guards/user-auth.guard.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | CanActivate,
4 | ExecutionContext,
5 | UnauthorizedException,
6 | } from '@nestjs/common'
7 | import { Request } from 'express'
8 | import { UsersService } from '../modules/users/users.service'
9 |
10 | @Injectable()
11 | export class UserAuthGuard implements CanActivate {
12 | constructor(private usersService: UsersService) {}
13 |
14 | async canActivate(context: ExecutionContext): Promise {
15 | const request = context.switchToHttp().getRequest()
16 | const token = this.extractTokenFromHeader(request)
17 |
18 | if (!token) {
19 | throw new UnauthorizedException('登录失效,请重新登录')
20 | }
21 |
22 | try {
23 | // 通过 token 查找用户
24 | const user = await this.usersService.findByToken(token)
25 |
26 | if (!user) {
27 | throw new UnauthorizedException('登录失效,请重新登录')
28 | }
29 |
30 | // 将用户信息附加到请求对象
31 | request['user'] = user
32 | return true
33 | } catch {
34 | throw new UnauthorizedException('登录失效,请重新登录')
35 | }
36 | }
37 |
38 | private extractTokenFromHeader(request: Request): string | undefined {
39 | // 从请求头中提取 token
40 | const token =
41 | request.headers.token ||
42 | request.headers.authorization ||
43 | (request.body && request.body.token)
44 |
45 | return token ? String(token) : undefined
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/server/src/modules/logs/logs.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, Body, UseGuards } from '@nestjs/common'
2 | import { UserAuthGuard } from '@/guards/user-auth.guard'
3 | import { LogsService } from './logs.service'
4 | import { CreateLogDto } from './dto/create-log.dto'
5 | import { UpdateLogDto } from './dto/update-log.dto'
6 | import { User } from '@/decorators/user.decorator'
7 | import { GetLogDto } from './dto/get-log.dto'
8 |
9 | @Controller('log')
10 | @UseGuards(UserAuthGuard)
11 | export class LogsController {
12 | constructor(private readonly logsService: LogsService) {}
13 |
14 | @Post('add')
15 | create(@User() user, @Body() createLogDto: CreateLogDto) {
16 | return this.logsService.create(user.uid, createLogDto)
17 | }
18 |
19 | @Post('getAll')
20 | findAll(@User() user, @Body() getLogDto: GetLogDto) {
21 | return this.logsService.findAll(user.uid, getLogDto)
22 | }
23 |
24 | @Post('get')
25 | findOne(@User() user, @Body() getLogDto: GetLogDto) {
26 | const { pageNo, pageSize, ...dto } = getLogDto
27 | return this.logsService.findOne(dto, user.uid)
28 | }
29 |
30 | @Post('update')
31 | update(@User() user, @Body() updateLogDto: UpdateLogDto) {
32 | return this.logsService.update(user.uid, updateLogDto)
33 | }
34 |
35 | @Post('delete')
36 | remove(@User() user, @Body('id') id: string) {
37 | return this.logsService.remove(id, user.uid)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/server/src/modules/company/company.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, Body, UseGuards, Request } from '@nestjs/common'
2 | import { UserAuthGuard } from '@/guards/user-auth.guard'
3 | import { CompanyService } from './company.service'
4 | import { CreateCompanyDto } from './dto/create-company.dto'
5 | import { UpdateCompanyDto } from './dto/update-company.dto'
6 | import { GetCompanyDto } from './dto/get-company.dto'
7 |
8 | @Controller('company')
9 | @UseGuards(UserAuthGuard)
10 | export class CompanyController {
11 | constructor(private readonly companyService: CompanyService) {}
12 |
13 | @Post('add')
14 | create(@Request() req, @Body() createCompanyDto: CreateCompanyDto) {
15 | return this.companyService.create(req.user.uid, createCompanyDto)
16 | }
17 |
18 | @Post('getAll')
19 | findAll(@Request() req, @Body() getCompanyDto: GetCompanyDto) {
20 | return this.companyService.findAll(req.user.uid, getCompanyDto)
21 | }
22 |
23 | @Post('get')
24 | findOne(@Request() req, @Body('id') id: string) {
25 | return this.companyService.findOne(id, req.user.uid)
26 | }
27 |
28 | @Post('update')
29 | update(@Request() req, @Body() updateCompanyDto: UpdateCompanyDto) {
30 | return this.companyService.update(req.user.uid, updateCompanyDto)
31 | }
32 |
33 | @Post('delete')
34 | remove(@Request() req, @Body('id') id: string) {
35 | return this.companyService.remove(id, req.user.uid)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/server/src/utils/markdown.ts:
--------------------------------------------------------------------------------
1 | import * as markdownit from 'markdown-it'
2 | import anchor from 'markdown-it-anchor'
3 | import hljs from 'highlight.js'
4 |
5 | const config: markdownit.Options = {
6 | html: true,
7 | linkify: true,
8 | typographer: true,
9 | breaks: true,
10 | highlight: function (str: string, lang: string) {
11 | if (lang && hljs.getLanguage(lang)) {
12 | try {
13 | return hljs.highlight(str, { language: lang }).value
14 | } catch {
15 | return str
16 | }
17 | }
18 |
19 | return ''
20 | },
21 | }
22 |
23 | const md = markdownit(config).use(anchor)
24 |
25 | const defaultRender =
26 | md.renderer.rules.link_open ||
27 | function (tokens, idx, options, env, self) {
28 | return self.renderToken(tokens, idx, options)
29 | }
30 |
31 | md.renderer.rules.link_open = function (
32 | tokens: markdownit.Token[],
33 | idx: number,
34 | options: markdownit.Options,
35 | env: any,
36 | self,
37 | ) {
38 | const aIndex = tokens[idx].attrIndex('target')
39 | const isAnchor = tokens[idx]?.attrs?.[0]?.[1]?.startsWith('#')
40 |
41 | if (!isAnchor) {
42 | if (aIndex < 0) {
43 | tokens[idx].attrPush(['target', '_blank'])
44 | } else {
45 | if (tokens[idx]?.attrs?.[aIndex]?.[1]) {
46 | tokens[idx].attrs[aIndex][1] = '_blank'
47 | }
48 | }
49 | }
50 |
51 | return defaultRender(tokens, idx, options, env, self)
52 | }
53 |
54 | export default md
55 |
--------------------------------------------------------------------------------
/src/components/private-route/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import CONFIG from '@/config'
3 | import qs from 'query-string'
4 | import { useLocation, Navigate } from 'react-router'
5 | import { useAppSelector } from '@/hooks'
6 |
7 | type Props = {
8 | element: React.FC | React.ComponentClass
9 | meta?: Record
10 | }
11 |
12 | const o = Object.create(null)
13 |
14 | const PrivateRoute: React.FC = function ({
15 | element: Component,
16 | meta = o,
17 | ...rest
18 | }) {
19 | const { pathname, search } = useLocation()
20 | const { isLogin } = useAppSelector((state) => state.user)
21 | const isLoginPage = pathname === '/' || pathname === '/login'
22 |
23 | React.useEffect(() => {
24 | if (meta.title) {
25 | document.title = `${meta.title} - ${CONFIG.title}`
26 | } else {
27 | document.title = CONFIG.title
28 | }
29 | }, [meta])
30 |
31 | if (isLoginPage && isLogin) {
32 | const redirectUrl = qs.parse(search).redirectUrl as string
33 | const url = redirectUrl || '/home/index' + search
34 | return
35 | }
36 |
37 | if (meta.requiresAuth) {
38 | if (isLogin) {
39 | return
40 | } else {
41 | if (!isLoginPage) {
42 | return
43 | }
44 | }
45 | }
46 |
47 | return
48 | }
49 |
50 | export default PrivateRoute
51 |
--------------------------------------------------------------------------------
/src/services/company.ts:
--------------------------------------------------------------------------------
1 | import http from '@/utils/http'
2 | import { formatDate, fromNow } from '@/utils'
3 |
4 | // 查询所有单位
5 | export async function serviceGetAllCompany(data?: object) {
6 | const res = await http.post('/company/getAll', data)
7 | res.rows = res.rows.map((item: any) => {
8 | item.startDate = formatDate(item.startDate)
9 | item.__amount__ = `¥${item.amount}`
10 | item.__jobDay__ = fromNow(item.startDate, item.endDate) + ' 天'
11 | if (item.endDate) {
12 | item.endDate = formatDate(item.endDate)
13 | }
14 | if (item.expectLeaveDate) {
15 | item.expectLeaveDate = formatDate(item.expectLeaveDate)
16 | item.__leaveDay__ = fromNow(Date.now(), item.expectLeaveDate)
17 | }
18 | item.__endDate__ = item.endDate ?? '至今'
19 | return item
20 | })
21 | return res
22 | }
23 |
24 | // 创建单位
25 | export function serviceCreateCompany(data: object) {
26 | return http.post('/company/add', data, {
27 | headers: { successAlert: 'true' },
28 | })
29 | }
30 |
31 | // 更新单位
32 | export function serviceUpdateCompany(id: string, data: object) {
33 | return http.post(
34 | `/company/update`,
35 | { id, ...data },
36 | {
37 | headers: { successAlert: 'true' },
38 | },
39 | )
40 | }
41 |
42 | // 删除单位
43 | export function serviceDelCompany(id: unknown) {
44 | return http.post(
45 | `/company/delete`,
46 | { id },
47 | {
48 | headers: { successAlert: 'true' },
49 | },
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/server/src/modules/user-configures/user-configures.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common'
2 | import { InjectRepository } from '@nestjs/typeorm'
3 | import { Repository } from 'typeorm'
4 | import { UpdateUserConfigureDto } from './dto/update-user-configure.dto'
5 | import { UserConfigure } from './entities/user-configure.entity'
6 |
7 | @Injectable()
8 | export class UserConfiguresService {
9 | constructor(
10 | @InjectRepository(UserConfigure)
11 | private userConfiguresRepository: Repository,
12 | ) {}
13 |
14 | async findOrCreate(uid: number): Promise {
15 | let userConfigure = await this.userConfiguresRepository.findOne({
16 | where: { uid },
17 | })
18 |
19 | if (!userConfigure) {
20 | userConfigure = this.userConfiguresRepository.create({
21 | uid,
22 | isTaskNotify: true,
23 | isMatterNotify: true,
24 | serverChanSckey: '',
25 | })
26 |
27 | await this.userConfiguresRepository.save(userConfigure)
28 | }
29 |
30 | return userConfigure
31 | }
32 |
33 | async update(
34 | uid: number,
35 | updateUserConfigureDto: UpdateUserConfigureDto,
36 | ): Promise {
37 | const userConfigure = await this.findOrCreate(uid)
38 | const updatedUserConfigure = Object.assign(
39 | userConfigure,
40 | updateUserConfigureDto,
41 | )
42 | return this.userConfiguresRepository.save(updatedUserConfigure)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/server/src/modules/reminders/reminders.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, Body, UseGuards } from '@nestjs/common'
2 | import { UserAuthGuard } from '@/guards/user-auth.guard'
3 | import { RemindersService } from './reminders.service'
4 | import { CreateReminderDto } from './dto/create-reminder.dto'
5 | import { UpdateReminderDto } from './dto/update-reminder.dto'
6 | import { User } from '@/decorators/user.decorator'
7 | import { GetReminderDto } from './dto/get-reminder.dto'
8 |
9 | @Controller('reminder')
10 | @UseGuards(UserAuthGuard)
11 | export class RemindersController {
12 | constructor(private readonly remindersService: RemindersService) {}
13 |
14 | @Post('add')
15 | create(@User() user, @Body() createReminderDto: CreateReminderDto) {
16 | return this.remindersService.create(user.uid, createReminderDto)
17 | }
18 |
19 | @Post('getAll')
20 | findAll(@User() user, @Body() getReminderDto: GetReminderDto) {
21 | return this.remindersService.findAll(user.uid, getReminderDto)
22 | }
23 |
24 | @Post('get')
25 | findOne(@User() user, @Body('id') id: string) {
26 | return this.remindersService.findOne(id, user.uid)
27 | }
28 |
29 | @Post('update')
30 | update(@User() user, @Body() updateReminderDto: UpdateReminderDto) {
31 | return this.remindersService.update(user.uid, updateReminderDto)
32 | }
33 |
34 | @Post('delete')
35 | remove(@User() user, @Body('id') id: string) {
36 | return this.remindersService.remove(id, user.uid)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Tomato Work 个人事务管理系统
5 |
6 |
7 |
8 |
9 |
10 |
11 | ## MySQL Setup
12 |
13 | MySQL >= 8.0
14 |
15 | - 创建数据库 `tomato_work`
16 | - 运行根目录 `sql.sql`
17 |
18 | ## 环境变量
19 |
20 | 项目不自带 `.env.local` 和 `.env.production`, 需要自己 Copy `.env.development`
21 |
22 | - `.env.development` 开发环境 `npm run start:dev`
23 | - `.env.local` 开发环境 `npm run start`
24 | - `.env.production` 生产环境 `npm run start:prod`
25 |
26 | 可以不使用文件环境变量,使用系统环境变量,已做兼容处理。
27 |
28 | ## Build Setup
29 |
30 | 启动项目之前请配置数据库信息 `.env.development`
31 |
32 | ```bash
33 | # Download
34 | git clone --depth=1 https://github.com/xjh22222228/tomato-work.git
35 |
36 | cd server
37 |
38 | # Install
39 | pnpm i
40 |
41 | # Port: 7003
42 | npm run start:dev
43 |
44 | # Build start
45 | npm run start
46 | ```
47 |
48 | ## 部署
49 |
50 | ```bash
51 | npm run build # 编译
52 | npm run pm2 # 启动
53 | ```
54 |
55 | ## SQL迁移
56 |
57 | ```bash
58 | $ npm run migration:generate -- migrations/sql
59 |
60 | $ npm run migration:run
61 | ```
62 |
63 | ## License
64 |
65 | [MIT](https://opensource.org/licenses/MIT)
66 |
--------------------------------------------------------------------------------
/src/components/table/Toolbar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button, Popconfirm } from 'antd'
3 | import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'
4 |
5 | interface Props {
6 | onDelete?: () => void
7 | onAdd?: () => void
8 | toolbar?: React.ReactNode
9 | selectedRowKeys: string[]
10 | }
11 |
12 | const Toolbar: React.FC = function ({
13 | selectedRowKeys,
14 | toolbar,
15 | onDelete,
16 | onAdd,
17 | }) {
18 | const showToolbar = onDelete || onAdd
19 | const selectedLen = selectedRowKeys.length
20 | const disabled = selectedLen <= 0
21 |
22 | return showToolbar ? (
23 |
24 |
25 | {onAdd && (
26 |
}>
27 | 新增
28 |
29 | )}
30 |
31 | {onDelete && (
32 |
39 | }
44 | >
45 | 删除
46 |
47 |
48 | )}
49 |
50 |
51 | {toolbar}
52 |
53 | ) : null
54 | }
55 |
56 | export default Toolbar
57 |
--------------------------------------------------------------------------------
/server/src/modules/todo-lists/todo-lists.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, Post, Body, Param, UseGuards } from '@nestjs/common'
2 | import { UserAuthGuard } from '@/guards/user-auth.guard'
3 | import { User } from '@/decorators/user.decorator'
4 | import { TodoListsService } from './todo-lists.service'
5 | import { CreateTodoListDto } from './dto/create-todo-list.dto'
6 | import { UpdateTodoListDto } from './dto/update-todo-list.dto'
7 | import { GetTodoListDto } from './dto/get-todo-list.dto'
8 |
9 | @Controller('todo-list')
10 | @UseGuards(UserAuthGuard)
11 | export class TodoListsController {
12 | constructor(private readonly todoListsService: TodoListsService) {}
13 |
14 | @Post('add')
15 | create(@User() user, @Body() createTodoListDto: CreateTodoListDto) {
16 | return this.todoListsService.create(user.uid, createTodoListDto)
17 | }
18 |
19 | @Post('getAll')
20 | findAll(@User() user, @Body() getTodoListDto: GetTodoListDto) {
21 | return this.todoListsService.findAll(user.uid, getTodoListDto)
22 | }
23 |
24 | @Get(':id')
25 | findOne(@User() user, @Param('id') id: string) {
26 | return this.todoListsService.findOne(id, user.uid)
27 | }
28 |
29 | @Post('update')
30 | update(@User() user, @Body() updateTodoListDto: UpdateTodoListDto) {
31 | return this.todoListsService.update(user.uid, updateTodoListDto)
32 | }
33 |
34 | @Post('delete')
35 | remove(@User() user, @Body('id') id: string) {
36 | return this.todoListsService.remove(id, user.uid)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/store/companySlice.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2018-present the xiejiahe. All rights reserved. MIT license.
2 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
3 | import { serviceGetAllCompany } from '@/services'
4 |
5 | export interface CompanyProps {
6 | amount: number | string
7 | companyName: string
8 | createdAt: string
9 | endDate: string | null
10 | expectLeaveDate: string | null
11 | id: string
12 | remark: string
13 | startDate: string
14 | uid: number
15 | updatedAt: string
16 | [key: string]: any
17 | }
18 |
19 | export interface SystemState {
20 | loading: boolean
21 | companyAll: CompanyProps[]
22 | }
23 |
24 | const initialState: SystemState = {
25 | loading: false,
26 | companyAll: [],
27 | }
28 |
29 | export const getAllCompany = createAsyncThunk(
30 | 'company/getAllCompany',
31 | async () => {
32 | const response = await serviceGetAllCompany()
33 | return response.rows
34 | },
35 | )
36 |
37 | export const companySlice = createSlice({
38 | name: 'company',
39 | initialState,
40 | reducers: {},
41 |
42 | extraReducers(builder) {
43 | builder
44 | .addCase(getAllCompany.pending, (state) => {
45 | state.loading = true
46 | })
47 | .addCase(getAllCompany.fulfilled, (state, action) => {
48 | state.companyAll = action.payload
49 | })
50 | .addCase(getAllCompany.rejected, (state) => {
51 | state.loading = false
52 | })
53 | },
54 | })
55 |
56 | export default companySlice.reducer
57 |
--------------------------------------------------------------------------------
/server/src/modules/bill-types/bill-types.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, Body, UseGuards } from '@nestjs/common'
2 | import { BillTypesService } from './bill-types.service'
3 | import { CreateBillTypeDto } from './dto/create-bill-type.dto'
4 | import { UpdateBillTypeDto } from './dto/update-bill-type.dto'
5 | import { UserAuthGuard } from '@/guards/user-auth.guard'
6 | import { User } from '@/decorators/user.decorator'
7 |
8 | @Controller('bill-type')
9 | @UseGuards(UserAuthGuard)
10 | export class BillTypesController {
11 | constructor(private readonly billTypesService: BillTypesService) {}
12 |
13 | @Post('add')
14 | create(
15 | @User('uid') uid: number,
16 | @Body() createBillTypeDto: CreateBillTypeDto,
17 | ) {
18 | return this.billTypesService.create(uid, createBillTypeDto)
19 | }
20 |
21 | @Post('getAll')
22 | async findAll(@User('uid') uid: number) {
23 | return this.billTypesService.findAll(uid)
24 | }
25 |
26 | @Post('get')
27 | findOne(@User('uid') uid: number, @Body('id') id: string) {
28 | return this.billTypesService.findOne(uid, id)
29 | }
30 |
31 | @Post('update')
32 | async update(
33 | @User('uid') uid: number,
34 | @Body() updateBillTypeDto: UpdateBillTypeDto,
35 | ) {
36 | await this.billTypesService.update(uid, updateBillTypeDto)
37 | return { msg: '更新成功' }
38 | }
39 |
40 | @Post('delete')
41 | async remove(@User('uid') uid: number, @Body('ids') ids: string[]) {
42 | return this.billTypesService.remove(uid, ids)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/server/src/modules/memorandums/memorandums.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, Body, UseGuards } from '@nestjs/common'
2 | import { UserAuthGuard } from '@/guards/user-auth.guard'
3 | import { MemorandumsService } from './memorandums.service'
4 | import { CreateMemorandumDto } from './dto/create-memorandum.dto'
5 | import { UpdateMemorandumDto } from './dto/update-memorandum.dto'
6 | import { User } from '@/decorators/user.decorator'
7 | import { GetMemorandumDto } from './dto/get-memorandum.dto'
8 |
9 | @Controller('memorandum')
10 | @UseGuards(UserAuthGuard)
11 | export class MemorandumsController {
12 | constructor(private readonly memorandumsService: MemorandumsService) {}
13 |
14 | @Post('add')
15 | create(@User() user, @Body() createMemorandumDto: CreateMemorandumDto) {
16 | return this.memorandumsService.create(user.uid, createMemorandumDto)
17 | }
18 |
19 | @Post('getAll')
20 | findAll(@User() user, @Body() getMemorandumDto: GetMemorandumDto) {
21 | return this.memorandumsService.findAll(user.uid, getMemorandumDto)
22 | }
23 |
24 | @Post('get')
25 | findOne(@User() user, @Body('id') id: string) {
26 | return this.memorandumsService.findOne(id, user.uid)
27 | }
28 |
29 | @Post('update')
30 | update(@User() user, @Body() updateMemorandumDto: UpdateMemorandumDto) {
31 | return this.memorandumsService.update(user.uid, updateMemorandumDto)
32 | }
33 |
34 | @Post('delete')
35 | remove(@User() user, @Body('id') id: string) {
36 | return this.memorandumsService.remove(id, user.uid)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/views/main/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 主页入口
3 | */
4 | import React, { useState } from 'react'
5 | import './style.scss'
6 | import Sidebar from '@/components/sidebar'
7 | import Header from '@/components/header'
8 | import { Layout } from 'antd'
9 | import { LOCAL_STORAGE } from '@/constants'
10 | import { Outlet } from 'react-router'
11 | import { isMobile } from '@/utils/index'
12 |
13 | const { Content } = Layout
14 | const { SIDEBAR_COLLAPSED } = LOCAL_STORAGE
15 |
16 | export interface HomeMainState {
17 | collapsed?: boolean
18 | setCollapsed?: () => void
19 | }
20 |
21 | function defaultCollapsed() {
22 | const local = localStorage.getItem(SIDEBAR_COLLAPSED)
23 | if (local) {
24 | return Number(localStorage.getItem(SIDEBAR_COLLAPSED)) === 0
25 | }
26 | if (isMobile()) {
27 | return true
28 | }
29 | return false
30 | }
31 |
32 | const HomeMainPage: React.FC = function () {
33 | const [collapsed, setCollapsed] = useState(defaultCollapsed())
34 |
35 | function handleToggleCollapsed() {
36 | setCollapsed(!collapsed)
37 | localStorage.setItem(SIDEBAR_COLLAPSED, Number(collapsed) + '')
38 | }
39 |
40 | return (
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | )
53 | }
54 |
55 | export default HomeMainPage
56 |
--------------------------------------------------------------------------------
/server/typeorm.config.ts:
--------------------------------------------------------------------------------
1 | // 生成迁移文件(根据实体变更自动生成):
2 | // npm run migration:generate -- migrations/sql
3 |
4 | // 创建空白迁移文件(手动编写 SQL):
5 | // npm run migration:create -- migrations/sql
6 |
7 | // 执行迁移:
8 | // npm run migration:run
9 |
10 | // 回滚最近的迁移:
11 | // npm run migration:revert
12 |
13 | import { DataSource } from 'typeorm'
14 | import * as dotenv from 'dotenv'
15 | import * as path from 'path'
16 | import * as tsconfig from 'tsconfig-paths'
17 |
18 | // 注册路径别名
19 | const { absoluteBaseUrl, paths } = require('./tsconfig.json').compilerOptions
20 | tsconfig.register({
21 | baseUrl: absoluteBaseUrl || '.',
22 | paths: paths || { '@/*': ['src/*'] },
23 | })
24 |
25 | // 导入转换器工具
26 | // import {
27 | // dateTransformer,
28 | // numberTransformer,
29 | // } from './src/utils/transformerUtils';
30 | // global.dateTransformer = dateTransformer;
31 | // global.numberTransformer = numberTransformer;
32 |
33 | // 根据环境加载不同的 .env 文件
34 | const env = process.env.NODE_ENV || 'local'
35 | const envFilePath = `.env.${env}`
36 | dotenv.config({ path: path.resolve(process.cwd(), envFilePath) })
37 |
38 | console.log('ENV:', path.resolve(process.cwd(), envFilePath), process.env)
39 |
40 | export default new DataSource({
41 | type: 'mysql',
42 | host: process.env.MYSQL_HOST,
43 | port: Number(process.env.MYSQL_PORT),
44 | username: process.env.MYSQL_USERNAME,
45 | password: process.env.MYSQL_PASSWORD,
46 | database: process.env.MYSQL_DATABASE,
47 | entities: [__dirname + '/src/**/*.entity{.ts,.js}'],
48 | migrations: [__dirname + '/migrations/**/*{.ts,.js}'],
49 | timezone: '+08:00',
50 | })
51 |
--------------------------------------------------------------------------------
/src/utils/helper.ts:
--------------------------------------------------------------------------------
1 | import { message } from 'antd'
2 |
3 | /**
4 | * 计算百分比
5 | * @example
6 | * totalPercentage(8589934592, 225492992) // => 98
7 | */
8 | export function totalPercentage(totalmem: number, freemem: number) {
9 | return Math.floor(((totalmem - freemem) / totalmem) * 100)
10 | }
11 |
12 | // 全屏浏览器
13 | export function fullscreen() {
14 | try {
15 | const docElm = document.documentElement as any
16 | if (docElm.requestFullscreen) {
17 | docElm.requestFullscreen()
18 | } else if (docElm.webkitRequestFullScreen) {
19 | docElm.webkitRequestFullScreen()
20 | } else if (docElm.mozRequestFullScreen) {
21 | docElm.mozRequestFullScreen()
22 | } else if (docElm.msRequestFullscreen) {
23 | docElm.msRequestFullscreen()
24 | }
25 | } catch {
26 | message.warning('您所使用的浏览器不支持全屏')
27 | }
28 | }
29 |
30 | // 退出全屏浏览器
31 | export function exitFullscreen() {
32 | try {
33 | const doc = document as any
34 | if (doc.exitFullscreen) {
35 | doc.exitFullscreen()
36 | } else if (doc.mozCancelFullScreen) {
37 | doc.mozCancelFullScreen()
38 | } else if (doc.webkitCancelFullScreen) {
39 | doc.webkitCancelFullScreen()
40 | } else if (doc.msExitFullscreen) {
41 | doc.msExitFullscreen()
42 | }
43 | } catch {
44 | message.warning('您所使用的浏览器不支持退出全屏, 请按ESC')
45 | }
46 | }
47 |
48 | // 随机字符串
49 | export function randomCode(num = 4) {
50 | const CODE = 'qwertyuipasdfghjklxcvbnm13456789'
51 | let data = ''
52 |
53 | for (let i = 0; i < num; i++) {
54 | const random = Math.floor(Math.random() * CODE.length)
55 | data += CODE[random]
56 | }
57 |
58 | return data
59 | }
60 |
--------------------------------------------------------------------------------
/src/store/systemSlice.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2018-present the xiejiahe. All rights reserved. MIT license.
2 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'
3 | import { serviceGetSystemInfo } from '@/services'
4 | import type { GetState, AppDispatch } from '.'
5 |
6 | export interface InfoProps {
7 | mysqlVersion: string
8 | currentSystemTime: number
9 | freemem: number
10 | totalmem: number
11 | platform: string
12 | type: string
13 | hostname: string
14 | arch: string
15 | nodeVersion: string
16 | cpus: any[]
17 | }
18 |
19 | export interface SystemState {
20 | info: InfoProps
21 | }
22 |
23 | const initialState: SystemState = {
24 | info: {
25 | mysqlVersion: '',
26 | currentSystemTime: Date.now(),
27 | freemem: 0,
28 | totalmem: 0,
29 | platform: '',
30 | type: '',
31 | hostname: '',
32 | arch: '',
33 | nodeVersion: '',
34 | cpus: [],
35 | },
36 | }
37 |
38 | export const systemSlice = createSlice({
39 | name: 'system',
40 | initialState,
41 | reducers: {
42 | SET_INFO: (state, action: PayloadAction) => {
43 | state.info = {
44 | ...action.payload,
45 | arch: action.payload.arch.slice(1),
46 | }
47 | },
48 | },
49 | })
50 |
51 | export const { SET_INFO } = systemSlice.actions
52 |
53 | export const getSystemInfo =
54 | () => (dispatch: AppDispatch, getState: GetState) => {
55 | const rootState = getState()
56 | if (rootState.system.info.nodeVersion) {
57 | return
58 | }
59 |
60 | serviceGetSystemInfo().then((res) => {
61 | dispatch(SET_INFO(res as InfoProps))
62 | })
63 | }
64 |
65 | export default systemSlice.reducer
66 |
--------------------------------------------------------------------------------
/src/utils/date.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs'
2 |
3 | export const FORMAT_DATETIME = 'YYYY-MM-DD HH:mm:ss'
4 | export const FORMAT_DATE_MINUTE = 'YYYY-MM-DD HH:mm'
5 | export const FORMAT_DATE = 'YYYY-MM-DD'
6 | export const DATE_WEEK: any = [dayjs().subtract(7, 'day'), dayjs()]
7 | export const DATE_YEAR: any = [dayjs().startOf('year'), dayjs().endOf('year')]
8 |
9 | // 判断传入时间是否小于今天时间戳
10 | export function isBefore(current: dayjs.ConfigType): boolean {
11 | const today = new Date().setHours(0, 0, 0, 0)
12 | return dayjs(current).isBefore(today)
13 | }
14 |
15 | export function formatDate(date: dayjs.ConfigType): string {
16 | return dayjs(date).format(FORMAT_DATE)
17 | }
18 |
19 | export function formatDateTime(date: dayjs.ConfigType): string {
20 | return dayjs(date).format(FORMAT_DATETIME)
21 | }
22 |
23 | export function formatDateMinute(date: dayjs.ConfigType): string {
24 | return dayjs(date).format(FORMAT_DATE_MINUTE)
25 | }
26 |
27 | export function fromNow(
28 | startDate: dayjs.ConfigType,
29 | endDate: dayjs.ConfigType,
30 | ): number {
31 | const start = dayjs(startDate).valueOf()
32 | const end = dayjs(endDate || Date.now()).valueOf()
33 | const n = end - start
34 | return Math.ceil(n / (1000 * 60 * 60 * 24))
35 | }
36 |
37 | export function getWeek(date: dayjs.ConfigType): string {
38 | const weeks = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
39 | return weeks[dayjs(date).day()]
40 | }
41 |
42 | export function isToDay(date: dayjs.ConfigType): boolean {
43 | const m = dayjs(date)
44 | const n = new Date()
45 | return (
46 | m.year() === n.getFullYear() &&
47 | m.month() === n.getMonth() &&
48 | m.date() === n.getDate()
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/src/assets/styles/nprogress.css:
--------------------------------------------------------------------------------
1 | /* NProgress Make clicks pass-through */
2 | #nprogress {
3 | pointer-events: none;
4 | }
5 |
6 | #nprogress .bar {
7 | background: #29d;
8 | position: fixed;
9 | z-index: 1031;
10 | top: 0;
11 | left: 0;
12 | width: 100%;
13 | height: 2px;
14 | }
15 |
16 | /* Fancy blur effect */
17 | #nprogress .peg {
18 | display: block;
19 | position: absolute;
20 | right: 0px;
21 | width: 100px;
22 | height: 100%;
23 | box-shadow:
24 | 0 0 10px #29d,
25 | 0 0 5px #29d;
26 | opacity: 1;
27 |
28 | -webkit-transform: rotate(3deg) translate(0px, -4px);
29 | -ms-transform: rotate(3deg) translate(0px, -4px);
30 | transform: rotate(3deg) translate(0px, -4px);
31 | }
32 |
33 | /* Remove these to get rid of the spinner */
34 | #nprogress .spinner {
35 | display: block;
36 | position: fixed;
37 | z-index: 1031;
38 | top: 25px;
39 | right: 25px;
40 | }
41 |
42 | #nprogress .spinner-icon {
43 | width: 18px;
44 | height: 18px;
45 | box-sizing: border-box;
46 | border: solid 2px transparent;
47 | border-top-color: #29d;
48 | border-left-color: #29d;
49 | border-radius: 50%;
50 | animation: nprogress-spinner 400ms linear infinite;
51 | }
52 |
53 | .nprogress-custom-parent {
54 | overflow: hidden;
55 | position: relative;
56 | }
57 |
58 | .nprogress-custom-parent #nprogress .spinner,
59 | .nprogress-custom-parent #nprogress .bar {
60 | position: absolute;
61 | }
62 |
63 | @-webkit-keyframes nprogress-spinner {
64 | 0% {
65 | -webkit-transform: rotate(0deg);
66 | }
67 | 100% {
68 | -webkit-transform: rotate(360deg);
69 | }
70 | }
71 | @keyframes nprogress-spinner {
72 | 0% {
73 | transform: rotate(0deg);
74 | }
75 | 100% {
76 | transform: rotate(360deg);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/views/setting/index/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense, useEffect, useState, useMemo } from 'react'
2 | import './style.scss'
3 | import { Layout, Menu } from 'antd'
4 | import { useLocation, Outlet, useNavigate } from 'react-router'
5 | import { SETTING_SIDER_MENU_LIST } from '@/constants'
6 | import type { MenuProps } from 'antd'
7 |
8 | const { Content, Sider } = Layout
9 |
10 | const SettingIndexPage: React.FC = function () {
11 | const location = useLocation()
12 | const navigate = useNavigate()
13 | const [selectedKeys, setSelectedKeys] = useState('')
14 |
15 | useEffect(() => {
16 | for (let i = 0; i < SETTING_SIDER_MENU_LIST.length; i++) {
17 | if (SETTING_SIDER_MENU_LIST[i].path === location.pathname) {
18 | setSelectedKeys(SETTING_SIDER_MENU_LIST[i].path)
19 | break
20 | }
21 | }
22 | }, [location.pathname])
23 |
24 | const onClick: MenuProps['onClick'] = (e) => {
25 | navigate(e.key)
26 | }
27 |
28 | const items: MenuProps['items'] = useMemo(() => {
29 | return SETTING_SIDER_MENU_LIST.map((item) => {
30 | const data: any = {
31 | key: item.path || item.name,
32 | label: item.name,
33 | }
34 | return data
35 | })
36 | }, [SETTING_SIDER_MENU_LIST])
37 |
38 | return (
39 |
40 |
41 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export default SettingIndexPage
60 |
--------------------------------------------------------------------------------
/server/src/modules/users/guards/user-controller-auth.guard.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | CanActivate,
4 | ExecutionContext,
5 | UnauthorizedException,
6 | } from '@nestjs/common'
7 | import { UsersService } from '../users.service'
8 | import { Request } from 'express'
9 | import { ModuleRef } from '@nestjs/core'
10 |
11 | @Injectable()
12 | export class UserControllerAuthGuard implements CanActivate {
13 | private usersService: UsersService
14 |
15 | constructor(private moduleRef: ModuleRef) {}
16 |
17 | async onModuleInit() {
18 | this.usersService = this.moduleRef.get(UsersService, { strict: false })
19 | }
20 |
21 | async canActivate(context: ExecutionContext): Promise {
22 | // 如果 usersService 没有被注入,通过 moduleRef 获取
23 | if (!this.usersService) {
24 | this.usersService = this.moduleRef.get(UsersService, { strict: false })
25 | }
26 |
27 | const request = context.switchToHttp().getRequest()
28 | const token = this.extractTokenFromHeader(request)
29 |
30 | if (!token) {
31 | throw new UnauthorizedException('登录失效,请重新登录')
32 | }
33 |
34 | try {
35 | // 通过 token 查找用户
36 | const user = await this.usersService.findByToken(token)
37 |
38 | if (!user) {
39 | throw new UnauthorizedException('登录失效,请重新登录')
40 | }
41 |
42 | // 将用户信息附加到请求对象
43 | request['user'] = user
44 | return true
45 | } catch {
46 | throw new UnauthorizedException('登录失效,请重新登录')
47 | }
48 | }
49 |
50 | private extractTokenFromHeader(request: Request): string | undefined {
51 | // 从请求头中提取 token
52 | const token =
53 | request.headers.token ||
54 | request.headers.authorization ||
55 | (request.body && request.body.token)
56 |
57 | return token ? token.toString() : undefined
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/services/bill.ts:
--------------------------------------------------------------------------------
1 | import http from '@/utils/http'
2 |
3 | // 类型
4 | export function serviceGetBillType() {
5 | return http.post('/bill-type/getAll')
6 | }
7 |
8 | export function serviceDeleteBillType(ids: string[]) {
9 | return http.post(
10 | `/bill-type/delete`,
11 | { ids },
12 | {
13 | headers: { successAlert: 'true' },
14 | },
15 | )
16 | }
17 |
18 | export function serviceUpdateBillType(id: string, data: object) {
19 | return http.post(
20 | `/bill-type/update`,
21 | { id, ...data },
22 | {
23 | headers: { successAlert: 'true' },
24 | },
25 | )
26 | }
27 |
28 | export function serviceCreateBillType(data: object) {
29 | return http.post('/bill-type/add', data, {
30 | headers: { successAlert: 'true' },
31 | })
32 | }
33 |
34 | // 资金流动
35 | export function serviceGetBill(data?: object) {
36 | return http.post('/bill/getAll', data)
37 | }
38 |
39 | export function serviceDeleteBill(id: string) {
40 | return http.post(
41 | `/bill/delete`,
42 | { id },
43 | {
44 | headers: { successAlert: 'true' },
45 | },
46 | )
47 | }
48 |
49 | export function serviceUpdateBill(id: string, data: object) {
50 | return http.post(
51 | `/bill/update`,
52 | { id, ...data },
53 | {
54 | headers: { successAlert: 'true' },
55 | },
56 | )
57 | }
58 |
59 | export function serviceCreateBill(data: object) {
60 | return http.post('/bill/add', data)
61 | }
62 |
63 | export function serviceGetBillAmount(params?: object) {
64 | return http.post('/bill/amount/statistics', { ...params })
65 | }
66 |
67 | export function serviceGetBillAmountGroup(params: object) {
68 | return http.post('/bill/amount/group', { ...params })
69 | }
70 |
71 | export function serviceGetAmountById(id: string) {
72 | return http.post(`/bill/get`, { id })
73 | }
74 |
--------------------------------------------------------------------------------
/src/views/memorandum/DetailPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useEffect, useState } from 'react'
2 | import '@/assets/styles/markdown.css'
3 | import './style.scss'
4 | import { useParams, Link, useNavigate } from 'react-router'
5 | import { serviceGetMemorandumById } from '@/services'
6 | import { defaultTitle } from './constants'
7 | import { LeftOutlined, EditOutlined } from '@ant-design/icons'
8 | import { Spin } from 'antd'
9 | import config from '@/config'
10 |
11 | const DetailPage: FC = () => {
12 | const navigate = useNavigate()
13 | const [title, setTitle] = useState('')
14 | const [content, setContent] = useState('')
15 | const [loading, setLoading] = useState(true)
16 | const { id } = useParams()
17 |
18 | useEffect(() => {
19 | if (!id) return
20 |
21 | serviceGetMemorandumById(id)
22 | .then((res) => {
23 | const title = res.title || defaultTitle
24 | document.title = `${title} - ${config.title}`
25 | setTitle(title)
26 | setContent(res.html)
27 | })
28 | .finally(() => setLoading(false))
29 | }, [id])
30 |
31 | function goBack() {
32 | navigate('/home/memorandum', { replace: true })
33 | }
34 |
35 | return (
36 |
37 |
38 |
39 |
40 |
{title}
41 |
42 |
43 |
44 |
45 |
46 |
50 |
51 |
52 | )
53 | }
54 |
55 | export default DetailPage
56 |
--------------------------------------------------------------------------------
/src/views/log/DetailDrawer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 | import { Drawer, Collapse } from 'antd'
3 | import type { CollapseProps } from 'antd/es/collapse'
4 | import { LOG_LIST } from './constants'
5 |
6 | interface Props {
7 | visible: boolean
8 | onClose: () => void
9 | detail: Record
10 | }
11 |
12 | const defaultActiveKey = ['1', '2', '3', '5']
13 |
14 | const DetailDrawer: React.FC = function ({ visible, onClose, detail }) {
15 | const record: Record = useMemo(() => {
16 | const data = LOG_LIST.find(
17 | (item) => Number(item.key) === Number(detail.logType),
18 | )
19 | if (!data) {
20 | return {}
21 | }
22 |
23 | return {
24 | ...data,
25 | title: `${data.name} - ${detail.companyName}`,
26 | }
27 | }, [detail])
28 |
29 | const items: CollapseProps['items'] = [
30 | {
31 | key: '1',
32 | label: record.doneTitle,
33 | children: {detail.doneContent || '无'},
34 | },
35 | {
36 | key: '2',
37 | label: record.undoneTitle,
38 | children: {detail.undoneContent || '无'},
39 | },
40 | {
41 | key: '3',
42 | label: record.planTitle,
43 | children: {detail.planContent || '无'},
44 | },
45 | {
46 | key: '5',
47 | label: record.summaryTitle,
48 | children: {detail.summaryContent || '无'},
49 | },
50 | ]
51 |
52 | return (
53 |
60 | 日期:{detail.__createdAt__}
61 |
66 |
67 | )
68 | }
69 |
70 | export default DetailDrawer
71 |
--------------------------------------------------------------------------------
/src/views/todo-list/CreateTodoModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { serviceCreateTodoList, serviceUpdateTodoList } from '@/services'
3 | import { Modal, Form, Input } from 'antd'
4 |
5 | type Props = {
6 | visible: boolean
7 | rowData?: Record | null
8 | onSuccess: () => void
9 | onCancel: () => void
10 | }
11 |
12 | const { TextArea } = Input
13 |
14 | const CreateTodoModal: React.FC = function ({
15 | visible,
16 | onSuccess,
17 | onCancel,
18 | rowData,
19 | }) {
20 | const [form] = Form.useForm()
21 | const [submitting, setSubmitting] = useState(false)
22 |
23 | async function handleSubmitForm() {
24 | try {
25 | setSubmitting(true)
26 | const values = await form.validateFields()
27 | const params = {
28 | content: values.content.trim(),
29 | }
30 |
31 | ;(!rowData
32 | ? serviceCreateTodoList(params)
33 | : serviceUpdateTodoList(rowData.id, params)
34 | )
35 | .then(() => {
36 | onSuccess()
37 | })
38 | .finally(() => {
39 | setSubmitting(false)
40 | })
41 | } catch (error) {
42 | console.log(error)
43 | }
44 | }
45 |
46 | return (
47 |
55 |
67 |
68 |
69 |
70 |
71 | )
72 | }
73 |
74 | export default React.memo(CreateTodoModal)
75 |
--------------------------------------------------------------------------------
/src/views/login/style.scss:
--------------------------------------------------------------------------------
1 | @use '@/assets/styles/variables.scss' as *;
2 |
3 | @keyframes octocat {
4 | 0%,
5 | 100% {
6 | transform: rotate(0);
7 | }
8 | 20%,
9 | 60% {
10 | transform: rotate(-25deg);
11 | }
12 | 40%,
13 | 80% {
14 | transform: rotate(100deg);
15 | }
16 | }
17 |
18 | .login-page {
19 | display: flex;
20 | min-height: 100vh;
21 | background: #f0f2f5
22 | url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg')
23 | no-repeat center 110px;
24 | background-size: 100%;
25 | flex-direction: column;
26 |
27 | .octo-arm {
28 | animation: octocat 1.1s linear infinite;
29 | }
30 |
31 | .svg-wrapper {
32 | z-index: 10001;
33 | fill: #20a0ff;
34 | color: #fff;
35 | position: fixed;
36 | top: -6px;
37 | border: 0;
38 | right: -6px;
39 |
40 | .octo-arm {
41 | transform-origin: 130px 106px;
42 | }
43 | }
44 |
45 | .wrap {
46 | flex: 1;
47 | margin-top: 90px;
48 | width: 320px;
49 | align-self: center;
50 | }
51 |
52 | .ant-input-affix-wrapper:not(:nth-child(1)) {
53 | margin-top: 20px;
54 | }
55 |
56 | .anticon {
57 | color: #0000004d;
58 | }
59 |
60 | .logo-wrap {
61 | margin-bottom: 30px;
62 | text-align: center;
63 |
64 | .logo {
65 | width: 96px;
66 | height: 96px;
67 | -webkit-user-drag: none;
68 | }
69 |
70 | em {
71 | font-size: 24px;
72 | font-weight: bold;
73 | color: #000;
74 | margin-left: 15px;
75 | vertical-align: text-top;
76 | }
77 | }
78 |
79 | .login-bar {
80 | text-align: right;
81 | padding-right: 10px;
82 | font-size: 20px;
83 | }
84 |
85 | .captcha {
86 | display: block;
87 | cursor: pointer;
88 | height: 30px;
89 | border-left: 1px solid #ccc;
90 | }
91 |
92 | .register {
93 | margin-top: 15px;
94 | text-align: right;
95 | color: $theme-color;
96 | font-size: 14px;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/server/src/modules/inner-messages/inner-messages.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Get,
4 | Post,
5 | Body,
6 | Patch,
7 | Param,
8 | Delete,
9 | UseGuards,
10 | Request,
11 | } from '@nestjs/common'
12 | import { UserAuthGuard } from '@/guards/user-auth.guard'
13 | import { InnerMessagesService } from './inner-messages.service'
14 | import { CreateInnerMessageDto } from './dto/create-inner-message.dto'
15 | import { UpdateInnerMessageDto } from './dto/update-inner-message.dto'
16 |
17 | @Controller('inner-messages')
18 | @UseGuards(UserAuthGuard)
19 | export class InnerMessagesController {
20 | constructor(private readonly innerMessagesService: InnerMessagesService) {}
21 |
22 | @Post()
23 | create(@Request() req, @Body() createInnerMessageDto: CreateInnerMessageDto) {
24 | return this.innerMessagesService.create(req.user.uid, createInnerMessageDto)
25 | }
26 |
27 | @Post('get')
28 | async findAll(@Request() req) {
29 | return {
30 | rows: await this.innerMessagesService.findAll(req.user.uid),
31 | }
32 | }
33 |
34 | @Get('unread')
35 | findUnread(@Request() req) {
36 | return this.innerMessagesService.findUnread(req.user.uid)
37 | }
38 |
39 | @Get(':id')
40 | findOne(@Request() req, @Param('id') id: string) {
41 | return this.innerMessagesService.findOne(id, req.user.uid)
42 | }
43 |
44 | @Patch(':id')
45 | update(
46 | @Request() req,
47 | @Param('id') id: string,
48 | @Body() updateInnerMessageDto: UpdateInnerMessageDto,
49 | ) {
50 | return this.innerMessagesService.update(
51 | id,
52 | req.user.uid,
53 | updateInnerMessageDto,
54 | )
55 | }
56 |
57 | @Patch(':id/read')
58 | markAsRead(@Request() req, @Param('id') id: string) {
59 | return this.innerMessagesService.markAsRead(id, req.user.uid)
60 | }
61 |
62 | @Delete(':id')
63 | remove(@Request() req, @Param('id') id: string) {
64 | return this.innerMessagesService.remove(id, req.user.uid)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/server/src/modules/reminders/dto/create-reminder.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsNotEmpty,
3 | IsNumber,
4 | IsOptional,
5 | IsString,
6 | IsBoolean,
7 | Min,
8 | Max,
9 | ValidateIf,
10 | ValidatorConstraint,
11 | ValidatorConstraintInterface,
12 | registerDecorator,
13 | } from 'class-validator'
14 | import { Transform } from 'class-transformer'
15 | import * as dayjs from 'dayjs'
16 | import { isValidCronExpression, getNextCronExecution } from '@/utils/cronUtils'
17 |
18 | @ValidatorConstraint({ name: 'isValidCron', async: false })
19 | export class IsValidCronConstraint implements ValidatorConstraintInterface {
20 | validate(value: string) {
21 | return isValidCronExpression(value)
22 | }
23 |
24 | defaultMessage() {
25 | return '请输入有效的 cron 表达式,例如:0 9 * * *(每天早上9点)'
26 | }
27 | }
28 |
29 | // 创建装饰器
30 | export function IsValidCron() {
31 | return function (object: object, propertyName: string) {
32 | registerDecorator({
33 | name: 'isValidCron',
34 | target: object.constructor,
35 | propertyName: propertyName,
36 | validator: IsValidCronConstraint,
37 | })
38 | }
39 | }
40 |
41 | export class CreateReminderDto {
42 | @IsNotEmpty()
43 | @IsString()
44 | content: string
45 |
46 | @IsOptional()
47 | @Transform(({ value, obj }) => {
48 | // 如果设置了 cron, 将 cron 的值更新到 date 中
49 | if (obj.cron && isValidCronExpression(obj.cron)) {
50 | value = getNextCronExecution(obj.cron)
51 | }
52 |
53 | if (typeof value === 'number') return value
54 | const now = Date.now()
55 | try {
56 | return dayjs(value).valueOf() || now
57 | } catch {
58 | return now
59 | }
60 | })
61 | date?: number
62 |
63 | @IsOptional()
64 | @IsNumber()
65 | @Min(1)
66 | @Max(2)
67 | type?: number = 1
68 |
69 | @IsOptional()
70 | @IsString()
71 | @ValidateIf((o) => o.cron !== undefined && o.cron !== '')
72 | @IsValidCron()
73 | cron?: string
74 |
75 | @IsOptional()
76 | @IsBoolean()
77 | open?: boolean
78 | }
79 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './helper'
2 | export * from './date'
3 | import { serviceLogout } from '@/services'
4 | import { LOCAL_STORAGE } from '@/constants'
5 | import type { RcFile } from 'antd/es/upload'
6 |
7 | export function filterOption(input: string, option: any): boolean {
8 | if (Array.isArray(option.options)) {
9 | return option.options.some(
10 | (item: any) =>
11 | item.children.toLowerCase().indexOf(input.toLowerCase()) >= 0,
12 | )
13 | } else {
14 | return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
15 | }
16 | }
17 |
18 | export function sleep(delay?: number) {
19 | return new Promise((resolve) => setTimeout(resolve, delay))
20 | }
21 |
22 | // 注销登录
23 | export function logout() {
24 | serviceLogout()
25 |
26 | const localStorageWhiteList = [LOCAL_STORAGE.LOGIN_NAME]
27 | Array.from({ length: localStorage.length }, (_, i) => {
28 | const key = localStorage.key(i)
29 | return key
30 | }).forEach((key) => {
31 | if (key && !localStorageWhiteList.includes(key)) {
32 | localStorage.removeItem(key)
33 | }
34 | })
35 |
36 | sessionStorage.clear()
37 | location.reload()
38 | }
39 |
40 | export const getBase64 = (file: RcFile): Promise =>
41 | new Promise((resolve, reject) => {
42 | const reader = new FileReader()
43 | reader.readAsDataURL(file)
44 | reader.onload = () => resolve(reader.result as string)
45 | reader.onerror = (error) => reject(error)
46 | })
47 |
48 | export function base64ToBlob(base64Data: string) {
49 | let arr = base64Data.split(',') as any,
50 | fileType = arr[0].match(/:(.*?);/)[1],
51 | bstr = atob(arr[1]),
52 | l = bstr.length,
53 | u8Arr = new Uint8Array(l)
54 |
55 | while (l--) {
56 | u8Arr[l] = bstr.charCodeAt(l)
57 | }
58 | return new Blob([u8Arr], {
59 | type: fileType,
60 | })
61 | }
62 |
63 | export function isMobile() {
64 | return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
65 | navigator.userAgent,
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/src/views/setting/notification/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 消息通知
3 | */
4 | import React, { useState, useEffect } from 'react'
5 | import './style.scss'
6 | import { Switch, Divider } from 'antd'
7 | import { serviceGetUserConfig, serviceUpdateUserConfig } from '@/services'
8 |
9 | const NotificationPage: React.FC = function () {
10 | const [userConfig, setUserConfig] = useState>({
11 | isMatterNotify: true,
12 | isTaskNotify: true,
13 | })
14 |
15 | useEffect(() => {
16 | serviceGetUserConfig().then((res) => {
17 | setUserConfig({
18 | ...res,
19 | })
20 | })
21 | }, [])
22 |
23 | function handleUpdateUserConfig(type: number, checked: boolean) {
24 | const fields: any = {
25 | 0: 'isTaskNotify',
26 | 1: 'isMatterNotify',
27 | }
28 | serviceUpdateUserConfig({
29 | [fields[type]]: checked,
30 | }).then(() => {
31 | setUserConfig({
32 | ...userConfig,
33 | [fields[type]]: checked,
34 | })
35 | })
36 | }
37 |
38 | return (
39 |
40 |
41 | 消息通知
42 |
43 |
44 |
45 |
待办任务
46 |
47 | 开通后将以站内信的形式通知并且通知到邮箱, 否则只会站内信通知
48 |
49 |
50 |
54 |
55 |
56 |
57 |
提醒事项
58 |
59 | 开通后将以站内信的形式通知并且通知到邮箱, 否则只会站内信通知
60 |
61 |
62 |
66 |
67 |
68 | )
69 | }
70 |
71 | export default NotificationPage
72 |
--------------------------------------------------------------------------------
/server/src/modules/tasks/tasks.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, Body, UseGuards } from '@nestjs/common'
2 | import { UserAuthGuard } from '@/guards/user-auth.guard'
3 | import { User } from '@/decorators/user.decorator'
4 | import { TasksService } from './tasks.service'
5 | import { CreateTaskDto } from './dto/create-task.dto'
6 | import { UpdateTaskDto } from './dto/update-task.dto'
7 | import { GetTaskDto } from './dto/get-task.dto'
8 | import { Task } from './entities/task.entity'
9 |
10 | @Controller('task')
11 | @UseGuards(UserAuthGuard)
12 | export class TasksController {
13 | constructor(private readonly tasksService: TasksService) {}
14 |
15 | @Post('add')
16 | create(@User() user, @Body() createTaskDto: CreateTaskDto) {
17 | return this.tasksService.create(user.uid, createTaskDto)
18 | }
19 |
20 | @Post('getAll')
21 | async findAll(@User() user, @Body() getTaskDto: GetTaskDto) {
22 | const tasks = await this.tasksService.findAll(user.uid, getTaskDto)
23 | const data: Record = {
24 | wait: [],
25 | process: [],
26 | finished: [],
27 | unfinished: [],
28 | }
29 |
30 | tasks.forEach((item) => {
31 | switch (item.type) {
32 | case 1:
33 | data.wait.push(item)
34 | break
35 | case 2:
36 | data.process.push(item)
37 | break
38 | case 3:
39 | data.finished.push(item)
40 | break
41 | case 4:
42 | data.unfinished.push(item)
43 | break
44 | default:
45 | }
46 | })
47 | return data
48 | }
49 |
50 | @Post('get')
51 | findOne(@User() user, @Body('id') id: string) {
52 | return this.tasksService.findOne(id as string, user.uid)
53 | }
54 |
55 | @Post('update')
56 | update(@User() user, @Body() updateTaskDto: UpdateTaskDto) {
57 | return this.tasksService.update(user.uid, updateTaskDto)
58 | }
59 |
60 | @Post('delete')
61 | remove(@User() user, @Body('id') id: string) {
62 | return this.tasksService.remove(id, user.uid)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/server/src/modules/bills/bills.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Post,
4 | Body,
5 | UseGuards,
6 | BadRequestException,
7 | } from '@nestjs/common'
8 | import { BillsService } from './bills.service'
9 | import { CreateBillDto } from './dto/create-bill.dto'
10 | import { UpdateBillDto } from './dto/update-bill.dto'
11 | import { UserAuthGuard } from '@/guards/user-auth.guard'
12 | import { User } from '@/decorators/user.decorator'
13 | import { GetBillDto } from './dto/get-bill.dto'
14 |
15 | @Controller('bill')
16 | @UseGuards(UserAuthGuard)
17 | export class BillsController {
18 | constructor(private readonly billsService: BillsService) {}
19 |
20 | @Post('add')
21 | create(@User('uid') uid: number, @Body() createBillDto: CreateBillDto) {
22 | return this.billsService.create(uid, createBillDto)
23 | }
24 |
25 | @Post('getAll')
26 | findAll(@User('uid') uid: number, @Body() getBillDto: GetBillDto) {
27 | return this.billsService.findAll(uid, getBillDto)
28 | }
29 |
30 | @Post('get')
31 | findOne(@User('uid') uid: number, @Body('id') id: string) {
32 | return this.billsService.findOne(uid, id)
33 | }
34 |
35 | @Post('update')
36 | async update(@User('uid') uid: number, @Body() updateBillDto: UpdateBillDto) {
37 | return this.billsService.update(uid, updateBillDto)
38 | }
39 |
40 | @Post('delete')
41 | async remove(@User('uid') uid: number, @Body('id') id: string) {
42 | return this.billsService.remove(uid, id)
43 | }
44 |
45 | @Post('amount/statistics')
46 | async sumAmount(@User('uid') uid: number, @Body() getBillDto: GetBillDto) {
47 | try {
48 | return {
49 | data: await this.billsService.findSumPriceByDate(uid, getBillDto),
50 | }
51 | } catch {
52 | throw new BadRequestException('获取金额统计失败')
53 | }
54 | }
55 |
56 | @Post('amount/group')
57 | amountGroup(@User('uid') uid: number, @Body() getBillDto: GetBillDto) {
58 | try {
59 | return this.billsService.findAmountGroup(uid, getBillDto)
60 | } catch {
61 | throw new BadRequestException('获取分组统计失败')
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/views/today-task/TaskItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './style.scss'
3 | import { serviceDeleteTask, serviceUpdateTask } from '@/services'
4 | import { Card, Button, Rate, Popconfirm } from 'antd'
5 | import { formatDateTime } from '@/utils'
6 |
7 | interface Props {
8 | data: Record
9 | onOk(): void
10 | }
11 |
12 | const TaskItem: React.FC = ({ data, onOk }) => {
13 | function handleDelete() {
14 | serviceDeleteTask(data.id).then(() => {
15 | onOk()
16 | })
17 | }
18 |
19 | function handleNextAction() {
20 | serviceUpdateTask(data.id, {
21 | type: data.type + 1,
22 | }).then(() => {
23 | onOk()
24 | })
25 | }
26 |
27 | function handleBackAction() {
28 | serviceUpdateTask(data.id, {
29 | type: data.type - 1,
30 | }).then(() => {
31 | onOk()
32 | })
33 | }
34 |
35 | return (
36 |
37 | {data.content}
38 |
39 |
优先级别:
40 |
41 |
创建时间: {formatDateTime(data.date)}
42 |
43 |
44 |
45 |
51 |
54 |
55 |
56 | {data.type === 1 && (
57 |
60 | )}
61 | {[2, 3].includes(data.type) && (
62 |
65 | )}
66 | {data.type === 2 && (
67 |
70 | )}
71 |
72 |
73 | )
74 | }
75 |
76 | export default React.memo(TaskItem)
77 |
--------------------------------------------------------------------------------
/src/constants/menu.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | HomeOutlined,
4 | ClockCircleOutlined,
5 | FileDoneOutlined,
6 | ScheduleOutlined,
7 | BarChartOutlined,
8 | FormOutlined,
9 | UserOutlined,
10 | InsertRowLeftOutlined,
11 | SnippetsOutlined,
12 | } from '@ant-design/icons'
13 |
14 | export const HOME_SIDER_MENU_LIST = [
15 | {
16 | path: '/home/index',
17 | icon: ,
18 | name: '后台首页',
19 | },
20 | {
21 | path: '/home/reminder',
22 | icon: ,
23 | name: '提醒事项',
24 | },
25 | {
26 | path: '/home/todoList',
27 | icon: ,
28 | name: '活动清单',
29 | },
30 | {
31 | path: '/home/todayTask',
32 | icon: ,
33 | name: '今日待办',
34 | },
35 | {
36 | path: '/home/log',
37 | icon: ,
38 | name: '日志管理',
39 | },
40 | {
41 | path: '/home/company',
42 | icon: ,
43 | name: '公司单位',
44 | },
45 | {
46 | path: '',
47 | icon: ,
48 | name: '我的账单',
49 | children: [
50 | {
51 | path: '/home/bill',
52 | name: '资金流动',
53 | },
54 | {
55 | path: '/home/bill/type',
56 | name: '账单类别',
57 | },
58 | ],
59 | },
60 | {
61 | path: '',
62 | icon: ,
63 | name: '我的备忘',
64 | children: [
65 | {
66 | path: '/home/memorandum',
67 | name: '备忘录列表',
68 | },
69 | {
70 | path: '/home/memorandum/create',
71 | name: '备忘录创建',
72 | },
73 | ],
74 | },
75 | {
76 | path: '/home/setting/base',
77 | icon: ,
78 | name: '个人中心',
79 | },
80 | ]
81 |
82 | export const SETTING_SIDER_MENU_LIST = [
83 | {
84 | path: '/home/setting/base',
85 | name: '个人中心',
86 | },
87 | {
88 | path: '/home/setting/innerMessage',
89 | name: '消息中心',
90 | },
91 | {
92 | path: '/home/setting/notification',
93 | name: '消息通知',
94 | },
95 | {
96 | path: '/home/setting/account',
97 | name: '账号设置',
98 | },
99 | ]
100 |
--------------------------------------------------------------------------------
/src/views/index/PenelGroup.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import './style.scss'
3 | import NumberFlow from '@number-flow/react'
4 | import { Link } from 'react-router'
5 | import { serviceGetPanelData } from '@/services'
6 | import {
7 | PropertySafetyFilled,
8 | ScheduleFilled,
9 | FileTextFilled,
10 | AlertFilled,
11 | } from '@ant-design/icons'
12 |
13 | const PanelGroup = () => {
14 | const isInit = useRef(false)
15 | const [state, setState] = useState([
16 | {
17 | title: '今日支出',
18 | total: 0,
19 | Icon: ,
20 | prefix: '¥',
21 | path: '/home/bill',
22 | },
23 | {
24 | title: '今日待办',
25 | total: 0,
26 | Icon: ,
27 | path: '/home/todayTask',
28 | },
29 | {
30 | title: '活动清单',
31 | total: 0,
32 | Icon: ,
33 | path: '/home/todoList',
34 | },
35 | {
36 | title: '提醒事项',
37 | total: 0,
38 | Icon: ,
39 | path: '/home/reminder',
40 | },
41 | ])
42 |
43 | useEffect(() => {
44 | if (isInit.current) return
45 |
46 | isInit.current = true
47 |
48 | serviceGetPanelData().then((res) => {
49 | const data = state.slice()
50 | data[0].total = Number(res.consumption)
51 | data[1].total = res.todayTaskCount
52 | data[2].total = res.unfinishedTodoListCount
53 | data[3].total = res.reminderCount
54 | setState(data)
55 | })
56 | }, [state])
57 |
58 | return (
59 |
60 | {state.map((item) => (
61 |
62 |
63 | {item.Icon}
64 |
65 |
{item.title}
66 |
67 |
68 |
69 |
70 | ))}
71 |
72 | )
73 | }
74 |
75 | export default React.memo(PanelGroup)
76 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tomato-work",
3 | "version": "3.0.0",
4 | "homepage": "https://github.com/xjh22222228/tomato-work",
5 | "bugs": {
6 | "url": "https://github.com/xjh22222228/tomato-work/issues"
7 | },
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/xjh22222228/tomato-work.git"
11 | },
12 | "scripts": {
13 | "format": "npm run lint && prettier --write \"**/*.{ts,tsx,scss,css,jsx}\"",
14 | "dev": "vite",
15 | "start": "vite",
16 | "start:prod": "vite --mode prod",
17 | "build": "tsc && vite build --mode prod",
18 | "serve": "vite preview",
19 | "lint": "oxlint"
20 | },
21 | "lint-staged": {
22 | "*.{ts,tsx,scss,css,jsx}": "npm run format"
23 | },
24 | "dependencies": {
25 | "@ant-design/icons": "^6.1.0",
26 | "@number-flow/react": "^0.5.10",
27 | "@reduxjs/toolkit": "^2.11.2",
28 | "@tailwindcss/postcss": "^4.1.18",
29 | "@tailwindcss/vite": "^4.1.18",
30 | "@toast-ui/editor": "^3.2.2",
31 | "ahooks": "^3.9.6",
32 | "antd": "^6.1.1",
33 | "axios": "^1.13.2",
34 | "blueimp-md5": "^2.19.0",
35 | "bytes": "^3.1.2",
36 | "classnames": "^2.5.1",
37 | "dayjs": "^1.11.19",
38 | "lodash": "^4.17.21",
39 | "nprogress": "^0.2.0",
40 | "query-string": "^9.3.1",
41 | "react": "^19.2.3",
42 | "react-beautiful-dnd": "^13.1.1",
43 | "react-dom": "^19.2.3",
44 | "react-redux": "^9.2.0",
45 | "react-router": "^7.11.0",
46 | "recharts": "^3.6.0",
47 | "redux": "^5.0.1",
48 | "redux-thunk": "^3.1.0",
49 | "sass": "^1.97.1",
50 | "tailwindcss": "^4.1.18",
51 | "vite-plugin-pwa": "^1.2.0",
52 | "workbox-window": "^7.4.0"
53 | },
54 | "devDependencies": {
55 | "@types/blueimp-md5": "^2.18.2",
56 | "@types/bytes": "^3.1.5",
57 | "@types/lodash": "^4.17.21",
58 | "@types/node": "^25.0.3",
59 | "@types/nprogress": "^0.2.3",
60 | "@types/react": "^19.2.7",
61 | "@types/react-beautiful-dnd": "^13.1.8",
62 | "@types/react-dom": "^19.2.3",
63 | "@types/react-redux": "^7.1.34",
64 | "@vitejs/plugin-react-refresh": "^1.3.6",
65 | "husky": "^9.1.7",
66 | "lint-staged": "^16.2.7",
67 | "oxlint": "^1.34.0",
68 | "prettier": "^3.7.4",
69 | "typescript": "^5.9.3",
70 | "vite": "^7.3.0"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/header/style.scss:
--------------------------------------------------------------------------------
1 | @use '@/assets/styles/mixins.scss' as *;
2 | @use '@/assets/styles/variables.scss' as *;
3 |
4 | .ant-layout-header {
5 | z-index: 1;
6 | height: 60px;
7 | line-height: 60px;
8 | padding: 0 20px !important;
9 | background: #fff !important;
10 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
11 | height: 60px !important;
12 | overflow: hidden;
13 |
14 | .left {
15 | float: left;
16 | }
17 |
18 | a {
19 | display: block;
20 | color: inherit;
21 | }
22 |
23 | .right {
24 | float: right;
25 | overflow: hidden;
26 | font-size: 14px;
27 |
28 | li {
29 | float: left;
30 | height: 100%;
31 | cursor: pointer;
32 | padding: 0 10px;
33 |
34 | &:hover {
35 | background: #f7f7f7;
36 | color: #333;
37 | }
38 |
39 | svg {
40 | vertical-align: middle;
41 | font-size: 18px;
42 | }
43 | }
44 |
45 | .username {
46 | margin-left: 5px;
47 | vertical-align: middle;
48 | }
49 | }
50 | }
51 |
52 | .popover-content {
53 | width: 200px;
54 |
55 | .ls {
56 | display: block;
57 | padding: 8px 15px;
58 | border-bottom: 1px solid #f7f7f7;
59 | cursor: pointer;
60 | color: inherit;
61 |
62 | &:hover {
63 | background: #f5f5f5 !important;
64 | color: #595959;
65 | }
66 | }
67 |
68 | .sign-out {
69 | background: #fafafa;
70 | text-align: center;
71 | }
72 | }
73 |
74 | .message-popover {
75 | width: 330px;
76 | font-size: 12px;
77 |
78 | .msg-header {
79 | display: flex;
80 | background: #f7f7f7;
81 |
82 | .left {
83 | flex: 1;
84 | font-size: 14px;
85 | }
86 |
87 | .right {
88 | cursor: pointer;
89 | color: $theme-color;
90 |
91 | &:hover {
92 | text-decoration: underline;
93 | }
94 | }
95 | }
96 |
97 | .item-block {
98 | padding: 10px 20px;
99 |
100 | .content {
101 | @include ellipsis;
102 | }
103 | }
104 |
105 | .ls {
106 | display: block;
107 | cursor: pointer;
108 | border-bottom: 1px solid #eee;
109 | color: inherit;
110 |
111 | &:hover {
112 | background: #f7f7f7;
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/views/setting/inner-message/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 消息中心
3 | */
4 | import React, { useState, useEffect, useRef, useCallback } from 'react'
5 | import './style.scss'
6 | import Table from '@/components/table'
7 | import { Button } from 'antd'
8 | import {
9 | serviceGetInnerMessage,
10 | serviceUpdateInnerMessageHasRead,
11 | } from '@/services'
12 | import { formatDateMinute } from '@/utils'
13 |
14 | const InnerMessagePage = () => {
15 | const tableRef = useRef(null)
16 | const [selectedRowKeys, setSelectedRowKeys] = useState([])
17 | const tableColumns = [
18 | {
19 | title: '',
20 | dataIndex: 'hasRead',
21 | width: 12,
22 | className: 'unread-row',
23 | render: (hasRead: boolean) => !hasRead && ●,
24 | },
25 | { title: '标题内容', dataIndex: 'content' },
26 | { title: '提交时间', dataIndex: 'createdAt', width: 150 },
27 | { title: '类型', dataIndex: 'title', width: 130 },
28 | ]
29 |
30 | const getInnerMessage = useCallback((params?: object) => {
31 | return serviceGetInnerMessage(params).then((res) => {
32 | res.rows.forEach((item: any) => {
33 | item.createdAt = formatDateMinute(item.createdAt)
34 | })
35 | return res
36 | })
37 | }, [])
38 |
39 | function markHaveRead(params?: unknown) {
40 | params ||= selectedRowKeys.join()
41 |
42 | serviceUpdateInnerMessageHasRead(params).then(() => {
43 | setSelectedRowKeys([])
44 | tableRef.current.getTableData()
45 | })
46 | }
47 |
48 | function markAllHaveRead() {
49 | markHaveRead('all')
50 | }
51 |
52 | useEffect(() => {
53 | tableRef.current.getTableData()
54 | }, [tableRef])
55 |
56 | return (
57 |
58 |
63 | setSelectedRowKeys(selectedKeys)
64 | }
65 | />
66 |
67 |
70 |
71 |
72 |
73 | )
74 | }
75 |
76 | export default InnerMessagePage
77 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/store/userSlice.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2018-present the xiejiahe. All rights reserved. MIT license.
2 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'
3 | import { LOCAL_STORAGE } from '@/constants'
4 | import { isPlainObject } from 'lodash'
5 | import { serviceLoginByToken, serviceLoginByCode } from '@/services'
6 | import type { AppDispatch } from '.'
7 | import { formatDate } from '@/utils'
8 |
9 | export interface UserProps {
10 | provider: string
11 | uid?: number
12 | username: string
13 | loginName: string
14 | avatarUrl: string
15 | email: string
16 | role: string
17 | token?: string
18 | bio: string
19 | location: string
20 | createdAt: string
21 | }
22 |
23 | export interface UserState {
24 | isLogin: boolean
25 | isLockScreen: boolean
26 | userInfo: UserProps
27 | }
28 |
29 | let localUser
30 | try {
31 | const r = JSON.parse(localStorage.getItem(LOCAL_STORAGE.USER) as string)
32 | if (isPlainObject(r)) {
33 | localUser = r
34 | }
35 | } catch {}
36 |
37 | const initialState: UserState = {
38 | isLogin: !!localUser,
39 | isLockScreen: false,
40 | userInfo: localUser || {
41 | provider: '', // github ?
42 | uid: undefined, // 用户ID
43 | createdAt: '', // 注册时间
44 | bio: '', // 简介
45 | username: '', // 昵称
46 | loginName: '', // 登录名
47 | avatarUrl: '', // 头像
48 | email: '',
49 | role: '',
50 | token: undefined, // 登录凭证
51 | location: '',
52 | },
53 | }
54 |
55 | export const userSlice = createSlice({
56 | name: 'user',
57 | initialState,
58 | reducers: {
59 | SET_USER_INFO: (state, action: PayloadAction) => {
60 | const userInfo = action.payload
61 | userInfo.createdAt &&= formatDate(userInfo.createdAt)
62 | state.isLogin = !!userInfo.token
63 | state.userInfo = userInfo
64 | },
65 | },
66 | })
67 |
68 | export const { SET_USER_INFO } = userSlice.actions
69 |
70 | export const loginByToken = (token: string) => (dispatch: AppDispatch) => {
71 | return serviceLoginByToken(token).then((res) => {
72 | return dispatch(SET_USER_INFO(res.user))
73 | })
74 | }
75 |
76 | export const loginByCode = (code: string) => (dispatch: AppDispatch) => {
77 | return serviceLoginByCode(code).then((res) => {
78 | return dispatch(SET_USER_INFO(res.user))
79 | })
80 | }
81 |
82 | export default userSlice.reducer
83 |
--------------------------------------------------------------------------------
/server/src/modules/tasks/tasks.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common'
2 | import { InjectRepository } from '@nestjs/typeorm'
3 | import { Between, LessThan, Repository, Not, In } from 'typeorm'
4 | import * as dayjs from 'dayjs'
5 | import { CreateTaskDto } from './dto/create-task.dto'
6 | import { UpdateTaskDto } from './dto/update-task.dto'
7 | import { Task, TaskType } from './entities/task.entity'
8 | import { GetTaskDto } from './dto/get-task.dto'
9 |
10 | @Injectable()
11 | export class TasksService {
12 | constructor(
13 | @InjectRepository(Task)
14 | private tasksRepository: Repository,
15 | ) {}
16 |
17 | async create(uid: number, createTaskDto: CreateTaskDto): Promise {
18 | const newTask = this.tasksRepository.create({
19 | ...createTaskDto,
20 | uid,
21 | })
22 |
23 | return this.tasksRepository.save(newTask)
24 | }
25 |
26 | async findAll(uid: number, getTaskDto: GetTaskDto): Promise {
27 | const startOfDay = dayjs(getTaskDto.startDate).startOf('day').valueOf()
28 | const endOfDay = dayjs(getTaskDto.endDate).endOf('day').valueOf()
29 |
30 | return this.tasksRepository.find({
31 | where: {
32 | uid,
33 | date: Between(startOfDay, endOfDay),
34 | },
35 | order: { createdAt: 'DESC' },
36 | })
37 | }
38 |
39 | async findOne(id: string, uid: number): Promise {
40 | const task = await this.tasksRepository.findOne({
41 | where: { id, uid },
42 | })
43 |
44 | if (!task) {
45 | throw new NotFoundException('任务不存在')
46 | }
47 |
48 | return task
49 | }
50 |
51 | async update(uid: number, updateTaskDto: UpdateTaskDto): Promise {
52 | const { id, ...updateData } = updateTaskDto
53 | await this.tasksRepository.update({ uid, id }, updateData)
54 | return await this.findOne(id, uid)
55 | }
56 |
57 | async remove(id: string, uid: number): Promise {
58 | const result = await this.tasksRepository.delete({ id, uid })
59 | if (result.affected === 0) {
60 | throw new NotFoundException('任务不存在')
61 | }
62 | }
63 |
64 | async updateBeforeToDay() {
65 | await this.tasksRepository.update(
66 | {
67 | date: LessThan(dayjs().startOf('day').valueOf()),
68 | type: Not(In([TaskType.COMPLETED, TaskType.UNCOMPLETED])),
69 | },
70 | {
71 | type: TaskType.UNCOMPLETED,
72 | },
73 | )
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/views/bill/CreateTypeModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { Modal, Form, Input, Select } from 'antd'
3 | import { serviceCreateBillType, serviceUpdateBillType } from '@/services'
4 | import { TYPES } from './enum'
5 |
6 | type Props = {
7 | visible: boolean
8 | onSuccess: (res?: any) => void
9 | onCancel: () => void
10 | rowData: null | Record
11 | }
12 |
13 | const CreateTypeModal: React.FC = function ({
14 | visible,
15 | rowData,
16 | onCancel,
17 | onSuccess,
18 | }) {
19 | const [form] = Form.useForm()
20 | const [submitting, setSubmitting] = useState(false)
21 |
22 | async function handleSubmitForm() {
23 | try {
24 | const values = await form.validateFields()
25 |
26 | const params = {
27 | type: values.type,
28 | name: values.name.trim(),
29 | }
30 |
31 | setSubmitting(true)
32 | ;(rowData
33 | ? serviceUpdateBillType(rowData.id, params)
34 | : serviceCreateBillType(params)
35 | )
36 | .then((res) => {
37 | onSuccess(res.data)
38 | })
39 | .finally(() => {
40 | setSubmitting(false)
41 | })
42 | } catch (err) {
43 | console.log(err)
44 | }
45 | }
46 |
47 | useEffect(() => {
48 | if (visible && rowData) {
49 | form.setFieldsValue({
50 | name: rowData.name,
51 | type: rowData.type,
52 | })
53 | }
54 | }, [visible, rowData])
55 |
56 | return (
57 |
65 |
76 |
77 |
78 |
79 |
89 |
90 |
91 |
92 |
93 | )
94 | }
95 |
96 | export default React.memo(CreateTypeModal)
97 |
--------------------------------------------------------------------------------
/server/src/modules/common/common.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, Post, Query, Res, UseGuards } from '@nestjs/common'
2 | import { Response } from 'express'
3 | import * as dayjs from 'dayjs'
4 | import * as svgCaptcha from 'svg-captcha'
5 | import { UserAuthGuard } from '@/guards/user-auth.guard'
6 | import { User } from '@/decorators/user.decorator'
7 | import { CommonService } from './common.service'
8 | import { BillsService } from '../bills/bills.service'
9 | import { TasksService } from '../tasks/tasks.service'
10 | import { TodoListsService } from '../todo-lists/todo-lists.service'
11 | import { RemindersService } from '../reminders/reminders.service'
12 |
13 | @Controller()
14 | export class CommonController {
15 | constructor(
16 | private readonly commonService: CommonService,
17 | private readonly billsService: BillsService,
18 | private readonly tasksService: TasksService,
19 | private readonly todoListsService: TodoListsService,
20 | private readonly remindersService: RemindersService,
21 | ) {}
22 |
23 | @Get()
24 | getIndex(): string {
25 | return 'Welcome to Tomaro Work !'
26 | }
27 |
28 | @Get('captcha')
29 | getCaptcha(@Query('code') code: string = '1234', @Res() res: Response): void {
30 | const captcha = svgCaptcha.create({
31 | size: 4,
32 | ignoreChars: '0o1il',
33 | noise: 2,
34 | color: true,
35 | background: '#f0f0f0',
36 | })
37 |
38 | res.type('svg')
39 | res.send(captcha.data)
40 | }
41 |
42 | @Post('panel')
43 | @UseGuards(UserAuthGuard)
44 | async getPanelData(@User() user) {
45 | // 获取当前日期
46 | const currentDate = dayjs().format('YYYY-MM-DD')
47 |
48 | // 并行获取各项数据
49 | const [consumption, todayTasks, unfinishedTodoLists, reminders] =
50 | await Promise.all([
51 | this.billsService.findSumPriceByDate(user.uid, {
52 | startDate: currentDate,
53 | endDate: currentDate,
54 | }), // 支出类型
55 | this.tasksService.findAll(user.uid, {
56 | startDate: currentDate,
57 | endDate: currentDate,
58 | }),
59 | this.todoListsService.findAll(user.uid, { status: 1 }), // 未完成的待办
60 | this.remindersService.findAll(user.uid, { type: 1, open: true }), // 类型1的提醒
61 | ])
62 |
63 | return {
64 | consumption: consumption.find((item) => item.type === 2)?.price,
65 | todayTaskCount: todayTasks.length,
66 | unfinishedTodoListCount: unfinishedTodoLists.rows.length,
67 | reminderCount: reminders.count,
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/views/memorandum/style.scss:
--------------------------------------------------------------------------------
1 | // Index
2 | .memorandum-spin {
3 | overflow: hidden;
4 | overflow-y: auto !important;
5 | }
6 | .memorandum {
7 | .button-group {
8 | margin-top: 15px;
9 | text-align: right;
10 | .ant-btn {
11 | margin-left: 10px;
12 | }
13 | }
14 |
15 | .ant-card-body {
16 | padding: 10px 20px;
17 | }
18 |
19 | .ant-col {
20 | margin-bottom: 15px;
21 | }
22 |
23 | .ant-card-head-title,
24 | .ant-card-body {
25 | overflow: hidden;
26 | white-space: nowrap;
27 | text-overflow: ellipsis;
28 | }
29 |
30 | .content {
31 | margin-top: 5px;
32 | white-space: nowrap;
33 | overflow: hidden;
34 | text-overflow: ellipsis;
35 | & * {
36 | display: inline;
37 | font-size: 14px;
38 | font-weight: normal;
39 | text-align: left;
40 | }
41 | ul {
42 | margin: 0;
43 | margin-block: 0;
44 | padding-inline: 0;
45 | }
46 | img {
47 | max-width: 100%;
48 | }
49 | }
50 | }
51 |
52 | // CreatePage
53 | .editor-page {
54 | .input-title {
55 | font-weight: bold;
56 | }
57 |
58 | #edit-section {
59 | display: flex;
60 | flex-direction: column;
61 | margin: 20px 0;
62 | flex: 1;
63 | background: #fff;
64 | .toastui-editor-defaultUI {
65 | flex: 1;
66 | }
67 | }
68 |
69 | .button-group {
70 | text-align: right;
71 | .ant-btn {
72 | margin-left: 15px;
73 | }
74 | }
75 | }
76 |
77 | // DetailPage
78 | .detail-spin {
79 | overflow-y: auto !important;
80 | display: flex;
81 | flex: 1;
82 | .ant-spin-container {
83 | display: flex;
84 | flex: 1;
85 | }
86 | }
87 | .memorandum-detail {
88 | position: relative;
89 | padding: 15px 20px;
90 | background: #fff;
91 | overflow-y: auto !important;
92 | display: flex;
93 | flex-direction: column;
94 | flex: 1;
95 | .tool-bar {
96 | display: flex;
97 | justify-content: space-between;
98 | margin-bottom: 15px;
99 | align-items: center;
100 | border-bottom: 0.5px solid #e6e6e6;
101 | padding-bottom: 12px;
102 | }
103 | .markdown-body {
104 | flex: 1;
105 | overflow: hidden;
106 | overflow-y: auto;
107 | }
108 |
109 | .icon-left {
110 | font-size: 22px;
111 | cursor: pointer;
112 | }
113 |
114 | .title {
115 | padding: 0 12px;
116 | font-size: 26px;
117 | color: #262626;
118 | margin: 0;
119 | }
120 |
121 | .edit {
122 | font-size: 16px;
123 | color: inherit;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/server/src/modules/users/users.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, ConflictException } from '@nestjs/common'
2 | import { InjectRepository } from '@nestjs/typeorm'
3 | import { Repository } from 'typeorm'
4 | import { CreateUserDto } from './dto/create-user.dto'
5 | import { UpdateUserDto } from './dto/update-user.dto'
6 | import { User } from './entities/user.entity'
7 |
8 | @Injectable()
9 | export class UsersService {
10 | constructor(
11 | @InjectRepository(User)
12 | private usersRepository: Repository,
13 | ) {}
14 |
15 | async create(createUserDto: CreateUserDto): Promise {
16 | // 检查用户名或UID是否已存在
17 | const existingUser = await this.usersRepository.findOne({
18 | where: [{ username: createUserDto.username }, { uid: createUserDto.uid }],
19 | })
20 |
21 | if (existingUser) {
22 | throw new ConflictException('用户名或用户ID已存在')
23 | }
24 |
25 | // 创建新用户
26 | const newUser = this.usersRepository.create({
27 | ...createUserDto,
28 | })
29 |
30 | return this.usersRepository.save(newUser)
31 | }
32 |
33 | async findAll(): Promise {
34 | return this.usersRepository.find()
35 | }
36 |
37 | async findOne(uid: number): Promise {
38 | const numericUid = typeof uid === 'string' ? Number(uid) : uid
39 | const user = await this.usersRepository.findOne({
40 | where: { uid: numericUid },
41 | })
42 | return user
43 | }
44 |
45 | async findByLoginName(loginName: string): Promise {
46 | const user = await this.usersRepository.findOne({ where: { loginName } })
47 | return user || null
48 | }
49 |
50 | /**
51 | * 通过登录名和密码查找用户
52 | * @param loginName 登录名
53 | * @param password 密码(前端已加密)
54 | * @returns 用户信息或 null
55 | */
56 | async findByLoginNameAndPassword(
57 | loginName: string,
58 | password: string,
59 | ): Promise {
60 | const user = await this.usersRepository.findOne({
61 | where: {
62 | loginName,
63 | password,
64 | },
65 | })
66 | return user || null
67 | }
68 |
69 | /**
70 | * 通过 token 查找用户
71 | * @param token 用户 token
72 | * @returns 用户信息或 null
73 | */
74 | async findByToken(token: string): Promise {
75 | if (!token) return null
76 | const user = await this.usersRepository.findOne({ where: { token } })
77 | return user || null
78 | }
79 |
80 | async update(
81 | uid: number,
82 | updateUserDto: UpdateUserDto,
83 | ): Promise {
84 | await this.usersRepository.update({ uid }, updateUserDto)
85 | return this.findOne(uid)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/utils/http.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import NProgress from 'nprogress'
3 | import type { AxiosInstance, AxiosRequestConfig } from 'axios'
4 | import CONFIG from '@/config'
5 | import { message, notification } from 'antd'
6 | import { logout } from '@/utils'
7 | import { LOCAL_STORAGE } from '@/constants/storage'
8 |
9 | interface RespData {
10 | statusCode: number
11 | error?: string
12 | message?: string
13 | [key: string]: any
14 | }
15 |
16 | let exiting = false
17 |
18 | function handleError(error: any) {
19 | const status =
20 | error.status || error.response?.data?.status || error.code || ''
21 | const errorMsg = error.response?.data?.message || error.message || ''
22 | notification.error({
23 | message: 'Error:' + status,
24 | description: errorMsg,
25 | })
26 | }
27 |
28 | interface IAxiosInstance {
29 | get(url: string, config?: AxiosRequestConfig): Promise>
30 | post(
31 | url: string,
32 | data?: any,
33 | config?: AxiosRequestConfig,
34 | ): Promise>
35 | }
36 |
37 | const httpInstance: IAxiosInstance & AxiosInstance = axios.create({
38 | timeout: 60000,
39 | baseURL: CONFIG.http.baseURL,
40 | })
41 |
42 | httpInstance.interceptors.request.use(
43 | function (config) {
44 | NProgress.start()
45 | const method = config.method
46 | const token = localStorage.getItem(LOCAL_STORAGE.TOKEN)
47 |
48 | if (token) {
49 | if (config.headers) {
50 | config.headers.token = token
51 | }
52 | }
53 |
54 | const data: Record = {}
55 |
56 | if (method === 'post' || method === 'put') {
57 | if (config.data instanceof FormData) {
58 | for (let key in data) {
59 | config.data.append(key, data[key])
60 | }
61 | } else {
62 | config.data = Object.assign(data, config.data)
63 | }
64 | }
65 |
66 | return config
67 | },
68 | function (error) {
69 | handleError(error)
70 | return Promise.reject(error)
71 | },
72 | )
73 |
74 | httpInstance.interceptors.response.use(
75 | function (res) {
76 | NProgress.done()
77 | const headers = res.config.headers
78 | const data: RespData = res.data
79 | if (headers?.successAlert) {
80 | message.success(data.message || 'OK')
81 | }
82 | return res.data
83 | },
84 | function (error) {
85 | NProgress.done()
86 | if (error.response?.data?.statusCode === 401 && !exiting) {
87 | exiting = true
88 | logout()
89 | }
90 |
91 | handleError(error)
92 | return Promise.reject(error)
93 | },
94 | )
95 |
96 | export default httpInstance
97 |
--------------------------------------------------------------------------------
/vite.config.mts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import reactRefresh from '@vitejs/plugin-react-refresh'
3 | import path from 'node:path'
4 | import tailwindcss from '@tailwindcss/vite'
5 | import { VitePWA } from 'vite-plugin-pwa'
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig({
9 | plugins: [
10 | tailwindcss(),
11 | reactRefresh(),
12 | VitePWA({
13 | registerType: 'autoUpdate',
14 | includeAssets: [
15 | 'apple-touch-icon.png',
16 | 'pwa-192x192.png',
17 | 'pwa-512x512.png',
18 | ],
19 | manifest: {
20 | name: 'TomatoWork',
21 | short_name: 'TomatoWork',
22 | description: 'TomatoWork',
23 | theme_color: '#ffffff',
24 | background_color: '#ffffff',
25 | display: 'standalone',
26 | orientation: 'portrait',
27 | scope: '/',
28 | start_url: '/',
29 | icons: [
30 | {
31 | src: 'pwa-192x192.png',
32 | sizes: '192x192',
33 | type: 'image/png',
34 | purpose: 'any',
35 | },
36 | {
37 | src: 'pwa-512x512.png',
38 | sizes: '512x512',
39 | type: 'image/png',
40 | purpose: 'any',
41 | },
42 | {
43 | src: 'pwa-512x512.png',
44 | sizes: '512x512',
45 | type: 'image/png',
46 | purpose: 'maskable',
47 | },
48 | ],
49 | },
50 | workbox: {
51 | maximumFileSizeToCacheInBytes: 3 * 1024 * 1024, // 3MB
52 | globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
53 | runtimeCaching: [
54 | {
55 | urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
56 | handler: 'CacheFirst',
57 | options: {
58 | cacheName: 'google-fonts-cache',
59 | expiration: {
60 | maxEntries: 10,
61 | maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
62 | },
63 | cacheableResponse: {
64 | statuses: [0, 200],
65 | },
66 | },
67 | },
68 | ],
69 | },
70 | devOptions: {
71 | enabled: true,
72 | },
73 | }),
74 | ],
75 |
76 | resolve: {
77 | alias: [{ find: '@', replacement: path.resolve(__dirname, 'src') }],
78 | },
79 |
80 | server: {
81 | port: 7001,
82 | proxy: {
83 | '/api/passport': {
84 | target: 'http://localhost:7003',
85 | changeOrigin: true,
86 | },
87 | },
88 | },
89 |
90 | build: {
91 | outDir: 'build',
92 | rollupOptions: {
93 | external: ['workbox-window'],
94 | },
95 | },
96 | })
97 |
--------------------------------------------------------------------------------
/src/views/today-task/CreateTaskModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { isBefore } from '@/utils'
3 | import { serviceCreateTask } from '@/services'
4 | import { Modal, Form, Input, DatePicker, Rate } from 'antd'
5 | import dayjs from 'dayjs'
6 |
7 | type Props = {
8 | visible: boolean
9 | onOk(): void
10 | onCancel(): void
11 | }
12 |
13 | const { TextArea } = Input
14 |
15 | const CreateTaskModal: React.FC = function ({
16 | visible,
17 | onOk,
18 | onCancel,
19 | }) {
20 | const [form] = Form.useForm()
21 | const [submitting, setSubmitting] = useState(false)
22 |
23 | async function handleSubmitForm() {
24 | try {
25 | const values = await form.validateFields()
26 | const params = {
27 | date: dayjs(values.date).valueOf(),
28 | content: values.content.trim(),
29 | count: values.count,
30 | }
31 |
32 | setSubmitting(true)
33 |
34 | serviceCreateTask(params)
35 | .then(() => {
36 | onOk()
37 | })
38 | .finally(() => {
39 | setSubmitting(false)
40 | })
41 | } catch (err) {
42 | console.log(err)
43 | }
44 | }
45 |
46 | return (
47 |
55 |
66 |
71 |
72 |
73 |
83 |
84 |
85 |
86 |
97 |
98 |
99 |
100 |
101 | )
102 | }
103 |
104 | export default React.memo(CreateTaskModal)
105 |
--------------------------------------------------------------------------------
/server/src/modules/inner-messages/inner-messages.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common'
2 | import { InjectRepository } from '@nestjs/typeorm'
3 | import { Repository } from 'typeorm'
4 | import { v4 as uuidv4 } from 'uuid'
5 | import { CreateInnerMessageDto } from './dto/create-inner-message.dto'
6 | import { UpdateInnerMessageDto } from './dto/update-inner-message.dto'
7 | import { InnerMessage } from './entities/inner-message.entity'
8 |
9 | @Injectable()
10 | export class InnerMessagesService {
11 | constructor(
12 | @InjectRepository(InnerMessage)
13 | private innerMessagesRepository: Repository,
14 | ) {}
15 |
16 | async create(
17 | uid: number,
18 | createInnerMessageDto: CreateInnerMessageDto,
19 | ): Promise {
20 | const newInnerMessage = this.innerMessagesRepository.create({
21 | ...createInnerMessageDto,
22 | uid,
23 | id: uuidv4(),
24 | type: createInnerMessageDto.type || 0,
25 | hasRead: createInnerMessageDto.hasRead || false,
26 | })
27 |
28 | return this.innerMessagesRepository.save(newInnerMessage)
29 | }
30 |
31 | async findAll(uid: number): Promise {
32 | return this.innerMessagesRepository.find({
33 | where: { uid },
34 | order: { createdAt: 'DESC' },
35 | })
36 | }
37 |
38 | async findUnread(uid: number): Promise {
39 | return this.innerMessagesRepository.find({
40 | where: { uid, hasRead: false },
41 | order: { createdAt: 'DESC' },
42 | })
43 | }
44 |
45 | async findOne(id: string, uid: number): Promise {
46 | const innerMessage = await this.innerMessagesRepository.findOne({
47 | where: { id, uid },
48 | })
49 |
50 | if (!innerMessage) {
51 | throw new NotFoundException('消息不存在')
52 | }
53 |
54 | return innerMessage
55 | }
56 |
57 | async update(
58 | id: string,
59 | uid: number,
60 | updateInnerMessageDto: UpdateInnerMessageDto,
61 | ): Promise {
62 | const innerMessage = await this.findOne(id, uid)
63 | const updatedInnerMessage = Object.assign(
64 | innerMessage,
65 | updateInnerMessageDto,
66 | )
67 | return this.innerMessagesRepository.save(updatedInnerMessage)
68 | }
69 |
70 | async markAsRead(id: string, uid: number): Promise {
71 | const innerMessage = await this.findOne(id, uid)
72 | innerMessage.hasRead = true
73 | return this.innerMessagesRepository.save(innerMessage)
74 | }
75 |
76 | async remove(id: string, uid: number): Promise {
77 | const result = await this.innerMessagesRepository.delete({ id, uid })
78 | if (result.affected === 0) {
79 | throw new NotFoundException('消息不存在')
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/server/src/modules/memorandums/memorandums.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common'
2 | import { InjectRepository } from '@nestjs/typeorm'
3 | import { Repository } from 'typeorm'
4 | import { CreateMemorandumDto } from './dto/create-memorandum.dto'
5 | import { UpdateMemorandumDto } from './dto/update-memorandum.dto'
6 | import { Memorandum } from './entities/memorandum.entity'
7 | import { GetMemorandumDto } from './dto/get-memorandum.dto'
8 | import markdown from '@/utils/markdown'
9 |
10 | export interface MemorandumItem extends Memorandum {
11 | html: string
12 | }
13 |
14 | @Injectable()
15 | export class MemorandumsService {
16 | constructor(
17 | @InjectRepository(Memorandum)
18 | private memorandumsRepository: Repository,
19 | ) {}
20 |
21 | async create(
22 | uid: number,
23 | createMemorandumDto: CreateMemorandumDto,
24 | ): Promise {
25 | const newMemorandum = this.memorandumsRepository.create({
26 | ...createMemorandumDto,
27 | uid,
28 | })
29 |
30 | return this.memorandumsRepository.save(newMemorandum)
31 | }
32 |
33 | async findAll(
34 | uid: number,
35 | getMemorandumDto: GetMemorandumDto,
36 | ): Promise<{
37 | rows: MemorandumItem[]
38 | count: number
39 | }> {
40 | const { pageNo, pageSize } = getMemorandumDto
41 | const [rows, count] = await this.memorandumsRepository.findAndCount({
42 | where: { uid },
43 | order: { updatedAt: 'DESC' },
44 | skip: pageNo && pageSize && pageNo * pageSize,
45 | take: pageSize,
46 | })
47 | const result = rows.map((item) => {
48 | const html = markdown.render(item.markdown)
49 | return {
50 | ...item,
51 | html,
52 | }
53 | })
54 | return {
55 | rows: result,
56 | count,
57 | }
58 | }
59 |
60 | async findOne(id: string, uid: number): Promise {
61 | const memorandum = await this.memorandumsRepository.findOne({
62 | where: { id, uid },
63 | })
64 |
65 | if (!memorandum) {
66 | throw new NotFoundException('备忘录不存在')
67 | }
68 |
69 | return {
70 | ...memorandum,
71 | html: markdown.render(memorandum.markdown),
72 | }
73 | }
74 |
75 | async update(
76 | uid: number,
77 | updateMemorandumDto: UpdateMemorandumDto,
78 | ): Promise {
79 | const { id, ...updateData } = updateMemorandumDto
80 | await this.memorandumsRepository.update({ uid, id }, updateData)
81 | return this.findOne(id, uid)
82 | }
83 |
84 | async remove(id: string, uid: number): Promise {
85 | const result = await this.memorandumsRepository.delete({ id, uid })
86 | if (result.affected === 0) {
87 | throw new NotFoundException('备忘录不存在')
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/views/index/style.scss:
--------------------------------------------------------------------------------
1 | @use '../../assets/styles/variables.scss' as *;
2 | @use 'sass:list';
3 |
4 | .home-index {
5 | display: block !important;
6 | }
7 |
8 | // SystemInfo
9 | .system-data {
10 | .item-text {
11 | white-space: nowrap;
12 | text-overflow: ellipsis;
13 | overflow: hidden;
14 |
15 | &:nth-last-child(1) {
16 | margin-bottom: 0;
17 | }
18 |
19 | em {
20 | color: #000;
21 | font-weight: 500;
22 | }
23 | }
24 |
25 | .mem .ant-card-body {
26 | text-align: center;
27 | .surplus {
28 | margin-top: 10px;
29 | }
30 | }
31 | }
32 |
33 | // PanelGroup
34 | .panel-group {
35 | margin-bottom: 15px;
36 | $colors: #f50 #34bfa3 #36a3f7 #40c9c6;
37 | .icon svg {
38 | width: 75px;
39 | height: 75px;
40 | }
41 |
42 | .item {
43 | @for $i from 1 through 4 {
44 | &:nth-child(#{$i}) {
45 | .anticon {
46 | color: list.nth($colors, $i);
47 | }
48 |
49 | &:hover {
50 | .anticon {
51 | color: #fff;
52 | & > svg {
53 | background: list.nth($colors, $i);
54 | }
55 | }
56 | }
57 | }
58 | }
59 | }
60 |
61 | .ant-statistic-content {
62 | text-align: center;
63 | }
64 |
65 | .block-item {
66 | display: flex;
67 | padding: 20px 20px 20px 15px;
68 | background: #fff;
69 | cursor: pointer;
70 |
71 | .icon {
72 | flex: 1;
73 | text-align: left;
74 | align-self: center;
75 |
76 | & > svg {
77 | padding: 15px;
78 | border-radius: 5px;
79 | transition: 0.1s linear;
80 | }
81 | }
82 |
83 | .data {
84 | padding-top: 10px;
85 | text-align: right;
86 | align-self: center;
87 | font-size: 26px;
88 | color: #000;
89 | font-weight: bold;
90 | .title {
91 | font-size: 18px;
92 | color: #666;
93 | margin-bottom: 5px;
94 | }
95 | }
96 | }
97 | }
98 |
99 | // AmountChart
100 | .amount-chart {
101 | position: relative;
102 | background: #fff;
103 | margin-top: 15px;
104 |
105 | .title {
106 | display: flex;
107 | align-items: center;
108 | position: relative;
109 | padding: 15px 0 15px 30px;
110 |
111 | &:after {
112 | content: '';
113 | position: absolute;
114 | top: 17px;
115 | left: 16px;
116 | height: 30px;
117 | width: 3px;
118 | background-color: $theme-color;
119 | }
120 | }
121 |
122 | .date-picker {
123 | margin-left: 20px;
124 | }
125 |
126 | .no-data {
127 | height: 399px;
128 | display: flex;
129 | align-items: center;
130 | justify-content: center;
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/views/memorandum/CreatePage.tsx:
--------------------------------------------------------------------------------
1 | // https://ui.toast.com/tui-editor/
2 | import React, { useState, useEffect } from 'react'
3 | import './style.scss'
4 | import Editor from '@toast-ui/editor'
5 | import { Input, Button, message } from 'antd'
6 | import { useNavigate, useParams } from 'react-router'
7 | import { defaultTitle } from './constants'
8 | import {
9 | serviceCreateMemorandum,
10 | serviceGetMemorandumById,
11 | serviceUpdateMemorandum,
12 | } from '@/services'
13 |
14 | let editor: Editor
15 |
16 | const CreatePage: React.FC = () => {
17 | const navigate = useNavigate()
18 | const { id } = useParams()
19 | const [title, setTitle] = useState(defaultTitle)
20 | const [loading, setLoading] = useState(false)
21 |
22 | function goBack() {
23 | navigate('/home/memorandum', { replace: true })
24 | }
25 |
26 | function handleSubmit() {
27 | if (loading) return
28 |
29 | // 创建或更新
30 | const params = {
31 | markdown: editor.getMarkdown(),
32 | title,
33 | }
34 | if (!params.markdown) {
35 | message.warning('实体内容不能为空')
36 | return
37 | }
38 |
39 | setLoading(true)
40 | ;(id
41 | ? serviceUpdateMemorandum(id, params)
42 | : serviceCreateMemorandum(params)
43 | )
44 | .then(() => {
45 | navigate('/home/memorandum', { replace: true })
46 | })
47 | .catch(() => {
48 | setLoading(false)
49 | })
50 | }
51 |
52 | function init() {
53 | editor = new Editor({
54 | el: document.querySelector('#edit-section') as HTMLDivElement,
55 | initialEditType: 'markdown',
56 | previewStyle: 'vertical',
57 | usageStatistics: false,
58 | })
59 |
60 | if (id) {
61 | setLoading(true)
62 | serviceGetMemorandumById(id)
63 | .then((res) => {
64 | setTitle(res.title)
65 | editor.setMarkdown(res.markdown)
66 | })
67 | .finally(() => {
68 | setLoading(false)
69 | })
70 | }
71 | }
72 |
73 | useEffect(() => {
74 | init()
75 |
76 | return () => {
77 | // 销毁实例
78 | editor?.destroy()
79 | }
80 | }, [])
81 |
82 | return (
83 |
84 |
setTitle(e.target.value)}
91 | onBlur={() => !title && setTitle(defaultTitle)}
92 | />
93 |
94 |
95 |
96 |
99 |
100 |
101 | )
102 | }
103 |
104 | export default CreatePage
105 |
--------------------------------------------------------------------------------
/server/src/modules/company/company.service.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | NotFoundException,
4 | InternalServerErrorException,
5 | } from '@nestjs/common'
6 | import { InjectRepository } from '@nestjs/typeorm'
7 | import { Repository, In } from 'typeorm'
8 | import { CreateCompanyDto } from './dto/create-company.dto'
9 | import { UpdateCompanyDto } from './dto/update-company.dto'
10 | import { Company } from './entities/company.entity'
11 | import { GetCompanyDto } from './dto/get-company.dto'
12 | import { LogsService } from '../logs/logs.service'
13 |
14 | @Injectable()
15 | export class CompanyService {
16 | constructor(
17 | @InjectRepository(Company)
18 | private companyRepository: Repository,
19 | private readonly logsService: LogsService,
20 | ) {}
21 |
22 | async create(
23 | uid: number,
24 | createCompanyDto: CreateCompanyDto,
25 | ): Promise {
26 | const newCompany = this.companyRepository.create({
27 | ...createCompanyDto,
28 | uid,
29 | })
30 |
31 | return this.companyRepository.save(newCompany)
32 | }
33 |
34 | async findAll(
35 | uid: number,
36 | getCompanyDto: GetCompanyDto,
37 | ): Promise<{
38 | rows: Company[]
39 | count: number
40 | }> {
41 | const { pageNo, pageSize } = getCompanyDto
42 |
43 | const [rows, count] = await this.companyRepository.findAndCount({
44 | where: { uid },
45 | order: { startDate: 'DESC' },
46 | skip: pageNo && pageSize && pageNo * pageSize,
47 | take: pageSize,
48 | })
49 | return {
50 | rows,
51 | count,
52 | }
53 | }
54 |
55 | async findOne(id: string, uid: number): Promise {
56 | const company = await this.companyRepository.findOne({
57 | where: { id, uid },
58 | })
59 |
60 | if (!company) {
61 | throw new NotFoundException('公司不存在')
62 | }
63 |
64 | return company
65 | }
66 |
67 | async findByIds(ids: string[]): Promise {
68 | return this.companyRepository.findBy({
69 | id: In(ids),
70 | })
71 | }
72 |
73 | async update(
74 | uid: number,
75 | updateCompanyDto: UpdateCompanyDto,
76 | ): Promise {
77 | const { id, ...updateData } = updateCompanyDto
78 | await this.companyRepository.update({ id, uid }, updateData)
79 | return this.findOne(id, uid)
80 | }
81 |
82 | async remove(id: string, uid: number): Promise {
83 | const ids = id.split(',')
84 | const logs = await this.logsService.findBy(
85 | {
86 | companyId: In(ids) as unknown as string,
87 | },
88 | uid,
89 | )
90 | if (logs.length > 0) {
91 | throw new InternalServerErrorException(
92 | `公司下存在日志,无法删除 ${logs.map((log) => log.id).join(',')}`,
93 | )
94 | }
95 | const result = await this.companyRepository.delete({ id: In(ids), uid })
96 | if (result.affected === 0) {
97 | throw new NotFoundException()
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/server/src/modules/todo-lists/todo-lists.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common'
2 | import { InjectRepository } from '@nestjs/typeorm'
3 | import { Repository, Between, In } from 'typeorm'
4 | import { CreateTodoListDto } from './dto/create-todo-list.dto'
5 | import { UpdateTodoListDto } from './dto/update-todo-list.dto'
6 | import { TodoList } from './entities/todo-list.entity'
7 | import { GetTodoListDto } from './dto/get-todo-list.dto'
8 | import * as dayjs from 'dayjs'
9 | import { PAGE_SIZE } from '@/constants/pagination'
10 |
11 | @Injectable()
12 | export class TodoListsService {
13 | constructor(
14 | @InjectRepository(TodoList)
15 | private todoListsRepository: Repository,
16 | ) {}
17 |
18 | async create(
19 | uid: number,
20 | createTodoListDto: CreateTodoListDto,
21 | ): Promise {
22 | const newTodoList = this.todoListsRepository.create({
23 | ...createTodoListDto,
24 | uid,
25 | })
26 |
27 | return this.todoListsRepository.save(newTodoList)
28 | }
29 |
30 | async findAll(
31 | uid: number,
32 | getTodoListDto: GetTodoListDto,
33 | ): Promise<{ rows: TodoList[]; count: number }> {
34 | const {
35 | startDate: start,
36 | endDate: end,
37 | status,
38 | pageNo,
39 | pageSize = PAGE_SIZE,
40 | } = getTodoListDto
41 |
42 | const format = 'YYYY-MM-DD HH:mm:ss'
43 | const startDate = start
44 | ? dayjs(start).format(format)
45 | : dayjs().startOf('year').format(format)
46 | const endDate = end
47 | ? dayjs(end).endOf('day').format(format)
48 | : dayjs().endOf('year').format(format)
49 |
50 | const where = {
51 | uid,
52 | createdAt: Between(startDate, endDate) as unknown as Date,
53 | }
54 | if (status) {
55 | where['status'] = status
56 | }
57 |
58 | const [rows, count] = await this.todoListsRepository.findAndCount({
59 | where,
60 | order: { createdAt: 'DESC' },
61 | skip: pageNo ? pageNo * pageSize : undefined,
62 | take: pageNo ? pageSize : undefined,
63 | })
64 | return {
65 | rows,
66 | count,
67 | }
68 | }
69 |
70 | async findOne(id: string, uid: number): Promise {
71 | const todoList = await this.todoListsRepository.findOne({
72 | where: { id, uid },
73 | })
74 |
75 | if (!todoList) {
76 | throw new NotFoundException()
77 | }
78 |
79 | return todoList
80 | }
81 |
82 | async update(
83 | uid: number,
84 | updateTodoListDto: UpdateTodoListDto,
85 | ): Promise {
86 | const { id, ...updateData } = updateTodoListDto
87 | await this.todoListsRepository.update({ uid, id }, updateData)
88 | return this.findOne(id, uid)
89 | }
90 |
91 | async remove(id: string, uid: number): Promise {
92 | const ids = id.split(',')
93 | const result = await this.todoListsRepository.delete({ id: In(ids), uid })
94 | if (result.affected === 0) {
95 | throw new NotFoundException()
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/views/reminder/CreateReminder.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import dayjs from 'dayjs'
3 | import { Modal, Form, Input, DatePicker, message } from 'antd'
4 | import { serviceCreateReminder, serviceUpdateReminder } from '@/services'
5 | import { isBefore, formatDateTime } from '@/utils'
6 |
7 | const { TextArea } = Input
8 |
9 | type Props = {
10 | visible: boolean
11 | onCancel: () => void
12 | onSuccess: (res?: any) => void
13 | rowData: Record | null
14 | }
15 |
16 | const CreateReminder: React.FC = function ({
17 | visible,
18 | rowData,
19 | onCancel,
20 | onSuccess,
21 | }) {
22 | const [form] = Form.useForm()
23 | const [submitting, setSubmitting] = useState(false)
24 |
25 | async function handleSubmitForm() {
26 | try {
27 | const values = await form.validateFields()
28 | if (!values.date && !values.cron) {
29 | message.error('请选择日期或填写Cron表达式定时任务')
30 | return
31 | }
32 | const params = {
33 | date: values.date ? formatDateTime(values.date) : null,
34 | content: values.content.trim(),
35 | cron: values.cron,
36 | type: 1, // 未提醒
37 | }
38 |
39 | setSubmitting(true)
40 | ;(!rowData
41 | ? serviceCreateReminder(params)
42 | : serviceUpdateReminder(rowData.id, params)
43 | )
44 | .then((res) => {
45 | onSuccess(res)
46 | })
47 | .finally(() => {
48 | setSubmitting(false)
49 | })
50 | } catch (err) {
51 | console.log(err)
52 | }
53 | }
54 |
55 | useEffect(() => {
56 | if (visible && rowData) {
57 | form.setFieldsValue({
58 | date: rowData.date ? dayjs(rowData.date) : undefined,
59 | content: rowData.content,
60 | cron: rowData.cron,
61 | })
62 | }
63 | }, [visible, rowData])
64 |
65 | return (
66 |
74 |
76 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
98 |
99 |
100 |
101 |
102 | )
103 | }
104 |
105 | export default React.memo(CreateReminder)
106 |
--------------------------------------------------------------------------------
/server/src/modules/bill-types/bill-types.service.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | ConflictException,
4 | NotFoundException,
5 | InternalServerErrorException,
6 | } from '@nestjs/common'
7 | import { InjectRepository } from '@nestjs/typeorm'
8 | import { Repository, In } from 'typeorm'
9 | import { CreateBillTypeDto } from './dto/create-bill-type.dto'
10 | import { UpdateBillTypeDto } from './dto/update-bill-type.dto'
11 | import { BillType } from './entities/bill-type.entity'
12 |
13 | @Injectable()
14 | export class BillTypesService {
15 | constructor(
16 | @InjectRepository(BillType)
17 | private billTypeRepository: Repository,
18 | ) {}
19 |
20 | async create(
21 | uid: number,
22 | createBillTypeDto: CreateBillTypeDto,
23 | ): Promise {
24 | // 检查同用户下是否已存在相同名称的类型
25 | const existingBillType = await this.findOneByName(uid, createBillTypeDto)
26 | if (existingBillType) {
27 | throw new ConflictException('不可重复创建')
28 | }
29 |
30 | const billType = this.billTypeRepository.create({
31 | uid,
32 | ...createBillTypeDto,
33 | })
34 |
35 | return this.billTypeRepository.save(billType)
36 | }
37 |
38 | async findAll(uid: number): Promise {
39 | const billTypes = this.billTypeRepository.find({
40 | where: { uid },
41 | order: { type: 'DESC' },
42 | })
43 | return billTypes
44 | }
45 |
46 | async findOne(uid: number, id: string): Promise {
47 | const billType = await this.billTypeRepository.findOne({
48 | where: { id, uid },
49 | })
50 |
51 | if (!billType) {
52 | throw new NotFoundException('账单类型不存在')
53 | }
54 |
55 | return billType
56 | }
57 |
58 | async findOneByName(
59 | uid: number,
60 | updateBillTypeDto: Partial,
61 | ): Promise {
62 | return this.billTypeRepository.findOne({
63 | where: { ...updateBillTypeDto, uid },
64 | })
65 | }
66 |
67 | async update(
68 | uid: number,
69 | updateBillTypeDto: UpdateBillTypeDto,
70 | ): Promise {
71 | const { id, ...updateData } = updateBillTypeDto
72 | // 获取现有实体以确保它存在
73 | await this.findOne(uid, id)
74 |
75 | // 如果更新名称,检查是否与其他类型重复
76 | if (updateBillTypeDto.name) {
77 | const existingBillType = await this.findOneByName(uid, {
78 | name: updateBillTypeDto.name,
79 | type: updateBillTypeDto.type,
80 | })
81 | if (existingBillType && existingBillType.id !== id) {
82 | throw new ConflictException('类型名称已存在')
83 | }
84 | }
85 |
86 | await this.billTypeRepository.update({ id, uid }, updateData)
87 | }
88 |
89 | async remove(uid: number, ids: string[]): Promise {
90 | try {
91 | const result = await this.billTypeRepository.delete({
92 | id: In(ids),
93 | uid,
94 | })
95 |
96 | if (result.affected === 0) {
97 | throw new NotFoundException('账单类型不存在或已删除')
98 | }
99 | } catch {
100 | throw new InternalServerErrorException('请先删除账单关类型数据')
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------