瀏覽代碼

update qrcode generator

SyuanYu 1 年之前
父節點
當前提交
c460e281c9

+ 233 - 206
frontend/package-lock.json

@@ -10,6 +10,7 @@
       "dependencies": {
         "axios": "^1.2.2",
         "pinia": "^2.0.28",
+        "qrcode.vue": "^3.4.1",
         "sass": "^1.57.1",
         "vue": "^3.2.45",
         "vue-i18n": "^9.2.2",
@@ -54,12 +55,13 @@
       }
     },
     "node_modules/@babel/code-frame": {
-      "version": "7.18.6",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
-      "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz",
+      "integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==",
       "dev": true,
       "dependencies": {
-        "@babel/highlight": "^7.18.6"
+        "@babel/highlight": "^7.23.4",
+        "chalk": "^2.4.2"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -105,13 +107,14 @@
       }
     },
     "node_modules/@babel/generator": {
-      "version": "7.20.7",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.7.tgz",
-      "integrity": "sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==",
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.4.tgz",
+      "integrity": "sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==",
       "dev": true,
       "dependencies": {
-        "@babel/types": "^7.20.7",
+        "@babel/types": "^7.23.4",
         "@jridgewell/gen-mapping": "^0.3.2",
+        "@jridgewell/trace-mapping": "^0.3.17",
         "jsesc": "^2.5.1"
       },
       "engines": {
@@ -186,34 +189,34 @@
       }
     },
     "node_modules/@babel/helper-environment-visitor": {
-      "version": "7.18.9",
-      "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
-      "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
+      "version": "7.22.20",
+      "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
+      "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-function-name": {
-      "version": "7.19.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz",
-      "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==",
+      "version": "7.23.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
+      "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
       "dev": true,
       "dependencies": {
-        "@babel/template": "^7.18.10",
-        "@babel/types": "^7.19.0"
+        "@babel/template": "^7.22.15",
+        "@babel/types": "^7.23.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-hoist-variables": {
-      "version": "7.18.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
-      "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
+      "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
       "dev": true,
       "dependencies": {
-        "@babel/types": "^7.18.6"
+        "@babel/types": "^7.22.5"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -325,30 +328,30 @@
       }
     },
     "node_modules/@babel/helper-split-export-declaration": {
-      "version": "7.18.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
-      "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
+      "version": "7.22.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
+      "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
       "dev": true,
       "dependencies": {
-        "@babel/types": "^7.18.6"
+        "@babel/types": "^7.22.5"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-string-parser": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
-      "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
+      "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.19.1",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
-      "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
+      "version": "7.22.20",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+      "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
@@ -378,13 +381,13 @@
       }
     },
     "node_modules/@babel/highlight": {
-      "version": "7.18.6",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
-      "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
+      "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
       "dev": true,
       "dependencies": {
-        "@babel/helper-validator-identifier": "^7.18.6",
-        "chalk": "^2.0.0",
+        "@babel/helper-validator-identifier": "^7.22.20",
+        "chalk": "^2.4.2",
         "js-tokens": "^4.0.0"
       },
       "engines": {
@@ -392,9 +395,9 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.20.7",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.7.tgz",
-      "integrity": "sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==",
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz",
+      "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==",
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -450,33 +453,33 @@
       }
     },
     "node_modules/@babel/template": {
-      "version": "7.20.7",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
-      "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==",
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
+      "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
       "dev": true,
       "dependencies": {
-        "@babel/code-frame": "^7.18.6",
-        "@babel/parser": "^7.20.7",
-        "@babel/types": "^7.20.7"
+        "@babel/code-frame": "^7.22.13",
+        "@babel/parser": "^7.22.15",
+        "@babel/types": "^7.22.15"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/traverse": {
-      "version": "7.20.12",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.12.tgz",
-      "integrity": "sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ==",
-      "dev": true,
-      "dependencies": {
-        "@babel/code-frame": "^7.18.6",
-        "@babel/generator": "^7.20.7",
-        "@babel/helper-environment-visitor": "^7.18.9",
-        "@babel/helper-function-name": "^7.19.0",
-        "@babel/helper-hoist-variables": "^7.18.6",
-        "@babel/helper-split-export-declaration": "^7.18.6",
-        "@babel/parser": "^7.20.7",
-        "@babel/types": "^7.20.7",
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.4.tgz",
+      "integrity": "sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/code-frame": "^7.23.4",
+        "@babel/generator": "^7.23.4",
+        "@babel/helper-environment-visitor": "^7.22.20",
+        "@babel/helper-function-name": "^7.23.0",
+        "@babel/helper-hoist-variables": "^7.22.5",
+        "@babel/helper-split-export-declaration": "^7.22.6",
+        "@babel/parser": "^7.23.4",
+        "@babel/types": "^7.23.4",
         "debug": "^4.1.0",
         "globals": "^11.1.0"
       },
@@ -485,13 +488,13 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.20.7",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz",
-      "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==",
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.4.tgz",
+      "integrity": "sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ==",
       "dev": true,
       "dependencies": {
-        "@babel/helper-string-parser": "^7.19.4",
-        "@babel/helper-validator-identifier": "^7.19.1",
+        "@babel/helper-string-parser": "^7.23.4",
+        "@babel/helper-validator-identifier": "^7.22.20",
         "to-fast-properties": "^2.0.0"
       },
       "engines": {
@@ -499,9 +502,9 @@
       }
     },
     "node_modules/@esbuild/android-arm": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.16.tgz",
-      "integrity": "sha512-BUuWMlt4WSXod1HSl7aGK8fJOsi+Tab/M0IDK1V1/GstzoOpqc/v3DqmN8MkuapPKQ9Br1WtLAN4uEgWR8x64A==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
+      "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
       "cpu": [
         "arm"
       ],
@@ -515,9 +518,9 @@
       }
     },
     "node_modules/@esbuild/android-arm64": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.16.tgz",
-      "integrity": "sha512-hFHVAzUKp9Tf8psGq+bDVv+6hTy1bAOoV/jJMUWwhUnIHsh6WbFMhw0ZTkqDuh7TdpffFoHOiIOIxmHc7oYRBQ==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
+      "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
       "cpu": [
         "arm64"
       ],
@@ -531,9 +534,9 @@
       }
     },
     "node_modules/@esbuild/android-x64": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.16.tgz",
-      "integrity": "sha512-9WhxJpeb6XumlfivldxqmkJepEcELekmSw3NkGrs+Edq6sS5KRxtUBQuKYDD7KqP59dDkxVbaoPIQFKWQG0KLg==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
+      "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
       "cpu": [
         "x64"
       ],
