feat: update admin page
This commit is contained in:
@@ -4,9 +4,14 @@
|
||||
"": {
|
||||
"name": "admin",
|
||||
"dependencies": {
|
||||
"@primeuix/themes": "^1.2.5",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@vueuse/core": "^14.0.0",
|
||||
"axios": "^1.13.1",
|
||||
"daisyui": "^5.4.2",
|
||||
"mitt": "^3.0.1",
|
||||
"pinia": "^3.0.3",
|
||||
"primevue": "^4.4.1",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3",
|
||||
@@ -239,6 +244,18 @@
|
||||
|
||||
"@prettier/plugin-oxc": ["@prettier/plugin-oxc@0.0.4", "", { "dependencies": { "oxc-parser": "0.74.0" } }, "sha512-UGXe+g/rSRbglL0FOJiar+a+nUrst7KaFmsg05wYbKiInGWP6eAj/f8A2Uobgo5KxEtb2X10zeflNH6RK2xeIQ=="],
|
||||
|
||||
"@primeuix/styled": ["@primeuix/styled@0.7.4", "", { "dependencies": { "@primeuix/utils": "^0.6.1" } }, "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ=="],
|
||||
|
||||
"@primeuix/styles": ["@primeuix/styles@1.2.5", "", { "dependencies": { "@primeuix/styled": "^0.7.3" } }, "sha512-nypFRct/oaaBZqP4jinT0puW8ZIfs4u+l/vqUFmJEPU332fl5ePj6DoOpQgTLzo3OfmvSmz5a5/5b4OJJmmi7Q=="],
|
||||
|
||||
"@primeuix/themes": ["@primeuix/themes@1.2.5", "", { "dependencies": { "@primeuix/styled": "^0.7.3" } }, "sha512-n3YkwJrHQaEESc/D/A/iD815sxp8cKnmzscA6a8Tm8YvMtYU32eCahwLLe6h5rywghVwxASWuG36XBgISYOIjQ=="],
|
||||
|
||||
"@primeuix/utils": ["@primeuix/utils@0.6.2", "", {}, "sha512-l+li4z6UAwamqbu4xodN3lqr1G4S37DKEJ7Y/nE3MjEx7zJ+KIBkb5voF26Nuly1UCmIcRWQJokhLRF7+sRhyg=="],
|
||||
|
||||
"@primevue/core": ["@primevue/core@4.4.1", "", { "dependencies": { "@primeuix/styled": "^0.7.4", "@primeuix/utils": "^0.6.1" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-RG56iDKIJT//EtntjQzOiWOHZZJczw/qWWtdL5vFvw8/QDS9DPKn8HLpXK7N5Le6KK1MLXUsxoiGTZK+poUFUg=="],
|
||||
|
||||
"@primevue/icons": ["@primevue/icons@4.4.1", "", { "dependencies": { "@primeuix/utils": "^0.6.1", "@primevue/core": "4.4.1" } }, "sha512-UfDimrIjVdY6EziwieyV4zPKzW6mnKHKhy4Dgyjv2oI6pNeuim+onbJo1ce22PEGXW78vfblG/3/JIzVHFweqQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.29", "", {}, "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.5", "", { "os": "android", "cpu": "arm" }, "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ=="],
|
||||
@@ -321,6 +338,8 @@
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
|
||||
|
||||
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.29" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.2.25" } }, "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw=="],
|
||||
|
||||
"@vue/babel-helper-vue-transform-on": ["@vue/babel-helper-vue-transform-on@1.5.0", "", {}, "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA=="],
|
||||
@@ -357,6 +376,12 @@
|
||||
|
||||
"@vue/shared": ["@vue/shared@3.5.22", "", {}, "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w=="],
|
||||
|
||||
"@vueuse/core": ["@vueuse/core@14.0.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "14.0.0", "@vueuse/shared": "14.0.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-d6tKRWkZE8IQElX2aHBxXOMD478fHIYV+Dzm2y9Ag122ICBpNKtGICiXKOhWU3L1kKdttDD9dCMS4bGP3jhCTQ=="],
|
||||
|
||||
"@vueuse/metadata": ["@vueuse/metadata@14.0.0", "", {}, "sha512-6yoGqbJcMldVCevkFiHDBTB1V5Hq+G/haPlGIuaFZHpXC0HADB0EN1ryQAAceiW+ryS3niUwvdFbGiqHqBrfVA=="],
|
||||
|
||||
"@vueuse/shared": ["@vueuse/shared@14.0.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-mTCA0uczBgurRlwVaQHfG0Ja7UdGe4g9mwffiJmvLiTtp1G4AQyIjej6si/k8c8pUwTfVpNufck+23gXptPAkw=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
@@ -369,6 +394,10 @@
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"axios": ["axios@1.13.1", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q=="],
|
||||
@@ -383,6 +412,8 @@
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001751", "", {}, "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw=="],
|
||||
@@ -393,6 +424,8 @@
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
@@ -417,8 +450,12 @@
|
||||
|
||||
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.243", "", {}, "sha512-ZCphxFW3Q1TVhcgS9blfut1PX8lusVi2SvXQgmEEnK4TCmE1JhH2JkjJN+DNt0pJJwfBri5AROBnz2b/C+YU9g=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
|
||||
@@ -427,6 +464,14 @@
|
||||
|
||||
"error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
@@ -477,18 +522,36 @@
|
||||
|
||||
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||
|
||||
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
@@ -569,8 +632,14 @@
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||
@@ -633,6 +702,10 @@
|
||||
|
||||
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="],
|
||||
|
||||
"primevue": ["primevue@4.4.1", "", { "dependencies": { "@primeuix/styled": "^0.7.4", "@primeuix/styles": "^1.2.5", "@primeuix/utils": "^0.6.1", "@primevue/core": "4.4.1", "@primevue/icons": "4.4.1" } }, "sha512-JbHBa5k30pZ7mn/z4vYBOnyt5GrR15eM3X0wa3VanonxnFLYkTEx8OMh33aU6ndWeOfi7Ef57dOL3bTH+3f4hQ=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="],
|
||||
|
||||
@@ -16,9 +16,14 @@
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primeuix/themes": "^1.2.5",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@vueuse/core": "^14.0.0",
|
||||
"axios": "^1.13.1",
|
||||
"daisyui": "^5.4.2",
|
||||
"mitt": "^3.0.1",
|
||||
"pinia": "^3.0.3",
|
||||
"primevue": "^4.4.1",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3"
|
||||
|
||||
@@ -1,10 +1,77 @@
|
||||
<template>
|
||||
<div v-if="! settings.error" class="tw:relative">
|
||||
<TopLead/>
|
||||
<ul class="nav nav-tabs">
|
||||
<li :class="{active: route.name === 'general'}">
|
||||
<RouterLink :to="{name: 'general'}">Общие</RouterLink>
|
||||
</li>
|
||||
|
||||
<li :class="{active: route.name === 'telegram'}">
|
||||
<RouterLink :to="{name: 'telegram'}">Telegram</RouterLink>
|
||||
</li>
|
||||
|
||||
<li :class="{active: route.name === 'metrics'}">
|
||||
<RouterLink :to="{name: 'metrics'}">Метрики</RouterLink>
|
||||
</li>
|
||||
|
||||
<li :class="{active: route.name === 'store'}">
|
||||
<RouterLink :to="{name: 'store'}">Магазин</RouterLink>
|
||||
</li>
|
||||
|
||||
<li :class="{active: route.name === 'texts'}">
|
||||
<RouterLink :to="{name: 'texts'}">Тексты</RouterLink>
|
||||
</li>
|
||||
|
||||
<li :class="{active: route.name === 'orders'}">
|
||||
<RouterLink :to="{name: 'orders'}">Заказы</RouterLink>
|
||||
</li>
|
||||
|
||||
<li :class="{active: route.name === 'slider'}">
|
||||
<RouterLink :to="{name: 'slider'}">Слайдер</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<section class="form-horizontal tab-content">
|
||||
<RouterView/>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Button label="Сохранить настройки" @click="settings.saveSettings"/>
|
||||
</section>
|
||||
|
||||
<div v-if="settings.isLoading" class="tw:w-full tw:h-full tw:absolute tw:top-0 tw:left-0 tw:z-30 tw:backdrop-blur-sm">
|
||||
<div class="tw:fixed tw:top-0 tw:left-0 tw:w-full tw:h-full tw:flex tw:justify-center tw:items-center tw:z-40 tw:text-4xl">
|
||||
<i class="fa fa-spin fa-spinner tw:mr-5"></i>
|
||||
<div>Загрузка...</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toast position="top-right"/>
|
||||
</div>
|
||||
|
||||
<div v-else class="tw:w-full tw:h-full tw:absolute tw:top-0 tw:left-0 tw:z-30 tw:backdrop-blur-sm">
|
||||
<div class="tw:fixed tw:top-0 tw:left-0 tw:w-full tw:h-full tw:flex tw:flex-col tw:justify-center tw:items-center tw:z-40">
|
||||
<i class="fa fa-ban tw:text-4xl"></i>
|
||||
<div class="tw:text-4xl">{{ settings.error }}</div>
|
||||
<div>Обратитесь в поддержку</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
import {RouterView, useRoute} from 'vue-router';
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import Toast from 'primevue/toast';
|
||||
import { toastBus } from '@/utils/toastHelper';
|
||||
import {useToast} from "primevue";
|
||||
import Button from 'primevue/button';
|
||||
import TopLead from "@/components/TopLead.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const settings = useSettingsStore();
|
||||
const toast = useToast();
|
||||
toastBus.on('show', (data) => toast.add(data));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
@@ -19,3 +19,23 @@
|
||||
all: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
.p-toast .p-toast-message-success {
|
||||
color: #3c763d;
|
||||
background-color: #dff0d8;
|
||||
border-color: #d6e9c6;
|
||||
}
|
||||
|
||||
.p-toggleswitch > input[type="checkbox"] {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
border-radius: unset;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
data-placeholder="/image/cache/no_image-100x100.png"
|
||||
alt="Image"
|
||||
@load="isLoaded = true"
|
||||
style="width: 100%; height: 100%;"
|
||||
>
|
||||
</a>
|
||||
<input ref="inputRef" type="hidden" value="" :id="`input-image-${id}`">
|
||||
@@ -48,11 +49,6 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oc-image {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.loader {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
|
||||
27
frontend/admin/src/components/Settings/ItemBool.vue
Normal file
27
frontend/admin/src/components/Settings/ItemBool.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<Switcher v-model="model"/>
|
||||
</template>
|
||||
<template #help>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Switcher from "@/components/Switcher.vue";
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
|
||||
const model = defineModel();
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
116
frontend/admin/src/components/Settings/ItemCategoriesSelect.vue
Normal file
116
frontend/admin/src/components/Settings/ItemCategoriesSelect.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<input
|
||||
ref="searchInput"
|
||||
type="text"
|
||||
placeholder="Начните вводить название категории..."
|
||||
class="form-control"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="well well-sm tw:h-90 tw:overflow-auto">
|
||||
<div v-if="isLoading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
Загрузка списка категорий...
|
||||
</div>
|
||||
<div v-else v-for="(product, index) in selectedProducts"
|
||||
class="tw:flex tw:items-center tw:mb-1">
|
||||
<button
|
||||
@click.prevent="removeItem(index)"
|
||||
class="btn btn-xs btn-danger"
|
||||
>
|
||||
<i class="fa fa-minus-circle"></i>
|
||||
</button>
|
||||
<div class="tw:ml-3">{{ product.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #help>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import {nextTick, onMounted, ref, watch} from "vue";
|
||||
import {apiPost} from "@/utils/http.js";
|
||||
|
||||
const searchInput = ref(null);
|
||||
const isLoading = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
});
|
||||
const model = defineModel();
|
||||
|
||||
function removeItem(index) {
|
||||
model.value.splice(index, 1);
|
||||
}
|
||||
|
||||
const selectedProducts = ref([]);
|
||||
watch(
|
||||
model.value,
|
||||
async (ids) => {
|
||||
if (!ids?.length) {
|
||||
selectedProducts.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const response = await apiPost('getCategoriesById', {
|
||||
category_ids: ids,
|
||||
});
|
||||
|
||||
selectedProducts.value = response.data.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
},
|
||||
{immediate: true}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (searchInput.value) {
|
||||
$(searchInput.value).autocomplete({
|
||||
source: function (request, response) {
|
||||
$.ajax({
|
||||
url: `/admin/index.php?route=catalog/category/autocomplete&user_token=${window.TeleCart.user_token}&filter_name=${encodeURIComponent(request)}`,
|
||||
dataType: 'json',
|
||||
success: function (json) {
|
||||
response($.map(json, function (item) {
|
||||
return {
|
||||
label: item['name'],
|
||||
value: Number(item['category_id']),
|
||||
};
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
select: function (item) {
|
||||
model.value.push(item['value']);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
26
frontend/admin/src/components/Settings/ItemImage.vue
Normal file
26
frontend/admin/src/components/Settings/ItemImage.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<OcImagePicker v-model="model"/>
|
||||
</template>
|
||||
<template #help><slot></slot></template>
|
||||
</SettingsItem>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import OcImagePicker from "@/components/OcImagePicker.vue";
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
|
||||
const model = defineModel();
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
45
frontend/admin/src/components/Settings/ItemInput.vue
Normal file
45
frontend/admin/src/components/Settings/ItemInput.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<InputText
|
||||
:type="type"
|
||||
v-model="model"
|
||||
class="form-control"
|
||||
:placeholder="placeholder"
|
||||
:readonly="readonly"
|
||||
/>
|
||||
</template>
|
||||
<template #help>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import InputText from 'primevue/inputtext';
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Введите значение'
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const model = defineModel();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
116
frontend/admin/src/components/Settings/ItemProductsSelect.vue
Normal file
116
frontend/admin/src/components/Settings/ItemProductsSelect.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<input
|
||||
ref="productsInput"
|
||||
type="text"
|
||||
placeholder="Начните вводить название товара..."
|
||||
class="form-control"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="well well-sm tw:h-90 tw:overflow-auto">
|
||||
<div v-if="isLoading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
Загрузка списка товаров...
|
||||
</div>
|
||||
<div v-else v-for="(product, index) in selectedProducts"
|
||||
class="tw:flex tw:items-center tw:mb-1">
|
||||
<button
|
||||
@click.prevent="removeItem(index)"
|
||||
class="btn btn-xs btn-danger"
|
||||
>
|
||||
<i class="fa fa-minus-circle"></i>
|
||||
</button>
|
||||
<div class="tw:ml-3">{{ product.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #help>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import {nextTick, onMounted, ref, watch} from "vue";
|
||||
import {apiPost} from "@/utils/http.js";
|
||||
|
||||
const productsInput = ref(null);
|
||||
const isLoading = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
});
|
||||
const model = defineModel();
|
||||
|
||||
function removeItem(index) {
|
||||
model.value.splice(index, 1);
|
||||
}
|
||||
|
||||
const selectedProducts = ref([]);
|
||||
watch(
|
||||
model.value,
|
||||
async (ids) => {
|
||||
if (!ids?.length) {
|
||||
selectedProducts.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const response = await apiPost('getProductsById', {
|
||||
product_ids: ids,
|
||||
});
|
||||
|
||||
selectedProducts.value = response.data.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
},
|
||||
{immediate: true}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (productsInput.value) {
|
||||
$(productsInput.value).autocomplete({
|
||||
source: function (request, response) {
|
||||
$.ajax({
|
||||
url: `/admin/index.php?route=catalog/product/autocomplete&user_token=${window.TeleCart.user_token}&filter_name=${encodeURIComponent(request)}`,
|
||||
dataType: 'json',
|
||||
success: function (json) {
|
||||
response($.map(json, function (item) {
|
||||
return {
|
||||
label: item['name'],
|
||||
value: Number(item['product_id']),
|
||||
};
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
select: function (item) {
|
||||
model.value.push(item['value']);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
34
frontend/admin/src/components/Settings/ItemSelect.vue
Normal file
34
frontend/admin/src/components/Settings/ItemSelect.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<select class="form-control" v-model="model">
|
||||
<option v-for="(value, key) in items" :value="key" :key="key">
|
||||
{{ value }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
<template #help>
|
||||
<slot/>
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
|
||||
const model = defineModel();
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
41
frontend/admin/src/components/Settings/ItemTextarea.vue
Normal file
41
frontend/admin/src/components/Settings/ItemTextarea.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<Textarea
|
||||
v-model="model"
|
||||
class="form-control"
|
||||
:placeholder="placeholder"
|
||||
:readonly="readonly"
|
||||
:rows="rows"
|
||||
/>
|
||||
</template>
|
||||
<template #help>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import Textarea from 'primevue/textarea';
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
});
|
||||
const model = defineModel();
|
||||
</script>
|
||||
145
frontend/admin/src/components/Settings/ItemTgBotToken.vue
Normal file
145
frontend/admin/src/components/Settings/ItemTgBotToken.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<div class="tw:flex tw:w-full">
|
||||
<span class="tw:flex">
|
||||
<button
|
||||
class="btn btn-primary tw:whitespace-nowrap"
|
||||
type="button"
|
||||
@click="validateBotToken"
|
||||
:disabled="isLoading || ! settings.items.telegram.bot_token"
|
||||
:class="{
|
||||
'tw:opacity-60 tw:cursor-not-allowed': isLoading
|
||||
}"
|
||||
>
|
||||
<i
|
||||
:class="isLoading ? 'fa fa-spinner fa-spin tw:mr-1' : 'fa fa-refresh tw:mr-1'"
|
||||
></i>
|
||||
{{ isLoading ? 'Проверяю...' : 'Проверить Bot Token' }}
|
||||
</button>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="model"
|
||||
@input="handleInput"
|
||||
@blur="validateBotToken"
|
||||
placeholder="Введите токен от Telegram бота"
|
||||
class="form-control"
|
||||
:readonly="isLoading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="validationStatus"
|
||||
class="alert"
|
||||
:class="validationStatusClass"
|
||||
>
|
||||
{{ validationStatus }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #help>
|
||||
Подробная инструкция доступна в
|
||||
<a href="https://telecart-labs.github.io/docs/telegram/telegram/#%D1%81%D0%BE%D0%B7%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5-%D0%B1%D0%BE%D1%82%D0%B0" target="_blank">документации
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>.
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import {ref, computed} from "vue";
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import {apiPost} from "@/utils/http.js";
|
||||
|
||||
const model = defineModel();
|
||||
const settings = useSettingsStore();
|
||||
const validationStatus = ref(null);
|
||||
const isLoading = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const validationStatusClass = computed(() => {
|
||||
if (!validationStatus.value) return '';
|
||||
|
||||
if (validationStatus.value.startsWith('✅')) {
|
||||
return 'alert-success';
|
||||
}
|
||||
|
||||
if (validationStatus.value.startsWith('❌')) {
|
||||
return 'alert-danger';
|
||||
}
|
||||
|
||||
return 'alert-info';
|
||||
});
|
||||
|
||||
function handleInput(event) {
|
||||
model.value = event.target.value;
|
||||
// Сбрасываем статус валидации при изменении токена
|
||||
if (validationStatus.value) {
|
||||
validationStatus.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function validateBotToken() {
|
||||
const botToken = model.value?.trim() || '';
|
||||
|
||||
// Валидация пустого токена
|
||||
if (botToken.length === 0) {
|
||||
validationStatus.value = '❌ Введите Bot Token!';
|
||||
return;
|
||||
}
|
||||
|
||||
// Сбрасываем предыдущий статус
|
||||
validationStatus.value = null;
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const result = await apiPost('configureBotToken', { botToken });
|
||||
|
||||
if (!result.success) {
|
||||
// Обработка ошибок
|
||||
if (result.status === 422) {
|
||||
validationStatus.value = `❌ Ошибка: ${result.error || 'Неверный токен'}`;
|
||||
} else {
|
||||
validationStatus.value = `❌ Ошибка проверки BotToken: ${result.error || 'Неизвестная ошибка'}`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const response = result.data;
|
||||
|
||||
// Проверка наличия обязательных полей в ответе
|
||||
if (!response?.id) {
|
||||
validationStatus.value = '❌ Ошибка: bot token не найден в ответе сервера.';
|
||||
console.error('Неожиданный формат ответа:', response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Успешная валидация
|
||||
const username = response.username ? `@${response.username}` : 'не указан';
|
||||
const webhookUrl = response.webhook_url || 'не настроен';
|
||||
validationStatus.value = `✅ Бот: ${username} (id: ${response.id}) webhook: ${webhookUrl}`;
|
||||
|
||||
// Обновляем токен в store, если нужно (на случай если сервер что-то изменил)
|
||||
if (response.bot_token && response.bot_token !== botToken) {
|
||||
model.value = response.bot_token;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при валидации BotToken:', error);
|
||||
validationStatus.value = '❌ Ошибка проверки BotToken. Проверьте подключение к серверу.';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
173
frontend/admin/src/components/Settings/ItemTgChatID.vue
Normal file
173
frontend/admin/src/components/Settings/ItemTgChatID.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<template v-if="settings.items.telegram.bot_token">
|
||||
<div class="tw:flex tw:w-full">
|
||||
<span class="tw:flex">
|
||||
<button
|
||||
class="btn btn-primary tw:whitespace-nowrap"
|
||||
type="button"
|
||||
@click="getChatId"
|
||||
:disabled="isLoading || !settings.items.telegram.bot_token"
|
||||
:class="{
|
||||
'tw:opacity-60 tw:cursor-not-allowed': isLoading
|
||||
}"
|
||||
>
|
||||
<i
|
||||
:class="isLoading ? 'fa fa-spinner fa-spin tw:mr-1' : 'fa fa-refresh tw:mr-1'"
|
||||
></i>
|
||||
{{ isLoading ? 'Получаю...' : 'Получить Chat ID' }}
|
||||
</button>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="model"
|
||||
@input="handleInput"
|
||||
:placeholder="placeholder"
|
||||
class="form-control"
|
||||
:readonly="isLoading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="statusMessage"
|
||||
class="alert"
|
||||
:class="statusMessageClass"
|
||||
>
|
||||
{{ statusMessage }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-link btn-xs"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
:data-target="`#${collapseId}`"
|
||||
aria-expanded="false"
|
||||
:aria-controls="collapseId"
|
||||
>
|
||||
Инструкция как получить ChatID.
|
||||
</button>
|
||||
<div class="collapse" :id="collapseId">
|
||||
<div class="well">
|
||||
<p class="text-primary">Как получить Chat ID</p>
|
||||
<ol>
|
||||
<li>Убедитесь, что Telegram Bot Token введён выше.</li>
|
||||
<li>Откройте вашего бота в Telegram и отправьте ему кодовое слово: <code>opencart_get_chatid</code>. Важно отправить именно такое сообщение, иначе не сработает.</li>
|
||||
<li>Вернитесь сюда и нажмите кнопку «Получить Chat ID» — скрипт автоматически подставит его в поле ниже.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="alert alert-warning">
|
||||
<strong>BotToken</strong> не указан. Пожалуйста, введите корректный BotToken. После этого здесь станет доступна настройка ChatID.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #help>
|
||||
Идентификатор Telegram-чата, куда будут отправляться уведомления о новых заказах. Если оставить поле пустым, уведомления отправляться не будут.
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import {ref, computed, useId} from "vue";
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import {apiGet} from "@/utils/http.js";
|
||||
|
||||
const model = defineModel();
|
||||
const settings = useSettingsStore();
|
||||
const statusMessage = ref(null);
|
||||
const isLoading = ref(false);
|
||||
const collapseId = useId();
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Chat ID будет получен автоматически',
|
||||
},
|
||||
});
|
||||
|
||||
const statusMessageClass = computed(() => {
|
||||
if (!statusMessage.value) return '';
|
||||
|
||||
if (statusMessage.value.startsWith('✅')) {
|
||||
return 'alert-success';
|
||||
}
|
||||
|
||||
if (statusMessage.value.startsWith('❌')) {
|
||||
return 'alert-danger';
|
||||
}
|
||||
|
||||
return 'alert-info';
|
||||
});
|
||||
|
||||
function handleInput(event) {
|
||||
model.value = event.target.value;
|
||||
// Сбрасываем статус сообщения при изменении значения
|
||||
if (statusMessage.value) {
|
||||
statusMessage.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getChatId() {
|
||||
// Проверка наличия bot_token
|
||||
if (!settings.items.telegram.bot_token?.trim()) {
|
||||
alert('Сначала введите Telegram Bot Token!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Сбрасываем предыдущее сообщение
|
||||
statusMessage.value = null;
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const response = await apiGet('getChatId');
|
||||
|
||||
if (!response.success) {
|
||||
// Обработка ошибок
|
||||
const errorMessage = response.data?.message || response.error || 'Неизвестная ошибка';
|
||||
|
||||
if (response.status === 422) {
|
||||
statusMessage.value = `❌ ${errorMessage}`;
|
||||
} else {
|
||||
statusMessage.value = `❌ Ошибка получения Chat ID: ${errorMessage}`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверка наличия chat_id в ответе
|
||||
if (!response.data?.chat_id) {
|
||||
statusMessage.value = '❌ Ошибка: Chat ID не найден в ответе сервера.';
|
||||
console.error('Неожиданный формат ответа:', response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Успешное получение Chat ID
|
||||
const chatId = response.data.chat_id;
|
||||
model.value = chatId;
|
||||
statusMessage.value = '✅ ChatID успешно получен и подставлен в поле. Не забудьте сохранить настройки!';
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении Chat ID:', error);
|
||||
statusMessage.value = '❌ Ошибка получения Chat ID. Проверьте подключение к серверу.';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
code {
|
||||
background-color: #f5f5f5;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
|
||||
169
frontend/admin/src/components/Settings/ItemTgMessageTemplate.vue
Normal file
169
frontend/admin/src/components/Settings/ItemTgMessageTemplate.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<textarea
|
||||
v-model="model"
|
||||
:rows="rows"
|
||||
:placeholder="placeholder"
|
||||
class="form-control"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-link"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
:data-target="`#${collapseId}`"
|
||||
aria-expanded="false"
|
||||
:aria-controls="collapseId"
|
||||
>
|
||||
Документация
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
@click="sendTestMessage"
|
||||
:disabled="isSending"
|
||||
:class="{
|
||||
'tw:opacity-60 tw:cursor-not-allowed': isSending
|
||||
}"
|
||||
>
|
||||
<i :class="isSending ? 'fa fa-spinner fa-spin' : 'fa fa-envelope'"></i>
|
||||
{{ isSending ? 'Отправляю...' : 'Отправить тестовое уведомление' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="collapse" :id="collapseId" style="margin-top: 15px">
|
||||
<div class="well">
|
||||
<p>Вы можете использовать переменные:</p>
|
||||
<ul>
|
||||
<li><code>{store_name}</code> — название магазина</li>
|
||||
<li><code>{order_id}</code> — номер заказа</li>
|
||||
<li><code>{customer}</code> — имя и фамилия покупателя</li>
|
||||
<li><code>{email}</code> — email покупателя</li>
|
||||
<li><code>{phone}</code> — телефон</li>
|
||||
<li><code>{comment}</code> — комментарий к заказу</li>
|
||||
<li><code>{address}</code> — адрес доставки</li>
|
||||
<li><code>{total}</code> — сумма заказа</li>
|
||||
<li><code>{ip}</code> — IP покупателя</li>
|
||||
<li><code>{created_at}</code> — дата и время создания заказа</li>
|
||||
</ul>
|
||||
<p>
|
||||
Форматирование: поддерживается
|
||||
<a href="https://core.telegram.org/bots/api#markdownv2-style" target="_blank">
|
||||
*MarkdownV2*
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>.
|
||||
</p>
|
||||
<p>Символы, которые нужно экранировать в тексте:</p>
|
||||
<pre>_ * [ ] ( ) ~ ` > # + - = | { } . !</pre>
|
||||
<p>
|
||||
Каждый из них нужно экранировать обратным слэшем \, если он не используется для форматирования.
|
||||
Например вместо <code>Заказ #123</code> нужно писать <code>Заказ \#123</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #help>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import {ref, toRaw, useId} from "vue";
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import {apiPost} from "@/utils/http.js";
|
||||
|
||||
const model = defineModel();
|
||||
const settings = useSettingsStore();
|
||||
const isSending = ref(false);
|
||||
const collapseId = useId();
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Введите шаблон сообщения',
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
});
|
||||
|
||||
async function sendTestMessage() {
|
||||
console.log(toRaw(settings.items.telegram));
|
||||
const telegramToken = settings.items.telegram.bot_token?.trim();
|
||||
|
||||
if (!telegramToken) {
|
||||
alert('Сначала введите Telegram Bot Token!');
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = settings.items.telegram.chat_id;
|
||||
|
||||
if (!chatId) {
|
||||
alert('Сначала введите Chat ID!');
|
||||
return;
|
||||
}
|
||||
|
||||
const template = model.value?.trim();
|
||||
|
||||
if (!template) {
|
||||
alert('Сначала задайте шаблон!');
|
||||
return;
|
||||
}
|
||||
|
||||
isSending.value = true;
|
||||
|
||||
try {
|
||||
const result = await apiPost('testTgMessage', {
|
||||
token: telegramToken,
|
||||
chat_id: chatId,
|
||||
template: template,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
const errorMessage = result.data?.message || result.error || 'Неизвестная ошибка';
|
||||
alert(`Ошибка: ${errorMessage}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = result.data;
|
||||
alert(response.message || 'Уведомление успешно отправлено');
|
||||
} catch (error) {
|
||||
console.error('Ошибка при отправке тестового сообщения:', error);
|
||||
alert('Ошибка при отправке тестового сообщения');
|
||||
} finally {
|
||||
isSending.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
code {
|
||||
background-color: #f5f5f5;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
31
frontend/admin/src/components/Settings/ItemTgMiniAppLink.vue
Normal file
31
frontend/admin/src/components/Settings/ItemTgMiniAppLink.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<ItemInput
|
||||
:label="label"
|
||||
type="text"
|
||||
:readonly="true"
|
||||
:modelValue="model"
|
||||
>
|
||||
Токен, полученный при создании бота через @BotFather.
|
||||
Он используется для взаимодействия модуля с Telegram API.
|
||||
Подробная инструкция доступна в
|
||||
<a href="https://nikitakiselev.github.io/telecart-docs/#telegram" target="_blank">
|
||||
документации <i class="fa fa-external-link"></i>
|
||||
</a>.
|
||||
</ItemInput>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ItemInput from "@/components/Settings/ItemInput.vue";
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const model = defineModel();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -157,7 +157,7 @@ import LinkSelector from "@/components/Slider/LinkSelector.vue";
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import Switcher from "@/components/Switcher.vue";
|
||||
|
||||
const slider = ref({});
|
||||
const slider = defineModel();
|
||||
|
||||
function removeSlide(index) {
|
||||
slider.value.slides.splice(index, 1);
|
||||
@@ -173,10 +173,6 @@ function addSlide() {
|
||||
image: '',
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
slider.value = JSON.parse(window.TeleCart.mainpage_slider);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
<template>
|
||||
<div class="btn-group btn-toggle tw:mt-3">
|
||||
<button
|
||||
class="btn btn-xs"
|
||||
:class="{active: model === true, 'btn-success': model === true, 'btn-default' : model === false }"
|
||||
@click.prevent="model = true"
|
||||
>
|
||||
Вкл
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs"
|
||||
:class="{active: model === false, 'btn-danger': model === false, 'btn-default' : model === true }"
|
||||
@click.prevent="model = false"
|
||||
>
|
||||
Выкл
|
||||
</button>
|
||||
</div>
|
||||
<ToggleSwitch v-model="model" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
|
||||
const model = defineModel({
|
||||
default: false,
|
||||
});
|
||||
|
||||
112
frontend/admin/src/components/TopLead.vue
Normal file
112
frontend/admin/src/components/TopLead.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="tw:bg-surface-0 tw:dark:bg-surface-950 tw:px-6 tw:py-8 tw:md:px-12 tw:lg:px-20">
|
||||
<div class="tw:flex tw:items-center tw:flex-col tw:lg:flex-row tw:lg:justify-between">
|
||||
<div class="tw:flex tw:items-start tw:flex-col tw:lg:flex-row tw:gap-8">
|
||||
<OcImagePicker v-model="settings.items.app.app_icon" class="tw:w-[6.42rem] tw:h-[6.42rem]"/>
|
||||
<div class="tw:flex tw:flex-col tw:gap-4">
|
||||
<div class="tw:flex tw:items-center">
|
||||
<span class="tw:text-surface-900 tw:dark:text-surface-0 tw:font-bold tw:text-3xl">
|
||||
{{ settings.items.app.app_name }}
|
||||
</span>
|
||||
<a
|
||||
v-if="tgMe?.result?.first_name"
|
||||
:href="`https://t.me/${tgMe?.result?.username}`"
|
||||
class="tw:ml-2 tw:text-surface-900 tw:dark:text-surface-0 tw:text-xl">
|
||||
@{{ tgMe?.result?.first_name }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="tw:flex tw:items-center tw:flex-wrap tw:gap-8">
|
||||
<div>
|
||||
<span class="tw:text-surface-500 tw:dark:text-surface-300">Количество заказов</span>
|
||||
<div
|
||||
class="tw:text-surface-700 tw:dark:text-surface-100 tw:mt-1 tw:text-sm tw:font-semibold">
|
||||
{{ stats.items.orders_count ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="tw:text-surface-500 tw:dark:text-surface-300">Общая сумма</span>
|
||||
<div
|
||||
class="tw:text-surface-700 tw:dark:text-surface-100 tw:mt-1 tw:text-sm tw:font-semibold">
|
||||
{{ stats.items.orders_total_amount ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="tw:text-surface-500 tw:dark:text-surface-300">Уникальные товары</span>
|
||||
<div
|
||||
class="tw:text-surface-700 tw:dark:text-surface-100 tw:mt-1 tw:text-sm tw:font-semibold">
|
||||
{{ stats.items.order_products_count ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="tw:text-surface-500 tw:dark:text-surface-300">Статус магазина</span>
|
||||
<div
|
||||
class="tw:text-surface-700 tw:dark:text-surface-100 tw:mt-1 tw:text-sm tw:font-semibold">
|
||||
<div v-if="settings.items.app.app_enabled" class="tw:flex tw:items-center">
|
||||
<div class="tw:h-2 tw:w-2 tw:rounded-full tw:bg-green-400 tw:flex tw:mr-2">
|
||||
<span
|
||||
class="tw:inline-flex tw:h-full tw:w-full tw:animate-ping tw:rounded-full tw:bg-green-400 tw:opacity-75"></span>
|
||||
</div>
|
||||
<div>Online</div>
|
||||
</div>
|
||||
|
||||
<div v-else
|
||||
class="tw:text-surface-700 tw:dark:text-surface-100 tw:mt-1 tw:text-sm tw:font-semibold">
|
||||
<div class="tw:flex tw:items-center">
|
||||
<div class="tw:h-2 tw:w-2 tw:rounded-full tw:bg-red-400 tw:flex tw:mr-2">
|
||||
<span
|
||||
class="tw:inline-flex tw:h-full tw:w-full tw:animate-ping tw:rounded-full tw:bg-red-400 tw:opacity-75"></span>
|
||||
</div>
|
||||
<div>Offline</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw:mt-6 tw:lg:mt-0 tw:flex tw:items-center tw:gap-4">
|
||||
<div class="btn-group">
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
:class="{'disabled': (tgMe?.result?.has_main_web_app !== true)}"
|
||||
rounded
|
||||
:href="`https://t.me/${tgMe?.result?.username}?startapp`"
|
||||
target="_blank"
|
||||
:title="(tgMe?.result?.has_main_web_app !== true) ? 'Вы не привязали Telegram Mini App к боту.' : 'Открыть Telegram магазин'"
|
||||
>
|
||||
<i class="fa fa-play"></i>
|
||||
</a>
|
||||
<a class="btn btn-default" target="_blank" href="https://telecart-labs.github.io/docs/" title="Документация по модулю TeleCart">
|
||||
<i class="fa fa-book"></i>
|
||||
</a>
|
||||
<a class="btn btn-default" target="_blank" href="https://t.me/ocstore3" title="Официальная Telegram группа модуля TeleCart">
|
||||
<i class="fa fa-group"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Button from "primevue/button";
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import {useStatsStore} from "@/stores/stats.js";
|
||||
import {onMounted, ref} from "vue";
|
||||
import OcImagePicker from "@/components/OcImagePicker.vue";
|
||||
import {apiGet} from "@/utils/http.js";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
const stats = useStatsStore();
|
||||
const tgMe = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
await stats.fetchStats();
|
||||
const response = await apiGet('tgGetMe');
|
||||
tgMe.value = response.data;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -3,6 +3,15 @@ import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import PrimeVue from 'primevue/config';
|
||||
import Aura from '@primeuix/themes/aura';
|
||||
import ToastService from 'primevue/toastservice';
|
||||
import {definePreset} from "@primeuix/themes";
|
||||
|
||||
const MyPreset = definePreset(Aura, {
|
||||
|
||||
});
|
||||
|
||||
function onReady(fn) {
|
||||
if (document.readyState === 'loading') {
|
||||
@@ -12,9 +21,20 @@ function onReady(fn) {
|
||||
}
|
||||
}
|
||||
|
||||
onReady(() => {
|
||||
onReady(async () => {
|
||||
const app = createApp(App);
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: MyPreset,
|
||||
options: {
|
||||
cssLayer: false, // если используешь Tailwind, отключает layering
|
||||
},
|
||||
}
|
||||
});
|
||||
app.use(ToastService);
|
||||
|
||||
app.mount('#app');
|
||||
await useSettingsStore().fetchSettings();
|
||||
});
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import {createMemoryHistory, createRouter} from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
import {createMemoryHistory, createRouter} from 'vue-router';
|
||||
import SliderView from "@/views/SliderView.vue";
|
||||
import GeneralView from "@/views/GeneralView.vue";
|
||||
import TextsView from "@/views/TextsView.vue";
|
||||
import OrdersView from "@/views/OrdersView.vue";
|
||||
import TelegramView from "@/views/TelegramView.vue";
|
||||
import MetricsView from "@/views/MetricsView.vue";
|
||||
import StoreView from "@/views/StoreView.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView,
|
||||
},
|
||||
{path: '/', name: 'general', component: GeneralView},
|
||||
{path: '/slider', name: 'slider', component: SliderView},
|
||||
{path: '/orders', name: 'orders', component: OrdersView},
|
||||
{path: '/texts', name: 'texts', component: TextsView},
|
||||
{path: '/telegram', name: 'telegram', component: TelegramView},
|
||||
{path: '/metrics', name: 'metrics', component: MetricsView},
|
||||
{path: '/store', name: 'store', component: StoreView},
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
||||
125
frontend/admin/src/stores/settings.js
Normal file
125
frontend/admin/src/stores/settings.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {apiGet, apiPost} from "@/utils/http.js";
|
||||
import {toastBus} from "@/utils/toastHelper.js";
|
||||
|
||||
export const useSettingsStore = defineStore('settings', {
|
||||
state: () => ({
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
items: {
|
||||
app: {
|
||||
app_enabled: true,
|
||||
app_name: '',
|
||||
app_icon: null,
|
||||
theme_light: 'light',
|
||||
theme_dark: 'dark',
|
||||
app_debug: false,
|
||||
},
|
||||
|
||||
telegram: {
|
||||
mini_app_url: '',
|
||||
bot_token: '',
|
||||
chat_id: '',
|
||||
owner_notification_template: '',
|
||||
customer_notification_template: '',
|
||||
},
|
||||
|
||||
metrics: {
|
||||
yandex_metrika_enabled: false,
|
||||
yandex_metrika_counter: '',
|
||||
},
|
||||
|
||||
store: {
|
||||
enable_store: true,
|
||||
mainpage_products: 'most_viewed',
|
||||
featured_products: [],
|
||||
mainpage_categories: 'latest10',
|
||||
featured_categories: [],
|
||||
feature_coupons: true,
|
||||
feature_vouchers: true,
|
||||
},
|
||||
|
||||
orders: {
|
||||
order_default_status_id: 1,
|
||||
},
|
||||
|
||||
texts: {
|
||||
text_no_more_products: '',
|
||||
text_empty_cart: '',
|
||||
text_order_created_success: '',
|
||||
},
|
||||
|
||||
sliders: {
|
||||
mainpage_slider: {
|
||||
is_enabled: false,
|
||||
effect: "slide",
|
||||
pagination: true,
|
||||
scrollbar: false,
|
||||
free_mode: false,
|
||||
space_between: 30,
|
||||
autoplay: false,
|
||||
loop: false,
|
||||
slides: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
getters: {
|
||||
app_icon_preview: (state) => {
|
||||
if (!state.items.app.app_icon) return '/image/cache/no_image-100x100.png';
|
||||
const extIndex = state.items.app.app_icon.lastIndexOf('.');
|
||||
const ext = state.items.app.app_icon.substring(extIndex);
|
||||
const filename = state.items.app.app_icon.substring(0, extIndex);
|
||||
return `/image/cache/${filename}-100x100${ext}`;
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchSettings() {
|
||||
this.isLoading = true;
|
||||
this.error = null;
|
||||
const response = await apiGet('getSettingsForm');
|
||||
if (response.success) {
|
||||
this.items = {
|
||||
...this.items,
|
||||
...response.data,
|
||||
};
|
||||
} else {
|
||||
this.error = 'Возникли проблемы при загрузке настроек.';
|
||||
}
|
||||
this.isLoading = false;
|
||||
},
|
||||
|
||||
async saveSettings() {
|
||||
this.isLoading = true;
|
||||
const settings = this.transformSettingsToStore(this.items);
|
||||
const response = await apiPost('saveSettingsForm', settings);
|
||||
|
||||
if (response.success === true) {
|
||||
toastBus.emit('show', {
|
||||
severity: 'success',
|
||||
summary: 'Готово!',
|
||||
detail: 'Настройки сохранены.',
|
||||
life: 2000,
|
||||
});
|
||||
} else {
|
||||
toastBus.emit('show', {
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: 'Возникли проблемы при сохранении настроек на сервере.',
|
||||
life: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
this.isLoading = false;
|
||||
},
|
||||
|
||||
transformSettingsToStore(items) {
|
||||
return items;
|
||||
},
|
||||
},
|
||||
});
|
||||
22
frontend/admin/src/stores/stats.js
Normal file
22
frontend/admin/src/stores/stats.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {apiGet, apiPost} from "@/utils/http.js";
|
||||
|
||||
export const useStatsStore = defineStore('stats', {
|
||||
state: () => ({
|
||||
items: {
|
||||
orders_count: null,
|
||||
orders_total_amount: null,
|
||||
order_products_count: null,
|
||||
}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async fetchStats() {
|
||||
const response = await apiPost('getDashboardStats');
|
||||
this.items.orders_count = response.data?.data?.orders_count;
|
||||
this.items.orders_total_amount = response.data?.data?.orders_total_amount;
|
||||
this.items.order_products_count = response.data?.data?.order_products_count;
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
142
frontend/admin/src/utils/http.js
Normal file
142
frontend/admin/src/utils/http.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* Получает user_token из глобального объекта TeleCart
|
||||
*/
|
||||
function getUserToken() {
|
||||
if (typeof window !== 'undefined' && window.TeleCart?.user_token) {
|
||||
return window.TeleCart.user_token;
|
||||
}
|
||||
|
||||
// Fallback: пытаемся получить из URL как запасной вариант
|
||||
if (typeof window !== 'undefined') {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('user_token') || '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Базовый URL для API запросов
|
||||
*/
|
||||
function getBaseUrl() {
|
||||
return '/admin/index.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает URL для API запроса
|
||||
* @param {string} apiAction - действие API (например, 'configureBotToken')
|
||||
* @returns {string} полный URL
|
||||
*/
|
||||
function buildApiUrl(apiAction) {
|
||||
const baseUrl = getBaseUrl();
|
||||
const userToken = getUserToken();
|
||||
return `${baseUrl}?route=extension/module/tgshop/handle&api_action=${apiAction}&user_token=${userToken}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP клиент для работы с API
|
||||
*/
|
||||
const httpClient = axios.create({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Выполняет POST запрос к API
|
||||
* @param {string} apiAction - действие API
|
||||
* @param {object} data - данные для отправки
|
||||
* @returns {Promise} результат запроса
|
||||
*/
|
||||
export async function apiPost(apiAction, data = {}) {
|
||||
const url = buildApiUrl(apiAction);
|
||||
|
||||
try {
|
||||
const response = await httpClient.post(url, data);
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
status: response.status,
|
||||
};
|
||||
} catch (error) {
|
||||
// Обработка ошибок axios
|
||||
if (error.response) {
|
||||
// Сервер вернул ошибку
|
||||
const status = error.response.status;
|
||||
const errorData = error.response.data;
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorData?.error || error.response.statusText,
|
||||
status,
|
||||
data: errorData,
|
||||
};
|
||||
} else if (error.request) {
|
||||
// Запрос был отправлен, но ответа не получено
|
||||
return {
|
||||
success: false,
|
||||
error: 'Не удалось получить ответ от сервера',
|
||||
status: 0,
|
||||
};
|
||||
} else {
|
||||
// Ошибка при настройке запроса
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Произошла неизвестная ошибка',
|
||||
status: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполняет GET запрос к API
|
||||
* @param {string} apiAction - действие API
|
||||
* @param {object} params - query параметры
|
||||
* @returns {Promise} результат запроса
|
||||
*/
|
||||
export async function apiGet(apiAction, params = {}) {
|
||||
const url = buildApiUrl(apiAction);
|
||||
|
||||
try {
|
||||
const response = await httpClient.get(url, { params: params });
|
||||
return {
|
||||
success: true,
|
||||
data: response.data.data,
|
||||
status: response.status,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
const status = error.response.status;
|
||||
const errorData = error.response.data;
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorData?.error || error.response.statusText,
|
||||
status,
|
||||
data: errorData,
|
||||
};
|
||||
} else if (error.request) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Не удалось получить ответ от сервера',
|
||||
status: 0,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Произошла неизвестная ошибка',
|
||||
status: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
apiPost,
|
||||
apiGet,
|
||||
getUserToken,
|
||||
};
|
||||
|
||||
2
frontend/admin/src/utils/toastHelper.js
Normal file
2
frontend/admin/src/utils/toastHelper.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import mitt from 'mitt';
|
||||
export const toastBus = mitt();
|
||||
56
frontend/admin/src/views/GeneralView.vue
Normal file
56
frontend/admin/src/views/GeneralView.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<ItemBool label="Статус" v-model="settings.items.app.app_enabled">
|
||||
Если выключено, покупатели в Telegram увидят сообщение, что магазин временно закрыт.
|
||||
Заказы и просмотр товаров будут недоступны.
|
||||
</ItemBool>
|
||||
|
||||
<ItemInput label="Название приложения"
|
||||
v-model="settings.items.app.app_name"
|
||||
placeholder="Без названия"
|
||||
>
|
||||
Отображается в заголовке Telegram Mini App при запуске, а также используется как подпись
|
||||
под иконкой, если пользователь добавит приложение на главный экран своего устройства.
|
||||
Рекомендуется короткое и понятное название (до 20 символов).
|
||||
Если оставить пустым, то название выводиться не будет.
|
||||
</ItemInput>
|
||||
|
||||
<ItemImage label="Иконка приложения" v-model="settings.items.app.app_icon">
|
||||
Изображение, которое будет отображаться в Telegram Mini App.
|
||||
</ItemImage>
|
||||
|
||||
<ItemSelect label="Светлая тема" v-model="settings.items.app.theme_light" :items="themes">
|
||||
Выберите стиль, который будет использоваться при отображении вашего магазина
|
||||
в Telegram для дневного режима.
|
||||
<a href="https://daisyui.com/docs/themes/#list-of-themes" target="_blank">
|
||||
Посмотреть как выглядят темы
|
||||
</a>
|
||||
</ItemSelect>
|
||||
|
||||
<ItemSelect label="Тёмная тема" v-model="settings.items.app.theme_dark" :items="themes">
|
||||
Выберите стиль, который будет использоваться при отображении вашего магазина
|
||||
в Telegram для ночного режима.
|
||||
<a href="https://daisyui.com/docs/themes/#list-of-themes" target="_blank">
|
||||
Посмотреть как выглядят темы
|
||||
</a>
|
||||
</ItemSelect>
|
||||
|
||||
<ItemBool label="Режим разработчика" v-model="settings.items.app.app_debug">
|
||||
Режим разработчика. Рекомендуется включать только по необходимости.
|
||||
В остальных случаях, для нормальной работы магазина, должен быть выключен.
|
||||
</ItemBool>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import ItemBool from "@/components/Settings/ItemBool.vue";
|
||||
import ItemImage from "@/components/Settings/ItemImage.vue";
|
||||
import ItemSelect from "@/components/Settings/ItemSelect.vue";
|
||||
import ItemInput from "@/components/Settings/ItemInput.vue";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
const themes = JSON.parse(window.TeleCart.themes);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<Slider/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Slider from "@/components/Slider/Slider.vue";
|
||||
</script>
|
||||
29
frontend/admin/src/views/MetricsView.vue
Normal file
29
frontend/admin/src/views/MetricsView.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<ItemBool
|
||||
label="Яндекс.Метрика"
|
||||
v-model="settings.items.metrics.yandex_metrika_enabled"
|
||||
>
|
||||
Задействовать Яндекс.Метрику для Telegram магазина.
|
||||
</ItemBool>
|
||||
|
||||
<ItemTextarea
|
||||
label="Код счётчика Яндекс Метрики"
|
||||
v-model="settings.items.metrics.yandex_metrika_counter"
|
||||
placeholder="Вставьте код счётчика Яндекс.Метрики"
|
||||
>
|
||||
<p>Код счётчика нужно предварительно настроить, чтобы он работал корректно с Telegram Mini App.
|
||||
<a href="https://telecart-labs.github.io/docs/analitycs/start/" target="_blank">
|
||||
Инструкция как настроить i.fa.fa-external-link
|
||||
</a>.</p>
|
||||
<p>Для проверки интеграции через кнопку "Проверить" в интерфейсе Яндекс Метрики,
|
||||
необходимо сначала включить "Режим разработчика" на вкладке "Общие".</p>
|
||||
</ItemTextarea>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import ItemTextarea from "@/components/Settings/ItemTextarea.vue";
|
||||
import ItemBool from "@/components/Settings/ItemBool.vue";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
</script>
|
||||
17
frontend/admin/src/views/OrdersView.vue
Normal file
17
frontend/admin/src/views/OrdersView.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<ItemSelect
|
||||
label="Статус заказов"
|
||||
v-model="settings.items.orders.order_default_status_id"
|
||||
:items="orderStatuses"
|
||||
>
|
||||
Статус, с которым будут создаваться заказы через Telegram по умолчанию.
|
||||
</ItemSelect>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import ItemSelect from "@/components/Settings/ItemSelect.vue";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
const orderStatuses = JSON.parse(window.TeleCart.order_statuses);
|
||||
</script>
|
||||
10
frontend/admin/src/views/SliderView.vue
Normal file
10
frontend/admin/src/views/SliderView.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<Slider v-model="settings.items.sliders.mainpage_slider"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Slider from "@/components/Slider/Slider.vue";
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
</script>
|
||||
80
frontend/admin/src/views/StoreView.vue
Normal file
80
frontend/admin/src/views/StoreView.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<ItemBool label="Разрешить покупки" v-model="settings.items.store.enable_store">
|
||||
<p>Если опция <strong>включена</strong> — пользователи смогут оформлять
|
||||
заказы прямо в Telegram-магазине. <br>
|
||||
Если <strong>выключена</strong> — оформление заказов будет недоступно. Вместо кнопки «Добавить
|
||||
в корзину» пользователи увидят кнопку «Перейти к товару», которая откроет страницу товара на
|
||||
вашем сайте. В этом режиме Telecart работает как каталог.</p>
|
||||
</ItemBool>
|
||||
|
||||
<ItemSelect
|
||||
label="Товары на главной"
|
||||
v-model="settings.items.store.mainpage_products"
|
||||
:items="mainpage_products_options"
|
||||
>
|
||||
Выберите, какие товары показывать на главной странице магазина в Telegram.
|
||||
Это влияет на первую видимую секцию каталога для пользователя.
|
||||
</ItemSelect>
|
||||
|
||||
<ItemProductsSelect
|
||||
label="Избранные товары"
|
||||
v-model="settings.items.store.featured_products"
|
||||
>
|
||||
На главной странице будут отображаться избранные товары, если вы выберете этот вариант в
|
||||
настройке “Товары на главной”. Если товары не выбраны, то будут показаны популярные товары.
|
||||
</ItemProductsSelect>
|
||||
|
||||
<ItemSelect
|
||||
label="Категории на главной"
|
||||
v-model="settings.items.store.mainpage_categories"
|
||||
:items="mainpage_categories_options"
|
||||
>
|
||||
Выберите, какие товары показывать на главной странице магазина в Telegram.
|
||||
Это влияет на первую видимую секцию каталога для пользователя.
|
||||
</ItemSelect>
|
||||
|
||||
<ItemCategoriesSelect
|
||||
label="Избранные категории"
|
||||
v-model="settings.items.store.featured_categories"
|
||||
>
|
||||
На главной странице будут отображаться эти категории,
|
||||
если вы выберете этот вариант в настройке “Категории на главной”.
|
||||
</ItemCategoriesSelect>
|
||||
|
||||
<ItemBool label="Промокоды" v-model="settings.items.store.feature_coupons">
|
||||
<p>
|
||||
Позволяет использовать стандартные
|
||||
<a :href="`/admin/index.php?route=marketing/coupon&user_token=${userToken}`"
|
||||
target="_blank">купоны OpenCart</a>
|
||||
для предоставления скидок при оформлении заказа.</p>
|
||||
</ItemBool>
|
||||
|
||||
<ItemBool label="Подарочные сертификаты" v-model="settings.items.store.feature_vouchers">
|
||||
<p>
|
||||
Позволяет использовать стандартные
|
||||
<a :href="`/admin/index.php?route=sale/voucher&user_token=${userToken}`"
|
||||
target="_blank">подарочные сертификаты OpenCart</a> при оформлении заказа.</p>
|
||||
</ItemBool>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import ItemBool from "@/components/Settings/ItemBool.vue";
|
||||
import ItemSelect from "@/components/Settings/ItemSelect.vue";
|
||||
import ItemProductsSelect from "@/components/Settings/ItemProductsSelect.vue";
|
||||
import ItemCategoriesSelect from "@/components/Settings/ItemCategoriesSelect.vue";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
const mainpage_products_options = {
|
||||
most_viewed: 'Популярные товары',
|
||||
latest: 'Последние сверху',
|
||||
featured: 'Избранные товары (задать в поле ниже)',
|
||||
};
|
||||
const mainpage_categories_options = {
|
||||
no_categories: 'Отображать только кнопку "Каталог"',
|
||||
latest10: 'Последние 10 категорий',
|
||||
featured: 'Избранные категории (задать в поле ниже)',
|
||||
};
|
||||
|
||||
const userToken = window.TeleCart.user_token;
|
||||
</script>
|
||||
28
frontend/admin/src/views/TelegramView.vue
Normal file
28
frontend/admin/src/views/TelegramView.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<ItemTgMiniAppLink label="Ссылка на Telegram Mini App"
|
||||
v-model="settings.items.telegram.mini_app_url"/>
|
||||
<ItemTgBotToken label="Telegram Bot Token" v-model="settings.items.telegram.bot_token"/>
|
||||
<ItemTgChatID label="Telegram ChatID" v-model="settings.items.telegram.chat_id"/>
|
||||
<ItemTgMessageTemplate
|
||||
label="Шаблон уведомления о новом заказе владельцу"
|
||||
v-model="settings.items.telegram.owner_notification_template"
|
||||
>
|
||||
Введите шаблон сообщения для Telegram-уведомлений о новом заказе владельцу магазина.
|
||||
</ItemTgMessageTemplate>
|
||||
<ItemTgMessageTemplate
|
||||
label="Шаблон уведомления о новом заказе покупателю"
|
||||
v-model="settings.items.telegram.customer_notification_template"
|
||||
>
|
||||
Введите шаблон сообщения для Telegram-уведомлений о новом заказе покупателю.
|
||||
</ItemTgMessageTemplate>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import ItemTgMiniAppLink from "@/components/Settings/ItemTgMiniAppLink.vue";
|
||||
import ItemTgBotToken from "@/components/Settings/ItemTgBotToken.vue";
|
||||
import ItemTgChatID from "@/components/Settings/ItemTgChatID.vue";
|
||||
import ItemTgMessageTemplate from "@/components/Settings/ItemTgMessageTemplate.vue";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
</script>
|
||||
21
frontend/admin/src/views/TextsView.vue
Normal file
21
frontend/admin/src/views/TextsView.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<ItemInput label="Текст в конце списка товаров" v-model="settings.items.texts.text_no_more_products">
|
||||
Текст, отображаемый в конце списка, когда больше нет доступных товаров.
|
||||
Покупатель дошел до конца списка.
|
||||
</ItemInput>
|
||||
|
||||
<ItemInput label="Текст пустой корзины" v-model="settings.items.texts.text_empty_cart">
|
||||
Текст, отображаемый на странице просмотра корзины, если в ней нет товаров.
|
||||
</ItemInput>
|
||||
|
||||
<ItemInput label="Текст для успешного заказа" v-model="settings.items.texts.text_order_created_success">
|
||||
Текст, отображаемый при успешном создании заказа.
|
||||
</ItemInput>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import ItemInput from "@/components/Settings/ItemInput.vue";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
</script>
|
||||
Reference in New Issue
Block a user