├── .editorconfig ├── .env.example ├── .gitignore ├── LICENSE.md ├── README.md ├── babel.config.js ├── certs └── .gitignore ├── jsconfig.json ├── mocker ├── admin │ ├── admin-group │ │ ├── _admin-group-acl.js │ │ ├── _admin-groups.js │ │ └── index.js │ ├── admin │ │ ├── _admins.js │ │ └── index.js │ ├── auth │ │ ├── _groupAcl.js │ │ ├── _groupMenus.js │ │ └── index.js │ ├── index.js │ ├── user │ │ ├── _users.js │ │ └── index.js │ └── util.js └── mobile │ └── index.js ├── package-lock.json ├── package.json ├── public ├── admin │ └── index.html ├── common │ ├── css │ │ └── spinner.css │ ├── favicon.ico │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg ├── mobile │ └── index.html └── website │ └── index.html ├── src ├── core │ ├── assets │ │ ├── images │ │ │ └── avatar.png │ │ └── styles │ │ │ ├── base.less │ │ │ └── mixin.less │ ├── components │ │ ├── Avatar │ │ │ └── index.vue │ │ ├── BlankLayout.vue │ │ ├── Icon │ │ │ └── index.vue │ │ ├── Placeholder │ │ │ ├── PlaceholderForm.vue │ │ │ ├── PlaceholderTextBlock.vue │ │ │ ├── PlaceholderTextRow.vue │ │ │ └── animation.less │ │ ├── PrefectScrollbar │ │ │ └── index.vue │ │ └── Tinymce │ │ │ └── index.vue │ ├── directives │ │ ├── SameWidth.js │ │ └── TransformDom.js │ ├── plugins │ │ └── HttpClient.js │ └── utils │ │ ├── flatry.js │ │ ├── request.js │ │ ├── upload.js │ │ └── util.js └── modules │ ├── admin │ ├── App.vue │ ├── assets │ │ ├── images │ │ │ ├── banner-white.png │ │ │ ├── banner.png │ │ │ ├── login-bg.jpg │ │ │ └── logo.png │ │ └── styles │ │ │ ├── default-layout.less │ │ │ └── style.less │ ├── components │ │ ├── AdminLayout.vue │ │ ├── Breadcrumb.vue │ │ ├── DistrictCascader.vue │ │ ├── ExportButton.vue │ │ ├── HeaderNav.vue │ │ ├── HeaderTab.vue │ │ ├── ImageCropper.vue │ │ ├── LoginForm.vue │ │ ├── LoginModal.vue │ │ ├── Pagination.vue │ │ ├── RemoteSelect.vue │ │ ├── SearchForm.vue │ │ ├── SiderMenu.vue │ │ ├── SiderMenuIcon.vue │ │ ├── SiderMenuItem.vue │ │ ├── SiderMenuSub.vue │ │ └── upload │ │ │ ├── AvatarUpload.vue │ │ │ ├── FileUpload.vue │ │ │ ├── ImageUpload.vue │ │ │ └── _Upload.vue │ ├── main.js │ ├── mixins │ │ ├── SearchFormFieldsMixin.js │ │ └── UnprocessableEntityHttpErrorMixin.js │ ├── plugins │ │ ├── DeleteDialog.js │ │ └── PermissionCheck.js │ ├── registerServiceWorker.js │ ├── router │ │ ├── admin │ │ │ ├── admin-log.js │ │ │ ├── admin.js │ │ │ └── group.js │ │ ├── cms │ │ │ ├── article.js │ │ │ └── category.js │ │ ├── district │ │ │ └── district.js │ │ ├── index.js │ │ ├── shop │ │ │ ├── brand.js │ │ │ ├── category.js │ │ │ └── product.js │ │ └── user │ │ │ ├── address.js │ │ │ └── user.js │ ├── services │ │ ├── AdminGroupService.js │ │ ├── AdminService.js │ │ ├── AuthService.js │ │ ├── CmsArticleService.js │ │ ├── CmsCategoryService.js │ │ ├── DistrictService.js │ │ ├── MiscService.js │ │ ├── OpenService.js │ │ ├── ShopBrandService.js │ │ ├── ShopCategoryService.js │ │ ├── ShopProductService.js │ │ ├── UserAddressService.js │ │ └── UserService.js │ ├── store │ │ ├── app.js │ │ ├── auth │ │ │ └── index.js │ │ ├── index.js │ │ └── tabs │ │ │ └── index.js │ └── views │ │ ├── admin-group │ │ ├── Create.vue │ │ ├── Edit.vue │ │ ├── Index.vue │ │ └── _EditForm.vue │ │ ├── admin-log │ │ └── Index.vue │ │ ├── admin │ │ ├── Create.vue │ │ ├── Edit.vue │ │ ├── Index.vue │ │ └── _EditForm.vue │ │ ├── cms-article │ │ ├── Create.vue │ │ ├── Edit.vue │ │ ├── Index.vue │ │ └── _EditForm.vue │ │ ├── cms-category │ │ ├── Create.vue │ │ ├── Edit.vue │ │ ├── Index.vue │ │ ├── Layout.vue │ │ └── _EditForm.vue │ │ ├── district │ │ ├── Create.vue │ │ ├── Edit.vue │ │ ├── Index.vue │ │ ├── Layout.vue │ │ └── _EditForm.vue │ │ ├── shop-brand │ │ ├── Create.vue │ │ ├── Edit.vue │ │ ├── Index.vue │ │ └── _EditForm.vue │ │ ├── shop-category │ │ ├── Create.vue │ │ ├── Edit.vue │ │ ├── Index.vue │ │ ├── Layout.vue │ │ └── _EditForm.vue │ │ ├── shop-product │ │ └── Index.vue │ │ ├── site │ │ ├── Index.vue │ │ ├── Login.vue │ │ ├── NotFound.vue │ │ └── Profile.vue │ │ ├── user-address │ │ ├── Create.vue │ │ ├── Edit.vue │ │ ├── Index.vue │ │ └── _EditForm.vue │ │ └── user │ │ ├── Create.vue │ │ ├── Edit.vue │ │ ├── Index.vue │ │ └── _EditForm.vue │ ├── mobile │ ├── App.vue │ ├── assets │ │ └── images │ │ │ └── logo.png │ ├── components │ │ └── DefaultLayout.vue │ ├── main.js │ ├── router │ │ └── index.js │ ├── store │ │ └── index.js │ └── views │ │ └── site │ │ ├── Index.vue │ │ └── NotFound.vue │ └── website │ ├── App.vue │ ├── components │ └── DefaultLayout.vue │ ├── entry.client.js │ ├── entry.server.js │ ├── main.js │ ├── router │ └── index.js │ └── views │ └── site │ └── Index.vue ├── vue.config.js ├── vue.entry.admin.js ├── vue.entry.mobile.js ├── vue.entry.website.client.js ├── vue.entry.website.js ├── vue.entry.website.server.js └── webstorm.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{js, jsx, ts, tsx, vue, json, scss, css, less}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | VUE_APP_ADMIN_API_HOST = 'http://localhost:8080/admin/api' 2 | VUE_APP_RESTFUL_API_HOST = 'http://localhost:8081/restful/api' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | .env.production 24 | .env.development 25 | webpack.entry.* 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Huijie Wei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | VUE + ELEMENT.UI 演示项目 2 | 3 | 项目目前正在开发,变动可能会很大,暂时只供研究学习参考 4 | 5 | ### 特点 6 | 7 | 1. 基于 VUE CLI 8 | 9 | 2. 自带 Mock 服务器 10 | 11 | 3. 使用 .env 配置环境变量 12 | 13 | 4. 使用不同配置文件来分割不同项目入口 14 | 15 | ### 在线演示 16 | https://agile.huijiewei.com/admin 17 | 账号:13098761234 18 | 密码:123456 19 | 20 | ### 教程 21 | 22 | #### 安装 23 | 24 | ```bash 25 | npm install 26 | ``` 27 | 28 | #### 配置 29 | 30 | ##### 修改 Hosts 31 | ```text 32 | 127.0.0.1 www.agile.test 33 | ``` 34 | 35 | #### vue.config.js 配置说明 36 | 可以根据 package.json 文件 scripts 内命令行 VUE_CLI_SERVICE_CONFIG_PATH 参数使用不同项目入口配置文件 37 | 38 | 39 | #### 运行本地开发 40 | 复制 .env.example 为 .env.development 41 | 42 | ```bash 43 | npm run serve:admin 44 | npm run serve:mobile 45 | ``` 46 | 47 | #### 构建生产版本 48 | 49 | ```bash 50 | npm run build:admin 51 | npm run build:mobile 52 | ``` 53 | 54 | ### 关键目录说明 55 | ``` 56 | ├── dist // 生成的生产环境文件 57 | ├── mocker // Mock 服务器和 Mock 数据 58 | ├── src // 源代码目录 59 | │   ├── core // 公用核心模块 60 | │   └── modules // 模块目录 61 | │   └── admin // 管理后台 62 | │   └── mobile // 移动端 63 | ``` 64 | 65 | ### 其他说明 66 | 67 | 默认 Mock 服务后台管理员: 68 | 69 | 用户:13012345679 70 | 密码:123456 71 | 72 | ### 后端配合 73 | Agile Spring Boot 74 | 75 | https://github.com/huijiewei/agile-boot 76 | 77 | ### 说明文件版本更新 78 | 79 | Version: 2020-10-31 19:30 80 | 81 | ### Element-UI 的一个小修正 82 | 打开 ./node_modules/element-ui/lib/cascader-panel.js 文件 1053 行 83 | 把 `if(value)` 改为 `if (value !== undefined && value !== null) {` 84 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'], 3 | } 4 | -------------------------------------------------------------------------------- /certs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*"], 3 | "compilerOptions": { 4 | "baseUrl": "./src", 5 | "paths": { 6 | "@core/*": ["core/*"], 7 | "@admin/*": ["modules/admin/*"], 8 | "@mobile/*": ["modules/mobile/*"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /mocker/admin/admin-group/_admin-group-acl.js: -------------------------------------------------------------------------------- 1 | const AdminGroupAcl = [ 2 | { 3 | name: '管理首页', 4 | children: [ 5 | { name: '测试短信发送', actionId: 'site/sms-send' }, 6 | { name: '更新系统缓存', actionId: 'site/clean-cache' }, 7 | ], 8 | }, 9 | { 10 | name: '会员管理', 11 | children: [ 12 | { name: '会员列表', actionId: 'user/index' }, 13 | { name: '会员导出', actionId: 'user/export' }, 14 | { name: '会员新建', actionId: 'user/create' }, 15 | { name: '会员编辑', actionId: 'user/edit' }, 16 | { name: '会员删除', actionId: 'user/delete' }, 17 | { name: '会员导入', actionId: 'user/import' }, 18 | ], 19 | }, 20 | { 21 | name: '商品管理', 22 | children: [ 23 | { 24 | children: [ 25 | { name: '商品列表', actionId: 'good/index' }, 26 | { name: '商品导入', actionId: 'good/export' }, 27 | { name: '新建商品', actionId: 'good/new' }, 28 | { name: '编辑商品', actionId: 'good/edit' }, 29 | { name: '删除商品', actionId: 'good/delete' }, 30 | ], 31 | }, 32 | { 33 | children: [ 34 | { name: '商品分类', actionId: 'shop-category/index' }, 35 | { name: '商品分类新建', actionId: 'shop-category/create' }, 36 | { name: '商品分类编辑', actionId: 'shop-category/edit' }, 37 | { name: '商品分类删除', actionId: 'shop-category/delete' }, 38 | ], 39 | }, 40 | { name: '商品属性', actionId: 'shop/property' }, 41 | { name: '商品特点', actionId: 'shop/feature' }, 42 | { name: '商品品牌', actionId: 'shop/brand' }, 43 | ], 44 | }, 45 | { 46 | name: '系统管理', 47 | children: [ 48 | { 49 | children: [ 50 | { name: '管理员列表', actionId: 'admin/index' }, 51 | { name: '管理员新建', actionId: 'admin/create' }, 52 | { name: '管理员编辑', actionId: 'admin/edit' }, 53 | { name: '管理员删除', actionId: 'admin/delete' }, 54 | ], 55 | }, 56 | { 57 | children: [ 58 | { name: '管理组列表', actionId: 'admin-group/index' }, 59 | { name: '管理组新建', actionId: 'admin-group/create' }, 60 | { name: '管理组编辑', actionId: 'admin-group/edit' }, 61 | { name: '管理组删除', actionId: 'admin-group/delete' }, 62 | ], 63 | }, 64 | ], 65 | }, 66 | ] 67 | 68 | module.exports = AdminGroupAcl 69 | -------------------------------------------------------------------------------- /mocker/admin/admin-group/_admin-groups.js: -------------------------------------------------------------------------------- 1 | const adminGroups = [ 2 | { id: 101, name: '开发组' }, 3 | { id: 102, name: '演示组' }, 4 | { id: 106, name: '测试组' }, 5 | { id: 107, name: '产品组' }, 6 | { id: 108, name: '市场组' }, 7 | { id: 109, name: '运营组' }, 8 | ] 9 | 10 | module.exports = adminGroups 11 | -------------------------------------------------------------------------------- /mocker/admin/admin-group/index.js: -------------------------------------------------------------------------------- 1 | const { authenticationCheck, notFoundCheck } = require('../util') 2 | const adminGroups = require('./_admin-groups') 3 | const adminGroupAcl = require('./_admin-group-acl') 4 | 5 | exports.adminGroupList = (req, res) => { 6 | const success = { 7 | items: adminGroups, 8 | } 9 | 10 | return authenticationCheck(req, res, () => { 11 | return res.json(success) 12 | }) 13 | } 14 | 15 | exports.adminGroupItem = (req, res) => { 16 | const { id } = req.params 17 | 18 | return authenticationCheck(req, res, () => { 19 | const adminGroup = adminGroups.find((item) => { 20 | return item.id === Number(id) 21 | }) 22 | 23 | return notFoundCheck(adminGroup, '管理组不存在', res, () => { 24 | const result = JSON.parse(JSON.stringify(adminGroup)) 25 | 26 | result.acl = [ 27 | 'site/sms-send', 28 | 'site/clean-cache', 29 | 'user/index', 30 | 'user/create', 31 | 'user/import', 32 | 'good/index', 33 | 'good/new', 34 | 'good/edit', 35 | 'good/delete', 36 | 'admin/index', 37 | 'admin/create', 38 | 'admin/edit', 39 | 'admin/delete', 40 | 'admin-group/index', 41 | 'admin-group/create', 42 | 'admin-group/edit', 43 | 'admin-group/delete', 44 | 'user/export', 45 | 'user/edit', 46 | 'user/delete', 47 | 'shop/property', 48 | 'shop/feature', 49 | 'shop/brand', 50 | 'shop-category/index', 51 | 'shop-category/create', 52 | 'shop-category/edit', 53 | 'shop-category/delete', 54 | ] 55 | 56 | return res.json(result) 57 | }) 58 | }) 59 | } 60 | 61 | exports.adminGroupCreate = (req, res) => { 62 | return authenticationCheck(req, res, () => { 63 | const { name } = req.body 64 | 65 | const errors = [] 66 | 67 | const adminGroup = adminGroups.find((item) => { 68 | return item.name === name 69 | }) 70 | 71 | if (adminGroup) { 72 | errors.push({ field: 'phone', message: '管理组 ' + name + ' 已存在' }) 73 | } 74 | 75 | if (errors.length) { 76 | return res.status(422).json(errors) 77 | } 78 | 79 | return res.json({ message: '新建管理组成功', adminId: 1022 }) 80 | }) 81 | } 82 | 83 | exports.adminGroupEdit = (req, res) => { 84 | return authenticationCheck(req, res, () => { 85 | return res.json({ message: '编辑管理组成功' }) 86 | }) 87 | } 88 | 89 | exports.adminGroupDelete = (req, res) => { 90 | return authenticationCheck(req, res, () => { 91 | return res.json({ message: '删除管理组成功' }) 92 | }) 93 | } 94 | 95 | exports.adminGroupAcl = (req, res) => { 96 | return authenticationCheck(req, res, () => { 97 | return res.json(adminGroupAcl) 98 | }) 99 | } 100 | 101 | exports.adminGroupMap = (req, res) => { 102 | return authenticationCheck(req, res, () => { 103 | const map = {} 104 | adminGroups.forEach((adminGroup) => { 105 | map[adminGroup.id] = adminGroup.name 106 | }) 107 | return res.json(map) 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /mocker/admin/admin/_admins.js: -------------------------------------------------------------------------------- 1 | const admins = [ 2 | { 3 | id: 1021, 4 | phone: '13012345678', 5 | email: '', 6 | name: '开发账号', 7 | avatar: 8 | 'https://yuncars-other.oss-cn-shanghai.aliyuncs.com//boilerplate/201903/v4md41vswd_7477uudbmg.jpg?x-oss-process=style/avatar', 9 | createdAt: '2018-07-24 11:48:31', 10 | adminGroup: { id: 101, name: '开发组' }, 11 | }, 12 | { 13 | id: 1022, 14 | phone: '13098761234', 15 | email: '', 16 | name: '演示账号', 17 | avatar: 18 | 'https://yuncars-other.oss-cn-shanghai.aliyuncs.com//boilerplate/201807/150zg7q4u7c.jpg@!avatar', 19 | createdAt: '2018-07-25 10:41:57', 20 | adminGroup: { id: 102, name: '演示组' }, 21 | }, 22 | { 23 | id: 1023, 24 | phone: '13512345678', 25 | email: '', 26 | name: '测试账号', 27 | avatar: 28 | 'https://yuncars-other.oss-cn-shanghai.aliyuncs.com//boilerplate/201808/blnp6u1xtmq.jpg@!avatar', 29 | createdAt: '2018-08-01 09:09:41', 30 | adminGroup: { id: 102, name: '演示组' }, 31 | }, 32 | ] 33 | 34 | module.exports = admins 35 | -------------------------------------------------------------------------------- /mocker/admin/admin/index.js: -------------------------------------------------------------------------------- 1 | const { authenticationCheck, notFoundCheck } = require('../util') 2 | const admins = require('./_admins') 3 | const adminGroups = require('../admin-group/_admin-groups') 4 | 5 | exports.adminList = (req, res) => { 6 | const success = { 7 | items: admins, 8 | } 9 | 10 | return authenticationCheck(req, res, () => { 11 | return res.json(success) 12 | }) 13 | } 14 | 15 | exports.adminItem = (req, res) => { 16 | const { id } = req.params 17 | 18 | return authenticationCheck(req, res, () => { 19 | const admin = admins.find((item) => { 20 | return item.id === Number(id) 21 | }) 22 | 23 | return notFoundCheck(admin, '管理员不存在', res, () => { 24 | const result = JSON.parse(JSON.stringify(admin)) 25 | delete result.group 26 | return res.json(result) 27 | }) 28 | }) 29 | } 30 | 31 | exports.adminCreate = (req, res) => { 32 | return authenticationCheck(req, res, () => { 33 | const { phone, groupId, password } = req.body 34 | 35 | const errors = [] 36 | 37 | const admin = admins.find((item) => { 38 | return item.phone === phone 39 | }) 40 | 41 | if (admin) { 42 | errors.push({ 43 | field: 'phone', 44 | message: '手机号码 ' + phone + ' 已被占用', 45 | }) 46 | } 47 | 48 | if (password.length < 6) { 49 | errors.push({ field: 'password', message: '密码不能少于 6 位' }) 50 | } 51 | 52 | const group = adminGroups.find((item) => { 53 | return item.id === Number(groupId) 54 | }) 55 | 56 | if (!group) { 57 | errors.push({ field: 'groupId', message: '选择了不存在的用户组' }) 58 | } 59 | 60 | if (errors.length) { 61 | return res.status(422).json(errors) 62 | } 63 | 64 | return res.json({ message: '新建管理员成功', adminId: 1022 }) 65 | }) 66 | } 67 | 68 | exports.adminEdit = (req, res) => { 69 | return authenticationCheck(req, res, () => { 70 | return res.json({ message: '编辑管理员成功' }) 71 | }) 72 | } 73 | 74 | exports.adminDelete = (req, res) => { 75 | return authenticationCheck(req, res, () => { 76 | return res.json({ message: '删除管理员成功' }) 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /mocker/admin/auth/_groupAcl.js: -------------------------------------------------------------------------------- 1 | const groupAcl = [ 2 | 'site/sms-send', 3 | 'site/clean-cache', 4 | 'user/index', 5 | 'user/create', 6 | 'user/import', 7 | 'good/index', 8 | 'good/new', 9 | 'good/edit', 10 | 'good/delete', 11 | 'admin/index', 12 | 'admin/create', 13 | 'admin/edit', 14 | 'admin/delete', 15 | 'admin-group/index', 16 | 'admin-group/edit', 17 | 'user/export', 18 | 'user/edit', 19 | 'user/delete', 20 | 'user/delete', 21 | 'shop/property', 22 | 'shop/feature', 23 | 'shop/brand', 24 | 'shop-category/index', 25 | 'shop-category/create', 26 | 'shop-category/edit', 27 | 'shop-category/delete', 28 | 'shop-category/index', 29 | 'shop-category/create', 30 | 'shop-category/edit', 31 | 'shop-category/delete', 32 | 'shop/property', 33 | 'shop/feature', 34 | 'shop/brand', 35 | ] 36 | 37 | module.exports = groupAcl 38 | -------------------------------------------------------------------------------- /mocker/admin/auth/_groupMenus.js: -------------------------------------------------------------------------------- 1 | const groupMenus = [ 2 | { 3 | label: '首页', 4 | icon: 'home', 5 | url: 'site/index', 6 | public: true, 7 | }, 8 | { 9 | label: '会员管理', 10 | icon: 'user', 11 | children: [ 12 | { 13 | label: '会员列表', 14 | url: 'user/index', 15 | }, 16 | { 17 | label: '新建会员', 18 | children: [ 19 | { 20 | label: '新建会员', 21 | url: 'user/create', 22 | }, 23 | { 24 | label: '批量导入', 25 | url: 'user/import', 26 | }, 27 | ], 28 | }, 29 | ], 30 | }, 31 | { 32 | label: '商品管理', 33 | icon: 'gift', 34 | children: [ 35 | { 36 | label: '新建商品', 37 | url: 'good/new', 38 | }, 39 | { 40 | label: '商品列表', 41 | url: 'good/index', 42 | }, 43 | { 44 | label: '商品分类', 45 | url: 'shop-category/index', 46 | }, 47 | { 48 | label: '商品属性', 49 | url: 'shop/property', 50 | }, 51 | { 52 | label: '商品特点', 53 | url: 'shop/feature', 54 | }, 55 | { 56 | label: '商品品牌', 57 | url: 'shop/brand', 58 | }, 59 | ], 60 | }, 61 | { 62 | label: '系统管理', 63 | icon: 'setting', 64 | children: [ 65 | { 66 | label: '管理员', 67 | url: 'admin/index', 68 | }, 69 | { 70 | label: '管理组', 71 | url: 'admin-group/index', 72 | }, 73 | ], 74 | }, 75 | ] 76 | 77 | module.exports = groupMenus 78 | -------------------------------------------------------------------------------- /mocker/admin/auth/index.js: -------------------------------------------------------------------------------- 1 | const groupAcl = require('./_groupAcl') 2 | const groupMenus = require('./_groupMenus') 3 | const { authenticationCheck } = require('../util') 4 | 5 | exports.authLogin = (req, res) => { 6 | const { account, password } = req.body 7 | 8 | if (!account || account.length === 0) { 9 | return res 10 | .status(422) 11 | .json([{ field: 'account', message: '请输入手机号码' }]) 12 | } 13 | 14 | if (!password || password.length === 0) { 15 | return res.status(422).json([{ field: 'password', message: '请输入密码' }]) 16 | } 17 | 18 | if (!/^1[3456789]\d{9}$/.test(account)) { 19 | return res 20 | .status(422) 21 | .json([{ field: 'account', message: '无效的手机号码' }]) 22 | } 23 | 24 | if (account !== '13012345679') { 25 | return res 26 | .status(422) 27 | .json([{ field: 'account', message: '手机号码不存在' }]) 28 | } 29 | 30 | if (password !== '123456') { 31 | return res.status(422).json([{ field: 'password', message: '密码错误' }]) 32 | } 33 | 34 | return res.json({ 35 | currentUser: { 36 | id: 1021, 37 | phone: '13012345678', 38 | email: '', 39 | name: '开发账号', 40 | avatar: 41 | 'https://yuncars-other.oss-cn-shanghai.aliyuncs.com//boilerplate/201903/v4md41vswd_7477uudbmg.jpg?x-oss-process=style/avatar', 42 | adminGroup: { id: 101, name: '开发组' }, 43 | }, 44 | accessToken: 'bmq7tDtL5GqT9b64', 45 | groupMenus: groupMenus, 46 | groupPermissions: groupAcl, 47 | }) 48 | } 49 | 50 | exports.authLogout = (req, res) => { 51 | return res.json({ 52 | message: '退出登录成功', 53 | }) 54 | } 55 | 56 | exports.authAccount = (req, res) => { 57 | const success = { 58 | currentUser: { 59 | id: 1021, 60 | phone: '13012345678', 61 | email: '', 62 | name: '开发账号', 63 | avatar: 64 | 'https://yuncars-other.oss-cn-shanghai.aliyuncs.com//boilerplate/201903/v4md41vswd_7477uudbmg.jpg?x-oss-process=style/avatar', 65 | adminGroup: { id: 101, name: '开发组' }, 66 | }, 67 | groupMenus: groupMenus, 68 | groupPermissions: groupAcl, 69 | } 70 | 71 | return authenticationCheck(req, res, () => { 72 | res.json(success) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /mocker/admin/index.js: -------------------------------------------------------------------------------- 1 | const delay = require('mocker-api/lib/delay') 2 | const { authLogin, authLogout, authAccount } = require('./auth/index') 3 | const { 4 | adminList, 5 | adminItem, 6 | adminEdit, 7 | adminCreate, 8 | adminDelete, 9 | } = require('./admin/index') 10 | const { 11 | adminGroupList, 12 | adminGroupItem, 13 | adminGroupEdit, 14 | adminGroupCreate, 15 | adminGroupDelete, 16 | adminGroupOptions, 17 | adminGroupAcl, 18 | } = require('./admin-group/index') 19 | const { userList, userItem, userCreate, userDelete } = require('./user/index') 20 | 21 | const noProxy = process.env.NO_PROXY === 'true' 22 | 23 | const proxy = { 24 | _proxy: { 25 | changeHost: true, 26 | }, 27 | 'POST /admin/api/auth/login': authLogin, 28 | 'POST /admin/api/auth/logout': authLogout, 29 | 'GET /admin/api/auth/account': authAccount, 30 | 'GET /admin/api/admins': adminList, 31 | 'POST /admin/api/admins': adminCreate, 32 | 'GET /admin/api/admins/:id(\\d+)': adminItem, 33 | 'PUT /admin/api/admins/:id(\\d+)': adminEdit, 34 | 'DELETE /admin/api/admins/:id(\\d+)': adminDelete, 35 | 'GET /admin/api/admin-groups': adminGroupList, 36 | 'POST /admin/api/admin-groups': adminGroupCreate, 37 | 'GET /admin/api/admin-groups/:id(\\d+)': adminGroupItem, 38 | 'PUT /admin/api/admin-groups/:id(\\d+)': adminGroupEdit, 39 | 'DELETE /admin/api/admin-groups/:id(\\d+)': adminGroupDelete, 40 | 'GET /admin/api/open/admin-group-acl': adminGroupAcl, 41 | 'GET /admin/api/open/admin-group-options': adminGroupOptions, 42 | 'GET /admin/api/users': userList, 43 | 'POST /admin/api/users': userCreate, 44 | 'GET /admin/api/users/:id(\\d+)': userItem, 45 | 'DELETE /admin/api/users/:id(\\d+)': userDelete, 46 | } 47 | 48 | module.exports = noProxy ? {} : delay(proxy, 500) 49 | -------------------------------------------------------------------------------- /mocker/admin/user/index.js: -------------------------------------------------------------------------------- 1 | const { authenticationCheck } = require('../util') 2 | const users = require('./_users') 3 | 4 | exports.userList = (req, res) => { 5 | const { page, withSearchFields } = req.query 6 | 7 | const success = { 8 | items: users, 9 | pages: { 10 | totalCount: 1200, 11 | pageCount: 120, 12 | currentPage: Number(page), 13 | perPage: 10, 14 | }, 15 | searchFields: withSearchFields 16 | ? [ 17 | { type: 'keyword', field: 'phone', label: '手机号码' }, 18 | { type: 'keyword', field: 'name', label: '姓名' }, 19 | { 20 | type: 'select', 21 | field: 'createdFrom', 22 | label: '注册来源', 23 | multiple: true, 24 | options: { 25 | SYSTEM: '系统', 26 | APP: '应用', 27 | WEB: '网站', 28 | WECHAT: '微信', 29 | }, 30 | }, 31 | { 32 | type: 'dateTimeRange', 33 | rangeType: 'daterange', 34 | field: 'createdRange', 35 | labelStart: '注册开始日期', 36 | labelEnd: '注册结束日期', 37 | shortcuts: [ 38 | { 39 | text: '最近一周', 40 | start: '2019-11-07', 41 | end: '2019-11-14', 42 | }, 43 | { 44 | text: '最近一个月', 45 | start: '2019-10-14', 46 | end: '2019-11-14', 47 | }, 48 | ], 49 | }, 50 | { type: 'br' }, 51 | ] 52 | : null, 53 | } 54 | 55 | return authenticationCheck(req, res, () => { 56 | return res.json(success) 57 | }) 58 | } 59 | 60 | exports.userItem = (req, res) => { 61 | return res.status(404).json({ 62 | message: '用户不存在', 63 | }) 64 | } 65 | 66 | exports.userCreate = (req, res) => { 67 | return res.status(404).json({ 68 | message: '服务不存在', 69 | }) 70 | } 71 | 72 | exports.userDelete = (req, res) => { 73 | return res.status(401).json({ 74 | message: '请先登录', 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /mocker/admin/util.js: -------------------------------------------------------------------------------- 1 | exports.authenticationCheck = function (req, res, success) { 2 | const clientId = req.get('X-Client-Id') || '' 3 | 4 | if (clientId === '') { 5 | return res.status(400).json({ 6 | name: 'BadRequest', 7 | message: '无效的 Client Id,请刷新页面重新操作', 8 | code: 0, 9 | status: 400, 10 | }) 11 | } 12 | 13 | const accessToken = req.get('X-Access-Token') || '' 14 | 15 | if (accessToken !== 'bmq7tDtL5GqT9b64') { 16 | return res.status(401).json({ 17 | name: 'Unauthorized', 18 | message: '必须登陆才能进行操作', 19 | code: 0, 20 | status: 401, 21 | }) 22 | } 23 | 24 | return success() 25 | } 26 | 27 | exports.notFoundCheck = function (item, message, res, success) { 28 | if (item === null) { 29 | return res.status(404).json({ 30 | name: 'Not Found', 31 | message: message, 32 | code: 0, 33 | status: 404, 34 | }) 35 | } else { 36 | return success() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /mocker/mobile/index.js: -------------------------------------------------------------------------------- 1 | const delay = require('mocker-api/lib/delay') 2 | 3 | const noProxy = process.env.NO_PROXY === 'true' 4 | 5 | const proxy = { 6 | _proxy: { 7 | changeHost: true, 8 | }, 9 | 'GET /mobile/api/auth/authentication': (req, res) => { 10 | return res.json({ 11 | id: 1, 12 | name: 'test', 13 | }) 14 | }, 15 | } 16 | 17 | module.exports = noProxy ? {} : delay(proxy, 1000) 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agile-vue", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve:admin": "cross-env MODULE_CONFIG=./vue.entry.admin.js vue-cli-service serve", 7 | "serve:mobile": "cross-env MODULE_CONFIG=./vue.entry.mobile.js vue-cli-service serve", 8 | "build:admin": "cross-env MODULE_CONFIG=./vue.entry.admin.js vue-cli-service build", 9 | "build:mobile": "cross-env MODULE_CONFIG=./vue.entry.mobile.js vue-cli-service build", 10 | "inspect:admin": "cross-env MODULE_CONFIG=./vue.entry.admin.js vue-cli-service inspect --mode production > webpack.entry.admin.js", 11 | "inspect:mobile": "cross-env MODULE_CONFIG=./vue.entry.mobile.js vue-cli-service inspect --mode production > webpack.entry.mobile.js" 12 | }, 13 | "dependencies": { 14 | "@tinymce/tinymce-vue": "^3.2.8", 15 | "axios": "^0.21.0", 16 | "axios-extensions": "^3.1.3", 17 | "axios-progress-bar": "^1.2.0", 18 | "core-js": "^3.11.1", 19 | "element-ui": "^2.15.1", 20 | "perfect-scrollbar": "^1.5.0", 21 | "query-string": "^7.0.0", 22 | "register-service-worker": "^1.7.1", 23 | "vue": "^2.6.12", 24 | "vue-context": "^6.0.0", 25 | "vue-cropperjs": "^4.2.0", 26 | "vue-router": "^3.4.9", 27 | "vue-router-back-button": "^1.3.0", 28 | "vue-server-renderer": "^2.6.12", 29 | "vuex": "^3.5.1" 30 | }, 31 | "devDependencies": { 32 | "@vue/cli-plugin-babel": "^4.5.12", 33 | "@vue/cli-plugin-eslint": "^4.5.12", 34 | "@vue/cli-plugin-pwa": "^4.5.12", 35 | "@vue/cli-plugin-router": "^4.5.12", 36 | "@vue/cli-plugin-vuex": "^4.5.12", 37 | "@vue/cli-service": "^4.5.12", 38 | "@vue/eslint-config-prettier": "^6.0.0", 39 | "assign-deep": "^1.0.1", 40 | "babel-eslint": "^10.1.0", 41 | "cross-env": "^7.0.2", 42 | "elliptic": ">=6.5.4", 43 | "eslint": "^7.12.1", 44 | "eslint-plugin-prettier": "^3.1.4", 45 | "eslint-plugin-vue": "^7.1.0", 46 | "koa": "^2.13.0", 47 | "less": "^4.1.1", 48 | "less-loader": "^7.3.0", 49 | "mocker-api": "^2.7.1", 50 | "prettier": "^2.1.2", 51 | "vue-template-compiler": "^2.6.12" 52 | }, 53 | "eslintConfig": { 54 | "root": true, 55 | "env": { 56 | "node": true 57 | }, 58 | "extends": [ 59 | "plugin:vue/essential", 60 | "@vue/prettier" 61 | ], 62 | "rules": {}, 63 | "parserOptions": { 64 | "parser": "babel-eslint" 65 | } 66 | }, 67 | "prettier": { 68 | "tabWidth": 2, 69 | "semi": false, 70 | "singleQuote": true 71 | }, 72 | "postcss": { 73 | "plugins": { 74 | "autoprefixer": {} 75 | } 76 | }, 77 | "browserslist": [ 78 | "> 1%", 79 | "last 2 versions" 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /public/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 15 |
16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /public/common/css/spinner.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | /* Absolute Center Spinner */ 7 | #spinner { 8 | position: fixed; 9 | z-index: 999; 10 | overflow: show; 11 | margin: auto; 12 | top: 0; 13 | left: 0; 14 | bottom: 0; 15 | right: 0; 16 | width: 50px; 17 | height: 50px; 18 | } 19 | 20 | /* Transparent Overlay */ 21 | #spinner:before { 22 | content: ''; 23 | display: block; 24 | position: fixed; 25 | top: 0; 26 | left: 0; 27 | width: 100%; 28 | height: 100%; 29 | background-color: rgba(255, 255, 255, 0.75); 30 | } 31 | 32 | /* :not(:required) hides these rules from IE9 and below */ 33 | #spinner:not(:required) { 34 | /* hide "loading..." text */ 35 | font: 0/0 a; 36 | color: transparent; 37 | text-shadow: none; 38 | background-color: transparent; 39 | border: 0; 40 | } 41 | 42 | #spinner:not(:required):after { 43 | content: ''; 44 | display: block; 45 | font-size: 10px; 46 | width: 50px; 47 | height: 50px; 48 | margin-top: -0.5em; 49 | 50 | border: 15px solid rgb(144, 147, 150); 51 | border-radius: 100%; 52 | border-bottom-color: transparent; 53 | -webkit-animation: spinner 1s linear 0s infinite; 54 | animation: spinner 1s linear 0s infinite; 55 | } 56 | 57 | /* Animation */ 58 | @-webkit-keyframes spinner { 59 | 0% { 60 | -webkit-transform: rotate(0deg); 61 | -moz-transform: rotate(0deg); 62 | -ms-transform: rotate(0deg); 63 | -o-transform: rotate(0deg); 64 | transform: rotate(0deg); 65 | } 66 | 100% { 67 | -webkit-transform: rotate(360deg); 68 | -moz-transform: rotate(360deg); 69 | -ms-transform: rotate(360deg); 70 | -o-transform: rotate(360deg); 71 | transform: rotate(360deg); 72 | } 73 | } 74 | 75 | @-moz-keyframes spinner { 76 | 0% { 77 | -webkit-transform: rotate(0deg); 78 | -moz-transform: rotate(0deg); 79 | -ms-transform: rotate(0deg); 80 | -o-transform: rotate(0deg); 81 | transform: rotate(0deg); 82 | } 83 | 100% { 84 | -webkit-transform: rotate(360deg); 85 | -moz-transform: rotate(360deg); 86 | -ms-transform: rotate(360deg); 87 | -o-transform: rotate(360deg); 88 | transform: rotate(360deg); 89 | } 90 | } 91 | 92 | @-o-keyframes spinner { 93 | 0% { 94 | -webkit-transform: rotate(0deg); 95 | -moz-transform: rotate(0deg); 96 | -ms-transform: rotate(0deg); 97 | -o-transform: rotate(0deg); 98 | transform: rotate(0deg); 99 | } 100 | 100% { 101 | -webkit-transform: rotate(360deg); 102 | -moz-transform: rotate(360deg); 103 | -ms-transform: rotate(360deg); 104 | -o-transform: rotate(360deg); 105 | transform: rotate(360deg); 106 | } 107 | } 108 | 109 | @keyframes spinner { 110 | 0% { 111 | -webkit-transform: rotate(0deg); 112 | -moz-transform: rotate(0deg); 113 | -ms-transform: rotate(0deg); 114 | -o-transform: rotate(0deg); 115 | transform: rotate(0deg); 116 | } 117 | 100% { 118 | -webkit-transform: rotate(360deg); 119 | -moz-transform: rotate(360deg); 120 | -ms-transform: rotate(360deg); 121 | -o-transform: rotate(360deg); 122 | transform: rotate(360deg); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /public/common/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huijiewei/agile-vue/d1bc73035a769c7364215a20351055913dd1f19e/public/common/favicon.ico -------------------------------------------------------------------------------- /public/common/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huijiewei/agile-vue/d1bc73035a769c7364215a20351055913dd1f19e/public/common/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/common/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huijiewei/agile-vue/d1bc73035a769c7364215a20351055913dd1f19e/public/common/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/common/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huijiewei/agile-vue/d1bc73035a769c7364215a20351055913dd1f19e/public/common/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/common/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huijiewei/agile-vue/d1bc73035a769c7364215a20351055913dd1f19e/public/common/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/common/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huijiewei/agile-vue/d1bc73035a769c7364215a20351055913dd1f19e/public/common/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/common/icons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huijiewei/agile-vue/d1bc73035a769c7364215a20351055913dd1f19e/public/common/icons/mstile-144x144.png -------------------------------------------------------------------------------- /public/common/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huijiewei/agile-vue/d1bc73035a769c7364215a20351055913dd1f19e/public/common/icons/mstile-150x150.png -------------------------------------------------------------------------------- /public/common/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/mobile/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /public/website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/core/assets/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huijiewei/agile-vue/d1bc73035a769c7364215a20351055913dd1f19e/src/core/assets/images/avatar.png -------------------------------------------------------------------------------- /src/core/assets/styles/base.less: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | background-color: #fff; 5 | } 6 | 7 | body { 8 | -webkit-font-smoothing: antialiased; 9 | background: #fff; 10 | margin: 0; 11 | overflow-x: hidden; 12 | color: #67757c; 13 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, 14 | Microsoft YaHei, SimSun, sans-serif; 15 | } 16 | 17 | a:focus, 18 | a:hover { 19 | text-decoration: none; 20 | } 21 | 22 | svg { 23 | pointer-events: none; 24 | } 25 | 26 | .uppercase, 27 | .uppercase input { 28 | text-transform: uppercase; 29 | } 30 | 31 | .cursor-pointer { 32 | cursor: pointer; 33 | } 34 | 35 | .vertical-center { 36 | vertical-align: middle !important; 37 | white-space: nowrap; 38 | } 39 | 40 | .vertical-bottom { 41 | vertical-align: bottom !important; 42 | white-space: nowrap; 43 | } 44 | 45 | .text-mono { 46 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 47 | 'Courier New', monospace; 48 | } 49 | 50 | .text-nowrap { 51 | white-space: nowrap; 52 | } 53 | 54 | .text-delete { 55 | text-decoration: line-through; 56 | } 57 | 58 | .text-right { 59 | text-align: right; 60 | } 61 | 62 | .color-red { 63 | color: #f00; 64 | } 65 | 66 | hr.spacer-lg, 67 | hr.spacer-md, 68 | hr.spacer-sm, 69 | hr.spacer-xl, 70 | hr.spacer-xs { 71 | border: medium none; 72 | margin-bottom: 0; 73 | margin-top: 0; 74 | } 75 | 76 | hr.spacer-lg { 77 | height: 50px; 78 | } 79 | 80 | hr.spacer-md { 81 | height: 39px; 82 | } 83 | 84 | hr.spacer-sm { 85 | height: 30px; 86 | } 87 | 88 | hr.spacer-xl { 89 | height: 20px; 90 | } 91 | 92 | hr.spacer-xs { 93 | height: 10px; 94 | } 95 | 96 | ::-webkit-scrollbar { 97 | width: 9px; 98 | height: 9px; 99 | } 100 | 101 | ::-webkit-scrollbar-track { 102 | border-radius: 3px; 103 | background: rgba(0, 0, 0, 0.06); 104 | -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.08); 105 | } 106 | 107 | ::-webkit-scrollbar-thumb { 108 | border-radius: 3px; 109 | background: rgba(0, 0, 0, 0.12); 110 | -webkit-box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.2); 111 | } 112 | -------------------------------------------------------------------------------- /src/core/assets/styles/mixin.less: -------------------------------------------------------------------------------- 1 | .clearfix { 2 | &::after { 3 | display: block; 4 | content: ''; 5 | clear: both; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/core/components/Avatar/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 38 | 39 | 53 | -------------------------------------------------------------------------------- /src/core/components/BlankLayout.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/core/components/Icon/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | 44 | -------------------------------------------------------------------------------- /src/core/components/Placeholder/PlaceholderForm.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 84 | 85 | 95 | -------------------------------------------------------------------------------- /src/core/components/Placeholder/PlaceholderTextBlock.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 48 | 49 | 54 | -------------------------------------------------------------------------------- /src/core/components/Placeholder/PlaceholderTextRow.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 38 | 39 | 47 | -------------------------------------------------------------------------------- /src/core/components/Placeholder/animation.less: -------------------------------------------------------------------------------- 1 | .placeholder-animation { 2 | &.placeholder-text-row, 3 | .placeholder-text-row { 4 | animation: placeholder-pulse 1s infinite ease-in-out; 5 | } 6 | } 7 | 8 | @keyframes placeholder-pulse { 9 | 0% { 10 | opacity: 0.3; 11 | } 12 | 13 | 50% { 14 | opacity: 0.9; 15 | } 16 | 17 | 100% { 18 | opacity: 0.3; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/core/components/PrefectScrollbar/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 55 | 56 | 105 | -------------------------------------------------------------------------------- /src/core/components/Tinymce/index.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 101 | -------------------------------------------------------------------------------- /src/core/directives/SameWidth.js: -------------------------------------------------------------------------------- 1 | const childrenSameWidth = function (el, childClassName) { 2 | if (el.dataset.sameWidth === 'DONE') { 3 | return 4 | } 5 | 6 | const items = el.getElementsByClassName(childClassName) 7 | 8 | if (items.length < 2) { 9 | return 10 | } 11 | 12 | let maxWidth = 0 13 | 14 | for (const item of items) { 15 | const width = item.offsetWidth 16 | 17 | if (width > maxWidth) { 18 | maxWidth = width 19 | } 20 | } 21 | 22 | for (const item of items) { 23 | item.style.width = maxWidth + 2 + 'px' 24 | } 25 | 26 | el.dataset.sameWidth = 'DONE' 27 | } 28 | 29 | const SameWidth = { 30 | inserted: function (el, binding) { 31 | childrenSameWidth(el, binding.value) 32 | }, 33 | componentUpdated: function (el, binding) { 34 | childrenSameWidth(el, binding.value) 35 | }, 36 | } 37 | 38 | export default SameWidth 39 | -------------------------------------------------------------------------------- /src/core/directives/TransformDom.js: -------------------------------------------------------------------------------- 1 | const TransformDom = { 2 | inserted: function (el, bindings) { 3 | const container = bindings.arg 4 | ? document.getElementById(bindings.arg) 5 | : document.body 6 | 7 | if (container) { 8 | bindings.modifiers.prepend && container.firstChild 9 | ? container.insertBefore(el, container.firstChild) 10 | : container.appendChild(el) 11 | } 12 | }, 13 | 14 | unbind: function (el) { 15 | if (el.parentNode) { 16 | el.parentNode.removeChild(el) 17 | } 18 | }, 19 | } 20 | 21 | export default TransformDom 22 | -------------------------------------------------------------------------------- /src/core/plugins/HttpClient.js: -------------------------------------------------------------------------------- 1 | import Request from '../utils/request' 2 | 3 | const UnauthorizedHttpCode = 401 4 | const UnprocessableEntityHttpCode = 422 5 | 6 | const HttpGetMethod = ['GET', 'HEAD'] 7 | 8 | const HttpClient = { 9 | install( 10 | Vue, 11 | { 12 | getApiHost, 13 | getAccessToken, 14 | setLoginAction, 15 | setErrorMessage, 16 | paramsSerializer = null, 17 | } 18 | ) { 19 | const request = new Request({ 20 | baseUrl: getApiHost(), 21 | paramsSerializer: paramsSerializer, 22 | beforeRequest: (config) => { 23 | const accessToken = getAccessToken() 24 | 25 | if (accessToken) { 26 | config.headers['X-Client-Id'] = accessToken.clientId 27 | config.headers['X-Access-Token'] = accessToken.accessToken 28 | } 29 | 30 | return config 31 | }, 32 | onSuccess: (response) => { 33 | if (response.config.responseType === 'blob') { 34 | return Promise.resolve(response) 35 | } 36 | 37 | return Promise.resolve(response.data) 38 | }, 39 | onError: (error) => { 40 | const historyBack = error.config.historyBack 41 | 42 | if (!error.response) { 43 | setErrorMessage(error.message, historyBack) 44 | 45 | return Promise.reject(error) 46 | } 47 | 48 | if (error.response.status === UnauthorizedHttpCode) { 49 | if (!error.config.__storeDispatch) { 50 | error.config.__storeDispatch = true 51 | 52 | if ( 53 | historyBack || 54 | HttpGetMethod.includes(error.config.method.toUpperCase()) 55 | ) { 56 | setLoginAction('direct') 57 | } else { 58 | setLoginAction('modal') 59 | } 60 | } 61 | 62 | return Promise.reject(error) 63 | } 64 | 65 | if (error.response.status === UnprocessableEntityHttpCode) { 66 | return Promise.reject(error) 67 | } 68 | 69 | setErrorMessage( 70 | error.response.data.detail || 71 | error.response.data.message || 72 | error.response.data.title || 73 | error.response.statusText || 74 | '网络请求错误', 75 | historyBack 76 | ) 77 | 78 | return Promise.reject(error) 79 | }, 80 | }) 81 | 82 | Vue.http = request 83 | Vue.prototype.$http = request 84 | }, 85 | } 86 | 87 | export default HttpClient 88 | -------------------------------------------------------------------------------- /src/core/utils/flatry.js: -------------------------------------------------------------------------------- 1 | const flatry = (promise) => 2 | promise 3 | .then((data) => ({ data, error: null })) 4 | .catch((error) => ({ error, data: null })) 5 | 6 | export default flatry 7 | -------------------------------------------------------------------------------- /src/core/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { loadProgressBar } from 'axios-progress-bar' 3 | import 'axios-progress-bar/dist/nprogress.css' 4 | import { throttleAdapterEnhancer } from 'axios-extensions' 5 | import flatry from '@core/utils/flatry' 6 | 7 | class Request { 8 | constructor(options) { 9 | const opt = { 10 | ...{ 11 | baseUrl: '', 12 | timeout: 10 * 1000, 13 | withCredentials: false, 14 | paramsSerializer: null, 15 | beforeRequest: (config) => { 16 | return config 17 | }, 18 | onSuccess: (response) => Promise.resolve(response), 19 | onError: (error) => Promise.reject(error), 20 | }, 21 | ...options, 22 | } 23 | 24 | const axiosConfig = { 25 | baseURL: opt.baseUrl, 26 | timeout: opt.timeout, 27 | withCredentials: opt.withCredentials, 28 | paramsSerializer: opt.paramsSerializer, 29 | adapter: throttleAdapterEnhancer(axios.defaults.adapter), 30 | } 31 | 32 | const httpClient = axios.create(axiosConfig) 33 | 34 | loadProgressBar({ showSpinner: false }, httpClient) 35 | 36 | httpClient.interceptors.request.use( 37 | (config) => { 38 | return opt.beforeRequest(config) 39 | }, 40 | (error) => { 41 | return opt.onError(error) 42 | } 43 | ) 44 | 45 | httpClient.interceptors.response.use( 46 | (response) => { 47 | return opt.onSuccess(response) 48 | }, 49 | (error) => { 50 | return opt.onError(error) 51 | } 52 | ) 53 | 54 | this.httpClient = httpClient 55 | } 56 | 57 | request( 58 | method, 59 | url, 60 | params = null, 61 | data = null, 62 | historyBack = false, 63 | cancelIgnore = false 64 | ) { 65 | const config = { 66 | url: url, 67 | method: method, 68 | historyBack: historyBack, 69 | cancelIgnore: cancelIgnore, 70 | } 71 | 72 | if (params) { 73 | config.params = params 74 | } 75 | 76 | if (data) { 77 | config.data = data 78 | } 79 | 80 | return flatry(this.httpClient.request(config)) 81 | } 82 | 83 | get(url, params = null, historyBack = true, cancelIgnore = false) { 84 | return this.request('GET', url, params, null, historyBack, cancelIgnore) 85 | } 86 | 87 | post(url, data = null, params = null, historyBack = false) { 88 | return this.request('POST', url, params, data, historyBack) 89 | } 90 | 91 | put(url, data = null, params = null, historyBack = false) { 92 | return this.request('PUT', url, params, data, historyBack) 93 | } 94 | 95 | path(url, data = null, params = null, historyBack = false) { 96 | return this.request('PATH', url, params, data, historyBack) 97 | } 98 | 99 | delete(url, params = null, historyBack = false) { 100 | return this.request('DELETE', url, params, null, historyBack) 101 | } 102 | 103 | download(method, url, params = null, data = null, historyBack = false) { 104 | const config = { 105 | url: url, 106 | method: method, 107 | timeout: 120 * 1000, 108 | responseType: 'blob', 109 | historyBack: historyBack, 110 | onDownloadProgress: (progressEvent) => { 111 | console.log(progressEvent) 112 | }, 113 | } 114 | 115 | if (params) { 116 | config.params = params 117 | } 118 | 119 | if (data) { 120 | config.data = data 121 | } 122 | 123 | return flatry( 124 | this.httpClient.request(config).then((response) => { 125 | let filename = response.headers['x-suggested-filename'] 126 | 127 | if (!filename) { 128 | filename = response.headers['content-disposition'].match( 129 | /filename="(.+)"/ 130 | )[1] 131 | } 132 | 133 | if (filename) { 134 | const url = window.URL.createObjectURL( 135 | new Blob([response.data], { 136 | type: response.headers['content-type'], 137 | }) 138 | ) 139 | const link = document.createElement('a') 140 | link.href = url 141 | link.setAttribute('download', decodeURIComponent(filename)) 142 | link.click() 143 | window.URL.revokeObjectURL(url) 144 | 145 | return true 146 | } else { 147 | return false 148 | } 149 | }) 150 | ) 151 | } 152 | } 153 | 154 | export default Request 155 | -------------------------------------------------------------------------------- /src/core/utils/upload.js: -------------------------------------------------------------------------------- 1 | import Request from '@core/utils/request' 2 | 3 | const Upload = { 4 | defaultOption: () => { 5 | return { 6 | url: '', 7 | cropUrl: '', 8 | timeout: 0, 9 | params: [], 10 | headers: [], 11 | dataType: '', 12 | paramName: '', 13 | imageProcess: '', 14 | responseParse: '', 15 | sizeLimit: 0, 16 | typesLimit: [], 17 | } 18 | }, 19 | upload: (url, option, file, onSuccess, onError) => { 20 | const request = new Request({ 21 | onSuccess: (response) => { 22 | const result = 23 | option.dataType === 'xml' 24 | ? new DOMParser().parseFromString(response.data, 'application/xml') 25 | : response.data 26 | 27 | // eslint-disable-next-line no-new-func 28 | const responseParse = new Function('result', option.responseParse) 29 | 30 | const upload = responseParse(result) 31 | 32 | onSuccess(upload) 33 | }, 34 | onError: (error) => { 35 | const message = 36 | error.response.data.detail || 37 | error.response.data.title || 38 | error.response.data.message || 39 | error.response.statusText || 40 | error.message 41 | 42 | onError(message) 43 | }, 44 | }) 45 | 46 | const headers = option.headers 47 | 48 | if (headers && Array.isArray(headers)) { 49 | request.httpClient.interceptors.request.use((config) => { 50 | for (const key in headers) { 51 | if (!headers.hasOwnProperty(key)) { 52 | continue 53 | } 54 | 55 | config.headers[key] = headers[key] 56 | } 57 | 58 | return config 59 | }, undefined) 60 | } 61 | 62 | const params = option.params 63 | 64 | const formData = new FormData() 65 | 66 | if (params) { 67 | for (const key in params) { 68 | if (!params.hasOwnProperty(key)) { 69 | continue 70 | } 71 | 72 | const value = params[key] 73 | 74 | // eslint-disable-next-line no-template-curly-in-string 75 | if (value.toString().indexOf('${filename}') !== -1) { 76 | let randomFileName = 77 | Math.random().toString(36).substring(3, 15) + 78 | '.' + 79 | file.name.split('.').pop() 80 | 81 | // eslint-disable-next-line no-template-curly-in-string 82 | formData.append( 83 | key, 84 | value.toString().replace('${filename}', randomFileName) 85 | ) 86 | } else { 87 | formData.append(key, value) 88 | } 89 | } 90 | } 91 | 92 | formData.append(option.paramName, file, file.name) 93 | 94 | request.post(url, formData, null, false) 95 | }, 96 | } 97 | 98 | export default Upload 99 | -------------------------------------------------------------------------------- /src/core/utils/util.js: -------------------------------------------------------------------------------- 1 | export const deepSearch = (needle, haystack, found = []) => { 2 | Object.keys(haystack).forEach((key) => { 3 | if (!haystack[key]) { 4 | return 5 | } 6 | 7 | if (key === needle) { 8 | found.push(haystack[key]) 9 | return found 10 | } 11 | if (typeof haystack[key] === 'object') { 12 | return deepSearch(needle, haystack[key], found) 13 | } 14 | }) 15 | 16 | return found 17 | } 18 | 19 | export const formatUrl = (url) => { 20 | if (url === 'site/index') { 21 | return 'home' 22 | } 23 | 24 | if (url.endsWith('/index')) { 25 | return url.substr(0, url.length - 6) 26 | } 27 | 28 | return url 29 | } 30 | 31 | export const tabledObject = (object, options = []) => { 32 | const result = [] 33 | 34 | Object.entries(object).forEach(([key, value]) => { 35 | const option = options.find((option) => { 36 | return option.property === key 37 | }) 38 | 39 | if (option) { 40 | if (option.callback) { 41 | result.push({ name: option.name, value: option.callback(value) }) 42 | } else { 43 | result.push({ name: option.name, value: value }) 44 | } 45 | } else { 46 | result.push({ name: key, value: value }) 47 | } 48 | }) 49 | 50 | return result 51 | } 52 | -------------------------------------------------------------------------------- /src/modules/admin/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 154 | 155 | 159 | -------------------------------------------------------------------------------- /src/modules/admin/assets/images/banner-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huijiewei/agile-vue/d1bc73035a769c7364215a20351055913dd1f19e/src/modules/admin/assets/images/banner-white.png -------------------------------------------------------------------------------- /src/modules/admin/assets/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huijiewei/agile-vue/d1bc73035a769c7364215a20351055913dd1f19e/src/modules/admin/assets/images/banner.png -------------------------------------------------------------------------------- /src/modules/admin/assets/images/login-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huijiewei/agile-vue/d1bc73035a769c7364215a20351055913dd1f19e/src/modules/admin/assets/images/login-bg.jpg -------------------------------------------------------------------------------- /src/modules/admin/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huijiewei/agile-vue/d1bc73035a769c7364215a20351055913dd1f19e/src/modules/admin/assets/images/logo.png -------------------------------------------------------------------------------- /src/modules/admin/assets/styles/default-layout.less: -------------------------------------------------------------------------------- 1 | @media (min-width: 992px) { 2 | .aside-fixed { 3 | .ag-aside, 4 | .ag-logo { 5 | width: 200px !important; 6 | } 7 | .ag-main { 8 | margin-left: 200px; 9 | } 10 | } 11 | 12 | .aside-icon { 13 | .ag-aside, 14 | .ag-logo { 15 | width: 60px !important; 16 | } 17 | 18 | .ag-main { 19 | margin-left: 60px; 20 | } 21 | } 22 | } 23 | 24 | .ag-layout { 25 | background: #f4f8fb; 26 | min-height: 100vh; 27 | 28 | .ag-header { 29 | position: fixed; 30 | width: 100%; 31 | z-index: 1030; 32 | padding: 0 0 20px 0; 33 | background: #fff; 34 | box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.02); 35 | } 36 | 37 | .ag-wrap { 38 | padding-top: 50px; 39 | } 40 | 41 | @media screen and (max-width: 491px) { 42 | .ag-logo { 43 | width: 50px !important; 44 | } 45 | } 46 | 47 | @media screen and (max-width: 991px) { 48 | .ag-logo { 49 | width: 200px; 50 | } 51 | .ag-aside { 52 | top: 50px; 53 | bottom: 0; 54 | right: -200px; 55 | width: 200px !important; 56 | z-index: 19811223; 57 | } 58 | .ag-aside.active { 59 | right: 0; 60 | } 61 | } 62 | 63 | .ag-aside { 64 | position: fixed; 65 | box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.01); 66 | transition: width 0.2s; 67 | 68 | .ag-scrollbar { 69 | height: calc(100vh - 50px); 70 | } 71 | 72 | .ag-menu { 73 | min-height: calc(100vh - 50px); 74 | } 75 | } 76 | 77 | .ag-main { 78 | padding: 16px 20px; 79 | } 80 | 81 | .ag-content { 82 | .box { 83 | background: #fff; 84 | padding: 20px; 85 | box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.02); 86 | 87 | .box-header { 88 | border-bottom: 1px solid #ddd; 89 | margin-bottom: 20px; 90 | padding-bottom: 13px; 91 | 92 | h4 { 93 | font-size: 14px; 94 | font-weight: normal; 95 | } 96 | 97 | h1, 98 | h2, 99 | h3, 100 | h4, 101 | h5, 102 | h6 { 103 | margin: 0; 104 | } 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/modules/admin/components/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 46 | 47 | 56 | -------------------------------------------------------------------------------- /src/modules/admin/components/DistrictCascader.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 82 | -------------------------------------------------------------------------------- /src/modules/admin/components/ExportButton.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 66 | -------------------------------------------------------------------------------- /src/modules/admin/components/ImageCropper.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 115 | 116 | 129 | -------------------------------------------------------------------------------- /src/modules/admin/components/LoginModal.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /src/modules/admin/components/Pagination.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 44 | 50 | -------------------------------------------------------------------------------- /src/modules/admin/components/RemoteSelect.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/modules/admin/components/SiderMenu.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 73 | 127 | -------------------------------------------------------------------------------- /src/modules/admin/components/SiderMenuIcon.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/modules/admin/components/SiderMenuItem.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 28 | -------------------------------------------------------------------------------- /src/modules/admin/components/SiderMenuSub.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 62 | -------------------------------------------------------------------------------- /src/modules/admin/components/upload/AvatarUpload.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 35 | -------------------------------------------------------------------------------- /src/modules/admin/components/upload/FileUpload.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 42 | -------------------------------------------------------------------------------- /src/modules/admin/components/upload/ImageUpload.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 75 | -------------------------------------------------------------------------------- /src/modules/admin/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import queryString from 'query-string' 3 | 4 | import ElementUI from 'element-ui' 5 | import 'element-ui/lib/theme-chalk/index.css' 6 | 7 | import router from './router' 8 | import store from './store' 9 | 10 | import App from './App.vue' 11 | 12 | import './registerServiceWorker' 13 | 14 | import HttpClient from '@core/plugins/HttpClient' 15 | import DeleteDialog from './plugins/DeleteDialog' 16 | import PermissionCheck from './plugins/PermissionCheck' 17 | 18 | Vue.use(ElementUI) 19 | 20 | Vue.use(HttpClient, { 21 | getApiHost: () => { 22 | return document 23 | .querySelector('meta[name="api-host"]') 24 | .getAttribute('content') 25 | }, 26 | getAccessToken: () => { 27 | return store.getters['auth/getAccessToken'] 28 | }, 29 | setLoginAction: async (action) => { 30 | await store.dispatch('auth/setLoginAction', action) 31 | }, 32 | setErrorMessage: async (message, historyBack) => { 33 | await store.dispatch('setError', { 34 | message: message, 35 | historyBack: historyBack, 36 | }) 37 | }, 38 | paramsSerializer: function (params) { 39 | return queryString.stringify(params, { 40 | arrayFormat: process.env.VUE_APP_QS_ARRAY_FORMAT || 'bracket', 41 | }) 42 | }, 43 | }) 44 | 45 | Vue.use(PermissionCheck, { 46 | store, 47 | }) 48 | 49 | Vue.use(DeleteDialog, { 50 | MessageBox: ElementUI.MessageBox, 51 | }) 52 | 53 | Vue.config.productionTip = false 54 | 55 | Vue.mixin({ 56 | data: function () { 57 | return { 58 | tinymceUrl: process.env.VUE_APP_TINYMCE_URL, 59 | tinymceLanguageUrl: process.env.VUE_APP_TINYMCE_LANGUAGE_URL, 60 | } 61 | }, 62 | }) 63 | 64 | new Vue({ 65 | router, 66 | store, 67 | render: (h) => h(App), 68 | }).$mount('#root') 69 | -------------------------------------------------------------------------------- /src/modules/admin/mixins/SearchFormFieldsMixin.js: -------------------------------------------------------------------------------- 1 | const SearchFormFieldsMixin = { 2 | data() { 3 | return { 4 | searchFields: false, 5 | } 6 | }, 7 | methods: { 8 | buildRouteQuery(query) { 9 | return this.searchFields !== false 10 | ? query 11 | : Object.assign({}, query, { withSearchFields: true }) 12 | }, 13 | setSearchFields(searchFields) { 14 | if (searchFields) { 15 | this.searchFields = searchFields || [] 16 | } 17 | }, 18 | }, 19 | } 20 | 21 | export default SearchFormFieldsMixin 22 | -------------------------------------------------------------------------------- /src/modules/admin/mixins/UnprocessableEntityHttpErrorMixin.js: -------------------------------------------------------------------------------- 1 | const UnprocessableEntityHttpErrorMixin = { 2 | data() { 3 | return { 4 | violations: [], 5 | } 6 | }, 7 | methods: { 8 | clearViolationError(formName) { 9 | this.$refs[formName].$children.forEach((child) => { 10 | child.$data.validateMessage = '' 11 | child.$data.validateState = 'success' 12 | }) 13 | }, 14 | handleViolationError(error, formName) { 15 | if ( 16 | !error || 17 | !error.response || 18 | !error.response.status || 19 | error.response.status !== 422 20 | ) { 21 | return 22 | } 23 | 24 | if (Array.isArray(error.response.data.violations)) { 25 | this.violations = error.response.data.violations 26 | } 27 | 28 | if (Array.isArray(error.response.data)) { 29 | this.violations = error.response.data 30 | } 31 | 32 | if (this.violations.length === 0) { 33 | return 34 | } 35 | 36 | this.$refs[formName].$children.forEach((child) => { 37 | if (child.prop) { 38 | const childProp = child.prop && child.prop.split('.')[0] 39 | 40 | const activeViolation = this.violations.find((violation) => { 41 | const violationFieldPath = violation.field.split('.') 42 | 43 | return childProp === violationFieldPath.pop() 44 | }) 45 | 46 | if (activeViolation) { 47 | child.$data.validateMessage = activeViolation.message 48 | child.$data.validateState = activeViolation.message 49 | ? 'error' 50 | : 'success' 51 | } else { 52 | child.$data.validateMessage = '' 53 | child.$data.validateState = 'success' 54 | } 55 | } 56 | }) 57 | }, 58 | }, 59 | } 60 | 61 | export default UnprocessableEntityHttpErrorMixin 62 | -------------------------------------------------------------------------------- /src/modules/admin/plugins/DeleteDialog.js: -------------------------------------------------------------------------------- 1 | const DeleteDialog = { 2 | install(Vue, { MessageBox }) { 3 | Vue.prototype.$deleteDialog = (option) => { 4 | const dialogOption = Object.assign( 5 | { 6 | title: '你确定吗?', 7 | message: '记录将被删除', 8 | callback: null, 9 | promptLabel: '', 10 | promptValue: '', 11 | }, 12 | option 13 | ) 14 | 15 | if (dialogOption.promptValue.length > 0) { 16 | MessageBox.prompt( 17 | '

