feat(slider): add slider feature

This commit is contained in:
2025-11-01 17:32:28 +03:00
parent 0cccc7e3d7
commit 3049bd3101
37 changed files with 685 additions and 256 deletions

View File

@@ -4,6 +4,8 @@ on:
push: push:
branches: branches:
- master - master
- 'issue/**'
- develop
permissions: permissions:
contents: write contents: write
@@ -35,7 +37,6 @@ jobs:
module-build: module-build:
name: Build module. name: Build module.
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [test]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2 - uses: oven-sh/setup-bun@v2
@@ -60,6 +61,7 @@ jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [test, module-build] needs: [test, module-build]
if: github.ref == 'refs/heads/master'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@@ -156,4 +158,16 @@ jobs:
} }
} }
} }
} }
deploy:
runs-on: ubuntu-latest
needs: [release]
if: github.ref == 'refs/heads/master'
steps:
- name: Trigger deployment
run: |
curl --fail-with-body -X POST "${{ secrets.SERVER_URL }}/index.php?route=github_deploy/deploy" \
-d "token=${{ secrets.OC_API_KEY }}"

View File

@@ -35,6 +35,7 @@ dev:
cd frontend/spa && bun run dev cd frontend/spa && bun run dev
dev-admin: dev-admin:
rm -rf module/oc_telegram_shop/upload/admin/view/javascript && \
$(MAKE) link && \ $(MAKE) link && \
cd frontend/admin && bun run dev cd frontend/admin && bun run dev
cd frontend/spa && bun run dev cd frontend/spa && bun run dev

View File

@@ -36,6 +36,7 @@ Telecart теперь поддерживает встроенную систем
• Добавлены автоматические тесты, запускаемые при каждом билде, что значительно снижает количество багов в новых версиях. • Добавлены автоматические тесты, запускаемые при каждом билде, что значительно снижает количество багов в новых версиях.
• Исправлены проблемы с поиском и оформлением заказов, обнаруженные на некоторых магазинах. • Исправлены проблемы с поиском и оформлением заказов, обнаруженные на некоторых магазинах.
• Повышена стабильность работы. • Повышена стабильность работы.
• Уменьшен размер архива с модулем примерно в 2 раза.
Купить модуль: https://liveopencart.ru/opencart-moduli-shablony/moduli/telecart Купить модуль: https://liveopencart.ru/opencart-moduli-shablony/moduli/telecart
Документация: https://telecart-labs.github.io/docs/ Документация: https://telecart-labs.github.io/docs/

View File

@@ -4,7 +4,10 @@
"": { "": {
"name": "admin", "name": "admin",
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.16",
"daisyui": "^5.4.2",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"tailwindcss": "^4.1.16",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.6.3", "vue-router": "^4.6.3",
}, },
@@ -282,6 +285,36 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg=="], "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.16", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.16" } }, "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.16", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.16", "@tailwindcss/oxide-darwin-arm64": "4.1.16", "@tailwindcss/oxide-darwin-x64": "4.1.16", "@tailwindcss/oxide-freebsd-x64": "4.1.16", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16", "@tailwindcss/oxide-linux-arm64-musl": "4.1.16", "@tailwindcss/oxide-linux-x64-gnu": "4.1.16", "@tailwindcss/oxide-linux-x64-musl": "4.1.16", "@tailwindcss/oxide-wasm32-wasi": "4.1.16", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16", "@tailwindcss/oxide-win32-x64-msvc": "4.1.16" } }, "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.16", "", { "os": "android", "cpu": "arm64" }, "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.16", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16", "", { "os": "linux", "cpu": "arm" }, "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.16", "", { "os": "linux", "cpu": "x64" }, "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.16", "", { "os": "linux", "cpu": "x64" }, "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.16", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.16", "", { "os": "win32", "cpu": "x64" }, "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.16", "", { "dependencies": { "@tailwindcss/node": "4.1.16", "@tailwindcss/oxide": "4.1.16", "tailwindcss": "4.1.16" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
@@ -372,6 +405,8 @@
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"daisyui": ["daisyui@5.4.2", "", {}, "sha512-yLoRFlx5hKvn5ODpT7CVb9oU/fAF2X1BGuLmVZo4LN33r7hcmO8v+gcxB6l33mcMas5jut3lZwHj9erqbMvvEA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
@@ -382,8 +417,12 @@
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"electron-to-chromium": ["electron-to-chromium@1.5.243", "", {}, "sha512-ZCphxFW3Q1TVhcgS9blfut1PX8lusVi2SvXQgmEEnK4TCmE1JhH2JkjJN+DNt0pJJwfBri5AROBnz2b/C+YU9g=="], "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=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="],
@@ -446,6 +485,8 @@
"globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
"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-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
@@ -470,6 +511,8 @@
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
@@ -494,6 +537,30 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
@@ -600,6 +667,10 @@
"synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="], "synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="],
"tailwindcss": ["tailwindcss@4.1.16", "", {}, "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
@@ -660,6 +731,18 @@
"@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], "@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.6.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.6.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@7.7.7", "", { "dependencies": { "@vue/devtools-shared": "^7.7.7", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA=="], "@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@7.7.7", "", { "dependencies": { "@vue/devtools-shared": "^7.7.7", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA=="],
"@vue/devtools-core/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], "@vue/devtools-core/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],

View File

@@ -16,7 +16,10 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.16",
"daisyui": "^5.4.2",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"tailwindcss": "^4.1.16",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.6.3" "vue-router": "^4.6.3"
}, },

