fix: display measurements throughout overseas fulfillment
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
- 查询与筛选使用 `QueryToolbar.vue`,通过 `fields`、`v-model`、`search`、`reset` 接入;查询即刷新当前结果,不另设独立刷新按钮。
|
||||
- 数据列表使用 `DataTable.vue`,通过 `columns`、命名插槽、`v-model:page`、`v-model:page-size` 和 `total` 接入,不重复拼装 `el-table` 与分页器。
|
||||
- “操作”列放在第一列,需要固定时固定在左侧。单行操作不超过 3 个时直接显示“图标 + 文案”;第 4 个起使用 `MoreHorizontal` 菜单,并优先直接保留详情、编辑、下载等高频操作。
|
||||
- 列表操作统一使用 `RowActions.vue`:默认直接展示 3 个,通过 `priority` 决定高频或关键操作的展示资格,其余自动进入带可访问名称的“更多”菜单;权限和状态由业务页面生成 `actions` 时决定,组件不推导业务资格。
|
||||
- 新增、导入、导出和批量操作放入 `DataTable` 的 `toolbar-actions`;不放入查询栏,不用 `toolbar-start` 重复列表标题、数量或说明,数量统一放在 `pagination-left`。
|
||||
- 路由标题和说明默认由 `AppLayout` 的 `page-heading` 根据路由元数据展示;标准列表正文不重复标题。只有明确关闭路由页头的详情页或工作台才自带独立页头。
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import RowActions, { type RowActionItem } from './RowActions.vue'
|
||||
|
||||
const actions: RowActionItem[] = [
|
||||
{ key: 'view', label: '查看', priority: 300 },
|
||||
{ key: 'print', label: '打印', priority: 200 },
|
||||
{ key: 'edit', label: '编辑', priority: 100 },
|
||||
{ key: 'transfer', label: '转派送', priority: 250, tone: 'success' },
|
||||
{ key: 'cancel', label: '取消', tone: 'danger' },
|
||||
]
|
||||
|
||||
describe('RowActions', () => {
|
||||
it('shows at most three prioritized actions and moves the rest into more', () => {
|
||||
const wrapper = mount(RowActions, {
|
||||
props: { actions, moreLabel: '更多操作 · PC-1' },
|
||||
global: { plugins: [ElementPlus] },
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('.row-actions > .el-button').map(button => button.text().trim()))
|
||||
.toEqual(['查看', '打印', '转派送'])
|
||||
expect(wrapper.get('.el-dropdown .el-button').attributes('aria-label')).toBe('更多操作 · PC-1')
|
||||
})
|
||||
|
||||
it('keeps input order when priorities choose the visible actions', () => {
|
||||
const wrapper = mount(RowActions, {
|
||||
props: { actions, maxVisible: 2 },
|
||||
global: { plugins: [ElementPlus] },
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('.row-actions > .el-button').map(button => button.text().trim()))
|
||||
.toEqual(['查看', '转派送'])
|
||||
})
|
||||
|
||||
it('emits enabled direct actions and ignores loading actions', async () => {
|
||||
const wrapper = mount(RowActions, {
|
||||
props: {
|
||||
actions: [
|
||||
{ key: 'view', label: '查看' },
|
||||
{ key: 'print', label: '打印', loading: true },
|
||||
],
|
||||
},
|
||||
global: { plugins: [ElementPlus] },
|
||||
})
|
||||
|
||||
await wrapper.findAll('.row-actions > .el-button')[0].trigger('click')
|
||||
await wrapper.findAll('.row-actions > .el-button')[1].trigger('click')
|
||||
expect(wrapper.emitted('action')).toEqual([['view']])
|
||||
})
|
||||
|
||||
it('omits hidden actions without rendering an empty more menu', () => {
|
||||
const wrapper = mount(RowActions, {
|
||||
props: { actions: [{ key: 'edit', label: '编辑', visible: false }] },
|
||||
global: { plugins: [ElementPlus] },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.el-dropdown').exists()).toBe(false)
|
||||
expect(wrapper.text()).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { MoreHorizontal } from '@lucide/vue'
|
||||
|
||||
export type RowActionTone = 'neutral' | 'warning' | 'purple' | 'primary' | 'success' | 'danger'
|
||||
|
||||
export interface RowActionItem {
|
||||
key: string
|
||||
label: string
|
||||
visible?: boolean
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
tone?: RowActionTone
|
||||
priority?: number
|
||||
divided?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
actions: RowActionItem[]
|
||||
maxVisible?: number
|
||||
moreLabel?: string
|
||||
}>(), {
|
||||
maxVisible: 3,
|
||||
moreLabel: '更多操作',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
action: [key: string]
|
||||
}>()
|
||||
|
||||
const availableActions = computed(() => props.actions.filter(action => action.visible !== false))
|
||||
|
||||
const directKeys = computed(() => new Set(
|
||||
availableActions.value
|
||||
.map((action, index) => ({ action, index }))
|
||||
.sort((left, right) => (right.action.priority ?? 0) - (left.action.priority ?? 0) || left.index - right.index)
|
||||
.slice(0, Math.max(0, props.maxVisible))
|
||||
.map(({ action }) => action.key),
|
||||
))
|
||||
|
||||
const directActions = computed(() => availableActions.value.filter(action => directKeys.value.has(action.key)))
|
||||
const overflowActions = computed(() => availableActions.value.filter(action => !directKeys.value.has(action.key)))
|
||||
|
||||
function select(action: RowActionItem) {
|
||||
if (action.disabled || action.loading) return
|
||||
emit('action', action.key)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="density-actions row-actions">
|
||||
<el-button
|
||||
v-for="action in directActions"
|
||||
:key="action.key"
|
||||
class="density-action"
|
||||
:data-tone="action.tone || 'neutral'"
|
||||
size="small"
|
||||
:disabled="action.disabled"
|
||||
:loading="action.loading"
|
||||
@click="select(action)"
|
||||
>{{ action.label }}</el-button>
|
||||
<el-dropdown v-if="overflowActions.length" trigger="click">
|
||||
<el-button
|
||||
class="density-action"
|
||||
data-tone="neutral"
|
||||
size="small"
|
||||
:aria-label="moreLabel"
|
||||
:title="moreLabel"
|
||||
><MoreHorizontal :size="15" aria-hidden="true" /></el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="action in overflowActions"
|
||||
:key="action.key"
|
||||
:class="{ 'danger-menu-item': action.tone === 'danger' }"
|
||||
:disabled="action.disabled || action.loading"
|
||||
:divided="action.divided"
|
||||
@click="select(action)"
|
||||
>{{ action.label }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user