@@ -547,9 +550,9 @@
       }
     },
     "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.16.tgz",
-      "integrity": "sha512-8Z+wld+vr/prHPi2O0X7o1zQOfMbXWGAw9hT0jEyU/l/Yrg+0Z3FO9pjPho72dVkZs4ewZk0bDOFLdZHm8jEfw==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
+      "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
       "cpu": [
         "arm64"
       ],
@@ -563,9 +566,9 @@
       }
     },
     "node_modules/@esbuild/darwin-x64": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.16.tgz",
-      "integrity": "sha512-CYkxVvkZzGCqFrt7EgjFxQKhlUPyDkuR9P0Y5wEcmJqVI8ncerOIY5Kej52MhZyzOBXkYrJgZeVZC9xXXoEg9A==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
+      "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
       "cpu": [
         "x64"
       ],
@@ -579,9 +582,9 @@
       }
     },
     "node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.16.tgz",
-      "integrity": "sha512-fxrw4BYqQ39z/3Ja9xj/a1gMsVq0xEjhSyI4a9MjfvDDD8fUV8IYliac96i7tzZc3+VytyXX+XNsnpEk5sw5Wg==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
+      "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
       "cpu": [
         "arm64"
       ],
@@ -595,9 +598,9 @@
       }
     },
     "node_modules/@esbuild/freebsd-x64": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.16.tgz",
-      "integrity": "sha512-8p3v1D+du2jiDvSoNVimHhj7leSfST9YlKsAEO7etBfuqjaBMndo0fmjNLp0JCMld+XIx9L80tooOkyUv1a1PQ==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
+      "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
       "cpu": [
         "x64"
       ],
@@ -611,9 +614,9 @@
       }
     },
     "node_modules/@esbuild/linux-arm": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.16.tgz",
-      "integrity": "sha512-bYaocE1/PTMRmkgSckZ0D0Xn2nox8v2qlk+MVVqm+VECNKDdZvghVZtH41dNtBbwADSvA6qkCHGYeWm9LrNCBw==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
+      "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
       "cpu": [
         "arm"
       ],
@@ -627,9 +630,9 @@
       }
     },
     "node_modules/@esbuild/linux-arm64": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.16.tgz",
-      "integrity": "sha512-N3u6BBbCVY3xeP2D8Db7QY8I+nZ+2AgOopUIqk+5yCoLnsWkcVxD2ay5E9iIdvApFi1Vg1lZiiwaVp8bOpAc4A==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
+      "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
       "cpu": [
         "arm64"
       ],
@@ -643,9 +646,9 @@
       }
     },
     "node_modules/@esbuild/linux-ia32": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.16.tgz",
-      "integrity": "sha512-dxjqLKUW8GqGemoRT9v8IgHk+T4tRm1rn1gUcArsp26W9EkK/27VSjBVUXhEG5NInHZ92JaQ3SSMdTwv/r9a2A==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
+      "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
       "cpu": [
         "ia32"
       ],
@@ -659,9 +662,9 @@
       }
     },
     "node_modules/@esbuild/linux-loong64": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.16.tgz",
-      "integrity": "sha512-MdUFggHjRiCCwNE9+1AibewoNq6wf94GLB9Q9aXwl+a75UlRmbRK3h6WJyrSGA6ZstDJgaD2wiTSP7tQNUYxwA==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
+      "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
       "cpu": [
         "loong64"
       ],
@@ -675,9 +678,9 @@
       }
     },
     "node_modules/@esbuild/linux-mips64el": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.16.tgz",
-      "integrity": "sha512-CO3YmO7jYMlGqGoeFeKzdwx/bx8Vtq/SZaMAi+ZLDUnDUdfC7GmGwXzIwDJ70Sg+P9pAemjJyJ1icKJ9R3q/Fg==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
+      "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
       "cpu": [
         "mips64el"
       ],
@@ -691,9 +694,9 @@
       }
     },
     "node_modules/@esbuild/linux-ppc64": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.16.tgz",
-      "integrity": "sha512-DSl5Czh5hCy/7azX0Wl9IdzPHX2H8clC6G87tBnZnzUpNgRxPFhfmArbaHoAysu4JfqCqbB/33u/GL9dUgCBAw==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
+      "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
       "cpu": [
         "ppc64"
       ],
@@ -707,9 +710,9 @@
       }
     },
     "node_modules/@esbuild/linux-riscv64": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.16.tgz",
-      "integrity": "sha512-sSVVMEXsqf1fQu0j7kkhXMViroixU5XoaJXl1u/u+jbXvvhhCt9YvA/B6VM3aM/77HuRQ94neS5bcisijGnKFQ==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
+      "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
       "cpu": [
         "riscv64"
       ],
@@ -723,9 +726,9 @@
       }
     },
     "node_modules/@esbuild/linux-s390x": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.16.tgz",
-      "integrity": "sha512-jRqBCre9gZGoCdCN/UWCCMwCMsOg65IpY9Pyj56mKCF5zXy9d60kkNRdDN6YXGjr3rzcC4DXnS/kQVCGcC4yPQ==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
+      "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
       "cpu": [
         "s390x"
       ],
@@ -739,9 +742,9 @@
       }
     },
     "node_modules/@esbuild/linux-x64": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.16.tgz",
-      "integrity": "sha512-G1+09TopOzo59/55lk5Q0UokghYLyHTKKzD5lXsAOOlGDbieGEFJpJBr3BLDbf7cz89KX04sBeExAR/pL/26sA==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
+      "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
       "cpu": [
         "x64"
       ],
@@ -755,9 +758,9 @@
       }
     },
     "node_modules/@esbuild/netbsd-x64": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.16.tgz",
-      "integrity": "sha512-xwjGJB5wwDEujLaJIrSMRqWkbigALpBNcsF9SqszoNKc+wY4kPTdKrSxiY5ik3IatojePP+WV108MvF6q6np4w==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
+      "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
       "cpu": [
         "x64"
       ],
@@ -771,9 +774,9 @@
       }
     },
     "node_modules/@esbuild/openbsd-x64": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.16.tgz",
-      "integrity": "sha512-yeERkoxG2nR2oxO5n+Ms7MsCeNk23zrby2GXCqnfCpPp7KNc0vxaaacIxb21wPMfXXRhGBrNP4YLIupUBrWdlg==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
+      "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
       "cpu": [
         "x64"
       ],
@@ -787,9 +790,9 @@
       }
     },
     "node_modules/@esbuild/sunos-x64": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.16.tgz",