View File

@@ -0,0 +1,21 @@
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme) prefix(tw);
@import "tailwindcss/utilities.css" layer(utilities) prefix(tw);
@plugin "daisyui" {
prefix: 'd-'
}
@layer components {
.tw\:d-toggle {
width: calc((var(--d-size) * 2) - (var(--border) + var(--d-toggle-p)) * 2) !important;
height: var(--d-size) !important;
border: var(--border) solid currentColor !important;
color: var(--d-input-color) !important;
border-radius: calc(var(--radius-selector) + min(var(--d-toggle-p), var(--radius-selector-max)) + min(var(--border), var(--radius-selector-max))) !important;
padding: var(--d-toggle-p) !important;
}
.tw\:d-toggle:after {
all: unset !important;
}
}

View File

@@ -1,82 +0,0 @@
<template>
<section>
<pre>{{ banners }}</pre>
<input type="text" name="module_tgshop_mainpage_banners" :value="JSON.stringify(banners)">
<table id="banners" class="table table-striped table-bordered table-hover">
<thead>
<tr>
<td class="text-left">Заголовок</td>
<td class="text-left">Ссылка</td>
<td class="text-center">Изображение</td>
<td>Действия</td>
</tr>
</thead>
<tbody>
<tr v-for="(banner, index) in banners">
<td class="text-left">
<input v-model="banner.title" type="text" placeholder="Заголовок слайда"
class="form-control"/>
</td>
<td class="text-left" style="width: 30%;">
<LinkSelector v-model="banner.link"/>
</td>
<td class="text-center">
<OcImagePIcker v-model="banner.image"/>
<div class="alert alert-info">
Минимальный размер: 370×200 <br>
Рекомендуется: 740×400 или больше, в тех же пропорциях (1.85:1) <br>
Картинка будет автоматически обрезана под нужный формат.
</div>
</td>
<td class="text-left">
<button type="button" class="btn btn-danger" @click="removeBanner(index)">
<i class="fa fa-minus-circle"></i>
</button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3"></td>
<td class="text-left">
<button @click="addBanner" type="button" class="btn btn-primary">
<i class="fa fa-plus-circle"></i>
</button>
</td>
</tr>
</tfoot>
</table>
</section>
</template>
<script setup>
import {onMounted, ref} from "vue";
import OcImagePIcker from "@/components/OcImagePIcker.vue";
import LinkSelector from "@/components/Banners/LinkSelector.vue";
const banners = ref([]);
function removeBanner(index) {
banners.value.splice(index, 1);
}
function addBanner() {
banners.value.push({
title: '',
link: {
type: 'none',
value: null,
},
image: '',
});
}
onMounted(() => {
banners.value = JSON.parse(window.TeleCart.banners || '[]');
});
</script>
<style scoped>
</style>

View File

