├── src ├── services │ ├── README.md │ └── demo │ │ ├── index.ts │ │ ├── typings.d.ts │ │ └── UserController.ts ├── pages │ ├── 404.tsx │ ├── ProjectDetail │ │ ├── components │ │ │ ├── Content │ │ │ │ ├── Test │ │ │ │ │ ├── useService.ts │ │ │ │ │ └── components │ │ │ │ │ │ ├── Filters.tsx │ │ │ │ │ │ └── CaseEditor.tsx │ │ │ │ ├── Task │ │ │ │ │ ├── components │ │ │ │ │ │ ├── Table │ │ │ │ │ │ │ ├── table.less │ │ │ │ │ │ │ └── useService.ts │ │ │ │ │ │ ├── Gantt │ │ │ │ │ │ │ └── useService.ts │ │ │ │ │ │ └── Card │ │ │ │ │ │ │ ├── useService.ts │ │ │ │ │ │ │ └── components │ │ │ │ │ │ │ ├── TaskGroup.tsx │ │ │ │ │ │ │ └── CardItem.tsx │ │ │ │ │ ├── Filter │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── FilterPanel.tsx │ │ │ │ │ │ │ ├── NewTask.tsx │ │ │ │ │ │ │ ├── SearchInput.tsx │ │ │ │ │ │ │ ├── TaskGroup.tsx │ │ │ │ │ │ │ ├── CardType.tsx │ │ │ │ │ │ │ ├── TaskType.tsx │ │ │ │ │ │ │ ├── ViewType.tsx │ │ │ │ │ │ │ └── TaskBelong.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Statistics │ │ │ │ │ └── components │ │ │ │ │ │ ├── TaskGroup.tsx │ │ │ │ │ │ ├── UserTaskCount.tsx │ │ │ │ │ │ ├── UserTaskLaborHour.tsx │ │ │ │ │ │ ├── UserTaskStatusTrend.tsx │ │ │ │ │ │ ├── UserTaskTypeTrend.tsx │ │ │ │ │ │ ├── TaskTypePie.tsx │ │ │ │ │ │ ├── Header.tsx │ │ │ │ │ │ └── TaskStatusPie.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── Setting │ │ │ │ │ ├── components │ │ │ │ │ ├── UserList │ │ │ │ │ │ └── columns.tsx │ │ │ │ │ ├── TaskType │ │ │ │ │ │ ├── useService.ts │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── TaskStatus │ │ │ │ │ │ └── useService.ts │ │ │ │ │ └── index.tsx │ │ │ └── Header │ │ │ │ ├── ContentTypeTab.tsx │ │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── types.ts │ ├── Auth │ │ └── index.tsx │ ├── Settings │ │ ├── components │ │ │ ├── Content │ │ │ │ ├── System │ │ │ │ │ └── index.tsx │ │ │ │ ├── Tenant │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── User │ │ │ │ │ ├── columns.tsx │ │ │ │ │ └── useService.ts │ │ │ │ ├── TaskPriority │ │ │ │ │ └── useService.ts │ │ │ │ └── ProjectGroup │ │ │ │ │ └── useService.ts │ │ │ └── Menu │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── model.ts │ ├── components │ │ ├── EmptyData │ │ │ └── index.tsx │ │ ├── HeaderImgUpload │ │ │ └── upload.less │ │ ├── NewTaskModal │ │ │ └── comppnents │ │ │ │ ├── LaborHour.tsx │ │ │ │ ├── Title.tsx │ │ │ │ ├── TaskType.tsx │ │ │ │ ├── TaskStatus.tsx │ │ │ │ ├── TimeSelect.tsx │ │ │ │ ├── TaskPriority.tsx │ │ │ │ ├── UserSelect.tsx │ │ │ │ └── TaskGroup.tsx │ │ ├── TaskPrioritySelect │ │ │ ├── useService.ts │ │ │ └── index.tsx │ │ ├── TaskStatusSelect │ │ │ ├── useService.ts │ │ │ └── index.tsx │ │ ├── TaskTypeSelect │ │ │ └── userService.ts │ │ ├── TaskDetailModal │ │ │ └── components │ │ │ │ ├── TaskStatus.tsx │ │ │ │ ├── TaskPriority.tsx │ │ │ │ ├── Comment │ │ │ │ └── comment.less │ │ │ │ ├── TaskLogs │ │ │ │ └── components │ │ │ │ │ ├── CommentLog.tsx │ │ │ │ │ └── TextLog.tsx │ │ │ │ ├── TaskTimeLine.tsx │ │ │ │ ├── LaborHourCpt │ │ │ │ └── index.tsx │ │ │ │ ├── TaskGroup.tsx │ │ │ │ ├── UserSelect.tsx │ │ │ │ ├── TaskDescription.tsx │ │ │ │ ├── TaskTitle.tsx │ │ │ │ └── TaskActor.tsx │ │ ├── TaskUploadFile │ │ │ └── index.tsx │ │ ├── ProjectSelect │ │ │ └── useService.ts │ │ ├── TaskSelect │ │ │ └── useService.ts │ │ └── TaskGroupSelect │ │ │ └── useService.ts │ ├── Home │ │ ├── index.tsx │ │ └── components │ │ │ └── Introduce.tsx │ ├── Project │ │ ├── components │ │ │ ├── Container │ │ │ │ ├── index.tsx │ │ │ │ └── components │ │ │ │ │ ├── ProjectList.tsx │ │ │ │ │ ├── ProjectItem.tsx │ │ │ │ │ └── GroupSelect │ │ │ │ │ └── useService.ts │ │ │ ├── Menu │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ └── NewProjectModal │ │ │ │ └── useService.ts │ │ └── index.tsx │ ├── Message │ │ ├── index.tsx │ │ ├── components │ │ │ ├── MsgMenuItem.tsx │ │ │ └── MsgContent.tsx │ │ └── model.ts │ └── LaborHour │ │ ├── index.tsx │ │ └── components │ │ ├── LaborHourList.tsx │ │ ├── Header.tsx │ │ ├── DateHeader.tsx │ │ ├── LaborHourItem.tsx │ │ ├── ContentCard.tsx │ │ ├── TimeLine.tsx │ │ └── UserHeader.tsx ├── utils │ ├── event-bus.ts │ ├── format.ts │ └── url-utils.ts ├── assets │ └── images │ │ ├── logo.png │ │ ├── home │ │ ├── lie-wait.png │ │ └── tips-view.png │ │ └── default │ │ ├── list-empty.png │ │ ├── injoin-project.png │ │ └── project-avatar.jpeg ├── components │ ├── EmptyLoading.tsx │ ├── AuthAccess.tsx │ ├── MindMap │ │ └── index.tsx │ └── RickEditor.tsx ├── wrappers │ └── auth.tsx ├── access.ts ├── loading.tsx ├── layouts │ ├── LeftMenu │ │ ├── components │ │ │ ├── BottomMenu.tsx │ │ │ ├── Message │ │ │ │ ├── MsgItem.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── MsgContent.tsx │ │ │ ├── TopMenu.tsx │ │ │ └── UserInfo │ │ │ │ └── index.tsx │ │ └── index.tsx │ └── index.tsx ├── api │ └── modules │ │ ├── Test.ts │ │ ├── Auth.ts │ │ ├── Login.ts │ │ ├── Upload.ts │ │ ├── index.ts │ │ ├── TaskActor.ts │ │ ├── TaskAttachment.ts │ │ ├── TaskOperationLog.ts │ │ ├── TaskLaborHour.ts │ │ ├── TaskType.ts │ │ ├── TaskGroup.ts │ │ ├── TaskPriority.ts │ │ ├── ProjectGroup.ts │ │ └── ProjectUser.ts ├── hooks │ ├── useScrollMove.ts │ └── useQueryParams.ts ├── models │ ├── global.ts │ └── message.ts ├── app.ts └── global.less ├── .eslintignore ├── .stylelintignore ├── .npmrc ├── .prettierignore ├── tsconfig.json ├── typings.d.ts ├── .eslintrc.js ├── .stylelintrc.js ├── .husky ├── pre-commit └── commit-msg ├── .vscode ├── settings.json └── launch.json ├── .gitignore ├── .prettierrc ├── .lintstagedrc ├── mock └── userAPI.ts ├── openapi.config.ts ├── README.md ├── config ├── config.ts └── routes.ts ├── tailwind.config.js └── package.json /src/services/README.md: -------------------------------------------------------------------------------- 1 | # mock 样例 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/api/modules/*/** 2 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | tailwind.css 2 | src/.umi -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com/ 2 | 3 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | export default () =>
404
; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .umi 3 | .umi-production 4 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Test/useService.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./src/.umi/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/event-bus.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt'; 2 | export default mitt(); 3 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | import '@umijs/max/typings'; 2 | import 'react-beautiful-dnd'; 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: require.resolve('@umijs/max/eslint'), 3 | }; 4 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: require.resolve('@umijs/max/stylelint'), 3 | }; 4 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlink/wktline-client/HEAD/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/pages/Auth/index.tsx: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return
登录中...
; 3 | }; 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged --quiet 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install max verify-commit $1 5 | -------------------------------------------------------------------------------- /src/assets/images/home/lie-wait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlink/wktline-client/HEAD/src/assets/images/home/lie-wait.png -------------------------------------------------------------------------------- /src/assets/images/home/tips-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlink/wktline-client/HEAD/src/assets/images/home/tips-view.png -------------------------------------------------------------------------------- /src/components/EmptyLoading.tsx: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return
loading...
; 3 | }; 4 | -------------------------------------------------------------------------------- /src/assets/images/default/list-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlink/wktline-client/HEAD/src/assets/images/default/list-empty.png -------------------------------------------------------------------------------- /src/assets/images/default/injoin-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlink/wktline-client/HEAD/src/assets/images/default/injoin-project.png -------------------------------------------------------------------------------- /src/assets/images/default/project-avatar.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlink/wktline-client/HEAD/src/assets/images/default/project-avatar.jpeg -------------------------------------------------------------------------------- /src/pages/Settings/components/Content/System/index.tsx: -------------------------------------------------------------------------------- 1 | const SystemSettings = () => { 2 | return
System
; 3 | }; 4 | 5 | export default SystemSettings; 6 | -------------------------------------------------------------------------------- /src/pages/Settings/components/Content/Tenant/index.tsx: -------------------------------------------------------------------------------- 1 | const TenantSettings = () => { 2 | return
Tenant
; 3 | }; 4 | 5 | export default TenantSettings; 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.fontSize": 14, 3 | "debug.console.fontSize": 14, 4 | "terminal.integrated.fontSize": 14, 5 | "chat.editor.fontSize": 14 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/components/EmptyData/index.tsx: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return
———没有更多数据了———
; 3 | }; 4 | -------------------------------------------------------------------------------- /src/services/demo/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // 该文件由 OneAPI 自动生成,请勿手动修改! 3 | 4 | import * as UserController from './UserController'; 5 | export default { 6 | UserController, 7 | }; 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "chrome", 5 | "name": "http://localhost:8000 ", 6 | "request": "launch", 7 | "url": "http://localhost:8000 " 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/wrappers/auth.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet } from '@umijs/max'; 2 | 3 | export default () => { 4 | const isLogin = true; 5 | if (isLogin) { 6 | return ; 7 | } else { 8 | return ; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import Introduce from '@/pages/Home/components/Introduce'; 2 | 3 | const HomePage = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default HomePage; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.env.local 3 | /.umirc.local.ts 4 | /config/config.local.ts 5 | /src/.umi 6 | /src/.umi-production 7 | /src/.umi-test 8 | /.umi 9 | /.umi-production 10 | /.umi-test 11 | /dist 12 | /.mfsu 13 | .swc 14 | yarn-error.log 15 | .idea 16 | .history 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /src/pages/Settings/index.tsx: -------------------------------------------------------------------------------- 1 | import Content from './components/Content'; 2 | import Menu from './components/Menu'; 3 | const SettingsPage = () => { 4 | return ( 5 |
6 | 7 | 8 |
9 | ); 10 | }; 11 | 12 | export default SettingsPage; 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "proseWrap": "never", 6 | "overrides": [{ "files": ".prettierrc", "options": { "parser": "json" } }], 7 | "plugins": ["prettier-plugin-organize-imports", "prettier-plugin-packagejson", "prettier-plugin-tailwindcss"] 8 | } 9 | -------------------------------------------------------------------------------- /src/access.ts: -------------------------------------------------------------------------------- 1 | import { SYSTEM_ROLE } from '@/constants'; 2 | 3 | export default (initialState: API.UserBaseInfo) => { 4 | const isSuperAdmin = initialState?.role === SYSTEM_ROLE.SUPER_ADMIN; 5 | const isAdmin = initialState?.role !== SYSTEM_ROLE.USER 6 | return { 7 | isSuperAdmin, 8 | isAdmin, 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{md,json}": ["prettier --cache --write"], 3 | "*.{js,jsx}": ["max lint --fix --eslint-only", "prettier --cache --write"], 4 | "*.{css,less}": ["max lint --fix --stylelint-only", "prettier --cache --write"], 5 | "*.ts?(x)": ["max lint --fix --eslint-only", "prettier --cache --parser=typescript --write"] 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/Project/components/Container/index.tsx: -------------------------------------------------------------------------------- 1 | import Header from './components/Header'; 2 | import ProjectList from './components/ProjectList'; 3 | 4 | export default function ProjectContainer() { 5 | return ( 6 |
7 |
8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Task/components/Table/table.less: -------------------------------------------------------------------------------- 1 | .task-table-container { 2 | .ant-table-body { 3 | scrollbar-width: none; 4 | -ms-overflow-style: none; 5 | &::-webkit-scrollbar { 6 | display: none; 7 | } 8 | &::-webkit-scrollbar { 9 | display: none; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/components/HeaderImgUpload/upload.less: -------------------------------------------------------------------------------- 1 | .common-header-img-upload { 2 | width: auto !important; 3 | .ant-upload-select { 4 | width: auto !important; 5 | height: auto !important; 6 | border-radius: 8px !important; 7 | border: 0 !important; 8 | overflow: hidden; 9 | margin: 0 !important; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/Message/index.tsx: -------------------------------------------------------------------------------- 1 | import MsgContent from '@/pages/Message/components/MsgContent'; 2 | import MsgMenu from '@/pages/Message/components/MsgMenu'; 3 | 4 | const MessagePage = () => { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | }; 12 | 13 | export default MessagePage; 14 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Content from './components/Content'; 3 | import Header from './components/Header'; 4 | const App: React.FC = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 | ); 11 | }; 12 | 13 | export default App; 14 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Task/Filter/components/FilterPanel.tsx: -------------------------------------------------------------------------------- 1 | import { Filter } from '@icon-park/react'; 2 | import { Tooltip } from 'antd'; 3 | 4 | export default () => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /src/loading.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigProvider, Spin } from 'antd'; 2 | import zhCN from 'antd/es/locale/zh_CN'; 3 | 4 | export default () => ( 5 |
6 | 12 | 13 | 14 |
15 | ); 16 | -------------------------------------------------------------------------------- /src/pages/Project/index.tsx: -------------------------------------------------------------------------------- 1 | import { useModel } from '@umijs/max'; 2 | import { useEffect } from 'react'; 3 | import Container from './components/Container'; 4 | import Menu from './components/Menu'; 5 | 6 | export default () => { 7 | const { getProjectGroupList } = useModel('Project.model'); 8 | useEffect(getProjectGroupList, []); 9 | return ( 10 |
11 | 12 | 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/pages/components/NewTaskModal/comppnents/LaborHour.tsx: -------------------------------------------------------------------------------- 1 | import { useModel } from '@umijs/max'; 2 | import { Input } from 'antd'; 3 | 4 | export default () => { 5 | const { setParams } = useModel('newTask'); 6 | return ( 7 | { 12 | setParams({ planHour: +e.target.value }); 13 | }} 14 | /> 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /mock/userAPI.ts: -------------------------------------------------------------------------------- 1 | const users = [ 2 | { id: 0, name: 'Umi', nickName: 'U', gender: 'MALE' }, 3 | { id: 1, name: 'Fish', nickName: 'B', gender: 'FEMALE' }, 4 | ]; 5 | 6 | export default { 7 | 'GET /api/v1/queryUserList': (req: any, res: any) => { 8 | res.json({ 9 | success: true, 10 | data: { list: users }, 11 | errorCode: 0, 12 | }); 13 | }, 14 | 'PUT /api/v1/user/': (req: any, res: any) => { 15 | res.json({ 16 | success: true, 17 | errorCode: 0, 18 | }); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/layouts/LeftMenu/components/BottomMenu.tsx: -------------------------------------------------------------------------------- 1 | import UserInfo from '@/layouts/LeftMenu/components/UserInfo'; 2 | import Message from './Message'; 3 | 4 | export default () => { 5 | return ( 6 |
11 | 12 | 13 | 14 | 15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/pages/components/TaskPrioritySelect/useService.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useRequest } from '@umijs/max'; 3 | import { useState } from 'react'; 4 | 5 | export default () => { 6 | const { 7 | data, 8 | loading, 9 | run: getData, 10 | } = useRequest(() => Api.TaskPriority.getTaskPriorityList({}), { 11 | manual: true, 12 | }); 13 | const [open, setOpen] = useState(false); 14 | return { 15 | open, 16 | setOpen, 17 | loading, 18 | data, 19 | getData, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /openapi.config.ts: -------------------------------------------------------------------------------- 1 | const { generateService } = require('@umijs/openapi'); 2 | 3 | generateService({ 4 | projectName: 'modules', 5 | schemaPath: 'http://127.0.0.1:9002/api.json', 6 | serversPath: './src/api', 7 | apiPrefix: '', 8 | isCamelCase: false, 9 | requestImportStatement: 'import {request} from "@umijs/max";', 10 | dataFields: ['code', 'data', 'message', 'success'], 11 | hook: { 12 | /** 自定义函数名称 */ 13 | customFunctionName: (data: any) => { 14 | return data.path.split('/').pop(); 15 | }, 16 | }, 17 | }).then(); 18 | -------------------------------------------------------------------------------- /src/pages/components/TaskStatusSelect/useService.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useRequest } from '@umijs/max'; 3 | import { useState } from 'react'; 4 | 5 | export default (projectId: string) => { 6 | const { 7 | data, 8 | loading, 9 | run: getData, 10 | } = useRequest(() => Api.TaskStatus.getTaskStatusList({ projectId }), { 11 | manual: true, 12 | }); 13 | const [open, setOpen] = useState(false); 14 | return { 15 | open, 16 | setOpen, 17 | loading, 18 | data, 19 | getData, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/api/modules/Test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** 测试 GET /test */ 6 | export async function test( 7 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 8 | params: API.testParams, 9 | options?: { [key: string]: any }, 10 | ) { 11 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 12 | '/test', 13 | { 14 | method: 'GET', 15 | params: { 16 | ...params, 17 | }, 18 | ...(options || {}), 19 | }, 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/LaborHour/index.tsx: -------------------------------------------------------------------------------- 1 | import { useModel } from '@umijs/max'; 2 | import { useEffect } from 'react'; 3 | import Header from './components/Header'; 4 | import LaborHourList from './components/LaborHourList'; 5 | 6 | const LaborHour = () => { 7 | const { data, getLaborHourList } = useModel('LaborHour.model'); 8 | useEffect(getLaborHourList, [data.startTime]); 9 | return ( 10 |
11 |
12 | 13 |
14 | ); 15 | }; 16 | 17 | export default LaborHour; 18 | -------------------------------------------------------------------------------- /src/pages/components/TaskTypeSelect/userService.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useRequest } from '@umijs/max'; 3 | import { useState } from 'react'; 4 | 5 | export default (projectId: string) => { 6 | const { 7 | data, 8 | loading, 9 | run: getData, 10 | } = useRequest(() => Api.TaskType.getTaskTypeList({ projectId }), { 11 | manual: true, 12 | }); 13 | 14 | const [open, setOpen] = useState(false); 15 | 16 | return { 17 | open, 18 | setOpen, 19 | loading, 20 | data, 21 | getData, 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/AuthAccess.tsx: -------------------------------------------------------------------------------- 1 | import { useModel } from '@umijs/max'; 2 | 3 | type AuthType = 'admin' | 'superAdmin'; 4 | 5 | type AuthAccessProps = { 6 | authType: AuthType; 7 | children?: JSX.Element; 8 | }; 9 | 10 | const AuthAccess = (props: AuthAccessProps) => { 11 | const { initialState: useInfo } = useModel('@@initialState'); 12 | if (useInfo?.role === 'SUPER_ADMIN') { 13 | return props.children; 14 | } else if (props.authType === useInfo?.role) { 15 | return props.children; 16 | } 17 | return null; 18 | }; 19 | 20 | export default AuthAccess; 21 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Task/components/Gantt/useService.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useModel } from '@umijs/max'; 3 | 4 | const useService = () => { 5 | const { data, updateTaskDetail } = useModel('ProjectDetail.model'); 6 | 7 | const updateTaskInfo = (params: Partial) => { 8 | params.projectId = data.projectId; 9 | Api.Task.updateTask(params as API.UpdateTaskReq).then(() => { 10 | updateTaskDetail(params.id!); 11 | }); 12 | }; 13 | return { 14 | updateTaskInfo, 15 | }; 16 | }; 17 | export default useService; 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wktline 项目管理前端 「大家动动小手点个免费的 star✨」 2 | 3 | > 体验地址:http://121.40.42.56/ 用户名:wktline 密码:123456,希望各位老爷手下留情,不要乱删数据,方便大家体验哈~~❤️~~ 4 | 5 | > 😄 我的另一个产品「**轻简历**」,访问地址: https://www.wktline.com 好用的话大家多多推广~ 6 | 7 | 后端项目地址:[后端github](https://github.com/umlink/wktline-server) 8 | 9 | 界面内容查看和介绍:[项目详细介绍](https://juejin.cn/post/7410062139275984936) 10 | 11 | ### 注意事项 12 | nodejs 版本 v18.20.2 13 | 14 | ### 本地启动 15 | 16 | 1. 安装依赖 17 | ```base 18 | yarn 19 | ``` 20 | 2. 启动 21 | ```base 22 | yarn run dev 23 | ``` 24 | 3. 同步 api(后端 api 更新时执行) 25 | > 注意项目根目录下的 `openapi.config.ts` 配置 26 | ```base 27 | yarn run genapi 28 | ``` 29 | -------------------------------------------------------------------------------- /src/pages/Settings/components/Menu/index.less: -------------------------------------------------------------------------------- 1 | @menu-width: 220px; 2 | 3 | .settings-menu { 4 | position: relative; 5 | width: @menu-width; 6 | height: 100vh; 7 | box-sizing: border-box; 8 | user-select: none; 9 | box-shadow: inset -1px 0 0 #f0f0f0; 10 | 11 | .title-box { 12 | height: 50px; 13 | white-space: nowrap; 14 | padding: 0 12px; 15 | box-shadow: inset 0 -1px 0 #f0f0f0; 16 | } 17 | .menus { 18 | border: 0; 19 | padding: 8px; 20 | background: transparent; 21 | } 22 | :global { 23 | .ant-menu-vertical { 24 | border-inline-end: 0 !important; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/pages/components/NewTaskModal/comppnents/Title.tsx: -------------------------------------------------------------------------------- 1 | import { Close } from '@icon-park/react'; 2 | import { useModel } from '@umijs/max'; 3 | import { Button } from 'antd'; 4 | 5 | export default () => { 6 | const { setData } = useModel('newTask'); 7 | return ( 8 |
9 | 新建任务 10 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/pages/components/TaskDetailModal/components/TaskStatus.tsx: -------------------------------------------------------------------------------- 1 | import TaskStatusSelect from '@/pages/components/TaskStatusSelect'; 2 | import { useModel } from '@umijs/max'; 3 | 4 | export default () => { 5 | const { data, updateTaskInfo } = useModel('taskDetail'); 6 | return ( 7 | { 10 | updateTaskInfo({ statusId }); 11 | }} 12 | > 13 | 14 | {data.task?.statusName || {'设置任务状态'}} 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/hooks/useScrollMove.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 滑到底部 3 | * */ 4 | import { useThrottleFn } from 'ahooks'; 5 | 6 | const useScrollMove = (ref: any, callback: (params?: any) => any) => { 7 | const { run } = useThrottleFn( 8 | () => { 9 | try { 10 | const target = ref.current; 11 | const clientHeight = parseInt(target.clientHeight + target.scrollTop); 12 | const scrollHeight = parseInt(target.scrollHeight); 13 | if (scrollHeight - clientHeight < 2) callback(); 14 | } catch (e) { 15 | console.warn(e); 16 | } 17 | }, 18 | { 19 | wait: 100, 20 | }, 21 | ); 22 | return run; 23 | }; 24 | 25 | export default useScrollMove; 26 | -------------------------------------------------------------------------------- /src/pages/components/TaskDetailModal/components/TaskPriority.tsx: -------------------------------------------------------------------------------- 1 | import TaskPrioritySelect from '@/pages/components/TaskPrioritySelect'; 2 | import { useModel } from '@umijs/max'; 3 | 4 | export default () => { 5 | const { data, updateTaskInfo } = useModel('taskDetail'); 6 | return ( 7 | updateTaskInfo({ priority: priority.id })}> 8 | 9 | {!!data.task?.priority ? ( 10 | {data.task?.priority} 11 | ) : ( 12 | {'设置优先级'} 13 | )} 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Task/Filter/components/NewTask.tsx: -------------------------------------------------------------------------------- 1 | import { Plus } from '@icon-park/react'; 2 | import { useModel } from '@umijs/max'; 3 | export default () => { 4 | const { setData } = useModel('newTask'); 5 | const { data, updateDataForNewTask } = useModel('ProjectDetail.model'); 6 | return ( 7 | 10 | setData({ 11 | show: true, 12 | projectId: data.projectId, 13 | updateCallback: updateDataForNewTask, 14 | }) 15 | } 16 | > 17 | 18 | 新建任务 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/layouts/LeftMenu/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ZhaoFuLin 3 | * @Date: 2022-10-21 20:52:12 4 | * @LastEditTime: 2022-10-24 10:43:15 5 | */ 6 | import logo from '@/assets/images/logo.png'; 7 | import BottomMenu from '@/layouts/LeftMenu/components/BottomMenu'; 8 | import TopMenu from '@/layouts/LeftMenu/components/TopMenu'; 9 | import { Avatar } from 'antd'; 10 | 11 | export default () => { 12 | return ( 13 |
14 | 15 | 16 | 17 | 18 | 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/pages/components/NewTaskModal/comppnents/TaskType.tsx: -------------------------------------------------------------------------------- 1 | import TaskTypeSelect from '@/pages/components/TaskTypeSelect'; 2 | import { useModel } from '@umijs/max'; 3 | 4 | export default () => { 5 | const { data, selectData, setSelectData, setParams } = useModel('newTask'); 6 | return ( 7 | { 10 | setParams({ typeId: taskType.id }); 11 | setSelectData({ taskType }); 12 | }} 13 | > 14 | 15 | {selectData.taskType?.name || {'任务类型'}} 16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /config/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@umijs/max'; 2 | import routes from './routes'; 3 | 4 | export default defineConfig({ 5 | title: 'wktline-项目管理', 6 | favicons: ['https://static.web3ling.com/004b68991c74c3d0fd97f61bc4bd97b0.ico'], 7 | antd: {}, 8 | access: {}, 9 | model: {}, 10 | initialState: {}, 11 | request: {}, 12 | routes, 13 | npmClient: 'yarn', 14 | tailwindcss: {}, 15 | cssLoaderModules: { 16 | exportLocalsConvention: 'camelCase', 17 | }, 18 | cssMinifier: 'none', 19 | esbuildMinifyIIFE: true, 20 | proxy: { 21 | '/wkt-api': { 22 | target: 'http://127.0.0.1:9002', 23 | changeOrigin: true, 24 | pathRewrite: { '^/wkt-api': '' }, 25 | }, 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/pages/components/NewTaskModal/comppnents/TaskStatus.tsx: -------------------------------------------------------------------------------- 1 | import TaskStatusSelect from '@/pages/components/TaskStatusSelect'; 2 | import { useModel } from '@umijs/max'; 3 | 4 | export default () => { 5 | const { data, selectData, setSelectData, setParams } = useModel('newTask'); 6 | return ( 7 | { 10 | setParams({ statusId }); 11 | setSelectData({ taskStatus }); 12 | }} 13 | > 14 | 15 | {selectData.taskStatus?.name || {'任务状态'}} 16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/modules/Auth.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** 授权登录 POST /auth */ 6 | export async function auth(body: API.AuthReq, options?: { [key: string]: any }) { 7 | return request<{ 8 | code: number; 9 | message: string; 10 | data?: { 11 | id?: string; 12 | username?: string; 13 | nickname?: string; 14 | avatar?: string; 15 | role?: string; 16 | status?: number; 17 | token?: string; 18 | expire?: string; 19 | }; 20 | success: boolean; 21 | }>('/auth', { 22 | method: 'POST', 23 | headers: { 24 | 'Content-Type': 'application/json', 25 | }, 26 | data: body, 27 | ...(options || {}), 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/api/modules/Login.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** 登录 POST /login */ 6 | export async function login(body: API.LoginReq, options?: { [key: string]: any }) { 7 | return request<{ 8 | code: number; 9 | message: string; 10 | data?: { 11 | id?: number; 12 | username?: string; 13 | nickname?: string; 14 | avatar?: string; 15 | role?: string; 16 | status?: number; 17 | token?: string; 18 | expire?: string; 19 | }; 20 | success: boolean; 21 | }>('/login', { 22 | method: 'POST', 23 | headers: { 24 | 'Content-Type': 'application/json', 25 | }, 26 | data: body, 27 | ...(options || {}), 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Task/Filter/components/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import { Search } from '@icon-park/react'; 2 | import { useModel } from '@umijs/max'; 3 | import { useDebounceFn } from 'ahooks'; 4 | import { Input } from 'antd'; 5 | 6 | export default () => { 7 | const { setTaskParams } = useModel('ProjectDetail.model'); 8 | const { run: onKeywords } = useDebounceFn((e) => setTaskParams({ keywords: e.target.value }), { 9 | wait: 300, 10 | }); 11 | return ( 12 | 13 | } 17 | placeholder="根据关键词搜索任务" 18 | /> 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/hooks/useQueryParams.ts: -------------------------------------------------------------------------------- 1 | import { replaceUrlByQuery } from '@/utils/url-utils'; 2 | import { useSetState, useUpdateEffect } from 'ahooks'; 3 | import queryString from 'query-string'; 4 | 5 | type IQueryEnum = { 6 | [key: string]: any; 7 | }; 8 | 9 | const useQueryParams = () => { 10 | const q: IQueryEnum = queryString.parseUrl(location.href).query; 11 | const [query, setQuery] = useSetState(q); 12 | // query 参数一直追加 13 | const addQueryString = (obj: IQueryEnum) => replaceUrlByQuery(obj); 14 | // 只保留当前的 query 15 | const replaceQuery = (obj: IQueryEnum) => replaceUrlByQuery(obj, true); 16 | 17 | useUpdateEffect(() => { 18 | addQueryString(query); 19 | }, [query]); 20 | 21 | return [query, setQuery, replaceQuery]; 22 | }; 23 | 24 | export default useQueryParams; 25 | -------------------------------------------------------------------------------- /src/pages/Settings/model.ts: -------------------------------------------------------------------------------- 1 | import useQueryParams from '@/hooks/useQueryParams'; 2 | import { useSetState, useUpdateEffect } from 'ahooks'; 3 | 4 | export enum SettingsType { 5 | USER = 'USER', 6 | SYSTEM = 'SYSTEM', 7 | TASK_PRIORITY = 'TASK_PRIORITY', 8 | PROJECT_GROUP = 'PROJECT_GROUP', 9 | TENANT = 'TENANT', 10 | } 11 | 12 | type SettingsDataType = { 13 | activeKey: SettingsType; 14 | }; 15 | 16 | const SettingsConfig = () => { 17 | const [query, setQuery] = useQueryParams(); 18 | const [state, setState] = useSetState({ 19 | activeKey: query.type || 'USER', 20 | }); 21 | 22 | useUpdateEffect(() => { 23 | setQuery({ type: state.activeKey }); 24 | }, [state.activeKey]); 25 | 26 | return { 27 | state, 28 | setState, 29 | }; 30 | }; 31 | 32 | export default SettingsConfig; 33 | -------------------------------------------------------------------------------- /src/pages/components/NewTaskModal/comppnents/TimeSelect.tsx: -------------------------------------------------------------------------------- 1 | import { useModel } from '@umijs/max'; 2 | import { DatePicker } from 'antd'; 3 | import dayjs from 'dayjs'; 4 | export default () => { 5 | const { params, setParams } = useModel('newTask'); 6 | return ( 7 | 8 | { 13 | setParams({ 14 | startTime: date?.toString(), 15 | }); 16 | }} 17 | /> 18 | { 22 | setParams({ 23 | endTime: date?.toString(), 24 | }); 25 | }} 26 | /> 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/pages/components/TaskDetailModal/components/Comment/comment.less: -------------------------------------------------------------------------------- 1 | .tribute-container { 2 | position: relative; 3 | height: auto; 4 | overflow: auto; 5 | display: block; 6 | z-index: 9999; 7 | min-height: 50px; 8 | } 9 | .tribute-container ul { 10 | margin: 0; 11 | list-style: none; 12 | background: #fff; 13 | border-radius: 4px; 14 | width: 120px; 15 | padding: 0 8px; 16 | border: 1px solid #f5f5f5; 17 | font-size: 13px; 18 | } 19 | .tribute-container li { 20 | padding: 5px 5px; 21 | cursor: pointer; 22 | border-radius: 4px; 23 | } 24 | .tribute-container li.highlight { 25 | background: #eee; 26 | } 27 | .tribute-container li span { 28 | font-weight: bold; 29 | } 30 | .tribute-container li.no-match { 31 | cursor: default; 32 | } 33 | .tribute-container .menu-highlighted { 34 | font-weight: bold; 35 | } 36 | .tribute-mention { 37 | background-color: #fff; 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Test/components/Filters.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Form, Input } from 'antd'; 2 | 3 | const TestCaseFilter = () => { 4 | const [form] = Form.useForm(); 5 | const onFinish = (values: any) => { 6 | console.log('Finish:', values); 7 | }; 8 | 9 | return ( 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default TestCaseFilter; 29 | -------------------------------------------------------------------------------- /src/pages/Settings/components/Content/index.tsx: -------------------------------------------------------------------------------- 1 | import { useModel } from '@umijs/max'; 2 | import { SettingsType } from '../../model'; 3 | import ProjectGroup from './ProjectGroup'; 4 | import System from './System'; 5 | import TaskPriority from './TaskPriority'; 6 | import Tenant from './Tenant'; 7 | import User from './User'; 8 | const SettingsContainer = () => { 9 | const { state } = useModel('Settings.model'); 10 | 11 | const containerViewMap = { 12 | [SettingsType.USER]: User, 13 | [SettingsType.SYSTEM]: System, 14 | [SettingsType.TASK_PRIORITY]: TaskPriority, 15 | [SettingsType.PROJECT_GROUP]: ProjectGroup, 16 | [SettingsType.TENANT]: Tenant, 17 | }; 18 | 19 | const SettingsView = containerViewMap[state.activeKey]; 20 | 21 | return ( 22 |
23 | 24 |
25 | ); 26 | }; 27 | export default SettingsContainer; 28 | -------------------------------------------------------------------------------- /src/pages/components/NewTaskModal/comppnents/TaskPriority.tsx: -------------------------------------------------------------------------------- 1 | import TaskPrioritySelect from '@/pages/components/TaskPrioritySelect'; 2 | import { useModel } from '@umijs/max'; 3 | import { Space } from 'antd'; 4 | 5 | export default () => { 6 | const { selectData, setSelectData, setParams } = useModel('newTask'); 7 | return ( 8 | { 10 | setSelectData({ taskPriority }); 11 | setParams({ priorityId: taskPriority.id }); 12 | }} 13 | > 14 | 15 | {!!selectData.taskPriority ? ( 16 | 17 | {selectData.taskPriority.value}({selectData.taskPriority.name}) 18 | 19 | ) : ( 20 | 任务优先级 21 | )} 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/pages/components/TaskDetailModal/components/TaskLogs/components/CommentLog.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar } from 'antd'; 2 | import dayjs from 'dayjs'; 3 | 4 | const CommentLog = (props: { comment: API.TaskOperationLogItem }) => { 5 | const { avatar, content, username, createdAt } = props.comment; 6 | return ( 7 |
8 | 9 |
10 |
11 | {username} 12 | {dayjs(createdAt).format('YYYY-MM-DD HH:mm')} 13 |
14 |
18 |
19 |
20 | ); 21 | }; 22 | 23 | export default CommentLog; 24 | -------------------------------------------------------------------------------- /src/pages/Project/components/Menu/index.less: -------------------------------------------------------------------------------- 1 | .project-menu-container { 2 | background-color: #fff; 3 | } 4 | 5 | @menu-width: 220px; 6 | 7 | .project-menu { 8 | position: relative; 9 | width: @menu-width; 10 | height: 100vh; 11 | box-sizing: border-box; 12 | transition: all 0.3s; 13 | user-select: none; 14 | box-shadow: inset -1px 0 0 #f0f0f0; 15 | 16 | .title-box { 17 | height: 50px; 18 | white-space: nowrap; 19 | padding: 0 12px; 20 | box-shadow: inset 0 -1px 0 #f0f0f0; 21 | } 22 | 23 | .title { 24 | margin: 0; 25 | } 26 | 27 | .project-category { 28 | padding: 4px; 29 | } 30 | 31 | .menus { 32 | border: 0; 33 | padding: 8px; 34 | background: transparent; 35 | } 36 | :global { 37 | .ant-menu-vertical { 38 | border-inline-end: 0 !important; 39 | } 40 | } 41 | } 42 | 43 | .hide { 44 | left: -@menu-width; 45 | width: 0; 46 | padding: 0; 47 | overflow: hidden; 48 | } 49 | -------------------------------------------------------------------------------- /src/pages/Message/components/MsgMenuItem.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar } from 'antd'; 2 | 3 | type PropsType = { 4 | item: API.MessageItem; 5 | openId?: string; 6 | }; 7 | 8 | const MsgMenuItem = ({ item, openId }: PropsType) => { 9 | const isActive = openId === item.id; 10 | return ( 11 |
14 | 15 |
16 |

{item.title}

17 |
18 | {item.senderName} 19 | {item.createdAt} 20 |
21 |

22 |
23 |
24 | ); 25 | }; 26 | 27 | export default MsgMenuItem; 28 | -------------------------------------------------------------------------------- /src/pages/Project/components/Container/components/ProjectList.tsx: -------------------------------------------------------------------------------- 1 | import useScrollMove from '@/hooks/useScrollMove'; 2 | import { useModel } from '@umijs/max'; 3 | import { useRef } from 'react'; 4 | import ProjectItem from './ProjectItem'; 5 | 6 | export default () => { 7 | const ref = useRef(null); 8 | const { projectData, params, resetMenuParams } = useModel('Project.model'); 9 | const onScroll = useScrollMove(ref, () => { 10 | if (projectData.finished) return; 11 | resetMenuParams({ pageNo: params.pageNo! + 1 }); 12 | }); 13 | 14 | return ( 15 |
16 |
17 |
18 | {projectData.projectList.map((item) => ( 19 | 20 | ))} 21 |
22 |
23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/pages/LaborHour/components/LaborHourList.tsx: -------------------------------------------------------------------------------- 1 | import useScrollMove from '@/hooks/useScrollMove'; 2 | import NewLaborHour from '@/pages/LaborHour/components/NewLaborHour'; 3 | import { useModel } from '@umijs/max'; 4 | import { useRef } from 'react'; 5 | import DateHeader from './DateHeader'; 6 | import LaborHourItem from './LaborHourItem'; 7 | 8 | const LaborHourList = () => { 9 | const { data, loadMore } = useModel('LaborHour.model'); 10 | const ref = useRef(null); 11 | const onScroll = useScrollMove(ref, loadMore); 12 | return ( 13 |
14 | 15 |
16 | {data.userLaborList.map((item, index) => ( 17 | 18 | ))} 19 |
20 | 21 |
22 | ); 23 | }; 24 | export default LaborHourList; 25 | -------------------------------------------------------------------------------- /src/components/MindMap/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import MindMap from 'simple-mind-map'; 3 | 4 | type PropsType = { 5 | readonly?: boolean; 6 | value: string; 7 | onChange: (v: string) => void; 8 | }; 9 | export default (props: PropsType) => { 10 | const ref = useRef(null); 11 | let mindMapInstance; 12 | useEffect(() => { 13 | const data = props.value 14 | ? JSON.parse(props.value) 15 | : { 16 | data: { 17 | text: '根节点', 18 | }, 19 | children: [], 20 | }; 21 | const defaultParams: any = { 22 | el: ref.current, 23 | readonly: props.readonly, 24 | 25 | data, 26 | }; 27 | mindMapInstance = new MindMap(defaultParams); 28 | mindMapInstance.on('data_change', (data: any) => { 29 | props.onChange(JSON.stringify(data)); 30 | }); 31 | }, []); 32 | return ( 33 |
34 |
35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/models/global.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useSetState } from 'ahooks'; 3 | import { useEffect } from 'react'; 4 | 5 | interface GlobalState { 6 | showProjectMenu: boolean; 7 | showTaskDetail: boolean; 8 | unreadCount: number; 9 | } 10 | 11 | const GlobalData = () => { 12 | const [globalData, setGlobalData] = useSetState({ 13 | showProjectMenu: true, 14 | showTaskDetail: false, 15 | unreadCount: 0, 16 | }); 17 | 18 | const getUnreadMsgCount = () => { 19 | Api.Message.getMsgUnreadCount({}).then((res) => { 20 | if (res.success) { 21 | setGlobalData({ unreadCount: res.data?.count ?? 0 }); 22 | } 23 | }); 24 | }; 25 | 26 | const onToggleMenu = () => { 27 | setGlobalData({ 28 | showProjectMenu: !globalData.showProjectMenu, 29 | }); 30 | }; 31 | 32 | useEffect(getUnreadMsgCount, []); 33 | 34 | return { 35 | globalData, 36 | setGlobalData, 37 | onToggleMenu, 38 | getUnreadMsgCount, 39 | }; 40 | }; 41 | 42 | export default GlobalData; 43 | -------------------------------------------------------------------------------- /src/utils/format.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | /** 3 | * @description: 4 | * @param {string} time 5 | * @return {*} 6 | */ 7 | export function laborHourFormat(time: string): number { 8 | if (!time) return 0; 9 | const _time = time.toLowerCase(); 10 | if (!/^\d+$/.test(_time)) { 11 | let _hour = 0; 12 | if (/\d+w$/.test(_time)) { 13 | _hour = Number(_time.split('w')[0]) * 5 * 8; 14 | } else if (/\d+d$/.test(_time)) { 15 | _hour = Number(_time.split('d')[0]) * 8; 16 | } else if (/\d+h$/.test(_time)) { 17 | _hour = Number(_time.split('h')[0]); 18 | } 19 | return _hour; 20 | } 21 | return Number(_time); 22 | } 23 | 24 | export const getCurrentDateStartAndEnd = (format = 'YYYY-MM-DD HH:mm:ss') => { 25 | let day = dayjs().day(); 26 | let startDay = day === 0 ? -6 : -day + 1; 27 | const startDate = dayjs().add(startDay, 'd'); 28 | return { 29 | currentMonday: startDate.hour(0).minute(0).second(0).format(format), 30 | currentSunday: startDate.add(6, 'd').hour(23).minute(59).second(59).format(format), 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Task/index.tsx: -------------------------------------------------------------------------------- 1 | import EmptyLoading from '@/components/EmptyLoading'; 2 | import { ViewTypeFilterKey } from '@/constants'; 3 | import { useModel } from '@umijs/max'; 4 | import React, { Suspense } from 'react'; 5 | import Filter from './Filter'; 6 | 7 | /** 8 | * 视图类型 9 | * */ 10 | const viewMap: { [key in ViewTypeFilterKey]: any } = { 11 | CARD: React.lazy(() => import('./components/Card')), 12 | TABLE: React.lazy(() => import('./components/Table')), 13 | GANTT: React.lazy(() => import('./components/Gantt')), 14 | }; 15 | 16 | const ProjectDetailContent: React.FC = () => { 17 | const { filterData } = useModel('ProjectDetail.model'); 18 | 19 | const ContainerView = viewMap[filterData.viewType]; 20 | 21 | return ( 22 |
23 | 24 |
25 | }> 26 | 27 | 28 |
29 |
30 | ); 31 | }; 32 | 33 | export default ProjectDetailContent; 34 | -------------------------------------------------------------------------------- /src/pages/LaborHour/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Left, Right } from '@icon-park/react'; 2 | import { useModel } from '@umijs/max'; 3 | import dayjs from 'dayjs'; 4 | const Header = () => { 5 | const { data, changeTime } = useModel('LaborHour.model'); 6 | const formatTime = (time: string) => dayjs(time).format('YYYY-MM-DD'); 7 | 8 | return ( 9 |
10 | 工时展示 11 | 12 | changeTime(-1)} 15 | theme="outline" 16 | size="22" 17 | /> 18 | 19 | {formatTime(data.startTime)} - {formatTime(data.endTime)} 20 | 21 | changeTime(1)} 24 | theme="outline" 25 | size="22" 26 | /> 27 | 28 | Bow 29 |
30 | ); 31 | }; 32 | 33 | export default Header; 34 | -------------------------------------------------------------------------------- /src/api/modules/Upload.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** 上传文件(任意文件类型) POST /common/upload */ 6 | export async function upload(body: API.UploadFileReq, options?: { [key: string]: any }) { 7 | const formData = new FormData(); 8 | 9 | Object.keys(body).forEach((ele) => { 10 | const item = (body as any)[ele]; 11 | 12 | if (item !== undefined && item !== null) { 13 | if (typeof item === 'object' && !(item instanceof File)) { 14 | if (item instanceof Array) { 15 | item.forEach((f) => formData.append(ele, f || '')); 16 | } else { 17 | formData.append(ele, JSON.stringify(item)); 18 | } 19 | } else { 20 | formData.append(ele, item); 21 | } 22 | } 23 | }); 24 | 25 | return request<{ 26 | code: number; 27 | message: string; 28 | data?: { id?: string; name?: string; type?: string; url?: string }; 29 | success: boolean; 30 | }>('/common/upload', { 31 | method: 'POST', 32 | data: formData, 33 | requestType: 'form', 34 | ...(options || {}), 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/Project/components/NewProjectModal/useService.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useRequest } from '@umijs/max'; 3 | import { App } from 'antd'; 4 | import { useEffect, useState } from 'react'; 5 | 6 | const useService = (onSuccess: (res: any) => void) => { 7 | const { message } = App.useApp(); 8 | const [projectGroupList, setProjectGroupList] = useState([]); 9 | 10 | useEffect(() => { 11 | Api.ProjectGroup.getProjectGroupList({}).then((res) => { 12 | if (res.success) { 13 | setProjectGroupList(res.data!.list!); 14 | } 15 | }); 16 | }, []); 17 | 18 | const { loading, run: createProject } = useRequest( 19 | (params: API.CreateProjectReq) => 20 | Api.Project.createProject(params).then((res) => { 21 | if (res.success) { 22 | onSuccess(res.data); 23 | } else { 24 | message.error(res.message); 25 | } 26 | }), 27 | { 28 | manual: true, 29 | }, 30 | ); 31 | 32 | return { 33 | projectGroupList, 34 | loading, 35 | createProject, 36 | }; 37 | }; 38 | 39 | export default useService; 40 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Task/Filter/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import CardType from './components/CardType'; 3 | import NewTask from './components/NewTask'; 4 | import SearchInput from './components/SearchInput'; 5 | import SortType from './components/SortType'; 6 | import TaskBelong from './components/TaskBelong'; 7 | import TaskGroup from './components/TaskGroup'; 8 | import TaskType from './components/TaskType'; 9 | import ViewType from './components/ViewType'; 10 | 11 | const ProjectDetailFilter: React.FC = memo(() => { 12 | return ( 13 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 | ); 32 | }); 33 | 34 | export default ProjectDetailFilter; 35 | -------------------------------------------------------------------------------- /src/pages/components/TaskDetailModal/components/TaskTimeLine.tsx: -------------------------------------------------------------------------------- 1 | import { useModel } from '@umijs/max'; 2 | import { DatePicker } from 'antd'; 3 | import dayjs, { Dayjs } from 'dayjs'; 4 | 5 | const TaskTimeLine = () => { 6 | const dateFormat = 'YYYY-MM-DD'; 7 | const { data, updateTaskInfo } = useModel('taskDetail'); 8 | const startTime = !!data.task?.startTime ? dayjs(data.task?.startTime, dateFormat) : undefined; 9 | const endTime = !!data.task?.endTime ? dayjs(data.task?.endTime, dateFormat) : undefined; 10 | return ( 11 | 12 | { 16 | updateTaskInfo({ 17 | startTime: date?.toString(), 18 | }); 19 | }} 20 | /> 21 | { 25 | updateTaskInfo({ 26 | endTime: date?.toString(), 27 | }); 28 | }} 29 | /> 30 | 31 | ); 32 | }; 33 | export default TaskTimeLine; 34 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Statistics/components/TaskGroup.tsx: -------------------------------------------------------------------------------- 1 | import TaskGroupSelect from '@/pages/components/TaskGroupSelect'; 2 | import { Down } from '@icon-park/react'; 3 | import { useModel } from '@umijs/max'; 4 | 5 | type PropsType = { 6 | group: API.TaskGroupItem; 7 | setGroup: (v: API.TaskGroupItem) => void; 8 | }; 9 | 10 | export default ({ group, setGroup }: PropsType) => { 11 | const { projectData } = useModel('ProjectDetail.model', (m) => { 12 | return { 13 | projectData: m.data, 14 | }; 15 | }); 16 | return ( 17 | setGroup(item)} 21 | onSelectAll={() => { 22 | setGroup({ 23 | id: '', 24 | groupName: '全部迭代', 25 | description: '', 26 | }); 27 | }} 28 | > 29 |
30 | 迭代: 31 | {group.groupName} 32 | 33 |
34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Task/Filter/components/TaskGroup.tsx: -------------------------------------------------------------------------------- 1 | import TaskGroupSelect from '@/pages/components/TaskGroupSelect'; 2 | import { CategoryManagement } from '@icon-park/react'; 3 | import { useModel } from '@umijs/max'; 4 | import { Tooltip } from 'antd'; 5 | 6 | export default () => { 7 | const { data, filterData, setFilterData, setTaskParams } = useModel('ProjectDetail.model'); 8 | return ( 9 | { 12 | setTaskParams({ groupId: item.id }); 13 | setFilterData({ group: { id: item.id, name: item.groupName } }); 14 | }} 15 | onSelectAll={() => { 16 | setTaskParams({ groupId: undefined }); 17 | setFilterData({ 18 | group: { 19 | id: '', 20 | name: '全部迭代', 21 | }, 22 | }); 23 | }} 24 | > 25 | 26 | 27 | 28 | {filterData.group.name} 29 | 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { RequestConfig } from '@@/plugin-request/request'; 3 | import { AxiosRequestConfig, AxiosResponse } from 'axios'; 4 | 5 | const toLogin = () => (location.href = `/login?target=${encodeURIComponent(location.href)}`); 6 | 7 | export async function getInitialState(): Promise | undefined> { 8 | const isLoginPage = location.pathname.includes('/login'); 9 | const res = await Api.User.getUseInfo().catch(() => { 10 | if (isLoginPage) return; 11 | toLogin(); 12 | }); 13 | if (!res?.data && !isLoginPage) { 14 | toLogin(); 15 | } 16 | if (res?.data && isLoginPage) { 17 | location.href = '/'; 18 | } 19 | return res?.data; 20 | } 21 | export const request: RequestConfig = { 22 | timeout: 20000, 23 | baseURL: '/wkt-api', 24 | headers: { 'X-Requested-With': 'XMLHttpRequest' }, 25 | errorConfig: { 26 | errorHandler() { 27 | console.log('errorHandler'); 28 | }, 29 | errorThrower() {}, 30 | }, 31 | requestInterceptors: [ 32 | (config: AxiosRequestConfig) => { 33 | return config; 34 | }, 35 | ], 36 | responseInterceptors: [ 37 | (response: AxiosResponse) => { 38 | return response; 39 | }, 40 | ], 41 | }; 42 | -------------------------------------------------------------------------------- /src/global.less: -------------------------------------------------------------------------------- 1 | body { 2 | //--primary-color: 59, 130, 246; 3 | //--primary-color: 22, 93, 255; 4 | //--primary-color: 0, 185, 107; 5 | //--primary-color: 146, 84, 222; 6 | --primary-color: 20, 201, 201; 7 | } 8 | 9 | .ant-modal-header { 10 | margin-bottom: 0 !important; 11 | } 12 | 13 | .task-detail-modal { 14 | .ant-modal-content { 15 | padding-bottom: 0; 16 | border-radius: 4px; 17 | overflow: hidden; 18 | } 19 | } 20 | 21 | /* 隐藏默认滚动条 */ 22 | .g-custom-y-scroll { 23 | overflow-y: auto; 24 | overflow-x: hidden; 25 | scrollbar-width: thin; 26 | scrollbar-color: transparent transparent; 27 | 28 | &:hover { 29 | scrollbar-color: #999 transparent; 30 | 31 | &::-webkit-scrollbar-track { 32 | background-color: #f0f0f0; 33 | /* 你可以设置轨道的颜色 */ 34 | } 35 | 36 | &::-webkit-scrollbar-thumb { 37 | background-color: #999; 38 | /* 你可以设置滑块的颜色 */ 39 | } 40 | } 41 | 42 | &::-webkit-scrollbar-track { 43 | background-color: transparent; 44 | } 45 | 46 | &::-webkit-scrollbar-thumb { 47 | background-color: transparent; 48 | } 49 | } 50 | .hidden-scrollbar { 51 | /* IE and Edge */ 52 | -ms-overflow-style: none; 53 | /* Firefox */ 54 | scrollbar-width: none; 55 | 56 | /* Safari and Chrome */ 57 | &::-webkit-scrollbar { 58 | display: none 59 | } 60 | } -------------------------------------------------------------------------------- /src/layouts/LeftMenu/components/Message/MsgItem.tsx: -------------------------------------------------------------------------------- 1 | import { Info } from '@icon-park/react'; 2 | import { Avatar } from 'antd'; 3 | 4 | type PropsType = { 5 | item: API.MessageItem; 6 | openId?: string; 7 | }; 8 | 9 | const MsgItem = ({ item, openId }: PropsType) => { 10 | const isActive = openId === item.id; 11 | return ( 12 |
15 | 16 |
17 |

18 | {item.msgType === 'TASK' ? item.taskName : item.content} 19 |

20 |

{item.title}

21 |
22 | 23 | 24 | {item.senderName} 25 | 26 | {item.createdAt} 27 |
28 |
29 |
30 | ); 31 | }; 32 | 33 | export default MsgItem; 34 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/index.tsx: -------------------------------------------------------------------------------- 1 | import { EVENTS, projectContentTypeMap } from '@/constants'; 2 | import useQueryParams from '@/hooks/useQueryParams'; 3 | import EventBus from '@/utils/event-bus'; 4 | import { useModel } from '@umijs/max'; 5 | import { Empty } from 'antd'; 6 | import { useEffect } from 'react'; 7 | import Setting from './Setting'; 8 | import Statistics from './Statistics'; 9 | import Task from './Task'; 10 | import Test from './Test'; 11 | 12 | const ProjectDetailContent = () => { 13 | const { filterData, setData, resetProjectData } = useModel('ProjectDetail.model'); 14 | const { TASK, STATISTIC, SETTING, TEST } = projectContentTypeMap; 15 | const [query] = useQueryParams(); 16 | 17 | // 第一次进来获取项目详情 关闭后清空项目数据 18 | useEffect(() => { 19 | setData({ projectId: query.id }); 20 | if (query.taskId) { 21 | EventBus.emit(EVENTS.OPEN_TASK_DETAIL, query.taskId); 22 | } 23 | return () => { 24 | resetProjectData(); 25 | }; 26 | }, []); 27 | 28 | const ViewMap = { 29 | [TASK]: Task, 30 | [STATISTIC]: Statistics, 31 | [SETTING]: Setting, 32 | [TEST]: Test, 33 | }; 34 | const SettingView = ViewMap[filterData.contentType] || Empty; 35 | return
{}
; 36 | }; 37 | 38 | export default ProjectDetailContent; 39 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Statistics/components/UserTaskCount.tsx: -------------------------------------------------------------------------------- 1 | import { Column } from '@ant-design/plots'; 2 | import {ProjectStatisticsParams} from "@/pages/ProjectDetail/components/Content/Statistics"; 3 | import Api from "@/api/modules"; 4 | import {useEffect, useState} from "react"; 5 | type PropsType = { 6 | params: ProjectStatisticsParams; 7 | }; 8 | const UserTask = ({params}: PropsType) => { 9 | const [data, setData] = useState([]) 10 | const config: any = { 11 | data, 12 | height: 320, 13 | xField: 'userName', 14 | yField: '任务数', 15 | sort: { 16 | reverse: true, 17 | by: 'y', 18 | }, 19 | style: { 20 | maxWidth: 50, 21 | radiusTopLeft: 4, 22 | radiusTopRight: 4, 23 | }, 24 | label: { 25 | text: (d: API.ProjectUserTaskCountItem) => d['任务数'], 26 | textBaseline: 'bottom', 27 | }, 28 | legend: false, 29 | }; 30 | const getData = () => { 31 | Api.ProjectOverview.getUserTaskCountStat(params).then((res) => { 32 | if (res.success && res.data) { 33 | setData(res.data || []); 34 | } 35 | }); 36 | }; 37 | useEffect(getData, [params]); 38 | return ( 39 |
40 |

任务数量分布

41 | 42 |
43 | ); 44 | }; 45 | 46 | export default UserTask; 47 | -------------------------------------------------------------------------------- /config/routes.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | name: 'login', 4 | path: '/login', 5 | component: '@/pages/Login', 6 | }, 7 | { 8 | name: 'auth', 9 | path: '/auth', 10 | component: '@/pages/Auth', 11 | }, 12 | { 13 | name: '404', 14 | path: '*', 15 | component: '@/pages/404', 16 | }, 17 | { 18 | path: '/', 19 | wrappers: ['@/wrappers/auth'], 20 | routes: [ 21 | { 22 | name: '首页', 23 | path: '', 24 | component: '@/pages/Home', 25 | }, 26 | { 27 | name: '项目', 28 | path: 'project', 29 | component: '@/pages/Project', 30 | }, 31 | { 32 | name: '项目详情', 33 | path: 'project/detail', 34 | component: '@/pages/ProjectDetail', 35 | }, 36 | { 37 | name: '工时', 38 | path: '/labor-hour', 39 | component: '@/pages/LaborHour', 40 | }, 41 | { 42 | name: '日程', 43 | path: '/schedule', 44 | component: '@/pages/Schedule', 45 | }, 46 | { 47 | name: '设置', 48 | path: '/settings', 49 | component: '@/pages/Settings', 50 | }, 51 | { 52 | name: '消息通知', 53 | path: '/message', 54 | component: '@/pages/Message', 55 | }, 56 | { 57 | name: '项目邀请', 58 | path: '/invite/:id', 59 | component: '@/pages/Invite', 60 | }, 61 | ], 62 | }, 63 | ]; 64 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Statistics/components/UserTaskLaborHour.tsx: -------------------------------------------------------------------------------- 1 | import { Column } from '@ant-design/plots'; 2 | import {ProjectStatisticsParams} from "@/pages/ProjectDetail/components/Content/Statistics"; 3 | import Api from "@/api/modules"; 4 | import {useEffect, useState} from "react"; 5 | type PropsType = { 6 | params: ProjectStatisticsParams; 7 | }; 8 | const UserTask = ({params}: PropsType) => { 9 | const [data, setData] = useState([]) 10 | const config: any = { 11 | data, 12 | height: 320, 13 | xField: 'userName', 14 | yField: '工时', 15 | sort: { 16 | reverse: true, 17 | by: 'y', 18 | }, 19 | label: { 20 | text: (d: API.ProjectUserLaborHourStatItem) => d['工时'], 21 | textBaseline: 'bottom', 22 | }, 23 | style: { 24 | maxWidth: 50, 25 | radiusTopLeft: 4, 26 | radiusTopRight: 4, 27 | }, 28 | legend: true, 29 | }; 30 | const getData = () => { 31 | Api.ProjectOverview.getUserLaborHourStat(params).then((res) => { 32 | if (res.success && res.data) { 33 | setData(res.data || []); 34 | } 35 | }); 36 | }; 37 | useEffect(getData, [params]); 38 | return ( 39 |
40 |

用户工时统计

41 | 42 |
43 | ); 44 | }; 45 | 46 | export default UserTask; 47 | -------------------------------------------------------------------------------- /src/pages/components/TaskDetailModal/components/LaborHourCpt/index.tsx: -------------------------------------------------------------------------------- 1 | import { useModel } from '@umijs/max'; 2 | import { Divider, Space } from 'antd'; 3 | import { useState } from 'react'; 4 | import LaborHour from './components/LaborHour'; 5 | import LaborList from './components/LaborList'; 6 | import PlanHour from './components/PlanHour'; 7 | 8 | const LaborHourCpt = () => { 9 | const { data } = useModel('taskDetail'); 10 | const [editLaborData, setEditLaborData] = useState(undefined); 11 | if (!data.task) { 12 | return <>; 13 | } 14 | const { planHour, laborHour } = data.task; 15 | return ( 16 |
17 | 18 | {!!data.task && ( 19 |
20 | 21 | 计划工时{planHour}小时,实际工时 22 | planHour ? 'text-red-600' : ''}`}>{laborHour}小时 23 | 24 | }> 25 | 26 | 27 | 28 |
29 | )} 30 |
31 | 32 |
33 | ); 34 | }; 35 | 36 | export default LaborHourCpt; 37 | -------------------------------------------------------------------------------- /src/pages/Message/components/MsgContent.tsx: -------------------------------------------------------------------------------- 1 | import { EVENTS } from '@/constants'; 2 | import EventBus from '@/utils/event-bus'; 3 | import { useModel } from '@umijs/max'; 4 | import { Avatar } from 'antd'; 5 | 6 | const MsgContent = () => { 7 | const { state } = useModel('Message.model'); 8 | if (!state.msgDetail) { 9 | return
no msg
; 10 | } 11 | 12 | const openTaskDetail = () => EventBus.emit(EVENTS.OPEN_TASK_DETAIL, state.msgDetail?.taskId); 13 | 14 | const keywordsClass = `[&_.last-text]:font-bold [&_.last-text]:text-zinc-800 [&_.pre-text]:font-bold [&_.pre-text]:text-zinc-800`; 15 | 16 | return ( 17 |
18 |

{state.msgDetail?.title}

19 |
20 | 21 | {state.msgDetail?.senderName} 22 | {state.msgDetail?.createdAt} 23 |
24 |

28 | {!!state.msgDetail?.taskId && ( 29 |

30 | 31 | 查看任务 32 | 33 |
34 | )} 35 |
36 | ); 37 | }; 38 | 39 | export default MsgContent; 40 | -------------------------------------------------------------------------------- /src/services/demo/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // 该文件由 OneAPI 自动生成,请勿手动修改! 3 | 4 | declare namespace API { 5 | interface PageInfo { 6 | /** 7 | 1 */ 8 | current?: number; 9 | pageSize?: number; 10 | total?: number; 11 | list?: Array>; 12 | } 13 | 14 | interface PageInfo_UserInfo_ { 15 | /** 16 | 1 */ 17 | current?: number; 18 | pageSize?: number; 19 | total?: number; 20 | list?: Array; 21 | } 22 | 23 | interface Result { 24 | success?: boolean; 25 | errorMessage?: string; 26 | data?: Record; 27 | } 28 | 29 | interface Result_PageInfo_UserInfo__ { 30 | success?: boolean; 31 | errorMessage?: string; 32 | data?: PageInfo_UserInfo_; 33 | } 34 | 35 | interface Result_UserInfo_ { 36 | success?: boolean; 37 | errorMessage?: string; 38 | data?: UserInfo; 39 | } 40 | 41 | interface Result_string_ { 42 | success?: boolean; 43 | errorMessage?: string; 44 | data?: string; 45 | } 46 | 47 | type UserGenderEnum = 'MALE' | 'FEMALE'; 48 | 49 | interface UserInfo { 50 | id?: string; 51 | name?: string; 52 | /** nick */ 53 | nickName?: string; 54 | /** email */ 55 | email?: string; 56 | gender?: UserGenderEnum; 57 | } 58 | 59 | interface UserInfoVO { 60 | name?: string; 61 | /** nick */ 62 | nickName?: string; 63 | /** email */ 64 | email?: string; 65 | } 66 | 67 | type definitions_0 = null; 68 | } 69 | -------------------------------------------------------------------------------- /src/pages/components/TaskUploadFile/index.tsx: -------------------------------------------------------------------------------- 1 | import { UploadOne } from '@icon-park/react'; 2 | import type { UploadProps } from 'antd'; 3 | import { Button, Space, Upload } from 'antd'; 4 | 5 | const TaskUploadFile = ({ fileList, setFileList, setCurrentFile, removeResource }: any) => { 6 | const handleChange: UploadProps['onChange'] = (info) => { 7 | let newFileList = [...info.fileList]; 8 | newFileList = newFileList.map((file) => { 9 | if (file.response) { 10 | file.url = file.response.data.url; 11 | file.uid = file.response.data.id; 12 | if (typeof setCurrentFile === 'function') { 13 | setCurrentFile(file); 14 | } 15 | } 16 | return file; 17 | }); 18 | if (typeof setFileList === 'function') { 19 | setFileList(newFileList); 20 | } 21 | }; 22 | 23 | const props = { 24 | action: '/wkt-api/common/upload', 25 | onChange: handleChange, 26 | onRemove: (file: any) => { 27 | if (typeof removeResource === 'function') { 28 | removeResource(file); 29 | } 30 | }, 31 | size: 'small', 32 | multiple: true, 33 | }; 34 | return ( 35 | 36 | 42 | 43 | ); 44 | }; 45 | 46 | export default TaskUploadFile; 47 | -------------------------------------------------------------------------------- /src/pages/Settings/components/Content/User/columns.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, Space } from 'antd'; 2 | import { ColumnsType } from 'antd/es/table'; 3 | 4 | const RoleMap: Record = { 5 | SUPER_ADMIN: 超级管理员, 6 | ADMIN: 管理员, 7 | USER: 普通用户, 8 | }; 9 | 10 | const columns: ColumnsType = [ 11 | { 12 | title: '序号', 13 | align: 'center', 14 | dataIndex: 'id', 15 | key: 'id', 16 | width: 80, 17 | ellipsis: true, 18 | render: (_, record, index) => {index + 1}, 19 | }, 20 | { 21 | title: '用户名', 22 | dataIndex: 'username', 23 | key: 'username', 24 | width: 300, 25 | ellipsis: true, 26 | render: (_, record) => ( 27 | 28 | 29 | {record.username || ''} 30 | 31 | ), 32 | }, 33 | { 34 | title: '昵称', 35 | align: 'center', 36 | dataIndex: 'nickname', 37 | key: 'nickname', 38 | ellipsis: true, 39 | render: (_, record) => {record.nickname || '--'}, 40 | }, 41 | { 42 | title: '权限', 43 | align: 'center', 44 | dataIndex: 'role', 45 | key: 'role', 46 | render: (text) => RoleMap[text], 47 | }, 48 | { 49 | title: '邮箱', 50 | align: 'center', 51 | dataIndex: 'email', 52 | key: 'email', 53 | ellipsis: true, 54 | }, 55 | ]; 56 | 57 | export default columns; 58 | -------------------------------------------------------------------------------- /src/pages/components/NewTaskModal/comppnents/UserSelect.tsx: -------------------------------------------------------------------------------- 1 | import UserSelect from '@/pages/components/UserSelect'; 2 | import { CloseOne, User } from '@icon-park/react'; 3 | import { useModel } from '@umijs/max'; 4 | import { Avatar, Space } from 'antd'; 5 | 6 | export default () => { 7 | const { data, selectData, setSelectData, params, setParams } = useModel('newTask'); 8 | return ( 9 | 10 | { 13 | setParams({ handlerId: user.userId }); 14 | setSelectData({ taskHandler: user }); 15 | }} 16 | > 17 | 18 | } size={24} /> 19 | {selectData.taskHandler ? ( 20 | {selectData.taskHandler.username} 21 | ) : ( 22 | {'待认领'} 23 | )} 24 | 25 | 26 | { 28 | setSelectData({ taskHandler: undefined }); 29 | setParams({ handlerId: undefined }); 30 | }} 31 | className={'hidden align-middle text-zinc-200 hover:text-zinc-300 group-hover:inline'} 32 | > 33 | {!!params.handlerId && } 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/api/modules/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | // API 更新时间: 4 | // API 唯一标识: 5 | import * as Auth from './Auth'; 6 | import * as Upload from './Upload'; 7 | import * as Login from './Login'; 8 | import * as Message from './Message'; 9 | import * as ProjectUser from './ProjectUser'; 10 | import * as Project from './Project'; 11 | import * as ProjectGroup from './ProjectGroup'; 12 | import * as ProjectInvite from './ProjectInvite'; 13 | import * as ProjectOverview from './ProjectOverview'; 14 | import * as TaskLaborHour from './TaskLaborHour'; 15 | import * as TaskActor from './TaskActor'; 16 | import * as TaskAttachment from './TaskAttachment'; 17 | import * as TaskPriority from './TaskPriority'; 18 | import * as Task from './Task'; 19 | import * as TaskGroup from './TaskGroup'; 20 | import * as TaskOperationLog from './TaskOperationLog'; 21 | import * as TaskStatus from './TaskStatus'; 22 | import * as TaskType from './TaskType'; 23 | import * as Test from './Test'; 24 | import * as TestCase from './TestCase'; 25 | import * as User from './User'; 26 | import * as UserWorkPanel from './UserWorkPanel'; 27 | export default { 28 | Auth, 29 | Upload, 30 | Login, 31 | Message, 32 | ProjectUser, 33 | Project, 34 | ProjectGroup, 35 | ProjectInvite, 36 | ProjectOverview, 37 | TaskLaborHour, 38 | TaskActor, 39 | TaskAttachment, 40 | TaskPriority, 41 | Task, 42 | TaskGroup, 43 | TaskOperationLog, 44 | TaskStatus, 45 | TaskType, 46 | Test, 47 | TestCase, 48 | User, 49 | UserWorkPanel, 50 | }; 51 | -------------------------------------------------------------------------------- /src/pages/components/TaskDetailModal/components/TaskGroup.tsx: -------------------------------------------------------------------------------- 1 | import TaskGroupSelect from '@/pages/components/TaskGroupSelect'; 2 | import { CloseOne, Down, Help } from '@icon-park/react'; 3 | import { useModel } from '@umijs/max'; 4 | import { Space, Tooltip } from 'antd'; 5 | 6 | export default () => { 7 | const { data, updateTaskInfo } = useModel('taskDetail'); 8 | return ( 9 | updateTaskInfo({ groupId: item.id })}> 10 | 11 | {data.task?.groupName || {'设置任务分组'}} 12 | 13 | 14 | {!!data.task?.groupId && ( 15 | { 17 | e.stopPropagation(); 18 | updateTaskInfo({ groupId: undefined }); 19 | }} 20 | className={'!hidden !text-zinc-300 hover:!text-zinc-400 group-hover:!inline-flex'} 21 | theme="filled" 22 | size="16" 23 | /> 24 | )} 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Task/Filter/components/CardType.tsx: -------------------------------------------------------------------------------- 1 | import { panelCardTypeFilterMap, ViewTypeFilterMap } from '@/constants'; 2 | import { ConnectionBox } from '@icon-park/react'; 3 | import { useModel } from '@umijs/max'; 4 | import { Dropdown, MenuProps, Tooltip } from 'antd'; 5 | 6 | export default () => { 7 | const { setFilterData, filterData } = useModel('ProjectDetail.model'); 8 | 9 | const panelTypeFilter: MenuProps['items'] = [ 10 | { label: '任务状态', key: panelCardTypeFilterMap.statusId }, 11 | { label: '任务类型', key: panelCardTypeFilterMap.typeId }, 12 | { label: '任务优先级', key: panelCardTypeFilterMap.priority }, 13 | ]; 14 | const keyMap = { 15 | [panelCardTypeFilterMap.statusId]: '任务状态', 16 | [panelCardTypeFilterMap.typeId]: '任务类型', 17 | [panelCardTypeFilterMap.priority]: '任务优先级', 18 | }; 19 | const onSelectPanelType: MenuProps['onClick'] = ({ key }: { key: any }) => { 20 | setFilterData({ cardType: key }); 21 | }; 22 | return ( 23 | <> 24 | {filterData.viewType === ViewTypeFilterMap.CARD && ( 25 | 26 | 27 | 28 | 29 | {keyMap[filterData.cardType]} 30 | 31 | 32 | 33 | )} 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Setting/components/UserList/columns.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, Space } from 'antd'; 2 | import { ColumnsType } from 'antd/es/table'; 3 | 4 | const RoleMap: Record = { 5 | SUPER_ADMIN: 项目负责人, 6 | ADMIN: '项目管理员', 7 | USER: 普通用户, 8 | }; 9 | 10 | const columns: ColumnsType = [ 11 | { 12 | title: '序号', 13 | align: 'center', 14 | dataIndex: 'id', 15 | key: 'id', 16 | width: 60, 17 | ellipsis: true, 18 | render: (_, record, index) => {index + 1}, 19 | }, 20 | { 21 | title: '用户名', 22 | dataIndex: 'username', 23 | key: 'username', 24 | width: 300, 25 | ellipsis: true, 26 | render: (_, record) => ( 27 | 28 | 29 | {record.username || ''} 30 | 31 | ), 32 | }, 33 | { 34 | title: '昵称', 35 | align: 'center', 36 | dataIndex: 'nickname', 37 | key: 'nickname', 38 | ellipsis: true, 39 | render: (_, record) => {record.nickname || '--'}, 40 | }, 41 | { 42 | title: '权限', 43 | align: 'center', 44 | dataIndex: 'role', 45 | key: 'role', 46 | ellipsis: true, 47 | render: (text) => RoleMap[text], 48 | }, 49 | { 50 | title: '邮箱', 51 | align: 'center', 52 | dataIndex: 'email', 53 | key: 'email', 54 | ellipsis: true, 55 | }, 56 | ]; 57 | 58 | export default columns; 59 | -------------------------------------------------------------------------------- /src/pages/LaborHour/components/DateHeader.tsx: -------------------------------------------------------------------------------- 1 | import { PeoplePlusOne } from '@icon-park/react'; 2 | import { useModel } from '@umijs/max'; 3 | import dayjs from 'dayjs'; 4 | import UserSelect from './AllUserSelect'; 5 | 6 | const LaborHourItem = () => { 7 | const { data } = useModel('LaborHour.model'); 8 | const getTime = (day: number = 0) => dayjs(data.startTime).add(day, 'd').format('M / D'); 9 | 10 | const itemClass = 'text-center p-4'; 11 | return ( 12 |
13 |
14 | 15 | void 0}> 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
周一 {getTime()}
24 |
周二 {getTime(1)}
25 |
周三 {getTime(2)}
26 |
周四 {getTime(3)}
27 |
周五 {getTime(4)}
28 |
周六 {getTime(5)}
29 |
周日 {getTime(6)}
30 |
31 |
32 | ); 33 | }; 34 | export default LaborHourItem; 35 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Statistics/components/UserTaskStatusTrend.tsx: -------------------------------------------------------------------------------- 1 | import { Column } from '@ant-design/plots'; 2 | import {useEffect, useState} from "react"; 3 | import {ProjectStatisticsParams} from "@/pages/ProjectDetail/components/Content/Statistics"; 4 | import Api from "@/api/modules"; 5 | type PropsType = { 6 | params: ProjectStatisticsParams; 7 | }; 8 | const UserTaskStatusTrend = ({params}: PropsType) => { 9 | const [data, setData] = useState([]) 10 | const config: any = { 11 | data, 12 | height: 350, 13 | xField: 'userName', 14 | yField: 'taskCount', 15 | colorField: 'statusName', 16 | stack: true, 17 | sort: { 18 | reverse: true, 19 | by: 'y', 20 | }, 21 | style: { 22 | maxWidth: 50, 23 | radiusTopLeft: 4, 24 | radiusTopRight: 4, 25 | }, 26 | axis: { 27 | y: { labelFormatter: '~s' }, 28 | x: { 29 | labelSpacing: 4, 30 | labelTransform: 'rotate(45)', 31 | }, 32 | }, 33 | }; 34 | const getData = () => { 35 | Api.ProjectOverview.getUserTaskStatusStat(params).then((res) => { 36 | if (res.success && res.data) { 37 | setData(res.data || []); 38 | } 39 | }); 40 | }; 41 | useEffect(getData, [params]); 42 | return ( 43 |
44 |

任务数按任务状态排行

45 | 46 |
47 | ); 48 | }; 49 | 50 | export default UserTaskStatusTrend; 51 | -------------------------------------------------------------------------------- /src/pages/Home/components/Introduce.tsx: -------------------------------------------------------------------------------- 1 | import tipsViewImg from '@/assets/images/home/tips-view.png'; 2 | import { Avatar } from 'antd'; 3 | 4 | const HomeIntroduce = () => { 5 | const blockCommonClass = 'border border-zinc-100 rounded-lg'; 6 | const modules = [ 7 | { 8 | name: '项目管理', 9 | desc: '', 10 | }, 11 | { 12 | name: '任务管理', 13 | desc: '', 14 | }, 15 | { 16 | name: '工时管理', 17 | desc: '', 18 | }, 19 | { 20 | name: '更多工具', 21 | desc: '', 22 | }, 23 | ]; 24 | return ( 25 |
26 |
27 |
28 |

💡 办公利器:一站式工作工具

29 |

30 | 全面的项目管理工具,集任务、项目、工时、办公、文档等功能于一体。高效管理任务进度,协调团队合作,记录工时消耗,提供办公工具支持,便捷管理文档。完美解决项目管理需求。 31 |

32 |
33 | {modules.map((item) => { 34 | return ( 35 |
36 |

{item.name}

37 |

{item.desc}

38 |
39 | ); 40 | })} 41 |
42 |
43 |
44 | 45 |
46 |
47 |
48 | ); 49 | }; 50 | 51 | export default HomeIntroduce; 52 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Task/Filter/components/TaskType.tsx: -------------------------------------------------------------------------------- 1 | import { panelCardTypeFilterMap } from '@/constants'; 2 | import TaskTypeSelect from '@/pages/components/TaskTypeSelect'; 3 | import { CategoryManagement } from '@icon-park/react'; 4 | import { useModel } from '@umijs/max'; 5 | import { useUpdateEffect } from 'ahooks'; 6 | import { Tooltip } from 'antd'; 7 | import { useState } from 'react'; 8 | const defaultTaskType: API.TaskTypeItem = { id: '', name: '全部', color: '' }; 9 | export default () => { 10 | const { data, filterData, setTaskParams } = useModel('ProjectDetail.model'); 11 | const [taskType, setTaskType] = useState(defaultTaskType); 12 | 13 | useUpdateEffect(() => { 14 | if (data.projectId) { 15 | setTaskType(defaultTaskType); 16 | } 17 | }, [data.projectId]); 18 | 19 | if (filterData.cardType === panelCardTypeFilterMap.typeId) return null; 20 | 21 | return ( 22 | } 24 | id={taskType?.id} 25 | projectId={data.projectId} 26 | onSelect={(item) => setTaskType(item)} 27 | onChange={(item) => { 28 | setTaskParams({ typeId: item.id || undefined }); 29 | setTaskType(item); 30 | }} 31 | > 32 | 33 | 34 | 35 | {taskType?.name}类型 36 | 37 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Task/components/Card/useService.ts: -------------------------------------------------------------------------------- 1 | import { ITaskCardItem } from '@/pages/ProjectDetail/types'; 2 | import { useModel } from '@umijs/max'; 3 | 4 | export default () => { 5 | const { data: projectData, setData, filterData, updateTaskInfo } = useModel('ProjectDetail.model'); 6 | 7 | const onDragEnd = (result: any) => { 8 | if (!result.destination) { 9 | return; 10 | } 11 | // 子节点拖拽 12 | const list: ITaskCardItem[] = projectData.taskCardList; 13 | const sourceDroppableId = result.source.droppableId; 14 | const destinationDroppableId = result.destination.droppableId; 15 | 16 | let sourceIndex = 0; 17 | let destinationIndex = 0; 18 | 19 | list.forEach((item: ITaskCardItem, index: number) => { 20 | if (item.cardTypeId === sourceDroppableId) sourceIndex = index; 21 | if (item.cardTypeId === destinationDroppableId) destinationIndex = index; 22 | }); 23 | const sourceList = list[sourceIndex].taskList || []; 24 | const destinationList = list[destinationIndex].taskList || []; 25 | const [delItem] = sourceList.splice(result.source.index, 1); 26 | destinationList.splice(result.destination.index, 0, delItem); 27 | 28 | // 当前拖拽任务ID 29 | const taskId = result.draggableId; 30 | updateTaskInfo({ 31 | id: taskId, 32 | projectId: projectData.projectId, 33 | [filterData.cardType]: list[destinationIndex].cardTypeId, 34 | }); 35 | // 插入到新列表中 - 含顺序 36 | setData({ 37 | taskCardList: [...list], 38 | }); 39 | }; 40 | 41 | return { 42 | onDragEnd, 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Task/components/Card/components/TaskGroup.tsx: -------------------------------------------------------------------------------- 1 | import useScrollMove from '@/hooks/useScrollMove'; 2 | import { ITaskCardItem } from '@/pages/ProjectDetail/types'; 3 | import { useModel } from '@umijs/max'; 4 | import { Spin } from 'antd'; 5 | import { useRef } from 'react'; 6 | import { Droppable } from 'react-beautiful-dnd'; 7 | import CardItem from './CardItem'; 8 | 9 | export default (props: { prefix: any; index: number; group: ITaskCardItem }) => { 10 | const ref = useRef(null); 11 | const { prefix, group, index: groupIndex } = props; 12 | const { loadMoreTaskByPanelType } = useModel('ProjectDetail.model'); 13 | return ( 14 |
loadMoreTaskByPanelType(groupIndex))} 18 | > 19 | 20 | {(provided) => ( 21 |
22 | {group.taskList?.map((item, i: number) => { 23 | return ; 24 | })} 25 | {group.loading && ( 26 |
27 | 28 |
29 | )} 30 | {provided.placeholder} 31 |
32 | )} 33 |
34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/pages/components/TaskDetailModal/components/UserSelect.tsx: -------------------------------------------------------------------------------- 1 | import UserSelect from '@/pages/components/UserSelect'; 2 | import { CloseOne, User } from '@icon-park/react'; 3 | import { useModel } from '@umijs/max'; 4 | import { Avatar, Space } from 'antd'; 5 | 6 | const HeadUserSelect = () => { 7 | const { data, updateTaskInfo } = useModel('taskDetail'); 8 | return ( 9 | 17 | {!!data.task && ( 18 | updateTaskInfo({ handlerId: user.userId })}> 19 | 20 | } 25 | size={25} 26 | /> 27 | {data.task?.handlerName || {'待认领'}} 28 | 29 | 30 | )} 31 | 32 | {!!data.task?.handlerId && ( 33 | { 35 | updateTaskInfo({ handlerId: '' }); 36 | }} 37 | className={'cursor-pointer'} 38 | theme="filled" 39 | size="16" 40 | /> 41 | )} 42 | 43 | 44 | ); 45 | }; 46 | export default HeadUserSelect; 47 | -------------------------------------------------------------------------------- /src/layouts/LeftMenu/components/Message/index.tsx: -------------------------------------------------------------------------------- 1 | import { useModel } from '@@/exports'; 2 | import { Remind } from '@icon-park/react'; 3 | import { Badge, Popover } from 'antd'; 4 | import { useRef, useState } from 'react'; 5 | import MsgContent from './MsgContent'; 6 | import MsgList from './MsgList'; 7 | 8 | export default () => { 9 | const { globalData } = useModel('global'); 10 | const [open, setOpen] = useState(false); 11 | const ref: any = useRef(); 12 | 13 | const content = () => { 14 | return ( 15 |
16 | 17 | setOpen(false)} /> 18 |
19 | ); 20 | }; 21 | 22 | return ( 23 |
24 | ref.current} 28 | placement="rightBottom" 29 | title={null} 30 | content={content} 31 | trigger="click" 32 | > 33 |
setOpen(true)}> 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 |
43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/pages/Project/components/Container/components/ProjectItem.tsx: -------------------------------------------------------------------------------- 1 | import projectAvatar from '@/assets/images/default/project-avatar.jpeg'; 2 | import { PROJECT_SHOW_TYPE } from '@/constants'; 3 | import { Lock } from '@icon-park/react'; 4 | import { history } from '@umijs/max'; 5 | import { Image } from 'antd'; 6 | 7 | export default (props: { item: API.ProjectInfoItem }) => { 8 | const goDetail = () => history.push(`/project/detail?id=${props.item.id}`); 9 | 10 | return ( 11 |
16 |
17 | 24 |
25 |
{ 28 | e.preventDefault(); 29 | e.stopPropagation(); 30 | }} 31 | > 32 |

{props.item.name}

33 | {props.item.showType === PROJECT_SHOW_TYPE.PRIVATE && ( 34 | 35 | 36 | 37 | )} 38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Statistics/components/UserTaskTypeTrend.tsx: -------------------------------------------------------------------------------- 1 | import { Column } from '@ant-design/plots'; 2 | import {useEffect, useState} from "react"; 3 | import {ProjectStatisticsParams} from "@/pages/ProjectDetail/components/Content/Statistics"; 4 | import Api from "@/api/modules"; 5 | type PropsType = { 6 | params: ProjectStatisticsParams; 7 | }; 8 | const UserTaskStatusTrend = ({params}: PropsType) => { 9 | const [data, setData] = useState([]) 10 | const config: any = { 11 | data, 12 | height: 350, 13 | xField: 'userName', 14 | yField: 'taskCount', 15 | colorField: 'typeName', 16 | stack: true, 17 | sort: { 18 | reverse: true, 19 | by: 'y', 20 | }, 21 | scale: { 22 | x: { padding: 0.2 }, 23 | }, 24 | style: { 25 | maxWidth: 50, 26 | radiusTopLeft: 4, 27 | radiusTopRight: 4, 28 | }, 29 | axis: { 30 | y: { labelFormatter: '~s' }, 31 | x: { 32 | labelSpacing: 4, 33 | style: { 34 | labelTransform: 'rotate(45)', 35 | }, 36 | }, 37 | }, 38 | }; 39 | const getData = () => { 40 | Api.ProjectOverview.getUserTaskTypeStat(params).then((res) => { 41 | if (res.success && res.data) { 42 | setData(res.data || []); 43 | } 44 | }); 45 | }; 46 | useEffect(getData, [params]); 47 | return ( 48 |
49 |

任务数按任务类型排行

50 | 51 |
52 | ); 53 | }; 54 | 55 | export default UserTaskStatusTrend; 56 | -------------------------------------------------------------------------------- /src/pages/Settings/components/Menu/index.tsx: -------------------------------------------------------------------------------- 1 | import { SettingsType } from '@/pages/Settings/model'; 2 | import { Group, Qiyehao, Setting, SortOne, User } from '@icon-park/react'; 3 | import { useModel } from '@umijs/max'; 4 | import { Menu } from 'antd'; 5 | import type { MenuProps } from 'antd/es/menu'; 6 | import React from 'react'; 7 | import styles from './index.less'; 8 | 9 | function genMenuItem(label: React.ReactNode, key?: React.Key | null, Icon?: any, children?: MenuItem[]): MenuItem { 10 | return { 11 | key, 12 | icon: , 13 | children, 14 | label, 15 | } as MenuItem; 16 | } 17 | 18 | type MenuItem = Required['items'][number]; 19 | 20 | export default () => { 21 | const { state, setState } = useModel('Settings.model'); 22 | 23 | const items: MenuItem[] = [ 24 | genMenuItem('用户管理', SettingsType.USER, User), 25 | genMenuItem('租户管理', SettingsType.TENANT, Qiyehao), 26 | genMenuItem('系统配置', SettingsType.SYSTEM, Setting), 27 | genMenuItem('项目分组', SettingsType.PROJECT_GROUP, Group), 28 | genMenuItem('任务优先级', SettingsType.TASK_PRIORITY, SortOne), 29 | ]; 30 | 31 | return ( 32 |
33 |
34 |
35 |

设置

36 |
37 | setState({ activeKey: key as SettingsType })} 41 | items={items} 42 | /> 43 |
44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/layouts/index.tsx: -------------------------------------------------------------------------------- 1 | import { StyleProvider } from '@ant-design/cssinjs'; 2 | import '@icon-park/react/styles/index.css'; 3 | import { Outlet, useModel } from '@umijs/max'; 4 | import { App, ConfigProvider, Watermark } from 'antd'; 5 | import zhCN from 'antd/es/locale/zh_CN'; 6 | import dayjs from 'dayjs'; 7 | import 'dayjs/locale/zh-cn'; 8 | import LeftMenu from './LeftMenu'; 9 | import React, {Suspense} from "react"; 10 | dayjs.locale('zh-cn'); 11 | 12 | const TaskDetailModal = React.lazy(() => import('@/pages/components/TaskDetailModal')) 13 | const NewTaskModal = React.lazy(() => import('@/pages/components/NewTaskModal')) 14 | 15 | export default () => { 16 | const { initialState } = useModel('@@initialState'); 17 | return ( 18 | 24 | 25 | 26 |
27 | {!!initialState?.id && } 28 |
29 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 |
42 |
43 |
44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Setting/components/TaskType/useService.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useModel } from '@umijs/max'; 3 | import { useSetState } from 'ahooks'; 4 | import { App } from 'antd'; 5 | import { useEffect, useState } from 'react'; 6 | 7 | type SettingDataType = { 8 | pageSize: number; 9 | pageNo: number; 10 | finished: boolean; 11 | }; 12 | const useService = () => { 13 | const { notification } = App.useApp(); 14 | const [typeList, setTypeList] = useState([]); 15 | const [data, setData] = useSetState({ 16 | pageSize: 20, 17 | pageNo: 1, 18 | finished: false, 19 | }); 20 | 21 | const { data: projectData } = useModel('ProjectDetail.model'); 22 | 23 | const getTaskTypeList = () => { 24 | Api.TaskType.getTaskTypeList({ 25 | projectId: projectData.projectId, 26 | pageSize: 888, 27 | }).then((res) => { 28 | if (res.data) { 29 | setTypeList(res.data.list || []); 30 | } 31 | }); 32 | }; 33 | 34 | const delType = (id: string) => { 35 | Api.TaskType.delTaskType({ 36 | id, 37 | projectId: projectData.projectId, 38 | }).then((res) => { 39 | if (res.success) { 40 | getTaskTypeList(); 41 | } else { 42 | notification.error({ 43 | message: '温馨提示', 44 | description: res.message, 45 | }); 46 | } 47 | }); 48 | }; 49 | 50 | useEffect(() => { 51 | getTaskTypeList(); 52 | }, [projectData.projectId]); 53 | 54 | return { 55 | data, 56 | setData, 57 | delType, 58 | typeList, 59 | setTypeList, 60 | getTaskTypeList, 61 | }; 62 | }; 63 | 64 | export default useService; 65 | -------------------------------------------------------------------------------- /src/pages/Settings/components/Content/TaskPriority/useService.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useRequest } from 'ahooks'; 3 | import { App } from 'antd'; 4 | import { useEffect, useState } from 'react'; 5 | 6 | const useService = () => { 7 | const { notification } = App.useApp(); 8 | const [priorityList, setPriorityList] = useState([]); 9 | 10 | const { loading, run: getTaskPriority } = useRequest( 11 | async () => { 12 | const res = await Api.TaskPriority.getTaskPriorityList({ 13 | pageNo: 1, 14 | pageSize: 10, 15 | }); 16 | if (res.data) { 17 | const tpList = res.data.list || []; 18 | setPriorityList(tpList); 19 | } 20 | }, 21 | { 22 | manual: true, 23 | }, 24 | ); 25 | 26 | const delPriority = (id: string, index: number) => { 27 | Api.TaskPriority.delTaskPriority({ id }).then((res) => { 28 | if (res.success) { 29 | priorityList.splice(index, 1); 30 | setPriorityList([...priorityList]); 31 | } else { 32 | notification.error({ 33 | message: '温馨提示', 34 | description: res.message, 35 | }); 36 | } 37 | }); 38 | }; 39 | 40 | const updatePriorityRow = (data: API.UpdateTaskPriorityReq, index: number) => { 41 | priorityList[index].name = data.name!; 42 | priorityList[index].color = data.color!; 43 | setPriorityList([...priorityList]); 44 | }; 45 | 46 | useEffect(() => { 47 | getTaskPriority(); 48 | }, []); 49 | 50 | return { 51 | loading, 52 | delPriority, 53 | priorityList, 54 | getTaskPriority, 55 | setPriorityList, 56 | updatePriorityRow, 57 | }; 58 | }; 59 | 60 | export default useService; 61 | -------------------------------------------------------------------------------- /src/pages/components/ProjectSelect/useService.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useRequest } from '@umijs/max'; 3 | import { useSetState } from 'ahooks'; 4 | import { useEffect, useState } from 'react'; 5 | 6 | const useProjectList = () => { 7 | const [data, setData] = useSetState<{ 8 | projectList: API.ProjectInfoItem[]; 9 | total: number; 10 | finished: boolean; 11 | }>({ 12 | total: 0, 13 | finished: false, 14 | projectList: [], 15 | }); 16 | const [params, setParams] = useSetState({ 17 | keywords: '', 18 | pageNo: 1, 19 | pageSize: 50, 20 | }); 21 | const [open, setOpen] = useState(false); 22 | 23 | const { loading, run: getProjectList } = useRequest( 24 | () => 25 | Api.Project.getProjectList(params).then((res) => { 26 | if (res.success) { 27 | let list = res.data?.list || []; 28 | list = params.pageNo === 1 ? list : [...data.projectList, ...list]; 29 | const total = res.data?.total || 0; 30 | setData({ 31 | projectList: list, 32 | total, 33 | finished: total === list.length, 34 | }); 35 | setParams({ 36 | pageNo: ++params.pageNo!, 37 | }); 38 | } 39 | }), 40 | { 41 | manual: true, 42 | }, 43 | ); 44 | 45 | const loadMore = () => { 46 | if (data.finished) return; 47 | getProjectList(); 48 | }; 49 | 50 | useEffect(() => { 51 | if (data.finished) return; 52 | getProjectList(); 53 | }, [params.keywords]); 54 | 55 | return { 56 | data, 57 | open, 58 | setData, 59 | setOpen, 60 | setParams, 61 | loading, 62 | loadMore, 63 | }; 64 | }; 65 | 66 | export default useProjectList; 67 | -------------------------------------------------------------------------------- /src/api/modules/TaskActor.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** 项目添加用户 POST /task/addTaskActor */ 6 | export async function addTaskActor(body: API.AddTaskActorReq, options?: { [key: string]: any }) { 7 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 8 | '/task/addTaskActor', 9 | { 10 | method: 'POST', 11 | headers: { 12 | 'Content-Type': 'application/json', 13 | }, 14 | data: body, 15 | ...(options || {}), 16 | }, 17 | ); 18 | } 19 | 20 | /** 删除任务参与人员 POST /task/delTaskActor */ 21 | export async function delTaskActor(body: API.DelTaskActorReq, options?: { [key: string]: any }) { 22 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 23 | '/task/delTaskActor', 24 | { 25 | method: 'POST', 26 | headers: { 27 | 'Content-Type': 'application/json', 28 | }, 29 | data: body, 30 | ...(options || {}), 31 | }, 32 | ); 33 | } 34 | 35 | /** 获取任务下的用户列表 GET /task/getTaskActor */ 36 | export async function getTaskActor( 37 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 38 | params: API.getTaskActorParams, 39 | options?: { [key: string]: any }, 40 | ) { 41 | return request<{ 42 | code: number; 43 | message: string; 44 | data?: { list?: API.TaskActorItem[]; pageNo?: number; pageSize?: number; total?: number }; 45 | success: boolean; 46 | }>('/task/getTaskActor', { 47 | method: 'GET', 48 | params: { 49 | // pageNo has a default value: 1 50 | pageNo: '1', 51 | // pageSize has a default value: 10 52 | pageSize: '10', 53 | ...params, 54 | }, 55 | ...(options || {}), 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Task/Filter/components/ViewType.tsx: -------------------------------------------------------------------------------- 1 | import { ViewTypeFilterKey } from '@/constants'; 2 | import { TableFile, Timeline, ViewGridCard } from '@icon-park/react'; 3 | import { useModel } from '@umijs/max'; 4 | import { Dropdown, MenuProps, Space, Tooltip } from 'antd'; 5 | 6 | export default () => { 7 | const { filterData, setFilterData } = useModel('ProjectDetail.model'); 8 | 9 | const fontSize = 13; 10 | const onSelectView: MenuProps['onClick'] = ({ key }: { key: any }) => { 11 | setFilterData({ viewType: key }); 12 | }; 13 | const viewTypeMap: { 14 | [key in ViewTypeFilterKey | string]: { 15 | name: string; 16 | icon: JSX.Element; 17 | }; 18 | } = { 19 | CARD: { 20 | name: '卡片视图', 21 | icon: , 22 | }, 23 | TABLE: { 24 | name: '表格视图', 25 | icon: , 26 | }, 27 | GANTT: { 28 | name: '甘特图', 29 | icon: , 30 | }, 31 | }; 32 | const viewList: MenuProps['items'] = Object.keys(viewTypeMap).map((key) => ({ 33 | key, 34 | label: ( 35 | 36 | {viewTypeMap[key].icon} 37 | {viewTypeMap[key].name} 38 | 39 | ), 40 | })); 41 | return ( 42 | 43 | 44 |
45 | {viewTypeMap[filterData.viewType].icon} 46 | {viewTypeMap[filterData.viewType].name} 47 |
48 |
49 |
50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/utils/url-utils.ts: -------------------------------------------------------------------------------- 1 | type anyObj = { [key: string]: any }; 2 | 3 | /** 4 | * @description: 5 | * @param {string} url 6 | * @param {anyObj} query 7 | * @return {string} 8 | */ 9 | export const addQuery = (url: string, query: anyObj): string => { 10 | const { protocol, host } = location; 11 | let path = url; 12 | const hasProtocol = path.includes('http'); 13 | if (!hasProtocol) { 14 | path = `${protocol}//${host}${url}`; 15 | } 16 | const _url = new URL(path); 17 | for (const key in query) { 18 | if (query[key]) { 19 | _url.searchParams.set(key, query[key]); 20 | } 21 | } 22 | return hasProtocol ? _url.href : _url.pathname + _url.search; 23 | }; 24 | 25 | /** 26 | * @description: 27 | * @param {string} key 28 | * @return {string} 29 | */ 30 | export const getQueryByKey = (key: string): string => { 31 | const query = new URL(location.href); 32 | return query.searchParams.get(key) || ''; 33 | }; 34 | 35 | /** 36 | * @description: 37 | * @param {*} url 38 | * @return {*} 39 | */ 40 | export const getQueryObject = (url: any = location.href): anyObj => { 41 | const kvs = new URL(url).search.slice(1).split('&'); 42 | const query: anyObj = {}; 43 | for (let i = 0; i < kvs.length; i++) { 44 | const q = kvs[i].split('='); 45 | if (q[1]) query[q[0]] = decodeURIComponent(q[1]); 46 | } 47 | return query; 48 | }; 49 | 50 | /** 51 | * @description: 52 | * @param {anyObj} data 53 | * @param clear 54 | * @return {*} 55 | */ 56 | export const replaceUrlByQuery = (data: anyObj, clear?: boolean): any => { 57 | const { protocol, host, pathname } = location; 58 | const url = `${protocol}//${host}${pathname}`; 59 | let query = Object.assign(getQueryObject(), data); 60 | if (clear) { 61 | query = data; 62 | } 63 | window.history.replaceState('replace', 'null', addQuery(url, query)); 64 | }; 65 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/types.ts: -------------------------------------------------------------------------------- 1 | import { panelCardTypeFilterKey, projectContentType, TaskBelongKey, ViewTypeFilterKey } from '@/constants'; 2 | 3 | export type CardDataType = API.TaskPriorityItem | API.TaskTypeItem | API.TaskStatusItem; 4 | 5 | export interface ITaskCardItem { 6 | cardTypeId: string; // panelCardTypeFilterKey 为分类 7 | cardTypeName: string; 8 | cardTypeColor: string; 9 | cardTypeData: CardDataType; 10 | taskList: API.TaskDetailItem[]; 11 | loading: boolean; 12 | finished: boolean; 13 | pageNo: number; 14 | pageSize: number; 15 | total: number; 16 | } 17 | 18 | export type GanttDataItem = { 19 | id: string; 20 | taskName: string; 21 | startDate: string; 22 | endDate: string; 23 | disabled: boolean; 24 | content: API.TaskDetailItem; 25 | children: GanttDataItem[]; 26 | }; 27 | type TaskSubProps = { 28 | list: T[]; 29 | kv: Record; 30 | }; 31 | export interface IData { 32 | projectId: string; 33 | project?: Partial; 34 | projectListData?: API.GetProjectListRes; 35 | taskCardList: ITaskCardItem[]; 36 | taskPriority: TaskSubProps; 37 | taskStatus: TaskSubProps; 38 | taskType: TaskSubProps; 39 | ganttData: { 40 | finished: boolean; 41 | list: GanttDataItem[]; 42 | pageNo: number; 43 | pageSize: number; 44 | total: number; 45 | }; 46 | tableData: { 47 | finished: boolean; 48 | pageNo: number; 49 | pageSize: number; 50 | total?: number; 51 | list: API.TaskDetailItem[]; 52 | }; 53 | } 54 | 55 | export interface IFilterType { 56 | cardType: panelCardTypeFilterKey; 57 | viewType: ViewTypeFilterKey; 58 | contentType: projectContentType; 59 | belongKey: TaskBelongKey | string; 60 | typeId?: string; 61 | group: { 62 | id: any; 63 | name: string; 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /src/pages/components/TaskSelect/useService.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useModel, useRequest } from '@umijs/max'; 3 | import { useSetState, useUpdateEffect } from 'ahooks'; 4 | import { useState } from 'react'; 5 | 6 | const useProjectList = (projectId?: string) => { 7 | const { initialState } = useModel('@@initialState'); 8 | const [data, setData] = useSetState<{ 9 | taskList: API.TaskDetailItem[]; 10 | projectId?: string; 11 | keywords: string; 12 | finished: boolean; 13 | pageNo: number; 14 | pageSize: number; 15 | }>({ 16 | taskList: [], 17 | projectId, 18 | keywords: '', 19 | finished: false, 20 | pageNo: 1, 21 | pageSize: 10, 22 | }); 23 | 24 | const [open, setOpen] = useState(false); 25 | 26 | const { loading, run: getTaskList } = useRequest( 27 | () => 28 | Api.Task.getTaskList({ 29 | handlerId: initialState?.id, 30 | keywords: data.keywords || undefined, 31 | pageNo: data.pageNo, 32 | pageSize: data.pageSize, 33 | }).then((res) => { 34 | if (res.success) { 35 | const dataList = res?.data?.list || []; 36 | const list = data.pageNo === 1 ? dataList : [...data.taskList, ...dataList]; 37 | setData({ 38 | taskList: list, 39 | pageNo: ++data.pageNo, 40 | finished: list.length === res.data?.total, 41 | }); 42 | } 43 | }), 44 | { 45 | manual: true, 46 | }, 47 | ); 48 | 49 | useUpdateEffect(() => { 50 | getTaskList(); 51 | }, [data.keywords]); 52 | 53 | useUpdateEffect(() => { 54 | if (open) { 55 | getTaskList(); 56 | } 57 | }, [open]); 58 | 59 | return { 60 | data, 61 | setData, 62 | getTaskList, 63 | loading, 64 | open, 65 | setOpen, 66 | }; 67 | }; 68 | 69 | export default useProjectList; 70 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Setting/index.tsx: -------------------------------------------------------------------------------- 1 | import useQueryParams from '@/hooks/useQueryParams'; 2 | import { Tabs, TabsProps } from 'antd'; 3 | 4 | import ProjectBaseInfo from './components/ProjectBaseInfo'; 5 | import TaskGroupPage from './components/TaskGroup'; 6 | import TaskStatusPage from './components/TaskStatus'; 7 | import TaskTypePage from './components/TaskType'; 8 | import ProjectUserList from './components/UserList'; 9 | 10 | const SettingKeyMap = { 11 | BaseInfo: 'BASE_INFO', 12 | UserList: 'USER_LIST', 13 | TaskGroup: 'TASK_GROUP', 14 | TaskStatus: 'TASK_STATUS', 15 | TaskType: 'TASK_TYPE', 16 | }; 17 | 18 | const ProjectDetailSetting = () => { 19 | const items: TabsProps['items'] = [ 20 | { 21 | key: SettingKeyMap.BaseInfo, 22 | label: '项目信息', 23 | children: , 24 | }, 25 | { 26 | key: SettingKeyMap.TaskStatus, 27 | label: '任务状态', 28 | children: , 29 | }, 30 | { 31 | key: SettingKeyMap.TaskType, 32 | label: '任务类型', 33 | children: , 34 | }, 35 | { 36 | key: SettingKeyMap.TaskGroup, 37 | label: '任务迭代', 38 | children: , 39 | }, 40 | { 41 | key: SettingKeyMap.UserList, 42 | label: '用户列表', 43 | children: , 44 | }, 45 | ]; 46 | 47 | const [query, setQuery] = useQueryParams(); 48 | const onTabChange = (key: string) => { 49 | setQuery({ settingTab: key }); 50 | }; 51 | return ( 52 |
53 | 59 |
60 | ); 61 | }; 62 | export default ProjectDetailSetting; 63 | -------------------------------------------------------------------------------- /src/api/modules/TaskAttachment.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** 添加任务附件 POST /task/addTaskAttachment */ 6 | export async function addTaskAttachment( 7 | body: API.AddTaskAttachmentReq, 8 | options?: { [key: string]: any }, 9 | ) { 10 | return request<{ code: number; message: string; data?: { id?: string }; success: boolean }>( 11 | '/task/addTaskAttachment', 12 | { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | data: body, 18 | ...(options || {}), 19 | }, 20 | ); 21 | } 22 | 23 | /** 删除 POST /task/delTaskAttachment */ 24 | export async function delTaskAttachment( 25 | body: API.DelTaskAttachmentReq, 26 | options?: { [key: string]: any }, 27 | ) { 28 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 29 | '/task/delTaskAttachment', 30 | { 31 | method: 'POST', 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | }, 35 | data: body, 36 | ...(options || {}), 37 | }, 38 | ); 39 | } 40 | 41 | /** 获取附件(项目|任务|创建者) GET /task/getTaskAttachmentList */ 42 | export async function getTaskAttachmentList( 43 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 44 | params: API.getTaskAttachmentListParams, 45 | options?: { [key: string]: any }, 46 | ) { 47 | return request<{ 48 | code: number; 49 | message: string; 50 | data?: { list?: API.TaskAttachmentItem[]; pageNo?: number; pageSize?: number; total?: number }; 51 | success: boolean; 52 | }>('/task/getTaskAttachmentList', { 53 | method: 'GET', 54 | params: { 55 | // pageNo has a default value: 1 56 | pageNo: '1', 57 | // pageSize has a default value: 10 58 | pageSize: '10', 59 | ...params, 60 | }, 61 | ...(options || {}), 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | plugin: [require('@tailwindcss/aspect-ratio'), require('@tailwindcss/typography')], 4 | content: ['./src/pages/**/*.tsx', './src/components/**/*.tsx', './src/layouts/**/*.tsx'], 5 | corePlugins: { 6 | preflight: false, 7 | }, 8 | theme: { 9 | extend: { 10 | colors: { 11 | primary: 'rgba(var(--primary-color), )', 12 | 'primary-10': 'rgb(var(--primary-color),1%)', 13 | 'primary-50': 'rgb(var(--primary-color),5%)', 14 | 'primary-100': 'rgb(var(--primary-color),10%)', 15 | 'primary-150': 'rgb(var(--primary-color),15%)', 16 | 'primary-200': 'rgb(var(--primary-color),20%)', 17 | 'primary-250': 'rgb(var(--primary-color),25%)', 18 | 'primary-300': 'rgb(var(--primary-color),30%)', 19 | 'primary-400': 'rgb(var(--primary-color),40%)', 20 | 'primary-500': 'rgb(var(--primary-color),50%)', 21 | 'primary-600': 'rgb(var(--primary-color),60%)', 22 | 'primary-700': 'rgb(var(--primary-color),70%)', 23 | 'primary-800': 'rgb(var(--primary-color),80%)', 24 | 'primary-900': 'rgb(var(--primary-color),90%)', 25 | }, 26 | transitionProperty: { 27 | height: 'height', 28 | }, 29 | }, 30 | boxShadow: { 31 | rl: '0 1px 5px 0 rgb(57 66 60 / 20%)', 32 | sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)', 33 | DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', 34 | md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', 35 | lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)', 36 | xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)', 37 | '2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)', 38 | inner: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)', 39 | none: 'none', 40 | }, 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Header/ContentTypeTab.tsx: -------------------------------------------------------------------------------- 1 | import { projectContentType, projectContentTypeMap } from '@/constants'; 2 | import { Analysis, InternalData, Setting, Workbench } from '@icon-park/react'; 3 | import { useModel } from '@umijs/max'; 4 | import { Space, Tabs, TabsProps } from 'antd'; 5 | 6 | const ContentTypeTab = () => { 7 | const { data: projectData, filterData, setFilterData } = useModel('ProjectDetail.model'); 8 | const onTabChange = (key: string) => setFilterData({ contentType: key as projectContentType }); 9 | 10 | const items: TabsProps['items'] = [ 11 | { 12 | key: projectContentTypeMap.TASK, 13 | label: ( 14 | 15 | 16 | 任务 17 | 18 | ), 19 | }, 20 | { 21 | key: projectContentTypeMap.TEST, 22 | label: ( 23 | 24 | 25 | 测试用例 26 | 27 | ), 28 | }, 29 | { 30 | key: projectContentTypeMap.STATISTIC, 31 | label: ( 32 | 33 | 34 | 统计 35 | 36 | ), 37 | }, 38 | ]; 39 | if (projectData.project?.canEdit) { 40 | items.push({ 41 | key: projectContentTypeMap.SETTING, 42 | label: ( 43 | 44 | 45 | 设置 46 | 47 | ), 48 | }); 49 | } 50 | return ( 51 | 59 | ); 60 | }; 61 | 62 | export default ContentTypeTab; 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "author": "umlink ", 4 | "licence": "GPL", 5 | "scripts": { 6 | "build": "max build", 7 | "dev": "max dev", 8 | "format": "prettier --cache --write .", 9 | "genapi": "ts-node openapi.config.ts", 10 | "postinstall": "max setup", 11 | "prepare": "husky install", 12 | "build:publish": "max build && scp -r ./dist/* root@121.40.42.56:/usr/share/nginx/html/", 13 | "publish": "scp -r ./dist/* root@121.40.42.56:/usr/share/nginx/html/", 14 | "setup": "max setup", 15 | "start": "npm run dev" 16 | }, 17 | "dependencies": { 18 | "@ant-design/plots": "^2.1.15", 19 | "@dnd-kit/core": "^6.0.8", 20 | "@dnd-kit/modifiers": "^6.0.1", 21 | "@dnd-kit/sortable": "^7.0.2", 22 | "@dnd-kit/utilities": "^3.2.1", 23 | "@icon-park/react": "^1.4.2", 24 | "@umijs/max": "^4.1.6", 25 | "@umlink/rc-gantt": "^0.0.1", 26 | "ahooks": "^3.7.11", 27 | "antd": "^5.23.0", 28 | "copy-to-clipboard": "^3.3.3", 29 | "mitt": "^3.0.1", 30 | "react-beautiful-dnd": "^13.1.1", 31 | "react-quill": "^2.0.0", 32 | "simple-mind-map": "^0.9.11", 33 | "tributejs": "^5.1.3", 34 | "ts-md5": "^1.3.1" 35 | }, 36 | "devDependencies": { 37 | "@ant-design/cssinjs": "1.21.0", 38 | "@tailwindcss/aspect-ratio": "^0.4.2", 39 | "@tailwindcss/line-clamp": "^0.4.4", 40 | "@tailwindcss/typography": "^0.5.12", 41 | "@types/react": "^18.2.73", 42 | "@types/react-beautiful-dnd": "^13.1.8", 43 | "@types/react-dom": "^18.2.23", 44 | "@umijs/openapi": "^1.11.1", 45 | "compression-webpack-plugin": "^11.1.0", 46 | "husky": "^8", 47 | "lint-staged": "^13", 48 | "prettier": "^3.2.5", 49 | "prettier-plugin-organize-imports": "^3.2.2", 50 | "prettier-plugin-packagejson": "^2.4.3", 51 | "prettier-plugin-tailwindcss": "^0.5.7", 52 | "tailwindcss": "^3", 53 | "typescript": "^4" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/pages/components/NewTaskModal/comppnents/TaskGroup.tsx: -------------------------------------------------------------------------------- 1 | import TaskGroupSelect from '@/pages/components/TaskGroupSelect'; 2 | import { CloseOne, Down, Help } from '@icon-park/react'; 3 | import { useModel } from '@umijs/max'; 4 | import { Space, Tooltip } from 'antd'; 5 | 6 | export default () => { 7 | const { data, selectData, setSelectData, setParams } = useModel('newTask'); 8 | return ( 9 | { 12 | setSelectData({ taskGroup }); 13 | setParams({ groupId: taskGroup.id }); 14 | }} 15 | > 16 | 17 | 18 | 19 | {selectData.taskGroup?.groupName || 设置任务分组} 20 | 21 | 22 | 28 | {!!selectData.taskGroup && ( 29 | { 31 | e.stopPropagation(); 32 | setSelectData({ taskGroup: undefined }); 33 | setParams({ groupId: undefined }); 34 | }} 35 | className={'hidden text-zinc-300 hover:text-zinc-400 group-hover:inline-flex'} 36 | theme="filled" 37 | size="16" 38 | /> 39 | )} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/layouts/LeftMenu/components/TopMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Application, Home, Plan, Schedule } from '@icon-park/react'; 2 | import { history } from '@umijs/max'; 3 | import { useEffect, useState } from 'react'; 4 | 5 | const menuList = [ 6 | { 7 | name: '首页', 8 | path: '/', 9 | Icon: Home, 10 | }, 11 | { 12 | name: '项目', 13 | path: '/project', 14 | Icon: Application, 15 | }, 16 | { 17 | name: '工时', 18 | path: '/labor-hour', 19 | Icon: Plan, 20 | }, 21 | { 22 | name: '日程', 23 | path: '/schedule', 24 | Icon: Schedule, 25 | }, 26 | ]; 27 | 28 | export default () => { 29 | const [pathReg, setPathReg] = useState(); 30 | const upPathReg = (str: string) => { 31 | setPathReg(new RegExp(str === '/' ? '/$' : `^${str}`, 'gi')); 32 | }; 33 | 34 | const checkSelected = (path: string): boolean => { 35 | return !!pathReg?.test(path); 36 | }; 37 | useEffect(() => { 38 | upPathReg(history.location.pathname); 39 | history.listen((update) => { 40 | upPathReg(update.location.pathname); 41 | }); 42 | }, []); 43 | 44 | return ( 45 |
46 | {menuList.map(({ path, name, Icon }, index) => { 47 | return ( 48 |
history.push(path)} 55 | > 56 | 57 | 58 | 59 | 60 | {name} 61 | 62 |
63 | ); 64 | })} 65 |
66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /src/api/modules/TaskOperationLog.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** 创建任务类型 POST /task/createTaskOperationLog */ 6 | export async function createTaskOperationLog( 7 | body: API.CreateTaskOperationLogReq, 8 | options?: { [key: string]: any }, 9 | ) { 10 | return request<{ code: number; message: string; data?: { id?: number }; success: boolean }>( 11 | '/task/createTaskOperationLog', 12 | { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | data: body, 18 | ...(options || {}), 19 | }, 20 | ); 21 | } 22 | 23 | /** 删除任务操作日志 POST /task/delTaskOperationLog */ 24 | export async function delTaskOperationLog( 25 | body: API.DelTaskOperationLogReq, 26 | options?: { [key: string]: any }, 27 | ) { 28 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 29 | '/task/delTaskOperationLog', 30 | { 31 | method: 'POST', 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | }, 35 | data: body, 36 | ...(options || {}), 37 | }, 38 | ); 39 | } 40 | 41 | /** 获取任务操作日志 GET /task/getTaskOperationLogList */ 42 | export async function getTaskOperationLogList( 43 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 44 | params: API.getTaskOperationLogListParams, 45 | options?: { [key: string]: any }, 46 | ) { 47 | return request<{ 48 | code: number; 49 | message: string; 50 | data?: { 51 | list?: API.TaskOperationLogItem[]; 52 | pageNo?: number; 53 | pageSize?: number; 54 | total?: number; 55 | }; 56 | success: boolean; 57 | }>('/task/getTaskOperationLogList', { 58 | method: 'GET', 59 | params: { 60 | // pageNo has a default value: 1 61 | pageNo: '1', 62 | // pageSize has a default value: 10 63 | pageSize: '10', 64 | ...params, 65 | }, 66 | ...(options || {}), 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /src/pages/LaborHour/components/LaborHourItem.tsx: -------------------------------------------------------------------------------- 1 | import { UserLaborItem } from '@/pages/LaborHour/model'; 2 | import { useModel } from '@umijs/max'; 3 | import ContentCard from './ContentCard'; 4 | import Timeline from './TimeLine'; 5 | import UserHeader from './UserHeader'; 6 | 7 | const LaborHourItem = (props: { item: UserLaborItem; preIndex: number }) => { 8 | const { laborHourMap, userInfo } = props.item; 9 | const { data: laborHourData } = useModel('LaborHour.model'); 10 | const itemClass = 'group text-center border-r p-2 border-r-zinc-200 w-[1/8]'; 11 | 12 | const openUserIds = laborHourData.openUserIds; 13 | 14 | return ( 15 |
20 | 21 |
22 | {laborHourData.dayList.map((date: string, index) => { 23 | return ( 24 |
28 | 34 | {openUserIds.includes(userInfo.id) && ( 35 |
36 | {laborHourMap.get(date)?.taskLaborHourList.map((item: API.WorkLaborHourDetailItem, index) => { 37 | return ; 38 | })} 39 |
40 | )} 41 |
42 | ); 43 | })} 44 |
45 |
46 | ); 47 | }; 48 | export default LaborHourItem; 49 | -------------------------------------------------------------------------------- /src/pages/components/TaskDetailModal/components/TaskLogs/components/TextLog.tsx: -------------------------------------------------------------------------------- 1 | import RickEditor from '@/components/RickEditor'; 2 | import { Avatar } from 'antd'; 3 | import dayjs from 'dayjs'; 4 | export default (props: { log: API.TaskOperationLogItem }) => { 5 | const { username, avatar, name, type, content, createdAt } = props.log; 6 | 7 | const genContent = (): JSX.Element => { 8 | let retContent: JSX.Element; 9 | switch (type) { 10 | case 'DYNAMIC_TASK_DESCRIPTION': 11 | retContent = ( 12 | <> 13 | {!!content ? ( 14 |
15 | 详情 16 |
17 | 18 |
19 |
20 | ) : ( 21 | 22 | )} 23 | 24 | ); 25 | break; 26 | default: 27 | retContent = ; 28 | } 29 | return retContent; 30 | }; 31 | 32 | return ( 33 |
34 | 35 |
36 |
37 | 38 | {username} 39 | {name} 40 | 41 | 42 | {dayjs(createdAt).format('YYYY-MM-DD HH:mm')} 43 | 44 |
45 |
{genContent()}
46 |
47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/pages/LaborHour/components/ContentCard.tsx: -------------------------------------------------------------------------------- 1 | import { CloseOne } from '@icon-park/react'; 2 | import { useModel } from '@umijs/max'; 3 | import { App, Tooltip } from 'antd'; 4 | const ContentCard = (props: { item: API.WorkLaborHourDetailItem; preIndex: number; subIndex: number }) => { 5 | const { modal } = App.useApp(); 6 | const item = props.item; 7 | const { onShow: onShowTaskDetail } = useModel('taskDetail'); 8 | const { delTaskLaborHourById } = useModel('LaborHour.model'); 9 | 10 | const delItem = () => { 11 | modal.confirm({ 12 | title: '温馨提示', 13 | content: '确认删除当前工时', 14 | onOk: () => { 15 | delTaskLaborHourById({ 16 | id: item.id, 17 | taskId: item.taskId, 18 | date: item.date, 19 | preIndex: props.preIndex, 20 | subIndex: props.subIndex, 21 | }); 22 | }, 23 | }); 24 | }; 25 | return ( 26 |
29 |

onShowTaskDetail(item.taskId)} 32 | > 33 | {item.taskName} 34 |

35 | 36 |

工作内容:{item.description}

37 |
38 | 39 |

40 | 实际工时 41 | {item.hour}h 42 |

43 | 51 |
52 | ); 53 | }; 54 | export default ContentCard; 55 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Test/components/CaseEditor.tsx: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import MindMap from '@/components/MindMap'; 3 | import { Close, FullScreen, OffScreen } from '@icon-park/react'; 4 | import { useFullscreen } from 'ahooks'; 5 | import { Input } from 'antd'; 6 | import { useRef } from 'react'; 7 | 8 | type PropsType = { 9 | projectId: string; 10 | caseDetail?: API.GetTestCaseRes; 11 | onClose: () => void; 12 | }; 13 | const CaseEditor = (props: PropsType) => { 14 | const ref = useRef(null); 15 | const [isFullscreen, { toggleFullscreen }] = useFullscreen(ref); 16 | const onChange = (v: string) => { 17 | Api.TestCase.updateCase({ 18 | id: props.caseDetail!.id!, 19 | projectId: props.projectId, 20 | value: v, 21 | }); 22 | }; 23 | 24 | if (!props.caseDetail) return <>; 25 | return ( 26 |
27 |
28 | 35 | 完善中。。。 36 |
37 | 41 | {isFullscreen ? : } 42 | 43 | 44 | 45 | 46 |
47 |
48 | 49 |
50 | ); 51 | }; 52 | 53 | export default CaseEditor; 54 | -------------------------------------------------------------------------------- /src/pages/LaborHour/components/TimeLine.tsx: -------------------------------------------------------------------------------- 1 | import { EVENTS } from '@/constants'; 2 | import EventBus from '@/utils/event-bus'; 3 | import { PlusCross } from '@icon-park/react'; 4 | import { useModel } from '@umijs/max'; 5 | import { Progress, Tooltip } from 'antd'; 6 | 7 | type TimeLineDataType = { 8 | hour: number; 9 | date: string; 10 | preIndex: number; 11 | useId: string; 12 | }; 13 | 14 | const TimeLine = ({ hour, date, preIndex, useId }: TimeLineDataType) => { 15 | const { initialState: userInfo } = useModel('@@initialState'); 16 | const isSelf = userInfo?.id === useId; 17 | const newLaborHour = () => { 18 | if (!isSelf) return; 19 | EventBus.emit(EVENTS.SHOW_NEW_TASK_LABOR_HOUR_MODAL, { 20 | date, 21 | useId, 22 | index: preIndex, 23 | }); 24 | }; 25 | 26 | return ( 27 |
36 | {hour > 0 && ( 37 | 38 | 0 ? 'text-zinc-600' : 'text-zinc-300'}`}>{hour}小时 39 | = 8 ? 'success' : 'exception'} 41 | className={`[&.ant-progress-line]:!mb-0 42 | [&.ant-progress-status-exception_.ant-progress-bg]:!bg-[#F9CC45] 43 | [&.ant-progress-status-success_.ant-progress-bg]:!bg-primary`} 44 | success={{ strokeColor: '#dedede' }} 45 | size="small" 46 | percent={hour >= 8 ? 100 : (hour / 8) * 100} 47 | showInfo={false} 48 | /> 49 | 50 | )} 51 | {isSelf && hour <= 0 && ( 52 | 53 | 54 | 55 | )} 56 |
57 | ); 58 | }; 59 | export default TimeLine; 60 | -------------------------------------------------------------------------------- /src/components/RickEditor.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, useImperativeHandle, useRef } from 'react'; 2 | import ReactQuill from 'react-quill'; 3 | import 'react-quill/dist/quill.snow.css'; 4 | 5 | type PropsType = { 6 | content?: string; 7 | readOnly?: boolean; 8 | placeholder?: string; 9 | onChange?: (v: string) => void; 10 | }; 11 | 12 | const RickEditor = forwardRef((props: PropsType, ref) => { 13 | const quill: any = useRef(null); 14 | useImperativeHandle(ref, () => ({ 15 | resetContent: (val: string) => { 16 | // quill.current.setEditorContents(Quill, val); 17 | quill.current.value = val; 18 | }, 19 | })); 20 | return ( 21 | 60 | ); 61 | }); 62 | 63 | export default RickEditor; 64 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Statistics/components/TaskTypePie.tsx: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { ProjectStatisticsParams } from '@/pages/ProjectDetail/components/Content/Statistics'; 3 | import { Pie } from '@ant-design/plots'; 4 | import { useEffect, useState } from 'react'; 5 | 6 | type PropsType = { 7 | params: ProjectStatisticsParams; 8 | }; 9 | 10 | type DataItem = { 11 | type: string; 12 | value: number; 13 | }; 14 | 15 | const TaskTypePie = ({ params }: PropsType) => { 16 | const [data, setData] = useState([]); 17 | const config = { 18 | data, 19 | height: 320, 20 | angleField: 'value', 21 | colorField: 'type', 22 | paddingRight: 80, 23 | innerRadius: 0.6, 24 | label: { 25 | text: 'value', 26 | style: { 27 | fontWeight: 'bold', 28 | }, 29 | }, 30 | tooltip: { 31 | title: 'type', 32 | items: [{ channel: 'y' }], 33 | }, 34 | legend: { 35 | color: { 36 | position: 'top', 37 | }, 38 | }, 39 | style: { 40 | stroke: '#fff', 41 | inset: 1, 42 | radius: 4, 43 | }, 44 | scale: { 45 | color: { 46 | offset: (t: number) => t * 0.8 + 0.1, 47 | }, 48 | }, 49 | annotations: [ 50 | { 51 | type: 'text', 52 | style: { 53 | text: '任务类型\n数量分布', 54 | x: '50%', 55 | y: '50%', 56 | textAlign: 'center', 57 | fontSize: 14, 58 | fontStyle: 'bold', 59 | }, 60 | }, 61 | ], 62 | }; 63 | const getData = () => { 64 | Api.ProjectOverview.getTaskTypeStat(params).then((res) => { 65 | if (res.success && res.data) { 66 | const list: DataItem[] = res.data.map((item) => { 67 | return { type: item.typeName, value: item.taskCount }; 68 | }); 69 | setData(list); 70 | } 71 | }); 72 | }; 73 | useEffect(getData, [params]); 74 | return ( 75 |
76 |

任务类型分布

77 | 78 |
79 | ); 80 | }; 81 | 82 | export default TaskTypePie; 83 | -------------------------------------------------------------------------------- /src/api/modules/TaskLaborHour.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** 添加任务工时 POST /task/addLaborHour */ 6 | export async function addLaborHour( 7 | body: API.AddTaskLaborHourReq, 8 | options?: { [key: string]: any }, 9 | ) { 10 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 11 | '/task/addLaborHour', 12 | { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | data: body, 18 | ...(options || {}), 19 | }, 20 | ); 21 | } 22 | 23 | /** 删除任务工时记录 POST /task/delLaborHour */ 24 | export async function delLaborHour( 25 | body: API.DelTaskLaborHourReq, 26 | options?: { [key: string]: any }, 27 | ) { 28 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 29 | '/task/delLaborHour', 30 | { 31 | method: 'POST', 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | }, 35 | data: body, 36 | ...(options || {}), 37 | }, 38 | ); 39 | } 40 | 41 | /** 获取实际工时记录列表 POST /task/getLaborHourList */ 42 | export async function getLaborHourList( 43 | body: API.GetTaskLaborHourReq, 44 | options?: { [key: string]: any }, 45 | ) { 46 | return request<{ 47 | code: number; 48 | message: string; 49 | data?: { list?: API.TaskLaborHourItem[]; total?: number; pageNo?: number; pageSize?: number }; 50 | success: boolean; 51 | }>('/task/getLaborHourList', { 52 | method: 'POST', 53 | headers: { 54 | 'Content-Type': 'application/json', 55 | }, 56 | data: body, 57 | ...(options || {}), 58 | }); 59 | } 60 | 61 | /** 更新任务工时记录 POST /task/updateLaborHour */ 62 | export async function updateLaborHour( 63 | body: API.UpdateTaskLaborHourReq, 64 | options?: { [key: string]: any }, 65 | ) { 66 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 67 | '/task/updateLaborHour', 68 | { 69 | method: 'POST', 70 | headers: { 71 | 'Content-Type': 'application/json', 72 | }, 73 | data: body, 74 | ...(options || {}), 75 | }, 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import projectAvatar from '@/assets/images/default/project-avatar.jpeg'; 2 | import ProjectSelect from '@/pages/components/ProjectSelect'; 3 | import InviteModal from '@/pages/ProjectDetail/components/InviteModal'; 4 | import { DownOne, Left } from '@icon-park/react'; 5 | import { history, useModel } from '@umijs/max'; 6 | import { Button, Image } from 'antd'; 7 | import ContentTypeTab from './ContentTypeTab'; 8 | 9 | const ProjectDetailHeader = () => { 10 | const { data, setData } = useModel('ProjectDetail.model'); 11 | 12 | return ( 13 |
14 |
15 | history.go(-1)} 18 | > 19 | 20 | 21 | 22 | 30 | setData({ projectId: val })}> 31 | 32 | {data.project?.name} 33 | 34 | 35 | 36 | 37 |
38 | 39 |
40 |
41 |
42 | 43 | 46 | 47 |
48 |
49 | ); 50 | }; 51 | 52 | export default ProjectDetailHeader; 53 | -------------------------------------------------------------------------------- /src/layouts/LeftMenu/components/UserInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import { SYSTEM_ROLE } from '@/constants'; 2 | import { Power, Qiyehao, SettingTwo } from '@icon-park/react'; 3 | import { history, useModel } from '@umijs/max'; 4 | import { Avatar, Divider, Popover } from 'antd'; 5 | import jsCookie from 'js-cookie'; 6 | import { useState } from 'react'; 7 | 8 | export default () => { 9 | const { initialState: userInfo } = useModel('@@initialState'); 10 | const [open, setOpen] = useState(false); 11 | 12 | const onLogout = () => { 13 | setOpen(false); 14 | jsCookie.remove('__wkt_tk__'); 15 | location.replace('/login'); 16 | }; 17 | const openSettingsPage = () => { 18 | setOpen(false); 19 | history.push('/settings'); 20 | }; 21 | 22 | const settings = () => { 23 | if (userInfo?.role === SYSTEM_ROLE.USER) return null; 24 | return ( 25 |
31 | 32 | 系统设置 33 |
34 | ); 35 | }; 36 | const content = ( 37 |
38 |
39 |

{userInfo?.username}

40 |

41 | 42 | 宇宙最大科技网络有限公司 43 |

44 |
45 | {settings()} 46 | 47 |
51 | 52 | 退出 53 |
54 |
55 | ); 56 | 57 | return ( 58 | 59 | 60 | 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Setting/components/TaskStatus/useService.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useModel } from '@umijs/max'; 3 | import { useMemoizedFn, useSetState } from 'ahooks'; 4 | import { App } from 'antd'; 5 | import { useEffect, useState } from 'react'; 6 | 7 | type SettingDataType = { 8 | pageSize: number; 9 | pageNo: number; 10 | finished: boolean; 11 | }; 12 | const useService = () => { 13 | const { notification } = App.useApp(); 14 | const [statusList, setStatusList] = useState([]); 15 | const [data, setData] = useSetState({ 16 | pageSize: 20, 17 | pageNo: 1, 18 | finished: false, 19 | }); 20 | 21 | const { data: projectData } = useModel('ProjectDetail.model'); 22 | 23 | const getTaskStatusList = () => { 24 | Api.TaskStatus.getTaskStatusList({ 25 | projectId: projectData.projectId, 26 | pageSize: 100, 27 | }).then((res) => { 28 | if (res.data) { 29 | setStatusList(res.data.list || []); 30 | } 31 | }); 32 | }; 33 | 34 | const updateStatusSort = useMemoizedFn(() => { 35 | const list: API.SortMapItem[] = statusList.map((item, index) => { 36 | return { 37 | id: item.id, 38 | sort: index + 1, 39 | }; 40 | }); 41 | Api.TaskStatus.updateTaskStatusSort({ 42 | projectId: projectData.projectId, 43 | sortMapList: list, 44 | }).then((res) => { 45 | if (res.success) { 46 | } 47 | }); 48 | }); 49 | 50 | const delStatus = (id: string) => { 51 | Api.TaskStatus.delTaskStatus({ projectId: projectData.projectId, id }).then((res) => { 52 | if (res.success) { 53 | getTaskStatusList(); 54 | } else { 55 | notification.error({ 56 | message: '温馨提示', 57 | description: res.message, 58 | }); 59 | } 60 | }); 61 | }; 62 | 63 | useEffect(() => { 64 | if (projectData.projectId) getTaskStatusList(); 65 | }, [projectData.projectId]); 66 | 67 | return { 68 | data, 69 | setData, 70 | delStatus, 71 | statusList, 72 | setStatusList, 73 | updateStatusSort, 74 | getTaskStatusList, 75 | }; 76 | }; 77 | 78 | export default useService; 79 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Statistics/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useRequest } from '@umijs/max'; 3 | import { Col, Row, Spin, Statistic } from 'antd'; 4 | import React, { useEffect, useState } from 'react'; 5 | import { ProjectStatisticsParams } from '../index'; 6 | 7 | type PropsType = { 8 | params: ProjectStatisticsParams; 9 | }; 10 | 11 | const StatisticsHeader = ({ params }: PropsType) => { 12 | const [data, setData] = useState({ 13 | taskCount: 0, 14 | userCount: 0, 15 | groupCount: 0, 16 | laborHour: 0, 17 | planHour: 0, 18 | overTimeDoneCount: 0, 19 | overTimeNoDoneCount: 0, 20 | }); 21 | 22 | const { run: getProjectGroupList, loading } = useRequest( 23 | () => 24 | Api.ProjectOverview.getStatistics(params).then((res) => { 25 | if (res.success && res.data) { 26 | setData(res.data as API.GetProjectStatisticsRes); 27 | } 28 | }), 29 | { 30 | manual: true, 31 | }, 32 | ); 33 | 34 | const renderContent = (content: React.ReactNode) => { 35 | if (loading || !data.userCount) return ; 36 | return content; 37 | }; 38 | 39 | useEffect(() => { 40 | getProjectGroupList(); 41 | }, [params]); 42 | 43 | return ( 44 |
45 | 46 | {renderContent()} 47 | {renderContent()} 48 | {renderContent()} 49 | {renderContent()} 50 | {renderContent()} 51 | {renderContent()} 52 | {renderContent()} 53 | 54 |
55 | ); 56 | }; 57 | 58 | export default StatisticsHeader; 59 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Task/components/Table/useService.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useModel } from '@umijs/max'; 3 | 4 | const useTableService = () => { 5 | const { data, updateTaskDetail } = useModel('ProjectDetail.model'); 6 | 7 | type TempObjType = { [key: string]: string }; 8 | type TempFilterType = { text: any; value: any }; 9 | 10 | let tempStatusObj: TempObjType = {}; 11 | let tempGroupObj: TempObjType = {}; 12 | let tempTypeObj: TempObjType = {}; 13 | let tempPriorityObj: TempObjType = {}; 14 | let tempUserObj: TempObjType = {}; 15 | 16 | // 供筛选 17 | data.tableData.list?.forEach((item: API.TaskDetailItem) => { 18 | if (item.statusId) tempStatusObj[item.statusName!] = item.statusId; 19 | if (item.groupId) tempGroupObj[item.groupName!] = item.groupId; 20 | if (item.typeId) tempTypeObj[item.typeName!] = item.typeId; 21 | if (item.priority) tempPriorityObj[item.priority] = item.priority; 22 | if (item.handlerId) tempUserObj[item.handlerName!] = item.handlerId; 23 | }); 24 | 25 | const statusFilters: TempFilterType[] = Object.keys(tempStatusObj).map((key) => ({ 26 | text: key, 27 | value: tempStatusObj[key], 28 | })); 29 | const userFilters: TempFilterType[] = Object.keys(tempUserObj).map((key) => ({ 30 | text: key, 31 | value: tempUserObj[key], 32 | })); 33 | const groupFilters: TempFilterType[] = Object.keys(tempGroupObj).map((key) => ({ 34 | text: key, 35 | value: tempGroupObj[key], 36 | })); 37 | const typeFilters: TempFilterType[] = Object.keys(tempTypeObj).map((key) => ({ 38 | text: key, 39 | value: tempTypeObj[key], 40 | })); 41 | const priorityFilters: TempFilterType[] = Object.keys(tempPriorityObj).map((key) => ({ 42 | text: key, 43 | value: tempPriorityObj[key], 44 | })); 45 | 46 | const updateTaskInfo = (params: Partial) => { 47 | params.projectId = data.projectId; 48 | Api.Task.updateTask(params as API.UpdateTaskReq).then(() => { 49 | updateTaskDetail(params.id!); 50 | }); 51 | }; 52 | 53 | return { 54 | updateTaskInfo, 55 | statusFilters, 56 | userFilters, 57 | groupFilters, 58 | typeFilters, 59 | priorityFilters, 60 | }; 61 | }; 62 | 63 | export default useTableService; 64 | -------------------------------------------------------------------------------- /src/pages/Project/components/Container/components/GroupSelect/useService.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useRequest } from '@umijs/max'; 3 | import { useDebounceFn, useSetState, useUpdateEffect } from 'ahooks'; 4 | import { useState } from 'react'; 5 | 6 | const useProjectList = () => { 7 | const [data, setData] = useSetState<{ 8 | groupList: API.ProjectGroupItem[]; 9 | projectId?: string; 10 | keywords: string | undefined; 11 | finished: boolean; 12 | pageNo: number; 13 | pageSize: number; 14 | }>({ 15 | groupList: [], 16 | keywords: '', 17 | finished: false, 18 | pageNo: 1, 19 | pageSize: 10, 20 | }); 21 | 22 | const [open, setOpen] = useState(false); 23 | 24 | const { loading, run: getGroupList } = useRequest( 25 | () => 26 | Api.ProjectGroup.getProjectGroupList({ 27 | keywords: data.keywords, 28 | pageNo: data.pageNo, 29 | pageSize: data.pageSize, 30 | }), 31 | { 32 | onSuccess(res) { 33 | const dataList = res?.list || []; 34 | const list = data.pageNo === 1 ? dataList : [...data.groupList, ...dataList]; 35 | setData({ 36 | groupList: list, 37 | pageNo: ++data.pageNo, 38 | finished: list.length === res?.total, 39 | }); 40 | }, 41 | manual: true, 42 | }, 43 | ); 44 | 45 | const getMoreGroup = () => { 46 | if (data.finished) return; 47 | getGroupList(); 48 | }; 49 | 50 | const resetData = () => { 51 | setData({ 52 | finished: false, 53 | pageNo: 1, 54 | groupList: [], 55 | keywords: undefined, 56 | }); 57 | }; 58 | 59 | const { run: keywordsChange } = useDebounceFn( 60 | (e) => { 61 | resetData(); 62 | setData({ keywords: e.target.value }); 63 | }, 64 | { wait: 200 }, 65 | ); 66 | 67 | useUpdateEffect(() => { 68 | getGroupList(); 69 | }, [data.keywords]); 70 | 71 | useUpdateEffect(() => { 72 | if (open) { 73 | getGroupList(); 74 | } else { 75 | resetData(); 76 | } 77 | }, [open]); 78 | 79 | return { 80 | data, 81 | setData, 82 | getMoreGroup, 83 | loading, 84 | keywordsChange, 85 | open, 86 | setOpen, 87 | }; 88 | }; 89 | 90 | export default useProjectList; 91 | -------------------------------------------------------------------------------- /src/api/modules/TaskType.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** 创建任务类型 POST /task/createTaskType */ 6 | export async function createTaskType( 7 | body: API.CreateTaskTypeReq, 8 | options?: { [key: string]: any }, 9 | ) { 10 | return request<{ code: number; message: string; data?: { id?: string }; success: boolean }>( 11 | '/task/createTaskType', 12 | { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | data: body, 18 | ...(options || {}), 19 | }, 20 | ); 21 | } 22 | 23 | /** 删除任务类型 POST /task/delTaskType */ 24 | export async function delTaskType(body: API.DelTaskTypeReq, options?: { [key: string]: any }) { 25 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 26 | '/task/delTaskType', 27 | { 28 | method: 'POST', 29 | headers: { 30 | 'Content-Type': 'application/json', 31 | }, 32 | data: body, 33 | ...(options || {}), 34 | }, 35 | ); 36 | } 37 | 38 | /** 获取项目类型列表 GET /task/getTaskTypeList */ 39 | export async function getTaskTypeList( 40 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 41 | params: API.getTaskTypeListParams, 42 | options?: { [key: string]: any }, 43 | ) { 44 | return request<{ 45 | code: number; 46 | message: string; 47 | data?: { list?: API.TaskTypeItem[]; pageNo?: number; pageSize?: number; total?: number }; 48 | success: boolean; 49 | }>('/task/getTaskTypeList', { 50 | method: 'GET', 51 | params: { 52 | // pageNo has a default value: 1 53 | pageNo: '1', 54 | // pageSize has a default value: 10 55 | pageSize: '10', 56 | ...params, 57 | }, 58 | ...(options || {}), 59 | }); 60 | } 61 | 62 | /** 更新任务类型 POST /task/updateTaskType */ 63 | export async function updateTaskType( 64 | body: API.UpdateTaskTypeReq, 65 | options?: { [key: string]: any }, 66 | ) { 67 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 68 | '/task/updateTaskType', 69 | { 70 | method: 'POST', 71 | headers: { 72 | 'Content-Type': 'application/json', 73 | }, 74 | data: body, 75 | ...(options || {}), 76 | }, 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /src/api/modules/TaskGroup.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** 创建任务分组 POST /task/createTaskGroup */ 6 | export async function createTaskGroup( 7 | body: API.CreateTaskGroupReq, 8 | options?: { [key: string]: any }, 9 | ) { 10 | return request<{ code: number; message: string; data?: { id?: string }; success: boolean }>( 11 | '/task/createTaskGroup', 12 | { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | data: body, 18 | ...(options || {}), 19 | }, 20 | ); 21 | } 22 | 23 | /** 删除任务分组 POST /task/delTaskGroup */ 24 | export async function delTaskGroup(body: API.DelTaskGroupReq, options?: { [key: string]: any }) { 25 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 26 | '/task/delTaskGroup', 27 | { 28 | method: 'POST', 29 | headers: { 30 | 'Content-Type': 'application/json', 31 | }, 32 | data: body, 33 | ...(options || {}), 34 | }, 35 | ); 36 | } 37 | 38 | /** 获取项目分组列表 GET /task/getTaskGroupList */ 39 | export async function getTaskGroupList( 40 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 41 | params: API.getTaskGroupListParams, 42 | options?: { [key: string]: any }, 43 | ) { 44 | return request<{ 45 | code: number; 46 | message: string; 47 | data?: { list?: API.TaskGroupItem[]; pageNo?: number; pageSize?: number; total?: number }; 48 | success: boolean; 49 | }>('/task/getTaskGroupList', { 50 | method: 'GET', 51 | params: { 52 | // pageNo has a default value: 1 53 | pageNo: '1', 54 | // pageSize has a default value: 10 55 | pageSize: '10', 56 | ...params, 57 | }, 58 | ...(options || {}), 59 | }); 60 | } 61 | 62 | /** 更新任务分组 POST /task/updateTaskGroup */ 63 | export async function updateTaskGroup( 64 | body: API.UpdateTaskGroupReq, 65 | options?: { [key: string]: any }, 66 | ) { 67 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 68 | '/task/updateTaskGroup', 69 | { 70 | method: 'POST', 71 | headers: { 72 | 'Content-Type': 'application/json', 73 | }, 74 | data: body, 75 | ...(options || {}), 76 | }, 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /src/layouts/LeftMenu/components/Message/MsgContent.tsx: -------------------------------------------------------------------------------- 1 | import ListEmptyImg from '@/assets/images/default/list-empty.png'; 2 | import { EVENTS } from '@/constants'; 3 | import EventBus from '@/utils/event-bus'; 4 | import { CloseSmall } from '@icon-park/react'; 5 | import { useModel } from '@umijs/max'; 6 | import { Avatar, Image } from 'antd'; 7 | 8 | const MsgContent = ({ onClose }: { onClose: () => void }) => { 9 | const { state } = useModel('message'); 10 | 11 | const openTaskDetail = () => EventBus.emit(EVENTS.OPEN_TASK_DETAIL, state.msgDetail?.taskId); 12 | 13 | const keywordsClass = `[&_.last-text]:font-bold [&_.last-text]:text-zinc-800 [&_.pre-text]:font-bold [&_.pre-text]:text-zinc-800`; 14 | 15 | const Content = () => { 16 | if (!state.msgDetail) { 17 | return ( 18 |
19 | 20 |
21 | ); 22 | } 23 | return ( 24 | <> 25 |

{state.msgDetail?.title}

26 |
27 | 28 | {state.msgDetail?.senderName} 29 | {state.msgDetail?.createdAt} 30 |
31 | {!!state.msgDetail?.taskId && ( 32 |
33 | 任务: 34 | 35 | {state.msgDetail.taskName} 36 | 37 |
38 | )} 39 |

43 | 44 | ); 45 | }; 46 | 47 | return ( 48 |

49 | 53 | 54 | 55 | 56 |
57 | ); 58 | }; 59 | 60 | export default MsgContent; 61 | -------------------------------------------------------------------------------- /src/pages/components/TaskGroupSelect/useService.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useRequest } from '@umijs/max'; 3 | import { useDebounceFn, useSetState, useUpdateEffect } from 'ahooks'; 4 | import { useState } from 'react'; 5 | 6 | const useProjectList = (projectId: string) => { 7 | const [data, setData] = useSetState<{ 8 | groupList: API.TaskGroupItem[]; 9 | projectId?: string; 10 | keywords: string | undefined; 11 | finished: boolean; 12 | pageNo: number; 13 | pageSize: number; 14 | }>({ 15 | groupList: [], 16 | projectId, 17 | keywords: '', 18 | finished: false, 19 | pageNo: 1, 20 | pageSize: 10, 21 | }); 22 | 23 | const [open, setOpen] = useState(false); 24 | 25 | const { loading, run: getTaskGroupList } = useRequest( 26 | () => 27 | Api.TaskGroup.getTaskGroupList({ 28 | projectId, 29 | keywords: data.keywords, 30 | pageNo: data.pageNo, 31 | pageSize: data.pageSize, 32 | }), 33 | { 34 | onSuccess(res) { 35 | const dataList = res?.list || []; 36 | const list = data.pageNo === 1 ? dataList : [...data.groupList, ...dataList]; 37 | setData({ 38 | groupList: list, 39 | pageNo: ++data.pageNo, 40 | finished: list.length === res?.total, 41 | }); 42 | }, 43 | manual: true, 44 | }, 45 | ); 46 | 47 | const getMoreGroup = () => { 48 | if (data.finished) return; 49 | getTaskGroupList(); 50 | }; 51 | 52 | const resetData = () => { 53 | setData({ 54 | finished: false, 55 | pageNo: 1, 56 | groupList: [], 57 | keywords: undefined, 58 | }); 59 | }; 60 | 61 | const { run: keywordsChange } = useDebounceFn( 62 | (e) => { 63 | resetData(); 64 | setData({ keywords: e.target.value }); 65 | }, 66 | { wait: 200 }, 67 | ); 68 | 69 | useUpdateEffect(() => { 70 | getTaskGroupList(); 71 | }, [data.keywords]); 72 | 73 | useUpdateEffect(() => { 74 | if (open) { 75 | getTaskGroupList(); 76 | } else { 77 | resetData(); 78 | } 79 | }, [open]); 80 | 81 | return { 82 | data, 83 | setData, 84 | getMoreGroup, 85 | loading, 86 | keywordsChange, 87 | open, 88 | setOpen, 89 | }; 90 | }; 91 | 92 | export default useProjectList; 93 | -------------------------------------------------------------------------------- /src/pages/Message/model.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useModel } from '@@/exports'; 3 | import { useRequest } from '@umijs/max'; 4 | import { useSetState, useUpdateEffect } from 'ahooks'; 5 | 6 | type StateProps = { 7 | msgList: API.MessageItem[]; 8 | pageNo: number; 9 | finished: boolean; 10 | activeTab: string; 11 | openId?: string; 12 | unreadCount: number; 13 | msgDetail?: Partial; 14 | params: { 15 | status?: string; 16 | type?: string; 17 | }; 18 | }; 19 | 20 | const useMessageModel = () => { 21 | const { getUnreadMsgCount } = useModel('global'); 22 | const [state, setState] = useSetState({ 23 | msgList: [], 24 | pageNo: 1, 25 | finished: false, 26 | params: {}, 27 | activeTab: 'all', 28 | unreadCount: 0, 29 | }); 30 | 31 | const { loading, run: getMessageList } = useRequest(() => Api.Message.getMsgList(state.params), { 32 | manual: true, 33 | onSuccess: (res) => { 34 | if (res) { 35 | const _list = res.list || []; 36 | const list = state.pageNo === 1 ? _list : [...state.msgList, ..._list]; 37 | setState({ 38 | msgList: list, 39 | finished: res.total === list.length, 40 | pageNo: state.pageNo + 1, 41 | }); 42 | } 43 | }, 44 | }); 45 | 46 | const getData = () => { 47 | if (state.finished || loading) return; 48 | getMessageList(); 49 | }; 50 | const readMsg = (id: string, index: number) => { 51 | Api.Message.readMsg({ id }).then((res) => { 52 | if (res.success) { 53 | getUnreadMsgCount(); 54 | state.msgList[index].status = 1; 55 | setState({ msgList: [...state.msgList] }); 56 | } 57 | }); 58 | }; 59 | const getMsgDetail = (msg: API.MessageItem, index: number) => { 60 | setState({ openId: msg.id }); 61 | if (!msg.status) { 62 | readMsg(msg.id, index); 63 | } 64 | Api.Message.getMsgDetail({ id: msg.id }).then((res) => { 65 | if (res.success) { 66 | setState({ msgDetail: res.data }); 67 | } 68 | }); 69 | }; 70 | 71 | useUpdateEffect(getData, [state.params]); 72 | 73 | return { 74 | loading, 75 | getData, 76 | state, 77 | readMsg, 78 | setState, 79 | getMsgDetail, 80 | }; 81 | }; 82 | 83 | export default useMessageModel; 84 | -------------------------------------------------------------------------------- /src/api/modules/TaskPriority.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** 添加任务优先级 POST /task/addTaskPriority */ 6 | export async function addTaskPriority( 7 | body: API.AddTaskPriorityReq, 8 | options?: { [key: string]: any }, 9 | ) { 10 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 11 | '/task/addTaskPriority', 12 | { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | data: body, 18 | ...(options || {}), 19 | }, 20 | ); 21 | } 22 | 23 | /** 删除任务优先级(已占用则不可删除) POST /task/delTaskPriority */ 24 | export async function delTaskPriority( 25 | body: API.DelTaskPriorityReq, 26 | options?: { [key: string]: any }, 27 | ) { 28 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 29 | '/task/delTaskPriority', 30 | { 31 | method: 'POST', 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | }, 35 | data: body, 36 | ...(options || {}), 37 | }, 38 | ); 39 | } 40 | 41 | /** 获取项目优先级列表 GET /task/getTaskPriorityList */ 42 | export async function getTaskPriorityList( 43 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 44 | params: API.getTaskPriorityListParams, 45 | options?: { [key: string]: any }, 46 | ) { 47 | return request<{ 48 | code: number; 49 | message: string; 50 | data?: { list?: API.TaskPriorityItem[]; pageNo?: number; pageSize?: number; total?: number }; 51 | success: boolean; 52 | }>('/task/getTaskPriorityList', { 53 | method: 'GET', 54 | params: { 55 | // pageNo has a default value: 1 56 | pageNo: '1', 57 | // pageSize has a default value: 10 58 | pageSize: '10', 59 | ...params, 60 | }, 61 | ...(options || {}), 62 | }); 63 | } 64 | 65 | /** 更新任务优先级 POST /task/updateTaskPriority */ 66 | export async function updateTaskPriority( 67 | body: API.UpdateTaskPriorityReq, 68 | options?: { [key: string]: any }, 69 | ) { 70 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 71 | '/task/updateTaskPriority', 72 | { 73 | method: 'POST', 74 | headers: { 75 | 'Content-Type': 'application/json', 76 | }, 77 | data: body, 78 | ...(options || {}), 79 | }, 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/pages/components/TaskDetailModal/components/TaskDescription.tsx: -------------------------------------------------------------------------------- 1 | import RickEditor from '@/components/RickEditor'; 2 | import { Editor } from '@icon-park/react'; 3 | import { useModel } from '@umijs/max'; 4 | import { Button, Tooltip } from 'antd'; 5 | import { useState } from 'react'; 6 | 7 | const StatusSelect = () => { 8 | const { data, updateTaskInfo } = useModel('taskDetail'); 9 | const [readOnly, setReadOnly] = useState(true); 10 | const [desc, setDesc] = useState(data.task!.description); 11 | 12 | const updateDescription = () => { 13 | updateTaskInfo({ description: desc || 'null' }); 14 | setReadOnly(true); 15 | }; 16 | 17 | const onCancel = () => { 18 | setReadOnly(true); 19 | setDesc(data.task!.description); 20 | }; 21 | 22 | if (!data.task) return null; 23 | 24 | const noDesc = !data.task.description || data.task.description === '


'; 25 | return ( 26 |
27 | {!noDesc || !readOnly ? ( 28 | <> 29 | setDesc(val)} 34 | /> 35 | {readOnly ? ( 36 | 37 | 42 | setReadOnly(false)} theme="outline" size="16" /> 43 | 44 | 45 | ) : ( 46 |
47 | 50 | 53 |
54 | )} 55 | 56 | ) : ( 57 |
setReadOnly(false)}> 58 | 添加任务描述 59 |
60 | )} 61 |
62 | ); 63 | }; 64 | export default StatusSelect; 65 | -------------------------------------------------------------------------------- /src/api/modules/ProjectGroup.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** 创建项目分组 POST /project/createProjectGroup */ 6 | export async function createProjectGroup( 7 | body: API.CreateProjectGroupReq, 8 | options?: { [key: string]: any }, 9 | ) { 10 | return request<{ code: number; message: string; data?: { id?: string }; success: boolean }>( 11 | '/project/createProjectGroup', 12 | { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | data: body, 18 | ...(options || {}), 19 | }, 20 | ); 21 | } 22 | 23 | /** 删除项目分组 POST /project/delProjectGroup */ 24 | export async function delProjectGroup( 25 | body: API.DelProjectGroupReq, 26 | options?: { [key: string]: any }, 27 | ) { 28 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 29 | '/project/delProjectGroup', 30 | { 31 | method: 'POST', 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | }, 35 | data: body, 36 | ...(options || {}), 37 | }, 38 | ); 39 | } 40 | 41 | /** 获取项目分组列表 GET /project/getProjectGroupList */ 42 | export async function getProjectGroupList( 43 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 44 | params: API.getProjectGroupListParams, 45 | options?: { [key: string]: any }, 46 | ) { 47 | return request<{ 48 | code: number; 49 | message: string; 50 | data?: { list?: API.ProjectGroupItem[]; pageNo?: number; pageSize?: number; total?: number }; 51 | success: boolean; 52 | }>('/project/getProjectGroupList', { 53 | method: 'GET', 54 | params: { 55 | // pageNo has a default value: 1 56 | pageNo: '1', 57 | // pageSize has a default value: 10 58 | pageSize: '10', 59 | ...params, 60 | }, 61 | ...(options || {}), 62 | }); 63 | } 64 | 65 | /** 更新项目分组 POST /project/updateProjectGroup */ 66 | export async function updateProjectGroup( 67 | body: API.UpdateProjectGroupReq, 68 | options?: { [key: string]: any }, 69 | ) { 70 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 71 | '/project/updateProjectGroup', 72 | { 73 | method: 'POST', 74 | headers: { 75 | 'Content-Type': 'application/json', 76 | }, 77 | data: body, 78 | ...(options || {}), 79 | }, 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Task/Filter/components/TaskBelong.tsx: -------------------------------------------------------------------------------- 1 | import { TaskBelongFilterMap } from '@/constants'; 2 | import { FullSelection } from '@icon-park/react'; 3 | import { useModel } from '@umijs/max'; 4 | import { undefined } from '@umijs/utils/compiled/zod'; 5 | import { Dropdown, MenuProps, Tooltip } from 'antd'; 6 | 7 | const taskBelongMap = { 8 | [TaskBelongFilterMap.all]: '全部任务', 9 | [TaskBelongFilterMap.creatorId]: '我创建的任务', 10 | [TaskBelongFilterMap.handlerId]: '我执行的任务', 11 | [TaskBelongFilterMap.actorId]: '我参与的任务', 12 | custom: '自定义 ', 13 | }; 14 | 15 | // 用于每次重置其它条件 16 | const TaskBelongParams = { 17 | handlerId: undefined, 18 | creatorId: undefined, 19 | actorId: undefined, 20 | }; 21 | 22 | export default () => { 23 | const { initialState } = useModel('@@initialState'); 24 | const { setTaskParams, filterData, setFilterData } = useModel('ProjectDetail.model'); 25 | const { all, creatorId, handlerId, actorId } = TaskBelongFilterMap; 26 | const belongList: MenuProps['items'] = [ 27 | { 28 | key: all, 29 | label: taskBelongMap[all], 30 | }, 31 | { 32 | key: creatorId, 33 | label: taskBelongMap[creatorId], 34 | }, 35 | { 36 | key: handlerId, 37 | label: taskBelongMap[handlerId], 38 | }, 39 | { 40 | key: actorId, 41 | label: taskBelongMap[actorId], 42 | }, 43 | { 44 | key: 'custom', 45 | label: taskBelongMap['custom'], 46 | }, 47 | ]; 48 | const handlerClick: MenuProps['onClick'] = ({ key }) => { 49 | setFilterData({ belongKey: key }); 50 | if (key === 'custom') { 51 | console.log('自定义-筛选人员'); 52 | } 53 | let params: any = TaskBelongParams; 54 | if (key !== TaskBelongFilterMap.all) { 55 | params = { 56 | ...TaskBelongParams, 57 | [key]: initialState?.id, 58 | }; 59 | } 60 | /** 61 | * 当前登录用户 id 以不同身份查询 62 | * key: creatorId handlerId actorId 63 | * */ 64 | setTaskParams(params); 65 | }; 66 | return ( 67 | 68 | 69 | 70 | 71 | {taskBelongMap[filterData.belongKey]} 72 | 73 | 74 | 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /src/api/modules/ProjectUser.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** 项目项目人员 POST /project/addProjectUserByIds */ 6 | export async function addProjectUserByIds( 7 | body: API.AddProjectUserByIdsReq, 8 | options?: { [key: string]: any }, 9 | ) { 10 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 11 | '/project/addProjectUserByIds', 12 | { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | data: body, 18 | ...(options || {}), 19 | }, 20 | ); 21 | } 22 | 23 | /** 删除项目内人员 POST /project/delProjectUserByIds */ 24 | export async function delProjectUserByIds( 25 | body: API.DelProjectUserByIdsReq, 26 | options?: { [key: string]: any }, 27 | ) { 28 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 29 | '/project/delProjectUserByIds', 30 | { 31 | method: 'POST', 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | }, 35 | data: body, 36 | ...(options || {}), 37 | }, 38 | ); 39 | } 40 | 41 | /** 获取项目下的用户列表 GET /project/getProjectUser */ 42 | export async function getProjectUser( 43 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 44 | params: API.getProjectUserParams, 45 | options?: { [key: string]: any }, 46 | ) { 47 | return request<{ 48 | code: number; 49 | message: string; 50 | data?: { list?: API.ProjectUserItem[]; pageNo?: number; pageSize?: number; total?: number }; 51 | success: boolean; 52 | }>('/project/getProjectUser', { 53 | method: 'GET', 54 | params: { 55 | // pageNo has a default value: 1 56 | pageNo: '1', 57 | // pageSize has a default value: 10 58 | pageSize: '10', 59 | ...params, 60 | }, 61 | ...(options || {}), 62 | }); 63 | } 64 | 65 | /** 更新项目成员角色 POST /project/updateProjectUserRole */ 66 | export async function updateProjectUserRole( 67 | body: API.UpdateProjectUserRoleReq, 68 | options?: { [key: string]: any }, 69 | ) { 70 | return request<{ code: number; message: string; data?: Record; success: boolean }>( 71 | '/project/updateProjectUserRole', 72 | { 73 | method: 'POST', 74 | headers: { 75 | 'Content-Type': 'application/json', 76 | }, 77 | data: body, 78 | ...(options || {}), 79 | }, 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Statistics/components/TaskStatusPie.tsx: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { ProjectStatisticsParams } from '@/pages/ProjectDetail/components/Content/Statistics'; 3 | import { Pie } from '@ant-design/plots'; 4 | import { FullScreenOne } from '@icon-park/react'; 5 | import { useEffect, useState } from 'react'; 6 | 7 | type PropsType = { 8 | params: ProjectStatisticsParams; 9 | }; 10 | 11 | const TaskStatusPie = ({ params }: PropsType) => { 12 | const [data, setData] = useState([]); 13 | const config = { 14 | data, 15 | height: 320, 16 | angleField: 'value', 17 | colorField: 'type', 18 | paddingRight: 80, 19 | innerRadius: 0.6, 20 | label: { 21 | text: 'value', 22 | style: { 23 | fontWeight: 'bold', 24 | }, 25 | }, 26 | tooltip: { 27 | title: 'type', 28 | items: [{ channel: 'y' }], 29 | }, 30 | style: { 31 | stroke: '#fff', 32 | inset: 1, 33 | radius: 4, 34 | }, 35 | legend: { 36 | color: { 37 | title: false, 38 | position: 'top', 39 | rowPadding: 5, 40 | }, 41 | }, 42 | scale: { 43 | color: { 44 | offset: (t: number) => t * 0.8 + 0.1, 45 | }, 46 | }, 47 | annotations: [ 48 | { 49 | type: 'text', 50 | style: { 51 | text: '任务状态\n数量分布', 52 | x: '50%', 53 | y: '50%', 54 | textAlign: 'center', 55 | fontSize: 14, 56 | fontStyle: 'bold', 57 | }, 58 | }, 59 | ], 60 | }; 61 | const getData = () => { 62 | Api.ProjectOverview.getTaskStatusStat(params).then((res) => { 63 | if (res.success && res.data) { 64 | const list: any = res.data.map((item) => { 65 | return { type: item.statusName, value: item.taskCount }; 66 | }); 67 | setData(list); 68 | } 69 | }); 70 | }; 71 | useEffect(getData, [params]); 72 | return ( 73 |
74 |
75 |

任务状态分布

76 | 77 | 78 | 79 |
80 | 81 |
82 | ); 83 | }; 84 | 85 | export default TaskStatusPie; 86 | -------------------------------------------------------------------------------- /src/models/message.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useModel } from '@@/exports'; 3 | import { useRequest } from '@umijs/max'; 4 | import { useSetState, useUpdateEffect } from 'ahooks'; 5 | 6 | type StateProps = { 7 | msgList: API.MessageItem[]; 8 | pageNo: number; 9 | finished: boolean; 10 | activeTab: string; 11 | openId?: string; 12 | unreadCount: number; 13 | msgDetail?: Partial; 14 | params: { 15 | status?: string; 16 | type?: string; 17 | }; 18 | }; 19 | 20 | const useMessageModel = () => { 21 | const { getUnreadMsgCount } = useModel('global'); 22 | const [state, setState] = useSetState({ 23 | msgList: [], 24 | pageNo: 1, 25 | finished: false, 26 | params: {}, 27 | activeTab: 'all', 28 | unreadCount: 0, 29 | }); 30 | 31 | const { loading, run: getMessageList } = useRequest( 32 | () => 33 | Api.Message.getMsgList({ 34 | ...state.params, 35 | pageNo: state.pageNo, 36 | }), 37 | { 38 | manual: true, 39 | onSuccess: (res) => { 40 | if (res) { 41 | const _list = res.list || []; 42 | const list = state.pageNo === 1 ? _list : [...state.msgList, ..._list]; 43 | setState({ 44 | msgList: list, 45 | finished: res.total === list.length, 46 | pageNo: state.pageNo + 1, 47 | }); 48 | } 49 | }, 50 | }, 51 | ); 52 | 53 | const getData = (): void => { 54 | if (state.finished || loading) return; 55 | getMessageList(); 56 | }; 57 | const readMsg = (id: string, index: number) => { 58 | Api.Message.readMsg({ id }).then((res) => { 59 | if (res.success) { 60 | getUnreadMsgCount(); 61 | state.msgList[index].status = 1; 62 | setState({ msgList: [...state.msgList] }); 63 | } 64 | }); 65 | }; 66 | const getMsgDetail = (msg: API.MessageItem, index: number) => { 67 | setState({ openId: msg.id }); 68 | if (!msg.status) { 69 | readMsg(msg.id, index); 70 | } 71 | Api.Message.getMsgDetail({ id: msg.id }).then((res) => { 72 | if (res.success) { 73 | setState({ msgDetail: res.data }); 74 | } 75 | }); 76 | }; 77 | 78 | useUpdateEffect(getData, [state.params]); 79 | 80 | return { 81 | loading, 82 | getData, 83 | state, 84 | setState, 85 | getMsgDetail, 86 | }; 87 | }; 88 | 89 | export default useMessageModel; 90 | -------------------------------------------------------------------------------- /src/pages/Settings/components/Content/ProjectGroup/useService.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useDebounceFn, useRequest, useSetState } from 'ahooks'; 3 | import { App } from 'antd'; 4 | import { useEffect, useState } from 'react'; 5 | 6 | const useService = () => { 7 | const { notification } = App.useApp(); 8 | const [groupList, setGroupList] = useState([]); 9 | const [params, setParams] = useSetState({ 10 | finished: false, 11 | keywords: '', 12 | pageSize: 20, 13 | pageNo: 1, 14 | }); 15 | 16 | const { loading, run: getGroupList } = useRequest( 17 | async () => { 18 | const res = await Api.ProjectGroup.getProjectGroupList(params); 19 | if (res.data) { 20 | const tpList = res.data.list || []; 21 | const currentList = res.data.pageNo === 1 ? tpList : [...groupList, ...tpList]; 22 | setGroupList(currentList); 23 | setParams({ 24 | pageNo: params.pageNo! + 1, 25 | finished: currentList.length === res.data.total, 26 | }); 27 | } 28 | }, 29 | { 30 | manual: true, 31 | }, 32 | ); 33 | 34 | const { run: reloadGroupList } = useDebounceFn( 35 | () => { 36 | if (params.finished) return; 37 | getGroupList(); 38 | }, 39 | { wait: 200 }, 40 | ); 41 | 42 | const delGroup = (id: string, index: number) => { 43 | Api.ProjectGroup.delProjectGroup({ id }).then((res) => { 44 | if (res.success) { 45 | groupList.splice(index, 1); 46 | setGroupList([...groupList]); 47 | } else { 48 | notification.error({ 49 | message: '温馨提示', 50 | description: res.message, 51 | }); 52 | } 53 | }); 54 | }; 55 | 56 | const updateGroupRow = (data: API.UpdateProjectGroupReq, index: number) => { 57 | groupList[index].name = data.name!; 58 | groupList[index].description = data.description!; 59 | setGroupList([...groupList]); 60 | }; 61 | 62 | const resetPageNo = () => { 63 | params.pageNo = 1; 64 | params.finished = false; 65 | reloadGroupList(); 66 | }; 67 | 68 | useEffect(() => { 69 | getGroupList(); 70 | }, []); 71 | 72 | return { 73 | loading, 74 | params, 75 | resetPageNo, 76 | setParams, 77 | delGroup, 78 | groupList, 79 | setGroupList, 80 | updateGroupRow, 81 | reloadGroupList, 82 | }; 83 | }; 84 | 85 | export default useService; 86 | -------------------------------------------------------------------------------- /src/services/demo/UserController.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // 该文件由 OneAPI 自动生成,请勿手动修改! 3 | import { request } from '@umijs/max'; 4 | 5 | /** 此处后端没有提供注释 GET /api/v1/queryUserList */ 6 | export async function queryUserList( 7 | params: { 8 | // query 9 | /** keyword */ 10 | keyword?: string; 11 | /** current */ 12 | current?: number; 13 | /** pageSize */ 14 | pageSize?: number; 15 | }, 16 | options?: { [key: string]: any }, 17 | ) { 18 | return request('/api/v1/queryUserList', { 19 | method: 'GET', 20 | params: { 21 | ...params, 22 | }, 23 | ...(options || {}), 24 | }); 25 | } 26 | 27 | /** 此处后端没有提供注释 POST /api/v1/user */ 28 | export async function addUser(body?: API.UserInfoVO, options?: { [key: string]: any }) { 29 | return request('/api/v1/user', { 30 | method: 'POST', 31 | headers: { 32 | 'Content-Type': 'application/json', 33 | }, 34 | data: body, 35 | ...(options || {}), 36 | }); 37 | } 38 | 39 | /** 此处后端没有提供注释 GET /api/v1/user/${param0} */ 40 | export async function getUserDetail( 41 | params: { 42 | // path 43 | /** userId */ 44 | userId?: string; 45 | }, 46 | options?: { [key: string]: any }, 47 | ) { 48 | const { userId: param0 } = params; 49 | return request(`/api/v1/user/${param0}`, { 50 | method: 'GET', 51 | params: { ...params }, 52 | ...(options || {}), 53 | }); 54 | } 55 | 56 | /** 此处后端没有提供注释 PUT /api/v1/user/${param0} */ 57 | export async function modifyUser( 58 | params: { 59 | // path 60 | /** userId */ 61 | userId?: string; 62 | }, 63 | body?: API.UserInfoVO, 64 | options?: { [key: string]: any }, 65 | ) { 66 | const { userId: param0 } = params; 67 | return request(`/api/v1/user/${param0}`, { 68 | method: 'PUT', 69 | headers: { 70 | 'Content-Type': 'application/json', 71 | }, 72 | params: { ...params }, 73 | data: body, 74 | ...(options || {}), 75 | }); 76 | } 77 | 78 | /** 此处后端没有提供注释 DELETE /api/v1/user/${param0} */ 79 | export async function deleteUser( 80 | params: { 81 | // path 82 | /** userId */ 83 | userId?: string; 84 | }, 85 | options?: { [key: string]: any }, 86 | ) { 87 | const { userId: param0 } = params; 88 | return request(`/api/v1/user/${param0}`, { 89 | method: 'DELETE', 90 | params: { ...params }, 91 | ...(options || {}), 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /src/pages/Settings/components/Content/User/useService.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api/modules'; 2 | import { useDebounceFn, useRequest, useSetState, useUpdateEffect } from 'ahooks'; 3 | import { message } from 'antd'; 4 | import { useEffect } from 'react'; 5 | 6 | type SettingDataType = { 7 | userList: API.UserBaseInfo[]; 8 | pageSize: number; 9 | pageNo: number; 10 | total: number; 11 | keywords?: string; 12 | role?: string; 13 | }; 14 | const useService = () => { 15 | const [data, setData] = useSetState({ 16 | userList: [], 17 | pageSize: 15, 18 | pageNo: 1, 19 | total: 0, 20 | }); 21 | 22 | // 项目任务分组列表 23 | const { loading, run: getUserList } = useRequest( 24 | () => 25 | Api.User.getUserList({ 26 | pageNo: data.pageNo, 27 | pageSize: data.pageSize, 28 | keywords: data.keywords, 29 | role: data.role, 30 | }).then((res) => { 31 | if (res.data) { 32 | const userList = res.data.list || []; 33 | setData({ 34 | userList, 35 | total: res.data.total || 0, 36 | }); 37 | } 38 | }), 39 | { manual: true }, 40 | ); 41 | 42 | const delUser = (userId: string, index: number) => { 43 | Api.User.delUserById({ 44 | id: userId, 45 | }).then((res) => { 46 | if (res.success) { 47 | message.success('删除成功'); 48 | data.userList.splice(index, 1); 49 | setData({ userList: [...data.userList] }); 50 | } 51 | }); 52 | }; 53 | 54 | const updateUserInfo = (params: API.UpdateUserInfoReq, index: number) => { 55 | Api.User.updateUserInfo(params).then((res) => { 56 | if (res.success) { 57 | data.userList[index].role = params.role!; 58 | setData({ userList: [...data.userList] }); 59 | message.success('更新成功'); 60 | } else { 61 | message.warning(res.message || '更新失败'); 62 | } 63 | }); 64 | }; 65 | 66 | const { run: onKeywordsChange } = useDebounceFn( 67 | (e) => { 68 | setData({ 69 | pageNo: 1, 70 | keywords: e.target.value, 71 | }); 72 | }, 73 | { wait: 300 }, 74 | ); 75 | 76 | useUpdateEffect(getUserList, [data.pageSize, data.pageNo]); 77 | 78 | useEffect(() => { 79 | data.pageNo = 1; 80 | getUserList(); 81 | }, [data.keywords, data.role]); 82 | 83 | return { 84 | data, 85 | setData, 86 | loading, 87 | delUser, 88 | updateUserInfo, 89 | getUserList, 90 | onKeywordsChange, 91 | }; 92 | }; 93 | 94 | export default useService; 95 | -------------------------------------------------------------------------------- /src/pages/components/TaskPrioritySelect/index.tsx: -------------------------------------------------------------------------------- 1 | import { Down } from '@icon-park/react'; 2 | import { useClickAway, useUpdateEffect } from 'ahooks'; 3 | import { Dropdown, MenuProps, Space, Spin, theme } from 'antd'; 4 | import React, { memo, useRef } from 'react'; 5 | import useService from './useService'; 6 | const { useToken } = theme; 7 | type PropsType = { 8 | mount?: boolean; 9 | children: React.ReactNode; 10 | icon?: React.ReactNode; 11 | onChange: (item: API.TaskPriorityItem) => void; 12 | }; 13 | const TaskPrioritySelect = memo((props: PropsType) => { 14 | const { data, loading, getData, open, setOpen } = useService(); 15 | 16 | const container = useRef(null); 17 | useClickAway(() => { 18 | setOpen(false); 19 | }, [container]); 20 | 21 | const items: MenuProps['items'] = data?.list?.map((item, index) => { 22 | return { 23 | label: ( 24 | 25 | {item.id}({item.name}) 26 | 27 | ), 28 | key: index, 29 | }; 30 | }); 31 | 32 | const onClickItem: MenuProps['onClick'] = ({ key }) => { 33 | const item = data?.list?.[+key]; 34 | if (item) { 35 | props.onChange(item); 36 | } 37 | }; 38 | 39 | const { token } = useToken(); 40 | const contentStyle = { 41 | backgroundColor: token.colorBgElevated, 42 | borderRadius: token.borderRadiusLG, 43 | boxShadow: token.boxShadowSecondary, 44 | }; 45 | 46 | useUpdateEffect(() => { 47 | if (open) getData(); 48 | }, [open]); 49 | 50 | return ( 51 | 52 | setOpen(v)} 59 | dropdownRender={(menu) => ( 60 |
61 | {loading ? ( 62 |
63 | 64 |
65 | ) : ( 66 | menu 67 | )} 68 |
69 | )} 70 | > 71 | setOpen(true)}> 72 | 73 | {props.children} 74 | {props.icon || } 75 | 76 | 77 |
78 |
79 | ); 80 | }); 81 | export default TaskPrioritySelect; 82 | -------------------------------------------------------------------------------- /src/pages/components/TaskStatusSelect/index.tsx: -------------------------------------------------------------------------------- 1 | import { Down } from '@icon-park/react'; 2 | import { useClickAway, useUpdateEffect } from 'ahooks'; 3 | import { Dropdown, MenuProps, Spin, theme } from 'antd'; 4 | import React, { memo, useRef } from 'react'; 5 | import useService from './useService'; 6 | const { useToken } = theme; 7 | type PropsType = { 8 | mount?: boolean; 9 | projectId: string; 10 | children: React.ReactNode; 11 | icon?: React.ReactNode; 12 | onChange: (id: string, item: API.TaskStatusItem) => void; 13 | }; 14 | const TaskStatusSelect = memo((props: PropsType) => { 15 | const { data, loading, getData, open, setOpen } = useService(props.projectId); 16 | const container = useRef(null); 17 | 18 | useClickAway(() => { 19 | setOpen(false); 20 | }, [container]); 21 | 22 | const items: MenuProps['items'] = data?.list?.map((item, index) => { 23 | return { 24 | label: {item.name}, 25 | key: index, 26 | }; 27 | }); 28 | 29 | const onClickItem: MenuProps['onClick'] = ({ key }: any) => { 30 | const item = data?.list?.[key]; 31 | if (item) { 32 | props.onChange(item.id, item); 33 | } 34 | }; 35 | 36 | const { token } = useToken(); 37 | const contentStyle = { 38 | backgroundColor: token.colorBgElevated, 39 | borderRadius: token.borderRadiusLG, 40 | boxShadow: token.boxShadowSecondary, 41 | }; 42 | 43 | useUpdateEffect(() => { 44 | if (open) getData(); 45 | }, [open]); 46 | 47 | return ( 48 | 49 | container.current!} 56 | onOpenChange={(v) => setOpen(v)} 57 | dropdownRender={(menu) => ( 58 |
59 | {loading ? ( 60 |
61 | 62 |
63 | ) : ( 64 | menu 65 | )} 66 |
67 | )} 68 | > 69 | setOpen(true)}> 70 | 71 | {props.children} 72 | {props.icon || } 73 | 74 | 75 |
76 |
77 | ); 78 | }); 79 | export default TaskStatusSelect; 80 | -------------------------------------------------------------------------------- /src/pages/LaborHour/components/UserHeader.tsx: -------------------------------------------------------------------------------- 1 | import { UserLaborItem } from '@/pages/LaborHour/model'; 2 | import { CloseOne, Right } from '@icon-park/react'; 3 | import { useModel } from '@umijs/max'; 4 | import { App, Avatar } from 'antd'; 5 | 6 | type DataType = { 7 | item: UserLaborItem; 8 | preIndex: number; 9 | }; 10 | 11 | const UserHeader = (props: DataType) => { 12 | const { modal } = App.useApp(); 13 | const { data: laborHourData, setData, getLaborHourByUserId, delProjectLaborUser } = useModel('LaborHour.model'); 14 | const { userInfo, totalHour } = props.item; 15 | const itemClass = 'group text-center border-r p-2 border-r-zinc-200 w-[1/8]'; 16 | const { initialState } = useModel('@@initialState'); 17 | const isSelf = initialState?.id === userInfo.id; 18 | const delUser = () => { 19 | modal.confirm({ 20 | title: '温馨提示', 21 | content: '确认删除当前订阅用户?', 22 | onOk: () => { 23 | delProjectLaborUser(userInfo.id, props.preIndex); 24 | }, 25 | }); 26 | }; 27 | 28 | const fetchLaborHourDetail = () => { 29 | const openUserIds = laborHourData.openUserIds; 30 | const userId: string = userInfo.id; 31 | const openIndex = openUserIds.indexOf(userId); 32 | if (openIndex > -1) { 33 | openUserIds.splice(openIndex, 1); 34 | } else { 35 | openUserIds.push(userId); 36 | getLaborHourByUserId(props.preIndex, userInfo.id); 37 | } 38 | setData({ openUserIds: [...openUserIds] }); 39 | }; 40 | 41 | return ( 42 |
43 |
44 |
45 | 46 |
47 |

{userInfo.username}

48 |

合计:{totalHour}h

49 |
50 |
51 | 58 |
59 | {!isSelf && ( 60 | 66 | )} 67 |
68 | ); 69 | }; 70 | 71 | export default UserHeader; 72 | -------------------------------------------------------------------------------- /src/pages/components/TaskDetailModal/components/TaskTitle.tsx: -------------------------------------------------------------------------------- 1 | import TaskTypeSelect from '@/pages/components/TaskTypeSelect'; 2 | import { AddSubset, Back, CategoryManagement, Close, CopyLink, Delete } from '@icon-park/react'; 3 | import { useModel } from '@umijs/max'; 4 | import { App, Button, Space } from 'antd'; 5 | 6 | const TaskTitle = ({ onClose }: { onClose: () => void }) => { 7 | const { modal } = App.useApp(); 8 | const { data, delTaskById, updateTaskInfo, getPrevTaskDetail, lookParentTask } = useModel('taskDetail'); 9 | 10 | const handleDelTask = () => { 11 | modal.confirm({ 12 | title: '确认删除当前任务?', 13 | content: '删除后无法恢复', 14 | okText: '确认', 15 | cancelText: '取消', 16 | onOk: () => { 17 | delTaskById(); 18 | }, 19 | }); 20 | }; 21 | 22 | return ( 23 |
24 | 25 | 26 | {data.taskIdQueue.length > 1 && ( 27 | 31 | )} 32 | {!!data.task?.parentId && ( 33 | 37 | )} 38 | 39 | 40 | {!!data.task && ( 41 | { 44 | updateTaskInfo({ typeId: item.id }); 45 | }} 46 | > 47 | {data.task?.typeName || '任务类型'} 48 | 49 | )} 50 | 51 | 52 | 53 |
54 | 55 |
65 |
66 | ); 67 | }; 68 | 69 | export default TaskTitle; 70 | -------------------------------------------------------------------------------- /src/pages/Project/components/Menu/index.tsx: -------------------------------------------------------------------------------- 1 | import { LOCAL_KEY } from '@/constants'; 2 | import { AllApplication, MenuUnfoldOne, Newlybuild, Palm, UserBusiness } from '@icon-park/react'; 3 | import { useModel } from '@umijs/max'; 4 | import { useLocalStorageState } from 'ahooks'; 5 | import { Button, Menu } from 'antd'; 6 | import type { MenuProps } from 'antd/es/menu'; 7 | import React, { useEffect } from 'react'; 8 | import styles from './index.less'; 9 | 10 | function genMenuItem( 11 | label: React.ReactNode, 12 | key?: React.Key | null, 13 | icon?: React.ReactNode, 14 | children?: MenuItem[], 15 | ): MenuItem { 16 | return { 17 | key, 18 | icon, 19 | children, 20 | label, 21 | } as MenuItem; 22 | } 23 | 24 | type MenuItem = Required['items'][number]; 25 | 26 | export default () => { 27 | const { globalData, onToggleMenu } = useModel('global'); 28 | const { resetMenuParams } = useModel('Project.model'); 29 | const [activeKey, setActiveKey] = useLocalStorageState(LOCAL_KEY.ACTIVE_PROJECT_TYPE, { 30 | defaultValue: 'ALL', 31 | }); 32 | // 33 | const items: MenuItem[] = [ 34 | genMenuItem('全部项目', 'ALL', ), 35 | genMenuItem('我参与的项目', `JOIN`, ), 36 | genMenuItem('我负责的项目', 'OWNER', ), 37 | genMenuItem('我创建的项目', 'CREATE', ), 38 | ]; 39 | 40 | const onSelect = (key: string) => { 41 | setActiveKey(key); 42 | let params: API.GetProjectListReq = {}; 43 | switch (key) { 44 | case 'JOIN': 45 | params.isJoined = true; 46 | break; 47 | case 'CREATE': 48 | params.isCreator = true; 49 | break; 50 | case 'OWNER': 51 | params.isOwner = true; 52 | break; 53 | } 54 | resetMenuParams(params); 55 | }; 56 | 57 | useEffect(() => { 58 | onSelect(activeKey!); 59 | }, []); 60 | 61 | return ( 62 |
63 |
64 |
65 |

项目

66 | 71 |
72 | onSelect(key)} 76 | items={items} 77 | /> 78 |
79 |
80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /src/pages/components/TaskDetailModal/components/TaskActor.tsx: -------------------------------------------------------------------------------- 1 | import { CloseOne, Help, Plus } from '@icon-park/react'; 2 | import { useModel } from '@umijs/max'; 3 | import { App, Avatar, Button, Space, Tooltip } from 'antd'; 4 | import ActorUserSelect from './ActorUserSelect'; 5 | 6 | const TaskActor = () => { 7 | const { modal } = App.useApp(); 8 | const { data, deleteActorForTask } = useModel('taskDetail'); 9 | // 任务创建者和任务执行者不能被移除 10 | const disabledIds = [data.task?.handlerId, data.task?.creatorId]; 11 | 12 | const onDeleteActor = (item: API.TaskActorItem) => { 13 | modal.confirm({ 14 | title: `移除任务参与者`, 15 | content: `确认移除任务参与者:${item.username}?`, 16 | okText: '确认', 17 | cancelText: '取消', 18 | onOk: () => { 19 | deleteActorForTask([item.id]); 20 | }, 21 | }); 22 | }; 23 | 24 | return ( 25 |
26 |
27 | 28 | 参与者 29 | · 30 | {data.actorList.length} 31 | 32 | 33 | 34 | 35 |
36 |
37 | 38 | {data.actorList.map((item: API.TaskActorItem, index: number) => ( 39 | 44 | 45 | 46 | {!disabledIds.includes(item.id) && ( 47 | onDeleteActor(item)} 49 | className={`absolute right-[-6px] top-[-10px] hidden 50 | cursor-pointer rounded-xl bg-white text-zinc-300 51 | hover:text-zinc-400 group-hover:inline-flex`} 52 | > 53 | 54 | 55 | )} 56 | 57 | 58 | ))} 59 | 60 | 61 |
66 |
67 | ); 68 | }; 69 | 70 | export default TaskActor; 71 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Setting/components/TaskType/index.tsx: -------------------------------------------------------------------------------- 1 | import { Delete, Editor } from '@icon-park/react'; 2 | import { App, Button, Space, Table } from 'antd'; 3 | import { ColumnsType } from 'antd/es/table'; 4 | import TaskTypeForm from './TypeForm'; 5 | import useService from './useService'; 6 | 7 | const TaskTypePage = () => { 8 | const { modal } = App.useApp(); 9 | const { typeList, getTaskTypeList, delType } = useService(); 10 | 11 | const onDelStatus = (id: string) => { 12 | modal.confirm({ 13 | title: '警告', 14 | content: '删除后无法恢复(该状态下含有任务时无法删除)', 15 | onOk: () => delType(id), 16 | }); 17 | }; 18 | 19 | const columns: ColumnsType = [ 20 | { 21 | title: '排序', 22 | dataIndex: 'index', 23 | key: 'index', 24 | width: 100, 25 | align: 'center', 26 | ellipsis: true, 27 | render: (_, record, index) => {index + 1}, 28 | }, 29 | { 30 | title: '名称', 31 | dataIndex: 'name', 32 | key: 'name', 33 | width: 180, 34 | ellipsis: true, 35 | render: (_, record) => {record.name}, 36 | }, 37 | { 38 | title: '颜色', 39 | align: 'center', 40 | dataIndex: 'color', 41 | key: 'color', 42 | render: (_, record) => {record.color}, 43 | }, 44 | { 45 | title: '操作', 46 | width: 100, 47 | dataIndex: '', 48 | align: 'center', 49 | key: 'action', 50 | render: (_, record) => ( 51 | 52 | 53 | 56 | 57 | 60 | 61 | ), 62 | }, 63 | ]; 64 | 65 | return ( 66 |
67 |
68 | 温馨提示:任务类型指:项目调研、项目立项、开发任务、UI设计等等 69 | 70 | 71 | 72 |
73 | 74 | bordered 75 | size={'small'} 76 | sticky={{ offsetHeader: 0 }} 77 | rowKey={(record) => record.id} 78 | columns={columns} 79 | pagination={false} 80 | dataSource={typeList || []} 81 | /> 82 |
83 | ); 84 | }; 85 | 86 | export default TaskTypePage; 87 | -------------------------------------------------------------------------------- /src/pages/ProjectDetail/components/Content/Task/components/Card/components/CardItem.tsx: -------------------------------------------------------------------------------- 1 | import { EVENTS, panelCardTypeFilterMap } from '@/constants'; 2 | import EventBus from '@/utils/event-bus'; 3 | import { useModel } from '@umijs/max'; 4 | import { Avatar } from 'antd'; 5 | import { Draggable } from 'react-beautiful-dnd'; 6 | 7 | export default (props: { item: API.TaskDetailItem; groupIndex: number; cardIndex: number }) => { 8 | const { item, cardIndex } = props; 9 | 10 | const openTaskDetail = (id: string) => EventBus.emit(EVENTS.OPEN_TASK_DETAIL, id); 11 | 12 | const { filterData, data } = useModel('ProjectDetail.model'); 13 | return ( 14 | 15 | {(provided: any) => { 16 | return ( 17 |
24 |
openTaskDetail(item.id)} 27 | className={'w-full cursor-pointer rounded-lg bg-white p-3'} 28 | > 29 |
30 |
31 |

{item.name}

32 | {!!item.handlerId && } 33 |
34 | {!!item.groupId &&

{item.groupName}

} 35 | 36 |
37 | {!!item.priority && filterData.cardType !== panelCardTypeFilterMap.priority && ( 38 | {item.priority} 39 | )} 40 | {!!item.statusId && filterData.cardType !== panelCardTypeFilterMap.statusId && ( 41 | 42 | {item.statusName} 43 | 44 | )} 45 | {!!item.typeId && filterData.cardType !== panelCardTypeFilterMap.typeId && ( 46 | 47 | {item.typeName} 48 | 49 | )} 50 |
51 |
52 |
53 |
54 | ); 55 | }} 56 |
57 | ); 58 | }; 59 | --------------------------------------------------------------------------------