LoginModal.vue 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. <template>
  2. <div v-if="visible" class="modal-overlay" @click.self="handleCancel">
  3. <div class="modal-content">
  4. <div class="modal-header">
  5. <h2>登录</h2>
  6. </div>
  7. <div class="modal-body">
  8. <div v-if="error" class="error-message">{{ error }}</div>
  9. <form @submit.prevent="handleSubmit">
  10. <div class="form-group">
  11. <label for="username">账号</label>
  12. <input
  13. id="username"
  14. v-model="formData.username"
  15. type="text"
  16. placeholder="请输入账号"
  17. required
  18. autocomplete="username"
  19. />
  20. </div>
  21. <div class="form-group">
  22. <label for="password">密码</label>
  23. <div class="password-input-wrapper">
  24. <input
  25. id="password"
  26. v-model="formData.password"
  27. :type="showPassword ? 'text' : 'password'"
  28. placeholder="请输入密码"
  29. required
  30. autocomplete="current-password"
  31. />
  32. <button
  33. type="button"
  34. class="password-toggle"
  35. @click="togglePasswordVisibility"
  36. :title="showPassword ? '隐藏密码' : '显示密码'"
  37. >
  38. <svg
  39. v-if="!showPassword"
  40. xmlns="http://www.w3.org/2000/svg"
  41. width="20"
  42. height="20"
  43. viewBox="0 0 24 24"
  44. fill="none"
  45. stroke="currentColor"
  46. stroke-width="2"
  47. stroke-linecap="round"
  48. stroke-linejoin="round"
  49. >
  50. <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
  51. <circle cx="12" cy="12" r="3"></circle>
  52. </svg>
  53. <svg
  54. v-else
  55. xmlns="http://www.w3.org/2000/svg"
  56. width="20"
  57. height="20"
  58. viewBox="0 0 24 24"
  59. fill="none"
  60. stroke="currentColor"
  61. stroke-width="2"
  62. stroke-linecap="round"
  63. stroke-linejoin="round"
  64. >
  65. <path
  66. d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"
  67. ></path>
  68. <line x1="1" y1="1" x2="23" y2="23"></line>
  69. </svg>
  70. </button>
  71. </div>
  72. </div>
  73. <div class="form-actions">
  74. <button type="submit" class="btn-primary" :disabled="loading">
  75. {{ loading ? '登录中...' : '登录' }}
  76. </button>
  77. </div>
  78. </form>
  79. </div>
  80. </div>
  81. </div>
  82. </template>
  83. <script setup lang="ts">
  84. import { ref, reactive } from 'vue'
  85. import { login } from '../api/auth'
  86. import { setToken } from '../utils/auth'
  87. interface Props {
  88. visible: boolean
  89. }
  90. interface Emits {
  91. (e: 'success'): void
  92. (e: 'cancel'): void
  93. }
  94. defineProps<Props>()
  95. const emit = defineEmits<Emits>()
  96. const formData = reactive({
  97. username: '',
  98. password: ''
  99. })
  100. const loading = ref(false)
  101. const error = ref('')
  102. const showPassword = ref(false)
  103. const togglePasswordVisibility = () => {
  104. showPassword.value = !showPassword.value
  105. }
  106. const handleSubmit = async () => {
  107. if (!formData.username || !formData.password) {
  108. error.value = '请输入账号和密码'
  109. return
  110. }
  111. loading.value = true
  112. error.value = ''
  113. try {
  114. const response = await login({
  115. username: formData.username,
  116. password: formData.password
  117. })
  118. setToken(response.token)
  119. emit('success')
  120. } catch (err: any) {
  121. error.value = err.message || '登录失败,请稍后重试'
  122. console.error('Login failed:', err)
  123. } finally {
  124. loading.value = false
  125. }
  126. }
  127. const handleCancel = () => {
  128. if (!loading.value) {
  129. emit('cancel')
  130. }
  131. }
  132. </script>
  133. <style scoped>
  134. .modal-overlay {
  135. position: fixed;
  136. top: 0;
  137. left: 0;
  138. right: 0;
  139. bottom: 0;
  140. background: rgba(0, 0, 0, 0.9);
  141. display: flex;
  142. align-items: center;
  143. justify-content: center;
  144. z-index: 1000;
  145. }
  146. .modal-content {
  147. background: #1a1a1a;
  148. border: 2px solid #00ff88;
  149. border-radius: 8px;
  150. width: 90%;
  151. max-width: 400px;
  152. box-shadow: 0 0 30px rgba(0, 255, 136, 0.3);
  153. }
  154. .modal-header {
  155. padding: 20px;
  156. border-bottom: 1px solid #333;
  157. }
  158. .modal-header h2 {
  159. margin: 0;
  160. color: #00ff88;
  161. font-size: 24px;
  162. text-align: center;
  163. }
  164. .modal-body {
  165. padding: 30px 20px;
  166. }
  167. .error-message {
  168. background: rgba(255, 68, 68, 0.15);
  169. border: 1px solid #ff4444;
  170. color: #ff6666;
  171. padding: 12px 16px;
  172. border-radius: 6px;
  173. margin-bottom: 20px;
  174. text-align: center;
  175. font-size: 14px;
  176. animation: shake 0.5s;
  177. box-shadow: 0 0 15px rgba(255, 68, 68, 0.2);
  178. }
  179. @keyframes shake {
  180. 0%,
  181. 100% {
  182. transform: translateX(0);
  183. }
  184. 10%,
  185. 30%,
  186. 50%,
  187. 70%,
  188. 90% {
  189. transform: translateX(-5px);
  190. }
  191. 20%,
  192. 40%,
  193. 60%,
  194. 80% {
  195. transform: translateX(5px);
  196. }
  197. }
  198. .form-group {
  199. margin-bottom: 20px;
  200. }
  201. .form-group label {
  202. display: block;
  203. color: #00ff88;
  204. margin-bottom: 8px;
  205. font-size: 14px;
  206. }
  207. .form-group input {
  208. width: 100%;
  209. padding: 12px;
  210. background: rgba(255, 255, 255, 0.05);
  211. border: 1px solid #333;
  212. border-radius: 4px;
  213. color: #fff;
  214. font-size: 14px;
  215. transition: all 0.3s;
  216. }
  217. .password-input-wrapper {
  218. position: relative;
  219. }
  220. .password-input-wrapper input {
  221. padding-right: 45px;
  222. }
  223. .password-toggle {
  224. position: absolute;
  225. right: 10px;
  226. top: 50%;
  227. transform: translateY(-50%);
  228. background: none;
  229. border: none;
  230. color: #666;
  231. cursor: pointer;
  232. padding: 5px;
  233. display: flex;
  234. align-items: center;
  235. justify-content: center;
  236. transition: color 0.3s;
  237. }
  238. .password-toggle:hover {
  239. color: #00ff88;
  240. }
  241. .form-group input:focus {
  242. outline: none;
  243. border-color: #00ff88;
  244. background: rgba(0, 255, 136, 0.05);
  245. }
  246. .form-group input::placeholder {
  247. color: #666;
  248. }
  249. .form-actions {
  250. margin-top: 30px;
  251. }
  252. .btn-primary {
  253. width: 100%;
  254. padding: 12px;
  255. background: #00ff88;
  256. border: none;
  257. border-radius: 4px;
  258. color: #000;
  259. font-size: 16px;
  260. font-weight: bold;
  261. cursor: pointer;
  262. transition: all 0.3s;
  263. }
  264. .btn-primary:hover:not(:disabled) {
  265. background: #00dd77;
  266. box-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
  267. }
  268. .btn-primary:disabled {
  269. opacity: 0.6;
  270. cursor: not-allowed;
  271. }
  272. </style>