移动端UI

This commit is contained in:
2025-12-29 22:36:35 -08:00
parent cffb51b58c
commit 11c05de565
23 changed files with 864 additions and 71 deletions
+33
View File
@@ -0,0 +1,33 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { useVModel } from "@vueuse/core"
import { cn } from "@/lib/utils"
const props = defineProps<{
defaultValue?: string | number
modelValue?: string | number
class?: HTMLAttributes["class"]
}>()
const emits = defineEmits<{
(e: "update:modelValue", payload: string | number): void
}>()
const modelValue = useVModel(props, "modelValue", emits, {
passive: true,
defaultValue: props.defaultValue,
})
</script>
<template>
<input
v-model="modelValue"
data-slot="input"
:class="cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
props.class,
)"
>
</template>
+1
View File
@@ -0,0 +1 @@
export { default as Input } from "./Input.vue"
+36
View File
@@ -0,0 +1,36 @@
<script setup lang="ts">
import { Laptop, Smartphone } from 'lucide-vue-next'
interface Device {
id: string
name: string
type: 'mobile' | 'desktop'
deviceId: string
}
defineProps<{
devices: Device[]
}>()
</script>
<template>
<div class="flex items-center justify-center gap-3">
<div
v-for="device in devices"
:key="device.id"
class="h-[76px] rounded-3xl border-2 p-4 text-[10px]"
>
<div class="flex items-center gap-3">
<Smartphone v-if="device.type === 'mobile'" class="h-5 w-5 text-white" />
<Laptop v-else class="h-5 w-5 text-white" />
<div class="flex flex-col overflow-hidden">
<span class="text-white">设备: {{ device.name }}</span>
<span class="font-mono text-[11px] tracking-tighter uppercase"
>ID: {{ device.deviceId }}</span
>
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,76 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Button } from '@/components/ui/button'
interface PaymentOption {
label: string
value: number
}
const list = ref<PaymentOption[]>([
{ label: '支付宝', value: 1 },
{ label: '微信', value: 2 },
{ label: '国际银行卡', value: 3 },
])
// 当前选中的值,默认选中第一个
const selectedValue = ref(1)
const handleSelect = (value: number) => {
selectedValue.value = value
}
</script>
<template>
<div>
<div class="bg-[#A8FF53] px-6 pt-7 font-sans text-black">
<h2 class="mb-1 text-center text-2xl font-bold">选择付款方式</h2>
<div class="flex flex-col gap-4 pt-[43px]">
<div
v-for="item in list"
:key="item.value"
@click="handleSelect(item.value)"
:class="[
'flex h-[66px] cursor-pointer items-center justify-between border-[5px] border-black px-6 transition-all active:scale-[0.98]',
'rounded-[32px]', // 超大圆角
selectedValue === item.value ? 'bg-[#A8FF53]' : 'bg-[#A8FF53]',
]"
>
<span class="text-xl font-semibold">{{ item.label }}</span>
<div
class="flex h-7 w-7 items-center justify-center rounded-full border-[1px] border-black bg-transparent"
>
<div
v-if="selectedValue === item.value"
class="h-3.5 w-3.5 rounded-full bg-black"
></div>
</div>
</div>
</div>
</div>
<div class="px-6 pt-[55px]">
<h2 class="mb-1 text-center text-2xl font-bold">订单信息</h2>
<div class="ml-[11px] pt-[22px] text-[16px] font-semibold">365天套餐</div>
<div class="ml-[11px] flex items-center justify-between pt-[22px] text-[16px] font-semibold">
<div>订单金额</div>
<div>USD 44.99</div>
</div>
</div>
<div class="px-6 pt-[85px] pb-[23px]">
<Button
class="h-[50px] w-full rounded-[32px] border-none bg-black text-[16px] font-black text-white hover:bg-black/90"
>
立即支付
</Button>
</div>
</div>
</template>
<style scoped>
/* 如果需要特殊的斜体感,可以在这里微调 */
span {
letter-spacing: -0.02em;
}
</style>
+40
View File
@@ -0,0 +1,40 @@
<template>
<div class="px-6 pt-7 font-sans">
<h2 class="mb-1 text-center text-2xl font-bold">套餐选择</h2>
<p class="mb-4 text-center text-sm font-[100] text-gray-600">*所有套餐均不限流量不限速度</p>
<div class="space-y-4">
<div
v-for="(plan, index) in plans"
:key="plan.id"
@click="$emit('select', plan.id)"
:class="[
'relative h-[126px] cursor-pointer overflow-hidden rounded-[40px] border-4 border-black py-4 pl-[45px] transition-all',
currentPlanIndex === index ? 'bg-black text-white' : 'bg-[#A8FF53] text-black',
]"
>
<div class="text-2xl font-bold">{{ plan.days }}</div>
<div class="text-[40px] leading-none font-semibold">${{ plan.price }}</div>
<div :class="'text-sm'">约${{ plan.daily }}/</div>
<div
v-if="plan.discount"
:class="[
index > 1 ? 'font-semibold' : '',
currentPlanIndex === index ? 'bg-[#A8FF53]! text-black' : ' ',
]"
class="absolute top-[20px] -right-[40px] h-[16px] w-[126px] origin-center rotate-45 bg-black text-center text-[14px] leading-[16px] font-[200] text-[#ADFF5B]"
>
111 off%
</div>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
plans: Array,
currentPlanIndex: Number,
})
defineEmits(['select'])
</script>