├── .gitignore ├── 500.html ├── README.md ├── backend ├── __init__.py ├── celery.service ├── common │ ├── JSONRenderer.py │ ├── __init__.py │ ├── dispath.py │ ├── django.py │ ├── exceptions.py │ ├── models.py │ ├── pagination.py │ ├── status.py │ └── views.py ├── core │ ├── __init__.py │ ├── celery.py │ ├── settings │ │ ├── __init__.py │ │ ├── base.py │ │ ├── dev.py │ │ ├── mac.py │ │ └── prod.py │ ├── urls.py │ └── wsgi.py ├── dev_requirements.txt ├── init.sh ├── manage.py ├── notices │ ├── __init__.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── init_notice.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── oms.ini ├── oms.service ├── requirements.txt ├── systems │ ├── __init__.py │ ├── admin.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── init_sys.py │ ├── menus.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── permissions.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── tickets │ ├── __init__.py │ ├── filters.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── init_ticket.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── tools │ ├── __init__.py │ ├── filesize.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── storage.py │ ├── urls.py │ └── views.py ├── utils │ ├── __init__.py │ ├── get_realip.py │ ├── index.py │ ├── mysql.py │ ├── sendmail.py │ ├── sendskype.py │ ├── test.py │ ├── time.py │ └── verifys.py └── workflows │ ├── __init__.py │ ├── admin.py │ ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── init_leave.py │ │ └── init_wf.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── frontend ├── .editorconfig ├── .env.development ├── .env.production ├── .eslintignore ├── .eslintrc.js ├── .travis.yml ├── LICENSE ├── babel.config.js ├── build │ └── index.js ├── jest.config.js ├── jsconfig.json ├── package.json ├── plop-templates │ ├── component │ │ ├── index.hbs │ │ └── prompt.js │ ├── store │ │ ├── index.hbs │ │ └── prompt.js │ ├── utils.js │ └── view │ │ ├── index.hbs │ │ └── prompt.js ├── plopfile.js ├── postcss.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── api │ │ ├── all.js │ │ ├── auths.js │ │ └── common.js │ ├── assets │ │ ├── 401_images │ │ │ └── 401.gif │ │ ├── 404_images │ │ │ ├── 404.png │ │ │ └── 404_cloud.png │ │ ├── custom-theme │ │ │ ├── fonts │ │ │ │ ├── element-icons.ttf │ │ │ │ └── element-icons.woff │ │ │ └── index.css │ │ └── panel-bg.png │ ├── components │ │ ├── Breadcrumb │ │ │ └── index.vue │ │ ├── Charts │ │ │ ├── keyboard.vue │ │ │ ├── lineMarker.vue │ │ │ ├── mixChart.vue │ │ │ └── mixins │ │ │ │ └── resize.js │ │ ├── DndList │ │ │ └── index.vue │ │ ├── Hamburger │ │ │ └── index.vue │ │ ├── HeaderSearch │ │ │ └── index.vue │ │ ├── Kanban │ │ │ └── index.vue │ │ ├── LangSelect │ │ │ └── index.vue │ │ ├── MDinput │ │ │ └── index.vue │ │ ├── Pagination │ │ │ └── index.vue │ │ ├── PanThumb │ │ │ └── index.vue │ │ ├── RightPanel │ │ │ └── index.vue │ │ ├── Screenfull │ │ │ └── index.vue │ │ ├── Share │ │ │ └── DropdownMenu.vue │ │ ├── Sticky │ │ │ └── index.vue │ │ ├── SvgIcon │ │ │ └── index.vue │ │ ├── TextHoverEffect │ │ │ └── Mallki.vue │ │ ├── ThemePicker │ │ │ └── index.vue │ │ └── TreeSelect │ │ │ └── index.vue │ ├── directive │ │ ├── clipboard │ │ │ ├── clipboard.js │ │ │ └── index.js │ │ ├── sticky.js │ │ └── waves │ │ │ ├── index.js │ │ │ ├── waves.css │ │ │ └── waves.js │ ├── filters │ │ └── index.js │ ├── icons │ │ ├── index.js │ │ ├── svg │ │ │ ├── 404.svg │ │ │ ├── all_ticket.svg │ │ │ ├── audit.svg │ │ │ ├── bug.svg │ │ │ ├── component.svg │ │ │ ├── dashboard.svg │ │ │ ├── exit-fullscreen.svg │ │ │ ├── eye-open.svg │ │ │ ├── eye.svg │ │ │ ├── fullscreen.svg │ │ │ ├── group.svg │ │ │ ├── icon.svg │ │ │ ├── international.svg │ │ │ ├── language.svg │ │ │ ├── list.svg │ │ │ ├── mail.svg │ │ │ ├── menu.svg │ │ │ ├── message.svg │ │ │ ├── money.svg │ │ │ ├── my_ticket.svg │ │ │ ├── new_ticket.svg │ │ │ ├── notice.svg │ │ │ ├── password.svg │ │ │ ├── rocket.svg │ │ │ ├── role.svg │ │ │ ├── search.svg │ │ │ ├── shopping.svg │ │ │ ├── sys.svg │ │ │ ├── telegram.svg │ │ │ ├── ticket.svg │ │ │ ├── todo_ticket.svg │ │ │ ├── tool.svg │ │ │ ├── user.svg │ │ │ ├── wfset.svg │ │ │ ├── wftype.svg │ │ │ └── workflow.svg │ │ └── svgo.yml │ ├── lang │ │ ├── en.js │ │ ├── index.js │ │ └── zh.js │ ├── layout │ │ ├── components │ │ │ ├── AppMain.vue │ │ │ ├── Navbar.vue │ │ │ ├── Settings │ │ │ │ └── index.vue │ │ │ ├── Sidebar │ │ │ │ ├── Item.vue │ │ │ │ ├── Link.vue │ │ │ │ ├── Logo.vue │ │ │ │ ├── SidebarItem.vue │ │ │ │ └── index.vue │ │ │ ├── TagsView │ │ │ │ ├── ScrollPane.vue │ │ │ │ └── index.vue │ │ │ └── index.js │ │ ├── index.vue │ │ └── mixin │ │ │ └── ResizeHandler.js │ ├── main.js │ ├── permission.js │ ├── router │ │ ├── index.js │ │ └── modules │ │ │ └── components.js │ ├── settings.js │ ├── store │ │ ├── getters.js │ │ ├── index.js │ │ └── modules │ │ │ ├── app.js │ │ │ ├── permission.js │ │ │ ├── settings.js │ │ │ ├── tagsView.js │ │ │ └── user.js │ ├── styles │ │ ├── btn.scss │ │ ├── csshake.scss │ │ ├── element-ui.scss │ │ ├── element-variables.scss │ │ ├── index.scss │ │ ├── mixin.scss │ │ ├── sidebar.scss │ │ ├── transition.scss │ │ └── variables.scss │ ├── utils │ │ ├── auth.js │ │ ├── clipboard.js │ │ ├── get-page-title.js │ │ ├── i18n.js │ │ ├── index.js │ │ ├── open-window.js │ │ ├── permission.js │ │ ├── request.js │ │ ├── scroll-to.js │ │ └── validate.js │ ├── vendor │ │ ├── Export2Excel.js │ │ └── Export2Zip.js │ └── views │ │ ├── components-demo │ │ ├── clipboard.vue │ │ ├── dnd-list.vue │ │ ├── drag-kanban.vue │ │ ├── mixin.vue │ │ └── sticky.vue │ │ ├── dashboard │ │ ├── components │ │ │ ├── BarChart.vue │ │ │ ├── BoxCard.vue │ │ │ ├── LineChart.vue │ │ │ ├── PanelGroup.vue │ │ │ ├── PieChart.vue │ │ │ ├── RaddarChart.vue │ │ │ ├── TodoList │ │ │ │ ├── Todo.vue │ │ │ │ ├── index.scss │ │ │ │ └── index.vue │ │ │ ├── TransactionTable.vue │ │ │ └── mixins │ │ │ │ └── resize.js │ │ └── index.vue │ │ ├── error-page │ │ ├── 401.vue │ │ └── 404.vue │ │ ├── icons │ │ ├── element-icons.js │ │ ├── index.vue │ │ └── svg-icons.js │ │ ├── login │ │ └── index.vue │ │ ├── notice │ │ ├── mail.vue │ │ └── telegram.vue │ │ ├── sys │ │ ├── group.vue │ │ ├── menu.vue │ │ ├── role.vue │ │ └── user.vue │ │ ├── ticket │ │ ├── all_ticket.vue │ │ ├── my_ticket.vue │ │ ├── new_ticket.vue │ │ ├── s_ticket.vue │ │ ├── todo_ticket.vue │ │ └── u_ticket.vue │ │ ├── tool │ │ ├── audit.vue │ │ └── test.vue │ │ └── workflow │ │ ├── pages │ │ ├── customfield.vue │ │ ├── state.vue │ │ └── transition.vue │ │ ├── wfconf.vue │ │ ├── wfset.vue │ │ └── wftype.vue └── vue.config.js ├── gifs ├── all.png ├── edit.png ├── leave.png ├── new.png ├── role.png └── role_edit.png └── oms_nginx.conf /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea/ 3 | .DS_Store 4 | .python-version 5 | upload/ 6 | *.log 7 | node_modules/ 8 | package-lock.json 9 | .vscode/ 10 | venv/ 11 | *.db 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django + vue 工作流管理系统 2 | 包含 `用户`、`角色`、`菜单`、`权限` 管理, 这是基础的工作流系统,初始化会生成请假工作流, 也可以自行配置其他工作流比如,发布工单等。 3 | 4 | [comment]: <> (- 后端model参考: [loonflow](https://github.com/blackholll/loonflow), 非常不错的一个项目) 5 | [comment]: <> (- 前端设计参考: [花裤衩 vue-element-admin](https://github.com/PanJiaChen/vue-element-admin), 大神作品没得说) 6 | ## 开发环境 7 | ### 后端 8 | 安装依赖 9 | ```bash 10 | cd backend 11 | pip install -r dev_requirements.txt 12 | ``` 13 | 14 | 初始化系统 15 | - 生成管理员账号 `admin 123456` 16 | ```bash 17 | python manage.py migrate 18 | python manage.py init_sys 19 | ``` 20 | 21 | 生成工作流 22 | - 用户 `ops`,`ops_tl`,`dev`,`dev_tl`,`hr`,`hr_tl` 23 | - 密码 `123456` 24 | 25 | ```bash 26 | python manage.py init_wf 27 | python manage.py init_ticket 28 | python manage.py init_leave 29 | ``` 30 | 31 | 运行 32 | ```bash 33 | python manage.py runserver 34 | ``` 35 | 36 | ### 前端 37 | ```bash 38 | cd frontend 39 | npm install 40 | npm run dev 41 | ``` 42 | 43 | ## 开始使用 44 | 使用 `admin` 登录 45 | ### 给所有角色分配工作流权限 46 |  47 | 48 | ### 分配菜单 和 数据 权限 49 |  50 | 51 | ### 配置假期工作流 52 |  53 | 54 | ### 新建工单 55 |  56 | 57 | ### 编辑工单 58 |  59 | 60 | ### 所有工单 61 |  -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | -------------------------------------------------------------------------------- /backend/celery.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=start celery worker 3 | 4 | [Service] 5 | ExecStart=/bin/bash -c 'cd /opt/projects/one-oms/backend; /root/.pyenv/versions/envoms/bin/celery -A core worker -B --loglevel=info -f /data/logs/celery.log' 6 | #非正常dead,自动重启 7 | Restart=on-failure 8 | #3秒后启动 9 | RestartSec=3s 10 | KillSignal=SIGQUIT 11 | Type=simple 12 | NotifyAccess=all 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /backend/common/JSONRenderer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | from rest_framework.renderers import JSONRenderer 5 | 6 | 7 | class CustomJSONRenderer(JSONRenderer): 8 | def render(self, data, accepted_media_type=None, renderer_context=None): 9 | response_data = {} 10 | object_list = 'results' 11 | try: 12 | meta_dict = getattr(renderer_context.get('view').get_serializer().Meta, 'meta_dict') 13 | except: 14 | meta_dict = dict() 15 | 16 | try: 17 | data.get('paginated_results') 18 | response_data['meta'] = data['meta'] 19 | response_data[object_list] = data['results'] 20 | except: 21 | response_data[object_list] = data 22 | response_data['meta'] = dict() 23 | response_data['meta'].update(meta_dict) 24 | 25 | response = super(CustomJSONRenderer, self).render(response_data, accepted_media_type, renderer_context) 26 | return response 27 | -------------------------------------------------------------------------------- /backend/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/backend/common/__init__.py -------------------------------------------------------------------------------- /backend/common/dispath.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | import six 5 | from rest_framework.serializers import Serializer 6 | from rest_framework.response import Response 7 | 8 | 9 | class JsonResponse(Response): 10 | """ 11 | An HttpResponse that allows its data to be rendered into 12 | arbitrary media types. 13 | """ 14 | 15 | def __init__(self, data=None, code=None, desc=None, 16 | status=None, 17 | template_name=None, headers=None, 18 | exception=False, content_type=None): 19 | """ 20 | Alters the init arguments slightly. 21 | For example, drop 'template_name', and instead use 'data'. 22 | Setting 'renderer' and 'media_type' will typically be deferred, 23 | For example being set automatically by the `APIView`. 24 | """ 25 | super().__init__(None, status=status) 26 | 27 | if isinstance(data, Serializer): 28 | msg = ( 29 | 'You passed a Serializer instance as data, but ' 30 | 'probably meant to pass serialized `.data` or ' 31 | '`.error`. representation.' 32 | ) 33 | raise AssertionError(msg) 34 | 35 | self.data = data 36 | self.template_name = template_name 37 | self.exception = exception 38 | self.content_type = content_type 39 | 40 | if headers: 41 | for name, value in six.iteritems(headers): 42 | self[name] = value 43 | -------------------------------------------------------------------------------- /backend/common/django.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: timor 3 | 4 | from django.utils.deprecation import MiddlewareMixin 5 | 6 | class DisableCSRF(MiddlewareMixin): 7 | def process_request(self, request): 8 | setattr(request, '_dont_enforce_csrf_checks', True) 9 | -------------------------------------------------------------------------------- /backend/common/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | from rest_framework.views import exception_handler 5 | from common import status 6 | 7 | 8 | def JSONExceptionHandler(exc, context): 9 | response = exception_handler(exc, context) 10 | 11 | if response is not None: 12 | resp = { 13 | 'code': status.HTTP_400_BAD_REQUEST, 14 | 'result': dict(response.data) 15 | } 16 | response.data = resp 17 | 18 | return response 19 | 20 | 21 | class ExceptionX_Result: 22 | exceptionType = None 23 | exceptionTitle = None 24 | exceptionDetail = None 25 | 26 | 27 | class ExceptionX: 28 | 29 | @staticmethod 30 | def ToString(e): 31 | result = ExceptionX_Result 32 | tempStr = str(type(e)) 33 | tempStrArray = tempStr.split("'") 34 | result.exceptionTitle = tempStrArray[1] 35 | result.exceptionType = tempStrArray[0][1:] 36 | result.exceptionDetail = str(e) 37 | 38 | if result.exceptionDetail[0] == "<": 39 | if result.exceptionDetail[result.exceptionDetail.__len__() - 1] == ">": 40 | result.exceptionDetail = result.exceptionDetail[1:result.exceptionDetail.__len__() - 1] 41 | return result 42 | 43 | @staticmethod 44 | def PasreRaise(e): 45 | result = ExceptionX_Result 46 | tempStr = str(type(e)) 47 | tempStrArray = tempStr.split("'") 48 | result.exceptionTitle = tempStrArray[1] 49 | result.exceptionType = tempStrArray[0][1:] 50 | result.exceptionDetail = str(e) 51 | 52 | if result.exceptionDetail[0] == "<": 53 | if result.exceptionDetail[result.exceptionDetail.__len__() - 1] == ">": 54 | result.exceptionDetail = result.exceptionDetail[1:result.exceptionDetail.__len__() - 1] 55 | return result.exceptionDetail 56 | -------------------------------------------------------------------------------- /backend/common/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | from django.db import models 5 | 6 | 7 | class BaseModel(models.Model): 8 | create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') 9 | update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') 10 | memo = models.TextField(blank=True, verbose_name='备注') 11 | 12 | class Meta: 13 | ordering = ['-create_time'] 14 | abstract = True 15 | 16 | -------------------------------------------------------------------------------- /backend/common/pagination.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | from collections import OrderedDict 5 | 6 | from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination 7 | 8 | from common import status 9 | from common.dispath import JsonResponse 10 | 11 | 12 | def _positive_int(integer_string, strict=False, cutoff=None): 13 | """ 14 | 分页大小为零不分页 15 | """ 16 | ret = int(integer_string) 17 | if ret < 0: 18 | raise ValueError() 19 | if (ret == 0) and strict: 20 | return None 21 | if cutoff: 22 | return min(ret, cutoff) 23 | return ret 24 | 25 | 26 | class StandardResultsSetPagination(PageNumberPagination): 27 | """ 28 | 配置分页规则 29 | """ 30 | page_size = 20 31 | page_size_query_param = 'limit' 32 | page_query_param = 'page' 33 | max_page_size = 1000 34 | 35 | def get_paginated_response(self, data): 36 | return JsonResponse(OrderedDict([ 37 | ('count', self.page.paginator.count), 38 | ('next', self.get_next_link()), 39 | ('previous', self.get_previous_link()), 40 | ('results', data) 41 | ], code=status.HTTP_200_OK)) 42 | 43 | def get_page_size(self, request): 44 | if self.page_size_query_param: 45 | try: 46 | return _positive_int( 47 | request.query_params[self.page_size_query_param], 48 | strict=True, 49 | cutoff=self.max_page_size 50 | ) 51 | except (KeyError, ValueError): 52 | return None 53 | return self.page_size 54 | 55 | 56 | class CustomLimitOffsetPagination(LimitOffsetPagination): 57 | def get_offset(self, request): 58 | try: 59 | return (int(request.query_params['offset']) - 1) * int(request.query_params['limit']) 60 | except (KeyError, ValueError): 61 | return 1 62 | -------------------------------------------------------------------------------- /backend/common/status.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | from __future__ import unicode_literals 5 | 6 | 7 | def is_informational(code): 8 | return 100 <= code <= 199 9 | 10 | 11 | def is_success(code): 12 | return 200 <= code <= 299 13 | 14 | 15 | def is_redirect(code): 16 | return 300 <= code <= 399 17 | 18 | 19 | def is_client_error(code): 20 | return 400 <= code <= 499 21 | 22 | 23 | def is_server_error(code): 24 | return 500 <= code <= 599 25 | 26 | 27 | HTTP_100_CONTINUE = 10000 28 | HTTP_101_SWITCHING_PROTOCOLS = 10100 29 | HTTP_200_OK = 20000 30 | HTTP_201_CREATED = 20100 31 | HTTP_202_ACCEPTED = 20200 32 | HTTP_203_NON_AUTHORITATIVE_INFORMATION = 20300 33 | HTTP_204_NO_CONTENT = 20400 34 | HTTP_205_RESET_CONTENT = 20500 35 | HTTP_206_PARTIAL_CONTENT = 20600 36 | HTTP_207_MULTI_STATUS = 20700 37 | HTTP_300_MULTIPLE_CHOICES = 30000 38 | HTTP_301_MOVED_PERMANENTLY = 30100 39 | HTTP_302_FOUND = 30200 40 | HTTP_303_SEE_OTHER = 30300 41 | HTTP_304_NOT_MODIFIED = 30400 42 | HTTP_305_USE_PROXY = 30500 43 | HTTP_306_RESERVED = 30600 44 | HTTP_307_TEMPORARY_REDIRECT = 30700 45 | HTTP_400_BAD_REQUEST = 40000 46 | HTTP_401_UNAUTHORIZED = 40100 47 | HTTP_402_PAYMENT_REQUIRED = 40200 48 | HTTP_403_FORBIDDEN = 40300 49 | HTTP_404_NOT_FOUND = 40400 50 | HTTP_405_METHOD_NOT_ALLOWED = 40500 51 | HTTP_406_NOT_ACCEPTABLE = 40600 52 | HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 40700 53 | HTTP_408_REQUEST_TIMEOUT = 40800 54 | HTTP_409_CONFLICT = 40900 55 | HTTP_410_GONE = 41000 56 | HTTP_411_LENGTH_REQUIRED = 41100 57 | HTTP_412_PRECONDITION_FAILED = 41200 58 | HTTP_413_REQUEST_ENTITY_TOO_LARGE = 41300 59 | HTTP_414_REQUEST_URI_TOO_LONG = 41400 60 | HTTP_415_UNSUPPORTED_MEDIA_TYPE = 41500 61 | HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 41600 62 | HTTP_417_EXPECTATION_FAILED = 41700 63 | HTTP_422_UNPROCESSABLE_ENTITY = 42200 64 | HTTP_423_LOCKED = 42300 65 | HTTP_424_FAILED_DEPENDENCY = 42400 66 | HTTP_428_PRECONDITION_REQUIRED = 42800 67 | HTTP_429_TOO_MANY_REQUESTS = 42900 68 | HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 43100 69 | HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 45100 70 | HTTP_500_INTERNAL_SERVER_ERROR = 50000 71 | HTTP_501_NOT_IMPLEMENTED = 50100 72 | HTTP_502_BAD_GATEWAY = 50200 73 | HTTP_503_SERVICE_UNAVAILABLE = 50300 74 | HTTP_504_GATEWAY_TIMEOUT = 50400 75 | HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 50500 76 | HTTP_507_INSUFFICIENT_STORAGE = 50700 77 | HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 51100 78 | -------------------------------------------------------------------------------- /backend/core/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | # This will make sure the app is always imported when 3 | # Django starts so that shared_task will use this app. 4 | # import pymysql 5 | # 6 | # pymysql.install_as_MySQLdb() 7 | -------------------------------------------------------------------------------- /backend/core/celery.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | from __future__ import absolute_import, unicode_literals 5 | import os 6 | from celery import Celery 7 | # set the default Django settings module for the 'celery' program. 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') 9 | celery_app = Celery('core', result_backend='django-db') 10 | # Using a string here means the worker doesn't have to serialize 11 | # the configuration object to child processes. 12 | # - namespace='CELERY' means all celery-related configuration keys 13 | # should have a `CELERY_` prefix. 14 | celery_app.config_from_object('django.conf:settings', namespace='CELERY') 15 | # Load task modules from all registered Django app configs. 16 | celery_app.autodiscover_tasks() 17 | celery_app.loader.override_backends['django-db'] = 'django_celery_results.backends.database:DatabaseBackend' -------------------------------------------------------------------------------- /backend/core/settings/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | import platform 5 | from .base import * 6 | 7 | os_type = platform.system() 8 | 9 | if os_type == 'Windows': 10 | print('进入 dev ') 11 | from .dev import * 12 | elif os_type == 'Linux': 13 | print('进入 prod ') 14 | from .prod import * 15 | else: 16 | print('进入 mac') 17 | from .mac import * 18 | -------------------------------------------------------------------------------- /backend/core/settings/dev.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | import os 5 | 6 | APP_ENV = 'dev' 7 | 8 | # SECURITY WARNING: keep the secret key used in production secret! 9 | SECRET_KEY = '64318ob@vbou7h50)b0a_pfda4d$bw2nhl4h*m$qo0_e_fxw=658!z*x' 10 | 11 | # SECURITY WARNING: don't run with debug turned on in production! 12 | DEBUG = True 13 | 14 | ALLOWED_HOSTS = ['*'] 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | # sqlite 20 | DATABASES = { 21 | 'default': { 22 | 'ENGINE': 'django.db.backends.sqlite3', 23 | 'NAME': os.path.join(BASE_DIR, '../core.db'), 24 | } 25 | } 26 | 27 | # mysql 28 | # DATABASES = { 29 | # 'default': { 30 | # 'ENGINE': 'django.db.backends.mysql', 31 | # 'NAME': 'one', 32 | # 'USER': 'root', 33 | # 'PASSWORD': 'momo520', 34 | # 'HOST': '1.1.1.11', 35 | # 'OPTIONS': { 36 | # "init_command": "SET foreign_key_checks=0;", 37 | # } 38 | # } 39 | # } 40 | 41 | # 加载 mysql 42 | # import pymysql 43 | # pymysql.install_as_MySQLdb() 44 | -------------------------------------------------------------------------------- /backend/core/settings/mac.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | import os 5 | 6 | APP_ENV = 'dev' 7 | 8 | # SECURITY WARNING: keep the secret key used in production secret! 9 | SECRET_KEY = '64318ob@vbou7h50)b0a_pfda4d$bw2nhl4h*m$qo0_e_fxw=658!z*x' 10 | 11 | # SECURITY WARNING: don't run with debug turned on in production! 12 | DEBUG = True 13 | 14 | ALLOWED_HOSTS = ['*'] 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | # sqlite 20 | DATABASES = { 21 | 'default': { 22 | 'ENGINE': 'django.db.backends.sqlite3', 23 | 'NAME': os.path.join(BASE_DIR, '../core.db'), 24 | } 25 | } 26 | 27 | # mysql 28 | # DATABASES = { 29 | # 'default': { 30 | # 'ENGINE': 'django.db.backends.mysql', 31 | # 'NAME': 'one', 32 | # 'USER': 'root', 33 | # 'PASSWORD': 'momo520', 34 | # 'HOST': '1.1.1.11', 35 | # 'OPTIONS': { 36 | # "init_command": "SET foreign_key_checks=0;", 37 | # } 38 | # } 39 | # } 40 | 41 | # 加载 mysql 42 | # import pymysql 43 | # pymysql.install_as_MySQLdb() 44 | -------------------------------------------------------------------------------- /backend/core/settings/prod.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | import os 5 | 6 | APP_ENV = 'prod' 7 | 8 | # SECURITY WARNING: keep the secret key used in production secret! 9 | SECRET_KEY = '64318ob@vbou7h50)b0a_pfda4d$bw2nhl4h*m$qo0_e_fxw=658!z*x' 10 | 11 | # SECURITY WARNING: don't run with debug turned on in production! 12 | DEBUG = False 13 | 14 | ALLOWED_HOSTS = ['*'] 15 | 16 | # mysql 17 | DATABASES = { 18 | 'default': { 19 | 'ENGINE': 'django.db.backends.mysql', 20 | 'NAME': 'one', 21 | 'USER': 'root', 22 | 'PASSWORD': 'TY%pwd123', 23 | 'HOST': 'localhost', 24 | 'OPTIONS': { 25 | "init_command": "SET foreign_key_checks=0;", 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/core/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: timor 3 | 4 | from django.conf.urls import url, include 5 | from django.conf.urls.static import static 6 | from django.views.generic.base import TemplateView 7 | from core import settings 8 | 9 | urlpatterns = static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + \ 10 | [ 11 | # 工具管理 12 | url(r'api/tool/', include(('tools.urls', 'tools'), namespace="tools")), 13 | # 系统管理 14 | url(r'api/sys/', include(('systems.urls', 'systems'), namespace="systems")), 15 | # 工作流管理 16 | url(r'api/workflow/', include(('workflows.urls', 'workflows'), namespace="workflows")), 17 | # 工单管理 18 | url(r'api/ticket/', include(('tickets.urls', 'tickets'), namespace="tickets")), 19 | # 通知管理 20 | url(r'api/notice/', include(('notices.urls', 'notices'), namespace="notices")), 21 | ] 22 | 23 | if settings.APP_ENV == 'prod': 24 | from rest_framework.documentation import include_docs_urls 25 | 26 | urlpatterns += [ 27 | # api文档 28 | url(r'^docs/', include_docs_urls(title='X Document')), 29 | # 静态模板 30 | url(r'', TemplateView.as_view(template_name="index.html")), 31 | ] 32 | else: 33 | from django.contrib import admin 34 | 35 | urlpatterns += [ 36 | # 管理后台 37 | url(r'^admin/', admin.site.urls), 38 | ] 39 | -------------------------------------------------------------------------------- /backend/core/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for core project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/dev_requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.2.3 2 | certifi==2019.11.28 3 | chardet==3.0.4 4 | coreapi==2.3.3 5 | coreschema==0.0.4 6 | Django==3.0.3 7 | django-cors-headers==3.2.1 8 | django-filter==2.2.0 9 | django-rest-auth==0.9.5 10 | djangorestframework==3.11.0 11 | djangorestframework-jwt==1.11.0 12 | idna==2.8 13 | IPy==1.0 14 | itypes==1.1.0 15 | Jinja2==2.10.3 16 | MarkupSafe==1.1.1 17 | PyMySQL==0.9.3 18 | PyJWT==1.7.1 19 | pytz==2019.3 20 | requests==2.22.0 21 | six==1.14.0 22 | sqlparse==0.3.0 23 | uritemplate==3.0.1 24 | urllib3==1.25.8 25 | -------------------------------------------------------------------------------- /backend/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | apps=(systems tools notices workflows tickets) 4 | rm -rf core.db 5 | for app in ${apps[@]};do 6 | rm -rf $app/migrations 7 | done 8 | 9 | for app in ${apps[@]};do 10 | echo $app 11 | python manage.py makemigrations $app 12 | done 13 | 14 | python manage.py migrate 15 | python manage.py init_sys 16 | python manage.py init_wf 17 | python manage.py init_ticket 18 | python manage.py init_leave 19 | python manage.py runserver -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | import os 5 | import sys 6 | 7 | if __name__ == '__main__': 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') 9 | 10 | # 排错 “The maximum column size is 767 bytes” 11 | from django.db.backends.mysql.schema import DatabaseSchemaEditor 12 | DatabaseSchemaEditor.sql_create_table += " ROW_FORMAT=DYNAMIC" 13 | 14 | try: 15 | from django.core.management import execute_from_command_line 16 | except ImportError as exc: 17 | raise ImportError( 18 | "Couldn't import Django. Are you sure it's installed and " 19 | "available on your PYTHONPATH environment variable? Did you " 20 | "forget to activate a virtual environment?" 21 | ) from exc 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /backend/notices/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/backend/notices/__init__.py -------------------------------------------------------------------------------- /backend/notices/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/backend/notices/management/__init__.py -------------------------------------------------------------------------------- /backend/notices/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/backend/notices/management/commands/__init__.py -------------------------------------------------------------------------------- /backend/notices/management/commands/init_notice.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | from django.core.management.base import BaseCommand, CommandError 5 | from systems.models import * 6 | from systems.menus import init_menu 7 | 8 | 9 | class Command(BaseCommand): 10 | help = '初始化工作流' 11 | 12 | def handle(self, *args, **options): 13 | topmenu = Menu.objects.get(name='top', code='top') 14 | self.stdout.write(self.style.SUCCESS('############ 初始化通知菜单 ###########')) 15 | noticemenu = Menu.objects.create(name='通知管理', code='notice', curl='/notice', icon='notice', sequence=5, type=1, 16 | parent_id=topmenu.id) 17 | menumodel = Menu.objects.create(name='mail通知', code='mail', curl='/mail', icon='mail', sequence=10, type=2, 18 | parent_id=noticemenu.id) 19 | init_menu(menumodel) 20 | menumodel = Menu.objects.create(name='telegram通知', code='telegram', curl='/telegram', icon='telegram', 21 | sequence=20, type=2, parent_id=noticemenu.id) 22 | init_menu(menumodel) 23 | self.stdout.write(self.style.SUCCESS('初始化完成')) 24 | -------------------------------------------------------------------------------- /backend/notices/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.3 on 2021-06-27 00:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='MailBot', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), 19 | ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), 20 | ('memo', models.TextField(blank=True, verbose_name='备注')), 21 | ('type', models.CharField(choices=[('mail', 'mail'), ('telegram', 'telegram')], default=0, max_length=10, verbose_name='通知类型')), 22 | ('name', models.CharField(max_length=112, unique=True, verbose_name='名称')), 23 | ('host', models.CharField(max_length=112, verbose_name='主机')), 24 | ('user', models.CharField(max_length=112, verbose_name='账号')), 25 | ('password', models.CharField(max_length=112, verbose_name='密码')), 26 | ('to', models.CharField(max_length=112, verbose_name='接收者')), 27 | ], 28 | options={ 29 | 'verbose_name': '邮件机器人', 30 | 'verbose_name_plural': '邮件机器人', 31 | }, 32 | ), 33 | migrations.CreateModel( 34 | name='TelegramBot', 35 | fields=[ 36 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 37 | ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), 38 | ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), 39 | ('memo', models.TextField(blank=True, verbose_name='备注')), 40 | ('type', models.CharField(choices=[('mail', 'mail'), ('telegram', 'telegram')], default=0, max_length=10, verbose_name='通知类型')), 41 | ('name', models.CharField(max_length=112, unique=True, verbose_name='名称')), 42 | ('uid', models.CharField(max_length=112, verbose_name='账号id')), 43 | ('token', models.CharField(max_length=112, verbose_name='token')), 44 | ('chat_id', models.CharField(max_length=112, verbose_name='chat_id')), 45 | ], 46 | options={ 47 | 'verbose_name': 'tg机器人', 48 | 'verbose_name_plural': 'tg机器人', 49 | }, 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /backend/notices/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/backend/notices/migrations/__init__.py -------------------------------------------------------------------------------- /backend/notices/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: timor 3 | 4 | from django.db import models 5 | from common.models import BaseModel 6 | 7 | notice_type = { 8 | 'mail': 'mail', 9 | 'telegram': 'telegram', 10 | } 11 | 12 | 13 | class MailBot(BaseModel): 14 | type = models.CharField(max_length=10, choices=tuple(notice_type.items()), default=0, verbose_name='通知类型') 15 | name = models.CharField(max_length=112, unique=True, verbose_name='名称') 16 | host = models.CharField(max_length=112, verbose_name='主机') 17 | user = models.CharField(max_length=112, verbose_name='账号') 18 | password = models.CharField(max_length=112, verbose_name='密码') 19 | to = models.CharField(max_length=112, verbose_name='接收者') 20 | 21 | def __str__(self): 22 | return self.name 23 | 24 | class Meta: 25 | verbose_name = "邮件机器人" 26 | verbose_name_plural = verbose_name 27 | 28 | 29 | class TelegramBot(BaseModel): 30 | type = models.CharField(max_length=10, choices=tuple(notice_type.items()), default=0, verbose_name='通知类型') 31 | name = models.CharField(max_length=112, unique=True, verbose_name='名称') 32 | uid = models.CharField(max_length=112, verbose_name='账号id') 33 | token = models.CharField(max_length=112, verbose_name='token') 34 | chat_id = models.CharField(max_length=112, verbose_name='chat_id') 35 | 36 | def __str__(self): 37 | return self.name 38 | 39 | class Meta: 40 | verbose_name = "tg机器人" 41 | verbose_name_plural = verbose_name 42 | -------------------------------------------------------------------------------- /backend/notices/serializers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: timor 3 | 4 | from notices.models import * 5 | from rest_framework import serializers 6 | 7 | 8 | class MailBotSerializer(serializers.ModelSerializer): 9 | class Meta: 10 | model = MailBot 11 | fields = '__all__' 12 | 13 | 14 | class TelegramBotSerializer(serializers.ModelSerializer): 15 | class Meta: 16 | model = TelegramBot 17 | fields = '__all__' 18 | -------------------------------------------------------------------------------- /backend/notices/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | 5 | from django.conf.urls import url 6 | from rest_framework import routers 7 | from notices.views import NoticeViewSet, MailBotViewSet, TelegramBotViewSet 8 | 9 | router = routers.DefaultRouter() 10 | 11 | router.register(r'notice', NoticeViewSet) 12 | router.register(r'mail', MailBotViewSet) 13 | router.register(r'telegram', TelegramBotViewSet) 14 | 15 | urlpatterns = [ 16 | ] 17 | 18 | urlpatterns += router.urls 19 | -------------------------------------------------------------------------------- /backend/oms.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | project = core 3 | base = /data/app/one/backend 4 | 5 | chdir = %(base) 6 | module = %(project).wsgi:application 7 | 8 | master = true 9 | processes = 5 10 | enable-threads = true 11 | 12 | socket = %(base)/%(project).sock 13 | chmod-socket = 666 14 | vacuum = true 15 | logto = /data/logs/django/one.log 16 | -------------------------------------------------------------------------------- /backend/oms.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=uWSGI instance to serve one-oms 3 | 4 | [Service] 5 | Type=simple 6 | User=root 7 | Group=root 8 | WorkingDirectory=/data/app/one/backend 9 | ExecStart=/root/.pyenv/versions/boce/bin/uwsgi --ini oms.ini --touch-reload=/etc/nginx/uwsgi_params 10 | Restart=on-failure 11 | KillSignal=SIGQUIT 12 | Type=notify 13 | NotifyAccess=all 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.2.3 2 | certifi==2019.11.28 3 | chardet==3.0.4 4 | coreapi==2.3.3 5 | coreschema==0.0.4 6 | Django==3.0.3 7 | django-cors-headers==3.2.1 8 | django-filter==2.2.0 9 | django-rest-auth==0.9.5 10 | djangorestframework==3.11.0 11 | djangorestframework-jwt==1.11.0 12 | idna==2.8 13 | IPy==1.0 14 | itypes==1.1.0 15 | Jinja2==2.10.3 16 | MarkupSafe==1.1.1 17 | python-telegram-bot==12.6.1 18 | mysqlclient==1.4.6 19 | PyJWT==1.7.1 20 | pytz==2019.3 21 | requests==2.22.0 22 | six==1.14.0 23 | sqlparse==0.3.0 24 | uritemplate==3.0.1 25 | urllib3==1.25.8 26 | -------------------------------------------------------------------------------- /backend/systems/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: timor 3 | 4 | -------------------------------------------------------------------------------- /backend/systems/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from systems.models import * 3 | 4 | admin.site.register(Menu) 5 | admin.site.register(Role) 6 | admin.site.register(Group) 7 | admin.site.register(User) 8 | -------------------------------------------------------------------------------- /backend/systems/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/backend/systems/management/__init__.py -------------------------------------------------------------------------------- /backend/systems/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/backend/systems/management/commands/__init__.py -------------------------------------------------------------------------------- /backend/systems/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/backend/systems/migrations/__init__.py -------------------------------------------------------------------------------- /backend/systems/permissions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | 5 | from rest_framework.permissions import BasePermission 6 | from systems.models import * 7 | from itertools import chain 8 | 9 | parse_method_action = { 10 | 'GET': 'view', 11 | 'POST': 'add', 12 | 'PUT': 'change', 13 | 'PATCH': 'change', 14 | 'DELETE': 'delete', 15 | } 16 | 17 | ignore_path = [ 18 | '/api/sys/auth/jwt-token-auth/', 19 | '/api/sys/auth/getuserinfo/', 20 | '/api/sys/auth/getmenubutons/', 21 | ] 22 | 23 | 24 | def check_permission(request, perm): 25 | user = User.objects.get(username=request.user) 26 | 27 | if user.is_admin: 28 | return True 29 | 30 | if request.path in ignore_path: 31 | return True 32 | 33 | user_roles = user.roles.all() 34 | group_roles = user.group.roles.all() 35 | all_roles = sorted(chain(user_roles, group_roles), key=lambda t: t.id, reverse=True) 36 | perms = Permission.objects.filter(role__in=all_roles) 37 | for i in perms: 38 | if i.codename == perm: 39 | return True 40 | 41 | 42 | class IsOwnerRoles(BasePermission): 43 | 44 | def has_permission(self, request, view): 45 | app = view.get_view_name().split() 46 | object = ''.join(app[:-1]).lower() 47 | perm = 'view_{}'.format(object) 48 | return check_permission(request, perm) 49 | 50 | def has_object_permission(self, request, view, obj): 51 | app_label = obj._meta.app_label 52 | model = obj._meta.object_name.lower() 53 | perm = '{}_{}'.format(parse_method_action[request.method], model) 54 | return check_permission(request, perm) 55 | -------------------------------------------------------------------------------- /backend/systems/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | 5 | from django.conf.urls import url, include 6 | from rest_framework import routers 7 | from rest_auth.views import PasswordChangeView 8 | from systems.views import UserViewSet, GroupViewSet, RoleViewSet, PermissionViewSet, MenuViewSet, AuthViewSet, \ 9 | ObtainJSONWebToken 10 | 11 | router = routers.DefaultRouter() 12 | 13 | router.register(r'user', UserViewSet) 14 | router.register(r'group', GroupViewSet) 15 | router.register(r'role', RoleViewSet) 16 | router.register(r'perm', PermissionViewSet) 17 | router.register(r'menu', MenuViewSet) 18 | router.register(r'auth', AuthViewSet) 19 | 20 | urlpatterns = [ 21 | url(r'^auth/changepwd/', PasswordChangeView.as_view(), name='changepwd'), 22 | # token认证 23 | url(r'^auth/jwt-token-auth/', ObtainJSONWebToken.as_view(), name='rest_framework_token'), 24 | url(r'^auth/api-token-auth/', include('rest_framework.urls', namespace='rest_framework')), 25 | ] 26 | 27 | urlpatterns += router.urls 28 | -------------------------------------------------------------------------------- /backend/tickets/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | -------------------------------------------------------------------------------- /backend/tickets/filters.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | from tickets.models import * 5 | from django_filters import rest_framework as filters 6 | 7 | 8 | class TicketFilter(filters.FilterSet): 9 | class Meta: 10 | model = Ticket 11 | 12 | fields = { 13 | 'id': ['exact'], 14 | 'name': ['exact'], 15 | 'participant': ['exact'], 16 | 'create_user__username': ['exact'], 17 | "transition__attribute_type": ['exact', "lt"], 18 | } 19 | -------------------------------------------------------------------------------- /backend/tickets/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/backend/tickets/management/__init__.py -------------------------------------------------------------------------------- /backend/tickets/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/backend/tickets/management/commands/__init__.py -------------------------------------------------------------------------------- /backend/tickets/management/commands/init_ticket.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | from django.core.management.base import BaseCommand, CommandError 5 | from systems.models import * 6 | from systems.menus import init_menu 7 | 8 | 9 | class Command(BaseCommand): 10 | help = '初始化工作流' 11 | 12 | def handle(self, *args, **options): 13 | topmenu = Menu.objects.get(name='top', code='top') 14 | self.stdout.write(self.style.SUCCESS('############ 初始化工单菜单 ###########')) 15 | ticketmenu = Menu.objects.create(name='工单系统', code='ticket', curl='/ticket', icon='ticket', sequence=4, type=1, 16 | parent_id=topmenu.id) 17 | menumodel = Menu.objects.create(name='新建工单', code='new_ticket', curl='/new_ticket', icon='new_ticket', sequence=10, type=2, 18 | parent_id=ticketmenu.id) 19 | init_menu(menumodel) 20 | menumodel = Menu.objects.create(name='编辑工单', code='u_ticket', curl='/u_ticket/:id', icon='u_ticket', sequence=10, type=2, 21 | hidden=True, active_menu='/new_ticket', parent_id=ticketmenu.id) 22 | init_menu(menumodel) 23 | menumodel = Menu.objects.create(name='审批工单', code='s_ticket', curl='/s_ticket/:id', icon='s_ticket', sequence=10, type=2, 24 | hidden=True, active_menu='/todo_ticket', parent_id=ticketmenu.id) 25 | init_menu(menumodel) 26 | menumodel = Menu.objects.create(name='我的工单', code='my_ticket', curl='/my_ticket', icon='my_ticket', sequence=30, type=2, 27 | no_cache=True, parent_id=ticketmenu.id) 28 | init_menu(menumodel) 29 | menumodel = Menu.objects.create(name='我的待办', code='todo_ticket', curl='/todo_ticket', icon='todo_ticket', sequence=40, type=2, 30 | no_cache=True, parent_id=ticketmenu.id) 31 | init_menu(menumodel) 32 | menumodel = Menu.objects.create(name='所有工单', code='all_ticket', curl='/all_ticket', icon='all_ticket', sequence=90, type=2, 33 | no_cache=True, parent_id=ticketmenu.id) 34 | init_menu(menumodel) 35 | self.stdout.write(self.style.SUCCESS('初始化完成')) 36 | -------------------------------------------------------------------------------- /backend/tickets/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/backend/tickets/migrations/__init__.py -------------------------------------------------------------------------------- /backend/tickets/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | 5 | from django.conf.urls import url, include 6 | from rest_framework import routers 7 | from tickets.views import TicketViewSet, TicketFlowLogViewSet, TicketCustomFieldViewSet, TicketUserViewSet 8 | 9 | router = routers.DefaultRouter() 10 | 11 | router.register(r'ticket', TicketViewSet) 12 | router.register('ticketflowlog', TicketFlowLogViewSet) 13 | router.register(r'ticketcustomfield', TicketCustomFieldViewSet) 14 | router.register(r'ticketuser', TicketUserViewSet) 15 | 16 | urlpatterns = [ 17 | ] 18 | 19 | urlpatterns += router.urls 20 | -------------------------------------------------------------------------------- /backend/tickets/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | from tickets.serializers import * 5 | from tickets.filters import * 6 | from common.views import ModelViewSet, FKModelViewSet, JsonResponse, BulkModelMixin 7 | 8 | 9 | class TicketViewSet(BulkModelMixin): 10 | queryset = Ticket.objects.all() 11 | serializer_class = TicketSerializer 12 | filterset_class = TicketFilter 13 | search_fields = ['name'] 14 | ordering_fields = ['state'] 15 | 16 | def get_serializer_class(self): 17 | if self.action in ['list', 'retrieve'] or self.resultData: 18 | return TicketReadSerializer 19 | return TicketSerializer 20 | 21 | def get_queryset(self): 22 | try: 23 | user = User.objects.get(username=self.request.user) 24 | if user.is_admin: 25 | return Ticket.objects.all() 26 | else: 27 | return Ticket.objects.filter(relation__icontains=self.request.user).distinct() 28 | except Exception as e: 29 | print(e) 30 | return Ticket.objects.all() 31 | 32 | 33 | class TicketFlowLogViewSet(BulkModelMixin): 34 | queryset = TicketFlowLog.objects.all() 35 | serializer_class = TicketFlowLogSerializer 36 | search_fields = ['ticket'] 37 | filter_fields = ['ticket', 'state'] 38 | 39 | def get_serializer_class(self): 40 | if self.action in ['list', 'retrieve'] or self.resultData: 41 | return TicketFlowLogReadSerializer 42 | return TicketFlowLogSerializer 43 | 44 | 45 | class TicketCustomFieldViewSet(BulkModelMixin): 46 | queryset = TicketCustomField.objects.all() 47 | serializer_class = TicketCustomFieldSerializer 48 | filter_fields = ['ticket', 'customfield'] 49 | 50 | def get_serializer_class(self): 51 | if self.action in ['list', 'retrieve'] or self.resultData: 52 | return TicketCustomFieldReadSerializer 53 | return TicketCustomFieldSerializer 54 | 55 | 56 | class TicketUserViewSet(BulkModelMixin): 57 | queryset = TicketUser.objects.all() 58 | serializer_class = TicketUserSerializer 59 | search_fields = ['username'] 60 | filter_fields = ['username', 'in_process', 'worked'] 61 | -------------------------------------------------------------------------------- /backend/tools/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: timor 3 | 4 | -------------------------------------------------------------------------------- /backend/tools/filesize.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: timor 3 | 4 | import math 5 | 6 | 7 | def convert_size(size_bytes): 8 | if size_bytes == 0: 9 | return "0B" 10 | size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") 11 | i = int(math.floor(math.log(size_bytes, 1024))) 12 | p = math.pow(1024, i) 13 | s = round(size_bytes / p, 2) 14 | return "%s %s" % (s, size_name[i]) 15 | -------------------------------------------------------------------------------- /backend/tools/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/backend/tools/migrations/__init__.py -------------------------------------------------------------------------------- /backend/tools/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: timor 3 | 4 | from django.db import models 5 | from common.models import BaseModel 6 | from tools.filesize import convert_size 7 | from tools.storage import PathAndRename 8 | import os 9 | 10 | 11 | class Upload(BaseModel): 12 | username = models.CharField(max_length=20, verbose_name=u'上传用户') 13 | file = models.FileField(upload_to=PathAndRename("./"), blank=True, verbose_name=u'上传文件') 14 | archive = models.CharField(max_length=201, default=u'其他', null=True, blank=True, verbose_name=u'文件归档') 15 | filename = models.CharField(max_length=201, null=True, blank=True, verbose_name=u'文件名') 16 | filepath = models.CharField(max_length=201, null=True, blank=True, verbose_name=u'文件路径') 17 | type = models.CharField(max_length=100, null=True, blank=True, verbose_name=u'文件类型') 18 | size = models.CharField(max_length=20, null=True, blank=True, verbose_name=u'文件大小') 19 | 20 | def save(self, *args, **kwargs): 21 | from re import sub 22 | self.size = '{}'.format(convert_size(self.file.size)) 23 | filename = os.path.splitext(self.file.name) 24 | self.filename = '{}-{}{}'.format(sub('\W+', '', filename[0]), self.create_time, filename[1]).replace(' ', '_') 25 | self.filepath = '{}/{}'.format(self.archive, self.filename) 26 | super(Upload, self).save(*args, **kwargs) 27 | 28 | def __str__(self): 29 | return self.filepath 30 | 31 | class Meta: 32 | verbose_name = u'文件上传' 33 | verbose_name_plural = u'文件上传' 34 | 35 | 36 | class FileUpload(models.Model): 37 | file = models.FileField(upload_to=("./tmp"), blank=True, verbose_name=u'上传文件') 38 | 39 | class Meta: 40 | verbose_name = u'文件上传' 41 | verbose_name_plural = u'文件上传' 42 | 43 | 44 | class RequestEvent(BaseModel): 45 | url = models.CharField(max_length=255, null=False, db_index=True, verbose_name='请求URI') 46 | method = models.CharField(max_length=20, null=False, db_index=True, verbose_name='请求方法') 47 | query_string = models.TextField(verbose_name='请求内容') 48 | user = models.CharField(max_length=255, null=True, verbose_name='用户') 49 | remote_ip = models.CharField(max_length=50, null=True, db_index=True, verbose_name='请求IP') 50 | 51 | class Meta: 52 | ordering = ['-create_time'] 53 | verbose_name = '请求事件' 54 | verbose_name_plural = verbose_name 55 | 56 | 57 | class SimpleModel(models.Model): 58 | name = models.CharField(max_length=255, unique=True, verbose_name='名称') 59 | -------------------------------------------------------------------------------- /backend/tools/serializers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: timor 3 | 4 | from rest_framework import serializers 5 | from tools.models import * 6 | 7 | 8 | class UploadSerializer(serializers.ModelSerializer): 9 | class Meta: 10 | model = Upload 11 | fields = '__all__' 12 | 13 | 14 | class FileUploadSerializer(serializers.ModelSerializer): 15 | class Meta: 16 | model = FileUpload 17 | fields = '__all__' 18 | 19 | 20 | class RequestEventSerializer(serializers.ModelSerializer): 21 | class Meta: 22 | model = RequestEvent 23 | fields = '__all__' 24 | 25 | class SimpleSerializer(serializers.ModelSerializer): 26 | class Meta(object): 27 | model = SimpleModel 28 | fields = '__all__' 29 | -------------------------------------------------------------------------------- /backend/tools/storage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: timor 3 | 4 | import os 5 | from django.utils.deconstruct import deconstructible 6 | from re import sub 7 | 8 | 9 | @deconstructible 10 | class PathAndRename(object): 11 | def __init__(self, sub_path): 12 | self.path = sub_path 13 | 14 | def __call__(self, instance, file): 15 | filename = os.path.splitext(file) 16 | last_filename = "%s-%s%s" % (sub('\W+', '', filename[0]), instance.create_time, filename[1]) 17 | return os.path.join(self.path, instance.archive, last_filename) 18 | -------------------------------------------------------------------------------- /backend/tools/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | 5 | from django.conf.urls import url 6 | from rest_framework import routers 7 | from tools.views import UploadViewSet, FileUploadViewSet, RequestEventViewSet, SimpleViewSet 8 | 9 | router = routers.DefaultRouter() 10 | 11 | router.register(r'upload', UploadViewSet) 12 | router.register(r'fileupload', FileUploadViewSet) 13 | router.register(r'audit', RequestEventViewSet) 14 | router.register('simple', SimpleViewSet) 15 | 16 | urlpatterns = [ 17 | ] 18 | 19 | urlpatterns += router.urls 20 | -------------------------------------------------------------------------------- /backend/tools/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: timor 3 | 4 | from rest_framework import viewsets, permissions 5 | from tools.models import * 6 | from tools.serializers import * 7 | from common.views import ModelViewSet, FKModelViewSet, JsonResponse, BulkModelMixin 8 | 9 | 10 | class UploadViewSet(ModelViewSet): 11 | queryset = Upload.objects.all().order_by("-create_time") 12 | serializer_class = UploadSerializer 13 | filter_fields = ('username', 'type',) 14 | 15 | 16 | class FileUploadViewSet(ModelViewSet): 17 | permission_classes = [permissions.AllowAny] 18 | queryset = FileUpload.objects.all() 19 | serializer_class = FileUploadSerializer 20 | 21 | 22 | class RequestEventViewSet(ModelViewSet): 23 | queryset = RequestEvent.objects.all() 24 | serializer_class = RequestEventSerializer 25 | search_fields = ['url', 'query_string', 'user', 'remote_ip'] 26 | filter_fields = ['method'] 27 | 28 | 29 | class SimpleViewSet(BulkModelMixin): 30 | queryset = SimpleModel.objects.all() 31 | serializer_class = SimpleSerializer 32 | permission_classes = [permissions.AllowAny] 33 | filter_fields = ['id', 'name'] 34 | 35 | -------------------------------------------------------------------------------- /backend/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: timor 3 | 4 | -------------------------------------------------------------------------------- /backend/utils/get_realip.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | 5 | import requests 6 | import socket 7 | import json 8 | 9 | #获取外网ip信息 10 | output = requests.get('https://ifconfig.me/all.json').json() 11 | 12 | # 获取本机计算机ip 13 | def get_local_ip(): 14 | try: 15 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 16 | s.connect(('8.8.8.8', 80)) 17 | ip = s.getsockname()[0] 18 | finally: 19 | s.close() 20 | 21 | return ip 22 | 23 | output['local_ip'] = get_local_ip() 24 | 25 | ## output 26 | """ 27 | { 28 | "ip_addr": "203.177.78.226", 29 | "remote_host": "unavailable", 30 | "user_agent": "python-requests/2.22.0", 31 | "port": 38542, 32 | "method": "GET", 33 | "encoding": "gzip, deflate", 34 | "mime": "*/*", 35 | "via": "1.1 google", 36 | "forwarded": "203.177.78.226, 216.239.32.21" 37 | "local_ip": "172.16.51.115" 38 | }""" 39 | 40 | with open('d:/ooxx.log', 'a+') as fn: 41 | print(output) 42 | fn.write(json.dumps(output)) 43 | -------------------------------------------------------------------------------- /backend/utils/index.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: timor 3 | 4 | from datetime import datetime, timedelta 5 | import time 6 | 7 | 8 | def gen_time_pid(prefix): 9 | pid = '{}_{}'.format(prefix, datetime.now().strftime('%Y%m%d%H%M%S') + str(time.time()).replace('.', '')[-3:]) 10 | return pid 11 | 12 | 13 | def diff_times_in_seconds(t1, t2): 14 | h1, m1, s1 = t1.hour, t1.minute, t1.second 15 | h2, m2, s2 = t2.hour, t2.minute, t2.second 16 | t1_secs = s1 + 60 * (m1 + 60 * h1) 17 | t2_secs = s2 + 60 * (m2 + 60 * h2) 18 | tc = str(timedelta(seconds=(t2_secs - t1_secs))) 19 | return tc 20 | 21 | 22 | if __name__ == '__main__': 23 | prefix = 'xxoo' 24 | print(gen_time_pid(prefix)) 25 | -------------------------------------------------------------------------------- /backend/utils/mysql.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | import MySQLdb 5 | 6 | 7 | class MYSQL: 8 | def __init__(self, db, sql): 9 | self.sql = sql 10 | self.conn = MySQLdb.connect( 11 | host=db["host"], 12 | port=db["port"], 13 | user=db["user"], 14 | passwd=db["passwd"], 15 | db=db["db"], 16 | charset='utf8') 17 | self.cursor = self.conn.cursor() 18 | 19 | def insert(self): 20 | self.cursor.execute(self.sql) 21 | self.conn.commit() 22 | self.cursor.close() 23 | self.conn.close() 24 | return True 25 | 26 | def select(self): 27 | self.cursor.execute(self.sql) 28 | alldata = self.cursor.fetchall() 29 | self.cursor.close() 30 | self.conn.close() 31 | return alldata 32 | 33 | def update(self): 34 | self.cursor.execute(self.sql) 35 | self.conn.commit() 36 | self.cursor.close() 37 | self.conn.close() 38 | return True 39 | 40 | 41 | if __name__ == '__main__': 42 | xxljob_info = { 43 | "host": "localhost", 44 | "port": 3306, 45 | "user": "root", 46 | "passwd": "TY%pwd123", 47 | "db": "xxl_job", 48 | } 49 | jobapi = MYSQL(xxljob_info) 50 | sql = "select * from xxl_job_group" 51 | data = jobapi.insert(sql) 52 | rep_data = [] 53 | for item in data: 54 | json_data = { 55 | "id": item[0], 56 | "app_name": item[1], 57 | "title": item[2], 58 | "order": item[3], 59 | "address_type": item[4], 60 | "address_list": item[5], 61 | } 62 | rep_data.append(json_data) 63 | print(rep_data) 64 | -------------------------------------------------------------------------------- /backend/utils/sendmail.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: timor 3 | 4 | import sys 5 | import smtplib 6 | from email.mime.text import MIMEText 7 | from email.mime.multipart import MIMEMultipart 8 | from omsBackend.settings import MAIL_ACOUNT 9 | 10 | # 设置服务器名称、用户名、密码以及邮件后缀 11 | mail_host = MAIL_ACOUNT["mail_host"] 12 | mail_user = MAIL_ACOUNT["mail_user"] 13 | mail_pass = MAIL_ACOUNT["mail_pass"] 14 | mail_postfix = MAIL_ACOUNT["mail_postfix"] 15 | 16 | 17 | # 发送邮件函数 18 | def send_mail(to_list, cc_list, sub, content): 19 | me = mail_user + "<" + mail_user + "@" + mail_postfix + ">" 20 | # f = open(context) 21 | # msg = MIMEText(f.read(),_charset="utf-8") 22 | # f.close() 23 | # msg = MIMEText(context) 24 | msg = MIMEMultipart('alternative') 25 | msg['Subject'] = sub 26 | msg['From'] = me 27 | msg['To'] = to_list 28 | msg['Cc'] = cc_list 29 | list = msg['Cc'].split(',') 30 | list.append(msg['To']) 31 | context = MIMEText(content, _subtype='html', _charset='utf-8') # 解决乱码 32 | msg.attach(context) 33 | try: 34 | send_smtp = smtplib.SMTP() 35 | send_smtp.connect(mail_host, 587) 36 | send_smtp.starttls() 37 | send_smtp.login(mail_user, mail_pass) 38 | 39 | send_smtp.sendmail(me, list, msg.as_string()) 40 | send_smtp.close() 41 | return {"code": 'success', "msg": "通知邮件发送成功"} 42 | except Exception as e: 43 | print(e) 44 | return {"code": 'error', "msg": "通知邮件发送失败"} 45 | 46 | 47 | if __name__ == '__main__': 48 | to_list = sys.argv[1] # 收件人列表 '111@126.com' 49 | cc_list = sys.argv[2] # 抄送人列表 '111@126.com;222@126.com;' 50 | sub = sys.argv[3] 51 | context = sys.argv[4] 52 | if send_mail(to_list, cc_list, sub, context): 53 | print({"code": 'success', "msg": "通知邮件发送成功"}) 54 | else: 55 | print({"code": 'error', "msg": "通知邮件发送失败"}) -------------------------------------------------------------------------------- /backend/utils/sendskype.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | # 登录skype 5 | from skpy import Skype 6 | 7 | # skype账号 8 | SK_ACOUNT = { 9 | 'sk_user': 'itimor@126.com', 10 | 'sk_pass': 'xxx' 11 | } 12 | SK = Skype(SK_ACOUNT["sk_user"], SK_ACOUNT["sk_pass"]) 13 | 14 | 15 | def skype_bot(user, content): 16 | chat = SK.chats[user] 17 | chat.sendMsg(content) 18 | 19 | 20 | if __name__ == '__main__': 21 | skypeid = 'live:dafaricky123' 22 | user = '8:' + skypeid # skypeid 前面需要加 8 23 | skype_bot(user, "hello,gay,你个逗比,不加好友,发你妹的消息啊") 24 | -------------------------------------------------------------------------------- /backend/utils/test.py: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | id=$1 4 | echo $id 5 | cp -r site-10 site-$id 6 | cd site-$id 7 | sed -i "s/10/$id/" * -------------------------------------------------------------------------------- /backend/utils/time.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: timor 3 | 4 | import time 5 | import datetime 6 | 7 | 8 | def utc2local(utc_st): 9 | """ 10 | UTC时间转本地时间 +8:00 11 | """ 12 | now_stamp = time.time() 13 | local_time = datetime.datetime.fromtimestamp(now_stamp) 14 | utc_time = datetime.datetime.utcfromtimestamp(now_stamp) 15 | offset = local_time - utc_time 16 | local_st = utc_st + offset 17 | return local_st 18 | 19 | 20 | def local2utc(local_st): 21 | """ 22 | 本地时间转UTC时间 -8:00 23 | """ 24 | time_struct = time.mktime(local_st.timetuple()) 25 | utc_st = datetime.datetime.utcfromtimestamp(time_struct) 26 | return utc_st 27 | 28 | 29 | # '2015-08-28 16:43:37.283' --> 1440751417.283 30 | # 或者 '2015-08-28 16:43:37' --> 1440751417.0 31 | def string2timestamp(strValue): 32 | try: 33 | d = datetime.datetime.strptime(strValue, "%Y-%m-%d %H:%M:%S.%f") 34 | t = d.timetuple() 35 | timeStamp = int(time.mktime(t)) 36 | timeStamp = float(str(timeStamp) + str("%06d" % d.microsecond)) / 1000000 37 | return int(timeStamp) 38 | except ValueError as e: 39 | d = datetime.datetime.strptime(strValue, "%Y-%m-%d %H:%M:%S") 40 | t = d.timetuple() 41 | timeStamp = int(time.mktime(t)) 42 | timeStamp = float(str(timeStamp) + str("%06d" % d.microsecond)) / 1000000 43 | return int(timeStamp) 44 | 45 | 46 | # 1440751417.283 --> '2015-08-28 16:43:37.283' 47 | def timestamp2string(timeStamp): 48 | try: 49 | d = datetime.datetime.fromtimestamp(timeStamp) 50 | str1 = d.strftime("%Y-%m-%d %H:%M:%S.%f") 51 | # 2015-08-28 16:43:37.283000' 52 | return str1 53 | except Exception as e: 54 | pass 55 | -------------------------------------------------------------------------------- /backend/utils/verifys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # author: itimor 3 | 4 | import re 5 | import IPy 6 | 7 | 8 | def is_valid_domain(value): 9 | domain_pattern = re.compile( 10 | r'^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|' 11 | r'([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|' 12 | r'([a-zA-Z0-9][-_.a-zA-Z0-9]{0,61}[a-zA-Z0-9]))\.' 13 | r'([a-zA-Z]{2,13}|[a-zA-Z0-9-]{2,30}.[a-zA-Z]{2,3})$' 14 | ) 15 | return True if domain_pattern.match(value) else False 16 | 17 | 18 | def is_domain(domain): 19 | domain_regex = re.compile( 20 | r'(?:[A-Z0-9_](?:[A-Z0-9-_]{0,247}[A-Z0-9])?\.)+(?:[A-Z]{2,6}|[A-Z0-9-]{2,}(? Preview at http://localhost:${port}${publicPath}`)) 28 | if (report) { 29 | console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) 30 | } 31 | 32 | }) 33 | } else { 34 | run(`vue-cli-service build ${args}`) 35 | } 36 | -------------------------------------------------------------------------------- /frontend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } -------------------------------------------------------------------------------- /frontend/plop-templates/component/index.hbs: -------------------------------------------------------------------------------- 1 | {{#if template}} 2 | 3 | 4 | 5 | {{/if}} 6 | 7 | {{#if script}} 8 | 20 | {{/if}} 21 | 22 | {{#if style}} 23 | 26 | {{/if}} 27 | -------------------------------------------------------------------------------- /frontend/plop-templates/component/prompt.js: -------------------------------------------------------------------------------- 1 | const { notEmpty } = require('../utils.js') 2 | 3 | module.exports = { 4 | description: 'generate vue component', 5 | prompts: [{ 6 | type: 'input', 7 | name: 'name', 8 | message: 'component name please', 9 | validate: notEmpty('name') 10 | }, 11 | { 12 | type: 'checkbox', 13 | name: 'blocks', 14 | message: 'Blocks:', 15 | choices: [{ 16 | name: '', 17 | value: 'template', 18 | checked: true 19 | }, 20 | { 21 | name: ' 20 | {{/if}} 21 | 22 | {{#if style}} 23 | 26 | {{/if}} 27 | -------------------------------------------------------------------------------- /frontend/plop-templates/view/prompt.js: -------------------------------------------------------------------------------- 1 | const { notEmpty } = require('../utils.js') 2 | 3 | module.exports = { 4 | description: 'generate a view', 5 | prompts: [{ 6 | type: 'input', 7 | name: 'name', 8 | message: 'view name please', 9 | validate: notEmpty('name') 10 | }, 11 | { 12 | type: 'checkbox', 13 | name: 'blocks', 14 | message: 'Blocks:', 15 | choices: [{ 16 | name: '', 17 | value: 'template', 18 | checked: true 19 | }, 20 | { 21 | name: ' 19 | -------------------------------------------------------------------------------- /frontend/src/api/all.js: -------------------------------------------------------------------------------- 1 | import Request from '@/api/common' 2 | 3 | // auth 4 | import * as auths from '@/api/auths' 5 | export const auth = auths 6 | 7 | // systems 8 | export const user = new Request('/sys/user/') 9 | export const group = new Request('/sys/group/') 10 | export const role = new Request('/sys/role/') 11 | export const menu = new Request('/sys/menu/') 12 | export const perm = new Request('/sys/perm/') 13 | 14 | // tools 15 | export const audit = new Request('/tool/audit/') 16 | export const simple = new Request('/tool/simple/') 17 | 18 | // workflows 19 | export const workflowtype = new Request('/workflow/workflowtype/') 20 | export const workflow = new Request('/workflow/workflow/') 21 | export const state = new Request('/workflow/state/') 22 | export const transition = new Request('/workflow/transition/') 23 | export const customfield = new Request('/workflow/customfield/') 24 | 25 | // tickets 26 | export const ticket = new Request('/ticket/ticket/') 27 | export const ticketflowlog = new Request('/ticket/ticketflowlog/') 28 | export const ticketcustomfield = new Request('/ticket/ticketcustomfield/') 29 | export const ticketuser = new Request('/ticket/ticketuser/') 30 | 31 | // notices 32 | export const mail = new Request('/notice/mail/') 33 | export const telegram = new Request('/notice/telegram/') 34 | -------------------------------------------------------------------------------- /frontend/src/api/auths.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function login(data) { 4 | return request({ 5 | url: '/sys/auth/jwt-token-auth/', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | 11 | export function getInfo() { 12 | return request({ 13 | url: '/sys/auth/getuserinfo/', 14 | method: 'get' 15 | }) 16 | } 17 | 18 | export function requestMenuButton(menucode) { 19 | return request({ 20 | url: '/sys/auth/getmenubutons/', 21 | method: 'get', 22 | params: {menucode} 23 | }) 24 | } 25 | 26 | export function changepwd(data) { 27 | return request({ 28 | url: '/sys/auth/changepwd/', 29 | method: 'post', 30 | data 31 | }) 32 | } 33 | 34 | export function getuser_by_group(data) { 35 | return request({ 36 | url: '/sys/user/getuser_by_group/', 37 | method: 'post', 38 | data 39 | }) 40 | } 41 | 42 | export function getuser_by_roles(data) { 43 | return request({ 44 | url: '/sys/user/getuser_by_roles/', 45 | method: 'post', 46 | data 47 | }) 48 | } -------------------------------------------------------------------------------- /frontend/src/api/common.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export default class Request { 4 | constructor(apiurl) { 5 | this.apiurl = apiurl; 6 | } 7 | 8 | requestPost(data) { 9 | return request({ 10 | url: this.apiurl, 11 | method: 'post', 12 | data 13 | }) 14 | } 15 | 16 | requestDelete(id) { 17 | return request({ 18 | url: this.apiurl + id + '/', 19 | method: 'delete' 20 | }) 21 | } 22 | 23 | requestPut(id, data) { 24 | return request({ 25 | url: this.apiurl + id + '/', 26 | method: 'put', 27 | data 28 | }) 29 | } 30 | 31 | requestGet(query) { 32 | return request({ 33 | url: this.apiurl, 34 | method: 'get', 35 | params: query 36 | }) 37 | } 38 | 39 | requestBulkPost(data) { 40 | return request({ 41 | url: this.apiurl + 'bulk_create/', 42 | method: 'post', 43 | data 44 | }) 45 | } 46 | 47 | requestBulkPut(data) { 48 | return request({ 49 | url: this.apiurl + 'bulk_update/', 50 | method: 'put', 51 | data 52 | }) 53 | } 54 | 55 | requestBulkDelete(data) { 56 | return request({ 57 | url: this.apiurl + 'bulk_delete/', 58 | method: 'delete', 59 | data 60 | }) 61 | } 62 | } -------------------------------------------------------------------------------- /frontend/src/assets/401_images/401.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/frontend/src/assets/401_images/401.gif -------------------------------------------------------------------------------- /frontend/src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/frontend/src/assets/404_images/404.png -------------------------------------------------------------------------------- /frontend/src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/frontend/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /frontend/src/assets/custom-theme/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/frontend/src/assets/custom-theme/fonts/element-icons.ttf -------------------------------------------------------------------------------- /frontend/src/assets/custom-theme/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/frontend/src/assets/custom-theme/fonts/element-icons.woff -------------------------------------------------------------------------------- /frontend/src/assets/panel-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/frontend/src/assets/panel-bg.png -------------------------------------------------------------------------------- /frontend/src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ generateTitle(item.meta.title) }} 6 | {{ generateTitle(item.meta.title) }} 7 | 8 | 9 | 10 | 11 | 12 | 71 | 72 | 85 | -------------------------------------------------------------------------------- /frontend/src/components/Charts/mixins/resize.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '@/utils' 2 | 3 | export default { 4 | data() { 5 | return { 6 | $_sidebarElm: null, 7 | $_resizeHandler: null 8 | } 9 | }, 10 | mounted() { 11 | this.initListener() 12 | }, 13 | activated() { 14 | if (!this.$_resizeHandler) { 15 | // avoid duplication init 16 | this.initListener() 17 | } 18 | 19 | // when keep-alive chart activated, auto resize 20 | this.resize() 21 | }, 22 | beforeDestroy() { 23 | this.destroyListener() 24 | }, 25 | deactivated() { 26 | this.destroyListener() 27 | }, 28 | methods: { 29 | // use $_ for mixins properties 30 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 31 | $_sidebarResizeHandler(e) { 32 | if (e.propertyName === 'width') { 33 | this.$_resizeHandler() 34 | } 35 | }, 36 | initListener() { 37 | this.$_resizeHandler = debounce(() => { 38 | this.resize() 39 | }, 100) 40 | window.addEventListener('resize', this.$_resizeHandler) 41 | 42 | this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0] 43 | this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler) 44 | }, 45 | destroyListener() { 46 | window.removeEventListener('resize', this.$_resizeHandler) 47 | this.$_resizeHandler = null 48 | 49 | this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler) 50 | }, 51 | resize() { 52 | const { chart } = this 53 | chart && chart.resize() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /frontend/src/components/Kanban/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ headerText }} 5 | 6 | 12 | 13 | {{ element.name }} {{ element.id }} 14 | 15 | 16 | 17 | 18 | 19 | 54 | 99 | 100 | -------------------------------------------------------------------------------- /frontend/src/components/LangSelect/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 中文 9 | 10 | 11 | English 12 | 13 | 14 | 15 | 16 | 17 | 36 | 37 | -------------------------------------------------------------------------------- /frontend/src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | 92 | 93 | 102 | -------------------------------------------------------------------------------- /frontend/src/components/Screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 50 | 51 | 61 | -------------------------------------------------------------------------------- /frontend/src/components/Share/DropdownMenu.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | {{ item.title }} 7 | {{ item.title }} 8 | 9 | 10 | 11 | 12 | 13 | 39 | 40 | 104 | -------------------------------------------------------------------------------- /frontend/src/components/Sticky/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | sticky 9 | 10 | 11 | 12 | 13 | 14 | 92 | -------------------------------------------------------------------------------- /frontend/src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 47 | 48 | 63 | -------------------------------------------------------------------------------- /frontend/src/directive/clipboard/clipboard.js: -------------------------------------------------------------------------------- 1 | // Inspired by https://github.com/Inndy/vue-clipboard2 2 | const Clipboard = require('clipboard') 3 | if (!Clipboard) { 4 | throw new Error('you should npm install `clipboard` --save at first ') 5 | } 6 | 7 | export default { 8 | bind(el, binding) { 9 | if (binding.arg === 'success') { 10 | el._v_clipboard_success = binding.value 11 | } else if (binding.arg === 'error') { 12 | el._v_clipboard_error = binding.value 13 | } else { 14 | const clipboard = new Clipboard(el, { 15 | text() { return binding.value }, 16 | action() { return binding.arg === 'cut' ? 'cut' : 'copy' } 17 | }) 18 | clipboard.on('success', e => { 19 | const callback = el._v_clipboard_success 20 | callback && callback(e) // eslint-disable-line 21 | }) 22 | clipboard.on('error', e => { 23 | const callback = el._v_clipboard_error 24 | callback && callback(e) // eslint-disable-line 25 | }) 26 | el._v_clipboard = clipboard 27 | } 28 | }, 29 | update(el, binding) { 30 | if (binding.arg === 'success') { 31 | el._v_clipboard_success = binding.value 32 | } else if (binding.arg === 'error') { 33 | el._v_clipboard_error = binding.value 34 | } else { 35 | el._v_clipboard.text = function() { return binding.value } 36 | el._v_clipboard.action = function() { return binding.arg === 'cut' ? 'cut' : 'copy' } 37 | } 38 | }, 39 | unbind(el, binding) { 40 | if (binding.arg === 'success') { 41 | delete el._v_clipboard_success 42 | } else if (binding.arg === 'error') { 43 | delete el._v_clipboard_error 44 | } else { 45 | el._v_clipboard.destroy() 46 | delete el._v_clipboard 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /frontend/src/directive/clipboard/index.js: -------------------------------------------------------------------------------- 1 | import Clipboard from './clipboard' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('Clipboard', Clipboard) 5 | } 6 | 7 | if (window.Vue) { 8 | window.clipboard = Clipboard 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | Clipboard.install = install 13 | export default Clipboard 14 | -------------------------------------------------------------------------------- /frontend/src/directive/waves/index.js: -------------------------------------------------------------------------------- 1 | import waves from './waves' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('waves', waves) 5 | } 6 | 7 | if (window.Vue) { 8 | window.waves = waves 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | waves.install = install 13 | export default waves 14 | -------------------------------------------------------------------------------- /frontend/src/directive/waves/waves.css: -------------------------------------------------------------------------------- 1 | .waves-ripple { 2 | position: absolute; 3 | border-radius: 100%; 4 | background-color: rgba(0, 0, 0, 0.15); 5 | background-clip: padding-box; 6 | pointer-events: none; 7 | -webkit-user-select: none; 8 | -moz-user-select: none; 9 | -ms-user-select: none; 10 | user-select: none; 11 | -webkit-transform: scale(0); 12 | -ms-transform: scale(0); 13 | transform: scale(0); 14 | opacity: 1; 15 | } 16 | 17 | .waves-ripple.z-active { 18 | opacity: 0; 19 | -webkit-transform: scale(2); 20 | -ms-transform: scale(2); 21 | transform: scale(2); 22 | -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 23 | transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 24 | transition: opacity 1.2s ease-out, transform 0.6s ease-out; 25 | transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out; 26 | } -------------------------------------------------------------------------------- /frontend/src/directive/waves/waves.js: -------------------------------------------------------------------------------- 1 | import './waves.css' 2 | 3 | const context = '@@wavesContext' 4 | 5 | function handleClick(el, binding) { 6 | function handle(e) { 7 | const customOpts = Object.assign({}, binding.value) 8 | const opts = Object.assign({ 9 | ele: el, // 波纹作用元素 10 | type: 'hit', // hit 点击位置扩散 center中心点扩展 11 | color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色 12 | }, 13 | customOpts 14 | ) 15 | const target = opts.ele 16 | if (target) { 17 | target.style.position = 'relative' 18 | target.style.overflow = 'hidden' 19 | const rect = target.getBoundingClientRect() 20 | let ripple = target.querySelector('.waves-ripple') 21 | if (!ripple) { 22 | ripple = document.createElement('span') 23 | ripple.className = 'waves-ripple' 24 | ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px' 25 | target.appendChild(ripple) 26 | } else { 27 | ripple.className = 'waves-ripple' 28 | } 29 | switch (opts.type) { 30 | case 'center': 31 | ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px' 32 | ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px' 33 | break 34 | default: 35 | ripple.style.top = 36 | (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop || 37 | document.body.scrollTop) + 'px' 38 | ripple.style.left = 39 | (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft || 40 | document.body.scrollLeft) + 'px' 41 | } 42 | ripple.style.backgroundColor = opts.color 43 | ripple.className = 'waves-ripple z-active' 44 | return false 45 | } 46 | } 47 | 48 | if (!el[context]) { 49 | el[context] = { 50 | removeHandle: handle 51 | } 52 | } else { 53 | el[context].removeHandle = handle 54 | } 55 | 56 | return handle 57 | } 58 | 59 | export default { 60 | bind(el, binding) { 61 | el.addEventListener('click', handleClick(el, binding), false) 62 | }, 63 | update(el, binding) { 64 | el.removeEventListener('click', el[context].removeHandle, false) 65 | el.addEventListener('click', handleClick(el, binding), false) 66 | }, 67 | unbind(el) { 68 | el.removeEventListener('click', el[context].removeHandle, false) 69 | el[context] = null 70 | delete el[context] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/404.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/all_ticket.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/audit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/bug.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/component.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/group.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/international.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/language.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/mail.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/message.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/money.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/my_ticket.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/new_ticket.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/notice.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/rocket.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/role.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/shopping.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/telegram.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/ticket.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/todo_ticket.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/wfset.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/wftype.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/workflow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /frontend/src/lang/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | import Cookies from 'js-cookie' 4 | import elementEnLocale from 'element-ui/lib/locale/lang/en' // element-ui lang 5 | import elementZhLocale from 'element-ui/lib/locale/lang/zh-CN'// element-ui lang 6 | import enLocale from './en' 7 | import zhLocale from './zh' 8 | 9 | Vue.use(VueI18n) 10 | 11 | const messages = { 12 | en: { 13 | ...enLocale, 14 | ...elementEnLocale 15 | }, 16 | zh: { 17 | ...zhLocale, 18 | ...elementZhLocale 19 | } 20 | } 21 | export function getLanguage() { 22 | const chooseLanguage = Cookies.get('language') 23 | if (chooseLanguage) return chooseLanguage 24 | 25 | // if has not choose language 26 | const language = (navigator.language || navigator.browserLanguage).toLowerCase() 27 | const locales = Object.keys(messages) 28 | for (const locale of locales) { 29 | if (language.indexOf(locale) > -1) { 30 | return locale 31 | } 32 | } 33 | return 'zh' 34 | } 35 | const i18n = new VueI18n({ 36 | // set locale 37 | // options: en | zh | es 38 | locale: getLanguage(), 39 | // set locale messages 40 | messages, 41 | silentTranslationWarn: true 42 | }) 43 | 44 | export default i18n 45 | -------------------------------------------------------------------------------- /frontend/src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 24 | 25 | 49 | 50 | 58 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 37 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ title }} 7 | 8 | 9 | 10 | {{ title }} 11 | 12 | 13 | 14 | 15 | 16 | 37 | 38 | 87 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 55 | -------------------------------------------------------------------------------- /frontend/src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as AppMain } from './AppMain' 2 | export { default as Navbar } from './Navbar' 3 | export { default as Settings } from './Settings' 4 | export { default as Sidebar } from './Sidebar/index.vue' 5 | export { default as TagsView } from './TagsView/index.vue' 6 | -------------------------------------------------------------------------------- /frontend/src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import Cookies from 'js-cookie' 4 | 5 | import 'normalize.css/normalize.css' // a modern alternative to CSS resets 6 | 7 | import Element from 'element-ui' 8 | import './styles/element-variables.scss' 9 | 10 | import '@/styles/index.scss' // global css 11 | 12 | import App from './App' 13 | import store from './store' 14 | import router from './router' 15 | 16 | import i18n from './lang' // Internationalization 17 | import './icons' // icon 18 | import './permission' // permission control 19 | 20 | import * as filters from './filters' // global filters 21 | 22 | Vue.use(Element, { 23 | size: Cookies.get('size') || 'medium', // set element-ui default size 24 | i18n: (key, value) => i18n.t(key, value) 25 | }) 26 | 27 | // register global utility filters 28 | Object.keys(filters).forEach(key => { 29 | Vue.filter(key, filters[key]) 30 | }) 31 | 32 | Vue.config.productionTip = false 33 | 34 | new Vue({ 35 | el: '#app', 36 | router, 37 | store, 38 | i18n, 39 | render: h => h(App) 40 | }) 41 | -------------------------------------------------------------------------------- /frontend/src/router/modules/components.js: -------------------------------------------------------------------------------- 1 | /** When your routing table is too long, you can split it into small modules **/ 2 | 3 | import Layout from '@/layout' 4 | 5 | const componentsRouter = { 6 | path: '/omponents-demo', 7 | component: Layout, 8 | redirect: 'noRedirect', 9 | name: 'ComponentDemo', 10 | meta: { 11 | title: 'ComponentDemo', 12 | icon: 'component' 13 | }, 14 | children: [ 15 | { 16 | path: 'clipboard', 17 | component: () => import('@/views/components-demo/clipboard'), 18 | name: 'clipboard', 19 | meta: { title: 'clipboard' } 20 | }, 21 | { 22 | path: 'sticky', 23 | component: () => import('@/views/components-demo/sticky'), 24 | name: 'StickyDemo', 25 | meta: { title: 'Sticky' } 26 | }, 27 | { 28 | path: 'mixin', 29 | component: () => import('@/views/components-demo/mixin'), 30 | name: 'ComponentMixinDemo', 31 | meta: { title: 'Component Mixin' } 32 | }, 33 | { 34 | path: 'dnd-list', 35 | component: () => import('@/views/components-demo/dnd-list'), 36 | name: 'DndListDemo', 37 | meta: { title: 'Dnd List' } 38 | }, 39 | { 40 | path: 'drag-kanban', 41 | component: () => import('@/views/components-demo/drag-kanban'), 42 | name: 'DragKanbanDemo', 43 | meta: { title: 'Drag Kanban' } 44 | } 45 | ] 46 | } 47 | 48 | export default componentsRouter 49 | -------------------------------------------------------------------------------- /frontend/src/settings.js: -------------------------------------------------------------------------------- 1 | // import i18n from '@/lang' 2 | 3 | module.exports = { 4 | // title: i18n.t('systemTitle'), 5 | title: '后台管理系统', 6 | 7 | /** 8 | * @type {boolean} true | false 9 | * @description Whether show the settings right-panel 10 | */ 11 | showSettings: true, 12 | 13 | /** 14 | * @type {boolean} true | false 15 | * @description Whether need tagsView 16 | */ 17 | tagsView: true, 18 | 19 | /** 20 | * @type {boolean} true | false 21 | * @description Whether fix the header 22 | */ 23 | fixedHeader: true, 24 | 25 | /** 26 | * @type {boolean} true | false 27 | * @description Whether show the logo in sidebar 28 | */ 29 | sidebarLogo: true, 30 | 31 | /** 32 | * @type {string | array} 'production' | ['production', 'development'] 33 | * @description Need show err logs component. 34 | * The default is only used in the production env 35 | * If you want to also use it in dev, you can pass ['production', 'development'] 36 | */ 37 | errorLog: 'production' 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | language: state => state.app.language, 4 | size: state => state.app.size, 5 | device: state => state.app.device, 6 | visitedViews: state => state.tagsView.visitedViews, 7 | cachedViews: state => state.tagsView.cachedViews, 8 | token: state => state.user.token, 9 | avatar: state => state.user.avatar, 10 | username: state => state.user.username, 11 | user_id: state => state.user.user_id, 12 | roles: state => state.user.roles, 13 | ip: state => state.user.ip, 14 | permission_routes: state => state.permission.routes, 15 | addRoutes: state => state.permission.addRoutes 16 | } 17 | export default getters -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | 5 | Vue.use(Vuex) 6 | 7 | // https://webpack.js.org/guides/dependency-management/#requirecontext 8 | const modulesFiles = require.context('./modules', true, /\.js$/) 9 | 10 | // you do not need `import app from './modules/app'` 11 | // it will auto require all vuex module from modules file 12 | const modules = modulesFiles.keys().reduce((modules, modulePath) => { 13 | // set './app.js' => 'app' 14 | const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') 15 | const value = modulesFiles(modulePath) 16 | modules[moduleName] = value.default 17 | return modules 18 | }, {}) 19 | 20 | const store = new Vuex.Store({ 21 | modules, 22 | getters 23 | }) 24 | 25 | export default store 26 | -------------------------------------------------------------------------------- /frontend/src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | import { getLanguage } from '@/lang/index' 3 | 4 | const state = { 5 | sidebar: { 6 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 7 | withoutAnimation: false 8 | }, 9 | device: 'desktop', 10 | language: getLanguage(), 11 | size: Cookies.get('size') || 'medium' 12 | } 13 | 14 | const mutations = { 15 | TOGGLE_SIDEBAR: state => { 16 | state.sidebar.opened = !state.sidebar.opened 17 | state.sidebar.withoutAnimation = false 18 | if (state.sidebar.opened) { 19 | Cookies.set('sidebarStatus', 1) 20 | } else { 21 | Cookies.set('sidebarStatus', 0) 22 | } 23 | }, 24 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 25 | Cookies.set('sidebarStatus', 0) 26 | state.sidebar.opened = false 27 | state.sidebar.withoutAnimation = withoutAnimation 28 | }, 29 | TOGGLE_DEVICE: (state, device) => { 30 | state.device = device 31 | }, 32 | SET_LANGUAGE: (state, language) => { 33 | state.language = language 34 | Cookies.set('language', language) 35 | }, 36 | SET_SIZE: (state, size) => { 37 | state.size = size 38 | Cookies.set('size', size) 39 | } 40 | } 41 | 42 | const actions = { 43 | toggleSideBar({ commit }) { 44 | commit('TOGGLE_SIDEBAR') 45 | }, 46 | closeSideBar({ commit }, { withoutAnimation }) { 47 | commit('CLOSE_SIDEBAR', withoutAnimation) 48 | }, 49 | toggleDevice({ commit }, device) { 50 | commit('TOGGLE_DEVICE', device) 51 | }, 52 | setLanguage({ commit }, language) { 53 | commit('SET_LANGUAGE', language) 54 | }, 55 | setSize({ commit }, size) { 56 | commit('SET_SIZE', size) 57 | } 58 | } 59 | 60 | export default { 61 | namespaced: true, 62 | state, 63 | mutations, 64 | actions 65 | } 66 | -------------------------------------------------------------------------------- /frontend/src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import variables from '@/styles/element-variables.scss' 2 | import defaultSettings from '@/settings' 3 | 4 | const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings 5 | 6 | const state = { 7 | theme: variables.theme, 8 | showSettings: showSettings, 9 | tagsView: tagsView, 10 | fixedHeader: fixedHeader, 11 | sidebarLogo: sidebarLogo 12 | } 13 | 14 | const mutations = { 15 | CHANGE_SETTING: (state, { key, value }) => { 16 | if (state.hasOwnProperty(key)) { 17 | state[key] = value 18 | } 19 | } 20 | } 21 | 22 | const actions = { 23 | changeSetting({ commit }, data) { 24 | commit('CHANGE_SETTING', data) 25 | } 26 | } 27 | 28 | export default { 29 | namespaced: true, 30 | state, 31 | mutations, 32 | actions 33 | } 34 | 35 | -------------------------------------------------------------------------------- /frontend/src/styles/btn.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | @mixin colorBtn($color) { 4 | background: $color; 5 | 6 | &:hover { 7 | color: $color; 8 | 9 | &:before, 10 | &:after { 11 | background: $color; 12 | } 13 | } 14 | } 15 | 16 | .blue-btn { 17 | @include colorBtn($blue) 18 | } 19 | 20 | .light-blue-btn { 21 | @include colorBtn($light-blue) 22 | } 23 | 24 | .red-btn { 25 | @include colorBtn($red) 26 | } 27 | 28 | .pink-btn { 29 | @include colorBtn($pink) 30 | } 31 | 32 | .green-btn { 33 | @include colorBtn($green) 34 | } 35 | 36 | .tiffany-btn { 37 | @include colorBtn($tiffany) 38 | } 39 | 40 | .yellow-btn { 41 | @include colorBtn($yellow) 42 | } 43 | 44 | .pan-btn { 45 | font-size: 14px; 46 | color: #fff; 47 | padding: 14px 36px; 48 | border-radius: 8px; 49 | border: none; 50 | outline: none; 51 | transition: 600ms ease all; 52 | position: relative; 53 | display: inline-block; 54 | width: 100%; 55 | text-align: center; 56 | 57 | &:hover { 58 | background: #fff; 59 | 60 | &:before, 61 | &:after { 62 | width: 100%; 63 | transition: 600ms ease all; 64 | } 65 | } 66 | 67 | &:before, 68 | &:after { 69 | content: ''; 70 | position: absolute; 71 | top: 0; 72 | right: 0; 73 | height: 2px; 74 | width: 0; 75 | transition: 400ms ease all; 76 | } 77 | 78 | &::after { 79 | right: inherit; 80 | top: inherit; 81 | left: 0; 82 | bottom: 0; 83 | } 84 | } 85 | 86 | .custom-button { 87 | display: inline-block; 88 | line-height: 1; 89 | white-space: nowrap; 90 | cursor: pointer; 91 | background: #fff; 92 | color: #fff; 93 | -webkit-appearance: none; 94 | text-align: center; 95 | box-sizing: border-box; 96 | outline: 0; 97 | margin: 0; 98 | padding: 10px 15px; 99 | font-size: 14px; 100 | border-radius: 4px; 101 | } 102 | -------------------------------------------------------------------------------- /frontend/src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | .cell { 19 | .el-tag { 20 | margin-right: 0px; 21 | } 22 | } 23 | 24 | .small-padding { 25 | .cell { 26 | padding-left: 5px; 27 | padding-right: 5px; 28 | } 29 | } 30 | 31 | .fixed-width { 32 | .el-button--mini { 33 | padding: 7px 10px; 34 | width: 60px; 35 | } 36 | } 37 | 38 | .status-col { 39 | .cell { 40 | padding: 0 10px; 41 | text-align: center; 42 | 43 | .el-tag { 44 | margin-right: 0px; 45 | } 46 | } 47 | } 48 | 49 | // to fixed https://github.com/ElemeFE/element/issues/2461 50 | .el-dialog { 51 | transform: none; 52 | left: 0; 53 | position: relative; 54 | margin: 0 auto; 55 | } 56 | 57 | // refine element ui upload 58 | .upload-container { 59 | .el-upload { 60 | width: 100%; 61 | 62 | .el-upload-dragger { 63 | width: 100%; 64 | height: 200px; 65 | } 66 | } 67 | } 68 | 69 | // dropdown 70 | .el-dropdown-menu { 71 | a { 72 | display: block 73 | } 74 | } 75 | 76 | // fix date-picker ui bug in filter-item 77 | .el-range-editor.el-input__inner { 78 | display: inline-flex !important; 79 | } 80 | 81 | // to fix el-date-picker css style 82 | .el-range-separator { 83 | box-sizing: content-box; 84 | } 85 | 86 | .el-transfer__buttons { 87 | padding: 0 5px!important; 88 | } 89 | 90 | .card-solt { 91 | width: 450px; 92 | } 93 | 94 | .card-span-title { 95 | font-family: "STXingkai", sans-serif; 96 | font-weight: 900; 97 | font-size: 1.8em; 98 | } 99 | 100 | .el-card { 101 | margin-bottom: 10px; 102 | .card-title { 103 | font-weight: 700; 104 | } 105 | .el-card__body{ 106 | padding: 5px 20px; 107 | } 108 | } -------------------------------------------------------------------------------- /frontend/src/styles/element-variables.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * I think element-ui's default theme color is too light for long-term use. 3 | * So I modified the default color and you can modify it to your liking. 4 | **/ 5 | 6 | /* theme color */ 7 | $--color-primary: #1890ff; 8 | $--color-success: #13ce66; 9 | $--color-warning: #FFBA00; 10 | $--color-danger: #ff4949; 11 | $--color-info: #1E1E1E; 12 | 13 | $--button-font-weight: 400; 14 | 15 | $--color-text-regular: #1f2d3d; 16 | 17 | $--border-color-light: #dfe4ed; 18 | $--border-color-lighter: #e6ebf5; 19 | 20 | $--table-border:1px solid#dfe6ec; 21 | 22 | /* icon font path, required */ 23 | $--font-path: '~element-ui/lib/theme-chalk/fonts'; 24 | 25 | @import "~element-ui/packages/theme-chalk/src/index"; 26 | 27 | // the :export directive is the magic sauce for webpack 28 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 29 | :export { 30 | theme: $--color-primary; 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | @mixin pct($pct) { 31 | width: #{$pct}; 32 | position: relative; 33 | margin: 0 auto; 34 | } 35 | 36 | @mixin triangle($width, $height, $color, $direction) { 37 | $width: $width/2; 38 | $color-border-style: $height solid $color; 39 | $transparent-border-style: $width solid transparent; 40 | height: 0; 41 | width: 0; 42 | 43 | @if $direction==up { 44 | border-bottom: $color-border-style; 45 | border-left: $transparent-border-style; 46 | border-right: $transparent-border-style; 47 | } 48 | 49 | @else if $direction==right { 50 | border-left: $color-border-style; 51 | border-top: $transparent-border-style; 52 | border-bottom: $transparent-border-style; 53 | } 54 | 55 | @else if $direction==down { 56 | border-top: $color-border-style; 57 | border-left: $transparent-border-style; 58 | border-right: $transparent-border-style; 59 | } 60 | 61 | @else if $direction==left { 62 | border-right: $color-border-style; 63 | border-top: $transparent-border-style; 64 | border-bottom: $transparent-border-style; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /frontend/src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // base color 2 | $blue:#324157; 3 | $light-blue:#3A71A8; 4 | $red:#C03639; 5 | $pink: #E65D6E; 6 | $green: #30B08F; 7 | $tiffany: #4AB7BD; 8 | $yellow:#FEC171; 9 | $panGreen: #30B08F; 10 | 11 | // sidebar 12 | $menuText:#bfcbd9; 13 | $menuActiveText:#409EFF; 14 | $subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951 15 | 16 | $menuBg:#304156; 17 | $menuHover:#263445; 18 | 19 | $subMenuBg:#1f2d3d; 20 | $subMenuHover:#001528; 21 | 22 | $sideBarWidth: 210px; 23 | 24 | // the :export directive is the magic sauce for webpack 25 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 26 | :export { 27 | menuText: $menuText; 28 | menuActiveText: $menuActiveText; 29 | subMenuActiveText: $subMenuActiveText; 30 | menuBg: $menuBg; 31 | menuHover: $menuHover; 32 | subMenuBg: $subMenuBg; 33 | subMenuHover: $subMenuHover; 34 | sideBarWidth: $sideBarWidth; 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'Admin-Token' 4 | 5 | export function getToken() { 6 | return Cookies.get(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookies.set(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return Cookies.remove(TokenKey) 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/utils/clipboard.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Clipboard from 'clipboard' 3 | 4 | function clipboardSuccess() { 5 | Vue.prototype.$message({ 6 | message: 'Copy successfully', 7 | type: 'success', 8 | duration: 1500 9 | }) 10 | } 11 | 12 | function clipboardError() { 13 | Vue.prototype.$message({ 14 | message: 'Copy failed', 15 | type: 'error' 16 | }) 17 | } 18 | 19 | export default function handleClipboard(text, event) { 20 | const clipboard = new Clipboard(event.target, { 21 | text: () => text 22 | }) 23 | clipboard.on('success', () => { 24 | clipboardSuccess() 25 | clipboard.destroy() 26 | }) 27 | clipboard.on('error', () => { 28 | clipboardError() 29 | clipboard.destroy() 30 | }) 31 | clipboard.onClick(event) 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || 'Vue Element Admin' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} | ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/utils/i18n.js: -------------------------------------------------------------------------------- 1 | // translate router.meta.title, be used in breadcrumb sidebar tagsview 2 | export function generateTitle(title) { 3 | const hasKey = this.$te('route.' + title) 4 | 5 | if (hasKey) { 6 | // $t :this method from vue-i18n, inject in @/lang/index.js 7 | const translatedTitle = this.$t('route.' + title) 8 | 9 | return translatedTitle 10 | } 11 | return title 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/utils/open-window.js: -------------------------------------------------------------------------------- 1 | /** 2 | *Created by PanJiaChen on 16/11/29. 3 | * @param {Sting} url 4 | * @param {Sting} title 5 | * @param {Number} w 6 | * @param {Number} h 7 | */ 8 | export default function openWindow(url, title, w, h) { 9 | // Fixes dual-screen position Most browsers Firefox 10 | const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left 11 | const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top 12 | 13 | const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width 14 | const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height 15 | 16 | const left = ((width / 2) - (w / 2)) + dualScreenLeft 17 | const top = ((height / 2) - (h / 2)) + dualScreenTop 18 | const newWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left) 19 | 20 | // Puts focus on the newWindow 21 | if (window.focus) { 22 | newWindow.focus() 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /frontend/src/utils/permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | /** 4 | * @param {Array} value 5 | * @returns {Boolean} 6 | * @example see @/views/permission/directive.vue 7 | */ 8 | export default function checkPermission(value) { 9 | if (value && value instanceof Array && value.length > 0) { 10 | const roles = store.getters && store.getters.roles 11 | const permissionRoles = value 12 | 13 | const hasPermission = roles.some(role => { 14 | return permissionRoles.includes(role) 15 | }) 16 | 17 | if (!hasPermission) { 18 | return false 19 | } 20 | return true 21 | } else { 22 | console.error(`need roles! Like v-permission="['admin','editor']"`) 23 | return false 24 | } 25 | } 26 | 27 | /** 28 | * @param {Array} arr ['add','del','view','update'] 29 | * @param {String} value 'add' 30 | * @returns {Boolean} 31 | * @example see @/views/permission/directive.vue 32 | */ 33 | export function checkAuth(arr, value) { 34 | const permissionarr = arr 35 | const permissionvalue = value 36 | const hasPermission = permissionarr.includes(permissionvalue) 37 | if (!hasPermission) { 38 | return false 39 | } 40 | return true 41 | } 42 | export function checkAuthAdd(arr) { 43 | return checkAuth(arr, 'add') 44 | } 45 | export function checkAuthDel(arr) { 46 | return checkAuth(arr, 'del') 47 | } 48 | export function checkAuthView(arr) { 49 | return checkAuth(arr, 'view') 50 | } 51 | export function checkAuthUpdate(arr) { 52 | return checkAuth(arr, 'update') 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MessageBox, Message } from 'element-ui' 3 | import store from '@/store' 4 | import { getToken } from '@/utils/auth' 5 | 6 | // create an axios instance 7 | const service = axios.create({ 8 | baseURL: process.env.VUE_APP_BASE_API, // api 的 base_url 9 | withCredentials: true, // 跨域请求时发送 cookies 10 | timeout: 500000 // request timeout 11 | }) 12 | 13 | // request interceptor 14 | service.interceptors.request.use( 15 | config => { 16 | // do something before request is sent 17 | 18 | if (store.getters.token) { 19 | // let each request carry token 20 | // ['X-Token'] is a custom headers key 21 | // please modify it according to the actual situation 22 | config.headers['Authorization'] = 'core ' + getToken() 23 | } 24 | return config 25 | }, 26 | error => { 27 | // do something with request error 28 | console.log(error) // for debug 29 | return Promise.reject(error) 30 | } 31 | ) 32 | 33 | // response interceptor 34 | service.interceptors.response.use( 35 | /** 36 | * If you want to get http information such as headers or status 37 | * Please return response => response 38 | */ 39 | 40 | /** 41 | * Determine the request status by custom code 42 | * Here is just an example 43 | * You can also judge the status by HTTP Status Code 44 | */ 45 | response => { 46 | const res = response.data 47 | 48 | // if the custom code is not 20000, it is judged as an error. 49 | if (res.code !== 20000) { 50 | Message({ 51 | message: res.message || 'Error', 52 | type: 'error', 53 | duration: 5 * 1000 54 | }) 55 | 56 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; 57 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 58 | // to re-login 59 | MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', { 60 | confirmButtonText: 'Re-Login', 61 | cancelButtonText: 'Cancel', 62 | type: 'warning' 63 | }).then(() => { 64 | store.dispatch('user/resetToken').then(() => { 65 | location.reload() 66 | }) 67 | }) 68 | } 69 | return Promise.reject(new Error(res.message || 'Error')) 70 | } else { 71 | return res 72 | } 73 | }, 74 | error => { 75 | console.log('err' + error) // for debug 76 | Message({ 77 | message: error.message, 78 | type: 'error', 79 | duration: 5 * 1000 80 | }) 81 | return Promise.reject(error) 82 | } 83 | ) 84 | 85 | export default service 86 | -------------------------------------------------------------------------------- /frontend/src/utils/scroll-to.js: -------------------------------------------------------------------------------- 1 | Math.easeInOutQuad = function(t, b, c, d) { 2 | t /= d / 2 3 | if (t < 1) { 4 | return c / 2 * t * t + b 5 | } 6 | t-- 7 | return -c / 2 * (t * (t - 2) - 1) + b 8 | } 9 | 10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts 11 | var requestAnimFrame = (function() { 12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } 13 | })() 14 | 15 | /** 16 | * Because it's so fucking difficult to detect the scrolling element, just move them all 17 | * @param {number} amount 18 | */ 19 | function move(amount) { 20 | document.documentElement.scrollTop = amount 21 | document.body.parentNode.scrollTop = amount 22 | document.body.scrollTop = amount 23 | } 24 | 25 | function position() { 26 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop 27 | } 28 | 29 | /** 30 | * @param {number} to 31 | * @param {number} duration 32 | * @param {Function} callback 33 | */ 34 | export function scrollTo(to, duration, callback) { 35 | const start = position() 36 | const change = to - start 37 | const increment = 20 38 | let currentTime = 0 39 | duration = (typeof (duration) === 'undefined') ? 500 : duration 40 | var animateScroll = function() { 41 | // increment the time 42 | currentTime += increment 43 | // find the value with the quadratic in-out easing function 44 | var val = Math.easeInOutQuad(currentTime, start, change, duration) 45 | // move the document.body 46 | move(val) 47 | // do the animation unless its over 48 | if (currentTime < duration) { 49 | requestAnimFrame(animateScroll) 50 | } else { 51 | if (callback && typeof (callback) === 'function') { 52 | // the animation is done so lets callback 53 | callback() 54 | } 55 | } 56 | } 57 | animateScroll() 58 | } 59 | -------------------------------------------------------------------------------- /frontend/src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * @param {string} path 7 | * @returns {Boolean} 8 | */ 9 | export function isExternal(path) { 10 | return /^(https?:|mailto:|tel:)/.test(path) 11 | } 12 | 13 | /** 14 | * @param {string} str 15 | * @returns {Boolean} 16 | */ 17 | export function validUsername(str) { 18 | const valid_map = ['admin', 'editor'] 19 | return valid_map.indexOf(str.trim()) >= 0 20 | } 21 | 22 | /** 23 | * @param {string} url 24 | * @returns {Boolean} 25 | */ 26 | export function validURL(url) { 27 | const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ 28 | return reg.test(url) 29 | } 30 | 31 | /** 32 | * @param {string} str 33 | * @returns {Boolean} 34 | */ 35 | export function validLowerCase(str) { 36 | const reg = /^[a-z]+$/ 37 | return reg.test(str) 38 | } 39 | 40 | /** 41 | * @param {string} str 42 | * @returns {Boolean} 43 | */ 44 | export function validUpperCase(str) { 45 | const reg = /^[A-Z]+$/ 46 | return reg.test(str) 47 | } 48 | 49 | /** 50 | * @param {string} str 51 | * @returns {Boolean} 52 | */ 53 | export function validAlphabets(str) { 54 | const reg = /^[A-Za-z]+$/ 55 | return reg.test(str) 56 | } 57 | 58 | /** 59 | * @param {string} email 60 | * @returns {Boolean} 61 | */ 62 | export function validEmail(email) { 63 | const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 64 | return reg.test(email) 65 | } 66 | 67 | /** 68 | * @param {string} str 69 | * @returns {Boolean} 70 | */ 71 | export function isString(str) { 72 | if (typeof str === 'string' || str instanceof String) { 73 | return true 74 | } 75 | return false 76 | } 77 | 78 | /** 79 | * @param {Array} arg 80 | * @returns {Boolean} 81 | */ 82 | export function isArray(arg) { 83 | if (typeof Array.isArray === 'undefined') { 84 | return Object.prototype.toString.call(arg) === '[object Array]' 85 | } 86 | return Array.isArray(arg) 87 | } 88 | -------------------------------------------------------------------------------- /frontend/src/vendor/Export2Zip.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { saveAs } from 'file-saver' 3 | import JSZip from 'jszip' 4 | 5 | export function export_txt_to_zip(th, jsonData, txtName, zipName) { 6 | const zip = new JSZip() 7 | const txt_name = txtName || 'file' 8 | const zip_name = zipName || 'file' 9 | const data = jsonData 10 | let txtData = `${th}\r\n` 11 | data.forEach((row) => { 12 | let tempStr = '' 13 | tempStr = row.toString() 14 | txtData += `${tempStr}\r\n` 15 | }) 16 | zip.file(`${txt_name}.txt`, txtData) 17 | zip.generateAsync({ 18 | type: "blob" 19 | }).then((blob) => { 20 | saveAs(blob, `${zip_name}.zip`) 21 | }, (err) => { 22 | alert('导出失败') 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/views/components-demo/clipboard.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | copy 8 | 9 | 10 | 11 | 12 | 13 | copy 14 | 15 | 16 | 17 | 18 | 19 | 20 | 49 | 50 | -------------------------------------------------------------------------------- /frontend/src/views/components-demo/dnd-list.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 47 | 48 | -------------------------------------------------------------------------------- /frontend/src/views/components-demo/drag-kanban.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 39 | 66 | 67 | -------------------------------------------------------------------------------- /frontend/src/views/dashboard/components/BarChart.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 103 | -------------------------------------------------------------------------------- /frontend/src/views/dashboard/components/PieChart.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 80 | -------------------------------------------------------------------------------- /frontend/src/views/dashboard/components/TodoList/Todo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 22 | 23 | 24 | 25 | 82 | -------------------------------------------------------------------------------- /frontend/src/views/dashboard/components/TransactionTable.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ row.order_no | orderNoFilter }} 5 | 6 | 7 | ¥{{ row.price | toThousandFilter }} 8 | 9 | 10 | 11 | {{ row.status }} 12 | 13 | 14 | 15 | 16 | 17 | 45 | -------------------------------------------------------------------------------- /frontend/src/views/dashboard/components/mixins/resize.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '@/utils' 2 | 3 | export default { 4 | data() { 5 | return { 6 | $_sidebarElm: null, 7 | $_resizeHandler: null 8 | } 9 | }, 10 | mounted() { 11 | this.$_resizeHandler = debounce(() => { 12 | if (this.chart) { 13 | this.chart.resize() 14 | } 15 | }, 100) 16 | this.$_initResizeEvent() 17 | this.$_initSidebarResizeEvent() 18 | }, 19 | beforeDestroy() { 20 | this.$_destroyResizeEvent() 21 | this.$_destroySidebarResizeEvent() 22 | }, 23 | // to fixed bug when cached by keep-alive 24 | // https://github.com/PanJiaChen/vue-element-admin/issues/2116 25 | activated() { 26 | this.$_initResizeEvent() 27 | this.$_initSidebarResizeEvent() 28 | }, 29 | deactivated() { 30 | this.$_destroyResizeEvent() 31 | this.$_destroySidebarResizeEvent() 32 | }, 33 | methods: { 34 | // use $_ for mixins properties 35 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 36 | $_initResizeEvent() { 37 | window.addEventListener('resize', this.$_resizeHandler) 38 | }, 39 | $_destroyResizeEvent() { 40 | window.removeEventListener('resize', this.$_resizeHandler) 41 | }, 42 | $_sidebarResizeHandler(e) { 43 | if (e.propertyName === 'width') { 44 | this.$_resizeHandler() 45 | } 46 | }, 47 | $_initSidebarResizeEvent() { 48 | this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0] 49 | this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler) 50 | }, 51 | $_destroySidebarResizeEvent() { 52 | this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/views/icons/svg-icons.js: -------------------------------------------------------------------------------- 1 | const req = require.context('../../icons/svg', false, /\.svg$/) 2 | const requireAll = requireContext => requireContext.keys() 3 | 4 | const re = /\.\/(.*)\.svg/ 5 | 6 | const svgIcons = requireAll(req).map(i => { 7 | return i.match(re)[1] 8 | }) 9 | 10 | export default svgIcons 11 | -------------------------------------------------------------------------------- /frontend/src/views/ticket/new_ticket.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{item.name}} 6 | 7 | 8 | 9 | 10 | {{w.name}} 11 | {{w.name}} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 90 | -------------------------------------------------------------------------------- /frontend/src/views/tool/test.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 这是一个测试页面 4 | 下面是你对这个页面的相关操作权限,如果没有,则不会显示相应的按钮 5 | 6 | {{ "增加" }} 7 | 8 | 9 | 10 | {{ "删除" }} 11 | 12 | 13 | 14 | {{ "编辑" }} 15 | 16 | 17 | 18 | {{ "查看" }} 19 | 20 | 21 | 22 | 23 | 60 | -------------------------------------------------------------------------------- /gifs/all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/gifs/all.png -------------------------------------------------------------------------------- /gifs/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/gifs/edit.png -------------------------------------------------------------------------------- /gifs/leave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/gifs/leave.png -------------------------------------------------------------------------------- /gifs/new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/gifs/new.png -------------------------------------------------------------------------------- /gifs/role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/gifs/role.png -------------------------------------------------------------------------------- /gifs/role_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itimor/one-workflow/805035def06b88b8e3f616441a435742d3ba0bf2/gifs/role_edit.png -------------------------------------------------------------------------------- /oms_nginx.conf: -------------------------------------------------------------------------------- 1 | upstream django { 2 | server unix:/data/app/one/backend/core.sock; # for a file socket 3 | } 4 | 5 | server { 6 | listen 80; 7 | server_name oms.itimor.com; 8 | root /data/app/one/; 9 | 10 | charset utf-8; 11 | client_max_body_size 200m; 12 | 13 | error_page 404 500 502 = @502_page; 14 | 15 | location @502_page { 16 | rewrite ^(.*)$ /500.html break; 17 | } 18 | 19 | location /static/ { 20 | alias /data/app/one/frontend/dist/static/; 21 | } 22 | 23 | location / { 24 | uwsgi_pass django; 25 | include uwsgi_params; 26 | proxy_http_version 1.1; 27 | proxy_set_header Upgrade $http_upgrade; 28 | proxy_set_header Connection "upgrade"; 29 | proxy_read_timeout 36000s; 30 | proxy_send_timeout 36000s; 31 | } 32 | } --------------------------------------------------------------------------------