@gindow/vant-go 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@gindow/vant-go",
3
+ "version": "1.0.0",
4
+ "description": "基于 Vant 的移动端扩展组件库",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "./dist/vant-go.cjs",
8
+ "types": "./dist/vant-go.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/vant-go.d.ts",
12
+ "import": "./dist/vant-go.mjs",
13
+ "require": "./dist/vant-go.cjs"
14
+ },
15
+ "./resolver": {
16
+ "types": "./dist/resolver.d.ts",
17
+ "import": "./dist/resolver.mjs",
18
+ "require": "./dist/resolver.cjs"
19
+ },
20
+ "./style.css": "./dist/style.css",
21
+ "./src/*": "./src/*",
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "src",
27
+ "README.md"
28
+ ],
29
+ "sideEffects": [
30
+ "**/*.css"
31
+ ],
32
+ "scripts": {
33
+ "build": "vue-tsc --noEmit && vite build"
34
+ },
35
+ "peerDependencies": {
36
+ "@gindow/vue": "^1.0.3",
37
+ "@iconify/vue": "^5.0.0",
38
+ "vant": "^4.9.24",
39
+ "vue": "^3.5.0",
40
+ "vue-router": "^4.0.0 || ^5.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@iconify/vue": "^5.0.1",
44
+ "@types/node": "^22.20.0",
45
+ "@vitejs/plugin-vue": "^6.0.7",
46
+ "@vue/tsconfig": "^0.9.1",
47
+ "typescript": "^6.0.3",
48
+ "unplugin-auto-import": "^21.0.0",
49
+ "unplugin-vue-components": "^32.1.0",
50
+ "vant": "^4.9.24",
51
+ "vite": "^8.1.0",
52
+ "vite-plugin-dts": "^4.5.4",
53
+ "vue": "^3.5.38",
54
+ "vue-router": "^5.1.0",
55
+ "vue-tsc": "^3.3.5"
56
+ }
57
+ }
Binary file
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <van-image-preview v-model:show="show" :images="assetUrls" :start-position="initialIndex" @change="onChange">
3
+ <template #image="{ src }">
4
+ <van-image fit="contain" :src="src" class="w-full">
5
+ <template #error>
6
+ <div class="flex flex-col justify-center items-center">
7
+ <vax-icon icon="file-question" :size="40" color="#093" />
8
+ <div>{{ currentAsset?.title }}</div>
9
+ </div>
10
+ </template>
11
+ </van-image>
12
+ </template>
13
+ </van-image-preview>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import type { PropType } from 'vue'
18
+ import { computed, ref } from 'vue'
19
+ import type { IAsset } from '../types'
20
+
21
+ const show = defineModel('show', { type: Boolean, default: false })
22
+
23
+ const props = defineProps({
24
+ asset: { type: Object as PropType<IAsset>, default: () => ({}) },
25
+ assets: { type: Array as PropType<IAsset[]>, default: () => [] },
26
+ })
27
+
28
+ const assetUrls = computed(() => props.assets.map(item => item.shrink ?? item?.url))
29
+
30
+ const initialIndex = computed(() => props.assets.findIndex(item => item.id === props.asset?.id) ?? 0)
31
+ const index = ref(initialIndex.value === -1 ? 0 : initialIndex.value)
32
+ const currentAsset = computed(() => props.assets[index.value])
33
+
34
+ const onChange = (value: number) => index.value = value
35
+ </script>
36
+
37
+ <style scoped>
38
+ </style>
@@ -0,0 +1,16 @@
1
+ <template>
2
+ <van-image :src :round :width="pixel" :height="pixel" />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import avatar from '../assets/avatar.png'
7
+
8
+ const { size, src } = defineProps({
9
+ src: { type: String, default: avatar },
10
+ size: { type: [String, Number], default: 'normal' },
11
+ round: { type: Boolean, default: true },
12
+ })
13
+
14
+ const sizes = { small: 24, normal: 48, large: 72 }
15
+ const pixel = computed(() => typeof size === 'number' ? size : (sizes[size as keyof typeof sizes] || sizes.normal))
16
+ </script>
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <van-button v-bind="$attrs">
3
+ <span class="flex-center gap-2">
4
+ <vax-icon v-if="icon" :icon :size :color :strokeWidth />
5
+ <slot />
6
+ </span>
7
+ </van-button>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ const props = defineProps({
12
+ icon: { type: String, default: '' },
13
+ iconSize: { type: Number, default: 14 },
14
+ iconColor: { type: String, default: '' },
15
+ iconStrokeWidth: { type: Number },
16
+ })
17
+
18
+ const size = computed(() => props.iconSize)
19
+ const color = computed(() => props.iconColor)
20
+ const strokeWidth = computed(() => props.iconStrokeWidth)
21
+ </script>
@@ -0,0 +1,60 @@
1
+ <template>
2
+ <van-field type="tel" v-model="phone" name="phone" :label="label ? t('auth.phone') : ''" :placeholder="t('auth.enterPhone')" center required clearable :rules="[{ required: true }]">
3
+ <template #button>
4
+ <van-button class="captcha" type="primary" :disabled="!!waiting" @click.stop="getCaptcha">{{ waiting > 0 ? waiting : t('auth.getOTP') }}</van-button>
5
+ </template>
6
+ </van-field>
7
+ <van-field type="number" v-model="captcha" :label="label ? t('auth.otp') : ''" :placeholder="t('auth.enterOTP')" center required clearable :rules="[{ required: true }]" />
8
+ <van-action-sheet v-model:show="show" :actions :cancel-text="t('cancel')" @select="select" />
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import { useLocale } from '../locale'
13
+ import { Validate } from '../utils/validate'
14
+ import { useCaptcha } from '../hooks/useCaptcha'
15
+ import { useMessage } from '../hooks/useMessage'
16
+
17
+ const { method, whatsApp, onSend } = defineProps({
18
+ method: { type: String, default: '' },
19
+ label: { type: Boolean, default: false }, // 是否显示 Label
20
+ whatsApp: { type: Boolean, default: false }, // 是否使用 WhatsApp
21
+ onSend: { type: Function as PropType<(data: any) => Promise<void>>, required: true }
22
+ })
23
+
24
+ const phone = defineModel('phone', { type: String, required: true })
25
+ const captcha = defineModel('captcha', { type: String, required: true })
26
+
27
+ const { t } = useLocale()
28
+ const { success, error } = useMessage()
29
+ const { waiting, send } = useCaptcha(onSend)
30
+
31
+ const channel = ref('sms')
32
+ const sent = ref(false)
33
+ const show = ref(false)
34
+ const actions = computed(() => [
35
+ { name: t('auth.viaWhatsApp'), value: 'whatsapp' },
36
+ { name: t('auth.viaPhone'), value: 'sms' },
37
+ ])
38
+
39
+ function sendOTP() {
40
+ return send({ method, phone: phone.value, channel: channel.value }).then(() => {
41
+ captcha.value = ''
42
+ sent.value = true
43
+ success(t('message.otpSent'))
44
+ }).catch(({ message }: { message: string }) => error(message))
45
+ }
46
+
47
+ // 获取验证码
48
+ function getCaptcha() {
49
+ if (!Validate.phone(phone.value)) return error(t('message.phoneInvalid'))
50
+ if (whatsApp) show.value = true
51
+ else sendOTP()
52
+ }
53
+
54
+ // 选择通道
55
+ function select({ value }: { value: string }) {
56
+ channel.value = value
57
+ show.value = false
58
+ sendOTP()
59
+ }
60
+ </script>
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <van-cell center :title :value :label :to :titleClass :valueClass :labelClass :isLink @click="onClick">
3
+ <template v-if="$slots.icon || icon" #icon>
4
+ <slot name="icon">
5
+ <vax-icon v-if="icon" :icon
6
+ :size="iconSize" :color="iconColor"
7
+ :background="iconBackground" :stroke-width="iconStrokeWidth" />
8
+ </slot>
9
+ </template>
10
+ <template v-if="$slots.title" #title><slot name="title" /></template>
11
+ <template v-if="$slots.label" #label><slot name="label" /></template>
12
+ <template v-if="$slots.value" #value><slot name="value" /></template>
13
+ </van-cell>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ const props = defineProps({
18
+ icon: { type: String },
19
+ iconSize: { type: Number },
20
+ iconColor: { type: String },
21
+ iconBackground: { type: String },
22
+ iconStrokeWidth: { type: Number },
23
+ title: { type: String },
24
+ label: { type: String },
25
+ value: { type: [String, Number] },
26
+ titleClass: { type: String },
27
+ valueClass: { type: String },
28
+ labelClass: { type: String },
29
+ isLink: { type: Boolean },
30
+ to: { type: String },
31
+ onClick: { type: Function as PropType<() => void> },
32
+ })
33
+
34
+ const isLink = computed(() => props.isLink || !! props.to)
35
+ </script>
@@ -0,0 +1,50 @@
1
+ <template>
2
+ <van-cell-group class="vax-card menu" :class="{ compact }" :title>
3
+ <template v-if="$slots.title" #title><slot name="title" /></template>
4
+ <slot />
5
+ <vax-cell v-for="(item, key) in items" :key v-bind="item"
6
+ :is-link="item.isLink ?? isLink"
7
+ :title-class="item.titleClass ?? titleClass"
8
+ :value-class="item.valueClass ?? valueClass"
9
+ :label-class="item.labelClass ?? labelClass"
10
+ :icon-size="item.iconSize ?? iconSize"
11
+ :icon-color="item.iconColor ?? iconColor"
12
+ :icon-background="item.iconBackground ?? iconBackground"
13
+ :icon-stroke-width="item.iconStrokeWidth ?? iconStrokeWidth" />
14
+ </van-cell-group>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import vaxCell from './VaxCell.vue'
19
+
20
+ interface IVaxCardItem {
21
+ icon?: string
22
+ iconSize?: number
23
+ iconColor?: string
24
+ iconBackground?: string
25
+ iconStrokeWidth?: number
26
+ title: string
27
+ value?: string | number
28
+ label?: string
29
+ to?: string
30
+ titleClass?: string
31
+ valueClass?: string
32
+ labelClass?: string
33
+ isLink?: boolean
34
+ onClick?: () => void
35
+ }
36
+
37
+ defineProps({
38
+ title: { type: String, default: '' },
39
+ iconSize: { type: Number },
40
+ iconColor: { type: String },
41
+ iconBackground: { type: String },
42
+ iconStrokeWidth: { type: Number },
43
+ items: { type: Array as PropType<IVaxCardItem[]>, default: () => [] },
44
+ titleClass: { type: String },
45
+ valueClass: { type: String },
46
+ labelClass: { type: String },
47
+ isLink: { type: Boolean },
48
+ compact: { type: Boolean, default: false }
49
+ })
50
+ </script>
@@ -0,0 +1,18 @@
1
+ <template>
2
+ <van-empty :image :imageSize :description>
3
+ <slot />
4
+ </van-empty>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { useLocale } from '../locale'
9
+
10
+ const props = defineProps({
11
+ image: { type: String, default: 'default' },
12
+ imageSize: { type: Number },
13
+ description: { type: String },
14
+ })
15
+
16
+ const { t } = useLocale()
17
+ const description = computed(() => props.description ?? t('empty.noData'))
18
+ </script>
@@ -0,0 +1,69 @@
1
+ <template>
2
+ <van-cell :title="label" :value center is-link @click="edit">
3
+ <template #icon>
4
+ <vax-icon :icon class="mr-2" />
5
+ </template>
6
+ </van-cell>
7
+ <!-- Text -->
8
+ <van-dialog v-model:show="show" :title="label" show-cancel-button @confirm="confirm" @cancel="cancel">
9
+ <van-field v-model="field" :type="(type as FieldType)" />
10
+ </van-dialog>
11
+ <!-- Date -->
12
+ <van-popup v-model:show="showData" position="bottom">
13
+ <van-date-picker :model-value="dates" :title="label" @confirm="dateConfirm" @cancel="showData = false" />
14
+ </van-popup>
15
+ <!-- Select -->
16
+ <van-popup v-model:show="showSelect" position="bottom">
17
+ <van-picker :columns :title="label" @confirm="pickerConfirm" @cancel="showSelect = false" />
18
+ </van-popup>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import type { FieldType } from 'vant'
23
+
24
+ const { readonly, type, columns } = defineProps({
25
+ icon: { type: String },
26
+ label: { type: String },
27
+ type: { type: String, default: 'text' },
28
+ readonly: { type: Boolean, default: false },
29
+ columns: { type: Array as PropType<{ text: string; value: string }[]>, default: () => [] }
30
+ })
31
+
32
+ const emit = defineEmits(['update:modelValue'])
33
+
34
+ const model = defineModel<string | number>()
35
+ const value = computed(() => {
36
+ if (type === 'select') return columns.find(item => item.value === model.value)?.text || ''
37
+ else return model.value
38
+ })
39
+
40
+ // Text
41
+ const show = ref(false)
42
+ const field = ref(model.value)
43
+ const cancel = () => field.value = model.value
44
+ const confirm = () => model.value = field.value
45
+ const edit = () => {
46
+ if (readonly) return
47
+ else if (type === 'date') showData.value = true
48
+ else if (type === 'select') showSelect.value = true
49
+ else show.value = true
50
+ }
51
+
52
+ // Data
53
+ const showData = ref(false)
54
+ const dates = computed(() => {
55
+ const date = new Date()
56
+ return [ String(date.getFullYear()), String(date.getMonth() + 1), String(date.getDate()) ]
57
+ })
58
+ const dateConfirm = ({ selectedValues }: { selectedValues: string[] }) => {
59
+ model.value = selectedValues.join('-')
60
+ showData.value = false
61
+ }
62
+
63
+ // Picker
64
+ const showSelect = ref(false)
65
+ const pickerConfirm = ({ selectedValues }: { selectedValues: string[] }) => {
66
+ model.value = selectedValues[0]
67
+ showSelect.value = false
68
+ }
69
+ </script>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <span v-if="icon" class="i-icon inline-flex items-center gap-1" :style="{ background, color, padding }" :class="{ 'rounded-full': round }">
3
+ <Icon :icon="name" :width="size" :height="size" :aria-hidden="null" />
4
+ <slot />
5
+ </span>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import { Icon } from '@iconify/vue'
10
+
11
+ const props = defineProps({
12
+ vendor: { type: String, default: 'icon-park-outline' },
13
+ icon: { type: String, default: '' },
14
+ size: { type: Number, default: 16 },
15
+ strokeWidth: { type: Number, default: 3 },
16
+ color: { type: String },
17
+ background: { type: String },
18
+ padding: { type: String },
19
+ round: { type: Boolean },
20
+ disabled: { type: Boolean },
21
+ })
22
+
23
+ const name = computed(() => props.icon.indexOf(':') !== -1 ? props.icon : `${props.vendor}:${props.icon}`)
24
+ const color = computed(() => props.disabled ? 'var(--van-text-color-3)' : props.color || (props.background ? '#FFF' : undefined))
25
+ const round = props.round || (props.background ? true : false)
26
+ const padding = props.padding ? parseInt(props.padding) + 'px' : (props.background ? '8px' : '0')
27
+ </script>
28
+
29
+ <style scoped>
30
+ :deep(g), :deep(path) { stroke-width: v-bind(strokeWidth) }
31
+ </style>
@@ -0,0 +1,32 @@
1
+ <template>
2
+ <Teleport to="body" :disabled="!fullscreen">
3
+ <div class="vax-loading flex-center p-4" :class="{ 'vax-loading--fullscreen': fullscreen }">
4
+ <van-loading :type :size :color :vertical>
5
+ <slot>{{ text }}</slot>
6
+ </van-loading>
7
+ </div>
8
+ </Teleport>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import type { LoadingType } from 'vant'
13
+
14
+ defineProps({
15
+ type: { type: String as PropType<LoadingType>, default: 'circular' },
16
+ size: { type: Number, default: 48 },
17
+ color: { type: String, default: '#ccc' },
18
+ vertical: { type: Boolean, default: false },
19
+ text: { type: String, default: '' },
20
+ fullscreen: { type: Boolean, default: false },
21
+ })
22
+ </script>
23
+
24
+ <style>
25
+ .vax-loading--fullscreen {
26
+ position: fixed;
27
+ inset: 0;
28
+ z-index: 2000;
29
+ padding: 0;
30
+ background: rgba(0, 0, 0, 0.45);
31
+ }
32
+ </style>
@@ -0,0 +1,28 @@
1
+ <template>
2
+ <van-nav-bar :leftArrow :leftText @click-left="onClickLeft">
3
+ <template v-if="$slots.right" #right>
4
+ <slot name="right"></slot>
5
+ </template>
6
+ </van-nav-bar>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ const props = defineProps({
11
+ back: { type: Boolean, default: false },
12
+ leftArrow: { type: Boolean, default: false },
13
+ leftText: { type: String, default: '' },
14
+ clickLeft: { type: Function, default: () => {} },
15
+ })
16
+
17
+ const textAlign = computed(() => props.leftText ? 'center' : 'left')
18
+ const leftArrow = computed(() => props.leftArrow || props.back)
19
+ const paddingLeft = computed(() => props.leftText || leftArrow.value ? '0' : '16px')
20
+
21
+ const router = useRouter()
22
+ const onClickLeft = () => props.back ? router.back() : props.clickLeft()
23
+ </script>
24
+
25
+ <style scoped>
26
+ .van-nav-bar :deep(.van-nav-bar__content > div) { position: static; }
27
+ .van-nav-bar :deep(.van-nav-bar__title) { flex: 1; max-width: 100%; text-align: v-bind(textAlign); padding-left: v-bind(paddingLeft); }
28
+ </style>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <div class="vax-page">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <style scoped>
8
+ .vax-page { padding: var(--ex-page-padding, 16px) 0; }
9
+ </style>
@@ -0,0 +1,78 @@
1
+ <template>
2
+ <van-field v-bind="$attrs" :model-value="value" colon readonly is-link @click="onShow">
3
+ <template #input>
4
+ <span v-if="!multiple">{{ model }}</span>
5
+ <van-space v-else>
6
+ <van-tag v-for="item in (model as string[])" plain type="primary">{{ item }}</van-tag>
7
+ </van-space>
8
+ </template>
9
+ </van-field>
10
+ <van-popup v-model:show="show" destroy-on-close position="bottom" round>
11
+ <!-- 单选 -->
12
+ <van-picker v-if="!multiple" v-model="pickerValue" :columns="columns" @confirm="onConfirm" @cancel="show = false" />
13
+ <!-- 多选 -->
14
+ <template v-else>
15
+ <div class="van-picker__toolbar">
16
+ <van-button class="border-none! bg-white!" @click="show = false">{{ t('cancel') }}</van-button>
17
+ <van-button class="border-none! bg-white!" plain type="primary" @click="confirm">{{ t('confirm') }}</van-button>
18
+ </div>
19
+ <van-checkbox-group v-model="checkboxValue" direction="vertical" class="h-[264px] overflow-auto">
20
+ <van-cell-group>
21
+ <van-cell
22
+ v-for="(item, index) in options"
23
+ clickable
24
+ :title="item"
25
+ @click="toggle(index)">
26
+ <template #right-icon>
27
+ <van-checkbox :ref="(el) => checkboxRefs[index] = el" :name="item" @click.stop />
28
+ </template>
29
+ </van-cell>
30
+ </van-cell-group>
31
+ </van-checkbox-group>
32
+ </template>
33
+ </van-popup>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import type { PropType } from 'vue'
38
+ import type { CheckboxInstance } from 'vant'
39
+ import { computed, ref } from 'vue'
40
+ import { useLocale } from '../locale'
41
+
42
+ const model = defineModel({ type: [String, Number, Array] as PropType<string | number | number[] | string[]> })
43
+ const value = computed(() => multiple ? Array.isArray(model.value) ? model.value.join(',') : '' : model.value as string | number)
44
+
45
+ const { multiple, options } = defineProps({
46
+ multiple: { type: Boolean, default: false },
47
+ options: { type: Array as PropType<string[]>, default: () => [] },
48
+ })
49
+
50
+ const { t } = useLocale()
51
+
52
+ const show = ref(false)
53
+ const onShow = () => {
54
+ if (multiple) {
55
+ checkboxValue.value = (model.value as string[]) ?? []
56
+ } else {
57
+ pickerValue.value = model.value ? [model.value] as string[] : []
58
+ }
59
+ show.value = true
60
+ }
61
+
62
+ // 单选
63
+ const pickerValue = ref<string[] | number[]>([])
64
+ const columns = computed(() => options?.map((value) => ({ text: value, value })) || [])
65
+ const onConfirm = ({ selectedValues }: { selectedValues: string[] }) => {
66
+ show.value = false
67
+ model.value = selectedValues[0]
68
+ }
69
+
70
+ // 多选
71
+ const checkboxValue = ref<string[] | number[]>([])
72
+ const checkboxRefs = ref<(CheckboxInstance | any)[]>([])
73
+ const toggle = (index: number) => checkboxRefs.value[index]?.toggle()
74
+ const confirm = () => {
75
+ show.value = false
76
+ model.value = checkboxValue.value
77
+ }
78
+ </script>
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <van-tabbar v-model="active" placeholder safe-area-inset-bottom>
3
+ <van-tabbar-item v-for="{ to, name, icon, title, flashed, dot } in data" replace :to :name :dot>
4
+ <template v-if="!flashed" #icon>
5
+ <vax-icon :icon :size />
6
+ </template>
7
+ <div v-if="!flash || flashed" :class="{ flashed }" class="mt-1.5 font-bold">{{ title }}</div>
8
+ </van-tabbar-item>
9
+ </van-tabbar>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import { Tabbar as vanTabbar, TabbarItem as vanTabbarItem } from 'vant'
14
+ import VaxIcon from './VaxIcon.vue'
15
+
16
+ const props = defineProps({
17
+ menu: { type: Array<{ title?: string, icon: string, to: string, dot?: boolean }>, required: true, default: () => ([]) },
18
+ flash: { type: Boolean, default: false },
19
+ })
20
+
21
+ const data = computed(() => props.menu.map(item => ({
22
+ title: item.title,
23
+ to: item.to,
24
+ name: item.to,
25
+ icon: item.icon,
26
+ flashed: props.flash && item.title && active.value === item.to,
27
+ dot: item.dot,
28
+ })))
29
+
30
+ // 当前路由
31
+ const route = useRoute()
32
+ const routePath = computed(() => {
33
+ let routePath = ''
34
+ props.menu.forEach(({ to }) => {
35
+ if ((route.path + '/').indexOf(to) !== -1 && to.length > routePath.length) routePath = to
36
+ })
37
+ return routePath
38
+ })
39
+
40
+ const active = ref(routePath.value)
41
+ const size = computed(() => props.flash ? 22 : 20)
42
+
43
+ watchEffect(() => active.value = routePath.value)
44
+ </script>
45
+
46
+ <style scoped>
47
+ .van-tabbar .van-tabbar-item { --van-tabbar-item-icon-margin-bottom: 0 }
48
+ .van-tabbar .flashed { margin-top: 0; font-size: 14px; }
49
+ </style>
@@ -0,0 +1,66 @@
1
+ <template>
2
+ <div class="vax-tabs" :class="{ 'vax-tabs--sticky': sticky }">
3
+ <button v-for="{ name, label, badge } in tabs" :key="name" type="button" class="vax-tab" :class="{ active: name === active }" @click="onPick(name)">
4
+ <van-badge :content="badge" class="badge">{{ label }}</van-badge>
5
+ </button>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ export interface IVaxTab {
11
+ name: string
12
+ label: string
13
+ badge?: number | string
14
+ }
15
+
16
+ const { tabs } = defineProps({
17
+ tabs: { type: Array as () => IVaxTab[], required: true },
18
+ sticky: { type: Boolean, default: false },
19
+ })
20
+
21
+ const active = defineModel<string>('active', { required: true })
22
+
23
+ function onPick(name: string) {
24
+ if (active.value !== name) active.value = name
25
+ }
26
+ </script>
27
+
28
+ <style scoped>
29
+ .vax-tabs {
30
+ display: flex;
31
+ gap: 4px;
32
+ margin: 12px 16px;
33
+ padding: 4px;
34
+ background: var(--van-gray-2);
35
+ border-radius: 10px;
36
+ overflow-x: auto;
37
+ scrollbar-width: none;
38
+ }
39
+ .vax-tabs::-webkit-scrollbar { display: none; }
40
+ .vax-tabs--sticky {
41
+ position: sticky;
42
+ top: 0;
43
+ z-index: 10;
44
+ background: var(--van-gray-2);
45
+ }
46
+ .vax-tab {
47
+ position: relative;
48
+ flex: 1;
49
+ min-width: max-content;
50
+ padding: 8px 14px;
51
+ border: 0;
52
+ border-radius: 8px;
53
+ background: transparent;
54
+ font-size: 13px;
55
+ font-weight: 500;
56
+ line-height: 1.25;
57
+ color: var(--van-text-color-2);
58
+ cursor: pointer;
59
+ transition: background 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
60
+ white-space: nowrap;
61
+ }
62
+ .vax-tab:active { transform: scale(0.98); }
63
+ .vax-tab.active { background: var(--van-background-2); color: var(--van-text-color); font-weight: 600; }
64
+ .vax-tab .badge { --van-badge-background: var(--van-primary-color); }
65
+ .vax-tab .badge :deep(.van-badge--top-right) { right: -8px; }
66
+ </style>