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 @@
-
-
-
農地工廠回報
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ mdi-temp
+
+
+ 農地工廠回報
+
+
+
+ 使用教學
+
+
+ 安全須知
+
+
+ 聯絡我們
+
+
+ 常見問題
+
+
+ 關於舉報系統
+
+
+ 問題回報
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-checkbox-marked-circle
+
+
+ 新增可疑工廠成功
+
+
+
+ 3 秒後自動關閉提示訊息
+
+
+
+
+
+
+
+
+ mdi-checkbox-marked-circle
+
+
+ 補充工廠照片成功
+
+
+
+ 3 秒後自動關閉提示訊息
+
+
+
+
+
+
+
+
+ mdi-checkbox-marked-circle
+
+
+ 補充工廠資訊成功
+
+
+
+ 3 秒後自動關閉提示訊息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ mdi-keyboard-backspace
+
+
+
+
+ 新增可疑工廠 步驟 ({{ appState.createStepIndex }}/3)
+
+
+
+
+
+
+ mdi-close
+
+
+
+ 放棄新增可疑工廠嗎?
+ 放棄新增可疑工廠時,你將遺失所有已輸入的資料。下次需重新填寫。
+
+ 繼續填寫資料
+ 放棄新增
+
+
+
+
+
+
+
+
+ 新增可疑工廠
+
+
+ 選擇工廠位置
+ mdi-chevron-right
+
+
+
+ 上傳工廠照片
+ mdi-chevron-right
+
+
+
+ 確認及補充工廠資訊
+
+
+
+
+
+
+
+ 取消新增
+
+
+
+ 放棄新增可疑工廠嗎?
+ 放棄新增可疑工廠時,你將遺失所有已輸入的資料。下次需重新填寫。
+
+ 繼續填寫資料
+ 放棄新增
+
+
+
+
+
+
+
+
+
+
以下經緯度版本為WGS84
+
+
+ 經度:{{ appState.mapLngLat[0].toFixed(7) }}
+
+
+
+ 緯度:{{ appState.mapLngLat[1].toFixed(7) }}
+
+
+
+ 搜尋經緯度
+
+
+
+
+
+ 搜尋經緯度
+
+
+
+
+
精度
+
+
+
+
緯度
+
+
+
+
清空
+
+
定位
+
+
+
+
+
+
+
+
+
+ 搜尋經緯度
+
+
+
+
+
+ 以下經緯度版本為WGS84
+
+
+ 精度
+
+
+
+ 緯度
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ showLongLat ? '隱藏經緯度' : '顯示經緯度' }}
+
+
+
+
+
+
+ 選擇此地點
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
-
-
-
- {{ isCreateMode ? '新增資訊' : '補充資訊' }}
-
-
-
-
-
-
-
-
-
- {{ FactoryStatusText[factoryFormState.status][0] }}
-
-
-
- 上傳工廠資訊
-
-
-
- 請確認工廠地點,
-
- 並上傳至少一張照片。
-
-
-
-
- 請上傳至少一張照片。
-
-
-
-
-
-
-
-
工廠照片*
-
-
-
-
- 若您的 iPhone 手機的系統為 iOS 13.3 以下(包含),可能無法上傳照片,工程師搶修中,敬請見諒 🥺
-
-
-
-
-
-
-
-
-
-
-
-
- 補充其他資訊
-
-
- 若無可跳過
-
-
-
-
工廠描述
-
-
-
-
-
- {{ fieldSubmittingState.others_submitting ? '更新中' : '更新' }}
-
-
-
-
-
工廠外部文字
-
-
-
-
-
- {{ fieldSubmittingState.name_submitting ? '更新中' : '更新' }}
-
-
-
-
-
工廠類型
-
-
-
-
-
- {{ fieldSubmittingState.factory_type_submitting ? '更新中' : '更新' }}
-
-
-
-
-
-
- {{ formPageState.submitting ? '上傳資料中' : '送出' }}
-
-
-
-
-
-
-
-
-
-
-
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 @@
-
-
-
- 新增資訊
-
+
+
+
+
+
+
+ mdi-map-marker
+ {{ button.text }}
+
-
-
-
-
-
-
{{ popupData.status }}
-
{{ popupData.name }}
-
{{ popupData.summary }}
-
- 補充資料
-
-
-
-
- 白色區域:農地範圍,為可回報範圍。
灰色區域:非農地範圍,不在回報範圍內。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ 我想新增可疑工廠
+
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 @@
-
-
-
-
-
- 安全須知
+
+
+
+
+
+ 安全須知
請參考以下建議,前往違章工廠拍照蒐證
-
-
+
+
場勘
@@ -17,54 +17,54 @@
假裝路過,檢查有無監視器;尋找適合拍照的隱蔽處
-
-
+
+
藏身
盡量避開監視器、他人視線;盡量在隱蔽處拍照(例如:車上、民宅內、樹叢間、遠方)
-
-
+
+
避嫌
假如地點不夠隱密,請用手機拍照,降低他人戒心
-
-
+
+
速離
拍到1張清晰照片即可離去
-
-
+
+
應變
假如有人上前問話,請表現得「充滿好奇心」,試著跟對方聊天(例如:好奇水電師傅的施工技術),再伺機離開。
-
-
-
-
-
+
+
+
+
+
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 @@
+
+
+
+
+
+ 補充照片
+
+ {{ updateFormAppTitle }}
+
+
+
+
+
+
+
+ mdi-close
+
+
+
+ 補充照片尚未完成
+ 放棄補充照片的話,你將遺失所有未新增的資料。下次需重新上傳照片
+
+ 繼續編輯
+ 確定放棄
+
+
+
+ {{ updateFormAppTitle }}尚未完成!
+ 放棄編輯的話,你將遺失所有未新增的資料。下次需重新填寫。
+
+ 繼續編輯
+ 確定放棄
+
+
+
+
+
+
+
+
+ 補充照片
+
+
+
+
+
+
+ 取消
+
+
+
+ 補充照片尚未完成
+ 放棄補充照片的話,你將遺失所有未新增的資料。下次需重新上傳照片
+
+ 繼續編輯
+ 確定放棄
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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')
+ }
}
}
{{ updateFormTitle }}
+ +