-      "integrity": "sha512-nHfbEym0IObXPhtX6Va3H5GaKBty2kdhlAhKmyCj9u255ktAj0b1YACUs9j5H88NRn9cJCthD1Ik/k9wn8YKVg==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
+      "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
       "cpu": [
         "x64"
       ],
@@ -803,9 +806,9 @@
       }
     },
     "node_modules/@esbuild/win32-arm64": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.16.tgz",
-      "integrity": "sha512-pdD+M1ZOFy4hE15ZyPX09fd5g4DqbbL1wXGY90YmleVS6Y5YlraW4BvHjim/X/4yuCpTsAFvsT4Nca2lbyDH/A==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
+      "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
       "cpu": [
         "arm64"
       ],
@@ -819,9 +822,9 @@
       }
     },
     "node_modules/@esbuild/win32-ia32": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.16.tgz",
-      "integrity": "sha512-IPEMfU9p0c3Vb8PqxaPX6BM9rYwlTZGYOf9u+kMdhoILZkVKEjq6PKZO0lB+isojWwAnAqh4ZxshD96njTXajg==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
+      "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
       "cpu": [
         "ia32"
       ],
@@ -835,9 +838,9 @@
       }
     },
     "node_modules/@esbuild/win32-x64": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.16.tgz",
-      "integrity": "sha512-1YYpoJ39WV/2bnShPwgdzJklc+XS0bysN6Tpnt1cWPdeoKOG4RMEY1g7i534QxXX/rPvNx/NLJQTTCeORYzipg==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
+      "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
       "cpu": [
         "x64"
       ],
@@ -1171,9 +1174,9 @@
       }
     },
     "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
-      "version": "7.3.8",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
-      "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
       "dev": true,
       "dependencies": {
         "lru-cache": "^6.0.0"
@@ -1315,9 +1318,9 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
-      "version": "7.3.8",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
-      "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
       "dev": true,
       "dependencies": {
         "lru-cache": "^6.0.0"
@@ -1374,9 +1377,9 @@
       }
     },
     "node_modules/@typescript-eslint/utils/node_modules/semver": {
-      "version": "7.3.8",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
-      "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
       "dev": true,
       "dependencies": {
         "lru-cache": "^6.0.0"
@@ -1857,9 +1860,9 @@
       }
     },
     "node_modules/axios": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.2.tgz",
-      "integrity": "sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==",
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
+      "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
       "dependencies": {
         "follow-redirects": "^1.15.0",
         "form-data": "^4.0.0",
@@ -2397,9 +2400,9 @@
       }
     },
     "node_modules/esbuild": {
-      "version": "0.16.16",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.16.tgz",
-      "integrity": "sha512-24JyKq10KXM5EBIgPotYIJ2fInNWVVqflv3gicIyQqfmUqi4HvDW1VR790cBgLJHCl96Syy7lhoz7tLFcmuRmg==",
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
+      "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
       "dev": true,
       "hasInstallScript": true,
       "bin": {
@@ -2409,28 +2412,28 @@
         "node": ">=12"
       },
       "optionalDependencies": {
-        "@esbuild/android-arm": "0.16.16",
-        "@esbuild/android-arm64": "0.16.16",
-        "@esbuild/android-x64": "0.16.16",
-        "@esbuild/darwin-arm64": "0.16.16",
-        "@esbuild/darwin-x64": "0.16.16",
-        "@esbuild/freebsd-arm64": "0.16.16",
-        "@esbuild/freebsd-x64": "0.16.16",
-        "@esbuild/linux-arm": "0.16.16",
-        "@esbuild/linux-arm64": "0.16.16",
-        "@esbuild/linux-ia32": "0.16.16",
-        "@esbuild/linux-loong64": "0.16.16",
-        "@esbuild/linux-mips64el": "0.16.16",
-        "@esbuild/linux-ppc64": "0.16.16",
-        "@esbuild/linux-riscv64": "0.16.16",
-        "@esbuild/linux-s390x": "0.16.16",
-        "@esbuild/linux-x64": "0.16.16",
-        "@esbuild/netbsd-x64": "0.16.16",
-        "@esbuild/openbsd-x64": "0.16.16",
-        "@esbuild/sunos-x64": "0.16.16",
-        "@esbuild/win32-arm64": "0.16.16",
-        "@esbuild/win32-ia32": "0.16.16",
-        "@esbuild/win32-x64": "0.16.16"
+        "@esbuild/android-arm": "0.18.20",
+        "@esbuild/android-arm64": "0.18.20",
+        "@esbuild/android-x64": "0.18.20",
+        "@esbuild/darwin-arm64": "0.18.20",
+        "@esbuild/darwin-x64": "0.18.20",
+        "@esbuild/freebsd-arm64": "0.18.20",
+        "@esbuild/freebsd-x64": "0.18.20",
+        "@esbuild/linux-arm": "0.18.20",
+        "@esbuild/linux-arm64": "0.18.20",
+        "@esbuild/linux-ia32": "0.18.20",
+        "@esbuild/linux-loong64": "0.18.20",
+        "@esbuild/linux-mips64el": "0.18.20",
+        "@esbuild/linux-ppc64": "0.18.20",
+        "@esbuild/linux-riscv64": "0.18.20",
+        "@esbuild/linux-s390x": "0.18.20",
+        "@esbuild/linux-x64": "0.18.20",
+        "@esbuild/netbsd-x64": "0.18.20",
+        "@esbuild/openbsd-x64": "0.18.20",
+        "@esbuild/sunos-x64": "0.18.20",
+        "@esbuild/win32-arm64": "0.18.20",
+        "@esbuild/win32-ia32": "0.18.20",
+        "@esbuild/win32-x64": "0.18.20"
       }
     },
     "node_modules/escalade": {
@@ -2656,9 +2659,9 @@
       }
     },
     "node_modules/eslint-plugin-vue/node_modules/semver": {
-      "version": "7.3.8",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
-      "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
       "dev": true,
       "dependencies": {
         "lru-cache": "^6.0.0"
@@ -3161,9 +3164,9 @@
       }
     },
     "node_modules/get-func-name": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
