[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: moshuying # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "\n# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: \"\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".github/workflows/blank.yml",
    "content": "# This is a basic workflow to help you get started with Actions\n\nname: CI\n\n# Controls when the action will run. \non:\n  # Triggers the workflow on push or pull request events but only for the main branch\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\n# A workflow run is made up of one or more jobs that can run sequentially or in parallel\njobs:\n  # This workflow contains a single job called \"build\"\n  build:\n    # The type of runner that the job will run on\n    runs-on: ubuntu-latest\n\n    # Steps represent a sequence of tasks that will be executed as part of the job\n    steps:\n      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it\n      - uses: actions/checkout@v2\n\n      # Runs a single command using the runners shell\n      - name: Run a one-line script\n        run: echo Hello, world!\n\n      # Runs a set of commands using the runners shell\n      - name: Run a multi-line script\n        run: |\n          echo Add other actions to build,\n          echo test, and deploy your project.\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ main ]\n  schedule:\n    - cron: '45 1 * * 2'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'java', 'javascript' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]\n        # Learn more:\n        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v2\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v1\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v1\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v1\n"
  },
  {
    "path": ".gitignore",
    "content": "# Compiled class file\n*.class\n\n# Log file\n*.log\n\n# BlueJ files\n*.ctxt\n\n# Mobile Tools for Java (J2ME)\n.mtj.tmp/\n\n# Package Files #\n*.jar\n*.war\n*.nar\n*.ear\n*.zip\n*.tar.gz\n*.rar\n\n# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml\nhs_err_pid*\nidea\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n  - \"14\"\ninstall: \n  - cd ./front\n  - npm i\nscript:\n  - npm run build\nnotifications:\n  email:\n    - 1460083332@qq.com\ncache:\n  directories:\n    - node_modules #缓存依赖\n\n#after_script前5句是把部署分支的.git文件夹保护起来，用于保留历史部署的commit日志，否则部署分支永远只有一条commit记录。\n#命令里面的变量都是在Travis CI里配置过的。\n# after_script:\n#   - git clone https://${GH_REF} .temp\n#   - cd .temp\n#   - git checkout gh-pages\n#   - cd ../\n#   - mv .temp/.git dist\n#   - cd dist\n#   - git config user.name \"${U_NAME}\"\n#   - git config user.email \"${U_EMAIL}\"\n#   - git add .\n#   - git commit -m \":construction_worker:- Build & Deploy by Travis CI\"\n#   - git push --force --quiet \"https://${Travis_Token}@${GH_REF}\" gh-pages:${D_BRANCH}\n# E: Build LifeCycle\n# 只有指定的分支提交时才会运行脚本\n# branches:\n#   only:\n#     - master"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\n.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "感谢贡献者们\n\n墨抒颖 MoShuYing 刘九江 LiuJiuJiang"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <2021>  <刘九江>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published\n    by the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "README.md",
    "content": "<center>\n\n# project-3 CRM 客户资源管理系统\n\n  \n<img align=\"right\" src='/front/src/assets/img/logo.png' />\n  \n[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) [![Join the chat at https://gitter.im/墨抒颖/project-3-crm](https://badges.gitter.im/墨抒颖/project-3-crm.svg)](https://gitter.im/墨抒颖/project-3-crm?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)\n \n ![GitHub language count](https://img.shields.io/github/languages/count/moshuying/project-3-crm) ![GitHub search hit counter](https://img.shields.io/github/search/moshuying/project-3-crm/1) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/moshuying/project-3-crm) ![GitHub repo size](https://img.shields.io/github/repo-size/moshuying/project-3-crm) \n  \n  ![GitHub closed issues](https://img.shields.io/github/issues-closed/moshuying/project-3-crm) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/moshuying/project-3-crm) ![GitHub](https://img.shields.io/github/license/moshuying/project-3-crm)\n\n### 国内用户请访问[同步仓库](https://gitee.com/moshuying/project-3-crm)\n\n\n# 简述\n\n[sql文件包含在/mysql文件夹内](https://github.com/moshuying/project-3-crm/blob/main/mysql)\n\n<a href=\"https://www.msy.plus/discover/\" target=\"_blank\">\n在线演示(向下翻页就有)\n</a>\n</center>\n\n\n- 前端使用 [vue-antd-admin](https://github.com/iczer/vue-antd-admin) \n- [项目文档地址](https://iczer.gitee.io/vue-antd-admin-docs/advance/authority.html#%E9%A1%B5%E9%9D%A2%E6%9D%83%E9%99%90) \n- 后台使用 [spring-boot-api-seedling](https://github.com/Zoctan/spring-boot-api-seedling) \n\n<h1>把项目拷贝下来后，导入`mysql/dev.sql`到`crm`数据库</h1>\n\n<h1>数据库配置的在`back/src/main/resources/application-dev.yml`下，默认账户密码都是`root`。使用`crm`数据库</h1>\n\n![image](https://user-images.githubusercontent.com/37231523/157181254-38b38973-522e-4fdb-803f-3e374caca5f4.png)\n\n\n系统包括：系统设置、客户管理、营销管理、服务管理、合同管理和统计分析六个功能模块。可满足管理人员日常对客户的资源维护、销售数据分析、潜在和有价值客户分析等需求。\n\n\n甲方需求文档和演讲ppt位于/docs目录下。较为详细的描述了甲方的功能需求。\n- [腾讯文档在线查看甲方需求](https://docs.qq.com/doc/DR0JVbFpmdXNEU1NM)\n- [ppt商业计划书在线查看](https://docs.qq.com/slide/DR2dIaXB1b3hVZkdw)\n- [商业计划书参考](https://max.book118.com/html/2017/0508/105355794.shtm)\n- [sourceforge](https://sourceforge.net/projects/project-3-crm/)\n\n\n\n系统经过github工作流，travis集成测试。尽可能多的测试了系统中的功能。\n\n客户关系管理系统用于管理与客户相关的信息与活动，包括企业与顾客间在销售、营销和服务上的交互。从而提升其管理方式，向客户提供创新式的个性化的客户交互和服务。CRM不仅仅是一个软件，它还是方法论、软件和IT能力综合，是一种商业策略。其最终目标是吸引新客户、保留老客户以及将已有客户转为忠实客户。为企业一系列的客户关系管理解决方案。\n\n# contributors\n[![](https://opencollective.com/project-3-crm/contributors.svg?width=890)](https://github.com/moshuying/project-3-crm/graphs/contributors)\n\n部分页面截图\n\n![](/images/Snipaste_2021-05-24_17-26-55.png)\n![](/images/Snipaste_2021-05-24_17-27-16.png)\n![](/images/Snipaste_2021-05-24_17-27-48.png)\n![](/images/Snipaste_2021-05-24_17-28-09.png)\n![](/images/Snipaste_2021-05-24_17-28-20.png)\n![](/images/Snipaste_2021-05-24_17-28-29.png)\n![](/images/Snipaste_2021-05-24_17-28-40.png)\n![](/images/Snipaste_2021-05-24_17-28-46.png)\n![](/images/Snipaste_2021-05-24_17-28-54.png)\n![](/images/Snipaste_2021-05-24_17-29-11.png)\n![](/images/Snipaste_2021-05-24_17-29-16.png)\n![](/images/Snipaste_2021-05-24_17-29-24.png)\n![](/images/Snipaste_2021-05-24_17-29-29.png)\n![](/images/Snipaste_2021-05-24_17-29-37.png)\n![](/images/Snipaste_2021-05-24_17-29-48.png)\n![](/images/Snipaste_2021-05-24_17-29-55.png)\n![](/images/Snipaste_2021-05-24_17-30-06.png)\n![](/images/Snipaste_2021-05-24_17-30-18.png)\n![](/images/Snipaste_2021-05-24_17-30-39.png)\n![](/images/Snipaste_2021-05-24_17-30-49.png)\n![](/images/Snipaste_2021-05-24_17-30-56.png)\n![](/images/Snipaste_2021-05-24_17-31-03.png)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nUse this section to tell people about which versions of your project are\ncurrently being supported with security updates.\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 5.1.x   | :white_check_mark: |\n| 5.0.x   | :x:                |\n| 4.0.x   | :white_check_mark: |\n| < 4.0   | :x:                |\n\n## Reporting a Vulnerability\n\nUse this section to tell people how to report a vulnerability.\n\nTell them where to go, how often they can expect to get an update on a\nreported vulnerability, what to expect if the vulnerability is accepted or\ndeclined, etc.\n"
  },
  {
    "path": "back/.gitignore",
    "content": "# Compiled class file\n*.class\n\n# Log file\n*.log\n\n# BlueJ files\n*.ctxt\n\n# Mobile Tools for Java (J2ME)\n.mtj.tmp/\n\n# Package Files #\n*.jar\n*.war\n*.ear\n*.zip\n*.tar.gz\n*.rar\n\n# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml\nhs_err_pid*\n\n.idea/\n\ntarget/\n\n*.iml\n\napplication-prod.yml"
  },
  {
    "path": "back/LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "back/README-zh.md",
    "content": "# Spring Boot API Seedling\n\n![stars](https://img.shields.io/github/stars/Zoctan/spring-boot-api-seedling.svg?style=flat-square&label=Stars)\n![license](https://img.shields.io/github/license/Zoctan/spring-boot-api-seedling.svg?style=flat-square)\n\n[English](./README.md) | 简体中文\n\n## 简介\n\n本项目修改自：[spring-boot-api-project-seed](https://github.com/lihengming/spring-boot-api-project-seed)\n\n原项目本身很简洁，已经能满足很多基本需求，在此感谢种子作者。\n\n我根据需求继续添加了一些小功能，比如 API 的签名认证、调用文档、一些小工具等，所以就有了该 Seedling 项目。\n\n添加的内容包括：\n- Spring Cache：缓存\n- Redis：缓存中间件\n- Swagger3：API 文档展示\n- Spring Security + JWT：对调用方签名认证\n- Jasypt：加密配置\n- 其他略\n\n代码规范参考阿里巴巴 Java 开发手册，安装 Alibaba Java Coding Guidelines 插件。\n\n风格规范使用 Google，安装 google-java-format 插件。\n\n注解工具：Lombok，安装同名 Idea 插件。\n\n## 版本\n\n| 依赖         | 版本    |\n|:-----------:|--------:|\n| Java        | 1.8     |\n| SpringBoot  | 2.3.5   |\n\n## 快速开始\n\n\\# 克隆项目\n\ngit clone https://github.com/Zoctan/spring-boot-api-seedling.git\n\n\\# 配置代码生成器\n\n对 test/java 包内的代码生成器 CodeGenerator 进行配置\n导入 test/resources/sql 目录下的开发环境 dev 的数据库文件 *.sql\n\n\\# 根据表名生成代码\n\n输入表名，运行 CodeGenerator.main() 方法，生成基础代码（观看[种子项目的快速演示视频](http://v.youku.com/v_show/id_XMjg1NjYwNDgxNg==.html?spm=a2h3j.8428770.3416059.1)）\n\n\\# last\n\n对开发环境配置文件 application-dev.properties 进行配置，启动项目，Have Fun Too：)\n\n## 技术选型&文档\n\n1. Spring Boot（[种子项目作者的学习&使用指南](https://www.jianshu.com/p/1a9fd8936bd8) | [基础教程](http://blog.didispace.com/Spring-Boot%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/)）\n2. MyBatis（[官方中文文档](http://www.mybatis.org/mybatis-3/zh/index.html)）\n3. MyBatis通用Mapper插件（[官方中文文档](https://mapperhelper.github.io/docs/)）\n4. MyBatis PageHelper分页插件（[官方中文文档](https://pagehelper.github.io/)）\n5. Druid Spring Boot Starter（[官方中文文档](https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter/)）\n6. FastJson（[官方中文文档](https://github.com/alibaba/fastjson/wiki/Quick-Start-CN) | [W3CSchool使用指南](https://www.w3cschool.cn/fastjson/fastjson-quickstart.html)）\n\n## 相关项目\n\n- [前端 Vue + 后端 Spring Boot 完全分离的用户角色管理模板](https://github.com/Zoctan/spring-boot-vue-admin)\n\n## 更新记录\n\n2020-11-09 更新 Swagger2 至 Swagger3，更新其他依赖版本。\n\n2019-08-13 更换 Tomcat 容器为 Jetty，修复 RSA 密钥文件无法读取问题，添加文件上传控制器，更新其他依赖版本。\n\n2018-11-29 配置改为 yml ，完善单元测试，更新其他依赖版本。\n\n2018-07-21 增加 Jasypt 自定义配置和配置密码加密，Tomcat 打包，修改 RSA 工具和添加相应配置。\n\n2018-07-15 增加 DTO 层，避免 DO 层被污染。\n\n2018-07-11 添加了可自定义缓存过期时间的注解，修改了数据表 user 为 account。\n"
  },
  {
    "path": "back/README.md",
    "content": "# Spring Boot API Seedling\n\n![stars](https://img.shields.io/github/stars/Zoctan/spring-boot-api-seedling.svg?style=flat-square&label=Stars)\n![license](https://img.shields.io/github/license/Zoctan/spring-boot-api-seedling.svg?style=flat-square)\n\nEnglish | [简体中文](./README-zh.md)\n\n## Introduction\n\nModified from: [spring-boot-api-project-seed](https://github.com/lihengming/spring-boot-api-project-seed)\n\nThe original project is very well and has been able to meet many basic needs. Thanks the seed author!\n\nSeedling project:\nI continued to add some small functions according to my needs, such as API signature authentication, API documents, some tools, etc.\n\nThe added content includes:\n- Spring Cache: To cache\n- Redis: Cache middleware\n- Swagger3：API Doc\n- Spring Security + JWT：Sign the caller authentication\n- Jasypt：Encryption configuration\n- etc.\n\nThe code specification refers to the《Alibaba Java Development》 and install the Alibaba Java Coding Guidelines plugin.\n\nThe style specification refers to Google and install google-java-format plugin.\n\nAnnotation tool: Lombok, install the Idea plugin of the same name.\n\n## Version\n\n| Dependencies | Version |\n|:------------:|--------:|\n| Java         | 1.8     |\n| SpringBoot   | 2.3.5   |\n\n## Start\n\n\\# Clone project\n\ngit clone https://github.com/Zoctan/spring-boot-api-seedling.git\n\n\\# Configure code generator\n\nconfigure package test/java/.../CodeGenerator, import directory test/resources/sql/dev/*.sql file\n\n\\# Generate code from database schema\n\ninput table name, run CodeGenerator.main() method to generate basic code (watch [demo video](http://v.youku.com/v_show/id_XMjg1NjYwNDgxNg==.html?spm=a2h3j.8428770.3416059.1))\n\n\\# Last\n\nconfigure the development environment configuration file application-dev.properties and start the project.\n\nHave Fun Too：)\n\n## Related project\n\n- [前端 Vue + 后端 Spring Boot 完全分离的用户角色管理模板](https://github.com/Zoctan/spring-boot-vue-admin)\n\n## Update log\n\n2020-11-09 Update Swagger2 to Swagger3, update other dependencies version.\n\n2019-08-13 Modify Tomcat to Jetty, read RSA file error have been fixed, add file upload controller, update dependencies version.\n\n2018-11-29 Modify setting file format to yml, improve unit testing, update dependencies version.\n\n2018-07-21 Add Jasypt custom setting and password encryption, add Tomcat pack, modify RSA tool.\n\n2018-07-15 Add DTO to prevent DO pollution.\n\n2018-07-11 Add annotation for customizable cache expiration time, modify the data table user to account.\n"
  },
  {
    "path": "back/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>com.github.zoctan</groupId>\n    <artifactId>spring-boot-api-seeding</artifactId>\n    <version>1.1</version>\n    <!-- 打包类型 -->\n    <packaging>war</packaging>\n\n    <!-- Inherit defaults from Spring Boot -->\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>2.3.5.RELEASE</version>\n    </parent>\n\n    <properties>\n        <!-- 打包配置 -->\n        <java.version>1.8</java.version>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n        <!-- 依赖版本 -->\n        <spring-security-test.version>5.3.5.RELEASE</spring-security-test.version>\n        <swagger3.version>3.0.0</swagger3.version>\n        <jjwt.version>0.9.1</jjwt.version>\n        <jedis.version>3.3.0</jedis.version>\n        <commons-beanutils.version>1.9.4</commons-beanutils.version>\n        <commons-codec.version>1.15</commons-codec.version>\n        <commons-lang3.version>3.11</commons-lang3.version>\n        <guava.version>32.0.0-jre</guava.version>\n        <mybatis.version>2.1.3</mybatis.version>\n        <mybatis-generator.version>1.3.7</mybatis-generator.version>\n        <mapper.version>4.1.5</mapper.version>\n        <mapper-starter.version>2.1.5</mapper-starter.version>\n        <pagehelper.version>1.3.0</pagehelper.version>\n        <fastjson.version>1.2.83</fastjson.version>\n        <druid.version>1.2.2</druid.version>\n        <jasypt.version>3.0.3</jasypt.version>\n        <freemarker.version>2.3.30</freemarker.version>\n        <lombok.version>1.18.16</lombok.version>\n        <hibernate.version>6.1.6.Final</hibernate.version>\n    </properties>\n\n    <dependencies>\n        <!-- swagger3 -->\n        <dependency>\n            <groupId>io.springfox</groupId>\n            <artifactId>springfox-boot-starter</artifactId>\n            <version>${swagger3.version}</version>\n        </dependency>\n        <!-- spring security + json web token -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-security</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.jsonwebtoken</groupId>\n            <artifactId>jjwt</artifactId>\n            <version>${jjwt.version}</version>\n        </dependency>\n        <!-- MySQL JDBC驱动 -->\n        <dependency>\n            <groupId>mysql</groupId>\n            <artifactId>mysql-connector-java</artifactId>\n        </dependency>\n        <!-- Spring Boot依赖 -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-aop</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n            <exclusions>\n                <exclusion>\n                    <groupId>org.springframework.boot</groupId>\n                    <artifactId>spring-boot-starter-tomcat</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-jetty</artifactId>\n        </dependency>\n        <!-- MockMvc带Auth测试需要 -->\n        <dependency>\n            <groupId>org.springframework.security</groupId>\n            <artifactId>spring-security-test</artifactId>\n            <version>${spring-security-test.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <!-- 热部署 -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-devtools</artifactId>\n            <!-- optional=true，依赖不会传递，该项目依赖devtools，之后依赖该项目的子项目要使用devtools，需重新引入 -->\n            <optional>true</optional>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-cache</artifactId>\n        </dependency>\n\n        <!-- redis -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-redis</artifactId>\n            <exclusions>\n                <exclusion>\n                    <groupId>io.lettuce</groupId>\n                    <artifactId>lettuce-core</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <!-- https://github.com/xetorthio/jedis -->\n        <dependency>\n            <groupId>redis.clients</groupId>\n            <artifactId>jedis</artifactId>\n            <version>${jedis.version}</version>\n        </dependency>\n\n        <!-- 常用库依赖 -->\n        <!-- http://commons.apache.org/proper/commons-beanutils/javadocs/v1.9.4/apidocs/index.html -->\n        <dependency>\n            <groupId>commons-beanutils</groupId>\n            <artifactId>commons-beanutils</artifactId>\n            <version>${commons-beanutils.version}</version>\n        </dependency>\n        <!-- https://commons.apache.org/proper/commons-codec/apidocs/index.html -->\n        <dependency>\n            <groupId>commons-codec</groupId>\n            <artifactId>commons-codec</artifactId>\n            <version>${commons-codec.version}</version>\n        </dependency>\n        <!-- http://commons.apache.org/proper/commons-lang/apidocs/index.html -->\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-lang3</artifactId>\n            <version>${commons-lang3.version}</version>\n        </dependency>\n        <!-- https://guava.dev/releases/snapshot-jre/api/docs/ -->\n        <dependency>\n            <groupId>com.google.guava</groupId>\n            <artifactId>guava</artifactId>\n            <version>${guava.version}</version>\n        </dependency>\n\n        <!-- FastJson -->\n        <dependency>\n            <groupId>com.alibaba</groupId>\n            <artifactId>fastjson</artifactId>\n            <version>${fastjson.version}</version>\n        </dependency>\n        <!-- Druid 数据库连接池 -->\n        <dependency>\n            <groupId>com.alibaba</groupId>\n            <artifactId>druid-spring-boot-starter</artifactId>\n            <version>${druid.version}</version>\n        </dependency>\n\n        <!-- jasypt 用于加密配置文件 -->\n        <dependency>\n            <groupId>com.github.ulisesbocchio</groupId>\n            <artifactId>jasypt-spring-boot-starter</artifactId>\n            <version>${jasypt.version}</version>\n        </dependency>\n\n        <!-- PageHelper -->\n        <dependency>\n            <groupId>com.github.pagehelper</groupId>\n            <artifactId>pagehelper-spring-boot-starter</artifactId>\n            <version>${pagehelper.version}</version>\n        </dependency>\n        <!-- MyBatis及插件依赖 -->\n        <dependency>\n            <groupId>org.mybatis.spring.boot</groupId>\n            <artifactId>mybatis-spring-boot-starter</artifactId>\n            <version>${mybatis.version}</version>\n        </dependency>\n        <!-- mapper -->\n        <dependency>\n            <groupId>tk.mybatis</groupId>\n            <artifactId>mapper</artifactId>\n            <version>${mapper.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>tk.mybatis</groupId>\n            <artifactId>mapper-spring-boot-starter</artifactId>\n            <version>${mapper-starter.version}</version>\n        </dependency>\n        <!-- 代码生成器依赖 -->\n        <dependency>\n            <groupId>org.freemarker</groupId>\n            <artifactId>freemarker</artifactId>\n            <version>${freemarker.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.mybatis.generator</groupId>\n            <artifactId>mybatis-generator-core</artifactId>\n            <version>${mybatis-generator.version}</version>\n        </dependency>\n\n        <!-- https://projectlombok.org/ -->\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <version>${lombok.version}</version>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hibernate.validator</groupId>\n            <artifactId>hibernate-validator</artifactId>\n            <version>${hibernate.version}</version>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <finalName>${project.artifactId}</finalName>\n        <plugins>\n            <!-- 打包插件 -->\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>repackage</goal>\n                        </goals>\n                        <configuration>\n                            <mainClass>com.msy.plus.Application</mainClass>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <configuration>\n                    <source>${java.version}</source>\n                    <!-- 指定JDK编译版本 -->\n                    <target>${java.version}</target>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n    <repositories>\n        <repository>\n            <id>aliyun-snapshots</id>\n            <url>https://maven.aliyun.com/repository/snapshots</url>\n        </repository>\n        <repository>\n            <id>aliyun-repo</id>\n            <url>https://maven.aliyun.com/repository/central</url>\n        </repository>\n    </repositories>\n\n    <pluginRepositories>\n        <pluginRepository>\n            <id>aliyun-plugin</id>\n            <url>https://maven.aliyun.com/repository/central</url>\n        </pluginRepository>\n    </pluginRepositories>\n\n</project>"
  },
  {
    "path": "back/resetDB.sh",
    "content": "#!/bin/bash\n\ndb=\"seedling_dev\"\n\nwhile IFS= read -r -d '' sql; do\n  echo \"$sql\"\" -> \"$db\n  mysql -uroot -proot $db <\"$sql\"\ndone < <(find src/test/resources/sql/dev/ -name '*.sql' -print0)\necho \"finished\"\necho \"import $db done\"\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/Application.java",
    "content": "package com.msy.plus;\n\nimport com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.builder.SpringApplicationBuilder;\nimport org.springframework.boot.web.servlet.ServletComponentScan;\nimport org.springframework.boot.web.servlet.support.SpringBootServletInitializer;\nimport org.springframework.cache.annotation.EnableCaching;\nimport org.springframework.transaction.annotation.EnableTransactionManagement;\nimport tk.mybatis.spring.annotation.MapperScan;\n\nimport static com.msy.plus.core.constant.ProjectConstant.FILTER_PACKAGE;\nimport static com.msy.plus.core.constant.ProjectConstant.MAPPER_PACKAGE;\n\n/**\n * 主程序\n *\n * @author MoShuying\n * @date 2018/05/27\n */\n@EnableCaching\n@SpringBootApplication\n@EnableEncryptableProperties\n@EnableTransactionManagement\n@MapperScan(basePackages = MAPPER_PACKAGE)\n@ServletComponentScan(basePackages = FILTER_PACKAGE)\npublic class Application extends SpringBootServletInitializer {\n\n  public static void main(final String[] args) {\n    SpringApplication.run(Application.class, args);\n  }\n\n  /** 容器启动配置 */\n  @Override\n  protected SpringApplicationBuilder configure(final SpringApplicationBuilder builder) {\n    return builder.sources(Application.class);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/aspect/ControllerLogAspect.java",
    "content": "package com.msy.plus.aspect;\n\nimport com.msy.plus.util.IpUtils;\nimport lombok.extern.slf4j.Slf4j;\nimport org.aspectj.lang.JoinPoint;\nimport org.aspectj.lang.annotation.*;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\nimport javax.servlet.http.HttpServletRequest;\nimport java.time.LocalDateTime;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Arrays;\nimport java.util.Optional;\n\nimport static com.msy.plus.core.constant.ProjectConstant.CONTROLLER_PACKAGE;\n\n/**\n * Controller log aspect\n *\n * @author MoShuying\n * @date 2018/07/13\n */\n@Aspect\n@Slf4j\n@Component\npublic class ControllerLogAspect {\n  private LocalDateTime startTime;\n\n  @Pointcut(\"execution(* \" + CONTROLLER_PACKAGE + \"..*.*(..))\")\n  public void controllers() {}\n\n  /**\n   * before controller handling, log something\n   *\n   * @param joinPoint controller join point\n   */\n  @Before(\"controllers()\")\n  public void doBefore(final JoinPoint joinPoint) {\n    log.debug(\"===========================================================\");\n    log.debug(\"================  Controller Log Start  ===================\");\n    log.debug(\"===========================================================\");\n    this.startTime = LocalDateTime.now();\n    final ServletRequestAttributes attributes =\n        (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();\n    if (Optional.ofNullable(attributes).isPresent()) {\n      final HttpServletRequest request = attributes.getRequest();\n      log.debug(\"==> Request: [{}]{}\", request.getMethod(), request.getRequestURL());\n      log.debug(\"==> From IP: {}\", IpUtils.getIpAddress());\n    }\n    log.debug(\n        \"==>  Method: {}\",\n        joinPoint.getSignature().getDeclaringTypeName() + \"#\" + joinPoint.getSignature().getName());\n    log.debug(\"==>    Args: {}\", Arrays.toString(joinPoint.getArgs()));\n  }\n\n  /**\n   * after controller handling, return result\n   *\n   * @param result origin result\n   */\n  @AfterReturning(pointcut = \"controllers()\", returning = \"result\")\n  public void doAfterReturning(final Object result) {\n    // 处理请求的时间差\n    final long difference = ChronoUnit.MILLIS.between(this.startTime, LocalDateTime.now());\n    log.debug(\"==>   Spend: {}s\", difference / 1000.0);\n    log.debug(\"==>  Return: {}\", result);\n    log.debug(\"================  Controller Log End  =====================\");\n  }\n\n  /**\n   * log when throwing error\n   *\n   * @param e error\n   */\n  @AfterThrowing(pointcut = \"controllers()\", throwing = \"e\")\n  public static void doAfterThrowing(final Throwable e) {\n    log.debug(\"==> Exception: {}\", e.toString());\n    e.printStackTrace();\n    log.debug(\"================  Controller Log End  =====================\");\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/controller/AccountController.java",
    "content": "package com.msy.plus.controller;\n\nimport com.msy.plus.core.jwt.JwtUtil;\nimport com.msy.plus.core.response.Result;\nimport com.msy.plus.core.response.ResultGenerator;\nimport com.msy.plus.dto.AccountDTO;\nimport com.msy.plus.dto.AccountLoginDTO;\nimport com.msy.plus.dto.LoginResultDTO;\nimport com.msy.plus.service.AccountService;\nimport com.msy.plus.service.impl.UserDetailsServiceImpl;\nimport io.swagger.annotations.Api;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.Parameter;\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport io.swagger.v3.oas.annotations.responses.ApiResponse;\nimport io.swagger.v3.oas.annotations.responses.ApiResponses;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.access.prepost.PreAuthorize;\nimport org.springframework.security.authentication.UsernamePasswordAuthenticationToken;\nimport org.springframework.security.core.annotation.AuthenticationPrincipal;\nimport org.springframework.security.core.userdetails.UserDetails;\nimport org.springframework.validation.BindingResult;\nimport org.springframework.validation.annotation.Validated;\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.annotation.Resource;\nimport javax.validation.Valid;\nimport java.util.*;\n\n/**\n * @author MoShuying\n * @date 2018/07/15\n */\n@Slf4j\n@Api(tags={\"账户操作接口(登录)\"})\n@Validated\n@RestController\n@RequestMapping(\"/account\")\npublic class AccountController {\n  @Resource private AccountService accountService;\n  @Resource private UserDetailsServiceImpl userDetailsService;\n  @Resource private JwtUtil jwtUtil;\n\n  @Operation(summary = \"账户注册\", description = \"注册账户，签发token\")\n  @ApiResponses({\n    @ApiResponse(responseCode = \"200\", description = \"OK\"),\n    @ApiResponse(responseCode = \"2004\", description = \"账户名重复\")\n  })\n  @PostMapping\n  public Result register(\n      @Parameter(required = true) @RequestBody @Valid final AccountDTO accountDTO,\n      final BindingResult bindingResult) {\n    // 账户持久化\n    this.accountService.save(accountDTO);\n    // 签发 token\n    final UserDetails userDetails =\n        this.userDetailsService.loadUserByUsername(accountDTO.getName());\n    final String token = this.jwtUtil.sign(\n            accountDTO.getName(),\n            userDetails.getAuthorities(),\n            accountService.getByNameWithRole(userDetails.getUsername()).getId());\n    return ResultGenerator.genOkResult(token);\n  }\n\n  @Operation(summary = \"账户登录\", description = \"账户登录，签发token\")\n  @ApiResponses({\n    @ApiResponse(responseCode = \"200\", description = \"OK\"),\n    @ApiResponse(responseCode = \"1000\", description = \"密码错误\")\n  })\n  @PostMapping(\"/token\")\n  public Result login(\n      @Parameter(required = true) @RequestBody @Valid final AccountLoginDTO accountLoginDTO,\n      final BindingResult bindingResult) {\n    // {\"name\":\"admin\",\"password\":\"admin\"}\n    final String name = accountLoginDTO.getName();\n    final String password = accountLoginDTO.getPassword();\n    // 验证账户\n    final UserDetails userDetails = this.userDetailsService.loadUserByUsername(name);\n    if (!this.accountService.verifyPassword(password, userDetails.getPassword())) {\n      return ResultGenerator.genFailedResult(\"密码错误\");\n    }\n    // 更新登录时间\n    this.accountService.updateLoginTimeByName(name);\n    final String token = this.jwtUtil.sign(name, userDetails.getAuthorities(),accountService.getByNameWithRole(name).getId());\n    // 返回Ant Design Admin提供的登录返回格式\n    LoginResultDTO loginResultDTO = new LoginResultDTO();\n\n    // 设置过期时间，和application-*.yml文件中的过期时间设定一致\n    final long expireTime = this.jwtUtil.getJwtProperties().getExpireTime().toMillis();\n    loginResultDTO.setExpireAt(new Date(new Date().getTime()+expireTime));\n    loginResultDTO.setToken(token);\n    loginResultDTO.setUserName(name);\n    Map<String,Object> roles = new HashMap<String,Object>();\n    roles.put(\"id\",name);\n    roles.put(\"operation\",new String[]{\"add\",\"edit\",\"delete\"});\n\n    loginResultDTO.getRoles().add(roles);\n    loginResultDTO.setMessage(\"欢迎回来 \"+name);\n    return ResultGenerator.genOkResult(loginResultDTO);\n  }\n\n  @Operation(summary = \"账户注销\", description = \"账户注销，使token失效\")\n  @ApiResponses({@ApiResponse(responseCode = \"200\", description = \"OK\")})\n  @DeleteMapping(\"/token\")\n  public Result logout(@RequestHeader Map<String, String> headers) {\n    String header = jwtUtil.getJwtProperties().getHeader();\n    jwtUtil.invalidRedisToken(jwtUtil.getName(headers.get(header)).get());\n    return ResultGenerator.genOkResult();\n  }\n\n  @PreAuthorize(\"#accountDTO.name == authentication.name or hasAuthority('ADMIN')\")\n  @Operation(summary = \"更新账户\", description = \"更新账户信息\")\n  @ApiResponses({@ApiResponse(responseCode = \"200\", description = \"OK\")})\n  @PatchMapping\n  public Result update(@Parameter(required = true) @RequestBody final AccountDTO accountDTO) {\n    this.accountService.updateByName(accountDTO);\n    return ResultGenerator.genOkResult();\n  }\n\n  @PreAuthorize(\"hasAuthority('ADMIN')\" +\n          \"or hasAuthority('主席')\"+\n          \"or hasAuthority('高级主席')\"+\n          \"or hasAuthority('副主席')\"+\n          \"or hasAuthority('总裁')\")\n  @Operation(summary = \"删除账户\", description = \"删除账户信息\")\n  @ApiResponses({@ApiResponse(responseCode = \"200\", description = \"OK\")})\n  @Parameter(\n      name = \"id\",\n      description = \"账户Id\",\n      required = true,\n      in = ParameterIn.QUERY,\n      example = \"1\")\n  @DeleteMapping(\"/{id}\")\n  public Result delete(@PathVariable final Long id) {\n    this.accountService.deleteById(id);\n    return ResultGenerator.genOkResult();\n  }\n//\n//  @Operation(summary = \"获取单个账户\", description = \"获取单个账户信息\")\n//  @ApiResponses({@ApiResponse(responseCode = \"200\", description = \"OK\")})\n//  @Parameter(\n//      name = \"id\",\n//      description = \"账户Id\",\n//      required = true,\n//      in = ParameterIn.PATH,\n//      example = \"1\")\n//  @GetMapping(\"/{id}\")\n//  public Result detail(@PathVariable final Long id) {\n//    final AccountWithRoleDO account = this.accountService.getByIdWithRole(id);\n//    return ResultGenerator.genOkResult(account);\n//  }\n//\n//  @Operation(summary = \"获取账户列表\", description = \"获取多个账户信息\")\n//  @ApiResponses({@ApiResponse(responseCode = \"200\", description = \"OK\")})\n//  @Parameters({\n//    @Parameter(name = \"page\", description = \"页号\", in = ParameterIn.QUERY, example = \"1\"),\n//    @Parameter(name = \"size\", description = \"页大小\", in = ParameterIn.QUERY, example = \"10\")\n//  })\n//  @Cacheable(value = \"account.list\", unless = \"#result == null or #result.code != 200\")\n//  @CacheExpire(expire = 60)\n//  @GetMapping\n//  public Result list(\n//      @RequestParam(defaultValue = \"0\") final Integer page,\n//      @RequestParam(defaultValue = \"0\") final Integer size) {\n//    AccountController.log.debug(\"==> No cache, find database\");\n//    PageHelper.startPage(page, size);\n//    final List<AccountDO> list = this.accountService.listAll();\n//    final PageInfo<AccountDO> pageInfo = PageInfo.of(list);\n//    // 不显示 password 字段\n//    final PageInfo<JSONObject> objectPageInfo = JsonUtils.deleteFields(pageInfo, PageInfo.class, \"password\");\n//    return ResultGenerator.genOkResult(objectPageInfo);\n//  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/controller/AnalysisController.java",
    "content": "package com.msy.plus.controller;\n\nimport com.github.pagehelper.PageHelper;\nimport com.github.pagehelper.PageInfo;\nimport com.msy.plus.core.jwt.JwtUtil;\nimport com.msy.plus.core.response.Result;\nimport com.msy.plus.core.response.ResultGenerator;\nimport com.msy.plus.dto.AnalysisQuery;\nimport com.msy.plus.entity.*;\nimport com.msy.plus.service.CustomerManagerService;\nimport com.msy.plus.service.EmployeeService;\nimport com.msy.plus.service.RoleService;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.annotations.Api;\nimport org.springframework.security.access.prepost.PreAuthorize;\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.annotation.Resource;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * @author MoShuYing\n * @date 2021/05/15\n */\n@PreAuthorize(\n        \"hasAuthority('ADMIN')\"+\n                \"or hasAuthority('董事长')\"+\n                \"or hasAuthority('主席')\"+\n                \"or hasAuthority('高级主席')\"+\n                \"or hasAuthority('副主席')\"+\n                \"or hasAuthority('总裁')\"+\n                \"or hasAuthority('会长')\"+\n                \"or hasAuthority('高级总裁')\"+\n                \"or hasAuthority('高级副总裁')\"+\n                \"or hasAuthority('副总裁')\"+\n                \"or hasAuthority('总经理')\"+\n                \"or hasAuthority('副总经理')\"+\n                \"or hasAuthority('总监')\"+\n                \"or hasAuthority('经理')\"+\n                \"or hasAuthority('高级经理')\"+\n                \"or hasAuthority('副经理')\"+\n                \"or hasAuthority('主任')\"+\n                \"or hasAuthority('高级主任')\"+\n                \"or hasAuthority('副主任')\"+\n                \"or hasAuthority('组长')\"+\n                \"or hasAuthority('副组长')\"+\n                \"or hasAuthority('普通员工')\"+\n                \"or hasAuthority('人事专员')\"+\n                \"or hasAuthority('市场专员')\"+\n                \"or hasAuthority('市场主管')\"+\n                \"or hasAuthority('销售主管')\"\n)\n@Api(tags={\"统计分析接口\"})\n@RestController\n@RequestMapping(\"/analysis\")\npublic class AnalysisController {\n    @Resource CustomerManagerService customerManagerService;\n    @Resource EmployeeService employeeService;\n    @Resource RoleService roleService;\n    @Resource private JwtUtil jwtUtil;\n    @Operation(description = \"统计分析\")\n    @PostMapping\n    public Result listAndSearch(@RequestBody AnalysisQuery analysisQuery,@RequestHeader Map<String, String> headers) {\n        String header = jwtUtil.getJwtProperties().getHeader();\n        String id= jwtUtil.getId(headers.get(header)).get();\n        List<Long> roleIds = employeeService.getDetailById(Integer.valueOf(id).longValue()).getRoleIds();\n        for(Long roleId:roleIds){\n            RoleWithPermissionDO  roleWithPermissionDO =  roleService.getDetailById(roleId);\n            if(roleWithPermissionDO==null) {\n                continue;\n            }\n            String roleName = roleWithPermissionDO.getName();\n            if(roleName==null || roleName.isEmpty()){\n                continue;\n            }\n            if(roleName.equals(\"董事长\")){\n                PageHelper.startPage(analysisQuery.getPage(),analysisQuery.getSize());\n                PageInfo<Analysis> pageInfo = PageInfo.of(customerManagerService.queryAnalysis(analysisQuery));\n                return ResultGenerator.genOkResult(pageInfo);\n            }\n        }\n        // 除了董事长 其他人都只能查看自己的\n        analysisQuery.setName(jwtUtil.getName(headers.get(header)).get());\n        PageHelper.startPage(analysisQuery.getPage(),analysisQuery.getSize());\n        PageInfo<Analysis> pageInfo = PageInfo.of(customerManagerService.queryAnalysis(analysisQuery));\n        return ResultGenerator.genOkResult(pageInfo);\n    }\n\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/controller/CustomerFollowUpHistoryController.java",
    "content": "package com.msy.plus.controller;\n\nimport com.msy.plus.core.jwt.JwtUtil;\nimport com.msy.plus.core.response.Result;\nimport com.msy.plus.core.response.ResultGenerator;\nimport com.msy.plus.entity.CFUHSearch;\nimport com.msy.plus.entity.CustomerFollowUpHistory;\nimport com.msy.plus.service.CustomerFollowUpHistoryService;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.annotations.Api;\nimport io.swagger.annotations.ApiImplicitParam;\nimport io.swagger.annotations.ApiImplicitParams;\nimport io.swagger.annotations.ApiOperation;\nimport org.springframework.format.annotation.DateTimeFormat;\nimport org.springframework.security.access.prepost.PreAuthorize;\nimport com.github.pagehelper.PageHelper;\nimport com.github.pagehelper.PageInfo;\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.annotation.Resource;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n* @author MoShuYing\n* @date 2021/05/21\n*/\n@PreAuthorize(\"hasAuthority('ADMIN')\")\n@Api(tags={\"客户跟进记录接口\"})\n@RestController\n@RequestMapping(\"/customer/follow/up/history\")\npublic class CustomerFollowUpHistoryController {\n    @Resource private CustomerFollowUpHistoryService customerFollowUpHistoryService;\n    @Resource private JwtUtil jwtUtil;\n    @Operation(description = \"客户跟进记录添加\")\n    @PostMapping\n    public Result add(@RequestBody CustomerFollowUpHistory customerFollowUpHistory,@RequestHeader Map<String, String> headers) {\n        if(customerFollowUpHistory.getId()!=null){\n            customerFollowUpHistory.setId(null);\n        }\n        String header = jwtUtil.getJwtProperties().getHeader();\n        String id= jwtUtil.getId(headers.get(header)).get();\n        customerFollowUpHistory.setInputuser(Integer.valueOf(id));\n        customerFollowUpHistoryService.save(customerFollowUpHistory);\n        return ResultGenerator.genOkResult();\n    }\n\n//    @Operation(description = \"客户跟进记录删除\")\n//    @DeleteMapping(\"/{id}\")\n//    public Result delete(@PathVariable Long id) {\n//    customerFollowUpHistoryService.deleteById(id);\n//        return ResultGenerator.genOkResult();\n//    }\n\n    @Operation(description = \"客户跟进记录更新\")\n    @PutMapping\n    public Result update(@RequestBody CustomerFollowUpHistory customerFollowUpHistory) {\n    customerFollowUpHistoryService.update(customerFollowUpHistory);\n        return ResultGenerator.genOkResult();\n    }\n\n    @Operation(description = \"客户跟进记录获取详细信息\")\n    @GetMapping(\"/{id}\")\n    public Result detail(@PathVariable Long id) {\n    CustomerFollowUpHistory customerFollowUpHistory = customerFollowUpHistoryService.getById(id);\n        return ResultGenerator.genOkResult(customerFollowUpHistory);\n    }\n\n    @Operation(description = \"客户跟进记录分页查询\")\n    @GetMapping\n    @ApiOperation(value=\"分页查询客户跟进记录\", notes=\"分页查询 \")\n    @ApiImplicitParams({\n        @ApiImplicitParam(name = \"page\", value = \"第几页\", required = true, dataType = \"Integer\", paramType=\"query\"),\n        @ApiImplicitParam(name = \"size\", value = \"一页有几条\", required = true, dataType = \"Integer\", paramType=\"query\")\n    })\n    public Result list(\n            @RequestParam(defaultValue = \"1\") Integer page,\n            @RequestParam(defaultValue = \"10\") Integer size,\n            @RequestParam(defaultValue = \"\") String keyword,\n            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Date startTime,\n            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Date endTime,\n            @RequestParam(required = false) Integer type) {\n        PageHelper.startPage(page, size);\n        List<CFUHSearch> list = customerFollowUpHistoryService.listAndSearch(keyword,startTime,endTime,type);\n        PageInfo<CFUHSearch> pageInfo = PageInfo.of(list);\n        return ResultGenerator.genOkResult(pageInfo);\n    }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/controller/CustomerHandoverController.java",
    "content": "package com.msy.plus.controller;\n\nimport com.msy.plus.core.jwt.JwtUtil;\nimport com.msy.plus.core.response.Result;\nimport com.msy.plus.core.response.ResultGenerator;\nimport com.msy.plus.dto.CustomerHandoverList;\nimport com.msy.plus.entity.CustomerHandover;\nimport com.msy.plus.service.CustomerHandoverService;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.annotations.Api;\nimport io.swagger.annotations.ApiImplicitParam;\nimport io.swagger.annotations.ApiImplicitParams;\nimport io.swagger.annotations.ApiOperation;\nimport org.springframework.format.annotation.DateTimeFormat;\nimport org.springframework.security.access.prepost.PreAuthorize;\nimport com.github.pagehelper.PageHelper;\nimport com.github.pagehelper.PageInfo;\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.annotation.Resource;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n* @author MoShuYing\n* @date 2021/05/21\n*/\n@PreAuthorize(\n        \"hasAuthority('ADMIN')\"+\n        \"or hasAuthority('董事长')\"+\n        \"or hasAuthority('主席')\"+\n        \"or hasAuthority('高级主席')\"+\n        \"or hasAuthority('副主席')\"+\n        \"or hasAuthority('总裁')\"+\n        \"or hasAuthority('会长')\"+\n        \"or hasAuthority('高级总裁')\"+\n        \"or hasAuthority('高级副总裁')\"+\n        \"or hasAuthority('副总裁')\"+\n        \"or hasAuthority('总经理')\"+\n        \"or hasAuthority('副总经理')\"+\n        \"or hasAuthority('总监')\"+\n        \"or hasAuthority('经理')\"+\n        \"or hasAuthority('高级经理')\"+\n        \"or hasAuthority('副经理')\"+\n        \"or hasAuthority('主任')\"+\n        \"or hasAuthority('高级主任')\"+\n        \"or hasAuthority('副主任')\"+\n        \"or hasAuthority('组长')\"+\n        \"or hasAuthority('副组长')\"+\n        \"or hasAuthority('普通员工')\"+\n        \"or hasAuthority('人事专员')\"+\n        \"or hasAuthority('市场专员')\"+\n        \"or hasAuthority('市场主管')\"+\n        \"or hasAuthority('销售主管')\"\n)\n@Api(tags={\"移交历史接口\"})\n@RestController\n@RequestMapping(\"/customer/handover\")\npublic class CustomerHandoverController {\n    @Resource private CustomerHandoverService customerHandoverService;\n    @Resource private JwtUtil jwtUtil;\n\n    @Operation(description = \"移交历史添加\")\n    @PostMapping\n    public Result add(@RequestBody CustomerHandover customerHandover,@RequestHeader Map<String, String> headers) {\n        if(customerHandover.getId()!=null){\n            customerHandover.setId(null);\n        }\n        String header = jwtUtil.getJwtProperties().getHeader();\n        String id= jwtUtil.getId(headers.get(header)).get();\n        customerHandover.setTransuser(Integer.valueOf(id));\n        customerHandoverService.save(customerHandover);\n        return ResultGenerator.genOkResult();\n    }\n\n//    @Operation(description = \"移交历史删除\")\n//    @DeleteMapping(\"/{id}\")\n//    public Result delete(@PathVariable Long id) {\n//    customerHandoverService.deleteById(id);\n//        return ResultGenerator.genOkResult();\n//    }\n//\n//    @Operation(description = \"移交历史更新\")\n//    @PutMapping\n//    public Result update(@RequestBody CustomerHandover customerHandover) {\n//    customerHandoverService.update(customerHandover);\n//        return ResultGenerator.genOkResult();\n//    }\n//\n//    @Operation(description = \"移交历史获取详细信息\")\n//    @GetMapping(\"/{id}\")\n//    public Result detail(@PathVariable Long id) {\n//    CustomerHandover customerHandover = customerHandoverService.getById(id);\n//        return ResultGenerator.genOkResult(customerHandover);\n//    }\n\n    @Operation(description = \"移交历史分页查询\")\n    @GetMapping\n    @ApiOperation(value=\"分页查询移交历史\", notes=\"分页查询 \")\n    @ApiImplicitParams({\n        @ApiImplicitParam(name = \"page\", value = \"第几页\", required = true, dataType = \"Integer\", paramType=\"query\"),\n        @ApiImplicitParam(name = \"size\", value = \"一页有几条\", required = true, dataType = \"Integer\", paramType=\"query\")\n    })\n    public Result list(@RequestParam(defaultValue = \"1\") Integer page,\n    @RequestParam(defaultValue = \"10\") Integer size,\n                       @RequestParam(defaultValue = \"\") String keyword,\n                       @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Date startTime,\n                       @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Date endTime) {\n        PageHelper.startPage(page, size);\n        List<CustomerHandoverList> list = customerHandoverService.listAndSearch(keyword,startTime,endTime);\n        PageInfo<CustomerHandoverList> pageInfo = PageInfo.of(list);\n        return ResultGenerator.genOkResult(pageInfo);\n    }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/controller/CustomerManagerController.java",
    "content": "package com.msy.plus.controller;\n\nimport com.msy.plus.core.jwt.JwtUtil;\nimport com.msy.plus.core.response.Result;\nimport com.msy.plus.core.response.ResultGenerator;\nimport com.msy.plus.dto.CustomerManagerList;\nimport com.msy.plus.entity.CustomerManager;\nimport com.msy.plus.service.CustomerManagerService;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.annotations.Api;\nimport io.swagger.annotations.ApiImplicitParam;\nimport io.swagger.annotations.ApiImplicitParams;\nimport io.swagger.annotations.ApiOperation;\nimport org.springframework.security.access.prepost.PreAuthorize;\nimport com.github.pagehelper.PageHelper;\nimport com.github.pagehelper.PageInfo;\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.annotation.Resource;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n* @author MoShuYing\n* @date 2021/05/20\n*/\n@PreAuthorize(\n        \"hasAuthority('ADMIN')\"+\n                \"or hasAuthority('董事长')\"+\n                \"or hasAuthority('主席')\"+\n                \"or hasAuthority('高级主席')\"+\n                \"or hasAuthority('副主席')\"+\n                \"or hasAuthority('总裁')\"+\n                \"or hasAuthority('会长')\"+\n                \"or hasAuthority('高级总裁')\"+\n                \"or hasAuthority('高级副总裁')\"+\n                \"or hasAuthority('副总裁')\"+\n                \"or hasAuthority('总经理')\"+\n                \"or hasAuthority('副总经理')\"+\n                \"or hasAuthority('总监')\"+\n                \"or hasAuthority('经理')\"+\n                \"or hasAuthority('高级经理')\"+\n                \"or hasAuthority('副经理')\"+\n                \"or hasAuthority('主任')\"+\n                \"or hasAuthority('高级主任')\"+\n                \"or hasAuthority('副主任')\"+\n                \"or hasAuthority('组长')\"+\n                \"or hasAuthority('副组长')\"+\n                \"or hasAuthority('普通员工')\"+\n                \"or hasAuthority('人事专员')\"+\n                \"or hasAuthority('市场专员')\"+\n                \"or hasAuthority('市场主管')\"+\n                \"or hasAuthority('销售主管')\"\n)\n@Api(tags={\"客户管理接口\"})\n@RestController\n@RequestMapping(\"/customer/manager\")\npublic class CustomerManagerController {\n    @Resource private CustomerManagerService customerManagerService;\n    @Resource private JwtUtil jwtUtil;\n\n    @Operation(description = \"客户管理添加\")\n    @PostMapping\n    public Result add(@RequestBody CustomerManager customerManager,@RequestHeader Map<String, String> headers) {\n        if(customerManager.getId()!=null){\n            customerManager.setId(null);\n        }\n        String header = jwtUtil.getJwtProperties().getHeader();\n        String id= jwtUtil.getId(headers.get(header)).get();\n        customerManager.setInputuser(Integer.valueOf(id));\n        customerManager.setSeller(Integer.valueOf(id));\n        customerManagerService.save(customerManager);\n        return ResultGenerator.genOkResult();\n    }\n\n//    @Operation(description = \"客户管理删除\")\n//    @DeleteMapping(\"/{id}\")\n//    public Result delete(@PathVariable Long id) {\n//    customerManagerService.deleteById(id);\n//        return ResultGenerator.genOkResult();\n//    }\n\n    @Operation(description = \"客户管理更新\")\n    @PutMapping\n    public Result update(@RequestBody CustomerManager customerManager) {\n    customerManagerService.update(customerManager);\n        return ResultGenerator.genOkResult();\n    }\n\n    @Operation(description = \"客户管理获取详细信息\")\n    @GetMapping(\"/{id}\")\n    public Result detail(@PathVariable Long id) {\n    CustomerManager customerManager = customerManagerService.getById(id);\n        return ResultGenerator.genOkResult(customerManager);\n    }\n\n    @Operation(description = \"客户管理分页查询\")\n    @GetMapping\n    @ApiOperation(value=\"分页查询客户管理\", notes=\"分页查询 \")\n    @ApiImplicitParams({\n        @ApiImplicitParam(name = \"page\", value = \"第几页\", required = true, dataType = \"Integer\", paramType=\"query\"),\n        @ApiImplicitParam(name = \"size\", value = \"一页有几条\", required = true, dataType = \"Integer\", paramType=\"query\")\n    })\n    public Result list(@RequestParam(defaultValue = \"1\") Integer page,\n       @RequestParam(defaultValue = \"10\") Integer size,\n       @RequestParam(defaultValue = \"\",required = false) String keyword,\n       @RequestParam(required = false) Integer status) {\n        PageHelper.startPage(page, size);\n        List<CustomerManagerList> list = customerManagerService.listAllWithDictionary(keyword,status);\n        PageInfo<CustomerManagerList> pageInfo = PageInfo.of(list);\n        return ResultGenerator.genOkResult(pageInfo);\n    }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/controller/DepartmentController.java",
    "content": "package com.msy.plus.controller;\n\nimport com.msy.plus.core.response.Result;\nimport com.msy.plus.core.response.ResultGenerator;\nimport com.msy.plus.entity.Department;\nimport com.msy.plus.service.DepartmentService;\nimport com.github.pagehelper.PageHelper;\nimport com.github.pagehelper.PageInfo;\nimport io.swagger.annotations.Api;\nimport io.swagger.annotations.ApiImplicitParam;\nimport io.swagger.annotations.ApiImplicitParams;\nimport io.swagger.annotations.ApiOperation;\nimport io.swagger.v3.oas.annotations.Operation;\nimport org.springframework.security.access.prepost.PreAuthorize;\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.annotation.Resource;\nimport java.util.List;\n\n/**\n * @author MoShuYing\n * @date 2021/05/12\n */\n@PreAuthorize(\n        \"hasAuthority('ADMIN') \" +\n                \"or hasAuthority('董事长') \" +\n                \"or hasAuthority('主席') \" +\n                \"or hasAuthority('高级主席') \" +\n                \"or hasAuthority('副主席') \" +\n                \"or hasAuthority('总裁') \" +\n                \"or hasAuthority('会长') \" +\n                \"or hasAuthority('高级总裁') \" +\n                \"or hasAuthority('高级副总裁')\")\n@Api(tags={\"部门接口\"})\n@RestController\n@RequestMapping(\"/department\")\npublic class DepartmentController {\n    @Resource\n    private DepartmentService departmentService;\n\n    @Operation(description = \"部门添加\")\n    @PostMapping\n    public Result add(@RequestBody Department department) {\n        departmentService.save(department);\n        return ResultGenerator.genOkResult();\n    }\n\n    @Operation(description = \"部门删除\")\n    @DeleteMapping(\"/{id}\")\n    public Result delete(@PathVariable Long id) {\n        departmentService.deleteById(id);\n        return ResultGenerator.genOkResult();\n    }\n\n    @Operation(description = \"部门更新\")\n    @PatchMapping\n    public Result update(@RequestBody Department department) {\n        departmentService.update(department);\n        return ResultGenerator.genOkResult();\n    }\n\n    @Operation(description = \"获取部门详细信息\")\n    @GetMapping(\"/{id}\")\n    public Result detail(@PathVariable Long id) {\n        Department department = departmentService.getById(id);\n        return ResultGenerator.genOkResult(department);\n    }\n\n    @Operation(description = \"分页查询部门\")\n    @GetMapping\n    @ApiOperation(value=\"分页查询部门\", notes=\"分页查询\")\n    @ApiImplicitParams({\n            @ApiImplicitParam(name = \"page\", value = \"第几页\", required = true, dataType = \"Integer\", paramType=\"query\"),\n            @ApiImplicitParam(name = \"size\", value = \"一页有几条\", required = true, dataType = \"Integer\", paramType=\"query\")\n    })\n    public Result list(@RequestParam(defaultValue = \"1\") Integer page,\n                       @RequestParam(defaultValue = \"10\") Integer size) {\n        PageHelper.startPage(page, size);\n        List<Department> list = departmentService.listAll();\n        PageInfo<Department> pageInfo = PageInfo.of(list);\n        return ResultGenerator.genOkResult(pageInfo);\n    }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/controller/DictionaryContentsController.java",
    "content": "package com.msy.plus.controller;\n\nimport com.msy.plus.core.response.Result;\nimport com.msy.plus.core.response.ResultGenerator;\nimport com.msy.plus.entity.DictionaryContents;\nimport com.msy.plus.service.DictionaryContentsService;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.annotations.Api;\nimport io.swagger.annotations.ApiImplicitParam;\nimport io.swagger.annotations.ApiImplicitParams;\nimport io.swagger.annotations.ApiOperation;\nimport org.springframework.security.access.prepost.PreAuthorize;\nimport com.github.pagehelper.PageHelper;\nimport com.github.pagehelper.PageInfo;\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.annotation.Resource;\nimport java.util.List;\n\n\n/**\n* @author MoShuYing\n* @date 2021/05/18\n*/\n@PreAuthorize(\"hasAuthority('ADMIN')\")\n@Api(tags={\"数据字典接口\"})\n@RestController\n@RequestMapping(\"/dictionary/contents\")\npublic class DictionaryContentsController {\n    @Resource\n    private DictionaryContentsService dictionaryContentsService;\n\n    @Operation(description = \"数据字典添加\")\n    @PostMapping\n    public Result add(@RequestBody DictionaryContents dictionaryContents) {\n        dictionaryContentsService.save(dictionaryContents);\n        return ResultGenerator.genOkResult();\n    }\n\n//    @Operation(description = \"数据字典删除\")\n//    @DeleteMapping(\"/{id}\")\n//    public Result delete(@PathVariable Long id) {\n//    dictionaryContentsService.deleteById(id);\n//        return ResultGenerator.genOkResult();\n//    }\n\n    @Operation(description = \"数据字典更新\")\n    @PutMapping\n    public Result update(@RequestBody DictionaryContents dictionaryContents) {\n    dictionaryContentsService.update(dictionaryContents);\n        return ResultGenerator.genOkResult();\n    }\n\n    @Operation(description = \"数据字典获取详细信息\")\n    @GetMapping(\"/{id}\")\n    public Result detail(@PathVariable Long id) {\n    DictionaryContents dictionaryContents = dictionaryContentsService.getById(id);\n        return ResultGenerator.genOkResult(dictionaryContents);\n    }\n\n    @Operation(description = \"数据字典分页查询\")\n    @GetMapping\n    @ApiOperation(value=\"分页查询数据字典\", notes=\"分页查询 \")\n    @ApiImplicitParams({\n        @ApiImplicitParam(name = \"page\", value = \"第几页\", required = true, dataType = \"Integer\", paramType=\"query\"),\n        @ApiImplicitParam(name = \"size\", value = \"一页有几条\", required = true, dataType = \"Integer\", paramType=\"query\")\n    })\n    public Result list(@RequestParam(defaultValue = \"1\") Integer page,\n                       @RequestParam(defaultValue = \"10\") Integer size,\n                       @RequestParam(defaultValue = \"null\") String keyword) {\n        String inKeyword = null;\n        if (!(keyword == null || keyword.equals(\"null\"))) {\n            inKeyword = keyword;\n        }\n        PageHelper.startPage(page, size);\n        List<DictionaryContents> list = dictionaryContentsService.listWithKeyword(inKeyword);\n        PageInfo<DictionaryContents> pageInfo = PageInfo.of(list);\n        return ResultGenerator.genOkResult(pageInfo);\n    }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/controller/DictionaryDetailsController.java",
    "content": "package com.msy.plus.controller;\n\nimport com.msy.plus.core.response.Result;\nimport com.msy.plus.core.response.ResultGenerator;\nimport com.msy.plus.entity.DictionaryContents;\nimport com.msy.plus.entity.DictionaryDetails;\nimport com.msy.plus.service.DictionaryDetailsService;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.annotations.Api;\nimport io.swagger.annotations.ApiImplicitParam;\nimport io.swagger.annotations.ApiImplicitParams;\nimport io.swagger.annotations.ApiOperation;\nimport org.springframework.security.access.prepost.PreAuthorize;\nimport com.github.pagehelper.PageHelper;\nimport com.github.pagehelper.PageInfo;\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.annotation.Resource;\nimport java.util.List;\n\n/**\n* @author MoShuYing\n* @date 2021/05/18\n*/\n@PreAuthorize(\"hasAuthority('ADMIN')\")\n@Api(tags={\"数据字典明细接口\"})\n@RestController\n@RequestMapping(\"/dictionary/details\")\npublic class DictionaryDetailsController {\n    @Resource\n    private DictionaryDetailsService dictionaryDetailsService;\n\n    @Operation(description = \"数据字典明细添加\")\n    @PostMapping\n    public Result add(@RequestBody DictionaryDetails dictionaryDetails) {\n        dictionaryDetailsService.save(dictionaryDetails);\n        return ResultGenerator.genOkResult();\n    }\n\n//    @Operation(description = \"数据字典明细删除\")\n//    @DeleteMapping(\"/{id}\")\n//    public Result delete(@PathVariable Long id) {\n//    dictionaryDetailsService.deleteById(id);\n//        return ResultGenerator.genOkResult();\n//    }\n\n    @Operation(description = \"数据字典明细更新\")\n    @PutMapping\n    public Result update(@RequestBody DictionaryDetails dictionaryDetails) {\n    dictionaryDetailsService.update(dictionaryDetails);\n        return ResultGenerator.genOkResult();\n    }\n\n    @Operation(description = \"数据字典明细获取详细信息\")\n    @GetMapping(\"/{id}\")\n    public Result detail(@PathVariable Long id) {\n    DictionaryDetails dictionaryDetails = dictionaryDetailsService.getById(id);\n        return ResultGenerator.genOkResult(dictionaryDetails);\n    }\n\n    @Operation(description = \"数据字典明细分页查询\")\n    @GetMapping\n    @ApiOperation(value=\"分页查询数据字典明细\", notes=\"分页查询 \")\n    @ApiImplicitParams({\n        @ApiImplicitParam(name = \"page\", value = \"第几页\", required = true, dataType = \"Integer\", paramType=\"query\"),\n        @ApiImplicitParam(name = \"size\", value = \"一页有几条\", required = true, dataType = \"Integer\", paramType=\"query\")\n    })\n    public Result list(@RequestParam(defaultValue = \"1\") Integer page,\n                       @RequestParam(defaultValue = \"10\") Integer size,\n                       @RequestParam(defaultValue = \"1\") Integer id,\n                       @RequestParam(defaultValue = \"null\") String keyword) {\n        String inKeyword = null;\n        if (!(keyword == null || keyword.equals(\"null\"))) {\n            inKeyword = keyword;\n        }\n        Integer inId = Integer.valueOf(id);\n        if(inId==null){\n            inId = dictionaryDetailsService.listAll().get(0).getId();\n        }\n        PageHelper.startPage(page, size);\n        List<DictionaryContents> list = dictionaryDetailsService.listWithKeyword(inId.intValue(),inKeyword);\n        PageInfo<DictionaryContents> pageInfo = PageInfo.of(list);\n        return ResultGenerator.genOkResult(pageInfo);\n    }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/controller/EmployeeController.java",
    "content": "package com.msy.plus.controller;\n\nimport com.alibaba.fastjson.JSONObject;\nimport com.msy.plus.core.jwt.JwtUtil;\nimport com.msy.plus.core.response.Result;\nimport com.msy.plus.core.response.ResultGenerator;\nimport com.msy.plus.entity.Employee;\nimport com.msy.plus.entity.EmployeeDetail;\nimport com.msy.plus.entity.EmployeeWithRoleDO;\nimport com.msy.plus.service.EmployeeService;\nimport com.msy.plus.util.JsonUtils;\nimport com.msy.plus.util.RedisUtils;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.annotations.Api;\nimport io.swagger.annotations.ApiImplicitParam;\nimport io.swagger.annotations.ApiImplicitParams;\nimport io.swagger.annotations.ApiOperation;\nimport org.springframework.security.access.prepost.PreAuthorize;\nimport com.github.pagehelper.PageHelper;\nimport com.github.pagehelper.PageInfo;\nimport org.springframework.security.crypto.password.PasswordEncoder;\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.annotation.Resource;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n* @author MoShuYing\n* @date 2021/05/15\n*/\n@PreAuthorize(\n        \"hasAuthority('ADMIN')\"+\n                \"or hasAuthority('董事长')\"+\n                \"or hasAuthority('主席')\"+\n                \"or hasAuthority('高级主席')\"+\n                \"or hasAuthority('副主席')\"+\n                \"or hasAuthority('总裁')\"+\n                \"or hasAuthority('会长')\"+\n                \"or hasAuthority('高级总裁')\"+\n                \"or hasAuthority('高级副总裁')\"+\n                \"or hasAuthority('副总裁')\"+\n                \"or hasAuthority('总经理')\"+\n                \"or hasAuthority('副总经理')\"+\n                \"or hasAuthority('总监')\"+\n                \"or hasAuthority('经理')\"+\n                \"or hasAuthority('高级经理')\"+\n                \"or hasAuthority('副经理')\"+\n                \"or hasAuthority('主任')\"+\n                \"or hasAuthority('高级主任')\"+\n                \"or hasAuthority('副主任')\"+\n                \"or hasAuthority('组长')\"+\n                \"or hasAuthority('副组长')\"+\n                \"or hasAuthority('人事专员')\"+\n                \"or hasAuthority('市场专员')\"+\n                \"or hasAuthority('市场主管')\"+\n                \"or hasAuthority('销售主管')\"\n)\n@Api(tags={\"员工接口\"})\n@RestController\n@RequestMapping(\"/employee\")\npublic class EmployeeController {\n    @Resource\n    private EmployeeService employeeService;\n    @Resource private PasswordEncoder passwordEncoder;\n    @Resource private JwtUtil jwtUtil;\n\n    @Operation(description = \"员工添加\")\n    @PostMapping\n    public Result add(@RequestBody EmployeeDetail employee) {\n        if (employee.getId()!=null){\n            employee.setId(null);\n        }\n        if(employee.getDept() ==null){\n            return ResultGenerator.genFailedResult(\"请填写员工部门信息\");\n        }\n        if(employee.getPassword()!=null && employee.getPassword().length()<=5){\n            return ResultGenerator.genFailedResult(\"密码长度不能少于或等于五位\");\n        }\n        employee.setPassword(this.passwordEncoder.encode(employee.getPassword().trim()));\n        try{\n            employeeService.save(employee);\n        }catch (Exception e){\n            e.printStackTrace();\n            String msg = \"信息有误\";\n            if(e.toString().contains(\"for key 'employee.employee_name_uindex'\")){\n                msg = \"已有同名员工，请检查员工名称\";\n            }else if(e.toString().contains(\"for key 'employee.employee_email_uindex'\")){\n                msg = \"已有同名邮箱，请检查员工邮箱\";\n            }\n            return ResultGenerator.genFailedResult(msg);\n        }\n        if(!(employee.getRoleIds() ==null || employee.getRoleIds().size()<1)){\n            employeeService.saveRoles(employee.getId(),employee.getRoleIds());\n        }\n        return ResultGenerator.genOkResult();\n    }\n\n    @Operation(description = \"员工删除\")\n    @DeleteMapping(\"/{id}\")\n    public Result delete(@PathVariable Long id) {\n        employeeService.deleteById(id);\n        employeeService.deleteEmployeeWithRole(id);\n        return ResultGenerator.genOkResult();\n    }\n\n    @Operation(description = \"员工更新\")\n    @PutMapping\n    public Result update(@RequestBody EmployeeDetail employee,@RequestHeader Map<String, String> headers) {\n        if(employee.getName().equals(\"admin\")){\n            return ResultGenerator.genFailedResult(\"禁止修改管理员角色！\");\n        }\n        // 更新员工基本信息\n        if(employee.getDept() ==null){\n            return ResultGenerator.genFailedResult(\"请填写员工部门信息\");\n        }\n        if(employee.getPassword()!=null){\n            if(employee.getPassword().length()<=5){\n                return ResultGenerator.genFailedResult(\"密码长度不能少于或等于五位\");\n            }\n            employee.setPassword(this.passwordEncoder.encode(employee.getPassword().trim()));\n        }\n        try{\n            employeeService.update((Employee) employee);\n        }catch (Exception e){\n            e.printStackTrace();\n            String msg = \"信息有误\";\n            if(e.toString().contains(\"for key 'employee.employee_name_uindex'\")){\n                msg = \"已有同名员工，请检查员工名称\";\n            }else if(e.toString().contains(\"for key 'employee.employee_email_uindex'\")){\n                msg = \"已有同名邮箱，请检查员工邮箱\";\n            }\n            return ResultGenerator.genFailedResult(msg);\n        }\n        List<Long> now= employee.getRoleIds();\n        if(now==null) {\n            return ResultGenerator.genOkResult();\n        }\n        List<Long> raw = this.employeeService.getAllEmployeeRoleTableRow(employee.getId());\n        // diff运算\n        List<Long> adds = new ArrayList<>();\n        List<Long> removes = new ArrayList<>();\n        for(Long i:now){\n            if(!raw.contains(i)){\n                adds.add(i);\n            }\n        }\n        for(Long i:raw){\n            if(!now.contains(i)){\n                removes.add(i);\n            }\n        }\n        // 更新权限即注销对应用户登录\n        if(!adds.isEmpty() || !removes.isEmpty()){\n            jwtUtil.invalidRedisToken(employee.getName());\n        }\n        if(!adds.isEmpty()){\n            this.employeeService.saveRoles(employee.getId(),adds);\n        }\n        if(!removes.isEmpty()){\n            for(Long i :removes){\n                this.employeeService.deleteEmployeeWithRoleItem(employee.getId(),i);\n            }\n        }\n\n        return ResultGenerator.genOkResult();\n    }\n\n    @Operation(description = \"员工获取详细信息\")\n    @GetMapping(\"/{id}\")\n    public Result detail(@PathVariable Long id) {\n        EmployeeDetail employee = employeeService.getDetailById(id);\n        final EmployeeDetail object = JsonUtils.deleteFields(employee, EmployeeDetail.class, \"password\");\n        return ResultGenerator.genOkResult(object);\n    }\n\n    @Operation(description = \"员工分页查询\")\n    @GetMapping\n    @ApiOperation(value=\"分页查询员工\", notes=\"分页查询 \")\n    @ApiImplicitParams({\n        @ApiImplicitParam(name = \"page\", value = \"第几页\", required = true, dataType = \"Integer\", paramType=\"query\"),\n        @ApiImplicitParam(name = \"size\", value = \"一页有几条\", required = true, dataType = \"Integer\", paramType=\"query\")\n    })\n    public Result list(\n            @RequestParam(defaultValue = \"1\") Integer page,\n            @RequestParam(defaultValue = \"10\") Integer size,\n            @RequestParam(required = false) Integer dept,\n            @RequestParam(defaultValue = \"\") String keyword) {\n        PageHelper.startPage(page, size);\n        List<EmployeeWithRoleDO> list = employeeService.listEmployeeWithRole(keyword, dept);\n        PageInfo<EmployeeWithRoleDO> pageInfo = PageInfo.of(list);\n        // 不显示 password 字段\n        final PageInfo<JSONObject> objectPageInfo = JsonUtils.deleteFields(pageInfo, PageInfo.class, \"password\");\n        return ResultGenerator.genOkResult(objectPageInfo);\n    }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/controller/PermissionController.java",
    "content": "package com.msy.plus.controller;\n\nimport com.msy.plus.core.response.Result;\nimport com.msy.plus.core.response.ResultGenerator;\nimport com.msy.plus.entity.Permission;\nimport com.msy.plus.service.PermissionService;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.annotations.Api;\nimport io.swagger.annotations.ApiImplicitParam;\nimport io.swagger.annotations.ApiImplicitParams;\nimport io.swagger.annotations.ApiOperation;\nimport org.springframework.security.access.prepost.PreAuthorize;\nimport com.github.pagehelper.PageHelper;\nimport com.github.pagehelper.PageInfo;\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.annotation.Resource;\nimport java.util.List;\n\n/**\n* @author MoShuYing\n* @date 2021/05/14\n*/\n@PreAuthorize(\"hasAuthority('ADMIN')\")\n@Api(tags={\"权限接口\"})\n@RestController\n@RequestMapping(\"/permission\")\npublic class PermissionController {\n    @Resource\n    private PermissionService permissionService;\n//\n//    @Operation(description = \"权限添加\")\n//    @PostMapping\n//    public Result add(@RequestBody Permission permission) {\n//        permissionService.save(permission);\n//        return ResultGenerator.genOkResult();\n//    }\n\n    @Operation(description = \"权限删除\")\n    @DeleteMapping(\"/{id}\")\n    public Result delete(@PathVariable Long id) {\n    permissionService.deleteById(id);\n        return ResultGenerator.genOkResult();\n    }\n\n//    @Operation(description = \"权限更新\")\n//    @PutMapping\n//    public Result update(@RequestBody Permission permission) {\n//    permissionService.update(permission);\n//        return ResultGenerator.genOkResult();\n//    }\n\n//    @Operation(description = \"权限获取详细信息\")\n//    @GetMapping(\"/{id}\")\n//    public Result detail(@PathVariable Long id) {\n//    Permission permission = permissionService.getById(id);\n//        return ResultGenerator.genOkResult(permission);\n//    }\n\n    @Operation(description = \"权限分页查询\")\n    @GetMapping\n    @ApiOperation(value=\"分页查询权限\", notes=\"分页查询 \")\n    @ApiImplicitParams({\n        @ApiImplicitParam(name = \"page\", value = \"第几页\", required = true, dataType = \"Integer\", paramType=\"query\"),\n        @ApiImplicitParam(name = \"size\", value = \"一页有几条\", required = true, dataType = \"Integer\", paramType=\"query\")\n    })\n    public Result list(@RequestParam(defaultValue = \"1\") Integer page,\n    @RequestParam(defaultValue = \"10\") Integer size) {\n        PageHelper.startPage(page, size);\n        List<Permission> list = permissionService.listAll();\n        PageInfo<Permission> pageInfo = PageInfo.of(list);\n        return ResultGenerator.genOkResult(pageInfo);\n    }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/controller/RoleController.java",
    "content": "package com.msy.plus.controller;\n\nimport com.github.pagehelper.PageHelper;\nimport com.github.pagehelper.PageInfo;\nimport com.msy.plus.core.response.Result;\nimport com.msy.plus.core.response.ResultGenerator;\nimport com.msy.plus.dto.RoleWithPermissionDTO;\nimport com.msy.plus.entity.RoleDO;\nimport com.msy.plus.entity.RolePermissionDO;\nimport com.msy.plus.service.RoleService;\nimport io.swagger.annotations.Api;\nimport io.swagger.annotations.ApiImplicitParam;\nimport io.swagger.annotations.ApiImplicitParams;\nimport io.swagger.annotations.ApiOperation;\nimport io.swagger.v3.oas.annotations.Operation;\nimport org.springframework.dao.DuplicateKeyException;\nimport org.springframework.security.access.prepost.PreAuthorize;\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.annotation.Resource;\nimport java.util.*;\n\n/**\n * 角色控制器\n *\n * @author MoShuying\n * @date 2018/05/27\n */\n@PreAuthorize(\"hasAuthority('ADMIN')\")\n@Api(tags={\"角色接口\"})\n@RestController\n@RequestMapping(\"/role\")\npublic class RoleController {\n  @Resource private RoleService roleService;\n\n  @Operation(description = \"角色添加\")\n  @PostMapping\n  public Result add(@RequestBody final RoleWithPermissionDTO roleDTO) {\n    if(roleDTO.getPermissions()==null){\n      return ResultGenerator.genFailedResult(\"尚未添加角色权限\");\n    }\n    try{\n      this.roleService.save(roleDTO);\n    }catch (DuplicateKeyException e){\n      return ResultGenerator.genFailedResult(\"提交的信息中包含已存在的字段\");\n    }\n    List<Long> temp = new ArrayList<>();\n    roleDTO.getPermissions().forEach(e->{ temp.add(e.getId()); });\n    this.roleService.savePermissions(roleDTO.getId(),temp);\n    return ResultGenerator.genOkResult();\n  }\n\n  @Operation(description = \"角色删除\")\n  @DeleteMapping(\"/{id}\")\n  public Result delete(@PathVariable final Long id) {\n    List<RolePermissionDO> raw = this.roleService.getAllRolePermissionTableRow(id);\n    for(RolePermissionDO e :raw){\n      this.roleService.deleteRolePermissionItem(id,e.getPermission_id());\n    }\n    this.roleService.deleteById(id);\n    return ResultGenerator.genOkResult();\n  }\n\n  @Operation(description = \"角色更新\")\n  @PutMapping\n  public Result update(@RequestBody final RoleWithPermissionDTO roleWithPermissionDTO) {\n    // 更新用户基本信息\n    this.roleService.update(roleWithPermissionDTO);\n    List<Long> nowPermissions = new ArrayList<>();\n    if(roleWithPermissionDTO.getPermissions()==null){\n      return ResultGenerator.genOkResult();\n    }\n\n    List<RolePermissionDO> rawPer = this.roleService.getAllRolePermissionTableRow(roleWithPermissionDTO.getId());\n    // 表中权限信息去重\n    Set<Long> raw = new HashSet<>();\n    for(RolePermissionDO e: rawPer){\n      raw.add(e.getPermission_id());\n    }\n\n    roleWithPermissionDTO.getPermissions().forEach(e->{ nowPermissions.add(e.getId()); });\n\n    // diff运算\n    Set<Long> adds = new HashSet<>();\n    Set<Long> removes = new HashSet<>();\n\n    // 如果修改后的不包含原来的 那么为新增元素\n    for(Long i:nowPermissions){\n      if(!raw.contains(i)){\n        adds.add(i);\n      }\n    }\n\n    // 如果原来的不包含修改后的 那么是删除元素\n    for(Long i:raw){\n      if(!nowPermissions.contains(i)){\n        removes.add(i);\n      }\n    }\n\n    if(!adds.isEmpty()){\n        this.roleService.savePermissions(roleWithPermissionDTO.getId(),new ArrayList<>(adds));\n    }\n    if(!removes.isEmpty()){\n      removes.forEach(e->{\n        this.roleService.deleteRolePermissionItem(roleWithPermissionDTO.getId(),e);\n      });\n    }\n    return ResultGenerator.genOkResult();\n  }\n  @Operation(description = \"角色详情\")\n  @GetMapping(\"/{id}\")\n  public Result detail(@PathVariable final Long id) {\n    final RoleDO role = this.roleService.getDetailById(id);\n    return ResultGenerator.genOkResult(role);\n  }\n\n  @Operation(description = \"角色列表\")\n  @GetMapping\n  @ApiOperation(value=\"分页查询角色\", notes=\"分页查询角色列表\")\n  @ApiImplicitParams({\n          @ApiImplicitParam(name = \"page\", value = \"第几页\", required = true, dataType = \"Integer\", paramType=\"query\"),\n          @ApiImplicitParam(name = \"size\", value = \"一页有几条\", required = true, dataType = \"Integer\", paramType=\"query\")\n  })\n  public Result list(\n      @RequestParam(defaultValue = \"1\") final Integer page,\n      @RequestParam(defaultValue = \"10\") final Integer size) {\n    PageHelper.startPage(page, size);\n    final List<RoleDO> list = this.roleService.listAll();\n    final PageInfo<RoleDO> pageInfo = new PageInfo<>(list);\n    return ResultGenerator.genOkResult(pageInfo);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/cache/CacheExpire.java",
    "content": "package com.msy.plus.core.cache;\n\nimport org.springframework.core.annotation.AliasFor;\n\nimport java.lang.annotation.*;\n\n/**\n * 缓存过期注解\n *\n * @author MoShuying\n * @date 2018/07/11\n */\n@Inherited\n@Documented\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD, ElementType.TYPE})\npublic @interface CacheExpire {\n  /** 过期时间，默认 60s */\n  @AliasFor(\"expire\")\n  long value() default 60L;\n\n  /** 过期时间，默认 60s */\n  @AliasFor(\"value\")\n  long expire() default 60L;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/cache/MyRedisCacheManager.java",
    "content": "package com.msy.plus.core.cache;\n\nimport com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.BeansException;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.cache.Cache;\nimport org.springframework.cache.annotation.CacheConfig;\nimport org.springframework.cache.annotation.Cacheable;\nimport org.springframework.cache.annotation.Caching;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationContextAware;\nimport org.springframework.core.annotation.AnnotationUtils;\nimport org.springframework.data.redis.cache.RedisCache;\nimport org.springframework.data.redis.cache.RedisCacheConfiguration;\nimport org.springframework.data.redis.cache.RedisCacheManager;\nimport org.springframework.data.redis.cache.RedisCacheWriter;\nimport org.springframework.data.redis.serializer.RedisSerializationContext;\nimport org.springframework.data.redis.serializer.StringRedisSerializer;\nimport org.springframework.lang.NonNull;\nimport org.springframework.util.ReflectionUtils;\n\nimport java.time.Duration;\nimport java.util.*;\nimport java.util.concurrent.Callable;\n\n/**\n * Redis 容易出现缓存问题（超时、Redis 宕机等），当使用 spring cache 的注释 Cacheable、Cacheput 等处理缓存问题时， 我们无法使用 try catch\n * 处理出现的异常，所以最后导致结果是整个服务报错无法正常工作。 通过自定义 MyRedisCacheManager 并继承 RedisCacheManager 来处理异常可以解决这个问题\n *\n * <p>http://www.spring4all.com/article/937\n *\n * @author MoShuying\n * @date 2018/07/11\n */\n@Slf4j\npublic class MyRedisCacheManager extends RedisCacheManager\n    implements ApplicationContextAware, InitializingBean {\n  /** key serializer */\n  public static final StringRedisSerializer STRING_SERIALIZER = new StringRedisSerializer();\n  /**\n   * value serializer\n   *\n   * <p>使用 FastJsonRedisSerializer 会报错：java.lang.ClassCastException FastJsonRedisSerializer<Object>\n   * fastSerializer = new FastJsonRedisSerializer<>(Object.class);\n   */\n  public static final GenericFastJsonRedisSerializer FASTJSON_SERIALIZER =\n      new GenericFastJsonRedisSerializer();\n  /** key serializer pair */\n  public static final RedisSerializationContext.SerializationPair<String> STRING_PAIR =\n      RedisSerializationContext.SerializationPair.fromSerializer(STRING_SERIALIZER);\n  /** value serializer pair */\n  public static final RedisSerializationContext.SerializationPair<Object> FASTJSON_PAIR =\n      RedisSerializationContext.SerializationPair.fromSerializer(FASTJSON_SERIALIZER);\n\n  private final Map<String, RedisCacheConfiguration> initialCacheConfiguration =\n      new LinkedHashMap<>();\n  private ApplicationContext applicationContext;\n\n  public MyRedisCacheManager(\n      final RedisCacheWriter cacheWriter, final RedisCacheConfiguration defaultCacheConfiguration) {\n    super(cacheWriter, defaultCacheConfiguration);\n  }\n\n  public MyRedisCacheManager(\n      final RedisCacheWriter cacheWriter,\n      final RedisCacheConfiguration defaultCacheConfiguration,\n      final Map<String, RedisCacheConfiguration> initialCacheConfigurations) {\n    super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);\n  }\n\n  @Override\n  public Cache getCache(@NonNull final String name) {\n    final Cache cache = super.getCache(name);\n    return new RedisCacheWrapper(cache);\n  }\n\n  @Override\n  public void setApplicationContext(@NonNull final ApplicationContext applicationContext)\n      throws BeansException {\n    this.applicationContext = applicationContext;\n  }\n\n  @Override\n  public void afterPropertiesSet() {\n    final String[] beanNames = this.applicationContext.getBeanNamesForType(Object.class);\n    for (final String beanName : beanNames) {\n      final Class clazz = this.applicationContext.getType(beanName);\n      this.add(clazz);\n    }\n    super.afterPropertiesSet();\n  }\n\n  @NonNull\n  @Override\n  protected Collection<RedisCache> loadCaches() {\n    final List<RedisCache> caches = new LinkedList<>();\n    for (final Map.Entry<String, RedisCacheConfiguration> entry :\n        this.initialCacheConfiguration.entrySet()) {\n      caches.add(super.createRedisCache(entry.getKey(), entry.getValue()));\n    }\n    return caches;\n  }\n\n  private void add(final Class clazz) {\n    ReflectionUtils.doWithMethods(\n        clazz,\n        method -> {\n          ReflectionUtils.makeAccessible(method);\n          final CacheExpire cacheExpire = AnnotationUtils.findAnnotation(method, CacheExpire.class);\n          if (!Optional.ofNullable(cacheExpire).isPresent()) {\n            return;\n          }\n          final Cacheable cacheable = AnnotationUtils.findAnnotation(method, Cacheable.class);\n          if (Optional.ofNullable(cacheable).isPresent()) {\n            this.add(cacheable.cacheNames(), cacheExpire);\n            return;\n          }\n          final Caching caching = AnnotationUtils.findAnnotation(method, Caching.class);\n          if (Optional.ofNullable(caching).isPresent()) {\n            final Cacheable[] cs = caching.cacheable();\n            if (cs.length > 0) {\n              for (final Cacheable c : cs) {\n                if (Optional.ofNullable(c).isPresent()) {\n                  this.add(c.cacheNames(), cacheExpire);\n                }\n              }\n            }\n          } else {\n            final CacheConfig cacheConfig =\n                AnnotationUtils.findAnnotation(clazz, CacheConfig.class);\n            if (Optional.ofNullable(cacheConfig).isPresent()) {\n              this.add(cacheConfig.cacheNames(), cacheExpire);\n            }\n          }\n        },\n        method -> null != AnnotationUtils.findAnnotation(method, CacheExpire.class));\n  }\n\n  private void add(final String[] cacheNames, final CacheExpire cacheExpire) {\n    for (final String cacheName : cacheNames) {\n      if (!Optional.ofNullable(cacheName).isPresent() || \"\".equals(cacheName.trim())) {\n        continue;\n      }\n      final long expire = cacheExpire.expire();\n      log.debug(\"cache name<{}> expire: {}\", cacheName, expire);\n      if (expire >= 0) {\n        // 缓存配置\n        final RedisCacheConfiguration config =\n            RedisCacheConfiguration.defaultCacheConfig()\n                .entryTtl(Duration.ofSeconds(expire))\n                .disableCachingNullValues()\n                // .prefixKeysWith(cacheName)\n                .serializeKeysWith(STRING_PAIR)\n                .serializeValuesWith(FASTJSON_PAIR);\n        this.initialCacheConfiguration.put(cacheName, config);\n      } else {\n        log.warn(\"{} use default expiration.\", cacheName);\n      }\n    }\n  }\n\n  protected static class RedisCacheWrapper implements Cache {\n    private final Cache cache;\n\n    RedisCacheWrapper(final Cache cache) {\n      this.cache = cache;\n    }\n\n    @Override\n    public String getName() {\n      MyRedisCacheManager.log.debug(\"get name: {}\", this.cache.getName());\n      try {\n        return this.cache.getName();\n      } catch (final Exception e) {\n        MyRedisCacheManager.log.error(\"get name => {}\", e.getMessage());\n        return null;\n      }\n    }\n\n    @Override\n    public Object getNativeCache() {\n      MyRedisCacheManager.log.debug(\"native cache: {}\", this.cache.getNativeCache());\n      try {\n        return this.cache.getNativeCache();\n      } catch (final Exception e) {\n        MyRedisCacheManager.log.error(\"get native cache => {}\", e.getMessage());\n        return null;\n      }\n    }\n\n    @Override\n    public ValueWrapper get(@NonNull final Object o) {\n      MyRedisCacheManager.log.debug(\"get => o: {}\", o);\n      try {\n        return this.cache.get(o);\n      } catch (final Exception e) {\n        MyRedisCacheManager.log.error(\"get => o: {}, error: {}\", o, e.getMessage());\n        return null;\n      }\n    }\n\n    @Override\n    public <T> T get(@NonNull final Object o, final Class<T> aClass) {\n      MyRedisCacheManager.log.debug(\"get => o: {}, clazz: {}\", o, aClass);\n      try {\n        return this.cache.get(o, aClass);\n      } catch (final Exception e) {\n        MyRedisCacheManager.log.error(\"get => o: {}, clazz: {}, error: {}\", o, aClass, e.getMessage());\n        return null;\n      }\n    }\n\n    @Override\n    public <T> T get(@NonNull final Object o, @NonNull final Callable<T> callable) {\n      MyRedisCacheManager.log.debug(\"get => o: {}\", o);\n      try {\n        return this.cache.get(o, callable);\n      } catch (final Exception e) {\n        MyRedisCacheManager.log.error(\"get => o: {}, error: {}\", o, e.getMessage());\n        return null;\n      }\n    }\n\n    @Override\n    public void put(@NonNull final Object o, final Object o1) {\n      MyRedisCacheManager.log.debug(\"put => o: {}, o1: {}\", o, o1);\n      try {\n        this.cache.put(o, o1);\n      } catch (final Exception e) {\n        MyRedisCacheManager.log.error(\"put => o: {}, o1: {}, error: {}\", o, o1, e.getMessage());\n      }\n    }\n\n    @Override\n    public ValueWrapper putIfAbsent(@NonNull final Object o, final Object o1) {\n      MyRedisCacheManager.log.debug(\"put if absent => o: {}, o1: {}\", o, o1);\n      try {\n        return this.cache.putIfAbsent(o, o1);\n      } catch (final Exception e) {\n        MyRedisCacheManager.log.error(\"put if absent => o: {}, o1: {}, error: {}\", o, o1, e.getMessage());\n        return null;\n      }\n    }\n\n    @Override\n    public void evict(@NonNull final Object o) {\n      MyRedisCacheManager.log.debug(\"evict => o: {}\", o);\n      try {\n        this.cache.evict(o);\n      } catch (final Exception e) {\n        MyRedisCacheManager.log.error(\"evict => o: {}, error: {}\", o, e.getMessage());\n      }\n    }\n\n    @Override\n    public void clear() {\n      MyRedisCacheManager.log.debug(\"clear\");\n      try {\n        this.cache.clear();\n      } catch (final Exception e) {\n        MyRedisCacheManager.log.error(\"clear => error: {}\", e.getMessage());\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/config/JasyptConfig.java",
    "content": "package com.msy.plus.core.config;\n\nimport com.msy.plus.core.rsa.RsaUtils;\nimport org.jasypt.encryption.StringEncryptor;\nimport org.jasypt.encryption.pbe.PooledPBEStringEncryptor;\nimport org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.util.Base64Utils;\n\nimport javax.annotation.Resource;\n\n/**\n * Jasypt 配置（2.1.1可以配置非对称加密，但是测试还有问题，等解决再更新）\n *\n * @author MoShuying\n * @date 2018/07/21\n */\n@Configuration\npublic class JasyptConfig {\n  @Value(\"${jasypt.encryptor.password}\")\n  private String passwordEncryptedByBase64AndRsa;\n\n  @Resource private RsaUtils rsaUtils;\n\n  @Bean\n  public StringEncryptor myStringEncryptor() throws Exception {\n    // 先 Base64，后 RSA 加密的密码\n    final byte[] passwordEncryptedByRsa =\n        Base64Utils.decodeFromString(this.passwordEncryptedByBase64AndRsa);\n    final String password = new String(this.rsaUtils.decrypt(passwordEncryptedByRsa));\n    // 配置\n    final SimpleStringPBEConfig config =\n        new SimpleStringPBEConfig() {\n          {\n            this.setPassword(password);\n            // 加密算法\n            this.setAlgorithm(\"PBEWithMD5AndDES\");\n            this.setKeyObtentionIterations(\"1000\");\n            this.setPoolSize(\"1\");\n            this.setProviderName(\"SunJCE\");\n            this.setSaltGeneratorClassName(\"org.jasypt.salt.RandomSaltGenerator\");\n            this.setStringOutputType(\"base64\");\n          }\n        };\n    final PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();\n    encryptor.setConfig(config);\n    return encryptor;\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/config/RedisCacheConfig.java",
    "content": "package com.msy.plus.core.config;\n\nimport com.msy.plus.core.cache.MyRedisCacheManager;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.boot.autoconfigure.data.redis.RedisProperties;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.cache.Cache;\nimport org.springframework.cache.CacheManager;\nimport org.springframework.cache.annotation.CachingConfigurerSupport;\nimport org.springframework.cache.annotation.EnableCaching;\nimport org.springframework.cache.interceptor.CacheErrorHandler;\nimport org.springframework.cache.interceptor.KeyGenerator;\nimport org.springframework.cache.interceptor.SimpleCacheErrorHandler;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.data.redis.cache.RedisCacheConfiguration;\nimport org.springframework.data.redis.cache.RedisCacheWriter;\nimport org.springframework.data.redis.connection.RedisConnectionFactory;\n\nimport javax.annotation.Resource;\nimport java.util.Optional;\n\n/**\n * Redis缓存配置\n *\n * @author MoShuying\n * @date 2018/07/11\n */\n@Slf4j\n@Configuration\n@EnableCaching(proxyTargetClass = true)\n@ConditionalOnProperty(name = \"spring.redis.host\")\n@EnableConfigurationProperties(RedisProperties.class)\npublic class RedisCacheConfig extends CachingConfigurerSupport {\n  @Resource private RedisConnectionFactory redisConnectionFactory;\n\n  @Bean\n  @Override\n  public CacheManager cacheManager() {\n    // 初始化一个 RedisCacheWriter\n    final RedisCacheWriter redisCacheWriter =\n        RedisCacheWriter.nonLockingRedisCacheWriter(this.redisConnectionFactory);\n    final RedisCacheConfiguration defaultCacheConfig =\n        RedisCacheConfiguration.defaultCacheConfig()\n            // 不缓存 null 值\n            // .disableCachingNullValues()\n            // 使用注解时的序列化、反序列化对\n            .serializeKeysWith(MyRedisCacheManager.STRING_PAIR)\n            .serializeValuesWith(MyRedisCacheManager.FASTJSON_PAIR);\n    // 初始化RedisCacheManager\n    return new MyRedisCacheManager(redisCacheWriter, defaultCacheConfig);\n  }\n\n  /**\n   * 如果 @Cacheable、@CachePut、@CacheEvict 等注解没有配置 key，则使用这个自定义 key 生成器\n   *\n   * <p>自定义缓存的 key 时，难以保证 key 的唯一性\n   *\n   * <p>此时最好指定方法名，比如：@Cacheable(value=\"\", key=\"{#root.methodName, #id}\")\n   */\n  @Bean\n  @Override\n  public KeyGenerator keyGenerator() {\n    // 比如 User 类 list(Integer page, Integer size) 方法\n    // 用户 A 请求：list(1, 2)\n    // redis 缓存的 key：User.list#1,2\n    return (target, method, params) -> {\n      final String dot = \".\";\n      final StringBuilder sb = new StringBuilder(32);\n      // 类名\n      sb.append(target.getClass().getSimpleName());\n      sb.append(dot);\n      // 方法名\n      sb.append(method.getName());\n      // 如果存在参数\n      if (0 < params.length) {\n        sb.append(\"#\");\n        // 带上参数\n        String comma = \"\";\n        for (final Object param : params) {\n          sb.append(comma);\n          if (!Optional.ofNullable(param).isPresent()) {\n            sb.append(\"NULL\");\n          } else {\n            sb.append(param.toString());\n          }\n          comma = \",\";\n        }\n      }\n      return sb.toString();\n    };\n  }\n\n  /** 错误处理，主要是打印日志 */\n  @Bean\n  @Override\n  public CacheErrorHandler errorHandler() {\n    return new SimpleCacheErrorHandler() {\n      @Override\n      public void handleCacheGetError(\n          final RuntimeException e, final Cache cache, final Object key) {\n        RedisCacheConfig.log.error(\"==> cache: {}\", cache);\n        RedisCacheConfig.log.error(\"==>   key: {}\", key);\n        RedisCacheConfig.log.error(\"==> error: {}\", e.getMessage());\n        super.handleCacheGetError(e, cache, key);\n      }\n\n      @Override\n      public void handleCachePutError(\n          final RuntimeException e, final Cache cache, final Object key, final Object value) {\n        RedisCacheConfig.log.error(\"==> cache: {}\", cache);\n        RedisCacheConfig.log.error(\"==>   key: {}\", key);\n        RedisCacheConfig.log.error(\"==> value: {}\", value);\n        RedisCacheConfig.log.error(\"==> error: {}\", e.getMessage());\n        super.handleCachePutError(e, cache, key, value);\n      }\n\n      @Override\n      public void handleCacheEvictError(\n          final RuntimeException e, final Cache cache, final Object key) {\n        RedisCacheConfig.log.error(\"==> cache: {}\", cache);\n        RedisCacheConfig.log.error(\"==>   key: {}\", key);\n        RedisCacheConfig.log.error(\"==> error: {}\", e.getMessage());\n        super.handleCacheEvictError(e, cache, key);\n      }\n\n      @Override\n      public void handleCacheClearError(final RuntimeException e, final Cache cache) {\n        RedisCacheConfig.log.error(\"==> cache: {}\", cache);\n        RedisCacheConfig.log.error(\"==> error: {}\", e.getMessage());\n        super.handleCacheClearError(e, cache);\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/config/RedisConfig.java",
    "content": "package com.msy.plus.core.config;\n\nimport com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;\nimport com.msy.plus.core.cache.MyRedisCacheManager;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.boot.autoconfigure.data.redis.RedisProperties;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.data.redis.connection.RedisConnectionFactory;\nimport org.springframework.data.redis.connection.RedisStandaloneConfiguration;\nimport org.springframework.data.redis.connection.jedis.JedisClientConfiguration;\nimport org.springframework.data.redis.connection.jedis.JedisConnectionFactory;\nimport org.springframework.data.redis.core.RedisTemplate;\nimport org.springframework.data.redis.serializer.StringRedisSerializer;\nimport redis.clients.jedis.JedisPoolConfig;\n\nimport javax.annotation.Resource;\nimport java.time.Duration;\n\n/**\n * Redis配置\n *\n * @author MoShuying\n * @date 2018/05/27\n */\n@Configuration\npublic class RedisConfig {\n  @Resource private RedisProperties redisProperties;\n\n  @Bean\n  @ConfigurationProperties(prefix = \"spring.redis.jedis.pool\")\n  public JedisPoolConfig jedisPoolConfig() {\n    return new JedisPoolConfig();\n  }\n\n  @Bean\n  @ConfigurationProperties(prefix = \"spring.redis\")\n  public RedisConnectionFactory redisConnectionFactory(\n      @Qualifier(value = \"jedisPoolConfig\") final JedisPoolConfig jedisPoolConfig) {\n    // 方法上的 @ConfigurationProperties 不生效\n    // 未知 bug，暂时这样手动设置\n    // fixme\n    // 单机版 jedis\n    final RedisStandaloneConfiguration redisStandaloneConfiguration =\n        new RedisStandaloneConfiguration(\n            this.redisProperties.getHost(), this.redisProperties.getPort());\n    redisStandaloneConfiguration.setDatabase(this.redisProperties.getDatabase());\n    redisStandaloneConfiguration.setPassword(this.redisProperties.getPassword());\n\n    // 获得默认的连接池构造器\n    final JedisClientConfiguration.JedisPoolingClientConfigurationBuilder jpcb =\n        (JedisClientConfiguration.JedisPoolingClientConfigurationBuilder)\n            JedisClientConfiguration.builder();\n    // 指定 jedisPoolConifig 来修改默认的连接池构造器\n    jpcb.poolConfig(jedisPoolConfig);\n    // 连接超时\n    jpcb.and().connectTimeout(Duration.ofSeconds(10));\n    // 通过构造器来构造 jedis 客户端配置\n    final JedisClientConfiguration jedisClientConfiguration = jpcb.build();\n    // 单机配置 + 客户端配置 = jedis 连接工厂\n    return new JedisConnectionFactory(redisStandaloneConfiguration, jedisClientConfiguration);\n  }\n\n  /**\n   * 配置 RedisTemplate，配置 key 和 value 的序列化类\n   *\n   * <p>key 序列化使用 StringRedisSerializer, 不配置的话，key 会出现乱码\n   */\n  @Bean\n  public RedisTemplate redisTemplate(\n      @Qualifier(value = \"redisConnectionFactory\") final RedisConnectionFactory factory) {\n    final RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();\n\n    // 设置 key 的序列化器为字符串 serializer\n    final StringRedisSerializer stringSerializer = MyRedisCacheManager.STRING_SERIALIZER;\n\n    redisTemplate.setKeySerializer(stringSerializer);\n    redisTemplate.setHashKeySerializer(stringSerializer);\n\n    // 设置 value 的序列化器为 fastjson serializer\n    final GenericFastJsonRedisSerializer fastSerializer = MyRedisCacheManager.FASTJSON_SERIALIZER;\n\n    redisTemplate.setValueSerializer(fastSerializer);\n    redisTemplate.setHashValueSerializer(fastSerializer);\n\n    // 如果 KeySerializer 或者 ValueSerializer 没有配置\n    // 则对应的 KeySerializer、ValueSerializer 才使用 fastjson serializer\n    redisTemplate.setDefaultSerializer(fastSerializer);\n\n    redisTemplate.setConnectionFactory(factory);\n    redisTemplate.afterPropertiesSet();\n    return redisTemplate;\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/config/Swagger3Config.java",
    "content": "package com.msy.plus.core.config;\n\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.annotation.PropertySource;\nimport springfox.documentation.service.*;\nimport springfox.documentation.builders.ApiInfoBuilder;\nimport springfox.documentation.builders.PathSelectors;\nimport springfox.documentation.builders.RequestHandlerSelectors;\nimport springfox.documentation.spi.DocumentationType;\nimport springfox.documentation.spring.web.plugins.Docket;\nimport springfox.documentation.spi.service.contexts.SecurityContext;\n\nimport java.util.Collections;\nimport java.util.List;\n\nimport static com.msy.plus.core.constant.ProjectConstant.SPRING_PROFILE_PRODUCTION;\n\n/**\n * Swagger3\n *\n * @author MoShuying\n * @date 2020/11/06\n */\n@PropertySource(\n    value = \"classpath:/META-INF/swagger3.yml\",\n    factory = YamlPropertySourceFactory.class)\n@Configuration\npublic class Swagger3Config {\n\n  @Value(\"${spring.profiles.active}\")\n  private String activeProfile;\n\n  @Value(\"${application.name}\")\n  private String applicationName;\n\n  @Value(\"${application.version}\")\n  private String applicationVersion;\n\n  @Value(\"${application.description}\")\n  private String applicationDescription;\n\n  @Value(\"${application.url.service}\")\n  private String applicationServiceUrl;\n\n  @Value(\"${application.license}\")\n  private String applicationLicense;\n\n  @Value(\"${application.url.license}\")\n  private String applicationLicenseUrl;\n\n  @Value(\"${application.apis.selector}\")\n  private String selector;\n\n  @Value(\"${author.name}\")\n  private String authorName;\n\n  @Value(\"${author.url}\")\n  private String authorUrl;\n\n  @Value(\"${author.email}\")\n  private String authorEmail;\n\n  private ApiInfo apiInfo() {\n    return new ApiInfoBuilder()\n            .title(applicationName)\n            .version(applicationVersion)\n            .description(applicationDescription)\n            .termsOfServiceUrl(applicationServiceUrl)\n            .contact(new Contact(authorName, authorUrl, authorEmail))\n            .license(applicationLicense)\n            .licenseUrl(applicationLicenseUrl)\n            .build();\n  }\n\n  @Bean\n  public Docket docket() {\n//添加head参数配置start\n    return new Docket(DocumentationType.SWAGGER_2)\n        .apiInfo(this.apiInfo())\n        // 仅在非生产环境下生效\n        .enable(!SPRING_PROFILE_PRODUCTION.equals(this.activeProfile))\n        .select()\n        .apis(RequestHandlerSelectors.basePackage(selector))\n        .paths(PathSelectors.any())\n        .build()\n        .securitySchemes(securitySchemes())\n        .securityContexts(securityContexts());\n  }\n\n  private List<SecurityScheme> securitySchemes(){\n    return Collections.singletonList(new ApiKey(\"Authorization\", \"Authorization\", \"header\"));\n  }\n\n  private List<SecurityContext> securityContexts() {\n    return Collections.singletonList(\n            SecurityContext.builder()\n                    .securityReferences(defaultAuth())\n                    .operationSelector(null)\n                    .build()\n    );\n  }\n\n  private List<SecurityReference> defaultAuth() {\n    AuthorizationScope authorizationScope = new AuthorizationScope(\"global\", \"accessEverything\");\n    return Collections.singletonList(new SecurityReference(\"Authorization\", new AuthorizationScope[]{authorizationScope}));\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/config/ValidatorConfig.java",
    "content": "package com.msy.plus.core.config;\n\nimport org.hibernate.validator.HibernateValidator;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.validation.beanvalidation.MethodValidationPostProcessor;\n\nimport javax.validation.Validation;\nimport javax.validation.Validator;\nimport javax.validation.ValidatorFactory;\n\n/**\n * 参数校验\n * https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-constraint-violation-methods\n *\n * @author MoShuying\n * @date 2018/05/27\n */\n@Configuration\npublic class ValidatorConfig {\n  @Bean\n  public MethodValidationPostProcessor methodValidationPostProcessor() {\n    final MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();\n    // 设置 validator 模式为快速失败返回\n    postProcessor.setValidator(this.validatorFailFast());\n    return postProcessor;\n    // 默认是普通模式，会返回所有的验证不通过信息集合\n    // return new MethodValidationPostProcessor();\n  }\n\n  @Bean\n  public Validator validatorFailFast() {\n    final ValidatorFactory validatorFactory =\n        Validation.byProvider(HibernateValidator.class)\n            .configure()\n            .addProperty(\"hibernate.validator.fail_fast\", \"true\")\n            .buildValidatorFactory();\n    return validatorFactory.getValidator();\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/config/WebMvcConfig.java",
    "content": "package com.msy.plus.core.config;\n\nimport com.alibaba.fastjson.serializer.SerializerFeature;\nimport com.alibaba.fastjson.support.config.FastJsonConfig;\nimport com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.converter.HttpMessageConverter;\nimport org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;\nimport org.springframework.web.servlet.config.annotation.ViewControllerRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Spring MVC 配置\n *\n * @author MoShuying\n * @date 2018/05/27\n */\n@Configuration\npublic class WebMvcConfig extends WebMvcConfigurationSupport {\n  /** 使用阿里 FastJson 作为 JSON MessageConverter */\n  @Override\n  public void configureMessageConverters(final List<HttpMessageConverter<?>> converters) {\n    final FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();\n    final FastJsonConfig config = new FastJsonConfig();\n    // 支持的输出类型\n    final List<MediaType> supportedMediaTypes = new ArrayList<>();\n    supportedMediaTypes.add(MediaType.APPLICATION_JSON);\n    supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);\n    supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);\n    supportedMediaTypes.add(MediaType.TEXT_HTML);\n    converter.setSupportedMediaTypes(supportedMediaTypes);\n    config.setSerializerFeatures(\n        // 保留空的字段\n        // SerializerFeature.WriteMapNullValue,\n        // Number null -> 0\n        SerializerFeature.WriteNullNumberAsZero,\n        // 美化输出\n        SerializerFeature.PrettyFormat);\n    converter.setFastJsonConfig(config);\n    converter.setDefaultCharset(StandardCharsets.UTF_8);\n    converters.add(converter);\n  }\n\n  /** 资源控制器 */\n  @Override\n  public void addResourceHandlers(final ResourceHandlerRegistry registry) {\n    if (!registry.hasMappingForPattern(\"/webjars/**\")) {\n      registry\n          .addResourceHandler(\"/webjars/**\")\n          .addResourceLocations(\"classpath:/META-INF/resources/webjars/\");\n    }\n\n    if (!registry.hasMappingForPattern(\"/swagger-ui/**\")) {\n      // It is recommended by Springfox 3.x to disable caching of the static Swagger page content\n      registry\n          .addResourceHandler(\"/swagger-ui/**\")\n          .addResourceLocations(\"classpath:/META-INF/resources/webjars/springfox-swagger-ui/\")\n          .resourceChain(false);\n    }\n  }\n\n  @Override\n  public void addViewControllers(ViewControllerRegistry registry) {\n    registry.addRedirectViewController(\"/swagger-ui.html\", \"/swagger-ui/index.html\");\n    registry.addRedirectViewController(\"/swagger-ui\", \"/swagger-ui/index.html\");\n    registry.addRedirectViewController(\"/swagger-ui/\", \"/swagger-ui/index.html\");\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/config/WebSecurityConfig.java",
    "content": "package com.msy.plus.core.config;\n\nimport com.msy.plus.filter.AuthenticationFilter;\nimport com.msy.plus.filter.MyAuthenticationEntryPoint;\nimport com.msy.plus.service.impl.UserDetailsServiceImpl;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;\nimport org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;\nimport org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;\nimport org.springframework.security.config.http.SessionCreationPolicy;\nimport org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;\nimport org.springframework.security.crypto.password.PasswordEncoder;\nimport org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;\n\nimport javax.annotation.Resource;\n\n/**\n * 安全设置\n *\n * @author MoShuying\n * @date 2018/05/27\n */\n@Configuration\n@EnableWebSecurity\n@EnableGlobalMethodSecurity(prePostEnabled = true)\nclass WebSecurityConfig extends WebSecurityConfigurerAdapter {\n  private static final String[] ANONYMOUS_LIST = {\n    \"/druid/**\",\n    \"/swagger-ui/\",\n    \"/swagger-ui/index.html\",\n    \"/swagger-ui.html\",\n    \"/swagger-ui/**\",\n    \"/v2/api-docs\",\n    \"/v3/api-docs\",\n    \"/swagger-resources\",\n    \"/swagger-resources/**\",\n    \"/webjars/**\",\n  };\n\n  @Resource private MyAuthenticationEntryPoint myAuthenticationEntryPoint;\n  @Resource private AuthenticationFilter authenticationFilter;\n\n  /** 使用随机加盐哈希算法对密码进行加密 */\n  @Bean\n  public static PasswordEncoder passwordEncoder() {\n    // 默认强度10，可以指定 4 到 31 之间的强度\n    return new BCryptPasswordEncoder();\n  }\n\n  @Bean\n  @Override\n  public UserDetailsServiceImpl userDetailsService() {\n    return new UserDetailsServiceImpl();\n  }\n\n  @Override\n  protected void configure(final AuthenticationManagerBuilder auth) throws Exception {\n    auth\n        // 自定义获取账户信息\n        .userDetailsService(this.userDetailsService())\n        // 设置密码加密\n        .passwordEncoder(WebSecurityConfig.passwordEncoder());\n  }\n\n  @Override\n  protected void configure(final HttpSecurity http) throws Exception {\n    http\n        // 禁用页面缓存\n        .headers()\n        .cacheControl()\n        .and()\n        .and()\n        // 关闭 cors 验证\n        .cors()\n        .disable()\n        // 关闭 csrf 验证\n        .csrf()\n        .disable()\n        // 无状态 session\n        .sessionManagement()\n        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)\n        .and()\n        // 异常处理\n        .exceptionHandling()\n        // 因为 RESTFul 没有登录界面所以只显示未登录 Json\n        .authenticationEntryPoint(this.myAuthenticationEntryPoint)\n        .and()\n        // 身份过滤器\n        .addFilterBefore(this.authenticationFilter, UsernamePasswordAuthenticationFilter.class)\n        // 认证访问\n        .authorizeRequests()\n        // 允许匿名请求\n        .antMatchers(ANONYMOUS_LIST)\n        .permitAll()\n        // 注册和登录\n        .antMatchers(HttpMethod.POST, \"/account\", \"/account/token\")\n        .permitAll()\n        // 预请求\n        .antMatchers(HttpMethod.OPTIONS)\n        .permitAll()\n        // 对除上面特别请求外所有都鉴权认证\n        .anyRequest()\n        .authenticated();\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/config/YamlPropertySourceFactory.java",
    "content": "package com.msy.plus.core.config;\n\nimport com.msy.plus.core.exception.YamlNotFoundException;\nimport org.springframework.beans.factory.config.YamlPropertiesFactoryBean;\nimport org.springframework.core.env.PropertiesPropertySource;\nimport org.springframework.core.env.PropertySource;\nimport org.springframework.core.io.support.EncodedResource;\nimport org.springframework.core.io.support.PropertySourceFactory;\n\nimport java.util.Optional;\nimport java.util.Properties;\n\n/**\n * Yml配置文件工厂\n *\n * @author MoShuying\n * @date 2020/11/12\n */\npublic class YamlPropertySourceFactory implements PropertySourceFactory {\n\n  @Override\n  public PropertySource<?> createPropertySource(final String name, final EncodedResource resource) {\n    final YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();\n    factory.setResources(resource.getResource());\n    factory.afterPropertiesSet();\n    final Properties ymlProperties =\n        Optional.ofNullable(factory.getObject()).orElseThrow(YamlNotFoundException::new);\n    final String propertyName =\n        Optional.ofNullable(name).orElse(resource.getResource().getFilename());\n    Optional.ofNullable(propertyName).orElseThrow(YamlNotFoundException::new);\n    return new PropertiesPropertySource(propertyName, ymlProperties);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/constant/ProjectConstant.java",
    "content": "package com.msy.plus.core.constant;\n\n/**\n * 项目常量\n *\n * @author MoShuying\n * @date 2018/05/27\n */\npublic final class ProjectConstant {\n  /** 开发环境 */\n  public static final String SPRING_PROFILE_DEVELOPMENT = \"dev\";\n\n  /** 生产环境 */\n  public static final String SPRING_PROFILE_PRODUCTION = \"prod\";\n\n  /** 测试环境 */\n  public static final String SPRING_PROFILE_TEST = \"test\";\n\n  /** 项目基础包名称 */\n  public static final String BASE_PACKAGE = \"com.msy.plus\";\n\n  /** Entity 所在包 */\n  public static final String ENTITY_PACKAGE = BASE_PACKAGE + \".entity\";\n\n  /** Mapper 所在包 */\n  public static final String MAPPER_PACKAGE = BASE_PACKAGE + \".mapper\";\n\n  /** Filter 所在包 */\n  public static final String FILTER_PACKAGE = BASE_PACKAGE + \".filter\";\n\n  /** Service 所在包 */\n  public static final String SERVICE_PACKAGE = BASE_PACKAGE + \".service\";\n\n  /** ServiceImpl 所在包 */\n  public static final String SERVICE_IMPL_PACKAGE = SERVICE_PACKAGE + \".impl\";\n\n  /** Controller 所在包 */\n  public static final String CONTROLLER_PACKAGE = BASE_PACKAGE + \".controller\";\n\n  /** Mapper 插件基础接口的完全限定名 */\n  public static final String MAPPER_INTERFACE_REFERENCE = BASE_PACKAGE + \".core.mapper.MyMapper\";\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/dto/AbstractConverter.java",
    "content": "package com.msy.plus.core.dto;\n\nimport com.google.common.base.Converter;\nimport org.springframework.beans.BeanUtils;\n\nimport javax.annotation.ParametersAreNonnullByDefault;\nimport java.lang.reflect.ParameterizedType;\n\n/**\n * DTO -> DO 抽象转换器\n *\n * @author MoShuying\n * @date 2018/11/28\n */\npublic abstract class AbstractConverter<DTO, DO> extends Converter<DTO, DO> {\n  private final Class<DO> doClass;\n  private final DTO theDTO = this.setDTO();\n  private DO theDO;\n\n  public AbstractConverter() {\n    final ParameterizedType parameterizedType =\n        (ParameterizedType) this.getClass().getGenericSuperclass();\n    this.doClass = (Class<DO>) parameterizedType.getActualTypeArguments()[1];\n  }\n\n  /**\n   * 设置 DTO\n   *\n   * @return DTO\n   */\n  protected abstract DTO setDTO();\n\n  @Override\n  @ParametersAreNonnullByDefault\n  public DO doForward(final DTO theDTO) {\n    BeanUtils.copyProperties(theDTO, this.theDO);\n    return this.theDO;\n  }\n\n  @Override\n  @ParametersAreNonnullByDefault\n  public DTO doBackward(final DO theDO) {\n    BeanUtils.copyProperties(theDO, this.theDTO);\n    return this.theDTO;\n  }\n\n  public DO convertToDO() {\n    try {\n      this.theDO = this.doClass.getDeclaredConstructor().newInstance();\n      return this.convert(this.theDTO);\n    } catch (final Exception ignored) {\n      return null;\n    }\n  }\n\n  public DTO convertFor(final DO aDO) {\n    return this.reverse().convert(aDO);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/exception/ExceptionResolver.java",
    "content": "package com.msy.plus.core.exception;\n\nimport com.msy.plus.core.response.Result;\nimport com.msy.plus.core.response.ResultCode;\nimport com.msy.plus.core.response.ResultGenerator;\nimport com.msy.plus.util.UrlUtils;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.dao.DataAccessException;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.security.access.AccessDeniedException;\nimport org.springframework.security.authentication.BadCredentialsException;\nimport org.springframework.security.core.AuthenticationException;\nimport org.springframework.security.core.userdetails.UsernameNotFoundException;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.ResponseStatus;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\nimport org.springframework.web.servlet.NoHandlerFoundException;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.validation.ConstraintViolation;\nimport javax.validation.ConstraintViolationException;\nimport java.sql.SQLException;\nimport java.util.stream.Collectors;\n\n/**\n * 统一异常处理\n *\n * 对于业务异常：返回头 Http 状态码一律使用500，避免浏览器缓存，在响应 Result 中指明异常的状态码 code\n *\n * @author MoShuying\n * @date 2018/06/09\n */\n@Slf4j\n@RestControllerAdvice\npublic class ExceptionResolver {\n  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)\n  @ExceptionHandler(ConstraintViolationException.class)\n  public Result validatorException(final ConstraintViolationException e) {\n    final String msg =\n        e.getConstraintViolations().stream()\n            .map(ConstraintViolation::getMessage)\n            .collect(Collectors.joining(\",\"));\n    // e.toString 多了不需要用户知道的属性路径\n    ExceptionResolver.log.error(\"==> 验证实体异常: {}\", e.toString());\n    e.printStackTrace();\n    return ResultGenerator.genFailedResult(ResultCode.VIOLATION_EXCEPTION, msg);\n  }\n\n  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)\n  @ExceptionHandler({ServiceException.class})\n  public Result serviceException(final ServiceException e) {\n    ExceptionResolver.log.error(\"==> 服务异常: {}\", e.getMessage());\n    e.printStackTrace();\n    return ResultGenerator.genFailedResult(e.getResultCode(), e.getMessage());\n  }\n\n  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)\n  @ExceptionHandler({ResourcesNotFoundException.class})\n  public Result resourcesException(final Throwable e) {\n    ExceptionResolver.log.error(\"==> 资源异常: {}\", e.getMessage());\n    e.printStackTrace();\n    return ResultGenerator.genFailedResult(ResultCode.FIND_FAILED);\n  }\n\n  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)\n  @ExceptionHandler({SQLException.class, DataAccessException.class})\n  public Result databaseException(final Throwable e) {\n    ExceptionResolver.log.error(\"==> 数据库异常: {}\", e.getMessage());\n    e.printStackTrace();\n    return ResultGenerator.genFailedResult(ResultCode.DATABASE_EXCEPTION);\n  }\n\n  @ResponseStatus(HttpStatus.UNAUTHORIZED)\n  @ExceptionHandler({BadCredentialsException.class, AuthenticationException.class})\n  public Result authException(final Throwable e) {\n    ExceptionResolver.log.error(\"==> 身份验证异常: {}\", e.getMessage());\n    e.printStackTrace();\n    return ResultGenerator.genFailedResult(ResultCode.UNAUTHORIZED_EXCEPTION);\n  }\n\n  @ResponseStatus(HttpStatus.FORBIDDEN)\n  @ExceptionHandler({AccessDeniedException.class, UsernameNotFoundException.class})\n  public Result accountException(final Throwable e) {\n    ExceptionResolver.log.error(\"==> 账户异常: {}\", e.getMessage());\n    e.printStackTrace();\n    return ResultGenerator.genFailedResult(e.getMessage());\n  }\n\n  @ResponseStatus(HttpStatus.NOT_FOUND)\n  @ExceptionHandler(NoHandlerFoundException.class)\n  public Result apiNotFoundException(final Throwable e, final HttpServletRequest request) {\n    ExceptionResolver.log.error(\"==> API不存在: {}\", e.getMessage());\n    e.printStackTrace();\n    return ResultGenerator.genFailedResult(\n        \"API [\" + UrlUtils.getMappingUrl(request) + \"] not existed\");\n  }\n\n  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)\n  @ExceptionHandler(Exception.class)\n  public Result globalException(final HttpServletRequest request, final Throwable e) {\n    ExceptionResolver.log.error(\"==> 全局异常: {}\", e.getMessage());\n    e.printStackTrace();\n    return ResultGenerator.genFailedResult(\n        HttpStatus.INTERNAL_SERVER_ERROR.value(),\n        String.format(\"%s => %s\", UrlUtils.getMappingUrl(request), e.getMessage()));\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/exception/ResourcesNotFoundException.java",
    "content": "package com.msy.plus.core.exception;\n\n/**\n * 资源没找到异常（更新和删除都需先确认存在才操作）\n *\n * @author MoShuying\n * @date 2018/07/20\n */\npublic class ResourcesNotFoundException extends RuntimeException {\n  private static final long serialVersionUID = -4770095291206546216L;\n\n  private static final String DEFAULT_MESSAGE = \"资源不存在\";\n\n  public ResourcesNotFoundException() {\n    super(ResourcesNotFoundException.DEFAULT_MESSAGE);\n  }\n\n  public ResourcesNotFoundException(final String message) {\n    super(message);\n  }\n\n  public ResourcesNotFoundException(final String message, final Throwable cause) {\n    super(message, cause);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/exception/RsaException.java",
    "content": "package com.msy.plus.core.exception;\n\n/**\n * Rsa 异常\n *\n * @author MoShuying\n * @date 2018/05/27\n */\npublic class RsaException extends RuntimeException {\n  private static final long serialVersionUID = 5010582133829256626L;\n\n  private static final String DEFAULT_MESSAGE = \"Rsa 异常\";\n\n  public RsaException() {\n    super(RsaException.DEFAULT_MESSAGE);\n  }\n\n  public RsaException(final String message) {\n    super(message);\n  }\n\n  public RsaException(final String message, final Throwable cause) {\n    super(message, cause);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/exception/ServiceException.java",
    "content": "package com.msy.plus.core.exception;\n\nimport com.msy.plus.core.response.ResultCode;\n\n/**\n * Service 异常\n *\n * @author MoShuying\n * @date 2018/05/27\n */\npublic class ServiceException extends RuntimeException {\n  private static final long serialVersionUID = 770293933438435163L;\n\n  private ResultCode resultCode;\n\n  public ServiceException(final String message) {\n    super(message);\n  }\n\n  public ServiceException(final String message, final Throwable cause) {\n    super(message, cause);\n  }\n\n  public ServiceException(final ResultCode resultCode, final String message) {\n    super(message);\n    this.resultCode = resultCode;\n  }\n\n  public ServiceException(final ResultCode resultCode) {\n    super(resultCode.getReason());\n    this.resultCode = resultCode;\n  }\n\n  public ResultCode getResultCode() {\n    return this.resultCode;\n  }\n\n  public void setResultCode(final ResultCode resultCode) {\n    this.resultCode = resultCode;\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/exception/UsernameNotFoundException2.java",
    "content": "package com.msy.plus.core.exception;\n\nimport org.springframework.security.core.userdetails.UsernameNotFoundException;\n\n/**\n * 用户名没找到异常\n *\n * @author MoShuying\n * @date 2020/11/12\n */\npublic class UsernameNotFoundException2 extends UsernameNotFoundException {\n  private static final long serialVersionUID = 90476943748478489L;\n\n  private static final String DEFAULT_MESSAGE = \"账户名不存在\";\n\n  public UsernameNotFoundException2() {\n    super(UsernameNotFoundException2.DEFAULT_MESSAGE);\n  }\n\n  public UsernameNotFoundException2(final String message) {\n    super(message);\n  }\n\n  public UsernameNotFoundException2(final String message, final Throwable cause) {\n    super(message, cause);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/exception/YamlNotFoundException.java",
    "content": "package com.msy.plus.core.exception;\n\n/**\n * Yml配置没找到异常\n *\n * @author MoShuying\n * @date 2020/11/12\n */\npublic class YamlNotFoundException extends RuntimeException {\n  private static final long serialVersionUID = 7081775224645592074L;\n\n  private static final String DEFAULT_MESSAGE = \"Yml不存在\";\n\n  public YamlNotFoundException() {\n    super(YamlNotFoundException.DEFAULT_MESSAGE);\n  }\n\n  public YamlNotFoundException(final String message) {\n    super(message);\n  }\n\n  public YamlNotFoundException(final String message, final Throwable cause) {\n    super(message, cause);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/jasypt/MyEncryptablePropertyDetector.java",
    "content": "package com.msy.plus.core.jasypt;\n\nimport com.ulisesbocchio.jasyptspringboot.EncryptablePropertyDetector;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Component;\n\n/**\n * 自定义被加密值的发现器 默认：ENC(abc) 自定义：MyEnc({abc})\n *\n * <p>https://github.com/ulisesbocchio/jasypt-spring-boot#provide-a-custom-encryptablepropertydetector\n *\n * <p>如果只是单纯想让前后缀不同，可以直接配置前后缀属性：\n *\n * <p>https://github.com/ulisesbocchio/jasypt-spring-boot#provide-a-custom-encrypted-property-prefix-and-suffix\n *\n * <p>jasypt.encryptor.property.prefix=TEST(\n *\n * <p>jasypt.encryptor.property.suffix=)\n *\n * @author MoShuying\n * @date 2018/07/20\n */\n@Component\npublic class MyEncryptablePropertyDetector implements EncryptablePropertyDetector {\n  /** 前缀 */\n  private static final String PREFIX = \"MyEnc({\";\n  /** 后缀 */\n  private static final String SUFFIX = \"})\";\n\n  @Override\n  public boolean isEncrypted(final String property) {\n    if (StringUtils.isBlank(property)) {\n      return false;\n    }\n    final String trimmedProperty = property.trim();\n\n    return trimmedProperty.startsWith(MyEncryptablePropertyDetector.PREFIX)\n        && trimmedProperty.endsWith(MyEncryptablePropertyDetector.SUFFIX);\n  }\n\n  @Override\n  public String unwrapEncryptedValue(final String property) {\n    return property.substring(\n        MyEncryptablePropertyDetector.PREFIX.length(),\n        property.length() - MyEncryptablePropertyDetector.SUFFIX.length());\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/jwt/JwtConfigurationProperties.java",
    "content": "package com.msy.plus.core.jwt;\n\nimport lombok.Data;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.stereotype.Component;\n\nimport java.time.Duration;\n\n/**\n * Json web token 配置\n *\n * @author MoShuying\n * @date 2018/06/09\n */\n@Data\n@Component\n@ConfigurationProperties(prefix = \"jwt\")\npublic class JwtConfigurationProperties {\n  /** claim authorities key */\n  private String claimKeyAuth;\n\n  /** token 前缀 */\n  private String tokenType;\n\n  /** 请求头或请求参数的key */\n  private String header;\n\n  /** 有效期 */\n  private Duration expireTime;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/jwt/JwtUtil.java",
    "content": "package com.msy.plus.core.jwt;\n\nimport com.msy.plus.core.rsa.RsaUtils;\nimport com.msy.plus.util.RedisUtils;\nimport io.jsonwebtoken.*;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.security.authentication.UsernamePasswordAuthenticationToken;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.authority.SimpleGrantedAuthority;\nimport org.springframework.security.core.userdetails.User;\nimport org.springframework.stereotype.Component;\n\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport java.security.PublicKey;\nimport java.time.Duration;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Date;\nimport java.util.Optional;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\n/**\n * Json web token 工具 验证、生成 token\n *\n * @author MoShuying\n * @date 2018/05/27\n */\n@Slf4j\n@Component\npublic class JwtUtil {\n  @Resource private JwtConfigurationProperties jwtProperties;\n  @Resource private RedisUtils redisUtils;\n  @Resource private RsaUtils rsaUtils;\n  public JwtConfigurationProperties getJwtProperties(){\n    return this.jwtProperties;\n  }\n  /** 根据 token 得到账户名 */\n  public Optional<String> getName(final String token) {\n    final Optional<Claims> claims = this.parseToken(token);\n    return claims.map(Claims::getSubject);\n  }\n  public Optional<String> getId(final String token){\n    final Optional<Claims> claims = this.parseToken(token);\n    return claims.map(Claims::getId);\n  }\n\n  /**\n   * 签发 token\n   *\n   * @param name 账户名\n   * @param grantedAuthorities 账户权限信息[ADMIN, TEST, ...]\n   */\n  public String sign(\n      final String name, final Collection<? extends GrantedAuthority> grantedAuthorities,Long id) {\n    // 函数式创建 token，避免重复书写\n    final Supplier<String> createToken = () -> this.createToken(name, grantedAuthorities,id);\n    // 看看缓存有没有账户token\n    final String token = (String) this.redisUtils.getValue(name);\n    // 没有登录过\n    if (StringUtils.isBlank(token)) {\n      return createToken.get();\n    }\n    final boolean isValidate = (boolean) this.redisUtils.getValue(token);\n    // 有 token，仍有效，将 token 置为无效，并重新签发（防止 token 被利用）\n    if (isValidate) {\n      this.invalidRedisToken(name);\n    }\n    // 重新签发\n    return createToken.get();\n  }\n\n  /**\n   * 清除账户在 Redis 中缓存的 token\n   *\n   * @param name 账户名\n   */\n  public void invalidRedisToken(final String name) {\n    // 将 token 设置为无效\n    Object value = this.redisUtils.getValue(name);\n    if(value==null){ return; }\n    Optional.ofNullable((String) value).ifPresent(_token -> this.redisUtils.setValue(_token, false));\n  }\n\n  /** 从请求头或请求参数中获取 token */\n  public String getTokenFromRequest(final HttpServletRequest httpRequest) {\n    final String header = this.jwtProperties.getHeader();\n    final String token = httpRequest.getHeader(header);\n    return StringUtils.isNotBlank(token) ? token : httpRequest.getParameter(header);\n  }\n\n  /** 返回账户认证 */\n  public UsernamePasswordAuthenticationToken getAuthentication(\n      final String name, final String token) {\n    // 解析 token 的 payload\n    final Optional<Claims> claims = this.parseToken(token);\n    final String claimKeyAuth = this.jwtProperties.getClaimKeyAuth();\n    // 账户角色列表\n    final String[] auths =\n        claims.map(c -> c.get(claimKeyAuth).toString().split(\",\")).orElse(new String[0]);\n    // 将元素转换为 GrantedAuthority 接口集合\n    final Collection<? extends GrantedAuthority> authorities =\n        Arrays.stream(auths).map(SimpleGrantedAuthority::new).collect(Collectors.toList());\n    final User user = new User(name, \"\", authorities);\n    return new UsernamePasswordAuthenticationToken(user, null, authorities);\n  }\n\n  /** 验证 token 是否正确 */\n  public boolean validateToken(final String token) {\n    boolean isValidate = true;\n    final Object redisTokenValidate = this.redisUtils.getValue(token);\n    // 可能 redis 部署出现了问题\n    // 或者清空了缓存导致 token 键不存在\n    if (redisTokenValidate != null) {\n      isValidate = (boolean) redisTokenValidate;\n    }\n    // 能正确解析 token，并且 redis 中缓存的 token 也是有效的\n    return this.parseToken(token).isPresent() && isValidate;\n  }\n\n  /** 生成 token */\n  private String createToken(\n      final String name,\n      final Collection<? extends GrantedAuthority> grantedAuthorities,\n      Long id) {\n    // 获取账户的角色字符串，如 USER,ADMIN\n    final String authorities =\n        grantedAuthorities.stream()\n            .map(GrantedAuthority::getAuthority)\n            .collect(Collectors.joining(\",\"));\n    JwtUtil.log.debug(\"==> Account<{}> authorities: {}\", name, authorities);\n\n    // 过期时间\n    final Duration expireTime = this.jwtProperties.getExpireTime();\n    // 当前时间 + 有效时长\n    final Date expireDate = new Date(System.currentTimeMillis() + expireTime.toMillis());\n    // 创建 token，比如 \"Bearer abc1234\"\n    final String token =\n        this.jwtProperties.getTokenType()\n            + \" \"\n            + Jwts.builder()\n                // 设置账户名\n                .setSubject(name)\n                .setId(id.toString())\n                // 添加权限属性\n                .claim(this.jwtProperties.getClaimKeyAuth(), authorities)\n                // 设置失效时间\n                .setExpiration(expireDate)\n                // 私钥加密生成签名\n                .signWith(SignatureAlgorithm.RS256, this.rsaUtils.loadPrivateKey())\n                // 使用LZ77算法与哈夫曼编码结合的压缩算法进行压缩\n                .compressWith(CompressionCodecs.DEFLATE)\n                .compact();\n    // 保存账户 token\n    // 因为账户注销后 JWT 本身只要没过期就仍然有效，所以只能通过 redis 缓存来校验有无效\n    // 校验时只要 redis 中的 token 无效即可（JWT 本身可以校验有无过期，而 redis 过期即被删除了）\n    // true 有效\n    this.redisUtils.setValue(token, true, expireTime);\n    // redis 过期时间和 JWT 的一致\n    this.redisUtils.setValue(name, token, expireTime);\n    JwtUtil.log.debug(\"==> Redis set Account<{}> token: {}\", name, token);\n    return token;\n  }\n\n  /** 解析 token */\n  private Optional<Claims> parseToken(final String token) {\n    Optional<Claims> claims = Optional.empty();\n    try {\n      final PublicKey publicKey = this.rsaUtils.loadPublicKey();\n      claims =\n          Optional.of(\n              Jwts.parser()\n                  // 公钥解密\n                  .setSigningKey(publicKey)\n                  .parseClaimsJws(token.replace(this.jwtProperties.getTokenType(), \"\"))\n                  .getBody());\n    } catch (final SignatureException e) {\n      // 签名异常\n      JwtUtil.log.debug(\"Invalid JWT signature\");\n    } catch (final MalformedJwtException e) {\n      // 格式错误\n      JwtUtil.log.debug(\"Invalid JWT token\");\n    } catch (final ExpiredJwtException e) {\n      // 过期\n      JwtUtil.log.debug(\"Expired JWT token\");\n    } catch (final UnsupportedJwtException e) {\n      // 不支持该JWT\n      JwtUtil.log.debug(\"Unsupported JWT token\");\n    } catch (final IllegalArgumentException e) {\n      // 参数错误异常\n      JwtUtil.log.debug(\"JWT token compact of handler are invalid\");\n    }\n    return claims;\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/mapper/MyMapper.java",
    "content": "package com.msy.plus.core.mapper;\n\nimport tk.mybatis.mapper.common.BaseMapper;\nimport tk.mybatis.mapper.common.ConditionMapper;\nimport tk.mybatis.mapper.common.IdsMapper;\nimport tk.mybatis.mapper.common.special.InsertListMapper;\n\n/**\n * 定制版 MyBatis Mapper 插件接口，如需其他接口参考官方文档自行添加\n *\n * @author MoShuying\n * @date 2018/05/27\n */\npublic interface MyMapper<T>\n    extends BaseMapper<T>, ConditionMapper<T>, IdsMapper<T>, InsertListMapper<T> {}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/response/Result.java",
    "content": "package com.msy.plus.core.response;\n\nimport com.alibaba.fastjson.JSON;\nimport io.swagger.annotations.ApiModel;\nimport io.swagger.annotations.ApiModelProperty;\n\n/**\n * @author MoShuying\n * @date 2018/07/15\n */\n@ApiModel(value = \"响应结果\")\npublic class Result<T> {\n  @ApiModelProperty(value = \"状态码\")\n  private Integer code;\n\n  @ApiModelProperty(value = \"消息\")\n  private String message;\n\n  @ApiModelProperty(value = \"数据\")\n  private T data;\n\n  @Override\n  public String toString() {\n    return JSON.toJSONString(this);\n  }\n\n  public Integer getCode() {\n    return this.code;\n  }\n\n  public Result<T> setCode(final Integer code) {\n    this.code = code;\n    return this;\n  }\n\n  public String getMessage() {\n    return this.message;\n  }\n\n  public Result<T> setMessage(final String message) {\n    this.message = message;\n    return this;\n  }\n\n  public T getData() {\n    return this.data;\n  }\n\n  public Result<T> setData(final T data) {\n    this.data = data;\n    return this;\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/response/ResultCode.java",
    "content": "package com.msy.plus.core.response;\n\n/**\n * 响应状态码枚举类\n *\n * <p>自定义业务异常 2*** 开始\n *\n * <p>原有类异常 4*** 开始\n *\n * @author MoShuying\n * @date 2018/07/14\n */\npublic enum ResultCode {\n  /** 成功请求，但结果不是期望的成功结果 */\n  SUCCEED_REQUEST_FAILED_RESULT(1000, \"成功请求，但结果不是期望的成功结果\"),\n\n  /** 查询失败 */\n  FIND_FAILED(2000, \"查询失败\"),\n\n  /** 保存失败 */\n  SAVE_FAILED(2001, \"保存失败\"),\n\n  /** 更新失败 */\n  UPDATE_FAILED(2002, \"更新失败\"),\n\n  /** 删除失败 */\n  DELETE_FAILED(2003, \"删除失败\"),\n\n  /** 账户名重复 */\n  DUPLICATE_NAME(2004, \"账户名重复\"),\n\n  /** 数据库异常 */\n  DATABASE_EXCEPTION(4001, \"数据库异常\"),\n\n  /** 认证异常 */\n  UNAUTHORIZED_EXCEPTION(4002, \"认证异常\"),\n\n  /** 验证异常 */\n  VIOLATION_EXCEPTION(4003, \"验证异常\");\n\n  private final int value;\n\n  private final String reason;\n\n  ResultCode(final int value, final String reason) {\n    this.value = value;\n    this.reason = reason;\n  }\n\n  public int getValue() {\n    return this.value;\n  }\n\n  public String getReason() {\n    return this.reason;\n  }\n\n  public String format(final Object... objects) {\n    return objects.length > 0 ? String.format(this.getReason(), objects) : this.getReason();\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/response/ResultGenerator.java",
    "content": "package com.msy.plus.core.response;\n\nimport org.springframework.http.HttpStatus;\n\n/**\n * 响应结果生成工具\n *\n * @author MoShuying\n * @date 2018/06/09\n */\npublic class ResultGenerator {\n  /**\n   * 成功响应结果\n   *\n   * @param data 内容\n   * @return 响应结果\n   */\n  public static <T> Result<T> genOkResult(final T data) {\n    return new Result<T>().setCode(HttpStatus.OK.value()).setData(data);\n  }\n\n  /**\n   * 成功响应结果\n   *\n   * @return 响应结果\n   */\n  public static <T> Result<T> genOkResult() {\n    return ResultGenerator.genOkResult(null);\n  }\n\n  /**\n   * 失败响应结果\n   *\n   * @param code 状态码\n   * @param message 消息\n   * @return 响应结果\n   */\n  public static <T> Result<T> genFailedResult(\n      final int code, final String message, final Object... objects) {\n    return new Result<T>().setCode(code).setMessage(String.format(message, objects));\n  }\n\n  /**\n   * 失败响应结果\n   *\n   * @param resultCode 状态码枚举\n   * @return 响应结果\n   */\n  public static <T> Result<T> genFailedResult(final ResultCode resultCode) {\n    return ResultGenerator.genFailedResult(resultCode.getValue(), resultCode.getReason());\n  }\n\n  /**\n   * 失败响应结果\n   *\n   * @param resultCode 状态码枚举\n   * @param message 消息\n   * @return 响应结果\n   */\n  public static <T> Result<T> genFailedResult(\n      final ResultCode resultCode, final String message, final Object... objects) {\n    return ResultGenerator.genFailedResult(resultCode.getValue(), message, objects);\n  }\n\n  /**\n   * 失败响应结果\n   *\n   * @param message 消息\n   * @return 响应结果\n   */\n  public static <T> Result<T> genFailedResult(final String message, final Object... objects) {\n    return ResultGenerator.genFailedResult(\n        ResultCode.SUCCEED_REQUEST_FAILED_RESULT.getValue(), message, objects);\n  }\n\n  /**\n   * 失败响应结果\n   *\n   * @return 响应结果\n   */\n  public static <T> Result<T> genFailedResult() {\n    return ResultGenerator.genFailedResult(ResultCode.SUCCEED_REQUEST_FAILED_RESULT);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/rsa/RsaConfigurationProperties.java",
    "content": "package com.msy.plus.core.rsa;\n\nimport lombok.Data;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.stereotype.Component;\n\n/**\n * RSA 配置\n *\n * @author MoShuying\n * @date 2019/08/12\n */\n@Data\n@Component\n@ConfigurationProperties(prefix = \"rsa\")\npublic class RsaConfigurationProperties {\n  /** 公钥头 */\n  private String publicKeyHead = \"-----BEGIN PUBLIC KEY-----\";\n\n  /** 公钥尾 */\n  private String publicKeyTail = \"-----END PUBLIC KEY-----\";\n\n  /** 私钥头 */\n  private String privateKeyHead = \"-----BEGIN PRIVATE KEY-----\";\n\n  /** 私钥尾 */\n  private String privateKeyTail = \"-----END PRIVATE KEY-----\";\n\n  /** 公钥位置，默认在 rsa 文件夹下 */\n  private String publicKeyPath = \"classpath:rsa/public-key.pem\";\n\n  /** 私钥位置，默认在 rsa 文件夹下 */\n  private String privateKeyPath = \"classpath:rsa/private-key.pem\";\n\n  /** 使用文件还是直接使用字符串，默认使用字符串 */\n  private boolean useFile = false;\n\n  /** 私钥 */\n  private String privateKey;\n\n  /** 公钥 */\n  private String publicKey;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/rsa/RsaUtils.java",
    "content": "package com.msy.plus.core.rsa;\n\nimport com.msy.plus.core.exception.RsaException;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.codec.binary.Base64;\nimport org.springframework.core.io.DefaultResourceLoader;\nimport org.springframework.core.io.Resource;\nimport org.springframework.core.io.ResourceLoader;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.FileCopyUtils;\n\nimport javax.crypto.Cipher;\nimport java.io.IOException;\nimport java.security.*;\nimport java.security.spec.InvalidKeySpecException;\nimport java.security.spec.PKCS8EncodedKeySpec;\nimport java.security.spec.X509EncodedKeySpec;\nimport java.util.Optional;\n\n/**\n * RSA 工具\n *\n * <p>用openssl生成512位RSA：\n *\n * <p>生成私钥： openssl genrsa -out key.pem 512\n *\n * <p>从私钥中导出公钥： openssl rsa -in key.pem -pubout -out public-key.pem\n *\n * <p>公钥加密： openssl rsautl -encrypt -in xx.file -inkey public-key.pem -pubin -out xx.en\n *\n * <p>私钥解密： openssl rsautl -decrypt -in xx.en -inkey key.pem -out xx.de\n *\n * <p>pkcs8编码（Java）： openssl pkcs8 -topk8 -inform PEM -in key.pem -outform PEM -out private-key.pem\n * -nocrypt\n *\n * <p>最后将公私玥放在/resources/rsa/：private-key.pem public-key.pem\n *\n * @author MoShuying\n * @date 2018/07/20\n */\n@Slf4j\n@Component\npublic class RsaUtils {\n  private static final String ALGORITHM = \"RSA\";\n  private final ResourceLoader resourceLoader = new DefaultResourceLoader();\n  @javax.annotation.Resource private RsaConfigurationProperties rsaProperties;\n\n  public RsaUtils() {\n    if (!Optional.ofNullable(this.rsaProperties).isPresent()) {\n      this.rsaProperties = new RsaConfigurationProperties();\n    }\n  }\n\n  /**\n   * 生成密钥对\n   *\n   * @param keyLength 密钥长度(最少512位)\n   * @return 密钥对 公钥 keyPair.getPublic() 私钥 keyPair.getPrivate()\n   * @throws Exception e\n   */\n  public static KeyPair genKeyPair(final int keyLength) throws Exception {\n    final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RsaUtils.ALGORITHM);\n    keyPairGenerator.initialize(keyLength);\n    return keyPairGenerator.generateKeyPair();\n  }\n\n  /**\n   * 公钥加密\n   *\n   * @param content 待加密数据\n   * @param publicKey 公钥\n   * @return 加密内容\n   * @throws Exception e\n   */\n  public static byte[] encrypt(final byte[] content, final PublicKey publicKey) throws Exception {\n    final Cipher cipher = Cipher.getInstance(RsaUtils.ALGORITHM);\n    cipher.init(Cipher.ENCRYPT_MODE, publicKey);\n    return cipher.doFinal(content);\n  }\n\n  /**\n   * 私钥解密\n   *\n   * @param content 加密数据\n   * @param privateKey 私钥\n   * @return 解密内容\n   * @throws Exception e\n   */\n  public static byte[] decrypt(final byte[] content, final PrivateKey privateKey) throws Exception {\n    final Cipher cipher = Cipher.getInstance(RsaUtils.ALGORITHM);\n    cipher.init(Cipher.DECRYPT_MODE, privateKey);\n    return cipher.doFinal(content);\n  }\n\n  /**\n   * 公钥加密\n   *\n   * @param content 待加密数据\n   * @return 加密内容\n   * @throws Exception e\n   */\n  public byte[] encrypt(final byte[] content) throws Exception {\n    return RsaUtils.encrypt(content, this.loadPublicKey());\n  }\n\n  /**\n   * 私钥解密\n   *\n   * @param content 加密数据\n   * @return 解密内容\n   * @throws Exception e\n   */\n  public byte[] decrypt(final byte[] content) throws Exception {\n    return RsaUtils.decrypt(content, this.loadPrivateKey());\n  }\n  /**\n   * 加载pem格式X509编码的公钥\n   *\n   * @return 公钥\n   */\n  public PublicKey loadPublicKey() throws RsaException {\n    final byte[] decoded;\n    if (this.rsaProperties.isUseFile()) {\n      decoded =\n          this.loadAndReplaceAndDecodeByBase64(\n              this.rsaProperties.getPublicKeyPath(),\n              this.rsaProperties.getPublicKeyHead(),\n              this.rsaProperties.getPublicKeyTail());\n    } else {\n      decoded = Base64.decodeBase64(this.rsaProperties.getPublicKey());\n    }\n    final X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);\n    try {\n      final KeyFactory keyFactory = KeyFactory.getInstance(RsaUtils.ALGORITHM);\n      return keyFactory.generatePublic(spec);\n    } catch (final NoSuchAlgorithmException | InvalidKeySpecException e) {\n      RsaUtils.log.error(\"==> {}\", e.getMessage());\n      return null;\n    }\n  }\n\n  /**\n   * 加载pem格式PKCS8编码的私钥\n   *\n   * @return 私钥\n   */\n  public PrivateKey loadPrivateKey() throws RsaException {\n    final byte[] decoded;\n    if (this.rsaProperties.isUseFile()) {\n      decoded =\n          this.loadAndReplaceAndDecodeByBase64(\n              this.rsaProperties.getPrivateKeyPath(),\n              this.rsaProperties.getPrivateKeyHead(),\n              this.rsaProperties.getPrivateKeyTail());\n    } else {\n      decoded = Base64.decodeBase64(this.rsaProperties.getPrivateKey());\n    }\n    final PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded);\n    try {\n      final KeyFactory keyFactory = KeyFactory.getInstance(RsaUtils.ALGORITHM);\n      return keyFactory.generatePrivate(spec);\n    } catch (final NoSuchAlgorithmException | InvalidKeySpecException e) {\n      RsaUtils.log.error(\"==> {}\", e.getMessage());\n      return null;\n    }\n  }\n\n  /**\n   * 加载文件后替换头和尾并解密\n   *\n   * @return 文件字节\n   */\n  private byte[] loadAndReplaceAndDecodeByBase64(\n      final String keyPath, final String headReplace, final String tailReplace)\n      throws RsaException {\n    final Resource resource =\n        Optional.ofNullable(keyPath).map(this.resourceLoader::getResource).get();\n    if (!resource.exists()) {\n      throw new RsaException(\"==> 密钥不存在\");\n    }\n    try {\n      final String key = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));\n      return Base64.decodeBase64(\n          key.replace(headReplace, \"\").trim().replace(tailReplace, \"\").trim());\n    } catch (final IOException e) {\n      throw new RsaException(\"==> 密钥读取异常: {}\", e);\n    }\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/service/AbstractService.java",
    "content": "package com.msy.plus.core.service;\n\nimport com.msy.plus.core.exception.ResourcesNotFoundException;\nimport com.msy.plus.core.exception.ServiceException;\nimport com.msy.plus.core.mapper.MyMapper;\nimport com.msy.plus.core.response.ResultCode;\nimport com.msy.plus.util.AssertUtils;\nimport org.apache.ibatis.exceptions.TooManyResultsException;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport tk.mybatis.mapper.entity.Condition;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.ParameterizedType;\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * 基于通用 MyBatis Mapper 插件的 Service 接口的实现\n *\n * @author MoShuying\n * @date 2018/05/27\n */\npublic abstract class AbstractService<T> implements Service<T> {\n  /** 当前泛型的实体 Class */\n  private final Class<T> entityClass;\n\n  @Autowired protected MyMapper<T> mapper;\n\n  protected AbstractService() {\n    final ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass();\n    //noinspection unchecked\n    this.entityClass = (Class<T>) pt.getActualTypeArguments()[0];\n  }\n\n  private static void assertSave(final boolean statement) {\n    AssertUtils.asserts(statement, ResultCode.SAVE_FAILED);\n  }\n\n  private static void assertDelete(final boolean statement) {\n    AssertUtils.asserts(statement, ResultCode.DELETE_FAILED);\n  }\n\n  private static void assertUpdate(final boolean statement) {\n    AssertUtils.asserts(statement, ResultCode.UPDATE_FAILED);\n  }\n\n  private T getEntity(final String fieldName, final Object value) throws Exception {\n    final T entity = this.entityClass.getDeclaredConstructor().newInstance();\n    final Field field = this.entityClass.getDeclaredField(fieldName);\n    field.setAccessible(true);\n    field.set(entity, value);\n    return entity;\n  }\n\n  @Override\n  public void assertById(final Object id) {\n    Optional.ofNullable(this.mapper.selectByPrimaryKey(id))\n        .orElseThrow(ResourcesNotFoundException::new);\n  }\n\n  @Override\n  public void assertBy(final T entity) {\n    Optional.ofNullable(this.mapper.select(entity)).orElseThrow(ResourcesNotFoundException::new);\n  }\n\n  @Override\n  public void assertByIds(final String ids) {\n    final int count = this.countByIds(ids);\n    // id数和列表数不对应\n    if (ids.split(\",\").length > count) {\n      throw new ResourcesNotFoundException();\n    }\n  }\n\n  @Override\n  public int countByIds(final String ids) {\n    return this.mapper.selectByIds(ids).size();\n  }\n\n  @Override\n  public int countByCondition(final Condition condition) {\n    return this.mapper.selectByCondition(condition).size();\n  }\n\n  @Override\n  public void save(final T entity) {\n    AbstractService.assertSave(this.mapper.insertSelective(entity) == 1);\n  }\n\n  @Override\n  public void save(final List<T> entities) {\n    AbstractService.assertSave(this.mapper.insertList(entities) == entities.size());\n  }\n\n  @Override\n  public void deleteById(final Object id) {\n    this.assertById(id);\n    AbstractService.assertDelete(this.mapper.deleteByPrimaryKey(id) == 1);\n  }\n\n  @Override\n  public void deleteBy(final String fieldName, final Object value) throws TooManyResultsException {\n    try {\n      final T entity = this.getEntity(fieldName, value);\n      this.assertBy(entity);\n      AbstractService.assertDelete(this.mapper.delete(entity) == 1);\n    } catch (final Exception e) {\n      throw new ServiceException(e.getMessage(), e);\n    }\n  }\n\n  @Override\n  public void deleteByIds(final String ids) {\n    this.assertByIds(ids);\n    AbstractService.assertDelete(this.mapper.deleteByIds(ids) == ids.split(\",\").length);\n  }\n\n  @Override\n  public void deleteByCondition(final Condition condition) {\n    final int count = this.countByCondition(condition);\n    AbstractService.assertDelete(this.mapper.deleteByCondition(condition) == count);\n  }\n\n  @Override\n  public void update(final T entity) {\n    AbstractService.assertUpdate(this.mapper.updateByPrimaryKeySelective(entity) == 1);\n  }\n\n  @Override\n  public void updateByCondition(final T entity, final Condition condition) {\n    AbstractService.assertUpdate(this.mapper.updateByConditionSelective(entity, condition) == 1);\n  }\n\n  @Override\n  public T getById(final Object id) {\n    return this.mapper.selectByPrimaryKey(id);\n  }\n\n  @Override\n  public T getBy(final String fieldName, final Object value) throws TooManyResultsException {\n    try {\n      final T entity = this.getEntity(fieldName, value);\n      return this.mapper.selectOne(entity);\n    } catch (final Exception e) {\n      throw new ServiceException(e.getMessage(), e);\n    }\n  }\n\n  @Override\n  public List<T> listByIds(final String ids) {\n    return this.mapper.selectByIds(ids);\n  }\n\n  @Override\n  public List<T> listByCondition(final Condition condition) {\n    return this.mapper.selectByCondition(condition);\n  }\n\n  @Override\n  public List<T> listAll() {\n    return this.mapper.selectAll();\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/service/Service.java",
    "content": "package com.msy.plus.core.service;\n\nimport com.msy.plus.core.exception.ResourcesNotFoundException;\nimport org.apache.ibatis.exceptions.TooManyResultsException;\nimport tk.mybatis.mapper.entity.Condition;\n\nimport java.util.List;\n\n/**\n * Service 层基础接口\n *\n * @author MoShuying\n * @date 2018/05/27\n */\npublic interface Service<T> {\n  /**\n   * 确保实体存在\n   *\n   * @param id 实体id\n   * @throws ResourcesNotFoundException 不存在实体异常\n   */\n  void assertById(Object id);\n\n  /**\n   * 确保实体存在\n   *\n   * @param entity 实体\n   * @throws ResourcesNotFoundException 不存在实体异常\n   */\n  void assertBy(T entity);\n\n  /**\n   * 确保实体存在\n   *\n   * @param ids ids\n   */\n  void assertByIds(String ids);\n\n  /**\n   * 根据 ids 获取实体数\n   *\n   * @param ids ids\n   */\n  int countByIds(String ids);\n\n  /**\n   * 根据条件获取实体数\n   *\n   * @param condition 条件\n   */\n  int countByCondition(Condition condition);\n\n  /**\n   * 持久化\n   *\n   * @param entity 实体\n   */\n  void save(T entity);\n\n  /**\n   * 批量持久化\n   *\n   * @param entities 实体列表\n   */\n  void save(List<T> entities);\n\n  /**\n   * 通过主鍵刪除\n   *\n   * @param id id\n   */\n  void deleteById(Object id);\n\n  /**\n   * 通过实体中某个成员变量名称（非数据表中 column 的名称）刪除\n   *\n   * @param fieldName 字段名\n   * @param value 字段值\n   * @throws TooManyResultsException 多条结果异常\n   */\n  void deleteBy(String fieldName, Object value) throws TooManyResultsException;\n\n  /**\n   * 批量刪除 ids -> “1,2,3,4”\n   *\n   * @param ids ids\n   */\n  void deleteByIds(String ids);\n\n  /**\n   * 根据条件刪除\n   *\n   * @param condition 条件\n   */\n  void deleteByCondition(Condition condition);\n\n  /**\n   * 按组件更新\n   *\n   * @param entity 实体\n   */\n  void update(T entity);\n\n  /**\n   * 按条件更新\n   *\n   * @param entity 实体\n   * @param condition 条件\n   */\n  void updateByCondition(T entity, Condition condition);\n\n  /**\n   * 通过 id 查找\n   *\n   * @param id id\n   * @return 实体\n   */\n  T getById(Object id);\n\n  /**\n   * 通过实体中某个成员变量名称查找 value 需符合 unique 约束\n   *\n   * @param fieldName 字段名\n   * @param value 字段值\n   * @return 实体\n   * @throws TooManyResultsException 多条结果异常\n   */\n  T getBy(String fieldName, Object value) throws TooManyResultsException;\n\n  /**\n   * 通过多个 id 查找 ids -> “1,2,3,4”\n   *\n   * @param ids ids\n   * @return 实体列表\n   */\n  List<T> listByIds(String ids);\n\n  /**\n   * 按条件查找\n   *\n   * @param condition 条件\n   * @return 实体列表\n   */\n  List<T> listByCondition(Condition condition);\n\n  /**\n   * 获取所有实体\n   *\n   * @return 实体列表\n   */\n  List<T> listAll();\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/core/upload/UploadConfigurationProperties.java",
    "content": "package com.msy.plus.core.upload;\n\nimport lombok.Data;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.unit.DataSize;\n\n/**\n * 上传配置\n *\n * @author MoShuying\n * @date 2019/08/13\n */\n@Data\n@Component\n@ConfigurationProperties(prefix = \"upload\")\npublic class UploadConfigurationProperties {\n  /** 本地路径 */\n  private String localPath;\n\n  /** 最小 */\n  private DataSize min;\n\n  /** 最大 */\n  private DataSize max;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/dto/AccountDTO.java",
    "content": "package com.msy.plus.dto;\n\nimport com.msy.plus.core.dto.AbstractConverter;\nimport com.msy.plus.entity.AccountDO;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\nimport javax.validation.constraints.Email;\nimport javax.validation.constraints.NotEmpty;\nimport javax.validation.constraints.Size;\nimport java.io.Serializable;\n\n/**\n * @author MoShuying\n * @date 2018/07/14\n */\n@Data\n@Schema(name = \"账户传输实体\")\n@EqualsAndHashCode(callSuper = true)\npublic class AccountDTO extends AbstractConverter<AccountDTO, AccountDO> implements Serializable {\n  private static final long serialVersionUID = 1473352811666797847L;\n\n  @Schema(name = \"账户Id\", accessMode = Schema.AccessMode.READ_ONLY)\n  private Long id;\n\n  @Schema(name = \"邮箱\", example = \"123@qq.com\")\n  @Email(message = \"邮箱格式不正确\")\n  private String email;\n\n  @Schema(name = \"账户名\", accessMode = Schema.AccessMode.READ_ONLY, example = \"admin\")\n  @NotEmpty(message = \"账户名不能为空\")\n  @Size(min = 1, message = \"账户名长度不能小于1\")\n  private String name;\n\n  @Schema(name = \"密码\", example = \"admin\")\n  @NotEmpty(message = \"密码不能为空\")\n  @Size(min = 5, message = \"密码长度不能小于5\")\n  private String password;\n\n  @Override\n  protected AccountDTO setDTO() {\n    return this;\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/dto/AccountLoginDTO.java",
    "content": "package com.msy.plus.dto;\n\nimport com.msy.plus.core.dto.AbstractConverter;\nimport com.msy.plus.entity.AccountDO;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\nimport javax.validation.constraints.NotEmpty;\nimport javax.validation.constraints.Size;\nimport java.io.Serializable;\n\n/**\n * @author MoShuying\n * @date 2018/07/15\n */\n@Data\n@Schema(name = \"账户登录传输实体\")\n@EqualsAndHashCode(callSuper = true)\npublic class AccountLoginDTO extends AbstractConverter<AccountLoginDTO, AccountDO>\n    implements Serializable {\n  private static final long serialVersionUID = 1945186812588516555L;\n\n  @Schema(name = \"账户名\", example = \"admin\")\n  @NotEmpty(message = \"账户名不能为空\")\n  @Size(min = 1, message = \"账户名长度不能小于1\")\n  private String name;\n\n  @Schema(name = \"密码\", example = \"admin\")\n  @NotEmpty(message = \"密码不能为空\")\n  @Size(min = 5, message = \"密码长度不能小于5\")\n  private String password;\n\n  @Override\n  protected AccountLoginDTO setDTO() {\n    return this;\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/dto/AnalysisQuery.java",
    "content": "package com.msy.plus.dto;\n\nimport lombok.*;\nimport org.springframework.format.annotation.DateTimeFormat;\n\nimport java.util.Date;\n\n@Getter\n@Setter\n@NoArgsConstructor\n@AllArgsConstructor\n@ToString\npublic class AnalysisQuery {\n    private Integer page=1;\n    private Integer size=10;\n    private String name=\"\";\n    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)\n    private Date startTime;\n    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)\n    private Date endTime;\n    /**\n     * groupType\n     * 1 员工\n     * 2 年\n     * 3 月\n     * 4 日\n     */\n    private int groupType;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/dto/CustomerHandoverList.java",
    "content": "package com.msy.plus.dto;\n\nimport lombok.Getter;\nimport lombok.Setter;\n\nimport java.util.Date;\n\n@Getter\n@Setter\npublic class CustomerHandoverList {\n    private Integer id;\n    private String customerName;\n    private Date transTime;\n    private String transUser;\n    private String oldSeller;\n    private String newSeller;\n    private String transReason;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/dto/CustomerManagerList.java",
    "content": "package com.msy.plus.dto;\n\nimport lombok.Getter;\nimport lombok.Setter;\n\nimport javax.persistence.Column;\nimport javax.persistence.GeneratedValue;\nimport javax.persistence.GenerationType;\nimport javax.persistence.Id;\nimport java.util.Date;\n\n@Getter\n@Setter\npublic class CustomerManagerList {\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private Integer id;\n\n    /**\n     * 客户姓名\n     */\n    private String name;\n\n    /**\n     * 客户年龄\n     */\n    private Integer age;\n\n    /**\n     * 客户性别 页面为下拉框\n     */\n    private Integer gender;\n\n    /**\n     * 电话号码\n     */\n    private String tel;\n\n    private String qq;\n\n    private String job;\n\n    /**\n     * 客户来源\n     */\n    private String source;\n\n    /**\n     * 负责人 填写为当前登录用户\n     */\n    private Integer seller;\n\n    /**\n     *  创建人 填写为当前登录用户\n     */\n    @Column(name = \"inputUser\")\n    private String inputuser;\n\n    private String inputUserId;\n\n    @Column(name = \"inputTime\")\n    private Date inputtime;\n\n    /**\n     * -2:流失 -1:开发失败 0:潜在客户 1:正式客户 2:资源池客户\n     */\n    private Integer status;\n\n    /**\n     * 转正时间\n     */\n    @Column(name = \"positiveTime\")\n    private Date positivetime;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/dto/LoginResultDTO.java",
    "content": "package com.msy.plus.dto;\n\nimport com.msy.plus.core.dto.AbstractConverter;\nimport com.msy.plus.entity.LoginResultDO;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.Setter;\n\nimport java.io.Serializable;\nimport java.util.*;\n\n@Getter\n@Setter\n@Data\n@Schema(name = \"登录传输实体\")\n@EqualsAndHashCode(callSuper = true)\npublic class LoginResultDTO extends AbstractConverter<LoginResultDTO, LoginResultDO> implements Serializable {\n    private static final long serialVersionUID = -12322384324L;\n\n    @Schema(name = \"账户令牌\")\n    private String token;\n\n    @Schema(name = \"过期时间\")\n    private Date expireAt;\n\n    private List<Object> permissions = new ArrayList<>();\n    private List<Object> roles = new ArrayList<>();\n\n    @Schema(name = \"用户信息\")\n    private Map<String, Object> user = new HashMap<>();\n\n    @Schema(name = \"用户登录成功的提示\")\n    private String message = \"欢迎回来\";\n\n    public void setUserName(String name) {\n        this.getUser().put(\"name\", name);\n    }\n\n    public LoginResultDTO() {\n//        注入虚拟数据\n        Map<String, Object> permissions = new HashMap<>();\n        permissions.put(\"id\", \"queryForm\");\n        permissions.put(\"operation\", new String[]{\"add\", \"edit\"});\n        this.permissions.add(permissions);\n\n        this.user.put(\"address\", \"贺州市\");\n        this.user.put(\"avatar\", \"https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png\");\n        Map<String, Object> position = new HashMap<>();\n        position.put(\"CN\", \"前端工程师 | 蚂蚁金服-计算服务事业群-REACT平台\");\n        position.put(\"HK\", \"前端工程師 | 螞蟻金服-計算服務事業群-REACT平台\");\n        position.put(\"US\", \"Front-end engineer | Ant Financial - Computing services business group - REACT platform\");\n        this.user.put(\"position\", position);\n\n//        Date date = new Date(); //取时间\n//        Calendar calendar = new GregorianCalendar();\n//        calendar.setTime(date);\n////        calendar.add(calendar.DATE, 1); //把日期往后增加一天,整数  往后推,负数往前移动\n//        calendar.add(calendar.HOUR, 4);\n//        this.expireAt = calendar.getTime().toString();\n    }\n\n    @Override\n    protected LoginResultDTO setDTO() {\n        return this;\n    }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/dto/RoleDTO.java",
    "content": "package com.msy.plus.dto;\n\nimport com.msy.plus.core.dto.AbstractConverter;\nimport com.msy.plus.entity.RoleDO;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\nimport javax.persistence.Column;\nimport java.io.Serializable;\n\n/**\n * @author MoShuying\n * @date 2018/07/15\n */\n@Data\n@Schema(name = \"角色传输实体\")\n@EqualsAndHashCode(callSuper = true)\npublic class RoleDTO extends AbstractConverter<RoleDTO, RoleDO> implements Serializable {\n  private static final long serialVersionUID = -145221735177809163L;\n\n  @Schema(name = \"角色Id\", accessMode = Schema.AccessMode.READ_ONLY)\n  private Long id;\n\n  @Schema(name = \"角色名称\")\n  private String name;\n\n  /** 角色编号 */\n  @Column(name = \"sn\")\n  private String sn;\n\n  @Override\n  protected RoleDTO setDTO() {\n    return this;\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/dto/RoleWithPermissionDTO.java",
    "content": "package com.msy.plus.dto;\n\nimport com.msy.plus.entity.Permission;\nimport com.msy.plus.entity.RoleWithPermissionDO;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.Setter;\n\nimport javax.persistence.Column;\nimport javax.persistence.GeneratedValue;\nimport javax.persistence.GenerationType;\nimport javax.persistence.Id;\nimport javax.validation.constraints.NotEmpty;\nimport java.util.List;\n\n@Getter\n@Setter\n@Data\n@Schema(name = \"角色及权限传输实体\")\n@EqualsAndHashCode(callSuper = true)\npublic class RoleWithPermissionDTO extends RoleWithPermissionDO {\n    private static final long serialVersionUID = -123223812341212L;\n    /** 角色Id */\n    @NotEmpty(message = \"ID不能为空\")\n    @Schema(name = \"角色Id\", accessMode = Schema.AccessMode.READ_ONLY)\n    private Long id;\n\n    /** 角色名称 */\n    @Schema(name = \"角色名称\")\n    private String name;\n\n    /** 角色编号 */\n    @Schema(name = \"角色编号\")\n    private String sn;\n\n    /** 角色权限 */\n    @Schema(name = \"角色权限列表\")\n    private List<Permission> permissions;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/AccountDO.java",
    "content": "package com.msy.plus.entity;\n\nimport lombok.Data;\n\nimport javax.persistence.*;\nimport java.sql.Timestamp;\n\n/**\n * @author MoShuying\n * @date 2018/05/27\n */\n@Data\n@Table(name = \"employee\")\npublic class AccountDO {\n  /** 账户Id */\n  @Id\n  @GeneratedValue(strategy = GenerationType.IDENTITY)\n  @Column(name = \"id\")\n  private Long id;\n\n  /** 邮箱 */\n  @Column(name = \"email\")\n  private String email;\n\n  /** 账户名 */\n  @Column(name = \"name\")\n  private String name;\n\n  /** 密码 */\n  @Column(name = \"password\")\n  private String password;\n\n  /** 注册时间 */\n  @Column(name = \"register_time\")\n  private Timestamp registerTime;\n\n  /** 上一次登录时间 */\n  @Column(name = \"login_time\")\n  private Timestamp loginTime;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/AccountWithRoleDO.java",
    "content": "package com.msy.plus.entity;\n\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\nimport java.util.List;\n\n/**\n * 包含角色信息的账户实体\n *\n * @author MoShuying\n * @date 2018/07/15\n */\n@Data\n@EqualsAndHashCode(callSuper = false)\npublic class AccountWithRoleDO extends AccountDO {\n  /** 账户的角色列表 */\n  private List<RoleDO> roles;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/Analysis.java",
    "content": "package com.msy.plus.entity;\n\nimport lombok.Getter;\nimport lombok.Setter;\n\n@Getter\n@Setter\npublic class Analysis {\n    private String name;\n    private int count;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/CFUHSearch.java",
    "content": "package com.msy.plus.entity;\n\nimport lombok.Getter;\nimport lombok.Setter;\n\n/**\n * 客户跟进历史分页查询搜索\n */\n@Getter\n@Setter\npublic class CFUHSearch extends CustomerFollowUpHistory{\n    private String name;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/CustomerFollowUpHistory.java",
    "content": "package com.msy.plus.entity;\n\nimport java.util.Date;\nimport javax.persistence.*;\n\n@Table(name = \"customer_follow_up_history\")\npublic class CustomerFollowUpHistory {\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private Integer id;\n\n    /**\n     * 跟进时间\n     */\n    @Column(name = \"traceTime\")\n    private Date tracetime;\n\n    /**\n     * 跟进方式 计划采用如电话、邀约上门等  数据字典\n     */\n    @Column(name = \"traceType\")\n    private Integer tracetype;\n\n    /**\n     * 跟进效果 优----3、中----2、差----1\n     */\n    @Column(name = \"traceResult\")\n    private Integer traceresult;\n\n    /**\n     * 跟进客户 编辑时不可编辑 潜在客户对象/客户对象\n     */\n    @Column(name = \"customerID\")\n    private Integer customerid;\n\n    /**\n     * 创建人 自动填入当前登录用户，用户不可更改 员工对象\n     */\n    @Column(name = \"inputUser\")\n    private Integer inputuser;\n\n    /**\n     * 跟进类型 0:潜在开发计划 1:客户跟进历史\n     */\n    private Integer type;\n\n    /**\n     * 跟进内容 计划的详细内容\n     */\n    @Column(name = \"traceDetails\")\n    private String tracedetails;\n\n    private String comment;\n\n    /**\n     * @return id\n     */\n    public Integer getId() {\n        return id;\n    }\n\n    /**\n     * @param id\n     */\n    public void setId(Integer id) {\n        this.id = id;\n    }\n\n    /**\n     * 获取跟进时间\n     *\n     * @return traceTime - 跟进时间\n     */\n    public Date getTracetime() {\n        return tracetime;\n    }\n\n    /**\n     * 设置跟进时间\n     *\n     * @param tracetime 跟进时间\n     */\n    public void setTracetime(Date tracetime) {\n        this.tracetime = tracetime;\n    }\n\n    /**\n     * 获取跟进方式 计划采用如电话、邀约上门等  数据字典\n     *\n     * @return traceType - 跟进方式 计划采用如电话、邀约上门等  数据字典\n     */\n    public Integer getTracetype() {\n        return tracetype;\n    }\n\n    /**\n     * 设置跟进方式 计划采用如电话、邀约上门等  数据字典\n     *\n     * @param tracetype 跟进方式 计划采用如电话、邀约上门等  数据字典\n     */\n    public void setTracetype(Integer tracetype) {\n        this.tracetype = tracetype;\n    }\n\n    /**\n     * 获取跟进效果 优----3、中----2、差----1\n     *\n     * @return traceResult - 跟进效果 优----3、中----2、差----1\n     */\n    public Integer getTraceresult() {\n        return traceresult;\n    }\n\n    /**\n     * 设置跟进效果 优----3、中----2、差----1\n     *\n     * @param traceresult 跟进效果 优----3、中----2、差----1\n     */\n    public void setTraceresult(Integer traceresult) {\n        this.traceresult = traceresult;\n    }\n\n    /**\n     * 获取跟进客户 编辑时不可编辑 潜在客户对象/客户对象\n     *\n     * @return customerID - 跟进客户 编辑时不可编辑 潜在客户对象/客户对象\n     */\n    public Integer getCustomerid() {\n        return customerid;\n    }\n\n    /**\n     * 设置跟进客户 编辑时不可编辑 潜在客户对象/客户对象\n     *\n     * @param customerid 跟进客户 编辑时不可编辑 潜在客户对象/客户对象\n     */\n    public void setCustomerid(Integer customerid) {\n        this.customerid = customerid;\n    }\n\n    /**\n     * 获取创建人 自动填入当前登录用户，用户不可更改 员工对象\n     *\n     * @return inputUser - 创建人 自动填入当前登录用户，用户不可更改 员工对象\n     */\n    public Integer getInputuser() {\n        return inputuser;\n    }\n\n    /**\n     * 设置创建人 自动填入当前登录用户，用户不可更改 员工对象\n     *\n     * @param inputuser 创建人 自动填入当前登录用户，用户不可更改 员工对象\n     */\n    public void setInputuser(Integer inputuser) {\n        this.inputuser = inputuser;\n    }\n\n    /**\n     * 获取跟进类型 0:潜在开发计划 1:客户跟进历史\n     *\n     * @return type - 跟进类型 0:潜在开发计划 1:客户跟进历史\n     */\n    public Integer getType() {\n        return type;\n    }\n\n    /**\n     * 设置跟进类型 0:潜在开发计划 1:客户跟进历史\n     *\n     * @param type 跟进类型 0:潜在开发计划 1:客户跟进历史\n     */\n    public void setType(Integer type) {\n        this.type = type;\n    }\n\n    /**\n     * 获取跟进内容 计划的详细内容\n     *\n     * @return traceDetails - 跟进内容 计划的详细内容\n     */\n    public String getTracedetails() {\n        return tracedetails;\n    }\n\n    /**\n     * 设置跟进内容 计划的详细内容\n     *\n     * @param tracedetails 跟进内容 计划的详细内容\n     */\n    public void setTracedetails(String tracedetails) {\n        this.tracedetails = tracedetails;\n    }\n\n    /**\n     * @return comment\n     */\n    public String getComment() {\n        return comment;\n    }\n\n    /**\n     * @param comment\n     */\n    public void setComment(String comment) {\n        this.comment = comment;\n    }\n}"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/CustomerHandover.java",
    "content": "package com.msy.plus.entity;\n\nimport java.util.Date;\nimport javax.persistence.*;\n\n@Table(name = \"customer_handover\")\npublic class CustomerHandover {\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private Integer id;\n\n    /**\n     * 客户 客户对象\n     */\n    @Column(name = \"customerID\")\n    private Integer customerid;\n\n    /**\n     * 移交人员 实行移交操作的管理人员\n     */\n    @Column(name = \"transUser\")\n    private Integer transuser;\n\n    @Column(name = \"transTime\")\n    private Date transtime;\n\n    /**\n     * 老市场专员 客户上的原始市场人员\n     */\n    @Column(name = \"oldSeller\")\n    private Integer oldseller;\n\n    /**\n     * 新市场专员 由公司重新指派后的新市场人员\n     */\n    @Column(name = \"newSeller\")\n    private Integer newseller;\n\n    /**\n     * 移交原因\n     */\n    @Column(name = \"transReason\")\n    private String transreason;\n\n    /**\n     * @return id\n     */\n    public Integer getId() {\n        return id;\n    }\n\n    /**\n     * @param id\n     */\n    public void setId(Integer id) {\n        this.id = id;\n    }\n\n    /**\n     * 获取客户 客户对象\n     *\n     * @return customerID - 客户 客户对象\n     */\n    public Integer getCustomerid() {\n        return customerid;\n    }\n\n    /**\n     * 设置客户 客户对象\n     *\n     * @param customerid 客户 客户对象\n     */\n    public void setCustomerid(Integer customerid) {\n        this.customerid = customerid;\n    }\n\n    /**\n     * 获取移交人员 实行移交操作的管理人员\n     *\n     * @return transUser - 移交人员 实行移交操作的管理人员\n     */\n    public Integer getTransuser() {\n        return transuser;\n    }\n\n    /**\n     * 设置移交人员 实行移交操作的管理人员\n     *\n     * @param transuser 移交人员 实行移交操作的管理人员\n     */\n    public void setTransuser(Integer transuser) {\n        this.transuser = transuser;\n    }\n\n    /**\n     * @return transTime\n     */\n    public Date getTranstime() {\n        return transtime;\n    }\n\n    /**\n     * @param transtime\n     */\n    public void setTranstime(Date transtime) {\n        this.transtime = transtime;\n    }\n\n    /**\n     * 获取老市场专员 客户上的原始市场人员\n     *\n     * @return oldSeller - 老市场专员 客户上的原始市场人员\n     */\n    public Integer getOldseller() {\n        return oldseller;\n    }\n\n    /**\n     * 设置老市场专员 客户上的原始市场人员\n     *\n     * @param oldseller 老市场专员 客户上的原始市场人员\n     */\n    public void setOldseller(Integer oldseller) {\n        this.oldseller = oldseller;\n    }\n\n    /**\n     * 获取新市场专员 由公司重新指派后的新市场人员\n     *\n     * @return newSeller - 新市场专员 由公司重新指派后的新市场人员\n     */\n    public Integer getNewseller() {\n        return newseller;\n    }\n\n    /**\n     * 设置新市场专员 由公司重新指派后的新市场人员\n     *\n     * @param newseller 新市场专员 由公司重新指派后的新市场人员\n     */\n    public void setNewseller(Integer newseller) {\n        this.newseller = newseller;\n    }\n\n    /**\n     * 获取移交原因\n     *\n     * @return transReason - 移交原因\n     */\n    public String getTransreason() {\n        return transreason;\n    }\n\n    /**\n     * 设置移交原因\n     *\n     * @param transreason 移交原因\n     */\n    public void setTransreason(String transreason) {\n        this.transreason = transreason;\n    }\n}"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/CustomerManager.java",
    "content": "package com.msy.plus.entity;\n\nimport java.util.Date;\nimport javax.persistence.*;\n\n@Table(name = \"customer_manager\")\npublic class CustomerManager {\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private Integer id;\n\n    /**\n     * 客户姓名\n     */\n    private String name;\n\n    /**\n     * 客户年龄\n     */\n    private Integer age;\n\n    /**\n     * 客户性别 页面为下拉框\n     */\n    private Integer gender;\n\n    /**\n     * 电话号码\n     */\n    private String tel;\n\n    private String qq;\n\n    private Integer job;\n\n    /**\n     * 客户来源\n     */\n    private Integer source;\n\n    /**\n     * 负责人 填写为当前登录用户\n     */\n    private Integer seller;\n\n    /**\n     *  创建人 填写为当前登录用户\n     */\n    @Column(name = \"inputUser\")\n    private Integer inputuser;\n\n    @Column(name = \"inputTime\")\n    private Date inputtime;\n\n    /**\n     * -2:流失 -1:开发失败 0:潜在客户 1:正式客户 2:资源池客户\n     */\n    private Integer status;\n\n    /**\n     * 转正时间\n     */\n    @Column(name = \"positiveTime\")\n    private Date positivetime;\n\n    /**\n     * @return id\n     */\n    public Integer getId() {\n        return id;\n    }\n\n    /**\n     * @param id\n     */\n    public void setId(Integer id) {\n        this.id = id;\n    }\n\n    /**\n     * 获取客户姓名\n     *\n     * @return name - 客户姓名\n     */\n    public String getName() {\n        return name;\n    }\n\n    /**\n     * 设置客户姓名\n     *\n     * @param name 客户姓名\n     */\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    /**\n     * 获取客户年龄\n     *\n     * @return age - 客户年龄\n     */\n    public Integer getAge() {\n        return age;\n    }\n\n    /**\n     * 设置客户年龄\n     *\n     * @param age 客户年龄\n     */\n    public void setAge(Integer age) {\n        this.age = age;\n    }\n\n    /**\n     * 获取客户性别 页面为下拉框\n     *\n     * @return gender - 客户性别 页面为下拉框\n     */\n    public Integer getGender() {\n        return gender;\n    }\n\n    /**\n     * 设置客户性别 页面为下拉框\n     *\n     * @param gender 客户性别 页面为下拉框\n     */\n    public void setGender(Integer gender) {\n        this.gender = gender;\n    }\n\n    /**\n     * 获取电话号码\n     *\n     * @return tel - 电话号码\n     */\n    public String getTel() {\n        return tel;\n    }\n\n    /**\n     * 设置电话号码\n     *\n     * @param tel 电话号码\n     */\n    public void setTel(String tel) {\n        this.tel = tel;\n    }\n\n    /**\n     * @return qq\n     */\n    public String getQq() {\n        return qq;\n    }\n\n    /**\n     * @param qq\n     */\n    public void setQq(String qq) {\n        this.qq = qq;\n    }\n\n    /**\n     * @return job\n     */\n    public Integer getJob() {\n        return job;\n    }\n\n    /**\n     * @param job\n     */\n    public void setJob(Integer job) {\n        this.job = job;\n    }\n\n    /**\n     * 获取客户来源\n     *\n     * @return source - 客户来源\n     */\n    public Integer getSource() {\n        return source;\n    }\n\n    /**\n     * 设置客户来源\n     *\n     * @param source 客户来源\n     */\n    public void setSource(Integer source) {\n        this.source = source;\n    }\n\n    /**\n     * 获取负责人 填写为当前登录用户\n     *\n     * @return seller - 负责人 填写为当前登录用户\n     */\n    public Integer getSeller() {\n        return seller;\n    }\n\n    /**\n     * 设置负责人 填写为当前登录用户\n     *\n     * @param seller 负责人 填写为当前登录用户\n     */\n    public void setSeller(Integer seller) {\n        this.seller = seller;\n    }\n\n    /**\n     * 获取 创建人 填写为当前登录用户\n     *\n     * @return inputUser -  创建人 填写为当前登录用户\n     */\n    public Integer getInputuser() {\n        return inputuser;\n    }\n\n    /**\n     * 设置 创建人 填写为当前登录用户\n     *\n     * @param inputuser  创建人 填写为当前登录用户\n     */\n    public void setInputuser(Integer inputuser) {\n        this.inputuser = inputuser;\n    }\n\n    /**\n     * @return inputTime\n     */\n    public Date getInputtime() {\n        return inputtime;\n    }\n\n    /**\n     * @param inputtime\n     */\n    public void setInputtime(Date inputtime) {\n        this.inputtime = inputtime;\n    }\n\n    /**\n     * 获取-2:流失 -1:开发失败 0:潜在客户 1:正式客户 2:资源池客户\n     *\n     * @return status - -2:流失 -1:开发失败 0:潜在客户 1:正式客户 2:资源池客户\n     */\n    public Integer getStatus() {\n        return status;\n    }\n\n    /**\n     * 设置-2:流失 -1:开发失败 0:潜在客户 1:正式客户 2:资源池客户\n     *\n     * @param status -2:流失 -1:开发失败 0:潜在客户 1:正式客户 2:资源池客户\n     */\n    public void setStatus(Integer status) {\n        this.status = status;\n    }\n\n    /**\n     * 获取转正时间\n     *\n     * @return positiveTime - 转正时间\n     */\n    public Date getPositivetime() {\n        return positivetime;\n    }\n\n    /**\n     * 设置转正时间\n     *\n     * @param positivetime 转正时间\n     */\n    public void setPositivetime(Date positivetime) {\n        this.positivetime = positivetime;\n    }\n}"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/Department.java",
    "content": "package com.msy.plus.entity;\n\nimport javax.persistence.*;\n\npublic class Department {\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private Integer id;\n\n    private String sn;\n\n    private String name;\n\n    /**\n     * @return id\n     */\n    public Integer getId() {\n        return id;\n    }\n\n    /**\n     * @param id\n     */\n    public void setId(Integer id) {\n        this.id = id;\n    }\n\n    /**\n     * @return sn\n     */\n    public String getSn() {\n        return sn;\n    }\n\n    /**\n     * @param sn\n     */\n    public void setSn(String sn) {\n        this.sn = sn;\n    }\n\n    /**\n     * @return name\n     */\n    public String getName() {\n        return name;\n    }\n\n    /**\n     * @param name\n     */\n    public void setName(String name) {\n        this.name = name;\n    }\n}"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/DictionaryContents.java",
    "content": "package com.msy.plus.entity;\n\nimport javax.persistence.*;\n\n@Table(name = \"dictionary_contents\")\npublic class DictionaryContents {\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private Integer id;\n\n    /**\n     * 字典目录编号\n     */\n    private String sn;\n\n    /**\n     * 字典目录名称\n     */\n    private String title;\n\n    /**\n     * 字典目录简介\n     */\n    private String intro;\n\n    /**\n     * @return id\n     */\n    public Integer getId() {\n        return id;\n    }\n\n    /**\n     * @param id\n     */\n    public void setId(Integer id) {\n        this.id = id;\n    }\n\n    /**\n     * 获取字典目录编号\n     *\n     * @return sn - 字典目录编号\n     */\n    public String getSn() {\n        return sn;\n    }\n\n    /**\n     * 设置字典目录编号\n     *\n     * @param sn 字典目录编号\n     */\n    public void setSn(String sn) {\n        this.sn = sn;\n    }\n\n    /**\n     * 获取字典目录名称\n     *\n     * @return title - 字典目录名称\n     */\n    public String getTitle() {\n        return title;\n    }\n\n    /**\n     * 设置字典目录名称\n     *\n     * @param title 字典目录名称\n     */\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    /**\n     * 获取字典目录简介\n     *\n     * @return intro - 字典目录简介\n     */\n    public String getIntro() {\n        return intro;\n    }\n\n    /**\n     * 设置字典目录简介\n     *\n     * @param intro 字典目录简介\n     */\n    public void setIntro(String intro) {\n        this.intro = intro;\n    }\n}"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/DictionaryDetails.java",
    "content": "package com.msy.plus.entity;\n\nimport javax.persistence.*;\n\n@Table(name = \"dictionary_details\")\npublic class DictionaryDetails {\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private Integer id;\n\n    /**\n     * 字典明细名称\n     */\n    private String title;\n\n    /**\n     * 字典明细序号\n     */\n    private Integer sequence;\n\n    @Column(name = \"parentId\")\n    private Integer parentid;\n\n    /**\n     * @return id\n     */\n    public Integer getId() {\n        return id;\n    }\n\n    /**\n     * @param id\n     */\n    public void setId(Integer id) {\n        this.id = id;\n    }\n\n    /**\n     * 获取字典明细名称\n     *\n     * @return title - 字典明细名称\n     */\n    public String getTitle() {\n        return title;\n    }\n\n    /**\n     * 设置字典明细名称\n     *\n     * @param title 字典明细名称\n     */\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    /**\n     * 获取字典明细序号\n     *\n     * @return sequence - 字典明细序号\n     */\n    public Integer getSequence() {\n        return sequence;\n    }\n\n    /**\n     * 设置字典明细序号\n     *\n     * @param sequence 字典明细序号\n     */\n    public void setSequence(Integer sequence) {\n        this.sequence = sequence;\n    }\n\n    /**\n     * @return parentId\n     */\n    public Integer getParentid() {\n        return parentid;\n    }\n\n    /**\n     * @param parentid\n     */\n    public void setParentid(Integer parentid) {\n        this.parentid = parentid;\n    }\n}"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/Employee.java",
    "content": "package com.msy.plus.entity;\n\nimport lombok.Getter;\nimport lombok.Setter;\n\nimport java.util.Date;\nimport javax.persistence.*;\n\n@Getter\n@Setter\npublic class Employee {\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private Long id;\n\n    private String name;\n\n    private String password;\n\n    private String email;\n\n    private Integer age;\n\n    private Integer dept;\n\n    @Column(name = \"hireDate\")\n    private Date hiredate;\n\n    /**\n     * 状态 1正常 0离职\n     */\n    private Integer state;\n\n    /**\n     * 超级管理员身份 1超管 0普通\n     */\n    private Integer admin;\n}"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/EmployeeDetail.java",
    "content": "package com.msy.plus.entity;\n\nimport lombok.*;\nimport java.util.List;\n\n@Getter\n@Setter\n@AllArgsConstructor\n@NoArgsConstructor\n@ToString\npublic class EmployeeDetail extends Employee{\n    List<Long> roleIds;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/EmployeeWithRoleDO.java",
    "content": "package com.msy.plus.entity;\n\nimport lombok.*;\n\n@Getter\n@Setter\n@AllArgsConstructor\n@NoArgsConstructor\n@ToString\npublic class EmployeeWithRoleDO extends Employee{\n    private String departmentName;\n    String roleNames;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/LoginResultDO.java",
    "content": "package com.msy.plus.entity;\n\nimport lombok.Data;\n\nimport java.util.Date;\nimport java.util.Map;\n\n@Data\npublic class LoginResultDO {\n    private String token;\n    private String expireAt = new Date().toString();\n    private Map permissions;\n    private Object roles;\n    private Object position;\n    private Map user;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/Permission.java",
    "content": "package com.msy.plus.entity;\n\nimport lombok.Getter;\nimport lombok.Setter;\nimport javax.persistence.*;\n\n@Getter\n@Setter\npublic class Permission {\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private Long id;\n\n    /**\n     * 权限名称\n     */\n    private String name;\n\n    /**\n     * 资源地址\n     */\n    private String expression;\n}"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/RoleDO.java",
    "content": "package com.msy.plus.entity;\n\nimport lombok.Data;\nimport lombok.Getter;\nimport lombok.Setter;\n\nimport javax.persistence.*;\n\n/**\n * 角色实体\n *\n * @author MoShuying\n * @date 2018/05/27\n */\n@Data\n@Getter\n@Setter\n@Table(name = \"role\")\npublic class RoleDO {\n  /** 角色Id */\n  @Id\n  @GeneratedValue(strategy = GenerationType.IDENTITY)\n  @Column(name = \"id\")\n  private Long id;\n\n  /** 角色名称 */\n  @Column(name = \"name\")\n  private String name;\n\n  /** 角色编号 */\n  @Column(name = \"sn\")\n  private String sn;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/RolePermissionDO.java",
    "content": "package com.msy.plus.entity;\n\nimport lombok.*;\n\nimport javax.persistence.GeneratedValue;\nimport javax.persistence.GenerationType;\nimport javax.persistence.Id;\n\n/**\n * 角色和权限中间表数据模型\n */\n@Getter\n@Setter\n@AllArgsConstructor\n@NoArgsConstructor\n@ToString\npublic class RolePermissionDO {\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private Long id;\n\n    /**\n     * 角色ID\n     */\n    private Long role_id;\n\n    /**\n     * 权限ID\n     */\n    private Long permission_id;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/RoleWithPermissionDO.java",
    "content": "package com.msy.plus.entity;\n\nimport lombok.*;\n\nimport java.util.List;\n\n@Getter\n@Setter\n@AllArgsConstructor\n@NoArgsConstructor\n@ToString\npublic class RoleWithPermissionDO extends RoleDO {\n    List<Permission> permissions;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/entity/Test.java",
    "content": "package com.msy.plus.entity;\n\nimport javax.persistence.GeneratedValue;\nimport javax.persistence.GenerationType;\nimport javax.persistence.Id;\n\npublic class Test {\n  /** 测试Id */\n  @Id\n  @GeneratedValue(strategy = GenerationType.IDENTITY)\n  private Long id;\n\n  /** 测试名称 */\n  private String name;\n\n  /**\n   * 获取测试Id\n   *\n   * @return id - 测试Id\n   */\n  public Long getId() {\n    return id;\n  }\n\n  /**\n   * 设置测试Id\n   *\n   * @param id 测试Id\n   */\n  public void setId(Long id) {\n    this.id = id;\n  }\n\n  /**\n   * 获取测试名称\n   *\n   * @return name - 测试名称\n   */\n  public String getName() {\n    return name;\n  }\n\n  /**\n   * 设置测试名称\n   *\n   * @param name 测试名称\n   */\n  public void setName(String name) {\n    this.name = name;\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/filter/AuthenticationFilter.java",
    "content": "package com.msy.plus.filter;\n\nimport com.msy.plus.core.jwt.JwtUtil;\nimport com.msy.plus.util.IpUtils;\nimport com.msy.plus.util.UrlUtils;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.security.authentication.UsernamePasswordAuthenticationToken;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.security.web.authentication.WebAuthenticationDetailsSource;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StringUtils;\n\nimport javax.annotation.Resource;\nimport javax.servlet.*;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.util.Optional;\n\n/**\n * 身份认证过滤器\n *\n * @author MoShuying\n * @date 2018/05/27\n */\n@Slf4j\n@Order(2)\n@Component\npublic class AuthenticationFilter implements Filter {\n  @Resource private JwtUtil jwtUtil;\n\n  @Override\n  public void init(final FilterConfig filterConfig) {\n    log.debug(\"==> AuthenticationFilter init\");\n  }\n\n  @Override\n  public void doFilter(\n      final ServletRequest servletRequest,\n      final ServletResponse servletResponse,\n      final FilterChain filterChain)\n      throws IOException, ServletException {\n    final HttpServletRequest request = (HttpServletRequest) servletRequest;\n    final HttpServletResponse response = (HttpServletResponse) servletResponse;\n\n    final String token = this.jwtUtil.getTokenFromRequest(request);\n    if (!StringUtils.isEmpty(token)) {\n      final Optional<String> name = this.jwtUtil.getName(token);\n      log.debug(\"==> Account<{}> token: {}\", name.orElse(\"\"), token);\n\n      if (name.isPresent()\n          && !Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())\n              .isPresent()) {\n        if (this.jwtUtil.validateToken(token)) {\n          final UsernamePasswordAuthenticationToken authentication =\n              this.jwtUtil.getAuthentication(name.get(), token);\n\n          authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));\n\n          // 向 security 上下文中注入已认证的账户\n          // 之后可以直接在控制器 controller 的入参获得 Principal 或 Authentication\n          SecurityContextHolder.getContext().setAuthentication(authentication);\n          log.debug(\"==> Account<{}> is authorized, set security context\", name);\n        }\n      }\n    } else {\n      log.debug(\n          \"==> IP<{}> Request: [{}] {}\",\n          IpUtils.getIpAddress(),\n          request.getMethod(),\n          UrlUtils.getMappingUrl(request));\n    }\n    filterChain.doFilter(request, response);\n  }\n\n  @Override\n  public void destroy() {\n    log.debug(\"==> AuthenticationFilter destroy\");\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/filter/CorsFilter.java",
    "content": "package com.msy.plus.filter;\n\nimport com.msy.plus.util.IpUtils;\nimport com.msy.plus.util.UrlUtils;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.stereotype.Component;\n\nimport javax.servlet.*;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.HashSet;\nimport java.util.Set;\n\nimport static com.msy.plus.core.constant.ProjectConstant.SPRING_PROFILE_DEVELOPMENT;\nimport static com.msy.plus.core.constant.ProjectConstant.SPRING_PROFILE_PRODUCTION;\n\n/**\n * 跨域过滤器\n *\n * @author MoShuying\n * @date 2018/06/04\n */\n@Slf4j\n@Order(1)\n@Component\npublic class CorsFilter implements Filter {\n    @Value(\"${spring.profiles.active}\")\n    private String activeProfile;\n\n    @Override\n    public void init(final FilterConfig filterConfig) {\n        log.debug(\"==> CorsFilter init\");\n    }\n\n    @Override\n    public void doFilter(\n            final ServletRequest servletRequest,\n            final ServletResponse servletResponse,\n            final FilterChain filterChain)\n            throws IOException, ServletException {\n        final HttpServletRequest request = (HttpServletRequest) servletRequest;\n        final HttpServletResponse response = (HttpServletResponse) servletResponse;\n\n        // 仅在生产环境下生效\n        if (SPRING_PROFILE_PRODUCTION.equals(this.activeProfile)) {\n            // 设置允许多个域名请求\n            String[] allowDomains = {\"http://project.crm3.msy.plus\",\"https://project.crm3.msy.plus\"};\n            Set<String> allowOrigins = new HashSet(Arrays.asList(allowDomains));\n            String originHeads = request.getHeader(\"Origin\");\n            if(allowOrigins.contains(originHeads)){\n                //设置允许跨域的配置\n                // 这里填写你允许进行跨域的主机ip（正式上线时可以动态配置具体允许的域名和IP）\n                response.setHeader(\"Access-Control-Allow-Origin\", originHeads);\n            }\n        }\n        if(SPRING_PROFILE_DEVELOPMENT.equals(this.activeProfile)){\n            // 允许所有来源\n            response.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n        }\n        response.setHeader(\"Access-Control-Allow-Credentials\", \"true\");\n        response.setHeader(\n                \"Access-Control-Allow-Headers\", \"Content-Type, Content-Length, Authorization\");\n        // 明确允许通过的方法，不建议使用 *\n        response.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, PUT, PATCH, OPTIONS\");\n        response.setHeader(\"Access-Control-Max-Age\", \"3600\");\n        response.setHeader(\"Access-Control-Expose-Headers\", \"*\");\n\n        // 预请求后，直接返回\n        // 返回码必须为 200 否则视为请求失败\n        if (HttpMethod.OPTIONS.matches(request.getMethod())) {\n            return;\n        }\n\n        log.debug(\n                \"==> IP<{}> Request: [{}] {}\",\n                IpUtils.getIpAddress(),\n                request.getMethod(),\n                UrlUtils.getMappingUrl(request));\n\n        filterChain.doFilter(request, response);\n    }\n\n    @Override\n    public void destroy() {\n        log.debug(\"==> CorsFilter destroy\");\n    }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/filter/MyAuthenticationEntryPoint.java",
    "content": "package com.msy.plus.filter;\n\nimport com.msy.plus.core.response.ResultCode;\nimport com.msy.plus.core.response.ResultGenerator;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.core.AuthenticationException;\nimport org.springframework.security.web.AuthenticationEntryPoint;\nimport org.springframework.stereotype.Component;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.io.Serializable;\nimport java.nio.charset.StandardCharsets;\n\n/**\n * 认证入口点 因为 RESTFul 没有登录界面所以只显示未登录提示\n *\n * @author MoShuying\n * @date 2018/05/27\n */\n@Component\npublic class MyAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {\n  @Override\n  public void commence(\n      final HttpServletRequest request,\n      final HttpServletResponse response,\n      final AuthenticationException authException)\n      throws IOException {\n    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);\n    response.setHeader(\"Content-type\", MediaType.APPLICATION_JSON_VALUE);\n    response.setCharacterEncoding(StandardCharsets.UTF_8.displayName());\n    response\n        .getWriter()\n        .println(ResultGenerator.genFailedResult(ResultCode.UNAUTHORIZED_EXCEPTION).toString());\n    response.getWriter().close();\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/filter/RequestWrapper.java",
    "content": "package com.msy.plus.filter;\n\nimport javax.servlet.ReadListener;\nimport javax.servlet.ServletInputStream;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletRequestWrapper;\nimport java.io.BufferedReader;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.util.Optional;\n\n/**\n * 请求装饰器，用于多次读取请求流\n *\n * @author MoShuying\n * @date 2018/07/13\n */\npublic class RequestWrapper extends HttpServletRequestWrapper {\n  private final StringBuilder body;\n\n  public RequestWrapper(final HttpServletRequest request) throws IOException {\n    super(request);\n    this.body = new StringBuilder();\n    final BufferedReader bufferedReader = request.getReader();\n    String line;\n    while (Optional.ofNullable(line = bufferedReader.readLine()).isPresent()) {\n      this.body.append(line);\n    }\n  }\n\n  @Override\n  public ServletInputStream getInputStream() {\n    final ByteArrayInputStream byteArrayInputStream =\n        new ByteArrayInputStream(this.body.toString().getBytes());\n    return new ServletInputStream() {\n      @Override\n      public int read() {\n        return byteArrayInputStream.read();\n      }\n\n      @Override\n      public boolean isFinished() {\n        return false;\n      }\n\n      @Override\n      public boolean isReady() {\n        return false;\n      }\n\n      @Override\n      public void setReadListener(final ReadListener readListener) {}\n    };\n  }\n\n  @Override\n  public BufferedReader getReader() {\n    return new BufferedReader(new InputStreamReader(this.getInputStream()));\n  }\n\n  public String getJson() {\n    return this.getReader()\n        .lines()\n        .sequential()\n        .reduce(System.lineSeparator(), (accumulator, actual) -> accumulator + actual);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/mapper/AccountMapper.java",
    "content": "package com.msy.plus.mapper;\n\nimport com.msy.plus.core.mapper.MyMapper;\nimport com.msy.plus.entity.AccountDO;\nimport com.msy.plus.entity.AccountWithRoleDO;\nimport com.msy.plus.query.AccountQuery;\nimport org.apache.ibatis.annotations.Param;\n\n/**\n * @author MoShuying\n * @date 2018/05/27\n */\npublic interface AccountMapper extends MyMapper<AccountDO> {\n  /**\n   * 按条件查询账户\n   *\n   * @param accountQuery 账户查询条件\n   * @return 账户\n   */\n  AccountWithRoleDO getByQueryWithRole(AccountQuery accountQuery);\n\n  /**\n   * 按账户名更新最后登陆时间\n   *\n   * @param name 账户名\n   * @return 影响行数\n   */\n  int updateLoginTimeByName(@Param(\"name\") String name);\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/mapper/CustomerFollowUpHistoryMapper.java",
    "content": "package com.msy.plus.mapper;\n\nimport com.msy.plus.core.mapper.MyMapper;\nimport com.msy.plus.entity.CFUHSearch;\nimport com.msy.plus.entity.CustomerFollowUpHistory;\n\nimport java.util.List;\n\npublic interface CustomerFollowUpHistoryMapper extends MyMapper<CustomerFollowUpHistory> {\n    List<CFUHSearch> listAndSearch(String keyword, String startTime, String endTime, Integer type);\n}"
  },
  {
    "path": "back/src/main/java/com/msy/plus/mapper/CustomerHandoverMapper.java",
    "content": "package com.msy.plus.mapper;\n\nimport com.msy.plus.core.mapper.MyMapper;\nimport com.msy.plus.dto.CustomerHandoverList;\nimport com.msy.plus.entity.CustomerHandover;\n\nimport java.util.Date;\nimport java.util.List;\n\npublic interface CustomerHandoverMapper extends MyMapper<CustomerHandover> {\n    List<CustomerHandoverList> listAndSearch(String keyword, Date startTime, Date endTime);\n}"
  },
  {
    "path": "back/src/main/java/com/msy/plus/mapper/CustomerManagerMapper.java",
    "content": "package com.msy.plus.mapper;\n\nimport com.msy.plus.core.mapper.MyMapper;\nimport com.msy.plus.dto.AnalysisQuery;\nimport com.msy.plus.entity.Analysis;\nimport com.msy.plus.entity.CustomerManager;\nimport com.msy.plus.dto.CustomerManagerList;\n\nimport java.util.List;\n\npublic interface CustomerManagerMapper extends MyMapper<CustomerManager> {\n    List<CustomerManagerList> listAllWithDictionary(String keyword,Integer status);\n    CustomerManager getDetailById(Object id);\n    List<Analysis> queryAnalysis(AnalysisQuery analysisQuery);\n}"
  },
  {
    "path": "back/src/main/java/com/msy/plus/mapper/DepartmentMapper.java",
    "content": "package com.msy.plus.mapper;\n\nimport com.msy.plus.core.mapper.MyMapper;\nimport com.msy.plus.entity.Department;\n\npublic interface DepartmentMapper extends MyMapper<Department> {\n}"
  },
  {
    "path": "back/src/main/java/com/msy/plus/mapper/DictionaryContentsMapper.java",
    "content": "package com.msy.plus.mapper;\n\nimport com.msy.plus.core.mapper.MyMapper;\nimport com.msy.plus.entity.DictionaryContents;\n\nimport java.util.List;\n\npublic interface DictionaryContentsMapper extends MyMapper<DictionaryContents> {\n    List<DictionaryContents> listWithKeyword(String keyword);\n}"
  },
  {
    "path": "back/src/main/java/com/msy/plus/mapper/DictionaryDetailsMapper.java",
    "content": "package com.msy.plus.mapper;\n\nimport com.msy.plus.core.mapper.MyMapper;\nimport com.msy.plus.entity.DictionaryContents;\nimport com.msy.plus.entity.DictionaryDetails;\n\nimport java.util.List;\n\npublic interface DictionaryDetailsMapper extends MyMapper<DictionaryDetails> {\n    List<DictionaryContents> listWithKeyword(int id,String keyword);\n\n}"
  },
  {
    "path": "back/src/main/java/com/msy/plus/mapper/EmployeeMapper.java",
    "content": "package com.msy.plus.mapper;\n\nimport com.msy.plus.core.mapper.MyMapper;\nimport com.msy.plus.entity.Employee;\nimport com.msy.plus.entity.EmployeeDetail;\nimport com.msy.plus.entity.EmployeeWithRoleDO;\n\nimport java.util.List;\n\npublic interface EmployeeMapper extends MyMapper<Employee> {\n    EmployeeDetail getDetailById(Long id);\n\n    /**\n     * 分页查询员工\n     * @return\n     */\n    List<EmployeeWithRoleDO> listEmployeeWithRole(String keyword,int dept);\n\n    /**\n     * 保存员工角色信息\n     * @param id\n     * @param roles\n     * @return\n     */\n    void saveRoles(Long id, List<Long> roles);\n\n    /**\n     * 删除员工权限\n     * @param id\n     * @return\n     */\n    int deleteEmployeeWithRole(Long id);\n    int deleteEmployeeWithRoleItem(Long id,Long roleId);\n\n    /**\n     * 获取所有中间表的id\n     * @param id\n     * @return List<RolePermissionDO>\n     */\n    List<Long> getAllEmployeeRoleTableRow(Long id);\n\n}"
  },
  {
    "path": "back/src/main/java/com/msy/plus/mapper/PermissionMapper.java",
    "content": "package com.msy.plus.mapper;\n\nimport com.msy.plus.core.mapper.MyMapper;\nimport com.msy.plus.entity.Permission;\n\npublic interface PermissionMapper extends MyMapper<Permission> {\n}"
  },
  {
    "path": "back/src/main/java/com/msy/plus/mapper/RoleMapper.java",
    "content": "package com.msy.plus.mapper;\n\nimport com.msy.plus.core.mapper.MyMapper;\nimport com.msy.plus.entity.Permission;\nimport com.msy.plus.entity.RoleDO;\nimport com.msy.plus.entity.RolePermissionDO;\nimport com.msy.plus.entity.RoleWithPermissionDO;\nimport org.apache.ibatis.annotations.Param;\n\nimport java.util.List;\n\n/**\n * @author MoShuying\n * @date 2018/07/15\n */\npublic interface RoleMapper extends MyMapper<RoleDO> {\n  /**\n   * 赋予默认角色给账户\n   *\n   * @param accountId 账户Id\n   * @return 影响行数\n   */\n  int saveAsDefaultRole(@Param(\"accountId\") Long accountId);\n\n  /**\n   * 获取角色信息并查询角色权限\n   * @param id\n   * @return\n   */\n  RoleWithPermissionDO getDetailById(Long id);\n\n  /**\n   * 保存用户权限\n   * @param permissions\n   */\n  int savePermissions(Long roleId,List<Long> permissions);\n\n  /**\n   * 删除中间表信息\n   * @param roleId\n   * @param permissionId\n   */\n  void deleteRolePermissionItem(Long roleId,Long permissionId);\n\n  /**\n   * 获取所有权限表中的字段\n   * @param roleId\n   * @return List<RolePermissionDO>\n   */\n  List<RolePermissionDO> getAllRolePermissionTableRow(Long roleId);\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/query/AccountQuery.java",
    "content": "package com.msy.plus.query;\n\nimport lombok.Builder;\n\nimport java.io.Serializable;\n\n/**\n * 账户查询实体\n *\n * @author MoShuying\n * @date 2018/07/15\n */\n@Builder\npublic class AccountQuery implements Serializable {\n  private static final long serialVersionUID = 4063412382769589319L;\n\n  /** 账户Id */\n  private final Long id;\n\n  /** 邮箱 */\n  private final String email;\n\n  /** 账户名 */\n  private final String name;\n\n  /** 密码 */\n  private final String password;\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/AccountService.java",
    "content": "package com.msy.plus.service;\n\nimport com.msy.plus.core.service.Service;\nimport com.msy.plus.dto.AccountDTO;\nimport com.msy.plus.entity.AccountDO;\nimport com.msy.plus.entity.AccountWithRoleDO;\n\n/**\n * @author MoShuying\n * @date 2018/05/27\n */\npublic interface AccountService extends Service<AccountDO> {\n  /**\n   * 保存账户\n   *\n   * @param accountDTO 账户传输实体\n   */\n  void save(AccountDTO accountDTO);\n\n  /**\n   * 按账户名查询带有角色信息的账户\n   *\n   * @param name 账户名\n   * @return 账户\n   */\n  AccountWithRoleDO getByNameWithRole(String name);\n\n  /**\n   * 按账户Id查询带有角色信息的账户\n   *\n   * @param id 账户Id\n   * @return 账户\n   */\n  AccountWithRoleDO getByIdWithRole(Long id);\n\n  /**\n   * 更新账户\n   *\n   * @param accountDTO 账户传输实体\n   */\n  void updateByName(AccountDTO accountDTO);\n\n  /**\n   * 按账户名更新最后一次登录时间\n   *\n   * @param name 账户名\n   * @return 是否更新成功\n   */\n  boolean updateLoginTimeByName(String name);\n\n  /**\n   * 验证账户密码\n   *\n   * @param rawPassword 原密码\n   * @param encodedPassword 加密后的密码\n   * @return {boolean}\n   */\n  boolean verifyPassword(String rawPassword, String encodedPassword);\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/CustomerFollowUpHistoryService.java",
    "content": "package com.msy.plus.service;\n\nimport com.msy.plus.entity.CFUHSearch;\nimport com.msy.plus.entity.CustomerFollowUpHistory;\nimport com.msy.plus.core.service.Service;\n\nimport java.util.Date;\nimport java.util.List;\n\n/**\n* @author MoShuYing\n* @date 2021/05/21\n*/\npublic interface CustomerFollowUpHistoryService extends Service<CustomerFollowUpHistory> {\n    List<CFUHSearch> listAndSearch(String keyword, Date startTime, Date endTime, Integer type);\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/CustomerHandoverService.java",
    "content": "package com.msy.plus.service;\n\nimport com.msy.plus.dto.CustomerHandoverList;\nimport com.msy.plus.entity.CustomerHandover;\nimport com.msy.plus.core.service.Service;\n\nimport java.util.Date;\nimport java.util.List;\n\n/**\n* @author MoShuYing\n* @date 2021/05/21\n*/\npublic interface CustomerHandoverService extends Service<CustomerHandover> {\n    List<CustomerHandoverList> listAndSearch(String keyword, Date startTime, Date endTime);\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/CustomerManagerService.java",
    "content": "package com.msy.plus.service;\n\nimport com.msy.plus.dto.AnalysisQuery;\nimport com.msy.plus.dto.CustomerManagerList;\nimport com.msy.plus.entity.Analysis;\nimport com.msy.plus.entity.CustomerManager;\nimport com.msy.plus.core.service.Service;\n\nimport java.util.List;\n\n/**\n* @author MoShuYing\n* @date 2021/05/20\n*/\npublic interface CustomerManagerService extends Service<CustomerManager> {\n    List<CustomerManagerList> listAllWithDictionary(String keyword, Integer status);\n    List<Analysis> queryAnalysis(AnalysisQuery analysisQuery);\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/DepartmentService.java",
    "content": "package com.msy.plus.service;\n\nimport com.msy.plus.entity.Department;\nimport com.msy.plus.core.service.Service;\n\n/**\n* @author MoShuYing\n* @date 2021/05/12\n*/\npublic interface DepartmentService extends Service<Department> {\n\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/DictionaryContentsService.java",
    "content": "package com.msy.plus.service;\n\nimport com.msy.plus.entity.DictionaryContents;\nimport com.msy.plus.core.service.Service;\n\nimport java.util.List;\n\n/**\n* @author MoShuYing\n* @date 2021/05/18\n*/\npublic interface DictionaryContentsService extends Service<DictionaryContents> {\n    List<DictionaryContents>  listWithKeyword(String keyword);\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/DictionaryDetailsService.java",
    "content": "package com.msy.plus.service;\n\nimport com.msy.plus.entity.DictionaryContents;\nimport com.msy.plus.entity.DictionaryDetails;\nimport com.msy.plus.core.service.Service;\n\nimport java.util.List;\n\n/**\n* @author MoShuYing\n* @date 2021/05/18\n*/\npublic interface DictionaryDetailsService extends Service<DictionaryDetails> {\n    List<DictionaryContents> listWithKeyword(int id,String keyword);\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/EmployeeService.java",
    "content": "package com.msy.plus.service;\n\nimport com.msy.plus.entity.Employee;\nimport com.msy.plus.core.service.Service;\nimport com.msy.plus.entity.EmployeeDetail;\nimport com.msy.plus.entity.EmployeeWithRoleDO;\nimport java.util.List;\n\n/**\n* @author MoShuYing\n* @date 2021/05/15\n*/\npublic interface EmployeeService extends Service<Employee> {\n    EmployeeDetail getDetailById(Long id);\n\n    /**\n     * 分页查询员工\n     * @return\n     */\n    List<EmployeeWithRoleDO> listEmployeeWithRole(String keyword,Integer dept);\n\n    /**\n     * 保存员工角色信息\n     * @param id\n     * @param roles\n     * @return\n     */\n    void saveRoles(Long id,List<Long> roles);\n\n    /**\n     * 删除员工权限\n     * @param id\n     * @return\n     */\n    int deleteEmployeeWithRole(Long id);\n    int deleteEmployeeWithRoleItem(Long id,Long roleId);\n\n    /**\n     * 获取所有中间表的id\n     * @param id\n     * @return List<RolePermissionDO>\n     */\n    List<Long> getAllEmployeeRoleTableRow(Long id);\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/PermissionService.java",
    "content": "package com.msy.plus.service;\n\nimport com.msy.plus.entity.Permission;\nimport com.msy.plus.core.service.Service;\n\n/**\n* @author MoShuYing\n* @date 2021/05/14\n*/\npublic interface PermissionService extends Service<Permission> {\n\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/RoleService.java",
    "content": "package com.msy.plus.service;\n\nimport com.msy.plus.core.service.Service;\nimport com.msy.plus.dto.RoleDTO;\nimport com.msy.plus.entity.RoleDO;\nimport com.msy.plus.entity.RolePermissionDO;\nimport com.msy.plus.entity.RoleWithPermissionDO;\n\nimport java.util.List;\n\n/**\n * @author MoShuying\n * @date 2018/05/27\n */\npublic interface RoleService extends Service<RoleDO> {\n  /**\n   * 赋予默认角色给账户\n   *\n   * @param accountId 账户Id\n   */\n  void saveAsDefaultRole(Long accountId);\n\n  /**\n   * 保存角色\n   *\n   * @param roleDTO 角色传输实体\n   */\n  void save(RoleDTO roleDTO);\n\n  /**\n   * 更新角色\n   *\n   * @param roleDTO 角色传输实体\n   */\n  void update(RoleDTO roleDTO);\n\n  /**\n   * 获取角色信息并查询角色权限\n   * @param id\n   * @return\n   */\n  RoleWithPermissionDO getDetailById(Long id);\n\n  /**\n   * 保存用户权限\n   * @param permissions\n   */\n  void savePermissions(Long roleId,List<Long> permissions);\n\n  /**\n   * 删除中间表信息\n   * @param roleId\n   * @param permissionId\n   */\n  void deleteRolePermissionItem(Long roleId,Long permissionId);\n\n  /**\n   * 获取所有权限表中的字段\n   * @param roleId\n   * @return List<RolePermissionDO>\n   */\n  List<RolePermissionDO> getAllRolePermissionTableRow(Long roleId);\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/impl/AccountServiceImpl.java",
    "content": "package com.msy.plus.service.impl;\n\nimport com.msy.plus.core.response.ResultCode;\nimport com.msy.plus.core.service.AbstractService;\nimport com.msy.plus.dto.AccountDTO;\nimport com.msy.plus.entity.AccountDO;\nimport com.msy.plus.entity.AccountWithRoleDO;\nimport com.msy.plus.mapper.AccountMapper;\nimport com.msy.plus.query.AccountQuery;\nimport com.msy.plus.service.AccountService;\nimport com.msy.plus.service.RoleService;\nimport com.msy.plus.util.AssertUtils;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.security.crypto.password.PasswordEncoder;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\nimport tk.mybatis.mapper.entity.Condition;\n\nimport javax.annotation.Resource;\nimport java.util.Optional;\n\n/**\n * @author MoShuying\n * @date 2018/05/27\n */\n@Slf4j\n@Service\n@Transactional(rollbackFor = Exception.class)\npublic class AccountServiceImpl extends AbstractService<AccountDO> implements AccountService {\n  @Resource private AccountMapper accountMapper;\n  @Resource private RoleService roleService;\n  @Resource private PasswordEncoder passwordEncoder;\n\n  /** 重写 save 方法，密码加密后再存，并且赋予默认角色 */\n  @Override\n  public void save(final AccountDTO accountDTO) {\n    AssertUtils.asserts(\n        !Optional.ofNullable(this.getBy(\"name\", accountDTO.getName())).isPresent(),\n        ResultCode.DUPLICATE_NAME);\n\n    final AccountDO accountDO = accountDTO.convertToDO();\n\n    accountDO.setPassword(this.passwordEncoder.encode(accountDTO.getPassword().trim()));\n    this.save(accountDO);\n    log.debug(\"==> Create Account<{}> Id<{}>\", accountDO.getName(), accountDO.getId());\n    // 新建账户默认角色\n    this.roleService.saveAsDefaultRole(accountDO.getId());\n  }\n\n  @Override\n  public void updateByName(final AccountDTO accountDTO) {\n    final AccountDO accountDO = accountDTO.convertToDO();\n    // 如果修改了密码\n    if (StringUtils.isNotBlank(accountDTO.getPassword())) {\n      // 密码修改后需要加密\n      accountDO.setPassword(this.passwordEncoder.encode(accountDTO.getPassword().trim()));\n    }\n    // 不能修改账户名\n    final String name = accountDO.getName();\n    accountDO.setName(null);\n    // 按 name 字段更新\n    final Condition condition = new Condition(AccountDO.class);\n    condition.createCriteria().andCondition(\"name = \", name);\n    this.updateByCondition(accountDO, condition);\n  }\n\n  @Override\n  public AccountWithRoleDO getByIdWithRole(final Long id) {\n    final AccountQuery accountQuery = AccountQuery.builder().id(id).build();\n    return this.accountMapper.getByQueryWithRole(accountQuery);\n  }\n\n  @Override\n  public AccountWithRoleDO getByNameWithRole(final String name) {\n    final AccountQuery accountQuery = AccountQuery.builder().name(name).build();\n    return this.accountMapper.getByQueryWithRole(accountQuery);\n  }\n\n  @Override\n  public boolean verifyPassword(final String rawPassword, final String encodedPassword) {\n    return this.passwordEncoder.matches(rawPassword, encodedPassword);\n  }\n\n  @Override\n  public boolean updateLoginTimeByName(final String name) {\n    final boolean success = this.accountMapper.updateLoginTimeByName(name) == 1;\n    if (!success) {\n      log.error(\"==> Update Account<{}> login time error\", name);\n    }\n    return success;\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/impl/CustomerFollowUpHistoryServiceImpl.java",
    "content": "package com.msy.plus.service.impl;\n\nimport com.msy.plus.entity.CFUHSearch;\nimport com.msy.plus.mapper.CustomerFollowUpHistoryMapper;\nimport com.msy.plus.entity.CustomerFollowUpHistory;\nimport com.msy.plus.service.CustomerFollowUpHistoryService;\nimport com.msy.plus.core.service.AbstractService;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport javax.annotation.Resource;\nimport java.sql.Timestamp;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n* @author MoShuYing\n* @date 2021/05/21\n*/\n@Service\n@Transactional(rollbackFor = Exception.class)\npublic class CustomerFollowUpHistoryServiceImpl extends AbstractService<CustomerFollowUpHistory> implements CustomerFollowUpHistoryService {\n    @Resource\n    private CustomerFollowUpHistoryMapper customerFollowUpHistoryMapper;\n\n\n    @Override\n    public List<CFUHSearch> listAndSearch(String keyword, Date startTime, Date endTime, Integer type) {\n        String st=null,et=null;\n        if(startTime!=null){\n            st=new Timestamp(startTime.getTime()).toString();\n        }\n        if(endTime!=null){\n            et=new Timestamp(endTime.getTime()).toString();\n        }\n        return this.customerFollowUpHistoryMapper.listAndSearch(keyword, st,et, type);\n    }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/impl/CustomerHandoverServiceImpl.java",
    "content": "package com.msy.plus.service.impl;\n\nimport com.msy.plus.dto.CustomerHandoverList;\nimport com.msy.plus.mapper.CustomerHandoverMapper;\nimport com.msy.plus.entity.CustomerHandover;\nimport com.msy.plus.service.CustomerHandoverService;\nimport com.msy.plus.core.service.AbstractService;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport javax.annotation.Resource;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n* @author MoShuYing\n* @date 2021/05/21\n*/\n@Service\n@Transactional(rollbackFor = Exception.class)\npublic class CustomerHandoverServiceImpl extends AbstractService<CustomerHandover> implements CustomerHandoverService {\n    @Resource\n    private CustomerHandoverMapper customerHandoverMapper;\n\n    @Override\n    public List<CustomerHandoverList> listAndSearch(String keyword, Date startTime, Date endTime) {\n        return this.customerHandoverMapper.listAndSearch(keyword, startTime, endTime);\n    }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/impl/CustomerManagerServiceImpl.java",
    "content": "package com.msy.plus.service.impl;\n\nimport com.msy.plus.dto.AnalysisQuery;\nimport com.msy.plus.dto.CustomerManagerList;\nimport com.msy.plus.entity.Analysis;\nimport com.msy.plus.mapper.CustomerManagerMapper;\nimport com.msy.plus.entity.CustomerManager;\nimport com.msy.plus.service.CustomerManagerService;\nimport com.msy.plus.core.service.AbstractService;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport javax.annotation.Resource;\nimport java.util.List;\n\n/**\n* @author MoShuYing\n* @date 2021/05/20\n*/\n@Service\n@Transactional(rollbackFor = Exception.class)\npublic class CustomerManagerServiceImpl extends AbstractService<CustomerManager> implements CustomerManagerService {\n    @Resource\n    private CustomerManagerMapper customerManagerMapper;\n\n    @Override\n    public CustomerManager getById(Object id) {\n        return this.customerManagerMapper.getDetailById(id);\n    }\n\n    @Override\n    public List<CustomerManagerList> listAllWithDictionary(String keyword, Integer status) {\n        return customerManagerMapper.listAllWithDictionary(keyword,status);\n    }\n\n    @Override\n    public List<Analysis> queryAnalysis(AnalysisQuery analysisQuery) {\n        if(analysisQuery.getName()==null){\n            analysisQuery.setName(\"\");\n        }\n        return customerManagerMapper.queryAnalysis(analysisQuery);\n    }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/impl/DepartmentServiceImpl.java",
    "content": "package com.msy.plus.service.impl;\n\nimport com.msy.plus.mapper.DepartmentMapper;\nimport com.msy.plus.entity.Department;\nimport com.msy.plus.service.DepartmentService;\nimport com.msy.plus.core.service.AbstractService;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport javax.annotation.Resource;\n\n/**\n* @author MoShuYing\n* @date 2021/05/12\n*/\n@Service\n@Transactional(rollbackFor = Exception.class)\npublic class DepartmentServiceImpl extends AbstractService<Department> implements DepartmentService {\n    @Resource\n    private DepartmentMapper departmentMapper;\n\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/impl/DictionaryContentsServiceImpl.java",
    "content": "package com.msy.plus.service.impl;\n\nimport com.msy.plus.mapper.DictionaryContentsMapper;\nimport com.msy.plus.entity.DictionaryContents;\nimport com.msy.plus.service.DictionaryContentsService;\nimport com.msy.plus.core.service.AbstractService;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport javax.annotation.Resource;\nimport java.util.List;\n\n/**\n* @author MoShuYing\n* @date 2021/05/18\n*/\n@Service\n@Transactional(rollbackFor = Exception.class)\npublic class DictionaryContentsServiceImpl extends AbstractService<DictionaryContents> implements DictionaryContentsService {\n    @Resource\n    private DictionaryContentsMapper dictionaryContentsMapper;\n\n    @Override\n    public List<DictionaryContents> listWithKeyword(String keyword) {\n        return dictionaryContentsMapper.listWithKeyword(keyword);\n    }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/impl/DictionaryDetailsServiceImpl.java",
    "content": "package com.msy.plus.service.impl;\n\nimport com.msy.plus.entity.DictionaryContents;\nimport com.msy.plus.mapper.DictionaryDetailsMapper;\nimport com.msy.plus.entity.DictionaryDetails;\nimport com.msy.plus.service.DictionaryDetailsService;\nimport com.msy.plus.core.service.AbstractService;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport javax.annotation.Resource;\nimport java.util.List;\n\n/**\n* @author MoShuYing\n* @date 2021/05/18\n*/\n@Service\n@Transactional(rollbackFor = Exception.class)\npublic class DictionaryDetailsServiceImpl extends AbstractService<DictionaryDetails> implements DictionaryDetailsService {\n    @Resource\n    private DictionaryDetailsMapper dictionaryDetailsMapper;\n\n    @Override\n    public List<DictionaryContents> listWithKeyword(int id,String keyword) {\n        return dictionaryDetailsMapper.listWithKeyword(id,keyword);\n    }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/impl/EmployeeServiceImpl.java",
    "content": "package com.msy.plus.service.impl;\n\nimport com.msy.plus.entity.EmployeeDetail;\nimport com.msy.plus.entity.EmployeeWithRoleDO;\nimport com.msy.plus.mapper.EmployeeMapper;\nimport com.msy.plus.entity.Employee;\nimport com.msy.plus.service.EmployeeService;\nimport com.msy.plus.core.service.AbstractService;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport javax.annotation.Resource;\nimport java.util.List;\n\n/**\n* @author MoShuYing\n* @date 2021/05/15\n*/\n@Service\n@Transactional(rollbackFor = Exception.class)\npublic class EmployeeServiceImpl extends AbstractService<Employee> implements EmployeeService {\n\n\n    @Resource\n    private EmployeeMapper employeeMapper;\n\n    @Override\n    public EmployeeDetail getDetailById(Long id) {\n        return this.employeeMapper.getDetailById(id);\n    }\n\n    @Override\n    public List<EmployeeWithRoleDO> listEmployeeWithRole(String keyword,Integer dept){\n        if(dept==null){ dept=0; }\n        return this.employeeMapper.listEmployeeWithRole(keyword,dept);\n    }\n\n    @Override\n    public void saveRoles(Long id, List<Long> roles) {\n        this.employeeMapper.saveRoles(id,roles);\n    }\n\n    @Override\n    public int deleteEmployeeWithRole(Long id) {\n        return this.employeeMapper.deleteEmployeeWithRole(id);\n    }\n\n    @Override\n    public int deleteEmployeeWithRoleItem(Long id, Long roleId) {\n        return this.employeeMapper.deleteEmployeeWithRoleItem(id,roleId);\n    }\n\n    @Override\n    public List<Long> getAllEmployeeRoleTableRow(Long id) {\n        return this.employeeMapper.getAllEmployeeRoleTableRow(id);\n    }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/impl/PermissionServiceImpl.java",
    "content": "package com.msy.plus.service.impl;\n\nimport com.msy.plus.mapper.PermissionMapper;\nimport com.msy.plus.entity.Permission;\nimport com.msy.plus.service.PermissionService;\nimport com.msy.plus.core.service.AbstractService;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport javax.annotation.Resource;\n\n/**\n* @author MoShuYing\n* @date 2021/05/14\n*/\n@Service\n@Transactional(rollbackFor = Exception.class)\npublic class PermissionServiceImpl extends AbstractService<Permission> implements PermissionService {\n    @Resource\n    private PermissionMapper permissionMapper;\n\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/impl/RoleServiceImpl.java",
    "content": "package com.msy.plus.service.impl;\n\nimport com.msy.plus.core.response.ResultCode;\nimport com.msy.plus.core.service.AbstractService;\nimport com.msy.plus.dto.RoleDTO;\nimport com.msy.plus.entity.RoleDO;\nimport com.msy.plus.entity.RolePermissionDO;\nimport com.msy.plus.entity.RoleWithPermissionDO;\nimport com.msy.plus.mapper.RoleMapper;\nimport com.msy.plus.service.RoleService;\nimport com.msy.plus.util.AssertUtils;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport javax.annotation.Resource;\nimport java.util.List;\n\n/**\n * @author MoShuying\n * @date 2018/05/27\n */\n@Service\n@Transactional(rollbackFor = Exception.class)\npublic class RoleServiceImpl extends AbstractService<RoleDO> implements RoleService {\n  @Resource private RoleMapper roleMapper;\n\n  @Override\n  public void saveAsDefaultRole(final Long accountId) {\n    final boolean success = this.roleMapper.saveAsDefaultRole(accountId) == 1;\n    AssertUtils.asserts(success, ResultCode.SAVE_FAILED, \"账户默认角色保存失败\");\n  }\n\n  @Override\n  public void save(final RoleDTO roleDTO) {\n    final RoleDO role = roleDTO.convertToDO();\n    this.save(role);\n  }\n\n  @Override\n  public void update(final RoleDTO roleDTO) {\n    final RoleDO role = roleDTO.convertToDO();\n    this.update(role);\n  }\n\n  @Override\n  public RoleWithPermissionDO getDetailById(Long id) {\n    return this.roleMapper.getDetailById(id);\n  }\n\n  @Override\n  public void savePermissions(Long roleId,List<Long> permissions) {\n    this.roleMapper.savePermissions(roleId,permissions);\n//    AssertUtils.asserts(success, ResultCode.SAVE_FAILED, \"账户角色权限保存失败\");\n  }\n\n  @Override\n  public void deleteRolePermissionItem(Long roleId, Long permissionId) {\n    this.roleMapper.deleteRolePermissionItem(roleId,permissionId);\n  }\n\n  @Override\n  public List<RolePermissionDO> getAllRolePermissionTableRow(Long roleId) {\n    return this.roleMapper.getAllRolePermissionTableRow(roleId);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/service/impl/UserDetailsServiceImpl.java",
    "content": "package com.msy.plus.service.impl;\n\nimport com.msy.plus.core.exception.UsernameNotFoundException2;\nimport com.msy.plus.entity.AccountWithRoleDO;\nimport com.msy.plus.service.AccountService;\nimport org.springframework.security.core.authority.SimpleGrantedAuthority;\nimport org.springframework.security.core.userdetails.UserDetails;\nimport org.springframework.security.core.userdetails.UserDetailsService;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport javax.annotation.Resource;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * @author MoShuying\n * @date 2018/05/27\n */\n@Service\n@Transactional(rollbackFor = Exception.class)\npublic class UserDetailsServiceImpl implements UserDetailsService {\n  @Resource private AccountService accountService;\n\n  @Override\n  public UserDetails loadUserByUsername(final String name) throws UsernameNotFoundException2 {\n    final AccountWithRoleDO account = this.accountService.getByNameWithRole(name);\n    Optional.ofNullable(account).orElseThrow(UsernameNotFoundException2::new);\n    final List<SimpleGrantedAuthority> authorities = new ArrayList<>();\n    account\n        .getRoles()\n        .forEach(roleDO -> authorities.add(new SimpleGrantedAuthority(roleDO.getName())));\n    return new org.springframework.security.core.userdetails.User(\n        account.getName(), account.getPassword(), authorities);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/util/AssertUtils.java",
    "content": "package com.msy.plus.util;\n\nimport com.msy.plus.core.exception.ServiceException;\nimport com.msy.plus.core.response.ResultCode;\n\n/**\n * 断言工具\n *\n * @author MoShuying\n * @date 2018/11/29\n */\npublic class AssertUtils {\n  public static void throwIf(\n      final boolean statement, final ResultCode resultCode, final String message) {\n    if (statement) {\n      throw toThrow(resultCode, message);\n    }\n  }\n\n  public static void throwIf(\n      final boolean statement, final ResultCode resultCode, final Object... messages) {\n    throwIf(statement, resultCode, resultCode.format(messages));\n  }\n\n  public static RuntimeException toThrow(final ResultCode resultCode, final Object... messages) {\n    return new ServiceException(resultCode, resultCode.format(messages));\n  }\n\n  public static void asserts(\n      final boolean statement, final ResultCode resultCode, final Object... messages) {\n    throwIf(!statement, resultCode, messages);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/util/ContextUtils.java",
    "content": "package com.msy.plus.util;\n\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n * 上下文工具\n *\n * @author MoShuying\n * @date 2018/07/20\n */\npublic class ContextUtils {\n  private ContextUtils() {}\n\n  /**\n   * 获取 request\n   *\n   * @return request\n   */\n  public static HttpServletRequest getRequest() {\n    final ServletRequestAttributes attributes =\n        ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());\n    return attributes == null ? null : attributes.getRequest();\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/util/DateUtils.java",
    "content": "package com.msy.plus.util;\n\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.time.temporal.ChronoUnit;\n\n/**\n * 线程安全的日期工具\n *\n * @author MoShuying\n * @date 2018/07/20\n */\npublic class DateUtils {\n  private static final DateTimeFormatter DTF_YEAR = DateTimeFormatter.ofPattern(\"yyyy\");\n  private static final DateTimeFormatter DTF_DAY = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\");\n  private static final DateTimeFormatter DTF_DAYS = DateTimeFormatter.ofPattern(\"yyyyMMdd\");\n  private static final DateTimeFormatter DTF_TIME =\n      DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss\");\n  private static final DateTimeFormatter DTF_TIMES = DateTimeFormatter.ofPattern(\"yyyyMMddHHmmss\");\n\n  private DateUtils() {}\n\n  /**\n   * 现在时间（年）\n   *\n   * @return DTF_YEAR\n   */\n  public static String getThisYear() {\n    return DTF_YEAR.format(LocalDateTime.now());\n  }\n\n  /**\n   * 校验时间（年）是否合法\n   *\n   * @param dateString 时间字符串\n   * @return Boolean\n   */\n  public static Boolean validateYear(final String dateString) {\n    return validate(dateString, DTF_YEAR);\n  }\n\n  /**\n   * 现在时间（天）\n   *\n   * @return DTF_DAY\n   */\n  public static String getThisDay() {\n    return DTF_DAY.format(LocalDateTime.now());\n  }\n\n  /**\n   * 校验时间（天）是否合法\n   *\n   * @param dateString 时间字符串\n   * @return Boolean\n   */\n  public static Boolean validateDay(final String dateString) {\n    return validate(dateString, DTF_DAY);\n  }\n\n  /**\n   * 现在时间（天）\n   *\n   * @return DTF_DAYS\n   */\n  public static String getThisDays() {\n    return DTF_DAYS.format(LocalDateTime.now());\n  }\n\n  /**\n   * 校验时间（天）是否合法\n   *\n   * @param dateString 时间字符串\n   * @return Boolean\n   */\n  public static Boolean validateDays(final String dateString) {\n    return validate(dateString, DTF_DAYS);\n  }\n\n  /**\n   * 现在时间（秒）\n   *\n   * @return DTF_TIME\n   */\n  public static String getThisTime() {\n    return DTF_TIME.format(LocalDateTime.now());\n  }\n\n  /**\n   * 校验时间（秒）是否合法\n   *\n   * @param dateString 时间字符串\n   * @return Boolean\n   */\n  public static Boolean validateTime(final String dateString) {\n    return validate(dateString, DTF_TIME);\n  }\n\n  /**\n   * 现在时间（秒）\n   *\n   * @return DTF_TIMES\n   */\n  public static String getThisTimes() {\n    return DTF_TIMES.format(LocalDateTime.now());\n  }\n\n  /**\n   * 校验时间（秒）是否合法\n   *\n   * @param dateString 时间字符串\n   * @return Boolean\n   */\n  public static Boolean validateTimes(final String dateString) {\n    return validate(dateString, DTF_TIMES);\n  }\n\n  /**\n   * 校验日期是否合法\n   *\n   * @param dateString 时间字符串\n   * @param dateTimeFormatString 时间格式字符串\n   * @return Boolean\n   */\n  public static Boolean validate(final String dateString, final String dateTimeFormatString) {\n    final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormatString);\n    try {\n      LocalDateTime.parse(dateString, dateTimeFormatter);\n      return true;\n    } catch (final Exception e) {\n      return false;\n    }\n  }\n\n  /**\n   * 校验日期是否合法\n   *\n   * @param dateString 时间字符串\n   * @param dateTimeFormatter 时间格式器\n   * @return Boolean\n   */\n  public static Boolean validate(\n      final String dateString, final DateTimeFormatter dateTimeFormatter) {\n    try {\n      LocalDateTime.parse(dateString, dateTimeFormatter);\n      return true;\n    } catch (final Exception e) {\n      return false;\n    }\n  }\n\n  /**\n   * 比较两个时间的大小\n   *\n   * @param dateString1 时间字符串1\n   * @param dateString2 时间字符串2\n   * @param dateTimeFormatString 时间格式\n   * @return -1:时间1小于时间2 | 0:时间1等于时间2 | 1:时间1大于时间2\n   */\n  public static Integer compare(\n      final String dateString1, final String dateString2, final String dateTimeFormatString) {\n    final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormatString);\n    final LocalDateTime dateTime1 = LocalDateTime.parse(dateString1, dateTimeFormatter);\n    final LocalDateTime dateTime2 = LocalDateTime.parse(dateString2, dateTimeFormatter);\n    if (dateTime1.isBefore(dateTime2)) {\n      return -1;\n    } else if (dateTime1.equals(dateTime2)) {\n      return 0;\n    } else {\n      return 1;\n    }\n  }\n\n  /**\n   * 在原时间上增加x个时间单位\n   *\n   * @param dateString 时间字符串\n   * @param x x个时间单位\n   * @param chronoUnit 时间单位\n   * @param dateTimeFormatString 时间格式\n   * @return 增加后的时间\n   */\n  public static String add(\n      final String dateString,\n      final Long x,\n      final ChronoUnit chronoUnit,\n      final String dateTimeFormatString) {\n    final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormatString);\n    final LocalDateTime dateTime = LocalDateTime.parse(dateString, dateTimeFormatter);\n    final LocalDateTime newTime = dateTime.plus(x, chronoUnit);\n    return dateTimeFormatter.format(newTime);\n  }\n\n  /**\n   * 增加x小时\n   *\n   * @param dateString 时间字符串\n   * @param hours 小时\n   * @param dateTimeFormatString 时间格式\n   * @return 增加后的时间\n   */\n  public static String addHours(\n      final String dateString, final Long hours, final String dateTimeFormatString) {\n    return add(dateString, hours, ChronoUnit.HOURS, dateTimeFormatString);\n  }\n\n  /**\n   * 增加x分钟\n   *\n   * @param dateString 时间字符串\n   * @param minutes 分钟\n   * @param dateTimeFormatString 时间格式\n   * @return 增加后的时间\n   */\n  public static String addMinutes(\n      final String dateString, final Long minutes, final String dateTimeFormatString) {\n    return add(dateString, minutes, ChronoUnit.MINUTES, dateTimeFormatString);\n  }\n\n  /**\n   * 增加x秒\n   *\n   * @param dateString 时间字符串\n   * @param seconds 秒\n   * @param dateTimeFormatString 时间格式\n   * @return 增加后的时间\n   */\n  public static String addSeconds(\n      final String dateString, final Long seconds, final String dateTimeFormatString) {\n    return add(dateString, seconds, ChronoUnit.SECONDS, dateTimeFormatString);\n  }\n\n  /**\n   * 是否为闰年\n   *\n   * @param dateString 时间字符串\n   * @param dateTimeFormatString 时间格式字符串\n   * @return Boolean\n   */\n  public static Boolean isLeapYear(final String dateString, final String dateTimeFormatString) {\n    final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormatString);\n    final LocalDate dateTime = LocalDate.parse(dateString, dateTimeFormatter);\n    return dateTime.isLeapYear();\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/util/FileUtils.java",
    "content": "package com.msy.plus.util;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class FileUtils {\n  /** 常见文件头信息 */\n  public static final Map<String, String> FILE_TYPE_MAP =\n      new HashMap<String, String>() {\n        private static final long serialVersionUID = 2413557023486330089L;\n\n        {\n          this.put(\"jpg\", \"FFD8FF\"); // JPEG\n          this.put(\"png\", \"89504E47\"); // PNG\n          this.put(\"gif\", \"47494638\"); // GIF\n          this.put(\"tif\", \"49492A00\"); // TIFF\n          this.put(\"bmp\", \"424D\"); // Windows Bitmap\n          this.put(\"dwg\", \"41433130\"); // CAD\n          this.put(\"html\", \"68746D6C3E\"); // HTML\n          this.put(\"rtf\", \"7B5C727466\"); // Rich Text Format\n          this.put(\"xml\", \"3C3F786D6C\");\n          this.put(\"zip\", \"504B0304\"); // ZIP Archive\n          this.put(\"rar\", \"52617221\"); // RAR Archive\n          this.put(\"7z\", \"377ABCAF271C\"); // 7z Archive\n          this.put(\"psd\", \"38425053\"); // PhotoShop\n          this.put(\"eml\", \"44656C69766572792D646174653A\"); // Email [thorough only]\n          this.put(\"dbx\", \"CFAD12FEC5FD746F\"); // Outlook Express\n          this.put(\"pst\", \"2142444E\"); // Outlook\n          this.put(\"xls\", \"D0CF11E0\"); // MS Word\n          this.put(\"doc\", \"D0CF11E0\"); // MS Excel 注意：word 和 excel的文件头一样\n          this.put(\"mdb\", \"5374616E64617264204A\"); // MS Access\n          this.put(\"wpd\", \"FF575043\"); // WordPerfect\n          this.put(\"eps\", \"252150532D41646F6265\");\n          this.put(\"ps\", \"252150532D41646F6265\");\n          this.put(\"pdf\", \"255044462D312E\"); // Adobe Acrobat\n          this.put(\"qdf\", \"AC9EBD8F\"); // Quicken\n          this.put(\"pwl\", \"E3828596\"); // Windows Password\n          this.put(\"wav\", \"57415645\"); // Wave\n          this.put(\"avi\", \"41564920\");\n          this.put(\"ram\", \"2E7261FD\"); // Real Audio\n          this.put(\"rm\", \"2E524D46\"); // Real Media\n          this.put(\"mpg\", \"000001BA\"); // Moving Pictures Experts Group\n          this.put(\"mov\", \"6D6F6F76\"); // QuickTime\n          this.put(\"asf\", \"3026B2758E66CF11\"); // Windows Media\n          this.put(\"mid\", \"4D546864\"); // MIDI\n        }\n      };\n\n  private FileUtils() {}\n\n  /**\n   * 获取文件类型\n   *\n   * @param file 文件\n   * @return 文件类型\n   */\n  public static String getFileType(final File file) {\n    String fileType = null;\n    final byte[] b = new byte[50];\n    try {\n      final InputStream is = new FileInputStream(file);\n      is.read(b);\n      fileType = FileUtils.getFileType(b);\n      is.close();\n    } catch (final IOException e) {\n      e.printStackTrace();\n    }\n    return fileType;\n  }\n\n  /**\n   * 获取文件类型\n   *\n   * @param fileBytes 文件二进制数据\n   * @return 文件类型\n   */\n  public static String getFileType(final byte[] fileBytes) {\n    final String fileTypeHex = String.valueOf(FileUtils.getFileHexString(fileBytes));\n    for (final Map.Entry<String, String> entry : FileUtils.FILE_TYPE_MAP.entrySet()) {\n      final String fileTypeHexValue = entry.getValue();\n      if (fileTypeHex.toUpperCase().startsWith(fileTypeHexValue)) {\n        return entry.getKey();\n      }\n    }\n    return null;\n  }\n\n  public static String getFileHexString(final byte[] b) {\n    final StringBuilder stringBuilder = new StringBuilder();\n    if (b == null || b.length <= 0) {\n      return null;\n    }\n    for (final byte value : b) {\n      final int v = value & 0xFF;\n      final String hv = Integer.toHexString(v);\n      if (hv.length() < 2) {\n        stringBuilder.append(0);\n      }\n      stringBuilder.append(hv);\n    }\n    return stringBuilder.toString();\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/util/IdCardUtils.java",
    "content": "package com.msy.plus.util;\n\nimport lombok.extern.slf4j.Slf4j;\n\nimport javax.validation.constraints.NotBlank;\nimport java.time.LocalDate;\nimport java.time.format.DateTimeFormatter;\n\n/**\n * 二代身份证工具\n *\n * <p>【中华人民共和国国家标准GB11643-1999】关于公民身份号码的规定： 公民身份号码是特征组合码，由十七位数字本体码和一位数字校验码组成。\n *\n * <p>排列顺序从左至右依次为：6位数字地址码，8位数字出生日期码，3位数字顺序码和1位数字校验码。\n *\n * <p>1-2位数字：所在省份的代码；\n *\n * <p>3-4位数字：所在城市的代码；\n *\n * <p>5-6位数字：所在区县的代码；\n *\n * <p>7-14位数字：出生xxxx年xx月xx日；\n *\n * <p>15-16位数字：所在地的派出所的代码；\n *\n * <p>17位数字表示性别：奇数表示男性，偶数表示女性； （在同一地址码所标识的区域范围内，对同年、同月、同 日出生的人编定的顺序号，顺序码的奇数分配给男性，偶数分配给女性。）\n *\n * <p>18位数字是校检码：检验身份证的正确性。校检码可以是数字0~9和字符X。\n *\n * <p>校验码的计算方法为：\n *\n * <p>1.将前面的身份证号码17位数分别乘以不同的系数。从第1位到第17位的系数分别为：7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2；\n *\n * <p>2.将这17位数字和系数相乘的结果相加；\n *\n * <p>3.用加出来和除以11，看余数是多少；\n *\n * <p>4.余数只可能有0 1 2 3 4 5 6 7 8 9 10这11个数字。其分别对应的最后一位身份证的号码为1 0 X 9 8 7 6 5 4 3 2；\n * （比如余数是2，就会在身份证的第18位数字上出现罗马数字的Ⅹ。如果余数是10，身份证的最后一位号码就是2）\n *\n * @author MoShuying\n * @date 2018/11/27\n */\n@Slf4j\npublic class IdCardUtils {\n  /** 省、直辖市代码表 */\n  private static final String AREA_CODE[][] = {\n    {\"11\", \"北京\"},\n    {\"12\", \"天津\"},\n    {\"13\", \"河北\"},\n    {\"14\", \"山西\"},\n    {\"15\", \"内蒙古\"},\n    {\"21\", \"辽宁\"},\n    {\"22\", \"吉林\"},\n    {\"23\", \"黑龙江\"},\n    {\"31\", \"上海\"},\n    {\"32\", \"江苏\"},\n    {\"33\", \"浙江\"},\n    {\"34\", \"安徽\"},\n    {\"35\", \"福建\"},\n    {\"36\", \"江西\"},\n    {\"37\", \"山东\"},\n    {\"41\", \"河南\"},\n    {\"42\", \"湖北\"},\n    {\"43\", \"湖南\"},\n    {\"44\", \"广东\"},\n    {\"45\", \"广西\"},\n    {\"46\", \"海南\"},\n    {\"50\", \"重庆\"},\n    {\"51\", \"四川\"},\n    {\"52\", \"贵州\"},\n    {\"53\", \"云南\"},\n    {\"54\", \"西藏\"},\n    {\"61\", \"陕西\"},\n    {\"62\", \"甘肃\"},\n    {\"63\", \"青海\"},\n    {\"64\", \"宁夏\"},\n    {\"65\", \"新疆\"},\n    {\"71\", \"台湾\"},\n    {\"81\", \"香港\"},\n    {\"82\", \"澳门\"},\n    {\"91\", \"国外\"}\n  };\n\n  /** 最后一位校验码 */\n  private static final String[] LAST_CODE = {\"1\", \"0\", \"X\", \"9\", \"8\", \"7\", \"6\", \"5\", \"4\", \"3\", \"2\"};\n\n  /** 每位加权因子 */\n  private static final int[] POWER = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};\n\n  private IdCardUtils() {}\n\n  public static void main(final String[] args) {\n    System.out.println(getProvince(\"44532319960516121a\", false));\n  }\n\n  /**\n   * 判断二代身份证合法性\n   *\n   * @param idCard 身份证\n   * @return Boolean\n   */\n  public static Boolean validate(@NotBlank final String idCard) {\n    // 18位长度\n    if (idCard.length() != 18) {\n      log.error(\"二代身份证长度错误\");\n      return false;\n    }\n\n    // 前17位全为数字\n    final String idCard17 = idCard.substring(0, 17);\n    if (!isDigital(idCard17)) {\n      log.error(\"前17位不全为数字\");\n      return false;\n    }\n\n    // 校验地区\n    if (getProvince(idCard, false) == null) {\n      log.error(\"省份错误\");\n      return false;\n    }\n\n    // 校验生日\n    if (getBirthday(idCard, false) == null) {\n      log.error(\"生日错误\");\n      return false;\n    }\n\n    // 校验第18位\n    final String idCard18Code = idCard.substring(17, 18);\n    int powerSum = 0;\n    for (int i = 0; i < 17; i++) {\n      powerSum += Integer.parseInt(String.valueOf(idCard17.charAt(i))) * POWER[i];\n    }\n    // 将对权值和取11模得到余数\n    final String lastCode = LAST_CODE[powerSum % 11];\n    // 身份的第18位与算出来的校码进行匹配\n    if (!idCard18Code.equalsIgnoreCase(lastCode)) {\n      log.error(\"第18位错误\");\n      return false;\n    }\n    return true;\n  }\n\n  /**\n   * 判断是否全为数字\n   *\n   * @param string 字符串\n   * @return Boolean\n   */\n  private static Boolean isDigital(final String string) {\n    final char[] cs = string.toCharArray();\n    for (final char c : cs) {\n      if (48 > c || c > 57) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  /**\n   * 获取省份\n   *\n   * @param idCard 身份证\n   * @param toValidate 是否校验\n   * @return 省份\n   */\n  public static String getProvince(@NotBlank final String idCard, final boolean toValidate) {\n    if (toValidate && !validate(idCard)) {\n      return null;\n    }\n    final String provinceCode = getProvinceCode(idCard);\n    for (final String[] area : AREA_CODE) {\n      if (area[0].equals(provinceCode)) {\n        return area[1];\n      }\n    }\n    return null;\n  }\n\n  /**\n   * 获取生日 生日格式：yyyy-mm-dd\n   *\n   * @param idCard 身份证\n   * @param toValidate 是否校验\n   * @return 生日\n   */\n  public static String getBirthday(@NotBlank final String idCard, final boolean toValidate) {\n    if (toValidate && !validate(idCard)) {\n      return null;\n    }\n    try {\n      final String birthday = getBirthdayCode(idCard);\n      final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(\"yyyyMMdd\");\n      return LocalDate.parse(birthday, dateTimeFormatter).toString();\n    } catch (final Exception e) {\n      e.printStackTrace();\n    }\n    return null;\n  }\n\n  /**\n   * 获取性别\n   *\n   * @param idCard 身份证\n   * @param toValidate 是否校验\n   * @return 女 | 男\n   */\n  public static String getSex(@NotBlank final String idCard, final boolean toValidate) {\n    if (toValidate && !validate(idCard)) {\n      return null;\n    }\n    final String sex = getSexCode(idCard);\n    return (Integer.valueOf(sex) & 1) == 0 ? \"女\" : \"男\";\n  }\n\n  private static String getProvinceCode(@NotBlank final String idCard) {\n    return idCard.substring(0, 2);\n  }\n\n  private static String getBirthdayCode(@NotBlank final String idCard) {\n    return idCard.substring(6, 14);\n  }\n\n  private static String getSexCode(@NotBlank final String idCard) {\n    return idCard.substring(16, 17);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/util/IdUtils.java",
    "content": "package com.msy.plus.util;\n\nimport org.apache.commons.codec.binary.Base64;\nimport org.apache.commons.lang3.RandomStringUtils;\n\nimport java.nio.ByteBuffer;\nimport java.time.LocalDate;\nimport java.time.format.DateTimeFormatter;\nimport java.util.UUID;\n\n/**\n * ID工具\n *\n * @author MoShuying\n * @date 2018/05/27\n */\npublic class IdUtils {\n  private static final DateTimeFormatter DTF_TIMES = DateTimeFormatter.ofPattern(\"yyyyMMddHHmmss\");\n\n  private IdUtils() {}\n\n  public static String uuid16() {\n    return UUID.randomUUID().toString().replaceAll(\"-\", \"\").toLowerCase();\n  }\n\n  public static String uuid64() {\n    final UUID uuid = UUID.randomUUID();\n    final ByteBuffer bb = ByteBuffer.wrap(new byte[16]);\n    bb.putLong(uuid.getMostSignificantBits());\n    bb.putLong(uuid.getLeastSignificantBits());\n    return Base64.encodeBase64URLSafeString(bb.array());\n  }\n\n  public static String timeId() {\n    return DTF_TIMES.format(LocalDate.now()) + RandomStringUtils.randomNumeric(5);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/util/IpUtils.java",
    "content": "package com.msy.plus.util;\n\nimport com.alibaba.fastjson.JSON;\nimport com.alibaba.fastjson.JSONObject;\nimport org.springframework.util.StringUtils;\n\nimport javax.servlet.http.HttpServletRequest;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.HttpURLConnection;\nimport java.net.InetAddress;\nimport java.net.URL;\nimport java.net.UnknownHostException;\nimport java.util.Optional;\n\n/**\n * IP工具\n *\n * @author MoShuying\n * @date 2018/05/27\n */\npublic class IpUtils {\n  private static final String UNKNOWN = \"unknown\";\n  private static final String LOCALHOST_IPV4 = \"127.0.0.1\";\n  private static final String LOCALHOST_IPV6 = \"0:0:0:0:0:0:0:1\";\n\n  private IpUtils() {}\n\n  public static String getIpAddress() {\n    return getIpAddress(ContextUtils.getRequest());\n  }\n\n  /**\n   * 获取请求中的 ip 地址\n   *\n   * @param request request\n   * @return IP\n   */\n  public static String getIpAddress(final HttpServletRequest request) {\n    String ip = LOCALHOST_IPV4;\n    if (Optional.ofNullable(request).isPresent()) {\n      ip = request.getHeader(\"x-forwarded-for\");\n      if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {\n        ip = request.getHeader(\"Proxy-Client-IP\");\n      }\n      if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {\n        ip = request.getHeader(\"WL-Proxy-Client-IP\");\n      }\n      if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {\n        ip = request.getHeader(\"HTTP_CLIENT_IP\");\n      }\n      if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {\n        ip = request.getHeader(\"HTTP_X_FORWARDED_FOR\");\n      }\n      if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {\n        ip = request.getRemoteAddr();\n        // request.getRemoteAddr() 获取客户端的 IP 地址在大部分情况下都是有效的\n        // 但是在通过了 Apache，Squid 等反向代理软件就不能获取到客户端的真实 IP 地址\n        // 如果通过了多级反向代理的话 X-Forwarded-For 的值并不止一个\n        // 而是一串 IP 值，例如：192.168.1.110,192.168.1.120,192.168.1.130,192.168.1.100\n        // 其中第一个 192.168.1.110 才是用户真实的 IP\n        if (LOCALHOST_IPV4.equals(ip) || LOCALHOST_IPV6.equals(ip)) {\n          // 根据网卡取本机配置的 IP，而不是环回地址\n          try {\n            ip = InetAddress.getLocalHost().getHostAddress();\n          } catch (final UnknownHostException ignored) {\n          }\n        }\n      }\n      // 多个 IP 中取第一个\n      final String ch = \",\";\n      if (!StringUtils.isEmpty(ip) && ip.contains(ch)) {\n        ip = ip.substring(0, ip.indexOf(ch));\n      }\n    }\n    return ip;\n  }\n\n  /**\n   * 通过 IP 获取相关信息(需要联网，调用淘宝的IP库)\n   *\n   * @param ip ip\n   * @return IP相关信息\n   */\n  public static String getInfoByIP(final String ip) {\n    try {\n      final URL url = new URL(\"http://ip.taobao.com/service/getIpInfo.php?ip=\" + ip);\n      final HttpURLConnection connection = (HttpURLConnection) url.openConnection();\n      connection.setRequestMethod(\"GET\");\n      connection.setDoOutput(true);\n      connection.setDoInput(true);\n      connection.setUseCaches(false);\n\n      final InputStream in = connection.getInputStream();\n      final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));\n      final StringBuilder buffer = new StringBuilder();\n      String line = bufferedReader.readLine();\n      while (Optional.ofNullable(line).isPresent()) {\n        buffer.append(line).append(\"\\r\\n\");\n        line = bufferedReader.readLine();\n      }\n      bufferedReader.close();\n      final JSONObject obj = (JSONObject) JSON.parse(buffer.toString());\n      final StringBuilder info = new StringBuilder();\n      final int responseCode = obj.getIntValue(\"code\");\n      if (responseCode == 0) {\n        final JSONObject data = obj.getJSONObject(\"data\");\n        info.append(data.getString(\"country\")).append(\" \");\n        info.append(data.getString(\"region\")).append(\" \");\n        info.append(data.getString(\"city\")).append(\" \");\n        info.append(data.getString(\"isp\"));\n      }\n      return info.toString();\n    } catch (final IOException e) {\n      e.printStackTrace();\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/util/JsonUtils.java",
    "content": "package com.msy.plus.util;\n\nimport com.alibaba.fastjson.JSON;\nimport com.alibaba.fastjson.serializer.SimplePropertyPreFilter;\n\nimport java.util.Arrays;\n\n/**\n * Json工具\n *\n * @author MoShuying\n * @date 2018/07/11\n */\npublic class JsonUtils {\n  private JsonUtils() {}\n\n  /**\n   * 保留某些字段\n   *\n   * @param target 目标对象\n   * @param fields 字段\n   * @return 保留字段后的对象\n   */\n  public static <T> T keepFields(final Object target, final Class<T> clz, final String... fields) {\n    final SimplePropertyPreFilter filter = new SimplePropertyPreFilter();\n    filter.getIncludes().addAll(Arrays.asList(fields));\n    return done(target, clz, filter);\n  }\n\n  /**\n   * 去除某些字段\n   *\n   * @param target 目标对象\n   * @param fields 字段\n   * @return 去除字段后的对象\n   */\n  public static <T> T deleteFields(\n      final Object target, final Class<T> clz, final String... fields) {\n    final SimplePropertyPreFilter filter = new SimplePropertyPreFilter();\n    filter.getExcludes().addAll(Arrays.asList(fields));\n    return done(target, clz, filter);\n  }\n\n  private static <T> T done(\n      final Object target, final Class<T> clz, final SimplePropertyPreFilter filter) {\n    final String jsonString = JSON.toJSONString(target, filter);\n    return JSON.parseObject(jsonString, clz);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/util/RedisUtils.java",
    "content": "package com.msy.plus.util;\n\nimport org.springframework.data.redis.core.RedisTemplate;\nimport org.springframework.stereotype.Component;\n\nimport javax.annotation.Resource;\nimport javax.validation.constraints.NotBlank;\nimport java.time.Duration;\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * Redis工具\n *\n * @author MoShuying\n * @date 2018/05/27\n */\n@Component\npublic class RedisUtils {\n  @Resource private RedisTemplate<String, Object> redisTemplate;\n\n  // =============================common============================\n\n  /**\n   * 设置缓存失效时间\n   *\n   * @param key 键\n   * @param timeout 时间\n   * @return {Boolean}\n   */\n  public Boolean setExpire(@NotBlank final String key, @NotBlank final Duration timeout) {\n    if (timeout.getSeconds() > 0) {\n      return this.redisTemplate.expire(key, timeout.getSeconds(), TimeUnit.SECONDS);\n    }\n    return false;\n  }\n\n  /**\n   * 获取缓存失效时间\n   *\n   * @param key 键\n   * @return 时间（秒） 0为永久有效\n   */\n  public Long getExpire(@NotBlank final String key) {\n    return this.redisTemplate.getExpire(key, TimeUnit.SECONDS);\n  }\n\n  /**\n   * key 是否存在\n   *\n   * @param key 键\n   * @return {Boolean}\n   */\n  public Boolean hasKey(@NotBlank final String key) {\n    return this.redisTemplate.hasKey(key);\n  }\n\n  /**\n   * 删除缓存\n   *\n   * @param keys 键\n   */\n  public Boolean delete(@NotBlank final String... keys) {\n    return keys.length\n        == Optional.ofNullable(this.redisTemplate.delete(Arrays.asList(keys))).orElse(-1L);\n  }\n\n  // ============================String=============================\n\n  /**\n   * 获取普通缓存\n   *\n   * @param key 键\n   * @return 值\n   */\n  public Object getValue(@NotBlank final String key) {\n    return this.redisTemplate.opsForValue().get(key);\n  }\n\n  /**\n   * 设置普通缓存\n   *\n   * @param key 键\n   * @param value 值\n   */\n  public void setValue(@NotBlank final String key, @NotBlank final Object value) {\n    this.redisTemplate.opsForValue().set(key, value);\n  }\n\n  /**\n   * 设置普通缓存\n   *\n   * @param key 键\n   * @param value 值\n   * @param timeout 时间 小于等于0时将设为无限期\n   */\n  public void setValue(\n      @NotBlank final String key, @NotBlank final Object value, @NotBlank final Duration timeout) {\n    this.redisTemplate.opsForValue().set(key, value, timeout);\n  }\n\n  /**\n   * 递增\n   *\n   * @param key 键\n   * @param delta 要增加几（大于0）\n   * @return 加上指定值之后 key 的值\n   */\n  public Long incrementValue(@NotBlank final String key, @NotBlank final long delta) {\n    if (delta > 0) {\n      throw new RuntimeException(\"递增因子必须大于0\");\n    }\n    return this.redisTemplate.opsForValue().increment(key, delta);\n  }\n\n  /**\n   * 递减\n   *\n   * @param key 键\n   * @param delta 要减少几(小于0)\n   * @return 减少指定值之后 key 的值\n   */\n  public Long decrementValue(@NotBlank final String key, @NotBlank final long delta) {\n    if (delta < 0) {\n      throw new RuntimeException(\"递减因子必须大于0\");\n    }\n    return this.redisTemplate.opsForValue().increment(key, -delta);\n  }\n\n  // ================================Map=================================\n\n  /**\n   * HashGet\n   *\n   * @param key 键\n   * @param item 项\n   * @return 值\n   */\n  public Object getHash(@NotBlank final String key, @NotBlank final String item) {\n    return this.redisTemplate.opsForHash().get(key, item);\n  }\n\n  /**\n   * 获取hashKey对应的所有键值\n   *\n   * @param key 键\n   * @return 对应的多个键值\n   */\n  public Map<Object, Object> getHash(@NotBlank final String key) {\n    return this.redisTemplate.opsForHash().entries(key);\n  }\n\n  /**\n   * HashSet\n   *\n   * @param key 键\n   * @param map 对应多个键值\n   */\n  public void putHash(@NotBlank final String key, @NotBlank final Map<String, Object> map) {\n    this.redisTemplate.opsForHash().putAll(key, map);\n  }\n\n  /**\n   * HashSet 并设置时间\n   *\n   * @param key 键\n   * @param map 对应多个键值\n   * @param timeout 时间\n   */\n  public void putHash(\n      @NotBlank final String key,\n      @NotBlank final Map<String, Object> map,\n      @NotBlank final Duration timeout) {\n    this.redisTemplate.opsForHash().putAll(key, map);\n    this.setExpire(key, timeout);\n  }\n\n  /**\n   * 向一张hash表中放入数据,如果不存在将创建\n   *\n   * @param key 键\n   * @param item 项\n   * @param value 值\n   */\n  public void putHash(\n      @NotBlank final String key, @NotBlank final String item, @NotBlank final Object value) {\n    this.redisTemplate.opsForHash().put(key, item, value);\n  }\n\n  /**\n   * 向一张hash表中放入数据,如果不存在将创建\n   *\n   * @param key 键\n   * @param item 项\n   * @param value 值\n   * @param timeout 时间 注意:如果已存在的hash表有时间,这里将会替换原有的时间\n   */\n  public void putHash(\n      @NotBlank final String key,\n      @NotBlank final String item,\n      @NotBlank final Object value,\n      @NotBlank final Duration timeout) {\n    this.redisTemplate.opsForHash().put(key, item, value);\n    this.setExpire(key, timeout);\n  }\n\n  /**\n   * 删除hash表中的值\n   *\n   * @param key 键\n   * @param item 项\n   */\n  public void deleteHash(@NotBlank final String key, @NotBlank final Object... item) {\n    this.redisTemplate.opsForHash().delete(key, item);\n  }\n\n  /**\n   * 判断hash表中是否有该项的值\n   *\n   * @param key 键\n   * @param item 项\n   * @return {Boolean}\n   */\n  public Boolean hasKeyHash(@NotBlank final String key, @NotBlank final String item) {\n    return this.redisTemplate.opsForHash().hasKey(key, item);\n  }\n\n  /**\n   * hash递增 如果不存在,就会创建一个 并把新增后的值返回\n   *\n   * @param key 键\n   * @param item 项\n   * @param by 要增加几(大于0)\n   * @return 加上指定值之后 key 的值\n   */\n  public Double incrementHash(\n      @NotBlank final String key, @NotBlank final String item, @NotBlank final double by) {\n    return this.redisTemplate.opsForHash().increment(key, item, by);\n  }\n\n  /**\n   * hash递减\n   *\n   * @param key 键\n   * @param item 项\n   * @param by 要减少记(小于0)\n   * @return 减少指定值之后 key 的值\n   */\n  public Double decrementHash(\n      @NotBlank final String key, @NotBlank final String item, @NotBlank final double by) {\n    return this.redisTemplate.opsForHash().increment(key, item, -by);\n  }\n\n  // ============================set=============================\n\n  /**\n   * 根据 key 获取 Set 中的所有值\n   *\n   * @param key 键\n   * @return Set<Object>\n   */\n  public Set<Object> getSet(@NotBlank final String key) {\n    return this.redisTemplate.opsForSet().members(key);\n  }\n\n  /**\n   * 根据 value 从一个 set 中查询,是否存在\n   *\n   * @param key 键\n   * @param value 值\n   * @return {Boolean}\n   */\n  public Boolean hasKeySet(@NotBlank final String key, @NotBlank final Object value) {\n    return this.redisTemplate.opsForSet().isMember(key, value);\n  }\n\n  /**\n   * 将数据放入set缓存\n   *\n   * @param key 键\n   * @param values 值\n   * @return 放入个数\n   */\n  public Long addSet(@NotBlank final String key, @NotBlank final Object... values) {\n    return this.redisTemplate.opsForSet().add(key, values);\n  }\n\n  /**\n   * 将set数据放入缓存\n   *\n   * @param key 键\n   * @param timeout 时间\n   * @param values 值\n   * @return 放入个数\n   */\n  public Long addSet(\n      @NotBlank final String key,\n      @NotBlank final Duration timeout,\n      @NotBlank final Object... values) {\n    final Long num = this.redisTemplate.opsForSet().add(key, values);\n    this.setExpire(key, timeout);\n    return num;\n  }\n\n  /**\n   * 获取set缓存的长度\n   *\n   * @param key 键\n   * @return 缓存的长度\n   */\n  public Long getSetSize(@NotBlank final String key) {\n    return this.redisTemplate.opsForSet().size(key);\n  }\n\n  /**\n   * 移除值为value的\n   *\n   * @param key 键\n   * @param values 值\n   * @return 移除个数\n   */\n  public Long removeSet(@NotBlank final String key, @NotBlank final Object... values) {\n    return this.redisTemplate.opsForSet().remove(key, values);\n  }\n  // ===============================list=================================\n\n  /**\n   * 获取list缓存的内容\n   *\n   * @param key 键\n   * @param start 开始\n   * @param end 结束 0 到 -1代表所有值\n   * @return list缓存的内容\n   */\n  public List<Object> getList(\n      @NotBlank final String key, @NotBlank final Long start, @NotBlank final Long end) {\n    return this.redisTemplate.opsForList().range(key, start, end);\n  }\n\n  /**\n   * 获取list缓存的长度\n   *\n   * @param key 键\n   * @return list缓存的长度\n   */\n  public Long getListSize(@NotBlank final String key) {\n    return this.redisTemplate.opsForList().size(key);\n  }\n\n  /**\n   * 通过索引 获取list中的值\n   *\n   * @param key 键\n   * @param index 索引 index>=0时， 0 表头，1 第二个元素，依次类推；index<0时，-1，表尾，-2倒数第二个元素，依次类推\n   * @return list中的值\n   */\n  public Object getListIndex(@NotBlank final String key, @NotBlank final Long index) {\n    return this.redisTemplate.opsForList().index(key, index);\n  }\n\n  /**\n   * 将list放入缓存\n   *\n   * @param key 键\n   * @param value 值\n   * @return 放入个数\n   */\n  public Long pushList(@NotBlank final String key, @NotBlank final Object value) {\n    return this.redisTemplate.opsForList().rightPush(key, value);\n  }\n\n  /**\n   * 将list放入缓存\n   *\n   * @param key 键\n   * @param value 值\n   * @param timeout 时间\n   */\n  public Long pushList(\n      @NotBlank final String key, @NotBlank final Object value, @NotBlank final Duration timeout) {\n    final Long num = this.redisTemplate.opsForList().rightPush(key, value);\n    this.setExpire(key, timeout);\n    return num;\n  }\n\n  /**\n   * 将list放入缓存\n   *\n   * @param key 键\n   * @param value 值\n   * @return 放入个数\n   */\n  public Long pushList(@NotBlank final String key, @NotBlank final List<Object> value) {\n    return this.redisTemplate.opsForList().rightPushAll(key, value);\n  }\n\n  /**\n   * 将list放入缓存\n   *\n   * @param key 键\n   * @param value 值\n   * @param timeout 时间\n   * @return 放入个数\n   */\n  public Long pushList(\n      @NotBlank final String key,\n      @NotBlank final List<Object> value,\n      @NotBlank final Duration timeout) {\n    final Long num = this.redisTemplate.opsForList().rightPushAll(key, value);\n    this.setExpire(key, timeout);\n    return num;\n  }\n\n  /**\n   * 根据索引修改 list 中的某条数据\n   *\n   * @param key 键\n   * @param index 索引\n   * @param value 值\n   */\n  public void updateListIndex(\n      @NotBlank final String key, @NotBlank final Long index, @NotBlank final Object value) {\n    this.redisTemplate.opsForList().set(key, index, value);\n  }\n\n  /**\n   * 移除N个值为value\n   *\n   * @param key 键\n   * @param count 移除多少个\n   * @param value 值\n   * @return 移除个数\n   */\n  public Long removeList(\n      @NotBlank final String key, @NotBlank final Long count, @NotBlank final Object value) {\n    return this.redisTemplate.opsForList().remove(key, count, value);\n  }\n}\n"
  },
  {
    "path": "back/src/main/java/com/msy/plus/util/UrlUtils.java",
    "content": "package com.msy.plus.util;\n\nimport javax.servlet.ServletRequest;\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n * Url工具\n *\n * @author MoShuying\n * @date 2018/07/13\n */\npublic class UrlUtils {\n  private UrlUtils() {}\n\n  /**\n   * 请求的相对路径 /user/list\n   *\n   * @param request request\n   * @return 相对路径\n   */\n  public static String getMappingUrl(final ServletRequest request) {\n    return getMappingUrl((HttpServletRequest) request);\n  }\n\n  public static String getMappingUrl(final HttpServletRequest request) {\n    return request.getRequestURI().substring(request.getContextPath().length());\n  }\n}\n"
  },
  {
    "path": "back/src/main/resources/META-INF/spring-devtools.yml",
    "content": "# devtools 热重启导致 mapper 出错的解决\n# https://github.com/abel533/MyBatis-Spring-Boot#spring-devtools-%E9%85%8D%E7%BD%AE\nrestart:\n  include:\n    mapper: /mapper-[\\\\w-\\\\.]+jar\n    pagehelper: /pagehelper-[\\\\w-\\\\.]+jar"
  },
  {
    "path": "back/src/main/resources/META-INF/swagger3.yml",
    "content": "application:\n  # 项目名称\n  name: APIs doc\n  # 项目版本信息\n  version: 1.0\n  # 项目描述信息\n  description: RESTFul APIs\n  # 项目许可\n  license: Apache License 2.0\n  url:\n    service: https://github.com/Zoctan/spring-boot-api-plus\n    license: https://github.com/Zoctan/spring-boot-api-plus/blob/master/LICENSE\n  # 扫描路径选择\n  apis.selector: com.msy.plus.controller\n# 作者信息\nauthor:\n  name: Zoctan\n  url: https://github.com/Zoctan\n  email: 752481828@qq.com"
  },
  {
    "path": "back/src/main/resources/application-dev.yml",
    "content": "server:\n  port: 80\n\nspring:\n  devtools:\n    restart:\n      # 修改代码后自动重启\n      enabled: true\n  servlet:\n    multipart:\n      # 最大文件上传大小\n      max-file-size: 20MB\n      # 最大请求大小\n      max-request-size: 20MB\n  # 数据源（应该全部加密）\n  datasource:\n    druid:\n      # 连接，注意各个配置，尤其是要一次性执行多条 SQL 时，要 allowMultiQueries=true\n      url: jdbc:mysql://localhost:3306/crm3?useUnicode=true&useSSL=false&useLegacyDatetimeCode=false&allowMultiQueries=true&characterEncoding=utf-8&serverTimezone=UTC\n      # 用户名 root\n      username: root\n      # 密码 root\n      password: root\n      # 驱动类\n      driver-class-name: MyEnc({mfkB3F21902N35InZN3DR4jdSuJR1w2bo+3Z4w1jgtfWVkHRZuclaw==})\n      # 连接池配置 连接数量、最小、最大、获取连接等待超时的时间\n      initial-size: 1\n      min-idle: 1\n      max-active: 20\n      max-wait: 60000\n      # 配置一个连接在池中最小生存的时间，单位是毫秒\n      minEvictableIdleTimeMillis: 300000\n      validationQuery: SELECT 1 FROM DUAL\n      testWhileIdle: true\n      testOnBorrow: false\n      testOnReturn: false\n      # 配置监控统计拦截的过滤器，去掉后监控界面 SQL 无法统计，wall 用于 SQL 防火墙防注入\n      filters: stat,wall\n      # WebStatFilter 配置\n      web-stat-filter:\n        enabled: true\n        url-pattern: /*\n        # 不统计\n        exclusions: /druid/*,*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico\n        session-stat-enable: true\n        session-stat-max-count: 10\n        principal-session-name: session_name\n        principal-cookie-name: cookie_name\n      # StatViewServlet 配置\n      stat-view-servlet:\n        enabled: true\n        # 配置 DruidStatViewServlet\n        url-pattern: /druid/*\n        # 禁止手动重置监控数据\n        reset-enable: false\n        # 监控页面登录的用户名/密码\n        login-username: admin\n        login-password: admin\n        # IP白名单（没有配置或者为空，则允许所有访问）\n        allow: 127.0.0.1\n        # IP黑名单（存在共同时，deny优先于allow）\n        deny:\n      # Spring 监控，对内部各接口调用的监控\n      aop-patterns: com.msy.plus.controller.*,com.msy.plus.dto.*,com.msy.plus.mapper.*,com.msy.plus.service.*\n  cache:\n    # 缓存类型\n    type: redis\n    redis:\n      # key 前缀\n      key-prefix: msy.plus[DEV]\n      # 过期时间\n      time-to-live: 60s\n  redis:\n    # 数据库索引（默认为0）\n    database: 0\n    # 服务器地址\n    host: 127.0.0.1\n    # 服务器连接端口\n    port: 6379\n    # 服务器连接密码 root\n    password:\n    jedis.pool:\n      # 连接池最大连接数（使用负值表示没有限制）\n      max-active: 8\n      # 连接池最大阻塞等待时间（使用负值表示没有限制）\n      max-wait: -1ms\n      # 连接池中的最大空闲连接\n      max-idle: 8\n      # 连接池中的最小空闲连接\n      min-idle: 0\n\nlogging:\n  # 日志级别\n  level.com.msy.plus: debug\n\n# Json web token\njwt:\n  # 过期时间（分钟）\n  expire-time: 300m\n  # claim 权限 key\n  claim-key-auth: auth\n  # 请求头或请求参数的 key\n  header: Authorization\n  # token 类型\n  token-type: Bearer\n\nupload:\n  # 上传路径\n  local-path: /tmp/\n  # 最小文件上传大小\n  min: 1KB\n  # 最大文件上传大小\n  max: 10MB\n"
  },
  {
    "path": "back/src/main/resources/application-test.yml",
    "content": "server:\n  port: 8082"
  },
  {
    "path": "back/src/main/resources/application.yml",
    "content": "spring:\n  profiles:\n    # 激活的配置\n    active: dev\n  # 终端彩色输出信息\n  output.ansi.enabled: ALWAYS\n  resources:\n    # 不映射工程中的静态资源文件比如：html、css\n    # 如果某些情况需要映射\n    # 比如 swagger2，可以在 addResourceHandlers 和 addViewControllers 中特别添加，参考 WebMvcConfig\n    add-mappings: false\n  mvc:\n    # 当出现 404 错误时，直接抛出异常（默认是显示一个错误页面）\n    throw-exception-if-no-handler-found: true\n  freemarker:\n    # 关闭模版检查\n    checkTemplateLocation: false\n\nrsa:\n  # 私钥\n  private-key: MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEArD4P1yMYRsS4YSEbB7V3LQs/+6MrTbAdnlF8CdangeD89gRrp9sz8MutI1s2xOPnpavSv8HeB3VpwE1Iw1WK2QIDAQABAkB4wFWolJD7ZASDC4uAnwZ6zK1Bg8XjA/nvuN6Fozfxw5s40HSPyild32CX47fCOYlt94shRrNaIHIN78N8+ioVAiEA4hWDEnzyqT1mkrLCdgVNnH36aow3/jonp0trQpSagKcCIQDDCLEDfjcPUovDkp9XQZ3LlYU8+zPGJ9Nccck0YtGIfwIhAOGGcgScTXhTfqGx3lfavGvyIz3r9+MLYgj5K9rz4BebAiB4CAtZSP598aGO1dg3DW0d9IGxzDBLDguo42afVQn75QIgBy9s8n1ZyWyLloCBb4+Wf0iTOUJC7II9Xq1LUF2QJGo=\n  # 公钥\n  public-key: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKw+D9cjGEbEuGEhGwe1dy0LP/ujK02wHZ5RfAnWp4Hg/PYEa6fbM/DLrSNbNsTj56Wr0r/B3gd1acBNSMNVitkCAwEAAQ==\n\njasypt:\n  encryptor:\n    # 自定义的加密器\n    bean: myStringEncryptor\n    # 自定义被加密值的发现器\n    property:\n      detector-bean: myEncryptablePropertyDetector\n    # 先 RSA，后 Base64 加密的密码\n    # 在 JasyptConfig#myStringEncryptor 中先解密后再使用\n    password: fnMa4sWpCFSG1Wl3+tkjSRKfdApiZBGms5NE75TqzudMq1/9py5uvKk7urU4dKnuV+3/Tq69Y2E4gohJlAD3cA==\n\nmybatis:\n  # 存放实体的位置\n  type-aliases-package: com.msy.plus.entity\n  # 存放 mapper 映射文件的位置\n  mapper-locations: classpath:mapper/*.xml\n\nmapper:\n  # 多个接口时逗号隔开\n  mappers: com.msy.plus.core.mapper.MyMapper\n  # insert 和 update 中，判断字符串类型 != ''\n  not-empty: false\n  # 取回主键的方式\n  identity: MYSQL\n\n# 分页插件\n# https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md\npagehelper:\n  # pageSize=0 时查出所有结果，相当于没分页\n  page-size-zero: true\n  # 数据库方言\n  helperDialect: mysql\n  # 分页合理化\n  # pageNum <= 0 时会查询第一页\n  # pageNum > pages（超过总数时），会查询最后一页\n  reasonable: true\n  # 支持通过 Mapper 接口参数来传递分页参数\n  supportMethodsArguments: true\n\n# 日志\n#logging:\n#  # 以文件方式记录日志\n#  file: plus.log\n#  # 设置目录\n#  path: /var/log"
  },
  {
    "path": "back/src/main/resources/banner.txt",
    "content": "////////////////////////////////////////\n//                                    //\n//         ::                         //\n//         ttttii;;,                  //\n//         itttttii;i;,               //\n//         tiiitiiitji;;i;,           //\n//        :,.iiiiiiiii;;i;;           //\n//      iiitiiii f   jjjjtttti,       //\n//     ,;iiiiii j  G G  iiiittii      //\n//        iiii   f  GE    :iiitii     //\n//         ii     DG          ti:     //\n//          ;     DG          ti      //\n//                D            ;      //\n//                DL                  //\n//                GL                  //\n//                GG                  //\n//  Great oaks                        //\n//        from little acorns grow.    //\n//                                    //\n////////////////////////////////////////"
  },
  {
    "path": "back/src/main/resources/mapper/AccountMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.msy.plus.mapper.AccountMapper\">\n    <resultMap id=\"AccountMap\" type=\"com.msy.plus.entity.AccountDO\">\n        <id column=\"id\" jdbcType=\"BIGINT\" property=\"id\"/>\n        <result column=\"email\" jdbcType=\"VARCHAR\" property=\"email\"/>\n        <result column=\"name\" jdbcType=\"VARCHAR\" property=\"name\"/>\n        <result column=\"password\" jdbcType=\"VARCHAR\" property=\"password\"/>\n        <result column=\"register_time\" jdbcType=\"TIMESTAMP\" property=\"registerTime\"/>\n        <result column=\"login_time\" jdbcType=\"TIMESTAMP\" property=\"loginTime\"/>\n    </resultMap>\n\n    <resultMap id=\"AccountWithRoleMap\" type=\"com.msy.plus.entity.AccountWithRoleDO\" extends=\"AccountMap\">\n        <collection property=\"roles\" javaType=\"java.util.List\" ofType=\"com.msy.plus.entity.RoleDO\">\n            <result column=\"role_name\" jdbcType=\"VARCHAR\" property=\"name\"/>\n            <result column=\"role_id\" jdbcType=\"BIGINT\" property=\"id\"/>\n        </collection>\n    </resultMap>\n\n    <!-- 按条件查询账户 -->\n    <select id=\"getByQueryWithRole\" resultMap=\"AccountWithRoleMap\">\n        SELECT\n        a.*,\n        r.name AS role_name,\n        r.id AS role_id\n        FROM employee a\n        LEFT JOIN employee_role er ON a.id = er.employeeId\n        LEFT JOIN role r ON er.roleId = r.id\n        <where>\n            <if test=\"id != null\">a.id = #{id}</if>\n            <if test=\"name != null\">a.name = #{name}</if>\n            <if test=\"email != null\">a.email = #{email}</if>\n        </where>\n    </select>\n\n    <!-- 按账户名更新最后登陆时间 -->\n    <update id=\"updateLoginTimeByName\">\n        UPDATE employee\n        SET login_time = NOW()\n        WHERE name = #{name}\n    </update>\n</mapper>"
  },
  {
    "path": "back/src/main/resources/mapper/CustomerFollowUpHistoryMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.msy.plus.mapper.CustomerFollowUpHistoryMapper\">\n  <resultMap id=\"BaseResultMap\" type=\"com.msy.plus.entity.CustomerFollowUpHistory\">\n    <!--\n      WARNING - @mbg.generated\n    -->\n    <id column=\"id\" jdbcType=\"INTEGER\" property=\"id\" />\n    <result column=\"traceTime\" jdbcType=\"TIMESTAMP\" property=\"tracetime\" />\n    <result column=\"traceType\" jdbcType=\"INTEGER\" property=\"tracetype\" />\n    <result column=\"traceResult\" jdbcType=\"INTEGER\" property=\"traceresult\" />\n    <result column=\"customerID\" jdbcType=\"INTEGER\" property=\"customerid\" />\n    <result column=\"inputUser\" jdbcType=\"INTEGER\" property=\"inputuser\" />\n    <result column=\"type\" jdbcType=\"INTEGER\" property=\"type\" />\n    <result column=\"traceDetails\" jdbcType=\"LONGVARCHAR\" property=\"tracedetails\" />\n    <result column=\"comment\" jdbcType=\"LONGVARCHAR\" property=\"comment\" />\n  </resultMap>\n  <select id=\"listAndSearch\" resultType=\"com.msy.plus.entity.CFUHSearch\">\n    select cfuh.id          as id,\n           cm2.name         as name,\n           cfuh.traceTime   as traceTime,\n           cfuh.comment     as comment,\n           cfuh.traceType   as traceType,\n           cfuh.traceResult as traceResult,\n           cfuh.traceDetails as traceDetails,\n           cfuh.inputUser   as inputUser,\n           cfuh.type        as type\n    from customer_follow_up_history cfuh\n           left join customer_manager cm2 on cfuh.customerID = cm2.id\n    where (cm2.tel like concat('%',#{keyword},'%') or cm2.name like concat('%',#{keyword},'%'))\n      <if test=\"startTime != null and startTime != ''\">\n        and cfuh.traceTime between #{startTime} and #{endTime}\n      </if>\n      <if test=\"type != null and type != 9999999\">\n        and cfuh.type = #{type}\n      </if>\n      order by cfuh.traceTime desc\n  </select>\n</mapper>"
  },
  {
    "path": "back/src/main/resources/mapper/CustomerHandoverMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.msy.plus.mapper.CustomerHandoverMapper\">\n  <resultMap id=\"BaseResultMap\" type=\"com.msy.plus.entity.CustomerHandover\">\n    <!--\n      WARNING - @mbg.generated\n    -->\n    <id column=\"id\" jdbcType=\"INTEGER\" property=\"id\" />\n    <result column=\"customerID\" jdbcType=\"INTEGER\" property=\"customerid\" />\n    <result column=\"transUser\" jdbcType=\"INTEGER\" property=\"transuser\" />\n    <result column=\"transTime\" jdbcType=\"TIMESTAMP\" property=\"transtime\" />\n    <result column=\"oldSeller\" jdbcType=\"INTEGER\" property=\"oldseller\" />\n    <result column=\"newSeller\" jdbcType=\"INTEGER\" property=\"newseller\" />\n    <result column=\"transReason\" jdbcType=\"VARCHAR\" property=\"transreason\" />\n  </resultMap>\n  <select id=\"listAndSearch\" resultType=\"com.msy.plus.dto.CustomerHandoverList\">\n    select ch.id          as id,\n           cm.name        as customerName,\n           ch.transTime   as transTime,\n           e.name         as transUser,\n           e2.name        as oldSeller,\n           e3.name        as newSeller,\n           ch.transReason as transReason\n    from customer_handover as ch\n           left join customer_manager as cm on ch.customerID = cm.id\n           left join employee as e on ch.transUser = e.id\n           left join employee as e2 on ch.oldSeller = e2.id\n           left join employee as e3 on ch.newSeller = e3.id\n    where (\n        e2.name like concat('%',#{keyword},'%')\n            or e.name like concat('%',#{keyword},'%')\n            or e3.name like concat('%',#{keyword},'%')\n            or cm.tel like concat('%',#{keyword},'%')\n            or cm.name like concat('%',#{keyword},'%')\n        )\n      <if test=\"startTime !=null or endTime !=null\">\n        and ch.transTime between #{startTime} and #{endTime}\n      </if>\n  </select>\n</mapper>"
  },
  {
    "path": "back/src/main/resources/mapper/CustomerManagerMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.msy.plus.mapper.CustomerManagerMapper\">\n    <resultMap id=\"BaseResultMap\" type=\"com.msy.plus.entity.CustomerManager\">\n        <!--\n          WARNING - @mbg.generated\n        -->\n        <id column=\"id\" jdbcType=\"INTEGER\" property=\"id\"/>\n        <result column=\"name\" jdbcType=\"VARCHAR\" property=\"name\"/>\n        <result column=\"age\" jdbcType=\"INTEGER\" property=\"age\"/>\n        <result column=\"gender\" jdbcType=\"INTEGER\" property=\"gender\"/>\n        <result column=\"tel\" jdbcType=\"VARCHAR\" property=\"tel\"/>\n        <result column=\"qq\" jdbcType=\"VARCHAR\" property=\"qq\"/>\n        <result column=\"job\" jdbcType=\"INTEGER\" property=\"job\"/>\n        <result column=\"source\" jdbcType=\"INTEGER\" property=\"source\"/>\n        <result column=\"seller\" jdbcType=\"INTEGER\" property=\"seller\"/>\n        <result column=\"inputUser\" jdbcType=\"INTEGER\" property=\"inputuser\"/>\n        <result column=\"inputTime\" jdbcType=\"TIMESTAMP\" property=\"inputtime\"/>\n        <result column=\"status\" jdbcType=\"INTEGER\" property=\"status\"/>\n        <result column=\"positiveTime\" jdbcType=\"TIMESTAMP\" property=\"positivetime\"/>\n    </resultMap>\n    <select id=\"listAllWithDictionary\" resultType=\"com.msy.plus.dto.CustomerManagerList\">\n        select c.id as id,\n        c.name as name,\n        c.age as age,\n        c.status as status,\n        gender,\n        tel,\n        qq,\n        dd1.title as job,\n        dd2.title as source,\n        if(e2.name is not null, e2.name, e.name)\n        as inputUser,\n        if(e2.id is not null, e2.id, e.id)\n        as inputUserId\n        from customer_manager c\n        left join\n        dictionary_details dd1\n        on c.job = dd1.id\n        left join\n        dictionary_details dd2\n        on c.source = dd2.id\n        left join\n        employee e\n        on e.id = c.inputUser\n        left join (\n        select *\n        from (select *\n        from customer_handover\n        order by transTime\n        desc\n        Limit 10000)\n        as chG\n        group by customerID\n        ) ch\n        on ch.oldSeller = e.id\n        and ch.customerID = c.id\n        left join\n        employee e2\n        on e2.id = ch.newSeller\n        where (c.name like concat('%',#{keyword},'%') or c.tel like concat('%',#{keyword},'%'))\n        <if test=\"status != null\">\n            and c.status =#{status}\n        </if>\n    </select>\n    <select id=\"getDetailById\" resultType=\"com.msy.plus.entity.CustomerManager\">\n        select c.id                                                as id,\n               c.name                                              as name,\n               c.gender                                            as gender,\n               c.tel                                               as tel,\n               c.qq                                                as qq,\n               c.job                                               as job,\n               c.source                                            as source,\n               c.seller                                            as seller,\n               if(ch.newSeller is null, c.inputUser, ch.newSeller) as inputUser\n        from customer_manager as c\n                 left join (select *\n                            from (select * from customer_handover order by transTime desc Limit 10000) as chG\n                            group by customerID) ch on ch.customerID = c.id\n        where c.id = #{id}\n    </select>\n    <select id=\"queryAnalysis\" resultType=\"com.msy.plus.entity.Analysis\">\n        select\n        <if test=\"groupType == null or groupType == 1 or groupType == 0\">\n            count(cm.inputUser) as count,\n            e.name as name\n        </if>\n        <if test=\"groupType != null and groupType > 1\">\n            count(cm.inputTime) as count,\n        </if>\n        <if test=\"groupType != null and groupType == 2\">\n            date_format(cm.inputTime,'%Y') as name\n        </if>\n        <if test=\"groupType != null and groupType == 3\">\n            date_format(cm.inputTime,'%Y-%m') as name\n        </if>\n        <if test=\"groupType != null and groupType == 4\">\n            date_format(cm.inputTime,'%Y-%m-%d') as name\n        </if>\n        from customer_manager as cm\n        left join employee e on cm.inputUser = e.id\n        where e.name like concat('%',#{name},'%')\n        <if test=\"startTime != null and endTime != null\">\n            and cm.inputTime between #{startTime} and #{endTime}\n        </if>\n        <if test=\"groupType == null or groupType == 1 or groupType == 0\">\n            group by e.name\n        </if>\n        <if test=\"groupType != null and groupType > 1\">\n            group by date_format(cm.inputTime,'%Y-%m')\n        </if>\n    </select>\n</mapper>"
  },
  {
    "path": "back/src/main/resources/mapper/DepartmentMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.msy.plus.mapper.DepartmentMapper\">\n  <resultMap id=\"BaseResultMap\" type=\"com.msy.plus.entity.Department\">\n    <!--\n      WARNING - @mbg.generated\n    -->\n    <id column=\"id\" jdbcType=\"INTEGER\" property=\"id\" />\n    <result column=\"sn\" jdbcType=\"VARCHAR\" property=\"sn\" />\n    <result column=\"name\" jdbcType=\"VARCHAR\" property=\"name\" />\n  </resultMap>\n</mapper>"
  },
  {
    "path": "back/src/main/resources/mapper/DictionaryContentsMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.msy.plus.mapper.DictionaryContentsMapper\">\n  <resultMap id=\"BaseResultMap\" type=\"com.msy.plus.entity.DictionaryContents\">\n    <!--\n      WARNING - @mbg.generated\n    -->\n    <id column=\"id\" jdbcType=\"INTEGER\" property=\"id\" />\n    <result column=\"sn\" jdbcType=\"VARCHAR\" property=\"sn\" />\n    <result column=\"title\" jdbcType=\"VARCHAR\" property=\"title\" />\n    <result column=\"intro\" jdbcType=\"VARCHAR\" property=\"intro\" />\n  </resultMap>\n  <select id=\"listWithKeyword\" resultMap=\"BaseResultMap\">\n    select *\n    from dictionary_contents as d\n    <if test=\"keyword != null and keyword != '' \">\n      where (d.intro like concat('%', #{keyword}, '%')\n      or d.sn like concat('%', #{keyword}, '%')\n      or d.title like concat('%', #{keyword}, '%'))\n    </if>\n  </select>\n</mapper>"
  },
  {
    "path": "back/src/main/resources/mapper/DictionaryDetailsMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.msy.plus.mapper.DictionaryDetailsMapper\">\n  <resultMap id=\"BaseResultMap\" type=\"com.msy.plus.entity.DictionaryDetails\">\n    <!--\n      WARNING - @mbg.generated\n    -->\n    <id column=\"id\" jdbcType=\"INTEGER\" property=\"id\" />\n    <result column=\"title\" jdbcType=\"VARCHAR\" property=\"title\" />\n    <result column=\"sequence\" jdbcType=\"INTEGER\" property=\"sequence\" />\n    <result column=\"parentId\" jdbcType=\"INTEGER\" property=\"parentid\" />\n  </resultMap>\n  <select id=\"listWithKeyword\" resultMap=\"BaseResultMap\">\n    select *\n    from dictionary_details as d\n    where d.parentId = #{id}\n    <if test=\"keyword != null and keyword != '' \">\n      and (d.sequence like concat('%', #{keyword}, '%')\n      or d.title like concat('%', #{keyword}, '%'))\n    </if>\n  </select>\n</mapper>"
  },
  {
    "path": "back/src/main/resources/mapper/EmployeeMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.msy.plus.mapper.EmployeeMapper\">\n    <resultMap id=\"BaseResultMap\" type=\"com.msy.plus.entity.Employee\">\n        <!--\n          WARNING - @mbg.generated\n        -->\n        <id column=\"id\" jdbcType=\"INTEGER\" property=\"id\"/>\n        <result column=\"name\" jdbcType=\"VARCHAR\" property=\"name\"/>\n        <result column=\"password\" jdbcType=\"VARCHAR\" property=\"password\"/>\n        <result column=\"email\" jdbcType=\"VARCHAR\" property=\"email\"/>\n        <result column=\"age\" jdbcType=\"INTEGER\" property=\"age\"/>\n        <result column=\"dept\" jdbcType=\"INTEGER\" property=\"dept\"/>\n        <result column=\"hireDate\" jdbcType=\"TIMESTAMP\" property=\"hiredate\"/>\n        <result column=\"state\" jdbcType=\"INTEGER\" property=\"state\"/>\n        <result column=\"admin\" jdbcType=\"INTEGER\" property=\"admin\"/>\n    </resultMap>\n    <resultMap id=\"EmployeeWithRole\" type=\"com.msy.plus.entity.EmployeeWithRoleDO\" extends=\"BaseResultMap\">\n        <result column=\"departmentName\" jdbcType=\"VARCHAR\" property=\"departmentName\"/>\n        <result column=\"roleNames\" jdbcType=\"VARCHAR\" property=\"roleNames\"/>\n    </resultMap>\n    <resultMap id=\"EmployeeDetail\" type=\"com.msy.plus.entity.EmployeeDetail\" extends=\"BaseResultMap\">\n        <collection property=\"roleIds\" javaType=\"java.util.List\" ofType=\"java.lang.Long\">\n            <result column=\"roleIds\" jdbcType=\"INTEGER\"/>\n        </collection>\n    </resultMap>\n    <select id=\"listEmployeeWithRole\" resultMap=\"EmployeeWithRole\">\n        select e.id as id,\n        e.name as name,\n        email,\n        age,\n        d.name as departmentName,\n        group_concat(r.name order by e.name ASC SEPARATOR ',') as roleNames\n        from employee as e\n        left join employee_role as er on er.employeeId = e.id\n        left join role as r on er.roleId = r.id\n        left join department as d on d.id = e.dept\n        where (e.name like concat('%',#{keyword},'%') or e.email like concat('%',#{keyword},'%'))\n        <if test=\"dept != null and dept >0\">\n            and e.dept = #{dept}\n        </if>\n        group by e.id order by e.id desc\n    </select>\n    <select id=\"getDetailById\" resultMap=\"EmployeeDetail\">\n        select e.id       as id,\n               e.name     as name,\n               e.password as password,\n               email,\n               age,\n               e.dept     as dept,\n               e.admin    as admin,\n               er.roleId  as roleIds\n        from employee as e\n                 left join employee_role er on er.employeeId = e.id\n        where e.id = #{id}\n    </select>\n    <delete id=\"deleteEmployeeWithRole\">\n        delete\n        from employee_role\n        where employeeId = #{id}\n    </delete>\n    <select id=\"saveRoles\">\n        <if test=\"roles !=null\">\n            insert into employee_role (employeeId,roleId) values\n            <foreach collection=\"roles\" item=\"item\" index=\"index\" separator=\",\">\n                (\n                #{id},\n                #{item}\n                )\n            </foreach>\n        </if>\n    </select>\n\n    <select id=\"getAllEmployeeRoleTableRow\" resultType=\"java.lang.Long\">\n        select roleId\n        from employee_role\n        where employeeId = #{id}\n    </select>\n    <delete id=\"deleteEmployeeWithRoleItem\">\n        delete\n        from employee_role\n        where employeeId = #{id}\n          and roleId = #{roleId}\n    </delete>\n</mapper>"
  },
  {
    "path": "back/src/main/resources/mapper/PermissionMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.msy.plus.mapper.PermissionMapper\">\n  <resultMap id=\"BaseResultMap\" type=\"com.msy.plus.entity.Permission\">\n    <!--\n      WARNING - @mbg.generated\n    -->\n    <id column=\"id\" jdbcType=\"INTEGER\" property=\"id\" />\n    <result column=\"name\" jdbcType=\"VARCHAR\" property=\"name\" />\n    <result column=\"expression\" jdbcType=\"VARCHAR\" property=\"expression\" />\n  </resultMap>\n</mapper>"
  },
  {
    "path": "back/src/main/resources/mapper/RoleMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.msy.plus.mapper.RoleMapper\">\n    <resultMap id=\"RoleMap\" type=\"com.msy.plus.entity.RoleDO\">\n        <id column=\"id\" jdbcType=\"BIGINT\" property=\"id\"/>\n        <result column=\"name\" jdbcType=\"VARCHAR\" property=\"name\"/>\n        <result column=\"sn\" jdbcType=\"VARCHAR\" property=\"sn\"/>\n        <result column=\"permission\" jdbcType=\"BIGINT\" property=\"permission\"/>\n    </resultMap>\n    <resultMap id=\"RoleWithPermission\" type=\"com.msy.plus.entity.RoleWithPermissionDO\" extends=\"RoleMap\">\n        <collection property=\"permissions\" javaType=\"java.util.List\" ofType=\"com.msy.plus.entity.Permission\">\n            <result column=\"permission_id\" jdbcType=\"BIGINT\" property=\"id\"/>\n            <result column=\"permission_name\" jdbcType=\"VARCHAR\" property=\"name\"/>\n            <result column=\"expression\" jdbcType=\"VARCHAR\" property=\"expression\"/>\n        </collection>\n    </resultMap>\n\n    <insert id=\"saveAsDefaultRole\" parameterType=\"java.lang.Long\">\n        INSERT INTO employee_role (employeeId, roleId)\n        VALUES (#{accountId}, (SELECT r.id FROM role r WHERE name=\"USER\"))\n    </insert>\n    <insert id=\"savePermissions\" parameterType=\"java.util.List\">\n        insert into role_permission (role_id, permission_id) values\n        <foreach collection=\"permissions\" item=\"item\" index=\"index\" separator=\",\">\n            (\n            #{roleId},\n            #{item}\n            )\n        </foreach>\n    </insert>\n    <delete id=\"deleteRolePermissionItem\" >\n        delete\n        from role_permission\n        where role_id = #{roleId}\n          and permission_id = #{permissionId}\n    </delete>\n    <select id=\"getAllRolePermissionTableRow\" parameterType=\"java.lang.Long\" resultType=\"com.msy.plus.entity.RolePermissionDO\">\n        select * from role_permission where role_id = #{roleId}\n    </select>\n    <select id=\"getDetailById\" parameterType=\"java.lang.Long\" resultMap=\"RoleWithPermission\">\n        select r.id         as id,\n               r.name       as name,\n               r.sn         as sn,\n               p.id         as permission_id,\n               p.name       as permission_name,\n               p.expression as expression\n        from role as r\n                 left join role_permission as rp on r.id = rp.role_id\n                 left join permission p on rp.permission_id = p.id\n        where r.id = #{id}\n    </select>\n</mapper>"
  },
  {
    "path": "back/src/main/resources/rsa/private-key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEArD4P1yMYRsS4YSEb\nB7V3LQs/+6MrTbAdnlF8CdangeD89gRrp9sz8MutI1s2xOPnpavSv8HeB3VpwE1I\nw1WK2QIDAQABAkB4wFWolJD7ZASDC4uAnwZ6zK1Bg8XjA/nvuN6Fozfxw5s40HSP\nyild32CX47fCOYlt94shRrNaIHIN78N8+ioVAiEA4hWDEnzyqT1mkrLCdgVNnH36\naow3/jonp0trQpSagKcCIQDDCLEDfjcPUovDkp9XQZ3LlYU8+zPGJ9Nccck0YtGI\nfwIhAOGGcgScTXhTfqGx3lfavGvyIz3r9+MLYgj5K9rz4BebAiB4CAtZSP598aGO\n1dg3DW0d9IGxzDBLDguo42afVQn75QIgBy9s8n1ZyWyLloCBb4+Wf0iTOUJC7II9\nXq1LUF2QJGo=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "back/src/main/resources/rsa/public-key.pem",
    "content": "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKw+D9cjGEbEuGEhGwe1dy0LP/ujK02w\nHZ5RfAnWp4Hg/PYEa6fbM/DLrSNbNsTj56Wr0r/B3gd1acBNSMNVitkCAwEAAQ==\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "back/src/test/java/CodeGenerator.java",
    "content": "import com.google.common.base.CaseFormat;\nimport freemarker.template.TemplateExceptionHandler;\nimport org.apache.commons.lang3.StringUtils;\nimport org.mybatis.generator.api.MyBatisGenerator;\nimport org.mybatis.generator.config.*;\nimport org.mybatis.generator.internal.DefaultShellCallback;\n\nimport java.io.File;\nimport java.io.FileWriter;\nimport java.io.IOException;\nimport java.text.SimpleDateFormat;\nimport java.util.*;\n\nimport static com.msy.plus.core.constant.ProjectConstant.*;\n\n/**\n * 代码生成器 根据数据表名称生成对应的 Entity、Mapper、Service、Controller 简化开发\n *\n * @author MoShuying\n * @date 2018/05/27\n */\nclass CodeGenerator {\n  private static final String DATABASE = \"mysql\";\n  // JDBC配置，请修改为你项目的实际配置\n  private static final String JDBC_URL =\n      \"jdbc:mysql://localhost:3306/crm3\"\n          + \"?useUnicode=true&characterEncoding=utf-8&useLegacyDatetimeCode=false&serverTimezone=UTC\";\n  private static final String JDBC_USERNAME = \"crm3\";\n  private static final String JDBC_PASSWORD = \"a1Bks32BAsdj12\";\n  private static final String JDBC_DIVER_CLASS_NAME = \"com.mysql.cj.jdbc.Driver\";\n  // 项目在硬盘上的基础路径\n  private static final String PROJECT_PATH = System.getProperty(\"user.dir\");\n  // 模板位置\n  private static final String TEMPLATE_FILE_PATH =\n      CodeGenerator.PROJECT_PATH + \"/src/test/resources/generator/template\";\n  // java文件路径\n  private static final String JAVA_PATH = \"/src/main/java\";\n  // 资源文件路径\n  private static final String RESOURCES_PATH = \"/src/main/resources\";\n  // 生成的Service存放路径\n  private static final String PACKAGE_PATH_SERVICE =\n      CodeGenerator.packageConvertPath(SERVICE_PACKAGE);\n  // 生成的Service实现存放路径\n  private static final String PACKAGE_PATH_SERVICE_IMPL =\n      CodeGenerator.packageConvertPath(SERVICE_IMPL_PACKAGE);\n  // 生成的Controller存放路径\n  private static final String PACKAGE_PATH_CONTROLLER =\n      CodeGenerator.packageConvertPath(CONTROLLER_PACKAGE);\n\n  // @author\n  private static final String AUTHOR = \"MoShuYing\";\n  // @date\n  private static final String DATE = new SimpleDateFormat(\"yyyy/MM/dd\").format(new Date());\n  private static final boolean isRestful = true;\n\n  public static void main(final String[] args) {\n    final Scanner scanner = new Scanner(System.in);\n    System.out.print(\"可能已存在相关文件，请尽可能确保无误。y/n:\");\n    if (!scanner.next().equals(\"y\")) {\n      return;\n    }\n    CodeGenerator.genCode(\"customer_follow_up_history\");\n//     genCodeByCustomModelName(\"employee\",null);\n  }\n\n  /**\n   * 通过数据表名称生成代码，Model 名称通过解析数据表名称获得，下划线转大驼峰的形式。 如输入表名称 \"t_user_detail\" 将生成\n   * TUserDetail、TUserDetailMapper、TUserDetailService ...\n   *\n   * @param tableNames 数据表名称...\n   */\n  private static void genCode(final String... tableNames) {\n    for (final String tableName : tableNames) {\n      CodeGenerator.genCodeByCustomModelName(tableName, null);\n    }\n  }\n\n  /**\n   * 通过数据表名称，和自定义的 Model 名称生成代码 如输入表名称 \"t_user_detail\" 和自定义的 Model 名称 \"sysUser\" 将生成\n   * sysUser、UserMapper、UserService ...\n   *\n   * @param tableName 数据表名称\n   * @param modelName 自定义的 Model 名称\n   */\n  private static void genCodeByCustomModelName(final String tableName, final String modelName) {\n    CodeGenerator.genModelAndMapper(tableName, modelName);\n    CodeGenerator.genService(tableName, modelName);\n    CodeGenerator.genController(tableName, modelName);\n  }\n\n  private static void genModelAndMapper(final String tableName, String modelName) {\n    final Context context = new Context(ModelType.FLAT);\n    context.setId(\"Potato\");\n    context.setTargetRuntime(\"MyBatis3Simple\");\n    context.addProperty(PropertyRegistry.CONTEXT_BEGINNING_DELIMITER, \"`\");\n    context.addProperty(PropertyRegistry.CONTEXT_ENDING_DELIMITER, \"`\");\n\n    final JDBCConnectionConfiguration jdbcConnectionConfiguration =\n        new JDBCConnectionConfiguration();\n    jdbcConnectionConfiguration.setConnectionURL(CodeGenerator.JDBC_URL);\n    jdbcConnectionConfiguration.setUserId(CodeGenerator.JDBC_USERNAME);\n    jdbcConnectionConfiguration.setPassword(CodeGenerator.JDBC_PASSWORD);\n    jdbcConnectionConfiguration.setDriverClass(CodeGenerator.JDBC_DIVER_CLASS_NAME);\n    context.setJdbcConnectionConfiguration(jdbcConnectionConfiguration);\n\n    final PluginConfiguration pluginConfiguration = new PluginConfiguration();\n    pluginConfiguration.setConfigurationType(\"tk.mybatis.mapper.generator.MapperPlugin\");\n    pluginConfiguration.addProperty(\"mappers\", MAPPER_INTERFACE_REFERENCE);\n    context.addPluginConfiguration(pluginConfiguration);\n\n    final JavaModelGeneratorConfiguration javaModelGeneratorConfiguration =\n        new JavaModelGeneratorConfiguration();\n    javaModelGeneratorConfiguration.setTargetProject(\n        CodeGenerator.PROJECT_PATH + CodeGenerator.JAVA_PATH);\n    javaModelGeneratorConfiguration.setTargetPackage(ENTITY_PACKAGE);\n    context.setJavaModelGeneratorConfiguration(javaModelGeneratorConfiguration);\n\n    final SqlMapGeneratorConfiguration sqlMapGeneratorConfiguration =\n        new SqlMapGeneratorConfiguration();\n    sqlMapGeneratorConfiguration.setTargetProject(\n        CodeGenerator.PROJECT_PATH + CodeGenerator.RESOURCES_PATH);\n    sqlMapGeneratorConfiguration.setTargetPackage(\"mapper\");\n    context.setSqlMapGeneratorConfiguration(sqlMapGeneratorConfiguration);\n\n    final JavaClientGeneratorConfiguration javaClientGeneratorConfiguration =\n        new JavaClientGeneratorConfiguration();\n    javaClientGeneratorConfiguration.setTargetProject(\n        CodeGenerator.PROJECT_PATH + CodeGenerator.JAVA_PATH);\n    javaClientGeneratorConfiguration.setTargetPackage(MAPPER_PACKAGE);\n    javaClientGeneratorConfiguration.setConfigurationType(\"XMLMAPPER\");\n    context.setJavaClientGeneratorConfiguration(javaClientGeneratorConfiguration);\n\n    final TableConfiguration tableConfiguration = new TableConfiguration(context);\n    tableConfiguration.setTableName(tableName);\n    if (StringUtils.isNotEmpty(modelName)) {\n      tableConfiguration.setDomainObjectName(modelName);\n    }\n    final GeneratedKey generatedKey;\n    if (\"oracle\".equalsIgnoreCase(CodeGenerator.DATABASE)) {\n      generatedKey = new GeneratedKey(\"id\", \"SELECT SEQ_{1}.NEXTVAL FROM DUAL\", false, \"pre\");\n    } else {\n      generatedKey = new GeneratedKey(\"id\", \"MYSQL\", true, null);\n    }\n    tableConfiguration.setGeneratedKey(generatedKey);\n    context.addTableConfiguration(tableConfiguration);\n\n    final List<String> warnings;\n    final MyBatisGenerator generator;\n    try {\n      final Configuration config = new Configuration();\n      config.addContext(context);\n      config.validate();\n\n      final DefaultShellCallback callback = new DefaultShellCallback(true);\n      warnings = new ArrayList<>();\n      generator = new MyBatisGenerator(config, callback, warnings);\n      generator.generate(null);\n    } catch (final Exception e) {\n      throw new RuntimeException(\"生成 Model 和 Mapper 失败\", e);\n    }\n\n    if (generator.getGeneratedJavaFiles().isEmpty() || generator.getGeneratedXmlFiles().isEmpty()) {\n      throw new RuntimeException(\"生成 Model 和 Mapper 失败：\" + warnings);\n    }\n    if (StringUtils.isEmpty(modelName)) {\n      modelName = CodeGenerator.tableNameConvertUpperCamel(tableName);\n    }\n    System.out.println(modelName + \".java 生成成功\");\n    System.out.println(modelName + \"MyMapper.java 生成成功\");\n    System.out.println(modelName + \"MyMapper.xml 生成成功\");\n  }\n\n  private static void genService(final String tableName, final String modelName) {\n    try {\n      final freemarker.template.Configuration cfg = CodeGenerator.getConfiguration();\n\n      final Map<String, Object> data = new HashMap<>();\n      data.put(\"date\", CodeGenerator.DATE);\n      data.put(\"author\", CodeGenerator.AUTHOR);\n      final String modelNameUpperCamel =\n          StringUtils.isEmpty(modelName)\n              ? CodeGenerator.tableNameConvertUpperCamel(tableName)\n              : modelName;\n      data.put(\"modelNameUpperCamel\", modelNameUpperCamel);\n      data.put(\"modelNameLowerCamel\", CodeGenerator.tableNameConvertLowerCamel(tableName));\n      data.put(\"basePackage\", BASE_PACKAGE);\n\n      final File file =\n          CodeGenerator.createFileDir(\n              CodeGenerator.PROJECT_PATH\n                  + CodeGenerator.JAVA_PATH\n                  + CodeGenerator.PACKAGE_PATH_SERVICE\n                  + modelNameUpperCamel\n                  + \"Service.java\");\n      cfg.getTemplate(\"service.ftl\").process(data, new FileWriter(file));\n      System.out.println(modelNameUpperCamel + \"Service.java 生成成功\");\n\n      final File file1 =\n          CodeGenerator.createFileDir(\n              CodeGenerator.PROJECT_PATH\n                  + CodeGenerator.JAVA_PATH\n                  + CodeGenerator.PACKAGE_PATH_SERVICE_IMPL\n                  + modelNameUpperCamel\n                  + \"ServiceImpl.java\");\n      cfg.getTemplate(\"service-impl.ftl\").process(data, new FileWriter(file1));\n      System.out.println(modelNameUpperCamel + \"ServiceImpl.java 生成成功\");\n    } catch (final Exception e) {\n      throw new RuntimeException(\"生成Service失败\", e);\n    }\n  }\n\n  private static File createFileDir(final String name) throws RuntimeException {\n    final File file = new File(name);\n    if (!file.getParentFile().exists()) {\n      final boolean createSuccess = file.getParentFile().mkdirs();\n      if (!createSuccess) {\n        throw new RuntimeException(\"文件夹创建失败\");\n      }\n    }\n    return file;\n  }\n\n  private static void genController(final String tableName, final String modelName) {\n    try {\n      final freemarker.template.Configuration cfg = CodeGenerator.getConfiguration();\n\n      final Map<String, Object> data = new HashMap<>();\n      data.put(\"date\", CodeGenerator.DATE);\n      data.put(\"author\", CodeGenerator.AUTHOR);\n      final String modelNameUpperCamel =\n          StringUtils.isEmpty(modelName)\n              ? CodeGenerator.tableNameConvertUpperCamel(tableName)\n              : modelName;\n      data.put(\n          \"baseRequestMapping\", CodeGenerator.modelNameConvertMappingPath(modelNameUpperCamel));\n      data.put(\"modelNameUpperCamel\", modelNameUpperCamel);\n      data.put(\n          \"modelNameLowerCamel\",\n          CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, modelNameUpperCamel));\n      data.put(\"basePackage\", BASE_PACKAGE);\n\n      final File file =\n          CodeGenerator.createFileDir(\n              CodeGenerator.PROJECT_PATH\n                  + CodeGenerator.JAVA_PATH\n                  + CodeGenerator.PACKAGE_PATH_CONTROLLER\n                  + modelNameUpperCamel\n                  + \"Controller.java\");\n\n      if (CodeGenerator.isRestful) {\n        cfg.getTemplate(\"controller-restful.ftl\").process(data, new FileWriter(file));\n      } else {\n        cfg.getTemplate(\"controller.ftl\").process(data, new FileWriter(file));\n      }\n      System.out.println(modelNameUpperCamel + \"Controller.java 生成成功\");\n    } catch (final Exception e) {\n      throw new RuntimeException(\"生成Controller失败\", e);\n    }\n  }\n\n  private static freemarker.template.Configuration getConfiguration() throws IOException {\n    final freemarker.template.Configuration cfg =\n        new freemarker.template.Configuration(freemarker.template.Configuration.VERSION_2_3_23);\n    cfg.setDirectoryForTemplateLoading(new File(CodeGenerator.TEMPLATE_FILE_PATH));\n    cfg.setDefaultEncoding(\"UTF-8\");\n    cfg.setTemplateExceptionHandler(TemplateExceptionHandler.IGNORE_HANDLER);\n    return cfg;\n  }\n\n  private static String tableNameConvertLowerCamel(final String tableName) {\n    return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, tableName.toLowerCase());\n  }\n\n  private static String tableNameConvertUpperCamel(final String tableName) {\n    return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, tableName.toLowerCase());\n  }\n\n  private static String tableNameConvertMappingPath(String tableName) {\n    tableName = tableName.toLowerCase(); // 兼容使用大写的表名\n    return \"/\" + (tableName.contains(\"_\") ? tableName.replaceAll(\"_\", \"/\") : tableName);\n  }\n\n  private static String modelNameConvertMappingPath(final String modelName) {\n    final String tableName = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, modelName);\n    return CodeGenerator.tableNameConvertMappingPath(tableName);\n  }\n\n  private static String packageConvertPath(final String packageName) {\n    return String.format(\n        \"/%s/\", packageName.contains(\".\") ? packageName.replaceAll(\"\\\\.\", \"/\") : packageName);\n  }\n}\n"
  },
  {
    "path": "back/src/test/java/JasyptStringEncryptor.java",
    "content": "import com.msy.plus.Application;\nimport org.jasypt.encryption.StringEncryptor;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.junit4.SpringRunner;\n\n/**\n * jasypt 用于加密配置文件 https://github.com/ulisesbocchio/jasypt-spring-boot\n *\n * @author MoShuying\n * @date 2018/05/27\n */\n@RunWith(SpringRunner.class)\n@SpringBootTest(classes = Application.class)\npublic class JasyptStringEncryptor {\n\n  @Qualifier(\"myStringEncryptor\")\n  @Autowired\n  private StringEncryptor stringEncryptor;\n\n  @Test\n  public void encode() throws Exception {\n    final String mysql = this.stringEncryptor.encrypt(\"com.mysql.cj.jdbc.Driver\");\n    final String name = this.stringEncryptor.encrypt(\"crm3\");\n    final String password = this.stringEncryptor.encrypt(\"123456\");\n\n    System.err.println(\"name = \" + name);\n    System.err.println(\"mysql = \" + mysql);\n    System.err.println(\"password = \" + password);\n  }\n}\n"
  },
  {
    "path": "back/src/test/java/PasswordEncryptor.java",
    "content": "import com.msy.plus.Application;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.security.crypto.password.PasswordEncoder;\nimport org.springframework.test.context.junit4.SpringRunner;\n\n/**\n * @author MoShuying\n * @date 2018/05/27\n */\n@RunWith(SpringRunner.class)\n@SpringBootTest(classes = Application.class)\npublic class PasswordEncryptor {\n\n  @Autowired private PasswordEncoder passwordEncoder;\n\n  @Test\n  public void encode() throws Exception {\n    final String admin = this.passwordEncoder.encode(\"admin\");\n    final String user = this.passwordEncoder.encode(\"1234657\");\n    System.err.println(\"admin password = \" + admin);\n    System.err.println(\"user password = \" + user);\n  }\n}\n//  MyEnc({cSs3wYoZ0BTijYqdYVj9xg==})\n"
  },
  {
    "path": "back/src/test/java/RsaEncryptor.java",
    "content": "import com.msy.plus.core.rsa.RsaUtils;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.springframework.util.Base64Utils;\n\nimport java.security.KeyPair;\nimport java.security.PrivateKey;\nimport java.security.PublicKey;\nimport java.util.Base64;\n\n/**\n * RSA工具测试\n *\n * @author MoShuying\n * @date 2018/05/27\n */\npublic class RsaEncryptor {\n  private final RsaUtils rsaUtil = new RsaUtils();\n\n  /** 加载公私钥pem格式文件测试 */\n  @Test\n  public void test1() throws Exception {\n    final PublicKey publicKey = this.rsaUtil.loadPublicKey();\n    final PrivateKey privateKey = this.rsaUtil.loadPrivateKey();\n    Assert.assertNotNull(publicKey);\n    Assert.assertNotNull(privateKey);\n    System.out.println(\"公钥：\" + publicKey);\n    System.out.println(\"私钥：\" + privateKey);\n\n    final String data = \"msy\";\n    // 公钥加密\n    final byte[] encrypted = this.rsaUtil.encrypt(data.getBytes());\n    System.out.println(\"加密后：\" + Base64Utils.encodeToString(encrypted));\n\n    // 私钥解密\n    final byte[] decrypted = this.rsaUtil.decrypt(encrypted);\n    System.out.println(\"解密后：\" + new String(decrypted));\n  }\n\n  /** 生成RSA密钥对并进行加解密测试 */\n  @Test\n  public void test2() throws Exception {\n    final String data = \"hello word\";\n    final KeyPair keyPair = RsaUtils.genKeyPair(512);\n\n    // 获取公钥，并以base64格式打印出来\n    final PublicKey publicKey = keyPair.getPublic();\n    System.out.println(\"公钥：\" + new String(Base64.getEncoder().encode(publicKey.getEncoded())));\n\n    // 获取私钥，并以base64格式打印出来\n    final PrivateKey privateKey = keyPair.getPrivate();\n    System.out.println(\"私钥：\" + new String(Base64.getEncoder().encode(privateKey.getEncoded())));\n\n    // 公钥加密\n    final byte[] encrypted = RsaUtils.encrypt(data.getBytes(), publicKey);\n    System.out.println(\"加密后：\" + new String(encrypted));\n\n    // 私钥解密\n    final byte[] decrypted = RsaUtils.decrypt(encrypted, privateKey);\n    System.out.println(\"解密后：\" + new String(decrypted));\n  }\n}\n"
  },
  {
    "path": "back/src/test/java/com/msy/plus/AccountControllerTest.java",
    "content": "package com.msy.plus;\n\nimport com.msy.plus.dto.AccountDTO;\nimport com.msy.plus.dto.AccountLoginDTO;\nimport org.junit.FixMethodOrder;\nimport org.junit.Test;\nimport org.junit.runners.MethodSorters;\n\n/**\n * 账户接口测试\n *\n * @author MoShuying\n * @date 2018/11/29\n */\n@FixMethodOrder(MethodSorters.NAME_ASCENDING)\npublic class AccountControllerTest extends BaseControllerTest {\n\n  private final String resource = \"/account\";\n\n  /** register */\n  @Test(timeout = 5000)\n  public void test1() throws Exception {\n    final String targetUrl = this.resource;\n    final AccountDTO account = new AccountDTO();\n    account.setEmail(\"12345@qq.com\");\n    account.setName(\"xxxxx\");\n    account.setPassword(\"12345\");\n    this.post(targetUrl, account, null);\n  }\n\n  /** login */\n  @Test(timeout = 5000)\n  public void test2() throws Exception {\n    final String targetUrl = this.resource + \"/token\";\n    final AccountLoginDTO accountLogin = new AccountLoginDTO();\n    accountLogin.setName(\"xxxxx\");\n    accountLogin.setPassword(\"12345\");\n    this.post(targetUrl, accountLogin, null);\n  }\n\n  /** logout */\n  @Test(timeout = 5000)\n  public void test3() throws Exception {\n    final String targetUrl = this.resource + \"/token\";\n    final AccountLoginDTO accountLogin = new AccountLoginDTO();\n    accountLogin.setName(\"admin\");\n    accountLogin.setPassword(\"admin\");\n    // 先登录获取token\n    final String token = (String) this.post(targetUrl, accountLogin, null).getData();\n    this.delete(targetUrl, null, token);\n  }\n\n  /** update */\n  @Test(timeout = 5000)\n  @WithCustomUser(name = \"user\")\n  public void test4() throws Exception {\n    final String targetUrl = this.resource;\n    final AccountDTO accountDTO = new AccountDTO();\n    accountDTO.setName(\"user\");\n    accountDTO.setEmail(\"xxxxx@qq.com\");\n    this.patch(targetUrl, accountDTO, null);\n  }\n\n  /** detail */\n  @Test(timeout = 5000)\n  @WithCustomUser(name = \"xxxxx\")\n  public void test5() throws Exception {\n    final String targetUrl = this.resource + \"/3\";\n    this.get(targetUrl, null, null);\n  }\n\n  /** list */\n  @Test(timeout = 5000)\n  @WithCustomUser(name = \"user\")\n  public void test6() throws Exception {\n    final String targetUrl = this.resource + \"?page=1&size=3\";\n    this.get(targetUrl, null, null);\n  }\n\n  /** delete */\n  @Test(timeout = 5000)\n  @WithCustomUser(name = \"admin\")\n  public void test7() throws Exception {\n    final String targetUrl = this.resource + \"/3\";\n    this.delete(targetUrl, null, null);\n  }\n}\n"
  },
  {
    "path": "back/src/test/java/com/msy/plus/BaseControllerTest.java",
    "content": "package com.msy.plus;\n\nimport com.alibaba.fastjson.JSON;\nimport com.msy.plus.core.response.Result;\nimport com.msy.plus.filter.AuthenticationFilter;\nimport com.msy.plus.filter.CorsFilter;\nimport org.apache.commons.lang3.StringUtils;\nimport org.junit.Before;\nimport org.junit.runner.RunWith;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.annotation.Rollback;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.test.web.servlet.MockMvc;\nimport org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;\nimport org.springframework.test.web.servlet.request.MockMvcRequestBuilders;\nimport org.springframework.test.web.servlet.setup.MockMvcBuilders;\nimport org.springframework.transaction.annotation.Transactional;\nimport org.springframework.web.context.WebApplicationContext;\n\nimport static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;\nimport static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;\nimport static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;\nimport static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;\n\n/**\n * MockMvc 测试控制器\n *\n * @author MoShuying\n * @date 2018/11/29\n */\n// 测试时如果为 true，就不会修改数据库数据\n@Rollback(value = false)\n@Transactional\n@AutoConfigureMockMvc\n@RunWith(SpringJUnit4ClassRunner.class)\n@SpringBootTest(classes = {Application.class})\npublic abstract class BaseControllerTest {\n  @Autowired protected WebApplicationContext context;\n  @Autowired protected CorsFilter corsFilter;\n  @Autowired protected AuthenticationFilter authenticationFilter;\n\n  protected MockMvc mockMvc;\n\n  @Before\n  public void setUp() {\n    this.mockMvc =\n        MockMvcBuilders.webAppContextSetup(this.context)\n            .addFilters(this.corsFilter)\n            // 不添加则无法维持 SecurityContext\n            .apply(springSecurity())\n            .build();\n  }\n\n  private Result execute(\n      final HttpMethod method, final String targetUrl, final Object args, final String token)\n      throws Exception {\n    final MockHttpServletRequestBuilder builders =\n        MockMvcRequestBuilders.request(method, targetUrl)\n            .contentType(MediaType.APPLICATION_JSON);\n    if (args != null) {\n      builders.content(JSON.toJSONString(args));\n    }\n    if (StringUtils.isNotBlank(token)) {\n      builders.header(\"Authorization\", token);\n    }\n    return JSON.parseObject(\n        this.mockMvc\n            .perform(builders)\n            .andDo(print())\n            .andExpect(status().isOk())\n            .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))\n            .andReturn()\n            .getResponse()\n            .getContentAsString(),\n        Result.class);\n  }\n\n  protected Result get(final String targetUrl, final Object args, final String token)\n      throws Exception {\n    return this.execute(HttpMethod.GET, targetUrl, args, token);\n  }\n\n  protected Result post(final String targetUrl, final Object args, final String token)\n      throws Exception {\n    return this.execute(HttpMethod.POST, targetUrl, args, token);\n  }\n\n  protected Result delete(final String targetUrl, final Object args, final String token)\n      throws Exception {\n    return this.execute(HttpMethod.DELETE, targetUrl, args, token);\n  }\n\n  protected Result patch(final String targetUrl, final Object args, final String token)\n      throws Exception {\n    return this.execute(HttpMethod.PATCH, targetUrl, args, token);\n  }\n}\n"
  },
  {
    "path": "back/src/test/java/com/msy/plus/WithCustomSecurityContextFactory.java",
    "content": "package com.msy.plus;\n\nimport com.msy.plus.service.impl.UserDetailsServiceImpl;\nimport org.springframework.security.authentication.UsernamePasswordAuthenticationToken;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.security.core.userdetails.UserDetails;\nimport org.springframework.security.test.context.support.WithSecurityContextFactory;\n\nimport javax.annotation.Resource;\n\n/**\n * 设置用户登陆时的 SecurityContext\n *\n * @author MoShuying\n * @date 2018/11/29\n */\npublic class WithCustomSecurityContextFactory\n    implements WithSecurityContextFactory<WithCustomUser> {\n  @Resource private UserDetailsServiceImpl userDetailsService;\n\n  @Override\n  public SecurityContext createSecurityContext(final WithCustomUser customUser) {\n    final SecurityContext context = SecurityContextHolder.createEmptyContext();\n    final UserDetails userDetails = this.userDetailsService.loadUserByUsername(customUser.name());\n    final Authentication auth =\n        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());\n    context.setAuthentication(auth);\n    return context;\n  }\n}\n"
  },
  {
    "path": "back/src/test/java/com/msy/plus/WithCustomUser.java",
    "content": "package com.msy.plus;\n\nimport org.springframework.security.test.context.support.WithSecurityContext;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * 访问控制器时以某用户已登录状态操作\n *\n * @author MoShuying\n * @date 2018/11/29\n */\n@Retention(RetentionPolicy.RUNTIME)\n@WithSecurityContext(factory = WithCustomSecurityContextFactory.class)\npublic @interface WithCustomUser {\n  String name() default \"admin\";\n}\n"
  },
  {
    "path": "back/src/test/java/com/msy/plus/util/JsonUtilsTest.java",
    "content": "package com.msy.plus.util;\n\nimport com.alibaba.fastjson.JSONObject;\nimport com.msy.plus.entity.AccountDO;\nimport org.junit.Test;\n\nimport java.util.Arrays;\nimport java.util.List;\n\npublic class JsonUtilsTest {\n  private List<AccountDO> getAccountList() {\n    final AccountDO account1 =\n        new AccountDO() {\n          {\n            this.setName(\"ll\");\n            this.setId(1L);\n            this.setEmail(\"ll@qq.com\");\n            this.setPassword(\"llllllllllll\");\n          }\n        };\n\n    final AccountDO account2 =\n        new AccountDO() {\n          {\n            this.setName(\"aa\");\n            this.setId(2L);\n            this.setEmail(\"aa@qq.com\");\n            this.setPassword(\"aaaaaaaaaa\");\n          }\n        };\n    return Arrays.asList(account1, account2);\n  }\n\n  @Test\n  public void keepFields() throws Exception {\n    final List<JSONObject> accountList =\n        JsonUtils.keepFields(this.getAccountList(), List.class, \"password\");\n    for (final JSONObject account : accountList) {\n      assert account.get(\"id\") == null;\n      assert account.get(\"password\") != null;\n    }\n  }\n\n  @Test\n  public void deleteFields() throws Exception {\n    final List<JSONObject> accountList =\n        JsonUtils.deleteFields(this.getAccountList(), List.class, \"password\");\n    for (final JSONObject account : accountList) {\n      assert account.get(\"id\") != null;\n      assert account.get(\"password\") == null;\n    }\n  }\n}\n"
  },
  {
    "path": "back/src/test/resources/generator/template/controller-restful.ftl",
    "content": "package ${basePackage}.controller;\n\nimport ${basePackage}.core.response.Result;\nimport ${basePackage}.core.response.ResultGenerator;\nimport ${basePackage}.entity.${modelNameUpperCamel};\nimport ${basePackage}.service.${modelNameUpperCamel}Service;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.annotations.Api;\nimport io.swagger.annotations.ApiImplicitParam;\nimport io.swagger.annotations.ApiImplicitParams;\nimport io.swagger.annotations.ApiOperation;\nimport org.springframework.security.access.prepost.PreAuthorize;\nimport com.github.pagehelper.PageHelper;\nimport com.github.pagehelper.PageInfo;\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.annotation.Resource;\nimport java.util.List;\n\n/**\n* @author ${author}\n* @date ${date}\n*/\n@PreAuthorize(\"hasAuthority('ADMIN')\")\n@Api(tags={\"生成接口\"})\n@RestController\n@RequestMapping(\"${baseRequestMapping}\")\npublic class ${modelNameUpperCamel}Controller {\n    @Resource\n    private ${modelNameUpperCamel}Service ${modelNameLowerCamel}Service;\n\n    @Operation(description = \"生成添加\")\n    @PostMapping\n    public Result add(@RequestBody ${modelNameUpperCamel} ${modelNameLowerCamel}) {\n        ${modelNameLowerCamel}Service.save(${modelNameLowerCamel});\n        return ResultGenerator.genOkResult();\n    }\n\n    @Operation(description = \"生成删除\")\n    @DeleteMapping(\"/{id}\")\n    public Result delete(@PathVariable Long id) {\n    ${modelNameLowerCamel}Service.deleteById(id);\n        return ResultGenerator.genOkResult();\n    }\n\n    @Operation(description = \"生成更新\")\n    @PutMapping\n    public Result update(@RequestBody ${modelNameUpperCamel} ${modelNameLowerCamel}) {\n    ${modelNameLowerCamel}Service.update(${modelNameLowerCamel});\n        return ResultGenerator.genOkResult();\n    }\n\n    @Operation(description = \"生成获取详细信息\")\n    @GetMapping(\"/{id}\")\n    public Result detail(@PathVariable Long id) {\n    ${modelNameUpperCamel} ${modelNameLowerCamel} = ${modelNameLowerCamel}Service.getById(id);\n        return ResultGenerator.genOkResult(${modelNameLowerCamel});\n    }\n\n    @Operation(description = \"生成分页查询\")\n    @GetMapping\n    @ApiOperation(value=\"分页查询生成\", notes=\"分页查询 \")\n    @ApiImplicitParams({\n        @ApiImplicitParam(name = \"page\", value = \"第几页\", required = true, dataType = \"Integer\", paramType=\"query\"),\n        @ApiImplicitParam(name = \"size\", value = \"一页有几条\", required = true, dataType = \"Integer\", paramType=\"query\")\n    })\n    public Result list(@RequestParam(defaultValue = \"1\") Integer page,\n    @RequestParam(defaultValue = \"10\") Integer size) {\n        PageHelper.startPage(page, size);\n        List<${modelNameUpperCamel}> list = ${modelNameLowerCamel}Service.listAll();\n        PageInfo<${modelNameUpperCamel}> pageInfo = PageInfo.of(list);\n        return ResultGenerator.genOkResult(pageInfo);\n    }\n}\n"
  },
  {
    "path": "back/src/test/resources/generator/template/controller.ftl",
    "content": "package ${basePackage}.controller;\n\nimport ${basePackage}.core.response.Result;\nimport ${basePackage}.core.response.ResultGenerator;\nimport ${basePackage}.entity.${modelNameUpperCamel};\nimport ${basePackage}.service.${modelNameUpperCamel}Service;\nimport com.github.pagehelper.PageHelper;\nimport com.github.pagehelper.PageInfo;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.springframework.security.access.prepost.PreAuthorize;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport javax.annotation.Resource;\nimport java.util.List;\n\n/**\n* @author ${author}\n* @date ${date}\n*/\n@PreAuthorize(\"hasAuthority('ADMIN')\")\n@Tag(name = \"接口\")\n@RestController\n@RequestMapping(\"${baseRequestMapping}\")\npublic class ${modelNameUpperCamel}Controller {\n    @Resource\n    private ${modelNameUpperCamel}Service ${modelNameLowerCamel}Service;\n\n    @Operation(description = \"添加\")\n    @PostMapping(\"/add\")\n    public Result add(${modelNameUpperCamel} ${modelNameLowerCamel}) {\n        ${modelNameLowerCamel}Service.save(${modelNameLowerCamel});\n        return ResultGenerator.genOkResult();\n    }\n\n    @Operation(description = \"删除\")\n    @PostMapping(\"/delete\")\n    public Result delete(@RequestParam Long id) {\n        ${modelNameLowerCamel}Service.deleteById(id);\n        return ResultGenerator.genOkResult();\n    }\n\n    @Operation(description = \"更新\")\n    @PostMapping(\"/update\")\n    public Result update(${modelNameUpperCamel} ${modelNameLowerCamel}) {\n        ${modelNameLowerCamel}Service.update(${modelNameLowerCamel});\n        return ResultGenerator.genOkResult();\n    }\n\n    @Operation(description = \"获取详细信息\")\n    @PostMapping(\"/detail\")\n    public Result detail(@RequestParam Long id) {\n        ${modelNameUpperCamel} ${modelNameLowerCamel} = ${modelNameLowerCamel}Service.getById(id);\n        return ResultGenerator.genOkResult(${modelNameLowerCamel});\n    }\n\n    @Operation(description = \"分页查询\")\n    @PostMapping(\"/list\")\n    public Result list(@RequestParam(defaultValue = \"0\") Integer page,\n        @RequestParam(defaultValue = \"0\") Integer size) {\n        PageHelper.startPage(page, size);\n        List<${modelNameUpperCamel}> list = ${modelNameLowerCamel}Service.listAll();\n        PageInfo<${modelNameUpperCamel}> pageInfo = PageInfo.of(list);\n        return ResultGenerator.genOkResult(pageInfo);\n    }\n}\n"
  },
  {
    "path": "back/src/test/resources/generator/template/service-impl.ftl",
    "content": "package ${basePackage}.service.impl;\n\nimport ${basePackage}.mapper.${modelNameUpperCamel}Mapper;\nimport ${basePackage}.entity.${modelNameUpperCamel};\nimport ${basePackage}.service.${modelNameUpperCamel}Service;\nimport ${basePackage}.core.service.AbstractService;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport javax.annotation.Resource;\n\n/**\n* @author ${author}\n* @date ${date}\n*/\n@Service\n@Transactional(rollbackFor = Exception.class)\npublic class ${modelNameUpperCamel}ServiceImpl extends AbstractService<${modelNameUpperCamel}> implements ${modelNameUpperCamel}Service {\n    @Resource\n    private ${modelNameUpperCamel}Mapper ${modelNameLowerCamel}Mapper;\n\n}\n"
  },
  {
    "path": "back/src/test/resources/generator/template/service.ftl",
    "content": "package ${basePackage}.service;\n\nimport ${basePackage}.entity.${modelNameUpperCamel};\nimport ${basePackage}.core.service.Service;\n\n/**\n* @author ${author}\n* @date ${date}\n*/\npublic interface ${modelNameUpperCamel}Service extends Service<${modelNameUpperCamel}> {\n\n}\n"
  },
  {
    "path": "back/src/test/resources/sql/dev/account.sql",
    "content": "-- MySQL dump 10.16  Distrib 10.1.34-MariaDB, for Linux (x86_64)\n--\n-- Host: localhost    Database: seedling_dev\n-- ------------------------------------------------------\n-- Server version\t10.1.34-MariaDB\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!40101 SET NAMES utf8 */;\n/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n/*!40103 SET TIME_ZONE='+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n--\n-- Table structure for table `account`\n--\n\nDROP TABLE IF EXISTS `account`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `account` (\n  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '账户Id',\n  `email` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '邮箱',\n  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '账户名',\n  `password` varchar(256) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '密码',\n  `register_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',\n  `login_time` datetime DEFAULT NULL COMMENT '上一次登录时间',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `idx_account_name` (`name`),\n  UNIQUE KEY `idx_account_email` (`email`)\n) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='账户表';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `account`\n--\n\nLOCK TABLES `account` WRITE;\n/*!40000 ALTER TABLE `account` DISABLE KEYS */;\nINSERT INTO `account` VALUES (1,'admin@qq.com','admin','$2a$10$OG1zaFHT2LUy4SGcQ4EnRu9sPQMjMGEE6jARz61aQwRQ3316N6ikG','2018-01-01 00:00:00','2018-02-01 00:00:00');\nINSERT INTO `account` VALUES (2,'user@qq.com','user','$2a$10$yjfcoyNWgoUh3QQ3I6Lwmux57rCz3mZP1j8V4BK60EIVdwT3SkwFO','2018-01-01 00:00:00','2018-02-01 00:00:00');\n/*!40000 ALTER TABLE `account` ENABLE KEYS */;\nUNLOCK TABLES;\n/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;\n\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n\n-- Dump completed on 2018-07-14 16:51:45\n"
  },
  {
    "path": "back/src/test/resources/sql/dev/account_role.sql",
    "content": "-- MySQL dump 10.16  Distrib 10.1.34-MariaDB, for Linux (x86_64)\n--\n-- Host: localhost    Database: seedling_dev\n-- ------------------------------------------------------\n-- Server version\t10.1.34-MariaDB\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!40101 SET NAMES utf8 */;\n/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n/*!40103 SET TIME_ZONE='+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n--\n-- Table structure for table `account_role`\n--\n\nDROP TABLE IF EXISTS `account_role`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `account_role` (\n  `account_id` bigint(20) unsigned NOT NULL COMMENT '账户Id',\n  `role_id` bigint(20) unsigned NOT NULL COMMENT '角色Id',\n  PRIMARY KEY (`account_id`,`role_id`),\n  KEY `fk_ref_role` (`role_id`),\n  CONSTRAINT `fk_ref_account` FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,\n  CONSTRAINT `fk_ref_role` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账户角色表';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `account_role`\n--\n\nLOCK TABLES `account_role` WRITE;\n/*!40000 ALTER TABLE `account_role` DISABLE KEYS */;\nINSERT INTO `account_role` VALUES (1,2);\nINSERT INTO `account_role` VALUES (1,3);\nINSERT INTO `account_role` VALUES (2,1);\n/*!40000 ALTER TABLE `account_role` ENABLE KEYS */;\nUNLOCK TABLES;\n/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;\n\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n\n-- Dump completed on 2018-07-14 16:51:59\n"
  },
  {
    "path": "back/src/test/resources/sql/dev/role.sql",
    "content": "-- MySQL dump 10.16  Distrib 10.1.34-MariaDB, for Linux (x86_64)\n--\n-- Host: localhost    Database: seedling_dev\n-- ------------------------------------------------------\n-- Server version\t10.1.34-MariaDB\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!40101 SET NAMES utf8 */;\n/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n/*!40103 SET TIME_ZONE='+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n--\n-- Table structure for table `role`\n--\n\nDROP TABLE IF EXISTS `role`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `role` (\n  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '角色Id',\n  `name` varchar(64) DEFAULT NULL COMMENT '角色名称',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `name` (`name`)\n) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `role`\n--\n\nLOCK TABLES `role` WRITE;\n/*!40000 ALTER TABLE `role` DISABLE KEYS */;\nINSERT INTO `role` VALUES (1,'USER');\nINSERT INTO `role` VALUES (2,'ADMIN');\nINSERT INTO `role` VALUES (3,'TEST');\n/*!40000 ALTER TABLE `role` ENABLE KEYS */;\nUNLOCK TABLES;\n/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;\n\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n\n-- Dump completed on 2018-07-14 16:51:33\n"
  },
  {
    "path": "back/src/test/rest-test/upload.http",
    "content": "// https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html\nPOST http://0.0.0.0:8080/account/token\nAccept: application/json;charset=UTF-8\nCache-Control: no-cache\nContent-Type: application/json;charset=UTF-8\n\n{\n  \"name\": \"admin\",\n  \"password\": \"admin\"\n}\n\n> {% client.global.set(\"token\", response.body.data); %}\n\n###\n\nPOST http://0.0.0.0:8080/upload\nAuthorization: Bearer {{token}}\nContent-Type: multipart/form-data; boundary=boundary\n\n--boundary\nContent-Disposition: form-data; name=\"file\"; filename=\"README.md\"\nContent-Type: application/octet-stream\n\n// 上传的文件路径\n< /home/zoctan/spring-boot-api-seedling/README.md\n\n###\n\n"
  },
  {
    "path": "front/.github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": "front/.github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": "front/.gitignore",
    "content": ".DS_Store\nnode_modules/\ndist/\nadmindb/\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n/test/unit/coverage/\n/test/e2e/reports/\nselenium-debug.log\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\npackage-lock.json\n.env.production.local\n"
  },
  {
    "path": "front/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 iczer\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "front/README.en-US.md",
    "content": "[简体中文](./README.md) | English\n<h1 align=\"center\">Vue Antd Admin</h1>\n\n<div align=\"center\">\n  \n[Ant Design Pro](https://github.com/ant-design/ant-design-pro)'s implementation with Vue.  \nAn out-of-box UI solution for enterprise applications as a React boilerplate.\n\n[![MIT](https://img.shields.io/github/license/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/blob/master/LICENSE)\n[![Dependence](https://img.shields.io/david/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin)\n[![DevDependencies](https://img.shields.io/david/dev/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin?type=dev)\n[![Release](https://img.shields.io/github/v/release/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/releases/latest)\n![image](./src/assets/img/preview.png)   \n\nMultiple theme modes available：  \n![image](./src/assets/img/preview-nine.png)\n</div>\n\n- Preview：https://iczer.gitee.io/vue-antd-admin\n- Documentation：https://iczer.gitee.io/vue-antd-admin-docs\n- FAQ：https://iczer.gitee.io/vue-antd-admin-docs/start/faq.html\n- Mirror Repo in China：https://gitee.com/iczer/vue-antd-admin\n\n## Browsers support\nModern browsers and IE10.\n\n| [<img src=\"https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png\" alt=\"IE / Edge\" width=\"24px\" height=\"24px\" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src=\"https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png\" alt=\"Firefox\" width=\"24px\" height=\"24px\" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src=\"https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png\" alt=\"Chrome\" width=\"24px\" height=\"24px\" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src=\"https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png\" alt=\"Safari\" width=\"24px\" height=\"24px\" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src=\"https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png\" alt=\"Opera\" width=\"24px\" height=\"24px\" />](http://godban.github.io/browsers-support-badges/)</br>Opera |\n| --- | --- | --- | --- | --- |\n| IE10, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions |\n\n## Usage\n### clone\n```bash\n$ git clone https://github.com/iczer/vue-antd-admin.git\n```\n### yarn\n```bash\n$ yarn install\n$ yarn serve\n```\n### or npm\n```\n$ npm install\n$ npm run serve\n```\nMore instructions at [documentation](https://iczer.gitee.io/vue-antd-admin-docs).\n\n## Contributing\nAny type of contribution is welcome, here are some examples of how you may contribute to this project: :star2:：\n- Use Vue Antd Admin in your daily work.\n- Submit [Issue](https://github.com/iczer/vue-antd-admin/issues) to report :bug: or ask questions.\n- Propose [Pull Request](https://github.com/iczer/vue-antd-admin/pulls) to improve our code.\n- Join the community and share your experiences with us. QQ Group:942083829、812277510(full)、610090280(full)\n"
  },
  {
    "path": "front/README.md",
    "content": "简体中文 | [English](./README.en-US.md)\n<h1 align=\"center\">Vue Antd Admin</h1>\n\n<div align=\"center\">\n  \n[Ant Design Pro](https://github.com/ant-design/ant-design-pro) 的 Vue 实现版本  \n开箱即用的中后台前端/设计解决方案\n\n[![MIT](https://img.shields.io/github/license/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/blob/master/LICENSE)\n[![Dependence](https://img.shields.io/david/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin)\n[![DevDependencies](https://img.shields.io/david/dev/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin?type=dev)\n[![Release](https://img.shields.io/github/v/release/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/releases/latest)\n![image](./src/assets/img/preview.png)  \n\n多种主题模式可选：  \n![image](./src/assets/img/preview-nine.png)\n</div>\n\n- 预览地址：https://iczer.gitee.io/vue-antd-admin\n- 使用文档：https://iczer.gitee.io/vue-antd-admin-docs\n- 常见问题：https://iczer.gitee.io/vue-antd-admin-docs/start/faq.html\n- 国内镜像：https://gitee.com/iczer/vue-antd-admin\n\n## 浏览器支持\n现代浏览器及 IE10\n\n| [<img src=\"https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png\" alt=\"IE / Edge\" width=\"24px\" height=\"24px\" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src=\"https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png\" alt=\"Firefox\" width=\"24px\" height=\"24px\" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src=\"https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png\" alt=\"Chrome\" width=\"24px\" height=\"24px\" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src=\"https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png\" alt=\"Safari\" width=\"24px\" height=\"24px\" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src=\"https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png\" alt=\"Opera\" width=\"24px\" height=\"24px\" />](http://godban.github.io/browsers-support-badges/)</br>Opera |\n| --- | --- | --- | --- | --- |\n| IE10, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions |\n\n## 使用\n### clone\n```bash\n$ git clone https://github.com/iczer/vue-antd-admin.git\n```\n### yarn\n```bash\n$ yarn install\n$ yarn serve\n```\n### or npm\n```\n$ npm install\n$ npm run serve\n```\n更多信息参考 [使用文档](https://iczer.gitee.io/vue-antd-admin-docs)\n\n## 参与贡献\n我们非常欢迎你的贡献，你可以通过以下方式和我们一起共建 :star2:：\n- 在你的公司或个人项目中使用 Vue Antd Admin。\n- 通过 [Issue](https://github.com/iczer/vue-antd-admin/issues) 报告:bug:或进行咨询。\n- 提交 [Pull Request](https://github.com/iczer/vue-antd-admin/pulls) 改进 Admin 的代码。\n- 加入社群，与小伙伴们一同交流心得。QQ群：942083829、 812277510（已满）、610090280（已满）\n\n## 打赏\n如果该项目对您有所帮助，可以请作者喝一杯咖啡。\n<p>\n  <img src=\"./src/assets/img/alipay.png\" width=\"320px\" style=\"display: inline-block;\" />\n  <img src=\"./src/assets/img/wechatpay.png\" width=\"320px\" style=\"display: inline-block; margin-left: 24px;\" />\n</p>\n"
  },
  {
    "path": "front/babel.config.js",
    "content": "const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)\n\nconst plugins = []\nif (IS_PROD) {\n  plugins.push('transform-remove-console')\n}\n\nmodule.exports = {\n  presets: [\n    '@vue/cli-plugin-babel/preset'\n  ],\n  plugins\n}\n"
  },
  {
    "path": "front/docs/.vuepress/components/Alert.vue",
    "content": "<template>\n  <div class=\"alert\" :style=\"`top: ${top}px`\">\n    <slot></slot>\n  </div>\n</template>\n\n<script>\n  export default {\n    name: 'Alert',\n    props: ['show'],\n    data() {\n      return {\n        top: 100\n      }\n    },\n    mounted() {\n      console.log(this)\n      // this.$page.alert = this.$page.alert ? this.$page.alert : {top: 100}\n      // this.$page.alert.top += 20\n      // this.top = this.$page.alert.top\n      setTimeout(() => {\n        this.$el.remove()\n      }, 1000)\n    }\n  }\n</script>\n\n<style scoped>\n  .alert{\n    position: absolute;\n    padding: 6px 8px;\n    background-color: #f0f2f5;\n    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);\n    border-radius: 4px;\n    margin: 0 auto;\n    z-index: 999;\n    top: 100px;\n    width: fit-content;\n    left: 0;\n    right: 0;\n  }\n</style>"
  },
  {
    "path": "front/docs/.vuepress/components/Color.vue",
    "content": "<template>\n  <div :data-clipboard-text=\"color\" class=\"color\" @click=\"onClick\" :style=\"`background-color:${color}`\" />\n</template>\n\n<script>\n  import Clipboard from 'clipboard'\n  export default {\n    name: 'Color',\n    props: ['color'],\n    data() {\n      return {\n        alert: false\n      }\n    },\n    methods: {\n      onClick() {\n        let clipboard = new Clipboard('.color')\n        clipboard.on('success', () => {\n          this.$alert(`颜色代码已复制：${this.color}`)\n          clipboard.destroy()\n        })\n      }\n    }\n  }\n</script>\n\n<style scoped>\n  .color{\n    border: 1px dashed #a0d911;\n    display: inline-block;\n    width: 20px;\n    height: 20px;\n    cursor: pointer;\n  }\n</style>"
  },
  {
    "path": "front/docs/.vuepress/components/ColorList.vue",
    "content": "<template>\n  <div>\n    <color class=\"color\" :key=\"index\" v-for=\"(color, index) in colors\" :color=\"color\" ></color>\n  </div>\n</template>\n\n<script>\n  export default {\n    name: 'ColorList',\n    props: ['colors']\n  }\n</script>\n\n<style scoped>\n  .color{\n    margin: 0 2px;\n  }\n</style>"
  },
  {
    "path": "front/docs/.vuepress/config.js",
    "content": "module.exports = {\n  title: 'Vue Antd Admin',\n  description: 'Vue Antd Admin',\n  base: '/vue-antd-admin-docs/',\n  head: [\n    ['link', { rel: 'icon', href: '/favicon.ico' }]\n  ],\n  themeConfig: {\n    logo: '/logo.png',\n    repo: 'iczer/vue-antd-admin',\n    docsDir: 'docs',\n    editLinks: true,\n    editLinkText: '在 Github 上帮助我们编辑此页',\n    nav: [\n      {text: '指南', link: '/'},\n      {text: '配置', link: '/develop/layout'},\n      {text: '主题', link: '/advance/theme'},\n    ],\n    lastUpdated: 'Last Updated',\n    sidebar: [\n      {\n        title: '开始',\n        collapsable: false,\n        children: [\n          '/start/use', '/start/faq'\n        ]\n      },\n      {\n        title: '开发',\n        collapsable: false,\n        children: [\n          '/develop/layout', '/develop/router', '/develop/page', '/develop/theme', '/develop/service', '/develop/mock'\n        ]\n      },\n      {\n        title: '进阶',\n        collapsable: false,\n        children: [\n          '/advance/i18n', '/advance/async', '/advance/authority', '/advance/login', '/advance/guard', '/advance/interceptors',\n          '/advance/api'\n        ]\n      },\n      {\n        title: '其它',\n        collapsable: false,\n        children: [\n          '/other/upgrade', '/other/community'\n        ]\n      }\n    ],\n    nextLinks: true,\n    prevLinks: true,\n  },\n  plugins: ['@vuepress/back-to-top', require('./plugins/alert')],\n  markdown: {\n    lineNumbers: true\n  }\n}\n"
  },
  {
    "path": "front/docs/.vuepress/plugins/alert/Alert.vue",
    "content": "<template>\n  <div class=\"alert\" :style=\"`top: ${top}px`\">\n    <slot></slot>\n  </div>\n</template>\n\n<script>\n  export default {\n    name: 'Alert',\n    props: ['alert'],\n    data() {\n      return {\n        top: 0\n      }\n    },\n    beforeMount() {\n      this.top = this.alert.top\n    },\n    mounted() {\n     window.addEventListener('alert_remove', (e) => {\n       this.top -= e.detail.height\n     })\n    },\n    watch: {\n      'page.alert.top': function (value) {\n      }\n    }\n  }\n</script>\n\n<style scoped>\n  .alert{\n    position: fixed;\n    padding: 6px 8px;\n    background-color: #fff;\n    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.25);\n    border-radius: 4px;\n    margin: 0 auto;\n    z-index: 999;\n    top: 100px;\n    width: fit-content;\n    left: 0;\n    right: 0;\n    transition: top 0.3s;\n  }\n</style>"
  },
  {
    "path": "front/docs/.vuepress/plugins/alert/alertMixin.js",
    "content": "import Alert from './Alert'\n\nconst AlertMixin = {\n  install(Vue) {\n    Vue.mixin({\n      methods: {\n        $alert(message, duration = 2000) {\n          let Constructor= Vue.extend(Alert)\n          let alert = new Constructor()\n          alert.$slots.default = message\n          alert.$props.alert = this.$page.alert\n          alert.$mount()\n          document.body.appendChild(alert.$el)\n\n          const appendHeight = alert.$el.offsetHeight + 16\n          this.$page.alert.top += appendHeight\n\n          setTimeout(() => {\n            this.$page.alert.top -= appendHeight\n            this.triggerRemoveAlert(appendHeight)\n            setTimeout(() => {\n              alert.$destroy()\n              alert.$el.remove()\n            }, 100)\n          }, duration)\n        },\n        triggerRemoveAlert(height) {\n          const event = new CustomEvent('alert_remove', {\n            detail: {height}\n          })\n          window.dispatchEvent(event)\n        }\n      }\n    })\n  }\n}\n\nexport default AlertMixin\n"
  },
  {
    "path": "front/docs/.vuepress/plugins/alert/clientRootMixin.js",
    "content": "export default {\n  updated() {\n    this.$page.alert.top = 100\n  }\n}\n"
  },
  {
    "path": "front/docs/.vuepress/plugins/alert/enhanceApp.js",
    "content": "import AlertMixin from './alertMixin'\n\nexport default ({Vue}) => {\n  Vue.use(AlertMixin)\n}"
  },
  {
    "path": "front/docs/.vuepress/plugins/alert/index.js",
    "content": "const path = require('path')\n\nmodule.exports = (options, ctx) => {\n  return {\n    clientRootMixin: path.resolve(__dirname, 'clientRootMixin.js'),\n    extendPageData($page) {\n      $page.alert = {\n        top: 100\n      }\n    },\n    enhanceAppFiles: path.resolve(__dirname, 'enhanceApp.js')\n  }\n}"
  },
  {
    "path": "front/docs/.vuepress/styles/index.styl",
    "content": ".custom-block.tip{\n  border-color: #1890ff\n}\n.theme-default-content code .token.inserted{\n  color: #60bd90;\n}\n//.custom-block.warning{\n//  border-color: #fa8c16\n//}\n//.custom-block.error{\n//  border-color: #f5222d\n//}\n"
  },
  {
    "path": "front/docs/.vuepress/styles/palette.styl",
    "content": "$accentColor = #1890ff\n$contentWidth = 940px\n"
  },
  {
    "path": "front/docs/README.md",
    "content": "---\ntitle: 首页\nhome: true\nheroImage: /logo.png\nheroText: Vue Antd Admin\ntagline: 开箱即用的中台前端/设计解决方案\nactionText: 快速上手 →\nactionLink: /start/use\nfeatures:\n- title: 简洁\n  details: 以 Markdown 为中心的项目结构，以最少的配置帮助你专注于写作。\n- title: 优雅\n  details: 享受 Vue + webpack 的开发体验，在 Markdown 中使用 Vue 组件，同时可以使用 Vue 来开发自定义主题。\n- title: 自然\n  details: VuePress 为每个页面预渲染生成静态的 HTML，同时在页面被加载的时候，将作为 SPA 运行。\nfooter: MIT Licensed | Copyright © 2018-present iczer\n---\n"
  },
  {
    "path": "front/docs/advance/README.md",
    "content": "---\ntitle: 进阶\nlang: zn-CN\n---\n# 进阶\n"
  },
  {
    "path": "front/docs/advance/api.md",
    "content": "---\ntitle: 全局API\nlang: zn-CN\n---\n# 全局API\n我们提供了一些全局Api，在日常功能开发中或许会有帮助，它们均被绑定到了页面组件或子组件实例上。  \n在组件内可以直接通过`this.$[apiName]`的方式调用。如下：\n\n## 多页签\n### $closePage(closeRoute, nextRoute)\n该api用于关闭当前已打开的页签，接收两个参数：\n* **closeRoute**  \n要关闭的页签对应的 route 对象，可简写为路由的 fullPath 字符串值。\n* **nextRoute**  \n关闭页签要后跳转的 route 对象，可不传，不传则会自动选择打开页签（临近原则）。\n\n### $refreshPage(route)\n该api用于刷新路由对应的页签，接收一个参数：\n* **route**  \n要刷新的页签对应的 route 对象，可简写为路由的 fullPath 字符串值。\n\n### $openPage(route, title)\n该api用于打开一个新页签，接收两个参数：\n* **route**  \n要打开的页签对应的 route 对象，可简写为路由的 fullPath 字符串值。\n* **title**  \n设置打开页签的标题，可不传。\n\n### $setPageTitle(route, title)\n该api用于设置页签的标题，接收两个参数：\n* **route**  \n要设置的页签对应的 route 对象，可简写为路由的 fullPath 字符串值。\n* **title**  \n页签的标题。\n\n## 权限\n### $auth(check, type)\n该api可以用于操作权限校验，接收两个参数：\n* **check**  \n需要要校验的操作权限\n* **type**  \n操作权限校验类别，可选 `permission` 和 `role`，即通过权限校验还是角色进行校验，可不传（不传的话，会对两种类型都进行匹配，任意一种匹配成功即校验通过）。"
  },
  {
    "path": "front/docs/advance/async.md",
    "content": "---\ntitle: 异步路由和菜单\nlang: zn-CN\n---\n# 异步路由和菜单\n在现实业务中，存在这样的场景，系统的路由和菜单会根据用户的角色变化而变化，或者路由菜单根据用户的权限动态生成。我们为此准备了一套完整的异步加载方案，\n可以让你很方便的从服务端加载路由和菜单配置，并应用到系统中。\n## 异步加载路由\n动态路由的实现主要有以下四个步骤：\n### 开启异步路由设置\n在 `/config/config.js` 文件中设置 `asyncRoutes` 的值为 true:\n```js {7}\nmodule.exports = {\n  theme: {\n    color: '#13c2c2',\n    mode: 'night'\n  },\n  multiPage: true,\n  asyncRoutes: true,       //异步加载路由，true:开启，false:不开启\n  animate: {\n    name: 'roll',\n    direction: 'default'\n  }\n}\n```\n### 注册路由组件\n基础路由组件包含路由基本配置和对应的视图组件，我们统一在 `/router/async/router.map.js` 文件中注册它们。它和正常的路由配置基本无异，相当于把完整的路由拆分成单个的路由配置进行注册，为后面的路由动态配置打好基础。  \n一个单独的路由组件注册示例如下：\n```jsx\nregisterName: {                               //路由组件注册名称，唯一标识\n  path: 'path',                               //路由path，可缺省，默认取路由注册名称 registerName 的值\n  name: '演示页',                             //路由名称\n  redirect: '/login',                         //路由重定向\n  component: () => import('@/pages/demo'),    //路由视图\n  icon: 'permission',                         //路由的菜单icon，会注入到路由元数据meta中\n  invisible: false,                           //是否隐藏菜单项，true 隐藏，false 不隐藏，会注入到路由元数据meta中。\n  authority: {                                //路由权限配置，会注入到路由元数据meta中。可缺省，默认为 ‘*’, 即无权限限制\n    permission: 'form',                       //路由需要的权限  \n    role: 'admin'                             //路由需要的角色。当permission未设置，通过 role 检查权限\n  },                     \n  page: {                                     //路由的页面数据，会注入到路由元数据meta中\n    title: '演示页',                          //页面标题\n    breadcrumb: ['首页', '演示页']            //页面面包屑\n  }             \n}\n```\n\n:::details 点击查看完整的路由注册示例：\n```js\n// 视图组件\nconst view = {\n  tabs: () => import('@/layouts/tabs'),\n  blank: () => import('@/layouts/BlankView'),\n  page: () => import('@/layouts/PageView')\n}\n\n// 路由组件注册\nconst routerMap = {\n  login: {\n    authority: '*',\n    path: '/login',\n    component: () => import('@/pages/login')\n  },\n  demo: {\n    name: '演示页',\n    renderMenu: false,\n    component: () => import('@/pages/demo')\n  },\n  exp403: {\n    authority: '*',\n    name: 'exp403',\n    path: '403',\n    component: () => import('@/pages/exception/403')\n  },\n  exp404: {\n    name: 'exp404',\n    path: '404',\n    component: () => import('@/pages/exception/404')\n  },\n  exp500: {\n    name: 'exp500',\n    path: '500',\n    component: () => import('@/pages/exception/500')\n  },\n  root: {\n    path: '/',\n    name: '首页',\n    redirect: '/login',\n    component: view.tabs\n  },\n  parent1: {\n    name: '父级路由1',\n    icon: 'dashboard',\n    component: view.blank\n  },\n  parent2: {\n    name: '父级路由2',\n    icon: 'form',\n    component: view.page\n  },\n  exception: {\n    name: '异常页',\n    icon: 'warning',\n    component: view.blank\n  }\n}\nexport default routerMap\n```\n:::\n### 配置基本路由\n如果没有任何路由，你的应用是无法访问的，所以我们需要在本地配置一些基本的路由，比如登录页、404、403 等。你可以在 `/router/async/config.async.js` 文件中配置一些本地必要的路由。如下：\n```js\nconst routesConfig = [\n  'login',                      //匹配 router.map.js 中注册的 registerName = login 的路由\n  'root',                       //匹配 router.map.js 中注册的 registerName = root 的路由\n  {\n    router: 'exp404',           //匹配 router.map.js 中注册的 registerName = exp404 的路由\n    path: '*',                  //重写 exp404 路由的 path 属性\n    name: '404'                 //重写 exp404 路由的 name 属性\n  },\n  {\n    router: 'exp403',           //匹配 router.map.js 中注册的 registerName = exp403 的路由\n    path: '/403',               //重写 exp403 路由的 path 属性\n    name: '403'                 //重写 exp403 路由的 name 属性\n  }\n]\n```\n完成配置后，即可通过 `routesConfig` 和已注册的 `routerMap` 生成 [router.options.routes](https://router.vuejs.org/zh/api/#router-%E6%9E%84%E5%BB%BA%E9%80%89%E9%A1%B9) 配置，如下：\n```js\nconst options = {\n  routes: parseRoutes(routesConfig, routerMap)\n}\n```\n:::details 点击查看完整的 config.async.js 代码\n```js\nimport routerMap from './router.map'\nimport {parseRoutes} from '@/utils/routerUtil'\n\n// 异步路由配置\nconst routesConfig = [\n  'login',\n  'root',\n  {\n    router: 'exp404',\n    path: '*',\n    name: '404'\n  },\n  {\n    router: 'exp403',\n    path: '/403',\n    name: '403'\n  }\n]\nconst options = {\n  routes: parseRoutes(routesConfig, routerMap)\n}\nexport default options\n```\n:::\n完成以上设置后，本地就已经有了包含 login、404、403 页面的路由，并且这些路由是可以直接访问的。\n### 异步获取路由配置\n当用户登录后（或者其它的前提条件），你可能想根据不同用户加载不同的路由和菜单。\n那么我们就需要先从后端服务获取异步路由配置，后端返回的异步路由配置 `routesConfig` 是一个异步路由配置数组， 应当如下格式：\n```jsx\n[{\n  router: 'root',                           //匹配 router.map.js 中注册名 registerName = root 的路由\n  children: [                               //root 路由的子路由配置\n    {\n      router: 'dashboard',                  //匹配 router.map.js 中注册名 registerName = dashboard 的路由\n      children: ['workplace', 'analysis'],  //dashboard 路由的子路由配置，依次匹配 registerName 为 workplace 和 analysis 的路由\n    },\n    {\n      router: 'form',                       //匹配 router.map.js 中注册名 registerName = form 的路由\n      children: [                           //form 路由的子路由配置\n        'basicForm',                        //匹配 router.map.js 中注册名 registerName = basicForm 的路由\n        'stepForm',                         //匹配 router.map.js 中注册名 registerName = stepForm 的路由\n        {\n          router: 'advanceForm',            //匹配 router.map.js 中注册名 registerName = advanceForm 的路由\n          path: 'advance'                   //重写 advanceForm 路由的 path 属性\n        }\n      ]   \n    },\n    {\n      router: 'basicForm',                  //匹配 router.map.js 中注册名 registerName = basicForm 的路由\n      name: '验权表单',                     //重写 basicForm 路由的 name 属性\n      icon: 'file-excel',                   //重写 basicForm 路由的 icon 属性\n      authority: 'form'                     //重写 basicForm 路由的 authority 属性\n    }\n  ]\n}]\n```\n其中 `router` 属性 对应 `router.map.js` 中已注册的`基础路由`的注册名称 `registerName`，`children` 属性为路由的嵌套子路由配置。  \n有些情况下你可能想重写已注册路由的属性，你可以为 `routesConfig` 配置同名属性去覆盖它。如上面的`验权表单`路由覆盖了注册路由的 `name`、`icon`、`authority` 属性。\n\n### 加载路由并应用\n我们提供了一个路由加载工具，你只需调用 `/utils/routerUtil.js` 中的 `loadRoutes` 方法加载上一步获取到的 `routesConfig` 即可，如下：\n```js {3}\ngetRoutesConfig().then(result => {\n  const routesConfig = result.data.data\n  loadRoutes(routesConfig)\n})\n```\n至此，异步路由的加载就完成了，你可以访问异步加载的路由了。\n:::tip\n上面获取异步路由的代码，在 /pages/login/Login.vue 文件中可以找到。   \nloadRoutes 方法会合并 /router/async/config.async.js 文件中配置的基本路由。\n:::\n:::details 点击查看 loadRoutes 的详细代码\n```js\n/**\n * 加载路由\n * @param routesConfig 路由配置\n */\nfunction loadRoutes(routesConfig) {\n  // 如果 routesConfig 有值，则更新到本地，否则从本地获取\n  if (routesConfig) {\n    store.commit('account/setRoutesConfig', routesConfig)\n  } else {\n    routesConfig = store.getters['account/routesConfig']\n  }\n  // 如果开启了异步路由，则加载异步路由配置\n  const asyncRoutes = store.state.setting.asyncRoutes\n  if (asyncRoutes) {\n    if (routesConfig && routesConfig.length > 0) {\n      const routes = parseRoutes(routesConfig, routerMap)\n      formatAuthority(routes)\n      const finalRoutes = mergeRoutes(router.options.routes, routes)\n      router.options = {...router.options, routes: finalRoutes}\n      router.matcher = new Router({...router.options, routes:[]}).matcher\n      router.addRoutes(finalRoutes)\n    }\n  }\n  // 初始化Admin后台菜单数据\n  const rootRoute = router.options.routes.find(item => item.path === '/')\n  const menuRoutes = rootRoute && rootRoute.children\n  if (menuRoutes) {\n    mergeI18nFromRoutes(i18n, menuRoutes)\n    store.commit('setting/setMenuData', menuRoutes)\n  }\n}\n```\n:::\n\n## 异步加载菜单\nVue Antd Admin 的菜单，是根据路由配置自动生成的，默认获取根路由 `‘/’` 下所有子路由作为菜单配置。  \n当你完成了异步路由的加载，菜单也会随之改变，无需你做其它额外的操作。主要代码如下：\n```js\n// 初始化Admin后台菜单数据\n  const rootRoute = router.options.routes.find(item => item.path === '/')\n  const menuRoutes = rootRoute && rootRoute.children\n  if (menuRoutes) {\n    mergeI18nFromRoutes(i18n, menuRoutes)\n    store.commit('setting/setMenuData', menuRoutes)\n  }\n```\n:::tip\n如果你不想从根路由 `‘/’` 下获取菜单数据，可以根据自己的需求更改。\n:::\n"
  },
  {
    "path": "front/docs/advance/authority.md",
    "content": "---\ntitle: 权限管理\nlang: zn-CN\n---\n# 权限管理\n权限控制是中后台系统中常见的需求之一，你可以利用 Vue Antd Admin 提供的权限控制脚手架，实现一些基本的权限控制功能。\n## 角色和权限\n通常情况下有两种方式可以控制用户权限，一种是通过用户角色 role 来控制权限，另一种是通过更细致的权限 permission 来控制。\n这两种方式 Vue Antd Admin 都支持。  \n我们定义了 role 和 permission 的基本格式，如果你获取的 role 和 permission 数据格式与 Vue Antd Admin 不一致，\n你需要在获取到 role 和 permission 后将其转换为 Vue Antd Admin 的格式。\n### 角色\nVue Antd Admin 的 `角色/role` 包含 `id` 和 `operation` 两个属性。其中 `id` 为 `角色/role` 的 id，`operation` 为 `角色/role` 具有的操作权限，是一个字符串数组。\n```js\nrole = {\n  id: 'admin',                                   //角色ID\n  operation: ['add', 'delete', 'edit', 'close']  //角色的操作权限\n}\n```\n你也可以设置 role 的值为字符串，比如 role = 'admin', 它等同于：\n```js\nrole = {\n  id: 'admin'\n}\n```\n### 权限\nVue Antd Admin 的 `权限/permission` 也包含 `id` 和 `operation` 两个属性。其中 `id` 为 `权限/permission` 的 id，`operation` 为 `权限/permission` 下的操作权限，是一个字符串数组。\n```js\npermission = {\n  id: 'form',                                    //权限ID\n  operation: ['add', 'delete', 'edit', 'close']  //权限下的操作权限\n}\n```\n你也可以设置 role 的值为字符串，比如 permission = 'form', 它等同于：\n```js\npermission = {\n  id: 'form'\n}\n```\n### 设置用户的角色和权限\n你只需为用户配置 roles 和 permissions 两者中的其中一种，即可完成权限管理功能。当然你也可以两者都配置。 \n \n获取到用户权限或角色后，将其格式化转为 Vue Antd Admin 可用的格式，然后使用 `store.commit('account/setPermissions', permissions)` 或 `store.commit('account/setRoles', roles)`\n将其存在本地即可。如下：\n```js\ngetPermissions().then(res => {\n  const permissions = res.data\n  this.$store.commit('account/setPermissions', permissions)\n})\ngetRoles().then(res => {\n  const roles = res.data\n  this.$store.commit('account/setRoles', roles)\n})\n```\n:::tip\n注意，存在本地的 permissions 和 roles 都应该是数组。  \n你可以在 /pages/login/Login.vue 查看完整的用户角色和权限设置代码。\n:::\n## 页面权限\n如果你想给一些页面设置准入权限，只需要给该页面对应的路由设置元数据 authority 即可。 authority 的值可以是一个字符串，也可以是对象。  \n\n如下路由配置，则表明 `验权页面` 需要准入权限(permission): `form`\n```js {5}\nconst route = {\n  name: '验权页面',\n  path: 'auth/demo',\n  meta: {\n    authority: 'form',\n  },\n  component: () => import('@/pages/demo')\n}\n```\n下面是 authority 的值为对象的写法，这种写法和上面字符串的写法具有相同的效果：\n```js {5-7}\nconst route = {\n  name: '验权页面',\n  path: 'auth/demo',\n  meta: {\n    authority: {\n      permission: 'form'\n    }\n  },\n  component: () => import('@/pages/demo')\n}\n```\n有时你可能需要通过用户角色来配置页面权限，我们同样支持，用法和上面类似。  \n\n如下配置，表明 `验权页面` 需要准入角色(role) `admin`：\n```js {5-7}\nconst route = {\n  name: '验权页面',\n  path: 'auth/demo',\n  meta: {\n    authority: {\n      role: 'admin'\n    }\n  },\n  component: () => import('@/pages/demo')\n}\n```\n:::tip\n当你未设置 authority 或 设置 authority 的值 为 `*` 时，等同于该页面无需权限限制，我们会忽略此页面的权限检查。\n:::\n:::tip\n当 authority 的值为字符串时，会以 [权限/permission](#权限) 验证权限。如果你需要以 [角色/role](#角色) 验证权限，请以对象形式设置 authority 的值。\n:::\n## 操作权限\n在一些复杂的些场景下，权限可能不仅仅是页面层级这么简单。在一些页面你可能需要校验用户是否具有某些操作的权限，比如 增、删、改、查等。  \n为此，我们提供了 `权限校验注入` 和 `权限校验指令` 两个实用的功能。\n### 权限校验注入\n通过对Vue组件的实例方法进行 `权限校验注入`，我们可以控制该实例方法的执行权限，从而精准且安全的验证用户操作。  \n\n比如，QueryList 页面的 deleteRecord 方法，我们希望具有操作权限 `delete` 的用户才能调用此方法。\n只需为 `deleteRecord` 方法注入权限校验，按如下方式配置 `authorize` 即可：\n```vue {9-11,13}\n<template>\n  ...\n</template>\n<script>\n...\nexport default {\n  name: 'QueryList',\n  data () {...},\n  authorize: {              //权限校验注入设置\n    deleteRecord: 'delete'  //key为需要注入权限校验的方法名，这里为 deleteRecord 方法；值为需要校验的操作权限，这里为 delete 操作权限\n  },\n  methods: {\n    deleteRecord(key) {\n      this.dataSource = this.dataSource.filter(item => item.key !== key)\n      this.selectedRows = this.selectedRows.filter(item => item.key !== key)\n    },\n    ...\n  }\n}\n</script>\n```\n如果用户没有 `delete` 权限，调用 deleteRecord 方法，会看到如下提示：  \n\n![无此权限](../assets/permission.png)\n### 操作权限校验的类型\n`authorize` 会根据当前页面匹配到的权限类型([permission](#权限) / [role](#角色))，来判断是使用 `permission.operation` 还是 `role.operation` 来进行权限校验。\n如果当前页面同时匹配到了 permission 和 role 权限，则默认通过 permission.operation 来进行操作权限校验。  \n\n当然你也可以指定操作权限校验的类型，如下设置即可：\n```js {2-5}\nauthorize: {             \n  deleteRecord: {        //需要 注入权限校验 的方法名：deleteRecord\n    check: 'delete',     //需要校验的操作权限：check\n    type: 'role'         //指定操作权限校验的类型，可选 permission 和 role。这里指定以 role.operation 校验操作权限\n  }\n}\n```\n### 权限校验指令\n有时我们可能希望用户能够更直观的了解自己的操作权限。比如给没有操作权限的控件应用 disable 样式，禁用 click 事件等。\n我们提供了权限校验指令 `v-auth` 来实现这个功能。  \n\n比如，我们想为 QueryList 页面的删除控件进行 `delete` 操作权限校验，只需为删除控件设置 v-auth=\"\\`delete\\`\" 指令即可，如下：\n```vue {6}\n<template>\n  <a-card>...\n    <standard-table ...>\n      ...\n      <div slot=\"action\" slot-scope=\"{text, record}\">\n        <a @click=\"deleteRecord(record.key)\" v-auth=\"`delete`\">\n          <a-icon type=\"delete\" />删除\n        </a>\n      </div>\n      ...\n    </standard-table>\n  </a-card>\n</template>\n```\n假如用户没有 `delete` 操作权限，则控件会被应用 disable 样式，且 click 事件无效，如下图：  \n\n![权限校验指令](../assets/auth.png)\n:::warning 重要！！！\nv-auth 是我们自定义的一个 [Vue指令](https://cn.vuejs.org/v2/guide/custom-directive.html#ad)。因为 `Vue指令` 的值需要是一个 javascript 表达式，因此你不能直接给 v-auth 赋值为字符串，\n需要把 v-auth 的字符串值用 ` `` ` 包裹起来，否则可能会报 undefined 错误。\n:::\n### 权限校验指令的类型\n你同样也可以指定 v-auth 的权限校验类型，可选 [permission](#权限) 和 [role](#角色)。它的校验方式和 [authorize](#权限校验注入) 类似，如未指定则会自动识别。\n`v-auth:role` 表示通过 `role.operation` 进行校验，`v-auth:permission` 表示通过 `permission.operation` 进行校验。 \n \n如下，指定通过 `role.operation` 校验删除控件的操作权限：\n```vue {3}\n<div slot=\"action\" slot-scope=\"{text, record}\">\n  ...\n  <a v-auth:role=\"`delete`\">\n    <a-icon type=\"delete\" />删除\n  </a>\n  ...\n</div>\n```\n## 异步路由权限\n异步路由同样可以进行权限校验配置，它和正常的路由权限配置基本无异，只是无需把 [authority](#页面权限) 配置在元数据属性 meta 里。\n你可以在路由组件注册时设置 authority，也可以在异步路由配置里设置 authority。  \n\n路由组件注册时设置 [authority](#页面权限)：\n```js {6}\n// 路由组件注册\nconst routerMap = {\n  ...\n  demo: {\n    name: '演示页',\n    authority: 'form',\n    component: () => import('@/pages/demo')\n  }\n  ...\n}\n```\n\n异步路由配置里设置 [authority](#页面权限)：\n```js {11-13}\nconst routesConfig = [{\n    router: 'root',\n    children: ['demo',\n      {router: 'parent1'...},\n      ...\n      {\n        router: 'demo',\n        icon: 'file-ppt',\n        path: 'auth/demo',\n        name: '验权页面',\n        authority: {\n          permission: 'form',\n        }\n      }\n    ]\n  }]\n```\n"
  },
  {
    "path": "front/docs/advance/chart.md",
    "content": "---\ntitle: 图表\nlang: zn-CN\n---\n# 图表\n\n### 作者还没来得及编辑该页面，如果你感兴趣，可以点击下方链接，帮助作者完善此页\n"
  },
  {
    "path": "front/docs/advance/error.md",
    "content": "---\ntitle: 错误处理\nlang: zn-CN\n---\n# 错误处理\n\n### 作者还没来得及编辑该页面，如果你感兴趣，可以点击下方链接，帮助作者完善此页\n"
  },
  {
    "path": "front/docs/advance/guard.md",
    "content": "---\ntitle: 路由守卫\nlang: zn-CN\n---\n# 路由守卫\nVue Antd Admin 使用 vue-router 实现路由导航功能，因此可以为路由配置一些守卫。  \n我们统一把导航守卫配置在 router/guards.js 文件中。\n\n## 前置守卫\nVue Antd Admin 为每个前置导航守卫函数注入 to,from,next,options 四个参数：\n* `to: Route`: 即将要进入的目标[路由对象](https://router.vuejs.org/zh/api/#%E8%B7%AF%E7%94%B1%E5%AF%B9%E8%B1%A1)\n* `from: Route`: 当前导航正要离开的路由对象\n* `next: Function`: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。详情查看 [Vue Router #导航守卫](https://router.vuejs.org/zh/guide/advanced/navigation-guards.html)\n* `options: Object`: 应用配置，包含: {router, i18n, store, message}，可根据需要扩展。  \n如下，是登录拦截导航守卫的定义\n```js\nconst loginGuard = (to, from, next, options) => {\n  const {message} = options\n  if (!loginIgnore.includes(to) && !checkAuthorization()) {\n    message.warning('登录已失效，请重新登录')\n    next({path: '/login'})\n  } else {\n    next()\n  }\n}\n```\n\n## 后置守卫\n你也可以定义后置导航守卫，Vue Antd Admin 为每个后置导航函数注入 to,from,options 三个参数：\n* `to: Route`: 即将要进入的目标[路由对象](https://router.vuejs.org/zh/api/#%E8%B7%AF%E7%94%B1%E5%AF%B9%E8%B1%A1)\n* `from: Route`: 当前导航正要离开的路由对象\n* `options: Object`: 应用配置，包含: {router, i18n, store, message}，可根据需要扩展。  \n如下，是一个后置导航守卫的定义\n```js\nconst afterGuard = (to, from, options) => {\n  const {store, message} = options\n  // 做些什么\n  message.info('do something')\n}\n```\n\n## 导出守卫配置\n定义好导航守卫后，只需按照类别在 guard.js 中导出即可。分为两类，`前置守卫`和`后置守卫`。如下：\n```js\nexport default {\n  beforeEach: [loginGuard, authorityGuard],\n  afterEach: [afterGuard]\n}\n```\n\n:::details 点击查看完整的导航守卫配置\n```js\nimport {loginIgnore} from '@/router/index'\nimport {checkAuthorization} from '@/utils/request'\n\n/**\n * 登录守卫\n * @param to\n * @param form\n * @param next\n * @param options\n */\nconst loginGuard = (to, from, next, options) => {\n  const {message} = options\n  if (!loginIgnore.includes(to) && !checkAuthorization()) {\n    message.warning('登录已失效，请重新登录')\n    next({path: '/login'})\n  } else {\n    next()\n  }\n}\n\n/**\n * 权限守卫\n * @param to\n * @param form\n * @param next\n * @param options\n */\nconst authorityGuard = (to, from, next, options) => {\n  const {store, message} = options\n  const permissions = store.getters['account/permissions']\n  const roles = store.getters['account/roles']\n  if (!hasAuthority(to, permissions, roles)) {\n    message.warning(`对不起，您无权访问页面: ${to.fullPath}，请联系管理员`)\n    next({path: '/403'})\n  } else {\n    next()\n  }\n}\n\n/**\n * 后置守卫\n * @param to\n * @param form\n * @param options\n */\nconst afterGuard = (to, from, options) => {\n  const {store, message} = options\n  // 做些什么\n  message.info('do something')\n}\n\nexport default {\n  beforeEach: [loginGuard, authorityGuard],\n  afterEach: [afterGuard]\n}\n```\n:::"
  },
  {
    "path": "front/docs/advance/i18n.md",
    "content": "---\ntitle: 国际化\nlang: zn-CN\n---\n# 国际化\nvue-antd-admin 采用 [vue-i18n](https://kazupon.github.io/vue-i18n/) 插件来实现国际化，该项目已经内置并且加载好了基础配置。可以直接上手使用。\n\n> 如果你还没有看快速入门，请先移步查看: [页面 -> i18n国际化配置](../develop/page.html#i18n国际化配置)\n\n\n## 菜单和路由\n\n### 默认情况\n如果你没有对菜单进行国际化配置，admin 默认会从路由数据中提取数据作为国际化配置。route.name 作为中文语言，route.path 作为英文语言。  \n国际化提取函数定义在 `@/utils/i18n.js` 文件中，会在路由加载时调用，如下：\n```js\n/**\n * 从路由提取国际化数据\n * @param i18n\n * @param routes\n */\nfunction mergeI18nFromRoutes(i18n, routes) {\n  formatFullPath(routes)\n  const CN = generateI18n(new Object(), routes, 'name')\n  const US = generateI18n(new Object(), routes, 'path')\n  i18n.mergeLocaleMessage('CN', CN)\n  i18n.mergeLocaleMessage('US', US)\n  const messages = routesI18n.messages\n  Object.keys(messages).forEach(lang => {\n    i18n.mergeLocaleMessage(lang, messages[lang])\n  })\n}\n```\n### 自定义\n如果你想自定义菜单国际化数据，可在 `@/router/i18n.js` 文件中配置。我们以路由的 path 作为 key（嵌套path 的写法也会被解析），name 作为 国际化语言的值。    \n假设你有一个路由的配置如下：\n```js\n[{\n  path: 'parent',\n  ...\n  children: [{\n    path: 'self',\n    ...\n  }]\n}]\n\nor \n\n[{\n  path: 'other',\n  ...\n  children: [{\n    path: '/parent/self',   // 在国际化配置中 key 会解析为 parent.self\n    ...\n  }]\n}]\n```\n那么你需要在 `@/router/i18n.js` 中这样配置：\n```jsx\n messages: {\n    CN: {\n      parent: {\n        name: '父級菜單',\n        self: {name: '菜單名'},\n    },\n    US: {\n      parent: {\n        name: 'parent menu',\n        self: {name: 'menu name'},\n    },\n    HK: {\n      parent: {\n        name: '父級菜單',\n        self: {name: '菜單名'},\n    },\n```\n\n## 添加语言\n\n首先在 `@/layouts/header/AdminHeader.vue` ，新增一门语言 (多个同理)。\n\n```vue {15}\n<template>\n  ...\n</template>\n<script>\n...\nexport default {\n  ...\n  data() {\n    return {\n      langList: [\n        {key: 'CN', name: '简体中文', alias: '简体'},\n        {key: 'HK', name: '繁體中文', alias: '繁體'},\n        {key: 'US', name: 'English', alias: 'English'},\n        // 新增一个语言选项, key是i18n的索引，name是菜单显示名称\n        {key: 'JP', name: 'Japanese', alias: 'Japanese'}\n      ],\n      searchActive: false\n    }\n  },\n}\n</script>\n```\n\n> TIP: 后续开发建议把这里改成动态配置的方式！\n\n然后开始往 `@/router/i18n.js` 和 `@/pages/你的页面/i18n.js` 里面分别添加上语言的翻译。\n\n```vue {12,13,14}\nmodule.exports = {\n    messages: {\n        CN: {\n            home: {name: '首页'},\n        },\n        US: {\n            home: {name: 'home'},\n        },\n        HK: {\n            home: {name: '首頁'},\n        },\n        JP: {\n            home: {name: '最初のページ'},\n        },\n    }\n}\n```\n\n> Notice: 更多用法请移步到 [vue-i18n](https://kazupon.github.io/vue-i18n/) 。"
  },
  {
    "path": "front/docs/advance/interceptors.md",
    "content": "---\ntitle: 拦截器配置\nlang: zn-CN\n---\n# 拦截器配置\nVue Antd Admin 基于 aixos 封装了 http 通信功能，我们可以为 http 请求响应配置一些拦截器。拦截器统一配置在 /utils/axios-interceptors.js 文件中。\n## 请求拦截器\n你可以为每个请求拦截器配置 `onFulfilled` 或 `onRejected` 两个钩子函数。\n### onFulfilled\n我们会为 onFulfilled 钩子函数注入 config 和 options 两个参数：\n* `config: AxiosRequestConfig`: axios 请求配置，详情参考 [axios 请求配置](http://www.axios-js.com/zh-cn/docs/#%E8%AF%B7%E6%B1%82%E9%85%8D%E7%BD%AE)\n* `options: Object`: 应用配置，包含: {router, i18n, store, message}，可根据需要扩展。\n\n### onRejected\n我们会为 onFulfilled 钩子函数注入 error 和 options 两个参数：\n* `error: Error`: axios 请求错误对象\n* `options: Object`: 应用配置，包含: {router, i18n, store, message}，可根据需要扩展。  \n  \n如下，为一个完整的请求拦截器配置：\n```js\nconst tokenCheck = {\n  // 发送请求之前做些什么\n  onFulfilled(config, options) {\n    const {message} = options\n    const {url, xsrfCookieName} = config\n    if (url.indexOf('login') === -1 && xsrfCookieName && !Cookie.get(xsrfCookieName)) {\n      message.warning('认证 token 已过期，请重新登录')\n    }\n    return config\n  },\n  // 请求出错时做点什么\n  onRejected(error, options) {\n    const {message} = options\n    message.error(error.message)\n    return Promise.reject(error)\n  }\n}\n```\n## 响应拦截器\n响应拦截器也同样可以配置 `onFulfilled` 或 `onRejected` 两个钩子函数。\n### onFulfilled\n我们会为 onFulfilled 钩子函数注入 response 和 options 两个参数：\n* `response: AxiosResponse`: axios 响应对象，详情参考 [axios 响应对象](http://www.axios-js.com/zh-cn/docs/#%E5%93%8D%E5%BA%94%E7%BB%93%E6%9E%84)\n* `options: Object`: 应用配置，包含: {router, i18n, store, message}，可根据需要扩展。\n\n### onRejected\n我们会为 onFulfilled 钩子函数注入 error 和 options 两个参数：\n* `error: Error`: axios 请求错误对象\n* `options: Object`: 应用配置，包含: {router, i18n, store, message}，可根据需要扩展。 \n\n如下，为一个完整的响应拦截器配置：\n```js\nconst resp401 = {\n  // 响应数据之前做点什么\n  onFulfilled(response, options) {\n    const {message} = options\n    if (response.status === 401) {\n      message.error('无此接口权限')\n    }\n    return response\n  },\n  // 响应出错时做点什么\n  onRejected(error, options) {\n    const {message} = options\n    if (response.status === 401) {\n      message.error('无此接口权限')\n    }\n    return Promise.reject(error)\n  }\n}\n```\n## 导出拦截器\n定义好拦截器后，只需在 axios-interceptors.js 文件中导出即可。分为两类，`请求拦截器`和`响应拦截器`。如下：\n```js\nexport default {\n  request: [tokenCheck], // 请求拦截\n  response: [resp401] // 响应拦截\n}\n```\n\n:::details 点击查看完整的拦截器配置示例\n```js\nimport Cookie from 'js-cookie'\n// 401拦截\nconst resp401 = {\n  onFulfilled(response, options) {\n    const {message} = options\n    if (response.status === 401) {\n      message.error('无此接口权限')\n    }\n    return response\n  },\n  onRejected(error, options) {\n    const {message} = options\n    message.error(error.message)\n    return Promise.reject(error)\n  }\n}\n\nconst resp403 = {\n  onFulfilled(response, options) {\n    const {message} = options\n    if (response.status === 403) {\n      message.error(`请求被拒绝`)\n    }\n    return response\n  }\n}\n\nconst reqCommon = {\n  onFulfilled(config, options) {\n    const {message} = options\n    const {url, xsrfCookieName} = config\n    if (url.indexOf('login') === -1 && xsrfCookieName && !Cookie.get(xsrfCookieName)) {\n      message.warning('认证 token 已过期，请重新登录')\n    }\n    return config\n  },\n  onRejected(error, options) {\n    const {message} = options\n    message.error(error.message)\n    return Promise.reject(error)\n  }\n}\n\nexport default {\n  request: [reqCommon], // 请求拦截\n  response: [resp401, resp403] // 响应拦截\n}\n```\n:::"
  },
  {
    "path": "front/docs/advance/login.md",
    "content": "---\ntitle: 登录认证\nlang: zn-CN\n---\n# 登录认证\nVue Antd Admin 使用 js-cookie.js 管理用户的 token，结合 axios 配置，可以为每个请求头加上 token 信息。\n\n## token名称\n后端系统通常会从请求 header 中获取用户的 token，因此我们需要配置好 token 名称，好让后端能正确的识别到用户 token。\nVue Antd Admin 默认token 名称为 `Authorization`，你可以在 /utils/request.js 中修改它。\n```js{5}\nimport axios from 'axios'\nimport Cookie from 'js-cookie'\n\n// 跨域认证信息 header 名\nconst xsrfHeaderName = 'Authorization'\n...\n```\n## token 设置\n调用登录接口后拿到用户的 token 和 token 过期时间（如无过期时间，可忽略），并使用 /utils/request.js #setAuthorization 方法保存token。\n```js{5}\nimport {setAuthorization} from '@/utils/request'\n\nlogin(name, password).then(res => {\n  const {token, expireAt} = res.data\n  setAuthorization({token, expireAt: new Date(expireAt)})\n})\n```\n## token 校验\nVue Antd Admin 默认添加了登录导航守卫，如检查到本地cookie 中不包含 token 信息，则会拦截跳转至登录页。你可以在 /router/index.js 中配置\n不需要登录拦截的路由\n```js\n// 不需要登录拦截的路由配置\nconst loginIgnore = {\n  names: ['404', '403'],      //根据路由名称匹配\n  paths: ['/login'],   //根据路由fullPath匹配\n  /**\n   * 判断路由是否包含在该配置中\n   * @param route vue-router 的 route 对象\n   * @returns {boolean}\n   */\n  includes(route) {\n    return this.names.includes(route.name) || this.paths.includes(route.path)\n  }\n}\n```\n或者在 /router/guards.js 中移出登录守卫\n```diff\n...\nexport default {\n-  beforeEach: [loginGuard, authorityGuard, redirectGuard],\n+  beforeEach: [authorityGuard, redirectGuard],\n  afterEach: []\n}\n```\n## Api\n### setAuthorization(auth, authType)\n来源：/utils/request.js  \n该方法用于保存用户 token，接收两个参数:  \n* **auth**   \n认证信息，包含 token、expireAt 等认证数据。  \n* **authType**  \n认证类型，默认为 `AUTH_TYPE.BEARER`（AUTH_TYPE.BEARER 默认会给token 加上 Bearer 识别前缀），可根据自己的认证类型自行扩展。  \n\n### checkAuthorization(authType)\n该方法用于校验用户 token 是否过期，接收一个参数:  \n* **authType**  \n认证类型，默认为 `AUTH_TYPE.BEARER`。 \n\n### removeAuthorization(authType)\n该方法用于移出用户本地存储的 token，接收一个参数:  \n* **authType**  \n认证类型，默认为 `AUTH_TYPE.BEARER`。\n:::tip\n以上 Api 均可在 /utils/request.js 文件中找到。\n:::"
  },
  {
    "path": "front/docs/advance/skill.md",
    "content": "---\ntitle: 108个小技巧\nlang: zn-CN\n---\n# 108个小技巧\n\n## 自定义菜单icon\n## 隐藏页面标题\n## 关闭页签API\n## 权限校验PI\n"
  },
  {
    "path": "front/docs/advance/theme.md",
    "content": "---\ntitle: 更换主题\nlang: zn-CN\n---\n# 更换主题\n\n### 作者还没来得及编辑该页面，如果你感兴趣，可以点击下方链接，帮助作者完善此页\n"
  },
  {
    "path": "front/docs/develop/README.md",
    "content": "---\ntitle: 开发\nlang: zh-CN\n---\n# 开发\n"
  },
  {
    "path": "front/docs/develop/layout.md",
    "content": "---\ntitle: 布局\nlang: zh-CN\n---\n# 布局\n页面整体布局是一个产品最外层的框架结构，往往会包含导航、页脚、侧边栏、通知栏以及内容等。在页面之中，也有很多区块的布局结构。在真实项目中，页面布局通常统领整个应用的界面，有非常重要的作用。\n\n## Admin 的布局\n在 Vue Antd Admin 中，我们抽离了使用过程中一些常用的布局，都放在 layouts 目录中，分别为：\n* [AdminLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/AdminLayout.vue) / **管理后台布局**，包含了头部导航，侧边导航、内容区和页脚，一般用于后台系统的整体布局\n\n![admin-layout](../assets/admin-layout.png)\n* [PageLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageLayout.vue) / **页面布局**，包含了页头和内容区，常用于需要页头（包含面包屑、标题、额外操作等）的页面\n\n![page-layout](../assets/page-layout.png)\n* [CommonLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/CommonLayout.vue) / **通用布局**，仅包含内容区和页脚的简单布局，项目中常用于注册、登录或展示页面\n\n![common-layout](../assets/common-layout.png)\n## Admin 的视图\n在 Vue Antd Admin 中，除了基本布局外，通常有很多页面的结构是相似的。因此，我们把这部分结构抽离为视图组件。  \n一个视图组件通常包含一个基本布局组件、视图公共区块、路由视图内容区、页脚等，常常结合路由配置使用。它们也被放入了 layouts 目录中，分别为：\n* [TabsView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/TabsView.vue) / **多页签视图**，包含了 AdminLayout 布局、多页签头和路由视图内容区\n\n![tabs-view](../assets/tabs-view.png)\n* [PageView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageView.vue) / **页面视图**，包含了 PageLayout 布局和路由视图内容区\n\n![page-view](../assets/page-view.png)\n* [BlankView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/BlankView.vue) / **空白视图**，仅包含一个路由视图内容区\n\n![blank-view](../assets/blank-view.png)\n## 如何使用\n通常我们会把视图组件和路由配置结合一起使用，我们把配置信息抽离在路由配置文件中 [src/router/config.js](https://github.com/iczer/vue-antd-admin/blob/master/src/router/config.js) 。如下：\n```jsx {7,12}\n{\n  path: 'form',\n  name: '表单页',\n  meta: {\n    icon: 'form',\n  },\n  component: PageView,\n  children: [\n    {\n      path: 'basic',\n      name: '基础表单',\n      component: () => import('@/pages/form/basic/BasicForm'),\n    }\n  ]\n}\n```\n当然，如果这满足不了你的需求，你也可以自定义一些视图组件，或者直接在页面组件中使用布局。参考\n[workplace](https://github.com/iczer/vue-antd-admin/blob/master/src/pages/dashboard/workplace/WorkPlace.vue) 页面:\n```vue {2,13}\n<template>\n  <page-layout :avatar=\"currUser.avatar\">\n    <div slot=\"headerContent\">\n      <div class=\"title\">{{$t('timeFix')}}，{{currUser.name}}，{{$t('welcome')}}</div>\n      <div>{{$t('position')}}</div>\n    </div>\n    <template slot=\"extra\">\n      <head-info class=\"split-right\" :title=\"$t('project')\" content=\"56\"/>\n      <head-info class=\"split-right\" :title=\"$t('ranking')\" content=\"8/24\"/>\n      <head-info class=\"split-right\" :title=\"$t('visit')\" content=\"2,223\"/>\n    </template>\n    <div>...</div>\n  </page-layout>\n</template>\n```\n## 其它布局组件\n除了 Admin 里的内建布局以外，在一些页面中需要进行布局，还可以使用 Ant Design Vue 提供的布局组件：Grid 和 Layout。\n### Grid 组件\n栅格布局是网页中最常用的布局，其特点就是按照一定比例划分页面，能够随着屏幕的变化依旧保持比例，从而具有弹性布局的特点。  \n\n而 Ant Design Vue 的栅格组件提供的功能更为强大，能够设置间距、具有支持响应式的比例设置，以及支持 flex 模式，基本上涵盖了大部分的布局场景，详情查看：[Grid](https://www.antdv.com/components/grid-cn/)。\n### Layout 组件\n如果你需要辅助页面框架级别的布局设计，那么 Layout 则是你最佳的选择，它抽象了大部分框架布局结构，使得只需要填空就可以开发规范专业的页面整体布局，详情查看：[Layout](https://www.antdv.com/components/layout-cn/)。\n### 根据不同场景区分抽离布局组件\n在大部分场景下，我们需要基于上面两个组件封装一些适用于当下具体业务的组件，包含了通用的导航、侧边栏、顶部通知、页面标题等元素。例如 Vue Antd Admin 的 [AdminLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/AdminLayout.vue)。  \n \n通常，我们会把抽象出来的布局组件，放到 layouts 文件夹中方便管理。需要注意的是，这些布局组件和我们平时使用的其它组件并没有什么不同，只不过功能性上是为了处理布局问题而单独归类。\n"
  },
  {
    "path": "front/docs/develop/mock.md",
    "content": "---\ntitle: Mock\nlang: zh-CN\n---\n# Mock\n\n### 作者还没来得及编辑该页面，如果你感兴趣，可以点击下方链接，帮助作者完善此页\n"
  },
  {
    "path": "front/docs/develop/page.md",
    "content": "---\ntitle: 页面\nlang: zh-CN\n---\n# 页面\n这里的『页面』包含新建页面文件，配置路由、样式文件及i18n国际化等。通常情况下，你仅需简单的配置就可以添加一个新的页面。\n## 新建页面文件\n在 src/pages 下创建新的 .vue 文件。如果页面相关文件过多，您可以创建一个文件夹来放置这些文件。\n```diff                    \n  ├── public\n  ├── src\n  │   ├── assets               # 本地静态资源\n  :   :\n  │   ├── pages                # 页面组件和通用模板\n+ │   │   └── NewPage.vue      # 新页面文件\nor\n+ │   │   └── newPage          # 为新页面创建一个文件夹\n+ │   │       ├── NewPage.vue  # 新页面文件\n+ │   │       ├── index.less   # 页面样式文件\n+ │   │       └── index.js     # import 引导文件\n  :   :\n  │   └── main.js              # 应用入口js\n  ├── package.json             # package.json\n  ├── README.md                # README.md\n  └── vue.config.js            # vue 配置文件\n```\n为了更好地演示，我们初始化 NewPage.vue 文件如下：\n```vue\n<template>\n  <div class=\"new-page\" :style=\"`min-height: ${pageMinHeight}px`\">\n    <h1>演示页面</h1>\n  </div>\n</template>\n<script>\n  import {mapState} from 'vuex'\n  export default {\n    name: 'NewPage',\n    data() {\n      return {\n        desc: '这是一个演示页面'\n      }\n    },\n    computed: {\n      ...mapState('setting', ['pageMinHeight']),\n    }\n  }\n</script>\n<style scoped lang=\"less\">\n@import \"index.less\";\n</style>\n```\nindex.less 文件：\n```less\n.new-page{\n  height: 100%;\n  background-color: @base-bg-color;\n  text-align: center;\n  padding: 200px 0 0 0;\n  margin-top: -24px;\n  h1{\n    font-size: 48px;\n  }\n}\n```\nindex.js 文件：\n```js\nimport NewPage from './NewPage'\nexport default NewPage\n```\n## 配置路由\n路由配置在 src/router/config.js 文件中，我们把上面创建的页面文件加入路由配置中\n```js {10-14}\nconst options = {\n  routes: [\n    {name: '登录页'...},\n    {\n      path: '/',\n      name: '首页',\n      component: TabsView,\n      redirect: '/login',\n      children: [\n        {\n          path: 'newPage',\n          name: '新页面',\n          component: () => import('@/pages/newPage'),\n        },\n        {\n          path: 'dashboard',\n          name: 'Dashboard',\n          meta: {\n            icon: 'dashboard'\n          },\n          component: BlankView,\n          children: [...]\n        }\n      ]\n      ...\n    }\n  ]\n}\n```\n:::tip\n我们建议使用英文设置路由的 path 属性，用中文设置路由的 name 属性。因为系统将自动提取路由的 path 和 name 属性作为国际化配置。这在后面的章节\n [进阶>国际化](../advance/i18n.md)中将会讲到。\n 当然，如果你的项目不需要国际化，可以忽略。\n:::\n启动服务，你将看到新增页面如下：\n![newPage](../assets/new-page.png)\n如果你想把它配置为二级页面或更深层级的页面，只需为它配置一个父级路由，并为父级路由配置一个[视图组件](./layout.md#admin-的视图)，\n这里我们选择 [PageView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageView.vue)，如下：\n```js {10-21}\nconst options = {\n  routes: [\n    {name: '登录页'...},\n    {\n      path: '/',\n      name: '首页',\n      component: TabsView,\n      redirect: '/login',\n      children: [\n        {\n          path: 'parent',\n          name: '父级路由',\n          component: PageView,\n          children: [\n            {\n              path: 'newPage',\n              name: '新页面',\n              component: () => import('@/pages/newPage'),\n            }\n          ]\n        },\n        {name: 'dashboard'...}\n      ]\n      ...\n    }\n  ]\n}\n```\n:::warning\n页面所有父级路由的组件必须配置为[视图组件](../develop/layout.md#admin-的视图)，否则页面的内容可能不会显示。  \n目前有 [PageView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageView.vue)、\n[TabsView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/tabs/TabsView.vue) 和\n[BlankView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/BlankView.vue) 可选，\n你也可以自己创建视图组件。（[什么是视图组件？](../develop/layout.md#admin-的视图)）\n:::\n页面如下：\n![newPage2](../assets/new-page-2.png)\n## i18n国际化配置\n如果你想为页面增加i18n国际化配置，只需在页面同级文件夹下创建 i18n.js 文件，然后在页面文件中引入并使用即可。  \n创建 i18n.js 文件：\n```diff {9}                    \n  ├── public\n  ├── src\n  │   ├── assets               # 本地静态资源\n  :   :\n  │   ├── pages                # 页面组件和通用模板\n  │   │   └── newPage        # 为新页面创建一个文件夹\n  │   │       ├── NewPage.vue  # 新页面文件\n  │   │       ├── index.less   # 页面样式文件\n+ │   │       ├── i18n.js      # i18n 国际化配置文件\n  │   │       └── index.js     # import 引导文件\n  :   :\n  │   └── main.js              # 应用入口js\n  ├── package.json             # package.json\n  ├── README.md                # README.md\n  └── vue.config.js            # vue 配置文件\n```\ni18n.js 文件内容：\n```js\nmodule.exports = {\n  messages: {\n    CN: {\n      content: '演示页面',\n      description: '这是一个演示页面'\n    },\n    HK: {\n      content: '演示頁面',\n      description: '這是一個演示頁面'\n    },\n    US: {\n      content: 'Demo Page',\n      description: 'This is a demo page'\n    }\n  }\n}\n```\n在 NewPage.vue 文件中引入 i18n.js，并添加需要国际化的内容。如下修改：\n```vue {3,10,13-15}\n<template>\n  <div class=\"new-page\" :style=\"`min-height: ${pageMinHeight}px`\">\n    <h1>{{$t('content')}}</h1>\n  </div>\n</template>\n<script>\n  import {mapState} from 'vuex'\n  export default {\n    name: 'NewPage',\n    i18n: require('./i18n'),\n    computed: {\n      ...mapState('setting', ['pageMinHeight']),\n      desc() {\n        return this.$t('description')\n      }\n    }\n  }\n</script>\n<style scoped lang=\"less\">\n@import \"index\";\n</style>\n```\n然后页面右上角语言项选择 ``English``，你会发现，页面语言切换为英文了。如下：\n![newPageUs](../assets/new-page-us.png)\n一切就是这么的简单！\n:::tip\n如果你尝试切换为繁体语言，可能会发现``页面标题``和``面包屑``显示为英文。  \n这涉及到路由的国际化配置，在章节 [进阶 > 国际化](../advance/i18n.md) 中，我们会对此作详细讲解。\n:::\n"
  },
  {
    "path": "front/docs/develop/router.md",
    "content": "---\ntitle: 路由和菜单\nlang: zh-CN\n---\n# 路由和菜单\n路由和菜单起到组织一个应用的关键骨架的作用，Vue Antd Admin 使用 [vue-router](https://router.vuejs.org/zh/) 来配置和管理我们的路由和菜单。\n## 基本结构\n得益于 vue-router 路由配置的可扩展性，Vue Antd Admin 通过结合 router 配置文件、基本算法及 [menu.js](https://github.com/iczer/vue-antd-admin/blob/master/src/components/menu/menu.js) 菜单生成工具，搭建了路由和菜单的基本框架，主要涉及以下几个模块/功能：\n\n|功能        |配置                            |\n|:----------|:-------------------------------|\n|*路由管理*  |通过 [vue-router](https://router.vuejs.org/zh/) 的路由规则进行管理和配置|\n|*菜单生成*  |根据路由配置自动生成菜单，菜单项名称、图标和层级等全部可以通过路由配置进行自定义|\n|*面包屑*    |布局组件 [PageLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageLayout.vue) 提取当前页面路由，并根据当前路由层次关系自动生成面包屑，当然你也可以自定义面包屑|\n|*页面标题*  |同面包屑，布局组件 [PageLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageLayout.vue) 根据提取到的当前页面的路由名称设置为页面标题，你也同样可以自定义标题|\n\n## 路由\nVue Antd Admin 的路由配置完全遵循 vue-router 的 [routes 配置规则](https://router.vuejs.org/zh/api/#routes)。\n另外我们还在 routes 的元数据属性 [meta](https://router.vuejs.org/zh/guide/advanced/meta.html#%E8%B7%AF%E7%94%B1%E5%85%83%E4%BF%A1%E6%81%AF) 中注入了三个属性 icon、invisible 和 page，它们将在生成菜单和页头时发挥作用。配置示例如下：\n```js {7,13}\nconst options = {\n  routes: [{\n    path: '/',\n    name: '首页',\n    component: TabsView,\n    meta: {\n      invisible: true\n    },\n    children: [{\n      path: 'dashboard',\n      name: 'Dashboard',\n      meta: {\n        icon: 'dashboard'\n      },\n      component: BlankView,\n      children: [{\n        path: 'workplace',\n        name: '工作台',\n        component: () => import('@/pages/dashboard/workplace/WorkPlace'),\n      }, {\n        path: 'analysis',\n        name: '分析页',\n        component: () => import('@/pages/dashboard/analysis/Analysis'),\n      }]\n    }]\n  }]\n}\n```\n完整配置示例，请查看 [src/router/config.js](https://github.com/iczer/vue-antd-admin/blob/master/src/router/config.js)\n\n## 菜单\nAdmin 系统的菜单直接通过路由配置生成，路由属性和菜单功能对应关系如下\n\n|路由属性|对应菜单功能|\n|:-----------------|:-------|\n|**name**          |菜单名称 |\n|**path**          |点击菜单时的跳转链接|\n|**meta.icon**     |菜单图标，图标使用 ant-design-vue 图标库，对应 [Icon](https://www.antdv.com/components/icon-cn/#API) 组件 的 type 属性|  \n|**meta.invisible**|是否不将此路由项渲染为菜单项，默认false；如设置为 true，则生成菜单时将忽略此路由|\n\n假如使用上面 [路由](#路由) 文档中的 [配置示例](#路由)，将会生成如下菜单：\n\n![menu-demo](../assets/menu-demo.png)\n实际项目中，我们是在 AdminLayout 组件创建之前，提取 router 配置中根路由 '/' 下所有子路由配置，\n并将此配置传递给 menu.js 插件，从而生成菜单。如下：\n```vue {4,12,13,14}\n<template>\n  <a-layout :class=\"['admin-layout'...]\">\n    ...\n    <side-menu :menuData=\"menuData\".../>\n  </a-layout>\n</template>\n<script>\nimport ...\nexport default {\n  name: 'AdminLayout',\n  ...\n  beforeCreate () {\n    menuData = this.$router.options.routes.find((item) => item.path === '/').children\n  }\n}\n</script>\n```\n详细代码可查看 [layouts/AdminLayout#L83](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/AdminLayout.vue#L83)。  \n当然你也可以不使用 router 配置生成菜单，你只需按照配置规则给菜单传递你所定义配置即可。菜单组件配置规则如下：\n```jsx {}\n[{\n  name: '菜单标题',\n  path: '菜单路由',\n  meta: {\n    icon: '菜单图标',\n    invisible: 'boolean, 是否隐藏此菜单项, 默认 false',\n  },\n  children: [ //子菜单配置\n    {\n      name: '子菜单标题',\n      path: '子菜单路由',\n      meta: {\n        icon: '子菜单图标',\n        invisible: 'boolean, 是否隐藏此菜单项, 默认 false',\n      },\n    }\n  ]\n}]\n```\n更多细节可查看 [components/menu/menu.js](https://github.com/iczer/vue-antd-admin/blob/master/src/components/menu/menu.js)\n\n## 面包屑\n面包屑由 [PageHeader](https://github.com/iczer/vue-antd-admin/blob/master/src/components/page/PageHeader.vue) 实现，PageLayout 组件会从当前页面路由提取面包屑配置（如未设置，则根据当前路由层次关系生成面包屑）。所以只要页面中使用了 PageLayout 布局或者它的父级组件使用了 PageLayout 布局，面包屑都将自动生成。  \n\n当然，如果你想在某个页面自定义面包屑，只需在对应的路由元数据 meta 中定义 page.breadcrumb 属性即可。Vue Antd Admin 将会优先使用路由元数据 meta 中定义的面包屑配置。  \n\n比如，想自定义工作台页面面包屑，可以在工作台的 route 配置中如下设置：\n```jsx {5,6,7}\n{\n  path: 'workplace',\n  name: '工作台',\n  meta: {\n    page: {\n      breadcrumb: ['首页', 'Dashboard', '自定义']\n    }\n  },\n  component: () => import('@/pages/dashboard/workplace/WorkPlace'),\n}\n```\n更多细节可查看 [layouts/PageLayout.vue#L55](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageLayout.vue#L55)\n## 页面标题\n页面标题的实现方式与面包屑基本一致，也是由 PageLayout 组件从当前页面路由提取标题（如未设置，则提取当前路由名称作为标题）。 \n \n如果你想自定义页面标题，在页面对应的路由元数据 meta 中定义 page.title 属性即可，如下示例，定义了工作台页面的标题：\n```jsx {5,6,7}\n{\n  path: 'workplace',\n  name: '工作台',\n  meta: {\n    page: {\n      title: '自定义标题'\n    }\n  },\n  component: () => import('@/pages/dashboard/workplace/WorkPlace'),\n}\n```\n更多细节可查看 [layouts/PageLayout.vue#L48](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageLayout.vue#L48)\n"
  },
  {
    "path": "front/docs/develop/service.md",
    "content": "---\ntitle: 服务端交互\nlang: zh-CN\n---\n# 服务端交互\n数据服务是一个应用的灵魂，它驱动着应用的各个功能模块的正常运转。Vue Antd Admin 在 service 模块封装了服务端交互，通过 API 的形式可以和任何技术栈的服务端应用一起工作。\n## 服务交互流程\n在 Vue Antd Admin 中，服务端交互流程如下：\n* 组件内调用 service 服务 API\n* service 服务 API 封装请求数据，通过 request.js 发送请求\n* 组件获取 service 返回的数据，更新视图数据或触发其它行为  \n\n我们以登录为例，Login.vue 组件内，用户输入账号密码，点击登录，调用 services/user/login api\n```vue {5,17}\n<template>\n  ...\n</template>\n<script>\nimport {login} from '@/services/user'\n...\nexport default {\n  name: 'Login',\n  methods: {\n    onSubmit (e) {\n      e.preventDefault()\n      this.form.validateFields((err) => {\n        if (!err) {\n          this.logging = true\n          const name = this.form.getFieldValue('name')\n          const password = this.form.getFieldValue('password')\n          login(name, password).then(res => this.afterLogin(res))\n        }\n      })\n    }\n  }\n}\n</script>\n```\n`services/user/login` 封装账户密码数据，通过 `request.js` 发送登录服务请求\n```js\nimport {request, METHOD} from '@/utils/request'\n/**\n * 登录服务\n * @param name 账户名\n * @param password 账户密码\n * @returns {Promise<AxiosResponse<T>>}\n */\nasync function login(name, password) {\n  return request(LOGIN, METHOD.POST, {\n    name: name,\n    password: password\n  })\n}\n```\nLogin.vue 获取登录服务返回的数据，进行后续操作\n```vue {14,18-23}\n<template>\n  ...\n</template>\n<script>\nimport {login} from '@/services/user'\n...\nexport default {\n  name: 'Login',\n  methods: {\n    onSubmit (e) {\n      this.form.validateFields((err) => {\n        if (!err) {\n          ...\n          login(name, password).then(res => this.afterLogin(res))\n        }\n      })\n    },\n    afterLogin(res) {\n      if (res.data.code >= 0) {                 //登录成功\n        ...\n      } else {                                  //登录失败\n        this.error = loginRes.message\n      }\n    }\n  }\n}\n</script>\n```\n## 服务模块结构\n服务模块结构如下：\n```bash\n...\n├── src\n│   └── services                # 数据服务模块\n│       ├── user.js             # 用户数据服务\n│       ├── product.js          # 产品服务\n│       ...           \n│       ├── api.js              # api 地址管理\n│       └── index.js            # 服务模块导出\n...\n│   └── utils                   # 数据服务模块\n│       ├── request.js          # 基于 axios 的 http 请求工具\n...\n```\nservices 文件夹下， api.js 用于服务请求地址的统一管理，index.js 用于模块化导出服务，其它 *.js 文件对应各个服务模块。\n## request.js\nrequest.js 基于 axios 封装了一些常用的函数，如下：  \n```js\nexport {\n  METHOD,                 //http method 常量\n  AUTH_TYPE,              //凭证认证类型 常量\n  request,                //http请求函数\n  setAuthorization,       //设置身份凭证函数\n  removeAuthorization,    //移除身份凭证函数\n  checkAuthorization      //检查身份凭证是否过期函数\n}\n```\n:::tip\n凭证认证类型默认为 [Bearer](https://www.jianshu.com/p/8f7009456abc)，你可以根据自己的需要实现其它类型的认证\n:::\n## Base url 配置\n你可以在项目根目录下的环境变量文件(.env 和 .env.development)中配置你的 API 服务 base url 地址。\n\n生产环境，.env 文件\n```properties\nVUE_APP_API_BASE_URL=https://www.server.com\n```\n开发环境，.env.development 文件：\n```properties\nVUE_APP_API_BASE_URL=https://localhost:8000\n```\n## 跨域设置\n在开发环境中，通常我们的Vue应用和服务应用运行在不同的地址或端口上。我们可以通过简单的设置，代理前端请求，来避免跨域问题。如下：  \n\n首先，在 services/api.js 文件中设置 API_PROXY_PREFIX 常量，BASE_URL 像下面这样设置：\n```js {2,4}\n//跨域代理前缀\nconst API_PROXY_PREFIX='/api'\n//base url\nconst BASE_URL = process.env.NODE_ENV === 'production' ? process.env.VUE_APP_API_BASE_URL : API_PROXY_PREFIX\n//导出api服务地址\nmodule.exports = {\n  LOGIN: `${BASE_URL}/login`,\n  ROUTES: `${BASE_URL}/routes`\n}\n```\n然后，在 vue.config.js 文件中配置代理：\n```js\nmodel.exports = {\n  devServer: {\n    proxy: {\n      '/api': {               //此处要与 /services/api.js 中的 API_PROXY_PREFIX 值保持一致\n        target: process.env.VUE_APP_API_BASE_URL,\n        changeOrigin: true,\n        pathRewrite: {\n          '^/api': ''\n        }\n      }\n    }\n  }\n}\n```\n:::tip\n此代理配置仅适用于开发环境，生产环境的跨域代理请在自己的web服务器配置。\n:::\n"
  },
  {
    "path": "front/docs/develop/theme.md",
    "content": "---\ntitle: 主题定制\nlang: zh-CN\n---\n# 主题定制\n\n## 主题颜色\n### 主题色\n我们内置了一个色盘供您选择 \n\n<color color=\"#fa541c\"/>\n<color color=\"#fadb14\"/>\n<color color=\"#3eaf7c\"/>\n<color color=\"#13c2c2\"/>\n<color color=\"#1890ff\"/>\n<color color=\"#722ed1\"/>\n<color color=\"#eb2f96\"/>\n\n如果这不能满足你的需求，你也可以使用任何你喜欢的颜色，只需要在 src/config/config.js 文件中配置你的主题色即可。如：\n```js {3}\nmodule.exports = {\n  theme: {\n    color: '#13c2c2', //换成任何你喜欢的颜色，支持 hex 色值\n    mode: 'night'\n  },\n  multiPage: true,\n  animate: {\n    name: 'roll',\n    direction: 'default'\n  }\n}\n```\n当你设置好主题色后，系统会根据这个主题色为你生成一系列配套的颜色，并应用到vue组件中。\n:::tip \n你可以在你的样式文件中直接使用 less 变量 ``@theme-color``。\n::: \n:::warning\n主题色目前只支持 ``hex`` 模式的色值。如果设置为 ``rgb`` 或其它模式的色值，可能会导致配套颜色无法生成。\n:::\n### 功能色\n除了主题色，系统还有一些功能性颜色，分别为：成功色、警告色和错误色。默认色值分别为：\n|名称|success   |warning  |error  |\n|:-:|:--------:|:-------:|:-----:|\n|色值|``#52c41a``|``#faad14``|``#f5222d``|\n|颜色|<color color=\"#52c41a\"/>|<color color=\"#faad14\"/>|<color color=\"#f5222d\" />|\n|less变量|@success-color|@warning-color|@error-color|\n\n你也可以在 src/config/config.js 重新定义这些功能色\n```js {5-7}\nmodule.exports = {\n  theme: {\n    color: '#13c2c2', \n    mode: 'night',\n    success: '#52c41a', //定义成功色，支持 hex 色值\n    warning: '#faad14', //定义警告色，支持 hex 色值\n    error: '#f5222d'    //定义错误色，支持 hex 色值\n  },\n  multiPage: true,\n  animate: {\n    name: 'roll',\n    direction: 'default'\n  }\n}\n```\n:::tip\n想在在你的样式文件中使用以上各功能色，引用各功能色对应的 less 变量即可。\n:::\n:::warning\n功能色目前也只支持 ``hex`` 模式的色值。如果设置为 ``rgb`` 或其它模式的色值，可能会导致配套颜色无法生成。\n:::\n### 文本色\n<table style=\"text-align: center\" >\n  <tr>\n    <th>主题模式</th>\n    <th>标题色</th>\n    <th>文本色</th>\n    <th>次级文本色</th>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">light/dark</td>\n    <td><color color=\"rgba(0,0,0,0.85)\"/></td>\n    <td><color color=\"rgba(0,0,0,0.65)\"/></td>\n    <td><color color=\"rgba(0,0,0,0.45)\"/></td>\n  </tr>\n  <tr>\n    <td><code>rgba(0,0,0,0.85)</code></td>\n    <td><code>rgba(0,0,0,0.65)</code></td>\n    <td><code>rgba(0,0,0,0.45)</code></td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">night</td>\n    <td><color color=\"rgba(255,255,255,0.85)\"/></td>\n    <td><color color=\"rgba(255,255,255,0.65)\"/></td>\n    <td><color color=\"rgba(255,255,255,0.45)\"/></td>\n  </tr>\n  <tr>\n    <td><code>rgba(255,255,255,0.85)</code></td>\n    <td><code>rgba(255,255,255,0.65)</code></td>\n    <td><code>rgba(255,255,255,0.45)</code></td>\n  </tr>\n  <tr>\n    <td>less变量</td>\n    <td>@title-color</td>\n    <td>@text-color</td>\n    <td>@text-color-second</td>\n  </tr>\n</table>\n\n:::tip\n想在在你的样式文件中使用以上文本色，引用各文本色对应的 less 变量即可。\n:::\n:::warning\n目前不支持自定义文本色，因为涉及到主题模式切换时文本色的置换问题。如强行修改，可能会导致主题模式切换时出现样式异常。\n如果你的项目不需要主题模式切换，可自行替换以上文本色。\n:::\n\n### 背景色\n\n<table style=\"text-align: center\">\n  <tr>\n    <th>主题模式</th>\n    <th>布局背景色</th>\n    <th>基础背景色</th>\n    <th>hover背景色</th>\n    <th>边框颜色</th>\n    <th>阴影颜色</th>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">light/dark</td>\n    <td><color color=\"#f0f2f5\"/></td>\n    <td><color color=\"#fff\"/></td>\n    <td><color color=\"rgba(0,0,0,0.025)\"/></td>\n    <td><color color=\"#f0f0f0\"/></td>\n    <td><color color=\"rgba(0,0,0,0.15)\"/></td>\n  </tr>\n  <tr>\n    <td><code>#f0f2f5</code></td>\n    <td><code>#fff</code></td>\n    <td><code>rgba(0,0,0,0.025)</code></td>\n    <td><code>#f0f0f0</code></td>\n    <td><code>rgba(0,0,0,0.15)</code></td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">night</td>\n    <td><color color=\"#000\"/></td>\n    <td><color color=\"#141414\"/></td>\n    <td><color color=\"rgba(255,255,255,0.025)\"/></td>\n    <td><color color=\"#303030\"/></td>\n    <td><color color=\"rgba(255,255,255,0.15)\"/></td>\n  </tr>\n  <tr>\n    <td><code>#000</code></td>\n    <td><code>#141414</code></td>\n    <td><code>rgba(255,255,255,0.025)</code></td>\n    <td><code>#303030</code></td>\n    <td><code>rgba(255,255,255,0.15)</code></td>\n  </tr>\n  <tr>\n    <td>less变量</td>\n    <td>@layout-bg-color</td>\n    <td>@base-bg-color</td>\n    <td>@hover-bg-color</td>\n    <td>@border-color</td>\n    <td>@shadow-color</td>\n  </tr>\n</table>\n\n:::tip\n想在在你的样式文件中使用以上背景色，引用各背景色对应的 less 变量即可。\n:::\n:::warning\n目前也不支持自定义背景色，因为涉及到主题模式切换时背景色的置换问题。如强行修改，可能会导致主题模式切换时出现样式异常。\n如果你的项目不需要主题模式切换，可自行替换以上背景色。\n:::\n\n### antd 的色系\n除了以上颜色，我们还引入了 ant-design 内置的色系。如下：\n\n<table style=\"text-align: center\">\n  <tr>\n    <th>色系</th>\n    <th>类型</th>\n    <th>颜色</th>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">blue/拂晓蓝</td>\n    <td>色盘</td>\n    <td >\n      <color-list\n       :colors=\"['#e6f7ff', '#bae7ff', '#91d5ff', '#69c0ff', '#40a9ff', '#1890ff', '#096dd9', '#0050b3', '#003a8c', '#002766']\" \n      />\n    </td>\n  </tr>\n  <tr>\n    <td>less变量</td>\n    <td>\n      <code>@blue-1</code>、\n      <code>@blue-2</code>\n      <code>...</code>\n      <code>@blue-10</code>\n    </td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">purple/酱紫</td>\n    <td>色盘</td>\n    <td>\n      <color-list\n       :colors=\"['#f9f0ff', '#efdbff', '#d3adf7', '#b37feb', '#9254de', '#722ed1', '#531dab', '#391085', '#22075e', '#120338']\" \n      />\n    </td>\n  </tr>\n  <tr>\n    <td>less变量</td>\n    <td>\n      <code>@purple-1</code>、\n      <code>@purple-2</code>\n      <code>...</code>\n      <code>@purple-10</code>\n    </td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">cyan/明青</td>\n    <td>色盘</td>\n    <td>\n      <color-list\n       :colors=\"['#e6fffb', '#b5f5ec', '#87e8de', '#5cdbd3', '#36cfc9', '#13c2c2', '#08979c', '#006d75', '#00474f', '#002329']\" \n      />\n    </td>\n  </tr>\n  <tr>\n    <td>less变量</td>\n    <td>\n      <code>@cyan-1</code>、\n      <code>@cyan-2</code>\n      <code>...</code>\n      <code>@cyan-10</code>\n    </td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">green/极光绿</td>\n    <td>色盘</td>\n    <td>\n      <color-list\n       :colors=\"['#f6ffed', '#d9f7be', '#b7eb8f', '#95de64', '#73d13d', '#52c41a', '#389e0d', '#237804', '#135200', '#092b00']\" \n      />\n    </td>\n  </tr>\n  <tr>\n    <td>less变量</td>\n    <td>\n      <code>@green-1</code>、\n      <code>@green-2</code>\n      <code>...</code>\n      <code>@green-10</code>\n    </td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">magenta/法式洋红</td>\n    <td>色盘</td>\n    <td>\n      <color-list\n       :colors=\"['#fff0f6', '#ffd6e7', '#ffadd2', '#ff85c0', '#f759ab', '#eb2f96', '#c41d7f', '#9e1068', '#780650', '#520339']\" \n      />\n    </td>\n  </tr>\n  <tr>\n    <td>less变量</td>\n    <td>\n      <code>@magenta-1</code>、\n      <code>@magenta-2</code>\n      <code>...</code>\n      <code>@magenta-10</code>\n    </td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">red/薄暮</td>\n    <td>色盘</td>\n    <td>\n      <color-list\n       :colors=\"['#fff1f0', '#ffccc7', '#ffa39e', '#ff7875', '#ff4d4f', '#f5222d', '#cf1322', '#a8071a', '#820014', '#5c0011']\" \n      />\n    </td>\n  </tr>\n  <tr>\n    <td>less变量</td>\n    <td>\n      <code>@red-1</code>、\n      <code>@red-2</code>\n      <code>...</code>\n      <code>@red-10</code>\n    </td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">orange/日暮</td>\n    <td>色盘</td>\n    <td>\n      <color-list\n       :colors=\"['#fff7e6', '#ffe7ba', '#ffd591', '#ffc069', '#ffa940', '#fa8c16', '#d46b08', '#ad4e00', '#873800', '#612500']\" \n      />\n    </td>\n  </tr>\n  <tr>\n    <td>less变量</td>\n    <td>\n      <code>@orange-1</code>、\n      <code>@orange-2</code>\n      <code>...</code>\n      <code>@orange-10</code>\n    </td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">yellow/日出</td>\n    <td>色盘</td>\n    <td>\n      <color-list\n       :colors=\"['#feffe6', '#ffffb8', '#fffb8f', '#fff566', '#ffec3d', '#fadb14', '#d4b106', '#ad8b00', '#876800', '#614700']\" \n      />\n    </td>\n  </tr>\n  <tr>\n    <td>less变量</td>\n    <td>\n      <code>@yellow-1</code>、\n      <code>@yellow-2</code>\n      <code>...</code>\n      <code>@yellow-10</code>\n    </td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">volcano/火山</td>\n    <td>色盘</td>\n    <td>\n      <color-list\n       :colors=\"['#fff2e8', '#ffd8bf', '#ffbb96', '#ff9c6e', '#ff7a45', '#fa541c', '#d4380d', '#ad2102', '#871400', '#610b00']\" \n      />\n    </td>\n  </tr>\n  <tr>\n    <td>less变量</td>\n    <td>\n      <code>@volcano-1</code>、\n      <code>@volcano-2</code>\n      <code>...</code>\n      <code>@volcano-10</code>\n    </td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">geekblue/极客蓝</td>\n    <td>色盘</td>\n    <td>\n      <color-list\n       :colors=\"['#f0f5ff', '#d6e4ff', '#adc6ff', '#85a5ff', '#597ef7', '#2f54eb', '#1d39c4', '#10239e', '#061178', '#030852']\" \n      />\n    </td>\n  </tr>\n  <tr>\n    <td>less变量</td>\n    <td>\n      <code>@geekblue-1</code>、\n      <code>@geekblue-2</code>\n      <code>...</code>\n      <code>@geekblue-10</code>\n    </td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">lime/青柠</td>\n    <td>色盘</td>\n    <td>\n      <color-list\n       :colors=\"['#fcffe6', '#f4ffb8', '#eaff8f', '#d3f261', '#bae637', '#a0d911', '#7cb305', '#5b8c00', '#3f6600', '#254000']\" \n      />\n    </td>\n  </tr>\n  <tr>\n    <td>less变量</td>\n    <td>\n      <code>@lime-1</code>、\n      <code>@lime-2</code>\n      <code>...</code>\n      <code>@lime-10</code>\n    </td>\n  </tr>\n  <tr>\n    <td rowspan=\"2\">gold/金盏花</td>\n    <td>色盘</td>\n    <td>\n      <color-list\n       :colors=\"['#fffbe6', '#fff1b8', '#ffe58f', '#ffd666', '#ffc53d', '#faad14', '#d48806', '#ad6800', '#874d00', '#613400']\" \n      />\n    </td>\n  </tr>\n  <tr>\n    <td>less变量</td>\n    <td>\n      <code>@gold-1</code>、\n      <code>@gold-2</code>\n      <code>...</code>\n      <code>@gold-10</code>\n    </td>\n  </tr>\n</table>\n以上色系对应的less变量均可以在你的样式代码中直接使用。\n\n:::tip\n我们建议在开发中使用 `less变量` 而不是直接使用 `颜色值` 来设置颜色。这样做对主题色和主题模式切换很有帮助。\n:::\n## 主题模式\nVue Antd Admin 有三种主题模式，分别为：`light/亮色菜单模式`、`dark/暗色菜单模式` 和 `night/黑夜模式`。\n\nlight / 亮色菜单模式:\n![light](../assets/mode-light.png)\ndark / 暗色菜单模式:\n![dark](../assets/mode-dark.png)\nnight / 黑夜模式:\n![night](../assets/mode-night.png)\n\n你可以在这三种模式之间随意切换，也可以在 src/config/config.js 中设置默认的主题模式。\n```js {4}\nmodule.exports = {\n  theme: {\n    color: '#13c2c2',\n    mode: 'night'       //设置你的默认主题模式，可选 light、dark 和 night\n  },\n  multiPage: true,\n  animate: {\n    name: 'roll',\n    direction: 'default'\n  }\n}\n```\n\n## 导航布局\nVue Antd Admin 有两种导航布局，`side/侧边导航` 和 `head/顶部导航`。  \n默认为侧边导航，你可以在 src/config/config.js 中修改导航布局\n```js {6}\nmodule.exports = {\n  theme: {\n    color: '#13c2c2',\n    mode: 'night'       \n  },\n  layout: 'side',     //设置你的默认导航布局，有 side 和 head 可选\n  multiPage: true,\n  animate: {\n    name: 'roll',\n    direction: 'default'\n  }\n}\n```\n## 动画\nVue Antd Admin 内置了 [animate.css](https://animate.style) 动画库，在页面切换时会应用动画效果。你可以在 src/config/config.js 中配置动画效果或者禁用动画。\n```js {7-11}\nmodule.exports = {\n  theme: {\n    color: '#13c2c2',\n    mode: 'night'       \n  },\n  multiPage: true,\n  animate: {\n    disabled: false,      //禁用动画，true:禁用，false:启用\n    name: 'roll',         //动画效果，支持的动画效果可参考 src/config/default/animate.config.js\n    direction: 'default'  //动画方向，切换页面时动画的方向，参考 src/config/default/animate.config.js\n  }\n}\n```\n支持的动画特效种类，可以参考 src/config/default/animate.config.js 文件。\n## 其它\n### 色弱模式\n对于有视觉障碍的群体，我们提供了色弱模式，你可以通过配置 src/config/config.js 启用色弱模式\n```js {7}\nmodule.exports = {\n  theme: {\n    color: '#13c2c2',\n    mode: 'night'       \n  },\n  multiPage: true,\n  weekMode: false,   //色弱模式，true:开启，false:不开启\n  animate: {\n    name: 'roll',         \n    direction: 'default'\n  }\n}\n```\n### 多页签\n在 src/config/config.js 设置 multiPage 来启用或关闭多页签模式\n```js {6}\nmodule.exports = {\n  theme: {\n    color: '#13c2c2',\n    mode: 'night'       \n  },\n  multiPage: true,          //多页签模式，true:开启，false:不开启\n  animate: {\n    name: 'roll',         \n    direction: 'default'\n  }\n}\n```\n完整的系统设置参考 src/config/default/setting.config.js\n:::tip\n以上所有主题设置项，均已映射到 vuex/setting 模块的 state 中，你可以通过提交 setting/mutations 实时修改设置项。  \n如何使用 [mutations](https://vuex.vuejs.org/zh/guide/mutations.html) ？\n:::\n"
  },
  {
    "path": "front/docs/other/README.md",
    "content": "---\ntitle: 其它\nlang: zh-CN\n---\n# 其它\n"
  },
  {
    "path": "front/docs/other/community.md",
    "content": "---\ntitle: 社区\nlang: zh-CN\n---\n# 社区\n\n## 交流学习\n### QQ群：812277510、610090280（已满）\n"
  },
  {
    "path": "front/docs/other/upgrade.md",
    "content": "---\ntitle: 更新日志\nlang: zh-CN\n---\n# 更新日志\n"
  },
  {
    "path": "front/docs/start/README.md",
    "content": "---\ntitle: 开始\nlang: zh-CN\n---\n## 开始\n"
  },
  {
    "path": "front/docs/start/faq.md",
    "content": "---\ntitle: 常见问题\nlang: zh-CN\n---\n# 常见问题\n### 为什么不是 Ant Design Pro Vue ？\n[Ant Design Pro Vue](https://github.com/vueComponent/ant-design-vue-pro) 是 [Ant Design Pro](https://github.com/ant-design/ant-design-pro) 的 Vue 版本，其中项目结构、组件、\n布局和使用方法等基本与 Ant Design Pro 的 react 版本保持一致。如果你比较熟悉 react 版，或者你已经在使用它，这确实是一个不错的选择。 \n\n[Vue Antd Admin](https://github.com/iczer/vue-antd-admin) 同样实现了 Ant Design Pro 的所有功能。与此同时，我们还根据 Vue 的特性，对 Ant Design Pro 的一些组件和布局作出了相应的修改及优化，同时不影响保持与 Ant Design Pro 的一致。 \n\n另外，我们还在添加一些 Ant Design Pro 没有的功能，比如全局动画、多页签模式等。  \n\n如果你想使用 Ant Design Pro，但又觉得它缺乏一些你想要的功能，不妨看看 [Vue Antd Admin](https://github.com/iczer/vue-antd-admin)，我们会认真考虑每个用户的需求。  \n\n因此，如果你有一些不错的想法和建议，欢迎随时和我们交流，很可能你的想法就在我们下一个版本中实现。\n\n### 如何使用 Vue Antd Admin ？\n请阅读文档 [开始使用](./use.md)。有任何疑问，欢迎在 github 上给我们提交 [issue](https://github.com/iczer/vue-antd-admin/issues/new)。\n\n### 是否支持国际化 ？\nVue Antd Admin 引入了 vue-i18n 支持。因此你可以使用 vue-i18n 的特性对项目做国际化修改，详细请查看 [国际化](../advance/i18n.md)\n"
  },
  {
    "path": "front/docs/start/use.md",
    "content": "---\ntitle: 使用\nlang: zh-CN\n---\n# 使用\n## 准备\n你的本地环境需要安装 yarn、node 和 git。我们的技术栈基于 ES2015+、Vue、Antd，提前学习这些知识会非常有帮助。\n## 安装\n克隆本项目到本地\n```bash\n$ git clone https://github.com/iczer/vue-antd-admin.git\n```\n安装依赖\n```bash\n$ yarn install\nor\n$ npm install\n```\n:::tip\nmaster 分支是 Vue Antd Admin 的标准版代码，此分支代码适合用于用于学习研究，不推荐在此分支做正式开发。\n我们在 basic 分支提供了 Vue Antd Admin 的基础版代码，正式开发请切换至此分支，以便于后续的版本更新。\n:::\n:::warning\n如果基于 `master分支` 进行开发，在版本更新时遇到的代码冲突问题请自行解决，我们不对基于 `master分支` 开发时遇到的问题提供技术支持。  \n再次强调，`master分支` 仅推荐用于学习参考，正式开发请切换至 `basic` 分支！！！\n:::\n## 目录结构\n我们已经为你生成了一个完整的开发框架，提供了涵盖中后台开发的各类功能和坑位，下面是整个项目的目录结构。\n\n```bash\n├── docs                     # 使用文档\n├── public\n│   └── favicon.png          # favicon\n│   └── index.html           # 入口 HTML\n├── src\n│   ├── assets               # 本地静态资源\n│   ├── components           # 内置通用组件\n│   ├── config               # 系统配置\n│   ├── layouts              # 通用布局\n│   ├── mock                 # 本地 mock 数据\n│   ├── pages                # 页面组件和通用模板\n│   ├── plugins              # vue 插件\n│   ├── router               # 路由配置\n│   ├── services             # 数据服务模块\n│   ├── store                # vuex 状态管理配置\n│   ├── theme                # 主题相关\n│   ├── utils                # js 工具\n│   ├── App.vue              # 应用入口组件\n│   ├── bootstrap.js         # 应用启动引导js\n│   └── main.js              # 应用入口js\n├── package.json             # package.json\n├── README.md                # README.md\n└── vue.config.js            # vue 配置文件\n```\n## 本地开发\n启动服务\n```bash\n$ yarn serve\nor\n$ npm run serve\n```\n启动成功后，会看到一个本地预览地址，通常是 http://localhost:8080 。接下来就可以修改代码，并实时预览修改结果啦！\n"
  },
  {
    "path": "front/package.json",
    "content": "{\n  \"name\": \"vue-antd-admin\",\n  \"version\": \"0.7.4\",\n  \"homepage\": \"https://iczer.github.io/vue-antd-admin\",\n  \"private\": true,\n  \"scripts\": {\n    \"serve\": \"vue-cli-service serve\",\n    \"build\": \"vue-cli-service build\",\n    \"lint\": \"vue-cli-service lint\",\n    \"predeploy\": \"yarn build\",\n    \"deploy\": \"gh-pages -d dist -b pages -r https://gitee.com/iczer/vue-antd-admin.git\",\n    \"docs:dev\": \"vuepress dev docs\",\n    \"docs:build\": \"vuepress build docs\",\n    \"docs:deploy\": \"vuepress build docs && gh-pages -d docs/.vuepress/dist -b master -r https://gitee.com/iczer/vue-antd-admin-docs.git\"\n  },\n  \"dependencies\": {\n    \"@antv/data-set\": \"^0.11.4\",\n    \"animate.css\": \"^4.1.0\",\n    \"ant-design-vue\": \"1.7.2\",\n    \"axios\": \"^0.21.1\",\n    \"clipboard\": \"^2.0.6\",\n    \"core-js\": \"^3.6.5\",\n    \"date-fns\": \"^2.14.0\",\n    \"echarts\": \"^5.1.1\",\n    \"enquire.js\": \"^2.1.6\",\n    \"highlight.js\": \"^10.2.1\",\n    \"js-cookie\": \"^2.2.1\",\n    \"lodash\": \"^4.17.21\",\n    \"mockjs\": \"^1.1.0\",\n    \"nprogress\": \"^0.2.0\",\n    \"viser-vue\": \"^2.4.8\",\n    \"vue\": \"^2.6.11\",\n    \"vue-i18n\": \"^8.18.2\",\n    \"vue-router\": \"^3.3.4\",\n    \"vuedraggable\": \"^2.23.2\",\n    \"vuex\": \"^3.4.0\",\n    \"xlsx\": \"^0.17.0\"\n  },\n  \"devDependencies\": {\n    \"@ant-design/colors\": \"^4.0.1\",\n    \"@vue/cli-plugin-babel\": \"^4.4.0\",\n    \"@vue/cli-plugin-eslint\": \"^4.4.0\",\n    \"@vue/cli-service\": \"^4.4.0\",\n    \"@vuepress/plugin-back-to-top\": \"^1.5.2\",\n    \"babel-eslint\": \"^10.1.0\",\n    \"babel-plugin-transform-remove-console\": \"^6.9.4\",\n    \"babel-polyfill\": \"^6.26.0\",\n    \"compression-webpack-plugin\": \"^2.0.0\",\n    \"deepmerge\": \"^4.2.2\",\n    \"eslint\": \"^6.7.2\",\n    \"eslint-plugin-vue\": \"^6.2.2\",\n    \"fast-deep-equal\": \"^3.1.3\",\n    \"gh-pages\": \"^3.1.0\",\n    \"less-loader\": \"^6.1.1\",\n    \"style-resources-loader\": \"^1.3.2\",\n    \"vue-cli-plugin-style-resources-loader\": \"^0.1.4\",\n    \"vue-template-compiler\": \"^2.6.11\",\n    \"vuepress\": \"^1.5.2\",\n    \"webpack-theme-color-replacer\": \"1.3.18\",\n    \"whatwg-fetch\": \"^3.0.0\"\n  },\n  \"eslintConfig\": {\n    \"root\": true,\n    \"env\": {\n      \"node\": true\n    },\n    \"extends\": [\n      \"plugin:vue/essential\",\n      \"eslint:recommended\"\n    ],\n    \"parserOptions\": {\n      \"parser\": \"babel-eslint\"\n    },\n    \"rules\": {}\n  },\n  \"browserslist\": [\n    \"> 1%\",\n    \"last 2 versions\",\n    \"not ie <= 10\"\n  ]\n}\n"
  },
  {
    "path": "front/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" class=\"beauty-scroll\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n    <link rel=\"icon\" href=\"<%= BASE_URL %>favicon.ico\">\n    <title><%= process.env.VUE_APP_NAME %></title>\n    <!-- require cdn assets css -->\n    <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>\n      <link rel=\"stylesheet\" href=\"<%= htmlWebpackPlugin.options.cdn.css[i] %>\" />\n    <% } %>\n  </head>\n  <body>\n    <noscript>\n      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>\n    </noscript>\n    <div id=\"popContainer\" class=\"beauty-scroll\" style=\"height: 100vh; overflow-y: scroll\">\n      <div id=\"app\"></div>\n    </div>\n    <!-- require cdn assets js -->\n    <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>\n      <script type=\"text/javascript\" src=\"<%= htmlWebpackPlugin.options.cdn.js[i] %>\"></script>\n    <% } %>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n"
  },
  {
    "path": "front/src/App.vue",
    "content": "<template>\n  <a-config-provider :locale=\"locale\" :get-popup-container=\"popContainer\">\n    <router-view/>\n  </a-config-provider>\n</template>\n\n<script>\nimport {enquireScreen} from './utils/util'\nimport {mapState, mapMutations} from 'vuex'\nimport themeUtil from '@/utils/themeUtil';\nimport {getI18nKey} from '@/utils/routerUtil'\n\nexport default {\n  data() {\n    return {\n      locale: {}\n    }\n  },\n  created () {\n    this.setHtmlTitle()\n    this.setLanguage(this.lang)\n    enquireScreen(isMobile => this.setDevice(isMobile))\n  },\n  mounted() {\n   this.setWeekModeTheme(this.weekMode)\n  },\n  watch: {\n    weekMode(val) {\n      this.setWeekModeTheme(val)\n    },\n    lang(val) {\n      this.setLanguage(val)\n      this.setHtmlTitle()\n    },\n    $route() {\n      this.setHtmlTitle()\n    },\n    'theme.mode': function(val) {\n      let closeMessage = this.$message.loading(`选择主题模式 ${val}, 正在切换......`)\n      themeUtil.changeThemeColor(this.theme.color, val).then(closeMessage)\n    },\n    'theme.color': function(val) {\n      let closeMessage = this.$message.loading(`选择主题色 ${val}, 正在切换......`)\n      themeUtil.changeThemeColor(val, this.theme.mode).then(closeMessage)\n    },\n    'layout': function() {\n      window.dispatchEvent(new Event('resize'))\n    }\n  },\n  computed: {\n    ...mapState('setting', ['layout', 'theme', 'weekMode', 'lang'])\n  },\n  methods: {\n    ...mapMutations('setting', ['setDevice']),\n    setWeekModeTheme(weekMode) {\n      if (weekMode) {\n        document.body.classList.add('week-mode')\n      } else {\n        document.body.classList.remove('week-mode')\n      }\n    },\n    setLanguage(lang) {\n      this.$i18n.locale = lang\n      switch (lang) {\n        case 'CN':\n          this.locale = require('ant-design-vue/es/locale-provider/zh_CN').default\n          break\n        case 'US':\n        default:\n          this.locale = require('ant-design-vue/es/locale-provider/en_US').default\n          break\n      }\n    },\n    setHtmlTitle() {\n      const route = this.$route\n      const key = route.path === '/' ? 'home.name' : getI18nKey(route.matched[route.matched.length - 1].path)\n      document.title = process.env.VUE_APP_NAME + ' | ' + this.$t(key)\n    },\n    popContainer() {\n      return document.getElementById(\"popContainer\")\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  #id{\n  }\n</style>\n"
  },
  {
    "path": "front/src/bootstrap.js",
    "content": "import {loadRoutes, loadGuards, setAppOptions} from '@/utils/routerUtil'\nimport {loadInterceptors} from '@/utils/request'\nimport guards from '@/router/guards'\nimport interceptors from '@/utils/axios-interceptors'\n\n/**\n * 启动引导方法\n * 应用启动时需要执行的操作放在这里\n * @param router 应用的路由实例\n * @param store 应用的 vuex.store 实例\n * @param i18n 应用的 vue-i18n 实例\n * @param i18n 应用的 message 实例\n */\nfunction bootstrap({router, store, i18n, message}) {\n  // 设置应用配置\n  setAppOptions({router, store, i18n})\n  // 加载 axios 拦截器\n  loadInterceptors(interceptors, {router, store, i18n, message})\n  // 加载路由\n  loadRoutes()\n  // 加载路由守卫\n  loadGuards(guards, {router, store, i18n, message})\n}\n\nexport default bootstrap\n"
  },
  {
    "path": "front/src/components/cache/AKeepAlive.js",
    "content": "import {isDef, isRegExp, remove} from '@/utils/util'\n\nconst patternTypes = [String, RegExp, Array]\n\nfunction matches (pattern, name) {\n  if (Array.isArray(pattern)) {\n    if (pattern.indexOf(name) > -1) {\n      return true\n    } else {\n      for (let item of pattern) {\n        if (isRegExp(item) && item.test(name)) {\n          return true\n        }\n      }\n      return false\n    }\n  } else if (typeof pattern === 'string') {\n    return pattern.split(',').indexOf(name) > -1\n  } else if (isRegExp(pattern)) {\n    return pattern.test(name)\n  }\n  /* istanbul ignore next */\n  return false\n}\n\nfunction getComponentName (opts) {\n  return opts && (opts.Ctor.options.name || opts.tag)\n}\n\nfunction getComponentKey (vnode) {\n  const {componentOptions, key} = vnode\n  return key == null\n    ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')\n    : key + componentOptions.Ctor.cid\n}\n\nfunction getFirstComponentChild (children) {\n  if (Array.isArray(children)) {\n    for (let i = 0; i < children.length; i++) {\n      const c = children[i]\n      if (isDef(c) && (isDef(c.componentOptions) || c.isAsyncPlaceholder)) {\n        return c\n      }\n    }\n  }\n}\n\nfunction pruneCache (keepAliveInstance, filter) {\n  const { cache, keys, _vnode } = keepAliveInstance\n  for (const key in cache) {\n    const cachedNode = cache[key]\n    if (cachedNode) {\n      const name = getComponentName(cachedNode.componentOptions)\n      const componentKey = getComponentKey(cachedNode)\n      if (name && !filter(name, componentKey)) {\n        pruneCacheEntry(cache, key, keys, _vnode)\n      }\n    }\n  }\n}\n\nfunction pruneCacheEntry2(cache, key, keys) {\n  const cached = cache[key]\n  if (cached) {\n    cached.componentInstance.$destroy()\n  }\n  cache[key] = null\n  remove(keys, key)\n}\n\nfunction pruneCacheEntry (cache, key, keys, current) {\n  const cached = cache[key]\n  if (cached && (!current || cached.tag !== current.tag)) {\n    cached.componentInstance.$destroy()\n  }\n  cache[key] = null\n  remove(keys, key)\n}\n\nexport default {\n  name: 'AKeepAlive',\n  abstract: true,\n  model: {\n    prop: 'clearCaches',\n    event: 'clear',\n  },\n  props: {\n    include: patternTypes,\n    exclude: patternTypes,\n    excludeKeys: patternTypes,\n    max: [String, Number],\n    clearCaches: Array\n  },\n  watch: {\n    clearCaches: function(val) {\n      if (val && val.length > 0) {\n        const {cache, keys} = this\n        val.forEach(key => {\n          pruneCacheEntry2(cache, key, keys)\n        })\n        this.$emit('clear', [])\n      }\n    }\n  },\n\n  created() {\n    this.cache = Object.create(null)\n    this.keys = []\n  },\n\n  destroyed () {\n    for (const key in this.cache) {\n      pruneCacheEntry(this.cache, key, this.keys)\n    }\n  },\n\n  mounted () {\n    this.$watch('include', val => {\n      pruneCache(this, (name) => matches(val, name))\n    })\n    this.$watch('exclude', val => {\n      pruneCache(this, (name) => !matches(val, name))\n    })\n    this.$watch('excludeKeys', val => {\n      pruneCache(this, (name, key) => !matches(val, key))\n    })\n  },\n\n  render () {\n    const slot = this.$slots.default\n    const vnode = getFirstComponentChild(slot)\n    const componentOptions = vnode && vnode.componentOptions\n    if (componentOptions) {\n      // check pattern\n      const name = getComponentName(componentOptions)\n      const componentKey = getComponentKey(vnode)\n      const { include, exclude, excludeKeys } = this\n      if (\n        // not included\n        (include && (!name || !matches(include, name))) ||\n        // excluded\n        (exclude && name && matches(exclude, name)) ||\n        (excludeKeys && componentKey && matches(excludeKeys, componentKey))\n      ) {\n        return vnode\n      }\n\n      const { cache, keys } = this\n      const key = vnode.key == null\n        // same constructor may get registered as different local components\n        // so cid alone is not enough (#3269)\n        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')\n        : vnode.key + componentOptions.Ctor.cid\n      if (cache[key]) {\n        vnode.componentInstance = cache[key].componentInstance\n        // make current key freshest\n        remove(keys, key)\n        keys.push(key)\n      } else {\n        cache[key] = vnode\n        keys.push(key)\n        // prune oldest entry\n        if (this.max && keys.length > parseInt(this.max)) {\n          pruneCacheEntry(cache, keys[0], keys, this._vnode)\n        }\n      }\n\n      vnode.data.keepAlive = true\n    }\n    return vnode || (slot && slot[0])\n  }\n}\n"
  },
  {
    "path": "front/src/components/card/ChartCard.vue",
    "content": "<template>\n  <a-card :loading=\"loading\" :body-style=\"{padding: '20px 24px 8px'}\" :bordered=\"false\">\n    <div class=\"chart-card-header\">\n      <div class=\"meta\">\n        <span class=\"chart-card-title\">{{title}}</span>\n        <span class=\"chart-card-action\">\n        <slot name=\"action\"></slot>\n      </span>\n      </div>\n      <div class=\"total\"><span>{{total}}</span></div>\n    </div>\n    <div class=\"chart-card-content\">\n      <div class=\"content-fix\">\n        <slot></slot>\n      </div>\n    </div>\n    <div class=\"chart-card-footer\">\n      <slot name=\"footer\"></slot>\n    </div>\n  </a-card>\n</template>\n\n<script>\nexport default {\n  name: 'ChartCard',\n  props: ['title', 'total', 'loading']\n}\n</script>\n\n<style scoped lang=\"less\">\n  .chart-card-header{\n    position: relative;\n    overflow: hidden;\n    width: 100%;\n  }\n  .chart-card-header .meta{\n    position: relative;\n    overflow: hidden;\n    width: 100%;\n    color: @text-color-second;\n    font-size: 14px;\n    line-height: 22px;\n  }\n  .chart-card-action{\n    cursor: pointer;\n    position: absolute;\n    top: 0;\n    right: 0;\n  }\n  .total {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    word-break: break-all;\n    white-space: nowrap;\n    margin-top: 4px;\n    margin-bottom: 0;\n    font-size: 30px;\n    line-height: 38px;\n    height: 38px;\n  }\n  .chart-card-footer{\n    border-top: 1px solid @border-color-base;\n    padding-top: 9px;\n    margin-top: 8px;\n  }\n  .chart-card-content{\n    margin-bottom: 12px;\n    position: relative;\n    height: 46px;\n    width: 100%;\n  }\n  .chart-card-content .content-fix{\n    position: absolute;\n    left: 0;\n    bottom: 0;\n    width: 100%;\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/chart/Bar.vue",
    "content": "<template>\n  <div class=\"bar\">\n    <h4>{{title}}</h4>\n    <div class=\"chart\">\n      <v-chart :force-fit=\"true\" height=\"312\" :data=\"data\" :padding=\"[24, 0, 0, 0]\">\n        <v-tooltip />\n        <v-axis />\n        <v-bar position=\"x*y\"/>\n      </v-chart>\n    </div>\n  </div>\n</template>\n\n<script>\n\nconst data = []\nfor (let i = 0; i < 12; i += 1) {\n  data.push({\n    x: `${i + 1}月`,\n    y: Math.floor(Math.random() * 1000) + 200\n  })\n}\nconst tooltip = [\n  'x*y',\n  (x, y) => ({\n    name: x,\n    value: y\n  })\n]\n\nconst scale = [{\n  dataKey: 'x',\n  min: 2\n}, {\n  dataKey: 'y',\n  title: '时间',\n  min: 1,\n  max: 22\n}]\nexport default {\n  name: 'Bar',\n  props: ['title'],\n  data () {\n    return {\n      data,\n      scale,\n      tooltip\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"less\">\n  .bar{\n    position: relative;\n    .chart{\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/chart/MiniArea.vue",
    "content": "<template>\n  <div class=\"mini-chart\">\n    <div class=\"chart-content\" :style=\"{height: 46}\">\n      <v-chart :force-fit=\"true\" :height=\"height\" :data=\"data\" :padding=\"[36, 5, 18, 5]\">\n        <v-tooltip />\n        <v-smooth-area position=\"x*y\" />\n      </v-chart>\n    </div>\n  </div>\n</template>\n\n<script>\nimport {format} from 'date-fns'\n\nconst data = []\nconst beginDay = new Date().getTime()\n\nconst fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5]\nfor (let i = 0; i < fakeY.length; i += 1) {\n  data.push({\n    x: format(new Date(beginDay + 1000 * 60 * 60 * 24 * i), 'yyyy-MM-dd'),\n    y: fakeY[i]\n  })\n}\n\nconst tooltip = [\n  'x*y',\n  (x, y) => ({\n    name: x,\n    value: y\n  })\n]\n\nconst scale = [{\n  dataKey: 'x',\n  min: 2\n}, {\n  dataKey: 'y',\n  title: '时间',\n  min: 1,\n  max: 22\n}]\n\nexport default {\n  name: 'MiniArea',\n  data () {\n    return {\n      data,\n      scale,\n      tooltip,\n      height: 100\n    }\n  }\n}\n</script>\n\n<style scoped>\n  .mini-chart {\n    position: relative;\n    width: 100%\n  }\n  .mini-chart .chart-content{\n    position: absolute;\n    bottom: -28px;\n    width: 100%;\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/chart/MiniBar.vue",
    "content": "<template>\n  <div class=\"mini-chart\">\n    <div class=\"chart-content\" :style=\"{height: 46}\">\n      <v-chart :force-fit=\"true\" :height=\"height\" :data=\"data\" :padding=\"[36, 5, 18, 5]\">\n        <v-tooltip />\n        <v-bar position=\"x*y\" />\n      </v-chart>\n    </div>\n  </div>\n</template>\n\n<script>\nimport {format} from 'date-fns'\n\nconst data = []\nconst beginDay = new Date().getTime()\n\nconst fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5]\nfor (let i = 0; i < fakeY.length; i += 1) {\n  data.push({\n    x: format(new Date(beginDay + 1000 * 60 * 60 * 24 * i), 'yyyy-MM-dd'),\n    y: fakeY[i]\n  })\n}\n\nconst tooltip = [\n  'x*y',\n  (x, y) => ({\n    name: x,\n    value: y\n  })\n]\n\nconst scale = [{\n  dataKey: 'x',\n  min: 2\n}, {\n  dataKey: 'y',\n  title: '时间',\n  min: 1,\n  max: 22\n}]\n\nexport default {\n  name: 'MiniBar',\n  data () {\n    return {\n      data,\n      scale,\n      tooltip,\n      height: 100\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n@import \"index.less\";\n</style>\n"
  },
  {
    "path": "front/src/components/chart/MiniProgress.vue",
    "content": "<template>\n  <div class=\"mini-progress\">\n    <a-tooltip :title=\"'目标值：' + target + '%'\">\n      <div class=\"target\" :style=\"{left: target + '%'}\">\n        <span :style=\"{backgroundColor: color}\" />\n        <span :style=\"{backgroundColor: color}\" />\n      </div>\n    </a-tooltip>\n    <div class=\"wrap\">\n      <div class=\"progress\" :style=\"{backgroundColor: color, width: percent + '%', height: height}\" />\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'MiniProgress',\n  props: ['target', 'color', 'percent', 'height']\n}\n</script>\n\n<style lang=\"less\"  scoped>\n  .mini-progress {\n    padding: 5px 0;\n    position: relative;\n    width: 100%;\n    .wrap {\n      background-color: @layout-bg-color;\n      position: relative;\n    }\n    .progress {\n      transition: all 0.4s cubic-bezier(0.08, 0.82, 0.17, 1) 0s;\n      border-radius: 1px 0 0 1px;\n      background-color: #13C2C2;\n      width: 0;\n      height: 100%;\n    }\n    .target {\n      position: absolute;\n      top: 0;\n      bottom: 0;\n      span {\n        border-radius: 100px;\n        position: absolute;\n        top: 0;\n        left: 0;\n        height: 4px;\n        width: 2px;\n      }\n      span:last-child {\n        top: auto;\n        bottom: 0;\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/chart/Radar.vue",
    "content": "<template>\n    <v-chart :forceFit=\"true\" height=\"400\" :data=\"data\" :padding=\"[20, 20, 95, 20]\" :scale=\"scale\">\n      <v-tooltip  />\n      <v-axis :dataKey=\"axis1Opts.dataKey\" :line=\"axis1Opts.line\" :tickLine=\"axis1Opts.tickLine\" :grid=\"axis1Opts.grid\" />\n      <v-axis :dataKey=\"axis2Opts.dataKey\" :line=\"axis2Opts.line\" :tickLine=\"axis2Opts.tickLine\" :grid=\"axis2Opts.grid\" />\n      <v-legend dataKey=\"user\" marker=\"circle\" :offset=\"30\" />\n      <v-coord type=\"polar\" radius=\"0.8\" />\n      <v-line position=\"item*score\" color=\"user\" :size=\"2\" />\n      <v-point position=\"item*score\" color=\"user\" :size=\"4\" shape=\"circle\" />\n    </v-chart>\n</template>\n\n<script>\nconst DataSet = require('@antv/data-set')\n\nconst sourceData = [\n  {item: '引用', a: 70, b: 30, c: 40},\n  {item: '口碑', a: 60, b: 70, c: 40},\n  {item: '产量', a: 50, b: 60, c: 40},\n  {item: '贡献', a: 40, b: 50, c: 40},\n  {item: '热度', a: 60, b: 70, c: 40},\n  {item: '引用', a: 70, b: 50, c: 40}\n]\n\nconst dv = new DataSet.View().source(sourceData)\ndv.transform({\n  type: 'fold',\n  fields: ['a', 'b', 'c'],\n  key: 'user',\n  value: 'score'\n})\n\nconst scale = [{\n  dataKey: 'score',\n  min: 0,\n  max: 80\n}]\n\nconst data = dv.rows\n\nconst axis1Opts = {\n  dataKey: 'item',\n  line: null,\n  tickLine: null,\n  grid: {\n    lineStyle: {\n      lineDash: null\n    },\n    hideFirstLine: false\n  }\n}\nconst axis2Opts = {\n  dataKey: 'score',\n  line: null,\n  tickLine: null,\n  grid: {\n    type: 'polygon',\n    lineStyle: {\n      lineDash: null\n    }\n  }\n}\n\nexport default {\n  name: 'Radar',\n  data () {\n    return {\n      sourceData,\n      data,\n      axis1Opts,\n      axis2Opts,\n      scale\n    }\n  }\n}\n</script>\n\n<style scoped>\n\n</style>\n"
  },
  {
    "path": "front/src/components/chart/RankingList.vue",
    "content": "<template>\n  <div class=\"rank\">\n    <h4 class=\"title\">{{title}}</h4>\n    <ul class=\"list\">\n      <li :key=\"index\" v-for=\"(item, index) in list\">\n        <span :class=\"index < 3 ? 'active' : null\">{{index + 1}}</span>\n        <span >{{item.name}}</span>\n        <span >{{item.total}}</span>\n      </li>\n    </ul>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'RankingList',\n  props: ['title', 'list']\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .rank{\n    padding: 0 32px 32px 72px;\n    .title{\n    }\n    .list{\n      margin: 25px 0 0;\n      padding: 0;\n      list-style: none;\n      li {\n        margin-top: 16px;\n        span {\n          color: @text-color-second;\n          font-size: 14px;\n          line-height: 22px;\n        }\n        span:first-child {\n          background-color: @layout-bg-color;\n          border-radius: 20px;\n          display: inline-block;\n          font-size: 12px;\n          font-weight: 600;\n          margin-right: 24px;\n          height: 20px;\n          line-height: 20px;\n          width: 20px;\n          text-align: center;\n        }\n        span.active {\n          background-color: #314659 !important;\n          color: @text-color-inverse !important;\n        }\n        span:last-child {\n          float: right;\n        }\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/chart/Trend.vue",
    "content": "<template>\n  <div class=\"chart-trend\">\n    {{term}}\n    <span>{{rate}}%</span>\n    <span :class=\"['chart-trend-icon', trend]\" style=\"\"><a-icon :type=\"'caret-' + trend\" /></span>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'Trend',\n  props: {\n    term: {\n      type: String,\n      required: true\n    },\n    target: {\n      type: Number,\n      required: false,\n      default: 0\n    },\n    value: {\n      type: Number,\n      required: false,\n      default: 0\n    },\n    isIncrease: {\n      type: Boolean,\n      required: false,\n      default: null\n    },\n    percent: {\n      type: Number,\n      required: false,\n      default: null\n    },\n    scale: {\n      type: Number,\n      required: false,\n      default: 2\n    }\n  },\n  data () {\n    return {\n      trend: this.isIncrease ? 'up' : 'down',\n      rate: this.percent\n    }\n  },\n  created () {\n    this.trend = this.caulateTrend()\n    this.rate = this.caulateRate()\n  },\n  methods: {\n    caulateRate () {\n      return (this.percent === null ? Math.abs(this.value - this.target) * 100 / this.target : this.percent).toFixed(this.scale)\n    },\n    caulateTrend () {\n      let isIncrease = this.isIncrease === null ? this.value >= this.target : this.isIncrease\n      return isIncrease ? 'up' : 'down'\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .chart-trend{\n    display: inline-block;\n    font-size: 14px;\n    .chart-trend-icon{\n      font-size: 12px;\n      &.up{\n        color: @red-6;\n      }\n      &.down{\n        color: @green-6;\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/chart/index.less",
    "content": ".mini-chart{\n  position: relative;\n  width: 100%;\n  .chart-content{\n    position: absolute;\n    bottom: -28px;\n    width: 100%;\n  }\n}\n"
  },
  {
    "path": "front/src/components/checkbox/ColorCheckbox.vue",
    "content": "<template>\n  <div class=\"theme-color\" :style=\"{backgroundColor: color}\" @click=\"toggle\">\n    <a-icon v-if=\"sChecked\" type=\"check\" />\n  </div>\n</template>\n\n<script>\nconst Group = {\n  name: 'ColorCheckboxGroup',\n  props: {\n    defaultValues: {\n      type: Array,\n      required: false,\n      default: () => []\n    },\n    multiple: {\n      type: Boolean,\n      required: false,\n      default: false\n    }\n  },\n  data () {\n    return {\n      values: [],\n      options: []\n    }\n  },\n  computed: {\n    colors () {\n      let colors = []\n      this.options.forEach(item => {\n        if (item.sChecked) {\n          colors.push(item.color)\n        }\n      })\n      return colors\n    }\n  },\n  provide () {\n    return {\n      groupContext: this\n    }\n  },\n  watch: {\n    values(value) {\n      this.$emit('change', value, this.colors)\n    }\n  },\n  methods: {\n    handleChange (option) {\n      if (!option.checked) {\n        if (this.values.indexOf(option.value) > -1) {\n          this.values = this.values.filter(item => item != option.value)\n        }\n      } else {\n        if (!this.multiple) {\n          this.values = [option.value]\n          this.options.forEach(item => {\n            if (item.value != option.value) {\n              item.sChecked = false\n            }\n          })\n        } else {\n          this.values.push(option.value)\n        }\n      }\n    }\n  },\n  render (h) {\n    const clear = h('div', {attrs: {style: 'clear: both'}})\n    return h(\n      'div',\n      {},\n      [this.$slots.default, clear]\n    )\n  }\n}\n\nexport default {\n  name: 'ColorCheckbox',\n  Group: Group,\n  props: {\n    color: {\n      type: String,\n      required: true\n    },\n    value: {\n      type: [String, Number],\n      required: true\n    },\n    checked: {\n      type: Boolean,\n      required: false,\n      default: false\n    }\n  },\n  data () {\n    return {\n      sChecked: this.initChecked()\n    }\n  },\n  computed: {\n  },\n  inject: ['groupContext'],\n  watch: {\n    'sChecked': function () {\n      const value = {\n        value: this.value,\n        color: this.color,\n        checked: this.sChecked\n      }\n      this.$emit('change', value)\n      const groupContext = this.groupContext\n      if (groupContext) {\n        groupContext.handleChange(value)\n      }\n    }\n  },\n  created () {\n    const groupContext = this.groupContext\n    if (groupContext) {\n      groupContext.options.push(this)\n    }\n  },\n  methods: {\n    toggle () {\n      if (this.groupContext.multiple || !this.sChecked) {\n        this.sChecked = !this.sChecked\n      }\n    },\n    initChecked() {\n      let groupContext = this.groupContext\n      if (!groupContext) {\n        return this.checked\n      }else if (groupContext.multiple) {\n        return groupContext.defaultValues.indexOf(this.value) > -1\n      } else {\n        return groupContext.defaultValues[0] == this.value\n      }\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .theme-color{\n    float: left;\n    width: 20px;\n    height: 20px;\n    border-radius: 2px;\n    cursor: pointer;\n    margin-right: 8px;\n    text-align: center;\n    color: @base-bg-color;\n    font-weight: bold;\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/checkbox/ImgCheckbox.vue",
    "content": "<template>\n  <a-tooltip :title=\"title\" :overlayStyle=\"{zIndex: 2001}\">\n    <div class=\"img-check-box\" @click=\"toggle\">\n      <img :src=\"img\" />\n      <div v-if=\"sChecked\" class=\"check-item\">\n        <a-icon type=\"check\" />\n      </div>\n    </div>\n  </a-tooltip>\n</template>\n\n<script>\nconst Group = {\n  name: 'ImgCheckboxGroup',\n  props: {\n    multiple: {\n      type: Boolean,\n      required: false,\n      default: false\n    },\n    defaultValues: {\n      type: Array,\n      required: false,\n      default: () => []\n    }\n  },\n  data () {\n    return {\n      values: [],\n      options: []\n    }\n  },\n  provide () {\n    return {\n      groupContext: this\n    }\n  },\n  watch: {\n    'values': function (value) {\n      this.$emit('change', value)\n      // // 此条件是为解决单选时，触发两次chang事件问题\n      // if (!(newVal.length === 1 && oldVal.length === 1 && newVal[0] === oldVal[0])) {\n      //   this.$emit('change', this.values)\n      // }\n    }\n  },\n  methods: {\n    handleChange (option) {\n      if (!option.checked) {\n        if (this.values.indexOf(option.value) > -1) {\n          this.values = this.values.filter(item => item != option.value)\n        }\n      } else {\n        if (!this.multiple) {\n          this.values = [option.value]\n          this.options.forEach(item => {\n            if (item.value != option.value) {\n              item.sChecked = false\n            }\n          })\n        } else {\n          this.values.push(option.value)\n        }\n      }\n    }\n  },\n  render (h) {\n    return h(\n      'div',\n      {\n        attrs: {style: 'display: flex'}\n      },\n      [this.$slots.default]\n    )\n  }\n}\n\nexport default {\n  name: 'ImgCheckbox',\n  Group,\n  props: {\n    checked: {\n      type: Boolean,\n      required: false,\n      default: false\n    },\n    img: {\n      type: String,\n      required: true\n    },\n    value: {\n      required: true\n    },\n    title: String\n  },\n  data () {\n    return {\n      sChecked: this.initChecked()\n    }\n  },\n  inject: ['groupContext'],\n  watch: {\n    'sChecked': function () {\n      const option = {\n        value: this.value,\n        checked: this.sChecked\n      }\n      this.$emit('change', option)\n      const groupContext = this.groupContext\n      if (groupContext) {\n        groupContext.handleChange(option)\n      }\n    }\n  },\n  created () {\n    const groupContext = this.groupContext\n    if (groupContext) {\n      this.sChecked = groupContext.defaultValues.length > 0 ? groupContext.defaultValues.indexOf(this.value) >= 0 : this.sChecked\n      groupContext.options.push(this)\n    }\n  },\n  methods: {\n    toggle () {\n      if (this.groupContext.multiple || !this.sChecked) {\n        this.sChecked = !this.sChecked\n      }\n    },\n    initChecked() {\n      let groupContext = this.groupContext\n      if (!groupContext) {\n        return this.checked\n      }else if (groupContext.multiple) {\n        return groupContext.defaultValues.indexOf(this.value) > -1\n      } else {\n        return groupContext.defaultValues[0] == this.value\n      }\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .img-check-box{\n    margin-right: 16px;\n    position: relative;\n    border-radius: 4px;\n    cursor: pointer;\n    .check-item{\n      position: absolute;\n      top: 0;\n      right: 0;\n      width: 100%;\n      padding-top: 15px;\n      padding-left: 24px;\n      height: 100%;\n      color: @primary-color;\n      font-size: 14px;\n      font-weight: bold;\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/checkbox/index.js",
    "content": "import ColorCheckbox from '@/components/checkbox/ColorCheckbox'\nimport ImgCheckbox from '@/components/checkbox/ImgCheckbox'\n\nexport {\n  ColorCheckbox,\n  ImgCheckbox\n}\n"
  },
  {
    "path": "front/src/components/exception/ExceptionPage.vue",
    "content": "<template>\n  <div class=\"exception-page\">\n    <div class=\"img\">\n      <img :src=\"config[type].img\" />\n    </div>\n    <div class=\"content\">\n      <h1>{{config[type].title}}</h1>\n      <div class=\"desc\">{{config[type].desc}}</div>\n      <div class=\"action\">\n        <a-button type=\"primary\" @click=\"backHome\">返回首页</a-button>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Config from './typeConfig'\n\nexport default {\n  name: 'ExceptionPage',\n  props: ['type', 'homeRoute'],\n  data () {\n    return {\n      config: Config\n    }\n  },\n  methods: {\n    backHome() {\n      if (this.homeRoute) {\n        this.$router.push(this.homeRoute)\n      }\n      this.$emit('backHome', this.type)\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .exception-page{\n    border-radius: 4px;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    background-color: @base-bg-color;\n    .img{\n      padding-right: 52px;\n      zoom: 1;\n      img{\n        max-width: 430px;\n      }\n    }\n    .content{\n      h1{\n        color: #434e59;\n        font-size: 72px;\n        font-weight: 600;\n        line-height: 72px;\n        margin-bottom: 24px;\n      }\n      .desc{\n        color: @text-color-second;\n        font-size: 20px;\n        line-height: 28px;\n        margin-bottom: 16px;\n      }\n    }\n  }\n\n</style>\n"
  },
  {
    "path": "front/src/components/exception/typeConfig.js",
    "content": "const config = {\n  403: {\n    img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',\n    title: '403',\n    desc: '抱歉，你无权访问该页面'\n  },\n  404: {\n    img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',\n    title: '404',\n    desc: '抱歉，你访问的页面不存在或仍在开发中'\n  },\n  500: {\n    img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',\n    title: '500',\n    desc: '抱歉，服务器出错了'\n  }\n}\n\nexport default config\n"
  },
  {
    "path": "front/src/components/form/FormRow.vue",
    "content": "<template>\n  <div class=\"form-row\">\n    <div class=\"label\">\n      <span>{{label}}</span>\n    </div>\n    <div class=\"content\">\n      <slot></slot>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'FormRow',\n  props: ['label']\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .form-row{\n    display: flex;\n    border-bottom: 1px dashed @border-color-base;\n    margin-bottom: 16px;\n    .label {\n      color: @title-color;\n      font-size: 14px;\n      margin-right: 24px;\n      flex: 0 0 auto;\n      text-align: right;\n      & > span {\n        display: inline-block;\n        height: 39px;\n        line-height: 39px;\n        &:after {\n          content: '：';\n        }\n      }\n    }\n    .content {\n      flex: 1 1 0;\n      :global {\n        .ant-form-item:last-child {\n          margin-right: 0;\n        }\n        .ant-form-item {\n          margin-bottom: 0px;\n        }\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/input/IInput.vue",
    "content": "<template>\n  <a-input\n    :addon-after=\"addonAfter\"\n    :addon-before=\"addonBefore\"\n    :default-value=\"defaultValue\"\n    :disabled=\"disabled\"\n    :id=\"id\"\n    :max-length=\"maxLength\"\n    :prefix=\"prefix\"\n    :size=\"size\"\n    :suffix=\"suffix || lenSuffix\"\n    :type=\"type\"\n    :allow-clear=\"allowClear\"\n    v-model=\"sValue\"\n    :value=\"value\"\n    @change=\"onChange\"\n    @input=\"onInput\"\n    @pressEnter=\"onPressEnter\"\n    @keydown=\"onKeydown\"\n  >\n    <template :slot=\"slot\" v-for=\"slot in Object.keys($slots)\">\n      <slot :name=\"slot\"></slot>\n    </template>\n  </a-input>\n</template>\n\n<script>\n  export default {\n    name: 'IInput',\n    model: {\n      prop: 'value',\n      event: 'change.value'\n    },\n    props: ['addonAfter', 'addonBefore', 'defaultValue', 'disabled', 'id', 'maxLength', 'prefix', 'size', 'suffix', 'type', 'value', 'allowClear'],\n    data() {\n      return {\n        sValue: this.value || this.defaultValue || ''\n      }\n    },\n    watch: {\n      value(val) {\n        this.sValue = val\n      }\n    },\n    computed: {\n      lenSuffix() {\n        return this.maxLength && `${(this.sValue + '').length}/${this.maxLength}`\n      }\n    },\n    methods: {\n      onChange(e) {\n        this.$emit('change', e)\n        this.$emit('change.value', e.target.value)\n      },\n      onInput(e) {\n        this.$emit('input', e)\n      },\n      onPressEnter(e) {\n        this.$emit('pressEnter', e)\n      },\n      onKeydown(e) {\n        this.$emit('keydown', e)\n      }\n    }\n  }\n</script>\n"
  },
  {
    "path": "front/src/components/menu/Contextmenu.vue",
    "content": "<template>\n  <a-menu\n    v-show=\"visible\"\n    class=\"contextmenu\"\n    :style=\"style\"\n    :selectedKeys=\"selectedKeys\"\n    @click=\"handleClick\"\n  >\n    <a-menu-item :key=\"item.key\" v-for=\"item in itemList\">\n      <a-icon v-if=\"item.icon\" :type=\"item.icon\" />\n      <span>{{ item.text }}</span>\n    </a-menu-item>\n  </a-menu>\n</template>\n\n<script>\nexport default {\n  name: 'Contextmenu',\n  props: {\n    visible: {\n      type: Boolean,\n      required: false,\n      default: false\n    },\n    itemList: {\n      type: Array,\n      required: true,\n      default: () => []\n    }\n  },\n  data () {\n    return {\n      left: 0,\n      top: 0,\n      target: null,\n      meta: null,\n      selectedKeys: []\n    }\n  },\n  computed: {\n    style () {\n      return {\n        left: this.left + 'px',\n        top: this.top + 'px'\n      }\n    }\n  },\n  created () {\n    window.addEventListener('click', this.closeMenu)\n    window.addEventListener('contextmenu', this.setPosition)\n  },\n  beforeDestroy() {\n    window.removeEventListener('click', this.closeMenu)\n    window.removeEventListener('contextmenu', this.setPosition)\n  },\n  methods: {\n    closeMenu () {\n      this.$emit('update:visible', false)\n    },\n    setPosition (e) {\n      this.left = e.clientX\n      this.top = e.clientY\n      this.target = e.target\n      this.meta = e.meta\n    },\n    handleClick ({ key }) {\n      this.$emit('select', key, this.target, this.meta)\n      this.closeMenu()\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .contextmenu{\n    position: fixed;\n    z-index: 1000;\n    border-radius: 4px;\n    box-shadow: -4px 4px 16px 1px @shadow-color !important;\n  }\n  .ant-menu-item {\n    margin: 0 !important // 菜单项之间的缝隙会影响点击\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/menu/SideMenu.vue",
    "content": "<template>\n  <a-layout-sider :theme=\"sideTheme\" :class=\"['side-menu', 'beauty-scroll', isMobile ? null : 'shadow']\" width=\"256px\" :collapsible=\"collapsible\" v-model=\"collapsed\" :trigger=\"null\">\n    <div :class=\"['logo', theme]\">\n      <router-link to=\"/dashboard/workplace\">\n        <img src=\"@/assets/img/logo.png\">\n        <h1>{{systemName}}</h1>\n      </router-link>\n    </div>\n    <i-menu :theme=\"theme\" :collapsed=\"collapsed\" :options=\"menuData\" @select=\"onSelect\" class=\"menu\"/>\n  </a-layout-sider>\n</template>\n\n<script>\nimport IMenu from './menu'\nimport {mapState} from 'vuex'\nexport default {\n  name: 'SideMenu',\n  components: {IMenu},\n  props: {\n    collapsible: {\n      type: Boolean,\n      required: false,\n      default: false\n    },\n    collapsed: {\n      type: Boolean,\n      required: false,\n      default: false\n    },\n    menuData: {\n      type: Array,\n      required: true\n    },\n    theme: {\n      type: String,\n      required: false,\n      default: 'dark'\n    }\n  },\n  computed: {\n    sideTheme() {\n      return this.theme == 'light' ? this.theme : 'dark'\n    },\n    ...mapState('setting', ['isMobile', 'systemName'])\n  },\n  methods: {\n    onSelect (obj) {\n      this.$emit('menuSelect', obj)\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n@import \"index\";\n</style>\n"
  },
  {
    "path": "front/src/components/menu/index.less",
    "content": ".shadow{\n  box-shadow: 2px 0 6px rgba(0, 21, 41, .35);\n}\n.side-menu{\n  min-height: 100vh;\n  overflow-y: auto;\n  z-index: 10;\n  .logo{\n    height: 64px;\n    position: relative;\n    line-height: 64px;\n    padding-left: 24px;\n    -webkit-transition: all .3s;\n    transition: all .3s;\n    overflow: hidden;\n    background-color: @layout-trigger-background;\n    &.light{\n      background-color: #fff;\n      h1{\n        color: @primary-color;\n      }\n    }\n    h1{\n      color: @menu-dark-highlight-color;\n      font-size: 20px;\n      margin: 0 0 0 12px;\n      display: inline-block;\n      vertical-align: middle;\n    }\n    img{\n      width: 32px;\n      vertical-align: middle;\n    }\n  }\n}\n.menu{\n  padding: 16px 0;\n}\n"
  },
  {
    "path": "front/src/components/menu/menu.js",
    "content": "/**\n * 该插件可根据菜单配置自动生成 ANTD menu组件\n * menuOptions示例：\n * [\n *  {\n *    name: '菜单名称',\n *    path: '菜单路由',\n *    meta: {\n *      icon: '菜单图标',\n *      invisible: 'boolean, 是否不可见, 默认 false',\n *    },\n *    children: [子菜单配置]\n *  },\n *  {\n *    name: '菜单名称',\n *    path: '菜单路由',\n *    meta: {\n *      icon: '菜单图标',\n *      invisible: 'boolean, 是否不可见, 默认 false',\n *    },\n *    children: [子菜单配置]\n *  }\n * ]\n *\n * i18n: 国际化配置。系统默认会根据 options route配置的 path 和 name 生成英文以及中文的国际化配置，如需自定义或增加其他语言，配置\n * 此项即可。如：\n * i18n: {\n *   messages: {\n *     CN: {dashboard: {name: '监控中心'}}\n *     HK: {dashboard: {name: '監控中心'}}\n *   }\n * }\n **/\nimport Menu from 'ant-design-vue/es/menu'\nimport Icon from 'ant-design-vue/es/icon'\nimport fastEqual from 'fast-deep-equal'\nimport {getI18nKey} from '@/utils/routerUtil'\n\nconst {Item, SubMenu} = Menu\n\nconst resolvePath = (path, params = {}) => {\n  let _path = path\n  Object.entries(params).forEach(([key, value]) => {\n    _path = _path.replace(new RegExp(`:${key}`, 'g'), value)\n  })\n  return _path\n}\n\nconst toRoutesMap = (routes) => {\n  const map = {}\n  routes.forEach(route => {\n    map[route.fullPath] = route\n    if (route.children && route.children.length > 0) {\n      const childrenMap = toRoutesMap(route.children)\n      Object.assign(map, childrenMap)\n    }\n  })\n  return map\n}\n\nexport default {\n  name: 'IMenu',\n  props: {\n    options: {\n      type: Array,\n      required: true\n    },\n    theme: {\n      type: String,\n      required: false,\n      default: 'dark'\n    },\n    mode: {\n      type: String,\n      required: false,\n      default: 'inline'\n    },\n    collapsed: {\n      type: Boolean,\n      required: false,\n      default: false\n    },\n    i18n: Object,\n    openKeys: Array\n  },\n  data () {\n    return {\n      selectedKeys: [],\n      sOpenKeys: [],\n      cachedOpenKeys: []\n    }\n  },\n  computed: {\n    menuTheme() {\n      return this.theme == 'light' ? this.theme : 'dark'\n    },\n    routesMap() {\n      return toRoutesMap(this.options)\n    }\n  },\n  created () {\n    this.updateMenu()\n    if (this.options.length > 0 && !this.options[0].fullPath) {\n      this.formatOptions(this.options, '')\n    }\n    // 自定义国际化配置\n    if(this.i18n && this.i18n.messages) {\n      const messages = this.i18n.messages\n      Object.keys(messages).forEach(key => {\n        this.$i18n.mergeLocaleMessage(key, messages[key])\n      })\n    }\n  },\n  watch: {\n    options(val) {\n      if (val.length > 0 && !val[0].fullPath) {\n        this.formatOptions(this.options, '')\n      }\n    },\n    i18n(val) {\n      if(val && val.messages) {\n        const messages = this.i18n.messages\n        Object.keys(messages).forEach(key => {\n          this.$i18n.mergeLocaleMessage(key, messages[key])\n        })\n      }\n    },\n    collapsed (val) {\n      if (val) {\n        this.cachedOpenKeys = this.sOpenKeys\n        this.sOpenKeys = []\n      } else {\n        this.sOpenKeys = this.cachedOpenKeys\n      }\n    },\n    '$route': function () {\n      this.updateMenu()\n    },\n    sOpenKeys(val) {\n      this.$emit('openChange', val)\n      this.$emit('update:openKeys', val)\n    }\n  },\n  methods: {\n    renderIcon: function (h, icon, key) {\n      if (this.$scopedSlots.icon && icon && icon !== 'none') {\n        const vnodes = this.$scopedSlots.icon({icon, key})\n        vnodes.forEach(vnode => {\n          vnode.data.class = vnode.data.class ? vnode.data.class : []\n          vnode.data.class.push('anticon')\n        })\n        return vnodes\n      }\n      return !icon || icon == 'none' ? null : h(Icon, {props: {type:  icon}})\n    },\n    renderMenuItem: function (h, menu) {\n      let tag = 'router-link'\n      const path = resolvePath(menu.fullPath, menu.meta.params)\n      let config = {props: {to: {path, query: menu.meta.query}, }, attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;'}}\n      if (menu.meta && menu.meta.link) {\n        tag = 'a'\n        config = {attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;', href: menu.meta.link, target: '_blank'}}\n      }\n      return h(\n        Item, {key: menu.fullPath},\n        [\n          h(tag, config,\n            [\n              this.renderIcon(h, menu.meta ? menu.meta.icon : 'none', menu.fullPath),\n              this.$t(getI18nKey(menu.fullPath))\n            ]\n          )\n        ]\n      )\n    },\n    renderSubMenu: function (h, menu) {\n      let this_ = this\n      let subItem = [h('span', {slot: 'title', attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;'}},\n        [\n          this.renderIcon(h, menu.meta ? menu.meta.icon : 'none', menu.fullPath),\n          this.$t(getI18nKey(menu.fullPath))\n        ]\n      )]\n      let itemArr = []\n      menu.children.forEach(function (item) {\n        itemArr.push(this_.renderItem(h, item))\n      })\n      return h(SubMenu, {key: menu.fullPath},\n        subItem.concat(itemArr)\n      )\n    },\n    renderItem: function (h, menu) {\n      const meta = menu.meta\n      if (!meta || !meta.invisible) {\n        let renderChildren = false\n        const children = menu.children\n        if (children != undefined) {\n          for (let i = 0; i < children.length; i++) {\n            const childMeta = children[i].meta\n            if (!childMeta || !childMeta.invisible) {\n              renderChildren = true\n              break\n            }\n          }\n        }\n        return (menu.children && renderChildren) ? this.renderSubMenu(h, menu) : this.renderMenuItem(h, menu)\n      }\n    },\n    renderMenu: function (h, menuTree) {\n      let this_ = this\n      let menuArr = []\n      menuTree.forEach(function (menu, i) {\n        menuArr.push(this_.renderItem(h, menu, '0', i))\n      })\n      return menuArr\n    },\n    formatOptions(options, parentPath) {\n      options.forEach(route => {\n        let isFullPath = route.path.substring(0, 1) == '/'\n        route.fullPath = isFullPath ? route.path : parentPath + '/' + route.path\n        if (route.children) {\n          this.formatOptions(route.children, route.fullPath)\n        }\n      })\n    },\n    updateMenu () {\n      this.selectedKeys = this.getSelectedKeys()\n      let openKeys = this.selectedKeys.filter(item => item !== '')\n      openKeys = openKeys.slice(0, openKeys.length -1)\n      if (!fastEqual(openKeys, this.sOpenKeys)) {\n        this.collapsed || this.mode === 'horizontal' ? this.cachedOpenKeys = openKeys : this.sOpenKeys = openKeys\n      }\n    },\n    getSelectedKeys() {\n      let matches = this.$route.matched\n      const route = matches[matches.length - 1]\n      let chose = this.routesMap[route.path]\n      if (chose.meta && chose.meta.highlight) {\n        chose = this.routesMap[chose.meta.highlight]\n        const resolve = this.$router.resolve({path: chose.fullPath})\n        matches = (resolve.resolved && resolve.resolved.matched) || matches\n      }\n      return matches.map(item => item.path)\n    }\n  },\n  render (h) {\n    return h(\n      Menu,\n      {\n        props: {\n          theme: this.menuTheme,\n          mode: this.$props.mode,\n          selectedKeys: this.selectedKeys,\n          openKeys: this.openKeys ? this.openKeys : this.sOpenKeys\n        },\n        on: {\n          'update:openKeys': (val) => {\n            this.sOpenKeys = val\n          },\n          click: (obj) => {\n            obj.selectedKeys = [obj.key]\n            this.$emit('select', obj)\n          }\n        }\n      }, this.renderMenu(h, this.options)\n    )\n  }\n}\n"
  },
  {
    "path": "front/src/components/page/header/PageHeader.vue",
    "content": "<template>\n  <div :class=\"['page-header', layout, pageWidth]\">\n    <div class=\"page-header-wide\">\n      <div class=\"breadcrumb\">\n        <a-breadcrumb>\n          <a-breadcrumb-item :key=\"index\" v-for=\"(item, index) in breadcrumb\">\n            <span>{{item}}</span>\n          </a-breadcrumb-item>\n        </a-breadcrumb>\n      </div>\n      <div class=\"detail\">\n        <div class=\"main\">\n          <div class=\"row\">\n            <h1 v-if=\"showPageTitle && title\" class=\"title\">{{title}}</h1>\n            <div class=\"action\"><slot name=\"action\"></slot></div>\n          </div>\n          <div class=\"row\">\n            <div v-if=\"this.$slots.content\" class=\"content\">\n              <div v-if=\"avatar\" class=\"avatar\"><a-avatar :src=\"avatar\" :size=\"72\" /></div>\n              <slot name=\"content\"></slot>\n            </div>\n            <div v-if=\"this.$slots.extra\" class=\"extra\"><slot name=\"extra\"></slot></div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport {mapState} from 'vuex'\nexport default {\n  name: 'PageHeader',\n  props: {\n    title: {\n      type: [String, Boolean],\n      required: false\n    },\n    breadcrumb: {\n      type: Array,\n      required: false\n    },\n    logo: {\n      type: String,\n      required: false\n    },\n    avatar: {\n      type: String,\n      required: false\n    },\n  },\n  computed: {\n    ...mapState('setting', ['layout', 'showPageTitle', 'pageWidth'])\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  @import \"index\";\n</style>\n"
  },
  {
    "path": "front/src/components/page/header/index.less",
    "content": ".page-header{\n  background: @base-bg-color;\n  padding: 16px 24px;\n  &.head.fixed{\n    margin: auto;\n    max-width: 1400px;\n  }\n  .page-header-wide{\n    .breadcrumb{\n      margin-bottom: 20px;\n    }\n    .detail{\n      display: flex;\n      .row {\n        display: flex;\n        flex-wrap: wrap;\n        justify-content: space-between;\n      }\n      .avatar {\n        margin:0 24px 0 0;\n      }\n      .main{\n        width: 100%;\n        .title{\n          font-size: 20px;\n          color: @title-color;\n          margin-bottom: 16px;\n        }\n        .content{\n          display: flex;\n          flex-wrap: wrap;\n          color: @text-color-second;\n        }\n        .extra{\n          display: flex;\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "front/src/components/result/Result.vue",
    "content": "<template>\n  <div class=\"result\">\n    <div >\n      <a-icon :class=\"[isSuccess ? 'success' : 'error' ,'icon']\" :type=\"isSuccess ? 'check-circle' : 'close-circle'\" />\n    </div>\n    <div class=\"title\" v-if=\"title\">{{title}}</div>\n    <div class=\"desc\" v-if=\"description\">{{description}}</div>\n    <div class=\"content\">\n      <slot></slot>\n    </div>\n    <div class=\"action\">\n      <slot name=\"action\"></slot>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'Result',\n  props: ['isSuccess', 'title', 'description']\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .result{\n    text-align: center;\n    width: 72%;\n    margin: 0 auto;\n    .icon{\n      font-size: 72px;\n      line-height: 72px;\n      margin-bottom: 24px;\n    }\n    .success {\n      color: @success-color;\n    }\n    .error {\n      color: @error-color;\n    }\n    .title{\n      font-size: 24px;\n      color: @title-color;\n      font-weight: 500;\n      line-height: 32px;\n      margin-bottom: 16px;\n    }\n    .desc{\n      font-size: 14px;\n      line-height: 22px;\n      color: @text-color-second;\n      margin-bottom: 24px;\n    }\n    .content{\n      background-color: @background-color-light;\n      padding: 24px 40px;\n      border-radius: 2px;\n      text-align: left;\n    }\n    .action{\n      margin-top: 32px;\n    }\n  }\n\n</style>\n"
  },
  {
    "path": "front/src/components/setting/Setting.vue",
    "content": "<template>\n  <div class=\"side-setting\">\n    <setting-item>\n      <a-button @click=\"saveSetting\" type=\"primary\" icon=\"save\">{{$t('save')}}</a-button>\n      <a-button @click=\"resetSetting\" type=\"dashed\" icon=\"redo\" style=\"float: right\">{{$t('reset')}}</a-button>\n    </setting-item>\n    <setting-item :title=\"$t('theme.title')\">\n      <img-checkbox-group\n        @change=\"values => setTheme({...theme, mode: values[0]})\"\n        :default-values=\"[theme.mode]\"\n      >\n        <img-checkbox :title=\"$t('theme.dark')\" img=\"https://gw.alipayobjects.com/zos/rmsportal/LCkqqYNmvBEbokSDscrm.svg\" value=\"dark\"/>\n        <img-checkbox :title=\"$t('theme.light')\" img=\"https://gw.alipayobjects.com/zos/rmsportal/jpRkZQMyYRryryPNtyIC.svg\" value=\"light\"/>\n        <img-checkbox :title=\"$t('theme.night')\" img=\"https://gw.alipayobjects.com/zos/antfincdn/hmKaLQvmY2/LCkqqYNmvBEbokSDscrm.svg\" value=\"night\"/>\n      </img-checkbox-group>\n    </setting-item>\n    <setting-item :title=\"$t('theme.color')\">\n      <color-checkbox-group\n        @change=\"(values, colors) => setTheme({...theme, color: colors[0]})\"\n        :defaultValues=\"[palettes.indexOf(theme.color)]\" :multiple=\"false\"\n      >\n        <color-checkbox v-for=\"(color, index) in palettes\" :key=\"index\" :color=\"color\" :value=\"index\" />\n      </color-checkbox-group>\n    </setting-item>\n    <a-divider/>\n    <setting-item :title=\"$t('navigate.title')\">\n      <img-checkbox-group\n        @change=\"values => setLayout(values[0])\"\n        :default-values=\"[layout]\"\n      >\n        <img-checkbox :title=\"$t('navigate.side')\" img=\"https://gw.alipayobjects.com/zos/rmsportal/JopDzEhOqwOjeNTXkoje.svg\" value=\"side\"/>\n        <img-checkbox :title=\"$t('navigate.head')\" img=\"https://gw.alipayobjects.com/zos/rmsportal/KDNDBbriJhLwuqMoxcAr.svg\" value=\"head\"/>\n        <img-checkbox :title=\"$t('navigate.mix')\" img=\"https://gw.alipayobjects.com/zos/antfincdn/x8Ob%26B8cy8/LCkqqYNmvBEbokSDscrm.svg\" value=\"mix\"/>\n      </img-checkbox-group>\n    </setting-item>\n    <setting-item>\n      <a-list :split=\"false\">\n        <a-list-item>\n          {{$t('navigate.content.title')}}\n          <a-select\n            :getPopupContainer=\"getPopupContainer\"\n            :value=\"pageWidth\"\n            @change=\"setPageWidth\"\n            class=\"select-item\" size=\"small\" slot=\"actions\"\n          >\n            <a-select-option value=\"fluid\">{{$t('navigate.content.fluid')}}</a-select-option>\n            <a-select-option value=\"fixed\">{{$t('navigate.content.fixed')}}</a-select-option>\n          </a-select>\n        </a-list-item>\n        <a-list-item>\n          {{$t('navigate.fixedHeader')}}\n          <a-switch :checked=\"fixedHeader\" slot=\"actions\" size=\"small\" @change=\"setFixedHeader\" />\n        </a-list-item>\n        <a-list-item>\n          {{$t('navigate.fixedSideBar')}}\n          <a-switch :checked=\"fixedSideBar\" slot=\"actions\" size=\"small\" @change=\"setFixedSideBar\" />\n        </a-list-item>\n      </a-list>\n    </setting-item>\n    <a-divider />\n    <setting-item :title=\"$t('other.title')\">\n      <a-list :split=\"false\">\n        <a-list-item>\n          {{$t('other.weekMode')}}\n          <a-switch :checked=\"weekMode\" slot=\"actions\" size=\"small\" @change=\"setWeekMode\" />\n        </a-list-item>\n        <a-list-item>\n          {{$t('other.multiPages')}}\n          <a-switch :checked=\"multiPage\" slot=\"actions\" size=\"small\" @change=\"setMultiPage\" />\n        </a-list-item>\n        <a-list-item>\n          {{$t('other.hideSetting')}}\n          <a-switch :checked=\"hideSetting\" slot=\"actions\" size=\"small\" @change=\"setHideSetting\" />\n        </a-list-item>\n      </a-list>\n    </setting-item>\n    <a-divider />\n    <setting-item :title=\"$t('animate.title')\">\n      <a-list :split=\"false\">\n        <a-list-item>\n          {{$t('animate.disable')}}\n          <a-switch :checked=\"animate.disabled\" slot=\"actions\" size=\"small\" @change=\"val => setAnimate({...animate, disabled: val})\" />\n        </a-list-item>\n        <a-list-item>\n          {{$t('animate.effect')}}\n          <a-select\n            :value=\"animate.name\"\n            :getPopupContainer=\"getPopupContainer\"\n            @change=\"val => setAnimate({...animate, name: val})\"\n            class=\"select-item\" size=\"small\" slot=\"actions\"\n          >\n            <a-select-option :key=\"index\" :value=\"item.name\" v-for=\"(item, index) in animates\">{{item.alias}}</a-select-option>\n          </a-select>\n        </a-list-item>\n        <a-list-item>\n          {{$t('animate.direction')}}\n          <a-select\n            :value=\"animate.direction\"\n            :getPopupContainer=\"getPopupContainer\"\n            @change=\"val => setAnimate({...animate, direction: val})\"\n            class=\"select-item\" size=\"small\" slot=\"actions\"\n          >\n            <a-select-option :key=\"index\" :value=\"item\" v-for=\"(item, index) in directions\">{{item}}</a-select-option>\n          </a-select>\n        </a-list-item>\n      </a-list>\n    </setting-item>\n    <a-alert\n      v-if=\"isDev\"\n      style=\"max-width: 240px; margin: -16px 0 8px; word-break: break-all\"\n      type=\"warning\"\n      :message=\"$t('alert')\"\n    >\n    </a-alert>\n    <a-button v-if=\"isDev\" id=\"copyBtn\" :data-clipboard-text=\"copyConfig\" @click=\"copyCode\" style=\"width: 100%\" icon=\"copy\" >{{$t('copy')}}</a-button>\n  </div>\n</template>\n\n<script>\nimport SettingItem from './SettingItem'\nimport {ColorCheckbox, ImgCheckbox} from '@/components/checkbox'\nimport Clipboard from 'clipboard'\nimport { mapState, mapMutations } from 'vuex'\nimport {formatConfig} from '@/utils/formatter'\nimport {setting} from '@/config/default'\nimport sysConfig from '@/config/config'\nimport fastEqual from 'fast-deep-equal'\nimport deepMerge from 'deepmerge'\n\nconst ColorCheckboxGroup = ColorCheckbox.Group\nconst ImgCheckboxGroup = ImgCheckbox.Group\nexport default {\n  name: 'Setting',\n  i18n: require('./i18n'),\n  components: {ImgCheckboxGroup, ImgCheckbox, ColorCheckboxGroup, ColorCheckbox, SettingItem},\n  data() {\n    return {\n      copyConfig: 'Sorry, you have copied nothing O(∩_∩)O~',\n      isDev: process.env.NODE_ENV === 'development'\n    }\n  },\n  computed: {\n    directions() {\n      return this.animates.find(item => item.name == this.animate.name).directions\n    },\n    ...mapState('setting', ['theme', 'layout', 'animate', 'animates', 'palettes', 'multiPage', 'weekMode', 'fixedHeader', 'fixedSideBar', 'hideSetting', 'pageWidth'])\n  },\n  watch: {\n    'animate.name': function(val) {\n      this.setAnimate({name: val, direction: this.directions[0]})\n    }\n  },\n  methods: {\n    getPopupContainer() {\n      return this.$el.parentNode\n    },\n    copyCode () {\n      let config = this.extractConfig(false)\n      this.copyConfig = `// 自定义配置，参考 ./default/setting.config.js，需要自定义的属性在这里配置即可\n      module.exports = ${formatConfig(config)}\n      `\n      let clipboard = new Clipboard('#copyBtn')\n      clipboard.on('success', () => {\n        this.$message.success(`复制成功，覆盖文件 src/config/config.js 然后重启项目即可生效`).then(() => {\n          const localConfig = localStorage.getItem(process.env.VUE_APP_SETTING_KEY)\n          if (localConfig) {\n            console.warn('检测到本地有历史保存的主题配置，想要要拷贝的配置代码生效，您可能需要先重置配置')\n            this.$message.warn('检测到本地有历史保存的主题配置，想要要拷贝的配置代码生效，您可能需要先重置配置', 5)\n          }\n        })\n        clipboard.destroy()\n      })\n    },\n    saveSetting() {\n      const closeMessage = this.$message.loading('正在保存到本地，请稍后...', 0)\n      const config = this.extractConfig(true)\n      localStorage.setItem(process.env.VUE_APP_SETTING_KEY, JSON.stringify(config))\n      setTimeout(closeMessage, 800)\n    },\n    resetSetting() {\n      this.$confirm({\n        title: '重置主题会刷新页面，当前页面内容不会保留，确认重置？',\n        onOk() {\n          localStorage.removeItem(process.env.VUE_APP_SETTING_KEY)\n          window.location.reload()\n        }\n      })\n    },\n    //提取配置\n    extractConfig(local = false) {\n      let config = {}\n      let mySetting = this.$store.state.setting\n      let dftSetting = local ? deepMerge(setting, sysConfig) : setting\n      Object.keys(mySetting).forEach(key => {\n        const dftValue = dftSetting[key], myValue = mySetting[key]\n        if (dftValue != undefined && !fastEqual(dftValue, myValue)) {\n          config[key] = myValue\n        }\n      })\n      return config\n    },\n    ...mapMutations('setting', ['setTheme', 'setLayout', 'setMultiPage', 'setWeekMode',\n      'setFixedSideBar', 'setFixedHeader', 'setAnimate', 'setHideSetting', 'setPageWidth'])\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .side-setting{\n    min-height: 100%;\n    background-color: @base-bg-color;\n    padding: 24px;\n    font-size: 14px;\n    line-height: 1.5;\n    word-wrap: break-word;\n    position: relative;\n    .flex{\n      display: flex;\n    }\n    .select-item{\n      width: 80px;\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/setting/SettingItem.vue",
    "content": "<template>\n  <div class=\"setting-item\">\n    <h3 v-if=\"title\" class=\"title\">{{title}}</h3>\n    <slot></slot>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'SettingItem',\n  props: ['title']\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .setting-item{\n    margin-bottom: 24px;\n    .title{\n      font-size: 14px;\n      color: @title-color;\n      line-height: 22px;\n      margin-bottom: 12px;\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/setting/i18n.js",
    "content": "module.exports = {\n  messages: {\n    CN: {\n      theme: {\n        title: '整体风格设置',\n        light: '亮色菜单风格',\n        dark: '暗色菜单风格',\n        night: '深夜模式',\n        color: '主题色'\n      },\n      navigate: {\n        title: '导航设置',\n        side: '侧边导航',\n        head: '顶部导航',\n        mix: '混合导航',\n        content: {\n          title: '内容区域宽度',\n          fluid: '流式',\n          fixed: '定宽'\n        },\n        fixedHeader: '固定Header',\n        fixedSideBar: '固定侧边栏',\n      },\n      other: {\n        title: '其他设置',\n        weekMode: '色弱模式',\n        multiPages: '多页签模式',\n        hideSetting: '隐藏设置抽屉'\n      },\n      animate: {\n        title: '页面切换动画',\n        disable: '禁用动画',\n        effect: '动画效果',\n        direction: '动画方向'\n      },\n      alert: '拷贝配置后，直接覆盖文件 src/config/config.js 中的全部内容，然后重启即可。（注意：仅会拷贝与默认配置不同的项）',\n      copy: '拷贝配置',\n      save: '保存配置',\n      reset: '重置配置',\n    },\n    HK: {\n      theme: {\n        title: '整體風格設置',\n        light: '亮色菜單風格',\n        dark: '暗色菜單風格',\n        night: '深夜模式',\n        color: '主題色'\n      },\n      navigate: {\n        title: '導航設置',\n        side: '側邊導航',\n        head: '頂部導航',\n        content: {\n          title: '內容區域寬度',\n          fluid: '流式',\n          fixed: '定寬'\n        },\n        fixedHeader: '固定Header',\n        fixedSideBar: '固定側邊欄',\n      },\n      other: {\n        title: '其他設置',\n        weekMode: '色弱模式',\n        multiPages: '多頁簽模式',\n        hideSetting: '隱藏設置抽屜'\n      },\n      animate: {\n        title: '頁面切換動畫',\n        disable: '禁用動畫',\n        effect: '動畫效果',\n        direction: '動畫方向'\n      },\n      alert: '拷貝配置后，直接覆蓋文件 src/config/config.js 中的全部內容，然後重啟即可。（注意：僅會拷貝與默認配置不同的項）',\n      copy: '拷貝配置',\n      save: '保存配置',\n      reset: '重置配置',\n    },\n    US: {\n      theme: {\n        title: 'Page Style Setting',\n        light: 'Light Style',\n        dark: 'Dark Style',\n        night: 'Night Style',\n        color: 'Theme Color'\n      },\n      navigate: {\n        title: 'Navigation Mode',\n        side: 'Side Menu Layout',\n        head: 'Top Menu Layout',\n        mix: 'Mix Menu Layout',\n        content: {\n          title: 'Content Width',\n          fluid: 'Fluid',\n          fixed: 'Fixed'\n        },\n        fixedHeader: 'Fixed Header',\n        fixedSideBar: 'Fixed SideBar',\n      },\n      other: {\n        title: 'Other Setting',\n        weekMode: 'Week Mode',\n        multiPages: 'Multi Pages',\n        hideSetting: 'Hide Setting Drawer'\n      },\n      animate: {\n        title: 'Page Toggle Animation',\n        disable: 'Disable',\n        effect: 'Effect',\n        direction: 'Direction'\n      },\n      alert: 'After copying the configuration code, directly cover all contents in the file src/config/config.js, then restart the server. (Note: only items that are different from the default configuration will be copied)',\n      copy: 'Copy Setting',\n      save: 'Save',\n      reset: 'Reset',\n    }\n  }\n}\n"
  },
  {
    "path": "front/src/components/table/StandardTable.vue",
    "content": "<template>\n  <div class=\"standard-table\">\n    <div class=\"alert\">\n      <a-alert type=\"info\" :show-icon=\"true\" v-if=\"selectedRows\">\n        <div class=\"message\" slot=\"message\">\n          已选择&nbsp;<a>{{selectedRows.length}}</a>&nbsp;项 <a class=\"clear\" @click=\"onClear\">清空</a>\n          <template  v-for=\"(item, index) in needTotalList\" >\n            <div v-if=\"item.needTotal\" :key=\"index\">\n              {{item.title}}总计&nbsp;\n              <a>{{item.customRender ? item.customRender(item.total) : item.total}}</a>\n            </div>\n          </template>\n        </div>\n      </a-alert>\n    </div>\n    <a-table\n      :bordered=\"bordered\"\n      :loading=\"loading\"\n      :columns=\"columns\"\n      :dataSource=\"dataSource\"\n      :rowKey=\"rowKey\"\n      :pagination=\"pagination\"\n      :expandedRowKeys=\"expandedRowKeys\"\n      :expandedRowRender=\"expandedRowRender\"\n      @change=\"onChange\"\n      :rowSelection=\"selectedRows ? {selectedRowKeys: selectedRowKeys, onChange: updateSelect} : undefined\"\n    >\n      <template slot-scope=\"text, record, index\" :slot=\"slot\" v-for=\"slot in Object.keys($scopedSlots).filter(key => key !== 'expandedRowRender') \">\n        <slot :name=\"slot\" v-bind=\"{text, record, index}\"></slot>\n      </template>\n      <template :slot=\"slot\" v-for=\"slot in Object.keys($slots)\">\n        <slot :name=\"slot\"></slot>\n      </template>\n      <template slot-scope=\"record, index, indent, expanded\" :slot=\"$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''\">\n        <slot v-bind=\"{record, index, indent, expanded}\" :name=\"$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''\"></slot>\n      </template>\n    </a-table>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'StandardTable',\n  props: {\n    bordered: Boolean,\n    loading: [Boolean, Object],\n    columns: Array,\n    dataSource: Array,\n    rowKey: {\n      type: [String, Function],\n      default: 'key'\n    },\n    pagination: {\n      type: [Object, Boolean],\n      default: true\n    },\n    selectedRows: Array,\n    expandedRowKeys: Array,\n    expandedRowRender: Function\n  },\n  data () {\n    return {\n      needTotalList: []\n    }\n  },\n  methods: {\n    updateSelect (selectedRowKeys, selectedRows) {\n      this.$emit('update:selectedRows', selectedRows)\n      this.$emit('selectedRowChange', selectedRowKeys, selectedRows)\n    },\n    initTotalList (columns) {\n      const totalList = columns.filter(item => item.needTotal)\n        .map(item => {\n          return {\n            ...item,\n            total: 0\n          }\n        })\n      return totalList\n    },\n    onClear() {\n      this.updateSelect([], [])\n      this.$emit('clear')\n    },\n    onChange(pagination, filters, sorter, {currentDataSource}) {\n      this.$emit('change', pagination, filters, sorter, {currentDataSource})\n    }\n  },\n  created () {\n    this.needTotalList = this.initTotalList(this.columns)\n  },\n  watch: {\n    selectedRows (selectedRows) {\n      this.needTotalList = this.needTotalList.map(item => {\n        return {\n          ...item,\n          total: selectedRows.reduce((sum, val) => {\n            let v\n            try{\n              v = val[item.dataIndex] ? val[item.dataIndex] : eval(`val.${item.dataIndex}`);\n            }catch(_){\n              v = val[item.dataIndex];\n            }\n            v = !isNaN(parseFloat(v)) ? parseFloat(v) : 0;\n            return sum + v\n          }, 0)\n        }\n      })\n    }\n  },\n  computed: {\n    selectedRowKeys() {\n      return this.selectedRows.map(record => {\n        return (typeof this.rowKey === 'function') ? this.rowKey(record) : record[this.rowKey]\n      })\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"less\">\n.standard-table{\n  .alert{\n    margin-bottom: 16px;\n    .message{\n      a{\n        font-weight: 600;\n      }\n    }\n    .clear{\n      float: right;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/components/table/advance/ActionColumns.vue",
    "content": "<template>\n  <div class=\"action-columns\" ref=\"root\">\n      <a-popover v-model=\"visible\" placement=\"bottomRight\" trigger=\"click\" :get-popup-container=\"() => $refs.root\">\n        <div slot=\"title\">\n          <a-checkbox :indeterminate=\"indeterminate\" :checked=\"checkAll\" @change=\"onCheckAllChange\" class=\"check-all\" />列展示\n          <a-button @click=\"resetColumns\" style=\"float: right\" type=\"link\" size=\"small\">重置</a-button>\n        </div>\n        <a-list style=\"width: 100%\" size=\"small\" :key=\"i\" v-for=\"(col, i) in columns\" slot=\"content\">\n          <a-list-item>\n            <a-checkbox v-model=\"col.visible\" @change=\"e => onCheckChange(e, col)\"/>\n            <template v-if=\"col.title\">\n              {{col.title}}:\n            </template>\n            <slot v-else-if=\"col.slots && col.slots.title\" :name=\"col.slots.title\"></slot>\n            <template slot=\"actions\">\n              <a-tooltip title=\"固定在列头\" :mouseEnterDelay=\"0.5\" :get-popup-container=\"() => $refs.root\">\n                <a-icon :class=\"['left', {active: col.fixed === 'left'}]\" @click=\"fixColumn('left', col)\" type=\"vertical-align-top\" />\n              </a-tooltip>\n              <a-tooltip title=\"固定在列尾\" :mouseEnterDelay=\"0.5\" :get-popup-container=\"() => $refs.root\">\n                <a-icon :class=\"['right', {active: col.fixed === 'right'}]\" @click=\"fixColumn('right', col)\" type=\"vertical-align-bottom\" />\n              </a-tooltip>\n              <a-tooltip title=\"添加搜索\" :mouseEnterDelay=\"0.5\" :get-popup-container=\"() => $refs.root\">\n                <a-icon :class=\"{active: col.searchAble}\" @click=\"setSearch(col)\" type=\"search\" />\n              </a-tooltip>\n            </template>\n          </a-list-item>\n        </a-list>\n        <a-icon class=\"action\" type=\"setting\" />\n      </a-popover>\n  </div>\n</template>\n\n<script>\n  import cloneDeep from 'lodash.clonedeep'\n\n  export default {\n    name: 'ActionColumns',\n    props: ['columns', 'visibleColumns'],\n    data() {\n      return {\n        visible: false,\n        indeterminate: false,\n        checkAll: true,\n        checkedCounts: this.columns.length,\n        backColumns: cloneDeep(this.columns)\n      }\n    },\n    watch: {\n      checkedCounts(val) {\n        this.checkAll = val === this.columns.length\n        this.indeterminate = val > 0 && val < this.columns.length\n      },\n      columns(newVal, oldVal) {\n        if (newVal != oldVal) {\n          this.checkedCounts = newVal.length\n          this.formatColumns(newVal)\n        }\n      }\n    },\n    created() {\n      this.formatColumns(this.columns)\n    },\n    methods: {\n      onCheckChange(e, col) {\n        if (!col.visible) {\n          this.checkedCounts -= 1\n        } else {\n          this.checkedCounts += 1\n        }\n      },\n      fixColumn(fixed, col) {\n        if (fixed !== col.fixed) {\n          this.$set(col, 'fixed', fixed)\n        } else {\n          this.$set(col, 'fixed', undefined)\n        }\n      },\n      setSearch(col) {\n        this.$set(col, 'searchAble', !col.searchAble)\n        if (!col.searchAble && col.search) {\n          this.resetSearch(col)\n        }\n      },\n      resetSearch(col) {\n        // col.search.value = col.dataType === 'boolean' ? false : undefined\n        col.search.value = undefined\n        col.search.backup = undefined\n      },\n      resetColumns() {\n        const {columns, backColumns} = this\n        let counts = columns.length\n        backColumns.forEach((back, index) => {\n          const column = columns[index]\n          column.visible = back.visible === undefined || back.visible\n          if (!column.visible) {\n            counts -= 1\n          }\n          if (back.fixed !== undefined) {\n            column.fixed = back.fixed\n          } else {\n            this.$set(column, 'fixed', undefined)\n          }\n          this.$set(column, 'searchAble', back.searchAble)\n          // column.searchAble = back.searchAble\n          this.resetSearch(column)\n        })\n        this.checkedCounts = counts\n        this.visible = false\n        this.$emit('reset', this.getConditions(columns))\n      },\n      onCheckAllChange(e) {\n        if (e.target.checked) {\n          this.checkedCounts = this.columns.length\n          this.columns.forEach(col => col.visible = true)\n        } else {\n          this.checkedCounts = 0\n          this.columns.forEach(col => col.visible = false)\n        }\n      },\n      getConditions(columns) {\n        const conditions = {}\n        columns.filter(item => item.search.value !== undefined && item.search.value !== '' && item.search.value !== null)\n          .forEach(col => {\n            conditions[col.dataIndex] = col.search.value\n          })\n        return conditions\n      },\n      formatColumns(columns) {\n        for (let col of columns) {\n          if (col.visible === undefined) {\n            this.$set(col, 'visible', true)\n          }\n          if (!col.visible) {\n            this.checkedCounts -= 1\n          }\n        }\n      }\n    }\n  }\n</script>\n\n<style scoped lang=\"less\">\n.action-columns{\n  display: inline-block;\n  .check-all{\n    margin-right: 8px;\n  }\n  .left,.right{\n    transform: rotate(-90deg);\n  }\n  .active{\n    color: @primary-color;\n  }\n}\n</style>"
  },
  {
    "path": "front/src/components/table/advance/ActionSize.vue",
    "content": "<template>\n  <div class=\"action-size\" ref=\"root\">\n    <a-tooltip title=\"密度\">\n      <a-dropdown placement=\"bottomCenter\" :trigger=\"['click']\" :get-popup-container=\"() => $refs.root\">\n        <a-icon class=\"action\" type=\"column-height\" />\n        <a-menu :selected-keys=\"[value]\" slot=\"overlay\" @click=\"onClick\">\n          <a-menu-item key=\"default\">\n            默认\n          </a-menu-item>\n          <a-menu-item key=\"middle\">\n            中等\n          </a-menu-item>\n          <a-menu-item key=\"small\">\n            紧密\n          </a-menu-item>\n        </a-menu>\n      </a-dropdown>\n    </a-tooltip>\n  </div>\n</template>\n\n<script>\n  export default {\n    name: 'ActionSize',\n    props: ['value'],\n    inject: ['table'],\n    data() {\n      return {\n        selectedKeys: ['middle']\n      }\n    },\n    methods: {\n      onClick({key}) {\n        this.$emit('input', key)\n      }\n    }\n  }\n</script>\n\n<style scoped lang=\"less\">\n.action-size{\n  display: inline-block;\n}\n</style>"
  },
  {
    "path": "front/src/components/table/advance/AdvanceTable.vue",
    "content": "<template>\n  <div ref=\"table\" :id=\"id\" class=\"advanced-table\">\n    <a-spin :spinning=\"loading\">\n    <div :class=\"['header-bar', size]\">\n      <div class=\"title\">\n        <template v-if=\"title\">{{title}}</template>\n        <slot v-else-if=\"$slots.title\" name=\"title\"></slot>\n        <template v-else>高级表格</template>\n      </div>\n      <div class=\"search\">\n        <search-area :format-conditions=\"formatConditions\" @change=\"onSearchChange\" :columns=\"columns\" >\n          <template :slot=\"slot\" v-for=\"slot in slots\">\n            <slot :name=\"slot\"></slot>\n          </template>\n        </search-area>\n      </div>\n      <div class=\"actions\">\n        <a-tooltip title=\"刷新\">\n          <a-icon @click=\"refresh\" class=\"action\" :type=\"loading ? 'loading' : 'reload'\" />\n        </a-tooltip>\n        <action-size v-model=\"sSize\" class=\"action\" />\n        <a-tooltip title=\"列配置\">\n          <action-columns :columns=\"columns\" @reset=\"onColumnsReset\" class=\"action\">\n            <template :slot=\"slot\" v-for=\"slot in slots\">\n              <slot :name=\"slot\"></slot>\n            </template>\n          </action-columns>\n        </a-tooltip>\n        <a-tooltip title=\"全屏\">\n          <a-icon @click=\"toggleScreen\" class=\"action\" :type=\"fullScreen ? 'fullscreen-exit' : 'fullscreen'\" />\n        </a-tooltip>\n      </div>\n    </div>\n    <a-table\n      v-bind=\"{...$props, columns: visibleColumns, title: undefined, loading: false}\"\n      :size=\"sSize\"\n      @expandedRowsChange=\"onExpandedRowsChange\"\n      @change=\"onChange\"\n      @expand=\"onExpand\"\n    >\n      <template slot-scope=\"text, record, index\" :slot=\"slot\" v-for=\"slot in scopedSlots \">\n        <slot :name=\"slot\" v-bind=\"{text, record, index}\"></slot>\n      </template>\n      <template :slot=\"slot\" v-for=\"slot in slots\">\n        <slot :name=\"slot\"></slot>\n      </template>\n      <template slot-scope=\"record, index, indent, expanded\" :slot=\"$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''\">\n        <slot v-bind=\"{record, index, indent, expanded}\" :name=\"$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''\"></slot>\n      </template>\n    </a-table>\n    </a-spin>\n  </div>\n</template>\n\n<script>\n  import ActionSize from '@/components/table/advance/ActionSize'\n  import ActionColumns from '@/components/table/advance/ActionColumns'\n  import SearchArea from '@/components/table/advance/SearchArea'\n  export default {\n    name: 'AdvanceTable',\n    components: {SearchArea, ActionColumns, ActionSize},\n    props: {\n      tableLayout: String,\n      bordered: Boolean,\n      childrenColumnName: {type: String, default: 'children'},\n      columns: Array,\n      components: Object,\n      dataSource: Array,\n      defaultExpandAllRows: Array[String],\n      expandedRowKeys: Array[String],\n      expandedRowRender: Function,\n      expandIcon: Function,\n      expandRowByClick: Boolean,\n      expandIconColumnIndex: Number,\n      footer: Function,\n      indentSize: Number,\n      loading: Boolean,\n      locale: Object,\n      pagination: [Object, Boolean],\n      rowClassName: Function,\n      rowKey: [String, Function],\n      rowSelection: Object,\n      scroll: Object,\n      showHeader: {type: Boolean, default: true},\n      size: String,\n      title: String,\n      customHeaderRow: Function,\n      customRow: Function,\n      getPopupContainer: Function,\n      transformCellText: Function,\n      formatConditions: Boolean\n    },\n    provide() {\n      return {\n        table: this\n      }\n    },\n    data() {\n      return {\n        id: `${new Date().getTime()}-${Math.floor(Math.random() * 10)}`,\n        sSize: this.size || 'default',\n        fullScreen: false,\n        conditions: {}\n      }\n    },\n    computed: {\n      slots() {\n        return Object.keys(this.$slots).filter(slot => slot !== 'title')\n      },\n      scopedSlots() {\n        return Object.keys(this.$scopedSlots).filter(slot => slot !== 'expandedRowRender' && slot !== 'title')\n      },\n      visibleColumns(){\n        return this.columns.filter(col => col.visible)\n      }\n    },\n    created() {\n      this.addListener()\n    },\n    beforeDestroy() {\n      this.removeListener()\n    },\n    methods: {\n      refresh() {\n        this.$emit('refresh', this.conditions)\n      },\n      onSearchChange(conditions, searchOptions) {\n        this.conditions = conditions\n        this.$emit('search', conditions, searchOptions)\n      },\n      toggleScreen() {\n        if (this.fullScreen) {\n          this.outFullScreen()\n        } else {\n          this.inFullScreen()\n        }\n      },\n      inFullScreen() {\n        const el = this.$refs.table\n        el.classList.add('beauty-scroll')\n        if (el.requestFullscreen) {\n          el.requestFullscreen()\n          return true\n        } else if (el.webkitRequestFullScreen) {\n          el.webkitRequestFullScreen()\n          return true\n        } else if (el.mozRequestFullScreen) {\n          el.mozRequestFullScreen()\n          return true\n        } else if (el.msRequestFullscreen) {\n          el.msRequestFullscreen()\n          return true\n        }\n        this.$message.warn('对不起，您的浏览器不支持全屏模式')\n        el.classList.remove('beauty-scroll')\n        return false\n      },\n      outFullScreen() {\n        if (document.exitFullscreen) {\n          document.exitFullscreen()\n        } else if (document.webkitCancelFullScreen) {\n          document.webkitCancelFullScreen();\n        } else if (document.mozCancelFullScreen) {\n          document.mozCancelFullScreen()\n        } else if (document.msExitFullscreen) {\n          document.msExitFullscreen()\n        }\n        this.$refs.table.classList.remove('beauty-scroll')\n      },\n      onColumnsReset(conditions) {\n        this.$emit('reset', conditions)\n      },\n      onExpandedRowsChange(expandedRows) {\n        this.$emit('expandedRowsChange', expandedRows)\n      },\n      onChange(pagination, filters, sorter, options) {\n        this.$emit('change', pagination, filters, sorter, options)\n      },\n      onExpand(expanded, record) {\n        this.$emit('expand', expanded, record)\n      },\n      addListener() {\n        document.addEventListener('fullscreenchange', this.fullScreenListener)\n        document.addEventListener('webkitfullscreenchange', this.fullScreenListener)\n        document.addEventListener('mozfullscreenchange', this.fullScreenListener)\n        document.addEventListener('msfullscreenchange', this.fullScreenListener)\n      },\n      removeListener() {\n        document.removeEventListener('fullscreenchange', this.fullScreenListener)\n        document.removeEventListener('webkitfullscreenchange', this.fullScreenListener)\n        document.removeEventListener('mozfullscreenchange', this.fullScreenListener)\n        document.removeEventListener('msfullscreenchange', this.fullScreenListener)\n      },\n      fullScreenListener(e) {\n        if (e.target.id === this.id) {\n          this.fullScreen = !this.fullScreen\n        }\n      }\n    }\n  }\n</script>\n\n<style scoped lang=\"less\">\n.advanced-table{\n  overflow-y: auto;\n  background-color: @component-background;\n  .header-bar{\n    padding: 16px 24px;\n    display: flex;\n    align-items: center;\n    border-radius: 4px;\n    transition: all 0.3s;\n    &.middle{\n      padding: 12px 16px;\n    }\n    &.small{\n      padding: 8px 12px;\n      border: 1px solid @border-color;\n      border-bottom: 0;\n      .title{\n        font-size: 16px;\n      }\n    }\n    .title{\n      transition: all 0.3s;\n      font-size: 18px;\n      color: @title-color;\n      font-weight: 700;\n    }\n    .search{\n      flex: 1;\n      text-align: right;\n      margin: 0 24px;\n    }\n    .actions{\n      text-align: right;\n      font-size: 17px;\n      color: @text-color;\n      .action{\n        margin: 0 8px;\n        cursor: pointer;\n        &:hover{\n          color: @primary-color;\n        }\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/components/table/advance/SearchArea.vue",
    "content": "<template>\n  <div class=\"search-area\" ref=\"root\">\n    <div class=\"select-root\" ref=\"selectRoot\"></div>\n    <div class=\"search-item\" :key=\"index\" v-for=\"(col, index) in searchCols\">\n      <div v-if=\"col.dataType === 'boolean'\" :class=\"['title', {active: col.search.value !== undefined}]\">\n        <template v-if=\"col.title\">\n          {{col.title}}:\n        </template>\n        <slot v-else-if=\"col.slots && col.slots.title\" :name=\"col.slots.title\"></slot>\n        <a-switch @change=\"onSwitchChange(col)\" class=\"switch\" v-model=\"col.search.value\" size=\"small\"\n                  :checked-children=\"(col.search.switchOptions && col.search.switchOptions.checkedText) || '是'\"\n                  :un-checked-children=\"(col.search.switchOptions && col.search.switchOptions.uncheckedText) || '否'\"\n        />\n        <a-icon v-if=\"col.search.value !== undefined\" class=\"close\" @click=\"e => onCloseClick(e, col)\" type=\"close-circle\" theme=\"filled\" />\n      </div>\n      <div v-else-if=\"col.dataType === 'time'\" :class=\"['title', {active: col.search.value}]\">\n        <template v-if=\"col.title\">\n          {{col.title}}:\n        </template>\n        <slot v-else-if=\"col.slots && col.slots.title\" :name=\"col.slots.title\"></slot>\n        <a-time-picker :format=\"col.search.format\" v-model=\"col.search.value\" placeholder=\"选择时间\" @change=\"(time, timeStr) => onCalendarChange(time, timeStr, col)\" @openChange=\"open => onCalendarOpenChange(open, col)\" class=\"time-picker\" size=\"small\" :get-popup-container=\"() => $refs.root\"/>\n      </div>\n      <div v-else-if=\"col.dataType === 'date'\" :class=\"['title', {active: col.search.value}]\">\n        <template v-if=\"col.title\">\n          {{col.title}}:\n        </template>\n        <slot v-else-if=\"col.slots && col.slots.title\" :name=\"col.slots.title\"></slot>\n        <a-date-picker :format=\"col.search.format\" v-model=\"col.search.value\" @change=\"onDateChange(col)\" class=\"date-picker\" size=\"small\" :getCalendarContainer=\"() => $refs.root\"/>\n      </div>\n      <div v-else-if=\"col.dataType === 'datetime'\" class=\"title datetime active\">\n        <template v-if=\"col.title\">\n          {{col.title}}:\n        </template>\n        <slot v-else-if=\"col.slots && col.slots.title\" :name=\"col.slots.title\"></slot>\n        <a-date-picker :format=\"col.search.format\" v-model=\"col.search.value\" @change=\"(date, dateStr) => onCalendarChange(date, dateStr, col)\" @openChange=\"open => onCalendarOpenChange(open, col)\" class=\"datetime-picker\" size=\"small\" show-time :getCalendarContainer=\"() => $refs.root\"/>\n      </div>\n      <div v-else-if=\"col.dataType === 'select'\" :class=\"['title', {active: col.search.value !== undefined}]\">\n        <template v-if=\"col.title\">\n          {{col.title}}:\n        </template>\n        <slot v-else-if=\"col.slots && col.slots.title\" :name=\"col.slots.title\"></slot>\n        <a-select :allowClear=\"true\" :options=\"col.search.selectOptions\" v-model=\"col.search.value\" placeholder=\"请选择...\" @change=\"onSelectChange(col)\" class=\"select\" slot=\"content\" size=\"small\" :get-popup-container=\"() => $refs.selectRoot\">\n        </a-select>\n      </div>\n      <div v-else :class=\"['title', {active: col.search.value}]\">\n        <a-popover @visibleChange=\"onVisibleChange(col, index)\" v-model=\"col.search.visible\" placement=\"bottom\" :trigger=\"['click']\" :get-popup-container=\"() => $refs.root\">\n          <template v-if=\"col.title\">\n            {{col.title}}\n          </template>\n          <slot v-else-if=\"col.slots && col.slots.title\" :name=\"col.slots.title\"></slot>\n          <div class=\"value \" v-if=\"col.search.value\">:&nbsp;&nbsp;{{col.search.format && typeof col.search.format === 'function' ? col.search.format(col.search.value) : col.search.value}}</div>\n          <a-icon v-if=\"!col.search.value\" class=\"icon-down\" type=\"down\"/>\n          <div class=\"operations\" slot=\"content\">\n            <a-button @click=\"onCancel(col)\" class=\"btn\" size=\"small\" type=\"link\">取消</a-button>\n            <a-button @click=\"onConfirm(col)\" class=\"btn\" size=\"small\" type=\"primary\">确认</a-button>\n          </div>\n          <div class=\"search-overlay\" slot=\"title\">\n            <a-input :id=\"`${searchIdPrefix}${index}`\" :allow-clear=\"true\" @keyup.esc=\"onCancel(col)\" @keyup.enter=\"onConfirm(col)\" v-model=\"col.search.value\" size=\"small\" />\n          </div>\n        </a-popover>\n        <a-icon v-if=\"col.search.value\" @click=\"e => onCloseClick(e, col)\" class=\"close\" type=\"close-circle\" theme=\"filled\"/>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\n  import fastEqual from 'fast-deep-equal'\n  import moment from 'moment'\n\n  export default {\n    name: 'SearchArea',\n    props: ['columns', 'formatConditions'],\n    inject: ['table'],\n    created() {\n      this.formatColumns(this.columns)\n    },\n    watch: {\n      columns(newVal, oldVal) {\n        if (newVal != oldVal) {\n          this.formatColumns(newVal)\n        }\n      },\n      searchCols(newVal, oldVal) {\n        if (newVal.length != oldVal.length) {\n          const newConditions = this.getConditions(newVal)\n          const newSearchOptions = this.getSearchOptions(newVal)\n          if (!fastEqual(newConditions, this.conditions)) {\n            this.conditions = newConditions\n            this.searchOptions = newSearchOptions\n            this.$emit('change', this.conditions, this.searchOptions)\n          }\n        }\n      }\n    },\n    data() {\n      return {\n        conditions: {},\n        searchOptions: []\n      }\n    },\n    computed: {\n      searchCols() {\n        return this.columns.filter(item => item.searchAble)\n      },\n      searchIdPrefix() {\n        return this.table.id + '-ipt-'\n      }\n    },\n    methods: {\n      onCloseClick(e, col) {\n        e.preventDefault()\n        e.stopPropagation()\n        col.search.value = undefined\n        const {backup, value} = col.search\n        if (backup !== value) {\n          this.backupAndEmitChange(col)\n        }\n      },\n      onCancel(col) {\n        col.search.value = col.search.backup\n        col.search.visible = false\n      },\n      onConfirm(col) {\n        const {backup, value} = col.search\n        col.search.visible = false\n        if (backup !== value) {\n          this.backupAndEmitChange(col)\n        }\n      },\n      onSwitchChange(col) {\n        const {backup, value} = col.search\n        if (backup !== value) {\n          this.backupAndEmitChange(col)\n        }\n      },\n      onSelectChange(col) {\n        this.backupAndEmitChange(col)\n      },\n      onCalendarOpenChange(open, col) {\n        col.search.visible = open\n        const {momentEqual, backupAndEmitChange} = this\n        const {value, backup, format} = col.search\n        if (!open && !momentEqual(value, backup, format)) {\n          backupAndEmitChange(col, moment(value))\n        }\n      },\n      onCalendarChange(date, dateStr, col) {\n        const {momentEqual, backupAndEmitChange} = this\n        const {value, backup, format} = col.search\n        if (!col.search.visible && !momentEqual(value, backup, format)) {\n          backupAndEmitChange(col, moment(value))\n        }\n      },\n      onDateChange(col) {\n        const {momentEqual, backupAndEmitChange} = this\n        const {value, backup, format} = col.search\n        if (!momentEqual(value, backup, format)) {\n          backupAndEmitChange(col, moment(value))\n        }\n      },\n      getFormat(col) {\n        if (col.search && col.search.format) {\n          return col.search.format\n        }\n        const dataType = col.dataType\n        switch(dataType) {\n          case 'time': return 'HH:mm:ss'\n          case 'date': return 'YYYY-MM-DD'\n          case 'datetime': return 'YYYY-MM-DD HH:mm:ss'\n          default: return undefined\n        }\n      },\n      backupAndEmitChange(col, backValue = col.search.value) {\n        const {getConditions, getSearchOptions} = this\n        col.search.backup = backValue\n        this.conditions = getConditions(this.searchCols)\n        this.searchOptions = getSearchOptions(this.searchCols)\n        this.$emit('change', this.conditions, this.searchOptions)\n      },\n      getConditions(columns) {\n        const conditions = {}\n        columns.filter(item => item.search.value !== undefined && item.search.value !== '' && item.search.value !== null)\n          .forEach(col => {\n            const {value, format} = col.search\n            if (this.formatConditions && format) {\n              if (typeof format === 'function') {\n                conditions[col.dataIndex] = format(col.search.value)\n              } else if (typeof format === 'string' && value.constructor.name === 'Moment') {\n                conditions[col.dataIndex] = value.format(format)\n              } else {\n                conditions[col.dataIndex] = value\n              }\n            } else {\n              conditions[col.dataIndex] = value\n            }\n          })\n        return conditions\n      },\n      getSearchOptions(columns) {\n        return columns.filter(item => item.search.value !== undefined && item.search.value !== '' && item.search.value !== null)\n          .map(({dataIndex, search}) => ({field: dataIndex, value: search.value, format: search.format}))\n      },\n      onVisibleChange(col, index) {\n        if (!col.search.visible) {\n          col.search.value = col.search.backup\n        } else {\n          let input = document.getElementById(`${this.searchIdPrefix}${index}`)\n          if (input) {\n            setTimeout(() => {input.focus()}, 0)\n          } else {\n            this.$nextTick(() => {\n              input = document.getElementById(`${this.searchIdPrefix}${index}`)\n              input.focus()\n            })\n          }\n        }\n      },\n      momentEqual(target, source, format) {\n        if (target === source) {\n          return true\n        } else if (target && source && target.format(format) === source.format(format)) {\n          return true\n        }\n        return false\n      },\n      formatColumns(columns) {\n        columns.forEach(item => {\n          this.$set(item, 'search', {...item.search, visible: false, value: undefined, format: this.getFormat(item)})\n        })\n      }\n    }\n  }\n</script>\n\n<style scoped lang=\"less\">\n.search-area{\n  .select-root{\n    text-align: left;\n  }\n  margin: -4px 0;\n  .search-item{\n    margin: 4px 4px;\n    display: inline-block;\n    .title{\n      padding: 4px 8px;\n      cursor: pointer;\n      border-radius: 4px;\n      user-select: none;\n      display: inline-flex;\n      align-items: center;\n      .close{\n        color: @text-color-second;\n        margin-left: 4px;\n        font-size: 12px;\n        vertical-align: middle;\n        :hover{\n          color: @text-color;\n        }\n      }\n      .switch{\n        margin-left: 4px;\n      }\n      .time-picker{\n        margin-left: 4px;\n        width: 96px;\n      }\n      .date-picker{\n        margin-left: 4px;\n        width: 120px;\n      }\n      .datetime-picker{\n        margin-left: 4px;\n        width: 195px;\n      }\n      .value{\n        display: inline-block;\n        overflow: hidden;\n        flex:1;\n        vertical-align: middle;\n        max-width: 144px;\n        text-overflow: ellipsis;\n        word-break: break-all;\n        white-space: nowrap;\n      }\n      &.active{\n        background-color: @layout-bg-color;\n      }\n    }\n    .icon-down{\n      vertical-align: middle;\n      font-size: 12px;\n    }\n  }\n  .search-overlay{\n    padding: 8px 0px;\n    text-align: center;\n  }\n  .select{\n    margin-left: 4px;\n    max-width: 144px;\n    min-width: 96px;\n    text-align: left;\n  }\n  .operations{\n    display: flex;\n    margin: -6px 0;\n    justify-content: space-between;\n    .btn{\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/components/table/advance/index.js",
    "content": "import AdvanceTable from './AdvanceTable'\nexport default AdvanceTable"
  },
  {
    "path": "front/src/components/table/api/ApiTable.vue",
    "content": "<template>\n  <a-table :data-source=\"apiSource\" :pagination=\"false\">\n    <h2 v-if=\"title\" style=\"margin: 0 16px 0\" slot=\"title\">{{title}}</h2>\n    <a-table-column width=\"20%\" data-index=\"param\" title=\"参数\">\n      <div slot-scope=\"text\" v-html=\"text\"></div>\n    </a-table-column>\n    <a-table-column width=\"50%\" data-index=\"desc\" title=\"说明\">\n      <div slot-scope=\"text\" v-html=\"text\"></div>\n    </a-table-column>\n    <a-table-column v-if=\"isApi\" width=\"15%\" data-index=\"type\" title=\"类型\">\n      <div slot-scope=\"text\" v-html=\"text\"></div>\n    </a-table-column>\n    <a-table-column v-if=\"isApi\" width=\"15%\" data-index=\"default\" title=\"默认值\">\n      <div slot-scope=\"text\" v-html=\"text\"></div>\n    </a-table-column>\n    <a-table-column v-if=\"!isApi\" width=\"30%\" data-index=\"callback\" title=\"回调函数\">\n      <div slot-scope=\"text\" v-html=\"text\"></div>\n    </a-table-column>\n  </a-table>\n</template>\n\n<script>\n  export default {\n    name: 'ApiTable',\n    props: {\n      title: {\n        type: String,\n        default: 'API'\n      },\n      type: {\n        type: String,\n        default: 'api',\n        validator(value) {\n          return ['api', 'event'].includes(value)\n        }\n      },\n      apiSource: Array\n    },\n    computed: {\n      isApi() {\n        return this.type === 'api'\n      }\n    }\n  }\n</script>\n\n<style scoped>\n\n</style>"
  },
  {
    "path": "front/src/components/task/TaskGroup.vue",
    "content": "<template>\n  <div class=\"task-group\">\n    <div class=\"task-head\">\n      <h3 class=\"title\"><span v-if=\"count\">{{count}}</span>{{title}}</h3>\n      <div class=\"actions\" style=\"float: right\">\n        <a-icon class=\"add\" type=\"plus\" draggable=\"true\"/>\n        <a-icon class=\"more\" style=\"margin-left: 8px\" type=\"ellipsis\" />\n      </div>\n    </div>\n    <div class=\"task-content\">\n      <draggable :options=\"dragOptions\">\n        <slot></slot>\n      </draggable>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Draggable from 'vuedraggable'\n\nconst dragOptions = {\n  sort: true,\n  scroll: true,\n  scrollSpeed: 2,\n  animation: 150,\n  ghostClass: 'dragable-ghost',\n  chosenClass: 'dragable-chose',\n  dragClass: 'dragable-drag'\n}\n\nexport default {\n  name: 'TaskGroup',\n  components: {Draggable},\n  props: ['title', 'group'],\n  data () {\n    return {\n      dragOptions: {...dragOptions, group: this.group}\n    }\n  },\n  computed: {\n    count () {\n      return this.$slots.default.length\n    }\n  }\n}\n</script>\n\n<style lang=\"less\">\n  .task-group{\n    width: 33.33%;\n    padding: 8px 8px;\n    background-color: @background-color-light;\n    border-radius: 6px;\n    border: 1px solid @shadow-color;\n    .task-head{\n      margin-bottom: 8px;\n      .title{\n        display: inline-block;\n        span{\n          display: inline-block;\n          border-radius: 10px;\n          margin: 0 8px;\n          font-size: 12px;\n          padding: 2px 6px;\n          background-color: @base-bg-color;\n        }\n      }\n      .actions{\n        display: inline-block;\n        float: right;\n        font-size: 18px;\n        font-weight: bold;\n        i{\n          cursor: pointer;\n        }\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/task/TaskItem.vue",
    "content": "<template>\n  <a-card class=\"task-item\" type=\"inner\">\n    {{content}}\n  </a-card>\n</template>\n\n<script>\nexport default {\n  name: 'TaskItem',\n  props: ['content']\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .task-item{\n    margin-bottom: 16px;\n    box-shadow: 0 1px 1px @shadow-color;\n    border-radius: 6px;\n    & :hover{\n      cursor: move;\n      box-shadow: 0 1px 2px @shadow-color;\n      border-radius: 6px;\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/tool/AStepItem.vue",
    "content": "<template>\n  <div\n    :class=\"['step-item', link ? 'linkable' : null]\"\n    @click=\"go\"\n  >\n    <span :style=\"titleStyle\">{{title}}</span>\n    <a-icon v-if=\"icon\" :style=\"iconStyle\" :type=\"icon\" />\n    <slot></slot>\n  </div>\n</template>\n\n<script>\nconst Group = {\n  name: 'AStepItemGroup',\n  props: {\n    align: {\n      type: String,\n      default: 'center',\n      validator(value) {\n        return ['left', 'center', 'right'].indexOf(value) != -1\n      }\n    }\n  },\n  render (h) {\n    return h(\n      'div',\n      {attrs: {style: `text-align: ${this.align}; margin-top: 8px`}},\n      [h('div', {attrs: {style: 'text-align: left; display: inline-block;'}}, [this.$slots.default])]\n    )\n  }\n}\n\nexport default {\n  name: 'AStepItem',\n  Group: Group,\n  props: ['title', 'icon', 'link', 'titleStyle', 'iconStyle'],\n  methods: {\n    go () {\n      const link = this.link\n      if (link) {\n        this.$router.push(link)\n      }\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .step-item{\n    cursor: pointer;\n  }\n  :global{\n    .ant-steps-item-process{\n      .linkable{\n        color: @primary-color;\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/tool/AvatarList.vue",
    "content": "<template>\n  <div class=\"avatar-list\">\n    <slot>\n    </slot>\n  </div>\n</template>\n\n<script>\nimport AAvatar from 'ant-design-vue/es/avatar/Avatar'\nimport ATooltip from 'ant-design-vue/es/tooltip/Tooltip'\nconst Item = {\n  name: 'AvatarListItem',\n  props: {\n    size: {\n      type: String,\n      required: false,\n      default: 'small'\n    },\n    src: {\n      type: String,\n      required: true\n    },\n    tips: {\n      type: String,\n      required: false\n    }\n  },\n  methods: {\n    renderAvatar (h, size, src) {\n      return h(AAvatar, {props: {size: size, src: src}}, [])\n    }\n  },\n  render (h) {\n    const avatar = this.renderAvatar(h, this.$props.size, this.$props.src)\n    return h(\n      'li',\n      {class: 'avatar-item'},\n      [this.$props.tips ? h(ATooltip, {props: {title: this.$props.tips}}, [avatar]) : avatar]\n    )\n  }\n}\nexport default {\n  name: 'AvatarList',\n  Item: Item\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .avatar-list {\n    display: inline-block;\n      display: inline-block;\n      margin-left: 8px;\n      font-size: 0;\n      .avatar-item {\n        display: inline-block;\n        font-size: 14px;\n        margin-left: -8px;\n        width: 20px;\n        height: 20px;\n        :global {\n          .ant-avatar {\n            border: 1px solid #fff;\n            width: 20px;\n            height: 20px;\n          }\n        }\n      }\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/tool/DetailList.vue",
    "content": "<template>\n  <div :class=\"['detail-list', size === 'small' ? 'small' : 'large', layout === 'vertical' ? 'vertical': 'horizontal']\">\n    <div v-if=\"title\" class=\"title\">{{title}}</div>\n    <a-row>\n      <slot></slot>\n    </a-row>\n  </div>\n</template>\n\n<script>\nimport ACol from 'ant-design-vue/es/grid/Col'\nconst Item = {\n  name: 'DetailListItem',\n  props: {\n    term: {\n      type: String,\n      required: false\n    }\n  },\n  inject: {\n    col: {\n      type: Number\n    }\n  },\n  methods: {\n    renderTerm (h, term) {\n      return term ? h(\n        'div',\n        {\n          attrs: {\n            class: 'term'\n          }\n        },\n        [term]\n      ) : null\n    },\n    renderContent (h, content) {\n      return h(\n        'div',\n        {\n          attrs: {\n            class: 'content'\n          }\n        },\n        [content]\n      )\n    }\n  },\n  render (h) {\n    const term = this.renderTerm(h, this.$props.term)\n    const content = this.renderContent(h, this.$slots.default)\n    return h(\n      ACol,\n      {\n        props: responsive[this.col]\n      },\n      [term, content]\n    )\n  }\n}\n\nconst responsive = {\n  1: { xs: 24 },\n  2: { xs: 24, sm: 12 },\n  3: { xs: 24, sm: 12, md: 8 },\n  4: { xs: 24, sm: 12, md: 6 }\n}\n\nexport default {\n  name: 'DetailList',\n  Item: Item,\n  props: {\n    title: {\n      type: String,\n      required: false\n    },\n    col: {\n      type: Number,\n      required: false,\n      default: 3\n    },\n    size: {\n      type: String,\n      required: false,\n      default: 'large'\n    },\n    layout: {\n      type: String,\n      required: false,\n      default: 'horizontal'\n    }\n  },\n  provide () {\n    return {\n      col: this.col > 4 ? 4 : this.col\n    }\n  }\n}\n</script>\n\n<style lang=\"less\">\n  .detail-list{\n    .title {\n      font-size: 16px;\n      color: @title-color;\n      font-weight: bolder;\n      margin-bottom: 16px;\n    }\n    .term {\n      // Line-height is 22px IE dom height will calculate error\n      line-height: 20px;\n      padding-bottom: 16px;\n      margin-right: 8px;\n      color: @title-color;\n      white-space: nowrap;\n      display: table-cell;\n      &:after {\n        content: ':';\n        margin: 0 8px 0 2px;\n        position: relative;\n        top: -0.5px;\n      }\n    }\n    .content{\n      line-height: 22px;\n      width: 100%;\n      padding-bottom: 16px;\n      color: @text-color;\n      display: table-cell;\n    }\n    &.small{\n      .title{\n        font-size: 14px;\n        color: @text-color;\n        font-weight: normal;\n        margin-bottom: 12px;\n      }\n      .term,.content{\n        padding-bottom: 8px;\n      }\n    }\n    &.large{\n      .term,.content{\n        padding-bottom: 16px;\n      }\n    }\n    &.vertical{\n      .term {\n        padding-bottom: 8px;\n      }\n      .term,.content{\n        display: block;\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/tool/Drawer.vue",
    "content": "<template>\n  <div >\n    <div :class=\"['mask', visible ? 'open' : 'close']\" @click=\"close\"></div>\n    <div :class=\"['drawer', placement, visible ? 'open' : 'close']\">\n      <div ref=\"drawer\" class=\"content beauty-scroll\">\n        <slot></slot>\n      </div>\n      <div v-if=\"showHandler\" :class=\"['handler-container', placement, visible ? 'open' : 'close']\" ref=\"handler\" @click=\"toggle\">\n        <slot v-if=\"$slots.handler\" name=\"handler\"></slot>\n        <div v-else class=\"handler\">\n          <a-icon :type=\"visible ? 'close'  : 'bars'\" />\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'Drawer',\n  data () {\n    return {\n    }\n  },\n  model: {\n    prop: 'visible',\n    event: 'change'\n  },\n  props: {\n    visible: {\n      type: Boolean,\n      required: false,\n      default: false\n    },\n    placement: {\n      type: String,\n      required: false,\n      default: 'left'\n    },\n    showHandler: {\n      type: Boolean,\n      required: false,\n      default: true\n    }\n  },\n  methods: {\n    open () {\n      this.$emit('change', true)\n    },\n    close () {\n      this.$emit('change', false)\n    },\n    toggle () {\n      this.$emit('change', !this.visible)\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .mask{\n    position: fixed;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    top: 0;\n    background-color: @shadow-color;\n    transition: all 0.5s;\n    z-index: 100;\n    &.open{\n      display: inline-block;\n    }\n    &.close{\n      display: none;\n    }\n  }\n  .drawer{\n    position: fixed;\n    transition: all 0.5s;\n    height: 100vh;\n    z-index: 100;\n    &.left{\n      left: 0px;\n      &.open{\n        .content{\n          box-shadow: 2px 0 8px @shadow-color;\n        }\n      }\n      &.close{\n        transform: translateX(-100%);\n      }\n    }\n    &.right{\n      right: 0px;\n      .content{\n        float: right;\n      }\n      &.open{\n        .content{\n          box-shadow: -2px 0 8px @shadow-color;\n        }\n      }\n      &.close{\n        transform: translateX(100%);\n      }\n    }\n  }\n  .content{\n    display: inline-block;\n    height: 100vh;\n    overflow-y: auto;\n  }\n  .handler-container{\n    position: absolute;\n    display: inline-block;\n    text-align: center;\n    transition: all 0.5s;\n    cursor: pointer;\n    top: 200px;\n    z-index: 100;\n    .handler {\n      height: 40px;\n      width: 40px;\n      background-color: @base-bg-color;\n      font-size: 26px;\n      box-shadow: 0 2px 8px @shadow-color;\n      line-height: 40px;\n    }\n    &.left{\n      right: -40px;\n      .handler{\n        border-radius: 0 5px 5px 0;\n      }\n    }\n    &.right{\n      left: -40px;\n      .handler{\n        border-radius: 5px 0 0 5px;\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/tool/FooterToolBar.vue",
    "content": "<template>\n  <div class=\"toolbar\">\n    <div style=\"float: left\">\n      <slot name=\"extra\"></slot>\n    </div>\n    <div style=\"float: right\">\n      <slot></slot>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'FooterToolBar'\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .toolbar{\n    position: fixed;\n    width: 100%;\n    bottom: 0;\n    right: 0;\n    box-shadow: 0 -1px 2px @shadow-color;\n    background: @base-bg-color;\n    border-top: 1px solid @border-color-split;\n    padding: 12px 24px;\n    z-index: 9;\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/tool/HeadInfo.vue",
    "content": "<template>\n  <div class=\"head-info\">\n    <span>{{title}}</span>\n    <p>{{content}}</p>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'HeadInfo',\n  props: ['title', 'content', 'bordered']\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .head-info{\n    text-align: center;\n    padding: 0 24px;\n    flex-grow: 1;\n    flex-shrink: 0;\n    align-self: center;\n    span{\n      color: @text-color-second;\n      display: inline-block;\n      font-size: 14px;\n      margin-bottom: 4px;\n    }\n    p{\n      color: @text-color;\n      font-size: 24px;\n      margin: 0;\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/tool/TagSelect.vue",
    "content": "<template>\n  <div class=\"tag-select\">\n    <tag-select-option @click=\"toggleCheck\">全部</tag-select-option>\n    <slot></slot>\n    <a @click=\"toggle\" v-show=\"showTrigger\" ref=\"trigger\" class=\"trigger\">展开<a-icon style=\"margin-left: 5px\" :type=\"collapsed ? 'down' : 'up'\"/></a>\n  </div>\n</template>\n\n<script>\nimport TagSelectOption from './TagSelectOption'\nexport default {\n  name: 'TagSelect',\n  Option: TagSelectOption,\n  components: {TagSelectOption},\n  data () {\n    return {\n      showTrigger: false,\n      collapsed: true,\n      screenWidth: document.body.clientWidth,\n      checkAll: false\n    }\n  },\n  watch: {\n    screenWidth: function () {\n      this.showTrigger = this.needTrigger()\n    },\n    collapsed: function (val) {\n      this.$el.style.maxHeight = val ? '39px' : '78px'\n    }\n  },\n  mounted () {\n    let _this = this\n    // 此处延迟执行，是为解决mouted未完全完成情况下引发的trigger显示bug\n    setTimeout(() => {\n      _this.showTrigger = _this.needTrigger()\n      _this.$refs.trigger.style.display = _this.showTrigger ? 'inline' : 'none'\n    }, 1)\n    window.onresize = () => {\n      return (() => {\n        window.screenWidth = document.body.clientWidth\n        _this.screenWidth = window.screenWidth\n      })()\n    }\n  },\n  methods: {\n    needTrigger () {\n      return this.$el.clientHeight < this.$el.scrollHeight || this.$el.scrollHeight > 39\n    },\n    toggle () {\n      this.collapsed = !this.collapsed\n    },\n    getAllTags () {\n      const tagList = this.$children.filter((item) => {\n        return item.isTagSelectOption\n      })\n      return tagList\n    },\n    toggleCheck () {\n      this.checkAll = !this.checkAll\n      const tagList = this.getAllTags()\n      tagList.forEach((item) => {\n        item.checked = this.checkAll\n      })\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .tag-select{\n    user-select: none;\n    position: relative;\n    overflow: hidden;\n    max-height: 39px;\n    padding-right: 50px;\n    display: inline-block;\n  }\n  .trigger{\n    position: absolute;\n    top: 0;\n    right: 0;\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/tool/TagSelectOption.vue",
    "content": "<template>\n  <a-checkable-tag @change=\"$emit('click')\" class=\"tag-default\" v-model=\"checked\">\n    <slot></slot>\n  </a-checkable-tag>\n</template>\n\n<script>\nexport default {\n  name: 'TagSelectOption',\n  props: {\n    size: {\n      type: String,\n      required: false,\n      default: 'default'\n    }\n  },\n  data () {\n    return {\n      checked: false,\n      isTagSelectOption: true\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .tag-default{\n    font-size: 14px;\n    padding: 0 8px;\n    height: auto;\n    margin-right: 24px;\n  }\n</style>\n"
  },
  {
    "path": "front/src/components/transition/PageToggleTransition.vue",
    "content": "<template>\n  <transition\n    v-if=\"!disabled\"\n    :enter-active-class=\"`animated ${enterAnimate} page-toggle-enter-active`\"\n    :leave-active-class=\"`animated ${leaveAnimate} page-toggle-leave-active`\"\n  >\n    <slot></slot>\n  </transition>\n  <div v-else><slot></slot></div>\n</template>\n\n<script>\n  import {preset as animates} from '@/config/default/animate.config'\n\n  export default {\n    name: 'PageToggleTransition',\n    props: {\n      disabled: {\n        type: Boolean,\n        default: false\n      },\n      animate: {\n        type: String,\n        validator(value) {\n          return animates.findIndex(item => item.name == value) != -1\n        },\n        default: 'bounce'\n      },\n      direction: {\n        type: String,\n        validator(value) {\n          return ['x', 'y', 'left', 'right', 'up', 'down', 'downLeft', 'upRight', 'downRight', 'upLeft', 'downBig',\n            'upBig', 'downLeft', 'downRight', 'topRight', 'bottomLeft', 'topLeft', 'bottomRight', 'default'].indexOf(value) > -1\n        }\n      },\n      reverse: {\n        type: Boolean,\n        default: true\n      }\n    },\n    computed: {\n      enterAnimate() {\n        return this.activeClass(false)\n      },\n      leaveAnimate() {\n        return this.activeClass(true)\n      }\n    },\n    methods: {\n      activeClass(isLeave) {\n        let animate = animates.find(item => this.animate == item.name)\n        if (animate == undefined) {\n          return ''\n        }\n        let direction = ''\n        if (this.direction == undefined) {\n          direction = animate.directions[0]\n        } else {\n          direction = animate.directions.find(item => item == this.direction)\n        }\n        direction = (direction == undefined || direction === 'default') ? '' : direction\n        if (direction != '') {\n          direction = isLeave && this.reverse ? this.reversePosition(direction, animate.directions) : direction\n          direction = direction[0].toUpperCase() + direction.substring(1)\n        }\n        let t = isLeave ? 'Out' : 'In'\n        return animate.name + t + direction\n      },\n      reversePosition(direction, directions) {\n        if(direction.length == 0 || direction == 'x' || direction == 'y') {\n          return direction\n        }\n        let index = directions.indexOf(direction)\n        index = (index % 2 == 1) ? index - 1 : index + 1\n        return directions[index]\n      }\n    }\n  }\n</script>\n\n<style lang=\"less\">\n  .page-toggle-enter-active{\n    position: absolute !important;\n    animation-duration: 0.8s !important;\n    width: calc(100%) !important;\n  }\n  .page-toggle-leave-active{\n    position: absolute !important;\n    animation-duration: 0.8s !important;\n    width: calc(100%) !important;\n  }\n  .page-toggle-enter{\n  }\n  .page-toggle-leave-to{\n  }\n</style>\n"
  },
  {
    "path": "front/src/config/config.js",
    "content": "// 自定义配置，参考 ./default/setting.config.js，需要自定义的属性在这里配置即可\nmodule.exports = {\n  pageWidth: 'fluid',\n  multiPage: true,\n  animate: {\n    disabled: false,\n    name: 'back',\n    direction: 'left'\n  }\n}\n"
  },
  {
    "path": "front/src/config/default/admin.config.js",
    "content": "// admin 配置\nconst ADMIN = {\n  palettes: ['#f5222d', '#fa541c', '#fadb14', '#3eaf7c', '#13c2c2', '#1890ff', '#722ed1', '#eb2f96'],\n  animates: require('./animate.config').preset,\n  theme: {\n    mode: {\n      DARK: 'dark',\n      LIGHT: 'light',\n      NIGHT: 'night'\n    }\n  },\n  layout: {\n    SIDE: 'side',\n    HEAD: 'head'\n  }\n}\n\nmodule.exports = ADMIN\n"
  },
  {
    "path": "front/src/config/default/animate.config.js",
    "content": "const direct_s = ['left', 'right']\nconst direct_1 = ['left', 'right', 'down', 'up']\nconst direct_1_b = ['downBig', 'upBig', 'leftBig', 'rightBig']\nconst direct_2 = ['topLeft', 'bottomRight', 'topRight', 'bottomLeft']\nconst direct_3 = ['downLeft', 'upRight', 'downRight', 'upLeft']\n\n// animate.css 配置\nconst ANIMATE = {\n  preset: [ //预设动画配置\n    {name: 'back', alias: '渐近', directions: direct_1},\n    {name: 'bounce', alias: '弹跳', directions: direct_1.concat('default')},\n    {name: 'fade', alias: '淡化', directions: direct_1.concat(direct_1_b).concat(direct_2).concat('default')},\n    {name: 'flip', alias: '翻转', directions: ['x', 'y']},\n    {name: 'lightSpeed', alias: '光速', directions: direct_s},\n    {name: 'rotate', alias: '旋转', directions: direct_3.concat('default')},\n    {name: 'roll', alias: '翻滚', directions: ['default']},\n    {name: 'zoom', alias: '缩放', directions: direct_1.concat('default')},\n    {name: 'slide', alias: '滑动', directions: direct_1},\n  ]\n}\nmodule.exports = ANIMATE\n"
  },
  {
    "path": "front/src/config/default/antd.config.js",
    "content": "// antd 配置\nconst ANTD = {\n  primary: {\n    color: '#1890ff',\n    warning: '#faad14',\n    success: '#52c41a',\n    error: '#f5222d',\n    light: {\n      menuColors: ['#000c17', '#001529', '#002140']\n    },\n    dark: {\n      menuColors: ['#000c17', '#001529', '#002140']\n    },\n    night: {\n      menuColors: ['#151515', '#1f1f1f', '#1e1e1e'],\n    }\n  },\n  theme: {\n    dark: {\n      'layout-body-background': '#f0f2f5',\n      'body-background': '#fff',\n      'component-background': '#fff',\n      'heading-color': 'rgba(0, 0, 0, 0.85)',\n      'text-color': 'rgba(0, 0, 0, 0.65)',\n      'text-color-inverse': '#fff',\n      'text-color-secondary': 'rgba(0, 0, 0, 0.45)',\n      'shadow-color': 'rgba(0, 0, 0, 0.15)',\n      'border-color-split': '#f0f0f0',\n      'background-color-light': '#fafafa',\n      'background-color-base': '#f5f5f5',\n      'table-selected-row-bg': '#fafafa',\n      'table-expanded-row-bg': '#fbfbfb',\n      'checkbox-check-color': '#fff',\n      'disabled-color': 'rgba(0, 0, 0, 0.25)',\n      'menu-dark-color': 'rgba(254, 254, 254, 0.65)',\n      'menu-dark-highlight-color': '#fefefe',\n      'menu-dark-arrow-color': '#fefefe',\n      'btn-primary-color': '#fff',\n    },\n    light: {\n      'layout-body-background': '#f0f2f5',\n      'body-background': '#fff',\n      'component-background': '#fff',\n      'heading-color': 'rgba(0, 0, 0, 0.85)',\n      'text-color': 'rgba(0, 0, 0, 0.65)',\n      'text-color-inverse': '#fff',\n      'text-color-secondary': 'rgba(0, 0, 0, 0.45)',\n      'shadow-color': 'rgba(0, 0, 0, 0.15)',\n      'border-color-split': '#f0f0f0',\n      'background-color-light': '#fafafa',\n      'background-color-base': '#f5f5f5',\n      'table-selected-row-bg': '#fafafa',\n      'table-expanded-row-bg': '#fbfbfb',\n      'checkbox-check-color': '#fff',\n      'disabled-color': 'rgba(0, 0, 0, 0.25)',\n      'menu-dark-color': 'rgba(1, 1, 1, 0.65)',\n      'menu-dark-highlight-color': '#fefefe',\n      'menu-dark-arrow-color': '#fefefe',\n      'btn-primary-color': '#fff',\n    },\n    night: {\n      'layout-body-background': '#000',\n      'body-background': '#141414',\n      'component-background': '#141414',\n      'heading-color': 'rgba(255, 255, 255, 0.85)',\n      'text-color': 'rgba(255, 255, 255, 0.85)',\n      'text-color-inverse': '#141414',\n      'text-color-secondary': 'rgba(255, 255, 255, 0.45)',\n      'shadow-color': 'rgba(255, 255, 255, 0.15)',\n      'border-color-split': '#303030',\n      'background-color-light': '#ffffff0a',\n      'background-color-base': '#2a2a2a',\n      'table-selected-row-bg': '#ffffff0a',\n      'table-expanded-row-bg': '#ffffff0b',\n      'checkbox-check-color': '#141414',\n      'disabled-color': 'rgba(255, 255, 255, 0.25)',\n      'menu-dark-color': 'rgba(254, 254, 254, 0.65)',\n      'menu-dark-highlight-color': '#fefefe',\n      'menu-dark-arrow-color': '#fefefe',\n      'btn-primary-color': '#141414',\n    }\n  }\n}\nmodule.exports = ANTD\n"
  },
  {
    "path": "front/src/config/default/index.js",
    "content": "const ANTD = require('./antd.config')\nconst ADMIN = require('./admin.config')\nconst ANIMATE = require('./animate.config')\nconst setting = require('./setting.config')\n\nmodule.exports = {ANTD, ADMIN, ANIMATE, setting}\n"
  },
  {
    "path": "front/src/config/default/setting.config.js",
    "content": "// 此配置为系统默认设置，需修改的设置项，在src/config/config.js中添加修改项即可。也可直接在此文件中修改。\nmodule.exports = {\n  lang: 'CN',                           //语言，可选 CN(简体)、HK(繁体)、US(英语)，也可扩展其它语言\n  theme: {                              //主题\n    color: '#1890ff',                   //主题色\n    mode: 'dark',                       //主题模式 可选 dark、 light 和 night\n    success: '#52c41a',                 //成功色\n    warning: '#faad14',                 //警告色\n    error: '#f5222f',                   //错误色\n  },\n  layout: 'side',                       //导航布局，可选 side 和 head，分别为侧边导航和顶部导航\n  fixedHeader: false,                   //固定头部状态栏，true:固定，false:不固定\n  fixedSideBar: true,                   //固定侧边栏，true:固定，false:不固定\n  fixedTabs: false,                      //固定页签头，true:固定，false:不固定\n  pageWidth: 'fixed',                   //内容区域宽度，fixed:固定宽度，fluid:流式宽度\n  weekMode: false,                      //色弱模式，true:开启，false:不开启\n  multiPage: false,                     //多页签模式，true:开启，false:不开启\n  cachePage: true,                      //是否缓存页面数据，仅多页签模式下生效，true 缓存, false 不缓存\n  hideSetting: false,                   //隐藏设置抽屉，true:隐藏，false:不隐藏\n  systemName: '客户关系管理系统',         //系统名称\n  copyright: '2021 抒颖工作室出品',     //copyright\n  asyncRoutes: false,                   //异步加载路由，true:开启，false:不开启\n  showPageTitle: true,                  //是否显示页面标题（PageLayout 布局中的页面标题），true:显示，false:不显示\n  filterMenu: true,                    //根据权限过滤菜单，true:过滤，false:不过滤\n  animate: {                            //动画设置\n    disabled: false,                    //禁用动画，true:禁用，false:启用\n    name: 'bounce',                     //动画效果，支持的动画效果可参考 ./animate.config.js\n    direction: 'left'                   //动画方向，切换页面时动画的方向，参考 ./animate.config.js\n  },\n  footerLinks: [                        //页面底部链接，{link: '链接地址', name: '名称/显示文字', icon: '图标，支持 ant design vue 图标库'}\n    {link: 'https://pro.ant.design', name: 'Pro首页'},\n    {link: 'https://msy.plus', name: '官方网站'},\n    {link: 'https://github.com/moshuying/project-3-crm',name: ' 项目地址 欢迎star ', icon: 'github'},\n    {link: 'https://ant.design', name: 'Ant Design'}\n  ],\n}\n"
  },
  {
    "path": "front/src/config/index.js",
    "content": "const deepMerge = require('deepmerge')\nconst _config = require('./config')\nconst {setting} = require('./default')\nconst config = deepMerge(setting, _config)\n\nmodule.exports = config\n"
  },
  {
    "path": "front/src/config/replacer/index.js",
    "content": "/**\n * webpack-theme-color-replacer 配置\n * webpack-theme-color-replacer 是一个高效的主题色替换插件，可以实现系统运行时动态切换主题功能。\n * 但有些情景下，我们需要为 webpack-theme-color-replacer 配置一些规则，以达到我们的个性化需求的目的\n *\n * @cssResolve: css处理规则，在 webpack-theme-color-replacer 提取 需要替换主题色的 css 后，应用此规则。一般在\n *              webpack-theme-color-replacer 默认规则无法达到我们的要求时使用。\n */\nconst cssResolve = require('./resolve.config')\nmodule.exports = {cssResolve}\n"
  },
  {
    "path": "front/src/config/replacer/resolve.config.js",
    "content": "/**\n * webpack-theme-color-replacer 插件的 resolve 配置\n * 为特定的 css 选择器（selector）配置 resolve 规则。\n *\n * key 为 css selector 值或合法的正则表达式字符串\n * 当 key 设置 css selector 值时，会匹配对应的 css\n * 当 key 设置为正则表达式时，会匹配所有满足此正则表达式的的 css\n *\n * value 可以设置为 boolean 值 false 或 一个对象\n * 当 value 为 false 时，则会忽略此 css，即此 css 不纳入 webpack-theme-color-replacer 管理\n * 当 value 为 对象时，会调用该对象的 resolve 函数，并传入 cssText（原始的 css文本） 和 cssObj（css对象）参数; resolve函数应该返\n * 回一个处理后的、合法的 css字符串（包含 selector）\n * 注意: value 不能设置为 true\n */\nconst cssResolve = {\n  '.ant-checkbox-checked .ant-checkbox-inner::after': {\n    resolve(cssText, cssObj) {\n      cssObj.rules.push('border-top:0', 'border-left:0')\n      return cssObj.toText()\n    }\n  },\n  '.ant-tree-checkbox-checked .ant-tree-checkbox-inner::after': {\n    resolve(cssText, cssObj) {\n      cssObj.rules.push('border-top:0', 'border-left:0')\n      return cssObj.toText()\n    }\n  },\n  '.ant-checkbox-checked .ant-checkbox-inner:after': {\n    resolve(cssText, cssObj) {\n      cssObj.rules.push('border-top:0', 'border-left:0')\n      return cssObj.toText()\n    }\n  },\n  '.ant-tree-checkbox-checked .ant-tree-checkbox-inner:after': {\n    resolve(cssText, cssObj) {\n      cssObj.rules.push('border-top:0', 'border-left:0')\n      return cssObj.toText()\n    }\n  },\n  '.ant-menu-dark .ant-menu-inline.ant-menu-sub': {\n    resolve(cssText, cssObj) {\n      cssObj.rules = cssObj.rules.filter(rule => rule.indexOf('box-shadow') == -1)\n      return cssObj.toText()\n    }\n  },\n  '.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu:hover,.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-submenu-selected': {\n    resolve(cssText, cssObj) {\n      cssObj.selector = cssObj.selector.replace(/.ant-menu-horizontal/g, '.ant-menu-horizontal:not(.ant-menu-dark)')\n      return cssObj.toText()\n    }\n  },\n  '.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-submenu-selected,.ant-menu-horizontal>.ant-menu-submenu:hover': {\n    resolve(cssText, cssObj) {\n      cssObj.selector = cssObj.selector.replace(/.ant-menu-horizontal/g, '.ant-menu-horizontal:not(.ant-menu-dark)')\n      return cssObj.toText()\n    }\n  },\n  '.ant-layout-sider': {\n    resolve(cssText, cssObj) {\n      cssObj.selector = '.ant-layout-sider-dark'\n      return cssObj.toText()\n    }\n  },\n  '/keyframes/': false\n}\n\nmodule.exports = cssResolve\n"
  },
  {
    "path": "front/src/layouts/AdminLayout.vue",
    "content": "<template>\n  <a-layout :class=\"['admin-layout', 'beauty-scroll']\">\n    <drawer v-if=\"isMobile\" v-model=\"drawerOpen\">\n      <side-menu :theme=\"theme.mode\" :menuData=\"menuData\" :collapsed=\"false\" :collapsible=\"false\" @menuSelect=\"onMenuSelect\"/>\n    </drawer>\n    <side-menu :class=\"[fixedSideBar ? 'fixed-side' : '']\" :theme=\"theme.mode\" v-else-if=\"layout === 'side' || layout === 'mix'\" :menuData=\"sideMenuData\" :collapsed=\"collapsed\" :collapsible=\"true\" />\n    <div v-if=\"fixedSideBar && !isMobile\" :style=\"`width: ${sideMenuWidth}; min-width: ${sideMenuWidth};max-width: ${sideMenuWidth};`\" class=\"virtual-side\"></div>\n    <drawer v-if=\"!hideSetting\" v-model=\"showSetting\" placement=\"right\">\n      <div class=\"setting\" slot=\"handler\">\n        <a-icon :type=\"showSetting ? 'close' : 'setting'\"/>\n      </div>\n      <setting />\n    </drawer>\n    <a-layout class=\"admin-layout-main beauty-scroll\">\n      <admin-header :class=\"[{'fixed-tabs': fixedTabs, 'fixed-header': fixedHeader, 'multi-page': multiPage}]\" :style=\"headerStyle\" :menuData=\"headMenuData\" :collapsed=\"collapsed\" @toggleCollapse=\"toggleCollapse\"/>\n      <a-layout-header :class=\"['virtual-header', {'fixed-tabs' : fixedTabs, 'fixed-header': fixedHeader, 'multi-page': multiPage}]\" v-show=\"fixedHeader\"></a-layout-header>\n      <a-layout-content class=\"admin-layout-content\" :style=\"`min-height: ${minHeight}px;`\">\n        <div style=\"position: relative\">\n          <slot></slot>\n        </div>\n      </a-layout-content>\n      <a-layout-footer style=\"padding: 0px\">\n        <page-footer :link-list=\"footerLinks\" :copyright=\"copyright\" />\n      </a-layout-footer>\n    </a-layout>\n  </a-layout>\n</template>\n\n<script>\nimport AdminHeader from './header/AdminHeader'\nimport PageFooter from './footer/PageFooter'\nimport Drawer from '../components/tool/Drawer'\nimport SideMenu from '../components/menu/SideMenu'\nimport Setting from '../components/setting/Setting'\nimport {mapState, mapMutations, mapGetters} from 'vuex'\n\n// const minHeight = window.innerHeight - 64 - 122\n\nexport default {\n  name: 'AdminLayout',\n  components: {Setting, SideMenu, Drawer, PageFooter, AdminHeader},\n  data () {\n    return {\n      minHeight: window.innerHeight - 64 - 122,\n      collapsed: false,\n      showSetting: false,\n      drawerOpen: false\n    }\n  },\n  provide() {\n    return {\n      adminLayout: this\n    }\n  },\n  watch: {\n    $route(val) {\n      this.setActivated(val)\n    },\n    layout() {\n      this.setActivated(this.$route)\n    },\n    isMobile(val) {\n      if(!val) {\n        this.drawerOpen = false\n      }\n    }\n  },\n  computed: {\n    ...mapState('setting', ['isMobile', 'theme', 'layout', 'footerLinks', 'copyright', 'fixedHeader', 'fixedSideBar',\n      'fixedTabs', 'hideSetting', 'multiPage']),\n    ...mapGetters('setting', ['firstMenu', 'subMenu', 'menuData']),\n    sideMenuWidth() {\n      return this.collapsed ? '80px' : '256px'\n    },\n    headerStyle() {\n      let width = (this.fixedHeader && this.layout !== 'head' && !this.isMobile) ? `calc(100% - ${this.sideMenuWidth})` : '100%'\n      let position = this.fixedHeader ? 'fixed' : 'static'\n      return `width: ${width}; position: ${position};`\n    },\n    headMenuData() {\n      const {layout, menuData, firstMenu} = this\n      return layout === 'mix' ? firstMenu : menuData\n    },\n    sideMenuData() {\n      const {layout, menuData, subMenu} = this\n      return layout === 'mix' ? subMenu : menuData\n    }\n  },\n  methods: {\n    ...mapMutations('setting', ['correctPageMinHeight', 'setActivatedFirst']),\n    toggleCollapse () {\n      this.collapsed = !this.collapsed\n    },\n    onMenuSelect () {\n      this.toggleCollapse()\n    },\n    setActivated(route) {\n      if (this.layout === 'mix') {\n        let matched = route.matched\n        matched = matched.slice(0, matched.length - 1)\n        const {firstMenu} = this\n        for (let menu of firstMenu) {\n          if (matched.findIndex(item => item.path === menu.fullPath) !== -1) {\n            this.setActivatedFirst(menu.fullPath)\n            break\n          }\n        }\n      }\n    }\n  },\n  created() {\n    this.correctPageMinHeight(this.minHeight - 24)\n    this.setActivated(this.$route)\n  },\n  beforeDestroy() {\n    this.correctPageMinHeight(-this.minHeight + 24)\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .admin-layout{\n    .side-menu{\n      &.fixed-side{\n        position: fixed;\n        height: 100vh;\n        left: 0;\n        top: 0;\n      }\n    }\n    .virtual-side{\n      transition: all 0.2s;\n    }\n    .virtual-header{\n      transition: all 0.2s;\n      opacity: 0;\n      &.fixed-tabs.multi-page:not(.fixed-header){\n        height: 0;\n      }\n    }\n    .admin-layout-main{\n      .admin-header{\n        top: 0;\n        right: 0;\n        overflow: hidden;\n        transition: all 0.2s;\n        &.fixed-tabs.multi-page:not(.fixed-header){\n          height: 0;\n        }\n      }\n    }\n    .admin-layout-content{\n      padding: 24px 24px 0;\n      /*overflow-x: hidden;*/\n      /*min-height: calc(100vh - 64px - 122px);*/\n    }\n    .setting{\n      background-color: @primary-color;\n      color: @base-bg-color;\n      border-radius: 5px 0 0 5px;\n      line-height: 40px;\n      font-size: 22px;\n      width: 40px;\n      height: 40px;\n      box-shadow: -2px 0 8px @shadow-color;\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/layouts/BlankView.vue",
    "content": "<template>\n  <page-toggle-transition :disabled=\"animate.disabled\" :animate=\"animate.name\" :direction=\"animate.direction\">\n    <router-view />\n  </page-toggle-transition>\n</template>\n\n<script>\nimport PageToggleTransition from '../components/transition/PageToggleTransition';\nimport {mapState} from 'vuex'\n\nexport default {\n  name: 'BlankView',\n  components: {PageToggleTransition},\n  computed: {\n    ...mapState('setting', ['multiPage', 'animate'])\n  }\n}\n</script>\n\n<style scoped>\n\n</style>\n"
  },
  {
    "path": "front/src/layouts/CommonLayout.vue",
    "content": "<template>\n  <div class=\"common-layout\">\n    <div class=\"content\"><slot></slot></div>\n    <page-footer :link-list=\"footerLinks\" :copyright=\"copyright\"></page-footer>\n  </div>\n</template>\n\n<script>\nimport PageFooter from '@/layouts/footer/PageFooter'\nimport {mapState} from 'vuex'\n\nexport default {\n  name: 'CommonLayout',\n  components: {PageFooter},\n  computed: {\n    ...mapState('setting', ['footerLinks', 'copyright'])\n  }\n}\n</script>\n\n<style scoped lang=\"less\">\n.common-layout{\n  display: flex;\n  flex-direction: column;\n  height: 100vh;\n  overflow: auto;\n  background-color: @layout-body-background;\n  background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');\n  background-repeat: no-repeat;\n  background-position-x: center;\n  background-position-y: 110px;\n  background-size: 100%;\n  .content{\n    padding: 32px 0;\n    flex: 1;\n    @media (min-width: 768px){\n\n      padding: 112px 0 24px;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/layouts/PageLayout.vue",
    "content": "<template>\n  <div class=\"page-layout\">\n    <page-header ref=\"pageHeader\" :style=\"`margin-top: ${multiPage ? 0 : -24}px`\" :breadcrumb=\"breadcrumb\" :title=\"pageTitle\" :logo=\"logo\" :avatar=\"avatar\">\n      <slot name=\"action\"  slot=\"action\"></slot>\n      <slot slot=\"content\" name=\"headerContent\"></slot>\n      <div slot=\"content\" v-if=\"!this.$slots.headerContent && desc\">\n        <p>{{desc}}</p>\n        <div v-if=\"this.linkList\" class=\"link\">\n          <template  v-for=\"(link, index) in linkList\">\n            <a :key=\"index\" :href=\"link.href\"><a-icon :type=\"link.icon\" />{{link.title}}</a>\n          </template>\n        </div>\n      </div>\n      <slot v-if=\"this.$slots.extra\" slot=\"extra\" name=\"extra\"></slot>\n    </page-header>\n    <div ref=\"page\" :class=\"['page-content', layout, pageWidth]\" >\n      <slot></slot>\n    </div>\n  </div>\n</template>\n\n<script>\nimport PageHeader from '@/components/page/header/PageHeader'\nimport {mapState, mapMutations} from 'vuex'\nimport {getI18nKey} from '@/utils/routerUtil'\n\nexport default {\n  name: 'PageLayout',\n  components: {PageHeader},\n  props: ['desc', 'logo', 'title', 'avatar', 'linkList', 'extraImage'],\n  data () {\n    return {\n      page: {},\n      pageHeaderHeight: 0,\n    }\n  },\n  watch: {\n    $route() {\n      this.page = this.$route.meta.page\n    }\n  },\n  updated() {\n    if (!this._inactive) {\n      this.updatePageHeight()\n    }\n  },\n  activated() {\n    this.updatePageHeight()\n  },\n  deactivated() {\n    this.updatePageHeight(0)\n  },\n  mounted() {\n    this.updatePageHeight()\n  },\n  created() {\n    this.page = this.$route.meta.page\n  },\n  beforeDestroy() {\n    this.updatePageHeight(0)\n  },\n  computed: {\n    ...mapState('setting', ['layout', 'multiPage', 'pageMinHeight', 'pageWidth', 'customTitles']),\n    pageTitle() {\n      let pageTitle = this.page && this.page.title\n      return this.customTitle || (pageTitle && this.$t(pageTitle)) || this.title || this.routeName\n    },\n    routeName() {\n      const route = this.$route\n      return this.$t(getI18nKey(route.matched[route.matched.length - 1].path))\n    },\n    breadcrumb() {\n      let page = this.page\n      let breadcrumb = page && page.breadcrumb\n      if (breadcrumb) {\n        let i18nBreadcrumb = []\n        breadcrumb.forEach(item => {\n          i18nBreadcrumb.push(this.$t(item))\n        })\n        return i18nBreadcrumb\n      } else {\n        return this.getRouteBreadcrumb()\n      }\n    },\n    marginCorrect() {\n      return this.multiPage ? 24 : 0\n    }\n  },\n  methods: {\n    ...mapMutations('setting', ['correctPageMinHeight']),\n    getRouteBreadcrumb() {\n      let routes = this.$route.matched\n      const path = this.$route.path\n      let breadcrumb = []\n      routes.filter(item => path.includes(item.path))\n        .forEach(route => {\n        const path = route.path.length === 0 ? '/home' : route.path\n        breadcrumb.push(this.$t(getI18nKey(path)))\n      })\n      let pageTitle = this.page && this.page.title\n      if (this.customTitle || pageTitle) {\n        breadcrumb[breadcrumb.length - 1] = this.customTitle || pageTitle\n      }\n      return breadcrumb\n    },\n    /**\n     * 用于计算页面内容最小高度\n     * @param newHeight\n     */\n    updatePageHeight(newHeight = this.$refs.pageHeader.$el.offsetHeight + this.marginCorrect) {\n      this.correctPageMinHeight(this.pageHeaderHeight - newHeight)\n      this.pageHeaderHeight = newHeight\n    }\n  }\n}\n</script>\n\n<style lang=\"less\">\n  .page-header{\n    margin: 0 -24px 0;\n  }\n  .link{\n    /*margin-top: 16px;*/\n    line-height: 24px;\n    a{\n      font-size: 14px;\n      margin-right: 32px;\n      i{\n        font-size: 22px;\n        margin-right: 8px;\n      }\n    }\n  }\n  .page-content{\n    position: relative;\n    padding: 24px 0 0;\n    &.side{\n    }\n    &.head.fixed{\n      margin: 0 auto;\n      max-width: 1400px;\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/layouts/PageView.vue",
    "content": "<template>\n  <page-layout :desc=\"desc\" :linkList=\"linkList\">\n    <div v-if=\"this.extraImage && !isMobile\" slot=\"extra\" class=\"extraImg\">\n      <img :src=\"extraImage\"/>\n    </div>\n    <page-toggle-transition :disabled=\"animate.disabled\" :animate=\"animate.name\" :direction=\"animate.direction\">\n        <router-view ref=\"page\" />\n    </page-toggle-transition>\n  </page-layout>\n</template>\n\n<script>\nimport PageLayout from './PageLayout'\nimport PageToggleTransition from '../components/transition/PageToggleTransition';\nimport {mapState} from 'vuex'\n\nexport default {\n  name: 'PageView',\n  components: {PageToggleTransition, PageLayout},\n  data () {\n    return {\n      page: {}\n    }\n  },\n  computed: {\n    ...mapState('setting', ['isMobile', 'multiPage', 'animate']),\n    desc() {\n      return this.page.desc\n    },\n    linkList() {\n      return this.page.linkList\n    },\n    extraImage() {\n      return this.page.extraImage\n    }\n  },\n  mounted () {\n    this.page = this.$refs.page\n  },\n  updated () {\n    this.page = this.$refs.page\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .extraImg{\n    margin-top: -60px;\n    text-align: center;\n    width: 195px;\n    img{\n      width: 100%;\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/layouts/footer/PageFooter.vue",
    "content": "<template>\n  <div class=\"footer\">\n    <div class=\"links\">\n      <a target=\"_blank\" :key=\"index\" :href=\"item.link ? item.link : 'javascript: void(0)'\" v-for=\"(item, index) in linkList\">\n        <a-icon v-if=\"item.icon\" :type=\"item.icon\"/>{{item.name}}\n      </a>\n    </div>\n    <div class=\"copyright\">\n      Copyright<a-icon type=\"copyright\" />{{copyright}}\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'PageFooter',\n  props: ['copyright', 'linkList']\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .footer{\n    padding: 48px 16px 24px;\n    /*margin: 48px 0 24px;*/\n    text-align: center;\n    .copyright{\n      color: @text-color-second;\n      font-size: 14px;\n      i {\n          margin: 0 4px;\n      }\n    }\n    .links{\n      margin-bottom: 8px;\n      a:not(:last-child) {\n        margin-right: 40px;\n      }\n      a{\n        color: @text-color-second;\n        -webkit-transition: all .3s;\n        transition: all .3s;\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/layouts/header/AdminHeader.vue",
    "content": "<template>\n  <a-layout-header :class=\"[headerTheme, 'admin-header']\">\n    <div :class=\"['admin-header-wide', layout, pageWidth]\">\n      <router-link v-if=\"isMobile || layout === 'head'\" to=\"/\" :class=\"['logo', isMobile ? null : 'pc', headerTheme]\">\n        <img width=\"32\" src=\"@/assets/img/logo.png\" />\n        <h1 v-if=\"!isMobile\">{{systemName}}</h1>\n      </router-link>\n      <a-divider v-if=\"isMobile\" type=\"vertical\" />\n      <a-icon v-if=\"layout !== 'head'\" class=\"trigger\" :type=\"collapsed ? 'menu-unfold' : 'menu-fold'\" @click=\"toggleCollapse\"/>\n      <div v-if=\"layout !== 'side' && !isMobile\" class=\"admin-header-menu\" :style=\"`width: ${menuWidth};`\">\n        <i-menu class=\"head-menu\" :theme=\"headerTheme\" mode=\"horizontal\" :options=\"menuData\" @select=\"onSelect\"/>\n      </div>\n      <div :class=\"['admin-header-right', headerTheme]\">\n<!--          <header-search class=\"header-item\" @active=\"val => searchActive = val\" />-->\n          <a-tooltip class=\"header-item\" title=\"帮助文档\" placement=\"bottom\" >\n            <a href=\"https://iczer.gitee.io/vue-antd-admin-docs/\" target=\"_blank\">\n              <a-icon type=\"question-circle-o\" />\n            </a>\n          </a-tooltip>\n<!--          <header-notice class=\"header-item\"/>-->\n          <header-avatar class=\"header-item\"/>\n          <a-dropdown class=\"lang header-item\">\n            <div>\n              <a-icon type=\"global\"/> {{langAlias}}\n            </div>\n            <a-menu @click=\"val => setLang(val.key)\" :selected-keys=\"[lang]\" slot=\"overlay\">\n              <a-menu-item v-for=\" lang in langList\" :key=\"lang.key\">{{lang.key.toLowerCase() + ' ' + lang.name}}</a-menu-item>\n            </a-menu>\n          </a-dropdown>\n      </div>\n    </div>\n  </a-layout-header>\n</template>\n\n<script>\nimport HeaderAvatar from './HeaderAvatar'\nimport IMenu from '@/components/menu/menu'\nimport {mapState, mapMutations} from 'vuex'\n\nexport default {\n  name: 'AdminHeader',\n  components: {IMenu, HeaderAvatar},\n  props: ['collapsed', 'menuData'],\n  data() {\n    return {\n      langList: [\n        {key: 'CN', name: '简体中文', alias: '简体'},\n        // {key: 'HK', name: '繁體中文', alias: '繁體'},\n        // {key: 'US', name: 'English', alias: 'English'}\n      ],\n      searchActive: false\n    }\n  },\n  computed: {\n    ...mapState('setting', ['theme', 'isMobile', 'layout', 'systemName', 'lang', 'pageWidth']),\n    headerTheme () {\n      if (this.layout == 'side' && this.theme.mode == 'dark' && !this.isMobile) {\n        return 'light'\n      }\n      return this.theme.mode\n    },\n    langAlias() {\n      let lang = this.langList.find(item => item.key == this.lang)\n      return lang.alias\n    },\n    menuWidth() {\n      const {layout, searchActive} = this\n      const headWidth = layout === 'head' ? '100% - 188px' : '100%'\n      const extraWidth = searchActive ? '600px' : '400px'\n      return `calc(${headWidth} - ${extraWidth})`\n    }\n  },\n  methods: {\n    toggleCollapse () {\n      this.$emit('toggleCollapse')\n    },\n    onSelect (obj) {\n      this.$emit('menuSelect', obj)\n    },\n    ...mapMutations('setting', ['setLang'])\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n@import \"index\";\n</style>\n"
  },
  {
    "path": "front/src/layouts/header/HeaderAvatar.vue",
    "content": "<template>\n  <a-dropdown>\n    <div class=\"header-avatar\" style=\"cursor: pointer\">\n      <a-avatar class=\"avatar\" size=\"small\" shape=\"circle\" :src=\"user.avatar\"/>\n      <span class=\"name\">{{user.name}}</span>\n    </div>\n    <a-menu :class=\"['avatar-menu']\" slot=\"overlay\">\n<!--      <a-menu-item>-->\n<!--        <a-icon type=\"user\" />-->\n<!--        <span>个人中心</span>-->\n<!--      </a-menu-item>-->\n<!--      <a-menu-item>-->\n<!--        <a-icon type=\"setting\" />-->\n<!--        <span>设置</span>-->\n<!--      </a-menu-item>-->\n<!--      <a-menu-divider />-->\n      <a-menu-item @click=\"logout\">\n        <a-icon style=\"margin-right: 8px;\" type=\"poweroff\" />\n        <span>退出登录</span>\n      </a-menu-item>\n    </a-menu>\n  </a-dropdown>\n</template>\n\n<script>\nimport {mapGetters} from 'vuex'\nimport {logout} from '@/services/user'\n\nexport default {\n  name: 'HeaderAvatar',\n  computed: {\n    ...mapGetters('account', ['user']),\n  },\n  methods: {\n    logout() {\n      logout()\n      this.$router.push('/login')\n    }\n  }\n}\n</script>\n\n<style lang=\"less\">\n  .header-avatar{\n    display: inline-flex;\n    .avatar, .name{\n      align-self: center;\n    }\n    .avatar{\n      margin-right: 8px;\n    }\n    .name{\n      font-weight: 500;\n    }\n  }\n  .avatar-menu{\n    width: 150px;\n  }\n\n</style>\n"
  },
  {
    "path": "front/src/layouts/header/HeaderNotice.vue",
    "content": "<template>\n  <a-dropdown :trigger=\"['click']\" v-model=\"show\">\n    <div slot=\"overlay\">\n      <a-spin :spinning=\"loading\">\n        <a-tabs class=\"dropdown-tabs\" :tabBarStyle=\"{textAlign: 'center'}\" :style=\"{width: '297px'}\">\n          <a-tab-pane tab=\"通知\" key=\"1\">\n            <a-list class=\"tab-pane\">\n              <a-list-item>\n                <a-list-item-meta title=\"你收到了 14 份新周报\" description=\"一年前\">\n                  <a-avatar style=\"background-color: white\" slot=\"avatar\" src=\"https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png\"/>\n                </a-list-item-meta>\n              </a-list-item>\n              <a-list-item>\n                <a-list-item-meta title=\"你推荐的 曲妮妮 已通过第三轮面试\" description=\"一年前\">\n                  <a-avatar style=\"background-color: white\" slot=\"avatar\" src=\"https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png\"/>\n                </a-list-item-meta>\n              </a-list-item>\n              <a-list-item>\n                <a-list-item-meta title=\"这种模板可以区分多种通知类型\" description=\"一年前\">\n                  <a-avatar style=\"background-color: white\" slot=\"avatar\" src=\"https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png\"/>\n                </a-list-item-meta>\n              </a-list-item>\n            </a-list>\n          </a-tab-pane>\n          <a-tab-pane tab=\"消息\" key=\"2\">\n            <a-list class=\"tab-pane\"></a-list>\n          </a-tab-pane>\n          <a-tab-pane tab=\"待办\" key=\"3\">\n            <a-list class=\"tab-pane\"></a-list>\n          </a-tab-pane>\n        </a-tabs>\n      </a-spin>\n    </div>\n    <span @click=\"fetchNotice\" class=\"header-notice\">\n      <a-badge class=\"notice-badge\" count=\"12\">\n        <a-icon :class=\"['header-notice-icon']\" type=\"bell\" />\n      </a-badge>\n    </span>\n  </a-dropdown>\n</template>\n\n<script>\nexport default {\n  name: 'HeaderNotice',\n  data () {\n    return {\n      loading: false,\n      show: false\n    }\n  },\n  computed: {\n  },\n  methods: {\n    fetchNotice () {\n      if (this.loading) {\n        this.loading = false\n        return\n      }\n      this.loadding = true\n      setTimeout(() => {\n        this.loadding = false\n      }, 1000)\n    }\n  }\n}\n</script>\n\n<style lang=\"less\">\n  .header-notice{\n    display: inline-block;\n    transition: all 0.3s;\n    span {\n      vertical-align: initial;\n    }\n    .notice-badge{\n      color: inherit;\n      .header-notice-icon{\n        font-size: 16px;\n        padding: 4px;\n      }\n    }\n  }\n  .dropdown-tabs{\n    background-color: @base-bg-color;\n    box-shadow: 0 2px 8px @shadow-color;\n    border-radius: 4px;\n    .tab-pane{\n      padding: 0 24px 12px;\n      min-height: 250px;\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/layouts/header/HeaderSearch.vue",
    "content": "<template>\n  <div class=\"header-search\">\n    <a-icon type=\"search\" class=\"search-icon\" @click=\"enterSearchMode\"/>\n    <a-auto-complete\n      ref=\"input\"\n      :getPopupContainer=\"e => {return e.parentNode || document.body}\"\n      :dataSource=\"dataSource\"\n      :class=\"['search-input', searchMode ? 'enter' : 'leave']\"\n      placeholder=\"站内搜索\"\n      @blur=\"leaveSearchMode\"\n    >\n    </a-auto-complete>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'HeaderSearch',\n  data () {\n    return {\n      dataSource: ['选项一', '选项二'],\n      searchMode: false\n    }\n  },\n  methods: {\n    enterSearchMode () {\n      this.searchMode = true\n      this.$emit('active', true)\n      setTimeout(() => this.$refs.input.focus(), 300)\n    },\n    leaveSearchMode () {\n      this.searchMode = false\n      setTimeout(() => this.$emit('active', false), 300)\n    }\n  }\n}\n</script>\n\n<style lang=\"less\">\n  .header-search{\n    .search-icon{\n      font-size: 16px;\n      cursor: pointer;\n    }\n    .search-input{\n      border: 0;\n      border-bottom: 1px solid @border-color-split;\n      transition: width 0.3s ease-in-out;\n      input{\n        border: 0;\n        box-shadow: 0 0 0 0;\n      }\n      &.leave{\n        width: 0px;\n        input{\n          display: none;\n        }\n      }\n      &.enter{\n        width: 200px;\n        input:focus{\n          box-shadow: 0 0 0 0;\n        }\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/layouts/header/index.less",
    "content": ".admin-header{\n  padding: 0;\n  z-index: 2;\n  box-shadow: @shadow-down;\n  position: relative;\n  background: @base-bg-color;\n  .head-menu{\n    height: 64px;\n    line-height: 64px;\n    vertical-align: middle;\n    box-shadow: none;\n  }\n  &.dark{\n    background: @header-bg-color-dark;\n    color: white;\n  }\n  &.night{\n    .head-menu{\n      background: @base-bg-color;\n    }\n  }\n  .admin-header-wide{\n    padding-left: 24px;\n    &.head.fixed{\n      max-width: 1400px;\n      margin: auto;\n      padding-left: 0;\n    }\n    &.side{\n      padding-right: 12px;\n    }\n    .logo {\n      height: 64px;\n      line-height: 58px;\n      vertical-align: top;\n      display: inline-block;\n      padding: 0 12px 0 24px;\n      cursor: pointer;\n      font-size: 20px;\n      color: inherit;\n      &.pc{\n        padding: 0 12px 0 0;\n      }\n      img {\n        vertical-align: middle;\n      }\n      h1{\n        color: inherit;\n        display: inline-block;\n        font-size: 16px;\n      }\n    }\n    .trigger {\n      font-size: 20px;\n      line-height: 64px;\n      padding: 0 24px;\n      cursor: pointer;\n      transition: color .3s;\n      &:hover{\n        color: @primary-color;\n      }\n    }\n    .admin-header-menu{\n      display: inline-block;\n    }\n    .admin-header-right{\n      float: right;\n      display: flex;\n      color: inherit;\n      .header-item{\n        color: inherit;\n        padding: 0 12px;\n        cursor: pointer;\n        align-self: center;\n        a{\n          color: inherit;\n          i{\n            font-size: 16px;\n          }\n        }\n      }\n      each(@theme-list, {\n        &.@{value} .header-item{\n          &:hover{\n            @class: ~'hover-bg-color-@{value}';\n            background-color: @@class;\n          }\n        }\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "front/src/layouts/tabs/TabsHead.vue",
    "content": "<template>\n  <div :class=\"['tabs-head', layout, pageWidth]\">\n    <a-tabs\n        type=\"editable-card\"\n        :class=\"['tabs-container', layout, pageWidth, {'affixed' : affixed, 'fixed-header' : fixedHeader, 'collapsed' : adminLayout.collapsed}]\"\n        :active-key=\"active\"\n        :hide-add=\"true\"\n    >\n      <a-tooltip placement=\"left\" :title=\"lockTitle\" slot=\"tabBarExtraContent\">\n        <a-icon\n            theme=\"filled\"\n            @click=\"onLockClick\"\n            class=\"header-lock\"\n            :type=\"fixedTabs ? 'lock' : 'unlock'\"\n        />\n      </a-tooltip>\n      <a-tab-pane v-for=\"page in pageList\" :key=\"page.path\">\n        <div slot=\"tab\" class=\"tab\" @contextmenu=\"e => onContextmenu(page.path, e)\">\n          <a-icon @click=\"onRefresh(page)\" :class=\"['icon-sync', {'hide': page.path !== active && !page.loading}]\" :type=\"page.loading ? 'loading' : 'sync'\" />\n          <div class=\"title\" @click=\"onTabClick(page.path)\" >{{pageName(page)}}</div>\n          <a-icon v-if=\"!page.unclose\" @click=\"onClose(page.path)\" class=\"icon-close\" type=\"close\"/>\n        </div>\n      </a-tab-pane>\n    </a-tabs>\n    <div v-if=\"affixed\" class=\"virtual-tabs\"></div>\n  </div>\n</template>\n\n<script>\n  import {mapState, mapMutations} from 'vuex'\n  import {getI18nKey} from '@/utils/routerUtil'\n\n  export default {\n    name: 'TabsHead',\n    i18n: {\n      messages: {\n        CN: {\n          lock: '点击锁定页签头',\n          unlock: '点击解除锁定',\n        },\n        HK: {\n          lock: '點擊鎖定頁簽頭',\n          unlock: '點擊解除鎖定',\n        },\n        US: {\n          lock: 'click to lock the tabs head',\n          unlock: 'click to unlock',\n        }\n      }\n    },\n    props: {\n      pageList: Array,\n      active: String,\n      fixed: Boolean\n    },\n    data() {\n      return {\n        affixed: false,\n      }\n    },\n    inject:['adminLayout'],\n    created() {\n      this.affixed = this.fixedTabs\n    },\n    computed: {\n      ...mapState('setting', ['layout', 'pageWidth', 'fixedHeader', 'fixedTabs', 'customTitles']),\n      lockTitle() {\n        return this.$t(this.fixedTabs ? 'unlock' : 'lock')\n      }\n    },\n    methods: {\n      ...mapMutations('setting', ['setFixedTabs']),\n      onLockClick() {\n        this.setFixedTabs(!this.fixedTabs)\n        if (this.fixedTabs) {\n          setTimeout(() => {\n            this.affixed = true\n          }, 200)\n        } else {\n          this.affixed = false\n        }\n      },\n      onTabClick(key) {\n        if (this.active !== key) {\n          this.$emit('change', key)\n        }\n      },\n      onClose(key) {\n        this.$emit('close', key)\n      },\n      onRefresh(page) {\n        this.$emit('refresh', page.path, page)\n      },\n      onContextmenu(pageKey, e) {\n        this.$emit('contextmenu', pageKey, e)\n      },\n      pageName(page) {\n        const pagePath = page.fullPath.split('?')[0]\n        const custom = this.customTitles.find(item => item.path === pagePath)\n        return (custom && custom.title) || page.title || this.$t(getI18nKey(page.keyPath))\n      }\n    }\n  }\n</script>\n\n<style scoped lang=\"less\">\n  .tab{\n    margin: 0 -16px;\n    padding: 0 16px;\n    font-size: 14px;\n    user-select: none;\n    transition: all 0.2s;\n    .title{\n      display: inline-block;\n      height: 100%;\n    }\n    .icon-close{\n      font-size: 12px;\n      margin-left: 6px;\n      margin-right: -4px !important;\n      color: @text-color-second;\n      &:hover{\n        color: @text-color;\n      }\n    }\n    .icon-sync{\n      margin-left: -4px;\n      color: @primary-4;\n      transition: all 0.3s ease-in-out;\n      &:hover{\n        color: @primary-color;\n      }\n      font-size: 14px;\n      &.hide{\n        font-size: 0;\n      }\n    }\n  }\n  .tabs-head{\n    margin: 0 auto;\n    &.head.fixed{\n      width: 1400px;\n    }\n  }\n  .tabs-container{\n    margin: -16px auto 8px;\n    transition: top,left 0.2s;\n    .header-lock{\n      font-size: 18px;\n      cursor: pointer;\n      color: @primary-3;\n      &:hover{\n        color: @primary-color;\n      }\n    }\n    &.affixed{\n      margin: 0 auto;\n      top: 0px;\n      padding: 8px 24px 0;\n      position: fixed;\n      height: 48px;\n      z-index: 1;\n      background-color: @layout-body-background;\n      &.side,&.mix{\n        right: 0;\n        left: 256px;\n        &.collapsed{\n          left: 80px;\n        }\n      }\n      &.head{\n        width: inherit;\n        padding: 8px 0 0;\n        &.fluid{\n          left: 0;\n          right: 0;\n          padding: 8px 24px 0;\n        }\n      }\n      &.fixed-header{\n        top: 64px;\n      }\n    }\n  }\n  .virtual-tabs{\n    height: 48px;\n  }\n</style>\n"
  },
  {
    "path": "front/src/layouts/tabs/TabsView.vue",
    "content": "<template>\n  <admin-layout>\n    <contextmenu :itemList=\"menuItemList\" :visible.sync=\"menuVisible\" @select=\"onMenuSelect\" />\n    <tabs-head\n        v-if=\"multiPage\"\n        :active=\"activePage\"\n        :page-list=\"pageList\"\n        @change=\"changePage\"\n        @close=\"remove\"\n        @refresh=\"refresh\"\n        @contextmenu=\"onContextmenu\"\n    />\n    <div :class=\"['tabs-view-content', layout, pageWidth]\" :style=\"`margin-top: ${multiPage ? -24 : 0}px`\">\n      <page-toggle-transition :disabled=\"animate.disabled\" :animate=\"animate.name\" :direction=\"animate.direction\">\n        <a-keep-alive :exclude-keys=\"excludeKeys\" v-if=\"multiPage && cachePage\" v-model=\"clearCaches\">\n          <router-view v-if=\"!refreshing\" ref=\"tabContent\" :key=\"$route.path\" />\n        </a-keep-alive>\n        <router-view ref=\"tabContent\" v-else-if=\"!refreshing\" />\n      </page-toggle-transition>\n    </div>\n  </admin-layout>\n</template>\n\n<script>\nimport AdminLayout from '@/layouts/AdminLayout'\nimport Contextmenu from '@/components/menu/Contextmenu'\nimport PageToggleTransition from '@/components/transition/PageToggleTransition'\nimport {mapState, mapMutations} from 'vuex'\nimport {getI18nKey} from '@/utils/routerUtil'\nimport AKeepAlive from '@/components/cache/AKeepAlive'\nimport TabsHead from '@/layouts/tabs/TabsHead'\n\nexport default {\n  name: 'TabsView',\n  i18n: require('./i18n'),\n  components: {TabsHead, PageToggleTransition, Contextmenu, AdminLayout , AKeepAlive },\n  data () {\n    return {\n      clearCaches: [],\n      pageList: [],\n      activePage: '',\n      menuVisible: false,\n      refreshing: false,\n      excludeKeys: []\n    }\n  },\n  computed: {\n    ...mapState('setting', ['multiPage', 'cachePage', 'animate', 'layout', 'pageWidth']),\n    menuItemList() {\n      return [\n        { key: '1', icon: 'vertical-right', text: this.$t('closeLeft') },\n        { key: '2', icon: 'vertical-left', text: this.$t('closeRight') },\n        { key: '3', icon: 'close', text: this.$t('closeOthers') },\n        { key: '4', icon: 'sync', text: this.$t('refresh') },\n      ]\n    },\n    tabsOffset() {\n      return this.multiPage ? 24 : 0\n    }\n  },\n  created () {\n    this.loadCacheConfig(this.$router?.options?.routes)\n    this.loadCachedTabs()\n    const route = this.$route\n    if (this.pageList.findIndex(item => item.path === route.path) === -1) {\n      this.pageList.push(this.createPage(route))\n    }\n    this.activePage = route.path\n    if (this.multiPage) {\n      this.$nextTick(() => {\n        this.setCachedKey(route)\n      })\n      this.addListener()\n    }\n  },\n  mounted () {\n    this.correctPageMinHeight(-this.tabsOffset)\n  },\n  beforeDestroy() {\n    this.removeListener()\n    this.correctPageMinHeight(this.tabsOffset)\n  },\n  watch: {\n    '$router.options.routes': function (val) {\n      this.excludeKeys = []\n      this.loadCacheConfig(val)\n    },\n    '$route': function (newRoute) {\n      this.activePage = newRoute.path\n      const page = this.pageList.find(item => item.path === newRoute.path)\n      if (!this.multiPage) {\n        this.pageList = [this.createPage(newRoute)]\n      } else if (page) {\n        page.fullPath = newRoute.fullPath\n      } else if (!page) {\n        this.pageList.push(this.createPage(newRoute))\n      }\n      if (this.multiPage) {\n        this.$nextTick(() => {\n          this.setCachedKey(newRoute)\n        })\n      }\n    },\n    'multiPage': function (newVal) {\n      if (!newVal) {\n        this.pageList = [this.createPage(this.$route)]\n        this.removeListener()\n      } else {\n        this.addListener()\n      }\n    },\n    tabsOffset(newVal, oldVal) {\n      this.correctPageMinHeight(oldVal - newVal)\n    }\n  },\n  methods: {\n    changePage (key) {\n      this.activePage = key\n      const page = this.pageList.find(item => item.path === key)\n      this.$router.push(page.fullPath)\n    },\n    remove (key, next) {\n      if (this.pageList.length === 1) {\n        return this.$message.warning(this.$t('warn'))\n      }\n      //清除缓存\n      let index = this.pageList.findIndex(item => item.path === key)\n      this.clearCaches = this.pageList.splice(index, 1).map(page => page.cachedKey)\n      if (next) {\n        this.$router.push(next)\n      } else if (key === this.activePage) {\n        index = index >= this.pageList.length ? this.pageList.length - 1 : index\n        this.activePage = this.pageList[index].path\n        this.$router.push(this.activePage)\n      }\n    },\n    refresh (key, page) {\n      page = page || this.pageList.find(item => item.path === key)\n      page.loading = true\n      this.clearCache(page)\n      if (key === this.activePage) {\n        this.reloadContent(() => page.loading = false)\n      } else {\n        // 其实刷新很快，加这个延迟纯粹为了 loading 状态多展示一会儿，让用户感知刷新这一过程\n        setTimeout(() => page.loading = false, 500)\n      }\n    },\n    onContextmenu(pageKey, e) {\n      if (pageKey) {\n        e.preventDefault()\n        e.meta = pageKey\n        this.menuVisible = true\n      }\n    },\n    onMenuSelect (key, target, pageKey) {\n      switch (key) {\n        case '1': this.closeLeft(pageKey); break\n        case '2': this.closeRight(pageKey); break\n        case '3': this.closeOthers(pageKey); break\n        case '4': this.refresh(pageKey); break\n        default: break\n      }\n    },\n    closeOthers (pageKey) {\n      // 清除缓存\n      const clearPages = this.pageList.filter(item => item.path !== pageKey && !item.unclose)\n      this.clearCaches = clearPages.map(item => item.cachedKey)\n      this.pageList = this.pageList.filter(item => !clearPages.includes(item))\n      // 判断跳转\n      if (this.activePage != pageKey) {\n        this.activePage = pageKey\n        this.$router.push(this.activePage)\n      }\n    },\n    closeLeft (pageKey) {\n      const index = this.pageList.findIndex(item => item.path === pageKey)\n      // 清除缓存\n      const clearPages = this.pageList.filter((item, i) => i < index && !item.unclose)\n      this.clearCaches = clearPages.map(item => item.cachedKey)\n      this.pageList = this.pageList.filter(item => !clearPages.includes(item))\n      // 判断跳转\n      if (!this.pageList.find(item => item.path === this.activePage)) {\n        this.activePage = pageKey\n        this.$router.push(this.activePage)\n      }\n    },\n    closeRight (pageKey) {\n      // 清除缓存\n      const index = this.pageList.findIndex(item => item.path === pageKey)\n      const clearPages = this.pageList.filter((item, i) => i > index && !item.unclose)\n      this.clearCaches = clearPages.map(item => item.cachedKey)\n      this.pageList = this.pageList.filter(item => !clearPages.includes(item))\n      // 判断跳转\n      if (!this.pageList.find(item => item.path === this.activePage)) {\n        this.activePage = pageKey\n        this.$router.push(this.activePage)\n      }\n    },\n    clearCache(page) {\n      page._init_ = false\n      this.clearCaches = [page.cachedKey]\n    },\n    reloadContent(onLoaded) {\n      this.refreshing = true\n      setTimeout(() => {\n        this.refreshing = false\n        this.$nextTick(() => {\n          this.setCachedKey(this.$route)\n          if (typeof onLoaded === 'function') {\n            onLoaded.apply(this, [])\n          }\n        })\n      }, 200)\n    },\n    pageName(page) {\n      return this.$t(getI18nKey(page.keyPath))\n    },\n    /**\n     * 添加监听器\n     */\n    addListener() {\n      window.addEventListener('page:close', this.closePageListener)\n      window.addEventListener('page:refresh', this.refreshPageListener)\n      window.addEventListener('unload', this.unloadListener)\n    },\n    /**\n     * 移出监听器\n     */\n    removeListener() {\n      window.removeEventListener('page:close', this.closePageListener)\n      window.removeEventListener('page:refresh', this.refreshPageListener)\n      window.removeEventListener('unload', this.unloadListener)\n    },\n    /**\n     * 页签关闭事件监听\n     * @param event 页签关闭事件\n     */\n    closePageListener(event) {\n      const {closeRoute, nextRoute} = event.detail\n      const closePath = typeof closeRoute === 'string' ? closeRoute : closeRoute.path\n      const path = closePath && closePath.split('?')[0]\n      this.remove(path, nextRoute)\n    },\n    /**\n     * 页面刷新事件监听\n     * @param event 页签关闭事件\n     */\n    refreshPageListener(event) {\n      const {pageKey} = event.detail\n      const path = pageKey && pageKey.split('?')[0]\n      this.refresh(path)\n    },\n    /**\n     * 页面 unload 事件监听器，添加页签到 session 缓存，用于刷新时保留页签\n     */\n    unloadListener() {\n      const tabs = this.pageList.map(item => ({...item, _init_: false}))\n      sessionStorage.setItem(process.env.VUE_APP_TBAS_KEY, JSON.stringify(tabs))\n    },\n    createPage(route) {\n      return {\n        keyPath: route.matched[route.matched.length - 1].path,\n        fullPath: route.fullPath, loading: false,\n        path: route.path,\n        title: route.meta && route.meta.page && route.meta.page.title,\n        unclose: route.meta && route.meta.page && (route.meta.page.closable === false),\n      }\n    },\n    /**\n     * 设置页面缓存的key\n     * @param route 页面对应的路由\n     */\n    setCachedKey(route) {\n      const page = this.pageList.find(item => item.path === route.path)\n      page.unclose = route.meta && route.meta.page && (route.meta.page.closable === false)\n      if (!page._init_) {\n        const vnode = this.$refs.tabContent.$vnode\n        page.cachedKey = vnode.key + vnode.componentOptions.Ctor.cid\n        page._init_ = true\n      }\n    },\n    /**\n     * 加载缓存的 tabs\n     */\n    loadCachedTabs() {\n      const cachedTabsStr = sessionStorage.getItem(process.env.VUE_APP_TBAS_KEY)\n      if (cachedTabsStr) {\n        try {\n          const cachedTabs = JSON.parse(cachedTabsStr)\n          if (cachedTabs.length > 0) {\n            this.pageList = cachedTabs\n          }\n        } catch (e) {\n          console.warn('failed to load cached tabs, got exception:', e)\n        } finally {\n          sessionStorage.removeItem(process.env.VUE_APP_TBAS_KEY)\n        }\n      }\n    },\n    loadCacheConfig(routes, pCache = true) {\n      routes.forEach(item => {\n        const cacheAble = item.meta?.page?.cacheAble ?? pCache ?? true\n        if (!cacheAble) {\n          this.excludeKeys.push(new RegExp(`${item.path}\\\\d+$`))\n        }\n        if (item.children) {\n          this.loadCacheConfig(item.children, cacheAble)\n        }\n      })\n    },\n    ...mapMutations('setting', ['correctPageMinHeight'])\n  }\n}\n</script>\n\n<style scoped lang=\"less\">\n  .tabs-view{\n    margin: -16px auto 8px;\n    &.head.fixed{\n      max-width: 1400px;\n    }\n  }\n  .tabs-view-content{\n    position: relative;\n    &.head.fixed{\n      width: 1400px;\n      margin: 0 auto;\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/layouts/tabs/i18n.js",
    "content": "module.exports = {\n  messages: {\n    CN: {\n      closeLeft: '关闭左侧',\n      closeRight: '关闭右侧',\n      closeOthers: '关闭其它',\n      refresh: '刷新页面',\n      warn: '这是最后一页，不能再关闭了',\n    },\n    HK: {\n      closeLeft: '關閉左側',\n      closeRight: '關閉右側',\n      closeOthers: '關閉其它',\n      refresh: '刷新頁面',\n      warn: '這是最後一頁，不能再關閉了',\n    },\n    US: {\n      closeLeft: 'close left',\n      closeRight: 'close right',\n      closeOthers: 'close others',\n      refresh: 'refresh the page',\n      warn: 'This is the last page, you can\\'t close it',\n    },\n  }\n}"
  },
  {
    "path": "front/src/layouts/tabs/index.js",
    "content": "import TabsView from './TabsView'\nexport default TabsView\n"
  },
  {
    "path": "front/src/main.js",
    "content": "import Vue from 'vue'\nimport App from './App.vue'\nimport {initRouter} from './router'\nimport './theme/index.less'\nimport Antd from 'ant-design-vue'\nimport Viser from 'viser-vue'\nimport '@/mock'\nimport store from './store'\nimport 'animate.css/source/animate.css'\nimport Plugins from '@/plugins'\nimport {initI18n} from '@/utils/i18n'\nimport bootstrap from '@/bootstrap'\nimport 'moment/locale/zh-cn'\n\nconst router = initRouter(store.state.setting.asyncRoutes)\nconst i18n = initI18n('CN', 'US')\nVue.use(Antd)\nVue.config.productionTip = false\nVue.use(Viser)\nVue.use(Plugins)\n\nbootstrap({router, store, i18n, message: Vue.prototype.$message})\n\nnew Vue({\n  router,\n  store,\n  i18n,\n  render: h => h(App),\n}).$mount('#app')\n"
  },
  {
    "path": "front/src/mock/common/activityData.js",
    "content": "import {users, groups} from './index'\n\nconst events = [\n    {\n        type: 0,\n        event: '八月迭代'\n    },\n    {\n        type: 1,\n        event: '留言'\n    },\n    {\n        type: 2,\n        event: '项目进展'\n    }\n]\n\nconst activities = users.map((user, index) => {\n    return {\n        user: Object.assign({}, user, {group: groups[user.groupId]}),\n        activity: events[index % events.length],\n        template: ''\n    }\n})\n\nconst templates = [\n    (user, activity) => { return `${user.name} 在 <a >${user.group}</a> 新建项目 <a>${activity.event}</a>` },\n    (user, activity) => { return `${user.name} 在 <a >${user.group}</a> 发布了 <a>${activity.event}</a>` },\n    (user, activity) => { return `${user.name} 将 <a >${activity.event}</a> 更新至已发布状态` }\n]\n\nexport {activities, templates}\n"
  },
  {
    "path": "front/src/mock/common/index.js",
    "content": "const avatars = [\n    'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png',\n    'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',\n    'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png',\n    'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png',\n    'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png',\n    'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png'\n]\n\nconst positions = [\n    {\n        CN: 'Java工程师 | 蚂蚁金服-计算服务事业群-微信平台部',\n        HK: 'Java工程師 | 螞蟻金服-計算服務事業群-微信平台部',\n        US: 'Java engineer | Ant financial - Computing services business group - WeChat platform division'\n    },{\n        CN: '前端工程师 | 蚂蚁金服-计算服务事业群-VUE平台',\n        HK: '前端工程師 | 螞蟻金服-計算服務事業群-VUE平台',\n        US: 'Front-end engineer | Ant Financial - Computing services business group - VUE platform'\n    },{\n        CN: '前端工程师 | 蚂蚁金服-计算服务事业群-REACT平台',\n        HK: '前端工程師 | 螞蟻金服-計算服務事業群-REACT平台',\n        US: 'Front-end engineer | Ant Financial - Computing services business group - REACT platform'\n    },{\n        CN: '产品分析师 | 蚂蚁金服-计算服务事业群-IOS平台部',\n        HK: '產品分析師 | 螞蟻金服-計算服務事業群-IOS平台部',\n        US: 'Product analyst | Ant Financial - Computing services business group - IOS platform division'\n    }\n]\n\nconst sayings = [\n    '那是一种内在的东西，他们到达不了，也无法触及的',\n    '希望是一个好东西，也许是最好的，好东西是不会消亡的',\n    '城镇中有那么多的酒馆，她却偏偏走进了我的酒馆',\n    '那时候我只会想自己想要什么，从不想自己拥有什么'\n]\n\nconst logos = [\n    'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png',\n    'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png',\n    'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png',\n    'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png',\n    'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png'\n]\n\nconst admins = ['ICZER', 'JACK', 'LUIS', 'DAVID']\n\nconst groups = ['高逼格设计天团', '中二少女团', '科学搬砖组', '骗你学计算机', '程序员日常']\n\nconst users = [\n    {\n        name: '曲丽丽',\n        avatar: avatars[0],\n        groupId: 0\n    },\n    {\n        name: '付晓晓',\n        avatar: avatars[1],\n        groupId: 0\n    },\n    {\n        name: '林东东',\n        avatar: avatars[2],\n        groupId: 1\n    },\n    {\n        name: '周星星',\n        avatar: avatars[3],\n        groupId: 2\n    },\n    {\n        name: '朱偏右',\n        avatar: avatars[4],\n        groupId: 3\n    },\n    {\n        name: '勒个',\n        avatar: avatars[5],\n        groupId: 4\n    }\n]\n\nconst teams = groups.map((item, index) => {\n    return {\n        name: item,\n        avatar: avatars[index]\n    }\n})\n\nexport {logos, sayings, positions, avatars, admins, groups, users, teams}\n"
  },
  {
    "path": "front/src/mock/common/tableData.js",
    "content": "const operation1 = [\n    {\n        key: 'op1',\n        type: '订购关系生效',\n        name: '曲丽丽',\n        status: 'agree',\n        updatedAt: '2017-10-03  19:23:12',\n        memo: '-'\n    },\n    {\n        key: 'op2',\n        type: '财务复审',\n        name: '付小小',\n        status: 'reject',\n        updatedAt: '2017-10-03  19:23:12',\n        memo: '不通过原因'\n    },\n    {\n        key: 'op3',\n        type: '部门初审',\n        name: '周毛毛',\n        status: 'agree',\n        updatedAt: '2017-10-03  19:23:12',\n        memo: '-'\n    },\n    {\n        key: 'op4',\n        type: '提交订单',\n        name: '林东东',\n        status: 'agree',\n        updatedAt: '2017-10-03  19:23:12',\n        memo: '很棒'\n    },\n    {\n        key: 'op5',\n        type: '创建订单',\n        name: '汗牙牙',\n        status: 'agree',\n        updatedAt: '2017-10-03  19:23:12',\n        memo: '-'\n    }\n]\n\nconst operation2 = [\n    {\n        key: 'op2',\n        type: '财务复审',\n        name: '付小小',\n        status: 'reject',\n        updatedAt: '2017-10-03  19:23:12',\n        memo: '不通过原因'\n    },\n    {\n        key: 'op3',\n        type: '部门初审',\n        name: '周毛毛',\n        status: 'agree',\n        updatedAt: '2017-10-03  19:23:12',\n        memo: '-'\n    },\n    {\n        key: 'op4',\n        type: '提交订单',\n        name: '林东东',\n        status: 'agree',\n        updatedAt: '2017-10-03  19:23:12',\n        memo: '很棒'\n    }\n]\n\nconst operation3 = [\n    {\n        key: 'op2',\n        type: '财务复审',\n        name: '付小小',\n        status: 'reject',\n        updatedAt: '2017-10-03  19:23:12',\n        memo: '不通过原因'\n    },\n    {\n        key: 'op3',\n        type: '部门初审',\n        name: '周毛毛',\n        status: 'agree',\n        updatedAt: '2017-10-03  19:23:12',\n        memo: '-'\n    }\n]\n\nconst operationColumns = [\n    {\n        title: '操作类型',\n        dataIndex: 'type',\n        key: 'type'\n    },\n    {\n        title: '操作人',\n        dataIndex: 'name',\n        key: 'name'\n    },\n    {\n        title: '执行结果',\n        dataIndex: 'status',\n        key: 'status'\n    },\n    {\n        title: '操作时间',\n        dataIndex: 'updatedAt',\n        key: 'updatedAt'\n    },\n    {\n        title: '备注',\n        dataIndex: 'memo',\n        key: 'memo'\n    }\n]\n\nexport {operation1, operation2, operation3, operationColumns}\n"
  },
  {
    "path": "front/src/mock/goods/index.js",
    "content": "import Mock from 'mockjs'\nimport '@/mock/extend'\nimport {parseUrlParams} from '@/utils/request'\n\nconst current = new Date().getTime()\n\nconst goodsList = Mock.mock({\n    'list|200': [{\n        'id|+1': 0,\n        'name': '@GOODS',\n        'orderId': `${current}-@integer(1,100)`,\n        'status|1-4': 1,\n        'send': '@BOOLEAN',\n        'sendTime': '@DATETIME',\n        'orderDate': '@DATE',\n        'auditTime': '@TIME'\n    }]\n})\n\nMock.mock(RegExp(`${process.env.VUE_APP_API_BASE_URL}/goods` + '.*'),'get', ({url}) => {\n    const params = parseUrlParams(decodeURI(url))\n    let {page, pageSize} = params\n    page = eval(page) - 1 || 0\n    pageSize = eval(pageSize) || 10\n    delete params.page\n    delete params.pageSize\n    let result = goodsList.list.filter(item => {\n        for (let [key, value] of Object.entries(params)) {\n            if (item[key] != value) {\n                return false\n            }\n        }\n        return true\n    })\n    const total = result.length\n    if ((page) * pageSize > total) {\n        result = []\n    } else {\n        result = result.slice(page * pageSize, (page + 1) * pageSize)\n    }\n    return {\n        code: 0,\n        message: 'success',\n        data: {\n            page: page + 1,\n            pageSize,\n            total,\n            list: result\n        }\n    }\n})\n\nconst columnsConfig = [\n    {\n        title: '商品名称',\n        dataIndex: 'name',\n        searchAble: true\n    },\n    {\n        title: '订单号',\n        dataIndex: 'orderId'\n    },\n    {\n        searchAble: true,\n        dataIndex: 'status',\n        dataType: 'select',\n        slots: {title: 'statusTitle'},\n        scopedSlots: {customRender: 'status'},\n        search: {\n            selectOptions: [\n                {title: '已下单', value: 1},\n                {title: '已付款', value: 2},\n                {title: '已审核', value: 3},\n                // {title: '已发货', value: 4}\n            ]\n        }\n    },\n    {\n        title: '发货',\n        searchAble: true,\n        dataIndex: 'send',\n        dataType: 'boolean',\n        scopedSlots: {customRender: 'send'}\n    },\n    {\n        title: '发货时间',\n        dataIndex: 'sendTime',\n        dataType: 'datetime'\n    },\n    {\n        title: '下单日期',\n        searchAble: true,\n        dataIndex: 'orderDate',\n        dataType: 'date',\n        visible: false\n    },\n    {\n        title: '审核时间',\n        dataIndex: 'auditTime',\n        dataType: 'time',\n    },\n]\n\nMock.mock(`${process.env.VUE_APP_API_BASE_URL}/columns`, 'get', () => {\n    return columnsConfig\n})\n"
  },
  {
    "path": "front/src/mock/index.js",
    "content": "import Mock from 'mockjs'\nimport '@/mock/workplace'\n\n// 设置全局延时\nMock.setup({\n  timeout: '200-400'\n})\n"
  },
  {
    "path": "front/src/mock/user/login.js",
    "content": "import Mock from 'mockjs'\nimport '@/mock/extend'\n\nconst user = Mock.mock({\n    name: '@ADMIN',\n    avatar: '@AVATAR',\n    address: '@CITY',\n    position: '@POSITION'\n})\nMock.mock(`${process.env.VUE_APP_API_BASE_URL}/login`, 'post', ({body}) => {\n    let result = {data: {}}\n    const {name, password} = JSON.parse(body)\n\n    let success = false\n\n    if (name === 'admin' && password === '888888') {\n        success = true\n        result.data.permissions = [{id: 'queryForm', operation: ['add', 'edit']}]\n        result.data.roles = [{id: 'admin', operation: ['add', 'edit', 'delete']}]\n    } else if (name === 'user' || password === '888888') {\n        success = true\n        result.data.permissions = [{id: 'queryForm', operation: ['add', 'edit']}]\n        result.data.roles = [{id: 'test', operation: ['add', 'edit', 'delete']}]\n    } else {\n        success = false\n    }\n\n    if (success) {\n        result.code = 0\n        result.message = Mock.mock('@TIMEFIX').CN + '，欢迎回到这里'\n        result.data.user = user\n        result.data.token = 'Authorization:' + Math.random()\n        result.data.expireAt = new Date(new Date().getTime() + 30 * 60 * 1000)\n    } else {\n        result.code = -1\n        result.message = '账户名或密码错误（admin/888888 or test/888888）'\n    }\n    return result\n})\n"
  },
  {
    "path": "front/src/mock/user/routes.js",
    "content": "import Mock from 'mockjs'\n\nMock.mock(`${process.env.VUE_APP_API_BASE_URL}/routes`, 'get', () => {\n    let result = {}\n    result.code = 0\n    result.data = [{\n        router: 'root',\n        children: [\n            {\n                router: 'dashboard',\n                children: ['workplace', 'analysis'],\n            },\n            {\n                router: 'form',\n                children: ['basicForm', 'stepForm', 'advanceForm']\n            },\n            {\n                router: 'basicForm',\n                name: '验权表单',\n                icon: 'file-excel',\n                authority: 'queryForm'\n            },\n            {\n                router: 'antdv',\n                path: 'antdv',\n                name: 'Ant Design Vue',\n                icon: 'ant-design',\n                link: 'https://www.antdv.com/docs/vue/introduce-cn/'\n            },\n            {\n                router: 'document',\n                path: 'document',\n                name: '使用文档',\n                icon: 'file-word',\n                link: 'https://iczer.gitee.io/vue-antd-admin-docs/'\n            }\n        ]\n    }]\n    return result\n})\n"
  },
  {
    "path": "front/src/mock/workplace/index.js",
    "content": "import Mock from 'mockjs'\nimport {activities, templates} from '@/mock/common/activityData'\nimport {teams} from '@/mock/common'\n\nactivities.forEach(item => {\n  item.template = templates[item.activity.type](item.user, item.activity)\n})\n\nMock.mock('/work/activity', 'get', () => {\n  return activities\n})\n\nMock.mock('/work/team', 'get', () => {\n  return teams\n})\n"
  },
  {
    "path": "front/src/pages/analysis/index.vue",
    "content": "<template>\n  <div>\n    <a-card>\n      <div style=\"width: 100%\">\n        <a-space class=\"operator\">\n          <a-form layout=\"inline\" :form=\"queryForm\">\n            <a-form-item label=\"关键字\">\n              <a-input\n                  v-decorator=\"['name', { rules: [{ required: false,min:1,max:120,message:'输入长度应在1到120之间'}] }]\"\n                  placeholder=\"请输入姓名\"\n              />\n            </a-form-item>\n            <a-form-item label=\"时间段查询\">\n              <a-range-picker\n                  :show-time=\"{ format: 'HH:mm' }\"\n                  v-decorator=\"['rangeTime', { rules: [{ required: false}] }]\"\n                  format=\"YYYY/MM/DD HH:mm:ss\"\n                  :placeholder=\"['开始时间', '结束时间']\"\n              />\n            </a-form-item>\n            <a-form-item label=\"分组类型\">\n              <a-select\n                  style=\"width: 8rem\"\n                  v-decorator=\"['groupType', { rules: [{ required: false}] }]\">\n                <a-select-option\n                    :key=\"index\"\n                    :value=\"key\"\n                    v-for=\"(value,key,index) in groupType\">\n                  {{ value }}\n                </a-select-option>\n              </a-select>\n            </a-form-item>\n            <a-form-item>\n              <a-button @click=\"query()\" :loading=\"queryLoading\">查询</a-button>\n            </a-form-item>\n          </a-form>\n        </a-space>\n      </div>\n        <a-space>\n          <a-button type=\"primary\" @click=\"showCharts()\">柱状图</a-button>\n          <a-button type=\"primary\" @click=\"showChartsPie()\">饼状图</a-button>\n        </a-space>\n      <p></p>\n        <a-table\n            :columns=\"columns\"\n            :data-source=\"dataSource\"\n            :pagination=\"pagination\"\n            :loading=\"loading\"\n            @change=\"handleTableChange\"\n        >\n        </a-table>\n    </a-card>\n    <a-modal\n        :title=\"title\"\n        @cancel=\"handleChartCancel\"\n        width=\"100%\"\n        centered\n        :footer=\"null\"\n        :visible=\"chartsVisible\">\n      <div :style=\"{width: '100%', height:'90vh' }\" ref=\"chart\" />\n    </a-modal>\n  </div>\n</template>\n\n<script>\nimport * as analysis from \"@/services/analysis\"\nimport * as Echarts from \"echarts\"\nimport validators from \"@/utils/validators\";\nconst groupType = {\n  \"1\": '员工',\n  \"2\": '年',\n  \"3\": '月',\n  \"4\": '日',\n}\nconst columns = [\n  {\n    title: '分组类型',\n    dataIndex: 'name'\n  },\n  {\n    title: '潜在客户新增数',\n    dataIndex: 'count',\n  }\n]\nexport default {\n  data() {\n    return {\n      validators,\n      title:'',\n      chartsVisible:false,\n      queryForm: this.$form.createForm(this, {name: 'coordinated'}),\n      queryLoading: false,\n      groupType,\n      // table\n      columns: columns,\n      dataSource: [],\n      selectedRows: [],\n      pagination: {},\n      loading: false,\n    }\n  },\n  async mounted() {\n    this.queryForm.setFieldsValue({\"groupType\": \"1\"})\n    this.fetch()\n  },\n  methods: {\n    showCharts(){\n      this.chartsVisible = true\n      this.init()\n      this.title = \"潜在客户报表-柱状图\"\n    },\n    handleChartCancel(){\n      this.chartsVisible = false\n      Echarts.dispose(this.$refs.chart)\n      window.removeEventListener(\"resize\",this.listenHandal,false)\n    },\n    showChartsPie(){\n      this.chartsVisible = true\n      this.init2()\n      this.title = \"潜在客户报表-饼状图\"\n    },\n    init2(){\n      this.$nextTick(async ()=>{\n        //2.初始化\n        this.chart = Echarts.init(this.$refs.chart);\n        const data =(await this.query(999999999)).data.list\n        //3.配置数据\n        let option = {\n          tooltip: {\n            trigger: 'item'\n          },\n          legend: {\n            orient: 'vertical',\n            left: 'left',\n          },\n          series: [\n            {\n              name: '访问来源',\n              type: 'pie',\n              radius: '50%',\n              data: data.map(e=>({value:e.count,name:e.name})),\n              emphasis: {\n                itemStyle: {\n                  shadowBlur: 10,\n                  shadowOffsetX: 0,\n                  shadowColor: 'rgba(0, 0, 0, 0.5)'\n                }\n              }\n            }\n          ]\n        };\n        // 4.传入数据\n        this.chart.setOption(option);\n        window.addEventListener('resize', this.listenHandal,false);\n      })\n    },\n    listenHandal(){\n        this.chart.resize();\n    },\n    init() {\n      this.$nextTick(async ()=>{\n        //2.初始化\n        this.chart = Echarts.init(this.$refs.chart);\n        const data =(await this.query(999999999)).data.list\n        //3.配置数据\n        let option = {\n          xAxis: { type: 'category', data: data.map(e=>e.name) }, //X轴\n          yAxis: { type: 'value' }, //Y轴\n          series: [{ data: data.map(e=>e.count), type: 'bar' }] //配置项\n        };\n        // 4.传入数据\n        this.chart.setOption(option);\n        window.addEventListener('resize', this.listenHandal,false);\n      })\n    },\n    async query(size) {\n      return new Promise((resolve) => {\n        this.queryLoading = true\n        this.queryForm.validateFields(async (err, values) => {\n          if (err) {\n            console.log(\"form error\");\n            this.queryLoading = false\n            return;\n          }\n          if (values.rangeTime) {\n            if (values.rangeTime.length !== 0) {\n              values.startTime = values.rangeTime[0].toDate().toISOString()\n              values.endTime = values.rangeTime[1].toDate().toISOString()\n            }\n            delete values.rangeTime\n          }\n          values.groupType = parseInt(values.groupType)\n          let res = await this.fetch({\"page\": 1, \"size\": size||10, ...values})\n          resolve(res)\n        })\n      })\n    },\n    // table\n    handleTableChange(pagination) {\n      const pager = {...this.pagination};\n      pager.current = pagination.current;\n      this.pagination = pager;\n      this.fetch({\n        size: pagination.pageSize,\n        page: pagination.current,\n      });\n    },\n    async fetch(params = {\"page\": 1, \"size\": 10}) {\n      this.loading = true\n      const {data} =await analysis.list(params || {\"page\": 1, \"size\": 10})\n        const res = data.data\n        const pagination = {...this.pagination};\n        pagination.total = res.total\n        pagination.current = params.page\n        this.dataSource = res.list.map((e, i) => ({key: i + \"\", ...e}))\n        this.pagination = pagination\n        this.loading = false\n        this.queryLoading = false\n      return data\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n.ant-modal-content{\n  width: 100%;\n  height: 100%;\n}\n.search {\n  margin-bottom: 54px;\n}\n\n.fold {\n  width: calc(100% - 216px);\n  display: inline-block\n}\n\n.operator {\n  margin-bottom: 18px;\n}\n\n@media screen and (max-width: 900px) {\n  .fold {\n    width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/pages/components/Palette.vue",
    "content": "<template>\n  <div style=\"text-align: center; margin-top: 48px\">\n    <color-checkbox-group :defaultValues=\"['1', '3', '4']\" @change=\"changeColor\" :multiple=\"true\" style=\"display: inline-block\">\n      <color-checkbox color=\"rgb(245, 34, 45)\" value=\"1\" />\n      <color-checkbox color=\"rgb(250, 84, 28)\" value=\"2\" />\n      <color-checkbox color=\"rgb(250, 173, 20)\" value=\"3\" />\n      <color-checkbox color=\"rgb(19, 194, 194)\" value=\"4\" />\n      <color-checkbox color=\"rgb(82, 196, 26)\" value=\"5\" />\n      <color-checkbox color=\"rgb(24, 144, 255)\" value=\"6\" />\n      <color-checkbox color=\"rgb(47, 84, 235)\" value=\"7\" />\n      <color-checkbox color=\"rgb(114, 46, 209)\" value=\"8\" />\n      <color-checkbox color=\"rgb(256, 0, 0)\" value=\"9\" />\n      <color-checkbox color=\"rgb(0, 256, 0)\" value=\"10\" />\n      <color-checkbox color=\"rgb(0, 0, 256)\" value=\"11\" />\n      <color-checkbox color=\"rgb(256, 256, 0)\" value=\"12\" />\n    </color-checkbox-group>\n    <div></div>\n    <div class=\"view-color\" :style=\"{backgroundColor: color}\"/>\n  </div>\n</template>\n\n<script>\nimport ColorCheckbox from '../../components/checkbox/ColorCheckbox'\n\nconst ColorCheckboxGroup = ColorCheckbox.Group\n\nexport default {\n  name: 'Palette',\n  data () {\n    return {\n      color: 'rgb(245, 34, 45)'\n    }\n  },\n  components: {ColorCheckbox, ColorCheckboxGroup},\n  methods: {\n    changeColor (values, colors) {\n      this.color = this.calculateColor(colors)\n    },\n    calculateColor (colors) {\n      let red = 0\n      let green = 0\n      let blue = 0\n      let values\n      colors.forEach(color => {\n        values = color.split('(')[1].split(')')[0].split(',')\n        red = Math.max(red, parseInt(values[0]))\n        green += Math.max(green, parseInt(values[1]))\n        blue += Math.max(blue, parseInt(values[2]))\n      })\n      return 'rgb(' + red + ',' + green + ',' + blue + ')'\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .view-color{\n    margin-top: 48px;\n    display: inline-block;\n    height: 96px;\n    width: 96px;\n    border-radius: 48px;\n    border: 1px dashed gray;\n  }\n</style>\n"
  },
  {
    "path": "front/src/pages/components/TaskCard.vue",
    "content": "<template>\n  <div style=\"display: flex\">\n    <task-group class=\"task-group\" title=\"ToDo\" group=\"task\">\n      <task-item :key=\"index\" v-for=\"(item, index) in todoList\" :content=\"item\" />\n    </task-group>\n    <task-group class=\"task-group\" title=\"In Progress\" group=\"task\">\n      <task-item :key=\"index\" v-for=\"(item, index) in inproList\" :content=\"item\" />\n    </task-group>\n    <task-group class=\"task-group\" title=\"Done\" group=\"task\">\n      <task-item :key=\"index\" v-for=\"(item, index) in doneList\" :content=\"item\" />\n    </task-group>\n  </div>\n</template>\n\n<script>\nimport TaskGroup from '../../components/task/TaskGroup'\nimport TaskItem from '../../components/task/TaskItem'\nconst todoList = ['任务一', '任务二', '任务三', '任务四', '任务五', '任务六']\nconst inproList = ['任务七', '任务八', '任务九', '任务十', '任务十一', '任务十二']\nconst doneList = ['任务十三', '任务十四', '任务十五', '任务十六', '任务十七', '任务十八']\nexport default {\n  name: 'TaskCard',\n  components: {TaskItem, TaskGroup},\n  data () {\n    return {\n      todoList,\n      inproList,\n      doneList\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .task-group{\n    margin: 0 48px;\n  }\n  .dragable-ghost{\n    border: 1px dashed red;\n    opacity: 1;\n  }\n  .dragable-chose{\n    border: 1px dashed red;\n    opacity: 0.8;\n  }\n  .dragable-drag{\n    border: 1px dashed red;\n    opacity: 1;\n  }\n</style>\n"
  },
  {
    "path": "front/src/pages/components/table/Api.vue",
    "content": "<template>\n  <div class=\"api\">\n    <div class=\"introduce\">\n      <h2 class=\"title\">说明</h2>\n      <p class=\"content\">\n        AdvanceTable 是基于 Ant Design Vue Table 组件封装，支持其所有 API。<br/>\n        主要添加了<em>列设置</em>及<em>搜索控件配置</em>的功能，可用于一些需要动态配置表格展示、动态配置搜索条件的场景。<br/>\n        使用方式 与 antd table 基本无异。添加了部分API，如下：\n      </p>\n    </div>\n    <api-table :api-source=\"apiSource\" />\n    <api-table type=\"event\" title=\"事件\" :api-source=\"events\" />\n    <api-table title=\"Column\" :api-source=\"columnApi\" />\n    <api-table title=\"Search\" :api-source=\"searchApi\" />\n  </div>\n</template>\n\n<script>\n  import ApiTable from '@/components/table/api/ApiTable'\n  export default {\n    name: 'Api',\n    components: {ApiTable},\n    data() {\n      return {\n        apiSource: [\n          {\n            key: 0,\n            param: '<a href=\"https://www.antdv.com/components/table-cn/#API\" target=\"_blank\">Ant Design Vue Table API</a>',\n            desc: '支持 Ant Design Vue Table 组件 所有 api',\n            type: '--',\n            default: '--',\n          },\n          {\n            key: 1,\n            param: 'title',\n            desc: '表格标题',\n            type: 'string | slot',\n            default: '\\'高级表格\\''\n          },\n          {\n            key: 2,\n            param: 'formatConditions',\n            desc: `是否格式化搜索条件的值，格式化规则参考 <a>Search 配置</a>。\n                   <br/>false：取搜索输入控件的原值 <br/>true：取搜索输入控件格式化后的值`,\n            type: 'boolean',\n            default: 'false',\n          },\n          {\n            key: 3,\n            param: 'columns',\n            desc: `表格列配置，参考 <a>Column 配置</a>`,\n            type: 'array',\n            default: '--',\n          }\n        ],\n        events: [\n          {\n            key: 0,\n            param: '<a href=\"https://www.antdv.com/components/table-cn/#API\" target=\"_blank\">Ant Design Vue Table Events API</a>',\n            desc: '支持 Ant Design Vue Table 所有事件',\n            callback: '--',\n          },\n          {\n            key: 1,\n            param: 'search',\n            desc: '搜索条件变化时触发',\n            callback: 'Function(conditions, searchOptions: [{field, value, format}])',\n          },\n          {\n            key: 2,\n            param: 'refresh',\n            desc: '表头刷新图标点击时触发',\n            callback: 'Function(conditions, searchOptions: [{field, value, format}])',\n          },\n          {\n            key: 3,\n            param: 'reset',\n            desc: '列配置重置按钮点击时触发',\n            callback: 'Function(conditions, searchOptions: [{field, value, format}])',\n          },\n        ],\n        columnApi: [\n          {\n            key: 0,\n            param: '<a href=\"https://www.antdv.com/components/table-cn/#API\" target=\"_blank\">Ant Design Vue Table Column API</a>',\n            desc: '支持 Ant Design Vue Table 组件 Column 配置所有 api',\n            type: '--',\n            default: '--'\n          },\n          {\n            key: 1,\n            param: 'searchAble',\n            desc: '是否启用列搜索',\n            type: 'boolean',\n            default: 'false'\n          },\n          {\n            key: 2,\n            param: 'dataType',\n            desc: `数据类型，该配置将决定列搜索输入控件的类型，与列搜索输入控件对应关系如下：<br/>\n                   string: 输入框组件<br/>\n                   boolean: 开关组件<br/>\n                   select: 下拉输入框组件<br/>\n                   date: 日期选择器<br/>\n                   time: 时间选择器<br/>\n                   datetime: 带时间选择器的日期选择器`,\n            type: `'string' | 'boolean' | 'select' | 'date' | 'time' | 'datetime'`,\n            default: `'string'`\n          },\n          {\n            key: 3,\n            param: 'search',\n            desc: '列搜索配置，参考 <a>Search 配置</a>',\n            type: 'object',\n            default: '--'\n          },\n        ],\n        searchApi: [\n          {\n            key: 0,\n            param: 'format',\n            desc: `列搜索输入控件值的格式化配置。<br/>如果输入控件支持格式化，则可设置该值为字符串，如日期输入组件，可设为为 'YYYY-MM-DD'。\n                   <br/>不支持格式化的输入控件，可设置为一个接收控件的输入值作为参数的函数，如 (value) => {return \\`prefix\\${value}\\`}。`,\n            type: 'string | Function(value)',\n            default: '取输入控件默认的格式化配置'\n          },\n          {\n            key: 1,\n            param: 'selectOptions',\n            desc: `select 数据类型的下拉输入组件的选项配置，可参考 <a href=\"https://www.antdv.com/components/select-cn/#API\" target=\"_blank\">Ant Design Vue Select Option props Api</a>`,\n            type: 'array',\n            default: '--'\n          }\n        ],\n      }\n    }\n  }\n</script>\n\n<style scoped lang=\"less\">\n.api{\n  .introduce{\n    padding: 16px;\n    .content{\n      em{\n        margin: 0 4px;\n        color: @primary-color;\n      }\n    }\n  }\n}\n</style>"
  },
  {
    "path": "front/src/pages/components/table/Table.vue",
    "content": "<template>\n  <div class=\"table\">\n    <advance-table\n      :columns=\"columns\"\n      :data-source=\"dataSource\"\n      title=\"高级表格-Beta\"\n      :loading=\"loading\"\n      rowKey=\"id\"\n      @search=\"onSearch\"\n      @refresh=\"onRefresh\"\n      :format-conditions=\"true\"\n      @reset=\"onReset\"\n      :pagination=\"{\n        current: page,\n        pageSize: pageSize,\n        total: total,\n        showSizeChanger: true,\n        showLessItems: true,\n        showQuickJumper: true,\n        showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条，总计 ${total} 条`,\n        onChange: onPageChange,\n        onShowSizeChange: onSizeChange,\n      }\"\n    >\n      <template slot=\"statusTitle\">\n        状态<a-icon style=\"margin: 0 4px\" type=\"info-circle\" />\n      </template>\n      <template slot=\"send\" slot-scope=\"{text}\">\n        {{text ? '是' : '否'}}\n      </template>\n      <template slot=\"status\" slot-scope=\"{text}\">\n        {{text | statusStr}}\n      </template>\n    </advance-table>\n    <api />\n  </div>\n</template>\n\n<script>\n  import AdvanceTable from '@/components/table/advance/AdvanceTable'\n  import {dataSource as ds} from '@/services'\n  import Api from '@/pages/components/table/Api'\n\n  export default {\n    name: 'Table',\n    components: {Api, AdvanceTable},\n    filters: {\n      statusStr(val) {\n        switch (val) {\n          case 1: return '已下单'\n          case 2: return '已付款'\n          case 3: return '已审核'\n          case 4: return '已发货'\n        }\n      }\n    },\n    data() {\n      return {\n        loading: false,\n        page: 1,\n        pageSize: 10,\n        total: 0,\n        columns: [\n          {\n            title: '商品名称',\n            dataIndex: 'name',\n            searchAble: true\n          },\n          {\n            title: '订单号',\n            dataIndex: 'orderId'\n          },\n          {\n            searchAble: true,\n            dataIndex: 'status',\n            dataType: 'select',\n            slots: {title: 'statusTitle'},\n            scopedSlots: {customRender: 'status'},\n            search: {\n              selectOptions: [\n                {title: '已下单', value: 1},\n                {title: '已付款', value: 2},\n                {title: '已审核', value: 3},\n                {title: '已发货', value: 4}\n              ]\n            }\n          },\n          {\n            title: '发货',\n            searchAble: true,\n            dataIndex: 'send',\n            dataType: 'boolean',\n            scopedSlots: {customRender: 'send'},\n            search: {\n              switchOptions: {\n                checkedText: '开',\n                uncheckedText: '关'\n              }\n            }\n          },\n          {\n            title: '审核时间',\n            dataIndex: 'auditTime',\n            dataType: 'time',\n          }\n        ],\n        dataSource: [],\n        conditions: {}\n      }\n    },\n    created() {\n      this.getGoodList()\n      this.getColumns()\n    },\n    methods: {\n      getGoodList() {\n        this.loading = true\n        const {page, pageSize, conditions} = this\n        ds.goodsList({page, pageSize, ...conditions}).then(result => {\n          const {list, page, pageSize, total} = result.data.data\n          this.dataSource = list\n          this.page = page\n          this.total = total\n          this.pageSize = pageSize\n          this.loading = false\n        })\n      },\n      getColumns() {\n        ds.goodsColumns().then(res => {\n          this.columns = res.data\n        })\n      },\n      onSearch(conditions, searchOptions) {\n        this.page = 1\n        this.conditions = conditions\n        this.getGoodList()\n      },\n      onSizeChange(current, size) {\n        this.page = 1\n        this.pageSize = size\n        this.getGoodList()\n      },\n      onRefresh(conditions) {\n        this.conditions = conditions\n        this.getGoodList()\n      },\n      onReset(conditions) {\n        this.conditions = conditions\n        this.getGoodList()\n      },\n      onPageChange(page, pageSize) {\n        this.page = page\n        this.pageSize = pageSize\n        this.getGoodList()\n      }\n    }\n  }\n</script>\n\n<style scoped lang=\"less\">\n.table{\n  background-color: @base-bg-color;\n  padding: 24px;\n}\n</style>\n"
  },
  {
    "path": "front/src/pages/components/table/index.js",
    "content": "import Table from './Table'\nexport default Table"
  },
  {
    "path": "front/src/pages/customer/followHistory.vue",
    "content": "<template>\n  <div>\n    <a-card>\n      <div>\n        <a-space class=\"operator\">\n          <a-form layout=\"inline\" :form=\"queryForm\">\n            <a-form-item label=\"关键字\">\n              <a-input\n                  v-decorator=\"['keyword', { rules: [{ required: false,min:1,max:120,message:'内容长度在1到120之间'}] }]\"\n                  placeholder=\"请输入姓名或电话\"\n              />\n            </a-form-item>\n            <a-form-item label=\"跟进时间\">\n              <a-range-picker\n                  :show-time=\"{ format: 'HH:mm' }\"\n                  v-decorator=\"['rangeTime', { rules: [{ required: false}] }]\"\n                  format=\"YYYY/MM/DD HH:mm:ss\"\n                  :placeholder=\"['开始时间', '结束时间']\"\n              />\n            </a-form-item>\n            <a-form-item label=\"跟进类型\">\n              <a-select\n                  style=\"width: 8rem\"\n                  v-decorator=\"['type', { rules: [{ required: false}] }]\">\n                <a-select-option :value=\"9999999\">\n                  全部\n                </a-select-option>\n                <a-select-option\n                    :key=\"index\"\n                    :value=\"key\"\n                    v-for=\"(value,key,index) in cfuhType\">\n                  {{value}}\n                </a-select-option>\n              </a-select>\n            </a-form-item>\n            <a-form-item>\n              <a-button @click=\"query()\" :loading=\"queryLoading\">查询</a-button>\n            </a-form-item>\n          </a-form>\n        </a-space>\n        <a-table\n            :columns=\"columns\"\n            :data-source=\"dataSource\"\n            :pagination=\"pagination\"\n            :loading=\"loading\"\n            @change=\"handleTableChange\"\n        >\n           <span slot=\"action\" slot-scope=\"text\">\n              <a @click=\"showFollowModal(text.id,text)\">编辑</a>\n           </span>\n        </a-table>\n      </div>\n    </a-card>\n    <!--    跟进-->\n    <a-modal\n        title=\"跟进记录\"\n        :visible=\"followVisible\"\n        :confirm-loading=\"followConfirmLoading\"\n        @ok=\"handleFollowOk\"\n        @cancel=\"handleFollowCancel\"\n        okText=\"保存\">\n      <a-form :form=\"followForm\" :layout=\"`horizontal`\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['id',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item disabled label=\"客户姓名\">\n          <a-input\n              v-decorator=\"['name', { rules: [{ required: true, message: '姓名'  }]}]\"\n              disabled />\n        </a-form-item>\n        <a-form-item disabled label=\"跟进时间\">\n          <a-date-picker\n              show-time\n              v-decorator=\"['tracetime', { rules: [{ required: true, message: '跟进时间'  }]}]\"\n          />\n        </a-form-item><a-form-item disabled label=\"跟进内容\">\n        <a-input\n            v-decorator=\"['tracedetails', { rules: [{ required: true, message: '跟进内容',min:1,max:120,message:'内容长度在1到120之间'  }]}]\"\n        />\n      </a-form-item>\n        <a-form-item disabled label=\"跟进方式\">\n          <a-select\n              v-decorator=\"['tracetype', { rules: [{ required: true, message: '跟进方式'  }]}]\">\n            <a-select-option\n                :value=\"value.id\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in dictionaryDetailsFollow\">\n              {{value.title}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item disabled label=\"跟进结果\">\n          <a-select\n              v-decorator=\"['traceresult', { rules: [{ required: true, message: '跟进结果'  }]}]\">\n            <a-select-option\n                :value=\"key\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in followTraceResult\">\n              {{value}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item disabled label=\"备注\">\n          <a-textarea\n              v-decorator=\"['comment', { rules: [{ required: true, message: '备注',min:1,max:120,message:'内容长度在1到120之间'  }]}]\"\n              :auto-size=\"{ minRows: 3, maxRows: 5 }\"\n          />\n        </a-form-item>\n        <a-form-item disabled label=\"跟进类型\">\n          <a-select\n              v-decorator=\"['type', { rules: [{ required: true, message: '跟进类型'  }]}]\"\n          >\n            <a-select-option\n                :value=\"key\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in followType\">\n              {{value}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n      </a-form>\n    </a-modal>\n  </div>\n</template>\n\n<script>\nimport * as customerFollowUpHistory from \"@/services/customerFollowUpHistory\"\nimport * as employee from \"@/services/employee\"\nimport * as dictionaryDetails from \"@/services/dictionaryDetails\";\nimport moment from \"moment\";\n\nconst columns = [\n  {\n    title: '编号',\n    dataIndex: 'id'\n  },\n  {\n    title: '姓名',\n    dataIndex: 'name',\n  },\n  {\n    title: '跟进日期',\n    dataIndex: 'tracetime',\n    customRender:(text)=>moment(text).format(\"YYYY-MM-DD\")\n  },\n  {\n    title: '跟进内容',\n    dataIndex: 'tracedetails',\n  },\n  {\n    title: '跟进方式',\n    dataIndex: 'tracetypeFind',\n  },\n  {\n    title: '跟进结果',\n    dataIndex: 'traceresult',\n    customRender:(text)=>customerFollowUpHistory.traceresult[parseInt(text)]\n  },\n  {\n    title: '录入人',\n    dataIndex: 'inputuserFind',\n  },\n  {\n    title: '跟进类型',\n    dataIndex: 'type',\n    customRender:(text)=>customerFollowUpHistory.type[parseInt(text)]\n  },\n  {\n    title: '操作',\n    scopedSlots: {customRender: 'action'}\n  }\n]\nexport default {\n  data() {\n    return {\n      queryForm:this.$form.createForm(this, {name: 'coordinated'}),\n      queryLoading:false,\n      cfuhType:customerFollowUpHistory.type,\n      // table\n      columns: columns,\n      dataSource: [],\n      selectedRows: [],\n      pagination: {},\n      loading: false,\n      // 跟进\n      followForm:this.$form.createForm(this, {name: 'coordinated'}),\n      followVisible :false,\n      followConfirmLoading :false,\n      followType:customerFollowUpHistory.type,\n      followTraceResult:customerFollowUpHistory.traceresult,\n      dictionaryDetailsFollow:[],\n      // 员工表\n      employeeList:[]\n    }\n  },\n  async mounted() {\n    // id 10 跟进方式\n    const {data} = await dictionaryDetails.list({page:1,size:999999,id:10})\n    this.dictionaryDetailsFollow = data.data.list\n\n    const employeeData = await employee.list({page:1,size:99999})\n    this.employeeList = employeeData.data.data.list\n\n    await this.query()\n    this.queryForm.setFieldsValue({\"type\":9999999})\n  },\n  methods: {\n    query(){\n      this.queryLoading = true\n      this.queryForm.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.queryLoading = false\n          return;\n        }\n        if(values.rangeTime){\n          if(values.rangeTime.length!==0){\n            values.startTime=values.rangeTime[0].toDate().toISOString()\n            values.endTime=values.rangeTime[1].toDate().toISOString()\n          }\n          delete values.rangeTime\n        }\n        this.fetch({\"page\": 1, \"size\": 10,...values})\n      })\n    },\n    // table\n    handleTableChange(pagination) {\n      const pager = {...this.pagination};\n      pager.current = pagination.current;\n      this.pagination = pager;\n      this.fetch({\n        size: pagination.pageSize,\n        page: pagination.current,\n      });\n    },\n    fetch(params = {\"page\": 1, \"size\": 10}) {\n      this.loading = true\n      customerFollowUpHistory.list(params || {\"page\": 1, \"size\": 10}).then(({data}) => {\n        const res = data.data\n        const pagination = {...this.pagination};\n        pagination.total = res.total\n        pagination.current = params.page\n        this.dataSource = res.list.map((e, i) => ({\n          ...e,\n          key: i + \"\",\n          tracetypeFind:this.dictionaryDetailsFollow.find(d=>d.id===e.tracetype).title,\n          inputuserFind:this.employeeList.find(eL=>eL.id===e.inputuser).name,\n          }))\n        this.pagination = pagination\n        this.loading = false\n        this.queryLoading = false\n      })\n    },\n    updateItem(id) {\n      this.showModal('更改')\n      customerFollowUpHistory.getDetail(id).then(({data}) => {\n        // 这里不能循环\n        this.form.setFieldsValue({\"id\": data.data[\"id\"]})\n        this.form.setFieldsValue({\"sn\": data.data[\"sn\"]})\n        this.form.setFieldsValue({\"title\": data.data[\"title\"]})\n        this.form.setFieldsValue({\"intro\": data.data[\"intro\"]})\n      })\n    },\n    // 移交\n    handleFollowCancel(){\n      this.followVisible = false;\n      this.followConfirmLoading = false\n      this.followForm.resetFields()\n    },\n    handleFollowOk(){\n      this.followConfirmLoading = true;\n      this.followForm.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.followConfirmLoading = false\n          return;\n        }\n        values.tracetime = values.tracetime.toDate()\n\n        customerFollowUpHistory['update'](values).then(({data}) => {\n          this.followConfirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message:'跟进记录出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else{\n            this.$notification.success({\n              message:'成功',\n              description:'跟进记录成功',\n            });\n          }\n          this.followVisible = false\n          this.query()\n        })\n      });\n    },\n    async showFollowModal(id,line){\n      this.followVisible = true;\n      this.followConfirmLoading = false\n      await this.followForm.resetFields()\n      // 这里不能循环\n      this.$nextTick(()=>{\n        this.followForm.setFieldsValue({id})\n        this.followForm.setFieldsValue({name:line.name})\n        this.followForm.setFieldsValue({comment:line.comment})\n        this.followForm.setFieldsValue({tracedetails:line.tracedetails})\n        this.followForm.setFieldsValue({tracetime:new moment(new Date())})\n        this.followForm.setFieldsValue({tracetype:this.dictionaryDetailsFollow[0].id})\n        this.followForm.setFieldsValue({type:Object.keys(this.followType)[0]})\n        this.followForm.setFieldsValue({traceresult:Object.keys(this.followTraceResult)[0]})\n      })\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n.search {\n  margin-bottom: 54px;\n}\n\n.fold {\n  width: calc(100% - 216px);\n  display: inline-block\n}\n\n.operator {\n  margin-bottom: 18px;\n}\n\n@media screen and (max-width: 900px) {\n  .fold {\n    width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/pages/customer/handoverHistory.vue",
    "content": "<template>\n  <div>\n    <a-card>\n      <div>\n        <a-space class=\"operator\">\n          <a-form layout=\"inline\" :form=\"queryForm\">\n            <a-form-item label=\"关键字\">\n              <a-input\n                  v-decorator=\"['keyword', { rules: [{ required: false,min:1,max:120,message:'内容长度在1到120之间'}] }]\"\n                  placeholder=\"请输入姓名/电话\"\n              />\n            </a-form-item>\n            <a-form-item label=\"跟进时间\">\n              <a-range-picker\n                  :show-time=\"{ format: 'HH:mm' }\"\n                  v-decorator=\"['rangeTime', { rules: [{ required: false}] }]\"\n                  format=\"YYYY/MM/DD HH:mm:ss\"\n                  :placeholder=\"['开始时间', '结束时间']\"\n              />\n            </a-form-item>\n            <a-form-item>\n              <a-button @click=\"query()\" :loading=\"queryLoading\">查询</a-button>\n            </a-form-item>\n          </a-form>\n        </a-space>\n        <a-table\n            :columns=\"columns\"\n            :data-source=\"dataSource\"\n            :pagination=\"pagination\"\n            :loading=\"loading\"\n            @change=\"handleTableChange\"\n        >\n        </a-table>\n      </div>\n    </a-card>\n  </div>\n</template>\n\n<script>\nimport * as customerHandover from \"@/services/customerHandover\"\nimport moment from \"moment\";\n\nconst columns = [\n  {\n    title: '编号',\n    dataIndex: 'id'\n  },\n  {\n    title: '客户姓名',\n    dataIndex: 'customerName',\n  },\n  {\n    title: '操作日期',\n    dataIndex: 'transTime',\n    customRender:(text)=>moment(text).format(\"YYYY-MM-DD\")\n  },\n  {\n    title: '操作人',\n    dataIndex: 'transUser',\n  },\n  {\n    title: '旧营销人员',\n    dataIndex: 'oldSeller',\n  },\n  {\n    title: '新营销人员',\n    dataIndex: 'newSeller',\n  },\n  {\n    title: '移交原因',\n    dataIndex: 'transReason',\n  }\n]\nexport default {\n  data() {\n    return {\n      queryForm:this.$form.createForm(this, {name: 'coordinated'}),\n      queryLoading:false,\n      // table\n      columns: columns,\n      dataSource: [],\n      selectedRows: [],\n      pagination: {},\n      loading: false,\n      // 员工表\n    }\n  },\n  async mounted() {\n    await this.query()\n  },\n  methods: {\n    query(){\n      this.queryLoading = true\n      this.queryForm.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.queryLoading = false\n          return;\n        }\n        if(values.rangeTime){\n          if(values.rangeTime.length!==0){\n            values.startTime=values.rangeTime[0].toDate().toISOString()\n            values.endTime=values.rangeTime[1].toDate().toISOString()\n          }\n          delete values.rangeTime\n        }\n        this.fetch({\"page\": 1, \"size\": 10,...values})\n      })\n    },\n    // table\n    handleTableChange(pagination) {\n      const pager = {...this.pagination};\n      pager.current = pagination.current;\n      this.pagination = pager;\n      this.fetch({\n        size: pagination.pageSize,\n        page: pagination.current,\n      });\n    },\n    fetch(params = {\"page\": 1, \"size\": 10}) {\n      this.loading = true\n      customerHandover.list(params || {\"page\": 1, \"size\": 10}).then(({data}) => {\n        const res = data.data\n        const pagination = {...this.pagination};\n        pagination.total = res.total\n        pagination.current = params.page\n        this.dataSource = res.list.map((e, i) => ({\n          ...e,\n          key: i + \"\",\n        }))\n        this.pagination = pagination\n        this.loading = false\n        this.queryLoading = false\n      })\n    },\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n.search {\n  margin-bottom: 54px;\n}\n\n.fold {\n  width: calc(100% - 216px);\n  display: inline-block\n}\n\n.operator {\n  margin-bottom: 18px;\n}\n\n@media screen and (max-width: 900px) {\n  .fold {\n    width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/pages/customer/manager.vue",
    "content": "<template>\n  <div>\n    <a-card>\n      <div>\n        <a-space class=\"operator\">\n          <a-form layout=\"inline\" :form=\"queryForm\">\n            <a-form-item label=\"关键字\">\n              <a-input\n                  v-decorator=\"['keyword', { rules: [{ required: false,min:1,max:120,message:'输入长度应在1到120之间'}] }]\"\n                  placeholder=\"请输入姓名/电话\"\n              />\n            </a-form-item>\n            <a-form-item label=\"状态\">\n              <a-select\n                  style=\"width: 6rem\"\n                  v-decorator=\"['status',{ rules: [{ required: true, message: '状态' }] }]\">\n                <a-select-option\n                    :value=\"key\"\n                    :key=\"index\"\n                    v-for=\"(value,key,index) in statusMap\">\n                  {{value}}\n                </a-select-option>\n              </a-select>\n            </a-form-item>\n            <a-form-item>\n              <a-button :loading=\"queryLoading\" @click=\"query()\">查询</a-button>\n            </a-form-item>\n          </a-form>\n          <a-button type=\"primary\" @click=\"showModal('新增')\">添加</a-button>\n        </a-space>\n        <a-table\n            :columns=\"columns\"\n            :data-source=\"dataSource\"\n            :pagination=\"pagination\"\n            :loading=\"loading\"\n            @change=\"handleTableChange\"\n            :scroll=\"{ x: 1500, y: 300 }\"\n        >\n           <span slot=\"action\" slot-scope=\"text\">\n             <a-button type=\"link\" shape=\"round\" icon=\"edit\" size=\"small\" @click=\"updateItem(text.id,text)\" >编辑</a-button>\n             <a-button type=\"link\" shape=\"round\" icon=\"edit\" size=\"small\" @click=\"showFollowModal(text.id,text)\" >跟进</a-button>\n             <a-button type=\"link\" shape=\"round\" icon=\"edit\" size=\"small\" @click=\"showHandoverModal(text.id)\">移交</a-button>\n             <a-button type=\"link\" shape=\"round\" icon=\"edit\" size=\"small\" @click=\"showStatusModal(text.id)\" >修改状态</a-button>\n           </span>\n        </a-table>\n      </div>\n    </a-card>\n<!--    新增修改-->\n    <a-modal\n        :title=\"title\"\n        :visible=\"visible\"\n        :confirm-loading=\"confirmLoading\"\n        @ok=\"handleOk\"\n        @cancel=\"handleCancel\"\n        okText=\"提交\"\n    >\n      <a-form :form=\"form\" :layout=\"`horizontal`\">\n          <a-form-item hidden>\n            <a-input v-decorator=\"['id',{ rules: [{ required: false}] }]\"/>\n          </a-form-item>\n        <div v-for=\"(item) in baseColumns\" :key=\"item.dataIndex\">\n          <a-form-item :label=\"item.title\">\n            <a-select v-if=\"item.dataIndex==='gender'\"\n                style=\"width: 6rem\"\n                v-decorator=\"[item.dataIndex,{ rules: [{ required: true, message: item.title }] }]\">\n              <a-select-option :value=\"1\">\n                男\n              </a-select-option>\n              <a-select-option :value=\"0\">\n                女\n              </a-select-option>\n            </a-select>\n            <a-select v-else-if=\"item.dataIndex==='job'\" v-decorator=\"[item.dataIndex,{ rules: [{ required: true, message: item.title }] }]\">\n              <a-select-option v-for=\"(list) in dictionaryDetailsJob\" :key=\"list.id\">\n                {{list.title}}\n              </a-select-option>\n            </a-select>\n            <a-select v-else-if=\"item.dataIndex==='source'\" v-decorator=\"[item.dataIndex,{ rules: [{ required: true, message: item.title }] }]\">\n              <a-select-option v-for=\"(list) in dictionaryDetailsSource\" :key=\"list.id\">\n                {{list.title}}\n              </a-select-option>\n            </a-select>\n            <a-input-number v-else-if=\"item.dataIndex==='age'\" :min=\"0\" :max=\"200\" v-decorator=\"[item.dataIndex, { rules: [{ required: true, message: item.title  }]}]\" />\n            <a-input v-else-if=\"item.dataIndex==='name'\"\n                     v-decorator=\"[item.dataIndex, { rules: [{ required: true,min:1,max:120,message:'输入长度应在1到120之间' }]}]\"\n                     :placeholder=\"`请输入`+item.title\"\n            />\n            <a-input v-else-if=\"item.dataIndex==='tel'\"\n                     v-decorator=\"[item.dataIndex, { rules: [{ required: true,pattern:validators.phoneReg,message:validators.phoneMsg  }]}]\"\n                     :placeholder=\"`请输入`+item.title\"\n            />\n            <a-input v-else-if=\"item.dataIndex==='qq'\"\n                     v-decorator=\"[item.dataIndex, { rules: [{ required: true,pattern:validators.qqReg,message:validators.qqMsg  }]}]\"\n                     :placeholder=\"`请输入`+item.title\"\n            />\n            <a-input v-else\n                v-decorator=\"[item.dataIndex, { rules: [{ required: true, message: item.title  }]}]\"\n                :placeholder=\"`请输入`+item.title\"\n            />\n          </a-form-item>\n        </div>\n      </a-form>\n    </a-modal>\n<!--    修改客户状态-->\n    <a-modal\n        title=\"修改客户状态\"\n        :visible=\"statusVisible\"\n        :confirm-loading=\"statusConfirmLoading\"\n        @ok=\"handleStatusOk\"\n        @cancel=\"handleStatusCancel\"\n        okText=\"保存\"\n    >\n      <a-form :form=\"statusForm\" :layout=\"`horizontal`\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['id',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item lable=\"姓名\">\n          <a-input\n              disabled\n               v-decorator=\"['name', { rules: [{ required: true, message: '名字长度在1到15之间',min:1,max:15  }]}]\"\n               :placeholder=\"`请输入姓名`\"\n          />\n        </a-form-item>\n        <a-form-item label=\"状态\">\n          <a-select\n              v-decorator=\"['status',{ rules: [{ required: true, message: '状态' }] }]\">\n            <a-select-option\n                :value=\"key\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in statusMap\">\n              {{value}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n      </a-form>\n    </a-modal>\n<!--    移交-->\n    <a-modal\n        title=\"移交\"\n        :visible=\"handoverVisible\"\n        :confirm-loading=\"handoverConfirmLoading\"\n        @ok=\"handleHandoverOk\"\n        @cancel=\"handleHandoverCancel\"\n        okText=\"保存\">\n      <a-form :form=\"handoverForm\" :layout=\"`horizontal`\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['oldseller',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item hidden>\n          <a-input v-decorator=\"['customerid',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item hidden>\n          <a-input v-decorator=\"['transuser',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item lable=\"客户姓名\">\n          <a-input\n              disabled\n              v-decorator=\"['name', { rules: [{ required: true, message: '姓名' }]}]\"\n          />\n        </a-form-item>\n        <a-form-item label=\"旧营销人员\">\n          <a-input\n              disabled\n              v-decorator=\"['oldsellerName',{ rules: [{ required: true, message: '旧营销人员' }] }]\"\n          />\n        </a-form-item>\n        <a-form-item label=\"新营销人员\">\n          <a-select\n              v-decorator=\"['newseller',{ rules: [{ required: true, message: '状态' }] }]\">\n            <a-select-option\n                :value=\"key\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in employeeList\">\n              {{value.name}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item label=\"移交原因\">\n          <a-textarea\n              v-decorator=\"['transreason',{ rules: [{ required: true, message: '移交原因的内容长度在10和120之间' ,min:10,max:120 }] }]\"\n              :auto-size=\"{ minRows: 3, maxRows: 5 }\"\n          />\n        </a-form-item>\n      </a-form>\n    </a-modal>\n<!--    跟进-->\n    <a-modal\n        title=\"跟进记录\"\n        :visible=\"followVisible\"\n        :confirm-loading=\"followConfirmLoading\"\n        @ok=\"handleFollowOk\"\n        @cancel=\"handleFollowCancel\"\n        okText=\"保存\">\n      <a-form :form=\"followForm\" :layout=\"`horizontal`\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['customerid',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item disabled label=\"客户姓名\">\n          <a-input\n              v-decorator=\"['name', { rules: [{ required: true, message: '姓名'  }]}]\"\n              disabled />\n        </a-form-item>\n        <a-form-item disabled label=\"跟进时间\">\n          <a-date-picker\n              show-time\n              v-decorator=\"['tracetime', { rules: [{ required: true, message: '跟进时间'  }]}]\"\n              />\n        </a-form-item><a-form-item disabled label=\"跟进内容\">\n          <a-input\n              v-decorator=\"['tracedetails', { rules: [{ required: true, message: '跟进内容',min:1,max:120,message:'跟进内容长度在1到120之间'  }]}]\"\n              />\n        </a-form-item>\n        <a-form-item disabled label=\"跟进方式\">\n          <a-select\n              v-decorator=\"['tracetype', { rules: [{ required: true, message: '跟进方式'  }]}]\">\n            <a-select-option\n                :value=\"value.id\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in dictionaryDetailsFollow\">\n              {{value.title}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item disabled label=\"跟进结果\">\n          <a-select\n              v-decorator=\"['traceresult', { rules: [{ required: true, message: '跟进结果'  }]}]\">\n            <a-select-option\n                :value=\"key\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in followTraceResult\">\n              {{value}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item disabled label=\"备注\">\n          <a-textarea\n              v-decorator=\"['comment', { rules: [{ required: true, message: '备注' ,min:1,max:120,message:'备注内容在1到120之间'}]}]\"\n              :auto-size=\"{ minRows: 3, maxRows: 5 }\"\n              />\n        </a-form-item>\n        <a-form-item disabled label=\"跟进类型\">\n          <a-select\n              v-decorator=\"['type', { rules: [{ required: true, message: '跟进类型'  }]}]\"\n              >\n            <a-select-option\n                :value=\"key\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in followType\">\n              {{value}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n      </a-form>\n    </a-modal>\n  </div>\n</template>\n\n<script>\nimport * as customerManager from \"@/services/customerManager\"\nimport * as dictionaryDetails from \"@/services/dictionaryDetails\"\nimport * as employee from \"@/services/employee\"\nimport * as customerHandover from \"@/services/customerHandover\"\nimport * as customerFollowUpHistory from \"@/services/customerFollowUpHistory\"\nimport moment from \"moment\";\nimport validators from \"@/utils/validators\";\n\nconst baseColumns =[\n  {\n    width:120,\n    title: '姓名',\n    dataIndex: 'name',\n    ellipsis: true,\n    fixed: 'left'\n  },\n  {\n    title: '年龄',\n    dataIndex: 'age',\n  },\n  {\n    title: '性别',\n    dataIndex: 'gender',\n    customRender:(text)=>text===1?'男':'女'\n  },\n  {\n    title: '电话',\n    dataIndex: 'tel',\n  },\n  {\n    title: 'QQ',\n    dataIndex: 'qq',\n  },\n  {\n    title: '职业',\n    dataIndex: 'job',\n    ellipsis: true,\n  },\n  {\n    width:60,\n    title: '来源',\n    dataIndex: 'source',\n    ellipsis: true,\n  }\n]\nconst columns = [\n  {\n    width:60,\n    title: '编号',\n    dataIndex: 'id',\n    fixed: 'left'\n  },\n    ...baseColumns,\n  {\n    title: '营销人员',\n    dataIndex: 'inputuser'\n  },\n  {\n    title: '状态',\n    dataIndex: 'status',\n    customRender:(text)=>customerManager.statusMap[parseInt(text)]\n  },\n  {\n    width:380,\n    title: '操作',\n    scopedSlots: {customRender: 'action'},\n    fixed: 'right'\n  }\n]\nexport default {\n  name: 'Department',\n  data() {\n    return {\n      validators,\n      queryForm:this.$form.createForm(this, {name: 'coordinated'}),\n      queryLoading:false,\n      statusMap:customerManager.statusMap,\n      // table\n      columns: columns,\n      dataSource: [],\n      selectedRows: [],\n      pagination: {current:1},\n      loading: false,\n      // 新增修改\n      baseColumns,\n      form:this.$form.createForm(this, {name: 'coordinated'}),\n      confirmLoading:false,\n      title:'新增',\n      visible: false,\n      // 修改客户状态\n      statusForm:this.$form.createForm(this, {name: 'coordinated'}),\n      statusConfirmLoading:false,\n      statusVisible:false,\n      // 移交\n      handoverForm:this.$form.createForm(this, {name: 'coordinated'}),\n      handoverVisible :false,\n      handoverConfirmLoading :false,\n      // 跟进\n      followForm:this.$form.createForm(this, {name: 'coordinated'}),\n      followVisible :false,\n      followConfirmLoading :false,\n      followType:customerFollowUpHistory.type,\n      followTraceResult:customerFollowUpHistory.traceresult,\n      // 字典细节\n      dictionaryDetailsJob:[],\n      dictionaryDetailsSource:[],\n      dictionaryDetailsFollow:[],\n      // 员工表\n      employeeList:[]\n    }\n  },\n  mounted() {\n    this.queryForm.setFieldsValue({\"status\":\"0\"})\n    this.query()\n    // id 1 职业\n    dictionaryDetails.list({page:1,size:999999,id:1}).then(({data})=>{\n      this.dictionaryDetailsJob = data.data.list\n    });\n    // id 2 来源\n    dictionaryDetails.list({page:1,size:999999,id:2}).then(({data})=>{\n      this.dictionaryDetailsSource = data.data.list\n    })\n    // id 10 跟进方式\n    dictionaryDetails.list({page:1,size:999999,id:10}).then(({data})=>{\n      this.dictionaryDetailsFollow = data.data.list\n    })\n\n    employee.list({page:1,size:99999}).then(({data})=>{\n      this.employeeList = data.data.list\n    })\n  },\n  methods: {\n    query(){\n      this.queryLoading = true\n      this.queryForm.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.queryLoading = false\n          return;\n        }\n        this.fetch({\"page\": this.pagination.current, \"size\": 10,...values})\n      })\n    },\n    // table\n    handleTableChange(pagination) {\n      const pager = {...this.pagination};\n      pager.current = pagination.current;\n      this.pagination = pager;\n      this.fetch({\n        size: pagination.pageSize,\n        page: pagination.current,\n      });\n    },\n    async fetch(params = {\"page\": 1, \"size\": 10}) {\n      this.loading = true\n      let {data} = await customerManager.list(params || {\"page\": 1, \"size\": 10})\n      const res = data.data\n      const pagination = {...this.pagination};\n      pagination.total = res.total\n      pagination.current = params.page\n      this.dataSource = res.list.map((e, i) => ({key: i + \"\", ...e}))\n      this.pagination = pagination\n      this.loading = false\n      this.queryLoading=false\n      return data\n    },\n    deleteItem(text) {\n      const title = '删除'\n      customerManager.deleteItem(text.id).then(({data}) => {\n        if (data.code !== 200) {\n          this.$notification['error']({\n            message: title + '客户信息出现错误',\n            description: '建议检查网络连接或重新登陆',\n          });\n        }\n        this.$notification.success({\n          message: title + '成功',\n          description: title + '客户信息成功',\n        });\n        this.fetch({\"page\": this.pagination.current, \"size\": 10})\n      })\n    },\n    // 客户状态\n    handleStatusOk(){\n      this.statusConfirmLoading = true;\n      this.statusForm.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.statusConfirmLoading = false\n          return;\n        }\n        values.status = parseInt(values.status)\n        customerManager['update'](values).then(({data}) => {\n          this.statusConfirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: this.title + '角色信息出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else {\n            this.$notification.success({\n              message: this.title + '成功',\n              description: this.title + '角色信息成功',\n            });\n          }\n          this.statusVisible = false\n          this.query()\n        })\n      });\n    },\n    handleStatusCancel(){\n      this.statusVisible = false;\n      this.statusConfirmLoading = false\n      this.statusForm.resetFields()\n    },\n    async showStatusModal(id){\n      this.statusVisible = true;\n      this.statusConfirmLoading = false\n      await this.statusForm.resetFields()\n      const {data} = await customerManager.getDetail(id)\n      if(!data.data) return;\n      // 这里不能循环\n      this.statusForm.setFieldsValue({id:data.data[\"id\"]})\n      this.statusForm.setFieldsValue({name:data.data[\"name\"]})\n      this.statusForm.setFieldsValue({status:data.data[\"status\"]+\"\"})\n    },\n    // 移交\n    handleHandoverCancel(){\n      this.handoverVisible = false;\n      this.handoverConfirmLoading = false\n      this.handoverForm.resetFields()\n    },\n    handleHandoverOk(){\n      this.handoverConfirmLoading = true;\n      this.handoverForm.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.handoverConfirmLoading = false\n          return;\n        }\n        customerHandover['add'](values).then(({data}) => {\n          this.handoverConfirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: this.title + '角色信息出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else{\n            this.$notification.success({\n              message: this.title + '成功',\n              description: this.title + '角色信息成功',\n            });\n          }\n          this.handoverVisible = false\n          this.query()\n        })\n      });\n    },\n    async showHandoverModal(id){\n      this.handoverVisible = true;\n      this.handoverConfirmLoading = false\n      await this.handoverForm.resetFields()\n      const {data} = await customerManager.getDetail(id)\n      if(!data.data) return;\n      // 这里不能循环\n      this.handoverForm.setFieldsValue({oldsellerName:this.employeeList.find(e=>e.id===data.data[\"inputuser\"]).name})\n      this.handoverForm.setFieldsValue({oldseller:data.data[\"inputuser\"]})\n      this.handoverForm.setFieldsValue({customerid:data.data[\"id\"]})\n      this.handoverForm.setFieldsValue({name:data.data[\"name\"]})\n    },\n    // 跟进\n    handleFollowCancel(){\n      this.followVisible = false;\n      this.followConfirmLoading = false\n      this.followForm.resetFields()\n    },\n    handleFollowOk(){\n      this.followConfirmLoading = true;\n      this.followForm.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.followConfirmLoading = false\n          return;\n        }\n        values.tracetime = values.tracetime.toDate()\n\n        customerFollowUpHistory['add'](values).then(({data}) => {\n          this.followConfirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: '跟进记录出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else{\n            this.$notification.success({\n              message: '成功',\n              description: '跟进记录成功',\n            });\n          }\n          this.followVisible = false\n          this.query()\n        })\n      });\n    },\n    async showFollowModal(id,line){\n      this.followVisible = true;\n      this.followConfirmLoading = false\n      await this.followForm.resetFields()\n      // 这里不能循环\n      this.$nextTick(()=>{\n        this.followForm.setFieldsValue({customerid:line.id})\n        this.followForm.setFieldsValue({name:line.name})\n        this.followForm.setFieldsValue({tracetime:new moment(new Date())})\n        this.followForm.setFieldsValue({tracetype:this.dictionaryDetailsFollow[0].id})\n        this.followForm.setFieldsValue({type:Object.keys(this.followType)[0]})\n        this.followForm.setFieldsValue({traceresult:Object.keys(this.followTraceResult)[0]})\n      })\n    },\n    // modal\n    async showModal(title) {\n      this.visible = true;\n      this.title = title || '新增'\n      await this.form.resetFields()\n    },\n    async updateItem(id,line) {\n      this.confirmLoading=false\n      await this.showModal('更改')\n      customerManager.getDetail(id).then(({data}) => {\n        if(!data.data) return;\n        // 这里不能循环\n        this.form.setFieldsValue({id:data.data[\"id\"]})\n        this.form.setFieldsValue({name:data.data[\"name\"]})\n        this.form.setFieldsValue({age:line[\"age\"]})\n        this.form.setFieldsValue({gender:data.data[\"gender\"]})\n        this.form.setFieldsValue({tel:data.data[\"tel\"]})\n        this.form.setFieldsValue({qq:data.data[\"qq\"]})\n        this.form.setFieldsValue({job:data.data[\"job\"]})\n        this.form.setFieldsValue({source:data.data[\"source\"]})\n      })\n    },\n    handleOk() {\n      this.confirmLoading = true;\n      this.form.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.confirmLoading=false\n          return;\n        }\n        let method = 'add';\n        if (values.id) method = 'update';\n\n        customerManager[method](values).then(({data}) => {\n          this.confirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: this.title + '角色信息出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else{\n            this.$notification.success({\n              message: this.title + '成功',\n              description: this.title + '角色信息成功',\n            });\n          }\n          this.visible = false\n          this.query()\n        })\n      });\n    },\n    handleCancel() {\n      this.visible = false;\n      this.title = ''\n      this.confirmLoading = false\n      this.form.resetFields()\n    },\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n.search {\n  margin-bottom: 54px;\n}\n\n.fold {\n  width: calc(100% - 216px);\n  display: inline-block\n}\n\n.operator {\n  margin-bottom: 18px;\n}\n\n@media screen and (max-width: 900px) {\n  .fold {\n    width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/pages/customer/official.vue",
    "content": "<template>\n  <div>\n    <a-card>\n      <div>\n        <a-space class=\"operator\">\n          <a-form layout=\"inline\" :form=\"queryForm\">\n            <a-form-item label=\"关键字\">\n              <a-input\n                  v-decorator=\"['keyword', { rules: [{ required: false,min:1,max:120,message:'输入长度应在1到120之间'}] }]\"\n                  placeholder=\"请输入姓名/电话\"\n              />\n            </a-form-item>\n            <a-form-item label=\"状态\">\n              <a-select\n                  style=\"width: 6rem\"\n                  v-decorator=\"['status',{ rules: [{ required: true, message: '状态' }] }]\">\n                <a-select-option\n                    :value=\"key\"\n                    :key=\"index\"\n                    v-for=\"(value,key,index) in statusMap\">\n                  {{value}}\n                </a-select-option>\n              </a-select>\n            </a-form-item>\n            <a-form-item>\n              <a-button :loading=\"queryLoading\" @click=\"query()\">查询</a-button>\n            </a-form-item>\n          </a-form>\n<!--          <a-button type=\"primary\" @click=\"showModal('新增')\">添加</a-button>-->\n        </a-space>\n        <a-table\n            :columns=\"columns\"\n            :data-source=\"dataSource\"\n            :pagination=\"pagination\"\n            :loading=\"loading\"\n            @change=\"handleTableChange\"\n            :scroll=\"{ x: 1500, y: 300 }\"\n        >\n           <span slot=\"action\" slot-scope=\"text\">\n             <a-button type=\"link\" shape=\"round\" icon=\"edit\" size=\"small\" @click=\"updateItem(text.id,text)\" >编辑</a-button>\n             <a-button type=\"link\" shape=\"round\" icon=\"edit\" size=\"small\" @click=\"showFollowModal(text.id,text)\" >跟进</a-button>\n             <a-button type=\"link\" shape=\"round\" icon=\"edit\" size=\"small\" @click=\"showHandoverModal(text.id)\">移交</a-button>\n             <a-button type=\"link\" shape=\"round\" icon=\"edit\" size=\"small\" @click=\"showStatusModal(text.id)\" >修改状态</a-button>\n           </span>\n        </a-table>\n      </div>\n    </a-card>\n    <!--    新增修改-->\n    <a-modal\n        :title=\"title\"\n        :visible=\"visible\"\n        :confirm-loading=\"confirmLoading\"\n        @ok=\"handleOk\"\n        @cancel=\"handleCancel\"\n        okText=\"提交\"\n    >\n      <a-form :form=\"form\" :layout=\"`horizontal`\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['id',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <div v-for=\"(item) in baseColumns\" :key=\"item.dataIndex\">\n          <a-form-item :label=\"item.title\">\n            <a-select v-if=\"item.dataIndex==='gender'\"\n                      style=\"width: 6rem\"\n                      v-decorator=\"[item.dataIndex,{ rules: [{ required: true, message: item.title }] }]\">\n              <a-select-option :value=\"1\">\n                男\n              </a-select-option>\n              <a-select-option :value=\"0\">\n                女\n              </a-select-option>\n            </a-select>\n            <a-select v-else-if=\"item.dataIndex==='job'\" v-decorator=\"[item.dataIndex,{ rules: [{ required: true, message: item.title }] }]\">\n              <a-select-option v-for=\"(list) in dictionaryDetailsJob\" :key=\"list.id\">\n                {{list.title}}\n              </a-select-option>\n            </a-select>\n            <a-select v-else-if=\"item.dataIndex==='source'\" v-decorator=\"[item.dataIndex,{ rules: [{ required: true, message: item.title }] }]\">\n              <a-select-option v-for=\"(list) in dictionaryDetailsSource\" :key=\"list.id\">\n                {{list.title}}\n              </a-select-option>\n            </a-select>\n            <a-input-number v-else-if=\"item.dataIndex==='age'\" :min=\"0\" :max=\"200\" v-decorator=\"[item.dataIndex, { rules: [{ required: true, message: item.title  }]}]\" />\n            <a-input v-else-if=\"item.dataIndex==='name'\"\n                     v-decorator=\"[item.dataIndex, { rules: [{ required: true,min:1,max:120,message:'输入长度应在1到120之间' }]}]\"\n                     :placeholder=\"`请输入`+item.title\"\n            />\n            <a-input v-else-if=\"item.dataIndex==='tel'\"\n                     v-decorator=\"[item.dataIndex, { rules: [{ required: true,pattern:validators.phoneReg,message:validators.phoneMsg  }]}]\"\n                     :placeholder=\"`请输入`+item.title\"\n            />\n            <a-input v-else-if=\"item.dataIndex==='qq'\"\n                     v-decorator=\"[item.dataIndex, { rules: [{ required: true,pattern:validators.qqReg,message:validators.qqMsg  }]}]\"\n                     :placeholder=\"`请输入`+item.title\"\n            />\n            <a-input v-else\n                     v-decorator=\"[item.dataIndex, { rules: [{ required: true, message: item.title  }]}]\"\n                     :placeholder=\"`请输入`+item.title\"\n            />\n          </a-form-item>\n        </div>\n      </a-form>\n    </a-modal>\n    <!--    修改客户状态-->\n    <a-modal\n        title=\"修改客户状态\"\n        :visible=\"statusVisible\"\n        :confirm-loading=\"statusConfirmLoading\"\n        @ok=\"handleStatusOk\"\n        @cancel=\"handleStatusCancel\"\n        okText=\"保存\"\n    >\n      <a-form :form=\"statusForm\" :layout=\"`horizontal`\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['id',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item lable=\"姓名\">\n          <a-input\n              disabled\n              v-decorator=\"['name', { rules: [{ required: true, message: '名字长度在1到15之间',min:1,max:15  }]}]\"\n              :placeholder=\"`请输入姓名`\"\n          />\n        </a-form-item>\n        <a-form-item label=\"状态\">\n          <a-select\n              v-decorator=\"['status',{ rules: [{ required: true, message: '状态' }] }]\">\n            <a-select-option\n                :value=\"key\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in cmStatusMap\">\n              {{value}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n      </a-form>\n    </a-modal>\n    <!--    移交-->\n    <a-modal\n        title=\"移交\"\n        :visible=\"handoverVisible\"\n        :confirm-loading=\"handoverConfirmLoading\"\n        @ok=\"handleHandoverOk\"\n        @cancel=\"handleHandoverCancel\"\n        okText=\"保存\">\n      <a-form :form=\"handoverForm\" :layout=\"`horizontal`\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['oldseller',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item hidden>\n          <a-input v-decorator=\"['customerid',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item hidden>\n          <a-input v-decorator=\"['transuser',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item lable=\"客户姓名\">\n          <a-input\n              disabled\n              v-decorator=\"['name', { rules: [{ required: true, message: '姓名' }]}]\"\n          />\n        </a-form-item>\n        <a-form-item label=\"旧营销人员\">\n          <a-input\n              disabled\n              v-decorator=\"['oldsellerName',{ rules: [{ required: true, message: '旧营销人员' }] }]\"\n          />\n        </a-form-item>\n        <a-form-item label=\"新营销人员\">\n          <a-select\n              v-decorator=\"['newseller',{ rules: [{ required: true, message: '状态' }] }]\">\n            <a-select-option\n                :value=\"value.id\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in employeeList\">\n              {{value.name}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item label=\"移交原因\">\n          <a-textarea\n              v-decorator=\"['transreason',{ rules: [{ required: true, message: '移交原因的内容长度在10和120之间' ,min:10,max:120 }] }]\"\n              :auto-size=\"{ minRows: 3, maxRows: 5 }\"\n          />\n        </a-form-item>\n      </a-form>\n    </a-modal>\n    <!--    跟进-->\n    <a-modal\n        title=\"跟进记录\"\n        :visible=\"followVisible\"\n        :confirm-loading=\"followConfirmLoading\"\n        @ok=\"handleFollowOk\"\n        @cancel=\"handleFollowCancel\"\n        okText=\"保存\">\n      <a-form :form=\"followForm\" :layout=\"`horizontal`\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['customerid',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item disabled label=\"客户姓名\">\n          <a-input\n              v-decorator=\"['name', { rules: [{ required: true, message: '姓名'  }]}]\"\n              disabled />\n        </a-form-item>\n        <a-form-item disabled label=\"跟进时间\">\n          <a-date-picker\n              show-time\n              v-decorator=\"['tracetime', { rules: [{ required: true, message: '跟进时间'  }]}]\"\n          />\n        </a-form-item><a-form-item disabled label=\"跟进内容\">\n        <a-input\n            v-decorator=\"['tracedetails', { rules: [{ required: true, message: '跟进内容',min:1,max:120,message:'跟进内容长度在1到120之间'  }]}]\"\n        />\n      </a-form-item>\n        <a-form-item disabled label=\"跟进方式\">\n          <a-select\n              v-decorator=\"['tracetype', { rules: [{ required: true, message: '跟进方式'  }]}]\">\n            <a-select-option\n                :value=\"value.id\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in dictionaryDetailsFollow\">\n              {{value.title}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item disabled label=\"跟进结果\">\n          <a-select\n              v-decorator=\"['traceresult', { rules: [{ required: true, message: '跟进结果'  }]}]\">\n            <a-select-option\n                :value=\"key\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in followTraceResult\">\n              {{value}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item disabled label=\"备注\">\n          <a-textarea\n              v-decorator=\"['comment', { rules: [{ required: true, message: '备注' ,min:1,max:120,message:'备注内容在1到120之间'}]}]\"\n              :auto-size=\"{ minRows: 3, maxRows: 5 }\"\n          />\n        </a-form-item>\n        <a-form-item disabled label=\"跟进类型\">\n          <a-select\n              v-decorator=\"['type', { rules: [{ required: true, message: '跟进类型'  }]}]\"\n          >\n            <a-select-option\n                :value=\"key\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in followType\">\n              {{value}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n      </a-form>\n    </a-modal>\n  </div>\n</template>\n\n<script>\nimport * as customerManager from \"@/services/customerManager\"\nimport * as dictionaryDetails from \"@/services/dictionaryDetails\"\nimport * as employee from \"@/services/employee\"\nimport * as customerHandover from \"@/services/customerHandover\"\nimport * as customerFollowUpHistory from \"@/services/customerFollowUpHistory\"\nimport validators from \"@/utils/validators\";\nimport moment from \"moment\";\n\nconst queryAll = \"9999999\"\nconst statusMap = {\n  \"-2\": \"流失\",\n  \"1\": \"正式客户\",\n  [queryAll]:\"全部\"\n}\nconst baseColumns =[\n  {\n    width:120,\n    title: '姓名',\n    dataIndex: 'name',\n    ellipsis: true,\n    fixed: 'left'\n  },\n  {\n    title: '年龄',\n    dataIndex: 'age',\n  },\n  {\n    title: '性别',\n    dataIndex: 'gender',\n    customRender:(text)=>text===1?'男':'女'\n  },\n  {\n    title: '电话',\n    dataIndex: 'tel',\n  },\n  {\n    title: 'QQ',\n    dataIndex: 'qq',\n  },\n  {\n    title: '职业',\n    dataIndex: 'job',\n    ellipsis: true,\n  },\n  {\n    width:60,\n    title: '来源',\n    dataIndex: 'source',\n    ellipsis: true,\n  }\n]\nconst columns = [\n  {\n    width:60,\n    title: '编号',\n    dataIndex: 'id',\n    fixed: 'left'\n  },\n  ...baseColumns,\n  {\n    width: 120,\n    title: '营销人员',\n    dataIndex: 'inputuser'\n  },\n  {\n    title: '状态',\n    width: 140,\n    dataIndex: 'status',\n    customRender:(text)=>customerManager.statusMap[parseInt(text)]\n  },\n  {\n    width:380,\n    title: '操作',\n    scopedSlots: {customRender: 'action'},\n    fixed: 'right'\n  }\n]\nexport default {\n  name: 'Department',\n  data() {\n    return {\n      validators,\n      cmStatusMap:customerManager.statusMap,\n      queryForm:this.$form.createForm(this, {name: 'coordinated'}),\n      queryLoading:false,\n      statusMap,\n      // table\n      columns: columns,\n      dataSource: [],\n      selectedRows: [],\n      pagination: {current:1},\n      loading: false,\n      // 新增修改\n      baseColumns,\n      form:this.$form.createForm(this, {name: 'coordinated'}),\n      confirmLoading:false,\n      title:'新增',\n      visible: false,\n      // 修改客户状态\n      statusForm:this.$form.createForm(this, {name: 'coordinated'}),\n      statusConfirmLoading:false,\n      statusVisible:false,\n      // 移交\n      handoverForm:this.$form.createForm(this, {name: 'coordinated'}),\n      handoverVisible :false,\n      handoverConfirmLoading :false,\n      // 跟进\n      followForm:this.$form.createForm(this, {name: 'coordinated'}),\n      followVisible :false,\n      followConfirmLoading :false,\n      followType:customerFollowUpHistory.type,\n      followTraceResult:customerFollowUpHistory.traceresult,\n      // 字典细节\n      dictionaryDetailsJob:[],\n      dictionaryDetailsSource:[],\n      dictionaryDetailsFollow:[],\n      // 员工表\n      employeeList:[]\n    }\n  },\n  mounted() {\n    this.queryForm.setFieldsValue({\"status\":\"1\"})\n    this.query()\n    // id 1 职业\n    dictionaryDetails.list({page:1,size:999999,id:1}).then(({data})=>{\n      this.dictionaryDetailsJob = data.data.list\n    });\n    // id 2 来源\n    dictionaryDetails.list({page:1,size:999999,id:2}).then(({data})=>{\n      this.dictionaryDetailsSource = data.data.list\n    })\n    // id 10 跟进方式\n    dictionaryDetails.list({page:1,size:999999,id:10}).then(({data})=>{\n      this.dictionaryDetailsFollow = data.data.list\n    })\n\n    employee.list({page:1,size:99999}).then(({data})=>{\n      this.employeeList = data.data.list\n    })\n  },\n  methods: {\n    query(){\n      this.queryLoading = true\n      this.queryForm.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.queryLoading = false\n          return;\n        }\n        if(values.status===queryAll){\n          delete values.status\n        }\n        this.fetch({\"page\": this.pagination.current, \"size\": 10,...values})\n      })\n    },\n    // table\n    handleTableChange(pagination) {\n      const pager = {...this.pagination};\n      pager.current = pagination.current;\n      this.pagination = pager;\n      this.fetch({\n        size: pagination.pageSize,\n        page: pagination.current,\n      });\n    },\n    async fetch(params = {\"page\": 1, \"size\": 10}) {\n      this.loading = true\n      let {data} = await customerManager.list(params || {\"page\": 1, \"size\": 10})\n      const res = data.data\n      const pagination = {...this.pagination};\n      pagination.total = res.total\n      pagination.current = params.page\n      this.dataSource = res.list.map((e, i) => ({key: i + \"\", ...e}))\n      this.pagination = pagination\n      this.loading = false\n      this.queryLoading=false\n      return data\n    },\n    deleteItem(text) {\n      const title = '删除'\n      customerManager.deleteItem(text.id).then(({data}) => {\n        if (data.code !== 200) {\n          this.$notification['error']({\n            message: title + '客户信息出现错误',\n            description: '建议检查网络连接或重新登陆',\n          });\n        }\n        this.$notification.success({\n          message: title + '成功',\n          description: title + '客户信息成功',\n        });\n        this.fetch({\"page\": this.pagination.current, \"size\": 10})\n      })\n    },\n    // 客户状态\n    handleStatusOk(){\n      this.statusConfirmLoading = true;\n      this.statusForm.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.statusConfirmLoading = false\n          return;\n        }\n        values.status = parseInt(values.status)\n        customerManager['update'](values).then(({data}) => {\n          this.statusConfirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: this.title + '角色信息出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else {\n            this.$notification.success({\n              message: this.title + '成功',\n              description: this.title + '角色信息成功',\n            });\n          }\n          this.statusVisible = false\n          this.query()\n        })\n      });\n    },\n    handleStatusCancel(){\n      this.statusVisible = false;\n      this.statusConfirmLoading = false\n      this.statusForm.resetFields()\n    },\n    async showStatusModal(id){\n      this.statusVisible = true;\n      this.statusConfirmLoading = false\n      await this.statusForm.resetFields()\n      const {data} = await customerManager.getDetail(id)\n      if(!data.data) return;\n      // 这里不能循环\n      this.statusForm.setFieldsValue({id:data.data[\"id\"]})\n      this.statusForm.setFieldsValue({name:data.data[\"name\"]})\n      this.statusForm.setFieldsValue({status:data.data[\"status\"]+\"\"})\n    },\n    // 移交\n    handleHandoverCancel(){\n      this.handoverVisible = false;\n      this.handoverConfirmLoading = false\n      this.handoverForm.resetFields()\n    },\n    handleHandoverOk(){\n      this.handoverConfirmLoading = true;\n      this.handoverForm.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.handoverConfirmLoading = false\n          return;\n        }\n        customerHandover['add'](values).then(({data}) => {\n          this.handoverConfirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: this.title + '角色信息出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else{\n            this.$notification.success({\n              message: this.title + '成功',\n              description: this.title + '角色信息成功',\n            });\n          }\n          this.handoverVisible = false\n          this.query()\n        })\n      });\n    },\n    async showHandoverModal(id){\n      this.handoverVisible = true;\n      this.handoverConfirmLoading = false\n      await this.handoverForm.resetFields()\n      const {data} = await customerManager.getDetail(id)\n      if(!data.data) return;\n      // 这里不能循环\n      this.handoverForm.setFieldsValue({oldsellerName:this.employeeList.find(e=>e.id===data.data[\"inputuser\"]).name})\n      this.handoverForm.setFieldsValue({oldseller:data.data[\"inputuser\"]})\n      this.handoverForm.setFieldsValue({customerid:data.data[\"id\"]})\n      this.handoverForm.setFieldsValue({name:data.data[\"name\"]})\n    },\n    // 移交\n    handleFollowCancel(){\n      this.followVisible = false;\n      this.followConfirmLoading = false\n      this.followForm.resetFields()\n    },\n    handleFollowOk(){\n      this.followConfirmLoading = true;\n      this.followForm.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.followConfirmLoading = false\n          return;\n        }\n        values.tracetime = values.tracetime.toDate()\n\n        customerFollowUpHistory['add'](values).then(({data}) => {\n          this.followConfirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: this.title + '跟进记录出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else{\n            this.$notification.success({\n              message: this.title + '成功',\n              description: this.title + '跟进记录成功',\n            });\n          }\n          this.followVisible = false\n          this.query()\n        })\n      });\n    },\n    async showFollowModal(id,line){\n      this.followVisible = true;\n      this.followConfirmLoading = false\n      await this.followForm.resetFields()\n      // 这里不能循环\n      this.$nextTick(()=>{\n        this.followForm.setFieldsValue({customerid:line.id})\n        this.followForm.setFieldsValue({name:line.name})\n        this.followForm.setFieldsValue({tracetime:new moment(new Date())})\n        this.followForm.setFieldsValue({tracetype:this.dictionaryDetailsFollow[0].id})\n        this.followForm.setFieldsValue({type:Object.keys(this.followType)[0]})\n        this.followForm.setFieldsValue({traceresult:Object.keys(this.followTraceResult)[0]})\n      })\n    },\n    // modal\n    async showModal(title) {\n      this.visible = true;\n      this.title = title || '新增'\n      await this.form.resetFields()\n    },\n    async updateItem(id,line) {\n      this.confirmLoading=false\n      await this.showModal('更改')\n      customerManager.getDetail(id).then(({data}) => {\n        if(!data.data) return;\n        // 这里不能循环\n        this.form.setFieldsValue({id:data.data[\"id\"]})\n        this.form.setFieldsValue({name:data.data[\"name\"]})\n        this.form.setFieldsValue({age:line[\"age\"]})\n        this.form.setFieldsValue({gender:data.data[\"gender\"]})\n        this.form.setFieldsValue({tel:data.data[\"tel\"]})\n        this.form.setFieldsValue({qq:data.data[\"qq\"]})\n        this.form.setFieldsValue({job:data.data[\"job\"]})\n        this.form.setFieldsValue({source:data.data[\"source\"]})\n      })\n    },\n    handleOk() {\n      this.confirmLoading = true;\n      this.form.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.confirmLoading = true;\n          return;\n        }\n        let method = 'add';\n        if (values.id) method = 'update';\n\n        customerManager[method](values).then(({data}) => {\n          this.confirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: this.title + '角色信息出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else{\n            this.$notification.success({\n              message: this.title + '成功',\n              description: this.title + '角色信息成功',\n            });\n          }\n          this.visible = false\n          this.query()\n        })\n      });\n    },\n    handleCancel() {\n      this.visible = false;\n      this.title = ''\n      this.confirmLoading = false\n      this.form.resetFields()\n    },\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n.search {\n  margin-bottom: 54px;\n}\n\n.fold {\n  width: calc(100% - 216px);\n  display: inline-block\n}\n\n.operator {\n  margin-bottom: 18px;\n}\n\n@media screen and (max-width: 900px) {\n  .fold {\n    width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/pages/customer/resource.vue",
    "content": "<template>\n  <div>\n    <a-card>\n      <div>\n        <a-space class=\"operator\">\n          <a-form layout=\"inline\" :form=\"queryForm\">\n            <a-form-item label=\"关键字\">\n              <a-input\n                  v-decorator=\"['keyword', { rules: [{ required: false,min:1,max:120,message:'输入长度应在1到120之间'}] }]\"\n                  placeholder=\"请输入姓名/电话\"\n              />\n            </a-form-item>\n            <a-form-item>\n              <a-button :loading=\"queryLoading\" @click=\"query()\">查询</a-button>\n            </a-form-item>\n          </a-form>\n          <!--          <a-button type=\"primary\" @click=\"showModal('新增')\">添加</a-button>-->\n        </a-space>\n        <a-table\n            :columns=\"columns\"\n            :data-source=\"dataSource\"\n            :pagination=\"pagination\"\n            :loading=\"loading\"\n            @change=\"handleTableChange\"\n            :scroll=\"{ x: 1500, y: 300 }\"\n        >\n           <span slot=\"action\" slot-scope=\"text\">\n<!--             <a-button type=\"link\" shape=\"round\" icon=\"edit\" size=\"small\" @click=\"updateItem(text.id)\" >编辑</a-button>-->\n<!--             <a-button type=\"link\" shape=\"round\" icon=\"edit\" size=\"small\" @click=\"showFollowModal(text.id,text)\" >跟进</a-button>-->\n             <a-button type=\"link\" shape=\"round\" icon=\"edit\" size=\"small\" @click=\"showHandoverModal(text.id,text,true)\">移交给我</a-button>\n             <a-button type=\"link\" shape=\"round\" icon=\"edit\" size=\"small\" @click=\"showHandoverModal(text.id,text)\">移交</a-button>\n<!--             <a-button type=\"link\" shape=\"round\" icon=\"edit\" size=\"small\" @click=\"showStatusModal(text.id)\" >修改状态</a-button>-->\n           </span>\n        </a-table>\n      </div>\n    </a-card>\n    <!--    新增修改-->\n    <a-modal\n        :title=\"title\"\n        :visible=\"visible\"\n        :confirm-loading=\"confirmLoading\"\n        @ok=\"handleOk\"\n        @cancel=\"handleCancel\"\n        okText=\"提交\"\n    >\n      <a-form :form=\"form\" :layout=\"`horizontal`\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['id',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <div v-for=\"(item) in baseColumns\" :key=\"item.dataIndex\">\n          <a-form-item :label=\"item.title\">\n            <a-select v-if=\"item.dataIndex==='gender'\"\n                      style=\"width: 6rem\"\n                      v-decorator=\"[item.dataIndex,{ rules: [{ required: true, message: item.title }] }]\">\n              <a-select-option :value=\"1\">\n                男\n              </a-select-option>\n              <a-select-option :value=\"0\">\n                女\n              </a-select-option>\n            </a-select>\n            <a-select v-else-if=\"item.dataIndex==='job'\" v-decorator=\"[item.dataIndex,{ rules: [{ required: true, message: item.title }] }]\">\n              <a-select-option v-for=\"(list) in dictionaryDetailsJob\" :key=\"list.id\">\n                {{list.title}}\n              </a-select-option>\n            </a-select>\n            <a-select v-else-if=\"item.dataIndex==='source'\" v-decorator=\"[item.dataIndex,{ rules: [{ required: true, message: item.title }] }]\">\n              <a-select-option v-for=\"(list) in dictionaryDetailsSource\" :key=\"list.id\">\n                {{list.title}}\n              </a-select-option>\n            </a-select>\n            <a-input v-else\n                     v-decorator=\"[item.dataIndex, { rules: [{ required: true, message: item.title  }]}]\"\n                     :placeholder=\"`请输入`+item.title\"\n            />\n          </a-form-item>\n        </div>\n      </a-form>\n    </a-modal>\n    <!--    修改客户状态-->\n    <a-modal\n        title=\"修改客户状态\"\n        :visible=\"statusVisible\"\n        :confirm-loading=\"statusConfirmLoading\"\n        @ok=\"handleStatusOk\"\n        @cancel=\"handleStatusCancel\"\n        okText=\"保存\"\n    >\n      <a-form :form=\"statusForm\" :layout=\"`horizontal`\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['id',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item lable=\"姓名\">\n          <a-input\n              v-decorator=\"['name', { rules: [{ required: true, message: '姓名'  }]}]\"\n              :placeholder=\"`请输入姓名`\"\n          />\n        </a-form-item>\n        <a-form-item label=\"状态\">\n          <a-select\n              v-decorator=\"['status',{ rules: [{ required: true, message: '状态' }] }]\">\n            <a-select-option\n                :value=\"key\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in statusMap\">\n              {{value}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n      </a-form>\n    </a-modal>\n    <!--    移交-->\n    <a-modal\n        :title=\"handoverForMe?`移交给我`:`移交`\"\n        :visible=\"handoverVisible\"\n        :confirm-loading=\"handoverConfirmLoading\"\n        @ok=\"handleHandoverOk\"\n        @cancel=\"handleHandoverCancel\"\n        okText=\"保存\">\n      <a-form :form=\"handoverForm\" :layout=\"`horizontal`\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['oldseller',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item hidden>\n          <a-input v-decorator=\"['customerid',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item hidden>\n          <a-input v-decorator=\"['transuser',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item lable=\"客户姓名\">\n          <a-input\n              disabled\n              v-decorator=\"['name', { rules: [{ required: true, message: '姓名',min:1,max:15,message:'姓名在1到15之间'  }]}]\"\n          />\n        </a-form-item>\n        <a-form-item label=\"旧营销人员\">\n          <a-input\n              disabled\n              v-decorator=\"['oldsellerName',{ rules: [{ required: true, message: '旧营销人员' }] }]\"\n          />\n        </a-form-item>\n        <a-form-item label=\"新营销人员\">\n          <a-select\n              :disabled=\"handoverForMe\"\n              v-decorator=\"['newseller',{ rules: [{ required: true, message: '状态' }] }]\">\n            <a-select-option\n                :value=\"value.id\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in employeeList\">\n              {{value.name}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item label=\"移交原因\">\n          <a-textarea\n              v-decorator=\"['transreason',{ rules: [{ required: true, message: '移交原因',min:1,max:120,message:'移交原因在1到120之间' }] }]\"\n              :auto-size=\"{ minRows: 3, maxRows: 5 }\"\n          />\n        </a-form-item>\n      </a-form>\n    </a-modal>\n    <!--    跟进-->\n    <a-modal\n        title=\"跟进记录\"\n        :visible=\"followVisible\"\n        :confirm-loading=\"followConfirmLoading\"\n        @ok=\"handleFollowOk\"\n        @cancel=\"handleFollowCancel\"\n        okText=\"保存\">\n      <a-form :form=\"followForm\" :layout=\"`horizontal`\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['customerid',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item disabled label=\"客户姓名\">\n          <a-input\n              v-decorator=\"['name', { rules: [{ required: true, message: '姓名'  }]}]\"\n              disabled />\n        </a-form-item>\n        <a-form-item disabled label=\"跟进时间\">\n          <a-date-picker\n              show-time\n              v-decorator=\"['tracetime', { rules: [{ required: true, message: '跟进时间'  }]}]\"\n          />\n        </a-form-item><a-form-item disabled label=\"跟进内容\">\n        <a-input\n            v-decorator=\"['tracedetails', { rules: [{ required: true, message: '跟进内容'  }]}]\"\n        />\n      </a-form-item>\n        <a-form-item disabled label=\"跟进方式\">\n          <a-select\n              v-decorator=\"['tracetype', { rules: [{ required: true, message: '跟进方式'  }]}]\">\n            <a-select-option\n                :value=\"value.id\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in dictionaryDetailsFollow\">\n              {{value.title}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item disabled label=\"跟进结果\">\n          <a-select\n              v-decorator=\"['traceresult', { rules: [{ required: true, message: '跟进结果'  }]}]\">\n            <a-select-option\n                :value=\"key\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in followTraceResult\">\n              {{value}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item disabled label=\"备注\">\n          <a-textarea\n              v-decorator=\"['comment', { rules: [{ required: true, message: '备注'  }]}]\"\n              :auto-size=\"{ minRows: 3, maxRows: 5 }\"\n          />\n        </a-form-item>\n        <a-form-item disabled label=\"跟进类型\">\n          <a-select\n              v-decorator=\"['type', { rules: [{ required: true, message: '跟进类型'  }]}]\"\n          >\n            <a-select-option\n                :value=\"key\"\n                :key=\"index\"\n                v-for=\"(value,key,index) in followType\">\n              {{value}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n      </a-form>\n    </a-modal>\n  </div>\n</template>\n\n<script>\nimport * as customerManager from \"@/services/customerManager\"\nimport * as dictionaryDetails from \"@/services/dictionaryDetails\"\nimport * as employee from \"@/services/employee\"\nimport * as customerHandover from \"@/services/customerHandover\"\nimport * as customerFollowUpHistory from \"@/services/customerFollowUpHistory\"\nimport validators from \"@/utils/validators\";\nimport moment from \"moment\";\n\nconst statusMap = {\n  \"2\": \"资源池客户\",\n}\nconst baseColumns =[\n  {\n    width:120,\n    title: '姓名',\n    dataIndex: 'name',\n    ellipsis: true,\n    fixed: 'left'\n  },\n  {\n    title: '年龄',\n    dataIndex: 'age',\n  },\n  {\n    title: '性别',\n    dataIndex: 'gender',\n    customRender:(text)=>text===1?'男':'女'\n  },\n  {\n    title: '电话',\n    dataIndex: 'tel',\n  },\n  {\n    title: 'QQ',\n    dataIndex: 'qq',\n  },\n  {\n    title: '职业',\n    dataIndex: 'job',\n    ellipsis: true,\n  },\n  {\n    width:60,\n    title: '来源',\n    dataIndex: 'source',\n    ellipsis: true,\n  }\n]\nconst columns = [\n  {\n    width:60,\n    title: '编号',\n    dataIndex: 'id',\n    fixed: 'left'\n  },\n  ...baseColumns,\n  {\n    title: '营销人员',\n    dataIndex: 'inputuser'\n  },\n  {\n    title: '状态',\n    dataIndex: 'status',\n    customRender:(text)=>statusMap[parseInt(text)]\n  },\n  {\n    width:220,\n    title: '操作',\n    scopedSlots: {customRender: 'action'},\n    fixed: 'right'\n  }\n]\nexport default {\n  name: 'Department',\n  data() {\n    return {\n      validators,\n      queryForm:this.$form.createForm(this, {name: 'coordinated'}),\n      queryLoading:false,\n      statusMap,\n      // table\n      columns: columns,\n      dataSource: [],\n      selectedRows: [],\n      pagination: {current:1},\n      loading: false,\n      // 新增修改\n      baseColumns,\n      form:this.$form.createForm(this, {name: 'coordinated'}),\n      confirmLoading:false,\n      title:'新增',\n      visible: false,\n      // 修改客户状态\n      statusForm:this.$form.createForm(this, {name: 'coordinated'}),\n      statusConfirmLoading:false,\n      statusVisible:false,\n      // 移交\n      handoverForMe:false,\n      handoverForm:this.$form.createForm(this, {name: 'coordinated'}),\n      handoverVisible :false,\n      handoverConfirmLoading :false,\n      // 跟进\n      followForm:this.$form.createForm(this, {name: 'coordinated'}),\n      followVisible :false,\n      followConfirmLoading :false,\n      followType:customerFollowUpHistory.type,\n      followTraceResult:customerFollowUpHistory.traceresult,\n      // 字典细节\n      dictionaryDetailsJob:[],\n      dictionaryDetailsSource:[],\n      dictionaryDetailsFollow:[],\n      // 员工表\n      employeeList:[]\n    }\n  },\n  mounted() {\n    this.query()\n    // id 1 职业\n    dictionaryDetails.list({page:1,size:999999,id:1}).then(({data})=>{\n      this.dictionaryDetailsJob = data.data.list\n    });\n    // id 2 来源\n    dictionaryDetails.list({page:1,size:999999,id:2}).then(({data})=>{\n      this.dictionaryDetailsSource = data.data.list\n    })\n    // id 10 跟进方式\n    dictionaryDetails.list({page:1,size:999999,id:10}).then(({data})=>{\n      this.dictionaryDetailsFollow = data.data.list\n    })\n\n    employee.list({page:1,size:99999}).then(({data})=>{\n      this.employeeList = data.data.list\n    })\n  },\n  methods: {\n    query(){\n      this.queryLoading = true\n      this.queryForm.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.queryLoading = false\n          return;\n        }\n        this.fetch({\"page\": this.pagination.current, \"size\": 10,status:2,...values})\n      })\n    },\n    // table\n    handleTableChange(pagination) {\n      const pager = {...this.pagination};\n      pager.current = pagination.current;\n      this.pagination = pager;\n      this.fetch({\n        size: pagination.pageSize,\n        page: pagination.current,\n      });\n    },\n    async fetch(params = {\"page\": 1, \"size\": 10}) {\n      this.loading = true\n      let {data} = await customerManager.list(params || {\"page\": 1, \"size\": 10})\n      const res = data.data\n      const pagination = {...this.pagination};\n      pagination.total = res.total\n      pagination.current = params.page\n      this.dataSource = res.list.map((e, i) => ({key: i + \"\", ...e}))\n      this.pagination = pagination\n      this.loading = false\n      this.queryLoading=false\n      return data\n    },\n    deleteItem(text) {\n      const title = '删除'\n      customerManager.deleteItem(text.id).then(({data}) => {\n        if (data.code !== 200) {\n          this.$notification['error']({\n            message: title + '客户信息出现错误',\n            description: '建议检查网络连接或重新登陆',\n          });\n        }\n        this.$notification.success({\n          message: title + '成功',\n          description: title + '客户信息成功',\n        });\n        this.fetch({\"page\": this.pagination.current, \"size\": 10})\n      })\n    },\n    // 客户状态\n    handleStatusOk(){\n      this.statusConfirmLoading = true;\n      this.statusForm.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.statusConfirmLoading = false\n          return;\n        }\n        values.status = parseInt(values.status)\n        customerManager['update'](values).then(({data}) => {\n          this.statusConfirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: this.title + '角色信息出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else {\n            this.$notification.success({\n              message: this.title + '成功',\n              description: this.title + '角色信息成功',\n            });\n          }\n          this.statusVisible = false\n          this.query()\n        })\n      });\n    },\n    handleStatusCancel(){\n      this.statusVisible = false;\n      this.statusConfirmLoading = false\n      this.statusForm.resetFields()\n    },\n    async showStatusModal(id){\n      this.statusVisible = true;\n      this.statusConfirmLoading = false\n      await this.statusForm.resetFields()\n      const {data} = await customerManager.getDetail(id)\n      if(!data.data) return;\n      // 这里不能循环\n      this.statusForm.setFieldsValue({id:data.data[\"id\"]})\n      this.statusForm.setFieldsValue({name:data.data[\"name\"]})\n      this.statusForm.setFieldsValue({status:data.data[\"status\"]+\"\"})\n    },\n    // 移交\n    handleHandoverCancel(){\n      this.handoverVisible = false;\n      this.handoverConfirmLoading = false\n      this.handoverForm.resetFields()\n    },\n    handleHandoverOk(){\n      this.handoverConfirmLoading = true;\n      this.handoverForm.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.handoverConfirmLoading = false\n          return;\n        }\n        customerHandover['add'](values).then(({data}) => {\n          this.handoverConfirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: this.title + '角色信息出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else{\n            this.$notification.success({\n              message: this.title + '成功',\n              description: this.title + '角色信息成功',\n            });\n          }\n          this.handoverVisible = false\n          this.query()\n        })\n      });\n    },\n    async showHandoverModal(id,line,handoverForMe){\n      this.handoverVisible = true;\n      this.handoverConfirmLoading = false\n      await this.handoverForm.resetFields()\n      const {data} = await customerManager.getDetail(id)\n      this.handoverForMe = handoverForMe === true;\n      if(!data.data) return;\n      // 这里不能循环\n      this.handoverForm.setFieldsValue({oldsellerName:this.employeeList.find(e=>e.id===data.data[\"inputuser\"]).name})\n      this.handoverForm.setFieldsValue({oldseller:data.data[\"inputuser\"]})\n      let name = JSON.parse(localStorage.getItem(\"admin.roles\"))[0][\"id\"]\n      if(Object.prototype.toString.call(name)!==\"[object Undefined]\"){\n        this.handoverForm.setFieldsValue({newseller:name})\n      }\n      this.handoverForm.setFieldsValue({customerid:data.data[\"id\"]})\n      this.handoverForm.setFieldsValue({name:line[\"name\"]})\n\n    },\n    // 移交\n    handleFollowCancel(){\n      this.followVisible = false;\n      this.followConfirmLoading = false\n      this.followForm.resetFields()\n    },\n    handleFollowOk(){\n      this.followConfirmLoading = true;\n      this.followForm.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.followConfirmLoading = false\n          return;\n        }\n        values.tracetime = values.tracetime.toDate()\n\n        customerFollowUpHistory['add'](values).then(({data}) => {\n          this.followConfirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: this.title + '跟进记录出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else{\n            this.$notification.success({\n              message: this.title + '成功',\n              description: this.title + '跟进记录成功',\n            });\n          }\n          this.followVisible = false\n          this.handoverForMe = false\n          this.query()\n        })\n      });\n    },\n    async showFollowModal(id,line){\n      this.followVisible = true;\n      this.followConfirmLoading = false\n      await this.followForm.resetFields()\n      // 这里不能循环\n      this.$nextTick(()=>{\n        this.followForm.setFieldsValue({customerid:line.id})\n        this.followForm.setFieldsValue({name:line.name})\n        this.followForm.setFieldsValue({tracetime:new moment(new Date())})\n        this.followForm.setFieldsValue({tracetype:this.dictionaryDetailsFollow[0].id})\n        this.followForm.setFieldsValue({type:Object.keys(this.followType)[0]})\n        this.followForm.setFieldsValue({traceresult:Object.keys(this.followTraceResult)[0]})\n      })\n    },\n    // modal\n    async showModal(title) {\n      this.visible = true;\n      this.title = title || '新增'\n      await this.form.resetFields()\n    },\n    async updateItem(id) {\n      this.confirmLoading=false\n      await this.showModal('更改')\n      customerManager.getDetail(id).then(({data}) => {\n        if(!data.data) return;\n        // 这里不能循环\n        this.form.setFieldsValue({id:data.data[\"id\"]})\n        this.form.setFieldsValue({name:data.data[\"name\"]})\n        this.form.setFieldsValue({age:data.data[\"age\"]})\n        this.form.setFieldsValue({gender:data.data[\"gender\"]})\n        this.form.setFieldsValue({tel:data.data[\"tel\"]})\n        this.form.setFieldsValue({qq:data.data[\"qq\"]})\n        this.form.setFieldsValue({job:data.data[\"job\"]})\n        this.form.setFieldsValue({source:data.data[\"source\"]})\n      })\n    },\n    handleOk() {\n      this.confirmLoading = true;\n      this.form.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.confirmLoading = true;\n          return;\n        }\n        let method = 'add';\n        if (values.id) method = 'update';\n\n        customerManager[method](values).then(({data}) => {\n          this.confirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: this.title + '角色信息出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else{\n            this.$notification.success({\n              message: this.title + '成功',\n              description: this.title + '角色信息成功',\n            });\n          }\n          this.visible = false\n          this.query()\n        })\n      });\n    },\n    handleCancel() {\n      this.visible = false;\n      this.title = ''\n      this.confirmLoading = false\n      this.form.resetFields()\n    },\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n.search {\n  margin-bottom: 54px;\n}\n\n.fold {\n  width: calc(100% - 216px);\n  display: inline-block\n}\n\n.operator {\n  margin-bottom: 18px;\n}\n\n@media screen and (max-width: 900px) {\n  .fold {\n    width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/pages/dashboard/workplace/WorkPlace.vue",
    "content": "<template>\n  <page-layout :avatar=\"currUser.avatar\">\n    <div slot=\"headerContent\">\n      <div class=\"title\">{{welcome.timeFix[lang]}}，{{currUser.name}}，{{welcome.message[lang]}}</div>\n      <div>{{currUser.position[lang]}}</div>\n    </div>\n    <template slot=\"extra\">\n      <head-info class=\"split-right\" :title=\"$t('project')\" content=\"56\"/>\n      <head-info class=\"split-right\" :title=\"$t('ranking')\" content=\"8/24\"/>\n      <head-info class=\"split-right\" :title=\"$t('visit')\" content=\"2,223\"/>\n    </template>\n    <template>\n      <a-row style=\"margin: 0 -12px\">\n        <a-col style=\"padding: 0 12px\" :xl=\"16\" :lg=\"24\" :md=\"24\" :sm=\"24\" :xs=\"24\">\n          <a-card class=\"project-list\" :loading=\"loading\" style=\"margin-bottom: 24px;\" :bordered=\"false\" :title=\"$t('progress')\" :body-style=\"{padding: 0}\">\n            <a slot=\"extra\">{{$t('all')}}</a>\n            <div>\n              <a-card-grid :key=\"i\" v-for=\"(item, i) in projects\">\n                <a-card :bordered=\"false\" :body-style=\"{padding: 0}\">\n                  <a-card-meta :description=\"item.desc\">\n                    <div slot=\"title\" class=\"card-title\">\n                      <a-avatar size=\"small\" :src=\"item.logo\" />\n                      <span>Alipay</span>\n                    </div>\n                  </a-card-meta>\n                  <div class=\"project-item\">\n                    <a class=\"group\" href=\"/#/\">科学搬砖组</a>\n                    <span class=\"datetime\">9小时前</span>\n                  </div>\n                </a-card>\n              </a-card-grid>\n            </div>\n          </a-card>\n          <a-card :loading=\"loading\" :title=\"$t('dynamic')\" :bordered=\"false\">\n            <a-list>\n              <a-list-item :key=\"index\" v-for=\"(item, index) in activities\">\n                <a-list-item-meta>\n                  <a-avatar slot=\"avatar\" :src=\"item.user.avatar\" />\n                  <div slot=\"title\" v-html=\"item.template\" />\n                  <div slot=\"description\">9小时前</div>\n                </a-list-item-meta>\n              </a-list-item>\n            </a-list>\n          </a-card>\n        </a-col>\n        <a-col style=\"padding: 0 12px\" :xl=\"8\" :lg=\"24\" :md=\"24\" :sm=\"24\" :xs=\"24\">\n          <a-card :title=\"$t('access')\" style=\"margin-bottom: 24px\" :bordered=\"false\" :body-style=\"{padding: 0}\">\n            <div class=\"item-group\">\n              <a>操作一</a>\n              <a>操作二</a>\n              <a>操作三</a>\n              <a>操作四</a>\n              <a>操作五</a>\n              <a>操作六</a>\n              <a-button size=\"small\" type=\"primary\" ghost icon=\"plus\">{{$t('add')}}</a-button>\n            </div>\n          </a-card>\n          <a-card :loading=\"loading\" :title=\"`XX ${$t('degree')}`\" style=\"margin-bottom: 24px\" :bordered=\"false\" :body-style=\"{padding: 0}\">\n            <div style=\"min-height: 400px;\">\n              <radar />\n            </div>\n          </a-card>\n          <a-card :loading=\"loading\" :title=\"$t('team')\" :bordered=\"false\">\n            <div class=\"members\">\n              <a-row>\n                <a-col :span=\"12\" v-for=\"(item, index) in teams\" :key=\"index\">\n                  <a>\n                    <a-avatar size=\"small\" :src=\"item.avatar\" />\n                    <span class=\"member\">{{item.name}}</span>\n                  </a>\n                </a-col>\n              </a-row>\n            </div>\n          </a-card>\n        </a-col>\n      </a-row>\n    </template>\n  </page-layout>\n</template>\n\n<script>\nimport PageLayout from '@/layouts/PageLayout'\nimport HeadInfo from '@/components/tool/HeadInfo'\nimport Radar from '@/components/chart/Radar'\nimport {mapState} from 'vuex'\nimport {request, METHOD} from '@/utils/request'\n\nexport default {\n  name: 'WorkPlace',\n  components: {Radar, HeadInfo, PageLayout},\n  i18n: require('./i18n'),\n  data () {\n    return {\n      projects: [],\n      loading: true,\n      activities: [],\n      teams: [],\n      welcome: {\n        timeFix: '',\n        message: ''\n      }\n    }\n  },\n  computed: {\n    ...mapState('account', {currUser: 'user'}),\n    ...mapState('setting', ['lang'])\n  },\n  created() {\n    request('/user/welcome', METHOD.GET).then(res => this.welcome = res.data)\n    request('/work/activity', METHOD.GET).then(res => this.activities = res.data)\n    request('/work/team', METHOD.GET).then(res => this.teams = res.data)\n    request('/project', METHOD.GET).then(res => {\n        this.projects = res.data\n        this.loading = false\n      })\n  }\n}\n</script>\n\n<style lang=\"less\">\n@import \"index\";\n</style>\n"
  },
  {
    "path": "front/src/pages/dashboard/workplace/i18n.js",
    "content": "module.exports = {\n  messages: {\n    CN: {\n      project: '项目数',\n      ranking: '团队排名',\n      visit: '项目访问',\n      progress: '进行中的项目',\n      all: '全部项目',\n      access: '快速开始/便捷导航',\n      dynamic: '动态',\n      degree: '指数',\n      team: '团队',\n      add: '添加'\n    },\n    HK: {\n      project: '項目數',\n      ranking: '團隊排名',\n      visit: '項目訪問',\n      progress: '進行中的項目',\n      all: '全部項目',\n      access: '快速開始/便捷導航',\n      dynamic: '動態',\n      degree: '指數',\n      team: '團隊',\n      add: '添加'\n    },\n    US: {\n      project: 'Project',\n      ranking: 'Ranking',\n      visit: 'Visit',\n      progress: 'Projects in progress',\n      all: 'All projects',\n      access: 'Quick start / Easy navigation',\n      dynamic: 'Dynamic',\n      degree: 'degree',\n      team: 'Team',\n      add: 'Add'\n    },\n  }\n}\n"
  },
  {
    "path": "front/src/pages/dashboard/workplace/index.js",
    "content": "import WorkPlace from './WorkPlace'\nexport default WorkPlace\n"
  },
  {
    "path": "front/src/pages/dashboard/workplace/index.less",
    "content": ".project-list {\n  .card-title {\n    span{\n      vertical-align: middle;\n      &:last-child{\n        margin-left: 12px;\n      }\n    }\n  }\n  .project-item {\n    display: flex;\n    justify-content: space-between;\n    margin-top: 8px;\n    overflow: hidden;\n    font-size: 12px;\n    color: inherit;\n    .group{\n      color: @text-color;\n      flex: 1 1 0;\n      &:hover {\n        color: @primary-color;\n      }\n    }\n    .datetime {\n      color: @text-color-second;\n      flex: 0 0 auto;\n    }\n  }\n  .ant-card-meta-description {\n    height: 44px;\n    line-height: 22px;\n    overflow: hidden;\n  }\n}\n.item-group{\n  padding: 20px 0 8px 24px;\n  font-size: 0;\n  a{\n    color: inherit;\n    display: inline-block;\n    font-size: 14px;\n    margin-bottom: 13px;\n    width: 25%;\n  }\n}\n.members {\n  a {\n    display: block;\n    margin: 12px 0;\n    color: @text-color;\n    &:hover {\n      color: @primary-color;\n    }\n    .member {\n      vertical-align: middle;\n      margin-left: 12px;\n    }\n  }\n}\n"
  },
  {
    "path": "front/src/pages/department/index.vue",
    "content": "<template>\n  <div>\n    <a-card>\n      <div>\n        <a-space class=\"operator\">\n          <a-button type=\"primary\" @click=\"showModal('新增')\">新增</a-button>\n        </a-space>\n        <a-table\n            :columns=\"columns\"\n            :data-source=\"dataSource\"\n            :pagination=\"pagination\"\n            :loading=\"loading\"\n            @change=\"handleTableChange\"\n        >\n           <span slot=\"action\" slot-scope=\"text\">\n              <a @click=\"updateItem(text.id)\">编辑</a> |\n               <a-popconfirm\n                   title=\"您真的要删除这行数据么？\"\n                   ok-text=\"是\"\n                   cancel-text=\"否\"\n                   @confirm=\"deleteItem(text)\"\n               >\n              <a>删除</a>\n              </a-popconfirm>\n           </span>\n        </a-table>\n      </div>\n    </a-card>\n    <a-modal\n        :title=\"title\"\n        :visible=\"visible\"\n        :confirm-loading=\"confirmLoading\"\n        @ok=\"handleOk\"\n        @cancel=\"handleCancel\"\n        okText=\"提交\"\n    >\n      <a-form :form=\"form\" :label-col=\"{ span: 5 }\" :wrapper-col=\"{ span: 12 }\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['id',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item label=\"部门名称\">\n          <a-input\n              v-decorator=\"['name', { rules: [{ required: true, min:1,max:15,message:'部门名称长度在1到15之间' }] }]\"\n          />\n        </a-form-item>\n        <a-form-item label=\"部门编号\">\n          <a-input\n              v-decorator=\"['sn',{ rules: [{ required: true, min:1,max:30,message:'输入内容应在1到50位之间' }] },]\"\n              placeholder=\"请输入部门编号\"\n          />\n        </a-form-item>\n      </a-form>\n    </a-modal>\n  </div>\n</template>\n\n<script>\nimport * as department from \"@/services/department\"\nimport validators from \"@/utils/validators\";\nconst columns = [\n  {\n    title: '编号',\n    dataIndex: 'id'\n  },\n  {\n    title: '部门名称',\n    dataIndex: 'name',\n    ellipsis: true,\n  },\n  {\n    title: '部门编号',\n    dataIndex: 'sn',\n    ellipsis: true,\n  },\n  {\n    title: '操作',\n    scopedSlots: {customRender: 'action'}\n  }\n]\nexport default {\n  name: 'Department',\n  data() {\n    return {\n      validators,\n      // table\n      columns: columns,\n      dataSource: [],\n      selectedRows: [],\n      pagination: {},\n      loading: false,\n      // modal\n      title: '新增',\n      visible: false,\n      confirmLoading: false,\n      // modal form\n      form: this.$form.createForm(this, {name: 'coordinated'}),\n    }\n  },\n  authorize: {\n    deleteRecord: 'delete'\n  },\n  created() {\n    this.fetch()\n  },\n  methods: {\n    // table\n    handleTableChange(pagination) {\n      const pager = {...this.pagination};\n      pager.current = pagination.current;\n      this.pagination = pager;\n      this.fetch({\n        size: pagination.pageSize,\n        page: pagination.current,\n      });\n    },\n    fetch(params = {\"page\": 1, \"size\": 10}) {\n      this.loading = true\n      department.list(params || {\"page\": 1, \"size\": 10}).then(({data}) => {\n        const res = data.data\n        const pagination = {...this.pagination};\n        pagination.total = res.total\n        pagination.current = params.page\n        this.dataSource = res.list.map((e, i) => ({key: i + \"\",...e}))\n        this.pagination = pagination\n        this.loading = false\n      })\n    },\n    deleteItem(text) {\n      const title = '删除'\n      department.deleteItem(text.id).then(({data})=>{\n        if (data.code !== 200) {\n          this.$notification['error']({\n            message: title + '部门信息出现错误',\n            description: '检查网络连接或重新登陆',\n          });\n        }\n        this.$notification.success({\n          message: title + '成功',\n          description: title + '部门信息成功',\n        });\n        this.fetch({\"page\": this.pagination.current, \"size\": 10})\n      })\n    },\n    updateItem(id) {\n      this.showModal('更改')\n      department.getDetail(id).then(({data}) => {\n        // 这里不能循环\n        this.form.setFieldsValue({\"id\": data.data[\"id\"]})\n        this.form.setFieldsValue({\"sn\": data.data[\"sn\"]})\n        this.form.setFieldsValue({\"name\": data.data[\"name\"]})\n      })\n    },\n    // modal\n    showModal(title = '新增') {\n      this.visible = true;\n      this.title = title || '新增'\n      this.form.resetFields()\n    },\n    handleOk() {\n      this.confirmLoading = true;\n      this.form.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.confirmLoading = true;\n          return;\n        }\n        let method = 'add';\n        if (values.id) method = 'update';\n\n        department[method](values).then(({data}) => {\n          this.confirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: this.title + '部门信息出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else{\n            this.$notification.success({\n              message: this.title + '成功',\n              description: this.title + '部门信息成功',\n            });\n          }\n          this.visible = false\n          this.fetch({\"page\": this.pagination.current, \"size\": 10})\n        })\n      });\n    },\n    handleCancel() {\n      this.visible = false;\n      this.title=''\n      this.confirmLoading = false\n      this.form.resetFields()\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n.search {\n  margin-bottom: 54px;\n}\n\n.fold {\n  width: calc(100% - 216px);\n  display: inline-block\n}\n\n.operator {\n  margin-bottom: 18px;\n}\n\n@media screen and (max-width: 900px) {\n  .fold {\n    width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/pages/dictionary/contents.vue",
    "content": "<template>\n  <div>\n    <a-card>\n      <div>\n        <a-space class=\"operator\">\n          <a-form layout=\"inline\" :form=\"queryForm\">\n            <a-form-item label=\"关键字\">\n              <a-input\n                  v-decorator=\"['keyword', { rules: [{ required: false,min:1,max:120,message:'输入长度应在1到120之间'}] }]\"\n                  placeholder=\"请输入关键字\"\n              />\n            </a-form-item>\n            <a-form-item>\n              <a-button @click=\"query()\" :loading=\"queryLoading\">查询</a-button>\n            </a-form-item>\n          </a-form>\n          <a-button type=\"primary\" @click=\"showModal('添加')\">添加数据字典</a-button>\n        </a-space>\n        <a-table\n            :columns=\"columns\"\n            :data-source=\"dataSource\"\n            :pagination=\"pagination\"\n            :loading=\"loading\"\n            @change=\"handleTableChange\"\n        >\n           <span slot=\"action\" slot-scope=\"text\">\n              <a @click=\"updateItem(text.id)\">编辑</a>\n           </span>\n        </a-table>\n      </div>\n    </a-card>\n    <a-modal\n        :title=\"title\"\n        :visible=\"visible\"\n        :confirm-loading=\"confirmLoading\"\n        @ok=\"handleOk\"\n        @cancel=\"handleCancel\"\n        okText=\"提交\"\n    >\n      <a-form :form=\"form\" :label-col=\"{ span: 5 }\" :wrapper-col=\"{ span: 12 }\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['id',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item label=\"数据字典名称\">\n          <a-input\n              v-decorator=\"['title', { rules: [{ required: true,min:1,max:15,message:'内容长度在1到15之间' }] }]\"\n          />\n        </a-form-item>\n        <a-form-item label=\"数据字典编号\">\n          <a-input\n              v-decorator=\"['sn',{ rules: [{ required: true, min:1,max:30,message:'输入内容应在1到50位之间' }] },]\"\n              placeholder=\"请输入数据字典编号\"\n          />\n        </a-form-item>\n        <a-form-item label=\"数据字典简介\">\n          <a-input\n              v-decorator=\"['intro',{ rules: [{ required: true, min:3,max:100,message:'内容长度在3到100之间' }] },]\"\n              placeholder=\"请输入数据字典简介\"\n          />\n        </a-form-item>\n      </a-form>\n    </a-modal>\n  </div>\n</template>\n\n<script>\nimport * as dictionaryContents from \"@/services/dictionaryContents\"\nimport validators from \"@/utils/validators\";\nconst columns = [\n  {\n    title: '编号',\n    dataIndex: 'id'\n  },\n  {\n    title: '名称',\n    dataIndex: 'title',\n    ellipsis: true,\n  },\n  {\n    title: '编码',\n    dataIndex: 'sn',\n    ellipsis: true,\n  },\n  {\n    title: '简介',\n    dataIndex: 'intro',\n    ellipsis: true,\n  },\n  {\n    title: '操作',\n    scopedSlots: {customRender: 'action'}\n  }\n]\nexport default {\n  data() {\n    return {\n      validators,\n      queryForm:this.$form.createForm(this, {name: 'coordinated'}),\n      queryLoading:false,\n      // table\n      columns: columns,\n      dataSource: [],\n      selectedRows: [],\n      pagination: {},\n      loading: false,\n      // modal\n      title: '添加',\n      visible: false,\n      confirmLoading: false,\n      // modal form\n      form: this.$form.createForm(this, {name: 'coordinated'}),\n    }\n  },\n  created() {\n    this.fetch()\n  },\n  methods: {\n    query(){\n      this.queryLoading = true\n      this.queryForm.validateFields((err, values) => {\n        if (err) {\n          this.queryLoading = false\n          console.log(\"form error\");\n          return;\n        }\n        this.fetch({\"page\": this.pagination.current, \"size\": 10,...values})\n      })\n    },\n    // table\n    handleTableChange(pagination) {\n      const pager = {...this.pagination};\n      pager.current = pagination.current;\n      this.pagination = pager;\n      this.fetch({\n        size: pagination.pageSize,\n        page: pagination.current,\n      });\n    },\n    fetch(params = {\"page\": 1, \"size\": 10}) {\n      this.loading = true\n      dictionaryContents.list(params || {\"page\": 1, \"size\": 10}).then(({data}) => {\n        const res = data.data\n        const pagination = {...this.pagination};\n        pagination.total = res.total\n        pagination.current = params.page\n        this.dataSource = res.list.map((e, i) => ({key: i + \"\",...e}))\n        this.pagination = pagination\n        this.loading = false\n        this.queryLoading = false\n      })\n    },\n    deleteItem(text) {\n      const title = '删除'\n      dictionaryContents.deleteItem(text.id).then(({data})=>{\n        if (data.code !== 200) {\n          this.$notification['error']({\n            message: title + '数据字典信息出现错误',\n            description: '建议检查网络连接或重新登陆',\n          });\n        }\n        this.$notification.success({\n          message: title + '成功',\n          description: title + '数据字典信息成功',\n        });\n        this.fetch({\"page\": this.pagination.current, \"size\": 10})\n      })\n    },\n    updateItem(id) {\n      this.showModal('更改')\n      dictionaryContents.getDetail(id).then(({data}) => {\n        // 这里不能循环\n        this.form.setFieldsValue({\"id\": data.data[\"id\"]})\n        this.form.setFieldsValue({\"sn\": data.data[\"sn\"]})\n        this.form.setFieldsValue({\"title\": data.data[\"title\"]})\n        this.form.setFieldsValue({\"intro\": data.data[\"intro\"]})\n      })\n    },\n    // modal\n    showModal(title = '添加') {\n      this.visible = true;\n      this.title = title || '添加'\n      this.form.resetFields()\n    },\n    handleOk() {\n      this.confirmLoading = true;\n      this.form.validateFields((err, values) => {\n        if (err) {\n          this.confirmLoading = true;\n          console.log(\"form error\");\n          return;\n        }\n        let method = 'add';\n        if (values.id) method = 'update';\n\n        dictionaryContents[method](values).then(({data}) => {\n          this.confirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: this.title + '数据字典信息出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else{\n            this.$notification.success({\n              message: this.title + '成功',\n              description: this.title + '数据字典信息成功',\n            });\n          }\n          this.visible = false\n          this.query()\n        })\n      });\n    },\n    handleCancel() {\n      this.visible = false;\n      this.title=''\n      this.confirmLoading = false\n      this.form.resetFields()\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n.search {\n  margin-bottom: 54px;\n}\n\n.fold {\n  width: calc(100% - 216px);\n  display: inline-block\n}\n\n.operator {\n  margin-bottom: 18px;\n}\n\n@media screen and (max-width: 900px) {\n  .fold {\n    width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/pages/dictionary/details.vue",
    "content": "<template>\n  <div style=\"display: flex;\">\n    <a-card style=\"width: 30%\" title=\"数据字典分组\">\n      <div :key=\"item.id\" v-for=\"(item) in dContentsList\">\n        <div\n            :style=\"{backgroundColor:leftFirstId===item.id?'#337ab7':''}\"\n            @click=\"changeRight(item.id)\">\n          <a-button\n              :style=\"{color:leftFirstId===item.id?'white':''}\"\n              type=\"link\">{{item.title}}</a-button>\n        </div>\n      </div>\n    </a-card>\n    <a-card style=\"width: 100%\">\n      <div style=\"width: 100%;\">\n        <a-space class=\"operator\">\n          <a-form layout=\"inline\" :form=\"queryForm\">\n            <a-form-item label=\"关键字\">\n              <a-input\n                  v-decorator=\"['keyword', { rules: [{ required: false,min:1,max:120,message:'输入长度应在1到120之间'}] }]\"\n                  placeholder=\"请输入关键字\"\n              />\n            </a-form-item>\n            <a-form-item>\n              <a-button @click=\"query()\" :loading=\"queryLoading\">查询</a-button>\n            </a-form-item>\n          </a-form>\n          <a-button type=\"primary\" @click=\"showModal('添加')\">添加字典明细</a-button>\n        </a-space>\n      </div>\n      <div>\n        <a-button\n            target=\"_blank\"\n            href=\"https://www.sxejgfyxgs.com/uploadfile/file/20200421/6dfb8b3f8.pdf\"\n            v-if=\"leftFirstId===1\"\n            type=\"link\">《中华人民共和国职业分类大典》目录</a-button>\n      </div>\n        <a-table\n            :columns=\"columns\"\n            :data-source=\"dataSource\"\n            :pagination=\"pagination\"\n            :loading=\"loading\"\n            @change=\"handleTableChange\"\n        >\n           <span slot=\"action\" slot-scope=\"text\">\n              <a @click=\"updateItem(text.id)\">编辑</a>\n           </span>\n        </a-table>\n    </a-card>\n    <a-modal\n        :title=\"title\"\n        :visible=\"visible\"\n        :confirm-loading=\"confirmLoading\"\n        @ok=\"handleOk\"\n        @cancel=\"handleCancel\"\n        okText=\"提交\"\n    >\n      <a-form :form=\"form\" :label-col=\"{ span: 5 }\" :wrapper-col=\"{ span: 12 }\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['id',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item hidden>\n          <a-input v-decorator=\"['parentid',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item label=\"字典明细名称\">\n          <a-input\n              v-decorator=\"['title', { rules: [{ required: true, min:1,max:125,message:'内容长度在1到125之间' }] }]\"\n          />\n        </a-form-item>\n        <a-form-item label=\"字典明细序列\">\n          <a-input-number\n              :min=\"0\"\n              :max=\"999\"\n              v-decorator=\"['sequence',{ rules: [{ required: true, message: '请输入字典明细序列' }] },]\"\n              placeholder=\"请输入字典明细序列\"\n          />\n        </a-form-item>\n      </a-form>\n    </a-modal>\n  </div>\n</template>\n\n<script>\nimport * as dictionaryDetails from \"@/services/dictionaryDetails\"\nimport * as dictionaryContents from \"@/services/dictionaryContents\"\nimport validators from \"@/utils/validators\";\nconst columns = [\n  {\n    title: '编号',\n    dataIndex: 'id'\n  },\n  {\n    title: '名称',\n    dataIndex: 'title',\n    ellipsis: true,\n  },\n  {\n    title: '序列',\n    dataIndex: 'sequence',\n    ellipsis: true,\n  },\n  {\n    title: '操作',\n    scopedSlots: {customRender: 'action'}\n  }\n]\nexport default {\n  data() {\n    return {\n      validators,\n      queryForm:this.$form.createForm(this, {name: 'coordinated'}),\n      queryLoading:false,\n      // table\n      columns: columns,\n      dataSource: [],\n      selectedRows: [],\n      pagination: {},\n      loading: false,\n      // modal\n      title: '添加',\n      visible: false,\n      confirmLoading: false,\n      dContentsList:[],\n      // modal form\n      form: this.$form.createForm(this, {name: 'coordinated'}),\n      // left card\n      leftFirstId:null,\n    }\n  },\n  created() {\n    dictionaryContents.list({page:1,size:999999}).then(({data})=>{\n      this.dContentsList = data.data.list\n      this.leftFirstId = this.dContentsList[0].id\n      this.fetch({page:1,size:10,id:this.leftFirstId})\n    })\n  },\n  methods: {\n    changeRight(id){\n      this.leftFirstId = id\n      this.query()\n    },\n    query(){\n      this.queryLoading = true\n      this.queryForm.validateFields((err, values) => {\n        if (err) {\n          this.queryLoading = false\n          console.log(\"form error\");\n          return;\n        }\n        this.fetch({\"page\": this.pagination.current, \"size\": 10,id:this.leftFirstId,...values})\n      })\n    },\n    // table\n    handleTableChange(pagination) {\n      const pager = {...this.pagination};\n      pager.current = pagination.current;\n      this.pagination = pager;\n      this.query()\n    },\n    fetch(params = {\"page\": 1, \"size\": 10}) {\n      this.loading = true\n      dictionaryDetails.list(params || {\"page\": 1, \"size\": 10,id:this.leftFirstId}).then(({data}) => {\n        const res = data.data\n        const pagination = {...this.pagination};\n        pagination.total = res.total\n        pagination.current = params.page\n        this.dataSource = res.list.map((e, i) => ({key: i + \"\",...e}))\n        this.pagination = pagination\n        this.loading = false\n        this.queryLoading = false\n      })\n    },\n    deleteItem(text) {\n      const title = '删除'\n      dictionaryDetails.deleteItem(text.id).then(({data})=>{\n        if (data.code !== 200) {\n          this.$notification['error']({\n            message: title + '字典明细信息出现错误',\n            description: '建议检查网络连接或重新登陆',\n          });\n        }\n        this.$notification.success({\n          message: title + '成功',\n          description: title + '字典明细信息成功',\n        });\n        this.fetch({\"page\": this.pagination.current, \"size\": 10,id:this.leftFirstId})\n      })\n    },\n    updateItem(id) {\n      this.showModal('更改')\n      dictionaryDetails.getDetail(id).then(({data}) => {\n        // 这里不能循环\n        this.form.setFieldsValue({\"id\": data.data[\"id\"]})\n        this.form.setFieldsValue({\"sequence\": data.data[\"sequence\"]})\n        this.form.setFieldsValue({\"title\": data.data[\"title\"]})\n        this.form.setFieldsValue({\"parentid\": data.data[\"parentid\"]})\n      })\n    },\n    // modal\n    showModal(title = '添加') {\n      this.visible = true;\n      this.title = title || '添加'\n      this.form.resetFields()\n      this.$nextTick(()=>{\n        this.form.setFieldsValue({\"parentid\": this.leftFirstId})\n      })\n    },\n    handleOk() {\n      this.confirmLoading = true;\n      this.form.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.confirmLoading = true;\n          return;\n        }\n        let method = 'add';\n        if (values.id) method = 'update';\n        dictionaryDetails[method](values).then(({data}) => {\n          this.confirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: this.title + '字典明细信息出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else{\n            this.$notification.success({\n              message: this.title + '成功',\n              description: this.title + '字典明细信息成功',\n            });\n          }\n          this.visible = false\n          this.fetch({\"page\": this.pagination.current, \"size\": 10,id:this.leftFirstId})\n        })\n      });\n    },\n    handleCancel() {\n      this.visible = false;\n      this.title=''\n      this.confirmLoading = false\n      this.form.resetFields()\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n.search {\n  margin-bottom: 54px;\n}\n\n.fold {\n  width: calc(100% - 216px);\n  display: inline-block\n}\n\n.operator {\n  margin-bottom: 18px;\n}\n\n@media screen and (max-width: 900px) {\n  .fold {\n    width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/pages/employee/index.vue",
    "content": "<template>\n  <div>\n    <a-card>\n      <div>\n        <a-space class=\"operator\">\n          <a-form layout=\"inline\" :form=\"queryForm\">\n            <a-form-item label=\"关键字\">\n              <a-input\n                  v-decorator=\"['keyword', { rules: [{ required: false,max:120,min:1}] }]\"\n                  placeholder=\"请输入姓名/邮箱\"\n              />\n            </a-form-item>\n            <a-form-item label=\"员工部门\">\n              <a-select\n                  style=\"width: 6rem\"\n                  v-decorator=\"['dept',{ rules: [{ required: true, message: '员工部门' }] }]\">\n                <a-select-option :value=\"0\">\n                  全部\n                </a-select-option>\n                <a-select-option\n                    :value=\"item.id\"\n                    :key=\"item.id\"\n                    v-for=\"(item) in departmentNames\">\n                  {{item.name}}\n                </a-select-option>\n              </a-select>\n            </a-form-item>\n            <a-form-item>\n              <a-button :loading=\"queryLoading\" @click=\"query()\">查询</a-button>\n            </a-form-item>\n          </a-form>\n          <a-button type=\"success\" @click=\"showModal('新增')\">添加</a-button>\n          <a-button type=\"primary\" @click=\"mDelete()\">批量删除</a-button>\n          <a-popconfirm\n              title=\"您确定导出当前搜索条件下的所有结果么？\"\n              ok-text=\"是\"\n              cancel-text=\"否\"\n              @confirm=\"exportFile()\"\n          >\n            <a-button icon=\"download\" type=\"primary\">导出</a-button>\n          </a-popconfirm>\n          <a-upload :showUploadList=\"false\" :file-list=\"fileList\" :remove=\"handleRemove\" :before-upload=\"beforeUpload\">\n            <a-button> <a-icon type=\"upload\" /> 导入数据</a-button>\n          </a-upload>\n          <a-button icon=\"download\" @click=\"downloadTemplate\" type=\"link\">导入模板</a-button>\n        </a-space>\n        <a-table\n            :columns=\"columns\"\n            :data-source=\"dataSource\"\n            :pagination=\"pagination\"\n            :row-selection=\"{selectedRowKeys: outSelectedRowKeys,onChange: onSelectChange }\"\n            :loading=\"loading\"\n            @change=\"handleTableChange\"\n        >\n           <span slot=\"action\" slot-scope=\"text\">\n              <a @click=\"updateItem(text.id)\">编辑</a> |\n               <a-popconfirm\n                   title=\"您真的要删除这行数据么？\"\n                   ok-text=\"是\"\n                   cancel-text=\"否\"\n                   @confirm=\"deleteItem(text)\"\n               >\n              <a>删除</a>\n              </a-popconfirm>\n           </span>\n        </a-table>\n      </div>\n    </a-card>\n    <a-modal\n        :title=\"title\"\n        :visible=\"visible\"\n        :confirm-loading=\"confirmLoading\"\n        @ok=\"handleOk\"\n        @cancel=\"handleCancel\"\n        okText=\"提交\"\n        width=\"80%\"\n    >\n      <a-form :form=\"form\" :layout=\"`horizontal`\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['id',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item label=\"员工名称\">\n          <a-input\n              v-decorator=\"['name', { rules: [{ required: true,min:1,max:15}] }]\"\n          />\n        </a-form-item>\n        <a-form-item label=\"员工密码\">\n          <a-input-password\n              v-decorator=\"['password', { rules: [{ required: title==='新增',message:validators.passwordMsg,pattern:validators.passwordReg }] }]\"\n          />\n        </a-form-item>\n        <a-form-item label=\"验证密码\">\n          <a-input-password\n              v-decorator=\"['rePassword', { rules: [{ required: title==='新增',validator: title==='新增' && checkRePassword }] }]\"\n          />\n        </a-form-item>\n        <a-form-item label=\"员工年龄\">\n          <a-input-number\n              :max=\"200\"\n              :min=\"1\"\n              v-decorator=\"['age',{ rules: [{ required: true, message: '请输入员工年龄' }] }]\"\n              placeholder=\"请输入员工年龄\"\n          />\n        </a-form-item>\n        <a-form-item label=\"员工email\">\n          <a-input\n              v-decorator=\"['email',{ rules: [{ required: true, pattern:validators.emailReg,message:'Email格式不匹配' }] }]\"\n              placeholder=\"请输入员工email\"\n          />\n        </a-form-item>\n        <a-form-item label=\"员工部门\">\n          <a-select\n              v-decorator=\"['dept',{ rules: [{ required: true, message: '请输入员工部门' }] }]\">\n            <a-select-option\n                :value=\"item.id\"\n                :key=\"item.id\"\n                v-for=\"(item) in departmentNames\">\n              {{item.name}}\n            </a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item label=\"超级管理员\">\n          <a-checkbox :checked=\"form.getFieldValue('admin')\" v-decorator=\"['admin',{ rules: [{ required: false }] }]\">\n          </a-checkbox>\n        </a-form-item>\n        <a-form-item label=\"员工角色\">\n          <a-transfer\n              v-decorator=\"['roleIds',{ rules: [{ required: false }] }]\"\n              :operations=\"['加入角色', '移除角色']\"\n              :data-source=\"roleIds\"\n              :target-keys=\"targetKeys\"\n              :disabled=\"disabled\"\n              :show-search=\"showSearch\"\n              :filter-option=\"(inputValue, item) => item.title.indexOf(inputValue) !== -1\"\n              :show-select-all=\"false\"\n              @change=\"handleChange\"\n          >\n            <template\n                slot=\"children\"\n                slot-scope=\"{\n                props: { direction, filteredItems, selectedKeys, disabled: listDisabled },\n                on: { itemSelectAll, itemSelect },\n              }\"\n            >\n              <a-table\n                  :row-selection=\"getRowSelection({ disabled: listDisabled, selectedKeys, itemSelectAll, itemSelect })\"\n                  :columns=\"direction === 'left' ? leftColumns : rightColumns\"\n                  :data-source=\"filteredItems\"\n                  size=\"small\"\n                  :style=\"{ pointerEvents: listDisabled ? 'none' : null }\"\n                  :custom-row=\"({ key, disabled: itemDisabled }) => ({\n                  on: {\n                    click: () => {\n                      if (itemDisabled || listDisabled) return;\n                      itemSelect(key, !selectedKeys.includes(key));\n                    },\n                  },\n                })\"\n              />\n            </template>\n            <a-button\n                slot=\"footer\"\n                size=\"small\"\n                style=\"float:right;margin: 5px\"\n                @click=\"updateItem(reloadId)\"\n            >\n              刷新所有权限\n            </a-button>\n            <span slot=\"notFoundContent\">\n              没数据\n            </span>\n          </a-transfer>\n        </a-form-item>\n      </a-form>\n    </a-modal>\n  </div>\n</template>\n\n<script>\nimport * as employee from \"@/services/employee\"\nimport * as role from \"@/services/role\"\nimport * as department from \"@/services/department\"\nimport difference from 'lodash/difference';\nimport * as XLSX from 'xlsx'\nimport validators from \"@/utils/validators\";\n\nconst columns = [\n  {\n    title: '编号',\n    dataIndex: 'id'\n  },\n  {\n    title: '名称',\n    dataIndex: 'name',\n    ellipsis: true,\n  },\n  {\n    title: 'email',\n    dataIndex: 'email',\n    ellipsis: true,\n  },\n  {\n    title: '年龄',\n    dataIndex: 'age',\n  },\n  {\n    title: '部门',\n    dataIndex: 'departmentName',\n    ellipsis: true,\n  },\n  {\n    title: '角色',\n    dataIndex: 'roleNames',\n    ellipsis: true,\n  },\n  {\n    title: '操作',\n    scopedSlots: {customRender: 'action'}\n  }\n]\nconst permissionTableColumns = [\n  {\n    title: '角色名称',\n    dataIndex: 'name',\n  },\n  {\n    title: '角色编号',\n    dataIndex: 'sn'\n  },\n];\nexport default {\n  name: 'Department',\n  data() {\n    return {\n      validators,\n      // 上传\n      fileList: [],\n      uploading: false,\n      // 外层table 多选\n      outSelectedRowKeys:[],\n      outSelectedRows:[],\n      queryForm:this.$form.createForm(this, {name: 'coordinated'}),\n      queryLoading:false,\n      departmentNames:[],\n      // table\n      columns: columns,\n      dataSource: [],\n      selectedRows: [],\n      pagination: {current:1},\n      loading: false,\n      // modal\n      title: '新增',\n      visible: false,\n      confirmLoading: false,\n      reloadId: null,\n      // modal form\n      form: this.$form.createForm(this, {name: 'coordinated'}),\n      // modal permission list\n      roleIds: [],\n      targetKeys: [],\n      selectedKeys: [],\n      disabled: false,\n      showSearch: false,\n      leftColumns: permissionTableColumns,\n      rightColumns: permissionTableColumns,\n    }\n  },\n  authorize: {\n    deleteRecord: 'delete'\n  },\n  created() {\n    department.list({\"page\": 1, \"size\": 99999}).then(({data})=>{\n      this.departmentNames = data.data.list.map((e,i)=>({key:i+'',...e}))\n    })\n    this.getAllroleIds()\n  },\n  mounted() {\n    this.queryForm.setFieldsValue({\"dept\":0})\n    this.query()\n  },\n  methods: {\n    // 上传\n    handleRemove(file) {\n      const index = this.fileList.indexOf(file);\n      const newFileList = this.fileList.slice();\n      newFileList.splice(index, 1);\n      this.fileList = newFileList;\n    },\n    async beforeUpload(file) {\n      this.fileList = [...this.fileList, file];\n      // const xlsxRead = XLSX.read(file)\n      let res = await new Promise((resolve) => {\n        let fileReader = new FileReader()\n        fileReader.readAsBinaryString(file)\n        fileReader.onload=e=>{\n          /* Parse data */\n          const bstr = e.target.result;\n          const wb = XLSX.read(bstr, {type:'binary'});\n          /* Get first worksheet */\n          const wsname = wb.SheetNames[0];\n          const ws = wb.Sheets[wsname];\n          /* Convert array of arrays */\n          const data = XLSX.utils.sheet_to_json(ws, {header:1});\n          const sLen =6\n          if(data[0].length!==sLen){\n            resolve(\"数据内容不合法，请先下载导入模板！\")\n          }\n          for(let i=1;i<data.length;i++){\n            for(let j=0;j<data[i].length;j++){\n              if(data[i].length!==sLen){\n                resolve(`第${i}行数据内容不足！`)\n                break;\n              }\n              const booleanCheck = Object.prototype.toString.call(data[i][j])\n              if(data[i][j]==='' || booleanCheck === 'Null' || booleanCheck === '[object Undefined]'){\n                resolve(`第${i+1}行${j+1}列数据内容不合法！`)\n              }\n            }\n          }\n\n          const json =[]\n          for(let i=1;i<data.length;i++){\n            const obj = {}\n            for(let k=0;k<data[i].length;k++){\n              const value = data[i][k].trim()\n              if(data[0][k]==='角色') {\n                const roleIds = this.roleIds\n                const roles = value.split(',')\n                const find=[]\n                roles.forEach(name => {\n                  const r = roleIds.find(e => e.name === name)\n                  if (r) {\n                    find.push(r.id)\n                  }else {\n                    resolve(`第${i+1}行${k+1}列数据内容不合法！无法找到此角色！请校验数据并刷新页面后重新导入！`)\n                  }\n                })\n                obj.roleIds = find\n              }else if(data[0][k]==='部门'){\n                const find =this.departmentNames.find(e=>e.name===value)\n                if('[object Undefined]' === Object.prototype.toString.call(find)){\n                  resolve(`第${i+1}行${k+1}列数据内容不合法！无法找到此部门！请校验数据并刷新页面后重新导入！`)\n                }\n                obj.dept = find.id\n              }else if(data[0][k]==='密码'){\n                const msg = validators.password()(null,value)\n                if(msg){\n                  resolve(`第${i+1}行${k+1}列数据内容不合法!${msg}`)\n                }\n                obj.password = value\n              }else if(data[0][k]==='密码'){\n                const msg = validators.email()(null,value)\n                if(msg){\n                  resolve(`第${i+1}行${k+1}列数据内容不合法!${msg}`)\n                }\n                obj.password = value\n              }else {\n                switch (data[0][k]){\n                  case '名称':obj.name = value;break;\n                  case '年龄':obj.age = value ;break;\n                  case 'Email': obj.email = value;break;\n                }\n              }\n            }\n            json.push(obj)\n          }\n         resolve(json)\n        }\n      })\n      if( \"[object Array]\" !== Object.prototype.toString.call(res)){\n        this.$notification.warning({\n          message: res,\n          description: '建议检查您所上传的文件内容',\n        })\n      }else{\n        for(let i=0;i<res.length;i++){\n          const {data} = await employee.add(res[i])\n          if(data.code!==200){\n            this.$notification.error({message: `第${i+1}条信息导入失败`, description: data.message||\"请检查您的网络，不建议一次上传过多内容\"})\n            break;\n          }else{\n            this.$notification.success({message: \"导入成功\"})\n          }\n        }\n      }\n      this.query()\n      return false;\n    },\n    // excel\n    async exportFile(){\n      this.queryForm.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          return;\n        }\n        this.fetch({\"page\": 1, \"size\": 99999999,...values}).then(({data})=>{\n          const {list} = data\n          const res = list.map(e=>({\n            '部门':e.departmentName,\n            '名称':e.name,\n            '角色':e.roleNames,\n            '年龄':e.age,\n            'Email':e.email\n          }))\n          /* convert state to workbook */\n          const ws = XLSX.utils.json_to_sheet(res);\n          const wb = XLSX.utils.book_new();\n          XLSX.utils.book_append_sheet(wb, ws, \"SheetJS\");\n          /* generate file and send to client */\n          XLSX.writeFile(wb, \"员工信息.xlsx\");\n          this.query()\n        })\n      })\n    },\n    downloadTemplate(){\n      const res = [\n        {\n          '部门':'test部门',\n          '名称':'test名称',\n          '角色':'test角色',\n          '密码':'testPassword',\n          '年龄':'18',\n          'Email':'c@c.c'\n        }\n      ]\n      /* convert state to workbook */\n      const ws = XLSX.utils.json_to_sheet(res);\n      const wb = XLSX.utils.book_new();\n      XLSX.utils.book_append_sheet(wb, ws, \"SheetJS\");\n      /* generate file and send to client */\n      XLSX.writeFile(wb, \"员工信息模板.xlsx\");\n    },\n    onSelectChange(selectedRowKeys,selectedRows) {\n      this.outSelectedRowKeys = selectedRowKeys;\n      this.outSelectedRows = selectedRows;\n    },\n    async mDelete(){\n      if(this.outSelectedRows.length<=0){\n        this.$message.warning(\"尚未批量选择\")\n      }else {\n        for(let i=0;i<this.outSelectedRows.length;i++){\n          const {data} = await employee.deleteItem(this.outSelectedRows[i].id)\n          if(data.code==200){\n            this.$notification.success({\n              message: '删除成功！',\n            });\n          }else {\n            this.$notification['error']({\n              message: '删除失败！',\n              description: data.message || '建议检查网络连接或重新登陆'\n            });\n          }\n        }\n        this.query().then(()=>{\n          document.querySelector(\"#popContainer > section > section > main > div > div.tabs-view-content.side.fluid > div > div.page-content.side.fluid > div > div > div > div > div.ant-table-wrapper > div > div > div > div > div > table > thead > tr > th.ant-table-selection-column > span > div > span.ant-table-column-title > div > label > span > input\").click()\n        })\n      }\n    },\n    query(){\n      return new Promise(resolve => {\n        this.queryLoading = true\n        this.queryForm.validateFields(async (err, values) => {\n          if (err) {\n            this.queryLoading = false\n            console.log(\"form error\");\n            return;\n          }\n          const data =await this.fetch({\"page\": this.pagination.current, \"size\": 10,...values})\n          resolve(data)\n        })\n      })\n    },\n    // table\n    handleTableChange(pagination) {\n      this.outSelectedRowKeys=[]\n      this.outSelectedRows =[]\n      const pager = {...this.pagination};\n      pager.current = pagination.current;\n      this.pagination = pager;\n      this.query()\n    },\n    async fetch(params = {\"page\": 1, \"size\": 10}) {\n      this.loading = true\n      let {data} = await employee.list(params || {\"page\": 1, \"size\": 10})\n      const res = data.data\n      const pagination = {...this.pagination};\n      pagination.total = res.total\n      pagination.current = res.pageNum\n      if(res.list){\n        this.dataSource = res.list.map((e, i) => ({key: i + \"\", ...e}))\n      }\n      this.pagination = pagination\n      this.loading = false\n      this.queryLoading=false\n      return data\n    },\n    deleteItem(text) {\n      const title = '删除'\n      employee.deleteItem(text.id).then(({data}) => {\n        if (data.code !== 200) {\n          this.$notification['error']({\n            message: title + '角色信息出现错误',\n            description: '建议检查网络连接或重新登陆',\n          });\n        }\n        this.$notification.success({\n          message: title + '成功',\n          description: title + '角色信息成功',\n        });\n        this.query()\n      })\n    },\n    async updateItem(id) {\n      if(this.confirmLoading){\n        this.confirmLoading=false\n      }\n      this.targetKeys = []\n      this.roleIds = []\n      this.reloadId = id\n      await this.showModal('更改')\n      employee.getDetail(id).then(({data}) => {\n        if(!data.data) return;\n        // 这里不能循环\n        this.form.setFieldsValue({\"id\": data.data[\"id\"]})\n        this.form.setFieldsValue({\"name\": data.data[\"name\"]})\n        this.form.setFieldsValue({\"admin\": data.data[\"admin\"] === 1})\n        this.form.setFieldsValue({\"age\": data.data[\"age\"]})\n        this.form.setFieldsValue({\"dept\": data.data[\"dept\"]})\n        this.form.setFieldsValue({\"email\": data.data[\"email\"]})\n        const {roleIds} = data.data\n        if (!roleIds) return;\n        for(let i=0;i<roleIds.length;i++){\n          const find = this.roleIds.find(e=>roleIds[i]==(e.id))\n          this.targetKeys.push(find.key)\n        }\n      })\n    },\n    // modal\n    async showModal(title) {\n      this.visible = true;\n      this.title = title || '新增'\n      this.form.resetFields()\n      this.targetKeys = []\n      await this.getAllroleIds()\n    },\n    handleOk() {\n      this.confirmLoading = true;\n      this.form.validateFields((err, values) => {\n        if (err) {\n          console.log(\"form error\");\n          this.confirmLoading = false\n          return;\n        }\n        let method = 'add';\n        if (values.id) method = 'update';\n        if(!values.roleIds){\n          delete values.roleIds\n        }\n        if(values.roleIds && values.roleIds.length>=1){\n          let arr =[]\n          for(let i=0;i<values.roleIds.length;i++){\n            const e = values.roleIds[i]\n            arr.push(this.roleIds[e].id)\n          }\n          values.roleIds = arr\n        }\n        employee[method](values).then(({data}) => {\n          this.confirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: this.title + '角色信息出现错误',\n              description: data.message,\n            });\n          }else{\n            this.$notification.success({\n              message: this.title + '成功',\n              description: this.title + '角色信息成功',\n            });\n            this.visible = false\n          }\n          this.query()\n        })\n      });\n    },\n    handleCancel() {\n      this.visible = false;\n      this.title = ''\n      this.confirmLoading = false\n      this.form.resetFields()\n    },\n    checkRePassword(rule, value, callback) {\n      if (value !== this.form.getFieldValue('password')) {\n        callback('两次密码输入不一致!');\n      }else {\n        callback()\n      }\n    },\n    // modal transfer\n    getAllroleIds() {\n      return role.list({page: 1, size: 999999999}).then(({data}) => {\n        this.roleIds = data.data.list.map((e, i) => ({key: i + \"\", title: e.name, ...e}))\n      })\n    },\n    handleChange(targetKeys) {\n      this.targetKeys = targetKeys;\n    },\n    handleSelectChange(sourceSelectedKeys, targetSelectedKeys) {\n      this.selectedKeys = [...sourceSelectedKeys, ...targetSelectedKeys];\n    },\n    getRowSelection({disabled, selectedKeys, itemSelectAll, itemSelect}) {\n      return {\n        getCheckboxProps: item => ({props: {disabled: disabled || item.disabled}}),\n        onSelectAll(selected, selectedRows) {\n          const treeSelectedKeys = selectedRows\n              .filter(item => !item.disabled)\n              .map(({key}) => key);\n          const diffKeys = selected\n              ? difference(treeSelectedKeys, selectedKeys)\n              : difference(selectedKeys, treeSelectedKeys);\n          itemSelectAll(diffKeys, selected);\n        },\n        onSelect({key}, selected) {\n          itemSelect(key, selected);\n        },\n        selectedRowKeys: selectedKeys,\n      };\n    },\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n.search {\n  margin-bottom: 54px;\n}\n\n.fold {\n  width: calc(100% - 216px);\n  display: inline-block\n}\n\n.operator {\n  margin-bottom: 18px;\n}\n\n@media screen and (max-width: 900px) {\n  .fold {\n    width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/pages/exception/403.vue",
    "content": "<template>\n  <exception-page home-route=\"/dashboard/workplace\" :style=\"`min-height: ${minHeight}`\" type=\"403\" />\n</template>\n\n<script>\nimport ExceptionPage from '@/components/exception/ExceptionPage'\nimport {mapState} from 'vuex'\nexport default {\n  name: 'Exp403',\n  components: {ExceptionPage},\n  computed: {\n    ...mapState('setting', ['pageMinHeight']),\n    minHeight() {\n      return this.pageMinHeight ? this.pageMinHeight + 'px' : '100vh'\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"less\">\n</style>\n"
  },
  {
    "path": "front/src/pages/exception/404.vue",
    "content": "<template>\n  <exception-page home-route=\"/dashboard/workplace\" :style=\"`min-height: ${minHeight}`\" type=\"404\" />\n</template>\n\n<script>\nimport ExceptionPage from '@/components/exception/ExceptionPage'\nimport {mapState} from 'vuex'\nexport default {\n  name: 'Exp404',\n  components: {ExceptionPage},\n  computed: {\n    ...mapState('setting', ['pageMinHeight']),\n    minHeight() {\n      return this.pageMinHeight ? this.pageMinHeight + 'px' : '100vh'\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"less\">\n</style>\n"
  },
  {
    "path": "front/src/pages/exception/500.vue",
    "content": "<template>\n  <exception-page home-route=\"/dashboard/workplace\" :style=\"`min-height: ${minHeight}`\" type=\"500\" />\n</template>\n\n<script>\nimport ExceptionPage from '@/components/exception/ExceptionPage'\nimport {mapState} from 'vuex'\nexport default {\n  name: 'Exp500',\n  components: {ExceptionPage},\n  computed: {\n    ...mapState('setting', ['pageMinHeight']),\n    minHeight() {\n      return this.pageMinHeight ? this.pageMinHeight + 'px' : '100vh'\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"less\">\n</style>\n"
  },
  {
    "path": "front/src/pages/login/Login.vue",
    "content": "<template>\n  <common-layout>\n    <div class=\"top\">\n      <div class=\"header\">\n        <img alt=\"logo\" class=\"logo\" src=\"@/assets/img/logo.png\" />\n        <span class=\"title\">{{systemName}}</span>\n      </div>\n<!--      <div class=\"desc\">Ant Design 是西湖区最具影响力的 Web 设计规范</div>-->\n    </div>\n    <div class=\"login\">\n      <a-form @submit=\"onSubmit\" :form=\"form\">\n        <a-tabs size=\"large\" :tabBarStyle=\"{textAlign: 'center'}\" style=\"padding: 0 2px;\">\n          <a-tab-pane tab=\"账户密码登录\" key=\"1\">\n            <a-alert type=\"error\" :closable=\"true\" v-show=\"error\" :message=\"error\" showIcon style=\"margin-bottom: 24px;\" />\n            <a-form-item>\n              <a-input\n                autocomplete=\"autocomplete\"\n                size=\"large\"\n                placeholder=\"admin\"\n                v-decorator=\"['name', {rules: [{ required: true, message: '请输入账户名', whitespace: true}]}]\"\n              >\n                <a-icon slot=\"prefix\" type=\"user\" />\n              </a-input>\n            </a-form-item>\n            <a-form-item>\n              <a-input\n                size=\"large\"\n                placeholder=\"12345678\"\n                autocomplete=\"autocomplete\"\n                type=\"password\"\n                v-decorator=\"['password', {rules: [{ required: true, message: '请输入密码', whitespace: true}]}]\"\n              >\n                <a-icon slot=\"prefix\" type=\"lock\" />\n              </a-input>\n            </a-form-item>\n          </a-tab-pane>\n<!--          <a-tab-pane tab=\"手机号登录\" key=\"2\">-->\n<!--            <a-form-item>-->\n<!--              <a-input size=\"large\" placeholder=\"mobile number\" >-->\n<!--                <a-icon slot=\"prefix\" type=\"mobile\" />-->\n<!--              </a-input>-->\n<!--            </a-form-item>-->\n<!--            <a-form-item>-->\n<!--              <a-row :gutter=\"8\" style=\"margin: 0 -4px\">-->\n<!--                <a-col :span=\"16\">-->\n<!--                  <a-input size=\"large\" placeholder=\"captcha\">-->\n<!--                    <a-icon slot=\"prefix\" type=\"mail\" />-->\n<!--                  </a-input>-->\n<!--                </a-col>-->\n<!--                <a-col :span=\"8\" style=\"padding-left: 4px\">-->\n<!--                  <a-button style=\"width: 100%\" class=\"captcha-button\" size=\"large\">获取验证码</a-button>-->\n<!--                </a-col>-->\n<!--              </a-row>-->\n<!--            </a-form-item>-->\n<!--          </a-tab-pane>-->\n        </a-tabs>\n        <div>\n          <a-checkbox :checked=\"true\" >自动登录</a-checkbox>\n          <a style=\"float: right\">忘记密码</a>\n        </div>\n        <a-form-item>\n          <a-button :loading=\"logging\" style=\"width: 100%;margin-top: 24px\" size=\"large\" htmlType=\"submit\" type=\"primary\">登录</a-button>\n        </a-form-item>\n<!--        <div>-->\n<!--          其他登录方式-->\n<!--          <a-icon class=\"icon\" type=\"alipay-circle\" />-->\n<!--          <a-icon class=\"icon\" type=\"taobao-circle\" />-->\n<!--          <a-icon class=\"icon\" type=\"weibo-circle\" />-->\n<!--          <router-link style=\"float: right\" to=\"/dashboard/workplace\" >注册账户</router-link>-->\n<!--        </div>-->\n      </a-form>\n    </div>\n  </common-layout>\n</template>\n\n<script>\nimport CommonLayout from '@/layouts/CommonLayout'\nimport {login} from '@/services/user'\nimport {setAuthorization} from '@/utils/request'\nimport {mapMutations} from 'vuex'\n\nexport default {\n  name: 'Login',\n  components: {CommonLayout},\n  data () {\n    return {\n      logging: false,\n      error: '',\n      form: this.$form.createForm(this)\n    }\n  },\n  computed: {\n    systemName () {\n      return this.$store.state.setting.systemName\n    }\n  },\n  methods: {\n    ...mapMutations('account', ['setUser', 'setPermissions', 'setRoles']),\n    onSubmit (e) {\n      e.preventDefault()\n      this.form.validateFields((err) => {\n        if (!err) {\n          this.logging = true\n          const name = this.form.getFieldValue('name')\n          const password = this.form.getFieldValue('password')\n          login(name, password).then(this.afterLogin)\n        }\n      })\n    },\n    afterLogin(res) {\n      this.logging = false\n      const loginRes = res.data\n      if (loginRes.code === 200) {\n        const {user, permissions, roles} = loginRes.data\n        this.setUser(user)\n        this.setPermissions(permissions)\n        this.setRoles(roles)\n        setAuthorization({token: loginRes.data.token, expireAt: new Date(loginRes.data.expireAt)})\n        this.$router.push('/dashboard/workplace')\n        this.$message.success(loginRes.data.message, 3)\n        // 获取路由配置\n        // getRoutesConfig().then(result => {\n        //   const routesConfig = result.data.data\n        //   loadRoutes(routesConfig)\n        //   this.$router.push('/dashboard/workplace')\n        //   this.$message.success(loginRes.message, 3)\n        // })\n      } else {\n        this.error = loginRes.message\n      }\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n  .common-layout{\n    .top {\n      text-align: center;\n      .header {\n        height: 44px;\n        line-height: 44px;\n        a {\n          text-decoration: none;\n        }\n        .logo {\n          height: 44px;\n          vertical-align: top;\n          margin-right: 16px;\n        }\n        .title {\n          font-size: 33px;\n          color: @title-color;\n          font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;\n          font-weight: 600;\n          position: relative;\n          top: 2px;\n        }\n      }\n      .desc {\n        font-size: 14px;\n        color: @text-color-second;\n        margin-top: 12px;\n        margin-bottom: 40px;\n      }\n    }\n    .login{\n      width: 368px;\n      margin: 0 auto;\n      @media screen and (max-width: 576px) {\n        width: 95%;\n      }\n      @media screen and (max-width: 320px) {\n        .captcha-button{\n          font-size: 14px;\n        }\n      }\n      .icon {\n        font-size: 24px;\n        color: @text-color-second;\n        margin-left: 16px;\n        vertical-align: middle;\n        cursor: pointer;\n        transition: color 0.3s;\n\n        &:hover {\n          color: @primary-color;\n        }\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "front/src/pages/login/index.js",
    "content": "import Login from './Login'\nexport default Login\n"
  },
  {
    "path": "front/src/pages/permission/index.vue",
    "content": "<template>\n  <div>\n    <a-card>\n      <div>\n        <a-space class=\"operator\">\n          <a-button type=\"primary\" @click=\"fetch()\">重新加载</a-button>\n        </a-space>\n        <a-table\n            :columns=\"columns\"\n            :data-source=\"dataSource\"\n            :pagination=\"pagination\"\n            :loading=\"loading\"\n            @change=\"handleTableChange\"\n        >\n           <span slot=\"action\" slot-scope=\"text\">\n<!--              <a disabled @click=\"updateItem(text.id)\">编辑</a> |-->\n               <a-popconfirm\n                   title=\"您真的要删除这行数据么？\"\n                   ok-text=\"是\"\n                   cancel-text=\"否\"\n                   @confirm=\"deleteItem(text)\"\n               >\n              <a>删除</a>\n              </a-popconfirm>\n           </span>\n        </a-table>\n      </div>\n    </a-card>\n    <a-modal\n        :title=\"title\"\n        :visible=\"visible\"\n        :confirm-loading=\"confirmLoading\"\n        @ok=\"handleOk\"\n        @cancel=\"handleCancel\"\n        okText=\"提交\"\n    >\n      <a-form :form=\"form\" :label-col=\"{ span: 5 }\" :wrapper-col=\"{ span: 12 }\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['id',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item label=\"权限名称\">\n          <a-input\n              v-decorator=\"['name', { rules: [{ required: true,min:1,max:15,message:'内容长度在1到15之间' }] }]\"\n          />\n        </a-form-item>\n        <a-form-item label=\"权限编号\">\n          <a-input\n              v-decorator=\"['expression',{ rules: [{ required: true,min:1,max:30,message:'输入内容应在1到50位之间' }] },]\"\n              placeholder=\"请输入权限编号\"\n          />\n        </a-form-item>\n      </a-form>\n    </a-modal>\n  </div>\n</template>\n\n<script>\nimport * as permission from \"@/services/permission\"\nimport validators from \"@/utils/validators\";\nconst columns = [\n  {\n    title: '编号',\n    dataIndex: 'id'\n  },\n  {\n    title: '权限名称',\n    dataIndex: 'name',\n    ellipsis: true,\n  },\n  {\n    title: '权限表达式',\n    dataIndex: 'expression',\n    ellipsis: true,\n  },\n  {\n    title: '操作',\n    scopedSlots: {customRender: 'action'}\n  }\n]\nexport default {\n  name: 'Department',\n  data() {\n    return {\n      validators,\n      // table\n      columns: columns,\n      dataSource: [],\n      selectedRows: [],\n      pagination: {},\n      loading: false,\n      // modal\n      title: '新增',\n      visible: false,\n      confirmLoading: false,\n      // modal form\n      form: this.$form.createForm(this, {name: 'coordinated'}),\n    }\n  },\n  authorize: {\n    deleteRecord: 'delete'\n  },\n  created() {\n    this.fetch()\n  },\n  methods: {\n    // table\n    handleTableChange(pagination) {\n      const pager = {...this.pagination};\n      pager.current = pagination.current;\n      this.pagination = pager;\n      this.fetch({\n        size: pagination.pageSize,\n        page: pagination.current,\n      });\n    },\n    fetch(params = {\"page\": 1, \"size\": 10}) {\n      this.loading = true\n      permission.list(params || {\"page\": 1, \"size\": 10}).then(({data}) => {\n        const res = data.data\n        const pagination = {...this.pagination};\n        pagination.total = res.total\n        pagination.current = params.page\n        this.dataSource = res.list.map((e, i) => ({key: i + \"\",...e}))\n        this.pagination = pagination\n        this.loading = false\n      })\n    },\n    deleteItem(text) {\n      const title = '删除'\n      permission.deleteItem(text.id).then(({data})=>{\n        if (data.code !== 200) {\n          this.$notification['error']({\n            message: title + '权限信息出现错误',\n            description: '建议检查网络连接或重新登陆',\n          });\n        }\n        this.$notification.success({\n          message: title + '成功',\n          description: title + '权限信息成功',\n        });\n        this.fetch({\"page\": this.pagination.current, \"size\": 10})\n      })\n    },\n    updateItem(id) {\n      this.showModal('更改')\n      permission.getDetail(id).then(({data}) => {\n        // 这里不能循环\n        this.form.setFieldsValue({\"id\": data.data[\"id\"]})\n        this.form.setFieldsValue({\"name\": data.data[\"name\"]})\n        this.form.setFieldsValue({\"expression\": data.data[\"expression\"]})\n      })\n    },\n    // modal\n    showModal(title) {\n      this.visible = true;\n      this.title = title || '新增'\n      this.form.resetFields()\n    },\n    handleOk() {\n      this.confirmLoading = true;\n      this.form.validateFields((err, values) => {\n        if (err) {\n          this.confirmLoading = true;\n          console.log(\"form error\");\n          return;\n        }\n        let method = 'add';\n        if (values.id) method = 'update';\n\n        permission[method](values).then(({data}) => {\n          this.confirmLoading = false;\n          if (data.code !== 200) {\n            this.$notification['error']({\n              message: this.title + '权限信息出现错误',\n              description: '建议检查网络连接或重新登陆',\n            });\n          }else {\n            this.$notification.success({\n              message: this.title + '成功',\n              description: this.title + '权限信息成功',\n            });\n          }\n          this.visible = false\n          this.fetch({\"page\": this.pagination.current, \"size\": 10})\n        })\n      });\n    },\n    handleCancel() {\n      this.visible = false;\n      this.title=''\n      this.confirmLoading = false\n      this.form.resetFields()\n    }\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n.search {\n  margin-bottom: 54px;\n}\n\n.fold {\n  width: calc(100% - 216px);\n  display: inline-block\n}\n\n.operator {\n  margin-bottom: 18px;\n}\n\n@media screen and (max-width: 900px) {\n  .fold {\n    width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/pages/result/Error.vue",
    "content": "<template>\n    <a-card class=\"result-error\" :bordered=\"false\">\n      <result style=\"margin-bottom: 16px; margin-top: 48px\" :is-success=\"false\" :title=\"title\" :description=\"description\">\n        <template slot=\"action\">\n          <a-button type=\"primary\" >返回修改</a-button>\n        </template>\n        <div>\n          <div style=\"fontSize: 16px; fontWeight: 500; marginBottom: 16px\">\n            您提交的内容有如下错误：\n          </div>\n          <div style=\"margin-bottom: 16px\">\n            <a-icon class=\"error-icon\" type=\"close-circle-o\"/>\n            您的账户已被冻结\n            <a style=\"margin-left: 16px\">立即解冻 <a-icon type=\"right\" /></a>\n          </div>\n          <div>\n            <a-icon class=\"error-icon\" type=\"close-circle-o\"/>\n            您的账户还不具备申请资格\n            <a style=\"margin-left: 16px\">立即升级 <a-icon type=\"right\" /></a>\n          </div>\n        </div>\n      </result>\n    </a-card>\n</template>\n\n<script>\nimport Result from '@/components/result/Result'\nexport default {\n  name: 'Error',\n  components: {Result},\n  data () {\n    return {\n      title: '提交失败',\n      description: '请核对并修改以下信息后，再重新提交。'\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"less\">\n.result-error{\n  .error-icon{\n    color: @red-6;\n    margin-right: 8px\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/pages/result/Success.vue",
    "content": "<template>\n  <a-card class=\"result-success\" :bordered=\"false\">\n    <result :is-success=\"true\" :description=\"description\" :title=\"title\">\n      <template slot=\"action\">\n        <a-button class=\"action\" type=\"primary\">返回列表</a-button>\n        <a-button class=\"action\" >查看项目</a-button>\n        <a-button class=\"action\" @click=\"print\">打印</a-button>\n      </template>\n      <div>\n        <div class=\"project-name\">项目名称</div>\n        <detail-list size=\"small\" style=\"max-width: 800px; margin-bottom: 8px\">\n          <detail-list-item term=\"项目ID\">20180724089</detail-list-item>\n          <detail-list-item term=\"负责人\">曲丽丽</detail-list-item>\n          <detail-list-item term=\"生效时间\">016-12-12 ~ 2017-12-12</detail-list-item>\n        </detail-list>\n        <a-steps :current=\"1\" progressDot>\n          <a-step title=\"创建项目\">\n            <a-step-item-group slot=\"description\">\n              <a-step-item title=\"曲丽丽\" icon=\"dingding-o\"/>\n              <a-step-item title=\"2016-12-12 12:32\"/>\n            </a-step-item-group>\n          </a-step>\n          <a-step title=\"部门初审\">\n            <a-step-item-group slot=\"description\">\n              <a-step-item title=\"周毛毛\" icon=\"dingding-o\" :iconStyle=\"{color: '#00A0E9'}\"/>\n              <a-step-item title=\"催一下\" :titleStyle=\"{color: '#00A0E9'}\"/>\n            </a-step-item-group>\n          </a-step>\n          <a-step title=\"财务复核\"></a-step>\n          <a-step title=\"完成\" ></a-step>\n        </a-steps>\n      </div>\n    </result>\n  </a-card>\n</template>\n\n<script>\nimport Result from '../../components/result/Result'\nimport DetailList from '../../components/tool/DetailList'\nimport AStepItem from '../../components/tool/AStepItem'\n\nconst AStepItemGroup = AStepItem.Group\nconst DetailListItem = DetailList.Item\nexport default {\n  name: 'Success',\n  components: {AStepItemGroup, AStepItem, DetailListItem, DetailList, Result},\n  data () {\n    return {\n      title: '提交成功',\n      description: '提交结果页用于反馈一系列操作任务的处理结果，\\n' +\n      ' 如果仅是简单操作，使用 Message 全局提示反馈即可。\\n' +\n      ' 本文字区域可以展示简单的补充说明，如果有类似展示\\n' +\n      ' “单据”的需求，下面这个灰色区域可以呈现比较复杂的内容。'\n    }\n  },\n  methods: {\n    print () {\n      window.print()\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"less\">\n.result-success{\n  .action:not(:first-child){\n    margin-left: 8px;\n  }\n  .project-name{\n    font-size: 16px;\n    color: @title-color;\n    font-weight: 500;\n    margin-bottom: 20px;\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/pages/role/index.vue",
    "content": "<template>\n  <div>\n    <a-card>\n      <div>\n        <a-space class=\"operator\">\n          <a-button type=\"primary\" @click=\"showModal('新增')\">新增</a-button>\n        </a-space>\n        <a-table\n            :columns=\"columns\"\n            :data-source=\"dataSource\"\n            :pagination=\"pagination\"\n            :loading=\"loading\"\n            @change=\"handleTableChange\"\n        >\n           <span slot=\"action\" slot-scope=\"text\">\n              <a @click=\"updateItem(text.id)\">编辑</a> |\n               <a-popconfirm\n                   title=\"您真的要删除这行数据么？\"\n                   ok-text=\"是\"\n                   cancel-text=\"否\"\n                   @confirm=\"deleteItem(text)\"\n               >\n              <a>删除</a>\n              </a-popconfirm>\n           </span>\n        </a-table>\n      </div>\n    </a-card>\n    <a-modal\n        :title=\"title\"\n        :visible=\"visible\"\n        :confirm-loading=\"confirmLoading\"\n        @ok=\"handleOk\"\n        @cancel=\"handleCancel\"\n        okText=\"提交\"\n        width=\"80%\"\n    >\n      <a-form :form=\"form\">\n        <a-form-item hidden>\n          <a-input v-decorator=\"['id',{ rules: [{ required: false}] }]\"/>\n        </a-form-item>\n        <a-form-item label=\"角色名称\">\n          <a-input\n              v-decorator=\"['name', { rules: [{ required: true, min:1,max:50,message:'内容长度在1到50之间' }] }]\"\n          />\n        </a-form-item>\n        <a-form-item label=\"角色编号\">\n          <a-input\n              v-decorator=\"['sn',{ rules: [{ required: true,min:1,max:30,message:'输入内容应在1到30位之间'  }] },]\"\n              placeholder=\"请输入角色编号\"\n          />\n        </a-form-item>\n        <a-form-item label=\"角色权限\">\n          <a-transfer\n              v-decorator=\"['permissions',{ rules: [{ required: this.title==='新增' }] },]\"\n              :operations=\"['加入权限', '移除权限']\"\n              :data-source=\"permissionList\"\n              :target-keys=\"targetKeys\"\n              :disabled=\"disabled\"\n              :show-search=\"showSearch\"\n              :filter-option=\"(inputValue, item) => item.title.indexOf(inputValue) !== -1\"\n              :show-select-all=\"false\"\n              @change=\"handleChange\"\n          >\n            <template\n                slot=\"children\"\n                slot-scope=\"{\n                props: { direction, filteredItems, selectedKeys, disabled: listDisabled },\n                on: { itemSelectAll, itemSelect },\n              }\"\n            >\n              <a-table\n                  :row-selection=\"getRowSelection({ disabled: listDisabled, selectedKeys, itemSelectAll, itemSelect })\"\n                  :columns=\"direction === 'left' ? leftColumns : rightColumns\"\n                  :data-source=\"filteredItems\"\n                  size=\"small\"\n                  :style=\"{ pointerEvents: listDisabled ? 'none' : null }\"\n                  :custom-row=\"({ key, disabled: itemDisabled }) => ({\n                  on: {\n                    click: () => {\n                      if (itemDisabled || listDisabled) return;\n                      itemSelect(key, !selectedKeys.includes(key));\n                    },\n                  },\n                })\"\n              />\n            </template>\n            <a-button\n                slot=\"footer\"\n                size=\"small\"\n                style=\"float:right;margin: 5px\"\n                @click=\"updateItem(reloadId)\"\n            >\n              刷新所有权限\n            </a-button>\n            <span slot=\"notFoundContent\">\n              没数据\n            </span>\n          </a-transfer>\n        </a-form-item>\n      </a-form>\n    </a-modal>\n  </div>\n</template>\n\n<script>\nimport * as role from \"@/services/role\"\nimport * as permission from \"@/services/permission\"\nimport difference from 'lodash/difference';\nimport validators from \"@/utils/validators\";\n\nconst columns = [\n  {\n    title: '编号',\n    dataIndex: 'id'\n  },\n  {\n    title: '角色名称',\n    dataIndex: 'name',\n  },\n  {\n    title: '角色编号',\n    dataIndex: 'sn',\n  },\n  {\n    title: '操作',\n    scopedSlots: {customRender: 'action'}\n  }\n]\nconst permissionTableColumns = [\n  {\n    dataIndex: 'name',\n    title: '权限名称',\n    ellipsis: true,\n  },\n  {\n    dataIndex: 'expression',\n    title: '权限表达式',\n    ellipsis: true,\n  },\n];\nexport default {\n  name: 'Department',\n  data() {\n    return {\n      validators,\n      // table\n      columns: columns,\n      dataSource: [],\n      selectedRows: [],\n      pagination: {},\n      loading: false,\n      // modal\n      title: '新增',\n      visible: false,\n      confirmLoading: false,\n      reloadId: null,\n      // modal form\n      form: this.$form.createForm(this, {name: 'coordinated'}),\n      // modal permission list\n      permissionList: [],\n      targetKeys: [],\n      selectedKeys: [],\n      disabled: false,\n      showSearch: false,\n      leftColumns: permissionTableColumns,\n      rightColumns: permissionTableColumns,\n    }\n  },\n  authorize: {\n    deleteRecord: 'delete'\n  },\n  created() {\n    this.fetch()\n  },\n  methods: {\n    // table\n    handleTableChange(pagination) {\n      const pager = {...this.pagination};\n      pager.current = pagination.current;\n      this.pagination = pager;\n      this.fetch({\n        size: pagination.pageSize,\n        page: pagination.current,\n      });\n    },\n    fetch(params = {\"page\": 1, \"size\": 10}) {\n      this.loading = true\n      role.list(params || {\"page\": 1, \"size\": 10}).then(({data}) => {\n        const res = data.data\n        const pagination = {...this.pagination};\n        pagination.total = res.total\n        pagination.current = params.page\n        this.dataSource = res.list.map((e, i) => ({key: i + \"\", ...e}))\n        this.pagination = pagination\n        this.loading = false\n      })\n    },\n    deleteItem(text) {\n      const title = '删除'\n      role.deleteItem(text.id).then(({data}) => {\n        if (data.code !== 200) {\n          this.$notification['error']({\n            message: title + '角色信息出现错误',\n            description: '建议检查网络连接或重新登陆',\n          });\n        }\n        this.$notification.success({\n          message: title + '成功',\n          description: title + '角色信息成功',\n        });\n        this.fetch({\"page\": this.pagination.current, \"size\": 10})\n      })\n    },\n    async updateItem(id) {\n      if(this.confirmLoading){\n        this.confirmLoading=false\n      }\n      this.targetKeys = []\n      this.permissionList = []\n      this.reloadId = id\n      await this.showModal('更改')\n      role.getDetail(id).then(({data}) => {\n        // 这里不能循环\n        this.form.setFieldsValue({\"id\": data.data[\"id\"]})\n        this.form.setFieldsValue({\"sn\": data.data[\"sn\"]})\n        this.form.setFieldsValue({\"name\": data.data[\"name\"]})\n        const {permissions} = data.data\n        if (!permissions) return;\n\n        for(let i=0;i<permissions.length;i++){\n          const ps = permissions[i];\n          this.targetKeys.push(this.permissionList.find(e=>e.id===ps.id).key)\n        }\n      })\n    },\n    // modal\n    async showModal(title) {\n      this.visible = true;\n      this.title = title || '新增'\n      this.form.resetFields()\n      this.targetKeys = []\n      await this.getAllPermissionList()\n    },\n    async handleOk() {\n      this.confirmLoading = true;\n      this.form.validateFields(async (err, values) => {\n        if (err) {\n          this.confirmLoading = false;\n          return;\n        }\n        let method = 'add';\n        if (values.id) method = 'update';\n        if(values.permissions && values.permissions.length>=1){\n          let arr =[]\n          for(let i=0;i<values.permissions.length;i++){\n            const e = values.permissions[i]\n            arr.push({\n              id:this.permissionList[e].id,\n              name:this.permissionList[e].name,\n              expression:this.permissionList[e].expression\n            })\n          }\n          values.permissions = arr\n        }\n        const {data} = await role[method](values)\n        this.confirmLoading = false;\n        if (data[\"code\"] !== 200) {\n          this.$notification['error']({\n            message: this.title + '角色信息出现错误',\n            description: data.message || '建议检查网络连接或重新登陆',\n          });\n        } else {\n          this.$notification.success({\n            message: this.title + '成功',\n            description: this.title + '角色信息成功',\n          });\n          this.visible = false\n          this.form.resetFields()\n          this.getAllPermissionList()\n          this.fetch({\"page\": this.pagination.current, \"size\": 10})\n        }\n      });\n    },\n    handleCancel() {\n      this.visible = false;\n      this.title = ''\n      this.confirmLoading = false\n      this.form.resetFields()\n    },\n    // modal transfer\n    getAllPermissionList() {\n      return permission.list({page: 1, size: 999999999}).then(({data}) => {\n        this.permissionList = data.data.list.map((e, i) => ({key: i + \"\", title: e.name, ...e}))\n      })\n    },\n    handleChange(targetKeys) {\n      this.targetKeys = targetKeys;\n    },\n    handleSelectChange(sourceSelectedKeys, targetSelectedKeys) {\n      this.selectedKeys = [...sourceSelectedKeys, ...targetSelectedKeys];\n    },\n    getRowSelection({disabled, selectedKeys, itemSelectAll, itemSelect}) {\n      return {\n        getCheckboxProps: item => ({props: {disabled: disabled || item.disabled}}),\n        onSelectAll(selected, selectedRows) {\n          const treeSelectedKeys = selectedRows\n              .filter(item => !item.disabled)\n              .map(({key}) => key);\n          const diffKeys = selected\n              ? difference(treeSelectedKeys, selectedKeys)\n              : difference(selectedKeys, treeSelectedKeys);\n          itemSelectAll(diffKeys, selected);\n        },\n        onSelect({key}, selected) {\n          itemSelect(key, selected);\n        },\n        selectedRowKeys: selectedKeys,\n      };\n    },\n  }\n}\n</script>\n\n<style lang=\"less\" scoped>\n.search {\n  margin-bottom: 54px;\n}\n\n.fold {\n  width: calc(100% - 216px);\n  display: inline-block\n}\n\n.operator {\n  margin-bottom: 18px;\n}\n\n@media screen and (max-width: 900px) {\n  .fold {\n    width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "front/src/plugins/authority-plugin.js",
    "content": "/**\n * 获取路由需要的权限\n * @param permissions\n * @param route\n * @returns {Permission}\n */\nconst getRoutePermission = (permissions, route) => permissions.find(item => item.id === route.meta.authority.permission)\n/**\n * 获取路由需要的角色\n * @param roles\n * @param route\n * @returns {Array[Role]}\n */\nconst getRouteRole = (roles, route) => {\n  const requiredRoles = route.meta.authority.role\n  return requiredRoles ? roles.filter(item => requiredRoles.findIndex(required => required === item.id) !== -1) : []\n}\n/**\n * 判断是否已为方法注入权限认证\n * @param method\n * @returns {boolean}\n */\nconst hasInjected = (method) => method.toString().indexOf('//--auth-inject') !== -1\n\n/**\n * 操作权限校验\n * @param authConfig\n * @param permission\n * @param role\n * @param permissions\n * @param roles\n * @returns {boolean}\n */\nconst auth = function(authConfig, permission, role, permissions, roles) {\n  const {check, type} = authConfig\n  if (check && typeof check === 'function') {\n    return check.apply(this, [permission, role, permissions, roles])\n  }\n  if (type === 'permission') {\n    return checkFromPermission(check, permission)\n  } else if (type === 'role') {\n    return checkFromRoles(check, role)\n  } else {\n    return checkFromPermission(check, permission) || checkFromRoles(check, role)\n  }\n}\n\n/**\n * 检查权限是否有操作权限\n * @param check 需要检查的操作权限\n * @param permission 权限\n * @returns {boolean}\n */\nconst checkFromPermission = function(check, permission) {\n  return permission && permission.operation && permission.operation.indexOf(check) !== -1\n}\n\n/**\n * 检查 roles 是否有操作权限\n * @param check 需要检查的操作权限\n * @param roles 角色数组\n * @returns {boolean}\n */\nconst checkFromRoles = function(check, roles) {\n  if (!roles) {\n    return false\n  }\n  for (let role of roles) {\n    const {operation} = role\n    if (operation && operation.indexOf(check) !== -1) {\n      return true\n    }\n  }\n  return false\n}\n\nconst checkInject = function (el, binding,vnode) {\n  const type = binding.arg\n  const check = binding.value\n  const instance = vnode.context\n  const $auth = instance.$auth\n  if (!$auth || !$auth(check, type)) {\n    addDisabled(el)\n  } else {\n    removeDisabled(el)\n  }\n}\n\nconst addDisabled = function (el) {\n  if (el.tagName === 'BUTTON') {\n    el.disabled = true\n  } else {\n    el.classList.add('disabled')\n  }\n  el.setAttribute('title', '无此权限')\n}\n\nconst removeDisabled = function (el) {\n  el.disabled = false\n  el.classList.remove('disabled')\n  el.removeAttribute('title')\n}\n\nconst AuthorityPlugin = {\n  install(Vue) {\n    Vue.directive('auth', {\n      bind(el, binding,vnode) {\n        setTimeout(() => checkInject(el, binding, vnode), 10)\n      },\n      componentUpdated(el, binding,vnode) {\n        setTimeout(() => checkInject(el, binding, vnode), 10)\n      },\n      unbind(el) {\n        removeDisabled(el)\n      }\n    })\n    Vue.mixin({\n      beforeCreate() {\n        if (this.$options.authorize) {\n          const authorize = this.$options.authorize\n          Object.keys(authorize).forEach(key => {\n            if (this.$options.methods[key]) {\n              const method = this.$options.methods[key]\n              if (!hasInjected(method)) {\n                let authConfig = authorize[key]\n                authConfig = (typeof authConfig === 'string') ? {check: authConfig} : authConfig\n                const {check, type, onFailure} = authConfig\n                this.$options.methods[key] = function () {\n                  //--auth-inject\n                  if (this.$auth(check, type)) {\n                    return method.apply(this, arguments)\n                  } else {\n                    if (onFailure && typeof onFailure === 'function') {\n                      this[`$${check}Failure`] = onFailure\n                      return this[`$${check}Failure`](check)\n                    } else {\n                      this.$message.error(`对不起，您没有操作权限：${check}`)\n                    }\n                    return 0\n                  }\n                }\n              }\n            }\n          })\n        }\n      },\n      methods: {\n        /**\n         * 操作权限校验\n         * @param check 需要校验的操作名\n         * @param type 校验类型，通过 permission 校验，还是通过 role 校验。\n         * 如未设置，则自动识别，如匹配到当前路由 permission 则 type = permission，否则 type = role\n         * @returns {boolean} 是否校验通过\n         */\n        $auth(check, type) {\n          const permissions = this.$store.getters['account/permissions']\n          const roles = this.$store.getters['account/roles']\n          const permission = getRoutePermission(permissions, this.$route)\n          const role = getRouteRole(roles, this.$route)\n          return auth.apply(this, [{check, type}, permission, role, permissions, roles])\n        }\n      }\n    })\n  }\n}\n\nexport default AuthorityPlugin\n"
  },
  {
    "path": "front/src/plugins/i18n-extend.js",
    "content": "// 语句模式\nconst MODE = {\n  STATEMENTS: 's', //语句模式\n  PHRASAL: 'p', //词组模式\n}\n\nconst VueI18nPlugin = {\n  install: function (Vue) {\n    Vue.mixin({\n      methods: {\n        $ta(syntaxKey, mode) {\n          let _mode = mode || MODE.STATEMENTS\n          let keys = syntaxKey.split('|')\n          let _this = this\n          let locale = this.$i18n.locale\n          let message = ''\n          let splitter = locale == 'US' ? ' ' : ''\n          // 拼接 message\n          keys.forEach(key => {\n            message += _this.$t(key) + splitter\n          })\n          // 英文环境语句模式下，转换单词大小写\n          if (keys.length > 0 && _mode == MODE.STATEMENTS && locale == 'US') {\n            message = message.charAt(0).toUpperCase() + message.toLowerCase().substring(1)\n          }\n          return message\n        }\n      }\n    })\n  }\n}\nexport default VueI18nPlugin\n"
  },
  {
    "path": "front/src/plugins/index.js",
    "content": "import VueI18nPlugin from './i18n-extend'\nimport AuthorityPlugin from './authority-plugin'\nimport TabsPagePlugin from './tabs-page-plugin'\n\nconst Plugins = {\n  install: function (Vue) {\n    Vue.use(VueI18nPlugin)\n    Vue.use(AuthorityPlugin)\n    Vue.use(TabsPagePlugin)\n  }\n}\nexport default Plugins\n"
  },
  {
    "path": "front/src/plugins/tabs-page-plugin.js",
    "content": "const TabsPagePlugin = {\n  install(Vue) {\n    Vue.mixin({\n      methods: {\n        $closePage(closeRoute, nextRoute) {\n          const event = new CustomEvent('page:close', {detail:{closeRoute, nextRoute}})\n          window.dispatchEvent(event)\n        },\n        $refreshPage(route) {\n          const path = typeof route === 'object' ? route.path : route\n          const event = new CustomEvent('page:refresh', {detail:{pageKey: path}})\n          window.dispatchEvent(event)\n        },\n        $openPage(route, title) {\n          this.$setPageTitle(route, title)\n          this.$router.push(route)\n        },\n        $setPageTitle(route, title) {\n          if (title) {\n            let path = typeof route === 'object' ? route.path : route\n            path = path && path.split('?')[0]\n            this.$store.commit('setting/setCustomTitle', {path, title})\n          }\n        }\n      },\n      computed: {\n        customTitle() {\n          const customTitles = this.$store.state.setting.customTitles\n          const path = this.$route.path.split('?')[0]\n          const custom = customTitles.find(item => item.path === path)\n          return custom && custom.title\n        }\n      }\n    })\n  }\n}\n\nexport default TabsPagePlugin\n"
  },
  {
    "path": "front/src/router/async/config.async.js",
    "content": "import routerMap from './router.map'\nimport {parseRoutes} from '@/utils/routerUtil'\n\n// 异步路由配置\nconst routesConfig = [\n  'login',\n  'root',\n  {\n    router: 'exp404',\n    path: '*',\n    name: '404'\n  },\n  {\n    router: 'exp403',\n    path: '/403',\n    name: '403'\n  }\n]\n\nconst options = {\n  routes: parseRoutes(routesConfig, routerMap)\n}\n\nexport default options\n"
  },
  {
    "path": "front/src/router/async/router.map.js",
    "content": "// 视图组件\nconst view = {\n  tabs: () => import('@/layouts/tabs'),\n  blank: () => import('@/layouts/BlankView'),\n  page: () => import('@/layouts/PageView')\n}\n\n// 路由组件注册\nconst routerMap = {\n  login: {\n    authority: '*',\n    path: '/login',\n    component: () => import('@/pages/login')\n  },\n  root: {\n    path: '/',\n    name: '首页',\n    redirect: '/login',\n    component: view.tabs\n  },\n  dashboard: {\n    name: 'Dashboard',\n    component: view.blank\n  },\n  workplace: {\n    name: '工作台',\n    component: () => import('@/pages/dashboard/workplace')\n  },\n  result: {\n    name: '结果页',\n    icon: 'check-circle-o',\n    component: view.page\n  },\n  success: {\n    name: '成功',\n    component: () => import('@/pages/result/Success')\n  },\n  error: {\n    name: '失败',\n    component: () => import('@/pages/result/Error')\n  },\n  exception: {\n    name: '异常页',\n    icon: 'warning',\n    component: view.blank\n  },\n  exp403: {\n    authority: '*',\n    name: 'exp403',\n    path: '403',\n    component: () => import('@/pages/exception/403')\n  },\n  exp404: {\n    name: 'exp404',\n    path: '404',\n    component: () => import('@/pages/exception/404')\n  },\n  exp500: {\n    name: 'exp500',\n    path: '500',\n    component: () => import('@/pages/exception/500')\n  },\n  components: {\n    name: '小组件',\n    icon: 'appstore-o',\n    component: view.page\n  },\n  taskCard: {\n    name: '任务卡片',\n    component: () => import('@/pages/components/TaskCard')\n  },\n  palette: {\n    name: '颜色复选框',\n    component: () => import('@/pages/components/Palette')\n  }\n}\nexport default routerMap\n\n"
  },
  {
    "path": "front/src/router/config.js",
    "content": "import TabsView from '@/layouts/tabs/TabsView'\nimport PageView from '@/layouts/PageView'\n\n// 路由配置\nconst options = {\n  routes: [\n    {\n      path: '/login',\n      name: '登录页',\n      component: () => import('@/pages/login')\n    },\n    {\n      path: '*',\n      name: '404',\n      component: () => import('@/pages/exception/404'),\n    },\n    {\n      path: '/403',\n      name: '403',\n      component: () => import('@/pages/exception/403'),\n    },\n    {\n      path: '/',\n      name: '首页',\n      component: TabsView,\n      redirect: '/login',\n      children: [\n        {\n          path: 'dashboard/workplace',\n          name: 'Dashboard',\n          meta: {\n            icon: 'dashboard'\n          },\n          component: () => import('@/pages/dashboard/workplace')\n        },\n        {\n\n          path: '/system',\n          name: '系统设置',\n          meta: {\n            icon: 'setting',\n            page: {\n              cacheAble: false\n            }\n          },\n          component: PageView,\n          children: [\n            {\n            path:'role',\n            name:'角色管理',\n            component:()=>import('@/pages/role/index')\n            },\n            {\n              path:'permission',\n              name:'权限管理',\n              component:()=>import('@/pages/permission/index')\n            },\n            {\n              path:'department',\n              name:'部门管理',\n              component:()=>import('@/pages/department/index')\n            },\n            {\n              path:'employee',\n              name:'员工管理',\n              component:()=>import('@/pages/employee/index')\n            },\n            {\n              path: 'dictionary/contents',\n              name: '字典列表',\n              component:()=>import('@/pages/dictionary/contents')\n            },\n            {\n              path: 'dictionary/details',\n              name: '字典明细',\n              component:()=>import('@/pages/dictionary/details')\n            }\n          ]\n        },\n        {\n          path: '/customer',\n          name: '客户管理',\n          meta: {\n            icon: 'team',\n            page: {\n              cacheAble: false\n            }\n          },\n          component: PageView,\n          children: [\n            {\n              path:'manager',\n              name:'潜在客户管理',\n              component:()=>import('@/pages/customer/manager')\n            },\n            {\n              path:'official',\n              name:'正式客户管理',\n              component:()=>import('@/pages/customer/official')\n            },\n            {\n              path:'resource',\n              name:'客户资源池',\n              component:()=>import('@/pages/customer/resource')\n            },\n            {\n              path:'followHistory',\n              name:'跟进历史',\n              component:()=>import('@/pages/customer/followHistory')\n            },\n            {\n              path:'handoverHistory',\n              name:'移交历史查询',\n              component:()=>import('@/pages/customer/handoverHistory')\n            },\n          ]\n        },\n        {\n          path: '/analysis',\n          name: '统计分析',\n          meta: {\n            icon: 'monitor',\n            page: {\n              cacheAble: false\n            }\n          },\n          component: ()=>import('@/pages/analysis/index'),\n        },\n        {\n          name: '关于创作者',\n          path: 'antdv',\n          meta: {\n            icon: 'ant-design',\n            link: 'https://msy.plus'\n          }\n        }\n      ]\n    },\n  ]\n}\n\nexport default options\n"
  },
  {
    "path": "front/src/router/guards.js",
    "content": "import {hasAuthority} from '@/utils/authority-utils'\nimport {loginIgnore} from '@/router/index'\nimport {checkAuthorization} from '@/utils/request'\nimport NProgress from 'nprogress'\n\nNProgress.configure({ showSpinner: false })\n\n/**\n * 进度条开始\n * @param to\n * @param form\n * @param next\n */\nconst progressStart = (to, from, next) => {\n  // start progress bar\n  if (!NProgress.isStarted()) {\n    NProgress.start()\n  }\n  next()\n}\n\n/**\n * 登录守卫\n * @param to\n * @param form\n * @param next\n * @param options\n */\nconst loginGuard = (to, from, next, options) => {\n  const {message} = options\n  if (!loginIgnore.includes(to) && !checkAuthorization()) {\n    message.warning('登录已失效，请重新登录')\n    next({path: '/login'})\n  } else {\n    next()\n  }\n}\n\n/**\n * 权限守卫\n * @param to\n * @param form\n * @param next\n * @param options\n */\nconst authorityGuard = (to, from, next, options) => {\n  const {store, message} = options\n  const permissions = store.getters['account/permissions']\n  const roles = store.getters['account/roles']\n  if (!hasAuthority(to, permissions, roles)) {\n    message.warning(`对不起，您无权访问页面: ${to.fullPath}，请联系管理员`)\n    next({path: '/403'})\n    // NProgress.done()\n  } else {\n    next()\n  }\n}\n\n/**\n * 混合导航模式下一级菜单跳转重定向\n * @param to\n * @param from\n * @param next\n * @param options\n * @returns {*}\n */\nconst redirectGuard = (to, from, next, options) => {\n  const {store} = options\n  const getFirstChild = (routes) => {\n    const route = routes[0]\n    if (!route.children || route.children.length === 0) {\n      return route\n    }\n    return getFirstChild(route.children)\n  }\n  if (store.state.setting.layout === 'mix') {\n    const firstMenu = store.getters['setting/firstMenu']\n    if (firstMenu.find(item => item.fullPath === to.fullPath)) {\n      store.commit('setting/setActivatedFirst', to.fullPath)\n      const subMenu = store.getters['setting/subMenu']\n      if (subMenu.length > 0) {\n        const redirect = getFirstChild(subMenu)\n        return next({path: redirect.fullPath})\n      }\n    }\n  }\n  next()\n}\n\n/**\n * 进度条结束\n * @param to\n * @param form\n * @param options\n */\nconst progressDone = () => {\n  // finish progress bar\n  NProgress.done()\n}\n\nexport default {\n  beforeEach: [progressStart, loginGuard, authorityGuard, redirectGuard],\n  afterEach: [progressDone]\n}\n"
  },
  {
    "path": "front/src/router/i18n.js",
    "content": "module.exports = {\n  messages: {\n    CN: {\n      home: {name: '首页'},\n    },\n    US: {\n      home: {name: 'home'},\n    },\n    HK: {\n      home: {name: '首頁'},\n      dashboard: {\n        name: 'Dashboard',\n        workplace: {name: '工作台'},\n        analysis: {name: '分析頁'}\n      },\n      form: {\n        name: '表單頁',\n        basic: {name: '基礎表單'},\n        step: {name: '分步表單'},\n        advance: {name: '分步表單'}\n      },\n      list: {\n        name: '列表頁',\n        query: {name: '查詢表格'},\n        primary: {name: '標準列表'},\n        card: {name: '卡片列表'},\n        search: {\n          name: '搜索列表',\n          article: {name: '文章'},\n          application: {name: '應用'},\n          project: {name: '項目'}\n        }\n      },\n      details: {\n        name: '詳情頁',\n        basic: {name: '基礎詳情頁'},\n        advance: {name: '高級詳情頁'}\n      },\n      result: {\n        name: '結果頁',\n        success: {name: '成功'},\n        error: {name: '失敗'}\n      },\n      exception: {\n        name: '異常頁',\n        404: {name: '404'},\n        403: {name: '403'},\n        500: {name: '500'}\n      },\n      components: {\n        name: '小組件',\n        taskCard: {name: '任務卡片'},\n        palette: {name: '顏色複選框'}\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "front/src/router/index.js",
    "content": "import Vue from 'vue'\nimport Router from 'vue-router'\nimport {formatRoutes} from '@/utils/routerUtil'\n\nVue.use(Router)\n\n// 不需要登录拦截的路由配置\nconst loginIgnore = {\n  names: ['404', '403'],      //根据路由名称匹配\n  paths: ['/login'],   //根据路由fullPath匹配\n  /**\n   * 判断路由是否包含在该配置中\n   * @param route vue-router 的 route 对象\n   * @returns {boolean}\n   */\n  includes(route) {\n    return this.names.includes(route.name) || this.paths.includes(route.path)\n  }\n}\n\n/**\n * 初始化路由实例\n * @param isAsync 是否异步路由模式\n * @returns {VueRouter}\n */\nfunction initRouter(isAsync) {\n  const options = isAsync ? require('./async/config.async').default : require('./config').default\n  formatRoutes(options.routes)\n  return new Router(options)\n}\nexport {loginIgnore, initRouter}\n"
  },
  {
    "path": "front/src/services/analysis.js",
    "content": "import {METHOD, request} from '@/utils/request'\nimport {ANALYSIS} from './api'\n\nexport async function list(params) {\n    return request(ANALYSIS, METHOD.POST, params)\n}\n"
  },
  {
    "path": "front/src/services/api.js",
    "content": "//跨域代理前缀\n// const API_PROXY_PREFIX='/api'\nconst BASE_URL = process.env.NODE_ENV === 'production' ?\n    '/spring-boot-api-seeding'\n    : process.env.VUE_APP_API_BASE_URL\n// const BASE_URL = process.env.VUE_APP_API_BASE_URL\n// const BASE_URL = process.env.VUE_APP_API_DEV_URL\nmodule.exports = {\n  BASE_URL,\n  // LOGIN: `${BASE_URL}/login`,\n  LOGIN: `${BASE_URL}/account/token`,\n  // ROUTES: `${BASE_URL}/routes`,\n  GOODS: `${BASE_URL}/goods`,\n  GOODS_COLUMNS: `${BASE_URL}/columns`,\n  DEPARTMENT:`${BASE_URL}/department`, // method CRUD\n  ROLE:`${BASE_URL}/role`, // method CRUD\n  PERMISSION:`${BASE_URL}/permission`, // method CRUD\n  EMPLOYEE:`${BASE_URL}/employee`, // method CRUD\n  DICTIONARY_CONTENTS:`${BASE_URL}/dictionary/contents`, // method CRUD\n  DICTIONARY_DETAILS:`${BASE_URL}/dictionary/details`, // method CRUD\n  CUSTOMER_MANAGER:`${BASE_URL}/customer/manager`, // method CRUD\n  CUSTOMER_HANDOVER:`${BASE_URL}/customer/handover`, // method CRUD\n  CUSTOMER_FOLLOW_UP_HISTORY:`${BASE_URL}/customer/follow/up/history`, // method CRUD\n  ANALYSIS:`${BASE_URL}/analysis`, // method CRUD\n}\n"
  },
  {
    "path": "front/src/services/customerFollowUpHistory.js",
    "content": "import {request, METHOD} from '@/utils/request'\nimport {CUSTOMER_FOLLOW_UP_HISTORY} from './api'\n\n/**\n * 获取员工管理明细信息，分页获取\n * @param page {{size: number, page: number}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function list(page ) {\n    return request(CUSTOMER_FOLLOW_UP_HISTORY, METHOD.GET, page||{\"page\": 1, \"size\": 10})\n}\n\n/**\n * 删除一个员工管理明细信息\n * @param id {string|number}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function deleteItem(id) {\n    return request(CUSTOMER_FOLLOW_UP_HISTORY + '/' + id, METHOD.DELETE)\n}\n\n/**\n * 获取单个员工管理明细的详细信息\n * @param id\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function getDetail(id) {\n    return request(CUSTOMER_FOLLOW_UP_HISTORY + '/' + id, METHOD.GET)\n}\n\n/**\n * 修改员工管理明细信息\n * @param object {{\n  \"comment\": \"string\",\n  \"customerid\": 0,\n  \"inputuser\": 0,\n  \"id\":0,\n  \"tracedetails\": \"string\",\n  \"traceresult\": 0,\n  \"tracetime\": \"2021-05-21T12:29:28.902Z\",\n  \"tracetype\": 0,\n  \"type\": 0\n}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function update(object) {\n    return request(CUSTOMER_FOLLOW_UP_HISTORY, METHOD.PUT, object)\n}\n\n/**\n * 添加员工管理明细信息\n * @param object {{\n  \"comment\": \"string\",\n  \"customerid\": 0,\n  \"inputuser\": 0,\n  \"tracedetails\": \"string\",\n  \"traceresult\": 0,\n  \"tracetime\": \"2021-05-21T12:29:28.902Z\",\n  \"tracetype\": 0,\n  \"type\": 0\n}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function add(object) {\n    return request(CUSTOMER_FOLLOW_UP_HISTORY, METHOD.POST, object)\n}\n\nexport const type ={\n    1:\"客户跟进历史\",\n    0:\"潜在开发计划\"\n}\nexport const traceresult={\n    3:\"优\",\n    2:\"中\",\n    1:\"差\"\n}\n"
  },
  {
    "path": "front/src/services/customerHandover.js",
    "content": "import {request, METHOD} from '@/utils/request'\nimport {CUSTOMER_HANDOVER} from './api'\n\n/**\n * 获取员工管理明细信息，分页获取\n * @param page {{size: number, page: number}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function list(page ) {\n    return request(CUSTOMER_HANDOVER, METHOD.GET, page||{\"page\": 1, \"size\": 10})\n}\n\n/**\n * 删除一个员工管理明细信息\n * @param id {string|number}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function deleteItem(id) {\n    return request(CUSTOMER_HANDOVER + '/' + id, METHOD.DELETE)\n}\n\n/**\n * 获取单个员工管理明细的详细信息\n * @param id\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function getDetail(id) {\n    return request(CUSTOMER_HANDOVER + '/' + id, METHOD.GET)\n}\n\n/**\n * 修改员工管理明细信息\n * @param object {{\n  \"customerid\": 0,\n  \"newseller\": 0,\n  \"oldseller\": 0,\n  \"transreason\": \"string\",\n  \"transtime\": \"2021-05-21T06:33:22.300Z\",\n  \"transuser\": 0\n}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function update(object) {\n    return request(CUSTOMER_HANDOVER, METHOD.PUT, object)\n}\n\n/**\n * 添加员工管理明细信息\n * @param object {{\n  \"customerid\": 0,\n  \"id\": 0,\n  \"newseller\": 0,\n  \"oldseller\": 0,\n  \"transreason\": \"string\",\n  \"transtime\": \"2021-05-21T06:33:22.300Z\",\n  \"transuser\": 0\n}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function add(object) {\n    return request(CUSTOMER_HANDOVER, METHOD.POST, object)\n}\n"
  },
  {
    "path": "front/src/services/customerManager.js",
    "content": "import {request, METHOD} from '@/utils/request'\nimport {CUSTOMER_MANAGER} from './api'\n\n/**\n * 获取员工管理明细信息，分页获取\n * @param page {{size: number, page: number}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function list(page ) {\n    return request(CUSTOMER_MANAGER, METHOD.GET, page||{\"page\": 1, \"size\": 10})\n}\n\n/**\n * 删除一个员工管理明细信息\n * @param id {string|number}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function deleteItem(id) {\n    return request(CUSTOMER_MANAGER + '/' + id, METHOD.DELETE)\n}\n\n/**\n * 获取单个员工管理明细的详细信息\n * @param id\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function getDetail(id) {\n    return request(CUSTOMER_MANAGER + '/' + id, METHOD.GET)\n}\n\n/**\n * 修改员工管理明细信息\n * @param object {{\n    \"age\": 18,\n    \"gender\": 1,\n    \"id\": 1,\n    \"inputtime\": 1621539029000,\n    \"inputuser\": 1,\n    \"job\": 3,\n    \"name\": \"马云\",\n    \"positivetime\": 1621539062000,\n    \"qq\": \"100001\",\n    \"seller\": 1,\n    \"source\": 17,\n    \"status\": 1,\n    \"tel\": \"18888888888\"\n  }}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function update(object) {\n    return request(CUSTOMER_MANAGER, METHOD.PUT, object)\n}\n\n/**\n * 添加员工管理明细信息\n * @param object {{\n    \"age\": 18,\n    \"gender\": 1,\n    \"inputuser\": 1,\n    \"job\": 3,\n    \"name\": \"马云\",\n    \"positivetime\": 1621539062000,\n    \"qq\": \"100001\",\n    \"seller\": 1,\n    \"source\": 17,\n    \"status\": 1,\n    \"tel\": \"18888888888\"\n  }}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function add(object) {\n    return request(CUSTOMER_MANAGER, METHOD.POST, object)\n}\n\nexport const statusMap = {\n    \"-2\": \"流失\",\n    \"-1\": \"开发失败\",\n    \"0\": \"潜在客户\",\n    \"1\": \"正式客户\",\n    \"2\": \"资源池客户\",\n}\n"
  },
  {
    "path": "front/src/services/dataSource.js",
    "content": "import {GOODS, GOODS_COLUMNS} from './api'\nimport {METHOD, request} from '@/utils/request'\n\nexport async function goodsList(params) {\n  return request(GOODS, METHOD.GET, params)\n}\n\nexport async function goodsColumns() {\n  return request(GOODS_COLUMNS, METHOD.GET)\n}\n\nexport default {goodsList, goodsColumns}"
  },
  {
    "path": "front/src/services/department.js",
    "content": "import {request, METHOD} from '@/utils/request'\nimport {DEPARTMENT} from './api'\n\n/**\n * 获取部门信息，分页获取\n * @param page {{size: number, page: number}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function list(page = {\n    \"page\": 1,\n    \"size\": 10\n}) {\n    return request(DEPARTMENT, METHOD.GET, page)\n}\n\n/**\n * 添加部门信息\n * @param object {{\n  \"name\": \"测试部门\",\n  \"sn\": \"test department\"\n}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function add(object) {\n    return request(DEPARTMENT, METHOD.POST, object)\n}\n\n/**\n * 删除一个部门信息\n * @param id {string|number}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function deleteItem(id) {\n    return request(DEPARTMENT + '/' + id, METHOD.DELETE)\n}\n\n/**\n * 获取单个部门的详细信息\n * @param id\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function getDetail(id) {\n    return request(DEPARTMENT + '/' + id, METHOD.GET)\n}\n\n/**\n *\n * @param object {{\n   \"id\":1,\n  \"name\": \"name\",\n  \"sn\": \"test department\"\n}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function update(object) {\n    return request(DEPARTMENT, METHOD.PATCH, object)\n}\n\n"
  },
  {
    "path": "front/src/services/dictionaryContents.js",
    "content": "import {request, METHOD} from '@/utils/request'\nimport {DICTIONARY_CONTENTS} from './api'\n\n/**\n * 获取字典列表信息，分页获取\n * @param page {{size: number, page: number}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function list(page ) {\n    return request(DICTIONARY_CONTENTS, METHOD.GET, page||{\"page\": 1, \"size\": 10})\n}\n\n/**\n * 删除一个字典列表信息\n * @param id {string|number}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function deleteItem(id) {\n    return request(DICTIONARY_CONTENTS + '/' + id, METHOD.DELETE)\n}\n\n/**\n * 获取单个字典列表的详细信息\n * @param id\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function getDetail(id) {\n    return request(DICTIONARY_CONTENTS + '/' + id, METHOD.GET)\n}\n\n/**\n * 修改字典列表信息\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function update(object) {\n    return request(DICTIONARY_CONTENTS, METHOD.PUT, object)\n}\n\n/**\n * 添加字典列表信息\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function add(object) {\n    return request(DICTIONARY_CONTENTS, METHOD.POST, object)\n}\n"
  },
  {
    "path": "front/src/services/dictionaryDetails.js",
    "content": "import {request, METHOD} from '@/utils/request'\nimport {DICTIONARY_DETAILS} from './api'\n\n/**\n * 获取字典明细信息，分页获取\n * @param page {{size: number, page: number}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function list(page ) {\n    return request(DICTIONARY_DETAILS, METHOD.GET, page||{\"page\": 1, \"size\": 10})\n}\n\n/**\n * 删除一个字典明细信息\n * @param id {string|number}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function deleteItem(id) {\n    return request(DICTIONARY_DETAILS + '/' + id, METHOD.DELETE)\n}\n\n/**\n * 获取单个字典明细的详细信息\n * @param id\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function getDetail(id) {\n    return request(DICTIONARY_DETAILS + '/' + id, METHOD.GET)\n}\n\n/**\n * 修改字典明细信息\n * @param object {{\n  \"expression\": \"string\",\n  \"id\": 0,\n  \"name\": \"string\"\n}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function update(object) {\n    return request(DICTIONARY_DETAILS, METHOD.PUT, object)\n}\n\n/**\n * 添加字典明细信息\n * @param object {{\n  \"expression\": \"string\",\n  \"name\": \"string\"\n}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function add(object) {\n    return request(DICTIONARY_DETAILS, METHOD.POST, object)\n}\n"
  },
  {
    "path": "front/src/services/employee.js",
    "content": "import {request, METHOD} from '@/utils/request'\nimport {EMPLOYEE} from './api'\n\n/**\n * 获取角色信息，分页获取\n * @param page {{size: number, page: number}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function list(page ) {\n    return request(EMPLOYEE, METHOD.GET, page||{\"page\": 1, \"size\": 10})\n}\n\n/**\n * 添加角色信息\n * @param object {{\n  \"admin\": 0,\n  \"age\": 22,\n  \"dept\": 2,\n  \"email\": \"c@c.c\",\n  \"hiredate\": \"2021-05-16T01:09:17.045Z\",\n  \"name\": \"testErT3\",\n  \"password\": \"string\",\n  \"roleIds\": [4,5,8,9],\n  \"state\": 0\n}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function add(object) {\n    return request(EMPLOYEE, METHOD.POST, object)\n}\n\n/**\n * 删除一个角色信息\n * @param id {string|number}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function deleteItem(id) {\n    return request(EMPLOYEE + '/' + id, METHOD.DELETE)\n}\n\n/**\n * 获取单个角色的详细信息\n * @param id\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function getDetail(id) {\n    return request(EMPLOYEE + '/' + id, METHOD.GET)\n}\n\n/**\n * 修改角色信息\n * @param object {{\n  \"id\":1008,\n  \"admin\": 0,\n  \"age\": 22,\n  \"dept\": 2,\n  \"email\": \"c@c.c\",\n  \"hiredate\": \"2021-05-16T01:09:17.045Z\",\n  \"name\": \"testErT3\",\n  \"password\": \"string\",\n  \"roleIds\": [8,9,10,11,12],\n  \"state\": 0\n}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function update(object) {\n    return request(EMPLOYEE, METHOD.PUT, object)\n}\n\n"
  },
  {
    "path": "front/src/services/index.js",
    "content": "import userService from './user'\nimport dataSource from './dataSource'\n\nexport {\n  userService,\n  dataSource\n}\n"
  },
  {
    "path": "front/src/services/permission.js",
    "content": "import {request, METHOD} from '@/utils/request'\nimport {PERMISSION} from './api'\n\n/**\n * 获取权限信息，分页获取\n * @param page {{size: number, page: number}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function list(page ) {\n    return request(PERMISSION, METHOD.GET, page||{\"page\": 1, \"size\": 10})\n}\n\n/**\n * 删除一个权限信息\n * @param id {string|number}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function deleteItem(id) {\n    return request(PERMISSION + '/' + id, METHOD.DELETE)\n}\n\n/**\n * 获取单个权限的详细信息\n * @param id\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function getDetail(id) {\n    return request(PERMISSION + '/' + id, METHOD.GET)\n}\n\n/**\n * 修改权限信息\n * @param object {{\n  \"expression\": \"string\",\n  \"id\": 0,\n  \"name\": \"string\"\n}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function update(object) {\n    return request(PERMISSION, METHOD.PUT, object)\n}\n\n/**\n * 添加权限信息\n * @param object {{\n  \"expression\": \"string\",\n  \"name\": \"string\"\n}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function add(object) {\n    return request(PERMISSION, METHOD.POST, object)\n}\n"
  },
  {
    "path": "front/src/services/role.js",
    "content": "import {request, METHOD} from '@/utils/request'\nimport {ROLE} from './api'\n\n/**\n * 获取角色信息，分页获取\n * @param page {{size: number, page: number}}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function list(page ) {\n    return request(ROLE, METHOD.GET, page||{\"page\": 1, \"size\": 10})\n}\n\n/**\n * 添加角色信息\n * @param object {{\n        \"name\": \"市场经理\",\n        \"permission\": \"1\",\n        \"sn\": \"Market Manager\"\n    }}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function add(object) {\n    return request(ROLE, METHOD.POST, object)\n}\n\n/**\n * 删除一个角色信息\n * @param id {string|number}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function deleteItem(id) {\n    return request(ROLE + '/' + id, METHOD.DELETE)\n}\n\n/**\n * 获取单个角色的详细信息\n * @param id\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function getDetail(id) {\n    return request(ROLE + '/' + id, METHOD.GET)\n}\n\n/**\n * 修改角色信息\n * @param object {{\n        \"id\": 4,\n        \"name\": \"市场经理\",\n        \"permission\": \"1\",\n        \"sn\": \"Market Manager\"\n    }}\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function update(object) {\n    return request(ROLE, METHOD.PUT, object)\n}\n\n"
  },
  {
    "path": "front/src/services/user.js",
    "content": "import {LOGIN, ROUTES} from '@/services/api'\nimport {request, METHOD, removeAuthorization} from '@/utils/request'\n\n/**\n * 登录服务\n * @param name 账户名\n * @param password 账户密码\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function login(name, password) {\n  return request(LOGIN, METHOD.POST, {\n    name: name,\n    password: password\n  })\n}\n\n/**\n * 登出服务\n * @returns {Promise<AxiosResponse<T>>}\n */\nexport async function logoutRequest() {\n  return request(LOGIN, METHOD.DELETE,)\n}\n\nexport async function getRoutesConfig() {\n  return request(ROUTES, METHOD.GET)\n}\n\n/**\n * 退出登录 使得token失效\n */\nexport function logout() {\n  logoutRequest().then(()=>undefined);\n  localStorage.removeItem(process.env.VUE_APP_ROUTES_KEY)\n  localStorage.removeItem(process.env.VUE_APP_PERMISSIONS_KEY)\n  localStorage.removeItem(process.env.VUE_APP_ROLES_KEY)\n  removeAuthorization()\n}\nexport default {\n  login,\n  logout,\n  getRoutesConfig\n}\n"
  },
  {
    "path": "front/src/store/index.js",
    "content": "import Vue from 'vue'\nimport Vuex from 'vuex'\nimport modules from './modules'\n\nVue.use(Vuex)\nconst store = new Vuex.Store({modules})\n\nexport default store\n"
  },
  {
    "path": "front/src/store/modules/account.js",
    "content": "export default {\n  namespaced: true,\n  state: {\n    user: undefined,\n    permissions: null,\n    roles: null,\n    routesConfig: null\n  },\n  getters: {\n    user: state => {\n      if (!state.user) {\n        try {\n          const user = localStorage.getItem(process.env.VUE_APP_USER_KEY)\n          state.user = JSON.parse(user)\n        } catch (e) {\n          console.error(e)\n        }\n      }\n      return state.user\n    },\n    permissions: state => {\n      if (!state.permissions) {\n        try {\n          const permissions = localStorage.getItem(process.env.VUE_APP_PERMISSIONS_KEY)\n          state.permissions = JSON.parse(permissions)\n          state.permissions = state.permissions ? state.permissions : []\n        } catch (e) {\n          console.error(e.message)\n        }\n      }\n      return state.permissions\n    },\n    roles: state => {\n      if (!state.roles) {\n        try {\n          const roles = localStorage.getItem(process.env.VUE_APP_ROLES_KEY)\n          state.roles = JSON.parse(roles)\n          state.roles = state.roles ? state.roles : []\n        } catch (e) {\n          console.error(e.message)\n        }\n      }\n      return state.roles\n    },\n    routesConfig: state => {\n      if (!state.routesConfig) {\n        try {\n          const routesConfig = localStorage.getItem(process.env.VUE_APP_ROUTES_KEY)\n          state.routesConfig = JSON.parse(routesConfig)\n          state.routesConfig = state.routesConfig ? state.routesConfig : []\n        } catch (e) {\n          console.error(e.message)\n        }\n      }\n      return state.routesConfig\n    }\n  },\n  mutations: {\n    setUser (state, user) {\n      state.user = user\n      localStorage.setItem(process.env.VUE_APP_USER_KEY, JSON.stringify(user))\n    },\n    setPermissions(state, permissions) {\n      state.permissions = permissions\n      localStorage.setItem(process.env.VUE_APP_PERMISSIONS_KEY, JSON.stringify(permissions))\n    },\n    setRoles(state, roles) {\n      state.roles = roles\n      localStorage.setItem(process.env.VUE_APP_ROLES_KEY, JSON.stringify(roles))\n    },\n    setRoutesConfig(state, routesConfig) {\n      state.routesConfig = routesConfig\n      localStorage.setItem(process.env.VUE_APP_ROUTES_KEY, JSON.stringify(routesConfig))\n    }\n  }\n}\n"
  },
  {
    "path": "front/src/store/modules/index.js",
    "content": "import account from './account'\nimport setting from './setting'\n\nexport default {account, setting}"
  },
  {
    "path": "front/src/store/modules/setting.js",
    "content": "import config from '@/config'\nimport {ADMIN} from '@/config/default'\nimport {formatFullPath} from '@/utils/i18n'\nimport {filterMenu} from '@/utils/authority-utils'\nimport {getLocalSetting} from '@/utils/themeUtil'\nimport deepClone from 'lodash.clonedeep'\n\nconst localSetting = getLocalSetting(true)\nconst customTitlesStr = sessionStorage.getItem(process.env.VUE_APP_TBAS_TITLES_KEY)\nconst customTitles = (customTitlesStr && JSON.parse(customTitlesStr)) || []\n\nexport default {\n  namespaced: true,\n  state: {\n    isMobile: false,\n    animates: ADMIN.animates,\n    palettes: ADMIN.palettes,\n    pageMinHeight: 0,\n    menuData: [],\n    activatedFirst: undefined,\n    customTitles,\n    ...config,\n    ...localSetting\n  },\n  getters: {\n    menuData(state, getters, rootState) {\n      if (state.filterMenu) {\n        const {permissions, roles} = rootState.account\n        return filterMenu(deepClone(state.menuData), permissions, roles)\n      }\n      return state.menuData\n    },\n    firstMenu(state, getters) {\n      const {menuData} = getters\n      if (menuData.length > 0 && !menuData[0].fullPath) {\n        formatFullPath(menuData)\n      }\n      return menuData.map(item => {\n        const menuItem = {...item}\n        delete menuItem.children\n        return menuItem\n      })\n    },\n    subMenu(state) {\n      const {menuData, activatedFirst} = state\n      if (menuData.length > 0 && !menuData[0].fullPath) {\n        formatFullPath(menuData)\n      }\n      const current = menuData.find(menu => menu.fullPath === activatedFirst)\n      return current && current.children || []\n    }\n  },\n  mutations: {\n    setDevice (state, isMobile) {\n      state.isMobile = isMobile\n    },\n    setTheme (state, theme) {\n      state.theme = theme\n    },\n    setLayout (state, layout) {\n      state.layout = layout\n    },\n    setMultiPage (state, multiPage) {\n      state.multiPage = multiPage\n    },\n    setAnimate (state, animate) {\n      state.animate = animate\n    },\n    setWeekMode(state, weekMode) {\n      state.weekMode = weekMode\n    },\n    setFixedHeader(state, fixedHeader) {\n      state.fixedHeader = fixedHeader\n    },\n    setFixedSideBar(state, fixedSideBar) {\n      state.fixedSideBar = fixedSideBar\n    },\n    setLang(state, lang) {\n      state.lang = lang\n    },\n    setHideSetting(state, hideSetting) {\n      state.hideSetting = hideSetting\n    },\n    correctPageMinHeight(state, minHeight) {\n      state.pageMinHeight += minHeight\n    },\n    setMenuData(state, menuData) {\n      state.menuData = menuData\n    },\n    setAsyncRoutes(state, asyncRoutes) {\n      state.asyncRoutes = asyncRoutes\n    },\n    setPageWidth(state, pageWidth) {\n      state.pageWidth = pageWidth\n    },\n    setActivatedFirst(state, activatedFirst) {\n      state.activatedFirst = activatedFirst\n    },\n    setFixedTabs(state, fixedTabs) {\n      state.fixedTabs = fixedTabs\n    },\n    setCustomTitle(state, {path, title}) {\n      if (title) {\n        const obj = state.customTitles.find(item => item.path === path)\n        if (obj) {\n          obj.title = title\n        } else {\n          state.customTitles.push({path, title})\n        }\n        sessionStorage.setItem(process.env.VUE_APP_TBAS_TITLES_KEY, JSON.stringify(state.customTitles))\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "front/src/theme/antd/ant-menu.less",
    "content": ".ant-menu-inline-collapsed-tooltip a{\n  color: @text-color-inverse;\n}"
  },
  {
    "path": "front/src/theme/antd/ant-message.less",
    "content": ".ant-message{\n  z-index: 1100;\n}\n"
  },
  {
    "path": "front/src/theme/antd/ant-table.less",
    "content": "\n.ant-table-thead{\n  tr{\n    th{\n      &.ant-table-column-has-actions{\n        &.ant-table-column-has-sorters:hover{\n          background-color: @background-color-base;\n        }\n        &.ant-table-column-has-filters{\n          &:hover{\n            .anticon-filter, .anticon-filter:hover{\n              background-color: @background-color-base;\n            }\n          }\n          .anticon-filter.ant-table-filter-open{\n            background-color: @background-color-base;\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "front/src/theme/antd/ant-time-picker.less",
    "content": ".ant-time-picker-panel-input{\n  background-color: @component-background;\n}\n"
  },
  {
    "path": "front/src/theme/antd/index.less",
    "content": "@import \"ant-time-picker\";\n@import \"ant-message\";\n@import \"ant-table\";\n@import \"ant-menu\";"
  },
  {
    "path": "front/src/theme/default/color.less",
    "content": "@import '~ant-design-vue/lib/style/themes/default';\n\n@gray-1: #ffffff;\n@gray-2: #fafafa;\n@gray-3: #f5f5f5;\n@gray-4: #f0f0f0;\n@gray-5: #d9d9d9;\n@gray-6: #bfbfbf;\n@gray-7: #8c8c8c;\n@gray-8: #595959;\n@gray-9: #434343;\n@gray-10: #262626;\n@gray-11: #1f1f1f;\n@gray-12: #141414;\n@gray-13: #000000;\n\n@primary-color: @primary-color;\n@success-color: @success-color;\n@warning-color: @warning-color;\n@error-color: @warning-color;\n\n@title-color: @heading-color;\n@text-color: @text-color;\n@text-color-second: @text-color-secondary;\n\n@layout-bg-color: @layout-body-background;\n@base-bg-color: @body-background;\n@hover-bg-color: rgba(0, 0, 0, 0.025);\n@border-color: @border-color-split;\n@shadow-color: @shadow-color;\n\n@text-color-inverse: @text-color-inverse;\n@hover-bg-color-light: @hover-bg-color;\n@hover-bg-color-dark: @primary-7;\n@hover-bg-color-night: rgba(255, 255, 255, 0.025);\n@header-bg-color-dark: @layout-header-background;\n\n@shadow-down: @shadow-1-down;\n@shadow-up: @shadow-1-up;\n@shadow-left: @shadow-1-left;\n@shadow-right: @shadow-1-right;\n\n@theme-list: light, dark, night;\n"
  },
  {
    "path": "front/src/theme/default/index.less",
    "content": "@import \"color\";\n@import \"style\";\n@import \"nprogress\";\n"
  },
  {
    "path": "front/src/theme/default/nprogress.less",
    "content": "@import '~ant-design-vue/lib/style/themes/default';\n\n/* Make clicks pass-through */\n#nprogress {\n  pointer-events: none;\n}\n\n#nprogress .bar {\n  background: @primary-color;\n\n  position: fixed;\n  z-index: 1031;\n  top: 0;\n  left: 0;\n\n  width: 100%;\n  height: 2px;\n}\n\n/* Fancy blur effect */\n#nprogress .peg {\n  display: block;\n  position: absolute;\n  right: 0px;\n  width: 100px;\n  height: 100%;\n  box-shadow: 0 0 10px @primary-color, 0 0 5px @primary-color;\n  opacity: 1.0;\n\n  -webkit-transform: rotate(3deg) translate(0px, -4px);\n  -ms-transform: rotate(3deg) translate(0px, -4px);\n  transform: rotate(3deg) translate(0px, -4px);\n}\n\n/* Remove these to get rid of the spinner */\n#nprogress .spinner {\n  display: block;\n  position: fixed;\n  z-index: 1031;\n  top: 15px;\n  right: 15px;\n}\n\n#nprogress .spinner-icon {\n  width: 18px;\n  height: 18px;\n  box-sizing: border-box;\n\n  border: solid 2px transparent;\n  border-top-color: @primary-color;\n  border-left-color: @primary-color;\n  border-radius: 50%;\n\n  -webkit-animation: nprogress-spinner 400ms linear infinite;\n  animation: nprogress-spinner 400ms linear infinite;\n}\n\n.nprogress-custom-parent {\n  overflow: hidden;\n  position: relative;\n}\n\n.nprogress-custom-parent #nprogress .spinner,\n.nprogress-custom-parent #nprogress .bar {\n  position: absolute;\n}\n\n@-webkit-keyframes nprogress-spinner {\n  0%   { -webkit-transform: rotate(0deg); }\n  100% { -webkit-transform: rotate(360deg); }\n}\n@keyframes nprogress-spinner {\n  0%   { transform: rotate(0deg); }\n  100% { transform: rotate(360deg); }\n}\n\n"
  },
  {
    "path": "front/src/theme/default/style.less",
    "content": ".week-mode{\n  overflow: hidden;\n  filter: invert(80%);\n}\n.beauty-scroll{\n  scrollbar-color: @primary-color @primary-2;\n  scrollbar-width: thin;\n  -ms-overflow-style:none;\n  position: relative;\n  &::-webkit-scrollbar{\n    width: 3px;\n    height: 1px;\n  }\n  &::-webkit-scrollbar-thumb {\n    border-radius: 3px;\n    background: @primary-color;\n  }\n  &::-webkit-scrollbar-track {\n    -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0);\n    border-radius: 3px;\n    background: @primary-3;\n  }\n}\n.split-right{\n  &:not(:last-child) {\n    border-right: 1px solid rgba(98, 98, 98, 0.2);\n  }\n}\n.disabled{\n  cursor: not-allowed;\n  color: @disabled-color;\n  pointer-events: none;\n}\n"
  },
  {
    "path": "front/src/theme/index.less",
    "content": "@import '~ant-design-vue/dist/antd.less';\n@import \"default/index\";\n@import \"antd/index\";\n"
  },
  {
    "path": "front/src/theme/theme.less",
    "content": "@import \"default/index\";\n"
  },
  {
    "path": "front/src/utils/Objects.js",
    "content": "/**\n * 给对象注入属性\n * @param keys 属性key数组， 如 keys = ['config', 'path'] , 则会给对象注入 object.config.path 的属性\n * @param value 属性值\n * @returns {Object}\n */\nObject.defineProperty(Object.prototype, 'assignProps', {\n  writable: false,\n  enumerable: false,\n  configurable: true,\n  value: function (keys, value) {\n    let props = this\n    for (let i = 0; i < keys.length; i++) {\n      let key = keys[i]\n      if (i == keys.length - 1) {\n        props[key] = value\n      } else {\n        props[key] = props[key] == undefined ? {} : props[key]\n        props = props[key]\n      }\n    }\n    return this\n  }\n})\n"
  },
  {
    "path": "front/src/utils/authority-utils.js",
    "content": "/**\n * 判断是否有路由的权限\n * @param authority 路由权限配置\n * @param permissions 用户权限集合\n * @returns {boolean|*}\n */\nfunction hasPermission(authority, permissions) {\n  let required = '*'\n  if (typeof authority === 'string') {\n    required = authority\n  } else if (typeof authority === 'object') {\n    required = authority.permission\n  }\n  return required === '*' || (permissions && permissions.findIndex(item => item === required || item.id === required) !== -1)\n}\n\n/**\n * 判断是否有路由需要的角色\n * @param authority 路由权限配置\n * @param roles 用户角色集合\n */\nfunction hasRole(authority, roles) {\n  let required = undefined\n  if (typeof authority === 'object') {\n    required = authority.role\n  }\n  return authority === '*' || hasAnyRole(required, roles)\n}\n\n/**\n * 判断是否有需要的任意一个角色\n * @param required {String | Array[String]} 需要的角色，可以是单个角色或者一个角色数组\n * @param roles 拥有的角色\n * @returns {boolean}\n */\nfunction hasAnyRole(required, roles) {\n  if (!required) {\n    return false\n  } else if(Array.isArray(required)) {\n    return roles.findIndex(role => {\n      return required.findIndex(item => item === role || item === role.id) !== -1\n    }) !== -1\n  } else {\n    return roles.findIndex(role => role === required || role.id === required) !== -1\n  }\n}\n\n/**\n * 路由权限校验\n * @param route 路由\n * @param permissions 用户权限集合\n * @param roles 用户角色集合\n * @returns {boolean}\n */\nfunction hasAuthority(route, permissions, roles) {\n  const authorities = [...route.meta.pAuthorities, route.meta.authority]\n  for (let authority of authorities) {\n    if (!hasPermission(authority, permissions) && !hasRole(authority, roles)) {\n      return false\n    }\n  }\n  return true\n}\n\n/**\n * 根据权限配置过滤菜单数据\n * @param menuData\n * @param permissions\n * @param roles\n */\nfunction filterMenu(menuData, permissions, roles) {\n  return menuData.filter(menu => {\n    if (menu.meta && menu.meta.invisible === undefined) {\n      if (!hasAuthority(menu, permissions, roles)) {\n        return false\n      }\n    }\n    if (menu.children && menu.children.length > 0) {\n      menu.children = filterMenu(menu.children, permissions, roles)\n    }\n    return true\n  })\n}\n\nexport {filterMenu, hasAuthority}\n"
  },
  {
    "path": "front/src/utils/axios-interceptors.js",
    "content": "import Cookie from 'js-cookie'\n// import {LOGIN} from \"@/services/api\";\n// 401拦截\n// const resp401 = {\n//   /**\n//    * 响应数据之前做点什么\n//    * @param response 响应对象\n//    * @param options 应用配置 包含: {router, i18n, store, message}\n//    * @returns {*}\n//    */\n//   onFulfilled(response, options) {\n//     const {message} = options\n//     if (response.code === 401) {\n//       message.error('无此权限')\n//     }\n//     return response\n//   },\n//   /**\n//    * 响应出错时执行\n//    * @param error 错误对象\n//    * @param options 应用配置 包含: {router, i18n, store, message}\n//    * @returns {Promise<never>}\n//    */\n//   onRejected(error, options) {\n//     const {message} = options\n//     const {response} = error\n//     if (response.status === 401) {\n//       message.error('无此权限')\n//     }\n//     return Promise.reject(error)\n//   }\n// }\n//\nconst resp403 = {\n  onFulfilled(response, options) {\n    const {message} = options\n    if (response.code === 1000) {\n      message.error('没有权限访问！')\n    }\n    return response\n  },\n  onRejected(error, options) {\n    const {message} = options\n    const {response} = error\n    if (response.status === 403) {\n      message.error('请求被拒绝！没有权限访问！')\n    }\n    return Promise.reject(error)\n  }\n}\nconst resp500 = {\n  onFulfilled(response, options) {\n    const {message} = options\n    if (response.code === 500) {\n      message.error('服务器错误')\n    }\n    return response\n  },\n  onRejected(error, options) {\n    const {message} = options\n    const {response} = error\n    if (response.status === 500) {\n      message.error('服务器错误')\n    }\n    return Promise.reject(error)\n  }\n}\nconst respA = {\n  onFulfilled(response, options) {\n    const {message} = options\n    if (response.code === 500) {\n      message.error('服务器错误')\n    }\n    return response\n  },\n  onRejected(error, options) {\n    const {message} = options\n    const {response} = error\n    if (response.status === 500) {\n      message.error('服务器错误')\n    }\n    return Promise.reject(error)\n  }\n}\n\nconst reqCommon = {\n  /**\n   * 发送请求之前做些什么\n   * @param config axios config\n   * @param options 应用配置 包含: {router, i18n, store, message}\n   * @returns {*}\n   */\n  onFulfilled(config, options) {\n    const {message} = options\n    const { xsrfCookieName} = config\n    if (options.router.currentRoute.fullPath !== '/login' && xsrfCookieName && !Cookie.get(xsrfCookieName)) {\n      message.warning('认证 token 已过期，请重新登录')\n      options.router.push('/login')\n    }\n    return config\n  },\n  /**\n   * 请求出错时做点什么\n   * @param error 错误对象\n   * @param options 应用配置 包含: {router, i18n, store, message}\n   * @returns {Promise<never>}\n   */\n  onRejected(error, options) {\n    const {message} = options\n    message.error(error.message)\n    return Promise.reject(error)\n  }\n}\n\nexport default {\n  request: [reqCommon], // 请求拦截\n  response: [resp500,resp403,respA] // 响应拦截\n}\n"
  },
  {
    "path": "front/src/utils/colors.js",
    "content": "const varyColor = require('webpack-theme-color-replacer/client/varyColor')\nconst {generate} =  require('@ant-design/colors')\nconst {ADMIN, ANTD} = require('../config/default')\nconst Config = require('../config')\n\nconst themeMode = ADMIN.theme.mode\n\n// 获取 ant design 色系\nfunction getAntdColors(color, mode) {\n  let options = mode && (mode == themeMode.NIGHT) ? {theme: 'dark'} : undefined\n  return generate(color, options)\n}\n\n// 获取功能性颜色\nfunction getFunctionalColors(mode) {\n  let options = mode && (mode == themeMode.NIGHT) ? {theme: 'dark'} : undefined\n  let {success, warning, error} = ANTD.primary\n  const  {success: s1, warning: w1, error: e1} = Config.theme\n  success = success && s1\n  warning = success && w1\n  error = success && e1\n  const successColors = generate(success, options)\n  const warningColors = generate(warning, options)\n  const errorColors = generate(error, options)\n  return {\n    success: successColors,\n    warning: warningColors,\n    error: errorColors\n  }\n}\n\n// 获取菜单色系\nfunction getMenuColors(color, mode) {\n  if (mode == themeMode.NIGHT) {\n    return ANTD.primary.night.menuColors\n  } else if (color == ANTD.primary.color) {\n    return ANTD.primary.dark.menuColors\n  } else {\n    return [varyColor.darken(color, 0.93), varyColor.darken(color, 0.83), varyColor.darken(color, 0.73)]\n  }\n}\n\n// 获取主题模式切换色系\nfunction getThemeToggleColors(color, mode) {\n  //主色系\n  const mainColors = getAntdColors(color, mode)\n  const primary = mainColors[5]\n  //辅助色系，因为 antd 目前没针对夜间模式设计，所以增加辅助色系以保证夜间模式的正常切换\n  const subColors = getAntdColors(primary, themeMode.LIGHT)\n  //菜单色系\n  const menuColors = getMenuColors(color, mode)\n  //内容色系（包含背景色、文字颜色等）\n  const themeCfg = ANTD.theme[mode]\n  let contentColors = Object.keys(themeCfg)\n    .map(key => themeCfg[key])\n    .map(color => isHex(color) ? color : toNum3(color).join(','))\n  // 内容色去重\n  contentColors = [...new Set(contentColors)]\n  // rgb 格式的主题色\n  let rgbColors = [toNum3(primary).join(',')]\n  let functionalColors = getFunctionalColors(mode)\n  return {primary, mainColors, subColors, menuColors, contentColors, rgbColors, functionalColors}\n}\n\nfunction toNum3(color) {\n  if (isHex(color)) {\n    return varyColor.toNum3(color)\n  }\n  let colorStr = ''\n  if (isRgb(color)) {\n    colorStr = color.slice(5, color.length)\n  } else if (isRgba(color)) {\n    colorStr = color.slice(6, color.lastIndexOf(','))\n  }\n  let rgb = colorStr.split(',')\n  const r = parseInt(rgb[0])\n  const g = parseInt(rgb[1])\n  const b = parseInt(rgb[2])\n  return [r, g, b]\n}\n\nfunction isHex(color) {\n  return color.length >= 4 && color[0] == '#'\n}\n\nfunction isRgb(color) {\n  return color.length >= 10 && color.slice(0, 3) == 'rgb'\n}\n\nfunction isRgba(color) {\n  return color.length >= 13 && color.slice(0, 4) == 'rgba'\n}\n\nmodule.exports = {\n  isHex,\n  isRgb,\n  isRgba,\n  toNum3,\n  getAntdColors,\n  getMenuColors,\n  getThemeToggleColors,\n  getFunctionalColors\n}\n"
  },
  {
    "path": "front/src/utils/formatter.js",
    "content": "/**\n * 把对象按照 js配置文件的格式进行格式化\n * @param obj 格式化的对象\n * @param dep 层级，此项无需传值\n * @returns {string}\n */\nfunction formatConfig(obj, dep) {\n  dep = dep || 1\n  const LN = '\\n', TAB = '  '\n  let indent = ''\n  for (let i = 0; i < dep; i++) {\n    indent += TAB\n  }\n  let isArray = false, arrayLastIsObj = false\n  let str = '', prefix = '{', subfix = '}'\n  if (Array.isArray(obj)) {\n    isArray = true\n    prefix = '['\n    subfix = ']'\n    str = obj.map((item, index) => {\n      let format = ''\n      if (typeof item == 'function') {\n        //\n      } else if (typeof item == 'object') {\n        arrayLastIsObj = true\n        format = `${LN}${indent}${formatConfig(item,dep + 1)},`\n      } else if ((typeof item == 'number' && !isNaN(item)) || typeof item == 'boolean') {\n        format = `${item},`\n      } else if (typeof item == 'string') {\n        format = `'${item}',`\n      }\n      if (index == obj.length - 1) {\n        format = format.substring(0, format.length - 1)\n      } else {\n        arrayLastIsObj = false\n      }\n      return format\n    }).join('')\n  } else if (typeof obj != 'function' && typeof obj == 'object') {\n    str = Object.keys(obj).map((key, index, keys) => {\n      const val = obj[key]\n      let format = ''\n      if (typeof val == 'function') {\n        //\n      } else if (typeof val == 'object') {\n        format = `${LN}${indent}${key}: ${formatConfig(val,dep + 1)},`\n      } else if ((typeof val == 'number' && !isNaN(val)) || typeof val == 'boolean') {\n        format = `${LN}${indent}${key}: ${val},`\n      } else if (typeof val == 'string') {\n        format = `${LN}${indent}${key}: '${val}',`\n      }\n      if (index == keys.length - 1) {\n        format = format.substring(0, format.length - 1)\n      }\n      return format\n    }).join('')\n  }\n  const len = TAB.length\n  if (indent.length >= len) {\n    indent = indent.substring(0, indent.length - len)\n  }\n  if (!isArray || arrayLastIsObj) {\n    subfix = LN + indent +subfix\n  }\n  return`${prefix}${str}${subfix}`\n}\n\nmodule.exports = {formatConfig}\n"
  },
  {
    "path": "front/src/utils/i18n.js",
    "content": "import Vue from 'vue'\nimport VueI18n from 'vue-i18n'\nimport routesI18n from '@/router/i18n'\nimport './Objects'\nimport {getI18nKey} from '@/utils/routerUtil'\n\n/**\n * 创建 i18n 配置\n * @param locale 本地化语言\n * @param fallback 回退语言\n * @returns {VueI18n}\n */\nfunction initI18n(locale, fallback) {\n  Vue.use(VueI18n)\n  let i18nOptions = {\n    locale,\n    fallbackLocale: fallback,\n    silentFallbackWarn: true,\n  }\n  return new VueI18n(i18nOptions)\n}\n\n/**\n * 根据 router options 配置生成 国际化语言\n * @param lang\n * @param routes\n * @param valueKey\n * @returns {*}\n */\nfunction generateI18n(lang, routes, valueKey) {\n  routes.forEach(route => {\n    let keys = getI18nKey(route.fullPath).split('.')\n    let value = valueKey === 'path' ? route[valueKey].split('/').filter(item => !item.startsWith(':') && item != '').join('.') : route[valueKey]\n    lang.assignProps(keys, value)\n    if (route.children) {\n      generateI18n(lang, route.children, valueKey)\n    }\n  })\n  return lang\n}\n\n/**\n * 格式化 router.options.routes，生成 fullPath\n * @param routes\n * @param parentPath\n */\nfunction formatFullPath(routes, parentPath = '') {\n  routes.forEach(route => {\n    let isFullPath = route.path.substring(0, 1) === '/'\n    route.fullPath = isFullPath ? route.path : (parentPath === '/' ? parentPath + route.path : parentPath + '/' + route.path)\n    if (route.children) {\n      formatFullPath(route.children, route.fullPath)\n    }\n  })\n}\n\n/**\n * 从路由提取国际化数据\n * @param i18n\n * @param routes\n */\nfunction mergeI18nFromRoutes(i18n, routes) {\n  formatFullPath(routes)\n  const CN = generateI18n(new Object(), routes, 'name')\n  const US = generateI18n(new Object(), routes, 'path')\n  i18n.mergeLocaleMessage('CN', CN)\n  i18n.mergeLocaleMessage('US', US)\n  const messages = routesI18n.messages\n  Object.keys(messages).forEach(lang => {\n    i18n.mergeLocaleMessage(lang, messages[lang])\n  })\n}\n\nexport {\n  initI18n,\n  mergeI18nFromRoutes,\n  formatFullPath\n}\n"
  },
  {
    "path": "front/src/utils/request.js",
    "content": "import axios from 'axios'\nimport Cookie from 'js-cookie'\nimport {message} from \"ant-design-vue\";\n\n// 跨域认证信息 header 名\nconst xsrfHeaderName = 'Authorization'\n\naxios.defaults.timeout = 5000\naxios.defaults.withCredentials= true\naxios.defaults.xsrfHeaderName= xsrfHeaderName\naxios.defaults.xsrfCookieName= xsrfHeaderName\n\n// 认证类型\nconst AUTH_TYPE = {\n  BEARER: 'Bearer',\n  BASIC: 'basic',\n  AUTH1: 'auth1',\n  AUTH2: 'auth2',\n}\n\n// http method\nconst METHOD = {\n  GET: 'get',\n  POST: 'post',\n  PATCH:'patch',\n  PUT:'put',\n  DELETE:'delete'\n}\n\n/**\n * axios请求\n * @param url 请求地址\n * @param method {METHOD} http method\n * @param params 请求参数\n * @returns {Promise<AxiosResponse<T>>}\n */\nasync function request(url, method, params, config) {\n  let promise =null;\n  switch (method) {\n    case METHOD.GET:\n      promise = axios.get(url, {params, ...config});\n      break;\n    case METHOD.POST:\n      promise = axios.post(url, params, config);\n      break;\n    case METHOD.PUT:\n      promise = axios.put(url, params, config);\n      break;\n    case METHOD.PATCH:\n      promise = axios.patch(url,params,config);\n      break;\n    case METHOD.DELETE:\n      promise = axios.delete(url,config);\n      break;\n    default:\n      promise = axios.get(url, {params, ...config});\n      break;\n  }\n  promise.catch(()=>{\n    message.warning('请求出错,您可能已经退出登录')\n  })\n  return promise;\n}\n\n/**\n * 设置认证信息\n * @param auth {Object}\n * @param authType {AUTH_TYPE} 认证类型，默认：{AUTH_TYPE.BEARER}\n */\nfunction setAuthorization(auth, authType = AUTH_TYPE.BEARER) {\n  switch (authType) {\n    case AUTH_TYPE.BEARER:\n      // Cookie.set(xsrfHeaderName, 'Bearer ' + auth.token, {expires: auth.expireAt})\n      Cookie.set(xsrfHeaderName, auth.token, {expires: auth.expireAt})\n      break\n    case AUTH_TYPE.BASIC:\n    case AUTH_TYPE.AUTH1:\n    case AUTH_TYPE.AUTH2:\n    default:\n      break\n  }\n}\n\n/**\n * 移出认证信息\n * @param authType {AUTH_TYPE} 认证类型\n */\nfunction removeAuthorization(authType = AUTH_TYPE.BEARER) {\n  switch (authType) {\n    case AUTH_TYPE.BEARER:\n      Cookie.remove(xsrfHeaderName)\n      break\n    case AUTH_TYPE.BASIC:\n    case AUTH_TYPE.AUTH1:\n    case AUTH_TYPE.AUTH2:\n    default:\n      break\n  }\n}\n\n/**\n * 检查认证信息\n * @param authType\n * @returns {boolean}\n */\nfunction checkAuthorization(authType = AUTH_TYPE.BEARER) {\n  switch (authType) {\n    case AUTH_TYPE.BEARER:\n      if (Cookie.get(xsrfHeaderName)) {\n        return true\n      }\n      break\n    case AUTH_TYPE.BASIC:\n    case AUTH_TYPE.AUTH1:\n    case AUTH_TYPE.AUTH2:\n    default:\n      break\n  }\n  return false\n}\n\n/**\n * 加载 axios 拦截器\n * @param interceptors\n * @param options\n */\nfunction loadInterceptors(interceptors, options) {\n  const {request, response} = interceptors\n  // 加载请求拦截器\n  request.forEach(item => {\n    let {onFulfilled, onRejected} = item\n    if (!onFulfilled || typeof onFulfilled !== 'function') {\n      onFulfilled = config => config\n    }\n    if (!onRejected || typeof onRejected !== 'function') {\n      onRejected = error => Promise.reject(error)\n    }\n    axios.interceptors.request.use(\n      config => onFulfilled(config, options),\n      error => onRejected(error, options)\n    )\n  })\n  // 加载响应拦截器\n  response.forEach(item => {\n    let {onFulfilled, onRejected} = item\n    if (!onFulfilled || typeof onFulfilled !== 'function') {\n      onFulfilled = response => response\n    }\n    if (!onRejected || typeof onRejected !== 'function') {\n      onRejected = error => Promise.reject(error)\n    }\n    axios.interceptors.response.use(\n      response => onFulfilled(response, options),\n      error => onRejected(error, options)\n    )\n  })\n}\n\n/**\n * 解析 url 中的参数\n * @param url\n * @returns {Object}\n */\nfunction parseUrlParams(url) {\n  const params = {}\n  if (!url || url === '' || typeof url !== 'string') {\n    return params\n  }\n  const paramsStr = url.split('?')[1]\n  if (!paramsStr) {\n    return params\n  }\n  const paramsArr = paramsStr.replace(/&|=/g, ' ').split(' ')\n  for (let i = 0; i < paramsArr.length / 2; i++) {\n    const value = paramsArr[i * 2 + 1]\n    params[paramsArr[i * 2]] = value === 'true' ? true : (value === 'false' ? false : value)\n  }\n  return params\n}\n\nexport {\n  METHOD,\n  AUTH_TYPE,\n  request,\n  setAuthorization,\n  removeAuthorization,\n  checkAuthorization,\n  loadInterceptors,\n  parseUrlParams\n}\n"
  },
  {
    "path": "front/src/utils/routerUtil.js",
    "content": "import routerMap from '@/router/async/router.map'\nimport {mergeI18nFromRoutes} from '@/utils/i18n'\nimport Router from 'vue-router'\nimport deepMerge from 'deepmerge'\nimport basicOptions from '@/router/async/config.async'\n\n//应用配置\nlet appOptions = {\n  router: undefined,\n  i18n: undefined,\n  store: undefined\n}\n\n/**\n * 设置应用配置\n * @param options\n */\nfunction setAppOptions(options) {\n  const {router, store, i18n} = options\n  appOptions.router = router\n  appOptions.store = store\n  appOptions.i18n = i18n\n}\n\n/**\n * 根据 路由配置 和 路由组件注册 解析路由\n * @param routesConfig 路由配置\n * @param routerMap 本地路由组件注册配置\n */\nfunction parseRoutes(routesConfig, routerMap) {\n  let routes = []\n  routesConfig.forEach(item => {\n    // 获取注册在 routerMap 中的 router，初始化 routeCfg\n    let router = undefined, routeCfg = {}\n    if (typeof item === 'string') {\n      router = routerMap[item]\n      routeCfg = {path: (router && router.path) || item, router: item}\n    } else if (typeof item === 'object') {\n      router = routerMap[item.router]\n      routeCfg = item\n    }\n    if (!router) {\n      console.warn(`can't find register for router ${routeCfg.router}, please register it in advance.`)\n      router = typeof item === 'string' ? {path: item, name: item} : item\n    }\n    // 从 router 和 routeCfg 解析路由\n    const meta = {\n      authority: router.authority,\n      icon: router.icon,\n      page: router.page,\n      link: router.link,\n      params: router.params,\n      query: router.query,\n      ...router.meta\n    }\n    const cfgMeta = {\n      authority: routeCfg.authority,\n      icon: routeCfg.icon,\n      page: routeCfg.page,\n      link: routeCfg.link,\n      params: routeCfg.params,\n      query: routeCfg.query,\n      ...routeCfg.meta\n    }\n    Object.keys(cfgMeta).forEach(key => {\n      if (cfgMeta[key] === undefined || cfgMeta[key] === null || cfgMeta[key] === '') {\n        delete cfgMeta[key]\n      }\n    })\n    Object.assign(meta, cfgMeta)\n    const route = {\n      path: routeCfg.path || router.path || routeCfg.router,\n      name: routeCfg.name || router.name,\n      component: router.component,\n      redirect: routeCfg.redirect || router.redirect,\n      meta: {...meta, authority: meta.authority || '*'}\n    }\n    if (routeCfg.invisible || router.invisible) {\n      route.meta.invisible = true\n    }\n    if (routeCfg.children && routeCfg.children.length > 0) {\n      route.children = parseRoutes(routeCfg.children, routerMap)\n    }\n    routes.push(route)\n  })\n  return routes\n}\n\n/**\n * 加载路由\n * @param routesConfig {RouteConfig[]} 路由配置\n */\nfunction loadRoutes(routesConfig) {\n  //兼容 0.6.1 以下版本\n  /*************** 兼容 version < v0.6.1 *****************/\n  if (arguments.length > 0) {\n    const arg0 = arguments[0]\n    if (arg0.router || arg0.i18n || arg0.store) {\n      routesConfig = arguments[1]\n      console.error('the usage of signature loadRoutes({router, store, i18n}, routesConfig) is out of date, please use the new signature: loadRoutes(routesConfig).')\n      console.error('方法签名 loadRoutes({router, store, i18n}, routesConfig) 的用法已过时, 请使用新的方法签名 loadRoutes(routesConfig)。')\n    }\n  }\n  /*************** 兼容 version < v0.6.1 *****************/\n\n  // 应用配置\n  const {router, store, i18n} = appOptions\n\n  // 如果 routesConfig 有值，则更新到本地，否则从本地获取\n  if (routesConfig) {\n    store.commit('account/setRoutesConfig', routesConfig)\n  } else {\n    routesConfig = store.getters['account/routesConfig']\n  }\n  // 如果开启了异步路由，则加载异步路由配置\n  const asyncRoutes = store.state.setting.asyncRoutes\n  if (asyncRoutes) {\n    if (routesConfig && routesConfig.length > 0) {\n      const routes = parseRoutes(routesConfig, routerMap)\n      const finalRoutes = mergeRoutes(basicOptions.routes, routes)\n      formatRoutes(finalRoutes)\n      router.options = {...router.options, routes: finalRoutes}\n      router.matcher = new Router({...router.options, routes:[]}).matcher\n      router.addRoutes(finalRoutes)\n    }\n  }\n  // 提取路由国际化数据\n  mergeI18nFromRoutes(i18n, router.options.routes)\n  // 初始化Admin后台菜单数据\n  const rootRoute = router.options.routes.find(item => item.path === '/')\n  const menuRoutes = rootRoute && rootRoute.children\n  if (menuRoutes) {\n    store.commit('setting/setMenuData', menuRoutes)\n  }\n}\n\n/**\n * 合并路由\n * @param target {Route[]}\n * @param source {Route[]}\n * @returns {Route[]}\n */\nfunction mergeRoutes(target, source) {\n  const routesMap = {}\n  target.forEach(item => routesMap[item.path] = item)\n  source.forEach(item => routesMap[item.path] = item)\n  return Object.values(routesMap)\n}\n\n/**\n * 深度合并路由\n * @param target {Route[]}\n * @param source {Route[]}\n * @returns {Route[]}\n */\nfunction deepMergeRoutes(target, source) {\n  // 映射路由数组\n  const mapRoutes = routes => {\n    const routesMap = {}\n    routes.forEach(item => {\n      routesMap[item.path] = {\n        ...item,\n        children: item.children ? mapRoutes(item.children) : undefined\n      }\n    })\n    return routesMap\n  }\n  const tarMap = mapRoutes(target)\n  const srcMap = mapRoutes(source)\n\n  // 合并路由\n  const merge = deepMerge(tarMap, srcMap)\n\n  // 转换为 routes 数组\n  const parseRoutesMap = routesMap => {\n    return Object.values(routesMap).map(item => {\n      if (item.children) {\n        item.children = parseRoutesMap(item.children)\n      } else {\n        delete item.children\n      }\n      return item\n    })\n  }\n  return parseRoutesMap(merge)\n}\n\n/**\n * 格式化路由\n * @param routes 路由配置\n */\nfunction formatRoutes(routes) {\n  routes.forEach(route => {\n    const {path} = route\n    if (!path.startsWith('/') && path !== '*') {\n      route.path = '/' + path\n    }\n  })\n  formatAuthority(routes)\n}\n\n/**\n * 格式化路由的权限配置\n * @param routes 路由\n * @param pAuthorities 父级路由权限配置集合\n */\nfunction formatAuthority(routes, pAuthorities = []) {\n  routes.forEach(route => {\n    const meta = route.meta\n    const defaultAuthority = pAuthorities[pAuthorities.length - 1] || {permission: '*'}\n    if (meta) {\n      let authority = {}\n      if (!meta.authority) {\n        authority = defaultAuthority\n      }else if (typeof meta.authority === 'string') {\n        authority.permission = meta.authority\n      } else if (typeof meta.authority === 'object') {\n        authority = meta.authority\n        const {role} = authority\n        if (typeof role === 'string') {\n          authority.role = [role]\n        }\n        if (!authority.permission && !authority.role) {\n          authority = defaultAuthority\n        }\n      }\n      meta.authority = authority\n    } else {\n      const authority = defaultAuthority\n      route.meta = {authority}\n    }\n    route.meta.pAuthorities = pAuthorities\n    if (route.children) {\n      formatAuthority(route.children, [...pAuthorities, route.meta.authority])\n    }\n  })\n}\n\n/**\n * 从路由 path 解析 i18n key\n * @param path\n * @returns {*}\n */\nfunction getI18nKey(path) {\n  const keys = path.split('/').filter(item => !item.startsWith(':') && item != '')\n  keys.push('name')\n  return keys.join('.')\n}\n\n/**\n * 加载导航守卫\n * @param guards\n * @param options\n */\nfunction loadGuards(guards, options) {\n  const {beforeEach, afterEach} = guards\n  const {router} = options\n  beforeEach.forEach(guard => {\n    if (guard && typeof guard === 'function') {\n      router.beforeEach((to, from, next) => guard(to, from, next, options))\n    }\n  })\n  afterEach.forEach(guard => {\n    if (guard && typeof guard === 'function') {\n      router.afterEach((to, from) => guard(to, from, options))\n    }\n  })\n}\n\nexport {parseRoutes, loadRoutes, formatAuthority, getI18nKey, loadGuards, deepMergeRoutes, formatRoutes, setAppOptions}\n"
  },
  {
    "path": "front/src/utils/theme-color-replacer-extend.js",
    "content": "const {cssResolve} = require('../config/replacer')\n// 修正 webpack-theme-color-replacer 插件提取的 css 结果\nfunction resolveCss(output, srcArr) {\n  let regExps = []\n  // 提取 resolve 配置中所有的正则配置\n  Object.keys(cssResolve).forEach(key => {\n    let isRegExp = false\n    let reg = {}\n    try {\n      reg = eval(key)\n      isRegExp = reg instanceof RegExp\n    } catch (e) {\n      isRegExp = false\n    }\n    if (isRegExp) {\n      regExps.push([reg, cssResolve[key]])\n    }\n  })\n\n  // 去重\n  srcArr = dropDuplicate(srcArr)\n\n  // 处理 css\n  let outArr = []\n  srcArr.forEach(text => {\n    // 转换为 css 对象\n    let cssObj = parseCssObj(text)\n    // 根据selector匹配配置，匹配成功，则按配置处理 css\n    if (cssResolve[cssObj.selector] != undefined) {\n      let cfg = cssResolve[cssObj.selector]\n      if (cfg) {\n        outArr.push(cfg.resolve(text, cssObj))\n      }\n    } else {\n      let cssText = ''\n      // 匹配不成功，则测试是否有匹配的正则配置，有则按正则对应的配置处理\n      for (let regExp of regExps) {\n        if (regExp[0].test(cssObj.selector)) {\n          let cssCfg = regExp[1]\n          cssText = cssCfg ? cssCfg.resolve(text, cssObj) : ''\n          break\n        }\n        // 未匹配到正则，则设置 cssText 为默认的 css（即不处理）\n        cssText = text\n      }\n      if (cssText != '') {\n        outArr.push(cssText)\n      }\n    }\n  })\n  output = outArr.join('\\n')\n  return output\n}\n\n// 数组去重\nfunction dropDuplicate(arr) {\n  let map = {}\n  let r = []\n  for (let s of arr) {\n    if (!map[s]) {\n      r.push(s)\n      map[s] = 1\n    }\n  }\n  return r\n}\n\n/**\n * 从字符串解析 css 对象\n * @param cssText\n * @returns {{\n *   name: String,\n *   rules: Array[String],\n *   toText: function\n * }}\n */\nfunction parseCssObj(cssText) {\n  let css = {}\n  const ruleIndex = cssText.indexOf('{')\n  css.selector = cssText.substring(0, ruleIndex)\n  const ruleBody = cssText.substring(ruleIndex + 1, cssText.length - 1)\n  const rules = ruleBody.split(';')\n  css.rules = rules\n  css.toText = function () {\n    let body = ''\n    this.rules.forEach(item => {body += item + ';'})\n    return `${this.selector}{${body}}`\n  }\n  return css\n}\n\nmodule.exports = {resolveCss}\n"
  },
  {
    "path": "front/src/utils/themeUtil.js",
    "content": "const client = require('webpack-theme-color-replacer/client')\nconst {theme} = require('../config')\nconst {getMenuColors, getAntdColors, getThemeToggleColors, getFunctionalColors} = require('../utils/colors')\nconst {ANTD} = require('../config/default')\n\nfunction getThemeColors(color, $theme) {\n  const _color = color || theme.color\n  const mode = $theme || theme.mode\n  const replaceColors = getThemeToggleColors(_color, mode)\n  const themeColors = [\n    ...replaceColors.mainColors,\n    ...replaceColors.subColors,\n    ...replaceColors.menuColors,\n    ...replaceColors.contentColors,\n    ...replaceColors.rgbColors,\n    ...replaceColors.functionalColors.success,\n    ...replaceColors.functionalColors.warning,\n    ...replaceColors.functionalColors.error,\n  ]\n  return themeColors\n}\n\nfunction changeThemeColor(newColor, $theme) {\n  let promise = client.changer.changeColor({newColors: getThemeColors(newColor, $theme)})\n  return promise\n}\n\nfunction modifyVars(color) {\n  let _color = color || theme.color\n  const palettes = getAntdColors(_color, theme.mode)\n  const menuColors = getMenuColors(_color, theme.mode)\n  const {success, warning, error} = getFunctionalColors(theme.mode)\n  const primary = palettes[5]\n  return {\n    'primary-color': primary,\n    'primary-1': palettes[0],\n    'primary-2': palettes[1],\n    'primary-3': palettes[2],\n    'primary-4': palettes[3],\n    'primary-5': palettes[4],\n    'primary-6': palettes[5],\n    'primary-7': palettes[6],\n    'primary-8': palettes[7],\n    'primary-9': palettes[8],\n    'primary-10': palettes[9],\n    'info-color': primary,\n    'success-color': success[5],\n    'warning-color': warning[5],\n    'error-color': error[5],\n    'alert-info-bg-color': palettes[0],\n    'alert-info-border-color': palettes[2],\n    'alert-success-bg-color': success[0],\n    'alert-success-border-color': success[2],\n    'alert-warning-bg-color': warning[0],\n    'alert-warning-border-color': warning[2],\n    'alert-error-bg-color': error[0],\n    'alert-error-border-color': error[2],\n    'processing-color': primary,\n    'menu-dark-submenu-bg': menuColors[0],\n    'layout-header-background': menuColors[1],\n    'layout-trigger-background': menuColors[2],\n    'btn-danger-bg': error[4],\n    'btn-danger-border': error[4],\n    ...ANTD.theme[theme.mode]\n  }\n}\n\nfunction loadLocalTheme(localSetting) {\n  if (localSetting && localSetting.theme) {\n    let {color, mode} = localSetting.theme\n    color = color || theme.color\n    mode = mode || theme.mode\n    changeThemeColor(color, mode)\n  }\n}\n\n/**\n * 获取本地保存的配置\n * @param load {boolean} 是否加载配置中的主题\n * @returns {Object}\n */\nfunction getLocalSetting(loadTheme) {\n  let localSetting = {}\n  try {\n    const localSettingStr = localStorage.getItem(process.env.VUE_APP_SETTING_KEY)\n    localSetting = JSON.parse(localSettingStr)\n  } catch (e) {\n    console.error(e)\n  }\n  if (loadTheme) {\n    loadLocalTheme(localSetting)\n  }\n  return localSetting\n}\n\nmodule.exports = {\n  getThemeColors,\n  changeThemeColor,\n  modifyVars,\n  loadLocalTheme,\n  getLocalSetting\n}\n"
  },
  {
    "path": "front/src/utils/util.js",
    "content": "import enquireJs from 'enquire.js'\n\nexport function isDef (v){\n  return v !== undefined && v !== null\n}\n\n/**\n * Remove an item from an array.\n */\nexport function remove (arr, item) {\n  if (arr.length) {\n    const index = arr.indexOf(item)\n    if (index > -1) {\n      return arr.splice(index, 1)\n    }\n  }\n}\n\nexport function isRegExp (v) {\n  return _toString.call(v) === '[object RegExp]'\n}\n\nexport function enquireScreen(call) {\n  const handler = {\n    match: function () {\n      call && call(true)\n    },\n    unmatch: function () {\n      call && call(false)\n    }\n  }\n  enquireJs.register('only screen and (max-width: 767.99px)', handler)\n}\n\nconst _toString = Object.prototype.toString\n"
  },
  {
    "path": "front/src/utils/validators.js",
    "content": "const passwordReg = /(?=.*[0-9])(?=.*[A-Z])(?=.*[a-z])(?=.*[^a-zA-Z0-9]).{8,30}/\nconst passwordMsg=\"密码中必须包含大小写字母、数字、特称字符，至少8个字符，最多30个字符\"\nconst emailReg =  /\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*/\nconst validators = {\n    passwordReg,\n    passwordMsg,\n    password(){\n        return function(rule, value,callback){\n            let reg = passwordReg\n            const msg = passwordMsg\n            if(!reg.test(value)){\n                if(!callback)return msg;\n                callback(msg)\n            }\n        }\n    },\n    email(){\n        return function(rule, value,callback){\n            let reg =emailReg;\n            const msg = \"您输入的email地址不正确\"\n            if(!reg.test(value)){\n                if(!callback) return msg;\n                callback(msg)\n            }\n        }\n    },\n    emailReg,\n    phoneReg:/^1(3\\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\\d|9[0-35-9])\\d{8}$/,\n    phoneMsg:\"手机号格式不正确\",\n    qqReg:/^[1-9][0-9]{4,9}$/gim,\n    qqMsg:\"您输入的QQ号不正确\",\n    install:function(Vue) {\n        Vue.prototype.validators = validators;\n    }\n}\n\nexport default validators\n"
  },
  {
    "path": "front/vue.config.js",
    "content": "let path = require('path')\nconst webpack = require('webpack')\nconst ThemeColorReplacer = require('webpack-theme-color-replacer')\nconst {getThemeColors, modifyVars} = require('./src/utils/themeUtil')\nconst {resolveCss} = require('./src/utils/theme-color-replacer-extend')\nconst CompressionWebpackPlugin = require('compression-webpack-plugin')\n\nconst productionGzipExtensions = ['js', 'css']\nconst isProd = process.env.NODE_ENV === 'production'\n\nconst assetsCDN = {\n  // webpack build externals\n  externals: {\n    vue: 'Vue',\n    'vue-router': 'VueRouter',\n    vuex: 'Vuex',\n    axios: 'axios',\n    nprogress: 'NProgress',\n    clipboard: 'ClipboardJS',\n    '@antv/data-set': 'DataSet',\n    'js-cookie': 'Cookies'\n  },\n  css: [\n  ],\n  js: [\n    '//cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js',\n    '//cdn.jsdelivr.net/npm/vue-router@3.3.4/dist/vue-router.min.js',\n    '//cdn.jsdelivr.net/npm/vuex@3.4.0/dist/vuex.min.js',\n    '//cdn.jsdelivr.net/npm/axios@0.19.2/dist/axios.min.js',\n    '//cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.min.js',\n    '//cdn.jsdelivr.net/npm/clipboard@2.0.6/dist/clipboard.min.js',\n    '//cdn.jsdelivr.net/npm/@antv/data-set@0.11.4/build/data-set.min.js',\n    '//cdn.jsdelivr.net/npm/js-cookie@2.2.1/src/js.cookie.min.js'\n  ]\n}\n\nmodule.exports = {\n  devServer: {\n    // proxy: {\n    //   '/api': { //此处要与 /services/api.js 中的 API_PROXY_PREFIX 值保持一致\n    //     target: process.env.VUE_APP_API_BASE_URL,\n    //     changeOrigin: true,\n    //     pathRewrite: {\n    //       '^/api': ''\n    //     }\n    //   }\n    // }\n  },\n  pluginOptions: {\n    'style-resources-loader': {\n      preProcessor: 'less',\n      patterns: [path.resolve(__dirname, \"./src/theme/theme.less\")],\n    }\n  },\n  configureWebpack: config => {\n    config.entry.app = [\"babel-polyfill\", \"whatwg-fetch\", \"./src/main.js\"];\n    config.performance = {\n      hints: false\n    }\n    config.plugins.push(\n      new ThemeColorReplacer({\n        fileName: 'css/theme-colors-[contenthash:8].css',\n        matchColors: getThemeColors(),\n        injectCss: true,\n        resolveCss\n      })\n    )\n    // Ignore all locale files of moment.js\n    config.plugins.push(new webpack.IgnorePlugin(/^\\.\\/locale$/, /moment$/))\n    // 生产环境下将资源压缩成gzip格式\n    if (isProd) {\n      // add `CompressionWebpack` plugin to webpack plugins\n      config.plugins.push(new CompressionWebpackPlugin({\n        algorithm: 'gzip',\n        test: new RegExp('\\\\.(' + productionGzipExtensions.join('|') + ')$'),\n        threshold: 10240,\n        minRatio: 0.8\n      }))\n    }\n    // if prod, add externals\n    if (isProd) {\n      config.externals = assetsCDN.externals\n    }\n  },\n  chainWebpack: config => {\n    // 生产环境下关闭css压缩的 colormin 项，因为此项优化与主题色替换功能冲突\n    if (isProd) {\n      config.plugin('optimize-css')\n        .tap(args => {\n            args[0].cssnanoOptions.preset[1].colormin = false\n          return args\n        })\n    }\n    // 生产环境下使用CDN\n    if (isProd) {\n      config.plugin('html')\n        .tap(args => {\n          args[0].cdn = assetsCDN\n        return args\n      })\n    }\n  },\n  css: {\n    loaderOptions: {\n      less: {\n        lessOptions: {\n          modifyVars: modifyVars(),\n          javascriptEnabled: true\n        }\n      }\n    }\n  },\n  publicPath: process.env.VUE_APP_PUBLIC_PATH,\n  outputDir: 'dist',\n  assetsDir: 'static',\n  productionSourceMap: false\n}\n"
  },
  {
    "path": "mysql/dev.sql",
    "content": "-- MySQL dump 10.13  Distrib 8.0.23, for Win64 (x86_64)\n--\n-- Host: 127.0.0.1    Database: crm3\n-- ------------------------------------------------------\n-- Server version\t8.0.23\n\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!50503 SET NAMES utf8mb4 */;\n/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n/*!40103 SET TIME_ZONE='+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n/* crm */;\n\n--\n-- Table structure for table `customer_follow_up_history`\n--\n\nDROP TABLE IF EXISTS `customer_follow_up_history`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `customer_follow_up_history` (\n  `id` int NOT NULL AUTO_INCREMENT,\n  `traceTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '跟进时间',\n  `traceDetails` text COMMENT '跟进内容 计划的详细内容',\n  `traceType` int DEFAULT NULL COMMENT '跟进方式 计划采用如电话、邀约上门等  数据字典',\n  `traceResult` int DEFAULT NULL COMMENT '跟进效果 优----3、中----2、差----1',\n  `customerID` int DEFAULT NULL COMMENT '跟进客户 编辑时不可编辑 潜在客户对象/客户对象',\n  `inputUser` int DEFAULT NULL COMMENT '创建人 自动填入当前登录用户，用户不可更改/员工对象',\n  `type` int DEFAULT NULL COMMENT '跟进类型 0:潜在开发计划 1:客户跟进历史',\n  `comment` text,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `customer_follow_up_history_id_uindex` (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `customer_follow_up_history`\n--\n\nLOCK TABLES `customer_follow_up_history` WRITE;\n/*!40000 ALTER TABLE `customer_follow_up_history` DISABLE KEYS */;\nINSERT INTO `customer_follow_up_history` VALUES (1,'2021-05-21 20:10:22','还不错',24,3,1,5,1,'阿迪斯发打发士大夫'),(2,'2021-05-21 21:05:46','123',24,2,1,2,0,'123123'),(3,'2021-05-21 21:06:46','还可以',24,3,7,3,0,'还不错'),(4,'2021-05-21 21:07:40','123',24,2,9,4,0,'13123'),(5,'2021-05-22 01:24:50','还可以1',24,2,1,5,0,'12313'),(6,'2021-05-01 08:00:17','123123',24,1,7,1,0,'拉了哭了'),(7,'2021-05-22 01:30:00','1231',24,2,7,6,1,'123123'),(8,'2021-05-22 01:31:11','123132',24,2,2,1,0,'123123'),(9,'2021-05-22 01:32:16','1231',24,1,4,7,0,'1231132'),(10,'2021-05-22 01:33:54','123',24,1,8,9,0,'123123'),(11,'2021-05-21 17:34:20','string',0,0,0,7,0,'string'),(12,'2021-05-22 01:36:53','123123',24,1,8,6,1,'德邦物流沟通不利'),(13,'2021-05-14 07:58:04','哔哩哔哩八零八零八',25,3,8,2,1,'叭叭叭粑粑'),(14,'2021-05-08 12:52:18','234234',26,3,3,5,1,'214143'),(15,'2021-05-23 02:37:17','天天',25,2,1,1031,0,'天天'),(16,'2021-05-23 02:47:09','天天',24,2,8,1031,0,'UI'),(17,'2021-05-23 03:06:37','天天',24,1,3,1031,0,'21');\n/*!40000 ALTER TABLE `customer_follow_up_history` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `customer_handover`\n--\n\nDROP TABLE IF EXISTS `customer_handover`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `customer_handover` (\n  `id` int NOT NULL AUTO_INCREMENT,\n  `customerID` int DEFAULT NULL COMMENT '客户 客户对象',\n  `transUser` int DEFAULT NULL COMMENT '移交人员 实行移交操作的管理人员',\n  `transTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n  `oldSeller` int DEFAULT NULL COMMENT '老市场专员 客户上的原始市场人员',\n  `newSeller` int DEFAULT NULL COMMENT '新市场专员 由公司重新指派后的新市场人员',\n  `transReason` varchar(255) DEFAULT NULL COMMENT '移交原因',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `customer_handover_id_uindex` (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `customer_handover`\n--\n\nLOCK TABLES `customer_handover` WRITE;\n/*!40000 ALTER TABLE `customer_handover` DISABLE KEYS */;\nINSERT INTO `customer_handover` VALUES (1,7,2,'2021-05-22 08:25:53',2,2,'下放任务'),(3,1,1,'2021-05-22 08:25:53',5,4,'123'),(4,1,1,'2021-05-22 08:25:53',3,2,'123'),(5,1,1,'2021-05-22 08:25:53',4,4,'123'),(6,1,1,'2021-05-22 08:25:53',7,1,'123'),(7,1,1,'2021-05-22 08:25:53',9,3,'123'),(8,1,1,'2021-05-22 08:25:53',6,3,'23423424'),(9,1,1,'2021-05-21 11:06:38',3,4,'123'),(10,1,1031,'2021-05-23 02:38:04',4,7,'天天天天'),(11,1,1031,'2021-05-23 02:38:14',7,3,'人员人员'),(12,8,1031,'2021-05-23 02:41:29',7,7,'让他'),(13,12,1031,'2021-05-23 02:43:15',2,7,'体验'),(14,6,5,'2021-05-23 02:44:34',6,9,'阿斯蒂芬'),(15,15,1031,'2021-05-23 02:46:45',1031,7,'一天'),(16,3,1,'2021-05-23 12:10:18',3,8,'任务'),(17,4,1092,'2021-05-24 08:33:16',5,1032,'123');\n/*!40000 ALTER TABLE `customer_handover` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `customer_manager`\n--\n\nDROP TABLE IF EXISTS `customer_manager`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `customer_manager` (\n  `id` int NOT NULL AUTO_INCREMENT,\n  `name` varchar(255) NOT NULL COMMENT '客户姓名',\n  `age` int NOT NULL COMMENT '客户年龄',\n  `gender` int NOT NULL COMMENT '客户性别 页面为下拉框 1男 0女',\n  `tel` varchar(255) NOT NULL COMMENT '电话号码',\n  `qq` varchar(255) DEFAULT NULL,\n  `job` int NOT NULL,\n  `source` int NOT NULL COMMENT '客户来源',\n  `seller` int DEFAULT NULL COMMENT '负责人 填写为当前登录用户',\n  `inputUser` int NOT NULL COMMENT ' 创建人 填写为当前登录用户',\n  `inputTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n  `status` int NOT NULL DEFAULT '0' COMMENT '-2:流失 -1:开发失败 0:潜在客户 1:正式客户 2:资源池客户',\n  `positiveTime` datetime DEFAULT NULL COMMENT '转正时间',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `customer_manager_id_uindex` (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `customer_manager`\n--\n\nLOCK TABLES `customer_manager` WRITE;\n/*!40000 ALTER TABLE `customer_manager` DISABLE KEYS */;\nINSERT INTO `customer_manager` VALUES (1,'秦农',24,1,'13766546213','100001',3,17,1,1,'2021-05-24 07:15:25',0,'2021-05-20 19:31:02'),(2,'马腾',33,1,'188888888','100002',3,17,2,2,'2019-05-22 08:26:27',2,'2021-05-20 19:31:02'),(3,'张云',21,1,'18888888888','100001',3,17,3,3,'2021-05-23 03:07:02',1,'2021-05-20 19:31:02'),(4,'权志龙',18,1,'18888888888','100001',3,17,5,5,'2021-04-22 08:26:27',2,'2021-05-20 19:31:02'),(5,'马钊',23,1,'16666666666','100001',3,17,4,4,'2021-05-23 12:16:30',1,'2021-05-20 19:31:02'),(6,'合理吗?🎃',18,0,'18888888888','100001',3,17,6,6,'2021-05-21 03:56:13',-2,'2021-05-20 19:31:02'),(7,'酒剑仙🗡',18,1,'17777777777','100001',3,17,5,5,'2021-05-22 08:26:27',0,'2021-05-20 19:31:02'),(8,'赵',21,1,'18888888888','100001',3,17,7,7,'2021-05-23 02:40:26',1,'2021-05-20 19:31:02'),(9,'伊泽',45,0,'1999999999','100001',3,17,8,8,'2021-05-23 08:26:27',2,'2021-05-20 19:31:02'),(10,'阿斯顿',18,1,'18888888888','100001',3,17,9,9,'2021-05-22 08:26:27',-1,'2021-05-20 19:31:02'),(11,'廖嘉积',54,0,'18888888888','100001',3,17,3,3,'2021-05-22 08:26:27',-2,'2021-05-20 19:31:02'),(12,'郭晋安',18,1,'18888888888','100001',3,17,2,2,'2021-05-22 08:26:27',2,'2021-05-20 19:31:02'),(13,'埃里克森',18,1,'13333333333','100002',3,17,2,2,'2021-05-22 08:26:27',2,'2021-05-20 19:31:02'),(14,'张🗡男',22,1,'123123123','123123',28,18,2,5,'2021-05-23 10:41:48',0,NULL),(15,'张华梁',12,1,'23444','2123',28,20,1,1031,'2021-05-23 10:43:50',1,NULL),(16,'林阳露',22,0,'1234567','12345678',5,21,2,1032,'2021-05-23 10:41:48',0,NULL),(17,'贵',21,1,'23444','6532',27,18,1,1031,'2021-05-23 10:41:48',0,NULL),(18,'胡12',123,1,'123','123',28,20,1,1,'2021-05-23 10:42:53',0,NULL);\n/*!40000 ALTER TABLE `customer_manager` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `department`\n--\n\nDROP TABLE IF EXISTS `department`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `department` (\n  `id` int NOT NULL AUTO_INCREMENT,\n  `sn` varchar(255) DEFAULT NULL,\n  `name` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `department_id_uindex` (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=46 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `department`\n--\n\nLOCK TABLES `department` WRITE;\n/*!40000 ALTER TABLE `department` DISABLE KEYS */;\nINSERT INTO `department` VALUES (2,'All Department Manager','总经办'),(3,'Human Resources Department','人力资源部'),(5,'Order Department','采购部'),(6,'Warehousing Department','仓储部'),(7,'Finance Department','财务部'),(11,'Publicity department','宣传部门'),(17,'Cultural Department🥼🧥👔👕','文化部门'),(20,'Testing department','测试部门'),(31,'Data center','数据中心'),(32,'Laboratory','实验中心'),(33,'123333Quality inspection department','质量校验部门'),(40,'Financial Business Department','金融事业部'),(41,'Executive Department','执行部'),(42,'Training place','培训部'),(43,'Marketing Department','市场部'),(44,'Channel Location Division','渠道选址事业部'),(45,'Data Collection Department','数据采集部');\n/*!40000 ALTER TABLE `department` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dictionary_contents`\n--\n\nDROP TABLE IF EXISTS `dictionary_contents`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `dictionary_contents` (\n  `id` int NOT NULL AUTO_INCREMENT,\n  `sn` varchar(255) DEFAULT NULL COMMENT '字典目录编号',\n  `title` varchar(255) DEFAULT NULL COMMENT '字典目录名称',\n  `intro` varchar(255) DEFAULT NULL COMMENT '字典目录简介',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `dictionary_contents_id_uindex` (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dictionary_contents`\n--\n\nLOCK TABLES `dictionary_contents` WRITE;\n/*!40000 ALTER TABLE `dictionary_contents` DISABLE KEYS */;\nINSERT INTO `dictionary_contents` VALUES (1,'job','职业','做什么的'),(2,'source','来源','客户来源渠道'),(3,'intentionDegree','意向程度','有多么想入坑'),(4,'subject','学科','学科分类'),(5,'Collection type','收款类型','学费收款方式'),(6,'School nature','办学性质','School nature'),(7,'Customer importance','客户重要程度','Customer importance'),(8,'Foreign language proficiency','外语水平','Foreign language proficiency'),(9,'Career test','职业测试','Career test'),(10,'Follow-up method','跟进方式','客户跟进的方式');\n/*!40000 ALTER TABLE `dictionary_contents` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `dictionary_details`\n--\n\nDROP TABLE IF EXISTS `dictionary_details`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `dictionary_details` (\n  `id` int NOT NULL AUTO_INCREMENT,\n  `title` varchar(255) DEFAULT NULL COMMENT '字典明细名称',\n  `sequence` int DEFAULT NULL COMMENT '字典明细序号',\n  `parentId` int NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY ` dictionary_details_id_uindex` (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=50 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `dictionary_details`\n--\n\nLOCK TABLES `dictionary_details` WRITE;\n/*!40000 ALTER TABLE `dictionary_details` DISABLE KEYS */;\nINSERT INTO `dictionary_details` VALUES (1,'教育学研究人员（GBM20104）',2,1),(2,'专业技术人员（GBM20000）',1,1),(3,'企业负责人（GBM10601）',1,1),(4,'党的机关、国家机关、群众团体和社会组织、企事业单位负责人（GBM10000）',12,1),(5,'教师',3,1),(8,'微信',1,2),(9,'抖音',5,2),(10,'微博',7,2),(11,'测试信息',1,3),(12,'支付宝',1,5),(13,'公立院校',1,6),(14,'重要',1,7),(15,'微信',2,5),(16,'私立院校',2,6),(17,'QQ',1,2),(18,'街头小广告',1,2),(19,'头条号',1,2),(20,'微信公众号',2,2),(21,'报纸',1,2),(22,'Bilibili',4,2),(23,'大学英语四级CET-4（四级）',1,8),(24,'营销QQ',1,10),(25,'营销微信',1,10),(26,'营销抖音',1,10),(27,'司机',999,1),(28,'编辑',45,1),(29,'办事人员和有关人员（GBM30000）',999,1),(30,'书信',12,10),(31,'中考英语分数',2,8),(32,'高考英语分数',2,8),(33,'大学英语六级CET-6（六级）',2,8),(34,'专业英语4级（专四）（TEM-4）',3,8),(35,'专业英语8级（专八）（TEM-8）',2,8),(36,'全国英语等级考试（PETS）',3,8),(37,'商务英语考试 (BEC)',2,8),(38,'翻译专业资格考试(CATTI)',2,8),(39,'上海外语口译证书',2,8),(40,'雅思(IELTS)',1,8),(41,'托福（TOEFL）',1,8),(42,'托业（TOEIC）',1,8),(43,'社会生产服务和生活服务人员（GBM40000）',1,1),(44,'其他批发与零售服务人员（GBM40199）',1,1),(45,'农、林、牧、渔业生产及辅助人员（GBM50000）',1,1),(46,'生产制造及有关人员（GBM60000）',12,1),(47,'军人GBM70000）',1,1),(48,'不便分类的其他从业人员（GBM80000）',1,1),(49,'宗教组织负责人（GBM10406）',11,1);\n/*!40000 ALTER TABLE `dictionary_details` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `employee`\n--\n\nDROP TABLE IF EXISTS `employee`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `employee` (\n  `id` int NOT NULL AUTO_INCREMENT,\n  `name` varchar(255) NOT NULL,\n  `password` varchar(255) NOT NULL,\n  `email` varchar(255) NOT NULL,\n  `age` int DEFAULT NULL,\n  `dept` int NOT NULL,\n  `hireDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',\n  `state` int NOT NULL DEFAULT '1' COMMENT '状态 1正常 0离职',\n  `admin` int NOT NULL DEFAULT '0' COMMENT '超级管理员身份 1超管 0普通',\n  `login_time` datetime DEFAULT NULL,\n  `register_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `employee_id_uindex` (`id`),\n  UNIQUE KEY `employee_name_uindex` (`name`),\n  UNIQUE KEY `employee_email_uindex` (`email`)\n) ENGINE=InnoDB AUTO_INCREMENT=1093 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `employee`\n--\n\nLOCK TABLES `employee` WRITE;\n/*!40000 ALTER TABLE `employee` DISABLE KEYS */;\nINSERT INTO `employee` VALUES (1,'admin','$2a$10$OG1zaFHT2LUy4SGcQ4EnRu9sPQMjMGEE6jARz61aQwRQ3316N6ikG','1623@163.com',20,2,'2021-05-14 00:28:00',1,1,'2021-05-26 14:00:55','2021-05-21 08:46:19'),(2,'肖总','$2a$10$./YLhMGRhhqMwJOoxJGKYeuKsXehDyTt5C6Eq9CfAshnGWlPL8SNG','163@163.com',35,43,'2021-05-16 01:19:51',1,1,'2021-05-21 16:46:24','2021-05-21 08:46:25'),(3,'赵一明','$2a$10$/h22UTKprujOhSnaugy0/.dJHpNsox.OvPuzWCMMKoFm2FOrBurwO','g@gmail.com',25,3,'2021-05-16 01:22:38',1,1,'2021-05-21 16:46:27','2021-05-21 08:46:27'),(4,'刘九江','$2a$10$4zNrZ/O1SsOcsFB6Hi9tPOGazrbU8dmV2igZaTxClNyQjONHDr3g2','msy@msy.plus',14,2,'2021-05-21 11:07:36',1,1,'2021-05-21 20:37:44','2021-05-21 11:07:29'),(5,'墨抒颖','$2a$10$H5uwoLQIGQCmZpH98UCLbezAFKBcV6XxziDXH89JuAy2LBzspoGjO','msy.plus@qq.com',101,2,'2021-05-21 11:49:52',1,0,'2021-05-26 10:48:16','2021-05-21 11:49:52'),(6,'Ralph V. Livengood','123123','RalphVLivengood@gmail.com',35,6,'2021-05-18 06:49:32',1,0,'2021-05-21 16:46:28','2021-05-21 08:46:29'),(7,'Lauren C. Young','123333','LaurenCYoung@gmail.com',33,7,'2021-05-18 07:10:31',1,0,'2021-05-21 16:46:29','2021-05-21 08:46:30'),(8,'钟汉良','123333','zhl@outlook.com',35,6,'2021-05-18 07:11:19',1,0,'2021-05-21 16:46:31','2021-05-21 08:46:31'),(9,'陈乔恩','$2a$10$meRc5DPOldNhSMJ3O61bAejjYrh9.0RCA4C7v5Vtg8ws7/Tci10hu','c@qq.com',23,2,'2021-05-21 11:34:48',1,0,NULL,'2021-05-21 11:34:48'),(1031,'宋佳鑫','$2a$10$enbn9aSc32x8o4a3mMdI0eMY2S1DIO6f70NIVhJGV0qix5JQSKaUy','songjiaxin@qq.com',18,2,'2021-05-23 01:49:58',1,1,'2021-05-23 10:18:02','2021-05-23 01:49:58'),(1032,'沈瑞渊','$2a$10$XCpj.stZ0YXnUjIyRRhzReVEX.XcPPr7fXnm0T3A0LjnWspRy6mcW','iosfgjksdkgkldsjfgl@qq.cp',22,2,'2021-05-23 02:07:37',1,0,'2021-05-23 10:27:10','2021-05-23 02:07:37'),(1091,'诺基亚','$2a$10$99f1zjjDOwsaeGcP8Qn4Bu10zPsDEA1FTHC7nFpKSX81bht3fEwh2','7231083332@qq.com',18,2,'2021-05-24 00:57:42',1,0,NULL,'2021-05-24 00:57:42'),(1092,'马♥','$2a$10$Er9G1wdLAv5CD9t0BGll8uaicn1TPuTKd1ALH88Yy9U/dLFN8wFSS','1460234233332@qq.com',18,7,'2021-05-24 03:39:34',1,0,'2021-05-24 16:22:58','2021-05-24 03:39:34');\n/*!40000 ALTER TABLE `employee` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `employee_role`\n--\n\nDROP TABLE IF EXISTS `employee_role`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `employee_role` (\n  `id` int NOT NULL AUTO_INCREMENT,\n  `employeeId` int DEFAULT NULL,\n  `roleId` int DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `employee_role_id_uindex` (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=166 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `employee_role`\n--\n\nLOCK TABLES `employee_role` WRITE;\n/*!40000 ALTER TABLE `employee_role` DISABLE KEYS */;\nINSERT INTO `employee_role` VALUES (14,1001,4),(15,1001,5),(16,1001,8),(17,1001,9),(18,1002,4),(19,1002,5),(20,1002,8),(21,1002,9),(22,1000,4),(23,1000,5),(24,1000,8),(25,1000,9),(39,1,2),(40,1,4),(44,1010,8),(45,1011,5),(49,1013,5),(50,1013,4),(51,1016,5),(52,1016,4),(53,1015,8),(54,1015,5),(55,1015,4),(57,4,1),(58,4,2),(59,4,3),(60,NULL,1),(61,NULL,2),(62,NULL,3),(63,NULL,1),(64,NULL,1),(65,NULL,1),(66,NULL,1),(67,NULL,1),(68,1018,1),(69,1030,1),(70,1030,2),(71,1030,3),(76,1031,2),(77,1031,1),(78,1031,3),(79,1032,2),(84,1031,4),(85,1031,5),(86,1031,7),(87,1031,8),(88,1031,9),(89,1031,10),(90,1031,11),(91,1031,12),(92,1031,13),(93,1031,15),(94,1031,17),(95,1031,18),(98,2,7),(109,9,42),(110,7,37),(111,6,42),(112,8,42),(113,3,42),(118,1032,1),(160,1092,1),(161,1091,42),(165,5,42);\n/*!40000 ALTER TABLE `employee_role` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `permission`\n--\n\nDROP TABLE IF EXISTS `permission`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `permission` (\n  `id` int NOT NULL AUTO_INCREMENT,\n  `name` varchar(255) DEFAULT NULL COMMENT '权限名称',\n  `expression` varchar(255) DEFAULT NULL COMMENT '资源地址',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `permission_id_uindex` (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `permission`\n--\n\nLOCK TABLES `permission` WRITE;\n/*!40000 ALTER TABLE `permission` DISABLE KEYS */;\nINSERT INTO `permission` VALUES (1,'客户列表','customer:list'),(2,'客户状态修改','customer:changeStatus'),(3,'客户新增修改','customer:saveOrUpdate'),(5,'客户池列表','customerPool:list'),(6,'跟进历史列表','followHistory:list'),(7,'跟进历史新增/修改','followHistory:saveOrUpdate'),(8,'移交历史列表','transferHistory:list'),(9,'移交历史新增/修改','transferHistory:saveOrUpdate'),(10,'部门列表','department:list'),(11,'部门删除','department:delete'),(12,'部门新增/修改','department:addOrUpdate'),(13,'员工删除','employee:delete'),(14,'员工列表','employee:list'),(15,'员工编辑','employee:edit'),(16,'员工批量删除','employee:deleteMultiple'),(21,'客户角色管理角色新增','12'),(23,'角色列表','role:list'),(24,'角色删除','role:delete'),(27,'角色新增/修改','role:addOrUpdate'),(28,'数据字典列表','dictionaryContents:list'),(29,'数据列表添加/修改','dictionaryContents:addOrUpdate'),(30,'字典明细列表','dictionaryDetails:list'),(31,'字典明细新增/修改','dictionaryDetails:addOrUpdate'),(32,'客户管理列表','CM:list'),(33,'客户管理新增/修改','CM:addOrUpdate'),(34,'跟进历史新增/修改','CF:addOrUpdate'),(35,'客户移交新增','CH:add'),(36,'跟进历史列表','CH:list'),(37,'统计分析列表','statisticalAnalysis:list');\n/*!40000 ALTER TABLE `permission` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `role`\n--\n\nDROP TABLE IF EXISTS `role`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `role` (\n  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '角色Id',\n  `name` varchar(255) DEFAULT NULL COMMENT '角色名称',\n  `sn` varchar(255) DEFAULT NULL COMMENT '角色编号',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `name` (`name`)\n) ENGINE=InnoDB AUTO_INCREMENT=49 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色表';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `role`\n--\n\nLOCK TABLES `role` WRITE;\n/*!40000 ALTER TABLE `role` DISABLE KEYS */;\nINSERT INTO `role` VALUES (1,'董事长','Chairman of the board'),(2,'ADMIN','System administrator'),(3,'主席','Chairman'),(4,'高级主席','Senior Chairman'),(5,'副主席','Vice Chairman'),(7,'总裁','Chairman'),(8,'会长','President'),(9,'高级总裁','Senior President'),(10,'高级副总裁','Senior Vice President'),(11,'副总裁','Vice president'),(12,'总经理','General manager'),(13,'副总经理','Deputy General Manager'),(22,'总监','Director'),(27,'经理','Manager'),(28,'高级经理','Senior Manager'),(36,'副经理','Deputy manager'),(37,'主任','Director'),(38,'高级主任','Senior Director'),(39,'副主任','Deputy director'),(40,'组长','Group leader'),(41,'副组长','Deputy head'),(42,'普通员工','Worker'),(45,'人事专员','Personnel Specialist'),(46,'市场专员','Marketing Specialist'),(47,'市场主管','Marketing Director'),(48,'销售主管','Sales Executive');\n/*!40000 ALTER TABLE `role` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `role_permission`\n--\n\nDROP TABLE IF EXISTS `role_permission`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!50503 SET character_set_client = utf8mb4 */;\nCREATE TABLE `role_permission` (\n  `id` int NOT NULL AUTO_INCREMENT,\n  `role_id` int NOT NULL COMMENT '角色id',\n  `permission_id` int NOT NULL COMMENT '权限id',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `role_permission_id_uindex` (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=415 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色权限中间表';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `role_permission`\n--\n\nLOCK TABLES `role_permission` WRITE;\n/*!40000 ALTER TABLE `role_permission` DISABLE KEYS */;\nINSERT INTO `role_permission` VALUES (188,4,5),(193,4,1),(194,4,2),(195,4,3),(213,13,1),(214,13,2),(215,13,3),(216,13,5),(217,2,1),(218,2,2),(219,2,3),(220,2,5),(221,2,6),(222,2,7),(223,2,8),(224,2,9),(225,2,10),(226,2,11),(227,2,12),(228,2,13),(229,2,14),(230,2,15),(231,2,16),(256,36,9),(257,37,8),(258,1,1),(259,1,2),(260,1,3),(261,1,5),(262,1,6),(263,1,7),(264,1,8),(265,1,9),(266,1,10),(267,1,11),(268,1,12),(269,1,13),(270,1,14),(271,1,15),(272,1,16),(273,1,21),(274,2,21),(275,3,1),(276,3,2),(277,3,3),(278,3,5),(279,3,6),(280,3,7),(281,3,8),(282,3,9),(283,3,10),(284,3,11),(285,3,12),(286,3,13),(287,3,14),(288,3,15),(289,3,16),(290,3,21),(291,5,1),(292,5,2),(293,5,3),(294,5,5),(295,5,6),(296,4,16),(297,4,21),(298,4,6),(299,4,7),(300,4,8),(301,4,9),(302,4,10),(303,4,11),(304,4,12),(305,4,13),(306,4,14),(307,4,15),(308,5,16),(309,5,21),(310,5,7),(311,5,8),(312,5,9),(313,5,10),(314,5,11),(315,5,12),(316,5,13),(317,5,14),(318,5,15),(319,7,1),(320,7,2),(321,7,3),(322,7,5),(323,7,6),(324,7,7),(325,7,8),(326,7,9),(327,7,10),(328,7,11),(329,7,12),(330,7,13),(331,7,14),(332,7,15),(333,7,16),(334,7,21),(335,8,1),(336,8,2),(337,8,3),(338,8,5),(339,8,6),(340,8,7),(341,8,8),(342,8,9),(343,8,10),(344,8,11),(345,8,12),(346,8,13),(347,8,14),(348,8,15),(349,8,16),(350,8,21),(351,12,1),(352,12,2),(353,12,3),(354,12,5),(355,12,6),(356,12,7),(357,12,8),(358,12,9),(359,12,10),(360,12,11),(361,12,12),(362,12,13),(363,12,14),(364,12,15),(365,12,16),(366,12,21),(367,38,9),(368,39,3),(369,40,9),(370,40,8),(371,41,9),(375,45,13),(376,45,14),(377,45,16),(378,45,15),(379,42,1),(380,42,2),(381,42,3),(382,42,5),(383,42,7),(384,46,3),(385,46,1),(386,47,1),(387,47,2),(388,2,32),(389,2,33),(390,2,34),(391,2,35),(392,2,36),(393,2,37),(394,2,23),(395,2,24),(396,2,27),(397,2,28),(398,2,29),(399,2,30),(400,2,31),(401,1,32),(402,1,33),(403,1,34),(404,1,35),(405,1,36),(406,1,37),(407,1,23),(408,1,24),(409,1,27),(410,1,28),(411,1,29),(412,1,30),(413,1,31),(414,48,36);\n/*!40000 ALTER TABLE `role_permission` ENABLE KEYS */;\nUNLOCK TABLES;\n/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;\n\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n\n-- Dump completed on 2021-05-26 19:23:20\n"
  }
]