This commit is contained in:
parent
de0c9ca198
commit
2a7e851cda
1225
package-lock.json
generated
1225
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,18 +17,21 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"lucide-vue-next": "^0.562.0",
|
"lucide-vue-next": "^0.562.0",
|
||||||
"reka-ui": "^2.7.0",
|
"reka-ui": "^2.8.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
|
"vee-validate": "^4.15.1",
|
||||||
"vue": "^3.5.26",
|
"vue": "^3.5.26",
|
||||||
"vue-router": "^4.6.4",
|
"vue-router": "^4.6.4",
|
||||||
"vue-sonner": "^2.0.9"
|
"vue-sonner": "^2.0.9",
|
||||||
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node24": "^24.0.3",
|
"@tsconfig/node24": "^24.0.3",
|
||||||
|
|||||||
52
pnpm-lock.yaml
generated
52
pnpm-lock.yaml
generated
@ -38,6 +38,9 @@ importers:
|
|||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.1.18
|
specifier: ^4.1.18
|
||||||
version: 4.1.18
|
version: 4.1.18
|
||||||
|
vee-validate:
|
||||||
|
specifier: ^4.15.1
|
||||||
|
version: 4.15.1(vue@3.5.26(typescript@5.9.3))
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.26
|
specifier: ^3.5.26
|
||||||
version: 3.5.26(typescript@5.9.3)
|
version: 3.5.26(typescript@5.9.3)
|
||||||
@ -847,14 +850,23 @@ packages:
|
|||||||
'@vue/devtools-api@6.6.4':
|
'@vue/devtools-api@6.6.4':
|
||||||
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
||||||
|
|
||||||
|
'@vue/devtools-api@7.7.9':
|
||||||
|
resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==}
|
||||||
|
|
||||||
'@vue/devtools-core@8.0.5':
|
'@vue/devtools-core@8.0.5':
|
||||||
resolution: {integrity: sha512-dpCw8nl0GDBuiL9SaY0mtDxoGIEmU38w+TQiYEPOLhW03VDC0lfNMYXS/qhl4I0YlysGp04NLY4UNn6xgD0VIQ==}
|
resolution: {integrity: sha512-dpCw8nl0GDBuiL9SaY0mtDxoGIEmU38w+TQiYEPOLhW03VDC0lfNMYXS/qhl4I0YlysGp04NLY4UNn6xgD0VIQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.0.0
|
vue: ^3.0.0
|
||||||
|
|
||||||
|
'@vue/devtools-kit@7.7.9':
|
||||||
|
resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==}
|
||||||
|
|
||||||
'@vue/devtools-kit@8.0.5':
|
'@vue/devtools-kit@8.0.5':
|
||||||
resolution: {integrity: sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==}
|
resolution: {integrity: sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==}
|
||||||
|
|
||||||
|
'@vue/devtools-shared@7.7.9':
|
||||||
|
resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==}
|
||||||
|
|
||||||
'@vue/devtools-shared@8.0.5':
|
'@vue/devtools-shared@8.0.5':
|
||||||
resolution: {integrity: sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==}
|
resolution: {integrity: sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==}
|
||||||
|
|
||||||
@ -1678,6 +1690,9 @@ packages:
|
|||||||
pathe@2.0.3:
|
pathe@2.0.3:
|
||||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||||
|
|
||||||
|
perfect-debounce@1.0.0:
|
||||||
|
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||||
|
|
||||||
perfect-debounce@2.0.0:
|
perfect-debounce@2.0.0:
|
||||||
resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==}
|
resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==}
|
||||||
|
|
||||||
@ -1907,6 +1922,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
type-fest@4.41.0:
|
||||||
|
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
typescript-eslint@8.50.1:
|
typescript-eslint@8.50.1:
|
||||||
resolution: {integrity: sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==}
|
resolution: {integrity: sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@ -1938,6 +1957,11 @@ packages:
|
|||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
|
vee-validate@4.15.1:
|
||||||
|
resolution: {integrity: sha512-DkFsiTwEKau8VIxyZBGdO6tOudD+QoUBPuHj3e6QFqmbfCRj1ArmYWue9lEp6jLSWBIw4XPlDLjFIZNLdRAMSg==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.4.26
|
||||||
|
|
||||||
vite-dev-rpc@1.1.0:
|
vite-dev-rpc@1.1.0:
|
||||||
resolution: {integrity: sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==}
|
resolution: {integrity: sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -2818,6 +2842,10 @@ snapshots:
|
|||||||
|
|
||||||
'@vue/devtools-api@6.6.4': {}
|
'@vue/devtools-api@6.6.4': {}
|
||||||
|
|
||||||
|
'@vue/devtools-api@7.7.9':
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-kit': 7.7.9
|
||||||
|
|
||||||
'@vue/devtools-core@8.0.5(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.26(typescript@5.9.3))':
|
'@vue/devtools-core@8.0.5(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.26(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-kit': 8.0.5
|
'@vue/devtools-kit': 8.0.5
|
||||||
@ -2830,6 +2858,16 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- vite
|
- vite
|
||||||
|
|
||||||
|
'@vue/devtools-kit@7.7.9':
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-shared': 7.7.9
|
||||||
|
birpc: 2.9.0
|
||||||
|
hookable: 5.5.3
|
||||||
|
mitt: 3.0.1
|
||||||
|
perfect-debounce: 1.0.0
|
||||||
|
speakingurl: 14.0.1
|
||||||
|
superjson: 2.2.6
|
||||||
|
|
||||||
'@vue/devtools-kit@8.0.5':
|
'@vue/devtools-kit@8.0.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-shared': 8.0.5
|
'@vue/devtools-shared': 8.0.5
|
||||||
@ -2840,6 +2878,10 @@ snapshots:
|
|||||||
speakingurl: 14.0.1
|
speakingurl: 14.0.1
|
||||||
superjson: 2.2.6
|
superjson: 2.2.6
|
||||||
|
|
||||||
|
'@vue/devtools-shared@7.7.9':
|
||||||
|
dependencies:
|
||||||
|
rfdc: 1.4.1
|
||||||
|
|
||||||
'@vue/devtools-shared@8.0.5':
|
'@vue/devtools-shared@8.0.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
rfdc: 1.4.1
|
rfdc: 1.4.1
|
||||||
@ -3625,6 +3667,8 @@ snapshots:
|
|||||||
|
|
||||||
pathe@2.0.3: {}
|
pathe@2.0.3: {}
|
||||||
|
|
||||||
|
perfect-debounce@1.0.0: {}
|
||||||
|
|
||||||
perfect-debounce@2.0.0: {}
|
perfect-debounce@2.0.0: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
@ -3801,6 +3845,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
|
|
||||||
|
type-fest@4.41.0: {}
|
||||||
|
|
||||||
typescript-eslint@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
typescript-eslint@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/eslint-plugin': 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
@ -3833,6 +3879,12 @@ snapshots:
|
|||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
|
vee-validate@4.15.1(vue@3.5.26(typescript@5.9.3)):
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-api': 7.7.9
|
||||||
|
type-fest: 4.41.0
|
||||||
|
vue: 3.5.26(typescript@5.9.3)
|
||||||
|
|
||||||
vite-dev-rpc@1.1.0(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)):
|
vite-dev-rpc@1.1.0(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
birpc: 2.9.0
|
birpc: 2.9.0
|
||||||
|
|||||||
@ -1,31 +1,3 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { X } from "lucide-vue-next"
|
|
||||||
import {
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogPortal,
|
|
||||||
useForwardPropsEmits,
|
|
||||||
} from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import DialogOverlay from "./DialogOverlay.vue"
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
inheritAttrs: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<DialogContentProps & { class?: HTMLAttributes["class"], showCloseButton?: boolean }>(), {
|
|
||||||
showCloseButton: true,
|
|
||||||
})
|
|
||||||
const emits = defineEmits<DialogContentEmits>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
@ -36,14 +8,15 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|||||||
cn(
|
cn(
|
||||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||||
props.class,
|
props.class,
|
||||||
)"
|
)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<DialogClose
|
<DialogClose
|
||||||
v-if="showCloseButton"
|
v-if="showCloseButton"
|
||||||
data-slot="dialog-close"
|
data-slot="dialog-close"
|
||||||
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-6 right-6 cursor-pointer rounded-xs font-bold transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-6"
|
||||||
>
|
>
|
||||||
<X />
|
<X />
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
@ -51,3 +24,31 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { X } from 'lucide-vue-next'
|
||||||
|
import { DialogClose, DialogContent, DialogPortal, useForwardPropsEmits } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import DialogOverlay from './DialogOverlay.vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<
|
||||||
|
DialogContentProps & { class?: HTMLAttributes['class']; showCloseButton?: boolean }
|
||||||
|
>(),
|
||||||
|
{
|
||||||
|
showCloseButton: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const emits = defineEmits<DialogContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|||||||
17
src/components/ui/form/FormControl.vue
Normal file
17
src/components/ui/form/FormControl.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Slot } from "reka-ui"
|
||||||
|
import { useFormField } from "./useFormField"
|
||||||
|
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Slot
|
||||||
|
:id="formItemId"
|
||||||
|
data-slot="form-control"
|
||||||
|
:aria-describedby="!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`"
|
||||||
|
:aria-invalid="!!error"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Slot>
|
||||||
|
</template>
|
||||||
21
src/components/ui/form/FormDescription.vue
Normal file
21
src/components/ui/form/FormDescription.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { useFormField } from "./useFormField"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { formDescriptionId } = useFormField()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<p
|
||||||
|
:id="formDescriptionId"
|
||||||
|
data-slot="form-description"
|
||||||
|
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
23
src/components/ui/form/FormItem.vue
Normal file
23
src/components/ui/form/FormItem.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { useId } from "reka-ui"
|
||||||
|
import { provide } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { FORM_ITEM_INJECTION_KEY } from "./injectionKeys"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const id = useId()
|
||||||
|
provide(FORM_ITEM_INJECTION_KEY, id)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="form-item"
|
||||||
|
:class="cn('grid gap-2', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
25
src/components/ui/form/FormLabel.vue
Normal file
25
src/components/ui/form/FormLabel.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { LabelProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { useFormField } from "./useFormField"
|
||||||
|
|
||||||
|
const props = defineProps<LabelProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const { error, formItemId } = useFormField()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Label
|
||||||
|
data-slot="form-label"
|
||||||
|
:data-error="!!error"
|
||||||
|
:class="cn(
|
||||||
|
'data-[error=true]:text-destructive',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
:for="formItemId"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
23
src/components/ui/form/FormMessage.vue
Normal file
23
src/components/ui/form/FormMessage.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { ErrorMessage } from "vee-validate"
|
||||||
|
import { toValue } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { useFormField } from "./useFormField"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { name, formMessageId } = useFormField()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ErrorMessage
|
||||||
|
:id="formMessageId"
|
||||||
|
data-slot="form-message"
|
||||||
|
as="p"
|
||||||
|
:name="toValue(name)"
|
||||||
|
:class="cn('text-destructive text-sm', props.class)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
7
src/components/ui/form/index.ts
Normal file
7
src/components/ui/form/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export { default as FormControl } from "./FormControl.vue"
|
||||||
|
export { default as FormDescription } from "./FormDescription.vue"
|
||||||
|
export { default as FormItem } from "./FormItem.vue"
|
||||||
|
export { default as FormLabel } from "./FormLabel.vue"
|
||||||
|
export { default as FormMessage } from "./FormMessage.vue"
|
||||||
|
export { FORM_ITEM_INJECTION_KEY } from "./injectionKeys"
|
||||||
|
export { Form, Field as FormField, FieldArray as FormFieldArray } from "vee-validate"
|
||||||
4
src/components/ui/form/injectionKeys.ts
Normal file
4
src/components/ui/form/injectionKeys.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import type { InjectionKey } from "vue"
|
||||||
|
|
||||||
|
export const FORM_ITEM_INJECTION_KEY
|
||||||
|
= Symbol() as InjectionKey<string>
|
||||||
30
src/components/ui/form/useFormField.ts
Normal file
30
src/components/ui/form/useFormField.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { FieldContextKey } from "vee-validate"
|
||||||
|
import { computed, inject } from "vue"
|
||||||
|
import { FORM_ITEM_INJECTION_KEY } from "./injectionKeys"
|
||||||
|
|
||||||
|
export function useFormField() {
|
||||||
|
const fieldContext = inject(FieldContextKey)
|
||||||
|
const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY)
|
||||||
|
|
||||||
|
if (!fieldContext)
|
||||||
|
throw new Error("useFormField should be used within <FormField>")
|
||||||
|
|
||||||
|
const { name, errorMessage: error, meta } = fieldContext
|
||||||
|
const id = fieldItemContext
|
||||||
|
|
||||||
|
const fieldState = {
|
||||||
|
valid: computed(() => meta.valid),
|
||||||
|
isDirty: computed(() => meta.dirty),
|
||||||
|
isTouched: computed(() => meta.touched),
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
formItemId: `${id}-form-item`,
|
||||||
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
|
formMessageId: `${id}-form-item-message`,
|
||||||
|
...fieldState,
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/components/ui/label/Label.vue
Normal file
26
src/components/ui/label/Label.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { LabelProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { Label } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<LabelProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Label
|
||||||
|
data-slot="label"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
1
src/components/ui/label/index.ts
Normal file
1
src/components/ui/label/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as Label } from "./Label.vue"
|
||||||
19
src/components/ui/select/Select.vue
Normal file
19
src/components/ui/select/Select.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectRootEmits, SelectRootProps } from "reka-ui"
|
||||||
|
import { SelectRoot, useForwardPropsEmits } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<SelectRootProps>()
|
||||||
|
const emits = defineEmits<SelectRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectRoot
|
||||||
|
v-slot="slotProps"
|
||||||
|
data-slot="select"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<slot v-bind="slotProps" />
|
||||||
|
</SelectRoot>
|
||||||
|
</template>
|
||||||
51
src/components/ui/select/SelectContent.vue
Normal file
51
src/components/ui/select/SelectContent.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectContentEmits, SelectContentProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
SelectContent,
|
||||||
|
SelectPortal,
|
||||||
|
SelectViewport,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { SelectScrollDownButton, SelectScrollUpButton } from "."
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<SelectContentProps & { class?: HTMLAttributes["class"] }>(),
|
||||||
|
{
|
||||||
|
position: "popper",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const emits = defineEmits<SelectContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectPortal>
|
||||||
|
<SelectContent
|
||||||
|
data-slot="select-content"
|
||||||
|
v-bind="{ ...$attrs, ...forwarded }"
|
||||||
|
:class="cn(
|
||||||
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
||||||
|
position === 'popper'
|
||||||
|
&& 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectViewport :class="cn('p-1', position === 'popper' && 'h-[var(--reka-select-trigger-height)] w-full min-w-[var(--reka-select-trigger-width)] scroll-my-1')">
|
||||||
|
<slot />
|
||||||
|
</SelectViewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectContent>
|
||||||
|
</SelectPortal>
|
||||||
|
</template>
|
||||||
15
src/components/ui/select/SelectGroup.vue
Normal file
15
src/components/ui/select/SelectGroup.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectGroupProps } from "reka-ui"
|
||||||
|
import { SelectGroup } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<SelectGroupProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectGroup
|
||||||
|
data-slot="select-group"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</SelectGroup>
|
||||||
|
</template>
|
||||||
44
src/components/ui/select/SelectItem.vue
Normal file
44
src/components/ui/select/SelectItem.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectItemProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { Check } from "lucide-vue-next"
|
||||||
|
import {
|
||||||
|
SelectItem,
|
||||||
|
SelectItemIndicator,
|
||||||
|
SelectItemText,
|
||||||
|
useForwardProps,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<SelectItemProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectItem
|
||||||
|
data-slot="select-item"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'focus:bg-accent focus:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span class="absolute right-2 flex size-3.5 items-center justify-center">
|
||||||
|
<SelectItemIndicator>
|
||||||
|
<slot name="indicator-icon">
|
||||||
|
<Check class="size-4" />
|
||||||
|
</slot>
|
||||||
|
</SelectItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SelectItemText>
|
||||||
|
<slot />
|
||||||
|
</SelectItemText>
|
||||||
|
</SelectItem>
|
||||||
|
</template>
|
||||||
15
src/components/ui/select/SelectItemText.vue
Normal file
15
src/components/ui/select/SelectItemText.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectItemTextProps } from "reka-ui"
|
||||||
|
import { SelectItemText } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<SelectItemTextProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectItemText
|
||||||
|
data-slot="select-item-text"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</SelectItemText>
|
||||||
|
</template>
|
||||||
17
src/components/ui/select/SelectLabel.vue
Normal file
17
src/components/ui/select/SelectLabel.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectLabelProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { SelectLabel } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectLabel
|
||||||
|
data-slot="select-label"
|
||||||
|
:class="cn('text-muted-foreground px-2 py-1.5 text-xs', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</SelectLabel>
|
||||||
|
</template>
|
||||||
26
src/components/ui/select/SelectScrollDownButton.vue
Normal file
26
src/components/ui/select/SelectScrollDownButton.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectScrollDownButtonProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { ChevronDown } from "lucide-vue-next"
|
||||||
|
import { SelectScrollDownButton, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<ChevronDown class="size-4" />
|
||||||
|
</slot>
|
||||||
|
</SelectScrollDownButton>
|
||||||
|
</template>
|
||||||
26
src/components/ui/select/SelectScrollUpButton.vue
Normal file
26
src/components/ui/select/SelectScrollUpButton.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectScrollUpButtonProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { ChevronUp } from "lucide-vue-next"
|
||||||
|
import { SelectScrollUpButton, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectScrollUpButton
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<ChevronUp class="size-4" />
|
||||||
|
</slot>
|
||||||
|
</SelectScrollUpButton>
|
||||||
|
</template>
|
||||||
19
src/components/ui/select/SelectSeparator.vue
Normal file
19
src/components/ui/select/SelectSeparator.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectSeparatorProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { SelectSeparator } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectSeparator
|
||||||
|
data-slot="select-separator"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn('bg-border pointer-events-none -mx-1 my-1 h-px', props.class)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
33
src/components/ui/select/SelectTrigger.vue
Normal file
33
src/components/ui/select/SelectTrigger.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectTriggerProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { ChevronDown } from "lucide-vue-next"
|
||||||
|
import { SelectIcon, SelectTrigger, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<SelectTriggerProps & { class?: HTMLAttributes["class"], size?: "sm" | "default" }>(),
|
||||||
|
{ size: "default" },
|
||||||
|
)
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class", "size")
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectTrigger
|
||||||
|
data-slot="select-trigger"
|
||||||
|
:data-size="size"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn(
|
||||||
|
'border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<SelectIcon as-child>
|
||||||
|
<ChevronDown class="size-4 opacity-50" />
|
||||||
|
</SelectIcon>
|
||||||
|
</SelectTrigger>
|
||||||
|
</template>
|
||||||
15
src/components/ui/select/SelectValue.vue
Normal file
15
src/components/ui/select/SelectValue.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectValueProps } from "reka-ui"
|
||||||
|
import { SelectValue } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<SelectValueProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectValue
|
||||||
|
data-slot="select-value"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</SelectValue>
|
||||||
|
</template>
|
||||||
11
src/components/ui/select/index.ts
Normal file
11
src/components/ui/select/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export { default as Select } from "./Select.vue"
|
||||||
|
export { default as SelectContent } from "./SelectContent.vue"
|
||||||
|
export { default as SelectGroup } from "./SelectGroup.vue"
|
||||||
|
export { default as SelectItem } from "./SelectItem.vue"
|
||||||
|
export { default as SelectItemText } from "./SelectItemText.vue"
|
||||||
|
export { default as SelectLabel } from "./SelectLabel.vue"
|
||||||
|
export { default as SelectScrollDownButton } from "./SelectScrollDownButton.vue"
|
||||||
|
export { default as SelectScrollUpButton } from "./SelectScrollUpButton.vue"
|
||||||
|
export { default as SelectSeparator } from "./SelectSeparator.vue"
|
||||||
|
export { default as SelectTrigger } from "./SelectTrigger.vue"
|
||||||
|
export { default as SelectValue } from "./SelectValue.vue"
|
||||||
@ -25,7 +25,7 @@ import CopyIcon from '../UserInfo/copy.svg?component'
|
|||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
|
|
||||||
const tools = [
|
const tools = [
|
||||||
{ label: 'iOS外区账号教程', url: 'hifastvpn.com/tutorial' },
|
{ label: 'iOS外区账号教程', url: 'http://getsapp.net/F6Lmev' },
|
||||||
{ label: 'app视频使用教程', url: 'hifastvpn.com/tutorialvids' },
|
{ label: 'app视频使用教程', url: 'hifastvpn.com/tutorialvids' },
|
||||||
{ label: '软件防丢名片', url: 'hifastvpn.com/alwaysfindus' },
|
{ label: '软件防丢名片', url: 'hifastvpn.com/alwaysfindus' },
|
||||||
{ label: '官方Telegram群', url: 't.me/hifastvpnofficial' },
|
{ label: '官方Telegram群', url: 't.me/hifastvpnofficial' },
|
||||||
|
|||||||
@ -0,0 +1,218 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog v-model:open="open">
|
||||||
|
<DialogContent
|
||||||
|
class="gap-0 rounded-[40px]! border-none bg-[#111111] px-12 py-10 text-white shadow-2xl! ring-0! outline-hidden! sm:max-w-[500px]"
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle class="text-left text-2xl font-bold tracking-tight text-white"
|
||||||
|
>佣金提现</DialogTitle
|
||||||
|
>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div class="my-4 text-left text-sm font-medium text-white/30">
|
||||||
|
{{
|
||||||
|
values.type === 'USDT(TRC20)'
|
||||||
|
? '将佣金提现至您的个人数字钱包,无手续费'
|
||||||
|
: '该方式涉及平台手续费,预计 1-3 个工作日到账'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit="onSubmit" class="space-y-[20px]">
|
||||||
|
<FormField v-slot="{ componentField }" name="type">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel class="block text-sm font-bold text-white">提现方式</FormLabel>
|
||||||
|
<Select v-bind="componentField">
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger
|
||||||
|
class="!h-[46px] w-full rounded-[32px] border-[4px] border-[#ADFF5B] bg-transparent px-6 text-sm font-medium text-[#ADFF5B] outline-hidden! focus:ring-0! [&_svg]:size-6 [&_svg]:text-[#ADFF5B] [&_svg]:opacity-100"
|
||||||
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent class="rounded-2xl border-white/10 bg-[#222222] text-white">
|
||||||
|
<SelectItem
|
||||||
|
v-for="t in ACCOUNT_TYPE"
|
||||||
|
:key="t"
|
||||||
|
:value="t"
|
||||||
|
class="py-3 text-sm focus:bg-[#ADFF5B] focus:text-black"
|
||||||
|
>
|
||||||
|
{{ t === 'USDT(TRC20)' ? 'USDT' : t }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-if="values.type === 'USDT(TRC20)'" v-slot="{ componentField }" name="account">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel class="block text-sm font-bold text-white">提现地址</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
v-bind="componentField"
|
||||||
|
class="h-[46px] rounded-[32px] border-none bg-[#222222] px-6 text-sm text-white transition-colors placeholder:text-white/20 focus:bg-[#2a2a2a]"
|
||||||
|
placeholder="输入地址"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage class="ml-4 text-red-400" />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-slot="{ componentField }" name="money">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel class="block text-sm font-bold text-white">提现金额</FormLabel>
|
||||||
|
<div
|
||||||
|
class="group relative flex h-[46px] items-center overflow-hidden rounded-[32px] bg-[#ADFF5B] p-[3px]"
|
||||||
|
>
|
||||||
|
<div class="flex h-full items-center px-6 text-xl font-bold text-black">¥</div>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
v-bind="componentField"
|
||||||
|
type="number"
|
||||||
|
class="h-full flex-1 rounded-[28px]! border-none bg-[#222222] px-4 text-center text-sm font-bold text-white placeholder:text-white/20 focus:ring-0! focus-visible:ring-0!"
|
||||||
|
placeholder="不小于200RMB"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<div class="flex h-full items-center px-6 text-sm font-bold text-black">RMB</div>
|
||||||
|
</div>
|
||||||
|
<FormMessage class="ml-4 text-red-400" />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-if="values.type !== 'USDT(TRC20)'" name="avatar">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel class="mb-4 block text-sm font-bold text-white">收款码</FormLabel>
|
||||||
|
<div
|
||||||
|
class="flex cursor-pointer flex-col items-center justify-center rounded-[32px] border-2 border-dashed border-white/10 bg-[#222222] p-10 transition-all hover:border-[#ADFF5B]/50 hover:bg-[#2a2a2a]"
|
||||||
|
>
|
||||||
|
<Upload class="mb-3 h-12 w-12 text-white/20" />
|
||||||
|
<span class="text-base font-medium text-white/30">点击上传高清收款二维码</span>
|
||||||
|
</div>
|
||||||
|
<FormMessage class="ml-4 text-red-400" />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<div class="flex justify-center pt-2">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
:disabled="isPending"
|
||||||
|
class="h-[30px] w-[100px] rounded-full bg-[#ADFF5B] text-sm font-black text-black shadow-[0_10px_30px_rgba(173,255,91,0.3)] transition-all hover:scale-105 hover:bg-[#9ded4e] active:scale-95 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Loader2 v-if="isPending" class="mr-2 h-7 w-7 animate-spin" />
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
|
import { Upload, Loader2 } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
// Shadcn UI 组件
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
|
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
|
||||||
|
// 业务 API
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
const props = defineProps<{ commission: number }>()
|
||||||
|
const emit = defineEmits(['confirm'])
|
||||||
|
|
||||||
|
const open = ref(false)
|
||||||
|
const isPending = ref(false) // 手动管理加载状态
|
||||||
|
const ACCOUNT_TYPE = ['USDT(TRC20)', '微信', '支付宝'] as const
|
||||||
|
|
||||||
|
// --- 表单验证 Schema ---
|
||||||
|
const formSchema = toTypedSchema(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.enum(ACCOUNT_TYPE),
|
||||||
|
account: z.string().optional(),
|
||||||
|
money: z
|
||||||
|
.string()
|
||||||
|
.min(1, '金额不能为空')
|
||||||
|
.regex(/^\d+(\.\d+)?$/, '格式错误'),
|
||||||
|
avatar: z.string().optional(),
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.type === 'USDT(TRC20)' && !data.account) {
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: '地址不能为空', path: ['account'] })
|
||||||
|
} else if (data.type !== 'USDT(TRC20)' && !data.avatar) {
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: '请上传收款码', path: ['avatar'] })
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const { handleSubmit, resetForm, values } = useForm({
|
||||||
|
validationSchema: formSchema,
|
||||||
|
initialValues: { type: 'USDT(TRC20)', account: '', money: '', avatar: '' },
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- 暴露给父组件的方法 ---
|
||||||
|
const show = () => {
|
||||||
|
resetForm()
|
||||||
|
open.value = true
|
||||||
|
}
|
||||||
|
defineExpose({ show })
|
||||||
|
|
||||||
|
// --- 提交逻辑 ---
|
||||||
|
const onSubmit = handleSubmit(async (val) => {
|
||||||
|
const amount = parseFloat(val.money)
|
||||||
|
|
||||||
|
// 1. 基础校验
|
||||||
|
if (amount > props.commission / 100) return toast.error('超过最大佣金')
|
||||||
|
if (amount < 200) return toast.error('金额不能小于200')
|
||||||
|
|
||||||
|
try {
|
||||||
|
isPending.value = true
|
||||||
|
|
||||||
|
// 2. 检查是否有未完成工单
|
||||||
|
const data = await request.get<any>('/v1/public/ticket/list', {
|
||||||
|
page: 1,
|
||||||
|
size: 1,
|
||||||
|
issue_type: 1,
|
||||||
|
})
|
||||||
|
if (data?.list?.length > 0) {
|
||||||
|
toast.info('已有待处理申请')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 执行提现请求
|
||||||
|
const description =
|
||||||
|
val.type === 'USDT(TRC20)' ? `${val.type}-${val.account}` : `${val.type}-${val.avatar}`
|
||||||
|
|
||||||
|
await request.post('/v1/public/ticket/', {
|
||||||
|
title: `提现-${val.money}`,
|
||||||
|
description,
|
||||||
|
issue_type: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. 成功后处理
|
||||||
|
toast.success('提交成功')
|
||||||
|
open.value = false
|
||||||
|
emit('confirm') // 触发父组件刷新 info 接口
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提现失败:', error)
|
||||||
|
toast.error('操作失败,请重试')
|
||||||
|
} finally {
|
||||||
|
isPending.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@ -28,14 +28,23 @@
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xl font-semibold">佣金账户余额</div>
|
<div class="text-xl font-semibold">佣金账户余额</div>
|
||||||
<div class="text-3xl font-black">$12344</div>
|
<div class="text-3xl font-black">
|
||||||
|
$ {{ (userInfo.commission / 100 || 0).toFixed(2) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" class="hover:bg-transparent">点击提现</Button>
|
<Button variant="ghost" class="hover:bg-transparent" @click="walletDialogRef?.show()">
|
||||||
|
点击提现
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<WalletDialog
|
||||||
|
ref="walletDialogRef"
|
||||||
|
:commission="userInfo.commission || 0"
|
||||||
|
@confirm="init"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col gap-[10px] px-6 pt-8 pb-9">
|
<div class="flex flex-col gap-[10px] px-6 pt-8 pb-9">
|
||||||
<div class="h-[50px] w-full rounded-[32px] bg-[#222222] px-4 leading-[50px] font-medium">
|
<div class="h-[50px] w-full rounded-[32px] bg-[#222222] px-4 leading-[50px] font-medium">
|
||||||
@ -60,6 +69,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import UserCenterSkeleton from '@/components/user-center/UserCenterSkeleton.vue'
|
import UserCenterSkeleton from '@/components/user-center/UserCenterSkeleton.vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import WalletDialog from './components/WalletDialog.vue'
|
||||||
import CopyIcon from './copy.svg?component'
|
import CopyIcon from './copy.svg?component'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
@ -67,9 +77,12 @@ import { useRouter } from 'vue-router'
|
|||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const walletDialogRef = ref<InstanceType<typeof WalletDialog> | null>(null)
|
||||||
const userInfo = ref({
|
const userInfo = ref({
|
||||||
email: '',
|
email: '',
|
||||||
created_at: '',
|
created_at: '',
|
||||||
|
share_link: '',
|
||||||
|
commission: 0,
|
||||||
})
|
})
|
||||||
const isUserLoading = ref(true)
|
const isUserLoading = ref(true)
|
||||||
async function init() {
|
async function init() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user