@aquiferre/ui-kit 0.4.5 → 0.5.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/ConfirmDialog.vue +11 -6
- package/components/DataTable.vue +8 -3
- package/components/DateTimePicker.vue +12 -8
- package/components/FileUploader.vue +7 -4
- package/components/FilterDropdown.vue +6 -1
- package/components/FormDialog.vue +11 -3
- package/components/Pagination.vue +16 -4
- package/components/SearchSelect.vue +8 -3
- package/components/SideMenu.vue +9 -3
- package/components/StatusTag.vue +20 -10
- package/components/SuggestInput.vue +15 -8
- package/components/TopNavbar.vue +63 -13
- package/components/Tree.vue +9 -3
- package/components/UserPicker.vue +8 -3
- package/index.ts +1 -0
- package/messages/en-US.ts +84 -0
- package/messages/index.ts +14 -0
- package/messages/zh-CN.ts +84 -0
- package/package.json +6 -4
|
@@ -5,7 +5,7 @@
|
|
|
5
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
|
-
<h3 class="dialog-title">{{
|
|
8
|
+
<h3 class="dialog-title">{{ titleText }}</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">
|
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
</template>
|
|
24
24
|
|
|
25
25
|
<script setup lang="ts">
|
|
26
|
-
|
|
26
|
+
import { computed } from 'vue'
|
|
27
|
+
import { useI18n } from 'vue-i18n'
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(defineProps<{
|
|
27
30
|
visible: boolean
|
|
28
31
|
title?: string
|
|
29
32
|
content?: string
|
|
@@ -35,14 +38,16 @@ withDefaults(defineProps<{
|
|
|
35
38
|
confirmClass?: string
|
|
36
39
|
testId?: string
|
|
37
40
|
}>(), {
|
|
38
|
-
title: '确认删除',
|
|
39
|
-
confirmText: '确认删除',
|
|
40
|
-
cancelText: '取消',
|
|
41
41
|
showCancel: true,
|
|
42
|
-
loadingText: '删除中...',
|
|
43
42
|
confirmClass: 'btn-danger',
|
|
44
43
|
})
|
|
45
44
|
|
|
45
|
+
const { t } = useI18n()
|
|
46
|
+
const titleText = computed(() => props.title ?? t('ui.confirmDialog.title'))
|
|
47
|
+
const confirmText = computed(() => props.confirmText ?? t('ui.confirmDialog.confirmText'))
|
|
48
|
+
const cancelText = computed(() => props.cancelText ?? t('ui.common.cancel'))
|
|
49
|
+
const loadingText = computed(() => props.loadingText ?? t('ui.confirmDialog.loadingText'))
|
|
50
|
+
|
|
46
51
|
const emit = defineEmits<{
|
|
47
52
|
confirm: []
|
|
48
53
|
cancel: []
|
package/components/DataTable.vue
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<tbody class="table-body">
|
|
31
31
|
<tr v-if="!loading && data.length === 0" class="table-row">
|
|
32
32
|
<td :colspan="colspan" class="table-empty">
|
|
33
|
-
{{
|
|
33
|
+
{{ emptyTextText }}
|
|
34
34
|
</td>
|
|
35
35
|
</tr>
|
|
36
36
|
<tr
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
<div v-if="loading" class="absolute inset-0 flex items-center justify-center bg-bg-elevated/60 backdrop-blur-[1px] z-10">
|
|
62
62
|
<div class="flex items-center space-x-2 text-text-secondary">
|
|
63
63
|
<div class="spinner"></div>
|
|
64
|
-
<span class="text-sm"
|
|
64
|
+
<span class="text-sm">{{ loadingText }}</span>
|
|
65
65
|
</div>
|
|
66
66
|
</div>
|
|
67
67
|
</div>
|
|
@@ -69,8 +69,11 @@
|
|
|
69
69
|
|
|
70
70
|
<script setup lang="ts">
|
|
71
71
|
import { computed, useAttrs } from 'vue'
|
|
72
|
+
import { useI18n } from 'vue-i18n'
|
|
72
73
|
import type { Column } from '../types'
|
|
73
74
|
|
|
75
|
+
const { t } = useI18n()
|
|
76
|
+
|
|
74
77
|
const props = withDefaults(defineProps<{
|
|
75
78
|
columns: Column[]
|
|
76
79
|
data: any[]
|
|
@@ -85,7 +88,6 @@ const props = withDefaults(defineProps<{
|
|
|
85
88
|
scrollable?: boolean
|
|
86
89
|
}>(), {
|
|
87
90
|
loading: false,
|
|
88
|
-
emptyText: '暂无数据',
|
|
89
91
|
selectable: false,
|
|
90
92
|
selected: () => [],
|
|
91
93
|
rowKey: 'id',
|
|
@@ -94,6 +96,9 @@ const props = withDefaults(defineProps<{
|
|
|
94
96
|
scrollable: false,
|
|
95
97
|
})
|
|
96
98
|
|
|
99
|
+
const loadingText = computed(() => t('ui.dataTable.loading'))
|
|
100
|
+
const emptyTextText = computed(() => props.emptyText ?? t('ui.dataTable.empty'))
|
|
101
|
+
|
|
97
102
|
const attrs = useAttrs()
|
|
98
103
|
const baseTestId = computed<string | undefined>(() => props.testId ?? (attrs['data-testid'] as string | undefined))
|
|
99
104
|
const rowPrefix = computed<string | undefined>(() => props.rowTestIdPrefix ?? baseTestId.value)
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<div class="flex items-center justify-between mb-2">
|
|
38
38
|
<div class="relative">
|
|
39
39
|
<button type="button" class="btn-sm btn-ghost text-xs font-semibold" @click="yearDropdownOpen = !yearDropdownOpen">
|
|
40
|
-
{{ viewYear }}
|
|
40
|
+
{{ t('ui.dateTimePicker.year', { year: viewYear }) }} ▾
|
|
41
41
|
</button>
|
|
42
42
|
<div
|
|
43
43
|
v-if="yearDropdownOpen"
|
|
@@ -61,12 +61,12 @@
|
|
|
61
61
|
</div>
|
|
62
62
|
<div class="flex items-center gap-1">
|
|
63
63
|
<button type="button" class="btn-icon text-text-secondary hover:text-text-primary" @click="prevMonth">◀</button>
|
|
64
|
-
<span class="text-xs font-semibold text-text-primary min-w-[3rem] text-center">{{ viewMonth }}
|
|
64
|
+
<span class="text-xs font-semibold text-text-primary min-w-[3rem] text-center">{{ t('ui.dateTimePicker.month', { month: viewMonth }) }}</span>
|
|
65
65
|
<button type="button" class="btn-icon text-text-secondary hover:text-text-primary" @click="nextMonth">▶</button>
|
|
66
66
|
</div>
|
|
67
67
|
</div>
|
|
68
68
|
<div class="grid grid-cols-7 gap-0.5 mb-1">
|
|
69
|
-
<div v-for="d in
|
|
69
|
+
<div v-for="d in weekdayKeys" :key="d" class="text-center text-xs text-text-tertiary py-0.5 font-medium">{{ t('ui.dateTimePicker.week.' + d) }}</div>
|
|
70
70
|
</div>
|
|
71
71
|
<div class="grid grid-cols-7 gap-0.5">
|
|
72
72
|
<button
|
|
@@ -98,20 +98,20 @@
|
|
|
98
98
|
</div>
|
|
99
99
|
<div class="w-px bg-border-primary" />
|
|
100
100
|
<div>
|
|
101
|
-
<div class="mb-1.5 px-1"><span class="text-xs text-text-secondary"
|
|
101
|
+
<div class="mb-1.5 px-1"><span class="text-xs text-text-secondary">{{ t('ui.dateTimePicker.monthSection') }}</span></div>
|
|
102
102
|
<div class="grid grid-cols-2 gap-1">
|
|
103
103
|
<button
|
|
104
104
|
v-for="m in 12" :key="m" type="button"
|
|
105
105
|
class="px-2 py-1 text-xs rounded-md transition-colors min-w-[2.5rem]"
|
|
106
106
|
:class="m === viewMonth ? 'bg-accent-primary text-white' : 'bg-bg-secondary text-text-primary hover:bg-bg-hover'"
|
|
107
107
|
@click="selectMonth(m)"
|
|
108
|
-
>{{ m }}
|
|
108
|
+
>{{ t('ui.dateTimePicker.month', { month: m }) }}</button>
|
|
109
109
|
</div>
|
|
110
110
|
</div>
|
|
111
111
|
<div class="w-px bg-border-primary" />
|
|
112
112
|
<div>
|
|
113
113
|
<div class="grid grid-cols-7 gap-0.5 mb-1">
|
|
114
|
-
<div v-for="d in
|
|
114
|
+
<div v-for="d in weekdayKeys" :key="d" class="text-center text-xs text-text-tertiary py-0.5 font-medium">{{ t('ui.dateTimePicker.week.' + d) }}</div>
|
|
115
115
|
</div>
|
|
116
116
|
<div class="grid grid-cols-7 gap-0.5">
|
|
117
117
|
<button
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
|
|
146
146
|
<!-- Confirm button -->
|
|
147
147
|
<div v-if="mode !== 'date'" class="flex justify-end mt-3 pt-2 border-t border-border-primary">
|
|
148
|
-
<button type="button" class="btn-sm btn-primary" @click="confirm"
|
|
148
|
+
<button type="button" class="btn-sm btn-primary" @click="confirm">{{ t('ui.common.confirm') }}</button>
|
|
149
149
|
</div>
|
|
150
150
|
</div>
|
|
151
151
|
</Teleport>
|
|
@@ -154,6 +154,7 @@
|
|
|
154
154
|
|
|
155
155
|
<script setup lang="ts">
|
|
156
156
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick, useAttrs } from 'vue'
|
|
157
|
+
import { useI18n } from 'vue-i18n'
|
|
157
158
|
import type { DateTimePreset } from '../types'
|
|
158
159
|
|
|
159
160
|
const props = withDefaults(defineProps<{
|
|
@@ -180,6 +181,8 @@ const props = withDefaults(defineProps<{
|
|
|
180
181
|
clearable: true,
|
|
181
182
|
})
|
|
182
183
|
|
|
184
|
+
const { t } = useI18n()
|
|
185
|
+
|
|
183
186
|
const emit = defineEmits<{
|
|
184
187
|
'update:modelValue': [value: string | [string, string]]
|
|
185
188
|
'change': [value: string | [string, string]]
|
|
@@ -210,7 +213,8 @@ const minutes = ref(0)
|
|
|
210
213
|
const seconds = ref(0)
|
|
211
214
|
const editingTime = ref<'h' | 'm' | 's' | null>(null)
|
|
212
215
|
|
|
213
|
-
|
|
216
|
+
// 星期表头 key 数组(周日~周六),文案在模板中按词典渲染
|
|
217
|
+
const weekdayKeys = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
|
|
214
218
|
|
|
215
219
|
const decadeYears = computed(() => Array.from({ length: 10 }, (_, i) => decadeStart.value + i))
|
|
216
220
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
/>
|
|
12
12
|
|
|
13
13
|
<button @click="triggerFileInput" class="btn-primary btn-sm inline-flex items-center gap-1" :data-testid="baseTestId ? `${baseTestId}-trigger` : undefined">
|
|
14
|
-
📎
|
|
14
|
+
📎 {{ t('ui.fileUploader.select') }}
|
|
15
15
|
</button>
|
|
16
16
|
|
|
17
17
|
<div v-if="uploadQueue.length > 0" class="mt-3 space-y-2">
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
|
|
32
32
|
<div class="text-xs mt-1">
|
|
33
33
|
<span v-if="item.status === 'uploading'" class="text-accent-primary">
|
|
34
|
-
|
|
34
|
+
{{ t('ui.fileUploader.uploading', { progress: item.progress.toFixed(2) }) }}
|
|
35
35
|
</span>
|
|
36
|
-
<span v-else-if="item.status === 'success'" class="text-accent-success">✓
|
|
36
|
+
<span v-else-if="item.status === 'success'" class="text-accent-success">✓ {{ t('ui.fileUploader.done') }}</span>
|
|
37
37
|
<span v-else-if="item.status === 'error'" class="text-accent-danger">✗ {{ item.error }}</span>
|
|
38
38
|
</div>
|
|
39
39
|
</div>
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
|
|
44
44
|
<script setup lang="ts">
|
|
45
45
|
import { ref, reactive, computed, useAttrs } from 'vue'
|
|
46
|
+
import { useI18n } from 'vue-i18n'
|
|
46
47
|
|
|
47
48
|
interface UploadItem {
|
|
48
49
|
id: string
|
|
@@ -61,6 +62,8 @@ const props = withDefaults(defineProps<{
|
|
|
61
62
|
acceptTypes: 'image/*,video/*'
|
|
62
63
|
})
|
|
63
64
|
|
|
65
|
+
const { t } = useI18n()
|
|
66
|
+
|
|
64
67
|
const attrs = useAttrs()
|
|
65
68
|
const baseTestId = computed<string | undefined>(() => props.testId ?? (attrs['data-testid'] as string | undefined))
|
|
66
69
|
|
|
@@ -113,7 +116,7 @@ async function handleFileSelect(event: Event) {
|
|
|
113
116
|
}, 1500)
|
|
114
117
|
} catch (error: any) {
|
|
115
118
|
item.status = 'error'
|
|
116
|
-
item.error = error.message || '
|
|
119
|
+
item.error = error.message || t('ui.fileUploader.failed')
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
122
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
:data-testid="baseTestId"
|
|
6
6
|
@change="$emit('update:modelValue', ($event.target as HTMLSelectElement).value)"
|
|
7
7
|
>
|
|
8
|
-
<option v-if="showAll" value="">{{
|
|
8
|
+
<option v-if="showAll" value="">{{ allText }}</option>
|
|
9
9
|
<option v-for="opt in options" :key="opt.value" :value="opt.value" :data-testid="baseTestId ? `${baseTestId}-option-${opt.value}` : undefined">
|
|
10
10
|
{{ opt.label }}
|
|
11
11
|
</option>
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
<script setup lang="ts">
|
|
16
16
|
import { computed, useAttrs } from 'vue'
|
|
17
|
+
import { useI18n } from 'vue-i18n'
|
|
17
18
|
|
|
18
19
|
const props = withDefaults(defineProps<{
|
|
19
20
|
modelValue: string
|
|
@@ -25,6 +26,10 @@ const props = withDefaults(defineProps<{
|
|
|
25
26
|
showAll: true
|
|
26
27
|
})
|
|
27
28
|
|
|
29
|
+
const { t } = useI18n()
|
|
30
|
+
// placeholder 显式传入时优先(既有调用约定),缺省走词典
|
|
31
|
+
const allText = computed(() => props.placeholder ?? t('ui.filterDropdown.all'))
|
|
32
|
+
|
|
28
33
|
defineEmits<{
|
|
29
34
|
'update:modelValue': [value: string]
|
|
30
35
|
}>()
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
:data-testid="testId ? `${testId}-cancel` : undefined"
|
|
18
18
|
@click="cancel"
|
|
19
19
|
>
|
|
20
|
-
|
|
20
|
+
{{ cancelText }}
|
|
21
21
|
</button>
|
|
22
22
|
<button
|
|
23
23
|
:disabled="loading"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
:data-testid="testId ? `${testId}-submit` : 'submit-btn'"
|
|
26
26
|
@click="confirm"
|
|
27
27
|
>
|
|
28
|
-
{{ loading ?
|
|
28
|
+
{{ loading ? submittingText : confirmTextText }}
|
|
29
29
|
</button>
|
|
30
30
|
</div>
|
|
31
31
|
</div>
|
|
@@ -35,7 +35,10 @@
|
|
|
35
35
|
</template>
|
|
36
36
|
|
|
37
37
|
<script setup lang="ts">
|
|
38
|
-
|
|
38
|
+
import { computed } from 'vue'
|
|
39
|
+
import { useI18n } from 'vue-i18n'
|
|
40
|
+
|
|
41
|
+
const props = defineProps<{
|
|
39
42
|
visible: boolean
|
|
40
43
|
title: string
|
|
41
44
|
loading?: boolean
|
|
@@ -44,6 +47,11 @@ defineProps<{
|
|
|
44
47
|
testId?: string
|
|
45
48
|
}>()
|
|
46
49
|
|
|
50
|
+
const { t } = useI18n()
|
|
51
|
+
const cancelText = computed(() => t('ui.common.cancel'))
|
|
52
|
+
const submittingText = computed(() => t('ui.common.submitting'))
|
|
53
|
+
const confirmTextText = computed(() => props.confirmText ?? t('ui.formDialog.confirmText'))
|
|
54
|
+
|
|
47
55
|
const emit = defineEmits<{
|
|
48
56
|
confirm: []
|
|
49
57
|
cancel: []
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
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
|
+
{{ totalText }}
|
|
5
5
|
</div>
|
|
6
6
|
<div class="flex items-center space-x-1">
|
|
7
7
|
<button
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
:data-testid="baseTestId ? `${baseTestId}-prev` : undefined"
|
|
11
11
|
@click="changePage(page - 1)"
|
|
12
12
|
>
|
|
13
|
-
←
|
|
13
|
+
← {{ prevText }}
|
|
14
14
|
</button>
|
|
15
15
|
<template v-for="p in visiblePages" :key="p">
|
|
16
16
|
<span v-if="p === '...'" class="btn-glass btn-sm pointer-events-none">…</span>
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
:data-testid="baseTestId ? `${baseTestId}-next` : undefined"
|
|
30
30
|
@click="changePage(page + 1)"
|
|
31
31
|
>
|
|
32
|
-
|
|
32
|
+
{{ nextText }} →
|
|
33
33
|
</button>
|
|
34
34
|
<select
|
|
35
35
|
:value="selectedPageSize"
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
:data-testid="baseTestId ? `${baseTestId}-size-select` : undefined"
|
|
38
38
|
@change="changePageSize(Number(($event.target as HTMLSelectElement).value))"
|
|
39
39
|
>
|
|
40
|
-
<option v-for="size in sizeOptions" :key="size" :value="size">{{ size }}
|
|
40
|
+
<option v-for="size in sizeOptions" :key="size" :value="size">{{ pageSizeText(size) }}</option>
|
|
41
41
|
</select>
|
|
42
42
|
</div>
|
|
43
43
|
</div>
|
|
@@ -46,10 +46,13 @@
|
|
|
46
46
|
<script setup lang="ts">
|
|
47
47
|
// @ts-nocheck
|
|
48
48
|
import { computed, useAttrs } from 'vue'
|
|
49
|
+
import { useI18n } from 'vue-i18n'
|
|
49
50
|
|
|
50
51
|
// 默认页容量选项(自定义值通过 pageSizeOptions 排在其前)
|
|
51
52
|
const DEFAULT_PAGE_SIZES = [20, 50, 100, 200, 500, 1000, 5000]
|
|
52
53
|
|
|
54
|
+
const { t } = useI18n()
|
|
55
|
+
|
|
53
56
|
const props = withDefaults(defineProps<{
|
|
54
57
|
page: number
|
|
55
58
|
pageSize: number
|
|
@@ -87,6 +90,15 @@ const totalPages = computed(() => Math.max(1, Math.ceil(props.total / props.page
|
|
|
87
90
|
const startItem = computed(() => (props.page - 1) * props.pageSize + 1)
|
|
88
91
|
const endItem = computed(() => Math.min(props.page * props.pageSize, props.total))
|
|
89
92
|
|
|
93
|
+
const totalText = computed(() =>
|
|
94
|
+
t('ui.pagination.total', { total: props.total, startItem: startItem.value, endItem: endItem.value })
|
|
95
|
+
)
|
|
96
|
+
const prevText = computed(() => t('ui.pagination.prev'))
|
|
97
|
+
const nextText = computed(() => t('ui.pagination.next'))
|
|
98
|
+
function pageSizeText(size: number) {
|
|
99
|
+
return t('ui.pagination.pageSize', { size })
|
|
100
|
+
}
|
|
101
|
+
|
|
90
102
|
/**
|
|
91
103
|
* 生成可见页码数组,当前页 ±2 范围内显示,其余用 '...' 省略
|
|
92
104
|
* 始终显示首页和末页
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<input
|
|
8
8
|
ref="inputRef"
|
|
9
9
|
:value="inputValue"
|
|
10
|
-
:placeholder="displayLabel ||
|
|
10
|
+
:placeholder="displayLabel || placeholderText"
|
|
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
13
|
:data-testid="baseTestId ? `${baseTestId}-input` : undefined"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
v-if="open"
|
|
36
36
|
class="absolute top-full left-0 mt-1 w-full max-h-60 overflow-auto card shadow-lg z-20 py-1"
|
|
37
37
|
>
|
|
38
|
-
<div v-if="filtered.length === 0" class="px-4 py-2 text-sm text-text-tertiary"
|
|
38
|
+
<div v-if="filtered.length === 0" class="px-4 py-2 text-sm text-text-tertiary">{{ noMatchText }}</div>
|
|
39
39
|
<div
|
|
40
40
|
v-for="opt in filtered"
|
|
41
41
|
:key="opt.value"
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
|
|
53
53
|
<script setup lang="ts">
|
|
54
54
|
import { ref, computed, onMounted, onUnmounted, watch, useAttrs } from 'vue'
|
|
55
|
+
import { useI18n } from 'vue-i18n'
|
|
55
56
|
import { match } from 'pinyin-pro'
|
|
56
57
|
import type { SelectOption } from '../types'
|
|
57
58
|
|
|
@@ -64,12 +65,16 @@ const props = withDefaults(defineProps<{
|
|
|
64
65
|
optionTestIdPrefix?: string
|
|
65
66
|
testId?: string
|
|
66
67
|
}>(), {
|
|
67
|
-
placeholder: '请选择',
|
|
68
68
|
clearable: true,
|
|
69
69
|
disabled: false,
|
|
70
70
|
optionTestIdPrefix: '',
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
+
const { t } = useI18n()
|
|
74
|
+
// 占位/空态兜底文案:显式传入优先,缺省走词典
|
|
75
|
+
const placeholderText = computed(() => props.placeholder ?? t('ui.searchSelect.placeholder'))
|
|
76
|
+
const noMatchText = computed(() => t('ui.common.noMatch'))
|
|
77
|
+
|
|
73
78
|
const attrs = useAttrs()
|
|
74
79
|
const baseTestId = computed<string | undefined>(() => props.testId ?? (attrs['data-testid'] as string | undefined))
|
|
75
80
|
const optionPrefix = computed<string | undefined>(() => props.optionTestIdPrefix || baseTestId.value)
|
package/components/SideMenu.vue
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<button
|
|
5
5
|
v-if="collapsible"
|
|
6
6
|
class="sidebar-collapse-btn text-xs w-5 h-5 flex items-center justify-center rounded hover:bg-white/10 transition-colors"
|
|
7
|
-
:title="collapsed ?
|
|
7
|
+
:title="collapsed ? expandText : collapseText"
|
|
8
8
|
@click="collapsed = !collapsed"
|
|
9
9
|
>{{ collapsed ? '▶' : '◀' }}</button>
|
|
10
10
|
</div>
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
class="sidebar-item text-xs"
|
|
16
16
|
exact-active-class="sidebar-item-active"
|
|
17
17
|
:data-testid="baseTestId ? `${baseTestId}-item-home` : undefined"
|
|
18
|
-
:title="collapsed ?
|
|
18
|
+
:title="collapsed ? homeText : undefined"
|
|
19
19
|
>
|
|
20
20
|
<span class="w-5 text-center text-sm">🏠</span>
|
|
21
|
-
<span v-if="!collapsed" class="ml-2"
|
|
21
|
+
<span v-if="!collapsed" class="ml-2">{{ homeText }}</span>
|
|
22
22
|
</router-link>
|
|
23
23
|
|
|
24
24
|
<template v-for="group in menuGroups" :key="group.key">
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
|
|
43
43
|
<script setup lang="ts">
|
|
44
44
|
import { computed, ref, useAttrs } from 'vue'
|
|
45
|
+
import { useI18n } from 'vue-i18n'
|
|
45
46
|
import type { MenuGroup } from '../types'
|
|
46
47
|
|
|
47
48
|
const props = withDefaults(defineProps<{
|
|
@@ -51,6 +52,11 @@ const props = withDefaults(defineProps<{
|
|
|
51
52
|
showHome?: boolean
|
|
52
53
|
}>(), { collapsible: false, showHome: true })
|
|
53
54
|
|
|
55
|
+
const { t } = useI18n()
|
|
56
|
+
const expandText = computed(() => t('ui.sideMenu.expand'))
|
|
57
|
+
const collapseText = computed(() => t('ui.sideMenu.collapse'))
|
|
58
|
+
const homeText = computed(() => t('ui.sideMenu.home'))
|
|
59
|
+
|
|
54
60
|
const collapsed = ref(false)
|
|
55
61
|
|
|
56
62
|
const attrs = useAttrs()
|
package/components/StatusTag.vue
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<script setup lang="ts">
|
|
8
8
|
import { computed, useAttrs } from 'vue'
|
|
9
|
+
import { useI18n } from 'vue-i18n'
|
|
9
10
|
|
|
10
11
|
const props = withDefaults(defineProps<{
|
|
11
12
|
status: string | number
|
|
@@ -15,22 +16,31 @@ const props = withDefaults(defineProps<{
|
|
|
15
16
|
statusMap: () => ({})
|
|
16
17
|
})
|
|
17
18
|
|
|
19
|
+
const { t } = useI18n()
|
|
18
20
|
const attrs = useAttrs()
|
|
19
21
|
const baseTestId = computed<string | undefined>(() => props.testId ?? (attrs['data-testid'] as string | undefined))
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
'
|
|
23
|
+
// 内置状态文案走词典(key 前缀 ui.status.);调用方显式传入 statusMap 时优先
|
|
24
|
+
const defaultMap: Record<string, { labelKey: string; cls: string }> = {
|
|
25
|
+
active: { labelKey: 'ui.status.active', cls: 'badge-success' },
|
|
26
|
+
locked: { labelKey: 'ui.status.locked', cls: 'badge-danger' },
|
|
27
|
+
deleted: { labelKey: 'ui.status.deleted', cls: 'badge-danger' },
|
|
28
|
+
'0': { labelKey: 'ui.status.disabled', cls: 'badge-warning' },
|
|
29
|
+
'1': { labelKey: 'ui.status.active', cls: 'badge-success' },
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
const
|
|
32
|
+
const label = computed(() => {
|
|
30
33
|
const key = String(props.status)
|
|
31
|
-
|
|
34
|
+
const custom = props.statusMap[key] || props.statusMap[props.status]
|
|
35
|
+
if (custom) return custom.label
|
|
36
|
+
const def = defaultMap[key]
|
|
37
|
+
return def ? t(def.labelKey) : key
|
|
32
38
|
})
|
|
33
39
|
|
|
34
|
-
const
|
|
35
|
-
const
|
|
40
|
+
const cls = computed(() => {
|
|
41
|
+
const key = String(props.status)
|
|
42
|
+
const custom = props.statusMap[key] || props.statusMap[props.status]
|
|
43
|
+
if (custom) return custom.cls
|
|
44
|
+
return (defaultMap[key] || { cls: 'badge-danger' }).cls
|
|
45
|
+
})
|
|
36
46
|
</script>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<input
|
|
5
5
|
ref="inputRef"
|
|
6
6
|
:value="modelValue"
|
|
7
|
-
:placeholder="
|
|
7
|
+
:placeholder="placeholderText"
|
|
8
8
|
:disabled="disabled"
|
|
9
9
|
class="flex-1 min-w-0 bg-transparent border-none outline-none text-input-text placeholder:text-input-placeholder"
|
|
10
10
|
:data-testid="baseTestId ? `${baseTestId}-input` : undefined"
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
:data-testid="baseTestId ? `${baseTestId}-load` : undefined"
|
|
30
30
|
@click.stop="onLoadBtnClick"
|
|
31
31
|
tabindex="-1"
|
|
32
|
-
>{{ loading ?
|
|
32
|
+
>{{ loading ? loadingText : loadBtnTextText }}</button>
|
|
33
33
|
<!-- 无 loadBtn 时提供展开按钮(配合外部 options 模式) -->
|
|
34
34
|
<button
|
|
35
35
|
v-if="!loadBtn && loading"
|
|
@@ -54,14 +54,14 @@
|
|
|
54
54
|
:data-testid="baseTestId ? `${baseTestId}-dropdown` : undefined"
|
|
55
55
|
>
|
|
56
56
|
<!-- 初始加载中 -->
|
|
57
|
-
<div v-if="initialLoading" class="px-4 py-2 text-sm text-text-tertiary"
|
|
57
|
+
<div v-if="initialLoading" class="px-4 py-2 text-sm text-text-tertiary">{{ loadingText }}</div>
|
|
58
58
|
<!-- 加载失败 -->
|
|
59
59
|
<div v-else-if="loadError" class="px-4 py-2">
|
|
60
60
|
<span class="text-sm text-accent-danger">{{ loadError }}</span>
|
|
61
|
-
<button class="ml-2 text-xs text-accent-primary hover:text-accent-primary-hover" @click="retryLoad"
|
|
61
|
+
<button class="ml-2 text-xs text-accent-primary hover:text-accent-primary-hover" @click="retryLoad">{{ retryText }}</button>
|
|
62
62
|
</div>
|
|
63
63
|
<!-- 无选项 -->
|
|
64
|
-
<div v-else-if="filtered.length === 0" class="px-4 py-2 text-sm text-text-tertiary"
|
|
64
|
+
<div v-else-if="filtered.length === 0" class="px-4 py-2 text-sm text-text-tertiary">{{ noMatchText }}</div>
|
|
65
65
|
<!-- 选项列表 -->
|
|
66
66
|
<div
|
|
67
67
|
v-for="opt in filtered"
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
|
|
80
80
|
<script setup lang="ts">
|
|
81
81
|
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
|
82
|
+
import { useI18n } from 'vue-i18n'
|
|
82
83
|
import { match } from 'pinyin-pro'
|
|
83
84
|
import type { SelectOption } from '../types'
|
|
84
85
|
|
|
@@ -94,13 +95,19 @@ const props = withDefaults(defineProps<{
|
|
|
94
95
|
loadBtnText?: string
|
|
95
96
|
}>(), {
|
|
96
97
|
options: () => [],
|
|
97
|
-
placeholder: '请输入或选择',
|
|
98
98
|
disabled: false,
|
|
99
99
|
clearable: true,
|
|
100
100
|
loadBtn: false,
|
|
101
|
-
loadBtnText: '获取',
|
|
102
101
|
})
|
|
103
102
|
|
|
103
|
+
const { t } = useI18n()
|
|
104
|
+
// 兜底文案:props 显式传入优先,缺省走词典
|
|
105
|
+
const placeholderText = computed(() => props.placeholder ?? t('ui.suggestInput.placeholder'))
|
|
106
|
+
const loadBtnTextText = computed(() => props.loadBtnText ?? t('ui.suggestInput.loadBtnText'))
|
|
107
|
+
const loadingText = computed(() => t('ui.suggestInput.loading'))
|
|
108
|
+
const retryText = computed(() => t('ui.common.retry'))
|
|
109
|
+
const noMatchText = computed(() => t('ui.common.noMatch'))
|
|
110
|
+
|
|
104
111
|
const emit = defineEmits<{
|
|
105
112
|
'update:modelValue': [value: string]
|
|
106
113
|
}>()
|
|
@@ -145,7 +152,7 @@ async function loadAsync() {
|
|
|
145
152
|
const result = await props.loadOptions()
|
|
146
153
|
innerOptions.value = result
|
|
147
154
|
} catch (err: any) {
|
|
148
|
-
loadError.value = err?.message || '
|
|
155
|
+
loadError.value = err?.message || t('ui.common.fetchFailed')
|
|
149
156
|
} finally {
|
|
150
157
|
initialLoading.value = false
|
|
151
158
|
loading.value = false
|
package/components/TopNavbar.vue
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
|
8
8
|
</svg>
|
|
9
9
|
</div>
|
|
10
|
-
<span class="navbar-brand">{{
|
|
10
|
+
<span class="navbar-brand">{{ displayTitle }}</span>
|
|
11
11
|
</slot>
|
|
12
12
|
<span v-if="tenantLabel" class="text-xs text-text-tertiary">| {{ tenantLabel }}</span>
|
|
13
13
|
|
|
@@ -28,11 +28,39 @@
|
|
|
28
28
|
</div>
|
|
29
29
|
|
|
30
30
|
<div class="navbar-user">
|
|
31
|
+
<!-- 语言切换 -->
|
|
32
|
+
<div v-if="showLocaleSwitch" class="relative" ref="localeDropdownRef">
|
|
33
|
+
<button
|
|
34
|
+
@click="showLocaleDropdown = !showLocaleDropdown"
|
|
35
|
+
:title="languageTitle"
|
|
36
|
+
class="btn-icon text-text-secondary hover:text-text-primary"
|
|
37
|
+
:data-testid="testId ? `${testId}-locale-toggle` : undefined"
|
|
38
|
+
>
|
|
39
|
+
🌐
|
|
40
|
+
</button>
|
|
41
|
+
|
|
42
|
+
<Transition name="dropdown">
|
|
43
|
+
<div v-if="showLocaleDropdown" class="absolute right-0 mt-2 w-40 card shadow-lg z-50 py-1">
|
|
44
|
+
<button
|
|
45
|
+
v-for="opt in SUPPORTED_LOCALES"
|
|
46
|
+
:key="opt.code"
|
|
47
|
+
class="w-full px-4 py-2 text-left text-sm flex items-center gap-2 hover:bg-bg-tertiary"
|
|
48
|
+
:class="localeStore.locale === opt.code ? 'text-accent-primary' : 'text-text-secondary'"
|
|
49
|
+
:data-testid="testId ? `${testId}-locale-${opt.code}` : undefined"
|
|
50
|
+
@click="selectLocale(opt.code)"
|
|
51
|
+
>
|
|
52
|
+
<span>{{ opt.code === localeStore.locale ? '✓' : '' }}</span>
|
|
53
|
+
<span>{{ opt.label }}</span>
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
</Transition>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
31
59
|
<!-- 主题切换 -->
|
|
32
60
|
<div class="relative" ref="themeDropdownRef">
|
|
33
|
-
<button
|
|
61
|
+
<button
|
|
34
62
|
@click="showThemeDropdown = !showThemeDropdown"
|
|
35
|
-
:title="
|
|
63
|
+
:title="themeTooltip"
|
|
36
64
|
class="btn-icon text-text-secondary hover:text-text-primary"
|
|
37
65
|
:data-testid="testId ? `${testId}-theme-toggle` : undefined"
|
|
38
66
|
>
|
|
@@ -42,15 +70,15 @@
|
|
|
42
70
|
<Transition name="dropdown">
|
|
43
71
|
<div v-if="showThemeDropdown" class="absolute right-0 mt-2 w-40 card shadow-lg z-50 py-1">
|
|
44
72
|
<button
|
|
45
|
-
v-for="
|
|
46
|
-
:key="
|
|
73
|
+
v-for="opt in themeStore.themeList"
|
|
74
|
+
:key="opt.mode"
|
|
47
75
|
class="w-full px-4 py-2 text-left text-sm flex items-center gap-2 hover:bg-bg-tertiary"
|
|
48
|
-
:class="themeStore.theme ===
|
|
49
|
-
:data-testid="testId ? `${testId}-theme-${
|
|
50
|
-
@click="selectTheme(
|
|
76
|
+
:class="themeStore.theme === opt.mode ? 'text-accent-primary' : 'text-text-secondary'"
|
|
77
|
+
:data-testid="testId ? `${testId}-theme-${opt.mode}` : undefined"
|
|
78
|
+
@click="selectTheme(opt.mode)"
|
|
51
79
|
>
|
|
52
|
-
<span>{{
|
|
53
|
-
<span>{{ t.
|
|
80
|
+
<span>{{ opt.icon }}</span>
|
|
81
|
+
<span>{{ t(opt.labelKey) }}</span>
|
|
54
82
|
</button>
|
|
55
83
|
</div>
|
|
56
84
|
</Transition>
|
|
@@ -100,7 +128,9 @@
|
|
|
100
128
|
|
|
101
129
|
<script setup lang="ts">
|
|
102
130
|
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
|
103
|
-
import {
|
|
131
|
+
import { useI18n } from 'vue-i18n'
|
|
132
|
+
import { useThemeStore, useLocaleStore, SUPPORTED_LOCALES } from '@aquiferre/theme-kit'
|
|
133
|
+
import type { LocaleCode } from '@aquiferre/theme-kit'
|
|
104
134
|
import type { NavModule, DropdownItem } from '../types'
|
|
105
135
|
|
|
106
136
|
const props = withDefaults(defineProps<{
|
|
@@ -110,23 +140,35 @@ const props = withDefaults(defineProps<{
|
|
|
110
140
|
modules?: NavModule[]
|
|
111
141
|
activeModule?: string
|
|
112
142
|
userDropdownItems?: DropdownItem[]
|
|
143
|
+
showLocaleSwitch?: boolean
|
|
113
144
|
testId?: string
|
|
114
145
|
}>(), {
|
|
115
|
-
title: '医疗设备智能平台',
|
|
116
146
|
avatarText: 'U',
|
|
117
147
|
tenantLabel: '',
|
|
118
148
|
modules: () => [],
|
|
119
149
|
activeModule: '',
|
|
120
|
-
userDropdownItems: () => []
|
|
150
|
+
userDropdownItems: () => [],
|
|
151
|
+
showLocaleSwitch: true,
|
|
121
152
|
})
|
|
122
153
|
|
|
154
|
+
const { t } = useI18n()
|
|
123
155
|
const themeStore = useThemeStore()
|
|
156
|
+
const localeStore = useLocaleStore()
|
|
124
157
|
const showUserDropdown = ref(false)
|
|
125
158
|
const showThemeDropdown = ref(false)
|
|
159
|
+
const showLocaleDropdown = ref(false)
|
|
126
160
|
const userDropdownRef = ref<HTMLElement | null>(null)
|
|
127
161
|
const themeDropdownRef = ref<HTMLElement | null>(null)
|
|
162
|
+
const localeDropdownRef = ref<HTMLElement | null>(null)
|
|
128
163
|
|
|
164
|
+
const displayTitle = computed(() => props.title ?? t('ui.navbar.title'))
|
|
129
165
|
const themeIcon = computed(() => themeStore.currentThemeInfo?.icon || '🎨')
|
|
166
|
+
const themeTooltip = computed(() =>
|
|
167
|
+
t('ui.navbar.currentTheme', {
|
|
168
|
+
theme: themeStore.currentThemeInfo ? t(themeStore.currentThemeInfo.labelKey) : '',
|
|
169
|
+
})
|
|
170
|
+
)
|
|
171
|
+
const languageTitle = computed(() => t('ui.navbar.language'))
|
|
130
172
|
const avatarInitial = computed(() => props.avatarText.charAt(0).toUpperCase())
|
|
131
173
|
|
|
132
174
|
function selectTheme(mode: 'light' | 'dark' | 'neutral') {
|
|
@@ -134,6 +176,11 @@ function selectTheme(mode: 'light' | 'dark' | 'neutral') {
|
|
|
134
176
|
showThemeDropdown.value = false
|
|
135
177
|
}
|
|
136
178
|
|
|
179
|
+
function selectLocale(code: LocaleCode) {
|
|
180
|
+
localeStore.setLocale(code)
|
|
181
|
+
showLocaleDropdown.value = false
|
|
182
|
+
}
|
|
183
|
+
|
|
137
184
|
function handleItemClick(item: DropdownItem) {
|
|
138
185
|
if (item.onClick) item.onClick()
|
|
139
186
|
showUserDropdown.value = false
|
|
@@ -146,6 +193,9 @@ function handleClickOutside(event: MouseEvent) {
|
|
|
146
193
|
if (themeDropdownRef.value && !themeDropdownRef.value.contains(event.target as Node)) {
|
|
147
194
|
showThemeDropdown.value = false
|
|
148
195
|
}
|
|
196
|
+
if (localeDropdownRef.value && !localeDropdownRef.value.contains(event.target as Node)) {
|
|
197
|
+
showLocaleDropdown.value = false
|
|
198
|
+
}
|
|
149
199
|
}
|
|
150
200
|
|
|
151
201
|
onMounted(() => document.addEventListener('click', handleClickOutside))
|
package/components/Tree.vue
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
v-model="keyword"
|
|
6
6
|
type="text"
|
|
7
7
|
class="tree-search"
|
|
8
|
-
:placeholder="
|
|
8
|
+
:placeholder="searchPlaceholderText"
|
|
9
9
|
:data-testid="testId ? `${testId}-search` : undefined"
|
|
10
10
|
/>
|
|
11
11
|
</div>
|
|
12
12
|
<div v-if="visibleRoot.length === 0" class="tree-empty">
|
|
13
|
-
<p>{{
|
|
13
|
+
<p>{{ emptyTextText }}</p>
|
|
14
14
|
</div>
|
|
15
15
|
<div v-else class="tree-scroll">
|
|
16
16
|
<TreeNodeItem
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
<script setup lang="ts">
|
|
34
34
|
import { ref, computed, watch } from 'vue'
|
|
35
|
+
import { useI18n } from 'vue-i18n'
|
|
35
36
|
import TreeNodeItem from './TreeNodeItem.vue'
|
|
36
37
|
|
|
37
38
|
/** 通用树节点:业务字段(type 等)可透传附加;children 由组件懒加载后写入 */
|
|
@@ -59,9 +60,14 @@ const props = withDefaults(
|
|
|
59
60
|
emptyText?: string
|
|
60
61
|
testId?: string
|
|
61
62
|
}>(),
|
|
62
|
-
{ showSearch: false
|
|
63
|
+
{ showSearch: false }
|
|
63
64
|
)
|
|
64
65
|
|
|
66
|
+
const { t } = useI18n()
|
|
67
|
+
// 搜索占位与空态文案:显式传入优先,缺省走词典
|
|
68
|
+
const searchPlaceholderText = computed(() => props.searchPlaceholder ?? t('ui.tree.searchPlaceholder'))
|
|
69
|
+
const emptyTextText = computed(() => props.emptyText ?? t('ui.common.noData'))
|
|
70
|
+
|
|
65
71
|
const emit = defineEmits<{
|
|
66
72
|
(e: 'select', node: TreeNode | null): void
|
|
67
73
|
}>()
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<input
|
|
5
5
|
class="input w-full"
|
|
6
6
|
v-model="keyword"
|
|
7
|
-
placeholder="
|
|
7
|
+
:placeholder="t('ui.userPicker.searchPlaceholder')"
|
|
8
8
|
:data-testid="baseTestId ? `${baseTestId}-search` : undefined"
|
|
9
9
|
@input="handleSearch"
|
|
10
10
|
/>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
<div v-if="loading" class="text-center py-4 text-text-secondary">
|
|
14
14
|
<div class="spinner mx-auto mb-2"></div>
|
|
15
|
-
<p
|
|
15
|
+
<p>{{ loadingText }}</p>
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
18
|
<div v-else-if="users.length > 0" class="user-list space-y-2 max-h-64 overflow-y-auto">
|
|
@@ -40,13 +40,14 @@
|
|
|
40
40
|
</div>
|
|
41
41
|
|
|
42
42
|
<div v-else class="text-center py-4 text-text-tertiary">
|
|
43
|
-
<p
|
|
43
|
+
<p>{{ emptyText }}</p>
|
|
44
44
|
</div>
|
|
45
45
|
</div>
|
|
46
46
|
</template>
|
|
47
47
|
|
|
48
48
|
<script setup lang="ts">
|
|
49
49
|
import { ref, onMounted, computed, useAttrs } from 'vue'
|
|
50
|
+
import { useI18n } from 'vue-i18n'
|
|
50
51
|
|
|
51
52
|
interface UserBasicInfo {
|
|
52
53
|
id: string
|
|
@@ -61,6 +62,10 @@ const props = defineProps<{
|
|
|
61
62
|
testId?: string
|
|
62
63
|
}>()
|
|
63
64
|
|
|
65
|
+
const { t } = useI18n()
|
|
66
|
+
const loadingText = computed(() => t('ui.common.loading'))
|
|
67
|
+
const emptyText = computed(() => t('ui.userPicker.empty'))
|
|
68
|
+
|
|
64
69
|
const attrs = useAttrs()
|
|
65
70
|
const baseTestId = computed<string | undefined>(() => props.testId ?? (attrs['data-testid'] as string | undefined))
|
|
66
71
|
|
package/index.ts
CHANGED
|
@@ -19,4 +19,5 @@ export { default as DateTimePicker } from './components/DateTimePicker.vue'
|
|
|
19
19
|
export { default as Tree } from './components/Tree.vue'
|
|
20
20
|
export type { TreeNode } from './components/Tree.vue'
|
|
21
21
|
export { useToast } from './composables/useToast'
|
|
22
|
+
export { uiKitMessages, uiKitMessagesZhCN, uiKitMessagesEnUS } from './messages'
|
|
22
23
|
export type { Column, SelectOption, MenuItem, MenuGroup, NavModule, DropdownItem, ToastType, DateTimePreset } from './types'
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// ui-kit 词典(en-US):扁平 key 结构,key 前缀 ui.,由 app 在 createI18n 时与自身词典合并。
|
|
2
|
+
// 覆盖 ui-kit 组件内置文案的默认值;组件 props 显式传入的文案始终优先。
|
|
3
|
+
export const uiKitMessagesEnUS: Record<string, string> = {
|
|
4
|
+
// Common
|
|
5
|
+
'ui.common.confirm': 'Confirm',
|
|
6
|
+
'ui.common.cancel': 'Cancel',
|
|
7
|
+
'ui.common.loading': 'Loading...',
|
|
8
|
+
'ui.common.submitting': 'Submitting...',
|
|
9
|
+
'ui.common.retry': 'Retry',
|
|
10
|
+
'ui.common.noMatch': 'No matches found',
|
|
11
|
+
'ui.common.noData': 'No data',
|
|
12
|
+
'ui.common.fetchFailed': 'Failed to load',
|
|
13
|
+
'ui.common.search': 'Search',
|
|
14
|
+
|
|
15
|
+
// ConfirmDialog
|
|
16
|
+
'ui.confirmDialog.title': 'Confirm deletion',
|
|
17
|
+
'ui.confirmDialog.confirmText': 'Delete',
|
|
18
|
+
'ui.confirmDialog.loadingText': 'Deleting...',
|
|
19
|
+
|
|
20
|
+
// DataTable
|
|
21
|
+
'ui.dataTable.loading': 'Loading...',
|
|
22
|
+
'ui.dataTable.empty': 'No data',
|
|
23
|
+
|
|
24
|
+
// Pagination
|
|
25
|
+
'ui.pagination.total': 'Total {total} items, {startItem} - {endItem}',
|
|
26
|
+
'ui.pagination.prev': 'Previous',
|
|
27
|
+
'ui.pagination.next': 'Next',
|
|
28
|
+
'ui.pagination.pageSize': '{size}/page',
|
|
29
|
+
|
|
30
|
+
// StatusTag (status value → label, matching StatusTag internal mapping keys)
|
|
31
|
+
'ui.status.active': 'Active',
|
|
32
|
+
'ui.status.locked': 'Locked',
|
|
33
|
+
'ui.status.deleted': 'Deleted',
|
|
34
|
+
'ui.status.disabled': 'Disabled',
|
|
35
|
+
|
|
36
|
+
// FormDialog
|
|
37
|
+
'ui.formDialog.confirmText': 'Confirm',
|
|
38
|
+
|
|
39
|
+
// FilterDropdown
|
|
40
|
+
'ui.filterDropdown.all': 'All',
|
|
41
|
+
|
|
42
|
+
// SearchSelect
|
|
43
|
+
'ui.searchSelect.placeholder': 'Please select',
|
|
44
|
+
|
|
45
|
+
// SuggestInput
|
|
46
|
+
'ui.suggestInput.placeholder': 'Type or select',
|
|
47
|
+
'ui.suggestInput.loadBtnText': 'Load',
|
|
48
|
+
'ui.suggestInput.loading': 'Loading...',
|
|
49
|
+
|
|
50
|
+
// Tree
|
|
51
|
+
'ui.tree.searchPlaceholder': 'Search...',
|
|
52
|
+
|
|
53
|
+
// SideMenu
|
|
54
|
+
'ui.sideMenu.expand': 'Expand menu',
|
|
55
|
+
'ui.sideMenu.collapse': 'Collapse menu',
|
|
56
|
+
'ui.sideMenu.home': 'Home',
|
|
57
|
+
|
|
58
|
+
// UserPicker
|
|
59
|
+
'ui.userPicker.searchPlaceholder': 'Search user...',
|
|
60
|
+
'ui.userPicker.empty': 'No matching users found',
|
|
61
|
+
|
|
62
|
+
// FileUploader
|
|
63
|
+
'ui.fileUploader.select': 'Choose file',
|
|
64
|
+
'ui.fileUploader.uploading': 'Uploading {progress}%',
|
|
65
|
+
'ui.fileUploader.done': 'Done',
|
|
66
|
+
'ui.fileUploader.failed': 'Upload failed',
|
|
67
|
+
|
|
68
|
+
// DateTimePicker
|
|
69
|
+
'ui.dateTimePicker.year': '{year}',
|
|
70
|
+
'ui.dateTimePicker.month': '{month}',
|
|
71
|
+
'ui.dateTimePicker.monthSection': 'Month',
|
|
72
|
+
'ui.dateTimePicker.week.sun': 'Su',
|
|
73
|
+
'ui.dateTimePicker.week.mon': 'Mo',
|
|
74
|
+
'ui.dateTimePicker.week.tue': 'Tu',
|
|
75
|
+
'ui.dateTimePicker.week.wed': 'We',
|
|
76
|
+
'ui.dateTimePicker.week.thu': 'Th',
|
|
77
|
+
'ui.dateTimePicker.week.fri': 'Fr',
|
|
78
|
+
'ui.dateTimePicker.week.sat': 'Sa',
|
|
79
|
+
|
|
80
|
+
// TopNavbar
|
|
81
|
+
'ui.navbar.title': 'Medical Device Intelligent Platform',
|
|
82
|
+
'ui.navbar.currentTheme': 'Theme: {theme}',
|
|
83
|
+
'ui.navbar.language': 'Language',
|
|
84
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { uiKitMessagesZhCN } from './zh-CN'
|
|
2
|
+
import { uiKitMessagesEnUS } from './en-US'
|
|
3
|
+
|
|
4
|
+
export { uiKitMessagesZhCN } from './zh-CN'
|
|
5
|
+
export { uiKitMessagesEnUS } from './en-US'
|
|
6
|
+
|
|
7
|
+
/** 各语言词典聚合:app 侧 createI18n 时展开合并 */
|
|
8
|
+
export const uiKitMessages = {
|
|
9
|
+
'zh-CN': uiKitMessagesZhCN,
|
|
10
|
+
'en-US': uiKitMessagesEnUS,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type UiKitMessagesZh = typeof uiKitMessagesZhCN
|
|
14
|
+
export type UiKitMessagesEn = typeof uiKitMessagesEnUS
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// ui-kit 词典(zh-CN):扁平 key 结构,key 前缀 ui.,由 app 在 createI18n 时与自身词典合并。
|
|
2
|
+
// 覆盖 ui-kit 组件内置文案的默认值;组件 props 显式传入的文案始终优先。
|
|
3
|
+
export const uiKitMessagesZhCN: Record<string, string> = {
|
|
4
|
+
// 通用
|
|
5
|
+
'ui.common.confirm': '确认',
|
|
6
|
+
'ui.common.cancel': '取消',
|
|
7
|
+
'ui.common.loading': '加载中...',
|
|
8
|
+
'ui.common.submitting': '提交中...',
|
|
9
|
+
'ui.common.retry': '重试',
|
|
10
|
+
'ui.common.noMatch': '无匹配项',
|
|
11
|
+
'ui.common.noData': '暂无数据',
|
|
12
|
+
'ui.common.fetchFailed': '获取失败',
|
|
13
|
+
'ui.common.search': '搜索',
|
|
14
|
+
|
|
15
|
+
// ConfirmDialog
|
|
16
|
+
'ui.confirmDialog.title': '确认删除',
|
|
17
|
+
'ui.confirmDialog.confirmText': '确认删除',
|
|
18
|
+
'ui.confirmDialog.loadingText': '删除中...',
|
|
19
|
+
|
|
20
|
+
// DataTable
|
|
21
|
+
'ui.dataTable.loading': '加载中...',
|
|
22
|
+
'ui.dataTable.empty': '暂无数据',
|
|
23
|
+
|
|
24
|
+
// Pagination
|
|
25
|
+
'ui.pagination.total': '共 {total} 条,第 {startItem} - {endItem} 条',
|
|
26
|
+
'ui.pagination.prev': '上一页',
|
|
27
|
+
'ui.pagination.next': '下一页',
|
|
28
|
+
'ui.pagination.pageSize': '{size}条/页',
|
|
29
|
+
|
|
30
|
+
// StatusTag(状态值→文案,同 StatusTag 内部映射 key)
|
|
31
|
+
'ui.status.active': '正常',
|
|
32
|
+
'ui.status.locked': '已锁定',
|
|
33
|
+
'ui.status.deleted': '已删除',
|
|
34
|
+
'ui.status.disabled': '已停用',
|
|
35
|
+
|
|
36
|
+
// FormDialog
|
|
37
|
+
'ui.formDialog.confirmText': '确认',
|
|
38
|
+
|
|
39
|
+
// FilterDropdown
|
|
40
|
+
'ui.filterDropdown.all': '全部',
|
|
41
|
+
|
|
42
|
+
// SearchSelect
|
|
43
|
+
'ui.searchSelect.placeholder': '请选择',
|
|
44
|
+
|
|
45
|
+
// SuggestInput
|
|
46
|
+
'ui.suggestInput.placeholder': '请输入或选择',
|
|
47
|
+
'ui.suggestInput.loadBtnText': '获取',
|
|
48
|
+
'ui.suggestInput.loading': '获取中...',
|
|
49
|
+
|
|
50
|
+
// Tree
|
|
51
|
+
'ui.tree.searchPlaceholder': '搜索...',
|
|
52
|
+
|
|
53
|
+
// SideMenu
|
|
54
|
+
'ui.sideMenu.expand': '展开菜单',
|
|
55
|
+
'ui.sideMenu.collapse': '收起菜单',
|
|
56
|
+
'ui.sideMenu.home': '首页',
|
|
57
|
+
|
|
58
|
+
// UserPicker
|
|
59
|
+
'ui.userPicker.searchPlaceholder': '搜索用户...',
|
|
60
|
+
'ui.userPicker.empty': '未找到符合条件的用户',
|
|
61
|
+
|
|
62
|
+
// FileUploader
|
|
63
|
+
'ui.fileUploader.select': '选择文件',
|
|
64
|
+
'ui.fileUploader.uploading': '上传中 {progress}%',
|
|
65
|
+
'ui.fileUploader.done': '完成',
|
|
66
|
+
'ui.fileUploader.failed': '上传失败',
|
|
67
|
+
|
|
68
|
+
// DateTimePicker
|
|
69
|
+
'ui.dateTimePicker.year': '{year}年',
|
|
70
|
+
'ui.dateTimePicker.month': '{month}月',
|
|
71
|
+
'ui.dateTimePicker.monthSection': '月份',
|
|
72
|
+
'ui.dateTimePicker.week.sun': '日',
|
|
73
|
+
'ui.dateTimePicker.week.mon': '一',
|
|
74
|
+
'ui.dateTimePicker.week.tue': '二',
|
|
75
|
+
'ui.dateTimePicker.week.wed': '三',
|
|
76
|
+
'ui.dateTimePicker.week.thu': '四',
|
|
77
|
+
'ui.dateTimePicker.week.fri': '五',
|
|
78
|
+
'ui.dateTimePicker.week.sat': '六',
|
|
79
|
+
|
|
80
|
+
// TopNavbar
|
|
81
|
+
'ui.navbar.title': '医疗设备智能平台',
|
|
82
|
+
'ui.navbar.currentTheme': '当前主题: {theme}',
|
|
83
|
+
'ui.navbar.language': '语言',
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aquiferre/ui-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -11,15 +11,17 @@
|
|
|
11
11
|
"*.ts",
|
|
12
12
|
"*.vue",
|
|
13
13
|
"components/**/*.vue",
|
|
14
|
-
"composables/**/*.ts"
|
|
14
|
+
"composables/**/*.ts",
|
|
15
|
+
"messages/*.ts"
|
|
15
16
|
],
|
|
16
17
|
"dependencies": {
|
|
17
18
|
"pinyin-pro": "^3.28.1"
|
|
18
19
|
},
|
|
19
20
|
"peerDependencies": {
|
|
20
|
-
"@aquiferre/theme-kit": ">=0.
|
|
21
|
+
"@aquiferre/theme-kit": ">=0.2.0",
|
|
21
22
|
"pinia": ">=2.0.0",
|
|
22
|
-
"vue": ">=3.0.0"
|
|
23
|
+
"vue": ">=3.0.0",
|
|
24
|
+
"vue-i18n": "^11.0.0"
|
|
23
25
|
},
|
|
24
26
|
"publishConfig": {
|
|
25
27
|
"access": "public"
|