-      "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
+      "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
       "dev": true,
       "engines": {
         "node": "*"
@@ -4102,9 +4105,15 @@
       "dev": true
     },
     "node_modules/nanoid": {
-      "version": "3.3.4",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
-      "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
+      "version": "3.3.7",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
       "bin": {
         "nanoid": "bin/nanoid.cjs"
       },
@@ -4149,9 +4158,9 @@
       }
     },
     "node_modules/normalize-package-data/node_modules/semver": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
       "dev": true,
       "bin": {
         "semver": "bin/semver"
@@ -4216,9 +4225,9 @@
       }
     },
     "node_modules/npm-run-all/node_modules/semver": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
       "dev": true,
       "bin": {
         "semver": "bin/semver"
@@ -4566,9 +4575,9 @@
       "dev": true
     },
     "node_modules/postcss": {
-      "version": "8.4.21",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
-      "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
+      "version": "8.4.31",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+      "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
       "funding": [
         {
           "type": "opencollective",
@@ -4577,10 +4586,14 @@
         {
           "type": "tidelift",
           "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
         }
       ],
       "dependencies": {
-        "nanoid": "^3.3.4",
+        "nanoid": "^3.3.6",
         "picocolors": "^1.0.0",
         "source-map-js": "^1.0.2"
       },
@@ -4657,6 +4670,14 @@
         "node": ">=6"
       }
     },
+    "node_modules/qrcode.vue": {
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/qrcode.vue/-/qrcode.vue-3.4.1.tgz",
+      "integrity": "sha512-wq/zHsifH4FJ1GXQi8/wNxD1KfQkckIpjK1KPTc/qwYU5/Bkd4me0w4xZSg6EXk6xLBkVDE0zxVagewv5EMAVA==",
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
     "node_modules/querystringify": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
@@ -4807,9 +4828,9 @@
       }
     },
     "node_modules/rollup": {
-      "version": "3.9.1",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.9.1.tgz",
-      "integrity": "sha512-GswCYHXftN8ZKGVgQhTFUJB/NBXxrRGgO2NCy6E8s1rwEJ4Q9/VttNqcYfEvx4dTo4j58YqdC3OVztPzlKSX8w==",
+      "version": "3.29.4",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
+      "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
       "dev": true,
       "bin": {
         "rollup": "dist/bin/rollup"
@@ -4894,9 +4915,9 @@
       }
     },
     "node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
       "dev": true,
       "bin": {
         "semver": "bin/semver.js"
@@ -5208,9 +5229,9 @@
       }
     },
     "node_modules/tough-cookie": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
