第一次上传
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="max-w-full overflow-x-auto custom-scrollbar">
|
||||
<div id="chartOne" class="-ml-5 min-w-[650px] xl:min-w-full pl-2">
|
||||
<VueApexCharts type="bar" height="180" :options="chartOptions" :series="series" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
|
||||
const series = ref([
|
||||
{
|
||||
name: 'Sales',
|
||||
data: [168, 385, 201, 298, 187, 195, 291, 110, 215, 390, 280, 112],
|
||||
},
|
||||
])
|
||||
|
||||
const chartOptions = ref({
|
||||
colors: ['#465fff'],
|
||||
chart: {
|
||||
fontFamily: 'Outfit, sans-serif',
|
||||
type: 'bar',
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: '39%',
|
||||
borderRadius: 5,
|
||||
borderRadiusApplication: 'end',
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
show: true,
|
||||
width: 4,
|
||||
colors: ['transparent'],
|
||||
},
|
||||
xaxis: {
|
||||
categories: [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
],
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
horizontalAlign: 'left',
|
||||
fontFamily: 'Outfit',
|
||||
markers: {
|
||||
radius: 99,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
title: false,
|
||||
},
|
||||
grid: {
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
opacity: 1,
|
||||
},
|
||||
tooltip: {
|
||||
x: {
|
||||
show: false,
|
||||
},
|
||||
y: {
|
||||
formatter: function (val) {
|
||||
return val.toString()
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="max-w-full overflow-x-auto custom-scrollbar">
|
||||
<div id="chartThree" class="-ml-4 min-w-[1000px] xl:min-w-full pl-2">
|
||||
<VueApexCharts type="area" height="310" :options="chartOptions" :series="series" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
|
||||
const series = ref([
|
||||
{
|
||||
name: 'Sales',
|
||||
data: [180, 190, 170, 160, 175, 165, 170, 205, 230, 210, 240, 235],
|
||||
},
|
||||
{
|
||||
name: 'Revenue',
|
||||
data: [40, 30, 50, 40, 55, 40, 70, 100, 110, 120, 150, 140],
|
||||
},
|
||||
])
|
||||
|
||||
const chartOptions = ref({
|
||||
legend: {
|
||||
show: false,
|
||||
position: 'top',
|
||||
horizontalAlign: 'left',
|
||||
},
|
||||
colors: ['#465FFF', '#9CB9FF'],
|
||||
chart: {
|
||||
fontFamily: 'Outfit, sans-serif',
|
||||
type: 'area',
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
gradient: {
|
||||
enabled: true,
|
||||
opacityFrom: 0.55,
|
||||
opacityTo: 0,
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
curve: 'straight',
|
||||
width: [2, 2],
|
||||
},
|
||||
markers: {
|
||||
size: 0,
|
||||
},
|
||||
labels: {
|
||||
show: false,
|
||||
position: 'top',
|
||||
},
|
||||
grid: {
|
||||
xaxis: {
|
||||
lines: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
tooltip: {
|
||||
x: {
|
||||
format: 'dd MMM yyyy',
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: 'category',
|
||||
categories: [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
],
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
style: {
|
||||
fontSize: '0px',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.area-chart {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="absolute right-0 top-0 -z-1 w-full max-w-[250px] xl:max-w-[450px]">
|
||||
<img src="/images/shape/grid-01.svg" alt="grid" />
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 -z-1 w-full max-w-[250px] rotate-180 xl:max-w-[450px]">
|
||||
<img src="/images/shape/grid-01.svg" alt="grid" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]',
|
||||
className,
|
||||
]"
|
||||
>
|
||||
<!-- Card Header -->
|
||||
<div class="px-6 py-5">
|
||||
<h3 class="text-base font-medium text-gray-800 dark:text-white/90">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p v-if="desc" class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ desc }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card Body -->
|
||||
<div class="p-4 border-t border-gray-100 dark:border-gray-800 sm:p-6">
|
||||
<div class="space-y-5">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
className?: string
|
||||
desc?: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="mb-10">
|
||||
<div
|
||||
class="flex flex-wrap justify-center gap-1 mb-2 font-bold text-title-md text-brand-500 dark:text-brand-400 xl:text-title-lg"
|
||||
>
|
||||
<!-- timer days -->
|
||||
<div v-for="(day, index) in daysArray" :key="index">
|
||||
<div v-show="day.visible" class="timer-box">
|
||||
<span>{{ day.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
:
|
||||
|
||||
<!-- timer hours -->
|
||||
<div v-for="(hour, index) in hoursArray" :key="index">
|
||||
<div v-show="hour.visible" class="timer-box">
|
||||
<span>{{ hour.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
:
|
||||
|
||||
<!-- timer minutes -->
|
||||
<div v-for="(minute, index) in minutesArray" :key="index">
|
||||
<div v-show="minute.visible" class="timer-box">
|
||||
<span>{{ minute.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
:
|
||||
|
||||
<!-- timer seconds -->
|
||||
<div v-for="(second, index) in secondsArray" :key="index">
|
||||
<div v-show="second.visible" class="timer-box">
|
||||
<span>{{ second.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-base text-center text-gray-500 dark:text-gray-400">
|
||||
<div class="flex justify-center gap-0.5">
|
||||
<div v-for="(day, index) in daysArray" :key="index">
|
||||
<span v-show="day.visible" class="inline-block timer-box">
|
||||
<span class="inline-block">{{ day.value }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div>days left</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const daysArray = ref([])
|
||||
const hoursArray = ref([])
|
||||
const minutesArray = ref([])
|
||||
const secondsArray = ref([])
|
||||
const endTime = new Date('December 20, 2025 23:59:59 GMT+0530').getTime()
|
||||
const now = ref(new Date().getTime())
|
||||
const timeLeft = ref(0)
|
||||
|
||||
let counter
|
||||
|
||||
const countdown = () => {
|
||||
counter = setInterval(() => {
|
||||
now.value = new Date().getTime()
|
||||
timeLeft.value = (endTime - now.value) / 1000
|
||||
|
||||
updateTimeArrays()
|
||||
|
||||
if (timeLeft.value <= 0) {
|
||||
clearInterval(counter)
|
||||
resetTimeArrays()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const format = (value) => {
|
||||
if (value < 10) {
|
||||
return '0' + Math.floor(value)
|
||||
} else return Math.floor(value)
|
||||
}
|
||||
|
||||
const updateTimeArrays = () => {
|
||||
daysArray.value = getTimeArray(timeLeft.value / (60 * 60 * 24), 'days')
|
||||
hoursArray.value = getTimeArray((timeLeft.value / (60 * 60)) % 24, 'hours')
|
||||
minutesArray.value = getTimeArray((timeLeft.value / 60) % 60, 'minutes')
|
||||
secondsArray.value = getTimeArray(timeLeft.value % 60, 'seconds')
|
||||
}
|
||||
|
||||
const getMaxValueForUnit = (unit) => {
|
||||
switch (unit) {
|
||||
case 'days':
|
||||
return 365
|
||||
case 'hours':
|
||||
return 24
|
||||
case 'minutes':
|
||||
return 60
|
||||
case 'seconds':
|
||||
return 60
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
const getTimeArray = (value, unit) => {
|
||||
let stringValue = format(value).toString()
|
||||
let percentage = (value / getMaxValueForUnit(unit)) * 100
|
||||
return stringValue.split('').map((digit) => ({
|
||||
value: digit,
|
||||
visible: true,
|
||||
remainingPercentage: percentage,
|
||||
}))
|
||||
}
|
||||
|
||||
const calcOverlayHeight = () => {
|
||||
if (daysArray.value.length > 0) {
|
||||
let remainingDaysPercentage = daysArray.value[0].remainingPercentage
|
||||
return `${remainingDaysPercentage}%`
|
||||
}
|
||||
return '0%'
|
||||
}
|
||||
|
||||
const resetTimeArrays = () => {
|
||||
daysArray.value = [{ value: '0', visible: true }]
|
||||
hoursArray.value = [{ value: '0', visible: true }]
|
||||
minutesArray.value = [{ value: '0', visible: true }]
|
||||
secondsArray.value = [{ value: '0', visible: true }]
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
countdown()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (counter) {
|
||||
clearInterval(counter)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="relative" v-click-outside="closeDropdown" ref="dropdown">
|
||||
<!-- Dropdown Trigger Button -->
|
||||
<button @click="toggleDropdown" :class="buttonClass">
|
||||
<slot name="icon">
|
||||
<!-- Default icon -->
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5.99902 10.245C6.96552 10.245 7.74902 11.0285 7.74902 11.995V12.005C7.74902 12.9715 6.96552 13.755 5.99902 13.755C5.03253 13.755 4.24902 12.9715 4.24902 12.005V11.995C4.24902 11.0285 5.03253 10.245 5.99902 10.245ZM17.999 10.245C18.9655 10.245 19.749 11.0285 19.749 11.995V12.005C19.749 12.9715 18.9655 13.755 17.999 13.755C17.0325 13.755 16.249 12.9715 16.249 12.005V11.995C16.249 11.0285 17.0325 10.245 17.999 10.245ZM13.749 11.995C13.749 11.0285 12.9655 10.245 11.999 10.245C11.0325 10.245 10.249 11.0285 10.249 11.995V12.005C10.249 12.9715 11.0325 13.755 11.999 13.755C12.9655 13.755 13.749 12.9715 13.749 12.005V11.995Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</slot>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
<div v-if="open" :class="menuClass">
|
||||
<slot name="menu">
|
||||
<!-- Default menu items -->
|
||||
<template v-for="(item, index) in menuItems">
|
||||
<router-link
|
||||
v-if="item.to"
|
||||
:key="`router-${index}`"
|
||||
:to="item.to"
|
||||
@click.native="handleMenuItemClick(item.onClick)"
|
||||
:class="itemClass"
|
||||
>
|
||||
{{ item.label }}
|
||||
</router-link>
|
||||
|
||||
<button
|
||||
v-else
|
||||
:key="`button-${index}`"
|
||||
@click="handleMenuItemClick(item.onClick)"
|
||||
:class="itemClass"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</template>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import vClickOutside from './v-click-outside.vue'
|
||||
|
||||
const props = defineProps({
|
||||
menuItems: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
buttonClass: {
|
||||
type: String,
|
||||
default: 'text-gray-500 dark:text-gray-400',
|
||||
},
|
||||
menuClass: {
|
||||
type: String,
|
||||
default:
|
||||
'absolute right-0 z-40 w-40 p-2 space-y-1 bg-white border border-gray-200 top-full rounded-2xl shadow-lg dark:border-gray-800 dark:bg-gray-dark',
|
||||
},
|
||||
itemClass: {
|
||||
type: String,
|
||||
default:
|
||||
'flex w-full px-3 py-2 font-medium text-left text-gray-500 rounded-lg text-theme-xs hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300',
|
||||
},
|
||||
})
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
const toggleDropdown = () => {
|
||||
open.value = !open.value
|
||||
}
|
||||
|
||||
const closeDropdown = () => {
|
||||
open.value = false
|
||||
}
|
||||
|
||||
const handleMenuItemClick = (callback) => {
|
||||
if (typeof callback === 'function') {
|
||||
callback() // Execute the provided callback function
|
||||
}
|
||||
closeDropdown() // Close the dropdown after the item is clicked
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
directives: {
|
||||
clickOutside: vClickOutside,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div class="mb-6 flex flex-wrap items-center gap-3">
|
||||
<h2 class="text-xl font-semibold text-gray-800 dark:text-white/90" x-text="pageTitle">
|
||||
{{ pageTitle }}
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue'
|
||||
|
||||
interface BreadcrumbProps {
|
||||
pageTitle: string
|
||||
}
|
||||
|
||||
defineProps<BreadcrumbProps>()
|
||||
</script>
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<button
|
||||
class="relative flex items-center justify-center text-gray-500 transition-colors bg-white border border-gray-200 rounded-full hover:text-dark-900 h-11 w-11 hover:bg-gray-100 hover:text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
||||
@click.prevent="toggleTheme"
|
||||
>
|
||||
<svg
|
||||
class="hidden dark:block"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.99998 1.5415C10.4142 1.5415 10.75 1.87729 10.75 2.2915V3.5415C10.75 3.95572 10.4142 4.2915 9.99998 4.2915C9.58577 4.2915 9.24998 3.95572 9.24998 3.5415V2.2915C9.24998 1.87729 9.58577 1.5415 9.99998 1.5415ZM10.0009 6.79327C8.22978 6.79327 6.79402 8.22904 6.79402 10.0001C6.79402 11.7712 8.22978 13.207 10.0009 13.207C11.772 13.207 13.2078 11.7712 13.2078 10.0001C13.2078 8.22904 11.772 6.79327 10.0009 6.79327ZM5.29402 10.0001C5.29402 7.40061 7.40135 5.29327 10.0009 5.29327C12.6004 5.29327 14.7078 7.40061 14.7078 10.0001C14.7078 12.5997 12.6004 14.707 10.0009 14.707C7.40135 14.707 5.29402 12.5997 5.29402 10.0001ZM15.9813 5.08035C16.2742 4.78746 16.2742 4.31258 15.9813 4.01969C15.6884 3.7268 15.2135 3.7268 14.9207 4.01969L14.0368 4.90357C13.7439 5.19647 13.7439 5.67134 14.0368 5.96423C14.3297 6.25713 14.8045 6.25713 15.0974 5.96423L15.9813 5.08035ZM18.4577 10.0001C18.4577 10.4143 18.1219 10.7501 17.7077 10.7501H16.4577C16.0435 10.7501 15.7077 10.4143 15.7077 10.0001C15.7077 9.58592 16.0435 9.25013 16.4577 9.25013H17.7077C18.1219 9.25013 18.4577 9.58592 18.4577 10.0001ZM14.9207 15.9806C15.2135 16.2735 15.6884 16.2735 15.9813 15.9806C16.2742 15.6877 16.2742 15.2128 15.9813 14.9199L15.0974 14.036C14.8045 13.7431 14.3297 13.7431 14.0368 14.036C13.7439 14.3289 13.7439 14.8038 14.0368 15.0967L14.9207 15.9806ZM9.99998 15.7088C10.4142 15.7088 10.75 16.0445 10.75 16.4588V17.7088C10.75 18.123 10.4142 18.4588 9.99998 18.4588C9.58577 18.4588 9.24998 18.123 9.24998 17.7088V16.4588C9.24998 16.0445 9.58577 15.7088 9.99998 15.7088ZM5.96356 15.0972C6.25646 14.8043 6.25646 14.3295 5.96356 14.0366C5.67067 13.7437 5.1958 13.7437 4.9029 14.0366L4.01902 14.9204C3.72613 15.2133 3.72613 15.6882 4.01902 15.9811C4.31191 16.274 4.78679 16.274 5.07968 15.9811L5.96356 15.0972ZM4.29224 10.0001C4.29224 10.4143 3.95645 10.7501 3.54224 10.7501H2.29224C1.87802 10.7501 1.54224 10.4143 1.54224 10.0001C1.54224 9.58592 1.87802 9.25013 2.29224 9.25013H3.54224C3.95645 9.25013 4.29224 9.58592 4.29224 10.0001ZM4.9029 5.9637C5.1958 6.25659 5.67067 6.25659 5.96356 5.9637C6.25646 5.6708 6.25646 5.19593 5.96356 4.90303L5.07968 4.01915C4.78679 3.72626 4.31191 3.72626 4.01902 4.01915C3.72613 4.31204 3.72613 4.78692 4.01902 5.07981L4.9029 5.9637Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
class="dark:hidden"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17.4547 11.97L18.1799 12.1611C18.265 11.8383 18.1265 11.4982 17.8401 11.3266C17.5538 11.1551 17.1885 11.1934 16.944 11.4207L17.4547 11.97ZM8.0306 2.5459L8.57989 3.05657C8.80718 2.81209 8.84554 2.44682 8.67398 2.16046C8.50243 1.8741 8.16227 1.73559 7.83948 1.82066L8.0306 2.5459ZM12.9154 13.0035C9.64678 13.0035 6.99707 10.3538 6.99707 7.08524H5.49707C5.49707 11.1823 8.81835 14.5035 12.9154 14.5035V13.0035ZM16.944 11.4207C15.8869 12.4035 14.4721 13.0035 12.9154 13.0035V14.5035C14.8657 14.5035 16.6418 13.7499 17.9654 12.5193L16.944 11.4207ZM16.7295 11.7789C15.9437 14.7607 13.2277 16.9586 10.0003 16.9586V18.4586C13.9257 18.4586 17.2249 15.7853 18.1799 12.1611L16.7295 11.7789ZM10.0003 16.9586C6.15734 16.9586 3.04199 13.8433 3.04199 10.0003H1.54199C1.54199 14.6717 5.32892 18.4586 10.0003 18.4586V16.9586ZM3.04199 10.0003C3.04199 6.77289 5.23988 4.05695 8.22173 3.27114L7.83948 1.82066C4.21532 2.77574 1.54199 6.07486 1.54199 10.0003H3.04199ZM6.99707 7.08524C6.99707 5.52854 7.5971 4.11366 8.57989 3.05657L7.48132 2.03522C6.25073 3.35885 5.49707 5.13487 5.49707 7.08524H6.99707Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useTheme } from '../layout/ThemeProvider.vue'
|
||||
|
||||
const { toggleTheme } = useTheme()
|
||||
</script>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
|
||||
export default {
|
||||
created(el, binding) {
|
||||
el.clickOutsideEvent = (event) => {
|
||||
if (!(el === event.target || el.contains(event.target))) {
|
||||
binding.value(event)
|
||||
}
|
||||
}
|
||||
document.addEventListener('click', el.clickOutsideEvent)
|
||||
},
|
||||
unmounted(el) {
|
||||
document.removeEventListener('click', el.clickOutsideEvent)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div
|
||||
class="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] sm:p-6"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-white/90">
|
||||
Customers Demographic
|
||||
</h3>
|
||||
<p class="mt-1 text-gray-500 text-theme-sm dark:text-gray-400">
|
||||
Number of customer based on country
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="px-4 py-6 my-6 overflow-hidden border border-gary-200 rounded-2xl bg-gray-50 dark:border-gray-800 dark:bg-gray-900 sm:px-6"
|
||||
>
|
||||
<div
|
||||
ref="mapOneRef"
|
||||
id="mapOne"
|
||||
class="mapOne map-btn -mx-4 -my-6 h-[212px] w-[252px] 2xsm:w-[307px] xsm:w-[358px] sm:-mx-6 md:w-[668px] lg:w-[634px] xl:w-[393px] 2xl:w-[554px]"
|
||||
></div>
|
||||
</div>
|
||||
<div class="space-y-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="items-center w-full rounded-full max-w-8">
|
||||
<img src="/images/country/country-01.svg" alt="usa" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-gray-800 text-theme-sm dark:text-white/90">USA</p>
|
||||
<span class="block text-gray-500 text-theme-xs dark:text-gray-400">
|
||||
2,379 Customers
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full max-w-[140px] items-center gap-3">
|
||||
<div class="relative block h-2 w-full max-w-[100px] rounded-sm bg-gray-200 dark:bg-gray-800">
|
||||
<div
|
||||
class="absolute left-0 top-0 flex h-full w-[79%] items-center justify-center rounded-sm bg-brand-500 text-xs font-medium text-white"
|
||||
></div>
|
||||
</div>
|
||||
<p class="font-medium text-gray-800 text-theme-sm dark:text-white/90">79%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="items-center w-full rounded-full max-w-8">
|
||||
<img src="/images/country/country-02.svg" alt="france" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-gray-800 text-theme-sm dark:text-white/90">France</p>
|
||||
<span class="block text-gray-500 text-theme-xs dark:text-gray-400">
|
||||
589 Customers
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full max-w-[140px] items-center gap-3">
|
||||
<div class="relative block h-2 w-full max-w-[100px] rounded-sm bg-gray-200 dark:bg-gray-800">
|
||||
<div
|
||||
class="absolute left-0 top-0 flex h-full w-[23%] items-center justify-center rounded-sm bg-brand-500 text-xs font-medium text-white"
|
||||
></div>
|
||||
</div>
|
||||
<p class="font-medium text-gray-800 text-theme-sm dark:text-white/90">23%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import jsVectorMap from 'jsvectormap'
|
||||
import 'jsvectormap/dist/maps/world'
|
||||
|
||||
const mapOneRef = ref<HTMLElement | null>(null)
|
||||
const mapInstance = ref<any>(null)
|
||||
|
||||
const initMap = () => {
|
||||
if (mapOneRef.value) {
|
||||
mapInstance.value = new jsVectorMap({
|
||||
selector: mapOneRef.value,
|
||||
map: 'world',
|
||||
zoomButtons: false,
|
||||
regionStyle: {
|
||||
initial: {
|
||||
fontFamily: 'Outfit',
|
||||
fill: '#D9D9D9',
|
||||
},
|
||||
hover: {
|
||||
fillOpacity: 1,
|
||||
fill: '#465fff',
|
||||
},
|
||||
},
|
||||
markers: [
|
||||
{
|
||||
name: 'Egypt',
|
||||
coords: [26.8206, 30.8025],
|
||||
},
|
||||
{
|
||||
name: 'United States',
|
||||
coords: [55.3781, 3.436],
|
||||
},
|
||||
{
|
||||
name: 'United States',
|
||||
coords: [37.0902, -95.7129],
|
||||
},
|
||||
],
|
||||
markerStyle: {
|
||||
initial: {
|
||||
strokeWidth: 1,
|
||||
fill: '#465fff',
|
||||
fillOpacity: 1,
|
||||
r: 4,
|
||||
},
|
||||
hover: {
|
||||
fill: '#465fff',
|
||||
fillOpacity: 1,
|
||||
},
|
||||
selected: {},
|
||||
selectedHover: {},
|
||||
},
|
||||
onRegionTooltipShow: function (event: MouseEvent, tooltip: any) {
|
||||
const code = (event.target as HTMLElement).getAttribute('data-code')
|
||||
if (code === 'EG') {
|
||||
tooltip.setContent(tooltip.text() + ' (Hello Egypt)')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initMap()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-6">
|
||||
<div
|
||||
class="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center w-12 h-12 bg-gray-100 rounded-xl dark:bg-gray-800"
|
||||
>
|
||||
<svg
|
||||
class="fill-gray-800 dark:fill-white/90"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8.80443 5.60156C7.59109 5.60156 6.60749 6.58517 6.60749 7.79851C6.60749 9.01185 7.59109 9.99545 8.80443 9.99545C10.0178 9.99545 11.0014 9.01185 11.0014 7.79851C11.0014 6.58517 10.0178 5.60156 8.80443 5.60156ZM5.10749 7.79851C5.10749 5.75674 6.76267 4.10156 8.80443 4.10156C10.8462 4.10156 12.5014 5.75674 12.5014 7.79851C12.5014 9.84027 10.8462 11.4955 8.80443 11.4955C6.76267 11.4955 5.10749 9.84027 5.10749 7.79851ZM4.86252 15.3208C4.08769 16.0881 3.70377 17.0608 3.51705 17.8611C3.48384 18.0034 3.5211 18.1175 3.60712 18.2112C3.70161 18.3141 3.86659 18.3987 4.07591 18.3987H13.4249C13.6343 18.3987 13.7992 18.3141 13.8937 18.2112C13.9797 18.1175 14.017 18.0034 13.9838 17.8611C13.7971 17.0608 13.4132 16.0881 12.6383 15.3208C11.8821 14.572 10.6899 13.955 8.75042 13.955C6.81096 13.955 5.61877 14.572 4.86252 15.3208ZM3.8071 14.2549C4.87163 13.2009 6.45602 12.455 8.75042 12.455C11.0448 12.455 12.6292 13.2009 13.6937 14.2549C14.7397 15.2906 15.2207 16.5607 15.4446 17.5202C15.7658 18.8971 14.6071 19.8987 13.4249 19.8987H4.07591C2.89369 19.8987 1.73504 18.8971 2.05628 17.5202C2.28015 16.5607 2.76117 15.2906 3.8071 14.2549ZM15.3042 11.4955C14.4702 11.4955 13.7006 11.2193 13.0821 10.7533C13.3742 10.3314 13.6054 9.86419 13.7632 9.36432C14.1597 9.75463 14.7039 9.99545 15.3042 9.99545C16.5176 9.99545 17.5012 9.01185 17.5012 7.79851C17.5012 6.58517 16.5176 5.60156 15.3042 5.60156C14.7039 5.60156 14.1597 5.84239 13.7632 6.23271C13.6054 5.73284 13.3741 5.26561 13.082 4.84371C13.7006 4.37777 14.4702 4.10156 15.3042 4.10156C17.346 4.10156 19.0012 5.75674 19.0012 7.79851C19.0012 9.84027 17.346 11.4955 15.3042 11.4955ZM19.9248 19.8987H16.3901C16.7014 19.4736 16.9159 18.969 16.9827 18.3987H19.9248C20.1341 18.3987 20.2991 18.3141 20.3936 18.2112C20.4796 18.1175 20.5169 18.0034 20.4837 17.861C20.2969 17.0607 19.913 16.088 19.1382 15.3208C18.4047 14.5945 17.261 13.9921 15.4231 13.9566C15.2232 13.6945 14.9995 13.437 14.7491 13.1891C14.5144 12.9566 14.262 12.7384 13.9916 12.5362C14.3853 12.4831 14.8044 12.4549 15.2503 12.4549C17.5447 12.4549 19.1291 13.2008 20.1936 14.2549C21.2395 15.2906 21.7206 16.5607 21.9444 17.5202C22.2657 18.8971 21.107 19.8987 19.9248 19.8987Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="flex items-end justify-between mt-5">
|
||||
<div>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">Customers</span>
|
||||
<h4 class="mt-2 font-bold text-gray-800 text-title-sm dark:text-white/90">3,782</h4>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="flex items-center gap-1 rounded-full bg-success-50 py-0.5 pl-2 pr-2.5 text-sm font-medium text-success-600 dark:bg-success-500/15 dark:text-success-500"
|
||||
>
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5.56462 1.62393C5.70193 1.47072 5.90135 1.37432 6.12329 1.37432C6.1236 1.37432 6.12391 1.37432 6.12422 1.37432C6.31631 1.37415 6.50845 1.44731 6.65505 1.59381L9.65514 4.5918C9.94814 4.88459 9.94831 5.35947 9.65552 5.65246C9.36273 5.94546 8.88785 5.94562 8.59486 5.65283L6.87329 3.93247L6.87329 10.125C6.87329 10.5392 6.53751 10.875 6.12329 10.875C5.70908 10.875 5.37329 10.5392 5.37329 10.125L5.37329 3.93578L3.65516 5.65282C3.36218 5.94562 2.8873 5.94547 2.5945 5.65248C2.3017 5.35949 2.30185 4.88462 2.59484 4.59182L5.56462 1.62393Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
|
||||
11.01%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center w-12 h-12 bg-gray-100 rounded-xl dark:bg-gray-800"
|
||||
>
|
||||
<svg
|
||||
class="fill-gray-800 dark:fill-white/90"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.665 3.75621C11.8762 3.65064 12.1247 3.65064 12.3358 3.75621L18.7807 6.97856L12.3358 10.2009C12.1247 10.3065 11.8762 10.3065 11.665 10.2009L5.22014 6.97856L11.665 3.75621ZM4.29297 8.19203V16.0946C4.29297 16.3787 4.45347 16.6384 4.70757 16.7654L11.25 20.0366V11.6513C11.1631 11.6205 11.0777 11.5843 10.9942 11.5426L4.29297 8.19203ZM12.75 20.037L19.2933 16.7654C19.5474 16.6384 19.7079 16.3787 19.7079 16.0946V8.19202L13.0066 11.5426C12.9229 11.5844 12.8372 11.6208 12.75 11.6516V20.037ZM13.0066 2.41456C12.3732 2.09786 11.6277 2.09786 10.9942 2.41456L4.03676 5.89319C3.27449 6.27432 2.79297 7.05342 2.79297 7.90566V16.0946C2.79297 16.9469 3.27448 17.726 4.03676 18.1071L10.9942 21.5857L11.3296 20.9149L10.9942 21.5857C11.6277 21.9024 12.3732 21.9024 13.0066 21.5857L19.9641 18.1071C20.7264 17.726 21.2079 16.9469 21.2079 16.0946V7.90566C21.2079 7.05342 20.7264 6.27432 19.9641 5.89319L13.0066 2.41456Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="flex items-end justify-between mt-5">
|
||||
<div>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">Orders</span>
|
||||
<h4 class="mt-2 font-bold text-gray-800 text-title-sm dark:text-white/90">5,359</h4>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="flex items-center gap-1 rounded-full bg-error-50 py-0.5 pl-2 pr-2.5 text-sm font-medium text-error-600 dark:bg-error-500/15 dark:text-error-500"
|
||||
>
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5.31462 10.3761C5.45194 10.5293 5.65136 10.6257 5.87329 10.6257C5.8736 10.6257 5.8739 10.6257 5.87421 10.6257C6.0663 10.6259 6.25845 10.5527 6.40505 10.4062L9.40514 7.4082C9.69814 7.11541 9.69831 6.64054 9.40552 6.34754C9.11273 6.05454 8.63785 6.05438 8.34486 6.34717L6.62329 8.06753L6.62329 1.875C6.62329 1.46079 6.28751 1.125 5.87329 1.125C5.45908 1.125 5.12329 1.46079 5.12329 1.875L5.12329 8.06422L3.40516 6.34719C3.11218 6.05439 2.6373 6.05454 2.3445 6.34752C2.0517 6.64051 2.05185 7.11538 2.34484 7.40818L5.31462 10.3761Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
|
||||
9.05%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div
|
||||
class="overflow-hidden rounded-2xl border border-gray-200 bg-white px-5 pt-5 dark:border-gray-800 dark:bg-white/[0.03] sm:px-6 sm:pt-6"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-white/90">Monthly Sales</h3>
|
||||
|
||||
<div class="relative h-fit">
|
||||
<DropdownMenu :menu-items="menuItems">
|
||||
<template #icon>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.2441 6C10.2441 5.0335 11.0276 4.25 11.9941 4.25H12.0041C12.9706 4.25 13.7541 5.0335 13.7541 6C13.7541 6.9665 12.9706 7.75 12.0041 7.75H11.9941C11.0276 7.75 10.2441 6.9665 10.2441 6ZM10.2441 18C10.2441 17.0335 11.0276 16.25 11.9941 16.25H12.0041C12.9706 16.25 13.7541 17.0335 13.7541 18C13.7541 18.9665 12.9706 19.75 12.0041 19.75H11.9941C11.0276 19.75 10.2441 18.9665 10.2441 18ZM11.9941 10.25C11.0276 10.25 10.2441 11.0335 10.2441 12C10.2441 12.9665 11.0276 13.75 11.9941 13.75H12.0041C12.9706 13.75 13.7541 12.9665 13.7541 12C13.7541 11.0335 12.9706 10.25 12.0041 10.25H11.9941Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-full overflow-x-auto custom-scrollbar">
|
||||
<div id="chartOne" class="-ml-5 min-w-[650px] xl:min-w-full pl-2">
|
||||
<VueApexCharts type="bar" height="180" :options="chartOptions" :series="series" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import DropdownMenu from '../common/DropdownMenu.vue'
|
||||
const menuItems = [
|
||||
{ label: 'View More', onClick: () => console.log('View More clicked') },
|
||||
{ label: 'Delete', onClick: () => console.log('Delete clicked') },
|
||||
]
|
||||
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
|
||||
const series = ref([
|
||||
{
|
||||
name: 'Sales',
|
||||
data: [168, 385, 201, 298, 187, 195, 291, 110, 215, 390, 280, 112],
|
||||
},
|
||||
])
|
||||
|
||||
const chartOptions = ref({
|
||||
colors: ['#465fff'],
|
||||
chart: {
|
||||
fontFamily: 'Outfit, sans-serif',
|
||||
type: 'bar',
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: '39%',
|
||||
borderRadius: 5,
|
||||
borderRadiusApplication: 'end',
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
show: true,
|
||||
width: 4,
|
||||
colors: ['transparent'],
|
||||
},
|
||||
xaxis: {
|
||||
categories: [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
],
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
horizontalAlign: 'left',
|
||||
fontFamily: 'Outfit',
|
||||
markers: {
|
||||
radius: 99,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
title: false,
|
||||
},
|
||||
grid: {
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
opacity: 1,
|
||||
},
|
||||
tooltip: {
|
||||
x: {
|
||||
show: false,
|
||||
},
|
||||
y: {
|
||||
formatter: function (val) {
|
||||
return val.toString()
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Any additional setup can be done here if needed
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div
|
||||
class="rounded-2xl border border-gray-200 bg-gray-100 dark:border-gray-800 dark:bg-white/[0.03]"
|
||||
>
|
||||
<div
|
||||
class="px-5 pt-5 bg-white shadow-default rounded-2xl pb-11 dark:bg-gray-900 sm:px-6 sm:pt-6"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-white/90">Monthly Target</h3>
|
||||
<p class="mt-1 text-gray-500 text-theme-sm dark:text-gray-400">
|
||||
Target you’ve set for each month
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<DropdownMenu :menu-items="menuItems">
|
||||
<template #icon>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.2441 6C10.2441 5.0335 11.0276 4.25 11.9941 4.25H12.0041C12.9706 4.25 13.7541 5.0335 13.7541 6C13.7541 6.9665 12.9706 7.75 12.0041 7.75H11.9941C11.0276 7.75 10.2441 6.9665 10.2441 6ZM10.2441 18C10.2441 17.0335 11.0276 16.25 11.9941 16.25H12.0041C12.9706 16.25 13.7541 17.0335 13.7541 18C13.7541 18.9665 12.9706 19.75 12.0041 19.75H11.9941C11.0276 19.75 10.2441 18.9665 10.2441 18ZM11.9941 10.25C11.0276 10.25 10.2441 11.0335 10.2441 12C10.2441 12.9665 11.0276 13.75 11.9941 13.75H12.0041C12.9706 13.75 13.7541 12.9665 13.7541 12C13.7541 11.0335 12.9706 10.25 12.0041 10.25H11.9941Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative max-h-[195px]">
|
||||
<div id="chartTwo" class="h-full">
|
||||
<div class="radial-bar-chart">
|
||||
<VueApexCharts type="radialBar" height="330" :options="chartOptions" :series="series" />
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="absolute left-1/2 top-[85%] -translate-x-1/2 -translate-y-[85%] rounded-full bg-success-50 px-3 py-1 text-xs font-medium text-success-600 dark:bg-success-500/15 dark:text-success-500"
|
||||
>+10%</span
|
||||
>
|
||||
</div>
|
||||
<p class="mx-auto mt-1.5 w-full max-w-[380px] text-center text-sm text-gray-500 sm:text-base">
|
||||
You earn $3287 today, it's higher than last month. Keep up your good work!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center gap-5 px-6 py-3.5 sm:gap-8 sm:py-5">
|
||||
<div>
|
||||
<p class="mb-1 text-center text-gray-500 text-theme-xs dark:text-gray-400 sm:text-sm">
|
||||
Target
|
||||
</p>
|
||||
<p
|
||||
class="flex items-center justify-center gap-1 text-base font-semibold text-gray-800 dark:text-white/90 sm:text-lg"
|
||||
>
|
||||
$20K
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.26816 13.6632C7.4056 13.8192 7.60686 13.9176 7.8311 13.9176C7.83148 13.9176 7.83187 13.9176 7.83226 13.9176C8.02445 13.9178 8.21671 13.8447 8.36339 13.6981L12.3635 9.70076C12.6565 9.40797 12.6567 8.9331 12.3639 8.6401C12.0711 8.34711 11.5962 8.34694 11.3032 8.63973L8.5811 11.36L8.5811 2.5C8.5811 2.08579 8.24531 1.75 7.8311 1.75C7.41688 1.75 7.0811 2.08579 7.0811 2.5L7.0811 11.3556L4.36354 8.63975C4.07055 8.34695 3.59568 8.3471 3.30288 8.64009C3.01008 8.93307 3.01023 9.40794 3.30321 9.70075L7.26816 13.6632Z"
|
||||
fill="#D92D20"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="w-px bg-gray-200 h-7 dark:bg-gray-800"></div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1 text-center text-gray-500 text-theme-xs dark:text-gray-400 sm:text-sm">
|
||||
Revenue
|
||||
</p>
|
||||
<p
|
||||
class="flex items-center justify-center gap-1 text-base font-semibold text-gray-800 dark:text-white/90 sm:text-lg"
|
||||
>
|
||||
$20K
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.60141 2.33683C7.73885 2.18084 7.9401 2.08243 8.16435 2.08243C8.16475 2.08243 8.16516 2.08243 8.16556 2.08243C8.35773 2.08219 8.54998 2.15535 8.69664 2.30191L12.6968 6.29924C12.9898 6.59203 12.9899 7.0669 12.6971 7.3599C12.4044 7.6529 11.9295 7.65306 11.6365 7.36027L8.91435 4.64004L8.91435 13.5C8.91435 13.9142 8.57856 14.25 8.16435 14.25C7.75013 14.25 7.41435 13.9142 7.41435 13.5L7.41435 4.64442L4.69679 7.36025C4.4038 7.65305 3.92893 7.6529 3.63613 7.35992C3.34333 7.06693 3.34348 6.59206 3.63646 6.29926L7.60141 2.33683Z"
|
||||
fill="#039855"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="w-px bg-gray-200 h-7 dark:bg-gray-800"></div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1 text-center text-gray-500 text-theme-xs dark:text-gray-400 sm:text-sm">
|
||||
Today
|
||||
</p>
|
||||
<p
|
||||
class="flex items-center justify-center gap-1 text-base font-semibold text-gray-800 dark:text-white/90 sm:text-lg"
|
||||
>
|
||||
$20K
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.60141 2.33683C7.73885 2.18084 7.9401 2.08243 8.16435 2.08243C8.16475 2.08243 8.16516 2.08243 8.16556 2.08243C8.35773 2.08219 8.54998 2.15535 8.69664 2.30191L12.6968 6.29924C12.9898 6.59203 12.9899 7.0669 12.6971 7.3599C12.4044 7.6529 11.9295 7.65306 11.6365 7.36027L8.91435 4.64004L8.91435 13.5C8.91435 13.9142 8.57856 14.25 8.16435 14.25C7.75013 14.25 7.41435 13.9142 7.41435 13.5L7.41435 4.64442L4.69679 7.36025C4.4038 7.65305 3.92893 7.6529 3.63613 7.35992C3.34333 7.06693 3.34348 6.59206 3.63646 6.29926L7.60141 2.33683Z"
|
||||
fill="#039855"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import DropdownMenu from '../common/DropdownMenu.vue'
|
||||
const menuItems = [
|
||||
{ label: 'View More', onClick: () => console.log('View More clicked') },
|
||||
{ label: 'Delete', onClick: () => console.log('Delete clicked') },
|
||||
]
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Number,
|
||||
default: 75.55,
|
||||
},
|
||||
})
|
||||
|
||||
const series = computed(() => [props.value])
|
||||
|
||||
const chartOptions = {
|
||||
colors: ['#465FFF'],
|
||||
chart: {
|
||||
fontFamily: 'Outfit, sans-serif',
|
||||
sparkline: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
radialBar: {
|
||||
startAngle: -90,
|
||||
endAngle: 90,
|
||||
hollow: {
|
||||
size: '80%',
|
||||
},
|
||||
track: {
|
||||
background: '#E4E7EC',
|
||||
strokeWidth: '100%',
|
||||
margin: 5,
|
||||
},
|
||||
dataLabels: {
|
||||
name: {
|
||||
show: false,
|
||||
},
|
||||
value: {
|
||||
fontSize: '36px',
|
||||
fontWeight: '600',
|
||||
offsetY: 60,
|
||||
color: '#1D2939',
|
||||
formatter: function (val: number) {
|
||||
return val.toFixed(2) + '%'
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
type: 'solid',
|
||||
colors: ['#465FFF'],
|
||||
},
|
||||
stroke: {
|
||||
lineCap: 'round',
|
||||
},
|
||||
labels: ['Progress'],
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.radial-bar-chart {
|
||||
width: 100%;
|
||||
max-width: 330px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div
|
||||
class="overflow-hidden rounded-2xl border border-gray-200 bg-white px-4 pb-3 pt-4 dark:border-gray-800 dark:bg-white/[0.03] sm:px-6"
|
||||
>
|
||||
<div class="flex flex-col gap-2 mb-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-white/90">Recent Orders</h3>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
class="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-theme-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200"
|
||||
>
|
||||
<svg
|
||||
class="stroke-current fill-white dark:fill-gray-800"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.29004 5.90393H17.7067"
|
||||
stroke=""
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M17.7075 14.0961H2.29085"
|
||||
stroke=""
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12.0826 3.33331C13.5024 3.33331 14.6534 4.48431 14.6534 5.90414C14.6534 7.32398 13.5024 8.47498 12.0826 8.47498C10.6627 8.47498 9.51172 7.32398 9.51172 5.90415C9.51172 4.48432 10.6627 3.33331 12.0826 3.33331Z"
|
||||
fill=""
|
||||
stroke=""
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M7.91745 11.525C6.49762 11.525 5.34662 12.676 5.34662 14.0959C5.34661 15.5157 6.49762 16.6667 7.91745 16.6667C9.33728 16.6667 10.4883 15.5157 10.4883 14.0959C10.4883 12.676 9.33728 11.525 7.91745 11.525Z"
|
||||
fill=""
|
||||
stroke=""
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
Filter
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-theme-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200"
|
||||
>
|
||||
See all
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-full overflow-x-auto custom-scrollbar">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr class="border-t border-gray-100 dark:border-gray-800">
|
||||
<th class="py-3 text-left">
|
||||
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">Products</p>
|
||||
</th>
|
||||
<th class="py-3 text-left">
|
||||
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">Category</p>
|
||||
</th>
|
||||
<th class="py-3 text-left">
|
||||
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">Price</p>
|
||||
</th>
|
||||
<th class="py-3 text-left">
|
||||
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">Status</p>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(product, index) in products"
|
||||
:key="index"
|
||||
class="border-t border-gray-100 dark:border-gray-800"
|
||||
>
|
||||
<td class="py-3 whitespace-nowrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-[50px] w-[50px] overflow-hidden rounded-md">
|
||||
<img :src="product.image" :alt="product.name" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-gray-800 text-theme-sm dark:text-white/90">
|
||||
{{ product.name }}
|
||||
</p>
|
||||
<span class="text-gray-500 text-theme-xs dark:text-gray-400"
|
||||
>{{ product.variants }} Variants</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-3 whitespace-nowrap">
|
||||
<p class="text-gray-500 text-theme-sm dark:text-gray-400">{{ product.category }}</p>
|
||||
</td>
|
||||
<td class="py-3 whitespace-nowrap">
|
||||
<p class="text-gray-500 text-theme-sm dark:text-gray-400">{{ product.price }}</p>
|
||||
</td>
|
||||
<td class="py-3 whitespace-nowrap">
|
||||
<span
|
||||
:class="{
|
||||
'rounded-full px-2 py-0.5 text-theme-xs font-medium': true,
|
||||
'bg-success-50 text-success-600 dark:bg-success-500/15 dark:text-success-500':
|
||||
product.status === 'Delivered',
|
||||
'bg-warning-50 text-warning-600 dark:bg-warning-500/15 dark:text-orange-400':
|
||||
product.status === 'Pending',
|
||||
'bg-error-50 text-error-600 dark:bg-error-500/15 dark:text-error-500':
|
||||
product.status === 'Canceled',
|
||||
}"
|
||||
>
|
||||
{{ product.status }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const products = ref([
|
||||
{
|
||||
name: 'Macbook pro 13"',
|
||||
variants: 2,
|
||||
image: '/images/product/product-01.jpg',
|
||||
category: 'Laptop',
|
||||
price: '$2399.00',
|
||||
status: 'Delivered',
|
||||
},
|
||||
{
|
||||
name: 'Apple Watch Ultra',
|
||||
variants: 1,
|
||||
image: '/images/product/product-02.jpg',
|
||||
category: 'Watch',
|
||||
price: '$879.00',
|
||||
status: 'Pending',
|
||||
},
|
||||
{
|
||||
name: 'iPhone 15 Pro Max',
|
||||
variants: 2,
|
||||
image: '/images/product/product-03.jpg',
|
||||
category: 'SmartPhone',
|
||||
price: '$1869.00',
|
||||
status: 'Delivered',
|
||||
},
|
||||
{
|
||||
name: 'iPad Pro 3rd Gen',
|
||||
variants: 2,
|
||||
image: '/images/product/product-04.jpg',
|
||||
category: 'Electronics',
|
||||
price: '$1699.00',
|
||||
status: 'Canceled',
|
||||
},
|
||||
{
|
||||
name: 'Airpods Pro 2nd Gen',
|
||||
variants: 1,
|
||||
image: '/images/product/product-05.jpg',
|
||||
category: 'Accessories',
|
||||
price: '$240.00',
|
||||
status: 'Delivered',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<div
|
||||
class="rounded-2xl border border-gray-200 bg-white px-5 pb-5 pt-5 dark:border-gray-800 dark:bg-white/[0.03] sm:px-6 sm:pt-6"
|
||||
>
|
||||
<div class="flex flex-col gap-5 mb-6 sm:flex-row sm:justify-between">
|
||||
<div class="w-full">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-white/90">Statistics</h3>
|
||||
<p class="mt-1 text-gray-500 text-theme-sm dark:text-gray-400">
|
||||
Target you’ve set for each month
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="relative">
|
||||
<div
|
||||
class="inline-flex items-center gap-0.5 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900"
|
||||
>
|
||||
<button
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
@click="selected = option.value"
|
||||
:class="[
|
||||
selected === option.value
|
||||
? 'shadow-theme-xs text-gray-900 dark:text-white bg-white dark:bg-gray-800'
|
||||
: 'text-gray-500 dark:text-gray-400',
|
||||
'px-3 py-2 font-medium rounded-md text-theme-sm hover:text-gray-900 hover:shadow-theme-xs dark:hover:bg-gray-800 dark:hover:text-white',
|
||||
]"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<flat-pickr
|
||||
v-model="date"
|
||||
:config="flatpickrConfig"
|
||||
class="pl-3 sm:pl-9 dark:bg-dark-900 h-10 w-10 sm:w-40 rounded-lg border border-gray-200 bg-white text-transparent sm:text-theme-sm sm:text-gray-800 shadow-theme-xs placeholder:text-transparent sm:placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:bg-white/[0.03] dark:text-transparent sm:dark:text-gray-400 dark:placeholder:text-transparent sm:dark:placeholder:text-gray-400 dark:focus:border-brand-800"
|
||||
placeholder="Select Date"
|
||||
/>
|
||||
<span
|
||||
class="absolute text-gray-500 -translate-y-1/2 pointer-events-none left-1/2 -translate-x-1/2 sm:left-3 sm:translate-x-0 top-1/2 dark:text-gray-400"
|
||||
>
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.66659 1.5415C7.0808 1.5415 7.41658 1.87729 7.41658 2.2915V2.99984H12.5833V2.2915C12.5833 1.87729 12.919 1.5415 13.3333 1.5415C13.7475 1.5415 14.0833 1.87729 14.0833 2.2915V2.99984L15.4166 2.99984C16.5212 2.99984 17.4166 3.89527 17.4166 4.99984V7.49984V15.8332C17.4166 16.9377 16.5212 17.8332 15.4166 17.8332H4.58325C3.47868 17.8332 2.58325 16.9377 2.58325 15.8332V7.49984V4.99984C2.58325 3.89527 3.47868 2.99984 4.58325 2.99984L5.91659 2.99984V2.2915C5.91659 1.87729 6.25237 1.5415 6.66659 1.5415ZM6.66659 4.49984H4.58325C4.30711 4.49984 4.08325 4.7237 4.08325 4.99984V6.74984H15.9166V4.99984C15.9166 4.7237 15.6927 4.49984 15.4166 4.49984H13.3333H6.66659ZM15.9166 8.24984H4.08325V15.8332C4.08325 16.1093 4.30711 16.3332 4.58325 16.3332H15.4166C15.6927 16.3332 15.9166 16.1093 15.9166 15.8332V8.24984Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-w-full overflow-x-auto custom-scrollbar">
|
||||
<div id="chartThree" class="-ml-4 min-w-[1000px] xl:min-w-full pl-2">
|
||||
<VueApexCharts type="area" height="310" :options="chartOptions" :series="series" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
|
||||
const options = [
|
||||
{ value: 'optionOne', label: 'Monthly' },
|
||||
{ value: 'optionTwo', label: 'Quarterly' },
|
||||
{ value: 'optionThree', label: 'Annually' },
|
||||
]
|
||||
|
||||
const selected = ref('optionOne')
|
||||
const date = ref('')
|
||||
|
||||
const flatpickrConfig = {
|
||||
mode: 'range',
|
||||
dateFormat: 'M j',
|
||||
defaultDate: [new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), new Date()],
|
||||
}
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
|
||||
const series = ref([
|
||||
{
|
||||
name: 'Sales',
|
||||
data: [180, 190, 170, 160, 175, 165, 170, 205, 230, 210, 240, 235],
|
||||
},
|
||||
{
|
||||
name: 'Revenue',
|
||||
data: [40, 30, 50, 40, 55, 40, 70, 100, 110, 120, 150, 140],
|
||||
},
|
||||
])
|
||||
|
||||
const chartOptions = ref({
|
||||
legend: {
|
||||
show: false,
|
||||
position: 'top',
|
||||
horizontalAlign: 'left',
|
||||
},
|
||||
colors: ['#465FFF', '#9CB9FF'],
|
||||
chart: {
|
||||
fontFamily: 'Outfit, sans-serif',
|
||||
type: 'area',
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
gradient: {
|
||||
enabled: true,
|
||||
opacityFrom: 0.55,
|
||||
opacityTo: 0,
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
curve: 'straight',
|
||||
width: [2, 2],
|
||||
},
|
||||
markers: {
|
||||
size: 0,
|
||||
},
|
||||
labels: {
|
||||
show: false,
|
||||
position: 'top',
|
||||
},
|
||||
grid: {
|
||||
xaxis: {
|
||||
lines: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
tooltip: {
|
||||
x: {
|
||||
format: 'dd MMM yyyy',
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: 'category',
|
||||
categories: [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
],
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
style: {
|
||||
fontSize: '0px',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.area-chart {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-center gap-8">
|
||||
<!-- Default Checkbox -->
|
||||
<div>
|
||||
<label
|
||||
for="checkboxLabelOne"
|
||||
class="flex items-center text-sm font-medium text-gray-700 cursor-pointer select-none dark:text-gray-400"
|
||||
>
|
||||
<div class="relative">
|
||||
<input type="checkbox" id="checkboxLabelOne" v-model="checkboxOne" class="sr-only" />
|
||||
<div
|
||||
:class="
|
||||
checkboxOne
|
||||
? 'border-brand-500 bg-brand-500'
|
||||
: 'bg-transparent border-gray-300 dark:border-gray-700'
|
||||
"
|
||||
class="mr-3 flex h-5 w-5 items-center justify-center rounded-md border-[1.25px] hover:border-brand-500 dark:hover:border-brand-500"
|
||||
>
|
||||
<span :class="checkboxOne ? '' : 'opacity-0'">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.6666 3.5L5.24992 9.91667L2.33325 7"
|
||||
stroke="white"
|
||||
stroke-width="1.94437"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
Default
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Checked Checkbox -->
|
||||
<div>
|
||||
<label
|
||||
for="checkboxLabelTwo"
|
||||
class="flex items-center text-sm font-medium text-gray-700 cursor-pointer select-none dark:text-gray-400"
|
||||
>
|
||||
<div class="relative">
|
||||
<input type="checkbox" id="checkboxLabelTwo" v-model="checkboxTwo" class="sr-only" />
|
||||
<div
|
||||
:class="
|
||||
checkboxTwo
|
||||
? 'border-brand-500 bg-brand-500'
|
||||
: 'bg-transparent border-gray-300 dark:border-gray-700'
|
||||
"
|
||||
class="mr-3 flex h-5 w-5 items-center justify-center rounded-md border-[1.25px] hover:border-brand-500 dark:hover:border-brand-500"
|
||||
>
|
||||
<span :class="checkboxTwo ? '' : 'opacity-0'">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.6666 3.5L5.24992 9.91667L2.33325 7"
|
||||
stroke="white"
|
||||
stroke-width="1.94437"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
Checked
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Disabled Checkbox -->
|
||||
<div>
|
||||
<label
|
||||
for="checkboxLabelThree"
|
||||
class="flex items-center text-sm font-medium text-gray-300 cursor-pointer select-none dark:text-gray-700"
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="checkboxLabelThree"
|
||||
v-model="checkboxThree"
|
||||
class="sr-only peer"
|
||||
disabled
|
||||
/>
|
||||
<div
|
||||
:class="
|
||||
checkboxThree
|
||||
? 'bg-transparent border-gray-200 dark:border-gray-800'
|
||||
: 'border-brand-500 bg-brand-500'
|
||||
"
|
||||
class="mr-3 flex h-5 w-5 items-center justify-center rounded-md border-[1.25px]"
|
||||
>
|
||||
<span :class="checkboxThree ? '' : 'opacity-0'">
|
||||
<svg
|
||||
class="stroke-gray-200 dark:stroke-gray-800"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.6666 3.5L5.24992 9.91667L2.33325 7"
|
||||
stroke=""
|
||||
stroke-width="2.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const checkboxOne = ref(false)
|
||||
const checkboxTwo = ref(true)
|
||||
const checkboxThree = ref(true)
|
||||
</script>
|
||||
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- Text Input -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Input
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="formData.input"
|
||||
class="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Input with Placeholder -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Input with Placeholder
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="formData.inputWithPlaceholder"
|
||||
placeholder="info@gmail.com"
|
||||
class="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Select Input -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Select Input
|
||||
</label>
|
||||
<div class="relative z-20 bg-transparent">
|
||||
<select
|
||||
v-model="formData.selectInput"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
:class="{ 'text-gray-800 dark:text-white/90': formData.selectInput }"
|
||||
>
|
||||
<option value="" disabled selected>Select Option</option>
|
||||
<option value="marketing">Marketing</option>
|
||||
<option value="template">Template</option>
|
||||
<option value="development">Development</option>
|
||||
</select>
|
||||
<span
|
||||
class="absolute z-30 text-gray-500 -translate-y-1/2 pointer-events-none right-4 top-1/2 dark:text-gray-400"
|
||||
>
|
||||
<svg
|
||||
class="stroke-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.79175 7.396L10.0001 12.6043L15.2084 7.396"
|
||||
stroke=""
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Input -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Password Input
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
v-model="formData.password"
|
||||
placeholder="Enter your password"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
<span
|
||||
@click="showPassword = !showPassword"
|
||||
class="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2"
|
||||
>
|
||||
<svg
|
||||
v-if="!showPassword"
|
||||
class="fill-gray-500 dark:fill-gray-400"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10.0002 13.8619C7.23361 13.8619 4.86803 12.1372 3.92328 9.70241C4.86804 7.26761 7.23361 5.54297 10.0002 5.54297C12.7667 5.54297 15.1323 7.26762 16.0771 9.70243C15.1323 12.1372 12.7667 13.8619 10.0002 13.8619ZM10.0002 4.04297C6.48191 4.04297 3.49489 6.30917 2.4155 9.4593C2.3615 9.61687 2.3615 9.78794 2.41549 9.94552C3.49488 13.0957 6.48191 15.3619 10.0002 15.3619C13.5184 15.3619 16.5055 13.0957 17.5849 9.94555C17.6389 9.78797 17.6389 9.6169 17.5849 9.45932C16.5055 6.30919 13.5184 4.04297 10.0002 4.04297ZM9.99151 7.84413C8.96527 7.84413 8.13333 8.67606 8.13333 9.70231C8.13333 10.7286 8.96527 11.5605 9.99151 11.5605H10.0064C11.0326 11.5605 11.8646 10.7286 11.8646 9.70231C11.8646 8.67606 11.0326 7.84413 10.0064 7.84413H9.99151Z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="fill-gray-500 dark:fill-gray-400"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.63803 3.57709C4.34513 3.2842 3.87026 3.2842 3.57737 3.57709C3.28447 3.86999 3.28447 4.34486 3.57737 4.63775L4.85323 5.91362C3.74609 6.84199 2.89363 8.06395 2.4155 9.45936C2.3615 9.61694 2.3615 9.78801 2.41549 9.94558C3.49488 13.0957 6.48191 15.3619 10.0002 15.3619C11.255 15.3619 12.4422 15.0737 13.4994 14.5598L15.3625 16.4229C15.6554 16.7158 16.1302 16.7158 16.4231 16.4229C16.716 16.13 16.716 15.6551 16.4231 15.3622L4.63803 3.57709ZM12.3608 13.4212L10.4475 11.5079C10.3061 11.5423 10.1584 11.5606 10.0064 11.5606H9.99151C8.96527 11.5606 8.13333 10.7286 8.13333 9.70237C8.13333 9.5461 8.15262 9.39434 8.18895 9.24933L5.91885 6.97923C5.03505 7.69015 4.34057 8.62704 3.92328 9.70247C4.86803 12.1373 7.23361 13.8619 10.0002 13.8619C10.8326 13.8619 11.6287 13.7058 12.3608 13.4212ZM16.0771 9.70249C15.7843 10.4569 15.3552 11.1432 14.8199 11.7311L15.8813 12.7925C16.6329 11.9813 17.2187 11.0143 17.5849 9.94561C17.6389 9.78803 17.6389 9.61696 17.5849 9.45938C16.5055 6.30925 13.5184 4.04303 10.0002 4.04303C9.13525 4.04303 8.30244 4.17999 7.52218 4.43338L8.75139 5.66259C9.1556 5.58413 9.57311 5.54303 10.0002 5.54303C12.7667 5.54303 15.1323 7.26768 16.0771 9.70249Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Picker Input -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Date Picker Input
|
||||
</label>
|
||||
<div class="relative">
|
||||
<flat-pickr
|
||||
v-model="date"
|
||||
:config="flatpickrConfig"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
placeholder="Select date"
|
||||
/>
|
||||
<span
|
||||
class="absolute text-gray-500 -translate-y-1/2 pointer-events-none right-3 top-1/2 dark:text-gray-400"
|
||||
>
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.66659 1.5415C7.0808 1.5415 7.41658 1.87729 7.41658 2.2915V2.99984H12.5833V2.2915C12.5833 1.87729 12.919 1.5415 13.3333 1.5415C13.7475 1.5415 14.0833 1.87729 14.0833 2.2915V2.99984L15.4166 2.99984C16.5212 2.99984 17.4166 3.89527 17.4166 4.99984V7.49984V15.8332C17.4166 16.9377 16.5212 17.8332 15.4166 17.8332H4.58325C3.47868 17.8332 2.58325 16.9377 2.58325 15.8332V7.49984V4.99984C2.58325 3.89527 3.47868 2.99984 4.58325 2.99984L5.91659 2.99984V2.2915C5.91659 1.87729 6.25237 1.5415 6.66659 1.5415ZM6.66659 4.49984H4.58325C4.30711 4.49984 4.08325 4.7237 4.08325 4.99984V6.74984H15.9166V4.99984C15.9166 4.7237 15.6927 4.49984 15.4166 4.49984H13.3333H6.66659ZM15.9166 8.24984H4.08325V15.8332C4.08325 16.1093 4.30711 16.3332 4.58325 16.3332H15.4166C15.6927 16.3332 15.9166 16.1093 15.9166 15.8332V8.24984Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Time Select Input -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Time Select Input
|
||||
</label>
|
||||
<div class="relative">
|
||||
<flat-pickr
|
||||
v-model="time"
|
||||
:config="flatpickrTimeConfig"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
placeholder="Select time"
|
||||
/>
|
||||
<span class="absolute text-gray-500 -translate-y-1/2 right-3 top-1/2 dark:text-gray-400">
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3.04175 9.99984C3.04175 6.15686 6.1571 3.0415 10.0001 3.0415C13.8431 3.0415 16.9584 6.15686 16.9584 9.99984C16.9584 13.8428 13.8431 16.9582 10.0001 16.9582C6.1571 16.9582 3.04175 13.8428 3.04175 9.99984ZM10.0001 1.5415C5.32867 1.5415 1.54175 5.32843 1.54175 9.99984C1.54175 14.6712 5.32867 18.4582 10.0001 18.4582C14.6715 18.4582 18.4584 14.6712 18.4584 9.99984C18.4584 5.32843 14.6715 1.5415 10.0001 1.5415ZM9.99998 10.7498C9.58577 10.7498 9.24998 10.4141 9.24998 9.99984V5.4165C9.24998 5.00229 9.58577 4.6665 9.99998 4.6665C10.4142 4.6665 10.75 5.00229 10.75 5.4165V9.24984H13.3334C13.7476 9.24984 14.0834 9.58562 14.0834 9.99984C14.0834 10.4141 13.7476 10.7498 13.3334 10.7498H10.0001H9.99998Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input with Payment -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Input with Payment
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
v-model="formData.cardNumber"
|
||||
placeholder="Card number"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-[62px] text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
<span
|
||||
class="absolute left-0 top-1/2 flex h-11 w-[46px] -translate-y-1/2 items-center justify-center border-r border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="6.25" cy="10" r="5.625" fill="#E80B26" />
|
||||
<circle cx="13.75" cy="10" r="5.625" fill="#F59D31" />
|
||||
<path
|
||||
d="M10 14.1924C11.1508 13.1625 11.875 11.6657 11.875 9.99979C11.875 8.33383 11.1508 6.8371 10 5.80713C8.84918 6.8371 8.125 8.33383 8.125 9.99979C8.125 11.6657 8.84918 13.1625 10 14.1924Z"
|
||||
fill="#FC6020"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
|
||||
const showPassword = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
input: '',
|
||||
inputWithPlaceholder: '',
|
||||
selectInput: '',
|
||||
password: '',
|
||||
date: '',
|
||||
time: '',
|
||||
cardNumber: '',
|
||||
})
|
||||
|
||||
const date = ref(null)
|
||||
|
||||
const flatpickrConfig = {
|
||||
dateFormat: 'Y-m-d',
|
||||
altInput: true,
|
||||
altFormat: 'F j, Y',
|
||||
wrap: true,
|
||||
}
|
||||
|
||||
const flatpickrTimeConfig = {
|
||||
enableTime: true,
|
||||
noCalendar: true,
|
||||
dateFormat: 'H:i',
|
||||
time_24hr: false,
|
||||
minuteIncrement: 1,
|
||||
wrap: false,
|
||||
}
|
||||
|
||||
const time = ref(null)
|
||||
</script>
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="file-uploader">
|
||||
<form
|
||||
ref="dropzoneForm"
|
||||
:id="dropzoneId"
|
||||
:action="uploadUrl"
|
||||
class="border-gray-300 border-dashed dropzone rounded-xl bg-gray-50 p-7 hover:border-brand-500 dark:border-gray-700 dark:bg-gray-900 dark:hover:border-brand-500 lg:p-10"
|
||||
>
|
||||
<div class="dz-message m-0!">
|
||||
<div class="mb-[22px] flex justify-center">
|
||||
<div
|
||||
class="flex h-[68px] w-[68px] items-center justify-center rounded-full bg-gray-200 text-gray-700 dark:bg-gray-800 dark:text-gray-400"
|
||||
>
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="29"
|
||||
height="28"
|
||||
viewBox="0 0 29 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14.5019 3.91699C14.2852 3.91699 14.0899 4.00891 13.953 4.15589L8.57363 9.53186C8.28065 9.82466 8.2805 10.2995 8.5733 10.5925C8.8661 10.8855 9.34097 10.8857 9.63396 10.5929L13.7519 6.47752V18.667C13.7519 19.0812 14.0877 19.417 14.5019 19.417C14.9161 19.417 15.2519 19.0812 15.2519 18.667V6.48234L19.3653 10.5929C19.6583 10.8857 20.1332 10.8855 20.426 10.5925C20.7188 10.2995 20.7186 9.82463 20.4256 9.53184L15.0838 4.19378C14.9463 4.02488 14.7367 3.91699 14.5019 3.91699ZM5.91626 18.667C5.91626 18.2528 5.58047 17.917 5.16626 17.917C4.75205 17.917 4.41626 18.2528 4.41626 18.667V21.8337C4.41626 23.0763 5.42362 24.0837 6.66626 24.0837H22.3339C23.5766 24.0837 24.5839 23.0763 24.5839 21.8337V18.667C24.5839 18.2528 24.2482 17.917 23.8339 17.917C23.4197 17.917 23.0839 18.2528 23.0839 18.667V21.8337C23.0839 22.2479 22.7482 22.5837 22.3339 22.5837H6.66626C6.25205 22.5837 5.91626 22.2479 5.91626 21.8337V18.667Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mb-3 font-semibold text-gray-800 text-theme-xl dark:text-white/90">
|
||||
Drag & Drop File Here
|
||||
</h4>
|
||||
<span
|
||||
class="mx-auto mb-5 block w-full max-w-[290px] text-sm text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
Drag and drop your PNG, JPG, WebP, SVG images here or browse
|
||||
</span>
|
||||
|
||||
<span class="font-medium underline cursor-pointer text-theme-sm text-brand-500">
|
||||
Browse File
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import Dropzone from 'dropzone'
|
||||
import 'dropzone/dist/dropzone.css'
|
||||
|
||||
const props = defineProps({
|
||||
uploadUrl: {
|
||||
type: String,
|
||||
default: '/upload',
|
||||
},
|
||||
})
|
||||
|
||||
const dropzoneForm = ref(null)
|
||||
const dropzoneId = `dropzone-${Math.random().toString(36).substr(2, 9)}`
|
||||
let dropzoneInstance = null
|
||||
|
||||
onMounted(() => {
|
||||
Dropzone.autoDiscover = false
|
||||
|
||||
dropzoneInstance = new Dropzone(`#${dropzoneId}`, {
|
||||
url: props.uploadUrl,
|
||||
thumbnailWidth: 150,
|
||||
maxFilesize: 0.5,
|
||||
acceptedFiles: 'image/jpeg,image/png,image/gif,image/webp,image/svg+xml',
|
||||
headers: { 'My-Awesome-Header': 'header value' },
|
||||
dictDefaultMessage: '',
|
||||
init: function () {
|
||||
this.on('addedfile', (file) => {
|
||||
console.log('A file has been added', file)
|
||||
})
|
||||
this.on('success', (file, response) => {
|
||||
console.log('File successfully uploaded', file, response)
|
||||
})
|
||||
this.on('error', (file, error) => {
|
||||
console.error('An error occurred during upload', file, error)
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (dropzoneInstance) {
|
||||
dropzoneInstance.destroy()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dropzone {
|
||||
border: 1px dashed #d0d5dd;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dropzone:hover {
|
||||
border-color: #465fff;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-image {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-details {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-progress {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-progress .dz-upload {
|
||||
background: #4f46e5;
|
||||
}
|
||||
|
||||
.dark .dropzone {
|
||||
background-color: #111827;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.dark .dropzone:hover {
|
||||
border-color: #6366f1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- Elements -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Upload file
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
class="focus:border-ring-brand-300 h-11 w-full overflow-hidden rounded-lg border border-gray-300 bg-transparent text-sm text-gray-500 shadow-theme-xs transition-colors file:mr-5 file:border-collapse file:cursor-pointer file:rounded-l-lg file:border-0 file:border-r file:border-solid file:border-gray-200 file:bg-gray-50 file:py-3 file:pl-3.5 file:pr-3 file:text-sm file:text-gray-700 placeholder:text-gray-400 hover:file:bg-gray-100 focus:outline-hidden focus:file:ring-brand-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-400 dark:text-white/90 dark:file:border-gray-800 dark:file:bg-white/[0.03] dark:file:text-gray-400 dark:placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- Email Input -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Email
|
||||
</label>
|
||||
<div class="relative">
|
||||
<span
|
||||
class="absolute left-0 top-1/2 -translate-y-1/2 border-r border-gray-200 px-3.5 py-3 text-gray-500 dark:border-gray-800 dark:text-gray-400"
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3.04175 7.06206V14.375C3.04175 14.6511 3.26561 14.875 3.54175 14.875H16.4584C16.7346 14.875 16.9584 14.6511 16.9584 14.375V7.06245L11.1443 11.1168C10.457 11.5961 9.54373 11.5961 8.85638 11.1168L3.04175 7.06206ZM16.9584 5.19262C16.9584 5.19341 16.9584 5.1942 16.9584 5.19498V5.20026C16.9572 5.22216 16.946 5.24239 16.9279 5.25501L10.2864 9.88638C10.1145 10.0062 9.8862 10.0062 9.71437 9.88638L3.07255 5.25485C3.05342 5.24151 3.04202 5.21967 3.04202 5.19636C3.042 5.15695 3.07394 5.125 3.11335 5.125H16.8871C16.9253 5.125 16.9564 5.15494 16.9584 5.19262ZM18.4584 5.21428V14.375C18.4584 15.4796 17.563 16.375 16.4584 16.375H3.54175C2.43718 16.375 1.54175 15.4796 1.54175 14.375V5.19498C1.54175 5.1852 1.54194 5.17546 1.54231 5.16577C1.55858 4.31209 2.25571 3.625 3.11335 3.625H16.8871C17.7549 3.625 18.4584 4.32843 18.4585 5.19622C18.4585 5.20225 18.4585 5.20826 18.4584 5.21428Z"
|
||||
fill="#667085"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
v-model="email"
|
||||
type="text"
|
||||
placeholder="info@gmail.com"
|
||||
class="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 pl-[62px] text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phone Input with Prepended Country Code -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Phone
|
||||
</label>
|
||||
<div class="relative">
|
||||
<div class="absolute">
|
||||
<select
|
||||
v-model="selectedCountry"
|
||||
@change="updatePhoneNumber"
|
||||
class="appearance-none rounded-l-lg border-0 border-r border-gray-200 bg-transparent bg-none py-3 pl-3.5 pr-8 leading-tight text-gray-700 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:text-gray-400"
|
||||
>
|
||||
<option v-for="(code, country) in countryCodes" :key="country" :value="country">
|
||||
{{ country }}
|
||||
</option>
|
||||
</select>
|
||||
<div
|
||||
class="absolute inset-y-0 flex items-center text-gray-700 pointer-events-none right-3 dark:text-gray-400"
|
||||
>
|
||||
<svg
|
||||
class="stroke-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.79175 7.396L10.0001 12.6043L15.2084 7.396"
|
||||
stroke=""
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
v-model="phoneNumber"
|
||||
placeholder="+1 (555) 000-0000"
|
||||
type="tel"
|
||||
class="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent py-3 pl-[84px] pr-4 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phone Input with Appended Country Code -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Phone
|
||||
</label>
|
||||
<div class="relative">
|
||||
<div class="absolute right-0">
|
||||
<select
|
||||
v-model="selectedCountry2"
|
||||
@change="updatePhoneNumber2"
|
||||
class="appearance-none rounded-r-lg border-0 border-l border-gray-200 bg-transparent bg-none py-3 pl-3.5 pr-8 leading-tight text-gray-700 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:text-gray-400"
|
||||
>
|
||||
<option v-for="(code, country) in countryCodes" :key="country" :value="country">
|
||||
{{ country }}
|
||||
</option>
|
||||
</select>
|
||||
<div
|
||||
class="absolute inset-y-0 flex items-center text-gray-700 pointer-events-none right-3 dark:text-gray-400"
|
||||
>
|
||||
<svg
|
||||
class="stroke-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.79175 7.396L10.0001 12.6043L15.2084 7.396"
|
||||
stroke=""
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
v-model="phoneNumber2"
|
||||
placeholder="+1 (555) 000-0000"
|
||||
type="tel"
|
||||
class="dark:bg-dark-900 h-11 w-full p-3 rounded-lg border border-gray-300 bg-transparent py-3 pr-[84px] text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- URL Input -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"> URL </label>
|
||||
<div class="relative">
|
||||
<span
|
||||
class="absolute left-0 top-1/2 inline-flex h-11 -translate-y-1/2 items-center justify-center border-r border-gray-200 py-3 pl-3.5 pr-3 text-gray-500 dark:border-gray-800 dark:text-gray-400"
|
||||
>
|
||||
http://
|
||||
</span>
|
||||
<input
|
||||
v-model="url"
|
||||
type="url"
|
||||
placeholder="www.tailadmin.com"
|
||||
class="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 pl-[90px] text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Website Input with Copy Button -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Website
|
||||
</label>
|
||||
<div class="relative">
|
||||
<button
|
||||
@click="copyWebsite"
|
||||
class="absolute right-0 top-1/2 inline-flex -translate-y-1/2 cursor-pointer items-center gap-1 border-l border-gray-200 py-3 pl-3.5 pr-3 text-sm font-medium text-gray-700 dark:border-gray-800 dark:text-gray-400"
|
||||
>
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.58822 4.58398C6.58822 4.30784 6.81207 4.08398 7.08822 4.08398H15.4154C15.6915 4.08398 15.9154 4.30784 15.9154 4.58398L15.9154 12.9128C15.9154 13.189 15.6916 13.4128 15.4154 13.4128H7.08821C6.81207 13.4128 6.58822 13.189 6.58822 12.9128V4.58398ZM7.08822 2.58398C5.98365 2.58398 5.08822 3.47942 5.08822 4.58398V5.09416H4.58496C3.48039 5.09416 2.58496 5.98959 2.58496 7.09416V15.4161C2.58496 16.5207 3.48039 17.4161 4.58496 17.4161H12.9069C14.0115 17.4161 14.9069 16.5207 14.9069 15.4161L14.9069 14.9128H15.4154C16.52 14.9128 17.4154 14.0174 17.4154 12.9128L17.4154 4.58398C17.4154 3.47941 16.52 2.58398 15.4154 2.58398H7.08822ZM13.4069 14.9128H7.08821C5.98364 14.9128 5.08822 14.0174 5.08822 12.9128V6.59416H4.58496C4.30882 6.59416 4.08496 6.81801 4.08496 7.09416V15.4161C4.08496 15.6922 4.30882 15.9161 4.58496 15.9161H12.9069C13.183 15.9161 13.4069 15.6922 13.4069 15.4161L13.4069 14.9128Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
<div>{{ copyText }}</div>
|
||||
</button>
|
||||
<input
|
||||
v-model="website"
|
||||
type="url"
|
||||
class="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent py-3 pl-4 pr-[90px] text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const email = ref('')
|
||||
const selectedCountry = ref('US')
|
||||
const selectedCountry2 = ref('US')
|
||||
const phoneNumber = ref('')
|
||||
const phoneNumber2 = ref('')
|
||||
const url = ref('')
|
||||
const website = ref('www.tailadmin.com')
|
||||
const copyText = ref('Copy')
|
||||
|
||||
const countryCodes = {
|
||||
US: '+1',
|
||||
GB: '+44',
|
||||
CA: '+1',
|
||||
AU: '+61',
|
||||
}
|
||||
|
||||
const updatePhoneNumber = () => {
|
||||
phoneNumber.value = countryCodes[selectedCountry.value as keyof typeof countryCodes]
|
||||
}
|
||||
|
||||
const updatePhoneNumber2 = () => {
|
||||
phoneNumber2.value = countryCodes[selectedCountry2.value as keyof typeof countryCodes]
|
||||
}
|
||||
|
||||
const copyWebsite = () => {
|
||||
navigator.clipboard.writeText(website.value)
|
||||
copyText.value = 'Copied!'
|
||||
setTimeout(() => {
|
||||
copyText.value = 'Copy'
|
||||
}, 2000)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- Error State Input -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Email
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
v-model="errorEmail"
|
||||
class="dark:bg-dark-900 w-full rounded-lg border border-error-300 bg-transparent px-4 py-2.5 pr-10 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-error-300 focus:outline-hidden focus:ring-3 focus:ring-error-500/10 dark:border-error-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-error-800"
|
||||
/>
|
||||
<span class="absolute right-3.5 top-1/2 -translate-y-1/2">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.58325 7.99967C2.58325 5.00813 5.00838 2.58301 7.99992 2.58301C10.9915 2.58301 13.4166 5.00813 13.4166 7.99967C13.4166 10.9912 10.9915 13.4163 7.99992 13.4163C5.00838 13.4163 2.58325 10.9912 2.58325 7.99967ZM7.99992 1.08301C4.17995 1.08301 1.08325 4.17971 1.08325 7.99967C1.08325 11.8196 4.17995 14.9163 7.99992 14.9163C11.8199 14.9163 14.9166 11.8196 14.9166 7.99967C14.9166 4.17971 11.8199 1.08301 7.99992 1.08301ZM7.09932 5.01639C7.09932 5.51345 7.50227 5.91639 7.99932 5.91639H7.99999C8.49705 5.91639 8.89999 5.51345 8.89999 5.01639C8.89999 4.51933 8.49705 4.11639 7.99999 4.11639H7.99932C7.50227 4.11639 7.09932 4.51933 7.09932 5.01639ZM7.99998 11.8306C7.58576 11.8306 7.24998 11.4948 7.24998 11.0806V7.29627C7.24998 6.88206 7.58576 6.54627 7.99998 6.54627C8.41419 6.54627 8.74998 6.88206 8.74998 7.29627V11.0806C8.74998 11.4948 8.41419 11.8306 7.99998 11.8306Z"
|
||||
fill="#F04438"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-1.5 text-theme-xs text-error-500">This is an error message.</p>
|
||||
</div>
|
||||
|
||||
<!-- Success State Input -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Email
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
v-model="successEmail"
|
||||
class="dark:bg-dark-900 w-full rounded-lg border border-success-300 bg-transparent px-4 py-2.5 pr-10 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-success-300 focus:outline-hidden focus:ring-3 focus:ring-success-500/10 dark:border-success-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-success-800"
|
||||
/>
|
||||
<span class="absolute right-3.5 top-1/2 -translate-y-1/2">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.61792 8.00034C2.61792 5.02784 5.0276 2.61816 8.00009 2.61816C10.9726 2.61816 13.3823 5.02784 13.3823 8.00034C13.3823 10.9728 10.9726 13.3825 8.00009 13.3825C5.0276 13.3825 2.61792 10.9728 2.61792 8.00034ZM8.00009 1.11816C4.19917 1.11816 1.11792 4.19942 1.11792 8.00034C1.11792 11.8013 4.19917 14.8825 8.00009 14.8825C11.801 14.8825 14.8823 11.8013 14.8823 8.00034C14.8823 4.19942 11.801 1.11816 8.00009 1.11816ZM10.5192 7.266C10.8121 6.97311 10.8121 6.49823 10.5192 6.20534C10.2264 5.91245 9.75148 5.91245 9.45858 6.20534L7.45958 8.20434L6.54162 7.28638C6.24873 6.99349 5.77385 6.99349 5.48096 7.28638C5.18807 7.57927 5.18807 8.05415 5.48096 8.34704L6.92925 9.79533C7.0699 9.93599 7.26067 10.015 7.45958 10.015C7.6585 10.015 7.84926 9.93599 7.98991 9.79533L10.5192 7.266Z"
|
||||
fill="#12B76A"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-1.5 text-theme-xs text-success-500">This is a success message.</p>
|
||||
</div>
|
||||
|
||||
<!-- Disabled State Input -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-300 dark:text-white/15">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="info@gmail.com"
|
||||
disabled
|
||||
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:shadow-focus-ring focus:outline-hidden disabled:border-gray-100 disabled:placeholder:text-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-gray-400 dark:focus:border-brand-300 dark:disabled:border-gray-800 dark:disabled:placeholder:text-white/15"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const errorEmail = ref('demoemail')
|
||||
const successEmail = ref('demoemail@gmail.com')
|
||||
</script>
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="relative" ref="multiSelectRef">
|
||||
<div
|
||||
@click="toggleDropdown"
|
||||
class="dark:bg-dark-900 h-11 flex items-center w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
:class="{ 'text-gray-800 dark:text-white/90': isOpen }"
|
||||
>
|
||||
<span v-if="selectedItems.length === 0" class="text-gray-400"> Select items... </span>
|
||||
<div class="flex flex-wrap items-center flex-auto gap-2">
|
||||
<div
|
||||
v-for="item in selectedItems"
|
||||
:key="item.value"
|
||||
class="group flex items-center justify-center h-[30px] rounded-full border-[0.7px] border-transparent bg-gray-100 py-1 pl-2.5 pr-2 text-sm text-gray-800 hover:border-gray-200 dark:bg-gray-800 dark:text-white/90 dark:hover:border-gray-800"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<button
|
||||
@click.stop="removeItem(item)"
|
||||
class="pl-2 text-gray-500 cursor-pointer group-hover:text-gray-400 dark:text-gray-400"
|
||||
aria-label="Remove item"
|
||||
>
|
||||
<svg
|
||||
role="button"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3.40717 4.46881C3.11428 4.17591 3.11428 3.70104 3.40717 3.40815C3.70006 3.11525 4.17494 3.11525 4.46783 3.40815L6.99943 5.93975L9.53095 3.40822C9.82385 3.11533 10.2987 3.11533 10.5916 3.40822C10.8845 3.70112 10.8845 4.17599 10.5916 4.46888L8.06009 7.00041L10.5916 9.53193C10.8845 9.82482 10.8845 10.2997 10.5916 10.5926C10.2987 10.8855 9.82385 10.8855 9.53095 10.5926L6.99943 8.06107L4.46783 10.5927C4.17494 10.8856 3.70006 10.8856 3.40717 10.5927C3.11428 10.2998 3.11428 9.8249 3.40717 9.53201L5.93877 7.00041L3.40717 4.46881Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
class="ml-auto"
|
||||
:class="{ 'transform rotate-180': isOpen }"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.79175 7.39551L10.0001 12.6038L15.2084 7.39551"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<transition
|
||||
enter-active-class="transition duration-100 ease-out"
|
||||
enter-from-class="transform scale-95 opacity-0"
|
||||
enter-to-class="transform scale-100 opacity-100"
|
||||
leave-active-class="transition duration-75 ease-in"
|
||||
leave-from-class="transform scale-100 opacity-100"
|
||||
leave-to-class="transform scale-95 opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="absolute z-10 w-full mt-1 bg-white rounded-lg shadow-sm dark:bg-gray-900"
|
||||
>
|
||||
<ul
|
||||
class="overflow-y-auto divide-y divide-gray-200 custom-scrollbar max-h-60 dark:divide-gray-800"
|
||||
role="listbox"
|
||||
aria-multiselectable="true"
|
||||
>
|
||||
<li
|
||||
v-for="item in props.options"
|
||||
:key="item.value"
|
||||
@click="toggleItem(item)"
|
||||
class="relative flex items-center w-full px-3 py-2 border-transparent cursor-pointer first:rounded-t-lg last:rounded-b-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
:class="{ 'bg-gray-50 dark:bg-white/[0.03]': isSelected(item) }"
|
||||
role="option"
|
||||
:aria-selected="isSelected(item)"
|
||||
>
|
||||
<span class="grow">{{ item.label }}</span>
|
||||
<svg
|
||||
v-if="isSelected(item)"
|
||||
class="w-5 h-5 text-gray-400 dark:text-gray-300"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
></path>
|
||||
</svg>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const isOpen = ref(false)
|
||||
const selectedItems = ref(props.modelValue)
|
||||
const multiSelectRef = ref(null)
|
||||
|
||||
const toggleDropdown = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
|
||||
const toggleItem = (item) => {
|
||||
const index = selectedItems.value.findIndex((selected) => selected.value === item.value)
|
||||
if (index === -1) {
|
||||
selectedItems.value.push(item)
|
||||
} else {
|
||||
selectedItems.value.splice(index, 1)
|
||||
}
|
||||
emit('update:modelValue', selectedItems.value)
|
||||
}
|
||||
|
||||
const removeItem = (item) => {
|
||||
const index = selectedItems.value.findIndex((selected) => selected.value === item.value)
|
||||
if (index !== -1) {
|
||||
selectedItems.value.splice(index, 1)
|
||||
emit('update:modelValue', selectedItems.value)
|
||||
}
|
||||
}
|
||||
|
||||
const isSelected = (item) => {
|
||||
return selectedItems.value.some((selected) => selected.value === item.value)
|
||||
}
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (multiSelectRef.value && !multiSelectRef.value.contains(event.target)) {
|
||||
isOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- Single Select Input -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Select Input
|
||||
</label>
|
||||
<div class="relative z-20 bg-transparent">
|
||||
<select
|
||||
v-model="singleSelect"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
:class="{ 'text-gray-800 dark:text-white/90': singleSelect }"
|
||||
>
|
||||
<option value="" disabled>Select Option</option>
|
||||
<option value="marketing" class="text-gray-700 dark:bg-gray-900 dark:text-gray-400">
|
||||
Marketing
|
||||
</option>
|
||||
<option value="template" class="text-gray-700 dark:bg-gray-900 dark:text-gray-400">
|
||||
Template
|
||||
</option>
|
||||
<option value="development" class="text-gray-700 dark:bg-gray-900 dark:text-gray-400">
|
||||
Development
|
||||
</option>
|
||||
</select>
|
||||
<span
|
||||
class="absolute z-30 text-gray-700 -translate-y-1/2 pointer-events-none right-4 top-1/2 dark:text-gray-400"
|
||||
>
|
||||
<svg
|
||||
class="stroke-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.79175 7.396L10.0001 12.6043L15.2084 7.396"
|
||||
stroke=""
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Multiple Select Input -->
|
||||
<div>
|
||||
<MultipleSelect v-model="selectedItems" :options="optionss" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import MultipleSelect from './MultipleSelect.vue'
|
||||
|
||||
const optionss = [
|
||||
{ value: 'apple', label: 'Apple' },
|
||||
{ value: 'banana', label: 'Banana' },
|
||||
{ value: 'cherry', label: 'Cherry' },
|
||||
{ value: 'date', label: 'Date' },
|
||||
{ value: 'elderberry', label: 'Elderberry' },
|
||||
{ value: 'graphs', label: 'Graphs' },
|
||||
]
|
||||
|
||||
const selectedItems = ref([])
|
||||
|
||||
const singleSelect = ref('')
|
||||
|
||||
const options = ref([
|
||||
{ text: 'Option 1', selected: false },
|
||||
{ text: 'Option 2', selected: false },
|
||||
{ text: 'Option 3', selected: false },
|
||||
{ text: 'Option 4', selected: false },
|
||||
])
|
||||
|
||||
const selected = computed(() => options.value.filter((option) => option.selected))
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- Normal Textarea -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
v-model="normalDescription"
|
||||
placeholder="Enter a description..."
|
||||
rows="6"
|
||||
class="dark:bg-dark-900 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Disabled Textarea -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-300 dark:text-white/15">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
v-model="disabledDescription"
|
||||
placeholder="Enter a description..."
|
||||
rows="6"
|
||||
disabled
|
||||
class="dark:bg-dark-900 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:shadow-focus-ring focus:outline-hidden focus:ring-0 disabled:border-gray-100 disabled:bg-gray-50 disabled:placeholder:text-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800 dark:disabled:border-gray-800 dark:disabled:bg-white/[0.03] dark:disabled:placeholder:text-white/15"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Error State Textarea -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
v-model="errorDescription"
|
||||
placeholder="Enter a description..."
|
||||
rows="6"
|
||||
class="dark:bg-dark-900 w-full rounded-lg border border-error-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-error-300 focus:outline-hidden focus:ring-3 focus:ring-error-500/10 dark:border-error-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-error-800"
|
||||
></textarea>
|
||||
<p class="mt-1.5 text-theme-xs text-error-500">Please enter a message in the textarea.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const normalDescription = ref('')
|
||||
const disabledDescription = ref('This textarea is disabled')
|
||||
const errorDescription = ref('')
|
||||
</script>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="min-h-screen xl:flex">
|
||||
<app-sidebar />
|
||||
<Backdrop />
|
||||
<div
|
||||
class="flex min-h-screen flex-1 flex-col transition-all duration-300 ease-in-out"
|
||||
:class="[isExpanded || isHovered ? 'lg:ml-[290px]' : 'lg:ml-[90px]']"
|
||||
>
|
||||
<app-header />
|
||||
<div class="mx-auto w-full max-w-(--breakpoint-2xl) flex-1 p-4 md:p-6">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<footer class="min-h-18 border-t border-gray-200 bg-white px-4 py-5 text-sm text-gray-500 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 md:px-6">
|
||||
<div v-if="site.homeFooterContent.value" class="site-footer-content mx-auto w-full max-w-(--breakpoint-2xl)" v-html="site.homeFooterContent.value"></div>
|
||||
<div v-else class="mx-auto flex min-h-8 w-full max-w-(--breakpoint-2xl) flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<span>Copyright 2026 {{ site.siteName.value }}. 请在合法合规场景中使用代理服务。</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AppSidebar from './AppSidebar.vue'
|
||||
import AppHeader from './AppHeader.vue'
|
||||
import { useSidebar } from '@/composables/useSidebar'
|
||||
import Backdrop from './Backdrop.vue'
|
||||
import { useSiteStore } from '@/stores/site'
|
||||
const { isExpanded, isHovered } = useSidebar()
|
||||
const site = useSiteStore()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.site-footer-content {
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.site-footer-content :deep(a) {
|
||||
color: #0284c7;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.site-footer-content :deep(img) {
|
||||
display: inline-block;
|
||||
max-height: 40px;
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.site-footer-content :deep(p) {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<header
|
||||
class="sticky top-0 flex w-full bg-white border-gray-200 z-99999 dark:border-gray-800 dark:bg-gray-900 lg:border-b"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-between grow lg:flex-row lg:px-6">
|
||||
<div
|
||||
class="flex items-center justify-between w-full gap-2 px-3 py-3 border-b border-gray-200 dark:border-gray-800 sm:gap-4 lg:justify-normal lg:border-b-0 lg:px-0 lg:py-4"
|
||||
>
|
||||
<button
|
||||
@click="handleToggle"
|
||||
class="flex items-center justify-center w-10 h-10 text-gray-500 border-gray-200 rounded-lg z-99999 dark:border-gray-800 dark:text-gray-400 lg:h-11 lg:w-11 lg:border"
|
||||
:class="[
|
||||
isMobileOpen
|
||||
? 'lg:bg-transparent dark:lg:bg-transparent bg-gray-100 dark:bg-gray-800'
|
||||
: '',
|
||||
]"
|
||||
>
|
||||
<svg
|
||||
v-if="isMobileOpen"
|
||||
class="fill-current"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.21967 7.28131C5.92678 6.98841 5.92678 6.51354 6.21967 6.22065C6.51256 5.92775 6.98744 5.92775 7.28033 6.22065L11.999 10.9393L16.7176 6.22078C17.0105 5.92789 17.4854 5.92788 17.7782 6.22078C18.0711 6.51367 18.0711 6.98855 17.7782 7.28144L13.0597 12L17.7782 16.7186C18.0711 17.0115 18.0711 17.4863 17.7782 17.7792C17.4854 18.0721 17.0105 18.0721 16.7176 17.7792L11.999 13.0607L7.28033 17.7794C6.98744 18.0722 6.51256 18.0722 6.21967 17.7794C5.92678 17.4865 5.92678 17.0116 6.21967 16.7187L10.9384 12L6.21967 7.28131Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
width="16"
|
||||
height="12"
|
||||
viewBox="0 0 16 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M0.583252 1C0.583252 0.585788 0.919038 0.25 1.33325 0.25H14.6666C15.0808 0.25 15.4166 0.585786 15.4166 1C15.4166 1.41421 15.0808 1.75 14.6666 1.75L1.33325 1.75C0.919038 1.75 0.583252 1.41422 0.583252 1ZM0.583252 11C0.583252 10.5858 0.919038 10.25 1.33325 10.25L14.6666 10.25C15.0808 10.25 15.4166 10.5858 15.4166 11C15.4166 11.4142 15.0808 11.75 14.6666 11.75L1.33325 11.75C0.919038 11.75 0.583252 11.4142 0.583252 11ZM1.33325 5.25C0.919038 5.25 0.583252 5.58579 0.583252 6C0.583252 6.41421 0.919038 6.75 1.33325 6.75L7.99992 6.75C8.41413 6.75 8.74992 6.41421 8.74992 6C8.74992 5.58579 8.41413 5.25 7.99992 5.25L1.33325 5.25Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<HeaderLogo />
|
||||
<button
|
||||
@click="toggleApplicationMenu"
|
||||
class="flex items-center justify-center w-10 h-10 text-gray-700 rounded-lg z-99999 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 lg:hidden"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.99902 10.4951C6.82745 10.4951 7.49902 11.1667 7.49902 11.9951V12.0051C7.49902 12.8335 6.82745 13.5051 5.99902 13.5051C5.1706 13.5051 4.49902 12.8335 4.49902 12.0051V11.9951C4.49902 11.1667 5.1706 10.4951 5.99902 10.4951ZM17.999 10.4951C18.8275 10.4951 19.499 11.1667 19.499 11.9951V12.0051C19.499 12.8335 18.8275 13.5051 17.999 13.5051C17.1706 13.5051 16.499 12.8335 16.499 12.0051V11.9951C16.499 11.1667 17.1706 10.4951 17.999 10.4951ZM13.499 11.9951C13.499 11.1667 12.8275 10.4951 11.999 10.4951C11.1706 10.4951 10.499 11.1667 10.499 11.9951V12.0051C10.499 12.8335 11.1706 13.5051 11.999 13.5051C12.8275 13.5051 13.499 12.8335 13.499 12.0051V11.9951Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<SearchBar />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="[isApplicationMenuOpen ? 'flex' : 'hidden']"
|
||||
class="items-center justify-between w-full gap-4 px-5 py-4 shadow-theme-md lg:flex lg:justify-end lg:px-0 lg:shadow-none"
|
||||
>
|
||||
<div class="flex items-center gap-2 2xsm:gap-3">
|
||||
<ThemeToggler />
|
||||
<NotificationMenu />
|
||||
</div>
|
||||
<UserMenu />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useSidebar } from '@/composables/useSidebar'
|
||||
import ThemeToggler from '../common/ThemeToggler.vue'
|
||||
import SearchBar from './header/SearchBar.vue'
|
||||
import HeaderLogo from './header/HeaderLogo.vue'
|
||||
import NotificationMenu from './header/NotificationMenu.vue'
|
||||
import UserMenu from './header/UserMenu.vue'
|
||||
|
||||
const { toggleSidebar, toggleMobileSidebar, isMobileOpen } = useSidebar()
|
||||
|
||||
const handleToggle = () => {
|
||||
if (window.innerWidth >= 1024) {
|
||||
toggleSidebar()
|
||||
} else {
|
||||
toggleMobileSidebar()
|
||||
}
|
||||
}
|
||||
|
||||
const dropdownOpen = ref(false)
|
||||
const notifying = ref(false)
|
||||
|
||||
const toggleDropdown = () => {
|
||||
dropdownOpen.value = !dropdownOpen.value
|
||||
notifying.value = false
|
||||
}
|
||||
|
||||
const isApplicationMenuOpen = ref(false)
|
||||
|
||||
const toggleApplicationMenu = () => {
|
||||
isApplicationMenuOpen.value = !isApplicationMenuOpen.value
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<aside
|
||||
:class="[
|
||||
'fixed mt-16 flex flex-col lg:mt-0 top-0 px-5 left-0 bg-white dark:bg-gray-900 dark:border-gray-800 text-gray-900 h-screen transition-all duration-300 ease-in-out z-99999 border-r border-gray-200',
|
||||
{
|
||||
'lg:w-[290px]': isExpanded || isMobileOpen || isHovered,
|
||||
'lg:w-[90px]': !isExpanded && !isHovered,
|
||||
'translate-x-0 w-[290px]': isMobileOpen,
|
||||
'-translate-x-full': !isMobileOpen,
|
||||
'lg:translate-x-0': true,
|
||||
},
|
||||
]"
|
||||
@mouseenter="!isExpanded && (isHovered = true)"
|
||||
@mouseleave="isHovered = false"
|
||||
>
|
||||
<div :class="['py-8 flex', !isExpanded && !isHovered ? 'lg:justify-center' : 'justify-start']">
|
||||
<router-link to="/" class="flex items-center gap-3">
|
||||
<img v-if="site.siteLogoUrl.value" :src="site.siteLogoUrl.value" :alt="site.siteName.value" class="h-10 w-10 shrink-0 rounded-xl object-contain shadow-theme-sm" />
|
||||
<span v-else class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-brand-500 text-base font-bold text-white shadow-theme-sm">{{ siteInitial }}</span>
|
||||
<span v-if="isExpanded || isHovered || isMobileOpen">
|
||||
<span class="block text-lg font-semibold text-gray-900 dark:text-white">{{ site.siteName.value }}</span>
|
||||
</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col overflow-y-auto duration-300 ease-linear no-scrollbar">
|
||||
<nav class="mb-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div v-for="menuGroup in menuGroups" :key="menuGroup.title">
|
||||
<h2
|
||||
:class="[
|
||||
'mb-4 text-xs uppercase flex leading-[20px] text-gray-400',
|
||||
!isExpanded && !isHovered ? 'lg:justify-center' : 'justify-start',
|
||||
]"
|
||||
>
|
||||
<template v-if="isExpanded || isHovered || isMobileOpen">{{ menuGroup.title }}</template>
|
||||
<HorizontalDots v-else />
|
||||
</h2>
|
||||
<ul class="flex flex-col gap-2">
|
||||
<li v-for="item in menuGroup.items" :key="item.name">
|
||||
<router-link
|
||||
:to="item.path"
|
||||
:class="[
|
||||
'menu-item group',
|
||||
{
|
||||
'menu-item-active': isActive(item),
|
||||
'menu-item-inactive': !isActive(item),
|
||||
},
|
||||
!isExpanded && !isHovered ? 'lg:justify-center' : 'lg:justify-start',
|
||||
]"
|
||||
>
|
||||
<span :class="['menu-item-icon', isActive(item) ? 'menu-item-icon-active' : 'menu-item-icon-inactive']">
|
||||
<component :is="item.icon" />
|
||||
</span>
|
||||
<span v-if="isExpanded || isHovered || isMobileOpen" class="menu-item-text">{{ item.name }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
BoxCubeIcon,
|
||||
Calendar2Line,
|
||||
DedicatedProxyIcon,
|
||||
GridIcon,
|
||||
HorizontalDots,
|
||||
HomeResidentialIcon,
|
||||
LayoutDashboardIcon,
|
||||
PageIcon,
|
||||
PlugInIcon,
|
||||
StaticAIcon,
|
||||
StaticBIcon,
|
||||
SupportIcon,
|
||||
TableIcon,
|
||||
UserCircleIcon,
|
||||
UserGroupIcon,
|
||||
} from '@/icons'
|
||||
import { useSidebar } from '@/composables/useSidebar'
|
||||
import { useSiteStore } from '@/stores/site'
|
||||
|
||||
const route = useRoute()
|
||||
const { isExpanded, isMobileOpen, isHovered } = useSidebar()
|
||||
const site = useSiteStore()
|
||||
const siteInitial = computed(() => site.siteName.value.slice(0, 1) || '站')
|
||||
|
||||
const menuGroups = [
|
||||
{
|
||||
title: '控制台',
|
||||
items: [
|
||||
{ icon: LayoutDashboardIcon, name: '总览', path: '/console/dashboard' },
|
||||
{ icon: BoxCubeIcon, name: '购买套餐', path: '/console/buy' },
|
||||
{ icon: TableIcon, name: '我的订单', path: '/console/orders' },
|
||||
{ icon: Calendar2Line, name: '我的钱包', path: '/console/wallet' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '购买产品',
|
||||
items: [
|
||||
{ icon: StaticAIcon, name: '静态长效A (高带宽)', path: '/console/buy?type=staticA', productType: 'staticA' },
|
||||
{ icon: StaticBIcon, name: '静态长效B(特惠)', path: '/console/buy?type=staticB', productType: 'staticB' },
|
||||
{ icon: HomeResidentialIcon, name: '住宅长效', path: '/console/buy?type=home', productType: 'home' },
|
||||
{ icon: DedicatedProxyIcon, name: '独享长效', path: '/console/buy?type=custom', productType: 'custom' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '资源',
|
||||
items: [
|
||||
{ icon: GridIcon, name: '我的代理', path: '/console/static-assets' },
|
||||
{ icon: PlugInIcon, name: '开放 API', path: '/console/open-api' },
|
||||
{ icon: UserGroupIcon, name: '推广中心', path: '/console/promotion' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '账户',
|
||||
items: [
|
||||
{ icon: SupportIcon, name: '实名认证', path: '/console/verify' },
|
||||
{ icon: UserCircleIcon, name: '账户资料', path: '/console/profile' },
|
||||
{ icon: PageIcon, name: '返回官网', path: '/' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function isActive(item: { path: string; productType?: string }) {
|
||||
if (item.productType) {
|
||||
return route.path === '/console/buy' && route.query.type === item.productType
|
||||
}
|
||||
if (item.path === '/console/buy') {
|
||||
return route.path === '/console/buy' && !route.query.type
|
||||
}
|
||||
if (item.path === '/console/open-api') {
|
||||
return route.path === '/console/open-api' || route.path === '/console/open-api/docs'
|
||||
}
|
||||
return route.path === item.path
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="isMobileOpen"
|
||||
class="fixed inset-0 bg-gray-900/50 z-9999 lg:hidden"
|
||||
@click="toggleMobileSidebar"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSidebar } from '@/composables/useSidebar'
|
||||
const { toggleMobileSidebar, isMobileOpen } = useSidebar()
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="min-h-screen">
|
||||
<main>
|
||||
<slot></slot>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<slot></slot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSidebarProvider } from '@/composables/useSidebar'
|
||||
|
||||
useSidebarProvider()
|
||||
</script>
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div
|
||||
class="mx-auto mb-10 w-full max-w-60 rounded-2xl bg-gray-50 px-4 py-5 text-center dark:bg-white/[0.03]"
|
||||
>
|
||||
<h3 class="mb-2 font-semibold text-gray-900 dark:text-white">#1 Tailwind CSS Dashboard</h3>
|
||||
<p class="mb-4 text-gray-500 text-theme-sm dark:text-gray-400">
|
||||
Leading Tailwind CSS Admin Template with 400+ UI Component and Pages.
|
||||
</p>
|
||||
<a
|
||||
href="https://tailadmin.com/pricing"
|
||||
target="_blank"
|
||||
rel="nofollow"
|
||||
class="flex items-center justify-center p-3 font-medium text-white rounded-lg bg-brand-500 text-theme-sm hover:bg-brand-600"
|
||||
>
|
||||
Purchase Plan
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<slot></slot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, provide, onMounted, watch, computed } from 'vue'
|
||||
|
||||
type Theme = 'light' | 'dark'
|
||||
|
||||
const theme = ref<Theme>('light')
|
||||
const isInitialized = ref(false)
|
||||
|
||||
const isDarkMode = computed(() => theme.value === 'dark')
|
||||
|
||||
const toggleTheme = () => {
|
||||
theme.value = theme.value === 'light' ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const savedTheme = localStorage.getItem('theme') as Theme | null
|
||||
const initialTheme = savedTheme || 'light' // Default to light theme
|
||||
|
||||
theme.value = initialTheme
|
||||
isInitialized.value = true
|
||||
})
|
||||
|
||||
watch([theme, isInitialized], ([newTheme, newIsInitialized]) => {
|
||||
if (newIsInitialized) {
|
||||
localStorage.setItem('theme', newTheme)
|
||||
if (newTheme === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
provide('theme', {
|
||||
isDarkMode,
|
||||
toggleTheme,
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { inject } from 'vue'
|
||||
|
||||
export function useTheme() {
|
||||
const theme = inject('theme')
|
||||
if (!theme) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider')
|
||||
}
|
||||
return theme
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<router-link to="/" class="flex items-center gap-2 lg:hidden">
|
||||
<img v-if="site.siteLogoUrl.value" :src="site.siteLogoUrl.value" :alt="site.siteName.value" class="h-9 w-9 rounded-xl object-contain" />
|
||||
<span v-else class="flex h-9 w-9 items-center justify-center rounded-xl bg-brand-500 text-sm font-bold text-white">{{ siteInitial }}</span>
|
||||
<span class="text-base font-semibold text-gray-900 dark:text-white">{{ site.siteName.value }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useSiteStore } from '@/stores/site'
|
||||
|
||||
const site = useSiteStore()
|
||||
const siteInitial = computed(() => site.siteName.value.slice(0, 1) || '站')
|
||||
</script>
|
||||
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div class="relative" ref="dropdownRef">
|
||||
<button
|
||||
class="relative flex h-11 w-11 items-center justify-center rounded-full border border-gray-200 bg-white text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
||||
aria-label="查看公告消息"
|
||||
@click="toggleDropdown"
|
||||
>
|
||||
<span v-if="notices.length" class="absolute right-0 top-0.5 z-1 h-2 w-2 rounded-full bg-brand-400"></span>
|
||||
<BellIcon />
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="dropdownOpen"
|
||||
class="absolute -right-[180px] mt-[17px] flex w-[330px] flex-col rounded-2xl border border-gray-200 bg-white p-3 shadow-theme-lg dark:border-gray-800 dark:bg-gray-dark sm:w-[380px] lg:right-0"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between border-b border-gray-100 pb-3 dark:border-gray-800">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-white">公告消息</h3>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">平台公告与服务提醒</p>
|
||||
</div>
|
||||
<button class="text-xs font-medium text-brand-500" :disabled="loading" @click="refreshNotices">
|
||||
{{ loading ? '刷新中' : '刷新' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="px-3 py-8 text-center text-sm text-gray-500 dark:text-gray-400">正在加载公告...</div>
|
||||
<ul v-else-if="notices.length" class="max-h-[360px] overflow-y-auto custom-scrollbar">
|
||||
<li v-for="notice in notices" :key="notice.id">
|
||||
<button
|
||||
class="block w-full rounded-xl px-3 py-3 text-left transition hover:bg-gray-50 dark:hover:bg-white/[0.04]"
|
||||
@click="openDetail(notice)"
|
||||
>
|
||||
<div class="mb-1 flex items-center gap-2">
|
||||
<span class="rounded-full px-2 py-0.5 text-[11px] font-medium" :class="levelClass(notice.level)">
|
||||
{{ levelText(notice.level) }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ notice.publishTime || notice.createTime || '-' }}</span>
|
||||
</div>
|
||||
<p class="line-clamp-2 text-sm font-medium leading-6 text-gray-800 dark:text-white/90">{{ notice.title || '公告消息' }}</p>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="px-3 py-8 text-center text-sm text-gray-500 dark:text-gray-400">暂无公告消息</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="detailVisible" class="fixed inset-0 z-999999 grid place-items-center bg-gray-950/50 p-5" @click.self="closeDetail">
|
||||
<div class="w-full max-w-2xl rounded-2xl bg-white p-6 shadow-theme-xl dark:bg-gray-900">
|
||||
<div class="mb-4 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-brand-500">公告详情</p>
|
||||
<h3 class="mt-1 text-lg font-semibold text-gray-900 dark:text-white">{{ detail?.title || '-' }}</h3>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ detail?.publishTime || '-' }}</p>
|
||||
</div>
|
||||
<button class="rounded-lg border border-gray-200 px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-50 dark:border-gray-800 dark:text-gray-300 dark:hover:bg-white/[0.04]" @click="closeDetail">
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
<div class="max-h-[55vh] overflow-y-auto rounded-xl bg-gray-50 p-4 text-sm leading-7 text-gray-700 dark:bg-white/[0.03] dark:text-gray-300" v-html="detailContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { BellIcon } from '@/icons'
|
||||
import { MemberAPI, type NoticeDetail, type NoticeItem } from '@/api/member'
|
||||
|
||||
const AUTO_POPUP_SEEN_KEY = 'member_notice_auto_popup_seen_ids'
|
||||
const MAX_CACHED_NOTICE_IDS = 200
|
||||
|
||||
const dropdownOpen = ref(false)
|
||||
const loading = ref(false)
|
||||
const notices = ref<NoticeItem[]>([])
|
||||
const detail = ref<NoticeDetail | null>(null)
|
||||
const detailVisible = ref(false)
|
||||
const dropdownRef = ref<HTMLElement | null>(null)
|
||||
const detailContent = computed(() => detail.value?.content || '<p>暂无内容</p>')
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
loadNotices({ autoPopup: true })
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
function toggleDropdown() {
|
||||
dropdownOpen.value = !dropdownOpen.value
|
||||
if (dropdownOpen.value && !notices.value.length) loadNotices()
|
||||
}
|
||||
|
||||
function refreshNotices() {
|
||||
void loadNotices()
|
||||
}
|
||||
|
||||
async function loadNotices(options: { autoPopup?: boolean } = {}) {
|
||||
loading.value = true
|
||||
try {
|
||||
const page = await MemberAPI.notices({ pageNum: 1, pageSize: 8 })
|
||||
notices.value = page.list || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
if (options.autoPopup) await maybeShowInitialNoticePopup()
|
||||
}
|
||||
|
||||
async function openDetail(notice: NoticeItem) {
|
||||
if (!notice.id) return false
|
||||
detail.value = await MemberAPI.noticeDetail(notice.id)
|
||||
detailVisible.value = true
|
||||
dropdownOpen.value = false
|
||||
return true
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
detailVisible.value = false
|
||||
detail.value = null
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (detailVisible.value) return
|
||||
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
|
||||
dropdownOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function levelText(level?: string) {
|
||||
if (level === 'H') return '重要'
|
||||
if (level === 'M') return '提醒'
|
||||
return '普通'
|
||||
}
|
||||
|
||||
function levelClass(level?: string) {
|
||||
if (level === 'H') return 'bg-error-50 text-error-600 dark:bg-error-500/15 dark:text-error-400'
|
||||
if (level === 'M') return 'bg-warning-50 text-warning-700 dark:bg-warning-500/15 dark:text-orange-400'
|
||||
return 'bg-brand-50 text-brand-600 dark:bg-brand-500/15 dark:text-brand-300'
|
||||
}
|
||||
|
||||
async function maybeShowInitialNoticePopup() {
|
||||
if (!notices.value.length || detailVisible.value) return
|
||||
|
||||
const seenIds = getAutoPopupSeenIds()
|
||||
const firstUnseen = notices.value.find((notice) => {
|
||||
const key = getNoticeIdKey(notice)
|
||||
return key && !seenIds.has(key)
|
||||
})
|
||||
if (!firstUnseen) return
|
||||
|
||||
try {
|
||||
const opened = await openDetail(firstUnseen)
|
||||
if (opened) rememberAutoPopupSeen(notices.value)
|
||||
} catch {
|
||||
// Ignore auto-popup failures; the notice list can still be opened manually.
|
||||
}
|
||||
}
|
||||
|
||||
function rememberAutoPopupSeen(items: NoticeItem[]) {
|
||||
const seenIds = getAutoPopupSeenIds()
|
||||
items.forEach((notice) => {
|
||||
const key = getNoticeIdKey(notice)
|
||||
if (key) seenIds.add(key)
|
||||
})
|
||||
const cachedIds = Array.from(seenIds).slice(-MAX_CACHED_NOTICE_IDS)
|
||||
localStorage.setItem(AUTO_POPUP_SEEN_KEY, JSON.stringify(cachedIds))
|
||||
}
|
||||
|
||||
function getAutoPopupSeenIds() {
|
||||
try {
|
||||
const value = localStorage.getItem(AUTO_POPUP_SEEN_KEY)
|
||||
const parsed = value ? JSON.parse(value) : []
|
||||
return new Set(Array.isArray(parsed) ? parsed.map(String) : [])
|
||||
} catch {
|
||||
return new Set<string>()
|
||||
}
|
||||
}
|
||||
|
||||
function getNoticeIdKey(notice: NoticeItem) {
|
||||
return notice.id ? String(notice.id) : ''
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,258 @@
|
||||
<template>
|
||||
<div class="hidden lg:block">
|
||||
<div ref="rootRef" class="relative">
|
||||
<span class="absolute -translate-y-1/2 left-4 top-1/2 text-gray-500 dark:text-gray-400">
|
||||
<Search class="h-5 w-5" />
|
||||
</span>
|
||||
<input
|
||||
v-model="keyword"
|
||||
type="text"
|
||||
placeholder="搜索订单、代理地址或节点..."
|
||||
class="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-200 bg-transparent py-2.5 pl-12 pr-4 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800 xl:w-[430px]"
|
||||
@focus="openPanel"
|
||||
@keydown.enter.prevent="submitSearch"
|
||||
@keydown.esc="closePanel"
|
||||
/>
|
||||
<span v-if="loading" class="absolute -translate-y-1/2 right-4 top-1/2 text-gray-400">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
</span>
|
||||
|
||||
<div
|
||||
v-if="panelOpen"
|
||||
class="absolute left-0 right-0 top-full z-999 mt-2 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-theme-lg dark:border-gray-800 dark:bg-gray-900"
|
||||
>
|
||||
<div class="custom-scrollbar max-h-96 overflow-y-auto p-2">
|
||||
<template v-if="quickResults.length">
|
||||
<p class="px-3 py-2 text-xs font-medium text-gray-400">功能入口</p>
|
||||
<button
|
||||
v-for="item in quickResults"
|
||||
:key="item.path"
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-left transition-colors hover:bg-gray-50 dark:hover:bg-white/[0.05]"
|
||||
@mousedown.prevent="goToPath(item.path)"
|
||||
>
|
||||
<span class="min-w-0">
|
||||
<span class="block truncate text-sm font-medium text-gray-800 dark:text-white/90">{{ item.title }}</span>
|
||||
<span class="mt-0.5 block truncate text-xs text-gray-500 dark:text-gray-400">{{ item.description }}</span>
|
||||
</span>
|
||||
<ArrowUpRight class="h-4 w-4 shrink-0 text-gray-400" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template v-if="orderResults.length">
|
||||
<p class="px-3 py-2 text-xs font-medium text-gray-400">订单</p>
|
||||
<button
|
||||
v-for="item in orderResults"
|
||||
:key="`order-${resultText(item, 'orderNo')}`"
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-left transition-colors hover:bg-gray-50 dark:hover:bg-white/[0.05]"
|
||||
@mousedown.prevent="goToOrders(item)"
|
||||
>
|
||||
<span class="min-w-0">
|
||||
<span class="block truncate text-sm font-medium text-gray-800 dark:text-white/90">{{ resultText(item, 'orderNo') || '订单' }}</span>
|
||||
<span class="mt-0.5 block truncate text-xs text-gray-500 dark:text-gray-400">{{ orderMeta(item) }}</span>
|
||||
</span>
|
||||
<ArrowUpRight class="h-4 w-4 shrink-0 text-gray-400" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template v-if="assetResults.length">
|
||||
<p class="px-3 py-2 text-xs font-medium text-gray-400">我的代理</p>
|
||||
<button
|
||||
v-for="item in assetResults"
|
||||
:key="`asset-${resultText(item, 'id') || proxyEndpoint(item)}`"
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-left transition-colors hover:bg-gray-50 dark:hover:bg-white/[0.05]"
|
||||
@mousedown.prevent="goToAssets(item)"
|
||||
>
|
||||
<span class="min-w-0">
|
||||
<span class="block truncate text-sm font-medium text-gray-800 dark:text-white/90">{{ proxyEndpoint(item) }}</span>
|
||||
<span class="mt-0.5 block truncate text-xs text-gray-500 dark:text-gray-400">{{ assetMeta(item) }}</span>
|
||||
</span>
|
||||
<ArrowUpRight class="h-4 w-4 shrink-0 text-gray-400" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div v-if="normalizedKeyword && !loading && !hasResults" class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
没有匹配结果
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ArrowUpRight, Loader2, Search } from 'lucide-vue-next'
|
||||
import { MemberAPI } from '@/api/member'
|
||||
|
||||
type SearchEntry = {
|
||||
title: string
|
||||
description: string
|
||||
path: string
|
||||
keywords: string[]
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const rootRef = ref<HTMLElement | null>(null)
|
||||
const keyword = ref('')
|
||||
const panelOpen = ref(false)
|
||||
const loading = ref(false)
|
||||
const orderResults = ref<unknown[]>([])
|
||||
const assetResults = ref<unknown[]>([])
|
||||
let searchTimer: number | undefined
|
||||
|
||||
const entries: SearchEntry[] = [
|
||||
{ title: '控制台总览', description: '账户余额、订单和资源概览', path: '/console/dashboard', keywords: ['首页', '总览', '控制台'] },
|
||||
{ title: '购买代理', description: '选择商品、节点、时长和数量', path: '/console/buy', keywords: ['购买', '套餐', '下单'] },
|
||||
{ title: '静态长效A (高带宽)', description: '进入高带宽产品购买页', path: '/console/buy?type=staticA', keywords: ['静态长效A', '高带宽', 'staticA'] },
|
||||
{ title: '静态长效B(特惠)', description: '进入特惠产品购买页', path: '/console/buy?type=staticB', keywords: ['静态长效B', '特惠', 'staticB'] },
|
||||
{ title: '住宅长效', description: '进入住宅长效产品购买页', path: '/console/buy?type=home', keywords: ['住宅', 'home'] },
|
||||
{ title: '独享长效', description: '进入独享长效产品购买页', path: '/console/buy?type=custom', keywords: ['独享', 'custom'] },
|
||||
{ title: '我的订单', description: '查看购买、支付和开通状态', path: '/console/orders', keywords: ['订单', '支付', '开通'] },
|
||||
{ title: '我的代理', description: '查看代理地址、账号和到期时间', path: '/console/static-assets', keywords: ['代理', '节点', '资产', 'IP'] },
|
||||
{ title: '我的钱包', description: '查看余额和资金流水', path: '/console/wallet', keywords: ['钱包', '余额', '流水'] },
|
||||
{ title: '开放 API', description: '查看开放能力和密钥信息', path: '/console/open-api', keywords: ['API', '接口', '开放'] },
|
||||
{ title: '推广中心', description: '查看推广账户和佣金', path: '/console/promotion', keywords: ['推广', '佣金', '邀请'] },
|
||||
{ title: '实名认证', description: '完善实名信息,保障账户使用', path: '/console/verify', keywords: ['实名', '认证'] },
|
||||
{ title: '账户资料', description: '查看账户信息和认证状态', path: '/console/profile', keywords: ['资料', '账号', '账户'] },
|
||||
]
|
||||
|
||||
const normalizedKeyword = computed(() => keyword.value.trim())
|
||||
const quickResults = computed(() => {
|
||||
const term = normalizedKeyword.value.toLowerCase()
|
||||
const source = term
|
||||
? entries.filter((entry) =>
|
||||
[entry.title, entry.description, ...entry.keywords].some((text) => text.toLowerCase().includes(term)),
|
||||
)
|
||||
: entries.slice(0, 6)
|
||||
return source.slice(0, 6)
|
||||
})
|
||||
const hasResults = computed(() => quickResults.value.length > 0 || orderResults.value.length > 0 || assetResults.value.length > 0)
|
||||
|
||||
watch(normalizedKeyword, (value) => {
|
||||
window.clearTimeout(searchTimer)
|
||||
orderResults.value = []
|
||||
assetResults.value = []
|
||||
if (!value) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
searchTimer = window.setTimeout(() => {
|
||||
searchRemote(value)
|
||||
}, 250)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('mousedown', handleDocumentMouseDown)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.clearTimeout(searchTimer)
|
||||
document.removeEventListener('mousedown', handleDocumentMouseDown)
|
||||
})
|
||||
|
||||
function openPanel() {
|
||||
panelOpen.value = true
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
panelOpen.value = false
|
||||
}
|
||||
|
||||
function handleDocumentMouseDown(event: MouseEvent) {
|
||||
const target = event.target as Node | null
|
||||
if (target && rootRef.value?.contains(target)) return
|
||||
closePanel()
|
||||
}
|
||||
|
||||
async function searchRemote(term: string) {
|
||||
try {
|
||||
const [orders, assets] = await Promise.allSettled([
|
||||
MemberAPI.orders({ pageNum: 1, pageSize: 5, keywords: term }),
|
||||
MemberAPI.staticAssets({ pageNum: 1, pageSize: 5, keywords: term }),
|
||||
])
|
||||
if (normalizedKeyword.value !== term) return
|
||||
orderResults.value = orders.status === 'fulfilled' ? orders.value.list || [] : []
|
||||
assetResults.value = assets.status === 'fulfilled' ? assets.value.list || [] : []
|
||||
} finally {
|
||||
if (normalizedKeyword.value === term) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function submitSearch() {
|
||||
if (!normalizedKeyword.value) return
|
||||
const firstEntry = quickResults.value[0]
|
||||
if (firstEntry) {
|
||||
goToPath(firstEntry.path)
|
||||
return
|
||||
}
|
||||
const firstOrder = orderResults.value[0]
|
||||
if (firstOrder) {
|
||||
goToOrders(firstOrder)
|
||||
return
|
||||
}
|
||||
const firstAsset = assetResults.value[0]
|
||||
if (firstAsset) {
|
||||
goToAssets(firstAsset)
|
||||
return
|
||||
}
|
||||
if (looksLikeProxyKeyword(normalizedKeyword.value)) {
|
||||
goToSearch('/console/static-assets', normalizedKeyword.value)
|
||||
return
|
||||
}
|
||||
goToSearch('/console/orders', normalizedKeyword.value)
|
||||
}
|
||||
|
||||
function goToPath(path: string) {
|
||||
closePanel()
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
function goToOrders(row: unknown) {
|
||||
goToSearch('/console/orders', resultText(row, 'orderNo') || normalizedKeyword.value)
|
||||
}
|
||||
|
||||
function goToAssets(row: unknown) {
|
||||
goToSearch('/console/static-assets', resultText(row, 'proxyAddress') || normalizedKeyword.value)
|
||||
}
|
||||
|
||||
function goToSearch(path: string, value: string) {
|
||||
closePanel()
|
||||
router.push({ path, query: { keywords: value } })
|
||||
}
|
||||
|
||||
function resultText(row: unknown, key: string) {
|
||||
if (!row || typeof row !== 'object') return ''
|
||||
const value = (row as Record<string, unknown>)[key]
|
||||
return value == null ? '' : String(value)
|
||||
}
|
||||
|
||||
function orderMeta(row: unknown) {
|
||||
return [resultText(row, 'orderStatus') || resultText(row, 'status') || '处理中', resultText(row, 'createTime')]
|
||||
.filter(Boolean)
|
||||
.join(' / ')
|
||||
}
|
||||
|
||||
function proxyEndpoint(row: unknown) {
|
||||
const address = resultText(row, 'proxyAddress')
|
||||
const port = resultText(row, 'port')
|
||||
return address && port ? `${address}:${port}` : address || '代理节点'
|
||||
}
|
||||
|
||||
function assetMeta(row: unknown) {
|
||||
return [
|
||||
resultText(row, 'cityName') || resultText(row, 'countryName') || resultText(row, 'countryCode'),
|
||||
resultText(row, 'expiredAt') || resultText(row, 'expireTime'),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' / ')
|
||||
}
|
||||
|
||||
function looksLikeProxyKeyword(value: string) {
|
||||
return /^[\d.:]+$/.test(value) || value.includes('.')
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="relative" ref="dropdownRef">
|
||||
<button class="flex items-center text-gray-700 dark:text-gray-400" @click.prevent="toggleDropdown">
|
||||
<span class="mr-3 flex h-11 w-11 items-center justify-center overflow-hidden rounded-full bg-brand-50 text-sm font-semibold text-brand-500 dark:bg-brand-500/15 dark:text-brand-300">
|
||||
{{ displayInitial }}
|
||||
</span>
|
||||
<span class="mr-1 hidden font-medium text-theme-sm sm:block">{{ auth.displayName.value }}</span>
|
||||
<ChevronDownIcon :class="{ 'rotate-180': dropdownOpen }" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="dropdownOpen"
|
||||
class="absolute right-0 mt-[17px] flex w-[260px] flex-col rounded-2xl border border-gray-200 bg-white p-3 shadow-theme-lg dark:border-gray-800 dark:bg-gray-dark"
|
||||
>
|
||||
<div>
|
||||
<span class="block font-medium text-gray-700 text-theme-sm dark:text-gray-400">
|
||||
{{ auth.displayName.value }}
|
||||
</span>
|
||||
<span class="mt-0.5 block text-theme-xs text-gray-500 dark:text-gray-400">
|
||||
ID {{ auth.user?.userId || auth.user?.id || '-' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul class="flex flex-col gap-1 border-b border-gray-200 pb-3 pt-4 dark:border-gray-800">
|
||||
<li v-for="item in menuItems" :key="item.href">
|
||||
<router-link
|
||||
:to="item.href"
|
||||
class="flex items-center gap-3 rounded-lg px-3 py-2 font-medium text-gray-700 group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
@click="closeDropdown"
|
||||
>
|
||||
<component :is="item.icon" class="text-gray-500 group-hover:text-gray-700 dark:group-hover:text-gray-300" />
|
||||
{{ item.text }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
class="mt-3 flex items-center gap-3 rounded-lg px-3 py-2 font-medium text-gray-700 group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
@click="signOut"
|
||||
>
|
||||
<LogoutIcon class="text-gray-500 group-hover:text-gray-700 dark:group-hover:text-gray-300" />
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ChevronDownIcon, InfoCircleIcon, LogoutIcon, SettingsIcon, UserCircleIcon } from '@/icons'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const dropdownOpen = ref(false)
|
||||
const dropdownRef = ref<HTMLElement | null>(null)
|
||||
const displayInitial = computed(() => auth.displayName.value.slice(0, 1))
|
||||
|
||||
const menuItems = [
|
||||
{ href: '/console/profile', icon: UserCircleIcon, text: '账户资料' },
|
||||
{ href: '/console/verify', icon: SettingsIcon, text: '实名认证' },
|
||||
{ href: '/console/open-api', icon: InfoCircleIcon, text: '开放 API' },
|
||||
]
|
||||
|
||||
function toggleDropdown() {
|
||||
dropdownOpen.value = !dropdownOpen.value
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
dropdownOpen.value = false
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
await auth.logout()
|
||||
closeDropdown()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
|
||||
closeDropdown()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<div class="relative bg-white py-4 dark:bg-gray-950">
|
||||
<div class="text-center">
|
||||
<h2 class="text-title-sm font-semibold text-gray-950 dark:text-white lg:text-title-md">全国IP分布热点图</h2>
|
||||
<p v-if="currentId !== initialMapId" class="mt-3 text-sm text-sky-600 dark:text-sky-300">{{ currentName }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 grid items-center gap-8 lg:grid-cols-[290px_1fr_220px]">
|
||||
<div class="lg:pt-4">
|
||||
<h3 class="text-xl font-semibold text-gray-700 dark:text-gray-200">节点遍布全国150+城市</h3>
|
||||
<ul class="mt-6 grid gap-3 text-base leading-7 text-gray-600 dark:text-gray-300">
|
||||
<li v-for="item in summaryItems" :key="item" class="flex gap-2">
|
||||
<span class="mt-3 h-1.5 w-1.5 shrink-0 rounded-full bg-gray-500 dark:bg-gray-400"></span>
|
||||
<span>{{ item }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="relative min-h-[420px]">
|
||||
<svg class="h-full min-h-[420px] w-full drop-shadow-[0_20px_36px_rgba(14,165,233,0.16)]" :viewBox="`0 0 ${svgWidth} ${svgHeight}`" role="img" aria-label="IP 节点热力地图">
|
||||
<path
|
||||
v-for="region in regions"
|
||||
:key="region.key"
|
||||
:d="region.path"
|
||||
:fill="region.fill"
|
||||
class="cursor-pointer stroke-white stroke-[1.3] transition hover:opacity-80 dark:stroke-gray-950"
|
||||
@click="drillDown(region)"
|
||||
/>
|
||||
<text
|
||||
v-for="region in labelRegions"
|
||||
:key="`${region.key}-label`"
|
||||
:x="region.labelX"
|
||||
:y="region.labelY"
|
||||
class="pointer-events-none select-none fill-gray-700 text-[11px] font-semibold dark:fill-gray-200"
|
||||
text-anchor="middle"
|
||||
>
|
||||
{{ region.name }}
|
||||
</text>
|
||||
</svg>
|
||||
|
||||
<div v-if="loading" class="absolute inset-0 grid place-items-center bg-white/70 text-sm font-semibold text-sky-700 backdrop-blur dark:bg-gray-950/70 dark:text-sky-200">
|
||||
正在加载地图...
|
||||
</div>
|
||||
<div v-if="error" class="absolute inset-x-4 bottom-4 rounded-xl bg-error-50 px-4 py-3 text-sm text-error-600 dark:bg-error-500/15 dark:text-error-300">
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex flex-wrap items-center justify-center gap-5 text-sm text-gray-700 dark:text-gray-300">
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<i class="h-4 w-6 rounded bg-[#1f7df2]"></i>
|
||||
已开通
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<i class="h-4 w-6 rounded bg-[#d8edff]"></i>
|
||||
暂未开通
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex h-full flex-col justify-end gap-4 pb-12">
|
||||
<strong class="text-xl font-semibold text-gray-600 dark:text-gray-300">全国IP分布情况</strong>
|
||||
<span class="text-sm text-gray-400 dark:text-gray-500">点击地图区域可进入下一级</span>
|
||||
<button
|
||||
v-if="currentId !== initialMapId"
|
||||
type="button"
|
||||
class="h-10 w-fit rounded-lg border border-sky-200 bg-white px-4 text-sm font-semibold text-sky-700 transition hover:bg-sky-50 dark:border-sky-500/20 dark:bg-white/[0.06] dark:text-sky-200"
|
||||
@click="resetMap"
|
||||
>
|
||||
返回全国
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
type Position = [number, number]
|
||||
type Ring = Position[]
|
||||
type PolygonCoordinates = Ring[]
|
||||
type MultiPolygonCoordinates = PolygonCoordinates[]
|
||||
|
||||
type GeoFeature = {
|
||||
type: 'Feature'
|
||||
properties?: {
|
||||
name?: string
|
||||
adcode?: number | string
|
||||
center?: Position
|
||||
centroid?: Position
|
||||
childrenNum?: number
|
||||
}
|
||||
geometry?: {
|
||||
type: 'Polygon' | 'MultiPolygon'
|
||||
coordinates: PolygonCoordinates | MultiPolygonCoordinates
|
||||
}
|
||||
}
|
||||
|
||||
type GeoJson = {
|
||||
type: 'FeatureCollection'
|
||||
features: GeoFeature[]
|
||||
}
|
||||
|
||||
type RegionPath = {
|
||||
key: string
|
||||
name: string
|
||||
adcode: string
|
||||
path: string
|
||||
fill: string
|
||||
labelX: number
|
||||
labelY: number
|
||||
}
|
||||
|
||||
const initialMapId = '100000_full'
|
||||
const svgWidth = 760
|
||||
const svgHeight = 500
|
||||
const padding = 24
|
||||
const openedColor = '#1f7df2'
|
||||
const unopenedColor = '#d8edff'
|
||||
const openedProvinceCodes = new Set([
|
||||
'110000',
|
||||
'120000',
|
||||
'130000',
|
||||
'140000',
|
||||
'210000',
|
||||
'220000',
|
||||
'230000',
|
||||
'310000',
|
||||
'320000',
|
||||
'330000',
|
||||
'340000',
|
||||
'350000',
|
||||
'360000',
|
||||
'370000',
|
||||
'410000',
|
||||
'420000',
|
||||
'430000',
|
||||
'440000',
|
||||
'450000',
|
||||
'460000',
|
||||
'500000',
|
||||
'510000',
|
||||
'520000',
|
||||
'530000',
|
||||
])
|
||||
const summaryItems = ['日更IP量超300万+', '可用率95%以上', '支持HTTP/HTTPS']
|
||||
|
||||
const currentId = ref(initialMapId)
|
||||
const currentName = ref('全国 IP 节点热力图')
|
||||
const geoJson = ref<GeoJson | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const regions = computed<RegionPath[]>(() => {
|
||||
if (!geoJson.value?.features?.length) return []
|
||||
return buildRegionPaths(geoJson.value.features)
|
||||
})
|
||||
|
||||
const labelRegions = computed(() => {
|
||||
if (currentId.value === initialMapId) return []
|
||||
return regions.value.length <= 45 ? regions.value : regions.value.slice(0, 45)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void loadMap(initialMapId, '全国 IP 节点热力图', false)
|
||||
})
|
||||
|
||||
async function drillDown(region: RegionPath) {
|
||||
if (!region.adcode) {
|
||||
await resetMap()
|
||||
return
|
||||
}
|
||||
await loadMap(`${region.adcode}_full`, `${region.name} IP 节点热力图`, true)
|
||||
}
|
||||
|
||||
async function resetMap() {
|
||||
await loadMap(initialMapId, '全国 IP 节点热力图', false)
|
||||
}
|
||||
|
||||
async function loadMap(id: string, name: string, fallbackToRoot: boolean) {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const response = await fetch(mapUrl(id))
|
||||
if (!response.ok) throw new Error(`地图数据请求失败:${response.status}`)
|
||||
const payload = await response.json()
|
||||
const data = normalizeGeoJson(payload)
|
||||
if (!data?.features?.length) throw new Error('暂无下级地图数据')
|
||||
geoJson.value = data
|
||||
currentId.value = id
|
||||
currentName.value = name
|
||||
} catch (err) {
|
||||
if (fallbackToRoot) {
|
||||
await loadMap(initialMapId, '全国 IP 节点热力图', false)
|
||||
error.value = '当前区域暂无下级地图,已返回全国。'
|
||||
return
|
||||
}
|
||||
error.value = err instanceof Error ? err.message : '地图数据加载失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function mapUrl(id: string) {
|
||||
const normalizedId = id.endsWith('_full') ? id : `${id}_full`
|
||||
const base = window.location.hostname.endsWith('qiyunip.com') ? '' : 'https://www.qiyunip.com'
|
||||
return `${base}/api/map/getjson?id=${encodeURIComponent(normalizedId)}`
|
||||
}
|
||||
|
||||
function normalizeGeoJson(payload: unknown): GeoJson | null {
|
||||
const value = payload as { type?: string; features?: GeoFeature[]; data?: unknown }
|
||||
if (value?.type === 'FeatureCollection' && Array.isArray(value.features)) return value as GeoJson
|
||||
const data = value?.data as { type?: string; features?: GeoFeature[] } | undefined
|
||||
if (data?.type === 'FeatureCollection' && Array.isArray(data.features)) return data as GeoJson
|
||||
return null
|
||||
}
|
||||
|
||||
function buildRegionPaths(features: GeoFeature[]) {
|
||||
const bbox = getFeatureBounds(features)
|
||||
const scale = Math.min(
|
||||
(svgWidth - padding * 2) / Math.max(bbox.maxX - bbox.minX, 1),
|
||||
(svgHeight - padding * 2) / Math.max(bbox.maxY - bbox.minY, 1),
|
||||
)
|
||||
const offsetX = (svgWidth - (bbox.maxX - bbox.minX) * scale) / 2
|
||||
const offsetY = (svgHeight - (bbox.maxY - bbox.minY) * scale) / 2
|
||||
const project = ([lng, lat]: Position) => ({
|
||||
x: offsetX + (lng - bbox.minX) * scale,
|
||||
y: offsetY + (bbox.maxY - lat) * scale,
|
||||
})
|
||||
|
||||
return features
|
||||
.map((feature, index) => {
|
||||
const name = feature.properties?.name || '未知区域'
|
||||
const adcode = feature.properties?.adcode ? String(feature.properties.adcode) : ''
|
||||
const center = feature.properties?.centroid || feature.properties?.center || getFeatureCenter(feature)
|
||||
const label = project(center)
|
||||
return {
|
||||
key: `${adcode || name}-${index}`,
|
||||
name,
|
||||
adcode,
|
||||
path: geometryToPath(feature, project),
|
||||
fill: isOpenedRegion(adcode, index) ? openedColor : unopenedColor,
|
||||
labelX: label.x,
|
||||
labelY: label.y,
|
||||
}
|
||||
})
|
||||
.filter((item) => item.path)
|
||||
}
|
||||
|
||||
function geometryToPath(feature: GeoFeature, project: (position: Position) => { x: number; y: number }) {
|
||||
const geometry = feature.geometry
|
||||
if (!geometry) return ''
|
||||
if (geometry.type === 'Polygon') return polygonToPath(geometry.coordinates as PolygonCoordinates, project)
|
||||
if (geometry.type === 'MultiPolygon') {
|
||||
return (geometry.coordinates as MultiPolygonCoordinates).map((polygon) => polygonToPath(polygon, project)).join(' ')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function polygonToPath(polygon: PolygonCoordinates, project: (position: Position) => { x: number; y: number }) {
|
||||
return polygon
|
||||
.map((ring) =>
|
||||
ring
|
||||
.map((position, index) => {
|
||||
const point = project(position)
|
||||
return `${index === 0 ? 'M' : 'L'}${point.x.toFixed(2)},${point.y.toFixed(2)}`
|
||||
})
|
||||
.join(' ') + ' Z',
|
||||
)
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
function getFeatureBounds(features: GeoFeature[]) {
|
||||
const positions = features.flatMap((feature) => getFeaturePositions(feature))
|
||||
return positions.reduce(
|
||||
(bbox, [lng, lat]) => ({
|
||||
minX: Math.min(bbox.minX, lng),
|
||||
minY: Math.min(bbox.minY, lat),
|
||||
maxX: Math.max(bbox.maxX, lng),
|
||||
maxY: Math.max(bbox.maxY, lat),
|
||||
}),
|
||||
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
||||
)
|
||||
}
|
||||
|
||||
function getFeaturePositions(feature: GeoFeature) {
|
||||
const geometry = feature.geometry
|
||||
if (!geometry) return []
|
||||
if (geometry.type === 'Polygon') return (geometry.coordinates as PolygonCoordinates).flat()
|
||||
return (geometry.coordinates as MultiPolygonCoordinates).flat(2)
|
||||
}
|
||||
|
||||
function getFeatureCenter(feature: GeoFeature): Position {
|
||||
const positions = getFeaturePositions(feature)
|
||||
if (!positions.length) return [105, 35]
|
||||
const sum = positions.reduce((acc, [lng, lat]) => ({ lng: acc.lng + lng, lat: acc.lat + lat }), { lng: 0, lat: 0 })
|
||||
return [sum.lng / positions.length, sum.lat / positions.length]
|
||||
}
|
||||
|
||||
function isOpenedRegion(adcode: string, index: number) {
|
||||
if (!adcode) return false
|
||||
if (currentId.value === initialMapId) return openedProvinceCodes.has(adcode)
|
||||
return index % 3 !== 0
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center rounded-2xl border border-dashed border-gray-200 bg-gray-50 px-6 py-10 text-center dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div class="mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-brand-50 text-brand-500 dark:bg-brand-500/15 dark:text-brand-400">
|
||||
<BoxIcon />
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-gray-800 dark:text-white/90">{{ title }}</h3>
|
||||
<p class="mt-2 max-w-md text-sm leading-6 text-gray-500 dark:text-gray-400">{{ description }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BoxIcon } from '@/icons'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
description: string
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div ref="rootRef" class="relative">
|
||||
<button
|
||||
type="button"
|
||||
class="app-input flex items-center justify-between gap-3 text-left"
|
||||
:class="[
|
||||
{ 'border-brand-300 ring-3 ring-brand-500/10 dark:border-brand-800': open },
|
||||
disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer',
|
||||
]"
|
||||
:disabled="disabled"
|
||||
@click="toggle"
|
||||
@keydown.down.prevent="openDropdown"
|
||||
@keydown.enter.prevent="toggle"
|
||||
@keydown.esc.prevent="close"
|
||||
>
|
||||
<span class="min-w-0 truncate" :class="selectedLabel ? 'text-gray-800 dark:text-white/90' : 'text-gray-400 dark:text-white/30'">
|
||||
{{ selectedLabel || placeholder }}
|
||||
</span>
|
||||
<ChevronDownIcon class="h-5 w-5 shrink-0 text-gray-400 transition-transform" :class="{ 'rotate-180': open }" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="open"
|
||||
class="absolute left-0 right-0 top-full z-999 mt-2 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-theme-lg dark:border-gray-800 dark:bg-gray-900"
|
||||
>
|
||||
<div v-if="searchable" class="border-b border-gray-100 p-2 dark:border-gray-800">
|
||||
<input
|
||||
ref="searchRef"
|
||||
v-model.trim="keyword"
|
||||
class="h-10 w-full rounded-lg border border-gray-200 bg-gray-50 px-3 text-sm text-gray-800 outline-none transition placeholder:text-gray-400 focus:border-brand-300 focus:bg-white focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-950 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800 dark:focus:bg-gray-900"
|
||||
:placeholder="searchPlaceholder"
|
||||
@keydown.down.prevent="focusNext"
|
||||
@keydown.up.prevent="focusPrevious"
|
||||
@keydown.enter.prevent="selectFocused"
|
||||
@keydown.esc.prevent="close"
|
||||
/>
|
||||
</div>
|
||||
<div class="custom-scrollbar max-h-72 overflow-y-auto p-1">
|
||||
<button
|
||||
v-for="(item, index) in filteredOptions"
|
||||
:key="item.value || `empty-${index}`"
|
||||
type="button"
|
||||
class="flex min-h-10 w-full items-center justify-between gap-3 rounded-lg px-3 py-2 text-left text-sm transition-colors"
|
||||
:class="[
|
||||
item.value === modelValue
|
||||
? 'bg-brand-50 text-brand-600 dark:bg-brand-500/15 dark:text-brand-300'
|
||||
: 'text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-white/[0.05]',
|
||||
index === focusedIndex ? 'bg-gray-50 dark:bg-white/[0.05]' : '',
|
||||
]"
|
||||
@mouseenter="focusedIndex = index"
|
||||
@click="selectOption(item)"
|
||||
>
|
||||
<span class="min-w-0 truncate">{{ item.label }}</span>
|
||||
<CheckIcon v-if="item.value === modelValue" class="h-3.5 w-3.5 shrink-0 rounded-full bg-brand-500 p-0.5 text-white" />
|
||||
</button>
|
||||
<div v-if="!filteredOptions.length" class="px-3 py-6 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
暂无匹配选项
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { CheckIcon, ChevronDownIcon } from '@/icons'
|
||||
|
||||
type SelectOption = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
options: SelectOption[]
|
||||
placeholder?: string
|
||||
searchPlaceholder?: string
|
||||
searchable?: boolean
|
||||
disabled?: boolean
|
||||
}>(),
|
||||
{
|
||||
placeholder: '请选择',
|
||||
searchPlaceholder: '搜索选项',
|
||||
searchable: true,
|
||||
disabled: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
change: []
|
||||
}>()
|
||||
|
||||
const open = ref(false)
|
||||
const keyword = ref('')
|
||||
const focusedIndex = ref(0)
|
||||
const rootRef = ref<HTMLElement | null>(null)
|
||||
const searchRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const selectedLabel = computed(() => props.options.find((item) => item.value === props.modelValue)?.label || '')
|
||||
const filteredOptions = computed(() => {
|
||||
const value = keyword.value.trim().toLowerCase()
|
||||
if (!value) return props.options
|
||||
return props.options.filter((item) => item.label.toLowerCase().includes(value) || item.value.toLowerCase().includes(value))
|
||||
})
|
||||
|
||||
watch(open, async (value) => {
|
||||
if (!value) return
|
||||
keyword.value = ''
|
||||
focusedIndex.value = Math.max(
|
||||
props.options.findIndex((item) => item.value === props.modelValue),
|
||||
0,
|
||||
)
|
||||
await nextTick()
|
||||
if (props.searchable) searchRef.value?.focus()
|
||||
})
|
||||
|
||||
watch(filteredOptions, () => {
|
||||
if (focusedIndex.value >= filteredOptions.value.length) {
|
||||
focusedIndex.value = Math.max(filteredOptions.value.length - 1, 0)
|
||||
}
|
||||
})
|
||||
|
||||
function toggle() {
|
||||
if (props.disabled) return
|
||||
open.value ? close() : openDropdown()
|
||||
}
|
||||
|
||||
function openDropdown() {
|
||||
if (props.disabled) return
|
||||
open.value = true
|
||||
}
|
||||
|
||||
function close() {
|
||||
open.value = false
|
||||
}
|
||||
|
||||
function focusNext() {
|
||||
if (!filteredOptions.value.length) return
|
||||
focusedIndex.value = (focusedIndex.value + 1) % filteredOptions.value.length
|
||||
}
|
||||
|
||||
function focusPrevious() {
|
||||
if (!filteredOptions.value.length) return
|
||||
focusedIndex.value = (focusedIndex.value - 1 + filteredOptions.value.length) % filteredOptions.value.length
|
||||
}
|
||||
|
||||
function selectFocused() {
|
||||
const item = filteredOptions.value[focusedIndex.value]
|
||||
if (item) selectOption(item)
|
||||
}
|
||||
|
||||
function selectOption(item: SelectOption) {
|
||||
emit('update:modelValue', item.value)
|
||||
emit('change')
|
||||
close()
|
||||
}
|
||||
|
||||
function handleDocumentMouseDown(event: MouseEvent) {
|
||||
if (!rootRef.value?.contains(event.target as Node)) close()
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleDocumentMouseDown)
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('mousedown', handleDocumentMouseDown)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
|
||||
<div class="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<h4 class="text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-6">Address</h4>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32">
|
||||
<div>
|
||||
<p class="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">Country</p>
|
||||
<p class="text-sm font-medium text-gray-800 dark:text-white/90">United States</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">City/State</p>
|
||||
<p class="text-sm font-medium text-gray-800 dark:text-white/90">
|
||||
Phoenix, United States
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
|
||||
Postal Code
|
||||
</p>
|
||||
<p class="text-sm font-medium text-gray-800 dark:text-white/90">ERT 2489</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">TAX ID</p>
|
||||
<p class="text-sm font-medium text-gray-800 dark:text-white/90">AS4568384</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="isProfileAddressModal = true"
|
||||
class="flex w-full items-center justify-center gap-2 rounded-full border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 lg:inline-flex lg:w-auto"
|
||||
>
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Modal v-if="isProfileAddressModal" @close="isProfileAddressModal = false">
|
||||
<template #body>
|
||||
<div
|
||||
class="no-scrollbar relative w-full max-w-[700px] overflow-y-auto rounded-3xl bg-white p-4 dark:bg-gray-900 lg:p-11"
|
||||
>
|
||||
<!-- close btn -->
|
||||
<button
|
||||
@click="isProfileAddressModal = false"
|
||||
class="transition-color absolute right-5 top-5 z-999 flex h-11 w-11 items-center justify-center rounded-full bg-gray-100 text-gray-400 hover:bg-gray-200 hover:text-gray-600 dark:bg-gray-700 dark:bg-white/[0.05] dark:text-gray-400 dark:hover:bg-white/[0.07] dark:hover:text-gray-300"
|
||||
>
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.04289 16.5418C5.65237 16.9323 5.65237 17.5655 6.04289 17.956C6.43342 18.3465 7.06658 18.3465 7.45711 17.956L11.9987 13.4144L16.5408 17.9565C16.9313 18.347 17.5645 18.347 17.955 17.9565C18.3455 17.566 18.3455 16.9328 17.955 16.5423L13.4129 12.0002L17.955 7.45808C18.3455 7.06756 18.3455 6.43439 17.955 6.04387C17.5645 5.65335 16.9313 5.65335 16.5408 6.04387L11.9987 10.586L7.45711 6.04439C7.06658 5.65386 6.43342 5.65386 6.04289 6.04439C5.65237 6.43491 5.65237 7.06808 6.04289 7.4586L10.5845 12.0002L6.04289 16.5418Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="px-2 pr-14">
|
||||
<h4 class="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
|
||||
Edit Address
|
||||
</h4>
|
||||
<p class="mb-6 text-sm text-gray-500 dark:text-gray-400 lg:mb-7">
|
||||
Update your details to keep your profile up-to-date.
|
||||
</p>
|
||||
</div>
|
||||
<form class="flex flex-col">
|
||||
<div class="px-2 overflow-y-auto custom-scrollbar">
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Country
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="United States"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
City/State
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="Poenix, Arizona, United States"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Postal Code
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="ERT 2489"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
TAX ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="AS4568384"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 mt-6 lg:justify-end">
|
||||
<button
|
||||
@click="isProfileAddressModal = false"
|
||||
type="button"
|
||||
class="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
@click="saveProfile"
|
||||
type="button"
|
||||
class="flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import Modal from './Modal.vue'
|
||||
|
||||
const isProfileAddressModal = ref(false)
|
||||
|
||||
const saveProfile = () => {
|
||||
// Implement save profile logic here
|
||||
console.log('Profile saved')
|
||||
isProfileInfoModal.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div class="fixed inset-0 flex items-center justify-center overflow-y-auto modal z-99999">
|
||||
<div
|
||||
class="fixed inset-0 h-full w-full bg-gray-400/50 backdrop-blur-[32px]"
|
||||
aria-hidden="true"
|
||||
@click="$emit('close')"
|
||||
></div>
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// No additional setup needed
|
||||
</script>
|
||||
@@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="p-5 mb-6 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
|
||||
<div class="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<h4 class="text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-6">
|
||||
Personal Information
|
||||
</h4>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32">
|
||||
<div>
|
||||
<p class="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">First Name</p>
|
||||
<p class="text-sm font-medium text-gray-800 dark:text-white/90">Musharof</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">Last Name</p>
|
||||
<p class="text-sm font-medium text-gray-800 dark:text-white/90">Chowdhury</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
|
||||
Email address
|
||||
</p>
|
||||
<p class="text-sm font-medium text-gray-800 dark:text-white/90">
|
||||
randomuser@pimjo.com
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">Phone</p>
|
||||
<p class="text-sm font-medium text-gray-800 dark:text-white/90">+09 363 398 46</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">Bio</p>
|
||||
<p class="text-sm font-medium text-gray-800 dark:text-white/90">Team Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="edit-button" @click="isProfileInfoModal = true">
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Modal v-if="isProfileInfoModal" @close="isProfileInfoModal = false">
|
||||
<template #body>
|
||||
<div
|
||||
class="no-scrollbar relative w-full max-w-[700px] overflow-y-auto rounded-3xl bg-white p-4 dark:bg-gray-900 lg:p-11"
|
||||
>
|
||||
<!-- close btn -->
|
||||
<button
|
||||
@click="isProfileInfoModal = false"
|
||||
class="transition-color absolute right-5 top-5 z-999 flex h-11 w-11 items-center justify-center rounded-full bg-gray-100 text-gray-400 hover:bg-gray-200 hover:text-gray-600 dark:bg-gray-700 dark:bg-white/[0.05] dark:text-gray-400 dark:hover:bg-white/[0.07] dark:hover:text-gray-300"
|
||||
>
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.04289 16.5418C5.65237 16.9323 5.65237 17.5655 6.04289 17.956C6.43342 18.3465 7.06658 18.3465 7.45711 17.956L11.9987 13.4144L16.5408 17.9565C16.9313 18.347 17.5645 18.347 17.955 17.9565C18.3455 17.566 18.3455 16.9328 17.955 16.5423L13.4129 12.0002L17.955 7.45808C18.3455 7.06756 18.3455 6.43439 17.955 6.04387C17.5645 5.65335 16.9313 5.65335 16.5408 6.04387L11.9987 10.586L7.45711 6.04439C7.06658 5.65386 6.43342 5.65386 6.04289 6.04439C5.65237 6.43491 5.65237 7.06808 6.04289 7.4586L10.5845 12.0002L6.04289 16.5418Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="px-2 pr-14">
|
||||
<h4 class="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
|
||||
Edit Personal Information
|
||||
</h4>
|
||||
<p class="mb-6 text-sm text-gray-500 dark:text-gray-400 lg:mb-7">
|
||||
Update your details to keep your profile up-to-date.
|
||||
</p>
|
||||
</div>
|
||||
<form class="flex flex-col">
|
||||
<div class="custom-scrollbar h-[458px] overflow-y-auto p-2">
|
||||
<div>
|
||||
<h5 class="mb-5 text-lg font-medium text-gray-800 dark:text-white/90 lg:mb-6">
|
||||
Social Links
|
||||
</h5>
|
||||
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
Facebook
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="https://www.facebook.com/PimjoHQ"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
X.com
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="https://x.com/PimjoHQ"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
Linkedin
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="https://www.linkedin.com/company/pimjo/posts/?feedView=all"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
Instagram
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="https://instagram.com/PimjoHQ"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-7">
|
||||
<h5 class="mb-5 text-lg font-medium text-gray-800 dark:text-white/90 lg:mb-6">
|
||||
Personal Information
|
||||
</h5>
|
||||
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
First Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="Musharof"
|
||||
class="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
Last Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="Chowdhury"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="emirhanboruch55@gmail.com"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
Phone
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="+09 363 398 46"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
Bio
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="Team Manager"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 px-2 mt-6 lg:justify-end">
|
||||
<button
|
||||
@click="isProfileInfoModal = false"
|
||||
type="button"
|
||||
class="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
@click="saveProfile"
|
||||
type="button"
|
||||
class="flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import Modal from './Modal.vue'
|
||||
|
||||
const isProfileInfoModal = ref(false)
|
||||
|
||||
const saveProfile = () => {
|
||||
// Implement save profile logic here
|
||||
console.log('Profile saved')
|
||||
isProfileInfoModal.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="p-5 mb-6 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
|
||||
<div class="flex flex-col gap-5 xl:flex-row xl:items-center xl:justify-between">
|
||||
<div class="flex flex-col items-center w-full gap-6 xl:flex-row">
|
||||
<div
|
||||
class="w-20 h-20 overflow-hidden border border-gray-200 rounded-full dark:border-gray-800"
|
||||
>
|
||||
<img src="/images/user/owner.jpg" alt="user" />
|
||||
</div>
|
||||
<div class="order-3 xl:order-2">
|
||||
<h4
|
||||
class="mb-2 text-lg font-semibold text-center text-gray-800 dark:text-white/90 xl:text-left"
|
||||
>
|
||||
Musharof Chowdhury
|
||||
</h4>
|
||||
<div
|
||||
class="flex flex-col items-center gap-1 text-center xl:flex-row xl:gap-3 xl:text-left"
|
||||
>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Team Manager</p>
|
||||
<div class="hidden h-3.5 w-px bg-gray-300 dark:bg-gray-700 xl:block"></div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Arizona, United States</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center order-2 gap-2 grow xl:order-3 xl:justify-end">
|
||||
<a
|
||||
href="https://www.facebook.com/PimjoHQ"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="social-button"
|
||||
>
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.6666 11.2503H13.7499L14.5833 7.91699H11.6666V6.25033C11.6666 5.39251 11.6666 4.58366 13.3333 4.58366H14.5833V1.78374C14.3118 1.7477 13.2858 1.66699 12.2023 1.66699C9.94025 1.66699 8.33325 3.04771 8.33325 5.58342V7.91699H5.83325V11.2503H8.33325V18.3337H11.6666V11.2503Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://x.com/PimjoHQ" target="_blank" rel="noopener" class="social-button">
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15.1708 1.875H17.9274L11.9049 8.75833L18.9899 18.125H13.4424L9.09742 12.4442L4.12578 18.125H1.36745L7.80912 10.7625L1.01245 1.875H6.70078L10.6283 7.0675L15.1708 1.875ZM14.2033 16.475H15.7308L5.87078 3.43833H4.23162L14.2033 16.475Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.linkedin.com/company/pimjo/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="social-button"
|
||||
>
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5.78381 4.16645C5.78351 4.84504 5.37181 5.45569 4.74286 5.71045C4.11391 5.96521 3.39331 5.81321 2.92083 5.32613C2.44836 4.83904 2.31837 4.11413 2.59216 3.49323C2.86596 2.87233 3.48886 2.47942 4.16715 2.49978C5.06804 2.52682 5.78422 3.26515 5.78381 4.16645ZM5.83381 7.06645H2.50048V17.4998H5.83381V7.06645ZM11.1005 7.06645H7.78381V17.4998H11.0672V12.0248C11.0672 8.97475 15.0422 8.69142 15.0422 12.0248V17.4998H18.3338V10.8914C18.3338 5.74978 12.4505 5.94145 11.0672 8.46642L11.1005 7.06645Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.instagram.com/PimjoHQ"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="social-button"
|
||||
>
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.8567 1.66699C11.7946 1.66854 12.2698 1.67351 12.6805 1.68573L12.8422 1.69102C13.0291 1.69766 13.2134 1.70599 13.4357 1.71641C14.3224 1.75738 14.9273 1.89766 15.4586 2.10391C16.0078 2.31572 16.4717 2.60183 16.9349 3.06503C17.3974 3.52822 17.6836 3.99349 17.8961 4.54141C18.1016 5.07197 18.2419 5.67753 18.2836 6.56433C18.2935 6.78655 18.3015 6.97088 18.3081 7.15775L18.3133 7.31949C18.3255 7.73011 18.3311 8.20543 18.3328 9.1433L18.3335 9.76463C18.3336 9.84055 18.3336 9.91888 18.3336 9.99972L18.3335 10.2348L18.333 10.8562C18.3314 11.794 18.3265 12.2694 18.3142 12.68L18.3089 12.8417C18.3023 13.0286 18.294 13.213 18.2836 13.4351C18.2426 14.322 18.1016 14.9268 17.8961 15.458C17.6842 16.0074 17.3974 16.4713 16.9349 16.9345C16.4717 17.397 16.0057 17.6831 15.4586 17.8955C14.9273 18.1011 14.3224 18.2414 13.4357 18.2831C13.2134 18.293 13.0291 18.3011 12.8422 18.3076L12.6805 18.3128C12.2698 18.3251 11.7946 18.3306 10.8567 18.3324L10.2353 18.333C10.1594 18.333 10.0811 18.333 10.0002 18.333H9.76516L9.14375 18.3325C8.20591 18.331 7.7306 18.326 7.31997 18.3137L7.15824 18.3085C6.97136 18.3018 6.78703 18.2935 6.56481 18.2831C5.67801 18.2421 5.07384 18.1011 4.5419 17.8955C3.99328 17.6838 3.5287 17.397 3.06551 16.9345C2.60231 16.4713 2.3169 16.0053 2.1044 15.458C1.89815 14.9268 1.75856 14.322 1.7169 13.4351C1.707 13.213 1.69892 13.0286 1.69238 12.8417L1.68714 12.68C1.67495 12.2694 1.66939 11.794 1.66759 10.8562L1.66748 9.1433C1.66903 8.20543 1.67399 7.73011 1.68621 7.31949L1.69151 7.15775C1.69815 6.97088 1.70648 6.78655 1.7169 6.56433C1.75786 5.67683 1.89815 5.07266 2.1044 4.54141C2.3162 3.9928 2.60231 3.52822 3.06551 3.06503C3.5287 2.60183 3.99398 2.31641 4.5419 2.10391C5.07315 1.89766 5.67731 1.75808 6.56481 1.71641C6.78703 1.70652 6.97136 1.69844 7.15824 1.6919L7.31997 1.68666C7.7306 1.67446 8.20591 1.6689 9.14375 1.6671L10.8567 1.66699ZM10.0002 5.83308C7.69781 5.83308 5.83356 7.69935 5.83356 9.99972C5.83356 12.3021 7.69984 14.1664 10.0002 14.1664C12.3027 14.1664 14.1669 12.3001 14.1669 9.99972C14.1669 7.69732 12.3006 5.83308 10.0002 5.83308ZM10.0002 7.49974C11.381 7.49974 12.5002 8.61863 12.5002 9.99972C12.5002 11.3805 11.3813 12.4997 10.0002 12.4997C8.6195 12.4997 7.50023 11.3809 7.50023 9.99972C7.50023 8.61897 8.61908 7.49974 10.0002 7.49974ZM14.3752 4.58308C13.8008 4.58308 13.3336 5.04967 13.3336 5.62403C13.3336 6.19841 13.8002 6.66572 14.3752 6.66572C14.9496 6.66572 15.4169 6.19913 15.4169 5.62403C15.4169 5.04967 14.9488 4.58236 14.3752 4.58308Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="isProfileInfoModal = true" class="edit-button">
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Modal v-if="isProfileInfoModal" @close="isProfileInfoModal = false">
|
||||
<template #body>
|
||||
<div
|
||||
class="no-scrollbar relative w-full max-w-[700px] overflow-y-auto rounded-3xl bg-white p-4 dark:bg-gray-900 lg:p-11"
|
||||
>
|
||||
<!-- close btn -->
|
||||
<button
|
||||
@click="isProfileInfoModal = false"
|
||||
class="transition-color absolute right-5 top-5 z-999 flex h-11 w-11 items-center justify-center rounded-full bg-gray-100 text-gray-400 hover:bg-gray-200 hover:text-gray-600 dark:bg-gray-700 dark:bg-white/[0.05] dark:text-gray-400 dark:hover:bg-white/[0.07] dark:hover:text-gray-300"
|
||||
>
|
||||
<svg
|
||||
class="fill-current"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.04289 16.5418C5.65237 16.9323 5.65237 17.5655 6.04289 17.956C6.43342 18.3465 7.06658 18.3465 7.45711 17.956L11.9987 13.4144L16.5408 17.9565C16.9313 18.347 17.5645 18.347 17.955 17.9565C18.3455 17.566 18.3455 16.9328 17.955 16.5423L13.4129 12.0002L17.955 7.45808C18.3455 7.06756 18.3455 6.43439 17.955 6.04387C17.5645 5.65335 16.9313 5.65335 16.5408 6.04387L11.9987 10.586L7.45711 6.04439C7.06658 5.65386 6.43342 5.65386 6.04289 6.04439C5.65237 6.43491 5.65237 7.06808 6.04289 7.4586L10.5845 12.0002L6.04289 16.5418Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="px-2 pr-14">
|
||||
<h4 class="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
|
||||
Edit Personal Information
|
||||
</h4>
|
||||
<p class="mb-6 text-sm text-gray-500 dark:text-gray-400 lg:mb-7">
|
||||
Update your details to keep your profile up-to-date.
|
||||
</p>
|
||||
</div>
|
||||
<form class="flex flex-col">
|
||||
<div class="custom-scrollbar h-[458px] overflow-y-auto p-2">
|
||||
<div>
|
||||
<h5 class="mb-5 text-lg font-medium text-gray-800 dark:text-white/90 lg:mb-6">
|
||||
Social Links
|
||||
</h5>
|
||||
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
Facebook
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="https://www.facebook.com/PimjoHQ"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
X.com
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="https://x.com/PimjoHQ"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
Linkedin
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="https://www.linkedin.com/company/pimjo/posts/?feedView=all"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
Instagram
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="https://instagram.com/PimjoHQ"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-7">
|
||||
<h5 class="mb-5 text-lg font-medium text-gray-800 dark:text-white/90 lg:mb-6">
|
||||
Personal Information
|
||||
</h5>
|
||||
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
First Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="Musharof"
|
||||
class="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
Last Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="Chowdhury"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="randomuser@pimjo.com"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
Phone
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="+09 363 398 46"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
|
||||
>
|
||||
Bio
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="Team Manager"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 px-2 mt-6 lg:justify-end">
|
||||
<button
|
||||
@click="isProfileInfoModal = false"
|
||||
type="button"
|
||||
class="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
@click="saveProfile"
|
||||
type="button"
|
||||
class="flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import Modal from './Modal.vue'
|
||||
|
||||
const isProfileInfoModal = ref(false)
|
||||
|
||||
const saveProfile = () => {
|
||||
// Implement save profile logic here
|
||||
console.log('Profile saved')
|
||||
isProfileInfoModal.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]"
|
||||
>
|
||||
<div class="max-w-full overflow-x-auto custom-scrollbar">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
<th class="px-5 py-3 text-left w-3/11 sm:px-6">
|
||||
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">User</p>
|
||||
</th>
|
||||
<th class="px-5 py-3 text-left w-2/11 sm:px-6">
|
||||
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">Project Name</p>
|
||||
</th>
|
||||
<th class="px-5 py-3 text-left w-2/11 sm:px-6">
|
||||
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">Team</p>
|
||||
</th>
|
||||
<th class="px-5 py-3 text-left w-2/11 sm:px-6">
|
||||
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">Status</p>
|
||||
</th>
|
||||
<th class="px-5 py-3 text-left w-2/11 sm:px-6">
|
||||
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">Budget</p>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr
|
||||
v-for="(user, index) in users"
|
||||
:key="index"
|
||||
class="border-t border-gray-100 dark:border-gray-800"
|
||||
>
|
||||
<td class="px-5 py-4 sm:px-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 overflow-hidden rounded-full">
|
||||
<img :src="user.avatar" :alt="user.name" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="block font-medium text-gray-800 text-theme-sm dark:text-white/90">
|
||||
{{ user.name }}
|
||||
</span>
|
||||
<span class="block text-gray-500 text-theme-xs dark:text-gray-400">
|
||||
{{ user.role }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 sm:px-6">
|
||||
<p class="text-gray-500 text-theme-sm dark:text-gray-400">{{ user.project }}</p>
|
||||
</td>
|
||||
<td class="px-5 py-4 sm:px-6">
|
||||
<div class="flex -space-x-2">
|
||||
<div
|
||||
v-for="(member, memberIndex) in user.team"
|
||||
:key="memberIndex"
|
||||
class="w-6 h-6 overflow-hidden border-2 border-white rounded-full dark:border-gray-900"
|
||||
>
|
||||
<img :src="member" alt="team member" />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 sm:px-6">
|
||||
<span
|
||||
:class="[
|
||||
'rounded-full px-2 py-0.5 text-theme-xs font-medium',
|
||||
{
|
||||
'bg-success-50 text-success-700 dark:bg-success-500/15 dark:text-success-500':
|
||||
user.status === 'Active',
|
||||
'bg-warning-50 text-warning-700 dark:bg-warning-500/15 dark:text-warning-400':
|
||||
user.status === 'Pending',
|
||||
'bg-error-50 text-error-700 dark:bg-error-500/15 dark:text-error-500':
|
||||
user.status === 'Cancel',
|
||||
},
|
||||
]"
|
||||
>
|
||||
{{ user.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 sm:px-6">
|
||||
<p class="text-gray-500 text-theme-sm dark:text-gray-400">{{ user.budget }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const users = ref([
|
||||
{
|
||||
name: 'Lindsey Curtis',
|
||||
role: 'Web Designer',
|
||||
avatar: '/images/user/user-17.jpg',
|
||||
project: 'Agency Website',
|
||||
team: ['/images/user/user-22.jpg', '/images/user/user-23.jpg', '/images/user/user-24.jpg'],
|
||||
status: 'Active',
|
||||
budget: '3.9K',
|
||||
},
|
||||
{
|
||||
name: 'Kaiya George',
|
||||
role: 'Project Manager',
|
||||
avatar: '/images/user/user-18.jpg',
|
||||
project: 'Technology',
|
||||
team: ['/images/user/user-25.jpg', '/images/user/user-26.jpg'],
|
||||
status: 'Pending',
|
||||
budget: '24.9K',
|
||||
},
|
||||
{
|
||||
name: 'Zain Geidt',
|
||||
role: 'Content Writer',
|
||||
avatar: '/images/user/user-19.jpg',
|
||||
project: 'Blog Writing',
|
||||
team: ['/images/user/user-27.jpg'],
|
||||
status: 'Active',
|
||||
budget: '12.7K',
|
||||
},
|
||||
{
|
||||
name: 'Abram Schleifer',
|
||||
role: 'Digital Marketer',
|
||||
avatar: '/images/user/user-20.jpg',
|
||||
project: 'Social Media',
|
||||
team: ['/images/user/user-28.jpg', '/images/user/user-29.jpg', '/images/user/user-30.jpg'],
|
||||
status: 'Cancel',
|
||||
budget: '2.8K',
|
||||
},
|
||||
{
|
||||
name: 'Carla George',
|
||||
role: 'Front-end Developer',
|
||||
avatar: '/images/user/user-21.jpg',
|
||||
project: 'Website',
|
||||
team: ['/images/user/user-31.jpg', '/images/user/user-32.jpg', '/images/user/user-33.jpg'],
|
||||
status: 'Active',
|
||||
budget: '4.5K',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add any additional styles here if needed */
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div :class="['rounded-xl border p-4', variantClasses[variant].container]">
|
||||
<div class="flex items-start gap-3">
|
||||
<div :class="['-mt-0.5', variantClasses[variant].icon]">
|
||||
<component :is="icons[variant]" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="mb-1 text-sm font-semibold text-gray-800 dark:text-white/90">
|
||||
{{ title }}
|
||||
</h4>
|
||||
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ message }}</p>
|
||||
|
||||
<router-link
|
||||
v-if="showLink"
|
||||
:to="linkHref"
|
||||
class="inline-block mt-3 text-sm font-medium text-gray-500 underline dark:text-gray-400"
|
||||
>
|
||||
{{ linkText }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SuccessIcon, ErrorIcon, WarningIcon, InfoCircleIcon } from '@/icons'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface AlertProps {
|
||||
variant: 'success' | 'error' | 'warning' | 'info'
|
||||
title: string
|
||||
message: string
|
||||
showLink?: boolean
|
||||
linkHref?: string
|
||||
linkText?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<AlertProps>(), {
|
||||
showLink: false,
|
||||
linkHref: '#',
|
||||
linkText: 'Learn more',
|
||||
})
|
||||
|
||||
const variantClasses = {
|
||||
success: {
|
||||
container: 'border-success-500 bg-success-50 dark:border-success-500/30 dark:bg-success-500/15',
|
||||
icon: 'text-success-500',
|
||||
},
|
||||
error: {
|
||||
container: 'border-error-500 bg-error-50 dark:border-error-500/30 dark:bg-error-500/15',
|
||||
icon: 'text-error-500',
|
||||
},
|
||||
warning: {
|
||||
container: 'border-warning-500 bg-warning-50 dark:border-warning-500/30 dark:bg-warning-500/15',
|
||||
icon: 'text-warning-500',
|
||||
},
|
||||
info: {
|
||||
container:
|
||||
'border-blue-light-500 bg-blue-light-50 dark:border-blue-light-500/30 dark:bg-blue-light-500/15',
|
||||
icon: 'text-blue-light-500',
|
||||
},
|
||||
}
|
||||
|
||||
const icons = {
|
||||
success: SuccessIcon,
|
||||
error: ErrorIcon,
|
||||
warning: WarningIcon,
|
||||
info: InfoCircleIcon,
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div :class="['relative rounded-full', sizeClasses[size]]">
|
||||
<img :src="src" :alt="alt" class="object-cover rounded-full" />
|
||||
<span
|
||||
v-if="status !== 'none'"
|
||||
:class="[
|
||||
'absolute bottom-0 right-0 rounded-full border-[1.5px] border-white dark:border-gray-900',
|
||||
statusSizeClasses[size],
|
||||
statusColorClasses[status] || '',
|
||||
]"
|
||||
></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface AvatarProps {
|
||||
src: string
|
||||
alt?: string
|
||||
size?: 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge'
|
||||
status?: 'online' | 'offline' | 'busy' | 'none'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<AvatarProps>(), {
|
||||
alt: 'User Avatar',
|
||||
size: 'medium',
|
||||
status: 'none',
|
||||
})
|
||||
|
||||
const sizeClasses = {
|
||||
xsmall: 'h-6 w-6 max-w-6',
|
||||
small: 'h-8 w-8 max-w-8',
|
||||
medium: 'h-10 w-10 max-w-10',
|
||||
large: 'h-12 w-12 max-w-12',
|
||||
xlarge: 'h-14 w-14 max-w-14',
|
||||
xxlarge: 'h-16 w-16 max-w-16',
|
||||
}
|
||||
|
||||
const statusSizeClasses = {
|
||||
xsmall: 'h-1.5 w-1.5 max-w-1.5',
|
||||
small: 'h-2 w-2 max-w-2',
|
||||
medium: 'h-2.5 w-2.5 max-w-2.5',
|
||||
large: 'h-3 w-3 max-w-3',
|
||||
xlarge: 'h-3.5 w-3.5 max-w-3.5',
|
||||
xxlarge: 'h-4 w-4 max-w-4',
|
||||
}
|
||||
|
||||
const statusColorClasses = {
|
||||
online: 'bg-success-500',
|
||||
offline: 'bg-error-400',
|
||||
busy: 'bg-warning-500',
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<span :class="[baseStyles, sizeClass, colorStyles]">
|
||||
<span v-if="startIcon" class="mr-1">
|
||||
<component :is="startIcon" />
|
||||
</span>
|
||||
<slot></slot>
|
||||
<span v-if="endIcon" class="ml-1">
|
||||
<component :is="endIcon" />
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
type BadgeVariant = 'light' | 'solid'
|
||||
type BadgeSize = 'sm' | 'md'
|
||||
type BadgeColor = 'primary' | 'success' | 'error' | 'warning' | 'info' | 'light' | 'dark'
|
||||
|
||||
interface BadgeProps {
|
||||
variant?: BadgeVariant
|
||||
size?: BadgeSize
|
||||
color?: BadgeColor
|
||||
startIcon?: object
|
||||
endIcon?: object
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<BadgeProps>(), {
|
||||
variant: 'light',
|
||||
color: 'primary',
|
||||
size: 'md',
|
||||
})
|
||||
|
||||
const baseStyles =
|
||||
'inline-flex items-center px-2.5 py-0.5 justify-center gap-1 rounded-full font-medium capitalize'
|
||||
|
||||
const sizeStyles = {
|
||||
sm: 'text-theme-xs',
|
||||
md: 'text-sm',
|
||||
}
|
||||
|
||||
const variants = {
|
||||
light: {
|
||||
primary: 'bg-brand-50 text-brand-500 dark:bg-brand-500/15 dark:text-brand-400',
|
||||
success: 'bg-success-50 text-success-600 dark:bg-success-500/15 dark:text-success-500',
|
||||
error: 'bg-error-50 text-error-600 dark:bg-error-500/15 dark:text-error-500',
|
||||
warning: 'bg-warning-50 text-warning-600 dark:bg-warning-500/15 dark:text-orange-400',
|
||||
info: 'bg-blue-light-50 text-blue-light-500 dark:bg-blue-light-500/15 dark:text-blue-light-500',
|
||||
light: 'bg-gray-100 text-gray-700 dark:bg-white/5 dark:text-white/80',
|
||||
dark: 'bg-gray-500 text-white dark:bg-white/5 dark:text-white',
|
||||
},
|
||||
solid: {
|
||||
primary: 'bg-brand-500 text-white dark:text-white',
|
||||
success: 'bg-success-500 text-white dark:text-white',
|
||||
error: 'bg-error-500 text-white dark:text-white',
|
||||
warning: 'bg-warning-500 text-white dark:text-white',
|
||||
info: 'bg-blue-light-500 text-white dark:text-white',
|
||||
light: 'bg-gray-400 dark:bg-white/5 text-white dark:text-white/80',
|
||||
dark: 'bg-gray-700 text-white dark:text-white',
|
||||
},
|
||||
}
|
||||
|
||||
const sizeClass = computed(() => sizeStyles[props.size])
|
||||
const colorStyles = computed(() => variants[props.variant][props.color])
|
||||
</script>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<button
|
||||
:class="[
|
||||
'inline-flex items-center justify-center font-medium gap-2 rounded-lg transition',
|
||||
sizeClasses[size],
|
||||
variantClasses[variant],
|
||||
className,
|
||||
{ 'cursor-not-allowed opacity-50': disabled },
|
||||
]"
|
||||
@click="onClick"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<span v-if="startIcon" class="flex items-center">
|
||||
<component :is="startIcon" />
|
||||
</span>
|
||||
<slot></slot>
|
||||
<span v-if="endIcon" class="flex items-center">
|
||||
<component :is="endIcon" />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface ButtonProps {
|
||||
size?: 'sm' | 'md'
|
||||
variant?: 'primary' | 'outline'
|
||||
startIcon?: object
|
||||
endIcon?: object
|
||||
onClick?: () => void
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
size: 'md',
|
||||
variant: 'primary',
|
||||
className: '',
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'px-4 py-3 text-sm',
|
||||
md: 'px-5 py-3.5 text-sm',
|
||||
}
|
||||
|
||||
const variantClasses = {
|
||||
primary: 'bg-brand-500 text-white shadow-theme-xs hover:bg-brand-600 disabled:bg-brand-300',
|
||||
outline:
|
||||
'bg-white text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-400 dark:ring-gray-700 dark:hover:bg-white/[0.03] dark:hover:text-gray-300',
|
||||
}
|
||||
|
||||
const onClick = () => {
|
||||
if (!props.disabled && props.onClick) {
|
||||
props.onClick()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="fixed inset-0 flex items-center justify-center overflow-y-auto z-99999">
|
||||
<div
|
||||
v-if="fullScreenBackdrop"
|
||||
class="fixed inset-0 h-full w-full bg-gray-400/50 backdrop-blur-[32px]"
|
||||
aria-hidden="true"
|
||||
@click="$emit('close')"
|
||||
></div>
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface ModalProps {
|
||||
fullScreenBackdrop?: boolean
|
||||
}
|
||||
|
||||
defineProps<ModalProps>()
|
||||
defineEmits(['close'])
|
||||
</script>
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div :class="['overflow-hidden rounded-lg', aspectRatioClass, className]">
|
||||
<iframe
|
||||
:src="`https://www.youtube.com/embed/${videoId}`"
|
||||
:title="title"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
class="w-full h-full"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
type AspectRatio = '16:9' | '4:3' | '21:9' | '1:1'
|
||||
|
||||
interface Props {
|
||||
videoId: string
|
||||
aspectRatio?: AspectRatio
|
||||
title?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
aspectRatio: '16:9',
|
||||
title: 'YouTube video',
|
||||
className: '',
|
||||
})
|
||||
|
||||
const aspectRatioClass = computed(() => {
|
||||
const aspectRatioClasses = {
|
||||
'16:9': 'aspect-video',
|
||||
'4:3': 'aspect-4/3',
|
||||
'21:9': 'aspect-21/9',
|
||||
'1:1': 'aspect-square',
|
||||
}
|
||||
return aspectRatioClasses[props.aspectRatio]
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div id="pane" class="overflow-hidden">
|
||||
<img
|
||||
src="/images/grid-image/image-01.png"
|
||||
alt="Cover"
|
||||
class="w-full border border-gray-200 rounded-xl dark:border-gray-800"
|
||||
/>
|
||||
</div>
|
||||
<div id="ghostpane" class="absolute top-0 left-0 duration-300 ease-in-out"></div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<div v-for="(image, index) in images" :key="index">
|
||||
<img
|
||||
:src="image.src"
|
||||
:alt="image.alt"
|
||||
class="w-full border border-gray-200 rounded-xl dark:border-gray-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const images = [
|
||||
{ src: '/images/grid-image/image-04.png', alt: 'Grid image 1' },
|
||||
{ src: '/images/grid-image/image-05.png', alt: 'Grid image 2' },
|
||||
{ src: '/images/grid-image/image-06.png', alt: 'Grid image 3' },
|
||||
]
|
||||
</script>
|
||||
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
||||
<div>
|
||||
<img
|
||||
src="/images/grid-image/image-02.png"
|
||||
alt="grid"
|
||||
class="w-full border border-gray-200 rounded-xl dark:border-gray-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<img
|
||||
src="/images/grid-image/image-03.png"
|
||||
alt="grid"
|
||||
class="w-full border border-gray-200 rounded-xl dark:border-gray-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user