diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8867a5..4e4bbce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: npm ci npm run build - name: Deploy to GitHub Pages - uses: JamesIves/github-pages-deploy-action@3.5.9 + uses: JamesIves/github-pages-deploy-action@3.7.1 with: ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} BASE_BRANCH: master diff --git a/package-lock.json b/package-lock.json index 62130a6..7e65de1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3796,6 +3796,14 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "dev": true }, + "copy-to-clipboard": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz", + "integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==", + "requires": { + "toggle-selection": "^1.0.6" + } + }, "copy-webpack-plugin": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.0.5.tgz", @@ -4522,7 +4530,8 @@ "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true }, "detect-node": { "version": "2.0.4", @@ -5566,9 +5575,10 @@ } }, "fibers": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/fibers/-/fibers-4.0.2.tgz", - "integrity": "sha512-FhICi1K4WZh9D6NC18fh2ODF3EWy1z0gzIdV9P7+s2pRjfRBnCkMDJ6x3bV1DkVymKH8HGrQa/FNOBjYvnJ/tQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fibers/-/fibers-4.0.3.tgz", + "integrity": "sha512-MW5VrDtTOLpKK7lzw4qD7Z9tXaAhdOmOED5RHzg3+HjUk+ibkjVW0Py2ERtdqgTXaerLkVkBy2AEmJiT6RMyzg==", + "dev": true, "requires": { "detect-libc": "^1.0.3" } @@ -7192,6 +7202,12 @@ } } }, + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -9044,11 +9060,6 @@ "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", "dev": true }, - "normalize.css": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", - "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==" - }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -10527,6 +10538,15 @@ "readable-stream": "^2.0.2" } }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -11215,6 +11235,17 @@ "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", "dev": true }, + "shelljs": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", + "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -12096,6 +12127,11 @@ "repeat-string": "^1.6.1" } }, + "toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" + }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", @@ -12648,6 +12684,24 @@ } } }, + "vue-cli-plugin-vuetify": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.0.6.tgz", + "integrity": "sha512-Y0VEK7gJTPW+LcSXBO8bx0YWzMoKMYHqcLpGF1EomGfAQrZafSulCZXcqF6++H4cPjFRkqB/YJ2xN/rTiu0DeA==", + "dev": true, + "requires": { + "semver": "^7.1.2", + "shelljs": "^0.8.3" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, "vue-eslint-parser": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-6.0.5.tgz", @@ -12735,6 +12789,21 @@ "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", "dev": true }, + "vuetify": { + "version": "2.3.17", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.3.17.tgz", + "integrity": "sha512-XqVDsCKHcWmJDJaSXEYg8Qt1jFdAhgm/L5ppAo19ATyld181lu0BC3V3vjakpU2uScHVwjxZw8Gre0lz+2cGvA==" + }, + "vuetify-loader": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vuetify-loader/-/vuetify-loader-1.6.0.tgz", + "integrity": "sha512-1bx3YeZ712dT1+QMX+XSFlP0O5k5O5Ui9ysBBmUZ9bWkAEHWZJQI9soI+qG5qmeFxUC0L9QYMCIKP0hOL/pf3Q==", + "dev": true, + "requires": { + "file-loader": "^4.0.0", + "loader-utils": "^1.2.0" + } + }, "watchpack": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", diff --git a/package.json b/package.json index cfec6d6..442c455 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,16 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "@disfactory/exif-js": "^2.3.0", "@vue/composition-api": "^0.3.2", "axios": "^0.19.0", + "copy-to-clipboard": "^3.3.1", "core-js": "^3.4.2", - "@disfactory/exif-js": "^2.3.0", - "fibers": "^4.0.2", - "normalize.css": "^8.0.1", "register-service-worker": "^1.6.2", "vue": "^2.6.10", "vue-carousel": "^0.18.0", - "vue-gtag": "^1.1.1" + "vue-gtag": "^1.1.1", + "vuetify": "^2.3.17" }, "devDependencies": { "@types/ol": "^5.3.6", @@ -39,12 +39,15 @@ "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1", "eslint-plugin-vue": "^6.0.1", + "fibers": "^4.0.3", "lint-staged": "^9.4.3", "ol": "^5.3.3", "sass": "^1.23.7", "sass-loader": "^8.0.0", "typescript": "^3.7.2", - "vue-template-compiler": "^2.6.10" + "vue-cli-plugin-vuetify": "~2.0.6", + "vue-template-compiler": "^2.6.10", + "vuetify-loader": "^1.3.0" }, "gitHooks": { "pre-commit": "lint-staged" diff --git a/public/images/locate.svg b/public/images/locate.svg index ad73666..b4a69be 100644 --- a/public/images/locate.svg +++ b/public/images/locate.svg @@ -1,7 +1,7 @@ - - - + + + diff --git a/public/images/marker-0.svg b/public/images/marker-0.svg new file mode 100644 index 0000000..92989a2 --- /dev/null +++ b/public/images/marker-0.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/marker-1.svg b/public/images/marker-1.svg new file mode 100644 index 0000000..9f3aacb --- /dev/null +++ b/public/images/marker-1.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/marker-2.svg b/public/images/marker-2.svg new file mode 100644 index 0000000..0d3e634 --- /dev/null +++ b/public/images/marker-2.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/marker-3.svg b/public/images/marker-3.svg new file mode 100644 index 0000000..1035c5d --- /dev/null +++ b/public/images/marker-3.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/marker-4.svg b/public/images/marker-4.svg new file mode 100644 index 0000000..9545235 --- /dev/null +++ b/public/images/marker-4.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/marker-5.svg b/public/images/marker-5.svg new file mode 100644 index 0000000..af14b36 --- /dev/null +++ b/public/images/marker-5.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/marker-6.svg b/public/images/marker-6.svg new file mode 100644 index 0000000..2a18d69 --- /dev/null +++ b/public/images/marker-6.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/marker-7.svg b/public/images/marker-7.svg new file mode 100644 index 0000000..b88db60 --- /dev/null +++ b/public/images/marker-7.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/marker-default.svg b/public/images/marker-default.svg new file mode 100644 index 0000000..57a8698 --- /dev/null +++ b/public/images/marker-default.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/remove.svg b/public/images/remove.svg new file mode 100644 index 0000000..e3b3a8d --- /dev/null +++ b/public/images/remove.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/zoom-in.svg b/public/images/zoom-in.svg index 725ce92..23ed9bf 100644 --- a/public/images/zoom-in.svg +++ b/public/images/zoom-in.svg @@ -1,3 +1,3 @@ - + diff --git a/public/images/zoom-out.svg b/public/images/zoom-out.svg index cbc0b9b..8cb50e8 100644 --- a/public/images/zoom-out.svg +++ b/public/images/zoom-out.svg @@ -1,3 +1,3 @@ - + diff --git a/public/index.html b/public/index.html index 09e6e6a..975cefe 100644 --- a/public/index.html +++ b/public/index.html @@ -27,6 +27,8 @@ 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); })(window,document,'script','dataLayer','GTM-WK2GJPS'); + + diff --git a/src/App.vue b/src/App.vue index 2dea66a..a88e655 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,49 +1,138 @@ + + diff --git a/src/components/ContactModal.vue b/src/components/ContactModal.vue index 0bc4bc2..c890fa7 100644 --- a/src/components/ContactModal.vue +++ b/src/components/ContactModal.vue @@ -11,7 +11,7 @@

