@aquiferre/ui-kit 0.1.9 → 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.
- package/components/Breadcrumb.vue +7 -2
- package/components/ConfirmDialog.vue +5 -4
- package/components/DataTable.vue +13 -4
- package/components/FileUploader.vue +9 -4
- package/components/FilterDropdown.vue +9 -2
- package/components/FormDialog.vue +6 -3
- package/components/Modal.vue +5 -3
- package/components/Pagination.vue +10 -2
- package/components/SearchInput.vue +10 -2
- package/components/SearchSelect.vue +13 -2
- package/components/SideMenu.vue +9 -2
- package/components/StatusTag.vue +6 -2
- package/components/Toast.vue +9 -2
- package/components/TopNavbar.vue +7 -1
- package/components/UserPicker.vue +8 -2
- package/package.json +1 -1
|
@@ -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: '确认删除',
|
package/components/DataTable.vue
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="table-container" :class="{ relative: loading }">
|
|
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">
|
|
@@ -35,11 +36,12 @@
|
|
|
35
36
|
<tr
|
|
36
37
|
v-for="(row, index) in data"
|
|
37
38
|
:key="row[rowKey] ?? index"
|
|
39
|
+
:data-testid="rowPrefix ? `${rowPrefix}-${row[rowKey] ?? index}` : undefined"
|
|
38
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
|
-
|
|
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">×</button>
|
|
9
|
+
<button class="text-text-tertiary hover:text-text-primary text-2xl leading-none" :data-testid="testId ? `${testId}-close` : undefined" @click="cancel">×</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<{
|
package/components/Modal.vue
CHANGED
|
@@ -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
|
>×</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
|
>×</button>
|
|
15
17
|
</div>
|
|
16
18
|
</template>
|
|
17
19
|
|
|
18
20
|
<script setup lang="ts">
|
|
19
|
-
|
|
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]
|
package/components/SideMenu.vue
CHANGED
|
@@ -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>
|
package/components/StatusTag.vue
CHANGED
|
@@ -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' },
|
package/components/Toast.vue
CHANGED
|
@@ -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">×</button>
|
|
14
|
+
<button @click="remove(toast.id)" class="ml-2 text-text-tertiary hover:text-text-primary" :data-testid="`${testId}-close-${toast.id}`">×</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> = {
|
package/components/TopNavbar.vue
CHANGED
|
@@ -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
|
}>()
|