@aquiferre/ui-kit 0.1.8 → 0.2.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.
@@ -1,7 +1,7 @@
1
1
  <template>
2
- <nav class="breadcrumb" aria-label="Breadcrumb">
2
+ <nav class="breadcrumb" aria-label="Breadcrumb" :data-testid="baseTestId">
3
3
  <ol class="breadcrumb-list">
4
- <li v-for="(item, index) in items" :key="index" class="breadcrumb-item">
4
+ <li v-for="(item, index) in items" :key="index" class="breadcrumb-item" :data-testid="baseTestId ? `${baseTestId}-item-${index}` : undefined">
5
5
  <template v-if="index < items.length - 1">
6
6
  <a v-if="item.path" :href="item.path" class="breadcrumb-link" @click.prevent="handleClick(item)">
7
7
  {{ item.label }}
@@ -16,6 +16,7 @@
16
16
  </template>
17
17
 
18
18
  <script setup lang="ts">
19
+ import { computed, useAttrs } from 'vue'
19
20
  import { useRouter } from 'vue-router'
20
21
 
21
22
  interface BreadcrumbItem {
@@ -25,8 +26,12 @@ interface BreadcrumbItem {
25
26
 
26
27
  const props = defineProps<{
27
28
  items: BreadcrumbItem[]
29
+ testId?: string
28
30
  }>()
29
31
 
32
+ const attrs = useAttrs()
33
+ const baseTestId = computed<string | undefined>(() => props.testId ?? (attrs['data-testid'] as string | undefined))
34
+
30
35
  const router = useRouter()
31
36
 
32
37
  function handleClick(item: BreadcrumbItem) {
@@ -1,18 +1,18 @@
1
1
  <template>
2
2
  <Teleport to="body">
3
3
  <Transition name="fade">
4
- <div v-if="visible" class="dialog-overlay">
5
- <div class="fixed inset-0 bg-bg-overlay" @click="cancel" />
4
+ <div v-if="visible" class="dialog-overlay" :data-testid="testId">
5
+ <div class="fixed inset-0 bg-bg-overlay" :data-testid="testId ? `${testId}-mask` : undefined" @click="cancel" />
6
6
  <div class="panel-glass dialog max-w-sm">
7
7
  <div class="px-6 py-4">
8
8
  <h3 class="dialog-title">{{ title }}</h3>
9
9
  <p v-if="content" class="text-text-secondary text-sm mt-2">{{ content }}</p>
10
10
  </div>
11
11
  <div class="dialog-actions px-6 py-4 border-t border-border-primary bg-bg-secondary/50 rounded-b-2xl">
12
- <button v-if="showCancel" class="btn-secondary" @click="cancel">
12
+ <button v-if="showCancel" class="btn-secondary" :data-testid="testId ? `${testId}-cancel` : undefined" @click="cancel">
13
13
  {{ cancelText }}
14
14
  </button>
15
- <button :disabled="loading" :class="confirmClass" @click="confirm">
15
+ <button :disabled="loading" :class="confirmClass" :data-testid="testId ? `${testId}-confirm` : 'confirm-btn'" @click="confirm">
16
16
  {{ loading ? loadingText : confirmText }}
17
17
  </button>
18
18
  </div>
@@ -33,6 +33,7 @@ withDefaults(defineProps<{
33
33
  loading?: boolean
34
34
  loadingText?: string
35
35
  confirmClass?: string
36
+ testId?: string
36
37
  }>(), {
37
38
  title: '确认删除',
38
39
  confirmText: '确认删除',
@@ -1,10 +1,10 @@
1
1
  <template>
2
- <div class="table-container relative w-full">
2
+ <div class="table-container" :class="{ relative: loading }" :data-testid="baseTestId">
3
3
  <table class="table">
4
4
  <thead class="table-header">
5
5
  <tr>
6
6
  <th v-if="selectable" class="table-header-cell w-8">
7
- <input type="checkbox" :checked="allSelected" @change="toggleAll" />
7
+ <input type="checkbox" :checked="allSelected" :data-testid="rowPrefix ? `${rowPrefix}-select-all` : undefined" @change="toggleAll" />
8
8
  </th>
9
9
  <th
10
10
  v-for="col in columns"
@@ -12,6 +12,7 @@
12
12
  class="table-header-cell"
13
13
  :class="[col.width, col.sortable ? 'cursor-pointer select-none' : '']"
14
14
  :style="col.align ? `text-align:${col.align}` : undefined"
15
+ :data-testid="rowPrefix && col.sortable ? `${rowPrefix}-sort-${col.sortField || col.key}` : undefined"
15
16
  @click="col.sortable ? onSortClick(col) : undefined"
16
17
  >
17
18
  <span class="inline-flex items-center gap-1">
@@ -27,7 +28,7 @@
27
28
  </tr>
28
29
  </thead>
29
30
  <tbody class="table-body">
30
- <tr v-if="!loading && data.length === 0">
31
+ <tr v-if="!loading && data.length === 0" class="table-row">
31
32
  <td :colspan="colspan" class="table-empty">
32
33
  {{ emptyText }}
33
34
  </td>
@@ -35,11 +36,12 @@
35
36
  <tr
36
37
  v-for="(row, index) in data"
37
38
  :key="row[rowKey] ?? index"
38
- class="table-row cursor-pointer"
39
+ :data-testid="rowPrefix ? `${rowPrefix}-${row[rowKey] ?? index}` : undefined"
40
+ class="table-row"
39
41
  @click="handleRowClick(row)"
40
42
  >
41
43
  <td v-if="selectable" class="table-cell" @click.stop>
42
- <input type="checkbox" :checked="isSelected(row)" @change="toggleRow(row)" />
44
+ <input type="checkbox" :checked="isSelected(row)" :data-testid="rowPrefix ? `${rowPrefix}-checkbox-${row[rowKey] ?? index}` : undefined" @change="toggleRow(row)" />
43
45
  </td>
44
46
  <td
45
47
  v-for="col in columns"
@@ -65,7 +67,7 @@
65
67
  </template>
66
68
 
67
69
  <script setup lang="ts">
68
- import { computed } from 'vue'
70
+ import { computed, useAttrs } from 'vue'
69
71
  import type { Column } from '../types'
70
72
 
71
73
  const props = withDefaults(defineProps<{
@@ -77,6 +79,8 @@ const props = withDefaults(defineProps<{
77
79
  selected?: string[]
78
80
  rowKey?: string
79
81
  sort?: { field: string; order: 'asc' | 'desc' } | null
82
+ rowTestIdPrefix?: string
83
+ testId?: string
80
84
  }>(), {
81
85
  loading: false,
82
86
  emptyText: '暂无数据',
@@ -84,8 +88,13 @@ const props = withDefaults(defineProps<{
84
88
  selected: () => [],
85
89
  rowKey: 'id',
86
90
  sort: null,
91
+ rowTestIdPrefix: undefined,
87
92
  })
88
93
 
94
+ const attrs = useAttrs()
95
+ const baseTestId = computed<string | undefined>(() => props.testId ?? (attrs['data-testid'] as string | undefined))
96
+ const rowPrefix = computed<string | undefined>(() => props.rowTestIdPrefix ?? baseTestId.value)
97
+
89
98
  const emit = defineEmits<{
90
99
  'update:selected': [ids: string[]]
91
100
  'row-click': [row: any]
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div>
2
+ <div :data-testid="baseTestId">
3
3
  <input
4
4
  type="file"
5
5
  multiple
@@ -7,14 +7,15 @@
7
7
  @change="handleFileSelect"
8
8
  class="hidden"
9
9
  ref="fileInput"
10
+ :data-testid="baseTestId ? `${baseTestId}-input` : undefined"
10
11
  />
11
12
 
12
- <button @click="triggerFileInput" class="btn-primary btn-sm inline-flex items-center gap-1">
13
+ <button @click="triggerFileInput" class="btn-primary btn-sm inline-flex items-center gap-1" :data-testid="baseTestId ? `${baseTestId}-trigger` : undefined">
13
14
  📎 选择文件
14
15
  </button>
15
16
 
16
17
  <div v-if="uploadQueue.length > 0" class="mt-3 space-y-2">
17
- <div v-for="item in uploadQueue" :key="item.id" class="bg-bg-secondary rounded-lg p-3">
18
+ <div v-for="item in uploadQueue" :key="item.id" class="bg-bg-secondary rounded-lg p-3" :data-testid="baseTestId ? `${baseTestId}-item-${item.id}` : undefined">
18
19
  <div class="flex justify-between text-sm mb-1">
19
20
  <span class="text-text-primary font-medium">{{ item.name }}</span>
20
21
  <span class="text-text-tertiary">{{ formatSize(item.size) }}</span>
@@ -41,7 +42,7 @@
41
42
  </template>
42
43
 
43
44
  <script setup lang="ts">
44
- import { ref, reactive } from 'vue'
45
+ import { ref, reactive, computed, useAttrs } from 'vue'
45
46
 
46
47
  interface UploadItem {
47
48
  id: string
@@ -55,10 +56,14 @@ interface UploadItem {
55
56
  const props = withDefaults(defineProps<{
56
57
  uploadFile: (file: File, onProgress: (percent: number) => void) => Promise<void>
57
58
  acceptTypes?: string
59
+ testId?: string
58
60
  }>(), {
59
61
  acceptTypes: 'image/*,video/*'
60
62
  })
61
63
 
64
+ const attrs = useAttrs()
65
+ const baseTestId = computed<string | undefined>(() => props.testId ?? (attrs['data-testid'] as string | undefined))
66
+
62
67
  const emit = defineEmits<{
63
68
  uploaded: []
64
69
  }>()
@@ -2,21 +2,25 @@
2
2
  <select
3
3
  :value="modelValue"
4
4
  class="select"
5
+ :data-testid="baseTestId"
5
6
  @change="$emit('update:modelValue', ($event.target as HTMLSelectElement).value)"
6
7
  >
7
8
  <option v-if="showAll" value="">{{ placeholder || '全部' }}</option>
8
- <option v-for="opt in options" :key="opt.value" :value="opt.value">
9
+ <option v-for="opt in options" :key="opt.value" :value="opt.value" :data-testid="baseTestId ? `${baseTestId}-option-${opt.value}` : undefined">
9
10
  {{ opt.label }}
10
11
  </option>
11
12
  </select>
12
13
  </template>
13
14
 
14
15
  <script setup lang="ts">
15
- withDefaults(defineProps<{
16
+ import { computed, useAttrs } from 'vue'
17
+
18
+ const props = withDefaults(defineProps<{
16
19
  modelValue: string
17
20
  options: Array<{ value: string; label: string }>
18
21
  placeholder?: string
19
22
  showAll?: boolean
23
+ testId?: string
20
24
  }>(), {
21
25
  showAll: true
22
26
  })
@@ -24,4 +28,7 @@ withDefaults(defineProps<{
24
28
  defineEmits<{
25
29
  'update:modelValue': [value: string]
26
30
  }>()
31
+
32
+ const attrs = useAttrs()
33
+ const baseTestId = computed<string | undefined>(() => props.testId ?? (attrs['data-testid'] as string | undefined))
27
34
  </script>
@@ -1,12 +1,12 @@
1
1
  <template>
2
2
  <Teleport to="body">
3
3
  <Transition name="fade">
4
- <div v-if="visible" class="dialog-overlay">
5
- <div class="fixed inset-0 bg-bg-overlay" @click="cancel" />
4
+ <div v-if="visible" class="dialog-overlay" :data-testid="testId">
5
+ <div class="fixed inset-0 bg-bg-overlay" :data-testid="testId ? `${testId}-mask` : undefined" @click="cancel" />
6
6
  <div class="panel-glass" :class="[width || 'max-w-lg', 'dialog']">
7
7
  <div class="flex items-center justify-between px-6 py-4 border-b border-border-primary">
8
8
  <h3 class="text-lg font-medium text-text-primary">{{ title }}</h3>
9
- <button class="text-text-tertiary hover:text-text-primary text-2xl leading-none" @click="cancel">&times;</button>
9
+ <button class="text-text-tertiary hover:text-text-primary text-2xl leading-none" :data-testid="testId ? `${testId}-close` : undefined" @click="cancel">&times;</button>
10
10
  </div>
11
11
  <div class="px-6 py-4">
12
12
  <slot />
@@ -14,6 +14,7 @@
14
14
  <div class="dialog-actions px-6 py-4 border-t border-border-primary bg-bg-secondary/50 rounded-b-2xl">
15
15
  <button
16
16
  class="btn-secondary"
17
+ :data-testid="testId ? `${testId}-cancel` : undefined"
17
18
  @click="cancel"
18
19
  >
19
20
  取消
@@ -21,6 +22,7 @@
21
22
  <button
22
23
  :disabled="loading"
23
24
  class="btn-primary"
25
+ :data-testid="testId ? `${testId}-submit` : 'submit-btn'"
24
26
  @click="confirm"
25
27
  >
26
28
  {{ loading ? '提交中...' : (confirmText || '确认') }}
@@ -39,6 +41,7 @@ defineProps<{
39
41
  loading?: boolean
40
42
  confirmText?: string
41
43
  width?: string
44
+ testId?: string
42
45
  }>()
43
46
 
44
47
  const emit = defineEmits<{
@@ -1,21 +1,22 @@
1
1
  <template>
2
2
  <Teleport to="body">
3
3
  <Transition name="fade">
4
- <div v-if="visible" class="dialog-overlay">
5
- <div class="fixed inset-0 bg-bg-overlay" @click="onOverlayClick" />
4
+ <div v-if="visible" class="dialog-overlay" :data-testid="testId">
5
+ <div class="fixed inset-0 bg-bg-overlay" :data-testid="testId ? `${testId}-mask` : undefined" @click="onOverlayClick" />
6
6
  <div class="panel-glass dialog" :class="[sizeClass, customClass]">
7
7
  <div v-if="title" class="flex items-center justify-between px-6 py-4 border-b border-border-primary">
8
8
  <h3 class="text-lg font-medium text-text-primary">{{ title }}</h3>
9
9
  <button
10
10
  v-if="closable"
11
11
  class="text-text-tertiary hover:text-text-primary text-2xl leading-none"
12
+ :data-testid="testId ? `${testId}-close` : undefined"
12
13
  @click="close"
13
14
  >&times;</button>
14
15
  </div>
15
16
  <div class="px-6 py-4">
16
17
  <slot />
17
18
  </div>
18
- <div v-if="$slots.footer" class="px-6 py-4 border-t border-border-primary bg-bg-secondary/50 rounded-b-2xl">
19
+ <div v-if="$slots.footer" class="px-6 py-4 border-t border-border-primary bg-bg-secondary/50 rounded-b-2xl" :data-testid="testId ? `${testId}-footer` : undefined">
19
20
  <slot name="footer" />
20
21
  </div>
21
22
  </div>
@@ -34,6 +35,7 @@ const props = withDefaults(defineProps<{
34
35
  closable?: boolean
35
36
  maskClosable?: boolean
36
37
  customClass?: string
38
+ testId?: string
37
39
  }>(), {
38
40
  closable: true,
39
41
  maskClosable: true,
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="flex items-center justify-between px-6 py-2">
2
+ <div class="flex items-center justify-between px-6 py-2" :data-testid="baseTestId">
3
3
  <div class="text-xs text-text-tertiary">
4
4
  共 {{ total }} 条,第 {{ startItem }} - {{ endItem }} 条
5
5
  </div>
@@ -7,6 +7,7 @@
7
7
  <button
8
8
  :disabled="page <= 1"
9
9
  class="btn-glass btn-sm disabled:opacity-50"
10
+ :data-testid="baseTestId ? `${baseTestId}-prev` : undefined"
10
11
  @click="changePage(page - 1)"
11
12
  >
12
13
  ← 上一页
@@ -16,6 +17,7 @@
16
17
  <button
17
18
  v-else
18
19
  :class="p === page ? 'pagination-btn-active btn-sm' : 'btn-glass btn-sm'"
20
+ :data-testid="baseTestId ? `${baseTestId}-page-${p}` : undefined"
19
21
  @click="changePage(p as number)"
20
22
  >
21
23
  {{ p }}
@@ -24,6 +26,7 @@
24
26
  <button
25
27
  :disabled="page >= totalPages"
26
28
  class="btn-glass btn-sm disabled:opacity-50"
29
+ :data-testid="baseTestId ? `${baseTestId}-next` : undefined"
27
30
  @click="changePage(page + 1)"
28
31
  >
29
32
  下一页 →
@@ -31,6 +34,7 @@
31
34
  <select
32
35
  :value="pageSize"
33
36
  class="select ml-2"
37
+ :data-testid="baseTestId ? `${baseTestId}-size-select` : undefined"
34
38
  @change="changePageSize(Number(($event.target as HTMLSelectElement).value))"
35
39
  >
36
40
  <option :value="10">10条/页</option>
@@ -43,14 +47,18 @@
43
47
 
44
48
  <script setup lang="ts">
45
49
  // @ts-nocheck
46
- import { computed } from 'vue'
50
+ import { computed, useAttrs } from 'vue'
47
51
 
48
52
  const props = defineProps<{
49
53
  page: number
50
54
  pageSize: number
51
55
  total: number
56
+ testId?: string
52
57
  }>()
53
58
 
59
+ const attrs = useAttrs()
60
+ const baseTestId = computed<string | undefined>(() => props.testId ?? (attrs['data-testid'] as string | undefined))
61
+
54
62
  const emit = defineEmits<{
55
63
  'update:page': [page: number]
56
64
  'update:pageSize': [size: number]
@@ -1,26 +1,34 @@
1
1
  <template>
2
- <div class="relative">
2
+ <div class="relative" :data-testid="baseTestId">
3
3
  <input
4
4
  :value="modelValue"
5
5
  type="text"
6
6
  :placeholder="placeholder"
7
7
  class="search-input w-full"
8
+ :data-testid="baseTestId ? `${baseTestId}-input` : undefined"
8
9
  @input="onInput"
9
10
  />
10
11
  <button
11
12
  v-if="modelValue"
12
13
  class="absolute right-3 top-1/2 -translate-y-1/2 text-text-tertiary hover:text-text-primary"
14
+ :data-testid="baseTestId ? `${baseTestId}-clear` : undefined"
13
15
  @click="clear"
14
16
  >&times;</button>
15
17
  </div>
16
18
  </template>
17
19
 
18
20
  <script setup lang="ts">
19
- defineProps<{
21
+ import { computed, useAttrs } from 'vue'
22
+
23
+ const props = defineProps<{
20
24
  modelValue: string
21
25
  placeholder?: string
26
+ testId?: string
22
27
  }>()
23
28
 
29
+ const attrs = useAttrs()
30
+ const baseTestId = computed<string | undefined>(() => props.testId ?? (attrs['data-testid'] as string | undefined))
31
+
24
32
  const emit = defineEmits<{
25
33
  'update:modelValue': [value: string]
26
34
  search: [value: string]
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div ref="rootRef" class="relative w-full">
2
+ <div ref="rootRef" class="relative w-full" :data-testid="baseTestId">
3
3
  <div
4
4
  class="input flex items-center w-full focus-within:border-accent-primary"
5
5
  :class="disabled ? 'opacity-60 cursor-not-allowed' : ''"
@@ -10,6 +10,7 @@
10
10
  :placeholder="displayLabel || placeholder"
11
11
  :disabled="disabled"
12
12
  class="flex-1 min-w-0 bg-transparent border-none outline-none text-input-text placeholder:text-input-placeholder"
13
+ :data-testid="baseTestId ? `${baseTestId}-input` : undefined"
13
14
  @focus="onFocus"
14
15
  @input="onInput"
15
16
  />
@@ -17,12 +18,14 @@
17
18
  v-if="clearable && modelValue"
18
19
  type="button"
19
20
  class="px-2 text-text-tertiary hover:text-text-primary"
21
+ :data-testid="baseTestId ? `${baseTestId}-clear` : undefined"
20
22
  @click.stop="clear"
21
23
  tabindex="-1"
22
24
  >✕</button>
23
25
  <button
24
26
  type="button"
25
27
  class="px-2 text-text-tertiary hover:text-text-primary"
28
+ :data-testid="baseTestId ? `${baseTestId}-toggle` : undefined"
26
29
  @click.stop="toggle"
27
30
  tabindex="-1"
28
31
  >▾</button>
@@ -38,6 +41,7 @@
38
41
  :key="opt.value"
39
42
  class="px-4 py-2 text-sm text-text-primary hover:bg-bg-hover cursor-pointer"
40
43
  :class="opt.value === modelValue ? 'bg-bg-secondary' : ''"
44
+ :data-testid="optionPrefix ? `${optionPrefix}-option-${opt.value}` : undefined"
41
45
  @mousedown.prevent="select(opt)"
42
46
  >
43
47
  {{ opt.label }}
@@ -47,7 +51,7 @@
47
51
  </template>
48
52
 
49
53
  <script setup lang="ts">
50
- import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
54
+ import { ref, computed, onMounted, onUnmounted, watch, useAttrs } from 'vue'
51
55
  import { match } from 'pinyin-pro'
52
56
  import type { SelectOption } from '../types'
53
57
 
@@ -57,12 +61,19 @@ const props = withDefaults(defineProps<{
57
61
  placeholder?: string
58
62
  clearable?: boolean
59
63
  disabled?: boolean
64
+ optionTestIdPrefix?: string
65
+ testId?: string
60
66
  }>(), {
61
67
  placeholder: '请选择',
62
68
  clearable: true,
63
69
  disabled: false,
70
+ optionTestIdPrefix: '',
64
71
  })
65
72
 
73
+ const attrs = useAttrs()
74
+ const baseTestId = computed<string | undefined>(() => props.testId ?? (attrs['data-testid'] as string | undefined))
75
+ const optionPrefix = computed<string | undefined>(() => props.optionTestIdPrefix || baseTestId.value)
76
+
66
77
  const emit = defineEmits<{
67
78
  'update:modelValue': [value: string]
68
79
  'change': [value: string]
@@ -1,10 +1,11 @@
1
1
  <template>
2
- <aside class="sidebar" style="width: 200px;">
2
+ <aside class="sidebar" style="width: 200px;" :data-testid="baseTestId">
3
3
  <nav class="mt-3">
4
4
  <router-link
5
5
  to="/"
6
6
  class="sidebar-item text-xs"
7
7
  exact-active-class="sidebar-item-active"
8
+ :data-testid="baseTestId ? `${baseTestId}-item-home` : undefined"
8
9
  >
9
10
  <span class="w-5 text-center text-sm">🏠</span>
10
11
  <span class="ml-2">首页</span>
@@ -18,6 +19,7 @@
18
19
  :to="item.path"
19
20
  class="sidebar-item text-xs"
20
21
  active-class="sidebar-item-active"
22
+ :data-testid="baseTestId ? `${baseTestId}-item-${item.path}` : undefined"
21
23
  >
22
24
  <span class="w-5 text-center text-sm">{{ item.icon }}</span>
23
25
  <span class="ml-2">{{ item.label }}</span>
@@ -28,9 +30,14 @@
28
30
  </template>
29
31
 
30
32
  <script setup lang="ts">
33
+ import { computed, useAttrs } from 'vue'
31
34
  import type { MenuGroup } from '../types'
32
35
 
33
- withDefaults(defineProps<{
36
+ const props = withDefaults(defineProps<{
34
37
  menuGroups: MenuGroup[]
38
+ testId?: string
35
39
  }>(), {})
40
+
41
+ const attrs = useAttrs()
42
+ const baseTestId = computed<string | undefined>(() => props.testId ?? (attrs['data-testid'] as string | undefined))
36
43
  </script>
@@ -1,19 +1,23 @@
1
1
  <template>
2
- <span class="badge" :class="cls">
2
+ <span class="badge" :class="cls" :data-testid="baseTestId">
3
3
  {{ label }}
4
4
  </span>
5
5
  </template>
6
6
 
7
7
  <script setup lang="ts">
8
- import { computed } from 'vue'
8
+ import { computed, useAttrs } from 'vue'
9
9
 
10
10
  const props = withDefaults(defineProps<{
11
11
  status: string | number
12
12
  statusMap?: Record<string | number, { label: string; cls: string }>
13
+ testId?: string
13
14
  }>(), {
14
15
  statusMap: () => ({})
15
16
  })
16
17
 
18
+ const attrs = useAttrs()
19
+ const baseTestId = computed<string | undefined>(() => props.testId ?? (attrs['data-testid'] as string | undefined))
20
+
17
21
  const defaultMap: Record<string, { label: string; cls: string }> = {
18
22
  active: { label: '正常', cls: 'badge-success' },
19
23
  locked: { label: '已锁定', cls: 'badge-danger' },
@@ -1,16 +1,17 @@
1
1
  <template>
2
2
  <Teleport to="body">
3
- <div class="fixed top-4 right-4 z-50 space-y-2">
3
+ <div class="fixed top-4 right-4 z-50 space-y-2" :data-testid="testId">
4
4
  <TransitionGroup name="toast">
5
5
  <div
6
6
  v-for="toast in toasts"
7
7
  :key="toast.id"
8
8
  class="flex items-center p-4 rounded-lg shadow-lg min-w-[300px] border-l-4"
9
9
  :class="typeClasses[toast.type]"
10
+ :data-testid="`${testId}-item-${toast.id}`"
10
11
  >
11
12
  <span class="mr-2">{{ iconMap[toast.type] }}</span>
12
13
  <span class="flex-1 text-text-primary">{{ toast.message }}</span>
13
- <button @click="remove(toast.id)" class="ml-2 text-text-tertiary hover:text-text-primary">&times;</button>
14
+ <button @click="remove(toast.id)" class="ml-2 text-text-tertiary hover:text-text-primary" :data-testid="`${testId}-close-${toast.id}`">&times;</button>
14
15
  </div>
15
16
  </TransitionGroup>
16
17
  </div>
@@ -21,6 +22,12 @@
21
22
  import { useToast } from '../composables/useToast'
22
23
  import type { ToastType } from '../types'
23
24
 
25
+ withDefaults(defineProps<{
26
+ testId?: string
27
+ }>(), {
28
+ testId: 'toast',
29
+ })
30
+
24
31
  const { toasts, remove } = useToast()
25
32
 
26
33
  const typeClasses: Record<ToastType, string> = {
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <header class="navbar">
2
+ <header class="navbar" :data-testid="testId || 'nav-bar'">
3
3
  <div class="flex items-center gap-3">
4
4
  <slot name="brand">
5
5
  <div class="w-8 h-8 rounded-lg bg-accent-primary flex items-center justify-center">
@@ -20,6 +20,7 @@
20
20
  :class="activeModule === mod.key
21
21
  ? 'font-medium border-accent-primary'
22
22
  : 'text-text-secondary hover:text-text-primary border-transparent'"
23
+ :data-testid="testId ? `${testId}-module-${mod.key}` : undefined"
23
24
  >
24
25
  {{ mod.label }}
25
26
  </router-link>
@@ -33,6 +34,7 @@
33
34
  @click="showThemeDropdown = !showThemeDropdown"
34
35
  :title="`当前主题: ${themeStore.currentThemeInfo?.label}`"
35
36
  class="btn-icon text-text-secondary hover:text-text-primary"
37
+ :data-testid="testId ? `${testId}-theme-toggle` : undefined"
36
38
  >
37
39
  {{ themeIcon }}
38
40
  </button>
@@ -44,6 +46,7 @@
44
46
  :key="t.mode"
45
47
  class="w-full px-4 py-2 text-left text-sm flex items-center gap-2 hover:bg-bg-tertiary"
46
48
  :class="themeStore.theme === t.mode ? 'text-accent-primary' : 'text-text-secondary'"
49
+ :data-testid="testId ? `${testId}-theme-${t.mode}` : undefined"
47
50
  @click="selectTheme(t.mode)"
48
51
  >
49
52
  <span>{{ t.icon }}</span>
@@ -57,6 +60,7 @@
57
60
  <div class="relative" ref="userDropdownRef">
58
61
  <button
59
62
  class="flex items-center gap-2 p-1 rounded-lg hover:bg-bg-tertiary transition-colors"
63
+ :data-testid="testId ? `${testId}-avatar` : 'avatar-button'"
60
64
  @click="showUserDropdown = !showUserDropdown"
61
65
  >
62
66
  <div class="navbar-avatar">{{ avatarInitial }}</div>
@@ -80,6 +84,7 @@
80
84
  v-else
81
85
  class="w-full px-4 py-2 text-left text-sm flex items-center gap-2 hover:bg-bg-tertiary"
82
86
  :class="item.danger ? 'text-accent-danger' : 'text-text-secondary'"
87
+ :data-testid="item.danger ? (testId ? `${testId}-logout` : 'logout-btn') : (testId ? `${testId}-menu-${idx}` : undefined)"
83
88
  @click="handleItemClick(item)"
84
89
  >
85
90
  <span v-if="item.icon">{{ item.icon }}</span>
@@ -105,6 +110,7 @@ const props = withDefaults(defineProps<{
105
110
  modules?: NavModule[]
106
111
  activeModule?: string
107
112
  userDropdownItems?: DropdownItem[]
113
+ testId?: string
108
114
  }>(), {
109
115
  title: '医疗设备智能平台',
110
116
  avatarText: 'U',
@@ -1,10 +1,11 @@
1
1
  <template>
2
- <div class="user-picker">
2
+ <div class="user-picker" :data-testid="baseTestId">
3
3
  <div class="mb-3">
4
4
  <input
5
5
  class="input w-full"
6
6
  v-model="keyword"
7
7
  placeholder="搜索用户..."
8
+ :data-testid="baseTestId ? `${baseTestId}-search` : undefined"
8
9
  @input="handleSearch"
9
10
  />
10
11
  </div>
@@ -20,6 +21,7 @@
20
21
  :key="user.id"
21
22
  class="user-item p-3 rounded-lg border-2 cursor-pointer transition-all"
22
23
  :class="selectedUser?.id === user.id ? 'border-accent-primary bg-accent-primary/10' : 'border-bg-tertiary hover:border-accent-primary/50'"
24
+ :data-testid="baseTestId ? `${baseTestId}-item-${user.id}` : undefined"
23
25
  @click="selectUser(user)"
24
26
  >
25
27
  <div class="flex items-center justify-between">
@@ -44,7 +46,7 @@
44
46
  </template>
45
47
 
46
48
  <script setup lang="ts">
47
- import { ref, onMounted } from 'vue'
49
+ import { ref, onMounted, computed, useAttrs } from 'vue'
48
50
 
49
51
  interface UserBasicInfo {
50
52
  id: string
@@ -56,8 +58,12 @@ interface UserBasicInfo {
56
58
 
57
59
  const props = defineProps<{
58
60
  fetchUsers: (keyword?: string) => Promise<UserBasicInfo[]>
61
+ testId?: string
59
62
  }>()
60
63
 
64
+ const attrs = useAttrs()
65
+ const baseTestId = computed<string | undefined>(() => props.testId ?? (attrs['data-testid'] as string | undefined))
66
+
61
67
  const emit = defineEmits<{
62
68
  select: [user: UserBasicInfo]
63
69
  }>()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aquiferre/ui-kit",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "index.ts",