地球公民基金會
- TEL: 07-2156809
+ TEL: 02-23920371
Email: cet@cet-taiwan.org

diff --git a/src/components/CreateFactorySteps.vue b/src/components/CreateFactorySteps.vue new file mode 100644 index 0000000..44b6a67 --- /dev/null +++ b/src/components/CreateFactorySteps.vue @@ -0,0 +1,452 @@ + + + + + diff --git a/src/components/DisplaySettingBottomSheet.vue b/src/components/DisplaySettingBottomSheet.vue new file mode 100644 index 0000000..5f4894e --- /dev/null +++ b/src/components/DisplaySettingBottomSheet.vue @@ -0,0 +1,76 @@ + + + diff --git a/src/components/FactoryDetailPage.vue b/src/components/FactoryDetailPage.vue new file mode 100644 index 0000000..f0c7513 --- /dev/null +++ b/src/components/FactoryDetailPage.vue @@ -0,0 +1,445 @@ + + + + + diff --git a/src/components/FilterModal.vue b/src/components/FilterModal.vue deleted file mode 100644 index 226ebdb..0000000 --- a/src/components/FilterModal.vue +++ /dev/null @@ -1,116 +0,0 @@ - - - - - diff --git a/src/components/FormPage.vue b/src/components/FormPage.vue deleted file mode 100644 index 1df8862..0000000 --- a/src/components/FormPage.vue +++ /dev/null @@ -1,522 +0,0 @@ - - - - - diff --git a/src/components/ImageUploadForm.vue b/src/components/ImageUploadForm.vue new file mode 100644 index 0000000..e1b753e --- /dev/null +++ b/src/components/ImageUploadForm.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/src/components/Map.vue b/src/components/Map.vue index 47ae790..6f4bc17 100644 --- a/src/components/Map.vue +++ b/src/components/Map.vue @@ -1,258 +1,98 @@ diff --git a/src/components/SafetyModal.vue b/src/components/SafetyModal.vue index b5bd84c..36ab611 100644 --- a/src/components/SafetyModal.vue +++ b/src/components/SafetyModal.vue @@ -1,15 +1,15 @@ diff --git a/src/components/UpdateFactorySteps.vue b/src/components/UpdateFactorySteps.vue new file mode 100644 index 0000000..4ad5f55 --- /dev/null +++ b/src/components/UpdateFactorySteps.vue @@ -0,0 +1,335 @@ + + + + + diff --git a/src/lib/appState.ts b/src/lib/appState.ts index 5cfc54b..f4e1ef3 100644 --- a/src/lib/appState.ts +++ b/src/lib/appState.ts @@ -1,4 +1,4 @@ -import { inject, provide, reactive } from '@vue/composition-api' +import { inject, provide, reactive, computed } from '@vue/composition-api' import { useGA } from './useGA' import { FactoryData } from '../types' @@ -6,17 +6,72 @@ const AppStateSymbol = Symbol('AppState') // A global state that can be shared across the entire application +export const enum PageState { + INITIAL = 'INITIAL', + CREATE_FACTORY_1 = 'CREATE_FACTORY_1', + CREATE_FACTORY_2 = 'CREATE_FACTORY_2', + CREATE_FACTORY_3 = 'CREATE_FACTORY_3', + UPDATE_FACTORY_IMAGES = 'UPDATE_FACTORY_IMAGES', + UPDATE_FACTORY_MODE = 'UPDATE_FACTORY_MODE' +} + +const CreateFactoryPageState = [ + PageState.CREATE_FACTORY_1, + PageState.CREATE_FACTORY_2, + PageState.CREATE_FACTORY_3 +] + +const UpdateFactoryPageState = [ + PageState.UPDATE_FACTORY_IMAGES, + PageState.UPDATE_FACTORY_MODE +] + export const provideAppState = () => { - const appState = reactive({ + const appState: { + pageState: PageState, + factoryData: FactoryData | null, + factoryLocation: number[], + isCreateMode: boolean, + createStepIndex: number, + isEditImagesMode: boolean, + selectFactoryMode: boolean, + formPageOpen: boolean, + mapLngLat: number[], + canPlaceFactory: boolean, + factoryDetailsExpanded: boolean, + updateFactoryField: string, + isEditComment: boolean, + isEditName: boolean, + isEditType: boolean, + isInitialPage: boolean + } = reactive({ // Page state - // TODO: should be rewritten with vue router? - formMode: 'create', - factoryFormOpen: false, - factoryData: null as FactoryData | null, + pageState: PageState.INITIAL, + + factoryData: null, factoryLocation: [] as number[], - // Map state - selectFactoryMode: false + isCreateMode: computed(() => CreateFactoryPageState.includes(appState.pageState)), + createStepIndex: computed(() => CreateFactoryPageState.indexOf(appState.pageState) + 1), + + isEditImagesMode: computed(() => appState.pageState === PageState.UPDATE_FACTORY_IMAGES), + isEditFactoryMode: computed(() => appState.pageState === PageState.UPDATE_FACTORY_MODE), + updateFactoryField: 'others', + isEditComment: computed(() => appState.pageState === PageState.UPDATE_FACTORY_MODE && appState.updateFactoryField === 'others'), + isEditName: computed(() => appState.pageState === PageState.UPDATE_FACTORY_MODE && appState.updateFactoryField === 'name'), + isEditType: computed(() => appState.pageState === PageState.UPDATE_FACTORY_MODE && appState.updateFactoryField === 'factory_type'), + + isEditMode: computed(() => UpdateFactoryPageState.includes(appState.pageState)), + isInitialState: computed(() => appState.pageState === PageState.INITIAL), + + selectFactoryMode: computed(() => appState.pageState === PageState.CREATE_FACTORY_1), + formPageOpen: computed(() => CreateFactoryPageState.includes(appState.pageState) || appState.pageState === PageState.UPDATE_FACTORY_IMAGES), + isInitialPage: computed(() => appState.pageState === PageState.INITIAL), + + // map states + mapLngLat: [] as number[], + canPlaceFactory: false, + factoryDetailsExpanded: false }) provide(AppStateSymbol, appState) @@ -27,28 +82,154 @@ export const provideAppState = () => { const registerMutator = (appState: AppState) => { const { event, pageview } = useGA() - return { - updateFactoryData (factory: FactoryData) { - appState.factoryData = factory + // page transition methods + const invalidPageTransition = () => { + throw new Error('Invalid page transition') + } + + // a state machine transition implementation + const pageTransition = { + startCreateFactory () { + if (appState.pageState === PageState.INITIAL) { + appState.pageState = PageState.CREATE_FACTORY_1 + } else { + invalidPageTransition() + } + + event('enterSelectFactoryMode') }, - openCreateFactoryForm () { - appState.factoryData = null - appState.formMode = 'create' - appState.factoryFormOpen = true - pageview('/create') + gotoNextCreate () { + const index = CreateFactoryPageState.indexOf(appState.pageState) + if (index !== -1 && index !== CreateFactoryPageState.length - 1) { + appState.pageState = CreateFactoryPageState[index + 1] + } else { + invalidPageTransition() + } + + if (index === 0) { + pageview('/create') + } }, - openEditFactoryForm (factory: FactoryData) { - appState.factoryData = factory - appState.formMode = 'edit' - appState.factoryFormOpen = true + nextCreateStep () { + const index = CreateFactoryPageState.indexOf(appState.pageState) + if (CreateFactoryPageState[index + 1]) { + appState.pageState = CreateFactoryPageState[index + 1] + } + }, + + previousCreateStep () { + const index = CreateFactoryPageState.indexOf(appState.pageState) + if (CreateFactoryPageState[index - 1]) { + appState.pageState = CreateFactoryPageState[index - 1] + } + }, + + cancelCreateFactory () { + if (appState.pageState in CreateFactoryPageState) { + appState.pageState = PageState.INITIAL + } else { + invalidPageTransition() + } + + event('exitSelectFactoryMode') + }, + + /** + * Goto create step + * Noted: **zero-based**, can be either 0, 1, 2 + */ + gotoCreateStep (step: number) { + if (CreateFactoryPageState[step]) { + appState.pageState = CreateFactoryPageState[step] + } else { + invalidPageTransition() + } + }, + + startUpdateFactoryImages () { + if (appState.pageState === PageState.INITIAL) { + appState.pageState = PageState.UPDATE_FACTORY_IMAGES + } else { + invalidPageTransition() + } + pageview('/edit') }, + cancelUpdateFactoryImages () { + if (appState.pageState === PageState.UPDATE_FACTORY_IMAGES) { + appState.pageState = PageState.INITIAL + } else { + invalidPageTransition() + } + + event('exitUpdateFactoryImagesMode') + }, + + startUpdateFactoryComment (field = 'others') { + if (appState.pageState === PageState.INITIAL) { + appState.pageState = PageState.UPDATE_FACTORY_MODE + appState.updateFactoryField = field + } else { + invalidPageTransition() + } + + pageview('/editComment') + }, + + cancelUpdateFactoryComment () { + if (appState.pageState === PageState.UPDATE_FACTORY_MODE) { + appState.pageState = PageState.INITIAL + } else { + invalidPageTransition() + } + + event('exitUpdateFactoryCommentsMode') + }, + closeFactoryPage () { - appState.factoryFormOpen = false + if (CreateFactoryPageState.includes(appState.pageState) || UpdateFactoryPageState.includes(appState.pageState)) { + appState.pageState = PageState.INITIAL + } else { + invalidPageTransition() + } event('closeFactoryPage') + } + } + + function updateFactoryData (factory: FactoryData) { + appState.factoryData = factory + } + + function expandFactoryDetail () { + appState.factoryDetailsExpanded = true + } + + function collapseFactoryDetail () { + if (appState.factoryData?.feature?.get('defaultStyle')) { + appState.factoryData?.feature?.setStyle(appState.factoryData?.feature?.get('defaultStyle')) + appState.factoryData?.feature?.unset('defaultStyle') + } + appState.factoryDetailsExpanded = false + appState.factoryData = null + } + + function toggleFactoryDetail () { + appState.factoryDetailsExpanded = !appState.factoryDetailsExpanded + } + + return { + pageTransition, + + updateFactoryData, + + openEditFactoryForm (factory: FactoryData) { + updateFactoryData(factory) + pageTransition.startUpdateFactoryImages() + + pageview('/edit') }, setFactoryLocation (value: [number, number]) { @@ -56,15 +237,9 @@ const registerMutator = (appState: AppState) => { event('setFactoryLocation') }, - enterSelectFactoryMode () { - appState.selectFactoryMode = true - event('enterSelectFactoryMode') - }, - - exitSelectFactoryMode () { - appState.selectFactoryMode = false - event('exitSelectFactoryMode') - } + expandFactoryDetail, + collapseFactoryDetail, + toggleFactoryDetail } } diff --git a/src/lib/factory.ts b/src/lib/factory.ts new file mode 100644 index 0000000..9811881 --- /dev/null +++ b/src/lib/factory.ts @@ -0,0 +1,16 @@ +import { FactoryData, FACTORY_TYPE, FactoryType } from '../types' + +const factoryTypeMap: { [key: string]: string } = FACTORY_TYPE.reduce((acc, obj) => { + return { + ...acc, + [obj.value]: obj.text + } +}, {}) + +export const getFactoryTypeText = (factory: FactoryData) => { + if (factory.type) { + return factoryTypeMap[factory.type] as FactoryType + } else { + return null + } +} diff --git a/src/lib/factoryPopup.ts b/src/lib/factoryPopup.ts deleted file mode 100644 index b0912cf..0000000 --- a/src/lib/factoryPopup.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { provide, inject, reactive } from '@vue/composition-api' -import { FactoryData, FACTORY_TYPE, FactoryStatusText } from '../types' -import { getStatusBorderColor, getFactoryStatus } from './map' - -const FactoryPopupSymbol = Symbol('FactoryPopup') - -export type FactoryPopupState = { - show: boolean -} - -export const providePopupState = () => { - const popupState = reactive({ - show: false - }) - - provide(FactoryPopupSymbol, popupState) - - return [popupState] -} - -export const useFactoryPopup = () => { - const popupState = inject(FactoryPopupSymbol) as FactoryPopupState - - return [popupState] -} - -const generateFactorySummary = (factory: FactoryData) => { - const imageStatus = factory.images.length > 0 ? '已有照片' : '缺照片' - - const type = FACTORY_TYPE.find(type => type.value === factory.type) - let typeText: string = (type && type.text) || '其他' - - if (typeText.includes('金屬')) { - typeText = '金屬' - } - - return [ - imageStatus, - typeText - ].filter(Boolean).join('\n') -} - -export const getPopupData = (factory: FactoryData) => { - const status = getFactoryStatus(factory) - - return { - id: factory.id, - name: factory.name, - color: getStatusBorderColor(status), - status: FactoryStatusText[status][0], - summary: generateFactorySummary(factory) - } -} diff --git a/src/lib/hooks.ts b/src/lib/hooks.ts index 00e011e..210db35 100644 --- a/src/lib/hooks.ts +++ b/src/lib/hooks.ts @@ -27,6 +27,7 @@ const ModalStateSymbol: InjectionKey = Symbol('ModalStateSymbol') export const provideModalState = () => { const modalState = reactive({ updateFactorySuccessModal: false, + updateFactoryImageSuccessModal: false, createFactorySuccessModal: false, aboutModalOpen: false, contactModalOpen: false, @@ -48,7 +49,7 @@ type ModalState = ReturnType type ModalActions = { openUpdateFactorySuccessModal: Function, - closeUpdateFactorySuccessModal: Function, + openUpdateFactoryImagesSuccessModal: Function, openCreateFactorySuccessModal: Function, closeCreateFactorySuccessModal: Function, @@ -84,10 +85,28 @@ export const useModalState: () => [ModalState, ModalActions] = () => { } const { event } = useGA() - const openUpdateFactorySuccessModal = () => { modalState.updateFactorySuccessModal = true } - const closeUpdateFactorySuccessModal = () => { modalState.updateFactorySuccessModal = false } + const openUpdateFactoryImagesSuccessModal = () => { + modalState.updateFactoryImageSuccessModal = true - const openCreateFactorySuccessModal = () => { modalState.createFactorySuccessModal = true } + window.setTimeout(() => { + modalState.updateFactoryImageSuccessModal = false + }, 3000) + } + + const openUpdateFactorySuccessModal = () => { + modalState.updateFactorySuccessModal = true + + window.setTimeout(() => { + modalState.updateFactorySuccessModal = false + }, 3000) + } + + const openCreateFactorySuccessModal = () => { + modalState.createFactorySuccessModal = true + window.setTimeout(() => { + modalState.createFactorySuccessModal = false + }, 3000) + } const closeCreateFactorySuccessModal = () => { modalState.createFactorySuccessModal = false } const openAboutModal = () => { modalState.aboutModalOpen = true } @@ -124,7 +143,7 @@ export const useModalState: () => [ModalState, ModalActions] = () => { const modalActions = { openUpdateFactorySuccessModal, - closeUpdateFactorySuccessModal, + openUpdateFactoryImagesSuccessModal, openCreateFactorySuccessModal, closeCreateFactorySuccessModal, diff --git a/src/lib/hooks/useInsideViewport.ts b/src/lib/hooks/useInsideViewport.ts new file mode 100644 index 0000000..65fcf19 --- /dev/null +++ b/src/lib/hooks/useInsideViewport.ts @@ -0,0 +1,38 @@ +import { onUnmounted, Ref, ref, watch } from '@vue/composition-api' + +export const useInsideViewport = (scrollContainer: Ref, div: Ref, defaultValue = true) => { + const inside = ref(defaultValue) + + const onScroll = () => { + if (!div.value) { + return + } + + const elem = div.value + const rect = elem.getBoundingClientRect() + + // taken from https://stackoverflow.com/a/7557433 + inside.value = rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /* or $(window).height() */ + rect.right <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */ + } + + watch(scrollContainer, (scrollElem) => { + if (!scrollElem) { + return + } + + scrollElem.addEventListener('scroll', onScroll) + }) + + onUnmounted(() => { + if (scrollContainer.value) { + scrollContainer.value.removeEventListener('scroll', onScroll) + } + }) + + return inside +} + +export default useInsideViewport diff --git a/src/lib/hooks/useScroll.ts b/src/lib/hooks/useScroll.ts new file mode 100644 index 0000000..b585dc0 --- /dev/null +++ b/src/lib/hooks/useScroll.ts @@ -0,0 +1,40 @@ +import { onUnmounted, Ref, ref, watch } from '@vue/composition-api' + +export const useScroll = (scrollContainer: Ref) => { + const scrollTop = ref(0) + const scrollLeft = ref(0) + + const onScroll = () => { + if (!scrollContainer.value) { + return + } + const container = scrollContainer.value + + scrollTop.value = container.scrollTop + scrollLeft.value = container.scrollLeft + } + + watch(scrollContainer, (scrollElem) => { + if (!scrollElem) { + return + } + + scrollTop.value = scrollElem.scrollTop + scrollLeft.value = scrollElem.scrollLeft + + scrollElem.addEventListener('scroll', onScroll) + }) + + onUnmounted(() => { + if (scrollContainer.value) { + scrollContainer.value.removeEventListener('scroll', onScroll) + } + }) + + return { + scrollTop, + scrollLeft + } +} + +export default useScroll diff --git a/src/lib/imageUpload.ts b/src/lib/imageUpload.ts new file mode 100644 index 0000000..737ebd0 --- /dev/null +++ b/src/lib/imageUpload.ts @@ -0,0 +1,86 @@ +import { UploadedImage, uploadImages } from '@/api' +import { computed, reactive, ref, watch } from '@vue/composition-api' + +export const useImageUpload = () => { + const uploadedImages = ref([]) + const imageUploadState = reactive({ + error: null as boolean | null, + uploading: false + }) + + const selectedImages = ref(null) + watch(selectedImages, () => { + imageUploadState.error = null + + if (!selectedImages.value) { + return + } + + imageUploadState.uploading = true + + uploadImages(selectedImages.value).then(images => { + uploadedImages.value = [ + ...uploadedImages.value, + ...images + ] + }).catch(err => { + console.error(err) + imageUploadState.error = true + }).finally(() => { + imageUploadState.uploading = false + }) + }) + + const onClickRemoveImage = ({ src }: UploadedImage) => { + uploadedImages.value = uploadedImages.value.filter(image => image.src !== src) + } + + const imageUploadFormValid = computed(() => uploadedImages.value.length > 0 && !imageUploadState.uploading) + + return { + selectedImages, + imageUploadState, + uploadedImages, + onClickRemoveImage, + imageUploadFormValid + } +} + +export const useUpdateFactoryImage = () => { + const uploadedImages = ref([]) + const imageUploadState = { + error: null, + uploading: false + } + + const selectedImages = ref(null) + watch(selectedImages, () => { + if (!selectedImages.value) { + return + } + + const images = Array.from(selectedImages.value).map(f => URL.createObjectURL(f)).map(v => ({ + src: v, + token: v + })) + + uploadedImages.value = [ + ...uploadedImages.value, + ...images + ] + }) + + const onClickRemoveImage = ({ src }: UploadedImage) => { + uploadedImages.value = uploadedImages.value.filter(image => image.src !== src) + } + + const imageUploadFormValid = computed(() => uploadedImages.value.length > 0) + + return { + selectedImages, + imageUploadState, + uploadedImages, + onClickRemoveImage, + imageUploadFormValid + } +} diff --git a/src/lib/map.ts b/src/lib/map.ts index f821242..6c1b875 100644 --- a/src/lib/map.ts +++ b/src/lib/map.ts @@ -12,35 +12,22 @@ import { Zoom, ScaleLine, Rotate } from 'ol/control' import Geolocation from 'ol/Geolocation' import { defaults as defaultInteractions, PinchRotate } from 'ol/interaction' -import { FactoryData, FactoryStatus } from '../types' +import { FactoryData, defaultFactoryDisplayStatuses, FactoryDisplayStatusType, FactoryDisplayStatuses } from '../types' import { flipArgriculturalLand } from '../lib/image' import RenderFeature from 'ol/render/Feature' import { MapOptions } from 'ol/PluggableMap' import IconOrigin from 'ol/style/IconOrigin' -const getFactoryStatusImage = (status: FactoryStatus) => `/images/marker-${status}.svg` -export const getStatusBorderColor = (status: FactoryStatus) => { - return { - [FactoryStatus.NEW]: '#447287', - [FactoryStatus.EXISTING_COMPLETE]: '#58585B', - [FactoryStatus.EXISTING_INCOMPLETE]: '#58585B', - [FactoryStatus.REPORTED]: '#6D8538' - }[status] +const getFactoryStatusImage = (status: FactoryDisplayStatusType) => `/images/marker-${status}.svg` +export const getStatusBorderColor = (status: FactoryDisplayStatusType) => { + return FactoryDisplayStatuses.find(s => s.type === status)?.color } -export function getFactoryStatus (factory: FactoryData) { - if (factory.cet_report_status === 'B') { - return FactoryStatus.REPORTED - } - - if (factory.before_release) { - if (factory.data_complete) { - return FactoryStatus.EXISTING_COMPLETE - } else { - return FactoryStatus.EXISTING_INCOMPLETE - } +export function getFactoryStatus (factory: FactoryData): FactoryDisplayStatusType { + if (factory.document_display_status) { + return FactoryDisplayStatuses.find(s => s.name === factory.document_display_status)?.type as FactoryDisplayStatusType } else { - return FactoryStatus.NEW + return FactoryDisplayStatuses[0].type } } @@ -50,6 +37,12 @@ export enum BASE_MAP { SATELITE } +export const BASE_MAP_NAME = { + [BASE_MAP.OSM]: '簡易地圖', + [BASE_MAP.TAIWAN]: '詳細地圖', + [BASE_MAP.SATELITE]: '衛星地圖' +} + type ButtonElements = { zoomIn: HTMLImageElement, zoomOut: HTMLImageElement @@ -70,16 +63,16 @@ const makeMapButtons = () => { }, {}) as ButtonElements } -const iconStyleMap = Object.entries(FactoryStatus).reduce((acc, [status, src]) => ({ +const iconStyleMap = defaultFactoryDisplayStatuses.reduce((acc, status) => ({ ...acc, [status]: new Style({ image: new Icon({ anchorYUnits: IconAnchorUnits.PIXELS, anchorOrigin: IconOrigin.BOTTOM_LEFT, - src: getFactoryStatusImage(src) + src: getFactoryStatusImage(status) }) }) -}), {}) as {[key in FactoryStatus]: Style} +}), {}) as {[key in FactoryDisplayStatusType]: Style} const nullStyle = new Style({}) @@ -98,13 +91,7 @@ const minimapPinStyle = new Style({ export class MapFactoryController { private _map: OLMap - private appliedFilters: FactoryStatus[] = [ - FactoryStatus.NEW, - FactoryStatus.EXISTING_INCOMPLETE, - FactoryStatus.EXISTING_COMPLETE, - FactoryStatus.REPORTED - ] - + private appliedFilters: FactoryDisplayStatusType[] = [] private _factoriesLayerSource?: VectorSource private factoryMap = new Map() @@ -168,14 +155,19 @@ export class MapFactoryController { }) } - public setFactoryStatusFilter (filters: FactoryStatus[]) { + public setFactoryStatusFilter (filters: FactoryDisplayStatusType[]) { this.appliedFilters = filters this.updateFactoriesFeatureStyle() } private isFactoryVisible (factory: FactoryData) { - return this.appliedFilters.includes(getFactoryStatus(factory)) + if (this.appliedFilters.length === 0) { + // always visible if no filters applied + return true + } else { + return this.appliedFilters.includes(getFactoryStatus(factory)) + } } private getFactoryStyle (factory: FactoryData): Style { @@ -398,6 +390,13 @@ export class OLMap { } }) + // snippet taken from https://gis.stackexchange.com/q/310775 + map.on('pointermove', event => { + if (!event.dragging) { + map.getTargetElement().style.cursor = map.hasFeatureAtPixel(map.getEventPixel(event.originalEvent)) ? 'pointer' : '' + } + }) + this.getLUIMAPLayer().then(layer => { layer.on('change:visible', function () { if (handler.onLUILayerVisibilityChange) { diff --git a/src/lib/useMapMode.ts b/src/lib/useMapMode.ts new file mode 100644 index 0000000..86bc0f4 --- /dev/null +++ b/src/lib/useMapMode.ts @@ -0,0 +1,20 @@ +import { inject, InjectionKey, provide, Ref, ref } from '@vue/composition-api' +import { BASE_MAP } from './map' + +const mapModeSymbol: InjectionKey<{ currentMapMode: Ref }> = Symbol('MapModeSymbol') + +export function provideMapMode () { + const currentMapMode = ref(BASE_MAP.OSM) + + return provide(mapModeSymbol, { currentMapMode }) +} + +export function useMapMode () { + const context = inject(mapModeSymbol) + + if (!context) { + throw new Error('Please use provideMapMode before useMapMode.') + } + + return context +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..164e95d --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,5 @@ +import { SetupContext } from '@vue/composition-api' + +export const sleep = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms)) + +export const waitNextTick = (context: SetupContext) => new Promise(resolve => context.root.$nextTick(resolve)) diff --git a/src/main.ts b/src/main.ts index a78ad07..1af5ad1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import App from './App.vue' import VueCompositionApi from '@vue/composition-api' import './registerServiceWorker' import VueGtag from 'vue-gtag' +import vuetify from './plugins/vuetify' Vue.config.productionTip = false Vue.use(VueCompositionApi) @@ -13,5 +14,6 @@ Vue.use(VueGtag, { }) new Vue({ + vuetify, render: (h) => h(App) }).$mount('#app') diff --git a/src/plugins/vuetify.ts b/src/plugins/vuetify.ts new file mode 100644 index 0000000..c2c0efa --- /dev/null +++ b/src/plugins/vuetify.ts @@ -0,0 +1,17 @@ +import Vue from 'vue' +import Vuetify from 'vuetify/lib' +import colors from 'vuetify/lib/util/colors' + +Vue.use(Vuetify) + +export default new Vuetify({ + theme: { + themes: { + light: { + primary: '#697F01', + secondary: '#364516', + accent: colors.green.base + } + } + } +}) diff --git a/src/styles/components/preview-images.scss b/src/styles/components/preview-images.scss new file mode 100644 index 0000000..e5c6a88 --- /dev/null +++ b/src/styles/components/preview-images.scss @@ -0,0 +1,38 @@ +.preview-images-container { + .uploaded-image { + position: relative; + width: calc(50% - 7.5px); + height: 0; + padding-top: 35%; + overflow: hidden; + display: inline-block; + + &:nth-child(odd) { + margin-right: 7.5px; + } + + &:nth-child(even) { + margin-left: 7.5px; + } + + img { + object-fit: cover; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + } + + .remove-image-btn { + position: absolute; + top: 5px; + right: 5px; + + width: 20px; + height: 20px; + background-image: url(/images/remove.svg); + cursor: pointer; + } + } +} diff --git a/src/styles/global.scss b/src/styles/global.scss index df67860..70df2f8 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -1,6 +1,8 @@ @import './variables.scss'; @import '~ol/ol.css'; @import './openlayer'; +@import './vuetify-extend'; +@import './utils'; html, body { @@ -14,6 +16,10 @@ body { touch-action: manipulation; } +html { + overflow-y: hidden; +} + * { line-height: 1; box-sizing: border-box; @@ -23,3 +29,14 @@ body { *:focus { outline:none } + +.required:after { + content: '*'; + color: #A22A29; + font-size: 13px; + position: absolute; +} + +.color-gray-light { + color: $gray-light-color; +} diff --git a/src/styles/index.scss b/src/styles/index.scss index 9555fbd..1aeee58 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -1,3 +1,2 @@ -@import '~normalize.css'; @import './global.scss'; diff --git a/src/styles/openlayer.scss b/src/styles/openlayer.scss index 6c221e1..e6f44e6 100644 --- a/src/styles/openlayer.scss +++ b/src/styles/openlayer.scss @@ -3,7 +3,7 @@ .ol-control { - background: none; + background: white; cursor: pointer; &:hover { @@ -18,21 +18,22 @@ } box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.5); - border: solid 1px #ffffff; } .ol-zoom, .ol-fit-location, .ol-map-search { - background-color: $primary-color; + color: $primary-color; + border-radius: 9px; left: 21px; &:hover { - background-color: $primary-color; + background-color: #f5f5f5; } } .ol-zoom { - top: 78px; + top: auto; + bottom: 100px; .ol-zoom-in, .ol-zoom-out { @@ -51,7 +52,7 @@ } .ol-fit-location { - top: 161px; + bottom: 185px; &.ol-control button { padding: 2px; @@ -77,14 +78,18 @@ } } -.ol-rotate { - background-color: $primary-color; +.ol-rotate{ + left: 21px; + right: auto; + bottom: 235px; + top: auto; &:hover { - background-color: $primary-color; + background-color: #f5f5f5; } .ol-compass { + color: $primary-color; margin-top: -5px; } } diff --git a/src/styles/typography.scss b/src/styles/typography.scss new file mode 100644 index 0000000..5c41e62 --- /dev/null +++ b/src/styles/typography.scss @@ -0,0 +1,17 @@ +@import "./variables"; + +h2 { + color: $dark-green-color; + font-size: 20px; + line-height: 30px; +} + +p { + font-size: 16px; + line-height: 24px; +} + +a { + color: $second-color; + text-decoration: underline; +} \ No newline at end of file diff --git a/src/styles/utils.scss b/src/styles/utils.scss index 45c35c7..9dd26f7 100644 --- a/src/styles/utils.scss +++ b/src/styles/utils.scss @@ -6,6 +6,10 @@ width: auto; } +.w-100 { + width: 100%; +} + @mixin close-button { position: absolute; top: 24px; diff --git a/src/styles/variables.scss b/src/styles/variables.scss index 68ab7a0..8083caf 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -12,3 +12,5 @@ $red-color: #bb4a37; $blue-color: #447287; $gray-color: #58585B; $dark-green-color: #364516; +$light-green-color: #697F01; +$gray-light-color: #A1A1A1; diff --git a/src/styles/vuetify-extend.scss b/src/styles/vuetify-extend.scss new file mode 100644 index 0000000..104f09f --- /dev/null +++ b/src/styles/vuetify-extend.scss @@ -0,0 +1,3 @@ +.v-btn.v-btn-plain:hover:before { + background-color: transparent; +} diff --git a/src/types.ts b/src/types.ts index aa87cc3..d96b3dd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import Feature from 'ol/Feature' + /* eslint-disable quote-props */ export const FACTORY_TYPE = [ { value: '2-1', text: '金屬: 沖床、銑床、車床、鏜孔' }, @@ -13,33 +15,81 @@ export const FACTORY_TYPE = [ ] as const export type FactoryType = (typeof FACTORY_TYPE)[number]['value'] -type CetReportStatus = 'A' | 'B' +export type FactoryDisplayStatusType = 'default' | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 + +export const defaultFactoryDisplayStatuses = [ + 'default', 0, 1, 2, 3, 4, 5, 6, 7 +] as FactoryDisplayStatusType[] -export const CetReportStatusText = { - A: '未舉報', - B: '已舉報' +type FactoryDisplayStatus = { + type: FactoryDisplayStatusType, + name: string, + color: string } +export const FactoryDisplayStatuses: FactoryDisplayStatus[] = [ + { + type: 'default', + name: '疑似工廠', + color: '#D27E00' + }, + { + type: 0, + name: '已檢舉', + color: '#697F01' + }, + { + type: 1, + name: '已排程稽查', + color: '#C8D48D' + }, + { + type: 2, + name: '陳述意見期', + color: '#457287' + }, + { + type: 3, + name: '已勒令停工', + color: '#E59B9B' + }, + { + type: 4, + name: '已發函斷電', + color: '#CF5E5D' + }, + { + type: 5, + name: '已排程拆除', + color: '#A22A29' + }, + { + type: 6, + name: '已拆除', + color: '#364516' + }, + { + type: 7, + name: '不再追蹤', + color: '#A1A1A1' + } +] -export enum FactoryStatus { - NEW = 'NEW', - EXISTING_INCOMPLETE = 'EXISTING_INCOMPLETE', - EXISTING_COMPLETE = 'EXISTING_COMPLETE', - REPORTED = 'REPORTED' +const FactoryDisplayStatusMap = FactoryDisplayStatuses.reduce((acc, c) => { + return { + ...acc, + [c.type]: c + } +}, {}) as { + [key in FactoryDisplayStatusType]: FactoryDisplayStatus } -export const FactoryStatusText = { - [FactoryStatus.NEW]: ['民眾回報工廠'], - [FactoryStatus.EXISTING_COMPLETE]: ['政府盤查工廠'], - [FactoryStatus.EXISTING_INCOMPLETE]: ['政府盤查工廠', '資料不齊'], - [FactoryStatus.REPORTED]: ['已舉報違章工廠'] +export const getDisplayStatusText = (status: FactoryDisplayStatusType) => { + return FactoryDisplayStatusMap[status].name } -export const FACTORY_STATUS_ITEMS: FactoryStatus[] = [ - FactoryStatus.NEW, - FactoryStatus.EXISTING_COMPLETE, - FactoryStatus.EXISTING_INCOMPLETE, - FactoryStatus.REPORTED -] +export const getDisplayStatusColor = (status: FactoryDisplayStatusType) => { + return FactoryDisplayStatusMap[status].color +} export type FactoryImage = { id: string, @@ -49,6 +99,7 @@ export type FactoryImage = { export type FactoryData = { id: string, + display_number: string, lat: number, lng: number, name: string, @@ -60,14 +111,15 @@ export type FactoryData = { reported_at: null | string, data_complete: boolean, before_release: boolean, - cet_report_status: CetReportStatus + document_display_status?: string, + feature?: Feature } export type FactoriesResponse = Array export type FactoryPostData = { name: string, - type: FactoryType, + type?: FactoryType, images?: string[], others?: string, lat: number, @@ -75,3 +127,9 @@ export type FactoryPostData = { nickname?: string, contact?: string } + +export type ReportRecord = { + id: string, + created_at: string, + others?: string +} diff --git a/tsconfig.json b/tsconfig.json index e0bd347..e39f62c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,8 @@ "types": [ "webpack-env", "mocha", - "chai" + "chai", + "vuetify" ], "paths": { "@/*": [ @@ -27,6 +28,9 @@ "dom", "dom.iterable", "scripthost" + ], + "typeRoots": [ + "./node_modules/vuetify/types" ] }, "include": [ diff --git a/vue.config.js b/vue.config.js index e7c5534..7975f13 100644 --- a/vue.config.js +++ b/vue.config.js @@ -10,22 +10,25 @@ module.exports = { '^/server': '' } } - } - }, - chainWebpack: config => { - if (process.env.NODE_ENV === 'development') { - config - .output - .filename('[name].[hash].js') - .end() + }, + headers: { + 'Cache-Control': 'no-store' } }, pwa: { name: '農地違章工廠', - themeColor: '#2196f3', + themeColor: '#697F01', workboxOptions: { skipWaiting: true, clientsClaim: true } + }, + transpileDependencies: [ + 'vuetify' + ], + chainWebpack: (config) => { + if (process.env.NODE_ENV === 'development') { + config.plugins.delete('preload') + } } }