├── .gitignore ├── doc ├── theme │ ├── face │ │ └── components │ │ │ └── icons │ │ │ ├── font │ │ │ ├── icon.eot │ │ │ ├── icon.ttf │ │ │ └── icon.woff │ │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ │ │ └── index.html │ ├── template │ │ ├── post.ejs │ │ ├── home.ejs │ │ └── default.ejs │ ├── head.html │ ├── media │ │ ├── index.html │ │ ├── logo.svg │ │ ├── filter.svg │ │ ├── user.svg │ │ ├── browser-5.svg │ │ ├── keyword.svg │ │ ├── smartphone.svg │ │ ├── loupe.svg │ │ ├── online-shop.svg │ │ ├── browser-4.svg │ │ ├── bar-chart.svg │ │ ├── online-shop-1.svg │ │ ├── mailing.svg │ │ ├── analytics-1.svg │ │ ├── promotion.svg │ │ ├── browser-3.svg │ │ ├── line-chart.svg │ │ ├── browsers-1.svg │ │ ├── search.svg │ │ ├── growth.svg │ │ ├── rating.svg │ │ ├── share.svg │ │ ├── update.svg │ │ ├── aim.svg │ │ ├── browser-1.svg │ │ ├── browser.svg │ │ ├── stats.svg │ │ ├── optimization.svg │ │ ├── laptop.svg │ │ ├── speedometer.svg │ │ ├── ads.svg │ │ ├── blog.svg │ │ ├── browsers-2.svg │ │ ├── activity.svg │ │ └── browser-2.svg │ ├── foot.html │ └── polyfill │ │ └── es6-promise.auto.js ├── intro.md ├── README.md ├── sidebar.html ├── jquery.demo.js └── API.md ├── compile ├── open.js ├── webpack.online.version.config.js ├── getConfig.js ├── webpack.karma.config.js ├── livereload.js ├── deploy-gh.js ├── server.js ├── webpack.online.config.js ├── karma.ci.js ├── karma.conf.js └── webpack.config.js ├── test └── basic.test.js ├── developers-to-read.md ├── lib └── index.js ├── README.md ├── compile.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | output 3 | yarn.lock 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /doc/theme/face/components/icons/font/icon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onface/page/HEAD/doc/theme/face/components/icons/font/icon.eot -------------------------------------------------------------------------------- /doc/theme/face/components/icons/font/icon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onface/page/HEAD/doc/theme/face/components/icons/font/icon.ttf -------------------------------------------------------------------------------- /doc/theme/face/components/icons/font/icon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onface/page/HEAD/doc/theme/face/components/icons/font/icon.woff -------------------------------------------------------------------------------- /doc/theme/face/components/icons/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onface/page/HEAD/doc/theme/face/components/icons/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /doc/theme/face/components/icons/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onface/page/HEAD/doc/theme/face/components/icons/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /doc/theme/face/components/icons/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onface/page/HEAD/doc/theme/face/components/icons/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /doc/theme/face/components/icons/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onface/page/HEAD/doc/theme/face/components/icons/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /doc/theme/face/components/icons/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onface/page/HEAD/doc/theme/face/components/icons/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /compile/open.js: -------------------------------------------------------------------------------- 1 | var open = require("open") 2 | var config = require('./getConfig')() 3 | setTimeout(function () { 4 | open("http://127.0.0.1:" + config.serverPort + '?若不显示则刷新页面') 5 | }, 500) 6 | -------------------------------------------------------------------------------- /compile/webpack.online.version.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var iPackage = require('../package.json') 3 | var webpackConfig = require('./webpack.online.config.js') 4 | webpackConfig.output.path = path.join(__dirname, '../output', iPackage.version) 5 | module.exports = webpackConfig 6 | -------------------------------------------------------------------------------- /compile/getConfig.js: -------------------------------------------------------------------------------- 1 | var hashPort = require('hash-to-port') 2 | var iPackgae = require('../package.json') 3 | module.exports = function () { 4 | return { 5 | serverPort: hashPort('server' + iPackgae.name), 6 | livereloadServerPort: hashPort('livereloadServer' + iPackgae.name) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /compile/webpack.karma.config.js: -------------------------------------------------------------------------------- 1 | var webpackConfig = require('./webpack.config.js') 2 | webpackConfig = { 3 | module: webpackConfig.module, 4 | devtool: '#inline-source-map', 5 | externals: { 6 | 'react/lib/ExecutionEnvironment': true, 7 | 'react/lib/ReactContext': true 8 | } 9 | } 10 | module.exports = webpackConfig 11 | -------------------------------------------------------------------------------- /compile/livereload.js: -------------------------------------------------------------------------------- 1 | var livereload = require('livereload') 2 | var config = require('./getConfig')() 3 | var path = require('path') 4 | var lrserver = livereload.createServer({ 5 | port: config.livereloadServerPort, 6 | delay: 100, 7 | exclusions: [/\\.git\//, /\\.svn\//, /\\.hg\//, /\.js/] 8 | }) 9 | var watchPath = path.join(__dirname, '../output') 10 | lrserver.watch(watchPath) 11 | console.log('Livereload: http://127.0.0.1:' + config.livereloadServerPort + '\n\t' + watchPath) 12 | -------------------------------------------------------------------------------- /compile/deploy-gh.js: -------------------------------------------------------------------------------- 1 | var ghpages = require('gh-pages') 2 | var path = require('path') 3 | var fs = require('fs') 4 | ghpages.publish(path.join(__dirname, '../output'), { 5 | message: 'onface/module auto-generated commit', 6 | logger: function(message) { 7 | console.log(message) 8 | }, 9 | add: true 10 | }, function(err) { 11 | console.log(err) 12 | if (err) {throw err} 13 | console.log('push success') 14 | }); 15 | console.log('git branch gh-pages pushing...') 16 | -------------------------------------------------------------------------------- /compile/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var compiler = webpack(require('./webpack.config.js')) 3 | var express = require('express') 4 | var path = require('path') 5 | var app = express(); 6 | require('./livereload.js') 7 | app.use(express.static(path.join(__dirname, '../output'))) 8 | app.use(require("webpack-dev-middleware")(compiler, { 9 | publicPath: '/', 10 | })); 11 | app.use(require("webpack-hot-middleware")(compiler)) 12 | var config = require('./getConfig')() 13 | app.listen(config.serverPort) 14 | -------------------------------------------------------------------------------- /test/basic.test.js: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | Enzyme.configure({ adapter: new Adapter() }); 4 | import { expect } from 'chai'; 5 | import { mount } from 'enzyme'; 6 | 7 | import Button from "../lib/index" 8 | import React, { Component } from "react" 9 | 10 | describe('', () => { 11 | it('has className', () => { 12 | expect( 13 | mount().find('.face-btn').length 14 | ).to.equal(1) 15 | }); 16 | }) 17 | -------------------------------------------------------------------------------- /developers-to-read.md: -------------------------------------------------------------------------------- 1 | ## Automated Builds 2 | 3 | You can use travis & saucelabs test your code. 4 | 5 | ### Travis environment Variables 6 | 7 | [travis](https://travis-ci.org/) 8 | 9 | ![](https://cloud.githubusercontent.com/assets/3949015/23390555/dafcf4ee-fda9-11e6-931a-8f4de5d0973b.png) 10 | 11 | ## SAUCE_USERNAME & SAUCE_ACCESS_KEY 12 | 13 | [Saucelabs signup OSS](https://saucelabs.com/beta/signup/OSS/None) 14 | 15 | ![](https://cloud.githubusercontent.com/assets/3949015/23390554/daf8a9e8-fda9-11e6-9de8-a796e2a89226.png) 16 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # 指引 2 | 3 | > 使用一个组件或模块之前,应当知道它能解决的问题是什么,不能解决的问题是什么。使用的最佳实践是什么? 4 | 5 | 6 | 项目中最常见的需求就是AJAX列表,不同项目的列表千变万化。很难找到两个项目的列表的界面和操作方式相同。 7 | 8 | 通过分析列表的的业务需求和代码逻辑,`face-page` 封装出列表的生命周期和配置参数 。让开发人员可以快速的基于 `face-page` 开发出一个AJAX列表。 9 | 10 | 你只需要编写列表的视觉代码和配置生命周期,所有的逻辑细节都会由 `face-page` 处理。 11 | 12 | > 可能你会想直接对着设计稿把AJAX列表给的业务逻辑代码写出来就好了,非常简单。 13 | 14 | > 但是请你考虑: 一个项目可能会有几十个列表,每个列表的界面和功能都不完全是一样的。 15 | 16 | > 唯一相同的只是他们会展示数据,用户进行操作后数据会变动。 并且还可能需要加入一些诸如“清空搜索结果” “搜索结果为空的” 的复杂的功能。 17 | 18 | > 直接编码还要解决[数据片段条件问题](./doc/README.md#数据片段条件) 19 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # 示例 2 | 3 | ## jQuery 4 | 5 | ````html 6 | 10 | 11 |
12 |
13 | ```` 14 | 15 | ````code 16 | { 17 | title: 'jQuery', 18 | desc: '生命周期: `getQuery` `willFetch` `fetch` `willFetch` `render` 触发:`query`', 19 | html: '', 20 | js: './jquery.demo.js', 21 | source: './jquery.demo.js', 22 | open: true 23 | } 24 | ```` 25 | -------------------------------------------------------------------------------- /doc/theme/template/post.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%- title %> 11 | 12 | 13 | 14 |
15 | <%- content %> 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /compile/webpack.online.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var iPackage = require('../package.json') 3 | var webpack = require('webpack') 4 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 5 | var webpackConfig = require('./webpack.config.js') 6 | Object.keys(webpackConfig.entry).forEach(function (key, index) { 7 | webpackConfig.entry[key] = webpackConfig.entry[key].filter(function (item) { 8 | return item !== 'webpack-hot-middleware/client' 9 | }) 10 | }) 11 | 12 | webpackConfig.plugins = [ 13 | new webpack.NamedModulesPlugin(), 14 | new webpack.NoEmitOnErrorsPlugin(), 15 | new webpack.DefinePlugin({ 16 | 'process.env': { 17 | NODE_ENV: JSON.stringify('production') 18 | } 19 | }) 20 | ] 21 | webpackConfig.output.publicPath = '/' + iPackage.$repository + '/' + iPackage.version 22 | module.exports = webpackConfig 23 | -------------------------------------------------------------------------------- /doc/theme/template/home.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%- title %> 11 | 12 | 13 | 14 | 15 | <%- content %> 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /doc/sidebar.html: -------------------------------------------------------------------------------- 1 |
2 | 指引 3 |
4 | 7 |
8 | 9 | 10 | 文档 11 | 12 |
13 |
示例
14 | jQuery 15 | React 16 |
17 |
18 |
API
19 | API 20 |
21 |
22 | -------------------------------------------------------------------------------- /doc/theme/head.html: -------------------------------------------------------------------------------- 1 |
2 | 6 | 7 | Write less code, do more things! 8 | 9 | 13 |
14 | 15 |
16 | 首页 17 | 指引 18 | 示例 19 | API 20 |
21 | 31 |
32 |
33 | -------------------------------------------------------------------------------- /doc/theme/template/default.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%- title %> 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 | 19 |
20 | 21 |
22 |
23 |
24 |
25 |
26 |
27 | <%- content %> 28 |
29 |
30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import spare from "sparejs" 2 | import extend from "extend" 3 | class Page { 4 | constructor(options) { 5 | const self = this 6 | self.options = options 7 | } 8 | } 9 | const _query = function (data, callback) { 10 | const self = this 11 | if (typeof callback !== 'function') { 12 | callback = () => {} 13 | } 14 | data = spare(data, {}) 15 | let queryData = self.options.getQuery() 16 | let fetchQuery = extend(true, {}, queryData, data) 17 | let rendered = false 18 | new Promise(self.options.willFetch).then(function () { 19 | self.options.fetch( 20 | fetchQuery, 21 | function render(...arg) { 22 | rendered = true 23 | self.options.render.apply(undefined , arg) 24 | }, 25 | function didFetch() { 26 | // 给 rendered 留出时间赋值 27 | setTimeout(function () { 28 | self.options.didFetch() 29 | callback({end: 'didFetch', rendered}) 30 | }, 0) 31 | } 32 | ) 33 | }).catch(function () { 34 | callback({ 35 | end: 'willFetch', 36 | rendered 37 | }) 38 | }) 39 | 40 | } 41 | 42 | Page.prototype.query = function (data, callback) { 43 | const self = this 44 | // 增加延迟是因为 react 框架的 setState 是异步的。 45 | // 有时会出现 setState 已经调用但是 getQuery 获取的值还是老的 46 | setTimeout(function () { 47 | _query.bind(self)(data, callback) 48 | }, 0) 49 | } 50 | export default Page 51 | module.exports = Page 52 | -------------------------------------------------------------------------------- /compile/karma.ci.js: -------------------------------------------------------------------------------- 1 | var karmaConf = require('./karma.conf.js') 2 | var iPackage = require('../package.json') 3 | var userConfig = require('../compile.js') 4 | var extend = require('extend') 5 | module.exports = function(config) { 6 | if (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) { 7 | var sauceUserErrorMsg = 'Make sure the SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are set.' 8 | console.error('---------------') 9 | console.error(sauceUserErrorMsg) 10 | console.error('---------------') 11 | throw new Error(sauceUserErrorMsg) 12 | } 13 | var sauceLabsConfig = { 14 | frameworks: ['jasmine'], 15 | reporters: ['progress', 'saucelabs'], 16 | sauceLabs: { 17 | testName: iPackage.name + ' unit tests', 18 | tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER, 19 | recordVideo: false, 20 | testName: iPackage.name, 21 | recordScreenshots: false, 22 | connectOptions: { 23 | 'no-ssl-bump-domains': 'all', // Ignore SSL error on Android emulator 24 | port: 5757, 25 | logfile: 'sauce_connect.log' 26 | }, 27 | public: 'public', 28 | build: process.env.CIRCLE_BUILD_NUM || process.env.SAUCE_BUILD_ID || Date.now() 29 | }, 30 | captureTimeout: (1000*60)*5, 31 | browserNoActivityTimeout: (1000*60)*5, 32 | customLaunchers: userConfig.test.launchers, 33 | browsers: Object.keys(userConfig.test.launchers), 34 | singleRun: true 35 | } 36 | config.plugins.push('karma-sauce-launcher') 37 | config.set(extend(true, {}, karmaConf, sauceLabsConfig)) 38 | } 39 | -------------------------------------------------------------------------------- /compile/karma.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var iPackage = require('../package.json') 3 | var hashToPort = require('hash-to-port') 4 | var compileConfig = require('../compile.js') 5 | var testServerPort = hashToPort(iPackage.name + 'onface/test:server') 6 | var webpackConfig = require('./webpack.karma.config') 7 | var karmaConf = function () { 8 | return { 9 | basePath: '../', 10 | frameworks: ['jasmine'], 11 | files: [ 12 | require.resolve('../doc/theme/polyfill/lt-ie10.js'), 13 | require.resolve('../doc/theme/polyfill/lte-ie11.js'), 14 | require.resolve('../doc/theme/polyfill/es6-promise.auto.js'), 15 | ].concat(compileConfig.test.files), 16 | preprocessors: { 17 | // add webpack as preprocessor 18 | 'lib/**/*.js': ['webpack', 'sourcemap'], 19 | 'lib/**/*.vue': ['webpack', 'sourcemap'], 20 | 'lib/**/*.css': ['webpack', 'sourcemap'], 21 | 'test/**/*.js': ['webpack', 'sourcemap'] 22 | }, 23 | webpack: webpackConfig, 24 | webpackServer: { 25 | noInfo: true 26 | }, 27 | plugins: [ 28 | 'karma-webpack', 29 | 'karma-jasmine', 30 | 'karma-sourcemap-loader', 31 | 'karma-chrome-launcher' 32 | ], 33 | babelPreprocessor: { 34 | options: compileConfig.babel 35 | }, 36 | reporters: ['progress'], 37 | port: testServerPort, 38 | colors: true, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | processKillTimeout: 1200000 43 | } 44 | } 45 | module.exports = function(config) { 46 | config.set(karmaConf(config)); 47 | }; 48 | -------------------------------------------------------------------------------- /doc/theme/media/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 18 | 19 | 20 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /doc/theme/media/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 31 | 32 | -------------------------------------------------------------------------------- /doc/theme/media/filter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # face-page 2 | 3 | 4 | 5 | 8 | 9 |
10 |
face-page
11 |

12 | 封装 搜索/列表/分页 逻辑代码 13 |

14 |
15 | 在线文档 16 | 20 |
21 |
22 |
23 | 24 | 25 | 33 | 41 | 49 | 50 |
26 |
27 | 28 |
29 |
定制开发
30 |
提供底层接口便于二次开发
31 |
32 |
34 |
35 | 36 |
37 |
场景丰富
38 |
涵盖绝大多数业务场景,提供业务样例代码。
39 |
40 |
42 |
43 | 44 |
45 |
逻辑封装
46 |
提供业务逻辑封装,快速开发
47 |
48 |
51 |
52 | 53 |
54 | Creator 55 |
56 |
57 | 58 | 59 | 66 | 67 |
60 | 61 | 62 |
63 |
NimoChu
64 |
65 |
68 |
69 | -------------------------------------------------------------------------------- /doc/theme/media/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /compile/webpack.config.js: -------------------------------------------------------------------------------- 1 | var glob = require("glob") 2 | var webpack = require('webpack') 3 | var path = require('path') 4 | var compileConfig = require('../compile') 5 | var entryMap = {} 6 | var entryFiles = glob.sync('**/**demo.js') 7 | var iPackage = require('../package.json') 8 | entryFiles.forEach(function (filePath) { 9 | if (/^(output|node_modules)/.test(filePath)) { return } 10 | var name = filePath 11 | entryMap[name] = [ 12 | './' + filePath, 13 | 'webpack-hot-middleware/client' 14 | ] 15 | }) 16 | module.exports = { 17 | entry: entryMap, 18 | output: { 19 | publicPath: '/', 20 | path: path.join(__dirname, '../output'), 21 | filename: '[name]', 22 | chunkFilename: '__chunk/[id]-[hash]-chunk.js' 23 | }, 24 | resolve: { 25 | alias: { 26 | 'vue': 'vue/dist/vue.js' 27 | } 28 | }, 29 | devServer: { 30 | hot: true 31 | }, 32 | plugins: [ 33 | new webpack.HotModuleReplacementPlugin(), 34 | new webpack.NamedModulesPlugin(), 35 | new webpack.NoEmitOnErrorsPlugin(), 36 | ], 37 | module: { 38 | rules: [ 39 | { 40 | test: /.(less|css)$/, 41 | use: [ 42 | { 43 | loader: 'style-loader' 44 | }, 45 | { 46 | loader: 'css-loader' 47 | }, 48 | { 49 | loader: 'less-loader' 50 | } 51 | ] 52 | }, 53 | { 54 | test: /\.vue$/, 55 | exclude: /node_modules/, 56 | use: [ 57 | 'vue-loader' 58 | ] 59 | }, 60 | { 61 | test: /\.js$/, 62 | exclude: /node_modules/, 63 | use: [ 64 | { 65 | loader: 'babel-loader', 66 | options: compileConfig.babel 67 | } 68 | ] 69 | }, 70 | { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=1&minetype=application/font-woff&name=__media/[path][name]-[hash:6].[ext]' }, 71 | { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=1&minetype=application/font-woff&name=__media/[path][name]-[hash:6].[ext]' }, 72 | { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=1&minetype=application/octet-stream&name=__media/[path][name]-[hash:6].[ext]' }, 73 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader' }, 74 | { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=1&minetype=image/svg+xml&name=__media/[path][name]-[hash:6].[ext]' }, 75 | { test: /\.(png|jpg|jpeg|gif)(\?v=\d+\.\d+\.\d+)?$/i, loader: 'url-loader?limit=1&name=__media/[path][name]-[hash:6].[ext]'}, 76 | { test: /\.json$/, loader: 'json-loader' } 77 | ] 78 | }, 79 | resolve: { 80 | alias: (function () { 81 | var alias = compileConfig.alias 82 | alias[iPackage.name] = path.resolve(__dirname, '../') 83 | return alias 84 | })() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /doc/jquery.demo.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery') 2 | var Page = require('face-page') 3 | var message = require('face-message') 4 | window.list = null 5 | $(function () { 6 | var $search = $('#search') 7 | var $list = $('#list') 8 | var $page = $('#page') 9 | var $loading = $('#loading') 10 | var page = new Page({ 11 | getQuery: function () { 12 | var serializeArray = $search.serializeArray() 13 | var data = {} 14 | serializeArray.forEach(function (item) { 15 | data[item.name] = item.value 16 | }) 17 | return data 18 | }, 19 | willFetch: function (resolve, reject) { 20 | if ($loading.is(":visible")) { 21 | message.info('防止重复提交') 22 | reject() 23 | return 24 | } 25 | $loading.show() 26 | resolve() 27 | }, 28 | didFetch: function () { 29 | $loading.hide() 30 | }, 31 | fetch: function (queryData, render, didFetch) { 32 | queryData.page = queryData.page || 1 33 | $.ajax({ 34 | url: 'http://echo.onface.live/onface/echo/mock/list?$delay=300', 35 | type: 'get', 36 | data: queryData, 37 | dataType: 'json' 38 | }).done(function (res) { 39 | render(res, queryData) 40 | }).fail(function () { 41 | message.error('网络出错,请刷新重试') 42 | }) 43 | .always(function () { 44 | didFetch() 45 | }) 46 | }, 47 | render: function (res, queryData) { 48 | // render list 49 | var html = res.data.map(function (item) { 50 | return '¥' + item.cash + '-' + item.name + '' 51 | }) 52 | if (res.data.length === 0) { 53 | html = '暂无数据 ' 54 | } 55 | $list.html(html) 56 | 57 | // render paging 58 | var paging = [] 59 | res.page = Number(res.page) 60 | if (res.pageCount != 1) { 61 | if (res.page != 1) { 62 | paging.push('') 63 | } 64 | paging.push(res.page + '/' + res.pageCount) 65 | if (res.page != res.pageCount) { 66 | paging.push('') 67 | } 68 | } 69 | $page.html(paging.join('')) 70 | 71 | } 72 | }) 73 | $('#search').on('submit', function (e) { 74 | e.preventDefault() 75 | page.query({}, function (info) { 76 | console.log('query callback', info) 77 | }) 78 | }) 79 | $('body').on('click', '#clearSearch', function () { 80 | $('#search').get(0).reset() 81 | page.query() 82 | }) 83 | $('#page').on('click', 'button', function (e) { 84 | page.query({ 85 | page: $(this).data('page') 86 | }) 87 | }) 88 | page.query() 89 | }) 90 | -------------------------------------------------------------------------------- /doc/theme/media/browser-5.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/keyword.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/smartphone.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/loupe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/online-shop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /compile.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | module.exports = { 3 | less: { 4 | plugins: [] 5 | }, 6 | babel: { 7 | presets: [ 8 | "es2015" 9 | ], 10 | plugins: [ 11 | [ 12 | "transform-react-jsx", 13 | {"pragma": "require(\"react\").createElement"} 14 | ], 15 | "transform-flow-strip-types", 16 | "syntax-flow", 17 | "syntax-jsx", 18 | "transform-react-display-name", 19 | "transform-decorators-legacy", 20 | "transform-class-properties" 21 | ] 22 | }, 23 | test: { 24 | files: [ 25 | (function () { 26 | if (process.env.files) { 27 | return process.env.files 28 | } 29 | else { 30 | return 'test/**/*.test.js' 31 | } 32 | })() 33 | ], 34 | launchers: { 35 | // ie family 36 | sl_ie_9: { 37 | base: 'SauceLabs', 38 | browserName: 'internet explorer', 39 | platform: 'Windows 7', 40 | version: '9' 41 | }, 42 | sl_ie_10: { 43 | base: 'SauceLabs', 44 | browserName: 'internet explorer', 45 | platform: 'Windows 8', 46 | version: '10' 47 | }, 48 | sl_ie_11: { 49 | base: 'SauceLabs', 50 | browserName: 'internet explorer', 51 | platform: 'Windows 8.1', 52 | version: '11' 53 | }, 54 | sl_edge: { 55 | base: 'SauceLabs', 56 | browserName: 'MicrosoftEdge', 57 | platform: 'Windows 10' 58 | }, 59 | // the cool kids 60 | sl_chrome: { 61 | base: 'SauceLabs', 62 | browserName: 'chrome', 63 | platform: 'Windows 7' 64 | }, 65 | sl_firefox: { 66 | base: 'SauceLabs', 67 | browserName: 'firefox', 68 | platform: 'Windows 7' 69 | }, 70 | sl_mac_safari: { 71 | base: 'SauceLabs', 72 | browserName: 'safari', 73 | platform: 'Windows 7' 74 | }, 75 | sl_mac_safari: { 76 | base: 'SauceLabs', 77 | browserName: 'safari', 78 | platform: 'OS X 10.10' 79 | }, // mobile 80 | sl_ios_safari_8: { 81 | base: 'SauceLabs', 82 | browserName: 'iphone', 83 | version: '8.4' 84 | }, 85 | sl_ios_safari_9: { 86 | base: 'SauceLabs', 87 | browserName: 'iphone', 88 | version: '9.3' 89 | }, 90 | sl_android_4_4: { 91 | base: 'SauceLabs', 92 | browserName: 'android', 93 | version: '4.4' 94 | }, 95 | sl_android_5_1: { 96 | base: 'SauceLabs', 97 | browserName: 'android', 98 | version: '5.1' 99 | } 100 | } 101 | }, 102 | fis: function (fis) { 103 | 104 | }, 105 | alias: { 106 | 'vue': 'vue/dist/vue.js', 107 | [path.resolve('./lib/index.css')]: path.resolve('./lib/index.less') 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /doc/theme/media/browser-4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "face-page", 3 | "main": "lib/index.js", 4 | "version": "0.1.0", 5 | "description": "Encapsulate list logic code | 封装列表逻辑代码", 6 | "keywords": "", 7 | "scripts": { 8 | "dev": "rm -rf output && fis3 release -w -d ./output -r ./ -f compile/fis-conf.js", 9 | "s": "node ./compile/open && node ./compile/server", 10 | "build:1": "rm -rf ./output && fis3 release build -d ./output -r ./ -f compile/fis-conf.js && fis3 release buildversion -d ./output -r ./ -f compile/fis-conf.js", 11 | "build:2": "webpack --progress --config ./compile/webpack.online.config.js && webpack --progress --config ./compile/webpack.online.version.config.js", 12 | "npm": "rm -rf ./output && fis3 release npm -d ./output -r ./ -f compile/fis-conf.js", 13 | "doc": "npm run build:1 && npm run build:2", 14 | "dp:gh": "node ./compile/deploy-gh", 15 | "test": "./node_modules/karma/bin/karma start ./compile/karma.conf.js --browsers Chrome", 16 | "test:ci": "./node_modules/karma/bin/karma start ./compile/karma.ci.js" 17 | }, 18 | "$username": "onface", 19 | "$repository": "page", 20 | "dependencies": { 21 | "calling": "^0.3.1", 22 | "extend": "^3.0.1", 23 | "jquery": "^3.3.1", 24 | "safe-extend": "^3.0.4", 25 | "sparejs": "^0.4.0" 26 | }, 27 | "devDependencies": { 28 | "axios": "^0.18.0", 29 | "babel": "^6.23.0", 30 | "babel-core": "^6.26.0", 31 | "babel-loader": "^7.1.2", 32 | "babel-plugin-syntax-flow": "^6.18.0", 33 | "babel-plugin-syntax-jsx": "^6.18.0", 34 | "babel-plugin-transform-class-properties": "^6.24.1", 35 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 36 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 37 | "babel-plugin-transform-react-display-name": "^6.25.0", 38 | "babel-plugin-transform-react-jsx": "^6.24.1", 39 | "babel-plugin-transform-react-jsx-source": "^6.22.0", 40 | "babel-polyfill": "^6.26.0", 41 | "babel-preset-es2015": "^6.24.1", 42 | "button.react": "^0.3.6", 43 | "chai": "^4.1.2", 44 | "css-loader": "^0.26.1", 45 | "ejs": "^2.5.7", 46 | "enzyme": "^3.3.0", 47 | "enzyme-adapter-react-16": "^1.1.1", 48 | "express": "^4.16.2", 49 | "face-message": "^0.1.1", 50 | "file-loader": "^1.1.6", 51 | "fis-parser-less-2.x": "^0.1.4", 52 | "fis3-hook-relative": "^2.0.1", 53 | "fis3-parser-vue-component": "^5.5.1", 54 | "gh-pages": "^1.1.0", 55 | "glob": "^7.1.2", 56 | "hash-to-port": "^1.0.0", 57 | "is-absolute-url": "^2.1.0", 58 | "jasmine-core": "^2.8.0", 59 | "json-loader": "^0.5.7", 60 | "json5": "^0.5.1", 61 | "karma": "^2.0.0", 62 | "karma-babel-preprocessor": "^7.0.0", 63 | "karma-chrome-launcher": "^2.2.0", 64 | "karma-jasmine": "^1.1.1", 65 | "karma-sauce-launcher": "^1.2.0", 66 | "karma-sourcemap-loader": "^0.3.7", 67 | "karma-webpack": "^2.0.9", 68 | "less": "^2.7.3", 69 | "less-loader": "^2.2.3", 70 | "less-plugin-autoprefix": "^1.5.1", 71 | "less-plugin-functions": "^1.0.0", 72 | "livereload": "^0.6.3", 73 | "markrun": "^0.23.0", 74 | "open": "^0.0.5", 75 | "query": "^0.2.0", 76 | "react": "^16.2.0", 77 | "react-dom": "^16.2.0", 78 | "react-hot-loader": "^4.0.0-beta.6", 79 | "style-loader": "^0.13.1", 80 | "table.react": "^0.3.2", 81 | "url-loader": "^0.6.2", 82 | "vue": "^2.5.13", 83 | "vue-loader": "^13.6.1", 84 | "vue-template-compiler": "^2.5.13", 85 | "webpack": "^3.10.0", 86 | "webpack-dev-middleware": "^2.0.3", 87 | "webpack-hot-middleware": "^2.21.0" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /doc/theme/face/components/icons/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 32 | 33 | 34 |
35 | 图标来源 ant-design http://iconfont.cn/collections/detail?spm=a313x.7781069.1998910419.d9df05512&cid=790 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 |
 44 |     <span class="fi fi-${name}" ></span>
 45 |     <span class="fi fi-${name}-${status}" ></span>
 46 | 
 47 |     <span class="fi fi-up ></span>
 48 |     <span class="fi fi-up-o ></span>
 49 |     <span class="fi fi-up-f ></span>
 50 |     <span class="fi fi-up-of ></span>
 51 |     <span class="fi fi-up-s ></span>
 52 |     <span class="fi fi-up-sf ></span>
 53 | 
54 | 55 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /doc/theme/media/bar-chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/online-shop-1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/foot.html: -------------------------------------------------------------------------------- 1 | 43 |
44 |
45 |
46 |
47 | 48 | Github 49 |
50 |
51 | <%= PACKAGE.$repository; %> 52 | - 项目源码 53 |
54 |
55 |
56 |
57 | 58 | 相关站点 59 |
60 |
61 | component-spec 62 | - 组件规范 63 |
64 |
65 | module 66 | - 开源项目脚手架 67 |
68 |
69 | onface.cc 70 | - 资源集合 71 |
72 |
73 |
74 |
75 | 76 | 社区 77 |
78 |
79 | Github issues 80 |
81 |
82 | 提交bug 83 |
84 |
85 | 90 |
91 |
92 | -------------------------------------------------------------------------------- /doc/theme/media/mailing.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/analytics-1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/promotion.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/browser-3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/line-chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/browsers-1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## new Page() 4 | 5 | ```js 6 | var Page = require('face-page').default 7 | // import Page from "lsit-logic" 8 | var list = new Page(options) 9 | ``` 10 | 11 | ## 生命周期 12 | 13 | > options 主要配置用于列表的生命周期触发时所需要获取的一些数据,因为不同列表获取数据的方式是不同的。 14 | 15 | ``` 16 | list.query() 17 | 1. getQuery 18 | 3. willFetch 19 | 4. fetch 20 | 5. render & didFetch 21 | ``` 22 | 23 | ### getQuery 24 | 25 | ```js 26 | new Page({ 27 | // 获取查询条件 28 | getQuery: function () { 29 | return { 30 | search_keyword: $('#search_keyword').val() 31 | } 32 | } 33 | }) 34 | ``` 35 | 36 | ### willFetch 37 | 38 | 即将获取数据时触发执行,可通过 `next()` 终止后续操作。 39 | 40 | 建议用来防止重复搜索和控制 loading 状态。 41 | 42 | > list.query() 调用后会执行 willFetch 若调用 next 则执行 fetch。 43 | > 若要防止重复搜索,不调用 next 则不会执行 fetch 44 | 45 | ```js 46 | new Page({ 47 | willFetch: function (next) { 48 | if ($('#tip').hasClass('loading')) { 49 | return 50 | } 51 | $('#tip').addClass('loading') 52 | next() 53 | } 54 | }) 55 | ``` 56 | 57 | ### fetch 58 | 59 | 获取数据时触发执行,将 `query` 作为参数查询数据。获取到数据后调用 `didFetch` 进入 `didFetch` 阶段。数据获取成功调用 `render` 调用 `options.render`。(有时服务器会拒绝用户访问数据,比如登录过期了。) 60 | 61 | > options.willFetch 中调用 `next()` 后执行 62 | 63 | ```js 64 | new Page({ 65 | fetch: function (queryData, render, didFetch) { 66 | $.ajax({ 67 | url: '/some', 68 | data: queryData, 69 | dataType: 'json' 70 | }).done(function (res) { 71 | if (res.error) { 72 | aler(res.error) 73 | } 74 | else { 75 | render(res, queryData) 76 | } 77 | }).always(function () { 78 | didFetch() 79 | }) 80 | } 81 | }) 82 | ``` 83 | 84 | ### didFetch 85 | 86 | 获取数据完成后触发执行。 87 | 88 | 建议用来控制 loading 状态。 89 | 90 | > 在 `options.fetch` 中调用 didFetch() 会执行 91 | 92 | 93 | ```js 94 | new Page({ 95 | didFetch: function (next) { 96 | $('#tip').removeClass('loading') 97 | } 98 | }) 99 | ``` 100 | 101 | ### render 102 | 103 | 成功获取数据后触发执行,用于渲染页面数据。 104 | 105 | > 在 `options.fetch` 中调用 `render(res, query)` 会执行 106 | 107 | ```js 108 | new Page({ 109 | render: function (res, query) { 110 | var html = '' 111 | // 渲染列表 112 | $('#list').html(html) 113 | // 设置页码 (因为用户可能点击第10页,但是第10页的内容已经被服务器删除了,服务器只会返回第9页) 114 | $('#page').val(query.page) 115 | // 同样设置搜索关键字是因为用户可能在加载过程中修改了搜索关键字,设置后搜索条件和结果能保持一致。 116 | // 或用户调用了 clearQuery 117 | $('#search_keyword').val(query.search_keyword) 118 | } 119 | }) 120 | ``` 121 | 122 | ## query() 123 | 124 | 搜索或翻页时调用 125 | 126 | ```js 127 | $('#search').on('submit', function () { 128 | // 根据 options.getQuery() 返回值进行查询 129 | list.query() 130 | }) 131 | 132 | $('#chanegPage').on('submit', function () { 133 | // 获取 options.getQuery() 返回值,与 {page: 2} 合并后进行查询 134 | list.query({page: 2}) 135 | }) 136 | ``` 137 | 138 | 139 | ### 数据片段条件 140 | 141 | > 不求完全理解,知道大概概念即可 142 | 143 | 注意,修改页码时需要传入 `{page: 2}` 而搜索时不需要传任何值。因为搜索条件分为两种: 144 | 145 | 1. 查询条件 146 | 2. 数据片段条件 147 | 148 | 149 | 区分查询条件和数据片段条件是用于解决类似 [github commits](https://github.com/onface/react/commits/master?after=e7be4bc86ef218f9654ad43398e363962f3127e3+34) 分页的复杂问题。这种列表没有页码,只有 `after` `before` 参数。(打开页面拉到下方点击 [Newer](https://github.com/onface/react/commits/master?before=e7be4bc86ef218f9654ad43398e363962f3127e3+35) 和 [Older](https://github.com/onface/react/commits/master?after=e7be4bc86ef218f9654ad43398e363962f3127e3+69)) 150 | 151 | 查询条件一般有 `用户名` `手机号` `开始日期` `按金额正序排列` `按金额倒序排列` `每页显示多少条件数据` 152 | 153 | 数据片段条件一般有 `页码` 154 | 155 | **他们的区别是:在多次查询中。`数据片段条件变化`时查询条件不变,`查询条件变化`时数据片段条件被设为为默认值。**(一般情况默认值是 `""` 或 `1`) 156 | 157 | 从用户的角度来看就是: 158 | 159 | ```shell 160 | 1次搜索: user=nimo&page=1 // 输入 nimo 点击搜索按钮 161 | 2次搜索: user=nimo&page=2 // 点击下一页 => page 改变,但 user 不变 162 | 3次搜索: user=code&page=1 // 输入 code 点击搜索按钮 => user 改变 page 改为默认值 1 163 | ``` 164 | 165 | 这里查询条件是 `user`,数据片段条件是 `page`。 166 | 167 | --- 168 | 169 | 有时也会定义成 170 | 171 | `https://github.com/onface/react/commits/master?after=e7be4bc86ef218f9654ad43398e363962f3127e3+34` 172 | 173 | 中的 `after` 174 | 175 | 什么条件是数据片段条件是由开发者根据项目实际情况去定义的。 176 | 177 | ### 复杂的情况 178 | 179 | > 不求完全理解,知道大概概念即可 180 | 181 | 用户多次查询时,如果用户搜索了 `user=nimo&age=13&page=2`,接着修改了 `` 的值为 `abc` 但没有点击搜索按钮,直接点击了下一页 。 请求参数会变成 `user=abc&age=13&page=2`。 182 | 183 | 有时根据产品需求请求参数需要是 `user=nimo&age=13&page=3` (用户没有点击搜索按钮,查询条件不变化,即使页面中的 input 变化了。) 184 | 185 | 这时就需要点击搜索按钮时将当前值保存起来,然后通过 `options.getQuery()` 获取的值是刚才保存的值。 186 | 187 | 比如用 jQuery 可以这样做 188 | 189 | ```js 190 | var list = new Page({ 191 | getQuery: function () { 192 | return $('#search').data('query') 193 | } 194 | }) 195 | function setQuery (this) { 196 | var query = {} 197 | var $this = $(this) 198 | $this.serializeArray().forEach(function (item) { 199 | query[item.name] = item.value 200 | }) 201 | $this.data('query', query) 202 | } 203 | $('#search').on('submit', function () { 204 | setQuery(this) 205 | list.query() 206 | }) 207 | setQuery( 208 | $('#search') 209 | ) 210 | 211 | ``` 212 | 213 | React Vue Angular 等声明式框架 则将 `setQuery` 改为将搜索条件命名为 `lastTimeQuery` 保存到 `state` 中 214 | -------------------------------------------------------------------------------- /doc/theme/media/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/growth.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/rating.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/share.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/update.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/aim.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/browser-1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/browser.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/stats.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/optimization.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/laptop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/speedometer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/ads.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/blog.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/polyfill/es6-promise.auto.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.ES6Promise=e()}(this,function(){"use strict";function t(t){return"function"==typeof t||"object"==typeof t&&null!==t}function e(t){return"function"==typeof t}function n(t){I=t}function r(t){J=t}function o(){return function(){return process.nextTick(a)}}function i(){return"undefined"!=typeof H?function(){H(a)}:c()}function s(){var t=0,e=new V(a),n=document.createTextNode("");return e.observe(n,{characterData:!0}),function(){n.data=t=++t%2}}function u(){var t=new MessageChannel;return t.port1.onmessage=a,function(){return t.port2.postMessage(0)}}function c(){var t=setTimeout;return function(){return t(a,1)}}function a(){for(var t=0;t -------------------------------------------------------------------------------- /doc/theme/media/activity.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/theme/media/browser-2.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------