├── 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 |
19 | 20 | 21 | 22 |
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 | Server 9 | React 10 | 11 |

12 |

13 | 14 | 本系统不提供测试账号密码,如果想预览请使用 `GitHub` 登录。 15 | 16 | ## Screenshot 17 | 18 | ![](media/screenshot.png) 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 | Server 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 | 29 | )} 30 | 31 | {onDelete && ( 32 | 39 | 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 |
56 | 67 |