@@ -1,9 +1,15 @@
<template> <template>
<div> <div class="oc-image">
<a href="#" data-toggle="image" class="img-thumbnail" :id="`thumb-image-${id}`"> <div v-if="isLoaded === false" class="loader">
<img :src="thumb" <i class="fa fa-spinner fa-spin"></i>
</div>
<a v-show="isLoaded" href="#" data-toggle="image" class="img-thumbnail" :id="`thumb-image-${id}`">
<img
:src="thumb"
data-placeholder="/image/cache/no_image-100x100.png" data-placeholder="/image/cache/no_image-100x100.png"
alt="Image" alt="Image"
@load="isLoaded = true"
> >
</a> </a>
<input ref="inputRef" type="hidden" value="" :id="`input-image-${id}`"> <input ref="inputRef" type="hidden" value="" :id="`input-image-${id}`">
@@ -17,6 +23,7 @@ const id = useId();
const model = defineModel(); const model = defineModel();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const inputRef = ref(null); const inputRef = ref(null);
const isLoaded = ref(false);
const thumb = computed(() => { const thumb = computed(() => {
if (!model.value) return '/image/cache/no_image-100x100.png'; if (!model.value) return '/image/cache/no_image-100x100.png';
@@ -39,3 +46,18 @@ onMounted(() => {
observer.observe(input, {attributes: true, attributeFilter: ['value']}); observer.observe(input, {attributes: true, attributeFilter: ['value']});
}); });
</script> </script>
<style scoped>
.oc-image {
display: flex;
justify-content: center;
align-items: center;
}
.loader {
width: 100px;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<div class="form-group">
<label class="col-sm-2 control-label" for="module_tgshop_status">
{{ label }}
</label>
<div class="col-sm-10">
<slot name="default"></slot>
<div class="help-block">
<slot name="help"></slot>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
label: {
type: String,
default: '',
},
});
</script>
<style scoped lang="scss">
</style>

View File

@@ -3,7 +3,7 @@
<input <input
type="search" type="search"
name="category" name="category"
:value="`${category?.name}`" :value="`${category?.name || ''}`"
placeholder="Начните вводить название категории..." placeholder="Начните вводить название категории..."
class="form-control" class="form-control"
ref="categoryRef" ref="categoryRef"

View File

@@ -34,18 +34,16 @@
</template> </template>
<script setup> <script setup>
import CategorySelect from "@/components/Banners/CategorySelect.vue"; import CategorySelect from "@/components/Slider/CategorySelect.vue";
import ProductSelect from "@/components/Banners/ProductSelect.vue"; import ProductSelect from "@/components/Slider/ProductSelect.vue";
const link = defineModel(); const link = defineModel();
function setLink(value) { function setLink(value) {
if (Object.is(link.value)) { if (link.value?.value) {
link.value.value.url = value; link.value.value.url = value;
} else { } else {
link.value.value = { link.value.value = { url: value };
url: value,
};
} }
} }
</script> </script>

View File

@@ -2,7 +2,7 @@
<div> <div>
<input <input
type="search" type="search"
:value="`${model?.name}`" :value="`${model?.name || ''}`"
placeholder="Начните вводить название товара..." placeholder="Начните вводить название товара..."
class="form-control" class="form-control"
ref="inputRef" ref="inputRef"

View File

@@ -0,0 +1,186 @@
<template>
<input type="hidden" name="module_tgshop_mainpage_slider" :value="JSON.stringify(slider)">
<div class="alert alert-info">
<p>Здесь настраивается слайдер, который выводится на главной странице.</p>
<p>Рекомендуемые размеры изображений: <span class="text-bold">370×200px</span>, <span
class="text-bold">740×400px</span>,
<span class="text-bold">1110×600px</span> либо другие, в тех же пропорциях (1.85:1)<br>
Изображение будет автоматически обрезана под нужный формат. <br>
Заголовок можно оставить пустым, но рекомендуется заполнить для корректной работы целей
Яндекс.Метрики.</p>
</div>
<section>
<SettingsItem label="Статус">
<template #default>
<Switcher v-model="slider.is_enabled"/>
</template>
<template #help>
Показывать слайдер на главной странице.
Для отображения слайдера нужно добавить минимум 1 слайд.
</template>
</SettingsItem>
<SettingsItem label="Эффект смены слайдов">
<template #default>
<select v-model="slider.effect" class="form-control">
<option value="slide">Слайд</option>
<option value="flip">Переворот</option>
<option value="cards">Карточки</option>
<option value="cube">Куб</option>
<option value="coverflow">Перекрывающиеся слайды</option>
</select>
</template>
</SettingsItem>
<SettingsItem label="Пагинация">
<template #default>
<Switcher v-model="slider.pagination"/>
</template>
<template #help>
Показывать точки под слайдером для индикации текущего слайда.
</template>
</SettingsItem>
<SettingsItem label="Полоса прокрутки">
<template #default>
<Switcher v-model="slider.scrollbar"/>
</template>
<template #help>
Показывать полосу прокрутки под слайдером для навигации между слайдами.
</template>
</SettingsItem>
<SettingsItem label="Расстояние между слайдами">
<template #default>
<div class="tw:max-w-2xl">
<div class="input-group">
<input
v-model="slider.space_between"
type="number"
min="0"
max="100"
class="form-control"
placeholder="30"
/>
<span class="input-group-addon">px</span>
</div>
</div>
</template>
<template #help>
Расстояние между слайдами в пикселях. По умолчанию - 30.
</template>
</SettingsItem>
<SettingsItem label="Свободный режим">
<template #default>
<Switcher v-model="slider.free_mode"/>
</template>
<template #help>
Позволяет свободно прокручивать слайды без привязки к конкретным позициям.
</template>
</SettingsItem>
<SettingsItem label="Бесконечная прокрутка">
<template #default>
<Switcher v-model="slider.loop"/>
</template>
<template #help>
Включите этот режим, чтобы после последнего слайда слайдер продолжал прокрутку с первого, создавая бесконечный цикл.
</template>
</SettingsItem>
<SettingsItem label="Автоматическая прокрутка">
<template #default>
<Switcher v-model="slider.autoplay"/>
</template>
<template #help>
Слайдер будет автоматически листать изображения каждые 3 секунды
</template>
</SettingsItem>
</section>
<section>
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<td class="text-left">Заголовок</td>
<td class="text-left">Ссылка</td>
<td class="text-center">Изображение</td>
<td>Действия</td>
</tr>
</thead>
<tbody>
<tr v-for="(slide, index) in slider.slides">
<td class="text-left">
<input v-model="slide.title" type="text" placeholder="Заголовок слайда"
class="form-control"/>
</td>
<td class="text-left" style="width: 30%;">
<LinkSelector v-model="slide.link"/>
</td>
<td class="text-center">
<OcImagePicker v-model="slide.image"/>
</td>
<td class="text-left">
<button type="button" class="btn btn-danger" @click="removeSlide(index)">
<i class="fa fa-minus-circle"></i>
</button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3"></td>
<td class="text-left">
<button @click="addSlide" type="button" class="btn btn-primary">
<i class="fa fa-plus-circle"></i>
</button>
</td>
</tr>
</tfoot>
</table>
</section>
</template>
<script setup>
import {onMounted, ref} from "vue";
import OcImagePIcker from "@/components/OcImagePIcker.vue";
import LinkSelector from "@/components/Slider/LinkSelector.vue";
import SettingsItem from "@/components/SettingsItem.vue";
import Switcher from "@/components/Switcher.vue";
const slider = ref({});
function removeSlide(index) {
slider.value.slides.splice(index, 1);
}
function addSlide() {
slider.value.slides.push({
title: '',
link: {
type: 'none',
value: null,
},
image: '',
});
}
onMounted(() => {
slider.value = JSON.parse(window.TeleCart.mainpage_slider);
});
</script>
<style scoped>
.text-bold {
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,28 @@
<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>
</template>
<script setup>
const model = defineModel({
default: false,
});
</script>
<style scoped lang="scss">
</style>

View File

@@ -1,14 +1,20 @@
import './assets/main.css' import './assets/main.css'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
const app = createApp(App) function onReady(fn) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fn);
} else {
fn();
}
}
app.use(createPinia()) onReady(() => {
app.use(router) const app = createApp(App);
app.use(createPinia());
app.mount('#app') app.use(router);
app.mount('#app');
});