' + 18 | dialogOption.title + 19 | '

' + 20 | dialogOption.message + 21 | '
', 22 | { 23 | showClose: false, 24 | confirmButtonText: '确定', 25 | cancelButtonText: '取消', 26 | iconClass: 'el-icon-warning-outline', 27 | customClass: 'delete-dialog', 28 | dangerouslyUseHTMLString: true, 29 | center: false, 30 | callback: (action) => { 31 | if (action === 'confirm') { 32 | dialogOption.callback() 33 | } 34 | }, 35 | inputPlaceholder: '' + dialogOption.promptLabel, 36 | inputValidator: (value) => { 37 | return dialogOption.promptValue === value 38 | }, 39 | inputErrorMessage: dialogOption.promptLabel + '不匹配', 40 | } 41 | ) 42 | } else { 43 | MessageBox.confirm( 44 | '

' + 45 | dialogOption.title + 46 | '

' + 47 | dialogOption.message + 48 | '
', 49 | { 50 | showClose: false, 51 | confirmButtonText: '确定', 52 | cancelButtonText: '取消', 53 | iconClass: 'el-icon-warning-outline', 54 | customClass: 'delete-dialog', 55 | dangerouslyUseHTMLString: true, 56 | center: false, 57 | callback: (action) => { 58 | if (action === 'confirm') { 59 | dialogOption.callback() 60 | } 61 | }, 62 | } 63 | ) 64 | } 65 | } 66 | }, 67 | } 68 | 69 | export default DeleteDialog 70 | -------------------------------------------------------------------------------- /src/modules/admin/plugins/PermissionCheck.js: -------------------------------------------------------------------------------- 1 | const PermissionCheck = { 2 | install(Vue, { store }) { 3 | Vue.prototype.$can = (actionId) => { 4 | return store.getters['auth/isRouteInAcl'](actionId) 5 | } 6 | }, 7 | } 8 | 9 | module.exports = PermissionCheck 10 | -------------------------------------------------------------------------------- /src/modules/admin/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { register } from 'register-service-worker' 4 | 5 | if (process.env.NODE_ENV === 'production') { 6 | register(`${process.env.BASE_URL}service-worker.js`, { 7 | ready() { 8 | console.log( 9 | 'App is being served from cache by a service worker.\n' + 10 | 'For more details, visit https://goo.gl/AFskqB' 11 | ) 12 | }, 13 | registered() { 14 | console.log('Service worker has been registered.') 15 | }, 16 | cached() { 17 | console.log('Content has been cached for offline use.') 18 | }, 19 | updatefound() { 20 | console.log('New content is downloading.') 21 | }, 22 | updated() { 23 | console.log('New content is available; please refresh.') 24 | }, 25 | offline() { 26 | console.log( 27 | 'No internet connection found. App is running in offline mode.' 28 | ) 29 | }, 30 | error(error) { 31 | console.error('Error during service worker registration:', error) 32 | }, 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/admin/router/admin/admin-log.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/admin-log', 4 | name: 'AdminLog', 5 | component: () => 6 | import( 7 | /* webpackChunkName: "chunk-admin" */ '@admin/views/admin-log/Index' 8 | ), 9 | meta: { 10 | title: '操作日志', 11 | }, 12 | }, 13 | ] 14 | -------------------------------------------------------------------------------- /src/modules/admin/router/admin/admin.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/admin', 4 | name: 'Admin', 5 | component: () => 6 | import(/* webpackChunkName: "chunk-admin" */ '@admin/views/admin/Index'), 7 | meta: { 8 | title: '管理员', 9 | }, 10 | }, 11 | { 12 | path: '/admin/create', 13 | name: 'AdminCreate', 14 | component: () => 15 | import(/* webpackChunkName: "chunk-admin" */ '@admin/views/admin/Create'), 16 | meta: { 17 | title: '新建管理员', 18 | }, 19 | }, 20 | { 21 | path: '/admin/edit/:id', 22 | name: 'AdminEdit', 23 | component: () => 24 | import(/* webpackChunkName: "chunk-admin" */ '@admin/views/admin/Edit'), 25 | meta: { 26 | title: '编辑管理员', 27 | }, 28 | }, 29 | ] 30 | -------------------------------------------------------------------------------- /src/modules/admin/router/admin/group.js: -------------------------------------------------------------------------------- 1 | import AdminLayout from '@admin/components/AdminLayout' 2 | 3 | export default [ 4 | { 5 | path: '/admin-group', 6 | name: 'AdminGroup', 7 | component: () => 8 | import( 9 | /* webpackChunkName: "chunk-admin" */ '@admin/views/admin-group/Index' 10 | ), 11 | meta: { 12 | title: '管理组', 13 | }, 14 | }, 15 | { 16 | path: '/admin-group/create', 17 | name: 'AdminGroupCreate', 18 | component: () => 19 | import( 20 | /* webpackChunkName: "chunk-admin" */ '@admin/views/admin-group/Create' 21 | ), 22 | meta: { 23 | title: '新建管理组', 24 | }, 25 | }, 26 | { 27 | path: '/admin-group/edit/:id', 28 | name: 'AdminGroupEdit', 29 | component: () => 30 | import( 31 | /* webpackChunkName: "chunk-admin" */ '@admin/views/admin-group/Edit' 32 | ), 33 | meta: { 34 | title: '编辑管理组', 35 | }, 36 | }, 37 | ] 38 | -------------------------------------------------------------------------------- /src/modules/admin/router/cms/article.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/cms-article', 4 | name: 'CmsArticle', 5 | component: () => 6 | import( 7 | /* webpackChunkName: "chunk-cms" */ '@admin/views/cms-article/Index' 8 | ), 9 | meta: { 10 | title: '文章管理', 11 | }, 12 | }, 13 | { 14 | path: '/cms-article/create', 15 | name: 'CmsArticleCreate', 16 | component: () => 17 | import( 18 | /* webpackChunkName: "chunk-cms" */ '@admin/views/cms-article/Create' 19 | ), 20 | meta: { 21 | title: '新建文章', 22 | }, 23 | }, 24 | { 25 | path: '/cms-article/edit/:id', 26 | name: 'CmsArticleEdit', 27 | component: () => 28 | import( 29 | /* webpackChunkName: "chunk-cms" */ '@admin/views/cms-article/Edit' 30 | ), 31 | meta: { 32 | title: '编辑文章', 33 | }, 34 | }, 35 | ] 36 | -------------------------------------------------------------------------------- /src/modules/admin/router/cms/category.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/cms-category', 4 | component: () => 5 | import( 6 | /* webpackChunkName: "chunk-cms" */ '@admin/views/cms-category/Layout' 7 | ), 8 | children: [ 9 | { 10 | path: '', 11 | name: 'CmsCategory', 12 | component: () => 13 | import( 14 | /* webpackChunkName: "chunk-cms" */ '@admin/views/cms-category/Index' 15 | ), 16 | meta: { 17 | title: '内容分类', 18 | }, 19 | }, 20 | { 21 | path: 'create/:id', 22 | name: 'CmsCategoryCreate', 23 | component: () => 24 | import( 25 | /* webpackChunkName: "chunk-cms" */ '@admin/views/cms-category/Create' 26 | ), 27 | meta: { 28 | title: '新建内容分类', 29 | parent: { 30 | name: 'CmsCategory', 31 | path: '/cms-category', 32 | title: '内容分类', 33 | }, 34 | }, 35 | }, 36 | { 37 | path: 'edit/:id', 38 | name: 'CmsCategoryEdit', 39 | component: () => 40 | import( 41 | /* webpackChunkName: "chunk-cms" */ '@admin/views/cms-category/Edit' 42 | ), 43 | meta: { 44 | title: '编辑内容分类', 45 | parent: { 46 | name: 'CmsCategory', 47 | path: '/cms-category', 48 | title: '内容分类', 49 | }, 50 | }, 51 | }, 52 | ], 53 | }, 54 | ] 55 | -------------------------------------------------------------------------------- /src/modules/admin/router/district/district.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/district', 4 | component: () => 5 | import( 6 | /* webpackChunkName: "chunk-district" */ '@admin/views/district/Layout' 7 | ), 8 | children: [ 9 | { 10 | path: '', 11 | name: 'District', 12 | component: () => 13 | import( 14 | /* webpackChunkName: "chunk-district" */ '@admin/views/district/Index' 15 | ), 16 | meta: { 17 | title: '地区', 18 | }, 19 | }, 20 | { 21 | path: 'create/:id', 22 | name: 'DistrictCreate', 23 | component: () => 24 | import( 25 | /* webpackChunkName: "chunk-district" */ '@admin/views/district/Create' 26 | ), 27 | meta: { 28 | title: '新建地区', 29 | parent: { 30 | name: 'District', 31 | path: '/district', 32 | title: '地区', 33 | }, 34 | }, 35 | }, 36 | { 37 | path: 'edit/:id', 38 | name: 'DistrictEdit', 39 | component: () => 40 | import( 41 | /* webpackChunkName: "chunk-district" */ '@admin/views/district/Edit' 42 | ), 43 | meta: { 44 | title: '编辑地区', 45 | parent: { 46 | name: 'District', 47 | path: '/district', 48 | title: '地区', 49 | }, 50 | }, 51 | }, 52 | ], 53 | }, 54 | ] 55 | -------------------------------------------------------------------------------- /src/modules/admin/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import VueRouterBackButton from 'vue-router-back-button' 4 | 5 | import notFound from '@admin/views/site/NotFound' 6 | import siteIndex from '@admin/views/site/Index' 7 | import siteLogin from '@admin/views/site/Login' 8 | import siteProfile from '@admin/views/site/Profile' 9 | 10 | import adminRoute from './admin/admin' 11 | import adminLogRoute from './admin/admin-log' 12 | import adminGroupRoute from './admin/group' 13 | 14 | import userRoute from './user/user' 15 | import userAddressRoute from './user/address' 16 | 17 | import shopCategoryRoute from './shop/category' 18 | import shopBrandRoute from './shop/brand' 19 | import shopProductRoute from './shop/product' 20 | 21 | import districtRoute from './district/district' 22 | 23 | import cmsCategoryRoute from './cms/category' 24 | import cmsArticleRoute from './cms/article' 25 | 26 | import AdminLayout from '@admin/components/AdminLayout' 27 | 28 | Vue.use(VueRouter) 29 | 30 | const originalPush = VueRouter.prototype.push 31 | VueRouter.prototype.push = function push(location, onResolve, onReject) { 32 | if (onResolve || onReject) { 33 | return originalPush.call(this, location, onResolve, onReject) 34 | } 35 | 36 | return originalPush.call(this, location).catch((err) => err) 37 | } 38 | 39 | const routes = [ 40 | { 41 | path: '/login', 42 | name: 'Login', 43 | component: siteLogin, 44 | meta: { 45 | title: '登录', 46 | }, 47 | }, 48 | { 49 | path: '/', 50 | component: AdminLayout, 51 | children: [ 52 | { 53 | path: '', 54 | redirect: '/home', 55 | }, 56 | { 57 | path: '/home', 58 | name: 'Home', 59 | component: siteIndex, 60 | meta: { 61 | affix: true, 62 | title: '首页', 63 | }, 64 | }, 65 | { 66 | path: '/profile', 67 | name: 'Profile', 68 | component: siteProfile, 69 | meta: { 70 | title: '个人资料', 71 | }, 72 | }, 73 | ...adminRoute, 74 | ...adminLogRoute, 75 | ...adminGroupRoute, 76 | ...userRoute, 77 | ...userAddressRoute, 78 | ...shopBrandRoute, 79 | ...shopProductRoute, 80 | ...shopCategoryRoute, 81 | ...districtRoute, 82 | ...cmsCategoryRoute, 83 | ...cmsArticleRoute, 84 | { 85 | path: '*', 86 | component: notFound, 87 | meta: { 88 | title: '页面未找到', 89 | }, 90 | }, 91 | ], 92 | }, 93 | ] 94 | 95 | const router = new VueRouter({ 96 | base: process.env.VUE_APP_PUBLIC_PATH, 97 | mode: 'history', 98 | routes: routes, 99 | scrollBehavior: (to, from, savedPosition) => { 100 | if (savedPosition) { 101 | return savedPosition 102 | } 103 | 104 | if (to.hash) { 105 | return { selector: to.hash } 106 | } 107 | 108 | return { x: 0, y: 0 } 109 | }, 110 | }) 111 | 112 | Vue.use(VueRouterBackButton, { router, ignoreRoutesWithSameName: false }) 113 | 114 | export default router 115 | -------------------------------------------------------------------------------- /src/modules/admin/router/shop/brand.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/shop-brand', 4 | name: 'ShopBrand', 5 | component: () => 6 | import( 7 | /* webpackChunkName: "chunk-shop" */ '@admin/views/shop-brand/Index' 8 | ), 9 | meta: { 10 | title: '商品品牌', 11 | }, 12 | }, 13 | { 14 | path: '/shop-brand/create', 15 | name: 'ShopBrandCreate', 16 | component: () => 17 | import( 18 | /* webpackChunkName: "chunk-shop" */ '@admin/views/shop-brand/Create' 19 | ), 20 | meta: { 21 | title: '新建商品品牌', 22 | }, 23 | }, 24 | { 25 | path: '/shop-brand/edit/:id', 26 | name: 'ShopBrandEdit', 27 | component: () => 28 | import( 29 | /* webpackChunkName: "chunk-shop" */ '@admin/views/shop-brand/Edit' 30 | ), 31 | meta: { 32 | title: '编辑商品品牌', 33 | }, 34 | }, 35 | ] 36 | -------------------------------------------------------------------------------- /src/modules/admin/router/shop/category.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/shop-category', 4 | component: () => 5 | import( 6 | /* webpackChunkName: "chunk-shop" */ '@admin/views/shop-category/Layout' 7 | ), 8 | children: [ 9 | { 10 | path: '', 11 | name: 'ShopCategory', 12 | component: () => 13 | import( 14 | /* webpackChunkName: "chunk-shop" */ '@admin/views/shop-category/Index' 15 | ), 16 | meta: { 17 | title: '商品分类', 18 | }, 19 | }, 20 | { 21 | path: 'create/:id', 22 | name: 'ShopCategoryCreate', 23 | component: () => 24 | import( 25 | /* webpackChunkName: "chunk-shop" */ '@admin/views/shop-category/Create' 26 | ), 27 | meta: { 28 | title: '新建商品分类', 29 | parent: { 30 | name: 'ShopCategory', 31 | path: '/shop-category', 32 | title: '商品分类', 33 | }, 34 | }, 35 | }, 36 | { 37 | path: 'edit/:id', 38 | name: 'ShopCategoryEdit', 39 | component: () => 40 | import( 41 | /* webpackChunkName: "chunk-shop" */ '@admin/views/shop-category/Edit' 42 | ), 43 | meta: { 44 | title: '编辑商品分类', 45 | parent: { 46 | name: 'ShopCategory', 47 | path: '/shop-category', 48 | title: '商品分类', 49 | }, 50 | }, 51 | }, 52 | ], 53 | }, 54 | ] 55 | -------------------------------------------------------------------------------- /src/modules/admin/router/shop/product.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/shop-product', 4 | name: 'ShopProduct', 5 | component: () => 6 | import( 7 | /* webpackChunkName: "chunk-shop" */ '@admin/views/shop-product/Index' 8 | ), 9 | meta: { 10 | title: '商品列表', 11 | }, 12 | }, 13 | ] 14 | -------------------------------------------------------------------------------- /src/modules/admin/router/user/address.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/user-address', 4 | name: 'UserAddress', 5 | component: () => 6 | import( 7 | /* webpackChunkName: "chunk-user" */ '@admin/views/user-address/Index' 8 | ), 9 | meta: { 10 | title: '用户地址', 11 | }, 12 | }, 13 | { 14 | path: '/user-address/create', 15 | name: 'UserAddressCreate', 16 | component: () => 17 | import( 18 | /* webpackChunkName: "chunk-user" */ '@admin/views/user-address/Create' 19 | ), 20 | meta: { 21 | title: '新建用户地址', 22 | }, 23 | }, 24 | { 25 | path: '/user-address/edit/:id', 26 | name: 'UserAddressEdit', 27 | component: () => 28 | import( 29 | /* webpackChunkName: "chunk-user" */ '@admin/views/user-address/Edit' 30 | ), 31 | meta: { 32 | title: '编辑用户地址', 33 | }, 34 | }, 35 | ] 36 | -------------------------------------------------------------------------------- /src/modules/admin/router/user/user.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/user', 4 | name: 'User', 5 | component: () => 6 | import(/* webpackChunkName: "chunk-user" */ '@admin/views/user/Index'), 7 | meta: { 8 | title: '用户', 9 | }, 10 | }, 11 | { 12 | path: '/user/create', 13 | name: 'UserCreate', 14 | component: () => 15 | import(/* webpackChunkName: "chunk-user" */ '@admin/views/user/Create'), 16 | meta: { 17 | title: '新建用户', 18 | }, 19 | }, 20 | { 21 | path: '/user/edit/:id', 22 | name: 'UserEdit', 23 | component: () => 24 | import(/* webpackChunkName: "chunk-user" */ '@admin/views/user/Edit'), 25 | meta: { 26 | title: '编辑用户', 27 | }, 28 | }, 29 | ] 30 | -------------------------------------------------------------------------------- /src/modules/admin/services/AdminGroupService.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const AdminGroupService = { 4 | all() { 5 | return Vue.http.get('admin-groups') 6 | }, 7 | 8 | delete(id) { 9 | return Vue.http.delete('admin-groups/' + id) 10 | }, 11 | 12 | create(adminGroup) { 13 | return Vue.http.post('admin-groups', adminGroup) 14 | }, 15 | 16 | view(id) { 17 | return Vue.http.get('admin-groups/' + id) 18 | }, 19 | 20 | edit(adminGroup) { 21 | return Vue.http.put('admin-groups/' + adminGroup.id, adminGroup) 22 | }, 23 | } 24 | 25 | export default AdminGroupService 26 | -------------------------------------------------------------------------------- /src/modules/admin/services/AdminService.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const AdminService = { 4 | all() { 5 | return Vue.http.get('admins') 6 | }, 7 | 8 | log(query) { 9 | return Vue.http.get('admin-logs', query) 10 | }, 11 | 12 | delete(id) { 13 | return Vue.http.delete('admins/' + id) 14 | }, 15 | 16 | create(admin) { 17 | return Vue.http.post('admins', admin) 18 | }, 19 | 20 | view(id) { 21 | return Vue.http.get('admins/' + id) 22 | }, 23 | 24 | edit(admin) { 25 | return Vue.http.put('admins/' + admin.id, admin) 26 | }, 27 | } 28 | 29 | export default AdminService 30 | -------------------------------------------------------------------------------- /src/modules/admin/services/AuthService.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const AuthService = { 4 | login(credentials) { 5 | return Vue.http.post('auth/login', credentials, null, false) 6 | }, 7 | logout() { 8 | return Vue.http.post('auth/logout', null, null, false) 9 | }, 10 | account() { 11 | return Vue.http.get('auth/account', null, false) 12 | }, 13 | profile(profile = null) { 14 | const endpoint = 'auth/profile' 15 | 16 | if (profile === null) { 17 | return Vue.http.get(endpoint) 18 | } 19 | 20 | return Vue.http.put(endpoint, profile) 21 | }, 22 | } 23 | 24 | export default AuthService 25 | -------------------------------------------------------------------------------- /src/modules/admin/services/CmsArticleService.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const CmsArticleService = { 4 | all(query) { 5 | return Vue.http.get('cms-articles', query) 6 | }, 7 | 8 | create(cmsArticle) { 9 | return Vue.http.post('cms-articles', cmsArticle) 10 | }, 11 | 12 | view(id) { 13 | return Vue.http.get('cms-articles/' + id) 14 | }, 15 | 16 | edit(cmsArticle) { 17 | return Vue.http.put('cms-articles/' + cmsArticle.id, cmsArticle) 18 | }, 19 | 20 | delete(id) { 21 | return Vue.http.delete('cms-articles/' + id) 22 | }, 23 | } 24 | 25 | export default CmsArticleService 26 | -------------------------------------------------------------------------------- /src/modules/admin/services/CmsCategoryService.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const CmsCategoryService = { 4 | create(cmsCategory) { 5 | return Vue.http.post('cms-categories', cmsCategory) 6 | }, 7 | 8 | view(id) { 9 | return Vue.http.get('cms-categories/' + id, { withParents: true }) 10 | }, 11 | 12 | edit(cmsCategory) { 13 | return Vue.http.put('cms-categories/' + cmsCategory.id, cmsCategory) 14 | }, 15 | 16 | delete(id) { 17 | return Vue.http.delete('cms-categories/' + id) 18 | }, 19 | } 20 | 21 | export default CmsCategoryService 22 | -------------------------------------------------------------------------------- /src/modules/admin/services/DistrictService.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const DistrictService = { 4 | create(district) { 5 | return Vue.http.post('districts', district) 6 | }, 7 | 8 | view(id) { 9 | return Vue.http.get('districts/' + id, { withParents: true }) 10 | }, 11 | 12 | edit(district) { 13 | return Vue.http.put('districts/' + district.id, district) 14 | }, 15 | 16 | delete(id) { 17 | return Vue.http.delete('districts/' + id) 18 | }, 19 | } 20 | 21 | export default DistrictService 22 | -------------------------------------------------------------------------------- /src/modules/admin/services/MiscService.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const MiscService = { 4 | districts(parentId) { 5 | return Vue.http.get('misc/districts', { parentId: parentId }, false) 6 | }, 7 | 8 | districtPath(id) { 9 | return Vue.http.get('misc/district-path', { id: id }, false) 10 | }, 11 | 12 | districtSearchTree(keyword) { 13 | return Vue.http.get( 14 | 'misc/district-search-tree', 15 | { keyword: keyword }, 16 | false 17 | ) 18 | }, 19 | 20 | adminGroups() { 21 | return Vue.http.get('misc/admin-groups', null, false) 22 | }, 23 | 24 | adminGroupPermissions() { 25 | return Vue.http.get('misc/admin-group-permissions', null, false) 26 | }, 27 | 28 | shopCategoryTree() { 29 | return Vue.http.get('misc/shop-category-tree', null, false) 30 | }, 31 | 32 | shopCategoryPath(id) { 33 | return Vue.http.get('misc/shop-category-path', { id: id }, false) 34 | }, 35 | 36 | cmsCategoryTree() { 37 | return Vue.http.get('misc/cms-category-tree', null, false) 38 | }, 39 | 40 | cmsCategoryPath(id) { 41 | return Vue.http.get('misc/cms-category-path', { id: id }, false) 42 | }, 43 | } 44 | 45 | export default MiscService 46 | -------------------------------------------------------------------------------- /src/modules/admin/services/OpenService.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const OpenService = { 4 | captcha() { 5 | return Vue.http.get('open/captcha', null, false, true) 6 | }, 7 | } 8 | 9 | export default OpenService 10 | -------------------------------------------------------------------------------- /src/modules/admin/services/ShopBrandService.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const ShopBrandService = { 4 | all(query) { 5 | return Vue.http.get('shop-brands', query) 6 | }, 7 | 8 | create(shopBrand) { 9 | return Vue.http.post('shop-brands', shopBrand) 10 | }, 11 | 12 | view(id) { 13 | return Vue.http.get('shop-brands/' + id) 14 | }, 15 | 16 | edit(shopBrand) { 17 | return Vue.http.put('shop-brands/' + shopBrand.id, shopBrand) 18 | }, 19 | 20 | delete(id) { 21 | return Vue.http.delete('shop-brands/' + id) 22 | }, 23 | } 24 | 25 | export default ShopBrandService 26 | -------------------------------------------------------------------------------- /src/modules/admin/services/ShopCategoryService.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const ShopCategoryService = { 4 | create(shopCategory) { 5 | return Vue.http.post('shop-categories', shopCategory) 6 | }, 7 | 8 | view(id) { 9 | return Vue.http.get('shop-categories/' + id, { withParents: true }) 10 | }, 11 | 12 | edit(shopCategory) { 13 | return Vue.http.put('shop-categories/' + shopCategory.id, shopCategory) 14 | }, 15 | 16 | delete(id) { 17 | return Vue.http.delete('shop-categories/' + id) 18 | }, 19 | } 20 | 21 | export default ShopCategoryService 22 | -------------------------------------------------------------------------------- /src/modules/admin/services/ShopProductService.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const ShopProductService = { 4 | all(query) { 5 | return Vue.http.get('shop-products', query) 6 | }, 7 | 8 | create(shopProduct) { 9 | return Vue.http.post('shop-products', shopProduct) 10 | }, 11 | 12 | view(id) { 13 | return Vue.http.get('shop-products/' + id) 14 | }, 15 | 16 | edit(shopProduct) { 17 | return Vue.http.put('shop-products/' + shopProduct.id, shopProduct) 18 | }, 19 | 20 | delete(id) { 21 | return Vue.http.delete('shop-products/' + id) 22 | }, 23 | } 24 | 25 | export default ShopProductService 26 | -------------------------------------------------------------------------------- /src/modules/admin/services/UserAddressService.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const UserAddressService = { 4 | all(query) { 5 | return Vue.http.get('user-addresses', query) 6 | }, 7 | 8 | delete(id) { 9 | return Vue.http.delete('user-addresses/' + id) 10 | }, 11 | 12 | create(userAddress) { 13 | return Vue.http.post('user-addresses', userAddress) 14 | }, 15 | 16 | view(id) { 17 | return Vue.http.get('user-addresses/' + id) 18 | }, 19 | 20 | edit(userAddress) { 21 | return Vue.http.put('user-addresses/' + userAddress.id, userAddress) 22 | }, 23 | } 24 | 25 | export default UserAddressService 26 | -------------------------------------------------------------------------------- /src/modules/admin/services/UserService.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const UserService = { 4 | all(query) { 5 | return Vue.http.get('users', query) 6 | }, 7 | 8 | delete(id) { 9 | return Vue.http.delete('users/' + id) 10 | }, 11 | 12 | create(user) { 13 | return Vue.http.post('users', user) 14 | }, 15 | 16 | view(id) { 17 | return Vue.http.get('users/' + id) 18 | }, 19 | 20 | edit(user) { 21 | return Vue.http.put('users/' + user.id, user) 22 | }, 23 | } 24 | 25 | export default UserService 26 | -------------------------------------------------------------------------------- /src/modules/admin/store/app.js: -------------------------------------------------------------------------------- 1 | import auth from './auth' 2 | import tabs from './tabs' 3 | 4 | const app = { 5 | strict: process.env.NODE_ENV !== 'production', 6 | modules: { 7 | auth: auth, 8 | tabs: tabs, 9 | }, 10 | state: { 11 | error: { 12 | message: '', 13 | historyBack: false, 14 | }, 15 | sidebar: { 16 | collapsed: window.matchMedia('(max-width: 991px)').matches, 17 | hidden: false, 18 | }, 19 | }, 20 | getters: { 21 | isSidebarCollapsed: (state) => { 22 | return state.sidebar.collapsed 23 | }, 24 | }, 25 | mutations: { 26 | TOGGLE_SIDEBAR: (state) => { 27 | state.sidebar.collapsed = !state.sidebar.collapsed 28 | }, 29 | TOGGLE_ERROR: (state, error) => { 30 | state.error = error 31 | }, 32 | }, 33 | actions: { 34 | toggleSidebar({ commit }) { 35 | commit('TOGGLE_SIDEBAR') 36 | }, 37 | setError({ commit }, error) { 38 | commit('TOGGLE_ERROR', error) 39 | }, 40 | clearError({ commit }) { 41 | commit('TOGGLE_ERROR', { message: '', routeBack: false }) 42 | }, 43 | }, 44 | } 45 | 46 | export default app 47 | -------------------------------------------------------------------------------- /src/modules/admin/store/auth/index.js: -------------------------------------------------------------------------------- 1 | import { deepSearch, formatUrl } from '@core/utils/util' 2 | 3 | const clientIdKey = 'ag:admin-client-id' 4 | const accessTokenKey = 'ag:admin-access-token' 5 | 6 | const auth = { 7 | namespaced: true, 8 | state: { 9 | accessToken: window.localStorage.getItem(accessTokenKey) || '', 10 | loginAction: 'none', // none, modal, direct 11 | currentUser: null, 12 | groupMenus: [], 13 | groupMenusUrl: [], 14 | groupPermissions: [], 15 | }, 16 | getters: { 17 | getAccessToken: (state) => { 18 | return { 19 | clientId: window.localStorage.getItem(clientIdKey), 20 | accessToken: state.accessToken, 21 | } 22 | }, 23 | getCurrentUser: (state) => { 24 | return state.currentUser 25 | }, 26 | getGroupMenus: (state) => { 27 | return state.groupMenus 28 | }, 29 | isRouteInAcl: (state) => (route) => { 30 | if (route.length === 0) { 31 | return false 32 | } 33 | 34 | const routeSplit = route.split('/') 35 | 36 | return ( 37 | state.groupPermissions.findIndex((permission) => { 38 | if (permission.length === 0) { 39 | return false 40 | } 41 | 42 | const permissionSplit = permission.split('/') 43 | 44 | let matched = true 45 | 46 | for (let i = permissionSplit.length - 1; i >= 0; i--) { 47 | if (permissionSplit[i].startsWith(':')) { 48 | continue 49 | } 50 | 51 | matched = permissionSplit[i] === routeSplit[i] && matched 52 | } 53 | 54 | return matched 55 | }) > -1 56 | ) 57 | }, 58 | isRouteInMenus: (state) => (route) => { 59 | const path = route.startsWith('/') ? route.substr(1) : route 60 | return state.groupMenusUrl.map((menu) => formatUrl(menu)).includes(path) 61 | }, 62 | }, 63 | mutations: { 64 | TOGGLE_ACCESS_TOKEN: (state, accessToken) => { 65 | state.accessToken = accessToken 66 | if (accessToken !== '') { 67 | window.localStorage.setItem(accessTokenKey, accessToken) 68 | } else { 69 | window.localStorage.removeItem(accessTokenKey) 70 | } 71 | }, 72 | TOGGLE_LOGIN_ACTION: (state, action) => { 73 | state.loginAction = action 74 | }, 75 | UPDATE_CURRENT_USER: (state, user) => { 76 | state.currentUser = user 77 | }, 78 | UPDATE_GROUP_MENUS: (state, menus) => { 79 | state.groupMenus = menus 80 | state.groupMenusUrl = deepSearch('url', menus) 81 | }, 82 | UPDATE_GROUP_PERMISSIONS: (state, permissions) => { 83 | state.groupPermissions = permissions 84 | }, 85 | }, 86 | actions: { 87 | init() { 88 | if (window.localStorage.getItem(clientIdKey) == null) { 89 | window.localStorage.setItem( 90 | clientIdKey, 91 | Math.random().toString(36).substr(2) 92 | ) 93 | } 94 | }, 95 | setLoginAction({ commit }, action) { 96 | commit('TOGGLE_LOGIN_ACTION', action) 97 | }, 98 | login({ commit }, data) { 99 | commit('TOGGLE_LOGIN_ACTION', 'none') 100 | commit('TOGGLE_ACCESS_TOKEN', data.accessToken) 101 | commit('UPDATE_CURRENT_USER', data.currentUser) 102 | commit('UPDATE_GROUP_MENUS', data.groupMenus) 103 | commit('UPDATE_GROUP_PERMISSIONS', data.groupPermissions) 104 | }, 105 | logout({ commit }) { 106 | commit('TOGGLE_ACCESS_TOKEN', '') 107 | commit('UPDATE_CURRENT_USER', null) 108 | commit('UPDATE_GROUP_MENUS', []) 109 | commit('UPDATE_GROUP_PERMISSIONS', []) 110 | }, 111 | account({ commit }, data) { 112 | commit('UPDATE_CURRENT_USER', data.currentUser) 113 | commit('UPDATE_GROUP_MENUS', data.groupMenus) 114 | commit('UPDATE_GROUP_PERMISSIONS', data.groupPermissions) 115 | }, 116 | }, 117 | } 118 | 119 | export default auth 120 | -------------------------------------------------------------------------------- /src/modules/admin/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import app from './app' 4 | 5 | Vue.use(Vuex) 6 | 7 | const store = new Vuex.Store(app) 8 | 9 | export default store 10 | -------------------------------------------------------------------------------- /src/modules/admin/views/admin-group/Create.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 59 | -------------------------------------------------------------------------------- /src/modules/admin/views/admin-group/Edit.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 66 | -------------------------------------------------------------------------------- /src/modules/admin/views/admin-group/Index.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 99 | -------------------------------------------------------------------------------- /src/modules/admin/views/admin/Create.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 62 | -------------------------------------------------------------------------------- /src/modules/admin/views/admin/Edit.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 64 | -------------------------------------------------------------------------------- /src/modules/admin/views/admin/Index.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 135 | -------------------------------------------------------------------------------- /src/modules/admin/views/cms-article/Create.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 63 | -------------------------------------------------------------------------------- /src/modules/admin/views/cms-article/Edit.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 68 | -------------------------------------------------------------------------------- /src/modules/admin/views/cms-category/Create.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 99 | -------------------------------------------------------------------------------- /src/modules/admin/views/cms-category/Edit.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 126 | -------------------------------------------------------------------------------- /src/modules/admin/views/cms-category/Index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | -------------------------------------------------------------------------------- /src/modules/admin/views/cms-category/Layout.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 154 | -------------------------------------------------------------------------------- /src/modules/admin/views/district/Create.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 97 | -------------------------------------------------------------------------------- /src/modules/admin/views/district/Edit.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 108 | -------------------------------------------------------------------------------- /src/modules/admin/views/district/Index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | -------------------------------------------------------------------------------- /src/modules/admin/views/district/_EditForm.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 144 | -------------------------------------------------------------------------------- /src/modules/admin/views/shop-brand/Create.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 64 | -------------------------------------------------------------------------------- /src/modules/admin/views/shop-brand/Edit.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 84 | -------------------------------------------------------------------------------- /src/modules/admin/views/shop-category/Create.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 98 | -------------------------------------------------------------------------------- /src/modules/admin/views/shop-category/Edit.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 126 | -------------------------------------------------------------------------------- /src/modules/admin/views/shop-category/Index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | -------------------------------------------------------------------------------- /src/modules/admin/views/shop-category/Layout.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 154 | -------------------------------------------------------------------------------- /src/modules/admin/views/site/Index.vue: -------------------------------------------------------------------------------- 1 | 99 | 100 | 123 | -------------------------------------------------------------------------------- /src/modules/admin/views/site/Login.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 34 | 73 | -------------------------------------------------------------------------------- /src/modules/admin/views/site/NotFound.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | 66 | -------------------------------------------------------------------------------- /src/modules/admin/views/site/Profile.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 61 | -------------------------------------------------------------------------------- /src/modules/admin/views/user-address/Create.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 56 | -------------------------------------------------------------------------------- /src/modules/admin/views/user-address/Edit.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 66 | -------------------------------------------------------------------------------- /src/modules/admin/views/user-address/Index.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 132 | -------------------------------------------------------------------------------- /src/modules/admin/views/user-address/_EditForm.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 130 | -------------------------------------------------------------------------------- /src/modules/admin/views/user/Create.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 56 | -------------------------------------------------------------------------------- /src/modules/admin/views/user/Edit.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 66 | -------------------------------------------------------------------------------- /src/modules/mobile/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | 30 | 39 | -------------------------------------------------------------------------------- /src/modules/mobile/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huijiewei/agile-vue/d1bc73035a769c7364215a20351055913dd1f19e/src/modules/mobile/assets/images/logo.png -------------------------------------------------------------------------------- /src/modules/mobile/components/DefaultLayout.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /src/modules/mobile/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import router from './router' 4 | import store from './store' 5 | 6 | import App from './App.vue' 7 | 8 | Vue.config.productionTip = false 9 | 10 | new Vue({ 11 | router, 12 | store, 13 | render: (h) => h(App), 14 | }).$mount('#root') 15 | -------------------------------------------------------------------------------- /src/modules/mobile/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import VueRouterBackButton from 'vue-router-back-button' 4 | 5 | import siteIndex from '@mobile/views/site/Index' 6 | import notFound from '@mobile/views/site/NotFound' 7 | 8 | import defaultLayout from '@mobile/components/DefaultLayout' 9 | 10 | Vue.use(VueRouter) 11 | 12 | const routes = [ 13 | { 14 | path: '/', 15 | component: defaultLayout, 16 | children: [ 17 | { 18 | path: '', 19 | redirect: '/home', 20 | }, 21 | { 22 | path: '/home', 23 | name: 'Home', 24 | component: siteIndex, 25 | meta: { 26 | affix: true, 27 | title: '首页', 28 | }, 29 | }, 30 | ], 31 | }, 32 | { 33 | path: '*', 34 | component: notFound, 35 | }, 36 | ] 37 | 38 | const router = new VueRouter({ 39 | base: process.env.VUE_APP_PUBLIC_PATH, 40 | mode: 'history', 41 | routes: routes, 42 | scrollBehavior: (to, from, savedPosition) => { 43 | if (savedPosition) { 44 | return savedPosition 45 | } 46 | 47 | if (to.hash) { 48 | return { selector: to.hash } 49 | } 50 | 51 | return { x: 0, y: 0 } 52 | }, 53 | }) 54 | 55 | Vue.use(VueRouterBackButton, { router }) 56 | 57 | export default router 58 | -------------------------------------------------------------------------------- /src/modules/mobile/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | export default new Vuex.Store({ 7 | state: {}, 8 | mutations: {}, 9 | actions: {}, 10 | modules: {}, 11 | }) 12 | -------------------------------------------------------------------------------- /src/modules/mobile/views/site/Index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | -------------------------------------------------------------------------------- /src/modules/mobile/views/site/NotFound.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 34 | 69 | -------------------------------------------------------------------------------- /src/modules/website/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/modules/website/components/DefaultLayout.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/modules/website/entry.client.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './main' 2 | 3 | const { app } = createApp() 4 | 5 | app.$mount('#app') 6 | -------------------------------------------------------------------------------- /src/modules/website/entry.server.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './main' 2 | 3 | export default (context) => { 4 | return new Promise((resolve, reject) => { 5 | const { app, router } = createApp() 6 | 7 | router.push(context.url) 8 | 9 | router.onReady(() => { 10 | const matchedComponents = router.getMatchedComponents() 11 | if (!matchedComponents.length) { 12 | return reject({ 13 | code: 404, 14 | }) 15 | } 16 | resolve(app) 17 | }, reject) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/website/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import App from './App.vue' 4 | 5 | Vue.config.productionTip = false 6 | 7 | export function createApp() { 8 | const app = new Vue({ 9 | render: (h) => h(App), 10 | }) 11 | return { app } 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/website/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import DefaultLayout from '../components/DefaultLayout' 4 | import SiteIndex from '../views/site/Index' 5 | 6 | Vue.use(VueRouter) 7 | 8 | const routes = [ 9 | { 10 | path: '/', 11 | component: DefaultLayout, 12 | children: [ 13 | { 14 | path: '', 15 | redirect: '/home', 16 | }, 17 | { 18 | path: '/home', 19 | name: 'Home', 20 | component: SiteIndex, 21 | meta: { 22 | affix: true, 23 | title: '首页', 24 | }, 25 | }, 26 | ], 27 | }, 28 | ] 29 | 30 | const router = new VueRouter({ 31 | base: '', 32 | mode: 'history', 33 | routes: routes, 34 | scrollBehavior: (to, from, savedPosition) => { 35 | if (savedPosition) { 36 | return savedPosition 37 | } 38 | 39 | if (to.hash) { 40 | return { selector: to.hash } 41 | } 42 | 43 | return { x: 0, y: 0 } 44 | }, 45 | }) 46 | 47 | Vue.use(router) 48 | 49 | export default router 50 | -------------------------------------------------------------------------------- /src/modules/website/views/site/Index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const assign = require('assign-deep') 3 | 4 | const moduleConfig = require(process.env.MODULE_CONFIG) 5 | 6 | process.env.VUE_APP_QS_ARRAY_FORMAT = 'none' 7 | process.env.VUE_APP_PUBLIC_PATH = moduleConfig.appPath 8 | 9 | const vueConfig = { 10 | publicPath: moduleConfig.appPath, 11 | outputDir: moduleConfig.outputDir, 12 | assetsDir: 'assets', 13 | devServer: { 14 | host: 'www.agile.test', 15 | compress: true, 16 | }, 17 | pwa: moduleConfig.pwaEnable 18 | ? { 19 | name: moduleConfig.appName, 20 | manifestOptions: { 21 | icons: [ 22 | { 23 | src: 'icons/android-chrome-192x192.png', 24 | sizes: '192x192', 25 | type: 'image/png', 26 | }, 27 | { 28 | src: 'icons/android-chrome-512x512.png', 29 | sizes: '512x512', 30 | type: 'image/png', 31 | }, 32 | ], 33 | }, 34 | iconPaths: { 35 | favicon16: 'icons/favicon-16x16.png', 36 | favicon32: 'icons/favicon-32x32.png', 37 | appleTouchIcon: 'icons/apple-touch-icon.png', 38 | maskIcon: 'icons/safari-pinned-tab.svg', 39 | msTileImage: 'icons/mstile-144x144.png', 40 | }, 41 | } 42 | : null, 43 | chainWebpack: (config) => { 44 | config.entryPoints.delete('app') 45 | 46 | if (moduleConfig.appEntry) { 47 | for (const [key, value] of Object.entries(moduleConfig.appEntry)) { 48 | config.entry(key).add(value).end() 49 | } 50 | } 51 | 52 | config.resolve.alias.set('@core', path.resolve('src/core')).delete('@') 53 | 54 | if (moduleConfig.appAlias) { 55 | for (const [key, value] of Object.entries(moduleConfig.appAlias)) { 56 | config.resolve.alias.set(key, value) 57 | } 58 | } 59 | 60 | config.plugin('html').tap((args) => { 61 | args[0].template = 'public' + moduleConfig.appPath + '/index.html' 62 | args[0].title = moduleConfig.appName 63 | args[0].chunks = moduleConfig.appChunks 64 | 65 | return args 66 | }) 67 | 68 | config.plugin('copy').tap((args) => { 69 | args[0][0].from = path.resolve('./public' + moduleConfig.appPath) 70 | 71 | args[0][1] = Object.assign({}, args[0][0], { 72 | from: path.resolve('./public/common'), 73 | }) 74 | 75 | return args 76 | }) 77 | 78 | if (moduleConfig.chainWebpack) { 79 | moduleConfig.chainWebpack(config) 80 | } 81 | 82 | config.optimization.splitChunks({ 83 | chunks: 'all', 84 | cacheGroups: { 85 | vendor: { 86 | test: /[\\/]node_modules[\\/]/, 87 | chunks: 'initial', 88 | name: 'vendor', 89 | priority: 10, 90 | enforce: true, 91 | }, 92 | element: { 93 | test: /[\\/]node_modules[\\/]_?element-ui(.*)/, 94 | name: 'element', 95 | priority: 20, 96 | enforce: true, 97 | }, 98 | agile: { 99 | test: /[\\/]src\/core[\\/]/, 100 | name: 'agile', 101 | priority: 5, 102 | enforce: true, 103 | }, 104 | }, 105 | }) 106 | }, 107 | } 108 | 109 | module.exports = assign(vueConfig, moduleConfig.vueConfig || {}) 110 | -------------------------------------------------------------------------------- /vue.entry.admin.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const apiMocker = require('mocker-api') 3 | 4 | module.exports = { 5 | appPath: '/admin', 6 | appName: 'Agile 管理后台', 7 | appEntry: { 8 | admin: './src/modules/admin/main.js', 9 | }, 10 | appAlias: { 11 | '@admin': path.resolve('src/modules/admin'), 12 | }, 13 | appChunks: ['vendor', 'element', 'agile', 'admin'], 14 | outputDir: 'dist/admin', 15 | pwaEnable: true, 16 | vueConfig: { 17 | devServer: { 18 | port: 8080, 19 | before(app) { 20 | apiMocker(app, path.resolve('./mocker/admin/index.js')) 21 | }, 22 | }, 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /vue.entry.mobile.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const apiMocker = require('mocker-api') 3 | 4 | module.exports = { 5 | appPath: '/mobile', 6 | appName: 'Agile 移动应用', 7 | appEntry: { 8 | mobile: './src/modules/mobile/main.js', 9 | }, 10 | appAlias: { 11 | '@mobile': path.resolve('src/modules/mobile'), 12 | }, 13 | appChunks: ['vendor', 'agile', 'mobile'], 14 | outputDir: 'dist/mobile', 15 | pwaEnable: true, 16 | vueConfig: { 17 | devServer: { 18 | port: 8081, 19 | before(app) { 20 | apiMocker(app, path.resolve('./mocker/mobile/index.js')) 21 | }, 22 | }, 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /vue.entry.website.client.js: -------------------------------------------------------------------------------- 1 | const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') 2 | 3 | module.exports = { 4 | entry: './src/modules/website/entry.client.js', 5 | optimization: { 6 | runtimeChunk: { 7 | name: 'manifest', 8 | }, 9 | }, 10 | plugins: [new VueSSRClientPlugin()], 11 | } 12 | -------------------------------------------------------------------------------- /vue.entry.website.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const assign = require('assign-deep') 3 | const vueConfig = require('./vue.config') 4 | 5 | const clientConfig = require('./vue.entry.website.client') 6 | const serverConfig = require('./vue.entry.website.server') 7 | 8 | const customConfig = { 9 | publicPath: '', 10 | outputDir: 'dist/website', 11 | configureWebpack: 12 | process.env.TARGET_ENV === 'server' ? serverConfig : clientConfig, 13 | devServer: { 14 | port: 8082, 15 | }, 16 | } 17 | 18 | module.exports = assign(vueConfig, customConfig) 19 | -------------------------------------------------------------------------------- /vue.entry.website.server.js: -------------------------------------------------------------------------------- 1 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') 2 | 3 | module.exports = { 4 | entry: './src/modules/website/entry.server.js', 5 | target: 'node', 6 | output: { 7 | libraryTarget: 'commonjs2', 8 | }, 9 | optimization: { 10 | splitChunks: false, 11 | }, 12 | module: { 13 | rules: [], 14 | }, 15 | plugins: [new VueSSRServerPlugin()], 16 | } 17 | -------------------------------------------------------------------------------- /webstorm.config.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('@vue/cli-service/webpack.config.js') 2 | module.exports = webpackConfig 3 | --------------------------------------------------------------------------------