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:
2025-10-10 01:30:30 +05:00
parent affaa2679e
commit 0e067c7477
12 changed files with 1532 additions and 245 deletions
+282 -2
View File
@@ -1,13 +1,14 @@
{ {
"name": "bbvue", "name": "bbvue",
"version": "0.0.0", "version": "0.0.13",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "bbvue", "name": "bbvue",
"version": "0.0.0", "version": "0.0.13",
"dependencies": { "dependencies": {
"axios": "^1.12.2",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
@@ -1939,6 +1940,23 @@
"dev": true, "dev": true,
"license": "Python-2.0" "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": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2033,6 +2051,19 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -2101,6 +2132,18 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2232,6 +2275,29 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/electron-to-chromium": {
"version": "1.5.227", "version": "1.5.227",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz", "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" "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": { "node_modules/esbuild": {
"version": "0.25.10", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
@@ -2702,6 +2813,42 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "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": "^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": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -2727,6 +2883,43 @@
"node": ">=6.9.0" "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": { "node_modules/get-stream": {
"version": "9.0.1", "version": "9.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
@@ -2770,6 +2963,18 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -2780,6 +2985,45 @@
"node": ">=8" "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": { "node_modules/hookable": {
"version": "5.5.3", "version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
@@ -3105,6 +3349,36 @@
"@jridgewell/sourcemap-codec": "^1.5.5" "@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": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -3488,6 +3762,12 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+1
View File
@@ -14,6 +14,7 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"axios": "^1.12.2",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
+37
View File
@@ -5,10 +5,47 @@ import { createPinia } from 'pinia'
import App from './App.vue' import App from './App.vue'
import router from './router' 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) const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
// Инициализация auth store после создания app
import { useAuthStore } from './stores/auth'
const authStore = useAuthStore()
authStore.initializeAuth()
app.mount('#app') app.mount('#app')
+7 -1
View File
@@ -58,8 +58,14 @@ const router = createRouter({
path: '/register', path: '/register',
name: 'Register', name: 'Register',
component: () => import('../views/Register.vue') component: () => import('../views/Register.vue')
},
{
path: '/profile/edit',
name: 'ProfileEdit',
component: () => import('../views/ProfileEdit.vue'),
meta: { requiresAuth: true }
} }
] ]
}) })
+161
View File
@@ -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
}
})
+136
View File
@@ -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
}
})
+106 -10
View File
@@ -3,21 +3,46 @@
<h1>🔐 Вход в систему</h1> <h1>🔐 Вход в систему</h1>
<p>Войдите в свой личный кабинет</p> <p>Войдите в свой личный кабинет</p>
<div class="login-form"> <form @submit.prevent="handleLogin" class="login-form">
<div class="form-group"> <div class="form-group">
<input type="email" placeholder="Email" class="form-input"> <input
type="email"
placeholder="Email"
class="form-input"
v-model="credentials.email"
required
:disabled="loading"
>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="password" placeholder="Пароль" class="form-input"> <input
type="password"
placeholder="Пароль"
class="form-input"
v-model="credentials.password"
required
:disabled="loading"
>
</div> </div>
<button class="btn" @click="$router.push('/profile')">Войти</button>
<button
type="submit"
class="btn btn-primary"
:disabled="loading"
>
{{ loading ? 'Вход...' : 'Войти' }}
</button>
<div v-if="error" class="error-message">
{{ error }}
</div> </div>
</form>
<div class="login-links"> <div class="login-links">
<div class="register-link"> <div class="register-link">
<p>Нет аккаунта? <router-link to="/register" class="link">Зарегистрируйтесь здесь</router-link></p> <p>Нет аккаунта? <router-link to="/register" class="link">Зарегистрируйтесь здесь</router-link></p>
</div> </div>
<p><a href="#">Забыли пароль?</a></p> <p><a href="#" class="link">Забыли пароль?</a></p>
</div> </div>
<button class="btn btn-secondary" @click="$router.push('/')"> На главную</button> <button class="btn btn-secondary" @click="$router.push('/')"> На главную</button>
@@ -25,9 +50,40 @@
</template> </template>
<script> <script>
import { useAuthStore } from '../stores/auth'
export default { export default {
// eslint-disable-next-line vue/multi-word-component-names // eslint-disable-next-line vue/multi-word-component-names
name: 'Login' 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> </script>
@@ -44,21 +100,61 @@ export default {
.form-input { .form-input {
width: 100%; width: 100%;
padding: 12px; padding: 12px;
border: 1px solid #ddd; border: 2px solid #e1e5e9;
border-radius: 5px; border-radius: 6px;
font-size: 1rem; 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 { .login-links {
margin-top: 1.5rem; margin-top: 1.5rem;
text-align: center;
} }
.login-links a { .link {
color: #2e8b57; color: #2e8b57;
text-decoration: none; text-decoration: none;
} }
.login-links a:hover { .link:hover {
text-decoration: underline; 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> </style>
+220 -88
View File
@@ -1,7 +1,12 @@
[file name]: Profile.vue
[file content begin]
<template> <template>
<div class="page"> <div class="page">
<h1>👤 Личный кабинет</h1> <h1>👤 Личный кабинет</h1>
<div v-if="authLoading" class="loading">Загрузка профиля...</div>
<div v-else-if="user" class="profile-content">
<div class="profile-header"> <div class="profile-header">
<img :src="getImageUrl('dinamo.jpg')" <img :src="getImageUrl('dinamo.jpg')"
alt="Аватар" alt="Аватар"
@@ -35,27 +40,66 @@
</div> </div>
<div class="profile-stats"> <div class="profile-stats">
<div class="stats-header">
<h3>📊 Моя статистика</h3> <h3>📊 Моя статистика</h3>
<div class="stats-grid"> <button class="btn-refresh" @click="refreshStats" :disabled="statsLoading">
{{ statsLoading ? '⟳' : '🔄' }}
</button>
</div>
<div v-if="statsError" class="error-message">
{{ statsError }}
</div>
<div v-else class="stats-grid">
<div class="stat-card"> <div class="stat-card">
<h4>🏃 Всего пробег</h4> <h4>🏃 Всего пробег</h4>
<p>245 км</p> <p>{{ userStats?.totalDistance || 0 }} км</p>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h4> Лучший результат</h4> <h4> Лучший результат</h4>
<p>10км - 48:15</p> <p>{{ userStats?.bestResult || 'Нет данных' }}</p>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h4>📅 Тренировок</h4> <h4>📅 Тренировок</h4>
<p>36</p> <p>{{ userStats?.totalWorkouts || 0 }}</p>
</div>
<div class="stat-card">
<h4>🔥 Сожжено калорий</h4>
<p>{{ userStats?.caloriesBurned || 0 }}</p>
</div> </div>
</div> </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"> <div class="profile-actions">
<button class="btn" @click="editProfile"> Редактировать профиль</button> <button class="btn" @click="editProfile"> Редактировать профиль</button>
<button class="btn">📊 Подробная статистика</button> <button class="btn" @click="viewDetailedStats">📊 Подробная статистика</button>
<button class="btn" @click="$router.push('/training')">📅 Мой план тренировок</button> <button class="btn" @click="$router.push('/training')">📅 Мой план тренировок</button>
<button class="btn btn-logout" @click="handleLogout" :disabled="authLoading">
{{ authLoading ? 'Выход...' : '🚪 Выйти' }}
</button>
</div>
</div>
<div v-else class="error-message">
Не удалось загрузить данные профиля.
<router-link to="/login" class="link">Войдите</router-link> снова.
</div> </div>
<button class="btn btn-secondary" @click="$router.push('/')"> На главную</button> <button class="btn btn-secondary" @click="$router.push('/')"> На главную</button>
@@ -63,27 +107,44 @@
</template> </template>
<script> <script>
import { useAuthStore } from '../stores/auth'
import { useUserStore } from '../stores/user'
export default { export default {
// eslint-disable-next-line vue/multi-word-component-names // eslint-disable-next-line vue/multi-word-component-names
name: 'Profile', name: 'Profile',
setup() {
const authStore = useAuthStore()
const userStore = useUserStore()
return { authStore, userStore }
},
data() { data() {
return { return {
user: { authLoading: false,
firstName: 'Йүгерек', statsLoading: false
lastName: 'Башҡортов',
email: 'йүгерекбашҡортов@bbclub.ru',
phone: '+7 (999) 123-45-67',
experience: 'intermediate',
goals: 'halfMarathon',
newsletter: true,
role: 'user',
createdAt: '2024-01-15T10:30:00Z'
}
} }
}, },
computed: { 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() { joinDate() {
if (!this.user.createdAt) return 'января 2024'; if (!this.user?.createdAt) return 'января 2024';
const date = new Date(this.user.createdAt); const date = new Date(this.user.createdAt);
const month = date.toLocaleString('ru-RU', { month: 'long' }); const month = date.toLocaleString('ru-RU', { month: 'long' });
@@ -97,7 +158,7 @@ export default {
'advanced': 'Опытный (2+ лет)', 'advanced': 'Опытный (2+ лет)',
'professional': 'Профессионал' 'professional': 'Профессионал'
}; };
return experienceMap[this.user.experience] || 'Не указан'; return experienceMap[this.user?.experience] || 'Не указан';
}, },
goalsLabel() { goalsLabel() {
const goalsMap = { const goalsMap = {
@@ -110,103 +171,136 @@ export default {
'improve': 'Улучшить результаты', 'improve': 'Улучшить результаты',
'social': 'Общение и компания' 'social': 'Общение и компания'
}; };
return goalsMap[this.user.goals] || 'Не указана'; return goalsMap[this.user?.goals] || 'Не указана';
} }
}, },
methods: { methods: {
getImageUrl(path) { getImageUrl(path) {
// В продакшене замените на правильный путь
const baseUrl = import.meta.env.BASE_URL const baseUrl = import.meta.env.BASE_URL
// Путь от корня public/
console.log(`${baseUrl}images/${path}`)
return `${baseUrl}images/${path}` return `${baseUrl}images/${path}`
}, },
async loadUserData() { async loadUserData() {
this.authLoading = true
try { try {
// TODO: Заменить на реальный API call await this.authStore.fetchProfile()
// const response = await this.$axios.get('/api/user/profile'); await this.loadStats()
// this.user = response.data;
// Временные данные для демонстрации
console.log('Загрузка данных пользователя...');
} catch (error) { } 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() { editProfile() {
this.$router.push('/profile/edit'); this.$router.push('/profile/edit')
},
viewDetailedStats() {
// TODO: Переход на страницу детальной статистики
alert('Функция в разработке')
} }
}, },
mounted() { async mounted() {
this.loadUserData(); if (!this.user) {
await this.loadUserData()
} else {
await this.loadStats()
}
} }
} }
</script> </script>
<style scoped> <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 { .stats-header {
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 {
display: flex; display: flex;
flex-direction: column; justify-content: space-between;
padding: 1rem; align-items: center;
background: #f8f9fa; margin-bottom: 1rem;
border-radius: 8px;
} }
.info-item label { .btn-refresh {
font-weight: 600; background: none;
color: #333; border: none;
margin-bottom: 0.5rem; font-size: 1.2rem;
font-size: 0.9rem; cursor: pointer;
padding: 5px;
border-radius: 50%;
transition: background-color 0.3s;
} }
.info-value { .btn-refresh:hover:not(:disabled) {
color: #555; background-color: #f0f0f0;
font-size: 1rem;
} }
.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; background: #2e8b57;
color: white; 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 { .stats-grid {
@@ -222,6 +316,7 @@ export default {
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1); box-shadow: 0 2px 8px rgba(0,0,0,0.1);
text-align: center; text-align: center;
border: 1px solid #e0e0e0;
} }
.profile-actions { .profile-actions {
@@ -232,19 +327,55 @@ export default {
margin: 2rem auto; 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) { @media (max-width: 768px) {
.info-grid { .info-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.profile-actions { .profile-actions {
max-width: 100%; max-width: 100%;
} }
.profile-header, .achievements-progress {
.profile-info { flex-direction: column;
padding: 1rem; gap: 0.5rem;
} }
} }
@@ -254,3 +385,4 @@ export default {
} }
} }
</style> </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>
+48 -20
View File
@@ -15,6 +15,7 @@
class="form-input" class="form-input"
placeholder="Введите ваше имя" placeholder="Введите ваше имя"
required required
:disabled="loading"
> >
</div> </div>
@@ -27,6 +28,7 @@
class="form-input" class="form-input"
placeholder="Введите вашу фамилию" placeholder="Введите вашу фамилию"
required required
:disabled="loading"
> >
</div> </div>
</div> </div>
@@ -40,6 +42,7 @@
class="form-input" class="form-input"
placeholder="example@mail.ru" placeholder="example@mail.ru"
required required
:disabled="loading"
> >
</div> </div>
@@ -51,6 +54,7 @@
type="tel" type="tel"
class="form-input" class="form-input"
placeholder="+7 (999) 123-45-67" placeholder="+7 (999) 123-45-67"
:disabled="loading"
> >
</div> </div>
@@ -65,6 +69,7 @@
placeholder="Не менее 6 символов" placeholder="Не менее 6 символов"
required required
minlength="6" minlength="6"
:disabled="loading"
> >
</div> </div>
@@ -77,6 +82,7 @@
class="form-input" class="form-input"
placeholder="Повторите пароль" placeholder="Повторите пароль"
required required
:disabled="loading"
> >
</div> </div>
</div> </div>
@@ -87,6 +93,7 @@
id="experience" id="experience"
v-model="formData.experience" v-model="formData.experience"
class="form-input" class="form-input"
:disabled="loading"
> >
<option value="">Выберите уровень</option> <option value="">Выберите уровень</option>
<option value="beginner">Начинающий (0-6 месяцев)</option> <option value="beginner">Начинающий (0-6 месяцев)</option>
@@ -102,6 +109,7 @@
id="goals" id="goals"
v-model="formData.goals" v-model="formData.goals"
class="form-input" class="form-input"
:disabled="loading"
> >
<option value="">Выберите цель</option> <option value="">Выберите цель</option>
<option value="health">Улучшить здоровье</option> <option value="health">Улучшить здоровье</option>
@@ -122,6 +130,7 @@
type="checkbox" type="checkbox"
class="checkbox" class="checkbox"
required required
:disabled="loading"
> >
<span class="checkmark"></span> <span class="checkmark"></span>
Я соглашаюсь с Я соглашаюсь с
@@ -136,6 +145,7 @@
v-model="formData.newsletter" v-model="formData.newsletter"
type="checkbox" type="checkbox"
class="checkbox" class="checkbox"
:disabled="loading"
> >
<span class="checkmark"></span> <span class="checkmark"></span>
Хочу получать новости о тренировках и мероприятиях Хочу получать новости о тренировках и мероприятиях
@@ -176,9 +186,15 @@
</template> </template>
<script> <script>
import { useAuthStore } from '../stores/auth'
export default { export default {
// eslint-disable-next-line vue/multi-word-component-names // eslint-disable-next-line vue/multi-word-component-names
name: 'Register', name: 'Register',
setup() {
const authStore = useAuthStore()
return { authStore }
},
data() { data() {
return { return {
formData: { formData: {
@@ -192,46 +208,52 @@ export default {
goals: '', goals: '',
agreeTerms: false, agreeTerms: false,
newsletter: true newsletter: true
}
}
}, },
loading: false, computed: {
error: '' loading() {
return this.authStore.loading
},
error() {
return this.authStore.error
} }
}, },
methods: { methods: {
async handleRegister() { async handleRegister() {
// Валидация // Валидация
if (this.formData.password !== this.formData.confirmPassword) { if (this.formData.password !== this.formData.confirmPassword) {
this.error = 'Пароли не совпадают' this.authStore.error = 'Пароли не совпадают'
return return
} }
if (this.formData.password.length < 6) { if (this.formData.password.length < 6) {
this.error = 'Пароль должен содержать не менее 6 символов' this.authStore.error = 'Пароль должен содержать не менее 6 символов'
return return
} }
if (!this.formData.agreeTerms) { if (!this.formData.agreeTerms) {
this.error = 'Необходимо согласие с правилами клуба' this.authStore.error = 'Необходимо согласие с правилами клуба'
return return
} }
this.loading = true // Подготовка данных для API
this.error = '' 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 { const result = await this.authStore.register(registerData)
// Здесь будет API call к backend
console.log('Регистрация:', this.formData)
// Имитация задержки сети if (result.success) {
await new Promise(resolve => setTimeout(resolve, 1500)) // Перенаправляем на страницу профиля после успешной регистрации
this.$router.push('/profile')
// Успешная регистрация
this.$router.push('/login?message=registered')
} catch (err) {
this.error = 'Ошибка регистрации. Попробуйте еще раз.' + err
} finally {
this.loading = false
} }
} }
} }
@@ -239,6 +261,7 @@ export default {
</script> </script>
<style scoped> <style scoped>
/* Стили остаются без изменений */
.register-container { .register-container {
max-width: 500px; max-width: 500px;
margin: 0 auto; margin: 0 auto;
@@ -284,6 +307,11 @@ export default {
border-color: #2e8b57; border-color: #2e8b57;
} }
.form-input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.checkbox-group { .checkbox-group {
margin: 1.5rem 0; margin: 1.5rem 0;
} }
Binary file not shown.
+1 -1
View File
@@ -113,7 +113,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
Value: token, Value: token,
Path: "/", Path: "/",
HttpOnly: true, HttpOnly: true,
Secure: false, // В production установить true Secure: false, // В production установить true :TODO
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(24 * time.Hour), Expires: time.Now().Add(24 * time.Hour),
}) })