modified: begushiybashkir/bbvue/package-lock.json
modified: begushiybashkir/bbvue/package.json modified: begushiybashkir/bbvue/src/main.js modified: begushiybashkir/bbvue/src/router/index.js new file: begushiybashkir/bbvue/src/stores/auth.js new file: begushiybashkir/bbvue/src/stores/user.js modified: begushiybashkir/bbvue/src/views/Login.vue modified: begushiybashkir/bbvue/src/views/Profile.vue new file: begushiybashkir/bbvue/src/views/ProfileEdit.vue modified: begushiybashkir/bbvue/src/views/Register.vue modified: serv_nginx/api_bb/bin/bb_api modified: serv_nginx/api_bb/internal/handlers/auth.go add axios, pinia store for user, auth, editProfile page
This commit is contained in:
Generated
+282
-2
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "bbvue",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.13",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bbvue",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.13",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.5.1"
|
||||
@@ -1939,6 +1940,23 @@
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -2033,6 +2051,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -2101,6 +2132,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -2232,6 +2275,29 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.227",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz",
|
||||
@@ -2261,6 +2327,51 @@
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
|
||||
@@ -2702,6 +2813,42 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -2717,6 +2864,15 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
@@ -2727,6 +2883,43 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
|
||||
@@ -2770,6 +2963,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -2780,6 +2985,45 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hookable": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||
@@ -3105,6 +3349,36 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -3488,6 +3762,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.5.1"
|
||||
|
||||
@@ -5,10 +5,47 @@ import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import axios from 'axios'
|
||||
|
||||
// Глобальная конфигурация axios
|
||||
axios.defaults.baseURL = 'https://begushiybashkir.ru/api/v1'
|
||||
axios.defaults.withCredentials = true // Для работы с куками
|
||||
|
||||
// Интерцептор для автоматического добавления токена
|
||||
axios.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('auth_token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// Интерцептор для обработки ошибок авторизации
|
||||
axios.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Токен истек или невалиден
|
||||
const authStore = useAuthStore()
|
||||
authStore.clearToken()
|
||||
authStore.clearUser()
|
||||
window.location.href = '/login'
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
// Инициализация auth store после создания app
|
||||
import { useAuthStore } from './stores/auth'
|
||||
const authStore = useAuthStore()
|
||||
authStore.initializeAuth()
|
||||
|
||||
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -4,62 +4,68 @@ import Home from '../views/Home.vue'
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
component: () => import('../views/About.vue')
|
||||
},
|
||||
{
|
||||
path: '/achievements',
|
||||
name: 'Achievements',
|
||||
component: () => import('../views/Achievements.vue')
|
||||
},
|
||||
{
|
||||
path: '/gallery',
|
||||
name: 'Gallery',
|
||||
component: () => import('../views/Gallery.vue')
|
||||
},
|
||||
{
|
||||
path: '/training',
|
||||
name: 'Training',
|
||||
component: () => import('../views/Training.vue')
|
||||
},
|
||||
{
|
||||
path: '/news',
|
||||
name: 'News',
|
||||
component: () => import('../views/News.vue')
|
||||
},
|
||||
{
|
||||
path: '/members',
|
||||
name: 'Members',
|
||||
component: () => import('../views/Members.vue')
|
||||
},
|
||||
{
|
||||
path: '/reviews',
|
||||
name: 'Reviews',
|
||||
component: () => import('../views/Reviews.vue')
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('../views/Login.vue')
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'Profile',
|
||||
component: () => import('../views/Profile.vue')
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
component: () => import('../views/Register.vue')
|
||||
}
|
||||
]
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
component: () => import('../views/About.vue')
|
||||
},
|
||||
{
|
||||
path: '/achievements',
|
||||
name: 'Achievements',
|
||||
component: () => import('../views/Achievements.vue')
|
||||
},
|
||||
{
|
||||
path: '/gallery',
|
||||
name: 'Gallery',
|
||||
component: () => import('../views/Gallery.vue')
|
||||
},
|
||||
{
|
||||
path: '/training',
|
||||
name: 'Training',
|
||||
component: () => import('../views/Training.vue')
|
||||
},
|
||||
{
|
||||
path: '/news',
|
||||
name: 'News',
|
||||
component: () => import('../views/News.vue')
|
||||
},
|
||||
{
|
||||
path: '/members',
|
||||
name: 'Members',
|
||||
component: () => import('../views/Members.vue')
|
||||
},
|
||||
{
|
||||
path: '/reviews',
|
||||
name: 'Reviews',
|
||||
component: () => import('../views/Reviews.vue')
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('../views/Login.vue')
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'Profile',
|
||||
component: () => import('../views/Profile.vue')
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
component: () => import('../views/Register.vue')
|
||||
},
|
||||
{
|
||||
path: '/profile/edit',
|
||||
name: 'ProfileEdit',
|
||||
component: () => import('../views/ProfileEdit.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
const API_BASE_URL = 'https://begushiybashkir.ru/api/v1/auth'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref(null)
|
||||
const token = ref(localStorage.getItem('auth_token') || '')
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
// Computed свойства
|
||||
const isAuthenticated = computed(() => !!token.value && !!user.value)
|
||||
const userFullName = computed(() => {
|
||||
if (!user.value) return ''
|
||||
return `${user.value.firstName} ${user.value.lastName}`
|
||||
})
|
||||
|
||||
// Установка токена
|
||||
const setToken = (newToken) => {
|
||||
token.value = newToken
|
||||
localStorage.setItem('auth_token', newToken)
|
||||
// Устанавливаем токен в заголовки axios по умолчанию
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`
|
||||
}
|
||||
|
||||
// Очистка токена
|
||||
const clearToken = () => {
|
||||
token.value = ''
|
||||
localStorage.removeItem('auth_token')
|
||||
delete axios.defaults.headers.common['Authorization']
|
||||
}
|
||||
|
||||
// Установка пользователя
|
||||
const setUser = (userData) => {
|
||||
user.value = userData
|
||||
}
|
||||
|
||||
// Очистка пользователя
|
||||
const clearUser = () => {
|
||||
user.value = null
|
||||
}
|
||||
|
||||
// Регистрация
|
||||
const register = async (userData) => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/register`, userData)
|
||||
|
||||
// После успешной регистрации автоматически логинимся
|
||||
const loginResponse = await axios.post(`${API_BASE_URL}/login`, {
|
||||
email: userData.email,
|
||||
password: userData.password
|
||||
})
|
||||
|
||||
const { token: authToken, user: userInfo } = loginResponse.data
|
||||
setToken(authToken)
|
||||
setUser(userInfo)
|
||||
|
||||
return { success: true, data: response.data }
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || 'Ошибка регистрации'
|
||||
return { success: false, error: error.value }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Логин
|
||||
const login = async (credentials) => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/login`, credentials)
|
||||
const { token: authToken, user: userInfo } = response.data
|
||||
|
||||
setToken(authToken)
|
||||
setUser(userInfo)
|
||||
|
||||
return { success: true, data: response.data }
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || 'Ошибка входа'
|
||||
return { success: false, error: error.value }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Выход
|
||||
const logout = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
await axios.post(`${API_BASE_URL}/logout`, {}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token.value}`
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Ошибка при выходе:', err)
|
||||
} finally {
|
||||
clearToken()
|
||||
clearUser()
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Получение профиля
|
||||
const fetchProfile = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/profile`)
|
||||
setUser(response.data)
|
||||
return { success: true, data: response.data }
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || 'Ошибка загрузки профиля'
|
||||
clearToken()
|
||||
clearUser()
|
||||
return { success: false, error: error.value }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация при загрузке приложения
|
||||
const initializeAuth = async () => {
|
||||
if (token.value) {
|
||||
// Восстанавливаем заголовок авторизации
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token.value}`
|
||||
// Загружаем данные пользователя
|
||||
await fetchProfile()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
user,
|
||||
token,
|
||||
loading,
|
||||
error,
|
||||
|
||||
// Getters
|
||||
isAuthenticated,
|
||||
userFullName,
|
||||
|
||||
// Actions
|
||||
register,
|
||||
login,
|
||||
logout,
|
||||
fetchProfile,
|
||||
initializeAuth,
|
||||
setToken,
|
||||
clearToken
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,136 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
// import axios from 'axios'
|
||||
|
||||
// const API_BASE_URL = 'https://begushiybashkir.ru/api/v1'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const userStats = ref(null)
|
||||
const userTraining = ref(null)
|
||||
const userAchievements = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
// Получение статистики пользователя
|
||||
const fetchUserStats = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
// TODO: Заменить на реальный endpoint когда будет доступен
|
||||
// const response = await axios.get(`${API_BASE_URL}/user/stats`)
|
||||
|
||||
// Временные данные для демонстрации
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
userStats.value = {
|
||||
totalDistance: 245,
|
||||
bestResult: '10км - 48:15',
|
||||
totalWorkouts: 36,
|
||||
weeklyDistance: 25,
|
||||
monthlyDistance: 98,
|
||||
avgPace: '5:15',
|
||||
caloriesBurned: 12450
|
||||
}
|
||||
|
||||
return { success: true, data: userStats.value }
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || 'Ошибка загрузки статистики'
|
||||
return { success: false, error: error.value }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Получение плана тренировок
|
||||
const fetchUserTraining = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
// TODO: Заменить на реальный endpoint
|
||||
// const response = await axios.get(`${API_BASE_URL}/user/training`)
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
userTraining.value = {
|
||||
currentWeek: 4,
|
||||
totalWeeks: 12,
|
||||
nextWorkout: '2024-03-20T18:00:00',
|
||||
workouts: [
|
||||
{ id: 1, date: '2024-03-18', type: 'interval', distance: '8km', completed: true },
|
||||
{ id: 2, date: '2024-03-20', type: 'tempo', distance: '10km', completed: false },
|
||||
{ id: 3, date: '2024-03-22', type: 'long', distance: '15km', completed: false }
|
||||
]
|
||||
}
|
||||
|
||||
return { success: true, data: userTraining.value }
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || 'Ошибка загрузки плана тренировок'
|
||||
return { success: false, error: error.value }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Получение достижений
|
||||
const fetchUserAchievements = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
// TODO: Заменить на реальный endpoint
|
||||
// const response = await axios.get(`${API_BASE_URL}/user/achievements`)
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
userAchievements.value = [
|
||||
{ id: 1, name: 'Первый забег', description: 'Пробежать первую 5км', achieved: true, date: '2024-01-20' },
|
||||
{ id: 2, name: 'Неделя тренировок', description: 'Тренироваться 7 дней подряд', achieved: true, date: '2024-02-15' },
|
||||
{ id: 3, name: '100 км', description: 'Пробежать 100 км', achieved: true, date: '2024-03-01' },
|
||||
{ id: 4, name: 'Полумарафон', description: 'Пробежать 21.1 км', achieved: false },
|
||||
{ id: 5, name: 'Скорость', description: 'Пробежать 5км быстрее 25 минут', achieved: false }
|
||||
]
|
||||
|
||||
return { success: true, data: userAchievements.value }
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || 'Ошибка загрузки достижений'
|
||||
return { success: false, error: error.value }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Computed свойства
|
||||
const completedAchievements = computed(() =>
|
||||
userAchievements.value.filter(achievement => achievement.achieved)
|
||||
)
|
||||
|
||||
const pendingAchievements = computed(() =>
|
||||
userAchievements.value.filter(achievement => !achievement.achieved)
|
||||
)
|
||||
|
||||
const achievementProgress = computed(() => {
|
||||
if (!userAchievements.value.length) return 0
|
||||
return Math.round((completedAchievements.value.length / userAchievements.value.length) * 100)
|
||||
})
|
||||
|
||||
return {
|
||||
// State
|
||||
userStats,
|
||||
userTraining,
|
||||
userAchievements,
|
||||
loading,
|
||||
error,
|
||||
|
||||
// Getters
|
||||
completedAchievements,
|
||||
pendingAchievements,
|
||||
achievementProgress,
|
||||
|
||||
// Actions
|
||||
fetchUserStats,
|
||||
fetchUserTraining,
|
||||
fetchUserAchievements
|
||||
}
|
||||
})
|
||||
@@ -1,64 +1,160 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<h1>🔐 Вход в систему</h1>
|
||||
<p>Войдите в свой личный кабинет</p>
|
||||
<div class="page">
|
||||
<h1>🔐 Вход в систему</h1>
|
||||
<p>Войдите в свой личный кабинет</p>
|
||||
|
||||
<div class="login-form">
|
||||
<div class="form-group">
|
||||
<input type="email" placeholder="Email" class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" placeholder="Пароль" class="form-input">
|
||||
</div>
|
||||
<button class="btn" @click="$router.push('/profile')">Войти</button>
|
||||
</div>
|
||||
<form @submit.prevent="handleLogin" class="login-form">
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
class="form-input"
|
||||
v-model="credentials.email"
|
||||
required
|
||||
:disabled="loading"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Пароль"
|
||||
class="form-input"
|
||||
v-model="credentials.password"
|
||||
required
|
||||
:disabled="loading"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="login-links">
|
||||
<div class="register-link">
|
||||
<p>Нет аккаунта? <router-link to="/register" class="link">Зарегистрируйтесь здесь</router-link></p>
|
||||
</div>
|
||||
<p><a href="#">Забыли пароль?</a></p>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
:disabled="loading"
|
||||
>
|
||||
{{ loading ? 'Вход...' : 'Войти' }}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary" @click="$router.push('/')">← На главную</button>
|
||||
<div v-if="error" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="login-links">
|
||||
<div class="register-link">
|
||||
<p>Нет аккаунта? <router-link to="/register" class="link">Зарегистрируйтесь здесь</router-link></p>
|
||||
</div>
|
||||
<p><a href="#" class="link">Забыли пароль?</a></p>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-secondary" @click="$router.push('/')">← На главную</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
export default {
|
||||
// eslint-disable-next-line vue/multi-word-component-names
|
||||
name: 'Login'
|
||||
// eslint-disable-next-line vue/multi-word-component-names
|
||||
name: 'Login',
|
||||
setup() {
|
||||
const authStore = useAuthStore()
|
||||
return { authStore }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
credentials: {
|
||||
email: '',
|
||||
password: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
loading() {
|
||||
return this.authStore.loading
|
||||
},
|
||||
error() {
|
||||
return this.authStore.error
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleLogin() {
|
||||
const result = await this.authStore.login(this.credentials)
|
||||
|
||||
if (result.success) {
|
||||
this.$router.push('/profile')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-form {
|
||||
max-width: 300px;
|
||||
margin: 2rem auto;
|
||||
max-width: 300px;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #2e8b57;
|
||||
}
|
||||
|
||||
.form-input:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
background-color: #2e8b57;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
font-size: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: #26734a;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-links {
|
||||
margin-top: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-links a {
|
||||
color: #2e8b57;
|
||||
text-decoration: none;
|
||||
.link {
|
||||
color: #2e8b57;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-links a:hover {
|
||||
text-decoration: underline;
|
||||
.link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: #fee;
|
||||
color: #c33;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-top: 1rem;
|
||||
border-left: 4px solid #c33;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,61 +1,105 @@
|
||||
[file name]: Profile.vue
|
||||
[file content begin]
|
||||
<template>
|
||||
<div class="page">
|
||||
<h1>👤 Личный кабинет</h1>
|
||||
|
||||
<div class="profile-header">
|
||||
<img :src="getImageUrl('dinamo.jpg')"
|
||||
alt="Аватар"
|
||||
style="width: 100px; height: 100px; border-radius: 50%;">
|
||||
<h2>{{ user.firstName }} {{ user.lastName }}</h2>
|
||||
<p>Участник с {{ joinDate }}</p>
|
||||
<p class="user-email">{{ user.email }}</p>
|
||||
<p v-if="user.phone" class="user-phone">📱 {{ user.phone }}</p>
|
||||
</div>
|
||||
<div v-if="authLoading" class="loading">Загрузка профиля...</div>
|
||||
|
||||
<div class="profile-info">
|
||||
<h3>📋 Информация о пользователе</h3>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>Уровень подготовки:</label>
|
||||
<span class="info-value">{{ experienceLabel }}</span>
|
||||
<div v-else-if="user" class="profile-content">
|
||||
<div class="profile-header">
|
||||
<img :src="getImageUrl('dinamo.jpg')"
|
||||
alt="Аватар"
|
||||
style="width: 100px; height: 100px; border-radius: 50%;">
|
||||
<h2>{{ user.firstName }} {{ user.lastName }}</h2>
|
||||
<p>Участник с {{ joinDate }}</p>
|
||||
<p class="user-email">{{ user.email }}</p>
|
||||
<p v-if="user.phone" class="user-phone">📱 {{ user.phone }}</p>
|
||||
</div>
|
||||
|
||||
<div class="profile-info">
|
||||
<h3>📋 Информация о пользователе</h3>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>Уровень подготовки:</label>
|
||||
<span class="info-value">{{ experienceLabel }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Цели:</label>
|
||||
<span class="info-value">{{ goalsLabel }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Рассылка:</label>
|
||||
<span class="info-value">{{ user.newsletter ? '✅ Подключена' : '❌ Отключена' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Роль:</label>
|
||||
<span class="info-value role-badge">{{ user.role }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Цели:</label>
|
||||
<span class="info-value">{{ goalsLabel }}</span>
|
||||
</div>
|
||||
|
||||
<div class="profile-stats">
|
||||
<div class="stats-header">
|
||||
<h3>📊 Моя статистика</h3>
|
||||
<button class="btn-refresh" @click="refreshStats" :disabled="statsLoading">
|
||||
{{ statsLoading ? '⟳' : '🔄' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Рассылка:</label>
|
||||
<span class="info-value">{{ user.newsletter ? '✅ Подключена' : '❌ Отключена' }}</span>
|
||||
|
||||
<div v-if="statsError" class="error-message">
|
||||
{{ statsError }}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Роль:</label>
|
||||
<span class="info-value role-badge">{{ user.role }}</span>
|
||||
|
||||
<div v-else class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<h4>🏃 Всего пробег</h4>
|
||||
<p>{{ userStats?.totalDistance || 0 }} км</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>⭐ Лучший результат</h4>
|
||||
<p>{{ userStats?.bestResult || 'Нет данных' }}</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>📅 Тренировок</h4>
|
||||
<p>{{ userStats?.totalWorkouts || 0 }}</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>🔥 Сожжено калорий</h4>
|
||||
<p>{{ userStats?.caloriesBurned || 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="achievements-preview">
|
||||
<h3>🏆 Достижения</h3>
|
||||
<div class="achievements-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: achievementProgress + '%' }"></div>
|
||||
</div>
|
||||
<span>{{ achievementProgress }}% выполнено</span>
|
||||
</div>
|
||||
<div class="achievements-count">
|
||||
<span>Получено: {{ completedAchievements.length }} из {{ userAchievements.length }}</span>
|
||||
</div>
|
||||
<button class="btn btn-outline" @click="$router.push('/achievements')">
|
||||
📜 Все достижения
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="profile-actions">
|
||||
<button class="btn" @click="editProfile">✏️ Редактировать профиль</button>
|
||||
<button class="btn" @click="viewDetailedStats">📊 Подробная статистика</button>
|
||||
<button class="btn" @click="$router.push('/training')">📅 Мой план тренировок</button>
|
||||
<button class="btn btn-logout" @click="handleLogout" :disabled="authLoading">
|
||||
{{ authLoading ? 'Выход...' : '🚪 Выйти' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-stats">
|
||||
<h3>📊 Моя статистика</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<h4>🏃 Всего пробег</h4>
|
||||
<p>245 км</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>⭐ Лучший результат</h4>
|
||||
<p>10км - 48:15</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>📅 Тренировок</h4>
|
||||
<p>36</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-actions">
|
||||
<button class="btn" @click="editProfile">✏️ Редактировать профиль</button>
|
||||
<button class="btn">📊 Подробная статистика</button>
|
||||
<button class="btn" @click="$router.push('/training')">📅 Мой план тренировок</button>
|
||||
<div v-else class="error-message">
|
||||
Не удалось загрузить данные профиля.
|
||||
<router-link to="/login" class="link">Войдите</router-link> снова.
|
||||
</div>
|
||||
|
||||
<button class="btn btn-secondary" @click="$router.push('/')">← На главную</button>
|
||||
@@ -63,27 +107,44 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
export default {
|
||||
// eslint-disable-next-line vue/multi-word-component-names
|
||||
name: 'Profile',
|
||||
setup() {
|
||||
const authStore = useAuthStore()
|
||||
const userStore = useUserStore()
|
||||
return { authStore, userStore }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
user: {
|
||||
firstName: 'Йүгерек',
|
||||
lastName: 'Башҡортов',
|
||||
email: 'йүгерекбашҡортов@bbclub.ru',
|
||||
phone: '+7 (999) 123-45-67',
|
||||
experience: 'intermediate',
|
||||
goals: 'halfMarathon',
|
||||
newsletter: true,
|
||||
role: 'user',
|
||||
createdAt: '2024-01-15T10:30:00Z'
|
||||
}
|
||||
authLoading: false,
|
||||
statsLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
return this.authStore.user
|
||||
},
|
||||
userStats() {
|
||||
return this.userStore.userStats
|
||||
},
|
||||
userAchievements() {
|
||||
return this.userStore.userAchievements
|
||||
},
|
||||
completedAchievements() {
|
||||
return this.userStore.completedAchievements
|
||||
},
|
||||
achievementProgress() {
|
||||
return this.userStore.achievementProgress
|
||||
},
|
||||
statsError() {
|
||||
return this.userStore.error
|
||||
},
|
||||
joinDate() {
|
||||
if (!this.user.createdAt) return 'января 2024';
|
||||
if (!this.user?.createdAt) return 'января 2024';
|
||||
|
||||
const date = new Date(this.user.createdAt);
|
||||
const month = date.toLocaleString('ru-RU', { month: 'long' });
|
||||
@@ -97,7 +158,7 @@ export default {
|
||||
'advanced': 'Опытный (2+ лет)',
|
||||
'professional': 'Профессионал'
|
||||
};
|
||||
return experienceMap[this.user.experience] || 'Не указан';
|
||||
return experienceMap[this.user?.experience] || 'Не указан';
|
||||
},
|
||||
goalsLabel() {
|
||||
const goalsMap = {
|
||||
@@ -110,103 +171,136 @@ export default {
|
||||
'improve': 'Улучшить результаты',
|
||||
'social': 'Общение и компания'
|
||||
};
|
||||
return goalsMap[this.user.goals] || 'Не указана';
|
||||
return goalsMap[this.user?.goals] || 'Не указана';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getImageUrl(path) {
|
||||
// В продакшене замените на правильный путь
|
||||
const baseUrl = import.meta.env.BASE_URL
|
||||
// Путь от корня public/
|
||||
console.log(`${baseUrl}images/${path}`)
|
||||
return `${baseUrl}images/${path}`
|
||||
},
|
||||
async loadUserData() {
|
||||
this.authLoading = true
|
||||
try {
|
||||
// TODO: Заменить на реальный API call
|
||||
// const response = await this.$axios.get('/api/user/profile');
|
||||
// this.user = response.data;
|
||||
|
||||
// Временные данные для демонстрации
|
||||
console.log('Загрузка данных пользователя...');
|
||||
await this.authStore.fetchProfile()
|
||||
await this.loadStats()
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки данных:', error);
|
||||
console.error('Ошибка загрузки данных:', error)
|
||||
} finally {
|
||||
this.authLoading = false
|
||||
}
|
||||
},
|
||||
async loadStats() {
|
||||
this.statsLoading = true
|
||||
try {
|
||||
await Promise.all([
|
||||
this.userStore.fetchUserStats(),
|
||||
this.userStore.fetchUserAchievements()
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки статистики:', error)
|
||||
} finally {
|
||||
this.statsLoading = false
|
||||
}
|
||||
},
|
||||
async refreshStats() {
|
||||
await this.loadStats()
|
||||
},
|
||||
async handleLogout() {
|
||||
await this.authStore.logout()
|
||||
this.$router.push('/login')
|
||||
},
|
||||
editProfile() {
|
||||
this.$router.push('/profile/edit');
|
||||
this.$router.push('/profile/edit')
|
||||
},
|
||||
viewDetailedStats() {
|
||||
// TODO: Переход на страницу детальной статистики
|
||||
alert('Функция в разработке')
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadUserData();
|
||||
async mounted() {
|
||||
if (!this.user) {
|
||||
await this.loadUserData()
|
||||
} else {
|
||||
await this.loadStats()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
/* Существующие стили остаются */
|
||||
|
||||
.user-email {
|
||||
color: #666;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.user-phone {
|
||||
color: #666;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
.stats-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.info-item label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
.btn-refresh {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #555;
|
||||
font-size: 1rem;
|
||||
.btn-refresh:hover:not(:disabled) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
.btn-refresh:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.achievements-preview {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.achievements-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background-color: #2e8b57;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.achievements-count {
|
||||
margin: 1rem 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: white;
|
||||
color: #2e8b57;
|
||||
border: 2px solid #2e8b57;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: #2e8b57;
|
||||
color: white;
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
@@ -222,6 +316,7 @@ export default {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.profile-actions {
|
||||
@@ -232,19 +327,55 @@ export default {
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-logout:hover:not(:disabled) {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.btn-logout:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: #fee;
|
||||
color: #c33;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
margin: 2rem 0;
|
||||
border-left: 4px solid #c33;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
font-size: 1.1rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.profile-actions {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.profile-header,
|
||||
.profile-info {
|
||||
padding: 1rem;
|
||||
.achievements-progress {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,3 +385,4 @@ export default {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
[file content end]
|
||||
@@ -0,0 +1,410 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<h1>✏️ Редактирование профиля</h1>
|
||||
|
||||
<div v-if="loading && !user" class="loading">Загрузка...</div>
|
||||
|
||||
<form v-else @submit.prevent="handleSubmit" class="profile-edit-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="firstName">Имя *</label>
|
||||
<input
|
||||
id="firstName"
|
||||
v-model="formData.firstName"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="Введите ваше имя"
|
||||
required
|
||||
:disabled="loading"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="lastName">Фамилия *</label>
|
||||
<input
|
||||
id="lastName"
|
||||
v-model="formData.lastName"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="Введите вашу фамилию"
|
||||
required
|
||||
:disabled="loading"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Email *</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="formData.email"
|
||||
type="email"
|
||||
class="form-input"
|
||||
placeholder="example@mail.ru"
|
||||
required
|
||||
:disabled="loading"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="phone">Телефон</label>
|
||||
<input
|
||||
id="phone"
|
||||
v-model="formData.phone"
|
||||
type="tel"
|
||||
class="form-input"
|
||||
placeholder="+7 (999) 123-45-67"
|
||||
:disabled="loading"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="experience">Уровень подготовки</label>
|
||||
<select
|
||||
id="experience"
|
||||
v-model="formData.experience"
|
||||
class="form-input"
|
||||
:disabled="loading"
|
||||
>
|
||||
<option value="">Выберите уровень</option>
|
||||
<option value="beginner">Начинающий (0-6 месяцев)</option>
|
||||
<option value="intermediate">Любитель (6-24 месяцев)</option>
|
||||
<option value="advanced">Опытный (2+ лет)</option>
|
||||
<option value="professional">Профессионал</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="goals">Цели</label>
|
||||
<select
|
||||
id="goals"
|
||||
v-model="formData.goals"
|
||||
class="form-input"
|
||||
:disabled="loading"
|
||||
>
|
||||
<option value="">Выберите цель</option>
|
||||
<option value="health">Улучшить здоровье</option>
|
||||
<option value="weight">Сбросить вес</option>
|
||||
<option value="first5k">Пробежать первые 5 км</option>
|
||||
<option value="first10k">Пробежать первые 10 км</option>
|
||||
<option value="halfMarathon">Подготовиться к полумарафону</option>
|
||||
<option value="marathon">Подготовиться к марафону</option>
|
||||
<option value="improve">Улучшить результаты</option>
|
||||
<option value="social">Общение и компания</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<label class="checkbox-label">
|
||||
<input
|
||||
v-model="formData.newsletter"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:disabled="loading"
|
||||
>
|
||||
<span class="checkmark"></span>
|
||||
Хочу получать новости о тренировках и мероприятиях
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
:disabled="loading || !isFormChanged"
|
||||
>
|
||||
{{ loading ? 'Сохранение...' : '💾 Сохранить изменения' }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
@click="cancelEdit"
|
||||
:disabled="loading"
|
||||
>
|
||||
❌ Отмена
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-if="success" class="success-message">
|
||||
✅ Профиль успешно обновлен!
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="navigation-actions">
|
||||
<button class="btn btn-secondary" @click="$router.push('/profile')">← Назад к профилю</button>
|
||||
<button class="btn btn-secondary" @click="$router.push('/')">🏠 На главную</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'ProfileEdit',
|
||||
setup() {
|
||||
const authStore = useAuthStore()
|
||||
return { authStore }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
experience: '',
|
||||
goals: '',
|
||||
newsletter: false
|
||||
},
|
||||
originalData: {},
|
||||
loading: false,
|
||||
error: '',
|
||||
success: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
return this.authStore.user
|
||||
},
|
||||
isFormChanged() {
|
||||
return JSON.stringify(this.formData) !== JSON.stringify(this.originalData)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initializeForm() {
|
||||
if (this.user) {
|
||||
this.formData = {
|
||||
firstName: this.user.firstName || '',
|
||||
lastName: this.user.lastName || '',
|
||||
email: this.user.email || '',
|
||||
phone: this.user.phone || '',
|
||||
experience: this.user.experience || '',
|
||||
goals: this.user.goals || '',
|
||||
newsletter: this.user.newsletter || false
|
||||
}
|
||||
this.originalData = { ...this.formData }
|
||||
}
|
||||
},
|
||||
async handleSubmit() {
|
||||
this.loading = true
|
||||
this.error = ''
|
||||
this.success = false
|
||||
|
||||
try {
|
||||
const response = await axios.put(
|
||||
'https://begushiybashkir.ru/api/v1/auth/profile',
|
||||
this.formData,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.authStore.token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Обновляем данные в store
|
||||
this.authStore.setUser(response.data)
|
||||
this.originalData = { ...this.formData }
|
||||
this.success = true
|
||||
|
||||
// Скрываем сообщение об успехе через 3 секунды
|
||||
setTimeout(() => {
|
||||
this.success = false
|
||||
}, 3000)
|
||||
|
||||
} catch (err) {
|
||||
this.error = err.response?.data?.message || 'Ошибка обновления профиля'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
cancelEdit() {
|
||||
this.initializeForm()
|
||||
this.error = ''
|
||||
this.success = false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.user) {
|
||||
this.initializeForm()
|
||||
} else {
|
||||
// Если пользователь не загружен, загружаем данные
|
||||
this.loading = true
|
||||
this.authStore.fetchProfile().finally(() => {
|
||||
this.loading = false
|
||||
this.initializeForm()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-edit-form {
|
||||
max-width: 500px;
|
||||
margin: 2rem auto;
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #2e8b57;
|
||||
}
|
||||
|
||||
.form-input:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-right: 10px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 3px;
|
||||
margin-right: 10px;
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.checkbox:checked + .checkmark {
|
||||
background-color: #2e8b57;
|
||||
border-color: #2e8b57;
|
||||
}
|
||||
|
||||
.checkbox:checked + .checkmark::after {
|
||||
content: '✓';
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: 2px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
flex: 1;
|
||||
background-color: #2e8b57;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: #26734a;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background-color: #efe;
|
||||
color: #2e8b57;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-top: 1rem;
|
||||
border-left: 4px solid #2e8b57;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: #fee;
|
||||
color: #c33;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-top: 1rem;
|
||||
border-left: 4px solid #c33;
|
||||
}
|
||||
|
||||
.navigation-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
font-size: 1.1rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 600px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.profile-edit-form {
|
||||
padding: 1.5rem;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.navigation-actions {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -15,6 +15,7 @@
|
||||
class="form-input"
|
||||
placeholder="Введите ваше имя"
|
||||
required
|
||||
:disabled="loading"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -27,6 +28,7 @@
|
||||
class="form-input"
|
||||
placeholder="Введите вашу фамилию"
|
||||
required
|
||||
:disabled="loading"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,6 +42,7 @@
|
||||
class="form-input"
|
||||
placeholder="example@mail.ru"
|
||||
required
|
||||
:disabled="loading"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -51,6 +54,7 @@
|
||||
type="tel"
|
||||
class="form-input"
|
||||
placeholder="+7 (999) 123-45-67"
|
||||
:disabled="loading"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -65,6 +69,7 @@
|
||||
placeholder="Не менее 6 символов"
|
||||
required
|
||||
minlength="6"
|
||||
:disabled="loading"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -77,6 +82,7 @@
|
||||
class="form-input"
|
||||
placeholder="Повторите пароль"
|
||||
required
|
||||
:disabled="loading"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,6 +93,7 @@
|
||||
id="experience"
|
||||
v-model="formData.experience"
|
||||
class="form-input"
|
||||
:disabled="loading"
|
||||
>
|
||||
<option value="">Выберите уровень</option>
|
||||
<option value="beginner">Начинающий (0-6 месяцев)</option>
|
||||
@@ -102,6 +109,7 @@
|
||||
id="goals"
|
||||
v-model="formData.goals"
|
||||
class="form-input"
|
||||
:disabled="loading"
|
||||
>
|
||||
<option value="">Выберите цель</option>
|
||||
<option value="health">Улучшить здоровье</option>
|
||||
@@ -122,6 +130,7 @@
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
required
|
||||
:disabled="loading"
|
||||
>
|
||||
<span class="checkmark"></span>
|
||||
Я соглашаюсь с
|
||||
@@ -136,6 +145,7 @@
|
||||
v-model="formData.newsletter"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:disabled="loading"
|
||||
>
|
||||
<span class="checkmark"></span>
|
||||
Хочу получать новости о тренировках и мероприятиях
|
||||
@@ -176,9 +186,15 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
export default {
|
||||
// eslint-disable-next-line vue/multi-word-component-names
|
||||
name: 'Register',
|
||||
setup() {
|
||||
const authStore = useAuthStore()
|
||||
return { authStore }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
@@ -192,46 +208,52 @@ export default {
|
||||
goals: '',
|
||||
agreeTerms: false,
|
||||
newsletter: true
|
||||
},
|
||||
loading: false,
|
||||
error: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
loading() {
|
||||
return this.authStore.loading
|
||||
},
|
||||
error() {
|
||||
return this.authStore.error
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleRegister() {
|
||||
// Валидация
|
||||
if (this.formData.password !== this.formData.confirmPassword) {
|
||||
this.error = 'Пароли не совпадают'
|
||||
this.authStore.error = 'Пароли не совпадают'
|
||||
return
|
||||
}
|
||||
|
||||
if (this.formData.password.length < 6) {
|
||||
this.error = 'Пароль должен содержать не менее 6 символов'
|
||||
this.authStore.error = 'Пароль должен содержать не менее 6 символов'
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.formData.agreeTerms) {
|
||||
this.error = 'Необходимо согласие с правилами клуба'
|
||||
this.authStore.error = 'Необходимо согласие с правилами клуба'
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
this.error = ''
|
||||
// Подготовка данных для API
|
||||
const registerData = {
|
||||
email: this.formData.email,
|
||||
password: this.formData.password,
|
||||
firstName: this.formData.firstName,
|
||||
lastName: this.formData.lastName,
|
||||
phone: this.formData.phone,
|
||||
experience: this.formData.experience,
|
||||
goals: this.formData.goals,
|
||||
newsletter: this.formData.newsletter
|
||||
}
|
||||
|
||||
try {
|
||||
// Здесь будет API call к backend
|
||||
console.log('Регистрация:', this.formData)
|
||||
const result = await this.authStore.register(registerData)
|
||||
|
||||
// Имитация задержки сети
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
|
||||
// Успешная регистрация
|
||||
this.$router.push('/login?message=registered')
|
||||
|
||||
} catch (err) {
|
||||
this.error = 'Ошибка регистрации. Попробуйте еще раз.' + err
|
||||
} finally {
|
||||
this.loading = false
|
||||
if (result.success) {
|
||||
// Перенаправляем на страницу профиля после успешной регистрации
|
||||
this.$router.push('/profile')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,6 +261,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Стили остаются без изменений */
|
||||
.register-container {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
@@ -284,6 +307,11 @@ export default {
|
||||
border-color: #2e8b57;
|
||||
}
|
||||
|
||||
.form-input:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -113,7 +113,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
Value: token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: false, // В production установить true
|
||||
Secure: false, // В production установить true :TODO
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Expires: time.Now().Add(24 * time.Hour),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user