-      "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
+      "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
       "dev": true,
       "dependencies": {
         "psl": "^1.1.33",
@@ -5407,15 +5428,14 @@
       }
     },
     "node_modules/vite": {
-      "version": "4.0.4",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-4.0.4.tgz",
-      "integrity": "sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==",
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
+      "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
       "dev": true,
       "dependencies": {
-        "esbuild": "^0.16.3",
-        "postcss": "^8.4.20",
-        "resolve": "^1.22.1",
-        "rollup": "^3.7.0"
+        "esbuild": "^0.18.10",
+        "postcss": "^8.4.27",
+        "rollup": "^3.27.1"
       },
       "bin": {
         "vite": "bin/vite.js"
@@ -5423,12 +5443,16 @@
       "engines": {
         "node": "^14.18.0 || >=16.0.0"
       },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
       "optionalDependencies": {
         "fsevents": "~2.3.2"
       },
       "peerDependencies": {
         "@types/node": ">= 14",
         "less": "*",
+        "lightningcss": "^1.21.0",
         "sass": "*",
         "stylus": "*",
         "sugarss": "*",
@@ -5441,6 +5465,9 @@
         "less": {
           "optional": true
         },
+        "lightningcss": {
+          "optional": true
+        },
         "sass": {
           "optional": true
         },
@@ -5610,9 +5637,9 @@
       }
     },
     "node_modules/vue-eslint-parser/node_modules/semver": {
-      "version": "7.3.8",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
-      "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
       "dev": true,
       "dependencies": {
         "lru-cache": "^6.0.0"
@@ -5847,9 +5874,9 @@
       }
     },
     "node_modules/word-wrap": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
-      "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+      "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
       "dev": true,
       "engines": {
         "node": ">=0.10.0"

+ 1 - 0
frontend/package.json

@@ -14,6 +14,7 @@
   "dependencies": {
     "axios": "^1.2.2",
     "pinia": "^2.0.28",
+    "qrcode.vue": "^3.4.1",
     "sass": "^1.57.1",
     "vue": "^3.2.45",
     "vue-i18n": "^9.2.2",

+ 16 - 4
frontend/src/api.ts

@@ -1,6 +1,6 @@
 import axios from "axios";
 import { apiUrl } from "@/env";
-import type { IUserProfile, IUserProfileUpdate, IUserProfileCreate, Video, VideoCreate, ArticleCreate, ImageDownload, VideoUploaded, YTViewsUserData, PaymentData} from "@/interfaces";
+import type { IUserProfile, IUserProfileUpdate, IUserProfileCreate, Video, VideoCreate, ArticleCreate, ImageDownload, VideoUploaded, YTViewsUserData, PaymentData, VideoContent } from "@/interfaces";
 import type { StringOptionsWithImporter } from "sass";
 
 function authHeaders(token: string) {
@@ -150,13 +150,13 @@ export const api = {
     };
     return axios.post<string>(`${apiUrl}/api/v1/payment/ytviews-ecpay-payment?lang=${lang}`, user_data);
   },
-  async YTViewsTestPayment(user_data: YTViewsUserData, lang:string) {
+  async YTViewsTestPayment(user_data: YTViewsUserData, lang: string) {
     return axios.post<string>(`${apiUrl}/api/v1/payment/ytviews-ecpay-test-payment?lang=${lang}`, user_data);
   },
   async getYTViewsList() {
     return axios.get(`${apiUrl}/api/v1/payment/ytviews-list-all`);
   },
-  async Payment(payment_data: PaymentData, token:string, lang: string) {
+  async Payment(payment_data: PaymentData, token: string, lang: string) {
     const config = {
       params: {
         lang: lang
@@ -164,10 +164,22 @@ export const api = {
     };
     return axios.post<string>(`${apiUrl}/api/v1/payment/ecpay-payment?lang=${lang}`, payment_data, authHeaders(token));
   },
-  async TestPayment(payment_data:PaymentData, token:string, lang:string) {
+  async TestPayment(payment_data: PaymentData, token: string, lang: string) {
     return axios.post<string>(`${apiUrl}/api/v1/payment/ecpay-test-payment?lang=${lang}`, payment_data, authHeaders(token));
   },
   async getPaymentList() {
     return axios.get(`${apiUrl}/api/v1/payment/list-all`);
   },
+  async uploadVideoContent(content: string, file: []) {
+    // let content = "";
+    console.log('api content',content);
+    console.log('api file',file);
+    
+    const formData = new FormData();
+    for (let index = 0; index < file.length; index++) {
+      const element = file[index];
+      formData.append("input_model", element)
+    }
+    return axios.post(`${apiUrl}/api/v1/videos/phone_input_json?text=${content}`, formData);
+  },
 };

+ 9 - 3
frontend/src/interfaces/index.ts

@@ -4,7 +4,7 @@ export interface IUserProfile {
   is_superuser: boolean;
   full_name?: string;
   id: number;
-  available_time: number; 
+  available_time: number;
 }
 
 export interface IUserProfileUpdate {
@@ -98,6 +98,12 @@ export interface YTViewsUserData {
 }
 
 export interface PaymentData {
-  item: string,
-  amount: number
+  item: string;
+  amount: number;
+}
+
+export interface VideoContent {
+  title: string;
+  text: string;
+  caption: [];
 }

+ 4 - 4
frontend/src/language/en.json

@@ -1,8 +1,8 @@
 {
-    "login" : "Login",
-    "loggedIn" : "Logged in",
-    "loggedError" : "Incorrect email or password",
-    "loginLink" : "Login",
+    "login": "Login",
+    "loggedIn": "Logged in",
+    "loggedError": "Incorrect email or password",
+    "loginLink": "Login",
     "logout": "Logout",
     "submit": "Submit",
     "cancel": "Cancel",

+ 4 - 4
frontend/src/language/zh.json

@@ -1,8 +1,8 @@
 {
-    "login" : "登入",
-    "loggedIn" : "登入成功",
-    "loggedError" : "帳號或密碼不正確",
-    "loginLink" : "立即登入",
+    "login": "登入",
+    "loggedIn": "登入成功",
+    "loggedError": "帳號或密碼不正確",
+    "loginLink": "立即登入",
     "logout": "登出",
     "submit": "送出",
     "cancel": "取消",

+ 15 - 0
frontend/src/router/index.ts

@@ -93,6 +93,21 @@ const router = createRouter({
               name: 'contact',
               component: () => import('@/views/main/Contact.vue'),
             },
+            // {
+            //   path: 'mobile-upload',
+            //   name: 'MobileUpload',
+            //   component: () => import('@/views/main/MobileUpload.vue'),
+            // },
+            // {
+            //   path: 'render-image',
+            //   name: 'RenderImage',
+            //   component: () => import('@/views/main/RenderImage.vue'),
+            // },
+            {
+              path: 'qrcode-generator',
+              name: 'QRCodeGenerator',
+              component: () => import('@/views/main/QRCodeGenerator.vue'),
+            },
             {
               path: 'profile',
               name: 'profile',

+ 27 - 3
frontend/src/stores/main.ts

@@ -4,7 +4,7 @@ import { api } from "@/api"
 import router from "@/router"
 import { getLocalToken, removeLocalToken, saveLocalToken } from "@/utils";
 import type { AppNotification } from '@/interfaces';
-import type { IUserProfile, IUserProfileCreate, IUserProfileUpdate, MainState, Video, VideoCreate, ArticleCreate, Image, ImageDownload, VideoUploaded, YTViewsUserData, PaymentData } from '@/interfaces';
+import type { IUserProfile, IUserProfileCreate, IUserProfileUpdate, MainState, Video, VideoCreate, ArticleCreate, Image, ImageDownload, VideoUploaded, YTViewsUserData, PaymentData, VideoContent } from '@/interfaces';
 import i18n from '@/plugins/i18n'
 import { wsUrl } from "@/env";
 
@@ -465,7 +465,7 @@ export const useMainStore = defineStore("MainStoreId", {
         await mainStore.checkApiError(error);
       }
     },
-    async YTViewsTestPayment(user_data: YTViewsUserData, lang:string) {
+    async YTViewsTestPayment(user_data: YTViewsUserData, lang: string) {
       const mainStore = useMainStore();
       try {
         const response = (
@@ -508,7 +508,7 @@ export const useMainStore = defineStore("MainStoreId", {
         await mainStore.checkApiError(error);
       }
     },
-    async TestPayment(payment_data: PaymentData, lang:string) {
+    async TestPayment(payment_data: PaymentData, lang: string) {
       const mainStore = useMainStore();
       try {
         const response = (
@@ -551,5 +551,29 @@ export const useMainStore = defineStore("MainStoreId", {
         await mainStore.checkApiError(error);
       }
     },
+    async uploadVideoContent(content: VideoContent, file: []) {
+      console.log('uploadVideoContent');
+
+      console.log('content', content);
+      console.log('file', file);
+
+      let jsonContent = JSON.stringify(content);
+      console.log('jsonContent', jsonContent);
+
+
+      const mainStore = useMainStore();
+      try {
+        const response = await api.uploadVideoContent(jsonContent, file);
+        console.log('uploadVideoContent response', response);
+
+        if (response) {
+          return response.data;
+        }
+      } catch (error) {
+        console.log('error', error);
+
+        // await mainStore.checkApiError(error);
+      }
+    },
   }
 });

+ 3 - 3
frontend/src/views/main/Dashboard.vue

@@ -26,13 +26,13 @@ const greetedUser = computed(() => {
 <template>
   <v-container fluid>
     <v-row dense>
-      <v-col cols="6">
+      <v-col cols="12" sm="6">
         <v-card class="ma-3 pa-3">
           <v-card-title primary-title>
             <h3>Welcome {{ greetedUser }}</h3>
           </v-card-title>
           <v-card-text class="my-3">
-            <h4 class="">
+            <h4>
               {{ t("userName") }}
             </h4>
             <span v-if="userProfile && userProfile.full_name">
@@ -89,7 +89,7 @@ const greetedUser = computed(() => {
           </v-card-actions>
         </v-card>
       </v-col> -->
-      <v-col cols="6">
+      <v-col cols="12" sm="6">
         <v-card class="ma-3 pa-3 second-item">
           <v-card-title primary-title>
             <h3>可使用秒數</h3>

+ 9 - 0
frontend/src/views/main/Main.vue

@@ -132,6 +132,15 @@ const routeGuardAdmin = async (
             <!-- <v-list-item to="/main/profile/password" prepend-icon="key">
               <v-list-item-title>{{ t("changePassword") }}</v-list-item-title>
             </v-list-item> -->
+            <!-- <v-list-item to="/main/render-image" prepend-icon="image">
+              <v-list-item-title>算圖測試</v-list-item-title>
+            </v-list-item> -->
+            <!-- <v-list-item to="/main/mobile-upload" prepend-icon="video_call">
+              <v-list-item-title>影片測試</v-list-item-title>
+            </v-list-item> -->
+            <v-list-item to="/main/qrcode-generator" prepend-icon="qr_code">
+              <v-list-item-title>QRCode</v-list-item-title>
+            </v-list-item>
           </v-list>
         </v-sheet>
         <!-- <v-divider></v-divider> -->

+ 551 - 0
frontend/src/views/main/MobileUpload.vue

@@ -0,0 +1,551 @@
+<script setup lang="ts">
+import { ref, reactive, watch, computed } from "vue";
+import { useMainStore } from "@/stores/main";
+import { required } from "@/utils";
+import { useI18n } from "vue-i18n";
+import { wsUrl } from "@/env";
+import type { VideoCreate, VideoUploaded, VideoContent } from "@/interfaces";
+import router from "@/router";
+import Dialog from "@/components/Dialog.vue";
+
+const { t } = useI18n();
+const mainStore = useMainStore();
+const WS = mainStore.videosWebSocket;
+const valid = ref(true);
+const title = ref("");
+const zipFiles = ref();
+const Form = ref();
+let anchor = ref(0);
+let templateId = ref(0);
+let selectAnchor = ref("angela");
+let selectTemplate = ref("style1");
+
+// props
+let dialog = reactive({
+  msg: "",
+  state: "info",
+  show: false,
+});
+
+const anchorList = reactive([
+  {
+    anchor_id: "angela",
+    name: "Angela",
+  },
+  {
+    anchor_id: "jocelyn",
+    name: "Jocelyn",
+  },
+  {
+    anchor_id: "summer",
+    name: "Summer",
+  },
+  {
+    anchor_id: "peggy",
+    name: "Peggy",
+  },
+]);
+
+const templateList = reactive([
+  {
+    template_id: "style1",
+    img: "鏡面-01",
+  },
+  {
+    template_id: "style2",
+    img: "鏡面-02",
+  },
+  {
+    template_id: "style3",
+    img: "鏡面-03",
+  },
+  {
+    template_id: "style4",
+    img: "鏡面-04",
+  },
+]);
+
+let anchorLang = ref("中文");
+let items = reactive([
+  { lang: "中文", id: 0 },
+  { lang: "英文", id: 1 },
+]);
+
+// 取得圖片路徑
+const getImageUrl = (imgFolder: string, name: string) => {
+  return new URL(`../../assets/img/${imgFolder}/${name}.png`, import.meta.url)
+    .href;
+};
+
+watch(dialog, (newVal) => {
+  if (!newVal.show && newVal.state === "error") {
+    return;
+  } else if (!newVal.show && newVal.state === "success") {
+    router.push("/main/progress");
+  }
+});
+
+watch(anchor, (newVal) => {
+  selectAnchor.value = anchorList[newVal].anchor_id;
+});
+
+watch(templateId, (newVal) => {
+  selectTemplate.value = templateList[newVal].template_id;
+});
+
+// 片段
+let fragments = reactive({
+  list: [] as any[],
+});
+
+let videoContent: VideoContent = reactive({
+  title: "",
+  text: "",
+  caption: [],
+});
+
+// 新增片段
+function addFragments() {
+  fragments.list.push(JSON.parse(JSON.stringify(videoContent)));
+}
+
+// 刪除片段
+function deleteFragments(index: number) {
+  deleteDialog[index] = false;
+  fragments.list.splice(index, 1);
+}
+
+addFragments();
+
+// 新增腳本
+function addCaption(index: number) {
+  let text: string = fragments.list[index].text;
+  fragments.list[index].caption.push(text);
+  fragments.list[index].text = ""; // 清空腳本
+}
+
+// 圖片
+let imgs = reactive({
+  list: [] as File[],
+});
+
+function addImgsFile(item: any) {
+  const selectedFiles = item.target.files;
+
+  for (let i = 0; i < selectedFiles.length; i++) {
+    imgs.list.push(selectedFiles[i]);
+  }
+}
+
+async function Submit() {
+  let files: any = imgs.list;
+  let data = JSON.parse(JSON.stringify(fragments.list));
+
+  for (let i = 0; i < data.length; i++) {
+    data[i].text = data[i].caption; // 將 caption 的值設置給 text
+    delete data[i].caption; // 刪除 caption 屬性
+    console.log("('>>> data", data);
+  }
+
+  console.log("data", data);
+  console.log("files", files);
+
+  const response = await mainStore.uploadVideoContent(data, files);
+
+  console.log("response", response);
+
+  // WS.send("subscribe");
+  // await (Form as any).value.validate();
+  // if (valid.value) {
+  //   valid.value = false;
+
+  //   const video_data: VideoCreate = {
+  //     title: title.value,
+  //     anchor: selectAnchor.value,
+  //     style: selectTemplate.value,
+  //     lang: "zh",
+  //   };
+
+  //   const ret: VideoUploaded = await mainStore.uploadPlot(
+  //     video_data,
+  //     zipFiles.value[0]
+  //   );
+
+  //   if (ret.accepted) {
+  //     dialog.msg = t("acceptZipMessage");
+  //     dialog.state = "success";
+  //     dialog.show = true;
+  //   } else {
+  //     dialog.msg = ret.error_message!;
+  //     dialog.state = "error";
+  //     dialog.show = true;
+  //   }
+
+  //   valid.value = true;
+
+  //   // (Form as any).value.reset();
+  // }
+}
+
+let deleteDialog = reactive([] as any);
+</script>
+
+<template>
+  <v-container fluid>
+    <v-card class="ma-3 pa-5">
+      <v-card-title primary-title>
+        <h3 class="card-title mb-3">{{ t("makeVideo") }}</h3>
+      </v-card-title>
+      <v-card-text>
+        <v-expansion-panels class="anchor-list">
+          <v-expansion-panel title="選擇主播">
+            <v-expansion-panel-text class="p-0">
+              <v-item-group mandatory v-model="anchor">
+                <v-container fluid>
+                  <ul>
+                    <li v-for="n in anchorList" :key="n.anchor_id">
+                      <v-item v-slot="{ isSelected, toggle }">
+                        <v-card
+                          :color="isSelected ? 'primary' : ''"
+                          class="d-flex flex-column align-center"
+                          dark
+                          @click="toggle"
+                          :title="n.name"
+                        >
+                          <v-scroll-y-transition>
+                            <img
+                              :src="getImageUrl('anchor', n.anchor_id)"
+                              alt=""
+                            />
+                          </v-scroll-y-transition>
+                        </v-card>
+                      </v-item>
+                    </li>
+                  </ul>
+                </v-container>
+              </v-item-group>
+            </v-expansion-panel-text>
+          </v-expansion-panel>
+        </v-expansion-panels>
+
+        <v-expansion-panels class="template-list mt-6">
+          <v-expansion-panel title="選擇模板">
+            <v-expansion-panel-text class="p-0">
+              <v-sheet class="mx-auto">
+                <v-slide-group
+                  v-model="templateId"
+                  selected-class="bg-primary"
+                  show-arrows
+                >
+                  <v-slide-group-item
+                    v-for="n in templateList"
+                    :key="n.template_id"
+                    v-slot="{ isSelected, toggle, selectedClass }"
+                  >
+                    <v-card
+                      color="grey-lighten-1"
+                      :class="['ma-4', selectedClass]"
+                      @click="toggle"
+                    >
+                      <span
+                        class="choose-btn"
+                        :class="{ 'active-color': isSelected }"
+                      >
+                        <v-icon icon="done" color="white" />
+                      </span>
+                      <img :src="getImageUrl('template', n.img)" alt="" />
+                    </v-card>
+                  </v-slide-group-item>
+                </v-slide-group>
+              </v-sheet>
+            </v-expansion-panel-text>
+          </v-expansion-panel>
+        </v-expansion-panels>
+      </v-card-text>
+      <!-- <v-card-actions>
+        <v-spacer></v-spacer>
+        <v-btn @click="Submit" :disabled="!valid" variant="outlined">
+          {{ t("send") }}
+        </v-btn>
+      </v-card-actions> -->
+    </v-card>
+
+    <v-card class="mt-10 ma-3 pa-sm-5">
+      <v-card-title primary-title>
+        <h3 class="card-title mb-3">{{ t("makeVideo") }}</h3>
+      </v-card-title>
+      <v-card-text class="mx-auto" style="max-width: 500px">
+        <v-form v-model="valid" ref="Form" class="upload-form">
+          <div
+            class="list-item"
+            v-for="(item, index) in fragments.list"
+            :key="index"
+          >
+            <div class="d-flex justify-end">
+              <v-btn
+                icon="close"
+                size="small"
+                variant="text"
+                color="grey-lighten-1"
+                :disabled="index === 0"
+              >
+                <v-icon icon="close" color="grey-lighten-1" />
+
+                <v-dialog
+                  v-model="deleteDialog[index]"
+                  activator="parent"
+                  width="350"
+                >
+                  <v-card>
+                    <v-card-text> 確定要刪除此片段? </v-card-text>
+                    <v-card-actions class="d-flex justify-center">
+                      <v-btn color="gray" @click="deleteDialog[index] = false"
+                        >取消</v-btn
+                      >
+                      <v-btn color="red" @click="deleteFragments(index)"
+                        >刪除</v-btn
+                      >
+                    </v-card-actions>
+                  </v-card>
+                </v-dialog>
+              </v-btn>
+            </div>
+
+            <h4 class="mb-5">片段 {{ index + 1 }}</h4>
+
+            <div class="img-item">
+              <div class="img-inputs">
+                <input @change="addImgsFile($event)" type="file" class="file" />
+                <p>
+                  點擊上傳圖片或影片 <br />
+                  或將檔案拖曳至此區塊
+                </p>
+              </div>
+            </div>
+
+            <v-text-field
+              label="標題"
+              v-model="item.title"
+              :rules="required()"
+              prepend-icon="title"
+              hint="不超過中文 10 字/英文 20 字"
+            >
+            </v-text-field>
+
+            <div class="d-flex">
+              <v-text-field
+                label="請輸入腳本"
+                v-model="item.text"
+                prepend-icon="message"
+                hint="不超過中文 20 字/英文 40 字"
+              >
+              </v-text-field>
+
+              <v-btn
+                @click="addCaption(index)"
+                icon="check"
+                size="small"
+                color="success"
+                variant="text"
+                class="ma-2"
+              ></v-btn>
+
+              <!-- <v-btn
+                icon="close"
+                size="small"
+                variant="text"
+                class="mt-2"
+                color="red"
+              ></v-btn> -->
+            </div>
+
+            <ul class="caption-list">
+              <li v-for="(e, i) in fragments.list[index].caption" :key="index">
+                <div class="d-flex align-center mb-5">
+                  <p class="me-5">{{ i + 1 }}.</p>
+                  <v-text-field
+                    variant="outlined"
+                    v-model="fragments.list[index].caption[i]"
+                    hide-details
+                  ></v-text-field>
+
+                  <!-- {{ `${index+1}. ${ item }` }} -->
+                  <v-btn
+                    icon="close"
+                    size="small"
+                    variant="text"
+                    class="mx-2"
+                    color="red"
+                  ></v-btn>
+                </div>
+              </li>
+            </ul>
+          </div>
+        </v-form>
+
+        <div class="d-flex">
+          <v-btn
+            @click="addFragments()"
+            icon="add"
+            size="large"
+            color="primary"
+            class="mx-auto"
+          ></v-btn>
+        </div>
+      </v-card-text>
+      <!-- <v-card-actions>
+        <v-spacer></v-spacer>
+        <v-btn @click="Submit" :disabled="!valid" variant="outlined">
+          {{ t("send") }}
+        </v-btn>
+      </v-card-actions> -->
+    </v-card>
+
+    <div class="d-flex justify-end pa-5">
+      <v-btn @click="Submit" variant="flat" color="primary">
+        {{ t("send") }}
+      </v-btn>
+    </div>
+
+    <template>
+      <div class="text-center">
+        <Dialog
+          :msg="dialog.msg"
+          :state="dialog.state"
+          :dialog="dialog.show"
+          @close="dialog.show = false"
+        ></Dialog>
+      </div>
+    </template>
+  </v-container>
+</template>
+
+<style lang="scss">
+.upload-form {
+  h4 {
+    font-size: 20px;
+  }
+
+  .list-item {
+    padding: 15px 25px 25px;
+    margin-bottom: 30px;
+    border-radius: 20px;
+    border: 1px solid #b5b5b5;
+    // box-shadow: 2px 2px 5px #ccc;
+  }
+
+  .img-item {
+    label {
+      border: 1px solid #000;
+      border-radius: 10px;
+    }
+  }
+
+  .img-inputs {
+    margin-bottom: 25px;
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 10px;
+    border: 1px dashed #9f9f9f;
+    background-color: #f6f6f6;
+    p {
+      position: absolute;
+      font-size: 16px;
+      color: #818181;
+      text-align: center;
+      line-height: 26px;
+    }
+    .file {
+      width: 100%;
+      padding: 50px;
+      position: relative;
+      opacity: 0;
+      z-index: 10;
+      cursor: pointer;
+    }
+  }
+
+  .v-input--horizontal .v-input__prepend {
+    margin-inline-end: 10px;
+  }
+}
+
+.anchor-list {
+  ul {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(150px, max-content));
+    grid-gap: 20px;
+    justify-content: center;
+    padding: initial;
+    li {
+      list-style-type: none;
+    }
+  }
+  img {
+    width: 100%;
+    height: 140px;
+    object-fit: cover;
+  }
+
+  .v-card--variant-elevated {
+    box-shadow: 0px 2px 5px 1px
+        var(--v-shadow-key-umbra-opacity, rgba(0, 0, 0, 0.2)),
+      0px 1px 1px 0px var(--v-shadow-key-penumbra-opacity, rgba(0, 0, 0, 0.14)),
+      0px 1px 3px 0px var(--v-shadow-key-penumbra-opacity, rgba(0, 0, 0, 0.12));
+  }
+
+  .v-card-item {
+    padding: 0;
+    text-align: center;
+    .v-card-title {
+      font-size: 18px;
+    }
+  }
+  .bg-success {
+    background: linear-gradient(
+      -225deg,
+      rgb(234, 84, 19) 35%,
+      rgb(178, 69, 146) 100%
+    ) !important;
+  }
+  .v-expansion-panel-text__wrapper {
+    padding: 0 !important;
+  }
+}
+
+.anchor-list,
+.template-list {
+  padding-left: 40px;
+  .v-expansion-panel-title {
+    height: 55px;
+    min-height: 0;
+  }
+}
+
+.template-list {
+  img {
+    width: 100%;
+    height: 180px;
+  }
+  .choose-btn {
+    padding: 5px;
+    position: absolute;
+    right: 8px;
+    bottom: 13px;
+    background: #ccc;
+    border-radius: 100px;
+  }
+  .active-color {
+    background: #ea5413;
+  }
+}
+
+.caption-list {
+  li {
+    font-size: 16px;
+    list-style: none;
+  }
+}
+</style>

+ 46 - 0
frontend/src/views/main/QRCodeGenerator.vue

@@ -0,0 +1,46 @@
+<script setup lang="ts">
+import { ref } from "vue";
+import { required } from "@/utils";
+import QrcodeVue from "qrcode.vue";
+let url = ref("");
+let qrCode = ref("");
+let size = ref("250");
+let valid = ref(true);
+
+function getQRCode() {
+  qrCode.value = url.value;
+}
+</script>
+
+<template>
+  <v-container fluid>
+    <v-card class="mt-10 ma-3 pa-sm-5">
+      <v-card-title primary-title>
+        <h3 class="card-title mb-3">QRCode Generator</h3>
+      </v-card-title>
+      <v-card-text class="mx-auto">
+        <v-row>
+          <v-col cols="6" class="d-flex flex-column">
+            <v-form v-model="valid" ref="Form">
+              <v-text-field
+                v-model="url"
+                :rules="required()"
+                placeholder="請輸入網址"
+              >
+              </v-text-field>
+              <v-btn @click="getQRCode()" variant="flat" color="primary">
+                取得 QRCode
+              </v-btn>
+            </v-form>
+          </v-col>
+
+          <v-col cols="6">
+            <div v-if="qrCode !== ''" class="d-flex justify-center">
+              <qrcode-vue :value="qrCode" :size="size" level="H" />
+            </div>
+          </v-col>
+        </v-row>
+      </v-card-text>
+    </v-card>
+  </v-container>
+</template>

+ 117 - 0
frontend/src/views/main/RenderImage.vue

@@ -0,0 +1,117 @@
+<script setup lang="ts">
+import { ref, reactive, watch, computed } from "vue";
+import { useMainStore } from "@/stores/main";
+import { required } from "@/utils";
+import { useI18n } from "vue-i18n";
+import { wsUrl } from "@/env";
+import type { VideoCreate, VideoUploaded, VideoContent } from "@/interfaces";
+import router from "@/router";
+import Dialog from "@/components/Dialog.vue";
+
+const { t } = useI18n();
+const mainStore = useMainStore();
+const WS = mainStore.videosWebSocket;
+const valid = ref(true);
+const Form = ref();
+
+// props
+let dialog = reactive({
+  msg: "",
+  state: "info",
+  show: false,
+});
+
+let data = reactive({
+  prompt: "SDv1.5",
+  model_name: "咖啡",
+});
+
+async function Submit() {
+  console.log("Submit");
+  console.log("data", JSON.stringify(data));
+
+  // const response = await mainStore.uploadVideoContent(content, files);
+  // console.log("response", response);
+}
+</script>
+
+<template>
+  <v-container fluid>
+    <v-card class="mt-10 ma-3 pa-sm-5">
+      <v-card-title primary-title>
+        <h3 class="card-title mb-3">AI 算圖</h3>
+      </v-card-title>
+      <v-card-text class="mx-auto">
+        <v-row>
+          <v-col cols="6" class="d-flex flex-column">
+            <v-form v-model="valid" ref="Form">
+              <p>Model Name:</p>
+              <v-chip-group v-model="data.model_name" column class="mb-3">
+                <v-chip
+                  filter
+                  color="primary"
+                  variant="outlined"
+                  value="SDv1.5"
+                >
+                  SDv1.5
+                </v-chip>
+                <v-chip
+                  filter
+                  color="primary"
+                  variant="outlined"
+                  value=" SDv2.1"
+                >
+                  SDv2.1
+                </v-chip>
+              </v-chip-group>
+
+              <v-text-field
+                label="Prompt"
+                v-model="data.prompt"
+                :rules="required()"
+              >
+              </v-text-field>
+            </v-form>
+
+            <div class="mt-auto">
+              <v-btn
+                @click="Submit"
+                variant="flat"
+                color="primary"
+                class="w-100"
+              >
+                {{ t("send") }}
+              </v-btn>
+            </div>
+          </v-col>
+
+          <v-col cols="6">
+            <div class="img-block">
+              <img src="" alt="" />
+            </div>
+          </v-col>
+        </v-row>
+      </v-card-text>
+    </v-card>
+
+    <template>
+      <div class="text-center">
+        <Dialog
+          :msg="dialog.msg"
+          :state="dialog.state"
+          :dialog="dialog.show"
+          @close="dialog.show = false"
+        ></Dialog>
+      </div>
+    </template>
+  </v-container>
+</template>
+
+<style lang="scss" scoped>
+.img-block {
+  height: 500px;
+  border-radius: 10px;
+  border: 1px solid #b5b5b5;
+  background-color: #f8f8f8;
+}
+</style>

+ 6 - 0
node_modules/.package-lock.json

@@ -0,0 +1,6 @@
+{
+  "name": "video-maker",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {}
+}

+ 6 - 0
package-lock.json

@@ -0,0 +1,6 @@
+{
+  "name": "video-maker",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {}
+}

+ 1 - 0
package.json

@@ -0,0 +1 @@
+{}