diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/Procfile" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/Procfile" new file mode 100644 index 0000000000000000000000000000000000000000..1a0241303f2399a201090a2b961cb3bd28b5b813 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/Procfile" @@ -0,0 +1 @@ +web: npm run server diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/README.md" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/README.md" new file mode 100644 index 0000000000000000000000000000000000000000..331cb5cecccb58ed8eac8b5ae6df5b20a60f4ee3 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/README.md" @@ -0,0 +1,161 @@ +蓝鲸应用前端开发框架使用指南 +------------------------ + +它是基于 `Vue.js` 研发的蓝鲸体系前端分离工程的单页面应用模板(BKUI-CLI),包括了: + +- 基础工程化能力,开箱即用,无需过多配置,开发完成直接在蓝鲸PaaS可部署 +- 基础 mock 服务,帮助开发者快速伪造接口数据,测试前端 +- 蓝鲸前端/设计规范,提供统一设计及代码检测 +- bk-magic-vue 组件库,提供丰富的组件 +- 蓝鲸前端通用逻辑,包含登录模块、异步请求管理等 +- 最佳实践以及开发示例 + + + +# 本地开发 + +#### 安装依赖包 +``` +npm install +``` + +#### 配置host +配置指定域名 +``` +127.0.0.1 域名 +``` + +#### 检查配置文件 + +注意:运行之前,请检查配置文件**.babelrc**、**eslintrc.js**等以.开头的配置文件是否存在。部分操作系统会默认隐藏这类文件,导致在推送到代码仓库时漏掉,最终影响部署结果。 + +#### 启动服务 +``` +npm run dev +``` + +#### 打开链接 + +> 开发域名及端口的配置都可在`bk.config.js`中修改 + +# 前后端分离 +当前代码仅仅是应用前端,作为前后端分离架构,还需要后端服务,前后端以ajax+json进行数据处理,因此,需要 + +#### 新建后端服务模块(开发者中心) + +#### 配置APP ID +- 开发环境下编辑根目录下`env.js`,修改`development.BKPAAS_APP_ID`,用于本地开发,线上部署时会自动注入 + +#### 配置后端接口 +- 本地开发修改`env.js`中`development`的`AJAX_URL_PREFIX`字段,作为接口的url前缀 +- 线上部署测试环境修改`env.js`中`stag`方法里的的`AJAX_URL_PREFIX`字段,作为接口的url前缀 +- 线上部署测正式境修改`env.js`中`production`方法里的的`AJAX_URL_PREFIX`字段,作为接口的url前缀 + +#### 配置用户登录态信息接口,作为前端判断登录状态的验证 +> 打开首页,前端会以/user 来发起用户信息请求,如果没登录会重定向回登录页面 +> 整个框架自带登录实现,在刚打开时,如果没有登录会直接跳到登录页,如果打开后,登录过期(接口返回401状态)会弹出登录窗口 +###### 本地开发环境登录配置 +####### 用户登录配置 +`main.js` 入口会首先进行用户鉴权,如果没有用户信息,会跳转至登录页面。 +####### 登录地址配置 +本地开发需要修改根目录下`env.js` 配置 `development.BK_LOGIN_URL` 与 `development.BKPAAS_APP_ID`,线上部署会自动注入 +####### 登录鉴权 +本地登录,会在本地服务 `pass-server/middleware/user.js` 转发登录接口请求,登录接口在 `env.js` 里面的 `development.BK_LOGIN_URL` 注入 + +需要后端接口在 `/user` 路径下实现获取当前登录用户的接口, 接口规范如下 +```json +{ + "code": 0, + "data": { + "bk_username": "test", + "avatar_url": "" + }, + "message": "用户信息获取成功" +} +``` + +#### 后端服务需要解决跨域问题,推荐使用CORS方案 +详情查看MDN https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS + +# 打包构建(生成dist目录) +``` +npm run build +``` + +## 类型项目目录结构 + +使用 `bkui-cli` 初始化后,项目目录结构如下: + +```bash +|-- ROOT/ # 项目根目录 + |-- .babelrc # babel 配置 + |-- .eslintignore # eslintignore 配置 + |-- .eslintrc.js # eslint 配置 + |-- .browserslistrc # 编译代码兼容浏览器配置 + |-- env.js # 项目启动配置的环境变量等 + |-- .bk.config.js # 🌟项目编译配置文件 提供 devServe & mockServer &生产包功能集合 + |-- .gitignore # gitignore 配置 + |-- README.md # 工程的 README + |-- index-dev.html # 本地开发使用的 html + |-- index.html # 构建部署使用的 html + |-- package-lock.json # package-lock file + |-- package.json # package.json,我们在提供了基本的 doc, dev, build 等 scripts,详细内容请参见文件 + |-- postcss.config.js # postcss 配置文件,我们提供了一些常用的 postcss 插件,详细内容请参见文件 + |-- paas-server/ # 🌟可部署运行编译后代码的node服务 + | |-- middleware # 公共的中间件 + | |-- user.js # 登录中间件 + | |-- index.js # 启动服务入口 + |-- src/ # 🌟实际项目的源码目录 + | |-- App.vue # App 组件 + | |-- main.js # 主入口 + | |-- public-path.js # __webpack_public_path__ 设置 + | |-- api/ # 对 axios 封装的目录 + | | |-- cached-promise.js # promise 缓存 + | | |-- index.js # axios 封装 + | | |-- request-queue.js # 请求队列 + | |-- common/ # 项目中常用模块的目录 + | | |-- auth.js # auth + | | |-- bkmagic.js # bk-magic-vue 组件的引入 + | | |-- bus.js # 全局的 event bus + | | |-- demand-import.js # 按需引入 bk-magic-vue 的组件 + | | |-- fully-import.js # 全量引入 bk-magic-vue 的组件 + | | |-- preload.js # 页面公共请求即每次切换 router 时都必须要发送的请求 + | | |-- util.js # 项目中的常用方法 + | |-- components/ # 项目中组件的存放目录 + | | |-- auth/ # auth 组件 + | | | |-- index.css # auth 组件的样式 + | | | |-- index.vue # auth 组件 + | | |-- exception/ # exception 组件 + | | |-- index.vue # exception 组件 + | |-- css/ # 项目中通用的 css 的存放目录。各个组件的样式通常在组件各自的目录里。 + | | |-- app.css # App.vue 使用的样式 + | | |-- reset.css # 全局 reset 样式 + | | |-- variable.css # 存放 css 变量的样式 + | | |-- mixins/ # mixins 存放目录 + | | |-- scroll.css # scroll mixin + | |-- images/ # 项目中使用的图片存放目录 + | | |-- 403.png # 403 错误的图片 + | | |-- 404.png # 404 错误的图片 + | | |-- 500.png # 500 错误的图片 + | | |-- building.png # 正在建设中的图片 + | |-- router/ # 项目 router 存放目录 + | | |-- index.js # index router + | |-- store/ # 项目 store 存放目录 + | | |-- index.js # store 主模块 + | | |-- modules/ # 其他 store 模块存放目录 + | | |-- example.js # example store 模块 + | |-- views/ # 项目页面组件存放目录 + | |-- 404.vue # 404 页面组件 + | |-- index.vue # 主入口页面组件,我们在这里多使用了一层 router-view 来承载,方便之后的扩展 + | |-- example1/ # example1 页面组件存放目录 + | | |-- index.css # example1 页面组件样式 + | | |-- index.vue # example1 页面组件 + | |-- example2/ # example2 页面组件 + | | |-- index.css # example2 页面组件样式 + | | |-- index.vue # example2 页面组件 + |-- static/ # 静态资源存放目录,通常情况下, 这个目录不会人为去改变 + | |-- lib-manifest.json # webpack dll 插件生成的文件,运行 npm run dll 或者 npm run build 会自动生成 + | |-- lib.bundle.js # webpack dll 插件生成的文件,运行 npm run dll 或者 npm run build 会自动生成 + | |-- images/ # 图片静态资源存放目录 + | |-- favicon.ico # 网站 favicon +``` diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/VERSION" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/VERSION" new file mode 100644 index 0000000000000000000000000000000000000000..524cb55242b53f6a64cc646ea05db6acc7696d2d --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/VERSION" @@ -0,0 +1 @@ +1.1.1 diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/api.text" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/api.text" new file mode 100644 index 0000000000000000000000000000000000000000..9daef735d6eebf04a43b8d1ab7042c1cbcbf095c --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/api.text" @@ -0,0 +1,7 @@ +/bbs/data-source/user/tableName/bbs post 创建帖子 +/bbs/data-source/user/tableName/bbs get 获取帖子 +/bbs/data-source/user/tableName/bbs/id/${detailId}` get 获取详情 + +https://magicbox.bk.tencent.com/static_api/v3/components_vue/2.0/example/index.html#/ + +https://www.npmjs.com/package/mavon-editor/v/next diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/bin/post-compile" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/bin/post-compile" new file mode 100644 index 0000000000000000000000000000000000000000..d53f63d70c3564d728c1cf41ce066cddb5065745 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/bin/post-compile" @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +echo "Post Hook: this runs after build" diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/bin/pre-compile" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/bin/pre-compile" new file mode 100644 index 0000000000000000000000000000000000000000..42f2b5c6f43f24a7431f23d697b7b50d66f540d3 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/bin/pre-compile" @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +echo "pre Hook: this runs before npm install" diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/bk.config.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/bk.config.js" new file mode 100644 index 0000000000000000000000000000000000000000..1380da53bff0abeceb13f0c60b98ab225b2d9012 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/bk.config.js" @@ -0,0 +1,36 @@ +const { resolve } = require('path'); +const webpack = require('webpack'); +const mockServer = require('./mock-server'); +const isModeProduction = process.env.NODE_ENV === 'production'; +const env = require('./env')(); + +module.exports = { + indexPath: isModeProduction ? './index.html' : './index-dev.html', + publicPath: env.assetsPublicPath, + devServer: { + host: env.HOST, + open: true, + port: 5000, + before: mockServer, + proxy: { + '/api/bbs': { + target: '代理地址', + changeOrigin: true, + pathRewrite: { + '^/api/bbs': '/api', + }, + }, + }, + }, + chainWebpack(config) { + config.resolve.alias + .set('vue$', 'vue/dist/vue.esm.js') + .set('@', resolve('src')) + .set('@doc', resolve('doc')); + + config.plugin('define') + .use(webpack.DefinePlugin, [env]); + config.plugin('replace').use(require('./replace-static-url-plugin'), [{}]) + .after('html'); + }, +}; diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/env.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/env.js" new file mode 100644 index 0000000000000000000000000000000000000000..a0642d365059d0ad0f4784f16019a7059a80d966 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/env.js" @@ -0,0 +1,37 @@ +const config = { + production: { + NODE_ENV: JSON.stringify('production'), + AJAX_URL_PREFIX: JSON.stringify(''), + USER_INFO_URL: JSON.stringify('/api/user'), + assetsPublicPath: '{{BK_STATIC_URL}}', + }, + development: { + NODE_ENV: JSON.stringify('development'), + AJAX_URL_PREFIX: JSON.stringify('api/'), + USER_INFO_URL: JSON.stringify('/user'), + assetsPublicPath: '/', + HOST: '', + BKPAAS_APP_ID: JSON.stringify(''), + BK_LOGIN_URL: JSON.stringify(''), + }, + stag: { + NODE_ENV: JSON.stringify('production'), + AJAX_URL_PREFIX: JSON.stringify(''), + USER_INFO_URL: JSON.stringify('/api/user'), + assetsPublicPath: '{{BK_STATIC_URL}}', + }, +}; + +module.exports = () => { + if (process.env.BK_NODE_ENV === 'development') { + return config.development; + } + + const BUILD_ENV = process.env.BKPAAS_ENVIRONMENT || process.env.BK_NODE_ENV; + + if (BUILD_ENV === 'stag') { + return config.stag; + } + return config.production; +}; + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/index-dev.html" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/index-dev.html" new file mode 100644 index 0000000000000000000000000000000000000000..2508a839a0593e16f7531e67b777437ef047c7b6 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/index-dev.html" @@ -0,0 +1,36 @@ + + + + + + + index + + +
+ + + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/index.html" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/index.html" new file mode 100644 index 0000000000000000000000000000000000000000..39a7666993d6016558a8ea90dc8d2203099b6bff --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/index.html" @@ -0,0 +1,38 @@ + + + + + + + index + + +
+ + + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/mock-server/index.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/mock-server/index.js" new file mode 100644 index 0000000000000000000000000000000000000000..c4f6b74f9d303291602eada95d2d44e0ff7c7333 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/mock-server/index.js" @@ -0,0 +1,23 @@ +module.exports = (app) => { + app.use(require('cookie-parser')()); + /** mock 接口 */ + app.get('/api/table', (req, res) => { + res.json({ + code: 0, + message: '', + data: { + list: [ + { + id: 1, + ip: '127.0.0.1', + source: '微信', + status: '正常', + create_time: '2018-05-25 15:02:24', + children: [], + }, + ], + }, + }); + }); + app.use(require('../paas-server/middleware/user')); +}; diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/paas-server/api/table.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/paas-server/api/table.js" new file mode 100644 index 0000000000000000000000000000000000000000..c70293bc36930d2f9426fcf530e26d28c96ac948 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/paas-server/api/table.js" @@ -0,0 +1,22 @@ +const tableList = (app) => { + app.get('/api/table', (req, res) => { + res.json({ + code: 0, + message: '', + data: { + list: [ + { + id: 1, + ip: '127.0.0.1', + source: '微信', + status: '正常', + create_time: '2018-05-25 15:02:24', + children: [], + }, + ], + }, + }); + }); +}; + +module.exports = tableList; diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/paas-server/index.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/paas-server/index.js" new file mode 100644 index 0000000000000000000000000000000000000000..ba7cdc1f444e05b8564d8b72c4c8ce87a2a43b88 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/paas-server/index.js" @@ -0,0 +1,98 @@ +/** + * @file prod server + * 静态资源 + * 模块渲染输出 + * 注入全局变量 + * 添加html模板引擎 + */ +const Express = require('express'); +const path = require('path'); +const artTemplate = require('express-art-template'); +const cookieParser = require('cookie-parser'); +const history = require('connect-history-api-fallback'); +const user = require('./middleware/user'); + +const mockTable = require('./api/table'); + +const app = new Express(); + +const PORT = process.env.PORT || 5000; + +/** 仅解决空模版直接部署时,模拟的接口,防止直接部署接口404,实际项目可删除 */ +mockTable(app); + +app.use(cookieParser()); +app.use(user); + +// 注入全局变量 +const GLOBAL_VAR = { + SITE_URL: process.env.SITE_URL || '', + BK_STATIC_URL: process.env.BK_STATIC_URL || '', + // 当前应用的环境,预发布环境为 stag,正式环境为 prod + BKPAAS_ENVIRONMENT: process.env.BKPAAS_ENVIRONMENT || '', + // EngineApp名称,拼接规则:bkapp-{appcode}-{BKPAAS_ENVIRONMENT} + BKPAAS_ENGINE_APP_NAME: process.env.BKPAAS_ENGINE_APP_NAME || '', + // 内部版对应ieod,外部版对应tencent,混合云版对应clouds + BKPAAS_ENGINE_REGION: process.env.BKPAAS_ENGINE_REGION || '', + // APP CODE + BKPAAS_APP_ID: process.env.BKPAAS_APP_ID || '', + BKPAAS_APP_SECRET: process.env.BKPAAS_APP_SECRET || '', + BK_LOGIN_URL: process.env.BK_LOGIN_URL || '', +}; + + +const distDir = path.resolve(__dirname, '../dist'); + +app.use(history({ + index: '/', + rewrites: [ + { + // connect-history-api-fallback 默认会对 url 中有 . 的 url 当成静态资源处理而不是当成页面地址来处理 + // 兼容 /cs/28aa9eda67644a6eb254d694d944307e/cluster/BCS-MESOS-10001/node/10.121.23.12 这样以 IP 结尾的 url + // from: /\d+\.\d+\.\d+\.\d+$/, + from: /\/(\d+\.)*\d+$/, + to: '/', + }, + { + // connect-history-api-fallback 默认会对 url 中有 . 的 url 当成静态资源处理而不是当成页面地址来处理 + // 兼容 下面带有.路径 + // /bcs/projectId/app/214/taskgroups/0.application-1-13.test123.10013.1510806131114508229/containers/containerId + from: /\/\/+.*\..*\//, + to: '/', + }, + ], +})); + + +// 首页 +app.get('/', (req, res) => { + const scriptName = (req.headers['x-script-name'] || '').replace(/\//g, ''); + // 使用子路径 + if (scriptName) { + GLOBAL_VAR.BK_STATIC_URL = `/${scriptName}`; + GLOBAL_VAR.SITE_URL = `/${scriptName}`; + } else { + // 使用系统分配域名 + GLOBAL_VAR.BK_STATIC_URL = ''; + GLOBAL_VAR.SITE_URL = ''; + } + // 注入全局变量 + res.render(path.join(distDir, 'index.html'), GLOBAL_VAR); +}); + + +app.use('/static', Express.static(path.join(distDir, '../dist/static'))); +// 配置视图 +app.set('views', path.join(__dirname, '../dist')); + +// 配置模板引擎 +// http://aui.github.io/art-template/zh-cn/docs/ +app.engine('html', artTemplate); +app.set('view engine', 'html'); + +// 配置端口 +app.listen(PORT, () => { + console.log(`App is running in port ${PORT}`); +}); + + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/paas-server/middleware/user.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/paas-server/middleware/user.js" new file mode 100644 index 0000000000000000000000000000000000000000..7ca8a3575e9229c28323e08d2f69cc7baf893bc2 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/paas-server/middleware/user.js" @@ -0,0 +1,44 @@ + +const env = require('../../env')(); +// 获取登录用户详情 +module.exports = async function user(req, res, next) { + if (req.path !== '/api/user' && req.path !== '/user') { + next(); + return; + } + const request = require('request'); + const loginUrl = process.env.BK_LOGIN_URL || (env.BK_LOGIN_URL && JSON.parse(env.BK_LOGIN_URL)) || ''; + const url = `${loginUrl}api/v3/is_login/?bk_token=${req.cookies.bk_token}`; + request(url, (err, response, body) => { + if (err) { + res.status(500); + res.send(''); + return; + } + const data = JSON.parse(body || '{}'); + + // 有登录状态 + if (data.code === 0) { + const { + bk_username, + avatar_url, + } = data.data; + res.json({ + code: 0, + message: data.msg, + data: { + username: bk_username, + avatar_url, + }, + }); + return; + } + // 登录状态失效 + // const loginURL = `${BK_LOGIN_URL}plain?`; + res.status(401); + // res.set({ + // 'X-Login-Url': loginURL, + // }); + res.send(''); + }); +}; diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/package.json" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/package.json" new file mode 100644 index 0000000000000000000000000000000000000000..51d7e485c1c7eeab76e5e84aadf9952fd8a0666d --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/package.json" @@ -0,0 +1,54 @@ +{ + "name": "node-vue-spa", + "version": "1.0.0", + "description": "Project description", + "author": "", + "scripts": { + "dev": "bk-cli-service dev", + "build": "bk-cli-service build", + "build:stag": "bk-cli-service build --mode=stag", + "server": "node ./paas-server/index.js", + "lint": "eslint --ext .js,.vue ./src", + "lint-fix": "eslint --fix --ext .js,.vue ./src" + }, + "rules": { + "no-unused-vars": 0 + }, + "keywords": [], + "license": "ISC", + "dependencies": { + "art-template": "^4.13.2", + "axios": "^0.21.1", + "babel-plugin-import-bk-magic-vue": "latest", + "bk-magic-vue": "latest", + "connect-history-api-fallback": "^1.6.0", + "cookie-parser": "^1.4.5", + "express": "^4.17.1", + "express-art-template": "^1.0.1", + "mavon-editor": "^2.10.4", + "postcss": "^8.3.0", + "postcss-advanced-variables": "~3.0.0", + "postcss-atroot": "~0.1.3", + "postcss-extend-rule": "~2.0.0", + "postcss-import": "~12.0.1", + "postcss-import-webpack-resolver": "~1.0.1", + "postcss-loader": "~3.0.0", + "postcss-mixins": "~6.2.1", + "postcss-nested": "~4.1.2", + "postcss-preset-env": "~6.6.0", + "postcss-property-lookup": "~2.0.0", + "postcss-url": "~8.0.0", + "request": "^2.88.2", + "vue-router": "~3.0.6", + "vuex": "~3.1.1" + }, + "devDependencies": { + "@blueking/babel-preset-bk": "^2.0.0", + "@blueking/cli-service-webpack4": "^2.0.0", + "@blueking/eslint-config-bk": "^2.0.0" + }, + "engines": { + "node": ">= 12.16.3", + "npm": ">= 6.4.1 <7" + } +} diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/postcss.config.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/postcss.config.js" new file mode 100644 index 0000000000000000000000000000000000000000..f6088949cc1ed83fff9effd0a7cecfbf37b404d3 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/postcss.config.js" @@ -0,0 +1,68 @@ +/** + * @file postcss 基本配置 + * @author + */ + +const createResolver = require('postcss-import-webpack-resolver'); +// const baseConf = require('./build/webpack.base.conf.js') +const { resolve } = require('path'); + +// https://github.com/michael-ciniawsky/postcss-load-config +module.exports = { + plugins: { + // 把 import 的内容转换为 inline + // @see https://github.com/postcss/postcss-import#postcss-import + 'postcss-import': { + // 使用 webpack 配置里的 resolve.alias + // https://github.com/krambuhl/postcss-import-webpack-resolver#postcss-import-webpack-resolver + resolve: createResolver({ + alias: { + '@': resolve('src'), + '@doc': resolve('doc'), + }, + modules: ['src', 'node_modules'], + }), + }, + + // mixins,本插件需要放在 postcss-simple-vars 和 postcss-nested 插件前面 + // @see https://github.com/postcss/postcss-mixins#postcss-mixins- + 'postcss-mixins': { + }, + + // 用于在 URL ( )上重新定位、内嵌或复制。 + // @see https://github.com/postcss/postcss-url#postcss-url + 'postcss-url': { + url: 'rebase', + }, + + // cssnext 已经不再维护,推荐使用 postcss-preset-env + 'postcss-preset-env': { + // see https://github.com/csstools/postcss-preset-env#options + stage: 0, + autoprefixer: { + grid: true, + }, + }, + // 这个插件可以在写 nested 样式时省略开头的 & + // @see https://github.com/postcss/postcss-nested#postcss-nested- + 'postcss-nested': {}, + + // 将 @at-root 里的规则放入到根节点 + // @see https://github.com/OEvgeny/postcss-atroot#postcss-at-root- + 'postcss-atroot': {}, + + // 提供 @extend 语法 + // @see https://github.com/jonathantneal/postcss-extend-rule#postcss-extend-rule- + 'postcss-extend-rule': {}, + + // 变量相关 + // @see https://github.com/jonathantneal/postcss-advanced-variables#postcss-advanced-variables- + 'postcss-advanced-variables': { + // variables 属性内的变量为全局变量 + }, + + // 类似于 stylus,直接引用属性而不需要变量定义 + // @see https://github.com/simonsmith/postcss-property-lookup#postcss-property-lookup- + 'postcss-property-lookup': {}, + }, +}; diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/replace-static-url-plugin.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/replace-static-url-plugin.js" new file mode 100644 index 0000000000000000000000000000000000000000..c084357d679de9d44ee49e69f4dc4378d0e7b4e0 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/replace-static-url-plugin.js" @@ -0,0 +1,44 @@ +/** + * @file 替换 asset css 中的 BK_STATIC_URL,__webpack_public_path__ 没法解决 asset 里静态资源的 url + * @author + */ + +const { extname } = require('path'); + +class ReplaceCSSStaticUrlPlugin { + apply(compiler) { + // emit: 在生成资源并输出到目录之前 + compiler.hooks.emit.tapAsync('ReplaceCSSStaticUrlPlugin', (compilation, callback) => { + const assets = Object.keys(compilation.assets); + const assetsLen = assets.length; + for (let i = 0; i < assetsLen; i++) { + const fileName = assets[i]; + if (extname(fileName) !== '.css') { + continue; + } + + const asset = compilation.assets[fileName]; + + const minifyFileContent = asset.source().toString() + .replace( + /\{\{\s*BK_STATIC_URL\s*\}\}/g, + () => '../../', + ); + // 设置输出资源 + compilation.assets[fileName] = { + // 返回文件内容 + source: () => minifyFileContent, + // 返回文件大小 + size: () => Buffer.byteLength(minifyFileContent, 'utf8'), + }; + } + + callback(); + }); + } +} + + +module.exports = ReplaceCSSStaticUrlPlugin; + + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/App.vue" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/App.vue" new file mode 100644 index 0000000000000000000000000000000000000000..d774c200ac1cde48985c9e3d554080e22879383f --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/App.vue" @@ -0,0 +1,285 @@ + + + + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/api/cached-promise.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/api/cached-promise.js" new file mode 100644 index 0000000000000000000000000000000000000000..e5a07ad7d21c4c48f985147e541631e9c12d139e --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/api/cached-promise.js" @@ -0,0 +1,64 @@ +/** + * @file promise 缓存 + * @author + */ + +export default class CachedPromise { + constructor() { + this.cache = {}; + } + + /** + * 根据 id 获取缓存对象,如果不传 id,则获取所有缓存 + * + * @param {string?} id id + * + * @return {Array|Promise} 缓存集合或promise 缓存对象 + */ + get(id) { + if (typeof id === 'undefined') { + return Object.keys(this.cache).map(requestId => this.cache[requestId]); + } + return this.cache[id]; + } + + /** + * 设置 promise 缓存对象 + * + * @param {string} id id + * @param {Promise} promise 要缓存的 promise 对象 + * + * @return {Promise} promise 对象 + */ + set(id, promise) { + Object.assign(this.cache, { [id]: promise }); + } + + /** + * 删除 promise 缓存对象 + * + * @param {string|Array?} deleteIds 要删除的缓存对象的 id,如果不传则删除所有 + * + * @return {Promise} 以成功的状态返回 Promise 对象 + */ + delete(deleteIds) { + let requestIds = []; + if (typeof deleteIds === 'undefined') { + requestIds = Object.keys(this.cache); + } else if (deleteIds instanceof Array) { + deleteIds.forEach((id) => { + if (this.get(id)) { + requestIds.push(id); + } + }); + } else if (this.get(deleteIds)) { + requestIds.push(deleteIds); + } + + requestIds.forEach((requestId) => { + delete this.cache[requestId]; + }); + + return Promise.resolve(deleteIds); + } +} diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/api/index.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/api/index.js" new file mode 100644 index 0000000000000000000000000000000000000000..4fa04ece46e77884035b6745c478ab76ff123139 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/api/index.js" @@ -0,0 +1,242 @@ +/** + * @file axios 封装 + * @author + */ + +/* eslint-disable */ + +import Vue from 'vue'; +import axios from 'axios'; +import cookie from 'cookie'; + +import CachedPromise from './cached-promise'; +import RequestQueue from './request-queue'; +import { bus } from '../common/bus'; +import { messageError } from '@/common/bkmagic'; +import UrlParse from 'url-parse'; +import queryString from 'query-string'; + +// axios 实例 +const axiosInstance = axios.create({ + baseURL: window.PROJECT_CONFIG.SITE_URL + AJAX_URL_PREFIX, + withCredentials: true, + headers: { 'X-REQUESTED-WITH': 'XMLHttpRequest' }, +}); + +/** + * request interceptor + */ +axiosInstance.interceptors.request.use((config) => { + const urlObj = new UrlParse(config.url); + const query = queryString.parse(urlObj.query); + // 在发起请求前,注入CSRFToken,解决跨域 + injectCSRFTokenToHeaders(); + return config; +}, error => Promise.reject(error)); + +/** + * response interceptor + */ +axiosInstance.interceptors.response.use( + response => response.data, + error => Promise.reject(error), +); + +const http = { + queue: new RequestQueue(), + cache: new CachedPromise(), + cancelRequest: requestId => http.queue.cancel(requestId), + cancelCache: requestId => http.cache.delete(requestId), + cancel: requestId => Promise.all([http.cancelRequest(requestId), http.cancelCache(requestId)]), +}; + +const methodsWithoutData = ['delete', 'get', 'head', 'options']; +const methodsWithData = ['post', 'put', 'patch']; +const allMethods = [...methodsWithoutData, ...methodsWithData]; + +// 在自定义对象 http 上添加各请求方法 +allMethods.forEach((method) => { + Object.defineProperty(http, method, { + get () { + return getRequest(method); + }, + }); +}); + +/** + * 获取 http 不同请求方式对应的函数 + * + * @param {string} method http method 与 axios 实例中的 method 保持一致 + * + * @return {Function} 实际调用的请求函数 + */ +function getRequest (method) { + if (methodsWithData.includes(method)) { + return (url, data, config) => getPromise(method, url, data, config); + } + return (url, config) => getPromise(method, url, null, config); +} + +/** + * 实际发起 http 请求的函数,根据配置调用缓存的 promise 或者发起新的请求 + * + * @param {string} method http method 与 axios 实例中的 method 保持一致 + * @param {string} url 请求地址 + * @param {Object} data 需要传递的数据, 仅 post/put/patch 三种请求方式可用 + * @param {Object} userConfig 用户配置,包含 axios 的配置与本系统自定义配置 + * + * @return {Promise} 本次http请求的Promise + */ +async function getPromise (method, url, data, userConfig = {}) { + const config = initConfig(method, url, userConfig); + let promise; + if (config.cancelPrevious) { + await http.cancel(config.requestId); + } + + if (config.clearCache) { + http.cache.delete(config.requestId); + } else { + promise = http.cache.get(config.requestId); + } + if (config.fromCache && promise) { + return promise; + } + + promise = new Promise(async (resolve, reject) => { + const axiosRequest = methodsWithData.includes(method) + ? axiosInstance[method](url, data, config) + : axiosInstance[method](url, config); + + try { + const response = await axiosRequest; + Object.assign(config, response.config || {}); + handleResponse({ config, response, resolve, reject }); + } catch (error) { + Object.assign(config, error.config); + reject(error); + } + }).catch(error => handleReject(error, config)) + .finally(() => { + // console.log('finally', config) + }); + + // 添加请求队列 + http.queue.set(config); + // 添加请求缓存 + http.cache.set(config.requestId, promise); + + return promise; +} + +/** + * 处理 http 请求成功结果 + * + * @param {Object} 请求配置 + * @param {Object} response cgi 原始返回数据 + * @param {Function} response, promise 完成函数 + * @param {Function} reject promise 拒绝函数 + */ +function handleResponse ({ config, response, resolve, reject }) { + if (!response.data && config.globalError) { + reject({ message: response.message }); + } else { + resolve(config.originalResponse ? response : response.data, config); + } + http.queue.delete(config.requestId); +} + +/** + * 处理 http 请求失败结果 + * + * @param {Object} error Error 对象 + * @param {config} config 请求配置 + * + * @return {Promise} promise 对象 + */ +function handleReject (error, config) { + if (axios.isCancel(error)) { + return Promise.reject(error); + } + + http.queue.delete(config.requestId); + if (config.globalError && error.response) { + const { status, data } = error.response; + const nextError = { message: error.message, response: error.response }; + if (status === 401) { + bus.$emit('show-login-modal', nextError.response); + } else if (status === 500) { + nextError.message = '系统出现异常'; + } else if (data && data.message) { + nextError.message = data.message; + } + messageError(nextError.message); + console.error(nextError.message); + return Promise.reject(nextError); + } + messageError(error.message); + console.error(error.message); + return Promise.reject(error); +} + +/** + * 初始化本系统 http 请求的各项配置 + * + * @param {string} method http method 与 axios 实例中的 method 保持一致 + * @param {string} url 请求地址, 结合 method 生成 requestId + * @param {Object} userConfig 用户配置,包含 axios 的配置与本系统自定义配置 + * + * @return {Promise} 本次 http 请求的 Promise + */ +function initConfig (method, url, userConfig) { + const defaultConfig = { + ...getCancelToken(), + // http 请求默认 id + requestId: `${method}_${url}`, + // 是否全局捕获异常 + globalError: true, + // 是否直接复用缓存的请求 + fromCache: false, + // 是否在请求发起前清楚缓存 + clearCache: false, + // 响应结果是否返回原始数据 + originalResponse: true, + // 当路由变更时取消请求 + cancelWhenRouteChange: true, + // 取消上次请求 + cancelPrevious: true, + }; + return Object.assign(defaultConfig, userConfig); +} + +/** + * 生成 http 请求的 cancelToken,用于取消尚未完成的请求 + * + * @return {Object} {cancelToken: axios 实例使用的 cancelToken, cancelExcutor: 取消http请求的可执行函数} + */ +function getCancelToken () { + let cancelExcutor; + const cancelToken = new axios.CancelToken((excutor) => { + cancelExcutor = excutor; + }); + return { + cancelToken, + cancelExcutor, + }; +} + +Vue.prototype.$http = http; + +export default http; + +/** + * 向 http header 注入 CSRFToken,CSRFToken key 值与后端一起协商制定 + */ +export function injectCSRFTokenToHeaders () { + const CSRFToken = cookie.parse(document.cookie)[`${window.PROJECT_CONFIG.BKPAAS_APP_ID}_csrftoken`]; + if (CSRFToken !== undefined) { + axiosInstance.defaults.headers.common['X-CSRFToken'] = CSRFToken; + } else { + console.warn('Can not find csrftoken in document.cookie'); + } +} diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/api/request-queue.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/api/request-queue.js" new file mode 100644 index 0000000000000000000000000000000000000000..64f54665aa47d8111c9d9f61c60b058d9dcf1a40 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/api/request-queue.js" @@ -0,0 +1,88 @@ +/** + * @file 请求队列 + * @author + */ + +export default class RequestQueue { + constructor() { + this.queue = []; + } + + /** + * 根据 id 获取请求对象,如果不传入 id,则获取整个地列 + * + * @param {string?} id id + * + * @return {Array|Object} 队列集合或队列对象 + */ + get(id) { + if (typeof id === 'undefined') { + return this.queue; + } + return this.queue.filter(request => request.requestId === id); + } + + /** + * 设置新的请求对象到请求队列中 + * + * @param {Object} newRequest 请求对象 + */ + set(newRequest) { + this.queue.push(newRequest); + // if (!this.queue.some(request => request.requestId === newRequest.requestId)) { + // this.queue.push(newRequest) + // } + } + + /** + * 根据 id 删除请求对象 + * + * @param {string} id id + */ + delete(id) { + // const target = this.queue.filter(request => request.requestId === id)[0] + // if (target) { + // const index = this.queue.indexOf(target) + // this.queue.splice(index, 1) + // } + this.queue = [...this.queue.filter(request => request.requestId !== id)]; + } + + /** + * cancel 请求队列中的请求 + * + * @param {string|Array?} requestIds 要 cancel 的请求 id,如果不传,则 cancel 所有请求 + * @param {string?} msg cancel 时的信息 + * + * @return {Promise} promise 对象 + */ + cancel(requestIds, msg = 'request canceled') { + let cancelQueue = []; + if (typeof requestIds === 'undefined') { + cancelQueue = [...this.queue]; + } else if (requestIds instanceof Array) { + requestIds.forEach((requestId) => { + const cancelRequest = this.get(requestId); + if (cancelRequest) { + cancelQueue = [...cancelQueue, ...cancelRequest]; + } + }); + } else { + const cancelRequest = this.get(requestIds); + if (cancelRequest) { + cancelQueue = [...cancelQueue, ...cancelRequest]; + } + } + + try { + cancelQueue.forEach((request) => { + const { requestId } = request; + this.delete(requestId); + request.cancelExcutor(`${msg}: ${requestId}`); + }); + return Promise.resolve(requestIds); + } catch (error) { + return Promise.reject(error); + } + } +} diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/auth.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/auth.js" new file mode 100644 index 0000000000000000000000000000000000000000..3c46e8c0d34e8f8805cb25e6d44d502b5db96e48 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/auth.js" @@ -0,0 +1,76 @@ +/** + * @file auth + * @author + */ + +import store from '@/store'; + +const ANONYMOUS_USER = { + id: null, + isAuthenticated: false, + username: 'anonymous', +}; + +let currentUser = { + id: '', + username: '', +}; + +export default { + /** + * 未登录状态码 + */ + HTTP_STATUS_UNAUTHORIZED: 401, + + /** + * 获取当前用户 + * + * @return {Object} 当前用户信息 + */ + getCurrentUser() { + return currentUser; + }, + + redirectToLogin() { + // const callbackUrl = `${location.origin}/static/login_success.html?is_ajax=1`; + const callbackUrl = encodeURIComponent(window.location.href); + const appCode = window.PROJECT_CONFIG.BKPAAS_APP_ID || BKPAAS_APP_ID; + const loginUrl = window.PROJECT_CONFIG.BK_LOGIN_URL || BK_LOGIN_URL; + window.location.href = `${loginUrl}?app_id=${appCode}&c_url=${callbackUrl}`; + }, + + /** + * 请求当前用户信息 + * + * @return {Promise} promise 对象 + */ + requestCurrentUser() { + let promise = null; + if (currentUser.isAuthenticated) { + promise = new Promise((resolve) => { + resolve(currentUser); + }); + } else { + if (!store.state.user || !Object.keys(store.state.user).length) { + // store action userInfo 里,如果请求成功会更新 state.user + const req = store.dispatch('userInfo'); + promise = new Promise((resolve, reject) => { + req.then(() => { + // 存储当前用户信息(全局) + currentUser = store.getters.user; + currentUser.isAuthenticated = true; + resolve(currentUser); + }, (err) => { + if (err.response.status === this.HTTP_STATUS_UNAUTHORIZED || err.crossDomain) { + resolve({ ...ANONYMOUS_USER }); + } else { + reject(err); + } + }); + }); + } + } + + return promise; + }, +}; diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/bkmagic.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/bkmagic.js" new file mode 100644 index 0000000000000000000000000000000000000000..690e088d6b8c997f2c56501369b81ef1502b3ce9 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/bkmagic.js" @@ -0,0 +1,58 @@ +/** + * @file 引入 bk-magic-vue 组件 + * @author + */ + +import Vue from 'vue'; + +// 全量引入 +import './fully-import'; + +// 按需引入 +// import './demand-import' + +const Message = Vue.prototype.$bkMessage; + +let messageInstance = null; + +export const messageError = (message, delay = 3000) => { + messageInstance?.close(); + messageInstance = Message({ + message, + delay, + theme: 'error', + }); +}; + +export const messageSuccess = (message, delay = 3000) => { + messageInstance?.close(); + messageInstance = Message({ + message, + delay, + theme: 'success', + }); +}; + +export const messageInfo = (message, delay = 3000) => { + messageInstance?.close(); + messageInstance = Message({ + message, + delay, + theme: 'primary', + }); +}; + +export const messageWarn = (message, delay = 3000) => { + messageInstance?.close(); + messageInstance = Message({ + message, + delay, + theme: 'warning', + hasCloseIcon: true, + }); +}; + +Vue.prototype.messageError = messageError; +Vue.prototype.messageSuccess = messageSuccess; +Vue.prototype.messageInfo = messageInfo; +Vue.prototype.messageWarn = messageWarn; diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/bus.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/bus.js" new file mode 100644 index 0000000000000000000000000000000000000000..8dd557bb609e804be5e73ed296c2319867142d30 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/bus.js" @@ -0,0 +1,11 @@ + +/** + * @file event bus + * @author + */ + +import Vue from 'vue'; + +// Use a bus for components communication, +// see https://vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication +export const bus = new Vue(); diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/demand-import.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/demand-import.js" new file mode 100644 index 0000000000000000000000000000000000000000..e31306864b9bdf617ca7cdc8b76ec3c4b60f0262 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/demand-import.js" @@ -0,0 +1,90 @@ +/** + * @file 按需引入 bk-magic-vue 组件 + * @author + */ + +/* eslint-disable import/no-duplicates */ + +import Vue from 'vue'; + +import { + bkBadge, bkButton, bkCheckbox, bkCheckboxGroup, bkCol, bkCollapse, bkCollapseItem, bkContainer, bkDatePicker, + bkDialog, bkDropdownMenu, bkException, bkForm, + bkFormItem, bkInfoBox, bkInput, bkLoading, bkMemberSelector, bkMessage, + bkNavigation, bkNavigationMenu, bkNavigationMenuItem, bkNotify, bkOption, bkOptionGroup, bkPagination, + bkPopover, bkProcess, bkProgress, bkRadio, bkRadioGroup, bkRoundProgress, bkRow, bkSearchSelect, bkSelect, + bkSideslider, bkSlider, bkSteps, bkSwitcher, bkTab, bkTabPanel, bkTable, bkTableColumn, bkTagInput, bkPopconfirm, + bkTimePicker, bkTimeline, bkTransfer, bkTree, + bkUpload, bkClickoutside, bkTooltips, bkSwiper, bkRate, bkAnimateNumber, + bkVirtualScroll, bkColorPicker, bkCard, +} from '@tencent/bk-magic-vue'; + +// bkDiff 组件体积较大且不是很常用,因此注释掉。如果需要,打开注释即可 +// import { bkDiff } from '@tencent/bk-magic-vue' + +// components use +Vue.use(bkBadge); +Vue.use(bkButton); +Vue.use(bkCheckbox); +Vue.use(bkCheckboxGroup); +Vue.use(bkCol); +Vue.use(bkCollapse); +Vue.use(bkCollapseItem); +Vue.use(bkContainer); +Vue.use(bkDatePicker); +Vue.use(bkDialog); +Vue.use(bkDropdownMenu); +Vue.use(bkException); +Vue.use(bkForm); +Vue.use(bkFormItem); +Vue.use(bkInput); +Vue.use(bkMemberSelector); +Vue.use(bkNavigation); +Vue.use(bkNavigationMenu); +Vue.use(bkNavigationMenuItem); +Vue.use(bkOption); +Vue.use(bkOptionGroup); +Vue.use(bkPagination); +Vue.use(bkPopover); +Vue.use(bkProcess); +Vue.use(bkProgress); +Vue.use(bkRadio); +Vue.use(bkRadioGroup); +Vue.use(bkRoundProgress); +Vue.use(bkRow); +Vue.use(bkSearchSelect); +Vue.use(bkSelect); +Vue.use(bkSideslider); +Vue.use(bkSlider); +Vue.use(bkSteps); +Vue.use(bkSwitcher); +Vue.use(bkTab); +Vue.use(bkTabPanel); +Vue.use(bkTable); +Vue.use(bkTableColumn); +Vue.use(bkTagInput); +Vue.use(bkPopconfirm); +Vue.use(bkTimePicker); +Vue.use(bkTimeline); +Vue.use(bkTransfer); +Vue.use(bkTree); +Vue.use(bkUpload); +Vue.use(bkSwiper); +Vue.use(bkRate); +Vue.use(bkAnimateNumber); +Vue.use(bkVirtualScroll); +Vue.use(bkColorPicker); +Vue.use(bkCard); + +// bkDiff 组件体积较大且不是很常用,因此注释了。如果需要,打开注释即可 +// Vue.use(bkDiff) + +// directives use +Vue.use(bkClickoutside); +Vue.use(bkTooltips); +Vue.use(bkLoading); + +// Vue prototype mount +Vue.prototype.$bkInfo = bkInfoBox; +Vue.prototype.$bkMessage = bkMessage; +Vue.prototype.$bkNotify = bkNotify; diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/fully-import.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/fully-import.js" new file mode 100644 index 0000000000000000000000000000000000000000..1fa2e3e7b25c75952f8d9206b525e5f79f448987 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/fully-import.js" @@ -0,0 +1,11 @@ +/** + * @file 全量引入 bk-magic-vue 组件 + * @author bk + */ + +import Vue from 'vue'; +import bkMagicVue from 'bk-magic-vue'; +import 'bk-magic-vue/dist/bk-magic-vue.min.css'; + +Vue.use(bkMagicVue); + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/preload.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/preload.js" new file mode 100644 index 0000000000000000000000000000000000000000..2eba092d3ce5526352877404c602d2ec461925ed --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/preload.js" @@ -0,0 +1,26 @@ +/** + * @file 页面公共请求即每切换 router 时都必须要发送的请求 + * @author + */ + +// import store from '@/store' + +// const config = { +// fromCache: true, +// cancelWhenRouteChange: false +// } + +/** + * 获取 user 信息 + * + * @return {Promise} promise 对象 + */ +// function getUser () { +// return store.dispatch('userInfo', config) +// } + +export default function () { + return Promise.all([ + // getUser() + ]); +} diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/util.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/util.js" new file mode 100644 index 0000000000000000000000000000000000000000..c9fbbc529f9af6c65508c5a0ee47d1d2837cd364 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/common/util.js" @@ -0,0 +1,366 @@ +/** + * @file 通用方法 + * @author + */ + +/* eslint-disable */ + +/** + * 函数柯里化 + * + * @example + * function add (a, b) {return a + b} + * curry(add)(1)(2) + * + * @param {Function} fn 要柯里化的函数 + * + * @return {Function} 柯里化后的函数 + */ +export function curry (fn) { + const judge = (...args) => args.length === fn.length + ? fn(...args) + : arg => judge(...args, arg); + return judge; +} + +/** + * 判断是否是对象 + * + * @param {Object} obj 待判断的 + * + * @return {boolean} 判断结果 + */ +export function isObject (obj) { + return obj !== null && typeof obj === 'object'; +} + +/** + * 规范化参数 + * + * @param {Object|string} type vuex type + * @param {Object} payload vuex payload + * @param {Object} options vuex options + * + * @return {Object} 规范化后的参数 + */ +export function unifyObjectStyle (type, payload, options) { + if (isObject(type) && type.type) { + options = payload; + payload = type; + type = type.type; + } + + if (process.env.NODE_ENV !== 'production') { + if (typeof type !== 'string') { + console.warn(`expects string as the type, but found ${typeof type}.`); + } + } + + return { type, payload, options }; +} + +/** + * 以 baseColor 为基础生成随机颜色 + * + * @param {string} baseColor 基础颜色 + * @param {number} count 随机颜色个数 + * + * @return {Array} 颜色数组 + */ +export function randomColor (baseColor, count) { + const segments = baseColor.match(/[\da-z]{2}/g); + // 转换成 rgb 数字 + for (let i = 0; i < segments.length; i++) { + segments[i] = parseInt(segments[i], 16); + } + const ret = []; + // 生成 count 组颜色,色差 20 * Math.random + for (let i = 0; i < count; i++) { + ret[i] = `#${ + Math.floor(segments[0] + (Math.random() < 0.5 ? -1 : 1) * Math.random() * 20).toString(16) + }${Math.floor(segments[1] + (Math.random() < 0.5 ? -1 : 1) * Math.random() * 20).toString(16) + }${Math.floor(segments[2] + (Math.random() < 0.5 ? -1 : 1) * Math.random() * 20).toString(16)}`; + } + return ret; +} + +/** + * min max 之间的随机整数 + * + * @param {number} min 最小值 + * @param {number} max 最大值 + * + * @return {number} 随机数 + */ +export function randomInt (min, max) { + return Math.floor(Math.random() * (max - min + 1) + min); +} + +/** + * 异常处理 + * + * @param {Object} err 错误对象 + * @param {Object} ctx 上下文对象,这里主要指当前的 Vue 组件 + */ +export function catchErrorHandler (err, ctx) { + const { data } = err; + if (data) { + if (!data.code || data.code === 404) { + ctx.exceptionCode = { + code: '404', + msg: '当前访问的页面不存在', + }; + } else if (data.code === 403) { + ctx.exceptionCode = { + code: '403', + msg: 'Sorry,您的权限不足!', + }; + } else { + console.error(err); + ctx.bkMessageInstance = ctx.$bkMessage({ + theme: 'error', + message: err.message || err.data.msg || err.statusText, + }); + } + } else { + console.error(err); + ctx.bkMessageInstance = ctx.$bkMessage({ + theme: 'error', + message: err.message || err.data.msg || err.statusText, + }); + } +} + +/** + * 获取字符串长度,中文算两个,英文算一个 + * + * @param {string} str 字符串 + * + * @return {number} 结果 + */ +export function getStringLen (str) { + let len = 0; + for (let i = 0; i < str.length; i++) { + if (str.charCodeAt(i) > 127 || str.charCodeAt(i) === 94) { + len += 2; + } else { + len++; + } + } + return len; +} + +/** + * 转义特殊字符 + * + * @param {string} str 待转义字符串 + * + * @return {string} 结果 + */ +export const escape = str => String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); + +/** + * 对象转为 url query 字符串 + * + * @param {*} param 要转的参数 + * @param {string} key key + * + * @return {string} url query 字符串 + */ +export function json2Query (param, key) { + const mappingOperator = '='; + const separator = '&'; + let paramStr = ''; + + if (param instanceof String || typeof param === 'string' + || param instanceof Number || typeof param === 'number' + || param instanceof Boolean || typeof param === 'boolean' + ) { + paramStr += separator + key + mappingOperator + encodeURIComponent(param); + } else { + Object.keys(param).forEach((p) => { + const value = param[p]; + const k = (key === null || key === '' || key === undefined) + ? p + : key + (param instanceof Array ? `[${p}]` : `.${p}`); + paramStr += separator + json2Query(value, k); + }); + } + return paramStr.substr(1); +} + +/** + * 字符串转换为驼峰写法 + * + * @param {string} str 待转换字符串 + * + * @return {string} 转换后字符串 + */ +export function camelize (str) { + return str.replace(/-(\w)/g, (strMatch, p1) => p1.toUpperCase()); +} + +/** + * 获取元素的样式 + * + * @param {Object} elem dom 元素 + * @param {string} prop 样式属性 + * + * @return {string} 样式值 + */ +export function getStyle (elem, prop) { + if (!elem || !prop) { + return false; + } + + // 先获取是否有内联样式 + let value = elem.style[camelize(prop)]; + + if (!value) { + // 获取的所有计算样式 + let css = ''; + if (document.defaultView && document.defaultView.getComputedStyle) { + css = document.defaultView.getComputedStyle(elem, null); + value = css ? css.getPropertyValue(prop) : null; + } + } + + return String(value); +} + +/** + * 获取元素相对于页面的高度 + * + * @param {Object} node 指定的 DOM 元素 + */ +export function getActualTop (node) { + let actualTop = node.offsetTop; + let current = node.offsetParent; + + while (current !== null) { + actualTop += current.offsetTop; + current = current.offsetParent; + } + + return actualTop; +} + +/** + * 获取元素相对于页面左侧的宽度 + * + * @param {Object} node 指定的 DOM 元素 + */ +export function getActualLeft (node) { + let actualLeft = node.offsetLeft; + let current = node.offsetParent; + + while (current !== null) { + actualLeft += current.offsetLeft; + current = current.offsetParent; + } + + return actualLeft; +} + +/** + * document 总高度 + * + * @return {number} 总高度 + */ +export function getScrollHeight () { + let scrollHeight = 0; + let bodyScrollHeight = 0; + let documentScrollHeight = 0; + + if (document.body) { + bodyScrollHeight = document.body.scrollHeight; + } + + if (document.documentElement) { + documentScrollHeight = document.documentElement.scrollHeight; + } + + scrollHeight = (bodyScrollHeight - documentScrollHeight > 0) ? bodyScrollHeight : documentScrollHeight; + + return scrollHeight; +} + +/** + * 滚动条在 y 轴上的滚动距离 + * + * @return {number} y 轴上的滚动距离 + */ +export function getScrollTop () { + let scrollTop = 0; + let bodyScrollTop = 0; + let documentScrollTop = 0; + + if (document.body) { + bodyScrollTop = document.body.scrollTop; + } + + if (document.documentElement) { + documentScrollTop = document.documentElement.scrollTop; + } + + scrollTop = (bodyScrollTop - documentScrollTop > 0) ? bodyScrollTop : documentScrollTop; + + return scrollTop; +} + +/** + * 浏览器视口的高度 + * + * @return {number} 浏览器视口的高度 + */ +export function getWindowHeight () { + const windowHeight = document.compatMode === 'CSS1Compat' + ? document.documentElement.clientHeight + : document.body.clientHeight; + + return windowHeight; +} + +/** + * 简单的 loadScript + * + * @param {string} url js 地址 + * @param {Function} callback 回调函数 + */ +export function loadScript (url, callback) { + const script = document.createElement('script'); + script.async = true; + script.src = url; + + script.onerror = () => { + callback(new Error(`Failed to load: ${url}`)); + }; + + script.onload = () => { + callback(); + }; + + document.getElementsByTagName('head')[0].appendChild(script); +} + +/** + * @description 不用正则匹配,如:var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)"); + * 因如一次性读取多个值,不如一次操作完,存在后再取。 + * @param strCookie {string} + * @return {{}} + */ +export function getCookies(strCookie = document.cookie) { + if (!strCookie) { + return {}; + } + const arrCookie = strCookie.split('; ');// 分割 + const cookiesObj = {}; + arrCookie.forEach((cookieStr) => { + const arr = cookieStr.split('='); + const [key, value] = arr; + if (key) { + cookiesObj[key] = value; + } + }); + return cookiesObj; +} diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/components/auth/index.vue" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/components/auth/index.vue" new file mode 100644 index 0000000000000000000000000000000000000000..2e0e8feb4bccba021c57592c9139c434ab8d67d0 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/components/auth/index.vue" @@ -0,0 +1,78 @@ + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/components/exception/index.vue" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/components/exception/index.vue" new file mode 100644 index 0000000000000000000000000000000000000000..cd5e6f6ecf5a729cc289a2d2ab07019c42fa2bd1 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/components/exception/index.vue" @@ -0,0 +1,85 @@ + + + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/components/scroll/index.css" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/components/scroll/index.css" new file mode 100644 index 0000000000000000000000000000000000000000..728e3d07370669443098b3774008e779c15bdfdc --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/components/scroll/index.css" @@ -0,0 +1,42 @@ +.list-warp { + display: inline-block; + min-width: 150px; + width: 100%; + } + .list-title { + background: wheat; + padding-left: 10px; + margin-bottom: 5px; + display: block; + } + .list-scroll { + overflow: hidden; + display: inline-block; + padding: 0px 10px; + width: 100%; + } + .hover { + overflow-y: auto; + } + .list-scroll .hide { + display: none; + } + + .item { + display: block; + width: 100%; + } + + .item li{ + line-height: 28px; + cursor: pointer; + font-size: 16px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + width: 100%; + } + + .item li:hover { + color: #3a84ff; + } \ No newline at end of file diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/components/scroll/index.vue" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/components/scroll/index.vue" new file mode 100644 index 0000000000000000000000000000000000000000..e0a468d87e82c24aa953c3954ed323fc35d6c853 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/components/scroll/index.vue" @@ -0,0 +1,129 @@ + + + + + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/app.css" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/app.css" new file mode 100644 index 0000000000000000000000000000000000000000..52d382a74488c0c38b3a19abf172beb2945b72cc --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/app.css" @@ -0,0 +1,46 @@ +@import './bk-patch.css'; + +@define-mixin is-left-mixin $needBgColor: true { + color: #63656E; + &:hover { + color: #3A84FF; + @if $needBgColor { + background: #F0F1F5; + } + } +}; + +@define-mixin popover-panel-mxin $width: 150px, $itemHoverColor: #3A84FF { + width: $width; + display: flex; + flex-direction: column; + background: #FFFFFF; + border: 1px solid #E2E2E2; + box-shadow: 0px 3px 4px 0px rgba(64,112,203,0.06); + padding: 6px 0; + margin: 0; + color: #63656E; + .nav-item { + flex: 0 0 32px; + display: flex; + align-items: center; + padding: 0 20px; + list-style: none; + &:hover { + color: $itemHoverColor; + cursor: pointer; + background-color: #F0F1F5; + } + } +} + +.full-width { + width: 100%; +} + +.center-content { + background-color: #fff; + display: flex; + justify-content: center; + align-items: center; +} diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/bk-patch.css" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/bk-patch.css" new file mode 100644 index 0000000000000000000000000000000000000000..c084cf585bb77e4e992ff20d7d37811b8fd1ee24 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/bk-patch.css" @@ -0,0 +1,15 @@ +.navigation-bar { + width: 100%; + height: 100%; +} +.navigation-bar-container { + width: 100%; + max-width: 100%; +} + +.bk-navigation { + min-width: 1360px; +} +.bk-navigation-wrapper .navigation-container { + max-width: 100% !important; +} diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/mixins/clearfix.css" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/mixins/clearfix.css" new file mode 100644 index 0000000000000000000000000000000000000000..68b275f2de221d0af094374dd14a0a06dd36e72c --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/mixins/clearfix.css" @@ -0,0 +1,10 @@ +@define-mixin clearfix { + &::after { + display: block; + clear: both; + content: ""; + font-size: 0; + height: 0; + visibility: hidden; + } +} diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/mixins/create-label.css" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/mixins/create-label.css" new file mode 100644 index 0000000000000000000000000000000000000000..01b9b5e321326f1be653f136f4b600fbde6651e1 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/mixins/create-label.css" @@ -0,0 +1,18 @@ +@define-mixin create-label $mainColor, $bgColor, $color, $fillColor { + & { + background-color: $bgColor; + border-color: $mainColor; + color: $color; + + &.is-fill { + background-color: $mainColor; + color: $fillColor; + } + + &.is-text { + border-color: #fff; + background-color: #fff; + color: $color; + } + } +} diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/mixins/ellipsis.css" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/mixins/ellipsis.css" new file mode 100644 index 0000000000000000000000000000000000000000..bb9e1d62eca94b703ebea29d8dbf80cc87e9b4ba --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/mixins/ellipsis.css" @@ -0,0 +1,7 @@ +@define-mixin ellipsis $maxWidth, $display: inline-block { + display: $display; + max-width: $maxWidth; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/mixins/scroller.css" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/mixins/scroller.css" new file mode 100644 index 0000000000000000000000000000000000000000..88a235cbc086f1f9e8522444aca4869f7f30b42a --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/mixins/scroller.css" @@ -0,0 +1,11 @@ +@define-mixin scroller $backgroundColor: #e6e9ea, $width: 4px { + &::-webkit-scrollbar { + width: $width; + background-color: lighten($backgroundColor, 80%); + } + &::-webkit-scrollbar-thumb { + height: 5px; + border-radius: 2px; + background-color: $backgroundColor; + } +} diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/reset.css" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/reset.css" new file mode 100644 index 0000000000000000000000000000000000000000..1776c6d4406a93e4513e8054bb5ff9fbf96a71d3 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/reset.css" @@ -0,0 +1,43 @@ +/* // 浏览器重置样式 */ + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; +} + +ul, +li { + margin: 0; + padding: 0; + list-style: none; +} + +dl, +dt, +dd, +p { + margin: 0; + padding: 0; +} + +a { + text-decoration: none; +} + +button { + outline: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} +td, +th { + padding: 0; +} diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/variable.css" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/variable.css" new file mode 100644 index 0000000000000000000000000000000000000000..28b43787419d75c6e431f984888ac54798e2edec --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/css/variable.css" @@ -0,0 +1,37 @@ +/* 存放常用变量 */ + +/* 边框色 */ +/* 相对较浅,常用于容器边框色 */ +$borderColor: #ebf0f5; +/* 相对较深,常用于输入框边框色 */ +$borderWeightColor: #dde4eb; + +/* 字体色 */ +$fontColor: #7b7d8a; +$fontWeightColor: #737987; + +/* 主体 icon 颜色 */ +$iconPrimaryColor: #3c96ff; +$primaryColor: #3c96ff; + +/* 成功 icon 颜色 */ +$iconSuccessColor: #34d97b; +$successColor: #34d97b; + +/* 失败 icon 颜色 */ +$iconFailColor: #ff5656; +$failColor: #ff5656; +$dangerColor: #ff5656; + +/* warning icon 颜色 */ +$iconWarningColor: #ffb400; +$warningColor: #ffb400; + +/* 淡色 */ +$primaryLightColor: #ebf4ff; + +/* 适配宽度 */ +$mediaWidth: 1500px; + +/* 背景 hover */ +$bgHoverColor: #fafbfd; diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/images/403.png" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/images/403.png" new file mode 100644 index 0000000000000000000000000000000000000000..858e46a2d986c8addea1c56a70fa6b0d59cad8b8 Binary files /dev/null and "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/images/403.png" differ diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/images/404.png" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/images/404.png" new file mode 100644 index 0000000000000000000000000000000000000000..87f162c0beda17e4504c4390e770ec2ed5bcb96b Binary files /dev/null and "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/images/404.png" differ diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/images/500.png" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/images/500.png" new file mode 100644 index 0000000000000000000000000000000000000000..17cda81b9e5183d5704d22bfd4b92c4913477eb6 Binary files /dev/null and "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/images/500.png" differ diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/images/building.png" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/images/building.png" new file mode 100644 index 0000000000000000000000000000000000000000..a29a367c80092aebc0add1c1f8185141269c220f Binary files /dev/null and "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/images/building.png" differ diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/main.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/main.js" new file mode 100644 index 0000000000000000000000000000000000000000..131d68215dabec355d6bd89190c795fecf8940a4 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/main.js" @@ -0,0 +1,68 @@ +/** + * @file main entry + * @author + */ +import './public-path'; +import Vue from 'vue'; + +import App from '@/App'; +import router from '@/router'; +import store from '@/store'; +import { injectCSRFTokenToHeaders } from '@/api'; +import auth from '@/common/auth'; +import Img403 from '@/images/403.png'; +import Exception from '@/components/exception'; +import { bus } from '@/common/bus'; +import AuthComponent from '@/components/auth'; +import '@/common/bkmagic'; +import mavonEditor from 'mavon-editor'; +import 'mavon-editor/dist/css/index.css'; +// use +Vue.use(mavonEditor); + +Vue.component('AppException', Exception); +Vue.component('AppAuth', AuthComponent); + +auth.requestCurrentUser().then((user) => { + injectCSRFTokenToHeaders(); + if (user.isAuthenticated) { + global.bus = bus; + global.mainComponent = new Vue({ + el: '#app', + router, + store, + components: { App }, + template: '', + }); + } else { + auth.redirectToLogin(); + } +}, (err) => { + let message; + if (err.status === 403) { + message = 'Sorry,您的权限不足!'; + if (err.data && err.data.msg) { + message = err.data.msg; + } + } else { + message = '无法连接到后端服务,请稍候再试。'; + } + + const divStyle = '' + + 'text-align: center;' + + 'width: 400px;' + + 'margin: auto;' + + 'position: absolute;' + + 'top: 50%;' + + 'left: 50%;' + + 'transform: translate(-50%, -50%);'; + + const h2Style = 'font-size: 20px;color: #979797; margin: 32px 0;font-weight: normal'; + + const content = '' + + `
` + + `