View File

@@ -1,7 +1,7 @@
<template> <template>
<Banners/> <Slider/>
</template> </template>
<script setup> <script setup>
import Banners from "@/components/Banners/Banners.vue"; import Slider from "@/components/Slider/Slider.vue";
</script> </script>

View File

@@ -1,14 +1,16 @@
import { fileURLToPath, URL } from 'node:url' import {fileURLToPath, URL} from 'node:url';
import { defineConfig } from 'vite' import {defineConfig} from 'vite';
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue';
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools';
import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
vueDevTools(), vueDevTools(),
tailwindcss(),
], ],
resolve: { resolve: {
alias: { alias: {
@@ -19,11 +21,11 @@ export default defineConfig({
build: { build: {
manifest: true, manifest: true,
sourcemap: true, sourcemap: true,
outDir: '../modules/oc_telegram_shop/upload/admin/view/javascript', outDir: '../../module/oc_telegram_shop/upload/admin/view/javascript/telecart',
emptyOutDir: true, // also necessary emptyOutDir: true, // also necessary
rollupOptions: { rollupOptions: {
input: { input: {
bulk_products: '/src/main.js', telecart: '/src/main.js',
}, },
output: { output: {
entryFileNames: `[name].js`, entryFileNames: `[name].js`,
@@ -43,4 +45,4 @@ export default defineConfig({
protocol: 'ws', protocol: 'ws',
}, },
}, },
}) });

View File

@@ -1,100 +0,0 @@
<template>
<div v-if="slides.length > 0" class="app-banner px-4">
<Swiper
class="select-none"
:slides-per-view="1"
:space-between="50"
pagination
:pagination="{ clickable: true }"
@swiper="onSwiper"
@slideChange="onSlideChange"
>
<SwiperSlide v-for="slide in slides" :key="slide.id">
<RouterLink
v-if="slide?.link?.type === 'category'"
:to="{name: 'product.categories.show', params: {category_id: slide.link.value.category_id}}"
@click="sliderClick(slide)"
>
<img :src="slide.image" :alt="slide.title">
</RouterLink>
<RouterLink
v-else-if="slide?.link?.type === 'product'"
:to="{name: 'product.show', params: {id: slide.link.value.product_id}}"
@click="sliderClick(slide)"
>
<img :src="slide.image" :alt="slide.title">
</RouterLink>
<img
v-else-if="slide?.link?.type === 'url'"
:src="slide.image"
:alt="slide.title"
@click="openExternalLink(slide.link.value.url, slide)"
>
<img v-else :src="slide.image" :alt="slide.title"/>
</SwiperSlide>
</Swiper>
</div>
</template>
<script setup>
import {Swiper, SwiperSlide} from 'swiper/vue';
import 'swiper/css';
import 'swiper/css/navigation';
import {onMounted, ref} from "vue";
import {fetchBanner} from "@/utils/ftch.js";
import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
const yaMetrika = useYaMetrikaStore();
const slides = ref([]);
const onSwiper = (swiper) => {
console.log(swiper);
};
const onSlideChange = () => {
console.log('slide change');
};
function sliderClick(slide) {
yaMetrika.reachGoal(YA_METRIKA_GOAL.SLIDER_HOME_CLICK, {
banner: slide.title,
});
}
function openExternalLink(link, slide) {
if (! link) {
return;
}
yaMetrika.reachGoal(YA_METRIKA_GOAL.SLIDER_HOME_CLICK, {
banner: slide.title,
});
window.Telegram.WebApp.openLink(link, {try_instant_view: false});
}
onMounted(async () => {
const response = await fetchBanner();
slides.value = response.data;
})
</script>
<style>
.app-banner .swiper-horizontal > .swiper-pagination-bullets {
position: relative;
bottom: 10px;
}
.app-banner .swiper-horizontal .swiper-slide {
display: flex;
align-items: center;
justify-content: center;
}
.app-banner .swiper-horizontal .swiper-slide img {
border-radius: var(--radius-box);
}
</style>

View File

@@ -0,0 +1,180 @@
<template>
<div
v-if="sliders.mainpage_slider.is_enabled && sliders.mainpage_slider.slides.length > 0"
class="app-banner"
:class="classList"
>
<Swiper
:effect="slideEffect"
class="select-none"
:slides-per-view="1"
:space-between="sliders.mainpage_slider.space_between"
:pagination="pagination"
:lazy="true"
:modules="modules"
:scrollbar="scrollbar"
:free-mode="sliders.mainpage_slider.free_mode"
:loop="sliders.mainpage_slider.loop"
:autoplay="autoplay"
@swiper="onSwiper"
@slideChange="onSlideChange"
>
<SwiperSlide v-for="slide in sliders.mainpage_slider.slides" :key="slide.id">
<RouterLink
v-if="slide?.link?.type === 'category'"
:to="{name: 'product.categories.show', params: {category_id: slide.link.value.category_id}}"
@click="sliderClick(slide)"
>
<img :src="slide.image" :alt="slide.title" loading="lazy">
</RouterLink>
<RouterLink
v-else-if="slide?.link?.type === 'product'"
:to="{name: 'product.show', params: {id: slide.link.value.product_id}}"
@click="sliderClick(slide)"
>
<img :src="slide.image" :alt="slide.title" loading="lazy">
</RouterLink>
<img
v-else-if="slide?.link?.type === 'url'"
:src="slide.image"
:alt="slide.title"
loading="lazy"
@click="openExternalLink(slide.link.value.url, slide)"
>
<img v-else :src="slide.image" :alt="slide.title" loading="lazy"/>
</SwiperSlide>
</Swiper>
</div>
</template>
<script setup>
import {Swiper, SwiperSlide} from 'swiper/vue';
import 'swiper/css';
import 'swiper/css/navigation';
import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
import {EffectCoverflow, EffectCards, EffectCube, EffectFlip, Scrollbar, Autoplay} from 'swiper/modules';
import {computed, onMounted} from "vue";
import {useSlidersStore} from "@/stores/SlidersStore.js";
const sliders = useSlidersStore();
const yaMetrika = useYaMetrikaStore();
const modules = [
Autoplay,
EffectCards,
EffectFlip,
EffectCube,
Scrollbar,
EffectCoverflow,
];
const classList = computed(() => {
if (sliders.mainpage_slider.effect === 'cards') {
return ['px-8'];
}
if (sliders.mainpage_slider.effect === 'flip') {
return ['px-4', 'pb-4', 'pt-4'];
}
if (sliders.mainpage_slider.effect === 'cube') {
return ['px-4', 'pb-10'];
}
return ['px-4'];
});
const onSwiper = (swiper) => {
console.log(swiper);
};
const onSlideChange = () => {
console.log('slide change');
};
const slideEffect = computed(() => {
if (sliders.mainpage_slider.effect === 'slide') {
return null;
}
return sliders.mainpage_slider.effect;
});
const pagination = computed(() => {
if (sliders.mainpage_slider.pagination) {
return {
clickable: true, dynamicBullets: false,
};
}
return false;
});
const scrollbar = computed(() => {
if (sliders.mainpage_slider.scrollbar) {
return {
hide: true,
};
}
return false;
});
const autoplay = computed(() => {
if (sliders.mainpage_slider.autoplay) {
return {
delay: 3000,
reverseDirection: false,
};
}
return false;
});
function sliderClick(slide) {
yaMetrika.reachGoal(YA_METRIKA_GOAL.SLIDER_HOME_CLICK, {
banner: slide.title,
});
}
function openExternalLink(link, slide) {
if (!link) {
return;
}
yaMetrika.reachGoal(YA_METRIKA_GOAL.SLIDER_HOME_CLICK, {
banner: slide.title,
});
window.Telegram.WebApp.openLink(link, {try_instant_view: false});
}
onMounted(() => {
console.debug('[Mainpage Slider] Status: ', sliders.mainpage_slider);
});
</script>
<style>
.app-banner {
aspect-ratio: 740 / 400;
}
.app-banner .swiper {
overflow: visible;
}
.app-banner .swiper-horizontal > .swiper-pagination-bullets {
position: relative;
bottom: 10px;
}
.app-banner .swiper-horizontal .swiper-slide {
display: flex;
align-items: center;
justify-content: center;
}
.app-banner .swiper-horizontal .swiper-slide img {
border-radius: var(--radius-box);
}
</style>

View File

@@ -16,6 +16,7 @@ import 'swiper/element/bundle';
import 'swiper/css/bundle'; import 'swiper/css/bundle';
import AppLoading from "@/AppLoading.vue"; import AppLoading from "@/AppLoading.vue";
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js"; import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
import {useSlidersStore} from "@/stores/SlidersStore.js";
register(); register();
const pinia = createPinia(); const pinia = createPinia();
@@ -26,6 +27,7 @@ app
.use(VueTelegramPlugin); .use(VueTelegramPlugin);
const settings = useSettingsStore(); const settings = useSettingsStore();
useSlidersStore().fetchMainpageSlider();
const appLoading = createApp(AppLoading); const appLoading = createApp(AppLoading);
appLoading.mount('#app'); appLoading.mount('#app');

View File

@@ -0,0 +1,26 @@
import {defineStore} from "pinia";
import {fetchBanner} from "@/utils/ftch.js";
export const useSlidersStore = defineStore('sliders', {
state: () => ({
mainpage_slider: {
is_enabled: false,
space_between: 30,
autoplay: false,
effect: 'cube', // null, flip, cards, cube
pagination: false,
scrollbar: false,
free_mode: false,
loop: false,
slides: [],
},
}),
actions: {
async fetchMainpageSlider() {
console.debug('[Sliders Store] Fetch mainpage slider from server.');
const response = await fetchBanner();
this.mainpage_slider = Object.assign({}, this.mainpage_slider, response.data);
}
},
});

View File

@@ -6,6 +6,7 @@
html, body, #app { html, body, #app {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow-x: hidden;
} }
html { html {

View File

@@ -2,7 +2,9 @@
<div ref="goodsRef" class="pb-20"> <div ref="goodsRef" class="pb-20">
<CategoriesInline/> <CategoriesInline/>
<Banner/> <div class="overflow-hidden">
<MainpageSlider/>
</div>
<div class="px-5 fixed z-50 w-full opacity-90" style="bottom: calc(var(--tg-safe-area-inset-bottom, 0px) + 80px);"> <div class="px-5 fixed z-50 w-full opacity-90" style="bottom: calc(var(--tg-safe-area-inset-bottom, 0px) + 80px);">
<div class="flex justify-center"> <div class="flex justify-center">
@@ -31,12 +33,12 @@
<script setup> <script setup>
import ProductsList from "@/components/ProductsList.vue"; import ProductsList from "@/components/ProductsList.vue";
import CategoriesInline from "../components/CategoriesInline.vue"; import CategoriesInline from "../components/CategoriesInline.vue";
import {nextTick, onActivated, onMounted, ref, toRaw} from "vue"; import {onActivated, onMounted, ref, toRaw} from "vue";
import IconFunnel from "@/components/Icons/IconFunnel.vue"; import IconFunnel from "@/components/Icons/IconFunnel.vue";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import ftch from "@/utils/ftch.js"; import ftch from "@/utils/ftch.js";
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js"; import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
import Banner from "@/components/Banner.vue"; import MainpageSlider from "@/components/MainpageSlider.vue";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js"; import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js"; import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js";
import {useSettingsStore} from "@/stores/SettingsStore.js"; import {useSettingsStore} from "@/stores/SettingsStore.js";

View File

@@ -17,7 +17,7 @@ export default defineConfig({
base: '/image/catalog/tgshopspa/', base: '/image/catalog/tgshopspa/',
build: { build: {
outDir: '../module/oc_telegram_shop/upload/image/catalog/tgshopspa', outDir: '../../module/oc_telegram_shop/upload/image/catalog/tgshopspa',
emptyOutDir: true, emptyOutDir: true,
sourcemap: true, sourcemap: true,
manifest: true, manifest: true,

View File

@@ -93,6 +93,7 @@ class ControllerExtensionModuleTgshop extends Controller
$hasConfig = $this->config->get('module_tgshop_app_name') !== null; $hasConfig = $this->config->get('module_tgshop_app_name') !== null;
if ($hasConfig) { if ($hasConfig) {
$this->cleanUpConfigs();
$this->updateConfigFromDefaults(); $this->updateConfigFromDefaults();
$this->cleanUpOldAssets(); $this->cleanUpOldAssets();
$this->injectVueJs(); $this->injectVueJs();
@@ -109,9 +110,9 @@ class ControllerExtensionModuleTgshop extends Controller
if (($this->request->server['REQUEST_METHOD'] === 'POST') && $this->validate()) { if (($this->request->server['REQUEST_METHOD'] === 'POST') && $this->validate()) {
$postData = $this->request->post; $postData = $this->request->post;
$postData['module_tgshop_mainpage_banners'] = []; $postData['module_tgshop_mainpage_slider'] = [];
if (! empty($_POST['module_tgshop_mainpage_banners'])) { if (! empty($_POST['module_tgshop_mainpage_slider'])) {
$postData['module_tgshop_mainpage_banners'] = $_POST['module_tgshop_mainpage_banners']; $postData['module_tgshop_mainpage_slider'] = $_POST['module_tgshop_mainpage_slider'];
} }
$this->model_setting_setting->editSetting('module_tgshop', $postData); $this->model_setting_setting->editSetting('module_tgshop', $postData);
@@ -136,11 +137,11 @@ class ControllerExtensionModuleTgshop extends Controller
$data['settings'] = $this->getSettingsConfig(); $data['settings'] = $this->getSettingsConfig();
$data['banners'] = []; $data['mainpage_slider'] = [];
$banners = $this->config->get('module_tgshop_mainpage_banners'); $banners = $this->config->get('module_tgshop_mainpage_slider');
if ($banners) { if ($banners) {
$banners = html_entity_decode($banners); $banners = html_entity_decode($banners);
$data['banners'] = $banners; $data['mainpage_slider'] = $banners;
} }
foreach ($data['settings'] as $configs) { foreach ($data['settings'] as $configs) {
@@ -364,11 +365,20 @@ TEXT,
'module_tgshop_enable_store' => 1, 'module_tgshop_enable_store' => 1,
'module_tgshop_feature_coupons' => 0, 'module_tgshop_feature_coupons' => 0,
'module_tgshop_feature_vouchers' => 0, 'module_tgshop_feature_vouchers' => 0,
'module_tgshop_home_banner_id' => null,
'module_tgshop_text_no_more_products' => 'Это всё по текущему запросу. Попробуйте уточнить фильтры или поиск.', 'module_tgshop_text_no_more_products' => 'Это всё по текущему запросу. Попробуйте уточнить фильтры или поиск.',
'module_tgshop_text_empty_cart' => 'Ваша корзина пуста', 'module_tgshop_text_empty_cart' => 'Ваша корзина пуста',
'module_tgshop_text_order_created_success' => 'Ваш заказ успешно оформлен и будет обработан в ближайшее время.', 'module_tgshop_text_order_created_success' => 'Ваш заказ успешно оформлен и будет обработан в ближайшее время.',
'module_tgshop_mainpage_banners' => '[]', 'module_tgshop_mainpage_slider' => json_encode([
'is_enabled' => false,
'effect' => 'slide',
'pagination' => true,
'scrollbar' => false,
'free_mode' => false,
'space_between' => 30,
'autoplay' => false,
'loop' => false,
'slides' => [],
], JSON_THROW_ON_ERROR),
]; ];
} }
@@ -384,11 +394,6 @@ TEXT,
'user_token=' . $this->session->data['user_token'], 'user_token=' . $this->session->data['user_token'],
true true
); );
$ocBannersLink = $this->url->link(
'design/banner',
'user_token=' . $this->session->data['user_token'],
true
);
return [ return [
'general' => [ 'general' => [
@@ -698,28 +703,22 @@ HTML,
} }
} }
private function getBannersList(): array
{
$this->load->model('design/banner');
$allBanners = $this->model_design_banner->getBanners();
$map = [];
foreach ($allBanners as $item) {
$map[(int) $item['banner_id']] = $item['name'];
}
return [null => 'Не показывать'] + $map;
}
private function injectVueJs(): void private function injectVueJs(): void
{ {
$appDir = rtrim(DIR_APPLICATION, '/'); $appDir = rtrim(DIR_APPLICATION, '/');
if (file_exists("$appDir/view/javascript/telecart/telecart.js")) { if (file_exists("$appDir/view/javascript/telecart/telecart.js")) {
$this->document->addScript('view/javascript/telecart/telecart.js'); $hash = md5(file_get_contents($appDir . '/view/javascript/telecart/manifest.json'));
$this->document->addStyle('view/javascript/telecart/telecart.css'); $this->document->addScript('view/javascript/telecart/telecart.js?v=' . $hash);
$this->document->addStyle('view/javascript/telecart/telecart.css?v=' . $hash);
} elseif (file_exists("$appDir/view/integration.js")) { } elseif (file_exists("$appDir/view/integration.js")) {
$this->document->addScript('view/integration.js'); $this->document->addScript('view/integration.js');
} else { } else {
throw new RuntimeException('Unable to load Vuejs frontend.'); throw new RuntimeException('Unable to load Vuejs frontend.');
} }
} }
private function cleanUpConfigs(): void
{
//
}
} }

View File

@@ -452,7 +452,7 @@
<script> <script>
window.TeleCart = { window.TeleCart = {
user_token: '{{ user_token }}', user_token: '{{ user_token }}',
banners: '{{ banners }}', mainpage_slider: '{{ mainpage_slider }}',
}; };
</script> </script>
<div id="app">App Loading...</div> <div id="app">App Loading...</div>

View File

@@ -94,7 +94,7 @@ class ControllerExtensionTgshopHandle extends Controller
$this->config->get('module_tgshop_feature_vouchers'), $this->config->get('module_tgshop_feature_vouchers'),
FILTER_VALIDATE_BOOLEAN FILTER_VALIDATE_BOOLEAN
), ),
'mainpage_banners' => $this->safeJsonDecode($this->config->get('module_tgshop_mainpage_banners'), []), 'mainpage_slider' => $this->safeJsonDecode($this->config->get('module_tgshop_mainpage_slider'), []),
'texts' => [ 'texts' => [
'no_more_products' => $this->config->get('module_tgshop_text_no_more_products'), 'no_more_products' => $this->config->get('module_tgshop_text_no_more_products'),
'empty_cart' => $this->config->get('module_tgshop_text_empty_cart'), 'empty_cart' => $this->config->get('module_tgshop_text_empty_cart'),

View File

@@ -111,7 +111,7 @@ class ImageTool implements ImageToolInterface
$image = $this->manager->make($fullOldPath) $image = $this->manager->make($fullOldPath)
->fit($width, $height, function ($constraint) { ->fit($width, $height, function ($constraint) {
$constraint->upsize(); // $constraint->upsize();
}, $position); }, $position);
$image->encode($format, 75)->save($fullNewPath, 75, $format); $image->encode($format, 75)->save($fullNewPath, 75, $format);

View File

@@ -24,13 +24,12 @@ class BannerHandler
public function show(): JsonResponse public function show(): JsonResponse
{ {
$data = []; $slider = $this->settings->get('mainpage_slider', []);
$slides = $this->settings->get('mainpage_banners', []);
if ($slides) { if ($slider && ! empty($slider['slides']) && is_array($slider['slides'])) {
foreach ($slides as $index => $slide) { foreach ($slider['slides'] as $index => $slide) {
if (is_file(DIR_IMAGE . $slide['image'])) { if (is_file(DIR_IMAGE . $slide['image'])) {
$data[] = [ $slider['slides'][$index] = [
'id' => $index, 'id' => $index,
'title' => $slide['title'], 'title' => $slide['title'],
'link' => $slide['link'], 'link' => $slide['link'],
@@ -41,7 +40,7 @@ class BannerHandler
} }
return new JsonResponse([ return new JsonResponse([
'data' => $data, 'data' => $slider,
]); ]);
} }
} }

View File

@@ -58,6 +58,7 @@ class SettingsHandler
'feature_vouchers' => $this->settings->get('feature_vouchers') ?? false, 'feature_vouchers' => $this->settings->get('feature_vouchers') ?? false,
'currency_code' => $this->settings->get('oc_default_currency', 'RUB'), 'currency_code' => $this->settings->get('oc_default_currency', 'RUB'),
'texts' => $this->settings->get('texts'), 'texts' => $this->settings->get('texts'),
'mainpage_slider' => $this->settings->get('mainpage_slider'),
]); ]);
} }

View File

0
rebuildgaa Normal file
View File

View File

@@ -29,7 +29,7 @@ if [ -z "$SRC_PATH" ]; then
exit 1 exit 1
fi fi
echo "Build Telecart Frontend..." echo "Build Telecart SPA Frontend..."
cd "${SRC_PATH}/frontend/spa" cd "${SRC_PATH}/frontend/spa"
bun install bun install
bun run build bun run build
@@ -43,9 +43,18 @@ bun install
bun run build bun run build
echo "Move manifest file" echo "Move manifest file"
cp "${SRC_PATH}/module/oc_telegram_shop/upload/admin/view/javascript/.vite/manifest.json" "${SRC_PATH}/module/oc_telegram_shop/upload/admin/view/javascript/manifest.json" cp "${SRC_PATH}/module/oc_telegram_shop/upload/admin/view/javascript/telecart/.vite/manifest.json" \
"${SRC_PATH}/module/oc_telegram_shop/upload/admin/view/javascript/telecart/manifest.json"
cd - > /dev/null
cd "$SRC_PATH"
echo "Cleanup frontend"
rm -v module/oc_telegram_shop/upload/image/catalog/tgshopspa/vite.svg
rm -v module/oc_telegram_shop/upload/image/catalog/tgshopspa/assets/*.map
rm -v module/oc_telegram_shop/upload/admin/view/integration.js
rm -v module/oc_telegram_shop/upload/admin/view/javascript/telecart/telecart.js.map
rm -v module/oc_telegram_shop/upload/admin/view/javascript/telecart/favicon.ico
echo "Install Composer dependencies." echo "Install Composer dependencies."
cd "${SRC_PATH}/module/oc_telegram_shop/upload/oc_telegram_shop" cd "${SRC_PATH}/module/oc_telegram_shop/upload/oc_telegram_shop"
@@ -55,7 +64,8 @@ composer install \
--optimize-autoloader \ --optimize-autoloader \
--no-interaction \ --no-interaction \
--no-cache --no-cache
cd - > /dev/null
cd "$SRC_PATH"
echo "Copy .env for production" echo "Copy .env for production"
cp "${SRC_PATH}/module/oc_telegram_shop/upload/oc_telegram_shop/.env.production" \ cp "${SRC_PATH}/module/oc_telegram_shop/upload/oc_telegram_shop/.env.production" \

View File

@@ -27,7 +27,7 @@ docker compose exec web bash -c '\
--username admin \ --username admin \
--password admin \ --password admin \
--email youremail@example.com \ --email youremail@example.com \
--http_server http://localhost:8000/; exit $?' --http_server https://api.tg.nikitakiselev.ru/; exit $?'
docker compose exec web bash -c "cd /web; composer update" docker compose exec web bash -c "cd /web; composer update"
echo "Moving storage folder outside the system." echo "Moving storage folder outside the system."