index.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. <script setup lang="ts">
  2. import { NForm, NFormItem, NInput, NButton, NCheckbox, NCarousel } from 'naive-ui'
  3. import {
  4. computed,
  5. defineAsyncComponent,
  6. onMounted,
  7. onUnmounted,
  8. reactive,
  9. ref,
  10. useTemplateRef,
  11. } from 'vue'
  12. import topographySvg from '@/assets/topography.svg'
  13. import { usePersonalization, useInjection } from '@/composable'
  14. import { mediaQueryInjectionKey } from '@/injection'
  15. import ThemeDropdown from '@/layouts/header/actions/component/ThemeDropdown.vue'
  16. import router from '@/router'
  17. import { useUserStore } from '@/stores'
  18. import twc from '@/utils/tailwindColor'
  19. import ThemeColorPopover from './component/ThemeColorPopover.vue'
  20. import type { FormItemRule } from 'naive-ui'
  21. defineOptions({
  22. name: 'SignIn',
  23. })
  24. const { isDark } = usePersonalization()
  25. const { isSmallScreen } = useInjection(mediaQueryInjectionKey)
  26. const userStore = useUserStore()
  27. const illustrations = [
  28. defineAsyncComponent(() => import('./component/Illustration1.vue')),
  29. defineAsyncComponent(() => import('./component/Illustration2.vue')),
  30. defineAsyncComponent(() => import('./component/Illustration3.vue')),
  31. ]
  32. const loading = ref(false)
  33. const isRememberMed = ref(false)
  34. const textureMaskParams = reactive({
  35. size: '666px 666px',
  36. x: 0,
  37. y: 0,
  38. })
  39. const textureStyle = computed(() => {
  40. return {
  41. filter: isDark.value ? 'invert(0.18)' : 'invert(0.86)',
  42. maskImage: `radial-gradient(circle 200px at ${textureMaskParams.x}px ${textureMaskParams.y}px, #f0f 0%, transparent 100%)`,
  43. WebkitMaskImage: `radial-gradient(circle 200px at ${textureMaskParams.x}px ${textureMaskParams.y}px, #f0f 0%, transparent 100%)`,
  44. }
  45. })
  46. const signInFormRef = useTemplateRef<InstanceType<typeof NForm>>('signInFormRef')
  47. const signInForm = reactive({
  48. account: 'admin',
  49. password: '123456',
  50. })
  51. const signInFormRules: Record<string, FormItemRule[]> = {
  52. account: [{ required: true, message: '请输入账号', trigger: ['input'] }],
  53. password: [{ required: true, message: '请输入密码', trigger: ['input'] }],
  54. }
  55. function toLayouts() {
  56. const { r } = router.currentRoute.value.query
  57. setTimeout(() => {
  58. if (signInForm.account.includes('admin')) {
  59. userStore.setToken('admin')
  60. } else {
  61. userStore.setToken('user')
  62. }
  63. router.replace({
  64. path: (r as string) || '/',
  65. })
  66. loading.value = false
  67. }, 1000)
  68. }
  69. const handleSubmitClick = () => {
  70. signInFormRef.value?.validate((errors) => {
  71. if (!errors) {
  72. loading.value = true
  73. toLayouts()
  74. }
  75. })
  76. }
  77. function updateTexturePosition(x: number, y: number) {
  78. textureMaskParams.x = x
  79. textureMaskParams.y = y
  80. }
  81. function onMouseMove(e: MouseEvent) {
  82. updateTexturePosition(e.clientX, e.clientY)
  83. }
  84. function onTouchMove(e: TouchEvent) {
  85. updateTexturePosition(e.touches[0].clientX, e.touches[0].clientY)
  86. }
  87. onMounted(() => {
  88. window.addEventListener('mousemove', onMouseMove)
  89. window.addEventListener('touchmove', onTouchMove)
  90. })
  91. onUnmounted(() => {
  92. window.removeEventListener('mousemove', onMouseMove)
  93. window.removeEventListener('touchmove', onTouchMove)
  94. })
  95. </script>
  96. <template>
  97. <div
  98. class="relative flex h-svh items-center justify-center overflow-hidden bg-neutral-50 p-6 transition-[background-color] dark:bg-neutral-900"
  99. >
  100. <div
  101. class="absolute top-0 left-0 size-full bg-neutral-200/45 transition-[background-color] dark:bg-neutral-800/50"
  102. :style="{
  103. 'mask-image': `url(${topographySvg})`,
  104. '-webkit-mask-image': `url(${topographySvg})`,
  105. 'mask-repeat': 'repeat',
  106. 'mask-size': textureMaskParams.size,
  107. '-webkit-mask-repeat': 'repeat',
  108. '-webkit-mask-size': textureMaskParams.size,
  109. }"
  110. />
  111. <div
  112. class="pointer-events-none absolute inset-0 z-10 transition-[filter]"
  113. :style="{
  114. 'background-image': `url(${topographySvg})`,
  115. 'background-size': textureMaskParams.size,
  116. '-webkit-mask-repeat': 'no-repeat',
  117. 'mask-repeat': 'no-repeat',
  118. ...textureStyle,
  119. }"
  120. />
  121. <div class="relative z-50 flex h-[480px] w-[800px] justify-center rounded shadow-lg">
  122. <div
  123. v-if="!isSmallScreen"
  124. class="flex-1 bg-neutral-50 py-6 pl-6 text-primary transition-[background-color] dark:bg-neutral-850"
  125. >
  126. <NCarousel
  127. draggable
  128. :show-dots="false"
  129. :show-arrow="false"
  130. loop
  131. autoplay
  132. >
  133. <div
  134. v-for="(illustration, index) in illustrations"
  135. :key="index"
  136. class="flex h-full items-center p-5"
  137. >
  138. <component :is="illustration" />
  139. </div>
  140. </NCarousel>
  141. </div>
  142. <div
  143. class="relative flex w-full flex-col bg-white px-10 py-12 transition-[background-color] sm:w-[340px] dark:bg-neutral-800"
  144. >
  145. <div class="absolute top-0 left-0 z-50 flex w-full items-center justify-end gap-x-4 p-4">
  146. <ThemeColorPopover />
  147. <ThemeDropdown />
  148. </div>
  149. <div>
  150. <div>
  151. <h2 class="text-2xl">登&nbsp;录</h2>
  152. <p class="text-neutral-400 transition-[color] dark:text-neutral-500">SIGN IN</p>
  153. </div>
  154. <div class="mt-12">
  155. <NForm
  156. ref="signInFormRef"
  157. :model="signInForm"
  158. :show-require-mark="false"
  159. :rules="signInFormRules"
  160. >
  161. <NFormItem
  162. path="account"
  163. label="账号"
  164. >
  165. <NInput
  166. v-model:value="signInForm.account"
  167. placeholder="请输入账号"
  168. clearable
  169. :input-props="{
  170. autocomplete: 'off',
  171. }"
  172. >
  173. <template #prefix>
  174. <span class="mr-1.5 iconify size-5.5 ph--user-circle" />
  175. </template>
  176. </NInput>
  177. </NFormItem>
  178. <NFormItem
  179. path="password"
  180. label="密码"
  181. >
  182. <NInput
  183. v-model:value="signInForm.password"
  184. placeholder="请输入密码"
  185. type="password"
  186. clearable
  187. :input-props="{
  188. autocomplete: 'off',
  189. }"
  190. >
  191. <template #prefix>
  192. <span class="mr-1.5 iconify size-5.5 ph--lock-key" />
  193. </template>
  194. </NInput>
  195. </NFormItem>
  196. <div class="flex justify-between">
  197. <NCheckbox
  198. :theme-overrides="{
  199. border: `1px solid ${twc.neutral[isDark ? 650 : 300]}`,
  200. }"
  201. v-model:checked="isRememberMed"
  202. >记住我</NCheckbox
  203. >
  204. <NButton
  205. text
  206. size="small"
  207. >忘记密码</NButton
  208. >
  209. </div>
  210. <div class="mt-4">
  211. <NButton
  212. type="primary"
  213. :loading="loading"
  214. attr-type="button"
  215. block
  216. size="medium"
  217. @click="handleSubmitClick()"
  218. >
  219. 登&nbsp;录
  220. </NButton>
  221. </div>
  222. </NForm>
  223. </div>
  224. </div>
  225. </div>
  226. </div>
  227. </div>
  228. </template>