${message}

` + + '
'; + + document.write(content); +}); diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/public-path.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/public-path.js" new file mode 100644 index 0000000000000000000000000000000000000000..5d8d0d9c1c6c43faf0d020c513a8b7f66a815e8b --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/public-path.js" @@ -0,0 +1,7 @@ +/** + * @file webpack public path config + * @author + */ + +// eslint-disable-next-line +__webpack_public_path__ = `${window.PROJECT_CONFIG.BK_STATIC_URL}/`; diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/router/index.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/router/index.js" new file mode 100644 index 0000000000000000000000000000000000000000..051f3a3f93d5a2b2ead7cf7581f6650976efe5f8 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/router/index.js" @@ -0,0 +1,155 @@ +/** + * @file router 配置 + * @author + */ + +import Vue from 'vue'; +import VueRouter from 'vue-router'; + +import store from '@/store'; +import http from '@/api'; +import preload from '@/common/preload'; + +const MainEntry = () => import(/* webpackChunkName: 'entry' */'@/views'); +// import MainEntry from '@/views' +const Example1 = () => import(/* webpackChunkName: 'example1' */'@/views/example1'); +// import Example1 from '@/views/example1' +const Example2 = () => import(/* webpackChunkName: 'example2' */'@/views/example2'); +// import Example2 from '@/views/example2' +const NotFound = () => import(/* webpackChunkName: 'none' */'@/views/404'); +// import NotFound from '@/views/404' + +const Demo = () => import('../views/demo'); +const List = () => import('../views/demo/list'); +const Detail = () => import('../views/demo/detail'); +const VuexDemo = () => import('../views/vuex'); +const Custom = () => import('../views/custom'); +Vue.use(VueRouter); + +const routes = [ + { + path: '/', + name: 'appMain', + component: MainEntry, + alias: '', + children: [ + { + path: '/demo', + name: 'demo', + component: Demo, + meta: { + matchRoute: 'demo', + }, + children: [ + { + path: 'list', + name: 'list', + component: List, + }, + { + path: 'detail/:id?', + name: 'detail', + component: Detail, + beforeEnter: (to, from, next) => { + next(); + }, + }, + ], + }, + { + path: 'vuex', + alias: '', + name: 'vuex', + component: VuexDemo, + meta: { + matchRoute: 'vuex', + }, + }, + { + path: 'example1', + alias: '', + name: 'example1', + component: Example1, + meta: { + matchRoute: '首页', + }, + }, + { + path: 'custom', + alias: '', + name: 'custom', + component: Custom, + meta: { + matchRoute: 'custom', + }, + }, + { + path: 'example2', + name: 'example2', + component: Example2, + meta: { + matchRoute: '登陆信息', + }, + }, + ], + }, + // 404 + { + path: '*', + name: '404', + component: NotFound, + }, +]; + +const router = new VueRouter({ + mode: 'history', + routes, +}); + +const cancelRequest = async () => { + const allRequest = http.queue.get(); + const requestQueue = allRequest.filter(request => request.cancelWhenRouteChange); + await http.cancel(requestQueue.map(request => request.requestId)); +}; + +let preloading = true; +let canceling = true; +let pageMethodExecuting = true; + +router.beforeEach(async (to, from, next) => { + canceling = true; + await cancelRequest(); + canceling = false; + next(); +}); + +router.afterEach(async (to) => { + store.commit('setMainContentLoading', true); + + preloading = true; + await preload(); + preloading = false; + + const pageDataMethods = []; + const routerList = to.matched; + routerList.forEach((r) => { + Object.values(r.instances).forEach((vm) => { + if (typeof vm.fetchPageData === 'function') { + pageDataMethods.push(vm.fetchPageData()); + } + if (typeof vm.$options.preload === 'function') { + pageDataMethods.push(vm.$options.preload.call(vm)); + } + }); + }); + + pageMethodExecuting = true; + await Promise.all(pageDataMethods); + pageMethodExecuting = false; + + if (!preloading && !canceling && !pageMethodExecuting) { + store.commit('setMainContentLoading', false); + } +}); + +export default router; diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/store/index.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/store/index.js" new file mode 100644 index 0000000000000000000000000000000000000000..0086540a4911a1006db8ba092374efbbe362a31a --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/store/index.js" @@ -0,0 +1,86 @@ +/* eslint-disable no-undef */ +/** + * @file main store + * @author + */ + +import Vue from 'vue'; +import Vuex from 'vuex'; + +import example from './modules/example'; +import vuexDemo from './modules/vuexDemo'; +import bbs from './modules/bbs'; +import http from '@/api'; +// import { getCookies } from '../common/util'; +Vue.use(Vuex); + +const store = new Vuex.Store({ + // 模块 + modules: { + example, + vuexDemo, + bbs, + }, + // 公共 store + state: { + mainContentLoading: false, + // 系统当前登录用户 + user: {}, + }, + // 公共 getters + getters: { + mainContentLoading: state => state.mainContentLoading, + user: state => state.user, + }, + // 公共 mutations + mutations: { + /** + * 设置内容区的 loading 是否显示 + * + * @param {Object} state store state + * @param {boolean} loading 是否显示 loading + */ + setMainContentLoading(state, loading) { + state.mainContentLoading = loading; + }, + + /** + * 更新当前用户 user + * + * @param {Object} state store state + * @param {Object} user user 对象 + */ + updateUser(state, user) { + state.user = Object.assign({}, user); + }, + }, + actions: { + /** + * 获取用户信息 + * + * @param {Object} context store 上下文对象 { commit, state, dispatch } + * + * @return {Promise} promise 对象 + */ + userInfo(context) { + // ajax 地址为 USER_INFO_URL,如果需要 mock,那么只需要在 url 后加上 AJAX_MOCK_PARAM 的参数, + // 参数值为 mock/ajax 下的路径和文件名,然后加上 invoke 参数,参数值为 AJAX_MOCK_PARAM 参数指向的文件里的方法名 + // 例如本例子里,ajax 地址为 USER_INFO_URL,mock 地址为 USER_INFO_URL?AJAX_MOCK_PARAM=index&invoke=getUserInfo + + // 后端提供的地址 + // const url = USER_INFO_URL + // mock 的地址,示例先使用 mock 地址, 修改mock/ajax/index文件 + // const loginUrl = window.PROJECT_CONFIG.BK_LOGIN_URL || window.BK_LOGIN_URL; + // const cookies = getCookies(); + // const url = `${loginUrl}api/v3/is_login/?bk_token=${cookies.bk_token}`; + const url = USER_INFO_URL; + return http.get(url).then((response) => { + const userData = response.data || {}; + context.commit('updateUser', userData); + return userData; + }); + }, + }, +}); + +export default store; diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/store/modules/bbs.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/store/modules/bbs.js" new file mode 100644 index 0000000000000000000000000000000000000000..612fc8c542ae60fe66dc47d71dac6184e0843c48 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/store/modules/bbs.js" @@ -0,0 +1,26 @@ +import http from '@/api'; +export default { + namespaced: true, + state: { + bbsList: [], + }, + mutations: { + setBbsList(state, data) { + console.log(data); + state.bbsList = data.list; + }, + }, + actions: { + getBbsList(context) { + http.get('/bbs/data-source/user/tableName/bbs').then((res) => { + context.commit('setBbsList', res.data); + }); + }, + getDetail(context, detailId) { + return http.get(`/bbs/data-source/user/tableName/bbs/id/${detailId}`); + }, + createBBs(context, data) { + return http.post('/bbs/data-source/user/tableName/bbs', data); + }, + }, +}; diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/store/modules/example.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/store/modules/example.js" new file mode 100644 index 0000000000000000000000000000000000000000..daee31c5f23486b436ae91419d954485cbed4eed --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/store/modules/example.js" @@ -0,0 +1,21 @@ +/** + * @file app store + * @author + */ + +import http from '@/api'; +import queryString from 'query-string'; + +export default { + namespaced: true, + state: { + }, + mutations: { + }, + actions: { + getTableData(context, params, config = {}) { + // eslint-disable-next-line no-undef + return http.get(`/table?&${queryString.stringify(params)}`, params, config); + }, + }, +}; diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/store/modules/vuexDemo.js" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/store/modules/vuexDemo.js" new file mode 100644 index 0000000000000000000000000000000000000000..0fc710eef16ff90b002d1e9d8506c4d94ebe20fb --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/store/modules/vuexDemo.js" @@ -0,0 +1,22 @@ + +export default { + namespaced: true, + state: { + count: 0, + }, + actions: { + increment(context) { + context.commit('increment'); + }, + }, + getters: { + autoIncrement(state) { + return state.count * 10; + }, + }, + mutations: { + increment(state) { + state.count = state.count + 1; + }, + }, +}; diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/404.vue" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/404.vue" new file mode 100644 index 0000000000000000000000000000000000000000..8ff83e29711758ad8e26199841b3f0125644496a --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/404.vue" @@ -0,0 +1,31 @@ + + + + + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/custom/index.vue" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/custom/index.vue" new file mode 100644 index 0000000000000000000000000000000000000000..43be83d9c68820af5de05fc01c050a47ab411dbc --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/custom/index.vue" @@ -0,0 +1,35 @@ + + + + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/demo/detail.vue" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/demo/detail.vue" new file mode 100644 index 0000000000000000000000000000000000000000..ccab0a8c62f92cd125edf520277bb46fe2456cec --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/demo/detail.vue" @@ -0,0 +1,70 @@ + + + + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/demo/index.vue" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/demo/index.vue" new file mode 100644 index 0000000000000000000000000000000000000000..edb1b771352e312e70c26f5d38888089955855b6 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/demo/index.vue" @@ -0,0 +1,35 @@ + + + + + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/demo/list.vue" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/demo/list.vue" new file mode 100644 index 0000000000000000000000000000000000000000..a5fb9b80706a77ddd2b491875536aaa10fc8e663 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/demo/list.vue" @@ -0,0 +1,45 @@ + + + + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/example1/index.css" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/example1/index.css" new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/example1/index.vue" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/example1/index.vue" new file mode 100644 index 0000000000000000000000000000000000000000..9855b6daf1e9b42d0eb285cd0b7efb3aee0682bb --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/example1/index.vue" @@ -0,0 +1,98 @@ + + + + + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/example2/index.css" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/example2/index.css" new file mode 100644 index 0000000000000000000000000000000000000000..358b3e17ccc80c35dbccdd46e883a879708783ab --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/example2/index.css" @@ -0,0 +1,11 @@ +.avatar_url { + margin-top: 30px; + display: flex; + align-items: center; +} + +.avatar_url img { + display: inline-block; + margin-left: 10px; + border-radius: 100%; +} diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/example2/index.vue" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/example2/index.vue" new file mode 100644 index 0000000000000000000000000000000000000000..b2efbce905529f6a019a1f5266cce66cfa9488d3 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/example2/index.vue" @@ -0,0 +1,28 @@ + + + + + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/index.vue" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/index.vue" new file mode 100644 index 0000000000000000000000000000000000000000..69ade599292a4f0d9b661bc6b5caad374b3304c7 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/index.vue" @@ -0,0 +1,3 @@ + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/vuex/index.vue" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/vuex/index.vue" new file mode 100644 index 0000000000000000000000000000000000000000..0787d0debf7ae57bad48e01bc96756642e45b08f --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/vuex/index.vue" @@ -0,0 +1,36 @@ + + + + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/vuex/state.vue" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/vuex/state.vue" new file mode 100644 index 0000000000000000000000000000000000000000..86811c781fe6fae6bbde9ce01b7d1e8d949243b4 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/src/views/vuex/state.vue" @@ -0,0 +1,29 @@ + + + + diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/static/images/favicon.png" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/static/images/favicon.png" new file mode 100644 index 0000000000000000000000000000000000000000..4d4767b5671e21c6b8fc16cc9206a18a37404d50 Binary files /dev/null and "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/static/images/favicon.png" differ diff --git "a/11_\345\211\215\347\253\257\350\277\233\351\230\266/static/login_success.html" "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/static/login_success.html" new file mode 100644 index 0000000000000000000000000000000000000000..d74c717a1033cf7df58a116f91cb01122acd4644 --- /dev/null +++ "b/11_\345\211\215\347\253\257\350\277\233\351\230\266/static/login_success.html" @@ -0,0 +1,14 @@ + + + + + + 登陆成功 + + + + + diff --git a/README.md b/README.md index 50dc2495f561639a057ad9b2963743287c111407..610d96ad9ef872204ff2de9161adcd7282b66fa8 100644 --- a/README.md +++ b/README.md @@ -58,3 +58,9 @@ [将原有代码重新部署至社区版7.0](./9_将原有代码重新部署至社区版7.0/如何将原有代码重新部署至社区版7.0.md) + + +## 11. 前端进阶 + +[前端进阶](./11_前端进阶/README.md) +