├── doc ├── appcenter.md ├── img │ └── git-version-ctrl.png ├── ionic.md ├── tools.md ├── jenkins.md ├── docker.md ├── travis.md ├── code-spec.md ├── fastlane.md └── android.md ├── src ├── theme │ ├── theme.light.scss │ ├── theme.dark.scss │ └── variables.scss ├── app │ ├── pages │ │ ├── home │ │ │ ├── home.page.scss │ │ │ ├── home.page.ts │ │ │ └── home.page.html │ │ ├── list │ │ │ ├── list.page.scss │ │ │ ├── dynamic-form │ │ │ │ ├── dynamic-form.scss │ │ │ │ ├── readme.md │ │ │ │ ├── models │ │ │ │ │ ├── field.interface.ts │ │ │ │ │ └── field-config.interface.ts │ │ │ │ ├── containers │ │ │ │ │ └── dynamic-form │ │ │ │ │ │ ├── dynamic-form.component.scss │ │ │ │ │ │ └── dynamic-form.component.ts │ │ │ │ ├── components │ │ │ │ │ ├── form-input │ │ │ │ │ │ ├── form-input.component.scss │ │ │ │ │ │ └── form-input.component.ts │ │ │ │ │ ├── form-button │ │ │ │ │ │ ├── form-button.component.scss │ │ │ │ │ │ └── form-button.component.ts │ │ │ │ │ ├── form-select │ │ │ │ │ │ ├── form-select.component.scss │ │ │ │ │ │ └── form-select.component.ts │ │ │ │ │ └── dynamic-field │ │ │ │ │ │ └── dynamic-field.directive.ts │ │ │ │ ├── dynamic-form.html │ │ │ │ ├── dynamic-form.module.ts │ │ │ │ └── dynamic-form.ts │ │ │ ├── calendar │ │ │ │ ├── calendar.page.scss │ │ │ │ ├── calendar.page.ts │ │ │ │ └── calendar.page.html │ │ │ ├── echarts │ │ │ │ ├── echarts.scss │ │ │ │ ├── echarts.html │ │ │ │ └── echarts.ts │ │ │ ├── redux │ │ │ │ ├── todo │ │ │ │ │ ├── todo.model.ts │ │ │ │ │ ├── todo.selectors.ts │ │ │ │ │ ├── todo.actions.ts │ │ │ │ │ ├── todo.reducer.ts │ │ │ │ │ ├── todo.actions.spec.ts │ │ │ │ │ ├── todo.reducer.spec.ts │ │ │ │ │ └── todo.selectors.spec.ts │ │ │ │ ├── filter │ │ │ │ │ ├── filter.actions.ts │ │ │ │ │ ├── filter.reducer.ts │ │ │ │ │ ├── filter.actions.spec.ts │ │ │ │ │ └── filter.reducer.spec.ts │ │ │ │ └── ngrxtodo.reducer.ts │ │ │ ├── ngrxtodo │ │ │ │ ├── new-todo │ │ │ │ │ ├── new-todo.component.html │ │ │ │ │ ├── new-todo.component.ts │ │ │ │ │ └── new-todo.component.spec.ts │ │ │ │ ├── todo-list │ │ │ │ │ ├── todo-list.component.html │ │ │ │ │ └── todo-list.component.ts │ │ │ │ ├── todo │ │ │ │ │ ├── todo.component.html │ │ │ │ │ └── todo.component.ts │ │ │ │ ├── footer │ │ │ │ │ ├── footer.component.html │ │ │ │ │ ├── footer.component.ts │ │ │ │ │ └── footer.component.spec.ts │ │ │ │ ├── ngrxtodo.page.html │ │ │ │ ├── ngrxtodo.page.ts │ │ │ │ ├── ngrxtodo.module.ts │ │ │ │ └── ngrxtodo.page.spec.ts │ │ │ ├── list.module.ts │ │ │ ├── list.page.html │ │ │ ├── list.router.module.ts │ │ │ └── list.page.ts │ │ └── test │ │ │ ├── test.page.scss │ │ │ └── test.page.html │ ├── tabs │ │ ├── tabs.page.scss │ │ ├── tabs.page.html │ │ ├── tabs.module.ts │ │ ├── tabs.page.ts │ │ ├── tabs.router.module.ts │ │ └── tabs.page.spec.ts │ ├── components │ │ ├── close-popup │ │ │ ├── close-popup.html │ │ │ ├── close-popup.scss │ │ │ ├── close-popup.module.ts │ │ │ └── close-popup.ts │ │ └── components.module.ts │ ├── config.ts │ ├── error.handler.ts │ ├── pipes │ │ ├── pipes.module.ts │ │ └── stringTruncate.pipe.ts │ ├── raven-error-handler..ts │ ├── shared │ │ ├── ion2-calendar │ │ │ ├── config.ts │ │ │ ├── index.ts │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ ├── calendar.modal.scss │ │ │ │ ├── calendar-week.component.scss │ │ │ │ ├── month-picker.component.ts │ │ │ │ ├── calendar-week.component.ts │ │ │ │ ├── calendar.component.scss │ │ │ │ └── month-picker.component.scss │ │ │ ├── calendar.module.ts │ │ │ ├── calendar.controller.ts │ │ │ └── calendar.model.ts │ │ └── shared.module.ts │ ├── app-routing.module.ts │ ├── modals │ │ └── qr-scanner │ │ │ ├── qr-scanner.html │ │ │ ├── qr-scanner.module.ts │ │ │ ├── qr-scanner.scss │ │ │ └── qr-scanner.ts │ ├── services │ │ ├── emit.service.ts │ │ ├── data.service.ts │ │ └── qiniu.upload.service.ts │ ├── directives │ │ ├── debounce-click.directive.ts │ │ └── trackEvent.directive.ts │ ├── core │ │ └── core.module.ts │ ├── app.component.html │ ├── app.module.ts │ ├── app.component.spec.ts │ └── http.interceptor.service.ts ├── assets │ ├── imgs │ │ └── logo.png │ ├── icon │ │ ├── favicon.ico │ │ └── favicon.png │ ├── icons │ │ ├── icon-72x72.png │ │ ├── icon-96x96.png │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ └── icon-512x512.png │ └── i18n │ │ ├── zh.json │ │ └── en.json ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── chcp.json ├── tsconfig.app.json ├── tsconfig.spec.json ├── test.ts ├── global.scss ├── main.ts ├── index.html ├── karma.conf.js ├── manifest.json ├── nativewrapper │ └── jpush │ │ ├── ngx │ │ ├── index.metadata.json │ │ └── index.d.ts │ │ └── index.d.ts └── polyfills.ts ├── resources ├── icon.png.md5 ├── splash.png.md5 ├── icon.png ├── splash.png ├── ios │ ├── icon │ │ ├── icon.png │ │ ├── icon-40.png │ │ ├── icon-50.png │ │ ├── icon-60.png │ │ ├── icon-72.png │ │ ├── icon-76.png │ │ ├── icon@2x.png │ │ ├── icon-1024.png │ │ ├── icon-40@2x.png │ │ ├── icon-40@3x.png │ │ ├── icon-50@2x.png │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-72@2x.png │ │ ├── icon-76@2x.png │ │ ├── icon-83.5@2x.png │ │ ├── icon-small.png │ │ ├── icon-small@2x.png │ │ └── icon-small@3x.png │ └── splash │ │ ├── Default-667h.png │ │ ├── Default-736h.png │ │ ├── Default~iphone.png │ │ ├── Default@2x~iphone.png │ │ ├── Default-568h@2x~iphone.png │ │ ├── Default-Landscape-736h.png │ │ ├── Default-Landscape~ipad.png │ │ ├── Default-Portrait~ipad.png │ │ ├── Default-Landscape@2x~ipad.png │ │ ├── Default-Portrait@2x~ipad.png │ │ ├── Default-Portrait@~ipadpro.png │ │ ├── Default-Landscape@~ipadpro.png │ │ └── Default@2x~universal~anyany.png ├── android │ ├── icon │ │ ├── drawable-hdpi-icon.png │ │ ├── drawable-ldpi-icon.png │ │ ├── drawable-mdpi-icon.png │ │ ├── drawable-xhdpi-icon.png │ │ ├── drawable-xxhdpi-icon.png │ │ └── drawable-xxxhdpi-icon.png │ └── splash │ │ ├── drawable-land-hdpi-screen.png │ │ ├── drawable-land-ldpi-screen.png │ │ ├── drawable-land-mdpi-screen.png │ │ ├── drawable-land-xhdpi-screen.png │ │ ├── drawable-port-hdpi-screen.png │ │ ├── drawable-port-ldpi-screen.png │ │ ├── drawable-port-mdpi-screen.png │ │ ├── drawable-port-xhdpi-screen.png │ │ ├── drawable-land-xxhdpi-screen.png │ │ ├── drawable-land-xxxhdpi-screen.png │ │ ├── drawable-port-xxhdpi-screen.png │ │ └── drawable-port-xxxhdpi-screen.png └── README.md ├── fastlane ├── Pluginfile ├── Appfile ├── Matchfile └── Fastfile ├── sh ├── release │ ├── certificates.tar.enc │ ├── certificates │ │ └── apple.cer │ ├── remove-key(deprecated).sh │ ├── decrypt-key.sh │ ├── package-and-upload(deprecated).sh │ ├── add-key(deprecated).sh │ └── xcconfig(deprecated) │ │ └── build-release.xcconfig.template ├── README.md ├── package-android.sh ├── build-android.sh ├── package-ios(deprecated).sh ├── build-ios(deprecated).sh ├── release.sh ├── ionic-login.sh └── addkeys(deprecated).sh ├── ionic.config.json ├── version_update ├── chcp.json └── apk_version.json ├── e2e ├── tsconfig.e2e.json ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts └── protractor.conf.js ├── Gemfile ├── ionic.starter.json ├── commitlint.config.js ├── ngsw-config.json ├── .gitignore ├── Jenkinsfile ├── tsconfig.json ├── LICENSE ├── hooks ├── before_build │ └── 010_update_config.js └── after_platform_add │ └── update-release-xcconfig.js └── tslint.json /doc/appcenter.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/theme/theme.light.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/pages/home/home.page.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/pages/list/list.page.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.page.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/pages/list/dynamic-form/dynamic-form.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/icon.png.md5: -------------------------------------------------------------------------------- 1 | 3f1bbdf1aefcb5ce7b60770ce907c68f -------------------------------------------------------------------------------- /resources/splash.png.md5: -------------------------------------------------------------------------------- 1 | 2412a8324a656ec5993eb50b3b293c69 -------------------------------------------------------------------------------- /src/app/pages/list/calendar/calendar.page.scss: -------------------------------------------------------------------------------- 1 | page-calendar {} -------------------------------------------------------------------------------- /src/app/pages/test/test.page.scss: -------------------------------------------------------------------------------- 1 | page-test { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/app/pages/list/echarts/echarts.scss: -------------------------------------------------------------------------------- 1 | page-echarts { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/icon.png -------------------------------------------------------------------------------- /resources/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/splash.png -------------------------------------------------------------------------------- /fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | gem 'fastlane-plugin-cordova' 2 | gem 'fastlane-plugin-pgyer' 3 | gem 'fastlane-plugin-ionic' -------------------------------------------------------------------------------- /src/assets/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/src/assets/imgs/logo.png -------------------------------------------------------------------------------- /resources/ios/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon.png -------------------------------------------------------------------------------- /src/assets/icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/src/assets/icon/favicon.ico -------------------------------------------------------------------------------- /src/assets/icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/src/assets/icon/favicon.png -------------------------------------------------------------------------------- /doc/img/git-version-ctrl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/doc/img/git-version-ctrl.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-40.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-50.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-60.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-72.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-76.png -------------------------------------------------------------------------------- /resources/ios/icon/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-1024.png -------------------------------------------------------------------------------- /sh/release/certificates.tar.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/sh/release/certificates.tar.enc -------------------------------------------------------------------------------- /src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-40@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-40@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-50@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-60@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-60@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-72@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-76@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-83.5@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-small.png -------------------------------------------------------------------------------- /sh/release/certificates/apple.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/sh/release/certificates/apple.cer -------------------------------------------------------------------------------- /src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/src/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-small@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/icon/icon-small@3x.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-667h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/splash/Default-667h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/splash/Default-736h.png -------------------------------------------------------------------------------- /sh/README.md: -------------------------------------------------------------------------------- 1 | see: 2 | 3 | - https://github.com/okode/ionic-travis/ 4 | - https://github.com/samueltbrown/ionic-continuous-delivery-blog -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic4-boilerplate", 3 | "integrations": { 4 | "cordova": {} 5 | }, 6 | "type": "angular" 7 | } -------------------------------------------------------------------------------- /resources/ios/splash/Default~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/splash/Default~iphone.png -------------------------------------------------------------------------------- /src/app/pages/list/redux/todo/todo.model.ts: -------------------------------------------------------------------------------- 1 | export interface Todo { 2 | id: number; 3 | text: string; 4 | completed: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /resources/ios/splash/Default@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/splash/Default@2x~iphone.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-hdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/icon/drawable-hdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-ldpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/icon/drawable-ldpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-mdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/icon/drawable-mdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/icon/drawable-xhdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/icon/drawable-xxhdpi-icon.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-568h@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/splash/Default-568h@2x~iphone.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/splash/Default-Landscape-736h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/splash/Default-Landscape~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/splash/Default-Portrait~ipad.png -------------------------------------------------------------------------------- /src/app/pages/list/dynamic-form/readme.md: -------------------------------------------------------------------------------- 1 | # notes 2 | 3 | the dynamic-form demo is reference from `https://toddmotto.com/angular-dynamic-components-forms` -------------------------------------------------------------------------------- /resources/android/icon/drawable-xxxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/icon/drawable-xxxhdpi-icon.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/splash/Default-Landscape@2x~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/splash/Default-Portrait@2x~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait@~ipadpro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/splash/Default-Portrait@~ipadpro.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape@~ipadpro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/splash/Default-Landscape@~ipadpro.png -------------------------------------------------------------------------------- /resources/ios/splash/Default@2x~universal~anyany.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/ios/splash/Default@2x~universal~anyany.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | mode: 'prod', 4 | serverUrl: 'http://192.168.3.130:7001/' 5 | }; 6 | -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/splash/drawable-land-hdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/splash/drawable-land-ldpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/splash/drawable-land-mdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/splash/drawable-land-xhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/splash/drawable-port-hdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/splash/drawable-port-ldpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/splash/drawable-port-mdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/splash/drawable-port-xhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/splash/drawable-land-xxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/splash/drawable-land-xxxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/splash/drawable-port-xxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengkobe/ionic4-boilerplate/HEAD/resources/android/splash/drawable-port-xxxhdpi-screen.png -------------------------------------------------------------------------------- /src/chcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "autogenerated": true, 3 | "release": "2018.10.27-15.07.09", 4 | "content_url": "", 5 | "update": "start", 6 | "min_native_interface": 1 7 | } 8 | -------------------------------------------------------------------------------- /version_update/chcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "autogenerated": true, 3 | "release": "2018.10.27-15.07.09", 4 | "content_url": "", 5 | "update": "start", 6 | "min_native_interface": 1 7 | } 8 | -------------------------------------------------------------------------------- /doc/ionic.md: -------------------------------------------------------------------------------- 1 | # Ionic 2 | 3 | 官网: https://ionicframework.com 4 | 5 | > 这里不做过多的说明,官网上有比较详细的介绍,ionic 主要基于 Angular 进行开发,必须对 Angular 框架比较熟悉才能使用,具体学习可以直接去官网,不仅文档详尽而且有一些综合应用的例子。参考: https://angular.io 6 | -------------------------------------------------------------------------------- /src/app/components/close-popup/close-popup.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/app/components/components.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | @NgModule({ 4 | declarations: [], 5 | imports: [], 6 | exports: [], 7 | }) 8 | export class ComponentsModule {} 9 | -------------------------------------------------------------------------------- /version_update/apk_version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.4.2", 3 | "updateContent": "refactor", 4 | "downloadUrl": "http://ipordomain/path/to/your.apk", 5 | "size": "17M", 6 | "datetime": "2018-10-27 21:45.01" 7 | } 8 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/config.ts: -------------------------------------------------------------------------------- 1 | import { environment } from '@app/env'; 2 | 3 | console.log('ENV:', environment.mode); 4 | console.log('ENV serverUrl:', environment.serverUrl); 5 | export const baseUrl = environment.serverUrl; 6 | export const qiniuDomain = ''; 7 | -------------------------------------------------------------------------------- /sh/release/remove-key(deprecated).sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | security delete-keychain ios-build.keychain 3 | rm -f "~/Library/MobileDevice/Provisioning Profiles/$PROFILE_NAME.mobileprovision" 4 | rm -f ~/Library/MobileDevice/Provisioning Profiles/team.mobileprovision -------------------------------------------------------------------------------- /src/app/pages/list/dynamic-form/models/field.interface.ts: -------------------------------------------------------------------------------- 1 | import { FormGroup } from '@angular/forms'; 2 | import { FieldConfig } from './field-config.interface'; 3 | 4 | export interface Field { 5 | config: FieldConfig; 6 | group: FormGroup; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/error.handler.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandler, Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class MyErrorHandler extends ErrorHandler { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | handleError(error: any): void {} 10 | } 11 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015" 7 | }, 8 | "exclude": [ 9 | "test.ts", 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/app/pages/list/ngrxtodo/new-todo/new-todo.component.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.className('home-title')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/theme/theme.dark.scss: -------------------------------------------------------------------------------- 1 | .dark-theme { 2 | ion-content { 3 | --background: #ddd; 4 | } 5 | .toolbar-title { 6 | --color: #fff; 7 | } 8 | .header .toolbar-background { 9 | --border-color: #ff0fff; 10 | --background-color: #090f2f; 11 | } 12 | } -------------------------------------------------------------------------------- /src/app/pages/list/redux/filter/filter.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | export const SET_FILTER = '[SET] filter'; 4 | 5 | export class SetFilterAction implements Action { 6 | readonly type = SET_FILTER; 7 | 8 | constructor( 9 | public filter: string 10 | ) {} 11 | } 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | source "https://rubygems.org" 6 | 7 | gem 'fastlane' 8 | gem 'cocoapods' 9 | 10 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 11 | eval_gemfile(plugins_path) if File.exist?(plugins_path) -------------------------------------------------------------------------------- /src/app/pipes/pipes.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { StringTruncatemPipe } from './stringTruncate.pipe'; 4 | 5 | export const pipes = [ 6 | StringTruncatemPipe, 7 | ]; 8 | 9 | @NgModule({ 10 | declarations: [pipes], 11 | exports: [pipes], 12 | }) 13 | export class PipesModule {} 14 | -------------------------------------------------------------------------------- /sh/package-android.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -v 2 | 3 | set -e 4 | 5 | if [[ "$TRAVIS_BRANCH" == "develop" ]] 6 | then 7 | echo "Skipping package Android for develop branch" 8 | exit 9 | fi 10 | 11 | mkdir -p output 12 | cp platforms/android/app/build/outputs/apk/release/app-release-signed.apk output/ionic4-boilerplate-release-signed.apk -------------------------------------------------------------------------------- /src/app/pages/list/dynamic-form/containers/dynamic-form/dynamic-form.component.scss: -------------------------------------------------------------------------------- 1 | :host /deep/ .dynamic-field { 2 | margin-bottom: 15px; 3 | label { 4 | display: block; 5 | font-size: 16px; 6 | font-weight: 400; 7 | letter-spacing: 0px; 8 | margin-bottom: 10px; 9 | color: rgba(0, 0, 0, 0.9); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /sh/build-android.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -v 2 | 3 | set -e 4 | 5 | # Build Ionic App for Android 6 | ionic cordova platform add android --nofetch --no-resources 7 | 8 | if [[ "$TRAVIS_BRANCH" == "develop" ]] 9 | then 10 | ionic cordova build android --no-resources 11 | else # --prod 12 | ionic cordova build android --prod --release --no-resources 13 | fi -------------------------------------------------------------------------------- /src/app/pages/list/dynamic-form/models/field-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { ValidatorFn } from '@angular/forms'; 2 | 3 | export interface FieldConfig { 4 | disabled?: boolean; 5 | label?: string; 6 | name: string; 7 | options?: string[]; 8 | placeholder?: string; 9 | type: string; 10 | validation?: ValidatorFn[]; 11 | value?: any; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/components/close-popup/close-popup.scss: -------------------------------------------------------------------------------- 1 | close-popup { 2 | .button { 3 | display: block; 4 | z-index: 1001; 5 | position: absolute; 6 | top: 0.5em; 7 | right: 0.2em; 8 | opacity: 0.6; 9 | } 10 | 11 | .icon { 12 | font-size: 2.5rem; 13 | 14 | &.large { 15 | font-size: 3rem !important; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('new App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display the right title', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toContain('Ionic Boilerplate'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/raven-error-handler..ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandler } from '@angular/core'; 2 | import * as Raven from 'raven-js'; 3 | 4 | Raven.config( 5 | 'https://8583117beafb40a8be2906252ee80fcc@sentry.io/240912' 6 | ).install(); 7 | 8 | export class RavenErrorHandler implements ErrorHandler { 9 | handleError(err: any): void { 10 | Raven.captureException(err); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/components/close-popup/close-popup.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicModule } from '@ionic/angular'; 3 | import { ClosePopupComponent } from './close-popup'; 4 | 5 | @NgModule({ 6 | declarations: [ClosePopupComponent], 7 | imports: [IonicModule], 8 | exports: [ClosePopupComponent] 9 | }) 10 | export class ClosePopupComponentModule { } 11 | -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier "com.yipeng.ionic4.boilerplate" # The bundle identifier of your app 2 | apple_id "yipeng.info@gmail.com" # Your Apple email address 3 | 4 | team_id "CVU2X68836" # Developer Portal Team ID 5 | 6 | # you can even provide different app identifiers, Apple IDs and team names per lane: 7 | # More information: https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Appfile.md -------------------------------------------------------------------------------- /src/app/shared/ion2-calendar/config.ts: -------------------------------------------------------------------------------- 1 | export const defaults = { 2 | DATE_FORMAT: 'YYYY-MM-DD', 3 | COLOR: 'primary', 4 | WEEKS_FORMAT: ['S', 'M', 'T', 'W', 'T', 'F', 'S'], 5 | MONTH_FORMAT: ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] 6 | }; 7 | 8 | export const pickModes = { 9 | SINGLE: 'single', 10 | RANGE: 'range', 11 | MULTI: 'multi' 12 | }; 13 | -------------------------------------------------------------------------------- /src/app/pages/list/dynamic-form/components/form-input/form-input.component.scss: -------------------------------------------------------------------------------- 1 | input { 2 | display: block; 3 | font-family: inherit; 4 | font-size: 14px; 5 | width: 100%; 6 | border: 1px solid rgba(0, 0, 0, 0.1); 7 | outline: none; 8 | padding: 10px 15px; 9 | color: rgba(0, 0, 0, 0.7); 10 | &:focus { 11 | border: 1px solid rgba(0, 0, 0, 0.4); 12 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.3); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /sh/package-ios(deprecated).sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -v 2 | 3 | set -e 4 | 5 | if [[ "$TRAVIS_BRANCH" == "develop" ]] 6 | then 7 | echo "Skipping package iOS for develop branch" 8 | exit 9 | fi 10 | 11 | mkdir -p output 12 | ls 13 | cd ./platforms/ios 14 | ls 15 | cd ./build 16 | ls 17 | cd ./emulator 18 | ls 19 | cd ../../../../ 20 | tar zcvf output/ionic4-boilerplate-release-unsigned.app.tgz platforms/ios/build/emulator/ionic4-boilerplate.app -------------------------------------------------------------------------------- /src/app/pages/list/redux/filter/filter.reducer.ts: -------------------------------------------------------------------------------- 1 | import * as FilterActions from './filter.actions'; 2 | 3 | export function FilterReducer(state: string = 'SHOW_ALL', action: FilterActions.SetFilterAction) { 4 | if (!action) { 5 | return state; 6 | } 7 | switch (action.type) { 8 | case FilterActions.SET_FILTER: { 9 | return action.filter; 10 | } 11 | default: { 12 | return state; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ionic.starter.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tabs Starter", 3 | "baseref": "master", 4 | "tarignore": [ 5 | "node_modules", 6 | "package-lock.json", 7 | "www" 8 | ], 9 | "scripts": { 10 | "test": "npm run lint && npm run ng -- build && npm run ng -- build --prod && npm run ng -- test --watch=false --progress=false && npm run ng -- e2e && npm run ng -- g pg my-page --dry-run && npm run ng -- g c my-component --dry-run" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sh/build-ios(deprecated).sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -v 2 | 3 | set -e 4 | 5 | # Build Ionic App for iOS 6 | ionic cordova platform add ios --nofetch 7 | cordova requirements 8 | # fix hook not work 9 | cd ./hooks/after_platform_add/ 10 | node update-release-xcconfig.js 11 | cd ../../ 12 | if [[ "$TRAVIS_BRANCH" == "develop" ]] 13 | then 14 | ionic cordova build ios --device 15 | else # --prod --release 16 | ionic cordova build ios --device --release 17 | fi -------------------------------------------------------------------------------- /src/app/pipes/stringTruncate.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'StringTruncate' 5 | }) 6 | export class StringTruncatemPipe implements PipeTransform { 7 | transform(value?: string, args?: any): any { 8 | const bitnum = args; 9 | if (value && value.length > bitnum) { 10 | return value.substr(0, bitnum) + '...'; 11 | } else { 12 | return value; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /fastlane/Matchfile: -------------------------------------------------------------------------------- 1 | git_url("https://pengkobe:#{ENV["GITHUB_PWD"]}@github.com/pengkobe/efos-ios-certs") 2 | 3 | type("adhoc") # The default type, can be: appstore, adhoc, enterprise or development 4 | 5 | app_identifier("com.yipeng.ionic4.boilerplate") 6 | username("yipeng.info@gmail.com") # Your Apple Developer Portal username 7 | 8 | # For all available options run `fastlane match --help` 9 | # Remove the # in the beginning of the line to enable the other options -------------------------------------------------------------------------------- /resources/README.md: -------------------------------------------------------------------------------- 1 | These are Cordova resources. You can replace icon.png and splash.png and run 2 | `ionic cordova resources` to generate custom icons and splash screens for your 3 | app. See `ionic cordova resources --help` for details. 4 | 5 | Cordova reference documentation: 6 | 7 | - Icons: https://cordova.apache.org/docs/en/latest/config_ref/images.html 8 | - Splash Screens: https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-splashscreen/ 9 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | const routes: Routes = [ 5 | { path: '', loadChildren: './tabs/tabs.module#TabsPageModule' }, 6 | { path: 'tabs', loadChildren: './tabs/tabs.module#TabsPageModule' }, 7 | ]; 8 | @NgModule({ 9 | imports: [RouterModule.forRoot(routes)], 10 | exports: [RouterModule] 11 | }) 12 | export class AppRoutingModule { } 13 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "types": [ 8 | "jasmine", 9 | "node", 10 | "cordova", "cordova-plugin-file" 11 | ], 12 | }, 13 | "files": [ 14 | "test.ts" 15 | ], 16 | "include": [ 17 | "polyfills.ts", 18 | "**/*.spec.ts", 19 | "**/*.d.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/app/pages/list/ngrxtodo/todo-list/todo-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 | 9 |
-------------------------------------------------------------------------------- /src/app/shared/ion2-calendar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './calendar.model'; 2 | export { CalendarModal } from './components/calendar.modal'; 3 | export { CalendarWeekComponent } from './components/calendar-week.component'; 4 | export { MonthComponent } from './components/month.component'; 5 | export { CalendarComponent } from './components/calendar.component'; 6 | export { CalendarModule } from './calendar.module'; 7 | export { CalendarController } from './calendar.controller'; 8 | -------------------------------------------------------------------------------- /sh/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $# -ne 1 ]; then 4 | echo "Syntax: release [VERSION]" 5 | exit 1 6 | fi 7 | 8 | VERSION=$1 9 | 10 | # Create release 11 | git flow release start $VERSION || exit 1 12 | GIT_MERGE_AUTOEDIT=no git flow release finish -m $VERSION $VERSION 13 | 14 | # Publish release 15 | git push origin HEAD --tags 16 | 17 | # Merge release into develop 18 | git checkout develop 19 | git merge master 20 | 21 | # Bump version 22 | echo "Bump next version" -------------------------------------------------------------------------------- /src/app/pages/home/home.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { DataService } from '@services/data.service'; 3 | 4 | @Component({ 5 | selector: 'page-home', 6 | templateUrl: 'home.page.html', 7 | }) 8 | export class HomePage { 9 | cacheData = ''; 10 | constructor(public dataservice: DataService) {} 11 | 12 | testCache() { 13 | this.dataservice.testCachedData().subscribe(num => { 14 | this.cacheData = num; 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/pages/list/redux/ngrxtodo.reducer.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducerMap } from '@ngrx/store'; 2 | 3 | import { TodosReducer } from './todo/todo.reducer'; 4 | import { FilterReducer } from './filter/filter.reducer'; 5 | import { Todo } from './todo/todo.model'; 6 | 7 | export interface AppState { 8 | todos: Todo[]; 9 | filter: string; 10 | } 11 | 12 | export const ngrxtodoReducer: ActionReducerMap = { 13 | todos: TodosReducer, 14 | filter: FilterReducer 15 | }; 16 | -------------------------------------------------------------------------------- /doc/tools.md: -------------------------------------------------------------------------------- 1 | # Tools 2 | 3 | ## VSCODE 4 | 5 | 推荐使用 VSCODE 作为开发工具,完善的生态,靠谱的开发团队,对 typescript 极其友好 6 | 7 | ### 插件推荐 8 | 9 | - [Top 10 Angular VS Code Extensions](http://devboosts.com/2017/02/08/top-10-vs-code-extensions/) 10 | - [Essential Angular VS Code Extensions](https://johnpapa.net/essential-angular-vs-code-extensions/) 11 | 12 | ## 谷歌控制台 13 | 14 | 使用谷歌控制台在开发时进行调试,只需要按下 F12, 选择对应的移动视图就 ok, 若需要对真机进行调试时,也可以直接使用谷歌 `chorome://inspect` 进行同步调试,之前微信跳一跳游戏自动点击小工具也是基于这样一个原理。**需要翻墙才能哦!** 15 | 16 | -------------------------------------------------------------------------------- /src/assets/i18n/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "HOME": { 3 | "Home": "首页", 4 | "List": "列表", 5 | "Test": "测试", 6 | "Me": "我", 7 | "Toggle": "侧边栏", 8 | "Menu": "菜单", 9 | "Scan": "扫码", 10 | "ScreenAlwaysLight": "屏幕常亮", 11 | "SwitchLanguage": "语言切换", 12 | "LanguageCH":"中文", 13 | "LanguageEN":"英文", 14 | "Theme":"皮肤切换", 15 | "Cancel":"取消", 16 | "confirm":"确定", 17 | "GetRandomNumber":"点击获取随机数", 18 | "Calendar":"日历", 19 | "DynamicForm":"动态表单" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-angular'], 3 | 4 | rules: { 5 | 'subject-case': [0, 'never'], 6 | 'type-enum': [ 7 | 2, 8 | 'always', 9 | [ 10 | 'docs', 11 | 'chore', 12 | 'feat', 13 | 'fix', 14 | 'build', 15 | 'ci', 16 | 'merge', 17 | 'perf', 18 | 'refactor', 19 | 'revert', 20 | 'style', 21 | 'test', 22 | ], 23 | ], 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/app/pages/list/redux/filter/filter.actions.spec.ts: -------------------------------------------------------------------------------- 1 | import * as FilterActions from './filter.actions'; 2 | 3 | describe('Redux: FilterActions', () => { 4 | 5 | describe('Test for SetFilterAction', () => { 6 | 7 | it('should return an action with type an filter', () => { 8 | const action = new FilterActions.SetFilterAction('new filter'); 9 | expect(action.type).toEqual(FilterActions.SET_FILTER); 10 | expect(action.filter).toEqual('new filter'); 11 | }); 12 | 13 | }); 14 | 15 | }); 16 | -------------------------------------------------------------------------------- /sh/release/decrypt-key.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ -z "$PROFILE_NAME" ]]; then 4 | echo "Error: Missing provision profile name" 5 | exit 1 6 | fi 7 | 8 | if [[ ! -e "./sh/release/certificates.tar.enc" ]]; then 9 | echo "Error: Missing encrypted certificates." 10 | exit 1 11 | fi 12 | openssl aes-256-cbc -K $encrypted_fd699ad937e2_key -iv $encrypted_fd699ad937e2_iv -in ./sh/release/certificates.tar.enc -out ./sh/release/certificates.tar -d 13 | tar xvf ./sh/release/certificates.tar -C ./sh/release/certificates -------------------------------------------------------------------------------- /src/app/shared/ion2-calendar/components/index.ts: -------------------------------------------------------------------------------- 1 | import { CalendarModal } from './calendar.modal'; 2 | import { CalendarWeekComponent } from './calendar-week.component'; 3 | import { MonthComponent } from './month.component'; 4 | import { CalendarComponent } from './calendar.component'; 5 | import { MonthPickerComponent } from './month-picker.component'; 6 | 7 | export const CALENDAR_COMPONENTS = [ 8 | CalendarModal, 9 | CalendarWeekComponent, 10 | MonthComponent, 11 | CalendarComponent, 12 | MonthPickerComponent, 13 | ]; 14 | -------------------------------------------------------------------------------- /src/app/components/close-popup/close-popup.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter, HostBinding } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'close-popup', 5 | templateUrl: 'close-popup.html' 6 | }) 7 | export class ClosePopupComponent { 8 | 9 | @HostBinding('style.z-index') style = 1000; 10 | 11 | @Input() large: boolean; 12 | @Input() color: string; 13 | @Output() closePopUp: EventEmitter = new EventEmitter(); 14 | 15 | constructor() {} 16 | 17 | close() { 18 | this.closePopUp.emit(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "/index.html", 3 | "assetGroups": [ 4 | { 5 | "name": "app", 6 | "installMode": "prefetch", 7 | "resources": { 8 | "files": [ 9 | "/favicon.ico", 10 | "/index.html", 11 | "/*.css", 12 | "/*.js" 13 | ] 14 | } 15 | }, { 16 | "name": "assets", 17 | "installMode": "lazy", 18 | "updateMode": "prefetch", 19 | "resources": { 20 | "files": [ 21 | "/assets/**" 22 | ] 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/app/pages/list/echarts/echarts.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | Echarts 13 | 14 | 15 | 16 | 17 | 18 |
19 |
-------------------------------------------------------------------------------- /src/assets/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "HOME": { 3 | "Home": "Home", 4 | "List": "List", 5 | "Test": "Test", 6 | "Me": "Me", 7 | "Toggle": "Toggle Menu", 8 | "Menu": "Menu", 9 | "Scan": "Scan", 10 | "ScreenAlwaysLight": "Screen Always Light", 11 | "SwitchLanguage": "Switch Language", 12 | "LanguageCH": "Chinese", 13 | "LanguageEN": "English", 14 | "Theme": "Theme", 15 | "Cancel": "Cancel", 16 | "confirm": "OK", 17 | "GetRandomNumber": "Get Random Number", 18 | "Calendar": "Calendar", 19 | "DynamicForm": "DynamicForm" 20 | } 21 | } -------------------------------------------------------------------------------- /src/app/pages/list/dynamic-form/dynamic-form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ 'HOME.DynamicForm' | translate }} 7 | 8 | 9 | 10 | 11 | 15 | 16 | {{ form.valid }} 17 | {{ form.value | json }} 18 | -------------------------------------------------------------------------------- /src/app/pages/list/ngrxtodo/todo/todo.component.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 7 | 11 | 12 |
    13 | 21 |
  • -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | *.log 7 | *.tmp 8 | *.tmp.* 9 | log.txt 10 | *.sublime-project 11 | *.sublime-workspace 12 | .vscode/ 13 | npm-debug.log* 14 | private.log 15 | cordova_plugins/ 16 | .idea/ 17 | .ionic/ 18 | .sourcemaps/ 19 | .sass-cache/ 20 | .tmp/ 21 | .versions/ 22 | coverage/ 23 | www/ 24 | node_modules/ 25 | tmp/ 26 | temp/ 27 | platforms/ 28 | plugins/ 29 | plugins/android.json 30 | plugins/ios.json 31 | $RECYCLE.BIN/ 32 | 33 | .DS_Store 34 | Thumbs.db 35 | UserInterfaceState.xcuserstate 36 | yarn-error.log -------------------------------------------------------------------------------- /src/app/tabs/tabs.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ 'HOME.Home' | translate }} 6 | 7 | 8 | 9 | 10 | {{ 'HOME.List' | translate }} 11 | 12 | 13 | 14 | 15 | {{ 'HOME.Test' | translate }} 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { IonicModule } from '@ionic/angular'; 3 | import { TabsPageRoutingModule } from './tabs.router.module'; 4 | 5 | import { SharedModule } from './../shared/shared.module'; 6 | import { TabsPage } from './tabs.page'; 7 | import { HomePage } from '../pages/home/home.page'; 8 | import { TestPage } from '../pages/test/test.page'; 9 | 10 | @NgModule({ 11 | imports: [IonicModule, SharedModule, TabsPageRoutingModule], 12 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 13 | declarations: [TabsPage, HomePage, TestPage], 14 | }) 15 | export class TabsPageModule { } 16 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | 3 | agent any 4 | 5 | stages { 6 | 7 | stage('NPM Setup') { 8 | steps { 9 | sh 'npm install' 10 | } 11 | } 12 | 13 | stage('Stage Web Test') { 14 | steps { 15 | sh 'npm run Test' 16 | } 17 | } 18 | 19 | stage('Stage Web Build') { 20 | steps { 21 | sh 'npm run build --prod' 22 | } 23 | } 24 | 25 | stage('Android Build') { 26 | steps { 27 | sh 'ionic cordova build android --release' 28 | } 29 | } 30 | 31 | 32 | } 33 | } -------------------------------------------------------------------------------- /src/app/shared/ion2-calendar/components/calendar.modal.scss: -------------------------------------------------------------------------------- 1 | :host ion-select { 2 | max-width: unset; } 3 | :host ion-select .select-icon > .select-icon-inner, 4 | :host ion-select .select-text { 5 | color: #fff !important; } 6 | :host ion-select.select-ios { 7 | max-width: unset; } 8 | 9 | :host .calendar-page { 10 | background-color: #fbfbfb; } 11 | 12 | :host .month-box { 13 | display: inline-block; 14 | width: 100%; 15 | padding-bottom: 1em; 16 | border-bottom: 1px solid #f1f1f1; } 17 | 18 | :host h4 { 19 | font-weight: 400; 20 | font-size: 1.8rem; 21 | display: block; 22 | text-align: center; 23 | margin: 1rem 0 0; 24 | color: #929292; } 25 | -------------------------------------------------------------------------------- /src/app/modals/qr-scanner/qr-scanner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 |

    对准二维码,即可自动扫描

    5 |
    6 |
    7 |
    8 |
    9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    18 |
    -------------------------------------------------------------------------------- /src/app/pages/list/calendar/calendar.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | /** 3 | * Generated class for the CalendarPage page. 4 | * 5 | * See https://ionicframework.com/docs/components/#navigation for more info on 6 | * Ionic pages and navigation. 7 | */ 8 | 9 | @Component({ 10 | selector: 'page-calendar', 11 | templateUrl: 'calendar.page.html', 12 | }) 13 | export class CalendarPage { 14 | date: string; 15 | type: 'string'; // 'string' | 'js-date' | 'moment' | 'time' | 'object' 16 | 17 | constructor() {} 18 | 19 | ionViewDidLoad() { 20 | console.log('ionViewDidLoad EchartsPage'); 21 | } 22 | 23 | onChange($event) { 24 | console.log($event); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/modals/qr-scanner/qr-scanner.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { QRScannerModal } from './qr-scanner'; 4 | import { QRScanner } from '@ionic-native/qr-scanner/ngx'; 5 | import { ClosePopupComponentModule } from '@components/close-popup/close-popup.module'; 6 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 7 | 8 | @NgModule({ 9 | declarations: [QRScannerModal], 10 | imports: [ 11 | RouterModule.forChild([{ path: '', component: QRScannerModal }]), 12 | ClosePopupComponentModule, 13 | ], 14 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 15 | providers: [QRScanner], 16 | }) 17 | export class QRScannerModalModule { } 18 | -------------------------------------------------------------------------------- /src/app/pages/list/calendar/calendar.page.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | {{ 'HOME.Calendar' | translate }} 13 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /sh/ionic-login.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Inspired by https://github.com/devillex/wercker-ionic-login-step/blob/master/run.sh 4 | cli="ionic" 5 | 6 | main() { 7 | 8 | if [ -z "$IONIC_EMAIL" ]; then 9 | fail "ionic-login: email argument cannot be empty" 10 | fi 11 | 12 | if [ -z "$IONIC_PASSWORD" ]; then 13 | fail "ionic-login: password argument cannot be empty" 14 | fi 15 | 16 | # Command 17 | cmd="login" 18 | 19 | # Command arguments 20 | args= 21 | 22 | # email 23 | if [ -n "$IONIC_LOGIN_EMAIL" ]; then 24 | args="$args $IONIC_EMAIL" 25 | fi 26 | 27 | # password 28 | if [ -n "$IONIC_LOGIN_PASSWORD" ]; then 29 | args="$args $IONIC_PASSWORD" 30 | fi 31 | 32 | eval "$cli" "$cmd" "$args" 33 | } 34 | 35 | main; 36 | -------------------------------------------------------------------------------- /src/app/pages/list/dynamic-form/components/form-button/form-button.component.scss: -------------------------------------------------------------------------------- 1 | button { 2 | letter-spacing: -0.5px; 3 | cursor: pointer; 4 | background-color: #9d62c8; 5 | outline: 0; 6 | line-height: 1; 7 | text-align: center; 8 | padding: 12px 30px; 9 | font-size: 15px; 10 | font-weight: 600; 11 | border-radius: 2px; 12 | display: inline-block; 13 | border: none; 14 | color: #fff; 15 | transition: background-color .3s, box-shadow .3s; 16 | 17 | &:hover { 18 | background-color: #a46dcc; 19 | box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2); 20 | } 21 | 22 | &:disabled { 23 | background: rgba(0, 0, 0, 0.2); 24 | color: rgba(0, 0, 0, 0.4); 25 | cursor: not-allowed; 26 | box-shadow: none; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/pages/home/home.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ 'HOME.Home' | translate }} 7 | 8 | 9 | 10 |

    Ionic4 Boilerplate

    11 | 12 |

    create by pengkobe .

    13 | {{ 'HOME.Toggle' | translate }} 14 | 15 | {{ 'HOME.GetRandomNumber' | translate }} 17 | {{ cacheData }} 18 |
    -------------------------------------------------------------------------------- /src/app/pages/list/dynamic-form/components/form-button/form-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormGroup } from '@angular/forms'; 3 | 4 | import { Field } from '../../models/field.interface'; 5 | import { FieldConfig } from '../../models/field-config.interface'; 6 | 7 | @Component({ 8 | selector: 'form-button', 9 | styleUrls: ['form-button.component.scss'], 10 | template: ` 11 |
    14 | 19 |
    20 | ` 21 | }) 22 | export class FormButtonComponent implements Field { 23 | config: FieldConfig; 24 | group: FormGroup; 25 | } 26 | -------------------------------------------------------------------------------- /src/app/services/emit.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, OnInit, Injectable } from '@angular/core'; 2 | import { BehaviorSubject, Observable } from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class EmitService implements OnInit { 8 | public eventEmit: EventEmitter = new EventEmitter(); 9 | public theme: BehaviorSubject = new BehaviorSubject('dark-theme'); 10 | 11 | constructor() { 12 | console.log('Hello EmitService Provider'); 13 | this.eventEmit = new EventEmitter(); 14 | this.theme = new BehaviorSubject('dark-theme'); 15 | } 16 | 17 | ngOnInit() { } 18 | 19 | setActiveTheme(val) { 20 | this.theme.next(val); 21 | } 22 | 23 | getActiveTheme(): Observable { 24 | return this.theme.asObservable(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/pages/list/list.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { NgxEchartsModule } from 'ngx-echarts'; 3 | 4 | import { ListPageRoutingModule } from './list.router.module'; 5 | 6 | import { SharedModule } from '../../shared/shared.module'; 7 | 8 | import { ListPage } from './list.page'; 9 | import { CalendarPage } from './calendar/calendar.page'; 10 | import { EchartsPage } from './echarts/echarts'; 11 | import { DynamicFormModule } from './dynamic-form/dynamic-form.module'; 12 | import { DynamicFormPage } from './dynamic-form/dynamic-form'; 13 | 14 | @NgModule({ 15 | imports: [SharedModule, ListPageRoutingModule, NgxEchartsModule, DynamicFormModule], 16 | declarations: [CalendarPage, EchartsPage, ListPage, DynamicFormPage], 17 | }) 18 | export class ListPageModule { } 19 | -------------------------------------------------------------------------------- /src/global.scss: -------------------------------------------------------------------------------- 1 | // http://ionicframework.com/docs/theming/ 2 | @import "~@ionic/angular/css/core.css"; 3 | @import "~@ionic/angular/css/normalize.css"; 4 | @import "~@ionic/angular/css/structure.css"; 5 | @import "~@ionic/angular/css/typography.css"; 6 | @import "~@ionic/angular/css/padding.css"; 7 | @import "~@ionic/angular/css/float-elements.css"; 8 | @import "~@ionic/angular/css/text-alignment.css"; 9 | @import "~@ionic/angular/css/text-transformation.css"; 10 | @import "~@ionic/angular/css/flex-utils.css"; 11 | @import '~todomvc-common/base.css'; 12 | @import '~todomvc-app-css/index.css'; 13 | @import "./theme/theme.dark.scss"; 14 | @import "./theme/theme.light.scss"; 15 | ion-content { 16 | height: 100vh !important; 17 | } 18 | 19 | .transparent { 20 | visibility: hidden; 21 | background: transparent !important; 22 | } -------------------------------------------------------------------------------- /src/app/pages/list/dynamic-form/components/form-input/form-input.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewContainerRef } from '@angular/core'; 2 | import { FormGroup } from '@angular/forms'; 3 | 4 | import { Field } from '../../models/field.interface'; 5 | import { FieldConfig } from '../../models/field-config.interface'; 6 | 7 | @Component({ 8 | selector: 'form-input', 9 | styleUrls: ['form-input.component.scss'], 10 | template: ` 11 |
    14 | 15 | 19 |
    20 | ` 21 | }) 22 | export class FormInputComponent implements Field { 23 | config: FieldConfig; 24 | group: FormGroup; 25 | } 26 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode, ApplicationRef } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import { enableDebugTools } from '@angular/platform-browser'; 4 | 5 | import { AppModule } from './app/app.module'; 6 | import { environment } from './environments/environment'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | 13 | platformBrowserDynamic().bootstrapModule(AppModule) 14 | .then(moduleRef => { 15 | if (!environment.production) { 16 | const applicationRef = moduleRef.injector.get(ApplicationRef); 17 | const componentRef = applicationRef.components[0]; 18 | // allows to run `ng.profiler.timeChangeDetection();` 19 | enableDebugTools(componentRef); 20 | } 21 | }) 22 | .catch(err => console.log(err)); 23 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/app/pages/list/dynamic-form/components/form-select/form-select.component.scss: -------------------------------------------------------------------------------- 1 | select { 2 | -webkit-appearance: none; 3 | -moz-appearance: none; 4 | text-indent: .01px; 5 | text-overflow: ''; 6 | overflow: hidden; 7 | font-family: inherit; 8 | font-size: 14px; 9 | padding: 10px 15px; 10 | border-radius: 0; 11 | color: rgba(0, 0, 0, 0.7); 12 | border: 1px solid rgba(0, 0, 0, 0.1); 13 | width: 100%; 14 | outline: none; 15 | background: 16 | linear-gradient(45deg, transparent 50%, rgba(0, 0, 0, 0.7) 50%) 17 | no-repeat calc(100% - 20px) calc(1em + 4px), 18 | linear-gradient(135deg, rgba(0, 0, 0, 0.7) 50%, transparent 50%) 19 | no-repeat calc(100% - 15px) calc(1em + 4px); 20 | background-size: 5px 5px, 5px 5px; 21 | &:focus { 22 | border: 1px solid rgba(0, 0, 0, 0.4); 23 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.3); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "lib": ["es2017", "dom"], 12 | "baseUrl": "./src/", 13 | "paths": { 14 | "@app/env": ["environments/environment"], 15 | "@components/*": ["app/components/*"], 16 | "@services/*": ["app/services/*"], 17 | "@modals/*": ["app/modals/*"], 18 | "@directives/*": ["app/directives/*"], 19 | "@pipes/*": ["app/pipes/*"], 20 | "@app/*": ["app/*"], 21 | "@root/*": ["./*"], 22 | "echarts": ["../node_modules/echarts/dist/echarts.min.js"] 23 | }, 24 | "types": ["cordova", "cordova-plugin-file", "jasmine"] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | export const environment = { 6 | production: false, 7 | mode: 'dev', 8 | serverUrl: 'http://192.168.3.130:7001/' 9 | }; 10 | 11 | /* 12 | * In development mode, to ignore zone related error stack frames such as 13 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 14 | * import the following file, but please comment it out in production mode 15 | * because it will have performance impact when throw error 16 | */ 17 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /src/app/pages/list/dynamic-form/components/form-select/form-select.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormGroup } from '@angular/forms'; 3 | 4 | import { Field } from '../../models/field.interface'; 5 | import { FieldConfig } from '../../models/field-config.interface'; 6 | 7 | @Component({ 8 | selector: 'form-select', 9 | styleUrls: ['form-select.component.scss'], 10 | template: ` 11 |
    14 | 15 | 21 |
    22 | ` 23 | }) 24 | export class FormSelectComponent implements Field { 25 | config: FieldConfig; 26 | group: FormGroup; 27 | } 28 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ionic4-boilerplate 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/app/pages/list/list.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ 'HOME.List' | translate }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 23 | {{ item.title }} 24 |
    {{ item.note }}
    25 |
    26 |
    27 |
    28 | You navigated here from {{ selectedItem.title }} 29 |
    30 |
    31 | -------------------------------------------------------------------------------- /src/app/pages/list/list.router.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { CalendarPage } from './calendar/calendar.page'; 5 | import { EchartsPage } from './echarts/echarts'; 6 | import { DynamicFormPage } from './dynamic-form/dynamic-form'; 7 | import { ListPage } from './list.page'; 8 | 9 | const routes: Routes = [ 10 | { 11 | path: '', 12 | component: ListPage, 13 | }, 14 | { 15 | path: 'calendar', 16 | component: CalendarPage, 17 | }, 18 | { 19 | path: 'echarts', 20 | component: EchartsPage, 21 | }, 22 | { 23 | path: 'dynamicform', 24 | component: DynamicFormPage, 25 | }, 26 | { 27 | path: 'todo', 28 | loadChildren: './ngrxtodo/ngrxtodo.module#NgrxTodoPageModule', 29 | }, 30 | ]; 31 | 32 | @NgModule({ 33 | imports: [RouterModule.forChild(routes)], 34 | exports: [RouterModule], 35 | }) 36 | export class ListPageRoutingModule { } 37 | -------------------------------------------------------------------------------- /src/app/pages/list/ngrxtodo/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /src/app/pages/list/redux/filter/filter.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import * as FilterActions from './filter.actions'; 2 | import { FilterReducer } from './filter.reducer'; 3 | 4 | describe('Redux: FilterReducer', () => { 5 | 6 | it('should return "new filter" as new state', () => { 7 | const action = new FilterActions.SetFilterAction('new filter'); 8 | const newState = FilterReducer('old state', action); 9 | expect(newState).toEqual('new filter'); 10 | }); 11 | 12 | it('should return the same state with null action', () => { 13 | const action = null; 14 | const newState = FilterReducer('old state', action); 15 | expect(newState).toEqual('old state'); 16 | }); 17 | 18 | it('should return the same state with unknown action', () => { 19 | const action: any = new FilterActions.SetFilterAction('new filter'); 20 | action.type = 'what'; 21 | const newState = FilterReducer('old state', action); 22 | expect(newState).toEqual('old state'); 23 | }); 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /sh/release/package-and-upload(deprecated).sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [[ "$TRAVIS_BRANCH" != "$ION4_APP_UPLOAD_BRANCH" ]]; then 3 | echo "This is not a deployment branch, skipping IPA build and upload." 4 | exit 0 5 | fi 6 | 7 | ##################### 8 | # Make the ipa file # 9 | ##################### 10 | OUTPUTDIR="$PWD/platforms/ios/build/device" 11 | 12 | xcrun -log -sdk iphoneos \ 13 | PackageApplication -v "$OUTPUTDIR/$APP_NAME.app" \ 14 | -o "$OUTPUTDIR/$APP_NAME.ipa" 15 | 16 | ####################### 17 | # Upload to pgyer # 18 | ####################### 19 | if [[ "$TRAVIS_BRANCH" == "$ION4_APP_UPLOAD_BRANCH" ]]; then 20 | 21 | if [[ -z "$PGYER_APIKEY" ]]; then 22 | echo "Error: Missing PGYER APIKEY" 23 | exit 1 24 | fi 25 | 26 | echo "At $ION4_APP_UPLOAD_BRANCH branch, upload to pgyer." 27 | 28 | ./pgyer_upload.sh "$OUTPUTDIR/$APP_NAME.ipa" $PGYER_APIKEY 29 | 30 | if [[ $? -ne 0 ]]; then 31 | echo "Error: Fail uploading to pgyer" 32 | exit 1 33 | fi 34 | fi 35 | 36 | -------------------------------------------------------------------------------- /src/app/pages/list/ngrxtodo/ngrxtodo.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TODO 7 | 8 | 9 | 10 | 11 |
    12 |
    13 |

    todos

    14 | 15 |
    16 | 17 | 18 |
    19 | 29 |
    30 | -------------------------------------------------------------------------------- /sh/addkeys(deprecated).sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -v 2 | 3 | set -e 4 | 5 | # Custom keychain 6 | KEY_CHAIN=ios-build.keychain 7 | KEY_CHAIN_PASSWORD=travis 8 | 9 | # Set working directory 10 | CWD=$(pwd) 11 | cd "$(dirname "$0")" 12 | 13 | # Create a custom keychain 14 | security create-keychain -p $KEY_CHAIN_PASSWORD $KEY_CHAIN 15 | 16 | # Unlock the keychain 17 | security unlock-keychain -p $KEY_CHAIN_PASSWORD $KEY_CHAIN 18 | 19 | # Set keychain timeout to 1 hour for long builds 20 | # see http://www.egeek.me/2013/02/23/jenkins-and-xcode-user-interaction-is-not-allowed/ 21 | security set-keychain-settings -t 3600 -u $KEY_CHAIN 22 | 23 | # Set keychain search list 24 | security list-keychains -s $KEY_CHAIN 25 | 26 | # Make the custom keychain default, so xcodebuild will use it for signing 27 | security default-keychain -s $KEY_CHAIN 28 | 29 | # Add certificates to keychain and allow codesign to access them 30 | security import release/certificates/apple.cer -k $KEY_CHAIN -T /usr/bin/codesign 31 | 32 | # Restore working directory 33 | cd - 34 | -------------------------------------------------------------------------------- /src/app/pages/list/ngrxtodo/new-todo/new-todo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormControl, FormGroup, Validators } from '@angular/forms'; 3 | import { Store } from '@ngrx/store'; 4 | 5 | import { AppState } from './../../redux/ngrxtodo.reducer'; 6 | import * as TodoActions from './../../redux/todo/todo.actions'; 7 | 8 | @Component({ 9 | selector: 'app-new-todo', 10 | templateUrl: './new-todo.component.html' 11 | }) 12 | export class NewTodoComponent implements OnInit { 13 | 14 | textField: FormControl; 15 | 16 | constructor( 17 | private store: Store 18 | ) { 19 | this.textField = new FormControl('', [Validators.required]); 20 | } 21 | 22 | ngOnInit() { 23 | } 24 | 25 | saveTodo() { 26 | if (this.textField.valid) { 27 | const text: string = this.textField.value; 28 | const action = new TodoActions.AddTodoAction(text.trim()); 29 | this.store.dispatch(action); 30 | this.textField.setValue('', { emitEvent: false }); 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /doc/jenkins.md: -------------------------------------------------------------------------------- 1 | # jenkins 2 | 3 | [jenkins](https://jenkins.io/) 是一个自动化部署,支持自动对程序进行构建和发布,能够与 SVN 和 GIT 做集成,与日常开发部署流程结合,可以大大提高开发效率。 4 | 5 | ## 安装 6 | 7 | ```bash 8 | java -jar jenkins.war --httpPort=8080 9 | ``` 10 | 11 | 找到初始化密码,位置为 `C:\Users\you_user_name\.jenkins\secrets\initialAdminPassword` 12 | 13 | 个人选择安装了社区默认的插件 14 | 15 | 设置账户密码: username/password 16 | Jenkins URL: http://localhost:8080/ 17 | 18 | ### 基于 docker 19 | 20 | 拉取 jenkins 并运行容器,使用 `http://YOUR_IP:10080` 访问 21 | 22 | ```bash 23 | docker pull docker.io/jenkins/jenkins 24 | docker run --name jenkins -p 10080:8080 docker.io/jenkins/jenkins 25 | ``` 26 | 27 | ## 如何集成 Ionic 项目 28 | 29 | ### IOS 30 | 31 | 集成步骤还是有点小麻烦,有许多东西需要配置,具体可以参考下述两篇文章 32 | 33 | * [iOS持续集成—Jenkins(最新最全)](https://www.jianshu.com/p/9cb3d8c8c78d) 34 | * [Jenkins持续集成ionic iOS项目](https://www.jianshu.com/p/d7822a92b575) 35 | 36 | #### IOS 上传至蒲公英 37 | 38 | https://www.pgyer.com/doc/view/jenkins_ios 39 | 40 | ### Android 41 | 42 | #### Android 上传至蒲公英 43 | 44 | https://www.pgyer.com/doc/view/jenkins 45 | 46 | ## 参考 47 | 48 | * https://jenkins.io/doc , 支持 pdf 版下载,使用更便捷 -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: false, 28 | browsers: ['Chrome'], 29 | singleRun: true 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/shared/ion2-calendar/calendar.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { CommonModule } from '@angular/common'; 4 | import { IonicModule, ModalController } from '@ionic/angular'; 5 | import { CalendarController } from './calendar.controller'; 6 | import { CalendarService } from './services/calendar.service'; 7 | import { CALENDAR_COMPONENTS } from './components/index'; 8 | 9 | export function calendarController(modalCtrl: ModalController, calSvc: CalendarService) { 10 | return new CalendarController(modalCtrl, calSvc); 11 | } 12 | 13 | @NgModule({ 14 | imports: [CommonModule, IonicModule, FormsModule], 15 | declarations: CALENDAR_COMPONENTS, 16 | exports: CALENDAR_COMPONENTS, 17 | entryComponents: CALENDAR_COMPONENTS, 18 | providers: [ 19 | CalendarService, 20 | { 21 | provide: CalendarController, 22 | useFactory: calendarController, 23 | deps: [ModalController, CalendarService], 24 | }, 25 | ], 26 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 27 | }) 28 | export class CalendarModule {} 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 yipeng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/app/directives/debounce-click.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | Input, 4 | OnInit, 5 | HostListener, 6 | EventEmitter, 7 | Output, 8 | OnDestroy, 9 | } from '@angular/core'; 10 | import { Subject } from 'rxjs'; 11 | import { debounceTime } from 'rxjs/operators'; 12 | 13 | @Directive({ selector: '[appDebounceClickDirective]' }) 14 | export class DebounceClickDirective implements OnInit, OnDestroy { 15 | @Input() private debounceClickDirective; 16 | @Output() private debounceClick: EventEmitter = new EventEmitter(); 17 | private clicks = new Subject(); 18 | private debounceStream$; 19 | constructor() { 20 | // pass 21 | } 22 | public ngOnInit() { 23 | this.debounceStream$ = this.clicks 24 | .pipe(debounceTime(this.debounceClickDirective)) 25 | .subscribe(event => this.debounceClick.emit(event)); 26 | } 27 | public ngOnDestroy() { 28 | this.debounceStream$.unsubscribe(); 29 | } 30 | @HostListener('click', ['$event']) 31 | private clickEvent(event: MouseEvent) { 32 | event.preventDefault(); 33 | event.stopPropagation(); 34 | this.clicks.next(event); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { GlobalService } from '@services/global.service'; 4 | import { TranslateService } from '@ngx-translate/core'; 5 | import { EmitService } from '@services/emit.service'; 6 | 7 | @Component({ 8 | selector: 'app-tabs', 9 | templateUrl: 'tabs.page.html', 10 | styleUrls: ['tabs.page.scss'], 11 | }) 12 | export class TabsPage { 13 | constructor( 14 | public translate: TranslateService, 15 | public globalservice: GlobalService, 16 | public emit: EmitService 17 | ) { 18 | this.initTranslate(); 19 | } 20 | 21 | initTranslate() { 22 | this.translate.addLangs(['en', 'zh']); 23 | this.translate.setDefaultLang('en'); 24 | if (this.globalservice.languageType) { 25 | this.translate.use(this.globalservice.languageType); 26 | } else { 27 | const browserLang = this.translate.getBrowserLang(); 28 | this.translate.use(browserLang.match(/en|zh/) ? browserLang : 'en'); 29 | } 30 | 31 | this.emit.eventEmit.subscribe(val => { 32 | if (val === 'languageType') { 33 | this.translate.use(this.globalservice.languageType); 34 | } 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/pages/list/dynamic-form/dynamic-form.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { DynamicFieldDirective } from './components/dynamic-field/dynamic-field.directive'; 6 | import { DynamicFormComponent } from './containers/dynamic-form/dynamic-form.component'; 7 | import { FormButtonComponent } from './components/form-button/form-button.component'; 8 | import { FormInputComponent } from './components/form-input/form-input.component'; 9 | import { FormSelectComponent } from './components/form-select/form-select.component'; 10 | 11 | 12 | @NgModule({ 13 | imports: [ 14 | CommonModule, 15 | ReactiveFormsModule 16 | ], 17 | declarations: [ 18 | DynamicFieldDirective, 19 | DynamicFormComponent, 20 | FormButtonComponent, 21 | FormInputComponent, 22 | FormSelectComponent 23 | ], 24 | exports: [ 25 | DynamicFormComponent 26 | ], 27 | entryComponents: [ 28 | FormButtonComponent, 29 | FormInputComponent, 30 | FormSelectComponent 31 | ] 32 | }) 33 | export class DynamicFormModule { } 34 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.router.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { TabsPage } from './tabs.page'; 5 | import { HomePage } from '../pages/home/home.page'; 6 | import { TestPage } from '../pages/test/test.page'; 7 | 8 | const routes: Routes = [ 9 | { 10 | path: 'tabs', 11 | component: TabsPage, 12 | children: [ 13 | { 14 | path: '', 15 | component: HomePage, 16 | }, 17 | { 18 | path: 'home', 19 | component: HomePage, 20 | }, 21 | { 22 | path: 'test', 23 | component: TestPage, 24 | }, 25 | { 26 | path: 'list', 27 | children: [{ 28 | path: '', 29 | loadChildren: '../pages/list/list.module#ListPageModule', 30 | }] 31 | } 32 | ], 33 | }, 34 | { 35 | path: '', 36 | redirectTo: '/tabs/home', 37 | pathMatch: 'full', 38 | }, 39 | { 40 | path: 'list', 41 | redirectTo: '/tabs/list', 42 | pathMatch: 'full', 43 | }, 44 | ]; 45 | 46 | @NgModule({ 47 | imports: [RouterModule.forChild(routes)], 48 | exports: [RouterModule], 49 | }) 50 | export class TabsPageRoutingModule { } 51 | -------------------------------------------------------------------------------- /src/app/pages/list/ngrxtodo/ngrxtodo.page.ts: -------------------------------------------------------------------------------- 1 | import { Router } from '@angular/router'; 2 | import { Component } from '@angular/core'; 3 | import { Store } from '@ngrx/store'; 4 | import { AppState } from './../redux/ngrxtodo.reducer'; 5 | import { Todo } from './../redux/todo/todo.model'; 6 | import * as TodoActions from './../redux/todo/todo.actions'; 7 | 8 | @Component({ 9 | selector: 'page-ngrxtodo-module', 10 | templateUrl: './ngrxtodo.page.html', 11 | }) 12 | export class NgRxTodoComponent { 13 | constructor(private store: Store, private router: Router) { 14 | this.populateTodos(); 15 | this.updateTodos(); 16 | } 17 | 18 | private populateTodos() { 19 | const todos: Todo[] = JSON.parse( 20 | localStorage.getItem('angular-ngrx-todos') || 21 | '[{"id":1,"completed":false,"text":"test"}]' 22 | ); 23 | this.store.dispatch(new TodoActions.PopulateTodosAction(todos)); 24 | } 25 | 26 | private updateTodos() { 27 | this.store.select('todos').subscribe(todos => { 28 | if (todos) { 29 | localStorage.setItem('angular-ngrx-todos', JSON.stringify(todos)); 30 | } 31 | }); 32 | } 33 | 34 | backTolist() { 35 | this.router.navigateByUrl('tabs/(list:list)'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/pages/list/redux/todo/todo.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, createFeatureSelector } from '@ngrx/store'; 2 | import { AppState } from './../ngrxtodo.reducer'; 3 | import { Todo } from './todo.model'; 4 | 5 | export const getTodoState = createFeatureSelector('ngrxtodo'); 6 | 7 | export const getState = createSelector( 8 | getTodoState, 9 | (state: AppState) => { 10 | return state; 11 | } 12 | ); 13 | export const getFilter = createSelector( 14 | getTodoState, 15 | (state: AppState) => { 16 | return state.filter; 17 | } 18 | ); 19 | export const getTodos = createSelector( 20 | getTodoState, 21 | (state: AppState) => { 22 | return state.todos; 23 | } 24 | ); 25 | 26 | export const getStateCompleted = createSelector( 27 | getTodos, 28 | todos => { 29 | return todos.every(todo => todo.completed); 30 | } 31 | ); 32 | 33 | export const getVisibleTodos = createSelector( 34 | getTodos, 35 | getFilter, 36 | (todos: Todo[], filter: string) => { 37 | switch (filter) { 38 | default: 39 | case 'SHOW_ALL': 40 | return todos; 41 | case 'SHOW_COMPLETED': 42 | return todos.filter(t => t.completed); 43 | case 'SHOW_ACTIVE': 44 | return todos.filter(t => !t.completed); 45 | } 46 | } 47 | ); 48 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "short_name": "app", 4 | "theme_color": "#1976d2", 5 | "background_color": "#fafafa", 6 | "display": "standalone", 7 | "scope": "/", 8 | "start_url": "/", 9 | "icons": [ 10 | { 11 | "src": "assets/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "assets/icons/icon-96x96.png", 17 | "sizes": "96x96", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "assets/icons/icon-128x128.png", 22 | "sizes": "128x128", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "assets/icons/icon-144x144.png", 27 | "sizes": "144x144", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "assets/icons/icon-152x152.png", 32 | "sizes": "152x152", 33 | "type": "image/png" 34 | }, 35 | { 36 | "src": "assets/icons/icon-192x192.png", 37 | "sizes": "192x192", 38 | "type": "image/png" 39 | }, 40 | { 41 | "src": "assets/icons/icon-384x384.png", 42 | "sizes": "384x384", 43 | "type": "image/png" 44 | }, 45 | { 46 | "src": "assets/icons/icon-512x512.png", 47 | "sizes": "512x512", 48 | "type": "image/png" 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /sh/release/add-key(deprecated).sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [[ -z "$KEY_PASSWORD" ]]; then 3 | echo "Error: Missing password for adding private key" 4 | exit 1 5 | fi 6 | 7 | security create-keychain -p travis ios-build.keychain 8 | 9 | security import ./sh/release/certificates/apple.cer \ 10 | -k ~/Library/Keychains/ios-build.keychain \ 11 | -T /usr/bin/codesign 12 | 13 | security import ./sh/release/certificates/ios_distribution.cer \ 14 | -k ~/Library/Keychains/ios-build.keychain \ 15 | -T /usr/bin/codesign 16 | 17 | security import ./sh/release/certificates/ios_distribution.p12 \ 18 | -k ~/Library/Keychains/ios-build.keychain \ 19 | -P $KEY_PASSWORD \ 20 | -T /usr/bin/codesign 21 | 22 | security import ./sh/release/certificates/ios_develop.cer \ 23 | -k ~/Library/Keychains/ios-build.keychain \ 24 | -T /usr/bin/codesign 25 | 26 | security import ./sh/release/certificates/ios_develop.p12 \ 27 | -k ~/Library/Keychains/ios-build.keychain \ 28 | -P $KEY_PASSWORD \ 29 | -T /usr/bin/codesign 30 | 31 | security set-keychain-settings -t 3600 \ 32 | -l ~/Library/Keychains/ios-build.keychain 33 | 34 | security default-keychain -s ios-build.keychain 35 | 36 | mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles 37 | 38 | cp "./sh/release/certificates/$PROFILE_NAME.mobileprovision" ~/Library/MobileDevice/Provisioning\ Profiles/ -------------------------------------------------------------------------------- /sh/release/xcconfig(deprecated)/build-release.xcconfig.template: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | // 20 | 21 | // 22 | // XCode Build settings for "Release" Build Configuration. 23 | // 24 | 25 | #include "build.xcconfig" 26 | PROVISIONING_PROFILE = %PROFILE_UUID% 27 | 28 | CODE_SIGN_IDENTITY = %DEVELOPER_NAME% 29 | CODE_SIGN_IDENTITY[sdk=iphoneos*] = %DEVELOPER_NAME% 30 | 31 | #include "build-extras.xcconfig" 32 | 33 | // (CB-11792) 34 | // @COCOAPODS_SILENCE_WARNINGS@ // 35 | #include "../pods-release.xcconfig" -------------------------------------------------------------------------------- /src/app/pages/list/echarts/echarts.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { EChartOption } from 'echarts'; 3 | /** 4 | * Generated class for the EchartsPage page. 5 | * 6 | * See https://ionicframework.com/docs/components/#navigation for more info on 7 | * Ionic pages and navigation. 8 | */ 9 | 10 | @Component({ 11 | selector: 'page-echarts', 12 | templateUrl: 'echarts.html', 13 | }) 14 | export class EchartsPage { 15 | isAlwaysLight = false; 16 | 17 | languageType: string; 18 | 19 | options: EChartOption = { 20 | color: ['#3398DB'], 21 | grid: { 22 | left: '3%', 23 | right: '4%', 24 | bottom: '3%', 25 | containLabel: true 26 | }, 27 | xAxis: [ 28 | { 29 | type: 'category', 30 | data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], 31 | axisTick: { 32 | alignWithLabel: true 33 | } 34 | } 35 | ], 36 | yAxis: [ 37 | { 38 | type: 'value' 39 | } 40 | ], 41 | series: [ 42 | { 43 | name: 'Test', 44 | type: 'bar', 45 | barWidth: '60%', 46 | data: [10, 52, 200, 334, 390, 330, 220] 47 | } 48 | ] 49 | }; 50 | 51 | constructor( 52 | ) {} 53 | 54 | ionViewDidLoad() { 55 | console.log('ionViewDidLoad EchartsPage'); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /hooks/before_build/010_update_config.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Save hook under `project-root/hooks/before_prepare/` 4 | // https://gist.github.com/ohh2ahh/f35ff6e0d9f8b4268cdb 5 | // Don't forget to install xml2js using npm 6 | // `$ npm install xml2js` 7 | 8 | var fs = require('fs'); 9 | var xml2js = require('xml2js'); 10 | var packageJson = require('../../package.json'); 11 | 12 | // Read config.xml 13 | fs.readFile('config.xml', 'utf8', function(err, data) { 14 | if(err) { 15 | return console.log(err); 16 | } 17 | 18 | // Get XML 19 | var xml = data; 20 | 21 | // Parse XML to JS Obj 22 | xml2js.parseString(xml, function (err, result) { 23 | if(err) { 24 | return console.log(err); 25 | } 26 | // Get JS Obj 27 | var obj = result; 28 | obj['widget']['$']['version'] = packageJson.version; 29 | // console.log(obj); 30 | // Build XML from JS Obj 31 | var builder = new xml2js.Builder(); 32 | var xml = builder.buildObject(obj); 33 | 34 | // Write config.xml 35 | fs.writeFile('config.xml', xml, function(err) { 36 | if(err) { 37 | return console.log(err); 38 | } 39 | 40 | console.log('Build number successfully incremented'); 41 | }); 42 | 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/app/shared/ion2-calendar/components/calendar-week.component.scss: -------------------------------------------------------------------------------- 1 | :host .toolbar-background-md, 2 | :host .toolbar-background-ios { 3 | background: transparent; } 4 | 5 | :host .week-toolbar { 6 | --padding-start: 0; 7 | --padding-end: 0; 8 | --padding-bottom: 0; 9 | --padding-top: 0; } 10 | :host .week-toolbar.primary { 11 | --background: var(--ion-color-primary); } 12 | :host .week-toolbar.secondary { 13 | --background: var(--ion-color-secondary); } 14 | :host .week-toolbar.danger { 15 | --background: var(--ion-color-danger); } 16 | :host .week-toolbar.dark { 17 | --background: var(--ion-color-dark); } 18 | :host .week-toolbar.light { 19 | --background: var(--ion-color-light); } 20 | :host .week-toolbar.transparent { 21 | --background: transparent; } 22 | :host .week-toolbar.toolbar-md { 23 | min-height: 44px; } 24 | 25 | :host .week-title { 26 | margin: 0; 27 | height: 44px; 28 | width: 100%; 29 | padding: 15px 0; 30 | color: #fff; } 31 | :host .week-title.light, :host .week-title.transparent { 32 | color: #9e9e9e; } 33 | :host .week-title li { 34 | list-style-type: none; 35 | display: block; 36 | float: left; 37 | width: 14%; 38 | text-align: center; } 39 | :host .week-title li:nth-of-type(7n), 40 | :host .week-title li:nth-of-type(7n + 1) { 41 | width: 15%; } 42 | -------------------------------------------------------------------------------- /src/app/shared/ion2-calendar/calendar.controller.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ModalController } from '@ionic/angular'; 3 | import { OverlayEventDetail } from '@ionic/core'; 4 | 5 | import { ModalOptions, CalendarModalOptions } from './calendar.model'; 6 | import { CalendarModal } from './components/calendar.modal'; 7 | import { CalendarService } from './services/calendar.service'; 8 | 9 | @Injectable() 10 | export class CalendarController { 11 | constructor(public modalCtrl: ModalController, public calSvc: CalendarService) {} 12 | 13 | /** 14 | * @deprecated 15 | * @param {CalendarModalOptions} calendarOptions 16 | * @param {ModalOptions} modalOptions 17 | * @returns {any} 18 | */ 19 | openCalendar(calendarOptions: CalendarModalOptions, modalOptions: ModalOptions = {}): Promise<{}> { 20 | const options = this.calSvc.safeOpt(calendarOptions); 21 | 22 | return this.modalCtrl 23 | .create({ 24 | component: CalendarModal, 25 | componentProps: { 26 | options, 27 | }, 28 | ...modalOptions, 29 | }) 30 | .then((calendarModal: HTMLIonModalElement) => { 31 | calendarModal.present(); 32 | 33 | return calendarModal.onDidDismiss().then((event: OverlayEventDetail) => { 34 | return event.data ? Promise.resolve(event.data) : Promise.reject('cancelled'); 35 | }); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/shared/ion2-calendar/components/month-picker.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { CalendarMonth } from '../calendar.model'; 3 | import { defaults } from '../config'; 4 | 5 | @Component({ 6 | selector: 'ion-calendar-month-picker', 7 | styleUrls: ['./month-picker.component.scss'], 8 | template: ` 9 |
    10 |
    13 | 14 |
    15 |
    16 | `, 17 | }) 18 | export class MonthPickerComponent { 19 | @Input() 20 | month: CalendarMonth; 21 | @Input() 22 | color = defaults.COLOR; 23 | @Output() 24 | select: EventEmitter = new EventEmitter(); 25 | _thisMonth = new Date(); 26 | _monthFormat = defaults.MONTH_FORMAT; 27 | 28 | @Input() 29 | set monthFormat(value: string[]) { 30 | if (Array.isArray(value) && value.length === 12) { 31 | this._monthFormat = value; 32 | } 33 | } 34 | 35 | get monthFormat(): string[] { 36 | return this._monthFormat; 37 | } 38 | 39 | constructor() {} 40 | 41 | _onSelect(month: number): void { 42 | this.select.emit(month); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/pages/list/ngrxtodo/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Store, select } from '@ngrx/store'; 3 | 4 | import { AppState } from './../../redux/ngrxtodo.reducer'; 5 | import * as TodoActions from './../../redux/todo/todo.actions'; 6 | import { getFilter, getTodos } from './../../redux/todo/todo.selectors'; 7 | 8 | @Component({ 9 | selector: 'app-footer', 10 | templateUrl: './footer.component.html', 11 | }) 12 | export class FooterComponent implements OnInit { 13 | countTodos: number; 14 | currentFilter: string; 15 | showFooter: boolean; 16 | 17 | constructor(private store: Store) { 18 | this.readFilterState(); 19 | this.readTodosState(); 20 | } 21 | 22 | ngOnInit() {} 23 | 24 | clearCompleted() { 25 | const action = new TodoActions.ClearCompletedAction(); 26 | this.store.dispatch(action); 27 | } 28 | 29 | completedAll() { 30 | const action = new TodoActions.CompletedAllAction(); 31 | this.store.dispatch(action); 32 | } 33 | 34 | private readTodosState() { 35 | this.store.pipe(select(getTodos)).subscribe(todos => { 36 | this.countTodos = todos.filter(t => !t.completed).length; 37 | this.showFooter = todos.length > 0; 38 | }); 39 | } 40 | 41 | private readFilterState() { 42 | this.store.pipe(select(getFilter)).subscribe(fitler => { 43 | this.currentFilter = fitler; 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 统一管理系统中通用服务 3 | */ 4 | 5 | import { NgModule } from '@angular/core'; 6 | import { CommonModule } from '@angular/common'; 7 | import { HttpClientModule } from '@angular/common/http'; 8 | 9 | import { FileTransfer } from '@ionic-native/file-transfer/ngx'; 10 | import { FileOpener } from '@ionic-native/file-opener/ngx'; 11 | import { File } from '@ionic-native/file/ngx'; 12 | import { Insomnia } from '@ionic-native/insomnia/ngx'; 13 | import { Network } from '@ionic-native/network/ngx'; 14 | import { BackgroundMode } from '@ionic-native/background-mode/ngx'; 15 | import { StatusBar } from '@ionic-native/status-bar/ngx'; 16 | import { SplashScreen } from '@ionic-native/splash-screen/ngx'; 17 | import { LocalNotifications } from '@ionic-native/local-notifications/ngx'; 18 | import { AppCenterAnalytics } from '@ionic-native/app-center-analytics/ngx'; 19 | import { AppCenterCrashes } from '@ionic-native/app-center-crashes/ngx'; 20 | 21 | import { JPush } from '../../nativewrapper/jpush/ngx'; 22 | 23 | @NgModule({ 24 | imports: [CommonModule, HttpClientModule], 25 | exports: [], 26 | declarations: [], 27 | providers: [ 28 | StatusBar, 29 | SplashScreen, 30 | BackgroundMode, 31 | File, 32 | JPush, 33 | FileTransfer, 34 | FileOpener, 35 | Insomnia, 36 | Network, 37 | AppCenterAnalytics, 38 | AppCenterCrashes, 39 | LocalNotifications, 40 | ], 41 | }) 42 | export class CoreModule { } 43 | -------------------------------------------------------------------------------- /src/app/services/data.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * 后台数据请求服务 3 | */ 4 | import { Injectable } from '@angular/core'; 5 | import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http'; 6 | 7 | import { Observable } from 'rxjs'; 8 | import { map } from 'rxjs/operators'; 9 | import * as querystring from 'querystring'; 10 | import { Cacheable } from './offlinecache.service'; 11 | 12 | import { baseUrl } from '../config'; 13 | import { NativeService } from './native.service'; 14 | 15 | @Injectable({ 16 | providedIn: 'root' 17 | }) 18 | export class DataService { 19 | baseUrl: string = baseUrl; 20 | headers: HttpHeaders = new HttpHeaders({ 21 | 'Content-Type': 'application/x-www-form-urlencoded', 22 | }); 23 | 24 | constructor(public http: HttpClient, public native: NativeService) {} 25 | 26 | amapHttpUtil(url: string, options: Object): Observable { 27 | const params = new HttpParams({ 28 | fromString: querystring.stringify(options), 29 | }); 30 | return this.http.get(url, { 31 | headers: this.headers, 32 | params: params, 33 | }); 34 | } 35 | 36 | @Cacheable({ pool: 'test' }) 37 | testCachedData(): Observable { 38 | const obs = Observable.create(observer => { 39 | setTimeout(() => { 40 | observer.next(Math.random()); 41 | observer.complete(); 42 | }, 10); 43 | }); 44 | obs.pipe( 45 | map(res => { 46 | return res; 47 | }) 48 | ); 49 | return obs; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/shared/ion2-calendar/components/calendar-week.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { defaults } from '../config'; 3 | 4 | @Component({ 5 | selector: 'ion-calendar-week', 6 | styleUrls: ['./calendar-week.component.scss'], 7 | template: ` 8 | 9 |
      10 |
    • {{ w }}
    • 11 |
    12 |
    13 | `, 14 | }) 15 | export class CalendarWeekComponent { 16 | _weekArray: string[] = defaults.WEEKS_FORMAT; 17 | _displayWeekArray: string[] = this._weekArray; 18 | _weekStart = 0; 19 | @Input() 20 | color: string = defaults.COLOR; 21 | 22 | constructor() {} 23 | 24 | @Input() 25 | set weekArray(value: string[]) { 26 | if (value && value.length === 7) { 27 | this._weekArray = [...value]; 28 | this.adjustSort(); 29 | } 30 | } 31 | 32 | @Input() 33 | set weekStart(value: number) { 34 | if (value === 0 || value === 1) { 35 | this._weekStart = value; 36 | this.adjustSort(); 37 | } 38 | } 39 | 40 | adjustSort(): void { 41 | if (this._weekStart === 1) { 42 | const cacheWeekArray = [...this._weekArray]; 43 | cacheWeekArray.push(cacheWeekArray.shift()); 44 | this._displayWeekArray = [...cacheWeekArray]; 45 | } else if (this._weekStart === 0) { 46 | this._displayWeekArray = [...this._weekArray]; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/shared/ion2-calendar/components/calendar.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | padding: 10px 20px; 3 | box-sizing: border-box; 4 | display: inline-block; 5 | background-color: #fff; 6 | width: 100%; } 7 | :host .title { 8 | padding: 0 40px 0 40px; 9 | overflow: hidden; } 10 | :host .title .back, 11 | :host .title .forward, 12 | :host .title .switch-btn { 13 | display: block; 14 | position: relative; 15 | float: left; 16 | min-height: 32px; 17 | margin: 0; 18 | padding: 0; 19 | font-size: 15px; } 20 | :host .title .back, 21 | :host .title .forward { 22 | color: #757575; } 23 | :host .title .back { 24 | margin-left: -100%; 25 | left: -40px; 26 | width: 40px; } 27 | :host .title .forward { 28 | margin-left: -40px; 29 | right: -40px; 30 | width: 40px; } 31 | :host .title .switch-btn { 32 | --margin-top: 0; 33 | --margin-bottom: 0; 34 | --margin-start: auto; 35 | --margin-end: auto; 36 | width: 100%; 37 | text-align: center; 38 | line-height: 32px; 39 | color: #757575; } 40 | :host .title .switch-btn .arrow-dropdown { 41 | margin-left: 5px; } 42 | :host .days.between .days-btn.is-last, 43 | :host .days.between .days-btn.is-first { 44 | border-radius: 0; } 45 | :host .component-mode .days.startSelection.is-last-wrap::after { 46 | border-radius: 0; } 47 | :host .component-mode .days.endSelection.is-first-wrap::after { 48 | border-radius: 0; } 49 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { IonicModule } from '@ionic/angular'; 4 | import { CommonModule } from '@angular/common'; 5 | import { FormsModule } from '@angular/forms'; 6 | 7 | import { PipesModule } from '@pipes/pipes.module'; 8 | import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; 9 | import { TranslateHttpLoader } from '@ngx-translate/http-loader'; 10 | import { HttpClient } from '@angular/common/http'; 11 | 12 | import { DebounceClickDirective } from '@directives/debounce-click.directive'; 13 | import { TrackEventDirective } from '@directives/trackEvent.directive'; 14 | 15 | import { CalendarModule } from './ion2-calendar'; 16 | 17 | // AoT requires an exported function for factories 18 | export function HttpLoaderFactory(httpClient: HttpClient) { 19 | return new TranslateHttpLoader(httpClient, 'assets/i18n/', '.json'); 20 | } 21 | 22 | @NgModule({ 23 | imports: [ 24 | IonicModule, 25 | CommonModule, 26 | FormsModule, 27 | PipesModule, 28 | CalendarModule, 29 | TranslateModule.forRoot({ 30 | loader: { 31 | provide: TranslateLoader, 32 | useFactory: HttpLoaderFactory, 33 | deps: [HttpClient], 34 | }, 35 | }), 36 | ], 37 | declarations: [DebounceClickDirective, TrackEventDirective], 38 | providers: [], // better be empty! 39 | exports: [ 40 | PipesModule, 41 | TranslateModule, 42 | IonicModule, 43 | CommonModule, 44 | FormsModule, 45 | CalendarModule, 46 | ], 47 | }) 48 | export class SharedModule { } 49 | -------------------------------------------------------------------------------- /hooks/after_platform_add/update-release-xcconfig.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Update build-release.xcconfig with correct profile and developer values for app signing 5 | */ 6 | var path = require('path'); 7 | var fs = require('fs'); 8 | 9 | // @deprecated 10 | fs.exists("../../platforms/ios", function (exists) { 11 | return true; 12 | if (exists) { 13 | var PROFILE_UUID_TEMPLATE_VAL = '%PROFILE_UUID%'; 14 | var PROFILE_UUID_ENV_VAR = process.env.PROFILE_UUID; 15 | var DEVELOPER_NAME_REGEX_GLOBAL = /%DEVELOPER_NAME%/g; 16 | var DEVELOPER_NAME_ENV_VAR = process.env.DEVELOPER_NAME; 17 | 18 | var xcconfigFinal = path.resolve(__dirname, '../../platforms/ios/cordova/build-release.xcconfig'); 19 | var xcconfigTemplate = path.resolve(__dirname, '../../sh/release/xcconfig/build-release.xcconfig.template'); 20 | 21 | fs.readFile(xcconfigTemplate, 'utf8', function (err, data) { 22 | if (err) { 23 | return console.log(err); 24 | } 25 | 26 | var result = data.replace(PROFILE_UUID_TEMPLATE_VAL, PROFILE_UUID_ENV_VAR) 27 | .replace(DEVELOPER_NAME_REGEX_GLOBAL, DEVELOPER_NAME_ENV_VAR); 28 | 29 | fs.writeFile(xcconfigFinal, result, 'utf8', function (err) { 30 | if (err) return console.log('No directory Found for cordova iOS! Skipping xcconfig creation. ', err); 31 | console.log('Cordova iOS build-release.xcconfig updated with profile and developer values.') 32 | }); 33 | }); 34 | } 35 | }); 36 | 37 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { TabsPage } from './tabs.page'; 4 | import { HomePage } from '../pages/home/home.page'; 5 | import { TestPage } from '../pages/test/test.page'; 6 | import { TabsPageRoutingModule } from './tabs.router.module'; 7 | import { SharedModule } from './../shared/shared.module'; 8 | import { RouterTestingModule } from '@angular/router/testing'; 9 | 10 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 11 | 12 | import { 13 | IonicRouteStrategy, 14 | } from '@ionic/angular'; 15 | import { RouteReuseStrategy } from '@angular/router'; 16 | 17 | describe('TabsPage', () => { 18 | let component: TabsPage; 19 | let fixture: ComponentFixture; 20 | beforeEach(async(() => { 21 | TestBed.configureTestingModule({ 22 | imports: [ 23 | SharedModule, 24 | TabsPageRoutingModule, 25 | RouterTestingModule, 26 | HttpClientTestingModule 27 | ], 28 | declarations: [TabsPage, HomePage, TestPage], 29 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 30 | providers: [ 31 | { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, 32 | ], 33 | }).compileComponents(); 34 | })); 35 | 36 | beforeEach(() => { 37 | fixture = TestBed.createComponent(TabsPage); 38 | component = fixture.componentInstance; 39 | fixture.detectChanges(); 40 | }); 41 | 42 | it('should create', () => { 43 | expect(component).toBeTruthy(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | {{ 'HOME.Menu' | translate }} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {{ 'HOME.Home' | translate }} 18 | 19 | 20 | 21 | 22 | 23 | 24 | {{ 'HOME.List' | translate }} 25 | 26 | 27 | 28 | 29 | 30 | 31 | {{ 'HOME.Test' | translate }} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/app/pages/list/ngrxtodo/ngrxtodo.module.ts: -------------------------------------------------------------------------------- 1 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 2 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | import { RouterModule, Routes } from '@angular/router'; 5 | import { StoreModule } from '@ngrx/store'; 6 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 7 | import { ngrxtodoReducer } from './../redux/ngrxtodo.reducer'; 8 | 9 | import { NgRxTodoComponent } from './ngrxtodo.page'; 10 | import { TodoComponent } from './todo/todo.component'; 11 | import { TodoListComponent } from './todo-list/todo-list.component'; 12 | import { FooterComponent } from './footer/footer.component'; 13 | import { NewTodoComponent } from './new-todo/new-todo.component'; 14 | 15 | const routes: Routes = [ 16 | { 17 | path: '', 18 | component: NgRxTodoComponent, 19 | children: [ 20 | { 21 | path: '', 22 | component: TodoListComponent, 23 | }, 24 | { 25 | path: ':filter', 26 | component: TodoListComponent, 27 | }, 28 | ], 29 | }, 30 | ]; 31 | 32 | @NgModule({ 33 | declarations: [ 34 | NgRxTodoComponent, 35 | TodoComponent, 36 | TodoListComponent, 37 | FooterComponent, 38 | NewTodoComponent, 39 | ], 40 | imports: [ 41 | CommonModule, 42 | FormsModule, 43 | ReactiveFormsModule, 44 | RouterModule.forChild(routes), 45 | StoreModule.forRoot({}), 46 | StoreModule.forFeature('ngrxtodo', ngrxtodoReducer), 47 | StoreDevtoolsModule.instrument({ 48 | maxAge: 15, // Retains last 15 states 49 | }), 50 | ], 51 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 52 | }) 53 | export class NgrxTodoPageModule {} 54 | -------------------------------------------------------------------------------- /src/app/directives/trackEvent.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | Input, 4 | OnInit, 5 | HostListener, 6 | OnDestroy, 7 | } from '@angular/core'; 8 | import { Subject } from 'rxjs'; 9 | import { debounceTime } from 'rxjs/operators'; 10 | import { AppCenterAnalytics, StringMap } from '@ionic-native/app-center-analytics/ngx'; 11 | 12 | @Directive({ selector: '[appTrackEventDirective]' }) 13 | export class TrackEventDirective implements OnInit, OnDestroy { 14 | @Input() private trackParams: { 15 | evtName; 16 | parames: StringMap 17 | }; 18 | private clicks = new Subject(); 19 | private trackStream$; 20 | constructor(private appCenterAnalytics: AppCenterAnalytics) { 21 | // pass 22 | } 23 | public ngOnInit() { 24 | this.trackStream$ = this.clicks 25 | .pipe(debounceTime(100)).subscribe(evt => { 26 | if (!this.trackParams.evtName) { 27 | console.error('evtName undefined'); 28 | } 29 | if (!this.trackParams.parames) { 30 | console.error('parames undefined'); 31 | } 32 | this.appCenterAnalytics.setEnabled(true).then(() => { 33 | this.appCenterAnalytics.trackEvent(this.trackParams.evtName, this.trackParams.parames).then(() => { 34 | console.log(this.trackParams.evtName + ' event tracked'); 35 | }); 36 | }); 37 | }); 38 | } 39 | public ngOnDestroy() { 40 | this.trackStream$.unsubscribe(); 41 | } 42 | @HostListener('click', ['$event']) 43 | private clickEvent(event: MouseEvent) { 44 | event.preventDefault(); 45 | event.stopPropagation(); 46 | this.clicks.next(event); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/pages/list/redux/todo/todo.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { Todo } from './todo.model'; 3 | 4 | export const ADD_TODO = '[TODO] add'; 5 | export const DELETE_TODO = '[TODO] delete'; 6 | export const TOGGLE_TODO = '[TODO] toggle'; 7 | export const UPDATE_TODO = '[TODO] update'; 8 | export const POPULATE_TODOS = '[TODO] populate'; 9 | export const CLEAR_COMPLETED_TODO = '[TODO] clear completed'; 10 | export const COMPLETE_ALL_TODO = '[TODO] complete all'; 11 | 12 | export class AddTodoAction implements Action { 13 | readonly type = ADD_TODO; 14 | public id: number; 15 | 16 | constructor( 17 | public text: string 18 | ) { 19 | this.id = Math.random(); 20 | } 21 | } 22 | 23 | export class PopulateTodosAction implements Action { 24 | readonly type = POPULATE_TODOS; 25 | 26 | constructor( 27 | public todos: Todo[] 28 | ) {} 29 | } 30 | 31 | export class DeleteTodoAction implements Action { 32 | readonly type = DELETE_TODO; 33 | 34 | constructor( 35 | public id: number 36 | ) {} 37 | } 38 | 39 | export class ToggleAction implements Action { 40 | readonly type = TOGGLE_TODO; 41 | 42 | constructor( 43 | public id: number 44 | ) {} 45 | } 46 | 47 | export class UpdateAction implements Action { 48 | readonly type = UPDATE_TODO; 49 | 50 | constructor( 51 | public id: number, 52 | public text: string, 53 | ) {} 54 | } 55 | 56 | export class ClearCompletedAction implements Action { 57 | readonly type = CLEAR_COMPLETED_TODO; 58 | } 59 | 60 | export class CompletedAllAction implements Action { 61 | readonly type = COMPLETE_ALL_TODO; 62 | } 63 | 64 | export type TodoActionType = 65 | AddTodoAction | 66 | PopulateTodosAction | 67 | ToggleAction | 68 | DeleteTodoAction | 69 | UpdateAction | 70 | ClearCompletedAction | 71 | CompletedAllAction; 72 | -------------------------------------------------------------------------------- /src/app/pages/list/redux/todo/todo.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Todo } from './todo.model'; 2 | import * as TodoActions from './todo.actions'; 3 | 4 | const initialState: Todo[] = []; 5 | 6 | export function TodosReducer( 7 | state: Todo[] = initialState, 8 | action: TodoActions.TodoActionType 9 | ) { 10 | switch (action.type) { 11 | case TodoActions.ADD_TODO: { 12 | return [ 13 | ...state, 14 | { 15 | id: action.id, 16 | text: action.text, 17 | completed: false, 18 | }, 19 | ]; 20 | } 21 | case TodoActions.POPULATE_TODOS: { 22 | return action.todos; 23 | } 24 | case TodoActions.TOGGLE_TODO: { 25 | return state.map(todo => { 26 | if (action.id === todo.id) { 27 | return { 28 | ...todo, 29 | completed: !todo.completed, 30 | }; 31 | } else { 32 | return todo; 33 | } 34 | }); 35 | } 36 | case TodoActions.DELETE_TODO: { 37 | return state.filter(todo => action.id !== todo.id); 38 | } 39 | case TodoActions.UPDATE_TODO: { 40 | return state.map(todo => { 41 | if (action.id === todo.id) { 42 | return { 43 | ...todo, 44 | text: action.text, 45 | }; 46 | } else { 47 | return todo; 48 | } 49 | }); 50 | } 51 | case TodoActions.CLEAR_COMPLETED_TODO: { 52 | return state.filter(todo => !todo.completed); 53 | } 54 | case TodoActions.COMPLETE_ALL_TODO: { 55 | const areAllMarked = state.every(todo => todo.completed); 56 | return state.map(todo => { 57 | return { 58 | ...todo, 59 | completed: !areAllMarked, 60 | }; 61 | }); 62 | } 63 | default: { 64 | return state; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/pages/list/dynamic-form/dynamic-form.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, AfterViewInit } from '@angular/core'; 2 | import { Validators } from '@angular/forms'; 3 | 4 | import { FieldConfig } from './models/field-config.interface'; 5 | import { DynamicFormComponent } from './containers/dynamic-form/dynamic-form.component'; 6 | 7 | @Component({ 8 | selector: 'page-dynamic-form', 9 | templateUrl: 'dynamic-form.html', 10 | }) 11 | export class DynamicFormPage implements AfterViewInit { 12 | @ViewChild(DynamicFormComponent) form: DynamicFormComponent; 13 | 14 | config: FieldConfig[] = [ 15 | { 16 | type: 'input', 17 | label: 'Full name', 18 | name: 'name', 19 | placeholder: 'Enter your name', 20 | validation: [Validators.required, Validators.minLength(4)] 21 | }, 22 | { 23 | type: 'select', 24 | label: 'Favourite Food', 25 | name: 'food', 26 | options: ['Pizza', 'Hot Dogs', 'Knakworstje', 'Coffee'], 27 | placeholder: 'Select an option', 28 | validation: [Validators.required] 29 | }, 30 | { 31 | label: 'Submit', 32 | name: 'submit', 33 | type: 'button' 34 | } 35 | ]; 36 | 37 | ngAfterViewInit() { 38 | setTimeout(() => { 39 | let previousValid = this.form.valid; 40 | this.form.changes.subscribe(() => { 41 | if (this.form.valid !== previousValid) { 42 | previousValid = this.form.valid; 43 | this.form.setDisabled('submit', !previousValid); 44 | } 45 | }); 46 | 47 | this.form.setDisabled('submit', true); 48 | this.form.setValue('name', 'Todd Motto'); 49 | }); 50 | } 51 | 52 | submit(value: { [name: string]: any }) { 53 | console.log(value); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/app/pages/list/ngrxtodo/todo/todo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, ViewChild, ElementRef } from '@angular/core'; 2 | import { FormControl, Validators } from '@angular/forms'; 3 | import { Store } from '@ngrx/store'; 4 | 5 | import { AppState } from './../../redux/ngrxtodo.reducer'; 6 | import { Todo } from './../../redux/todo/todo.model'; 7 | import * as TodoActions from './../../redux/todo/todo.actions'; 8 | 9 | @Component({ 10 | selector: 'page-ngrxtodo', 11 | templateUrl: './todo.component.html' 12 | }) 13 | export class TodoComponent implements OnInit { 14 | 15 | @Input() todo: Todo; 16 | @ViewChild('textInput') textInput: ElementRef; 17 | textField: FormControl; 18 | checkField: FormControl; 19 | editing: boolean; 20 | 21 | constructor( 22 | private store: Store 23 | ) { 24 | this.textField = new FormControl('', [Validators.required]); 25 | this.checkField = new FormControl(false); 26 | this.checkField.valueChanges 27 | .subscribe(state => { 28 | const action = new TodoActions.ToggleAction(this.todo.id); 29 | this.store.dispatch(action); 30 | }); 31 | } 32 | 33 | ngOnInit() { 34 | this.textField.setValue(this.todo.text); 35 | this.checkField.setValue(this.todo.completed, {emitEvent: false}); 36 | } 37 | 38 | updateText() { 39 | if (this.textField.valid && this.editing) { 40 | const id = this.todo.id; 41 | const newText: string = this.textField.value; 42 | const action = new TodoActions.UpdateAction(id, newText.trim()); 43 | this.store.dispatch(action); 44 | this.editing = false; 45 | } 46 | } 47 | 48 | activeEditMode() { 49 | this.editing = true; 50 | setTimeout(() => { 51 | this.textInput.nativeElement.focus(); 52 | }); 53 | } 54 | 55 | deleteTodo() { 56 | const id = this.todo.id; 57 | const action = new TodoActions.DeleteTodoAction(id); 58 | this.store.dispatch(action); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/app/pages/list/dynamic-form/components/dynamic-field/dynamic-field.directive.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFactoryResolver, ComponentRef, Directive, Input, OnChanges, OnInit, Type, ViewContainerRef } from '@angular/core'; 2 | import { FormGroup } from '@angular/forms'; 3 | 4 | import { FormButtonComponent } from '../form-button/form-button.component'; 5 | import { FormInputComponent } from '../form-input/form-input.component'; 6 | import { FormSelectComponent } from '../form-select/form-select.component'; 7 | 8 | import { Field } from '../../models/field.interface'; 9 | import { FieldConfig } from '../../models/field-config.interface'; 10 | 11 | const components: {[type: string]: Type} = { 12 | button: FormButtonComponent, 13 | input: FormInputComponent, 14 | select: FormSelectComponent 15 | }; 16 | 17 | @Directive({ 18 | selector: '[dynamicField]' 19 | }) 20 | export class DynamicFieldDirective implements Field, OnChanges, OnInit { 21 | @Input() 22 | config: FieldConfig; 23 | 24 | @Input() 25 | group: FormGroup; 26 | 27 | component: ComponentRef; 28 | 29 | constructor( 30 | private resolver: ComponentFactoryResolver, 31 | private container: ViewContainerRef 32 | ) {} 33 | 34 | ngOnChanges() { 35 | if (this.component) { 36 | this.component.instance.config = this.config; 37 | this.component.instance.group = this.group; 38 | } 39 | } 40 | 41 | ngOnInit() { 42 | if (!components[this.config.type]) { 43 | const supportedTypes = Object.keys(components).join(', '); 44 | throw new Error( 45 | `Trying to use an unsupported type (${this.config.type}). 46 | Supported types: ${supportedTypes}` 47 | ); 48 | } 49 | const component = this.resolver.resolveComponentFactory(components[this.config.type]); 50 | this.component = this.container.createComponent(component); 51 | this.component.instance.config = this.config; 52 | this.component.instance.group = this.group; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /doc/docker.md: -------------------------------------------------------------------------------- 1 | # Docker 2 | 3 | ## 镜像切换 4 | 5 | 鉴于国内网络问题,后续拉取 Docker 镜像十分缓慢,需要配置加速器来解决, 6 | 新版的 Docker 使用 `/etc/docker/daemon.json(Linux)` 或者 `%programdata%\docker\config\daemon.json(Windows)` 来配置 Daemon。 7 | 请在该配置文件中加入(没有该文件的话,先建一个) 8 | 9 | ```json 10 | { 11 | "registry-mirrors": ["http://hub-mirror.c.163.com"] 12 | } 13 | ``` 14 | 15 | ## 安装 16 | 17 | 参考教程: http://www.runoob.com/docker/windows-docker-install.html (文中有提到在 win10 环境下需要开启 hyperV 功能,实际上只支持 win10 专业版) 18 | 19 | windows 安装非常简单,主要会安装以下几个组件 20 | 21 | - Kitematic,为 Windows提供了与 Mac 相同的用户体验和功能,支持 powershell 等,可视化方式从 docker hub 拉取镜像和运行镜像 22 | - Docker Machine,可以让你在windows的命令行中运行docker引擎命令 23 | - Docker Cli,用来运行docker引擎创建镜像和容器 24 | - Docker Compose. 用来运行docker-compose命令 25 | - Docker Quickstart Terminal,是一个已经配置好 Docker 的命令行环境 26 | - VirtualBox, 虚拟机 27 | 28 | ## 镜像 29 | 30 | ### ionic-docker 31 | 32 | 可以基于 docker 镜像进行搭建,如`ionic-docker`, Github 地址: https://github.com/marcoturi/ionic-docker 33 | 34 | > 作者只是修改了这个项目的一些配置,连文档都没改过来,该项目主要源自: https://hub.docker.com/r/agileek/ionic-framework 35 | 36 | 运行以下命令就 ok 37 | 38 | ```bash 39 | alias ionic="docker run -ti --rm --net host --privileged -v /dev/bus/usb:/dev/bus/usb -v ~/.gradle:/root/.gradle -v $PWD:/myApp:rw marcoturi/ionic ionic" 40 | ``` 41 | 42 | #### windows 下存在问题 43 | 44 | windows 下环境下运行一直识别不出 ionic 项目,按照 Issue 中修改 myApp 为 Sources 也不行。后知后觉,是 windows 本机上的磁盘没有挂载到虚拟机,挂载后就没问题了。 45 | 46 | ### docker-ionic 47 | 48 | https://github.com/beevelop/docker-ionic 49 | 50 | ## 报错 51 | 52 | - boot2docker.iso,不一定能下下来,会导致报错 `wsarecv: An existing connection was forcibly closed by the remote host.` 个人因为装了代理,后来关闭代理后才解决 53 | - 和 windows 结合开发有各种不确定的问题,一方面 windows 不是直接的宿主机而是依赖于 linux 虚拟机,虚拟机自然有其局限性,此外,对于两者之间的环境和目录处理需要倍加小心 54 | 55 | ## 参考 56 | 57 | - 镜像推荐1: https://github.com/beevelop/docker-ionic 58 | - 镜像推荐2: https://github.com/marcoturi/ionic-docker 59 | - https://hub.docker.com/r/agileek/ionic-framework/~/dockerfile/ 60 | - https://github.com/svenlaater/travis-ci-ionic-yml/blob/master/Dockerfile-node-java-android -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { ErrorHandler, NgModule } from '@angular/core'; 3 | import { RouteReuseStrategy } from '@angular/router'; 4 | import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; 5 | // import { IonicStorageModule } from '@ionic/Storage'; 6 | import { ServiceWorkerModule } from '@angular/service-worker'; 7 | 8 | import { EffectsModule } from '@ngrx/effects'; 9 | import { StoreModule } from '@ngrx/store'; 10 | 11 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 12 | import { RebirthHttpModule } from 'rebirth-http'; 13 | 14 | import { CoreModule } from './core/core.module'; 15 | import { SharedModule } from './shared/shared.module'; 16 | import { environment } from '../environments/environment'; 17 | import { QRScannerModalModule } from './modals/qr-scanner/qr-scanner.module'; 18 | 19 | import { MyApp } from './app.component'; 20 | import { AppRoutingModule } from './app-routing.module'; 21 | 22 | import { MyErrorHandler } from './error.handler'; 23 | import { RavenErrorHandler } from './raven-error-handler.'; 24 | import { MYHttpInterceptor } from './http.interceptor.service'; 25 | 26 | @NgModule({ 27 | declarations: [MyApp], 28 | imports: [ 29 | AppRoutingModule, 30 | CoreModule, 31 | SharedModule, 32 | BrowserModule, 33 | QRScannerModalModule, 34 | // IonicStorageModule.forRoot(), 35 | IonicModule.forRoot(), 36 | StoreModule.forRoot({}), 37 | EffectsModule.forRoot([]), 38 | ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }), 39 | ], 40 | bootstrap: [MyApp], 41 | entryComponents: [MyApp], 42 | providers: [ 43 | { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, 44 | RebirthHttpModule, 45 | { 46 | provide: HTTP_INTERCEPTORS, 47 | useClass: MYHttpInterceptor, 48 | multi: true 49 | } 50 | // { provide: ErrorHandler, useClass: MyErrorHandler }, 51 | // { provide: ErrorHandler, useClass: RavenErrorHandler } 52 | ], 53 | }) 54 | export class AppModule { } 55 | -------------------------------------------------------------------------------- /src/app/pages/list/list.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | import { GlobalService } from '@services/global.service'; 5 | import { TranslateService } from '@ngx-translate/core'; 6 | import { EmitService } from '@services/emit.service'; 7 | 8 | @Component({ 9 | selector: 'page-list', 10 | templateUrl: 'list.page.html', 11 | }) 12 | export class ListPage { 13 | selectedItem: any; 14 | icons: string[]; 15 | items: Array<{ title: string; note: string; icon: string }>; 16 | 17 | constructor( 18 | public route: Router, 19 | public translate: TranslateService, 20 | public globalservice: GlobalService, 21 | public emit: EmitService 22 | ) { 23 | this.items = [ 24 | { 25 | title: 'echarts', 26 | note: '', 27 | icon: 'speedometer', 28 | }, 29 | { 30 | title: 'todo', 31 | note: '', 32 | icon: 'create', 33 | }, 34 | { 35 | title: 'calendar', 36 | note: '', 37 | icon: 'calendar', 38 | }, 39 | { 40 | title: 'dynamicform', 41 | note: '', 42 | icon: 'paper', 43 | }, 44 | ]; 45 | 46 | this.initTranslate(); 47 | } 48 | 49 | toHome(event) { 50 | this.route.navigate(['tabs']); 51 | } 52 | 53 | doRefresh(refresher) { 54 | console.log('Begin async operation', refresher); 55 | 56 | setTimeout(() => { 57 | console.log('Async operation has ended'); 58 | refresher.complete(); 59 | }, 600); 60 | } 61 | 62 | initTranslate() { 63 | this.translate.addLangs(['en', 'zh']); 64 | this.translate.setDefaultLang('en'); 65 | if (this.globalservice.languageType) { 66 | this.translate.use(this.globalservice.languageType); 67 | } else { 68 | const browserLang = this.translate.getBrowserLang(); 69 | this.translate.use(browserLang.match(/en|zh/) ? browserLang : 'en'); 70 | } 71 | 72 | this.emit.eventEmit.subscribe(val => { 73 | if (val === 'languageType') { 74 | this.translate.use(this.globalservice.languageType); 75 | } 76 | }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/pages/list/ngrxtodo/todo-list/todo-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormControl } from '@angular/forms'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | import { Store, select } from '@ngrx/store'; 5 | 6 | import { AppState } from './../../redux/ngrxtodo.reducer'; 7 | import { Todo } from './../../redux/todo/todo.model'; 8 | import * as FilterActions from './../../redux/filter/filter.actions'; 9 | import * as TodoActions from './../../redux/todo/todo.actions'; 10 | import { 11 | getVisibleTodos, 12 | getStateCompleted, 13 | } from './../../redux/todo/todo.selectors'; 14 | 15 | @Component({ 16 | selector: 'page-ngrxtodo-list', 17 | templateUrl: './todo-list.component.html', 18 | }) 19 | export class TodoListComponent implements OnInit { 20 | todos: Todo[] = []; 21 | checkField: FormControl; 22 | 23 | constructor(private store: Store, private route: ActivatedRoute) { 24 | this.checkField = new FormControl(); 25 | this.readParams(); 26 | this.readStateCompleted(); 27 | this.readTodosState(); 28 | } 29 | 30 | ngOnInit() {} 31 | 32 | toggleAll() { 33 | this.store.dispatch(new TodoActions.CompletedAllAction()); 34 | } 35 | 36 | private setFilter(filter: string) { 37 | switch (filter) { 38 | case 'active': { 39 | this.store.dispatch(new FilterActions.SetFilterAction('SHOW_ACTIVE')); 40 | break; 41 | } 42 | case 'completed': { 43 | this.store.dispatch( 44 | new FilterActions.SetFilterAction('SHOW_COMPLETED') 45 | ); 46 | break; 47 | } 48 | default: { 49 | this.store.dispatch(new FilterActions.SetFilterAction('SHOW_ALL')); 50 | break; 51 | } 52 | } 53 | } 54 | 55 | private readTodosState() { 56 | this.store.pipe(select(getVisibleTodos)).subscribe(todos => { 57 | this.todos = todos; 58 | }); 59 | } 60 | 61 | private readStateCompleted() { 62 | this.store.pipe(select(getStateCompleted)).subscribe(status => { 63 | this.checkField.setValue(status); 64 | }); 65 | } 66 | 67 | private readParams() { 68 | this.route.params.subscribe(params => { 69 | this.setFilter(params.filter); 70 | }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/nativewrapper/jpush/ngx/index.metadata.json: -------------------------------------------------------------------------------- 1 | [{"__symbolic":"module","version":4,"metadata":{"TagOptions":{"__symbolic":"interface"},"AliasOptions":{"__symbolic":"interface"},"JPush":{"__symbolic":"class","extends":{"__symbolic":"reference","module":"@ionic-native/core","name":"IonicNativePlugin","line":28,"character":27},"decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Injectable","line":27,"character":1}}],"members":{"init":[{"__symbolic":"method"}],"setDebugMode":[{"__symbolic":"method"}],"getRegistrationID":[{"__symbolic":"method"}],"stopPush":[{"__symbolic":"method"}],"resumePush":[{"__symbolic":"method"}],"isPushStopped":[{"__symbolic":"method"}],"setTags":[{"__symbolic":"method"}],"addTags":[{"__symbolic":"method"}],"deleteTags":[{"__symbolic":"method"}],"cleanTags":[{"__symbolic":"method"}],"getAllTags":[{"__symbolic":"method"}],"checkTagBindState":[{"__symbolic":"method"}],"setAlias":[{"__symbolic":"method"}],"deleteAlias":[{"__symbolic":"method"}],"getAlias":[{"__symbolic":"method"}],"getUserNotificationSettings":[{"__symbolic":"method"}],"clearLocalNotifications":[{"__symbolic":"method"}],"setBadge":[{"__symbolic":"method"}],"resetBadge":[{"__symbolic":"method"}],"setApplicationIconBadgeNumber":[{"__symbolic":"method"}],"getApplicationIconBadgeNumber":[{"__symbolic":"method"}],"addLocalNotificationForIOS":[{"__symbolic":"method"}],"deleteLocalNotificationWithIdentifierKeyInIOS":[{"__symbolic":"method"}],"addDismissActions":[{"__symbolic":"method"}],"addNotificationActions":[{"__symbolic":"method"}],"setLocation":[{"__symbolic":"method"}],"startLogPageView":[{"__symbolic":"method"}],"stopLogPageView":[{"__symbolic":"method"}],"beginLogPageView":[{"__symbolic":"method"}],"getConnectionState":[{"__symbolic":"method"}],"setBasicPushNotificationBuilder":[{"__symbolic":"method"}],"setCustomPushNotificationBuilder":[{"__symbolic":"method"}],"clearAllNotification":[{"__symbolic":"method"}],"clearNotificationById":[{"__symbolic":"method"}],"setLatestNotificationNum":[{"__symbolic":"method"}],"addLocalNotification":[{"__symbolic":"method"}],"removeLocalNotification":[{"__symbolic":"method"}],"reportNotificationOpened":[{"__symbolic":"method"}],"requestPermission":[{"__symbolic":"method"}],"setSilenceTime":[{"__symbolic":"method"}],"setPushTime":[{"__symbolic":"method"}]}}}}] 2 | -------------------------------------------------------------------------------- /src/app/pages/test/test.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ 'HOME.Test' | translate }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {{ 'HOME.Scan' | translate }} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {{ 'HOME.ScreenAlwaysLight' | translate }} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {{ 'HOME.SwitchLanguage' | translate }} 36 | 38 | 中文 39 | English 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {{ 'HOME.Theme' | translate }} 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {{ versionNumber }} 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/app/pages/list/redux/todo/todo.actions.spec.ts: -------------------------------------------------------------------------------- 1 | import * as TodoActions from './todo.actions'; 2 | 3 | describe('Redux: TodoActions', () => { 4 | 5 | describe('Test for AddTodoAction', () => { 6 | 7 | it('should return an action with random id', () => { 8 | const action = new TodoActions.AddTodoAction('new todo'); 9 | expect(action.type).toEqual(TodoActions.ADD_TODO); 10 | expect(action.id).toBeGreaterThan(0); 11 | }); 12 | 13 | }); 14 | 15 | describe('Test for PopulateTodosAction', () => { 16 | 17 | it('should return an action with todos', () => { 18 | const action = new TodoActions.PopulateTodosAction([]); 19 | expect(action.type).toEqual(TodoActions.POPULATE_TODOS); 20 | expect(action.todos).toEqual([]); 21 | }); 22 | 23 | }); 24 | 25 | describe('Test for DeleteTodoAction', () => { 26 | 27 | it('should return an action with an id', () => { 28 | const action = new TodoActions.DeleteTodoAction(1); 29 | expect(action.type).toEqual(TodoActions.DELETE_TODO); 30 | expect(action.id).toEqual(1); 31 | }); 32 | 33 | }); 34 | 35 | describe('Test for ToggleAction', () => { 36 | 37 | it('should return an action with an id', () => { 38 | const action = new TodoActions.ToggleAction(1); 39 | expect(action.type).toEqual(TodoActions.TOGGLE_TODO); 40 | expect(action.id).toEqual(1); 41 | }); 42 | 43 | }); 44 | 45 | describe('Test for UpdateAction', () => { 46 | 47 | it('should return an action with an id and text', () => { 48 | const action = new TodoActions.UpdateAction(1, 'new text'); 49 | expect(action.type).toEqual(TodoActions.UPDATE_TODO); 50 | expect(action.id).toEqual(1); 51 | expect(action.text).toEqual('new text'); 52 | }); 53 | 54 | }); 55 | 56 | describe('Test for ClearCompletedAction', () => { 57 | 58 | it('should return an action the right type', () => { 59 | const action = new TodoActions.ClearCompletedAction(); 60 | expect(action.type).toEqual(TodoActions.CLEAR_COMPLETED_TODO); 61 | }); 62 | 63 | }); 64 | 65 | describe('Test for CompletedAllAction', () => { 66 | 67 | it('should return an action the right type', () => { 68 | const action = new TodoActions.CompletedAllAction(); 69 | expect(action.type).toEqual(TodoActions.COMPLETE_ALL_TODO); 70 | }); 71 | 72 | }); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /src/app/shared/ion2-calendar/components/month-picker.component.scss: -------------------------------------------------------------------------------- 1 | :host .month-picker { 2 | margin: 20px 0; 3 | display: inline-block; 4 | width: 100%; } 5 | 6 | :host .month-packer-item { 7 | width: 25%; 8 | box-sizing: border-box; 9 | float: left; 10 | height: 50px; 11 | padding: 5px; } 12 | :host .month-packer-item button { 13 | border-radius: 32px; 14 | width: 100%; 15 | height: 100%; 16 | font-size: 1em; 17 | background-color: transparent; } 18 | 19 | :host .month-picker.primary .month-packer-item.this-month button { 20 | border: 1px solid var(--ion-color-primary); } 21 | 22 | :host .month-picker.primary .month-packer-item.active button { 23 | background-color: var(--ion-color-primary); 24 | color: #fff; } 25 | 26 | :host .month-picker.secondary .month-packer-item.this-month button { 27 | border: 1px solid var(--ion-color-secondary); } 28 | 29 | :host .month-picker.secondary .month-packer-item.active button { 30 | background-color: var(--ion-color-secondary); 31 | color: #fff; } 32 | 33 | :host .month-picker.danger .month-packer-item.this-month button { 34 | border: 1px solid var(--ion-color-danger); } 35 | 36 | :host .month-picker.danger .month-packer-item.active button { 37 | background-color: var(--ion-color-danger); 38 | color: #fff; } 39 | 40 | :host .month-picker.dark .month-packer-item.this-month button { 41 | border: 1px solid var(--ion-color-dark); } 42 | 43 | :host .month-picker.dark .month-packer-item.active button { 44 | background-color: var(--ion-color-dark); 45 | color: #fff; } 46 | 47 | :host .month-picker.light .month-packer-item.this-month button { 48 | border: 1px solid var(--ion-color-light); } 49 | 50 | :host .month-picker.light .month-packer-item.active button { 51 | background-color: var(--ion-color-light); 52 | color: #9e9e9e; } 53 | 54 | :host .month-picker.transparent { 55 | background-color: transparent; } 56 | :host .month-picker.transparent .month-packer-item.this-month button { 57 | border: 1px solid var(--ion-color-light); } 58 | :host .month-picker.transparent .month-packer-item.active button { 59 | background-color: var(--ion-color-light); 60 | color: #9e9e9e; } 61 | 62 | :host .month-picker.cal-color .month-packer-item.this-month button { 63 | border: 1px solid; } 64 | 65 | :host .month-picker.cal-color .month-packer-item.active button { 66 | color: #fff; } 67 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # Customise this file, documentation can be found here: 2 | # https://github.com/fastlane/fastlane/tree/master/fastlane/docs 3 | # All available actions: https://docs.fastlane.tools/actions 4 | # can also be listed using the `fastlane actions` command 5 | 6 | # Change the syntax highlighting to Ruby 7 | # All lines starting with a # are ignored when running `fastlane` 8 | 9 | # If you want to automatically update fastlane if a new version is available: 10 | # update_fastlane 11 | 12 | # This is the minimum version number required. 13 | # Update this, if you use features of a newer version 14 | fastlane_version "2.53.1" 15 | 16 | default_platform :ios 17 | 18 | platform :ios do 19 | before_all do 20 | # ENV["SLACK_URL"] = "https://hooks.slack.com/services/..." 21 | end 22 | 23 | desc "Runs all the tests" 24 | lane :test do 25 | scan 26 | end 27 | 28 | desc "Submit a new Beta Build to PGYER" 29 | desc "This will also make sure the profile is up to date" 30 | lane :beta do 31 | setup_travis 32 | 33 | match( 34 | type: "adhoc", 35 | ) 36 | 37 | # sh "ls -l" 38 | 39 | ionic( 40 | platform: "ios", 41 | type: "adhoc" 42 | ) 43 | 44 | sh "cd .." 45 | # sh "ls" 46 | pgyer(api_key: ENV["PGYER_APIKEY"], user_key: ENV["PGYER_USERKEY"], update_description: "upload by fastlane", ipa:'/Users/travis/build/pengkobe/ionic4-boilerplate/platforms/ios/build/device/ionic4-boilerplate.ipa') 47 | 48 | end 49 | 50 | desc "Deploy a new version to the App Store" 51 | lane :release do 52 | # match(type: "appstore") 53 | # snapshot 54 | gym # Build your app - more options available 55 | deliver(force: true) 56 | # frameit 57 | end 58 | 59 | # You can define as many lanes as you want 60 | 61 | after_all do |lane| 62 | # This block is called, only if the executed lane was successful 63 | 64 | # slack( 65 | # message: "Successfully deployed new App Update." 66 | # ) 67 | end 68 | 69 | error do |lane, exception| 70 | # slack( 71 | # message: exception.message, 72 | # success: false 73 | # ) 74 | end 75 | end 76 | 77 | 78 | # More information about multiple platforms in fastlane: https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Platforms.md 79 | # All available actions: https://docs.fastlane.tools/actions 80 | 81 | # fastlane reports which actions are used. No personal data is recorded. 82 | # Learn more at https://github.com/fastlane/fastlane#metrics -------------------------------------------------------------------------------- /src/app/modals/qr-scanner/qr-scanner.scss: -------------------------------------------------------------------------------- 1 | .transparent-bg { 2 | background-color: rgba(0, 0, 0, 0.85); 3 | } 4 | 5 | .scanner-top { 6 | height: 16%; 7 | position: relative; 8 | &>p { 9 | position: absolute; 10 | bottom: 0; 11 | font-size: 12px; 12 | color: #f97271; 13 | margin-bottom: 10px; 14 | width: 100%; 15 | text-align: center; 16 | } 17 | } 18 | 19 | .scanner-left { 20 | position: absolute; 21 | left: 0; 22 | top: 0; 23 | bottom: 0; 24 | width: 15%; 25 | } 26 | 27 | .scanner-right { 28 | position: absolute; 29 | right: 0; 30 | top: 0; 31 | bottom: 0; 32 | width: 15%; 33 | } 34 | 35 | .scanner-bottom { 36 | height: calc(84% - 70vw); 37 | text-align: center; 38 | } 39 | 40 | .scanner-content { 41 | position: relative; 42 | padding-top: 70%; 43 | .scanner-box { 44 | position: absolute; 45 | width: 70%; 46 | left: 15%; 47 | border: 1px solid #f97271; 48 | top: 0; 49 | bottom: 0; 50 | @keyframes linemove { 51 | from { 52 | top: 0; 53 | } 54 | to { 55 | top: 100%; 56 | } 57 | } 58 | .angle { 59 | border-color: #f97271; 60 | border-style: solid; 61 | position: absolute; 62 | width: 15px; 63 | height: 15px; 64 | } 65 | .angle-top-left { 66 | border-width: 2px 0 0 2px; 67 | top: 0; 68 | } 69 | .angle-top-right { 70 | border-width: 2px 2px 0 0; 71 | right: 0; 72 | } 73 | .angle-bottom-right { 74 | border-width: 0 2px 2px 0; 75 | bottom: 0; 76 | right: 0; 77 | } 78 | .angle-bottom-left { 79 | border-width: 0 0 2px 2px; 80 | bottom: 0; 81 | } 82 | .aimate-line { 83 | height: 3px; 84 | width: 100%; 85 | background: radial-gradient(#f97271 24%, transparent 100%); 86 | position: absolute; 87 | animation: linemove 3s linear infinite; 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /src/app/modals/qr-scanner/qr-scanner.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { 3 | ModalController, 4 | Events, 5 | } from '@ionic/angular'; 6 | 7 | import { QRScanner, QRScannerStatus } from '@ionic-native/qr-scanner/ngx'; 8 | import { Vibration } from '@ionic-native/vibration/ngx'; 9 | 10 | 11 | @Component({ 12 | selector: 'modal-qr-scanner', 13 | templateUrl: 'qr-scanner.html', 14 | styleUrls: ['./qr-scanner.scss'], 15 | providers: [Vibration], 16 | }) 17 | export class QRScannerModal { 18 | private ionApp: HTMLElement; 19 | 20 | constructor( 21 | private events: Events, 22 | private qrScanner: QRScanner, 23 | private modalCtrl: ModalController, 24 | private vibration: Vibration 25 | ) { 26 | this.scanQrCode(); 27 | } 28 | 29 | private scanQrCode(): void { 30 | this.qrScanner 31 | .prepare() 32 | .then((status: QRScannerStatus) => { 33 | this.ionApp = document.getElementsByTagName( 34 | 'ion-split-pane' 35 | )[0]; 36 | if (status.authorized) { 37 | const scanSub = this.qrScanner.scan().subscribe((qrCode: string) => { 38 | this.vibration.vibrate(30); 39 | let response; 40 | try { 41 | response = JSON.parse(qrCode); 42 | } catch (e) { 43 | response = qrCode; 44 | } 45 | 46 | this.hideCamera(); 47 | scanSub.unsubscribe(); 48 | this.dismiss(response); 49 | }); 50 | 51 | this.ionApp.classList.add('transparent'); 52 | this.showCamera(); 53 | } else if (status.denied) { 54 | console.error('QR_CODE.PERMISSION_PERMANENTLY_DENIED'); 55 | this.dismiss(); 56 | } else { 57 | console.error('QR_CODE.PERMISSION_DENIED'); 58 | this.dismiss(); 59 | } 60 | }) 61 | .catch((e: any) => { 62 | console.warn('QR_CODE.PROBLEM_TEXT'); 63 | this.dismiss(); 64 | }); 65 | } 66 | 67 | private showCamera() { 68 | this.qrScanner.show(); 69 | this.events.publish('qrScanner:show'); 70 | } 71 | 72 | private hideCamera() { 73 | this.qrScanner.hide(); 74 | this.events.publish('qrScanner:hide'); 75 | } 76 | 77 | public dismiss(qrCode: object = null) { 78 | this.qrScanner.getStatus().then((status: QRScannerStatus) => { 79 | if (status.showing) { 80 | this.hideCamera(); 81 | } 82 | }); 83 | if (this.ionApp) { 84 | this.ionApp.classList.remove('transparent'); 85 | } 86 | this.modalCtrl.dismiss(qrCode); 87 | } 88 | 89 | ionViewDidLeave() { 90 | this.hideCamera(); 91 | this.qrScanner.destroy(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | // Ionic Variables and Theming. For more info, please see: 2 | // http://ionicframework.com/docs/theming/ 3 | 4 | /** Ionic CSS Variables **/ 5 | :root { 6 | 7 | /** primary **/ 8 | --ion-color-primary: #3880ff; 9 | --ion-color-primary-rgb: 56,128,255; 10 | --ion-color-primary-contrast: #ffffff; 11 | --ion-color-primary-contrast-rgb: 255,255,255; 12 | --ion-color-primary-shade: #3171e0; 13 | --ion-color-primary-tint: #4c8dff; 14 | 15 | /** secondary **/ 16 | --ion-color-secondary: #0cd1e8; 17 | --ion-color-secondary-rgb: 12,209,232; 18 | --ion-color-secondary-contrast: #ffffff; 19 | --ion-color-secondary-contrast-rgb: 255,255,255; 20 | --ion-color-secondary-shade: #0bb8cc; 21 | --ion-color-secondary-tint: #24d6ea; 22 | 23 | /** tertiary **/ 24 | --ion-color-tertiary: #7044ff; 25 | --ion-color-tertiary-rgb: 112,68,255; 26 | --ion-color-tertiary-contrast: #ffffff; 27 | --ion-color-tertiary-contrast-rgb: 255,255,255; 28 | --ion-color-tertiary-shade: #633ce0; 29 | --ion-color-tertiary-tint: #7e57ff; 30 | 31 | /** success **/ 32 | --ion-color-success: #10dc60; 33 | --ion-color-success-rgb: 16,220,96; 34 | --ion-color-success-contrast: #ffffff; 35 | --ion-color-success-contrast-rgb: 255,255,255; 36 | --ion-color-success-shade: #0ec254; 37 | --ion-color-success-tint: #28e070; 38 | 39 | /** warning **/ 40 | --ion-color-warning: #ffce00; 41 | --ion-color-warning-rgb: 255,206,0; 42 | --ion-color-warning-contrast: #ffffff; 43 | --ion-color-warning-contrast-rgb: 255,255,255; 44 | --ion-color-warning-shade: #e0b500; 45 | --ion-color-warning-tint: #ffd31a; 46 | 47 | /** danger **/ 48 | --ion-color-danger: #f04141; 49 | --ion-color-danger-rgb: 245,61,61; 50 | --ion-color-danger-contrast: #ffffff; 51 | --ion-color-danger-contrast-rgb: 255,255,255; 52 | --ion-color-danger-shade: #d33939; 53 | --ion-color-danger-tint: #f25454; 54 | 55 | /** dark **/ 56 | --ion-color-dark: #222428; 57 | --ion-color-dark-rgb: 34,34,34; 58 | --ion-color-dark-contrast: #ffffff; 59 | --ion-color-dark-contrast-rgb: 255,255,255; 60 | --ion-color-dark-shade: #1e2023; 61 | --ion-color-dark-tint: #383a3e; 62 | 63 | /** medium **/ 64 | --ion-color-medium: #989aa2; 65 | --ion-color-medium-rgb: 152,154,162; 66 | --ion-color-medium-contrast: #ffffff; 67 | --ion-color-medium-contrast-rgb: 255,255,255; 68 | --ion-color-medium-shade: #86888f; 69 | --ion-color-medium-tint: #a2a4ab; 70 | 71 | /** light **/ 72 | --ion-color-light: #f4f5f8; 73 | --ion-color-light-rgb: 244,244,244; 74 | --ion-color-light-contrast: #000000; 75 | --ion-color-light-contrast-rgb: 0,0,0; 76 | --ion-color-light-shade: #d7d8da; 77 | --ion-color-light-tint: #f5f6f9; 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { TestBed, async } from '@angular/core/testing'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 5 | import { 6 | IonicRouteStrategy, 7 | Events, 8 | } from '@ionic/angular'; 9 | 10 | import { SplashScreen } from '@ionic-native/splash-screen/ngx'; 11 | import { StatusBar } from '@ionic-native/status-bar/ngx'; 12 | 13 | import { RouteReuseStrategy } from '@angular/router'; 14 | import { AppRoutingModule } from './app-routing.module'; 15 | 16 | import { CoreModule } from './core/core.module'; 17 | import { SharedModule } from './shared/shared.module'; 18 | 19 | import { MyApp } from './app.component'; 20 | 21 | describe('AppComponent', () => { 22 | let statusBarSpy, splashScreenSpy, platformReadySpy, platformSpy, routerSpy, eventsSpy; 23 | 24 | beforeEach(async(() => { 25 | statusBarSpy = jasmine.createSpyObj('StatusBar', [ 26 | 'styleDefault', 27 | 'overlaysWebView', 28 | 'backgroundColorByHexString', 29 | ]); 30 | splashScreenSpy = jasmine.createSpyObj('SplashScreen', ['hide']); 31 | platformReadySpy = Promise.resolve(); 32 | platformSpy = jasmine.createSpyObj('Platform', { 33 | ready: platformReadySpy, 34 | is: () => true, 35 | backButton: {subscribeWithPriority: () => true}, 36 | }); 37 | routerSpy = jasmine.createSpyObj('events', ['subscribe', 'events']); 38 | eventsSpy = jasmine.createSpyObj('Events', ['subscribe']); 39 | TestBed.configureTestingModule({ 40 | imports: [CoreModule, SharedModule, AppRoutingModule, RouterTestingModule, HttpClientTestingModule], 41 | declarations: [MyApp], 42 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 43 | providers: [ 44 | { provide: StatusBar, useValue: statusBarSpy }, 45 | { provide: SplashScreen, useValue: splashScreenSpy }, 46 | // { provide: Platform, useValue: platformSpy }, 47 | { provide: Events, useValue: eventsSpy }, 48 | { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, 49 | ], 50 | }).compileComponents(); 51 | })); 52 | 53 | it('should create the app', () => { 54 | const fixture = TestBed.createComponent(MyApp); 55 | const app = fixture.debugElement.componentInstance; 56 | expect(app).toBeTruthy(); 57 | }); 58 | 59 | it('should initialize the app', async () => { 60 | TestBed.createComponent(MyApp); 61 | // expect(platformSpy.ready).toHaveBeenCalled(); 62 | console.log('statusBarSpy', statusBarSpy); 63 | await platformReadySpy; 64 | expect(statusBarSpy.styleDefault).toHaveBeenCalled(); 65 | expect(splashScreenSpy.hide).toHaveBeenCalled(); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/app/http.interceptor.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | HttpRequest, 4 | HttpResponse, 5 | HttpHandler, 6 | HttpEvent, 7 | HttpInterceptor 8 | } from '@angular/common/http'; 9 | import { Observable } from 'rxjs'; 10 | import { finalize, tap } from 'rxjs/operators'; 11 | import { 12 | NavController 13 | } from '@ionic/angular'; 14 | 15 | @Injectable() 16 | export class MYHttpInterceptor implements HttpInterceptor { 17 | status = { 18 | '400': '错误的请求,由于语法错误,该请求无法完成.', 19 | '401': '未经授权,服务器拒绝响应.', 20 | '403': '已禁止,服务器拒绝响应.', 21 | '404': '未找到,无法找到请求的位置.', 22 | '405': '方法不被允许,使用该位置不支持的请求方法进行了请求.', 23 | '406': '不可接受,服务器只生成客户端不接受的响应.', 24 | '407': '需要代理身份验证,客户端必须先使用代理对自身进行身份验证.', 25 | '408': '请求超时,等待请求的服务器超时.', 26 | '409': '冲突,由于请求中的冲突,无法完成该请求.', 27 | '410': '过期,请求页不再可用.', 28 | '411': '长度必需,未定义“内容长度”.', 29 | '412': '前提条件不满足,请求中给定的前提条件由服务器评估为 false.', 30 | '413': '请求实体太大,服务器不会接受请求,因为请求实体太大.', 31 | '414': '请求 URI 太长,服务器不会接受该请求,因为 URL 太长.', 32 | '415': '不支持的媒体类型,服务器不会接受该请求,因为媒体类型不受支持.', 33 | '416': 'HTTP 状态代码 {0}', 34 | '500': '内部服务器错误.', 35 | '501': '未实现,服务器不识别该请求方法或者服务器没有能力完成请求.', 36 | '503': '服务器当前不可用(过载或故障).' 37 | }; 38 | 39 | constructor(private navCtrl: NavController, ) { } 40 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 41 | const started = Date.now(); 42 | let ok: string; 43 | return next.handle(request).pipe(tap(event => { 44 | if (event instanceof HttpResponse) { 45 | if (event.status === 200) { 46 | if (event.body.status === 'fail' && event.body.description === 'Token verify failed') { 47 | ok = 'fail'; 48 | this.navCtrl.navigateForward(['login']); 49 | } 50 | ok = 'succeed'; 51 | } else { 52 | ok = 'failed'; 53 | if (this.status['' + event.status]) { 54 | ok = 'failed:' + this.status['' + event.status]; 55 | console.error(this.status['' + event.status]); 56 | if (event.status === 401) { 57 | this.navCtrl.navigateForward(['login']); 58 | } 59 | } 60 | } 61 | } 62 | }, error => ok = 'failed,' + error), finalize(() => { 63 | const elapsed = Date.now() - started; 64 | const msg = `${request.method} "${request.urlWithParams}" ${ok} in ${elapsed} ms.`; 65 | console.log(msg); 66 | })); 67 | } 68 | } 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/app/pages/list/ngrxtodo/new-todo/new-todo.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { Component } from '@angular/core'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | import { ReactiveFormsModule, FormsModule } from '@angular/forms'; 5 | import { StoreModule, Store } from '@ngrx/store'; 6 | import { RouterTestingModule } from '@angular/router/testing'; 7 | 8 | import { ngrxtodoReducer, AppState } from './../../redux/ngrxtodo.reducer'; 9 | import * as TodoActions from './../../redux/todo/todo.actions'; 10 | 11 | import { NewTodoComponent } from './new-todo.component'; 12 | 13 | @Component({ 14 | // tslint:disable-next-line:component-selector 15 | selector: 'blank-cmp', 16 | template: ``, 17 | }) 18 | // tslint:disable-next-line:component-class-suffix 19 | export class BlankCmp {} 20 | 21 | describe('NewTodoComponent', () => { 22 | let component: NewTodoComponent; 23 | let fixture: ComponentFixture; 24 | let store: Store; 25 | 26 | beforeEach(async(() => { 27 | TestBed.configureTestingModule({ 28 | declarations: [NewTodoComponent, BlankCmp], 29 | imports: [ 30 | ReactiveFormsModule, 31 | FormsModule, 32 | RouterTestingModule.withRoutes([{ path: '', component: BlankCmp }]), 33 | StoreModule.forRoot({}), 34 | StoreModule.forFeature('ngrxtodo', ngrxtodoReducer), 35 | ], 36 | }).compileComponents(); 37 | })); 38 | 39 | beforeEach(() => { 40 | store = TestBed.get(Store); 41 | spyOn(store, 'dispatch').and.callThrough(); 42 | 43 | fixture = TestBed.createComponent(NewTodoComponent); 44 | component = fixture.componentInstance; 45 | fixture.detectChanges(); 46 | }); 47 | 48 | it('should be created', () => { 49 | expect(component).toBeTruthy(); 50 | }); 51 | 52 | describe('Test for textField', () => { 53 | it('should textField be defined', () => { 54 | expect(component.textField).toBeDefined(); 55 | }); 56 | 57 | it('should textField be valid', () => { 58 | component.textField.setValue('new todo'); 59 | expect(component.textField.valid).toBeTruthy(); 60 | }); 61 | 62 | it('should textField be invalid', () => { 63 | component.textField.setValue(''); 64 | expect(component.textField.invalid).toBeTruthy(); 65 | }); 66 | }); 67 | 68 | describe('Test for saveTodo', () => { 69 | it('should dispatch an action', () => { 70 | component.textField.setValue('new todo', { emitEvent: false }); 71 | component.saveTodo(); 72 | expect(store.dispatch).toHaveBeenCalled(); 73 | }); 74 | 75 | it('should set value of textField in empty', () => { 76 | component.textField.setValue('new todo', { emitEvent: false }); 77 | component.saveTodo(); 78 | expect(component.textField.value).toEqual(''); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /doc/travis.md: -------------------------------------------------------------------------------- 1 | # Travis 基本使用 2 | 3 | 官网: https://travis-ci.com 4 | 5 | ## 下载客户端 6 | 7 | 仓库地址: https://github.com/travis-ci/travis.rb 8 | 9 | ## 登录 10 | 11 | travis login --github-token YOUR_TOKEN 12 | 13 | ## 单元测试 14 | 15 | ### xvfb 16 | 17 | 如果你选择语言为 nodejs,那么镜像会自带这个工具 18 | 19 | ## deploy 20 | 21 | - https://docs.travis-ci.com/user/deployment 22 | - 如何发布至 Github Release https://docs.travis-ci.com/user/deployment/releases/ 23 | 24 | ### 上传至蒲公英 25 | 26 | 查看具体步骤,可以点击网址查看官网教程 27 | 28 | - https://www.pgyer.com/doc/view/travis_android 29 | - https://www.pgyer.com/doc/view/travis_ios 30 | - https://github.com/Pgyer/TravisUploadiOSDemo 31 | 32 | ## cocoapods 33 | 34 | 部分cordova 插件依赖于这个库,需要确保这个库已经安装,常见的安装方式有两种,一种是通过 `brew` 进行安装,一种是通过 `gem` 进行安装。 35 | 36 | ### brew 安装 37 | 38 | ```bash 39 | brew install cocoapods 40 | pop setup 41 | ``` 42 | 43 | ### gem 安装 44 | 45 | ```bash 46 | mkdir -p $HOME/Software/ruby 47 | export GEM_HOME=$HOME/Software/ruby 48 | gem install cocoapods 49 | gem installed 50 | export PATH=$PATH:$HOME/Software/ruby/bin 51 | pod --version 52 | ``` 53 | 54 | ### 报错 55 | 56 | - 提示版本不对: 注意镜像是否为 10.1 57 | - 提示安装不成功,无法建立 link: [解决参考](https://stackoverflow.com/questions/37904588/cocoapods-not-installing/48335801#48335801) 58 | 59 | ### github token 生成 60 | 61 | #### 加密 token 62 | 63 | `travis encrypt YOUR_GITHUB_RAW_TOKEN -r pengkobe/ionic4-boilerplate --add` 64 | 65 | #### 加密文件 66 | 67 | 只支持打包加密,不支持一个一个文件加密 68 | 69 | ```bash 70 | # 打包 71 | tar cvf certificates.tar ios_distribution.cer ios_distribution.p12 ionic4_Ad_Hoc_Profile.mobileprovision ionic4travis.jks ios_develop.cer ios_develop.p12 ios_push_distribution.cer ios_push_distribution.p12 72 | # 加密 73 | travis encrypt-file certificates.tar -r pengkobe/ionic4-boilerplate 74 | ``` 75 | 76 | ## 运行报错 77 | 78 | - 无法成功安装 `oracle-java8-installer`,android 语言自带有 JDK,实际上无需安装 79 | - error installing travis:ERROR: Failed to build gem native extension. **事实上在 windows 上生成的 enc 文件都是会报错的** 80 | - 加密多个文件时,必须得打包成一个文件进行加密,否则会报错! 81 | - 构建 IOS 环境时,老是提示证书找不到,我后来直接使用 [fastlane](https://fastlane.tools/) 去管理了,硬是需要使用 travis 构建,可以参考这个 [travis-ci-fails-to-build-with-a-code-signing-error](https://stackoverflow.com/questions/27671854/travis-ci-fails-to-build-with-a-code-signing-error?rq=1) 和看看这个 ISSUE[Code Sign error: No code signing identities found](https://github.com/travis-ci/travis-ci/issues/3072) 82 | - 提示: `No output has been received in the last 10m0s` , 属于 `Mac: macOS Sierra (10.12) Code Signing Error` , 参见 Travis 官方文档对应的 [解决办法](https://docs.travis-ci.com/user/common-build-problems/#Build-times-out-because-no-output-was-received) 83 | 84 | ## 参考 85 | 86 | * https://github.com/samueltbrown/ionic-continuous-delivery-blog ,这篇文章年代有点久远,还是需要做一些修正才行 87 | - 文中对 XCode7+ iOS9 中的 App Transport Security 处理脚本其实不再需要了,默认就是设置为 true 了 88 | - update_xcconfig 那段也不需要了,高版本 Xcode 也已经默认移除了相关配置 89 | * [为iOS建立Travis CI(史上最全版)](https://blog.csdn.net/qq_30817073/article/details/51719473) ,事实上还是比较全的 90 | -------------------------------------------------------------------------------- /doc/code-spec.md: -------------------------------------------------------------------------------- 1 | # 开发流程与代码规范 2 | 3 | ## Git 4 | 5 | ### 仓库 6 | 7 | 托管在[码云](https://gitee.com/)或者 Github 私有仓库 8 | 9 | ### 分支管理 10 | 11 | 参见下图 12 | ![git 分支管理规范](./img/git-version-ctrl.png) 13 | 14 | ### 提交流程 15 | 16 | 参见教程:[Commit message 和 Change log 编写指南](http://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html), 17 | 本脚手架基于 commitizen 与 commitlint 来对提交信息做验证, 脚本示例如下,commitlint 具体介绍,下面也会提到: 18 | 19 | ```bash 20 | git add YOUR_EDITED_FILES 21 | git cz 22 | git push 23 | ``` 24 | 25 | ## 测试 26 | 27 | 使用 Ionic/Angular 默认测试工具即可, 根据需求可以微调配置 28 | 29 | ```bash 30 | # 单元测试 31 | npm run test 32 | # 端到端测试 33 | npm run e2e 34 | ``` 35 | 36 | ## 代码规范 37 | 38 | ### 业界参考 39 | 40 | - JavaScript 代码规范, 参见业界公认的 [airbnb JavaScript 规范](https://github.com/airbnb/javascript) 41 | - Angular 规范, 直接参考官网[styleguide](https://angular.io/guide/styleguide), Angular 默认已集成部分检测工具, 如 `tslint-angular`, 如需手动验证,可以执行命令 `npm run lint` 42 | - Scss, 基于 scsslint 可验证 sass 代码是否规范, 参见: https://sass-guidelin.es/#tools 43 | 44 | ## 工具集成 45 | 46 | ### 安装 commitizen 47 | 48 | [commitizen](https://github.com/commitizen/cz-cli ), 可以实现代码提交可视化,可以使用以下脚本安装 49 | 50 | ```bash 51 | # 全局安装 52 | npm install -g commitizen 53 | # 初始化 54 | commitizen init cz-conventional-changelog --save --save-exact 55 | ``` 56 | 57 | 默认提交类目主要有以下几个 58 | 59 | - feat: 新功能( feature ) 60 | - fix: 修补 bug 61 | - docs: 文档( documentation ) 62 | - style: 格式( 不影响代码运行的变动 ) 63 | - refactor: 重构( 即不是新增功能, 也不是修改bug的代码变动 ) 64 | - test: 测试相关更改 65 | - chore: 构建过程或辅助工具的变动 66 | - build: 与构建工具相关的更改 67 | - ci: 持续集成相关的更改 68 | 69 | ### commitlint 70 | 71 | 基于 [commitlint](https://github.com/marionebl/commitlint) 验证 git message 是否规范 72 | 73 | 本脚手架使用 angular 规则, 详情参见: 74 | https://github.com/marionebl/commitlint/tree/master/@commitlint/config-angular 75 | 76 | ### standard-version 77 | 78 | 本脚手架使用 [standard-version](https://github.com/conventional-changelog/standard-version) 可以基于 `Git Commit Message` 生成 changelog, 需要对文档中生成规则进行熟悉。执行下述基本可以安装 standard-version,其也有全局安装选项,可以自由选择 79 | 80 | ```bash 81 | npm i --save-dev standard-version 82 | 83 | # 全局安装 84 | npm i -g standard-version 85 | ``` 86 | 87 | ### husky 88 | 89 | 通过 [husky](https://www.npmjs.com/package/husky) 可以执行生命周期内的相关钩子, 自动验证代码的是否符合规范 90 | 91 | ```bash 92 | npm install husky --save-dev 93 | ``` 94 | 95 | ### type doc 96 | 97 | [type doc](https://github.com/TypeStrong/typedoc/) 可以自动根据注释生成文档的工具 98 | > 前提是你按照标准的格式写好注释 99 | 100 | 建议全局安装 101 | 102 | ```bash 103 | npm install typedoc --global 104 | typedoc 105 | ``` 106 | 107 | ### better-npm-run 108 | 109 | [better-npm-run](https://github.com/benoror/better-npm-run) 能够去除配置文件硬编码, 可以直接在 package.json 中配置命令行参数 110 | 111 | ```bash 112 | npm install better-npm-run --save-dev 113 | ``` 114 | 115 | ### ionic docker (TODO:) 116 | 117 | [ionic docker](https://github.com/marcoturi/ionic-docker) 能够屏蔽部署环境差异, 可惜对 Windows 支持不大好,必须要基于虚拟机进行安装,否则大家可以使用一套环境相同的开发环境。 118 | -------------------------------------------------------------------------------- /src/app/pages/list/dynamic-form/containers/dynamic-form/dynamic-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; 2 | import { FormGroup, FormBuilder } from '@angular/forms'; 3 | 4 | import { FieldConfig } from '../../models/field-config.interface'; 5 | 6 | @Component({ 7 | exportAs: 'dynamicForm', 8 | selector: 'dynamic-form', 9 | styleUrls: ['dynamic-form.component.scss'], 10 | template: ` 11 |
    15 | 20 | 21 |
    22 | ` 23 | }) 24 | export class DynamicFormComponent implements OnChanges, OnInit { 25 | @Input() 26 | config: FieldConfig[] = []; 27 | 28 | @Output() 29 | submit: EventEmitter = new EventEmitter(); 30 | 31 | form: FormGroup; 32 | 33 | get controls() { return this.config.filter(({ type }) => type !== 'button'); } 34 | get changes() { return this.form.valueChanges; } 35 | get valid() { return this.form.valid; } 36 | get value() { return this.form.value; } 37 | 38 | constructor(private fb: FormBuilder) { } 39 | 40 | ngOnInit() { 41 | this.form = this.createGroup(); 42 | } 43 | 44 | ngOnChanges() { 45 | if (this.form) { 46 | const controls = Object.keys(this.form.controls); 47 | const configControls = this.controls.map((item) => item.name); 48 | 49 | controls 50 | .filter((control) => !configControls.includes(control)) 51 | .forEach((control) => this.form.removeControl(control)); 52 | 53 | configControls 54 | .filter((control) => !controls.includes(control)) 55 | .forEach((name) => { 56 | const config = this.config.find((control) => control.name === name); 57 | this.form.addControl(name, this.createControl(config)); 58 | }); 59 | 60 | } 61 | } 62 | 63 | createGroup() { 64 | const group = this.fb.group({}); 65 | this.controls.forEach(control => group.addControl(control.name, this.createControl(control))); 66 | return group; 67 | } 68 | 69 | createControl(config: FieldConfig) { 70 | const { disabled, validation, value } = config; 71 | return this.fb.control({ disabled, value }, validation); 72 | } 73 | 74 | handleSubmit(event: Event) { 75 | event.preventDefault(); 76 | event.stopPropagation(); 77 | this.submit.emit(this.value); 78 | } 79 | 80 | setDisabled(name: string, disable: boolean) { 81 | if (this.form.controls[name]) { 82 | const method = disable ? 'disable' : 'enable'; 83 | this.form.controls[name][method](); 84 | return; 85 | } 86 | 87 | this.config = this.config.map((item) => { 88 | if (item.name === name) { 89 | item.disabled = disable; 90 | } 91 | return item; 92 | }); 93 | } 94 | 95 | setValue(name: string, value: any) { 96 | this.form.controls[name].setValue(value, { emitEvent: true }); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/nativewrapper/jpush/ngx/index.d.ts: -------------------------------------------------------------------------------- 1 | import { IonicNativePlugin } from '@ionic-native/core'; 2 | export interface TagOptions { 3 | sequence: number; 4 | tags?: string[]; 5 | } 6 | export interface AliasOptions { 7 | sequence: number; 8 | alias?: string; 9 | } 10 | export declare class JPush extends IonicNativePlugin { 11 | init(): Promise; 12 | setDebugMode(enable: boolean): Promise; 13 | getRegistrationID(): Promise; 14 | stopPush(): Promise; 15 | resumePush(): Promise; 16 | isPushStopped(): Promise; 17 | setTags(params: TagOptions): Promise; 18 | addTags(params: TagOptions): Promise; 19 | deleteTags(params: TagOptions): Promise; 20 | cleanTags(params: TagOptions): Promise; 21 | getAllTags(params: TagOptions): Promise; 22 | /** 23 | * @param params { sequence: number, tag: string } 24 | */ 25 | checkTagBindState(params: object): Promise; 26 | setAlias(params: AliasOptions): Promise; 27 | deleteAlias(params: AliasOptions): Promise; 28 | getAlias(params: AliasOptions): Promise; 29 | /** 30 | * Determinate whether the application notification has been opened. 31 | * 32 | * iOS: 0: closed; >1: opened. 33 | * UIRemoteNotificationTypeNone = 0, 34 | * UIRemoteNotificationTypeBadge = 1 << 0, 35 | * UIRemoteNotificationTypeSound = 1 << 1, 36 | * UIRemoteNotificationTypeAlert = 1 << 2, 37 | * UIRemoteNotificationTypeNewsstandContentAvailability = 1 << 3 38 | * 39 | * Android: 0: closed; 1: opened. 40 | */ 41 | getUserNotificationSettings(): Promise; 42 | clearLocalNotifications(): Promise; 43 | setBadge(badge: number): Promise; 44 | resetBadge(): Promise; 45 | setApplicationIconBadgeNumber(badge: number): Promise; 46 | getApplicationIconBadgeNumber(): Promise; 47 | addLocalNotificationForIOS(delayTime: number, content: string, badge: number, identifierKey: string, extras?: object): Promise; 48 | deleteLocalNotificationWithIdentifierKeyInIOS(identifierKey: string): Promise; 49 | addDismissActions(actions: object[], categoryId: string): Promise; 50 | addNotificationActions(actions: object[], categoryId: string): Promise; 51 | setLocation(latitude: number, longitude: number): Promise; 52 | startLogPageView(pageName: string): Promise; 53 | stopLogPageView(pageName: string): Promise; 54 | beginLogPageView(pageName: string, duration: number): Promise; 55 | getConnectionState(): Promise; 56 | setBasicPushNotificationBuilder(): Promise; 57 | setCustomPushNotificationBuilder(): Promise; 58 | clearAllNotification(): Promise; 59 | clearNotificationById(id: number): Promise; 60 | setLatestNotificationNum(num: number): Promise; 61 | addLocalNotification(builderId: number, content: string, title: string, notificationId: number, broadcastTime: number, extras?: string): Promise; 62 | removeLocalNotification(notificationId: number): Promise; 63 | reportNotificationOpened(msgId: number): Promise; 64 | requestPermission(): Promise; 65 | setSilenceTime(startHour: number, startMinute: number, endHour: number, endMinute: number): Promise; 66 | setPushTime(weekdays: string[], startHour: number, endHour: number): Promise; 67 | } 68 | -------------------------------------------------------------------------------- /src/app/shared/ion2-calendar/calendar.model.ts: -------------------------------------------------------------------------------- 1 | import { AnimationBuilder } from '@ionic/core'; 2 | 3 | export interface CalendarOriginal { 4 | time: number; 5 | date: Date; 6 | year: number; 7 | month: number; 8 | firstWeek: number; 9 | howManyDays: number; 10 | } 11 | 12 | export interface CalendarDay { 13 | time: number; 14 | isToday: boolean; 15 | selected: boolean; 16 | disable: boolean; 17 | cssClass: string; 18 | isLastMonth?: boolean; 19 | isNextMonth?: boolean; 20 | title?: string; 21 | subTitle?: string; 22 | marked?: boolean; 23 | style?: { 24 | title?: string; 25 | subTitle?: string; 26 | }; 27 | isFirst?: boolean; 28 | isLast?: boolean; 29 | } 30 | 31 | export class CalendarMonth { 32 | original: CalendarOriginal; 33 | days: Array; 34 | } 35 | 36 | export interface DayConfig { 37 | date: Date; 38 | marked?: boolean; 39 | disable?: boolean; 40 | title?: string; 41 | subTitle?: string; 42 | cssClass?: string; 43 | } 44 | 45 | export interface ModalOptions { 46 | showBackdrop?: boolean; 47 | backdropDismiss?: boolean; 48 | enterAnimation?: AnimationBuilder; 49 | leaveAnimation?: AnimationBuilder; 50 | } 51 | 52 | export interface CalendarOptions { 53 | from?: Date | number; 54 | to?: Date | number; 55 | pickMode?: string; 56 | weekStart?: number; 57 | disableWeeks?: Array; 58 | weekdays?: Array; 59 | monthFormat?: string; 60 | color?: string; 61 | defaultTitle?: string; 62 | defaultSubtitle?: string; 63 | daysConfig?: Array; 64 | /** 65 | * show last month & next month days fill six weeks 66 | */ 67 | showAdjacentMonthDay?: boolean; 68 | } 69 | 70 | export interface CalendarModalOptions extends CalendarOptions { 71 | autoDone?: boolean; 72 | format?: string; 73 | cssClass?: string; 74 | id?: string; 75 | isSaveHistory?: boolean; 76 | closeLabel?: string; 77 | doneLabel?: string; 78 | closeIcon?: boolean; 79 | doneIcon?: boolean; 80 | canBackwardsSelected?: boolean; 81 | title?: string; 82 | defaultScrollTo?: Date; 83 | defaultDate?: DefaultDate; 84 | defaultDates?: DefaultDate[]; 85 | defaultDateRange?: { from: DefaultDate; to?: DefaultDate } | null; 86 | step?: number; 87 | /** 88 | * @deprecated this version notwork 89 | */ 90 | showYearPicker?: boolean; 91 | } 92 | 93 | export interface CalendarComponentOptions extends CalendarOptions { 94 | showToggleButtons?: boolean; 95 | showMonthPicker?: boolean; 96 | monthPickerFormat?: string[]; 97 | } 98 | 99 | export class CalendarResult { 100 | time: number; 101 | unix: number; 102 | dateObj: Date; 103 | string: string; 104 | years: number; 105 | months: number; 106 | date: number; 107 | } 108 | 109 | export class CalendarComponentMonthChange { 110 | oldMonth: CalendarResult; 111 | newMonth: CalendarResult; 112 | } 113 | 114 | export type DefaultDate = Date | string | number | null; 115 | export type Colors = 116 | | 'primary' 117 | | 'secondary' 118 | | 'danger' 119 | | 'light' 120 | | 'dark' 121 | | string; 122 | export type PickMode = 'multi' | 'single' | 'range'; 123 | export type CalendarComponentTypeProperty = 124 | | 'string' 125 | | 'js-date' 126 | | 'moment' 127 | | 'time' 128 | | 'object'; 129 | export type CalendarComponentPayloadTypes = string | Date | number | {}; 130 | -------------------------------------------------------------------------------- /src/nativewrapper/jpush/index.d.ts: -------------------------------------------------------------------------------- 1 | import { IonicNativePlugin } from '@ionic-native/core'; 2 | export interface TagOptions { 3 | sequence: number; 4 | tags?: string[]; 5 | } 6 | export interface AliasOptions { 7 | sequence: number; 8 | alias?: string; 9 | } 10 | export declare class JPushOriginal extends IonicNativePlugin { 11 | init(): Promise; 12 | setDebugMode(enable: boolean): Promise; 13 | getRegistrationID(): Promise; 14 | stopPush(): Promise; 15 | resumePush(): Promise; 16 | isPushStopped(): Promise; 17 | setTags(params: TagOptions): Promise; 18 | addTags(params: TagOptions): Promise; 19 | deleteTags(params: TagOptions): Promise; 20 | cleanTags(params: TagOptions): Promise; 21 | getAllTags(params: TagOptions): Promise; 22 | /** 23 | * @param params { sequence: number, tag: string } 24 | */ 25 | checkTagBindState(params: object): Promise; 26 | setAlias(params: AliasOptions): Promise; 27 | deleteAlias(params: AliasOptions): Promise; 28 | getAlias(params: AliasOptions): Promise; 29 | /** 30 | * Determinate whether the application notification has been opened. 31 | * 32 | * iOS: 0: closed; >1: opened. 33 | * UIRemoteNotificationTypeNone = 0, 34 | * UIRemoteNotificationTypeBadge = 1 << 0, 35 | * UIRemoteNotificationTypeSound = 1 << 1, 36 | * UIRemoteNotificationTypeAlert = 1 << 2, 37 | * UIRemoteNotificationTypeNewsstandContentAvailability = 1 << 3 38 | * 39 | * Android: 0: closed; 1: opened. 40 | */ 41 | getUserNotificationSettings(): Promise; 42 | clearLocalNotifications(): Promise; 43 | setBadge(badge: number): Promise; 44 | resetBadge(): Promise; 45 | setApplicationIconBadgeNumber(badge: number): Promise; 46 | getApplicationIconBadgeNumber(): Promise; 47 | addLocalNotificationForIOS(delayTime: number, content: string, badge: number, identifierKey: string, extras?: object): Promise; 48 | deleteLocalNotificationWithIdentifierKeyInIOS(identifierKey: string): Promise; 49 | addDismissActions(actions: object[], categoryId: string): Promise; 50 | addNotificationActions(actions: object[], categoryId: string): Promise; 51 | setLocation(latitude: number, longitude: number): Promise; 52 | startLogPageView(pageName: string): Promise; 53 | stopLogPageView(pageName: string): Promise; 54 | beginLogPageView(pageName: string, duration: number): Promise; 55 | getConnectionState(): Promise; 56 | setBasicPushNotificationBuilder(): Promise; 57 | setCustomPushNotificationBuilder(): Promise; 58 | clearAllNotification(): Promise; 59 | clearNotificationById(id: number): Promise; 60 | setLatestNotificationNum(num: number): Promise; 61 | addLocalNotification(builderId: number, content: string, title: string, notificationId: number, broadcastTime: number, extras?: string): Promise; 62 | removeLocalNotification(notificationId: number): Promise; 63 | reportNotificationOpened(msgId: number): Promise; 64 | requestPermission(): Promise; 65 | setSilenceTime(startHour: number, startMinute: number, endHour: number, endMinute: number): Promise; 66 | setPushTime(weekdays: string[], startHour: number, endHour: number): Promise; 67 | } 68 | 69 | export declare const JPush: JPushOriginal; -------------------------------------------------------------------------------- /src/app/services/qiniu.upload.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpResponse, HttpHeaders } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { map } from 'rxjs/operators'; 6 | 7 | import { baseUrl } from '../config'; 8 | import { GlobalService } from './global.service'; 9 | 10 | declare var window; 11 | 12 | @Injectable({ 13 | providedIn: 'root' 14 | }) 15 | export class QiniuUploadService { 16 | private _qiuniutokeninfo: any; 17 | 18 | constructor(public http: HttpClient, public _g: GlobalService) {} 19 | 20 | public getUploadToken(): Observable { 21 | const httpOptions = { 22 | headers: new HttpHeaders({ 23 | Authorization: this._g.token, 24 | }), 25 | }; 26 | 27 | return this.http.get(baseUrl + 'qiu/uploadtoken', httpOptions).pipe( 28 | map((res: HttpResponse) => { 29 | return res; 30 | }) 31 | ); 32 | } 33 | 34 | public init(uploadToken): Observable { 35 | return Observable.create(observer => { 36 | window.plugins.QiNiuUploadPlugin.init( 37 | uploadToken, 38 | data => { 39 | console.log('qiniu,init ret suncceed:', data); 40 | observer.next(data); 41 | observer.complete(); 42 | }, 43 | err => { 44 | console.log('qiniu,init ret err:', err); 45 | observer.error(err); 46 | } 47 | ); 48 | }); 49 | } 50 | 51 | public uploadLocFile(filePath, name) { 52 | return Observable.create(observer => { 53 | window.plugins.QiNiuUploadPlugin.simpleUploadFile( 54 | { 55 | filePath: filePath, 56 | name: name, 57 | }, 58 | data => { 59 | console.log('qiniu uploadLocFile succeed:', data); 60 | observer.next({ 61 | data: true, 62 | value: data, 63 | }); 64 | observer.complete(); 65 | }, 66 | progress => { 67 | observer.next({ 68 | data: false, 69 | value: progress, 70 | }); 71 | }, 72 | err => { 73 | console.log('qiniu,uploadLocFile ret err:', err); 74 | observer.error(err); 75 | } 76 | ); 77 | }); 78 | } 79 | 80 | /** 81 | * 七牛初始化 82 | */ 83 | initQiniu(): Observable { 84 | return Observable.create(observer => { 85 | if (this._qiuniutokeninfo) { 86 | const timespan = new Date().getTime() - this._qiuniutokeninfo.getTime(); 87 | const timespan_milliseconds = timespan % (3600 * 1000); 88 | const timespan_minutes = Math.floor( 89 | timespan_milliseconds / (60 * 1000) 90 | ); 91 | if (timespan_minutes < 115) { 92 | observer.next(true); 93 | } else { 94 | this.getQiniuTokenAndInit(observer); 95 | } 96 | } else { 97 | this.getQiniuTokenAndInit(observer); 98 | } 99 | }); 100 | } 101 | 102 | getQiniuTokenAndInit(observer) { 103 | this.getUploadToken().subscribe( 104 | data => { 105 | console.log('qiniutoken:', data); 106 | this._qiuniutokeninfo = data.uploadToken; 107 | this.init(data.uploadToken).subscribe(d => { 108 | observer.next(true); 109 | }); 110 | }, 111 | err => { 112 | console.log('qiniu init error:', err); 113 | observer.error(false); 114 | } 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-spacing": true, 20 | "indent": [ 21 | true, 22 | "spaces" 23 | ], 24 | "interface-over-type-literal": true, 25 | "label-position": true, 26 | "max-line-length": [ 27 | true, 28 | 140 29 | ], 30 | "member-access": false, 31 | "member-ordering": [ 32 | true, 33 | { 34 | "order": [ 35 | "static-field", 36 | "instance-field", 37 | "static-method", 38 | "instance-method" 39 | ] 40 | } 41 | ], 42 | "no-arg": true, 43 | "no-bitwise": true, 44 | "no-console": [ 45 | true, 46 | "debug", 47 | "info", 48 | "time", 49 | "timeEnd", 50 | "trace" 51 | ], 52 | "no-construct": true, 53 | "no-debugger": true, 54 | "no-duplicate-super": true, 55 | "no-empty": false, 56 | "no-empty-interface": true, 57 | "no-eval": true, 58 | "no-inferrable-types": [ 59 | true, 60 | "ignore-params" 61 | ], 62 | "no-misused-new": true, 63 | "no-non-null-assertion": true, 64 | "no-shadowed-variable": true, 65 | "no-string-literal": false, 66 | "no-string-throw": true, 67 | "no-switch-case-fall-through": true, 68 | "no-trailing-whitespace": true, 69 | "no-unnecessary-initializer": true, 70 | "no-unused-expression": true, 71 | "no-use-before-declare": false, 72 | "no-var-keyword": true, 73 | "object-literal-sort-keys": false, 74 | "one-line": [ 75 | true, 76 | "check-open-brace", 77 | "check-catch", 78 | "check-else", 79 | "check-whitespace" 80 | ], 81 | "prefer-const": true, 82 | "quotemark": [ 83 | true, 84 | "single" 85 | ], 86 | "radix": true, 87 | "semicolon": [ 88 | true, 89 | "always" 90 | ], 91 | "triple-equals": [ 92 | true, 93 | "allow-null-check" 94 | ], 95 | "typedef-whitespace": [ 96 | true, 97 | { 98 | "call-signature": "nospace", 99 | "index-signature": "nospace", 100 | "parameter": "nospace", 101 | "property-declaration": "nospace", 102 | "variable-declaration": "nospace" 103 | } 104 | ], 105 | "unified-signatures": true, 106 | "variable-name": false, 107 | "whitespace": [ 108 | true, 109 | "check-branch", 110 | "check-decl", 111 | "check-operator", 112 | "check-separator", 113 | "check-type" 114 | ], 115 | "directive-selector": [ 116 | true, 117 | "attribute", 118 | "camelCase" 119 | ], 120 | "component-selector": [ 121 | true, 122 | "element", 123 | "app", 124 | "page", 125 | "kebab-case" 126 | ], 127 | "no-output-on-prefix": true, 128 | "use-input-property-decorator": true, 129 | "use-output-property-decorator": true, 130 | "use-host-property-decorator": true, 131 | "no-input-rename": true, 132 | "no-output-rename": true, 133 | "use-life-cycle-interface": true, 134 | "use-pipe-transform-interface": true, 135 | "directive-class-suffix": true 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/app/pages/list/ngrxtodo/footer/footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { Component } from '@angular/core'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | import { StoreModule, Store } from '@ngrx/store'; 5 | import { RouterTestingModule } from '@angular/router/testing'; 6 | 7 | import { ngrxtodoReducer, AppState } from './../../redux/ngrxtodo.reducer'; 8 | import * as FilterActions from './../../redux/filter/filter.actions'; 9 | import * as TodoActions from './../../redux/todo/todo.actions'; 10 | 11 | import { FooterComponent } from './footer.component'; 12 | 13 | @Component({ 14 | // tslint:disable-next-line:component-selector 15 | selector: 'blank-cmp', 16 | template: ``, 17 | }) 18 | // tslint:disable-next-line:component-class-suffix 19 | export class BlankCmp {} 20 | 21 | describe('FooterComponent', () => { 22 | let component: FooterComponent; 23 | let fixture: ComponentFixture; 24 | let store: Store; 25 | 26 | beforeEach(async(() => { 27 | TestBed.configureTestingModule({ 28 | declarations: [FooterComponent, BlankCmp], 29 | imports: [ 30 | RouterTestingModule.withRoutes([{ path: '', component: BlankCmp }]), 31 | StoreModule.forRoot({}), 32 | StoreModule.forFeature('ngrxtodo', ngrxtodoReducer), 33 | ], 34 | }).compileComponents(); 35 | })); 36 | 37 | beforeEach(() => { 38 | store = TestBed.get(Store); 39 | spyOn(store, 'dispatch').and.callThrough(); 40 | 41 | fixture = TestBed.createComponent(FooterComponent); 42 | component = fixture.componentInstance; 43 | fixture.detectChanges(); 44 | }); 45 | 46 | it('should be created', () => { 47 | expect(component).toBeTruthy(); 48 | }); 49 | 50 | describe('Test for clearCompleted', () => { 51 | it('should dispatch an action', () => { 52 | component.clearCompleted(); 53 | const action = new TodoActions.ClearCompletedAction(); 54 | expect(store.dispatch).toHaveBeenCalledWith(action); 55 | }); 56 | }); 57 | 58 | describe('Test for completedAll', () => { 59 | it('should dispatch an action', () => { 60 | component.completedAll(); 61 | const action = new TodoActions.CompletedAllAction(); 62 | expect(store.dispatch).toHaveBeenCalledWith(action); 63 | }); 64 | }); 65 | 66 | describe('Test for countTodos', () => { 67 | it('should return 2 undone todos and showFooter is true', () => { 68 | const todos = [ 69 | { id: 1, text: 'todo', completed: false }, 70 | { id: 2, text: 'todo', completed: true }, 71 | { id: 3, text: 'todo', completed: false }, 72 | ]; 73 | const action = new TodoActions.PopulateTodosAction(todos); 74 | store.dispatch(action); 75 | fixture.detectChanges(); 76 | expect(component.countTodos).toEqual(2); 77 | expect(component.showFooter).toBeTruthy(); 78 | }); 79 | 80 | it('should return 0 undone todos and showFooter is false', () => { 81 | const todos = []; 82 | const action = new TodoActions.PopulateTodosAction(todos); 83 | store.dispatch(action); 84 | fixture.detectChanges(); 85 | expect(component.countTodos).toEqual(0); 86 | expect(component.showFooter).toBeFalsy(); 87 | }); 88 | }); 89 | 90 | describe('Test for currentFilter', () => { 91 | it('should currentFilter be "SHOW_ALL"', () => { 92 | const action = new FilterActions.SetFilterAction('SHOW_ALL'); 93 | store.dispatch(action); 94 | fixture.detectChanges(); 95 | expect(component.currentFilter).toEqual('SHOW_ALL'); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/regexp'; 32 | // import 'core-js/es6/map'; 33 | // import 'core-js/es6/weak-map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** 37 | * If your app need to indexed by Google Search, your app require polyfills 'core-js/es6/array' 38 | * Google bot use ES5. 39 | * FYI: Googlebot uses a renderer following the similar spec to Chrome 41. 40 | * https://developers.google.com/search/docs/guides/rendering 41 | **/ 42 | // import 'core-js/es6/array'; 43 | 44 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 45 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 46 | 47 | /** IE10 and IE11 requires the following for the Reflect API. */ 48 | // import 'core-js/es6/reflect'; 49 | 50 | 51 | /** Evergreen browsers require these. **/ 52 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 53 | import 'core-js/es7/reflect'; 54 | 55 | 56 | /** 57 | * Web Animations `@angular/platform-browser/animations` 58 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 59 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 60 | **/ 61 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 62 | 63 | /** 64 | * By default, zone.js will patch all possible macroTask and DomEvents 65 | * user can disable parts of macroTask/DomEvents patch by setting following flags 66 | */ 67 | 68 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 69 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 70 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 71 | 72 | /* 73 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 74 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 75 | */ 76 | // (window as any).__Zone_enable_cross_context_check = true; 77 | 78 | /*************************************************************************************************** 79 | * Zone JS is required by default for Angular itself. 80 | */ 81 | import 'zone.js/dist/zone'; // Included with Angular CLI. 82 | 83 | 84 | 85 | /*************************************************************************************************** 86 | * APPLICATION IMPORTS 87 | */ 88 | -------------------------------------------------------------------------------- /doc/fastlane.md: -------------------------------------------------------------------------------- 1 | # fastlane 2 | 3 | 一个快速打包发布 app 的工具, 能够大大提高开发人员的打包发布效率,你要是人工去整这些东西,非常容易出错,而且搞一次还得花不少时间。 4 | 5 | ## IOS 构建 6 | 7 | ### Mac 本地安装 8 | 9 | ```bash 10 | xcode-select --install 11 | # Using RubyGems 12 | sudo gem install fastlane -NV 13 | # Alternatively using Homebrew 14 | # brew cask install fastlane 15 | fastlane init 16 | # to have your Fastfile configuration written in Swift (Beta) 17 | fastlane init swift 18 | ``` 19 | 20 | ### 证书管理 21 | 22 | fastlane 提供了好几种证书管理的方式,非常方便团队协作 23 | 24 | #### 手动导入证书 25 | 26 | 若你直接将证书加密放到仓库下,可以直接通过解压导入, 比如,本项目证书放置在 `sh/release/certificates` 文件夹下,可以解压后再导入 27 | 28 | (解压代码位于 `sh/release/decrypt-key.sh`) 29 | 30 | ```bash 31 | #!/bin/sh 32 | 33 | if [[ -z "$PROFILE_NAME" ]]; then 34 | echo "Error: Missing provision profile name" 35 | exit 1 36 | fi 37 | 38 | if [[ ! -e "./sh/release/certificates.tar.enc" ]]; then 39 | echo "Error: Missing encrypted certificates." 40 | exit 1 41 | fi 42 | openssl aes-256-cbc -K $encrypted_28b1957c839b_key -iv $encrypted_28b1957c839b_iv -in ./sh/release/certificates.tar.enc -out ./sh/release/certificates.tar -d 43 | tar xvf ./sh/release/certificates.tar -C ./sh/release/certificates 44 | 45 | ``` 46 | 47 | fastlane 导入脚本 48 | 49 | ```bash 50 | # Import distribution certificate 51 | import_certificate( 52 | certificate_path: "sh/release/certificates/ios_distribution.p12", 53 | certificate_password: ENV["KEY_PASSWORD"], 54 | keychain_name: keychain_name, 55 | keychain_password: keychain_password 56 | ) 57 | 58 | # Import push certificate 59 | import_certificate( 60 | certificate_path: "sh/release/certificates/ios_push_distribution.p12", 61 | certificate_password: ENV["KEY_PASSWORD"], 62 | keychain_name: keychain_name, 63 | keychain_password: keychain_password 64 | ) 65 | ``` 66 | 67 | #### match 68 | 69 | match 可以自动为你选择和生成需要的证书,这也是官方推荐的方式,你只需要将团队用到的证书统一一个仓库进行管理。 70 | 可以参考这篇文章([setup-fastlane-match-for-ios](https://medium.com/@danielvivek2006/setup-fastlane-match-for-ios-6260758a9a4e) 71 | )进行设置 72 | 73 | #### cert 与 sigh 74 | 75 | 可以方便你手动对证书进行管理。 76 | 77 | - cert 可以自动帮你下载或者生成 `Certificates` ,**不要用于 第三方 CI 机器** 78 | - sign 可以自动帮你下载 `Provisioning Profiles` 79 | 80 | ## 插件 81 | 82 | 使用到三个插件 83 | 84 | - 集成 Cordova 构建的插件,[fastlane-plugin-cordova](https://github.com/bamlab/fastlane-plugin-cordova) 85 | - [fastlane-plugin-ionic](https://github.com/janpio/ionic-fastlane) 86 | - 蒲公英集成,fastlane-plugin-pgyer 87 | 88 | ## 上传至蒲公英 89 | 90 | 参见[蒲公英官网教程](https://www.pgyer.com/doc/view/fastlane) 91 | 92 | ## 一些疑问 93 | 94 | - [是否支持 windows?](https://github.com/fastlane/fastlane/issues/3594),答案是`不支持` 95 | 96 | ## Two-factor authentication 97 | 98 | 首先,必须开通 Two-factor authentication 99 | 100 | - 参见官网文档: https://docs.fastlane.tools/best-practices/continuous-integration/#authentication-with-apple-services 101 | - 相关 ISSUE: https://github.com/fastlane/fastlane/issues/14239 102 | 103 | 再来,你可以在 CI 机器上设置 session( 如果产生 session 的机器与 CI 机器不在一个区内,那也悲剧,`本项目基于 travis 构建时会要求输入安全码,所以构建会不成功`) 104 | 105 | - https://github.com/fastlane/fastlane/tree/master/spaceship 106 | 107 | ## 参考 108 | 109 | - [fastlane 官网](https://fastlane.tools/) 110 | - [fastlane 官方文档](https://docs.fastlane.tools/) 111 | - [automatic-ionic-and-ios-builds-with-jenkins-and-fastlane](https://www.3pillarglobal.com/insights/automatic-ionic-and-ios-builds-with-jenkins-and-f) 112 | 113 | ### demo 项目 114 | 115 | 可以学习这些项目打包构建的流程 116 | 117 | - 基于 `match` 打包证书,然后上传至 hockeyapp,[ionic-fastlane-travisci-hockeyapp](https://github.com/tim-hoffmann/ionic-fastlane-travisci-hockeyapp) 118 | - IOS 构建示例(基于 fastlane sigh): https://github.com/macoscope/ContinuousIntegrationExample 119 | - 一个涵盖 travis fastlane Jenkins 等一个大杂烩 Repo, [qm-ionic-quantimodo](https://github.com/mikepsinn/qm-ionic-quantimodo/blob/master/fastlane/Fastfile) -------------------------------------------------------------------------------- /src/app/pages/list/redux/todo/todo.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import * as TodoActions from './todo.actions'; 2 | import { TodosReducer } from './todo.reducer'; 3 | 4 | describe('Redux: TodosReducer', () => { 5 | 6 | it('should return a new state with new todo: AddTodoAction', () => { 7 | const action = new TodoActions.AddTodoAction('new todo'); 8 | const oldState = [{ id: 1, text: 'todo', completed: false }]; 9 | const newState = TodosReducer(oldState, action); 10 | expect(newState.length).toEqual(2); 11 | expect(newState[1].text).toEqual('new todo'); 12 | }); 13 | 14 | it('should return a new state with new todos: PopulateTodosAction', () => { 15 | const newTodos = [{ id: 2, text: 'new todo', completed: true }]; 16 | const action = new TodoActions.PopulateTodosAction(newTodos); 17 | const oldState = [{ id: 1, text: 'todo', completed: false }]; 18 | const newState = TodosReducer(oldState, action); 19 | expect(newState.length).toEqual(1); 20 | expect(newState[0].text).toEqual('new todo'); 21 | }); 22 | 23 | it('should return a new state with new todos: ToggleAction', () => { 24 | const action = new TodoActions.ToggleAction(1); 25 | const oldState = [ 26 | { id: 1, text: 'todo', completed: false }, 27 | { id: 2, text: 'todo', completed: false } 28 | ]; 29 | const newState = TodosReducer(oldState, action); 30 | expect(newState.length).toEqual(2); 31 | expect(newState[0].completed).toBeTruthy(); 32 | expect(newState[1].completed).toBeFalsy(); 33 | }); 34 | 35 | it('should return a new state with new todos: DeleteTodoAction', () => { 36 | const action = new TodoActions.DeleteTodoAction(1); 37 | const oldState = [ 38 | { id: 1, text: 'todo', completed: false }, 39 | { id: 2, text: 'todo', completed: false } 40 | ]; 41 | const newState = TodosReducer(oldState, action); 42 | expect(newState.length).toEqual(1); 43 | expect(newState[0].id).toEqual(2); 44 | }); 45 | 46 | it('should return a new state with new todos: UpdateAction', () => { 47 | const action = new TodoActions.UpdateAction(1, 'update todo'); 48 | const oldState = [ 49 | { id: 1, text: 'todo', completed: false }, 50 | { id: 2, text: 'todo', completed: false } 51 | ]; 52 | const newState = TodosReducer(oldState, action); 53 | expect(newState.length).toEqual(2); 54 | expect(newState[0].text).toEqual('update todo'); 55 | expect(newState[1].text).toEqual('todo'); 56 | }); 57 | 58 | it('should return a new state with new todos: ClearCompletedAction', () => { 59 | const action = new TodoActions.ClearCompletedAction(); 60 | const oldState = [ 61 | { id: 1, text: 'todo', completed: false }, 62 | { id: 2, text: 'todo', completed: true }, 63 | { id: 3, text: 'todo', completed: false }, 64 | ]; 65 | const newState = TodosReducer(oldState, action); 66 | expect(newState.length).toEqual(2); 67 | }); 68 | 69 | it('should return a new state with new todos: CompletedAllAction', () => { 70 | const action = new TodoActions.CompletedAllAction(); 71 | const oldState = [ 72 | { id: 1, text: 'todo', completed: false }, 73 | { id: 2, text: 'todo', completed: true }, 74 | { id: 3, text: 'todo', completed: false }, 75 | ]; 76 | const newState = TodosReducer(oldState, action); 77 | expect(newState.length).toEqual(3); 78 | expect(newState.every(t => t.completed)).toBeTruthy(); 79 | }); 80 | 81 | it('should return the same state with unknown action', () => { 82 | const action: any = new TodoActions.CompletedAllAction(); 83 | action.type = 'what'; 84 | const oldState = [ 85 | { id: 1, text: 'todo', completed: false }, 86 | { id: 2, text: 'todo', completed: true }, 87 | { id: 3, text: 'todo', completed: false }, 88 | ]; 89 | const newState = TodosReducer(oldState, action); 90 | expect(newState).toEqual(oldState); 91 | }); 92 | 93 | }); 94 | -------------------------------------------------------------------------------- /src/app/pages/list/ngrxtodo/ngrxtodo.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { Component } from '@angular/core'; 3 | import { RouterModule, Routes, ActivatedRoute } from '@angular/router'; 4 | import { ReactiveFormsModule, FormsModule } from '@angular/forms'; 5 | import { StoreModule, Store } from '@ngrx/store'; 6 | import { RouterTestingModule } from '@angular/router/testing'; 7 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 8 | import { ngrxtodoReducer, AppState } from './../redux/ngrxtodo.reducer'; 9 | import * as TodoActions from './../redux/todo/todo.actions'; 10 | 11 | import { TodoComponent } from './todo/todo.component'; 12 | import { FooterComponent } from './footer/footer.component'; 13 | import { NewTodoComponent } from './new-todo/new-todo.component'; 14 | import { TodoListComponent } from './todo-list/todo-list.component'; 15 | import { NgRxTodoComponent } from './ngrxtodo.page'; 16 | 17 | describe('AppComponent', () => { 18 | beforeEach(async(() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [ 21 | NgRxTodoComponent, 22 | TodoComponent, 23 | FooterComponent, 24 | TodoListComponent, 25 | NewTodoComponent, 26 | ], 27 | imports: [ 28 | ReactiveFormsModule, 29 | FormsModule, 30 | RouterTestingModule.withRoutes([ 31 | { path: '', component: TodoListComponent }, 32 | ]), 33 | StoreModule.forRoot({}), 34 | StoreModule.forFeature('ngrxtodo', ngrxtodoReducer), 35 | ], 36 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 37 | }).compileComponents(); 38 | })); 39 | 40 | it('should be created', () => { 41 | const store = TestBed.get(Store); 42 | spyOn(store, 'dispatch').and.callThrough(); 43 | 44 | spyOn(localStorage, 'getItem').and.returnValue('[]'); 45 | 46 | const fixture = TestBed.createComponent(NgRxTodoComponent); 47 | const component = fixture.componentInstance; 48 | fixture.detectChanges(); 49 | expect(component).toBeTruthy(); 50 | }); 51 | 52 | describe('Test for populateTodos', () => { 53 | it('should dispatch an action with item in localStorage', () => { 54 | const store = TestBed.get(Store); 55 | spyOn(store, 'dispatch').and.callThrough(); 56 | 57 | spyOn(localStorage, 'getItem').and.returnValue('[]'); 58 | 59 | const fixture = TestBed.createComponent(NgRxTodoComponent); 60 | const component = fixture.componentInstance; 61 | fixture.detectChanges(); 62 | 63 | const action = new TodoActions.PopulateTodosAction([]); 64 | expect(store.dispatch).toHaveBeenCalledWith(action); 65 | }); 66 | 67 | // it('should dispatch an action with null in localStorage', () => { 68 | // const store = TestBed.get(Store); 69 | // spyOn(store, 'dispatch').and.callThrough(); 70 | 71 | // spyOn(localStorage, 'getItem').and.returnValue(null); 72 | // const fixture = TestBed.createComponent(NgRxTodoComponent); 73 | // const component = fixture.componentInstance; 74 | // fixture.detectChanges(); 75 | 76 | // const action = new TodoActions.PopulateTodosAction([]); 77 | // expect(store.dispatch).toHaveBeenCalledWith(action); 78 | // }); 79 | }); 80 | 81 | describe('Test for updateTodos', () => { 82 | it('should called localStorage.setItem', () => { 83 | const store = TestBed.get(Store); 84 | spyOn(store, 'dispatch').and.callThrough(); 85 | 86 | spyOn(localStorage, 'getItem').and.returnValue('[]'); 87 | spyOn(localStorage, 'setItem').and.callThrough(); 88 | 89 | const fixture = TestBed.createComponent(NgRxTodoComponent); 90 | const component = fixture.componentInstance; 91 | fixture.detectChanges(); 92 | 93 | // expect(localStorage.setItem).toHaveBeenCalledWith( 94 | // 'angular-ngrx-todos', 95 | // '[]' 96 | // ); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /src/app/pages/list/redux/todo/todo.selectors.spec.ts: -------------------------------------------------------------------------------- 1 | import * as TodoSelectors from './todo.selectors'; 2 | 3 | describe('Redux: TodoSelectors', () => { 4 | describe('Test for getState', () => { 5 | it('should return the state', () => { 6 | const state = { 7 | todos: [], 8 | filter: '', 9 | }; 10 | const rta = TodoSelectors.getState.projector(state); 11 | expect(rta).toEqual(state); 12 | }); 13 | }); 14 | 15 | describe('Test for getTodos', () => { 16 | it('should return the todos', () => { 17 | const state = { 18 | todos: [], 19 | filter: '', 20 | }; 21 | const rta = TodoSelectors.getTodos.projector(state); 22 | expect(rta).toEqual(state.todos); 23 | }); 24 | }); 25 | 26 | describe('Test for getVisibleTodos', () => { 27 | it('should return the all todos with fitler "SHOW_ALL"', () => { 28 | const state = { 29 | todos: [ 30 | { id: 1, text: 'todo', completed: false }, 31 | { id: 2, text: 'todo', completed: true }, 32 | { id: 3, text: 'todo', completed: false }, 33 | ], 34 | filter: 'SHOW_ALL', 35 | }; 36 | const todos = TodoSelectors.getVisibleTodos.projector( 37 | state.todos, 38 | state.filter 39 | ); 40 | expect(todos.length).toEqual(3); 41 | }); 42 | 43 | it('should return the completed todos with fitler "SHOW_COMPLETED"', () => { 44 | const state = { 45 | todos: [ 46 | { id: 1, text: 'todo', completed: false }, 47 | { id: 2, text: 'todo', completed: true }, 48 | { id: 3, text: 'todo', completed: false }, 49 | ], 50 | filter: 'SHOW_COMPLETED', 51 | }; 52 | const todos = TodoSelectors.getVisibleTodos.projector( 53 | state.todos, 54 | state.filter 55 | ); 56 | expect(todos.length).toEqual(1); 57 | }); 58 | 59 | it('should return the undone todos with fitler "SHOW_ACTIVE"', () => { 60 | const state = { 61 | todos: [ 62 | { id: 1, text: 'todo', completed: false }, 63 | { id: 2, text: 'todo', completed: true }, 64 | { id: 3, text: 'todo', completed: false }, 65 | ], 66 | filter: 'SHOW_ACTIVE', 67 | }; 68 | const todos = TodoSelectors.getVisibleTodos.projector( 69 | state.todos, 70 | state.filter 71 | ); 72 | expect(todos.length).toEqual(2); 73 | }); 74 | 75 | it('should return the all todos with unknown fitler ', () => { 76 | const state = { 77 | todos: [ 78 | { id: 1, text: 'todo', completed: false }, 79 | { id: 2, text: 'todo', completed: true }, 80 | { id: 3, text: 'todo', completed: false }, 81 | ], 82 | filter: '', 83 | }; 84 | const todos = TodoSelectors.getVisibleTodos.projector( 85 | state.todos, 86 | state.filter 87 | ); 88 | expect(todos.length).toEqual(3); 89 | }); 90 | }); 91 | 92 | describe('Test for getStateCompleted', () => { 93 | it('should return false if one todo is undone', () => { 94 | const state = { 95 | todos: [ 96 | { id: 1, text: 'todo', completed: false }, 97 | { id: 2, text: 'todo', completed: true }, 98 | { id: 3, text: 'todo', completed: false }, 99 | ], 100 | filter: 'SHOW_ALL', 101 | }; 102 | const rta = TodoSelectors.getStateCompleted.projector(state.todos); 103 | expect(rta).toBeFalsy(); 104 | }); 105 | 106 | it('should return true if all todos is done', () => { 107 | const state = { 108 | todos: [ 109 | { id: 1, text: 'todo', completed: true }, 110 | { id: 2, text: 'todo', completed: true }, 111 | { id: 3, text: 'todo', completed: true }, 112 | ], 113 | filter: 'SHOW_ALL', 114 | }; 115 | const rta = TodoSelectors.getStateCompleted.projector(state.todos); 116 | expect(rta).toBeTruthy(); 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /doc/android.md: -------------------------------------------------------------------------------- 1 | # Android For Ionic 2 | 3 | see more from: https://yipeng.info/p/58aededee78659dd0b37f5c2 4 | 5 | ## 安装 6 | 7 | 1. 安装 JDK,在该[地址](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html)选择对应的版本下载即可,当然,最后我还是跑 CSDN 上去下载了,速度快,具体还是得看你的网络环境。安装好后,配置环境变量( JAVA_HOME ),并加入到 path 即可(%JAVA_HOME%/bin) 8 | 9 | 2. 安装 ADT后, 打开 SDK Manager 并配置好代理服务器,勾选需要用到的工具以下内容,高版本向低版本兼容,没必要一次装几个版本,一般是往新版本装 10 | - Android SDK Tools 11 | - Android SDK Platform-tools(平台) 12 | - Android SDK Build-tools(编译) 13 | - API: SDK Platform(编译依赖版本) 14 | - SDK Platform 15 | - TV/Wear SDK(可以不装,卸载掉就行) 16 | - Android Support Repository(Crosswalk 需要) 17 | - Extra 18 | - Android Support Repository 19 | - Google Repository(Crosswalk 需要) 20 | - 设置国内镜像代理,具体可以参考:http://www.androiddevtools.cn/ 21 | 22 | 3. 配置好 Android 开发环境:[参考](http://www.cnblogs.com/zoupeiyang/p/4034517.html),主要是一系列的环境变量得配置 23 | 24 | 4. **注意**: 这里并没有说到要安装 ant ,主要原因是 ionic/cordova Cli 在初次运行时若检测到没有安装时会自动安装,不过安装过程还真是折腾人,有一次挂了一网上都没有结果,第二天开了个 VPN 才接着开始下。 25 | 26 | ### 设置 SDK Manager 代理 27 | 28 | - 打开 ADT 内 Android SDK Manager,程序会自动检查需要更新的内容,正常情况下是无法成功的,因为你无法跨墙,所以,你得设置代理服务器。[androiddevtools.cn/](http://www.androiddevtools.cn/) 上有需要的代理服务器,**注意端口设置** 29 | 30 | ### 设置环境变量 31 | 32 | 主要有以下所述几个,具体设置需要根据你程序安装的路径来,这里至提供一个简单的参考,具体网上也有完善的教程 33 | 34 | ```bash 35 | JAVA_HOME:C:\Program Files\Java\jdk1.8.0_65 36 | 37 | _JAVA_OPTIONS:-Xmx512M 38 | 39 | ANDROID_HOME:C:\Users\path\to\adt-bundle-windows-x86-20131030\sdk 40 | 41 | ANDROID_PLATFORM_TOOLS:%ANDROID_HOME%\platforms 42 | 43 | ANDROID_TOOLS:%ANDROID_HOME%\tools 44 | 45 | ANT_HOME:C:\Users\path\to\apache-ant-1.9.5-bin\apache-ant-1.9.5 46 | ClASSPATH:.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar;C:\Users\path\to\apache-ant-1.9.5-bin\apache-ant-1.9.5\lib 47 | 48 | Path:C:\ProgramData\Oracle\Java\javapath;%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;%ANDROID_HOME%;C:\Users\path\to\apache-ant-1.9.5-bin\apache-ant-1.9.5\bin;C:\Users\path\to\adt-bundle-windows-x86- 49 | 20130917.467161976\adt-bundle-windows-x86-20130917\sdk\tools;C:\Users\path\to\adt-bundle- 50 | windows-x86-20130917.467161976\adt-bundle-windows-x86-20130917\sdk\platform-tools; 51 | 52 | ``` 53 | 54 | ## Android Studio 55 | 56 | 这个工具必装,主要有以下好处 57 | 58 | - 可以查看原生代码报错但没有反馈回 JS 控制台的问题,比如某些 jar 包没有引入或者路径不对等等 59 | - 可以用来查看 Android 日志,这其中日志比控制台全而且详细 60 | - 可以用来修改 Cordova 插件代码,并且实时断点调试,当然,这个工具要比 eclipse 还是好用得多 61 | 62 | ## Android 查看 sha1 63 | 64 | ### 基于 CMD 65 | 66 | Android 开发 app 时,必须使用 keystore 进行签名,否则应用将无法安装在手机等设备上 67 | 参考链接: http://jingyan.baidu.com/album/a3f121e4dece5ffc9052bbd9.html?picindex=1 68 | 69 | 1. 进入 `C:\Program Files\Java\jdk1.8.0_65\bin` 70 | 2. 运行命令 `keytool -list -keystore C:/Users/YOUR_USER_NAME/.android/debug.keystore -storepass android` 71 | 72 | ### 基于 Eclipse 73 | 74 | 打开 eclipse,走以下路径可以找到 75 | `window -> preference -> Android -> build -> sha1` 76 | 77 | ## Android 签名 78 | 79 | 生成命令 80 | 81 | ```bash 82 | # 使用 keytool 生成安全钥匙和证书 83 | keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000 84 | # 编译生成 Apk 包 85 | cordova build --release android 86 | # JAR 文件签名和验证 87 | ./jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-release-key.keystore android-release-unsigned.apk your_key 88 | # 对打包的 Android 应用程序进行优化 89 | zipalign -v 4 HelloWorld-release-unsigned.apk HelloWorld.apk 90 | 91 | ``` 92 | 93 | ### 使用 360 加固签名 94 | 95 | 这个时候,首先需要生成无签名的包,若包已签名则会报错,ionic 可以直接敲入命令`ionic build android -release`来生成: 96 | 97 | ```bash 98 | 自签名失败:jarsigner 无法对 jar 进行签名 java.util.zipexception 99 | ``` 100 | 101 | ## ERROR List 102 | 103 | - [unable to find attribute android:fontVariationSettings and android:ttcIndex](https://stackoverflow.com/questions/49162538/running-cordova-build-android-unable-to-find-attribute-androidfontvariation) 104 | 105 | ## 其它 106 | 107 | ### Gradle 108 | 109 | 从 Cordova-Android 6.4.0 开始, 必须安装 Gradle 才能 build Android,如果你使用的是 Windows, 需要将 Gradle 加到 path 中 110 | 111 | ## 参考 112 | 113 | - [Ionic Android 应用 Release 指南](https://segmentfault.com/a/1190000002617037) 114 | --------------------------------------------------------------------------------