feat(admin): validate and cancel co-load groups

This commit is contained in:
2026-08-31 14:54:57 +08:00
parent dc1027804e
commit cd290b8dc6
5 changed files with 149 additions and 17 deletions
@@ -25,12 +25,12 @@ describe('co-load group API', () => {
expect(result.members.map((member) => member.tenantId)).toEqual(['2093000000000000004', '2093000000000000007'])
})
it('cancels only the whole group with optimistic version and an optional reason', async () => {
it('cancels the whole group by its visible inbound code without exposing group identifiers', async () => {
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: { ...group, status: 'CANCELLED', version: 1 } })
await cancelPlatformCoLoadGroup('2093000000000000001', 0, '路线调整')
await cancelPlatformCoLoadGroup('NYEXX2608310001', '路线调整')
expect(post).toHaveBeenCalledWith(
'/platform/v1/first-leg/coload-groups/2093000000000000001/cancel',
{ expectedVersion: 0, reason: '路线调整' },
'/platform/v1/first-leg/coload-groups/by-inbound-code/NYEXX2608310001/cancel',
{ reason: '路线调整' },
{ headers: { 'Idempotency-Key': expect.any(String) } },
)
})
@@ -81,10 +81,10 @@ export async function createPlatformCoLoadGroup(orderIds: string[]) {
return normalizeGroup(unwrapApiResponse(response))
}
export async function cancelPlatformCoLoadGroup(groupId: string, expectedVersion: number, reason?: string | null) {
export async function cancelPlatformCoLoadGroup(inboundCode: string, reason?: string | null) {
const response = await http.post<ApiEnvelope<RawCoLoadGroup>, ApiEnvelope<RawCoLoadGroup>>(
`${basePath}/${encodeURIComponent(groupId)}/cancel`,
{ expectedVersion, reason: reason?.trim() || null },
`${basePath}/by-inbound-code/${encodeURIComponent(inboundCode)}/cancel`,
{ reason: reason?.trim() || null },
{ headers: { 'Idempotency-Key': operationId('admin-coload-cancel') } },
)
return normalizeGroup(unwrapApiResponse(response))
@@ -1,8 +1,9 @@
import { flushPromises, shallowMount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ElMessageBox } from 'element-plus'
import { ApiResponseError } from '@/shared/api/http'
import { listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders'
import { createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode } from '@/modules/unified-orders/api/coLoadGroups'
import { cancelPlatformCoLoadGroup, createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode } from '@/modules/unified-orders/api/coLoadGroups'
import UnifiedOrdersView from './UnifiedOrdersView.vue'
vi.mock('vue-router', () => ({ useRoute: () => ({ params: {}, query: {} }), useRouter: () => ({ push: vi.fn() }) }))
@@ -17,7 +18,7 @@ vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
unifiedOrderStatusOptions: [{ value: 'WAIT_SPLIT', label: '待分柜' }],
}))
vi.mock('@/modules/unified-orders/api/coLoadGroups', () => ({
createPlatformCoLoadGroup: vi.fn(), getPlatformCoLoadGroupByInboundCode: vi.fn(),
cancelPlatformCoLoadGroup: vi.fn(), createPlatformCoLoadGroup: vi.fn(), getPlatformCoLoadGroupByInboundCode: vi.fn(),
}))
const rows = ['5001', '5002'].map((orderId, index) => ({
@@ -38,10 +39,10 @@ function mountView() {
ListPageLayout: { template: '<section><slot name="toolbar" /><slot name="state" /><slot /></section>' },
QueryToolbar: { template: '<div />' },
DataTable: { name: 'DataTable', props: ['data'], emits: ['selection-change'], template: '<section><slot name="toolbar-actions" /><button data-testid="select" @click="$emit(\'selection-change\', data)">勾选两票</button><template v-for="row in data" :key="row.orderId"><slot name="actions" :row="row" /><slot name="inboundCode" :row="row" /></template></section>' },
AppDialog: { name: 'AppDialog', props: ['modelValue', 'confirmText'], emits: ['confirm', 'update:modelValue'], template: '<section v-if="modelValue"><slot /><button data-testid="dialog-confirm" @click="$emit(\'confirm\')">{{ confirmText }}</button></section>' },
AppDialog: { name: 'AppDialog', props: ['modelValue', 'confirmText', 'confirmDisabled', 'confirmLoading'], emits: ['confirm', 'update:modelValue'], methods: { close() { this.$emit('update:modelValue', false) } }, template: '<section v-if="modelValue"><slot /><slot name="footer" :close="close" :submitting="confirmLoading"><button data-testid="dialog-confirm" :disabled="confirmDisabled" @click="$emit(\'confirm\')">{{ confirmText }}</button></slot></section>' },
ApiErrorMeta: { props: ['details'], template: '<div>{{ details.code }} {{ details.requestId }} {{ details.traceId }}</div>' },
CopyButton: { props: ['value', 'label'], template: '<button :data-copy="value">复制 ID</button>' },
'el-button': { template: '<button @click="$emit(\'click\')"><slot /></button>' },
'el-button': { props: ['disabled', 'loading'], emits: ['click'], template: '<button :disabled="disabled" @click="$emit(\'click\')"><slot /></button>' },
'el-tag': { template: '<span><slot /></span>' },
'el-alert': { props: ['title'], template: '<div>{{ title }}</div>' },
} },
@@ -52,6 +53,7 @@ describe('UnifiedOrdersView co-load group behavior', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rows, page: 1, size: 20, total: 2, pages: 1 })
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue({ action: 'confirm', value: '' } as never)
})
it('keeps both selected orders and backend tracing evidence when create validation fails', async () => {
@@ -69,6 +71,22 @@ describe('UnifiedOrdersView co-load group behavior', () => {
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1)
})
it.each([
['平台收货仓不同', [{ ...rows[0], receivingWarehouseId: '8' }, { ...rows[1], receivingWarehouseId: '9' }], '所选订单的平台收货仓不同,不能设为同柜组'],
['渠道不同', [{ ...rows[0], channelName: 'NY-EXX' }, { ...rows[1], channelName: 'LA-SEA' }], '所选订单的渠道不同,不能设为同柜组'],
['已有同柜组', [rows[0], { ...rows[1], coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-2', 'IN-3'] } }], '所选订单中已有同柜组,请先解除原同柜组'],
])('blocks create immediately when %s', async (_case, selectedRows, reason) => {
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: selectedRows, page: 1, size: 20, total: 2, pages: 1 })
const wrapper = mountView()
await flushPromises()
await wrapper.get('[data-testid="select"]').trigger('click')
const createButton = wrapper.findAll('button').find((button) => button.text().includes('设为同柜不可拆组'))!
expect(createButton.attributes('disabled')).toBeDefined()
expect(wrapper.text()).toContain(reason)
await createButton.trigger('click')
expect(createPlatformCoLoadGroup).not.toHaveBeenCalled()
})
it('uses inbound codes as the only row identifier and shows an explicit pending state', async () => {
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: [rows[0], { ...rows[1], inboundCode: null }], page: 1, size: 20, total: 2, pages: 1 })
const wrapper = mountView()
@@ -104,6 +122,59 @@ describe('UnifiedOrdersView co-load group behavior', () => {
expect(wrapper.text()).not.toContain('5001')
})
it('cancels an ACTIVE group by the clicked member inbound code and preserves dialog error evidence', async () => {
const rowsWithGroup = rows.map((row) => ({ ...row, coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-1', 'IN-2'] } }))
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rowsWithGroup, page: 1, size: 20, total: 2, pages: 1 })
vi.mocked(getPlatformCoLoadGroupByInboundCode).mockResolvedValue({ status: 'ACTIVE', memberCount: 2, members: [{ inboundCode: 'IN-1' }, { inboundCode: 'IN-2' }] })
vi.mocked(cancelPlatformCoLoadGroup).mockRejectedValueOnce(new ApiResponseError('组已被并发锁定', 'A0409', 409, undefined, 'request-cancel', 'trace-cancel'))
const wrapper = mountView()
await flushPromises()
await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click')
await flushPromises()
const cancelButton = wrapper.findAll('button').find((button) => button.text() === '解除同柜组')!
expect(cancelButton.exists()).toBe(true)
await cancelButton.trigger('click')
await flushPromises()
expect(ElMessageBox.confirm).toHaveBeenCalledWith(expect.stringContaining('解除整个同柜组'), '解除同柜组', expect.objectContaining({ confirmButtonText: '确认解除' }))
expect(cancelPlatformCoLoadGroup).toHaveBeenCalledWith('IN-1', '重新调整同柜组成员')
expect(wrapper.text()).toContain('组已被并发锁定')
expect(wrapper.text()).toContain('A0409 request-cancel trace-cancel')
expect(wrapper.findAllComponents({ name: 'AppDialog' }).some((dialog) => dialog.props('modelValue') === true)).toBe(true)
expect(wrapper.text()).not.toContain('CLG-')
expect(wrapper.text()).not.toContain('5001')
})
it('closes and refreshes after one successful whole-group cancellation request', async () => {
const rowsWithGroup = rows.map((row) => ({ ...row, coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-1', 'IN-2'] } }))
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rowsWithGroup, page: 1, size: 20, total: 2, pages: 1 })
vi.mocked(getPlatformCoLoadGroupByInboundCode).mockResolvedValue({ status: 'ACTIVE', memberCount: 2, members: [{ inboundCode: 'IN-1' }, { inboundCode: 'IN-2' }] })
vi.mocked(cancelPlatformCoLoadGroup).mockResolvedValue({} as never)
const wrapper = mountView()
await flushPromises()
await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click')
await flushPromises()
await wrapper.findAll('button').find((button) => button.text() === '解除同柜组')!.trigger('click')
await flushPromises()
expect(cancelPlatformCoLoadGroup).toHaveBeenCalledTimes(1)
expect(cancelPlatformCoLoadGroup).toHaveBeenCalledWith('IN-1', '重新调整同柜组成员')
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(2)
expect(wrapper.findAllComponents({ name: 'AppDialog' }).every((dialog) => dialog.props('modelValue') !== true)).toBe(true)
})
it.each(['LOCKED', 'CONSUMED'])('does not offer group cancellation for %s', async (status) => {
const rowsWithGroup = rows.map((row) => ({ ...row, coLoadGroup: { status, memberCount: 2, memberInboundCodes: ['IN-1', 'IN-2'] } }))
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rowsWithGroup, page: 1, size: 20, total: 2, pages: 1 })
vi.mocked(getPlatformCoLoadGroupByInboundCode).mockResolvedValue({ status: status as 'LOCKED' | 'CONSUMED', memberCount: 2, members: [{ inboundCode: 'IN-1' }, { inboundCode: 'IN-2' }] })
const wrapper = mountView()
await flushPromises()
await wrapper.get(`[aria-label="查看同柜组 · 2 票 · ${status === 'LOCKED' ? '已锁定' : '已消费'}成员"]`).trigger('click')
await flushPromises()
expect(wrapper.findAll('button').some((button) => button.text() === '解除同柜组')).toBe(false)
expect(cancelPlatformCoLoadGroup).not.toHaveBeenCalled()
})
it.each([
['undefined', undefined],
['null', null],
@@ -174,6 +174,10 @@ describe('UnifiedOrdersView contract', () => {
expect(createSource).not.toContain('splitStatus')
expect(createSource).not.toContain('relation')
expect(createSource).toContain('createPlatformCoLoadGroup(selectedOrders.value.map((row) => String(row.orderId)))')
expect(source).toContain(':disabled="loading || !canCreateCoLoadGroup"')
expect(source).toContain('所选订单的平台收货仓不同,不能设为同柜组')
expect(source).toContain('所选订单的渠道不同,不能设为同柜组')
expect(source).toContain('所选订单中已有同柜组,请先解除原同柜组')
})
it('uses inbound code as the only list identifier and opens a business-only group dialog', () => {
@@ -194,6 +198,10 @@ describe('UnifiedOrdersView contract', () => {
expect(source).not.toContain('visibleOrderNo')
expect(source).not.toContain('<th>订单编码</th>')
expect(source).not.toContain('客户 ID')
expect(source).toContain("coLoadDetailSummary?.status === 'ACTIVE'")
expect(source).toContain('解除同柜组')
expect(source).toContain('cancelPlatformCoLoadGroup(inboundCode')
expect(source).toContain('<ApiErrorMeta :details="coLoadCancelError"')
})
it('does not expose a generic relationship probe in list rows', () => {
@@ -16,7 +16,7 @@ import {
type UnifiedOrderMainStatus,
type UnifiedOrderResource,
} from '@/modules/unified-orders/api/unifiedOrders'
import { createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode, type CoLoadBusinessSummary, type CoLoadGroup } from '@/modules/unified-orders/api/coLoadGroups'
import { cancelPlatformCoLoadGroup, createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode, type CoLoadBusinessSummary, type CoLoadGroup } from '@/modules/unified-orders/api/coLoadGroups'
import OrderFulfillmentTreePanel from '@/modules/unified-orders/components/OrderFulfillmentTreePanel.vue'
import InboundFeeDetailManager from '@/modules/unified-orders/components/InboundFeeDetailManager.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
@@ -131,6 +131,8 @@ const coLoadDetailInboundCode = ref('')
const coLoadDetailSummary = ref<CoLoadBusinessSummary | null>(null)
const coLoadDetailLoading = ref(false)
const coLoadDetailError = ref<ApiErrorDetails | null>(null)
const coLoadCancelError = ref<ApiErrorDetails | null>(null)
const coLoadCancelSaving = ref(false)
const coLoadSaving = ref(false)
const coLoadError = ref<ApiErrorDetails | null>(null)
const approvalOrder = ref<UnifiedOrderResource | null>(null)
@@ -813,6 +815,19 @@ function handleOrderSelectionChange(rows: UnifiedOrderResource[]) {
selectedApprovalOrders.value = rows.filter(canApprove)
}
const coLoadSelectionProblem = computed(() => {
if (selectedOrders.value.length < 2) return null
if (selectedOrders.value.some((row) => row.coLoadGroup)) return '所选订单中已有同柜组,请先解除原同柜组'
const warehouseIds = selectedOrders.value.map((row) => String(row.receivingWarehouseId || '').trim())
if (warehouseIds.some((value) => !value)) return '所选订单缺少平台收货仓,请刷新后重试'
if (new Set(warehouseIds).size > 1) return '所选订单的平台收货仓不同,不能设为同柜组'
const channels = selectedOrders.value.map((row) => String(row.channelName || '').trim())
if (channels.some((value) => !value)) return '所选订单缺少渠道,请刷新后重试'
if (new Set(channels).size > 1) return '所选订单的渠道不同,不能设为同柜组'
return null
})
const canCreateCoLoadGroup = computed(() => selectedOrders.value.length >= 2 && !coLoadSelectionProblem.value)
function coLoadGroup(row: UnifiedOrderResource) {
return row.coLoadGroup || null
}
@@ -860,18 +875,22 @@ function openCoLoadDetail(row: UnifiedOrderResource) {
coLoadDetailInboundCode.value = row.inboundCode?.trim() || ''
coLoadDetailOpen.value = true
coLoadDetailSummary.value = null
coLoadCancelError.value = null
coLoadDetailError.value = coLoadDetailInboundCode.value ? null : getApiErrorDetails(new Error('同柜组成员入仓码尚未返回'), '同柜组成员入仓码尚未返回,请刷新后重试')
if (coLoadDetailInboundCode.value) void loadCoLoadDetail()
}
function openCoLoadCreate() {
if (selectedOrders.value.length < 2) return
if (!canCreateCoLoadGroup.value) {
if (coLoadSelectionProblem.value) ElMessage.warning(coLoadSelectionProblem.value)
return
}
coLoadError.value = null
coLoadCreateOpen.value = true
}
async function createCoLoadGroup() {
if (selectedOrders.value.length < 2 || coLoadSaving.value) return
if (!canCreateCoLoadGroup.value || coLoadSaving.value) return
coLoadSaving.value = true
coLoadError.value = null
try {
@@ -886,6 +905,33 @@ async function createCoLoadGroup() {
}
}
async function cancelCoLoadGroup() {
const inboundCode = coLoadDetailInboundCode.value
if (!inboundCode || coLoadDetailSummary.value?.status !== 'ACTIVE' || coLoadCancelSaving.value) return
try {
await ElMessageBox.confirm(
'确认解除整个同柜组吗?解除后这些订单可重新选择成员建组;不会拆分订单或子单。',
'解除同柜组',
{ confirmButtonText: '确认解除', cancelButtonText: '取消', type: 'warning' },
)
} catch (error) {
if (error === 'cancel' || error === 'close') return
throw error
}
coLoadCancelSaving.value = true
coLoadCancelError.value = null
try {
await cancelPlatformCoLoadGroup(inboundCode, '重新调整同柜组成员')
coLoadDetailOpen.value = false
ElMessage.success('同柜组已整组解除')
await loadOrders()
} catch (error) {
coLoadCancelError.value = getApiErrorDetails(error, '解除同柜组失败')
} finally {
coLoadCancelSaving.value = false
}
}
async function batchApproveOrders() {
const selected = [...selectedApprovalOrders.value]
if (!selected.length || batchApprovalSaving.value) return
@@ -1372,7 +1418,10 @@ onBeforeUnmount(() => {
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table-v2" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" row-key="orderId" border stripe empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleOrderSelectionChange">
<template #toolbar-actions>
<el-button v-if="can('platform:order:approve')" type="success" :loading="batchApprovalSaving" :disabled="loading || !selectedApprovalOrders.length" @click="batchApproveOrders"><CircleCheck :size="16" />批量审核通过<span v-if="selectedApprovalOrders.length">({{ selectedApprovalOrders.length }})</span></el-button>
<el-button v-if="can('platform:firstleg:smart-loading:assign')" type="primary" plain :loading="coLoadSaving" :disabled="loading || selectedOrders.length < 2" @click="openCoLoadCreate"><Link2 :size="16" />设为同柜不可拆组<span v-if="selectedOrders.length">({{ selectedOrders.length }})</span></el-button>
<div v-if="can('platform:firstleg:smart-loading:assign')" class="coload-toolbar-action">
<el-button type="primary" plain :loading="coLoadSaving" :disabled="loading || !canCreateCoLoadGroup" :title="coLoadSelectionProblem || undefined" @click="openCoLoadCreate"><Link2 :size="16" />设为同柜不可拆组<span v-if="selectedOrders.length">({{ selectedOrders.length }})</span></el-button>
<small v-if="selectedOrders.length >= 2 && coLoadSelectionProblem" role="status">{{ coLoadSelectionProblem }}</small>
</div>
<el-button v-if="can('platform:firstleg:smart-loading:list')" type="primary" @click="smartLoadingOpen = true"><PackageOpen :size="16" />智能装柜</el-button>
</template>
<template #actions="{ row }">
@@ -1420,12 +1469,12 @@ onBeforeUnmount(() => {
</template>
</ListPageLayout>
<SmartLoadingDialog v-model="smartLoadingOpen" />
<AppDialog v-model="coLoadCreateOpen" title="设为同柜不可拆组" confirm-text="确认建组" :confirm-loading="coLoadSaving" :confirm-disabled="selectedOrders.length < 2" @confirm="createCoLoadGroup">
<AppDialog v-model="coLoadCreateOpen" title="设为同柜不可拆组" confirm-text="确认建组" :confirm-loading="coLoadSaving" :confirm-disabled="!canCreateCoLoadGroup" @confirm="createCoLoadGroup">
<el-alert type="warning" :closable="false" show-icon :title="`所选 ${selectedOrders.length} 票须属于同一国内仓、同一渠道,并全量进入同一柜;目的仓或目的港可以不同。资格由服务端按完整入仓片段校验。`" />
<div class="coload-selection"><span v-for="row in selectedOrders" :key="String(row.orderId)">{{ row.inboundCode || '待生成入仓码' }}</span></div>
<div v-if="coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" /></div>
</AppDialog>
<AppDialog v-model="coLoadDetailOpen" title="同柜组成员" :show-confirm="false" cancel-text="关闭" width="min(720px, 94vw)" :loading="coLoadDetailLoading">
<AppDialog v-model="coLoadDetailOpen" title="同柜组成员" :show-confirm="false" cancel-text="关闭" width="min(720px, 94vw)" :loading="coLoadDetailLoading" :confirm-loading="coLoadCancelSaving">
<LoadErrorState v-if="coLoadDetailError" title="同柜组成员加载失败" :message="coLoadDetailError.message" :loading="coLoadDetailLoading" @retry="loadCoLoadDetail"><template #details><ApiErrorMeta :details="coLoadDetailError" /></template></LoadErrorState>
<template v-else-if="coLoadDetailSummary">
<div class="coload-detail-summary"><el-tag size="small" :type="coLoadDetailSummary.status === 'ACTIVE' ? 'warning' : 'info'">{{ coLoadGroupSummary(coLoadDetailSummary) }}</el-tag><span>{{ coLoadDetailSummary.memberCount }} 个成员</span></div>
@@ -1437,7 +1486,9 @@ onBeforeUnmount(() => {
</table>
</div>
<el-empty v-else description="暂无同柜组成员" :image-size="56" />
<div v-if="coLoadCancelError" class="coload-error" role="alert"><span>{{ coLoadCancelError.message }}</span><ApiErrorMeta :details="coLoadCancelError" /></div>
</template>
<template #footer="{ close }"><div class="app-dialog__footer"><el-button :disabled="coLoadCancelSaving" @click="close">关闭</el-button><el-button v-if="coLoadDetailSummary?.status === 'ACTIVE' && can('platform:firstleg:smart-loading:assign')" type="danger" :loading="coLoadCancelSaving" @click="cancelCoLoadGroup">解除同柜组</el-button></div></template>
</AppDialog>
</template>
@@ -1522,6 +1573,8 @@ onBeforeUnmount(() => {
.coload-detail-table td strong,.coload-detail-table td span { display: block; overflow-wrap: anywhere; }
.coload-detail-table td span { color: var(--cc-muted); }
.coload-detail-table :deep(.el-button) { min-height: 24px; padding: 2px 0; }
.coload-toolbar-action { display: flex; min-width: 0; align-items: center; gap: 8px; }
.coload-toolbar-action small { max-width: 360px; overflow: hidden; color: var(--el-color-danger); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.subsequent-fulfillment { min-height: 72px; margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--cc-border); }
.subsequent-fulfillment h3 { margin: 0 0 10px; font-size: 15px; }
.fulfillment-facts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0 0 10px; border: 1px solid var(--cc-border); border-radius: 6px; overflow: hidden; }