style: 统一压感卡片阴影并精简充值/监控页交互
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m41s

对齐登录、工作台、H5、官网与充值页的轻压 inset;监控改胶囊 Tab 减闪;充值页去步骤条并收窄布局。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-24 17:03:18 +08:00
parent 07b71ca565
commit fbf1064ab8
29 changed files with 1931 additions and 1312 deletions

View File

@ -2,9 +2,14 @@
<div
ref="rootRef"
class="t-tabs"
:class="[`t-tabs--${size}`]"
:class="[`t-tabs--${size}`, { 't-tabs--dragging': isDragging }]"
role="tablist"
:aria-label="ariaLabel"
@pointerdown="onPointerDown"
@pointermove="onPointerMove"
@pointerup="onPointerUp"
@pointercancel="onPointerUp"
@click.capture="onClickCapture"
>
<span ref="pillRef" class="t-tabs-pill" aria-hidden="true" />
<button
@ -59,6 +64,14 @@ const emit = defineEmits<{
const rootRef = ref<HTMLElement | null>(null)
const pillRef = ref<HTMLElement | null>(null)
const tabRefs = new Map<string, HTMLElement>()
const isDragging = ref(false)
const DRAG_THRESHOLD = 6
let dragPointerId: number | null = null
let dragStartX = 0
let dragScrollLeft = 0
let didDrag = false
let suppressClick = false
function setTabRef(key: string, el: unknown) {
const node = el as HTMLElement | null
@ -66,6 +79,12 @@ function setTabRef(key: string, el: unknown) {
else tabRefs.delete(key)
}
function canScroll() {
const el = rootRef.value
if (!el) return false
return el.scrollWidth > el.clientWidth + 1
}
function movePill(animate: boolean) {
const pill = pillRef.value
const tab = tabRefs.get(props.modelValue)
@ -88,7 +107,21 @@ function movePill(animate: boolean) {
pill.style.width = `${width}px`
}
function scrollActiveIntoView() {
const root = rootRef.value
const tab = tabRefs.get(props.modelValue)
if (!root || !tab || !canScroll()) return
const left = tab.offsetLeft - 8
const right = tab.offsetLeft + tab.offsetWidth + 8
if (left < root.scrollLeft) {
root.scrollTo({ left: Math.max(0, left), behavior: 'smooth' })
} else if (right > root.scrollLeft + root.clientWidth) {
root.scrollTo({ left: right - root.clientWidth, behavior: 'smooth' })
}
}
function onSelect(key: string) {
if (suppressClick) return
if (key === props.modelValue) return
const item = props.items.find((i) => i.key === key)
if (item?.disabled) return
@ -96,6 +129,64 @@ function onSelect(key: string) {
emit('change', key)
}
function onPointerDown(e: PointerEvent) {
if (e.button !== 0 || !canScroll()) return
const root = rootRef.value
if (!root) return
// 仅记录起点;真正拖动超阈值后再 capture,避免吞掉点击选中
dragPointerId = e.pointerId
dragStartX = e.clientX
dragScrollLeft = root.scrollLeft
didDrag = false
isDragging.value = false
}
function onPointerMove(e: PointerEvent) {
if (dragPointerId == null || e.pointerId !== dragPointerId) return
const root = rootRef.value
if (!root) return
const dx = e.clientX - dragStartX
if (!didDrag) {
if (Math.abs(dx) < DRAG_THRESHOLD) return
didDrag = true
isDragging.value = true
try {
root.setPointerCapture(e.pointerId)
} catch {
/* ignore */
}
}
root.scrollLeft = dragScrollLeft - dx
}
function onPointerUp(e: PointerEvent) {
if (dragPointerId == null || e.pointerId !== dragPointerId) return
const root = rootRef.value
const wasDragging = didDrag
if (root && wasDragging) {
try {
root.releasePointerCapture(e.pointerId)
} catch {
/* ignore */
}
}
dragPointerId = null
didDrag = false
isDragging.value = false
if (wasDragging) {
suppressClick = true
window.setTimeout(() => {
suppressClick = false
}, 0)
}
}
function onClickCapture(e: MouseEvent) {
if (!suppressClick) return
e.preventDefault()
e.stopPropagation()
}
function onWindowResize() {
movePill(false)
}
@ -103,7 +194,10 @@ function onWindowResize() {
let resizeObserver: ResizeObserver | null = null
onMounted(() => {
nextTick(() => movePill(false))
nextTick(() => {
movePill(false)
scrollActiveIntoView()
})
if (typeof ResizeObserver !== 'undefined' && rootRef.value) {
resizeObserver = new ResizeObserver(() => movePill(false))
resizeObserver.observe(rootRef.value)
@ -122,6 +216,7 @@ watch(
async () => {
await nextTick()
movePill(true)
scrollActiveIntoView()
}
)
@ -130,6 +225,7 @@ watch(
async () => {
await nextTick()
movePill(false)
scrollActiveIntoView()
}
)
@ -162,10 +258,21 @@ defineExpose({ syncPill: () => movePill(false) })
overflow-y: hidden;
scrollbar-width: none;
box-sizing: border-box;
touch-action: pan-x;
user-select: none;
&::-webkit-scrollbar {
display: none;
}
&--dragging {
cursor: grabbing;
.t-tab {
pointer-events: none;
cursor: grabbing;
}
}
}
.t-tab {

View File

@ -423,7 +423,9 @@ export default {
'warehouse.inbound.tip.currentWarehouse': 'Notice belongs to the current warehouse; switch it from the top bar',
'warehouse.inbound.tip.markReference': 'Optional; usually the tracking number or master mark for receiving checks',
'warehouse.inbound.tip.inboundType': 'Normal or return inbound; used for later reporting',
'warehouse.inbound.tip.boxSkuDetail': 'Maintain SKUs and planned qty per box; add one box or five at once',
'warehouse.inbound.tip.boxSkuDetail': 'Maintain SKUs and planned qty per box; “Copy & add” duplicates weight, size, SKU and planned qty from the current box',
'warehouse.inbound.tip.copyAddBox': 'Copies weight, size, SKU and planned qty from the selected box',
'warehouse.inbound.form.batchAddCount': 'Boxes to add',
'warehouse.inbound.tip.markBoxNo': 'Per-box mark / box number (optional)',
'warehouse.inbound.tip.skuLines': 'Select at least one SKU per box; a blank row is added after selection',
'warehouse.inbound.form.markReference': 'Mark Ref',
@ -449,6 +451,9 @@ export default {
'warehouse.inbound.form.autoAddSkuRow': 'A new row is added after you select a SKU',
'warehouse.inbound.actions.addBox': 'Add box',
'warehouse.inbound.actions.addBox5': 'Add 5 boxes',
'warehouse.inbound.actions.addBoxN': 'Add {n} boxes',
'warehouse.inbound.actions.copyAddBox': 'Copy & add',
'warehouse.inbound.actions.copyAddBoxTitle': 'Copy current box',
'warehouse.inbound.actions.removeBox': 'Remove boxes',
'warehouse.inbound.actions.removeCurrentBox': 'Remove current box',
'warehouse.inbound.actions.addSku': 'Add SKU line',
@ -458,7 +463,7 @@ export default {
'warehouse.inbound.drawer.detail': 'Inbound Detail',
'warehouse.inbound.box': 'Box',
'warehouse.inbound.line.skuCode': 'SKU',
'warehouse.inbound.line.plannedQty': 'Planned',
'warehouse.inbound.line.plannedQty': 'Planned Qty',
'warehouse.inbound.line.receivedQty': 'Received',
'warehouse.inbound.line.binLocation': 'Putaway Bin',
'warehouse.inbound.form.selectBin': 'Select bin',
@ -4666,7 +4671,7 @@ export default {
// User - Recharge
'user.recharge.deposit.panel.subtitle': 'Fund Recharge',
'user.recharge.deposit.panel.title': 'Complete Recharge in 3 Steps',
'user.recharge.deposit.panel.title': 'Account Recharge',
'user.recharge.deposit.panel.desc': 'Select Amount · Upload Voucher · Wait for Review',
'user.recharge.deposit.steps.selectAmount.title': 'Select Amount',
'user.recharge.deposit.steps.selectAmount.desc': 'Select amount and confirm payment method',
@ -4683,10 +4688,13 @@ export default {
'user.recharge.deposit.tips.item2': 'Upload clear and complete payment screenshot',
'user.recharge.deposit.tips.item3': 'SMS/In-site message reminder after funds arrive',
'user.recharge.deposit.form.currentAmount': 'Current recharge amount',
'user.recharge.deposit.form.stepChip': 'Step {step} · {tip}',
'user.recharge.deposit.form.stepChip': 'Step {step}/3',
'user.recharge.deposit.form.selectAmount': 'Select Amount',
'user.recharge.deposit.form.custom': 'Custom',
'user.recharge.deposit.form.inputAmount': 'Enter Amount',
'user.recharge.deposit.form.customPlaceholder': 'Enter recharge amount',
'user.recharge.deposit.form.customConfirm': 'Confirm',
'user.recharge.deposit.form.customCancel': 'Cancel',
'user.recharge.deposit.form.paymentMethod': 'Payment Method',
'user.recharge.deposit.form.qrTip': 'Click QR code to zoom in and view details',
'user.recharge.deposit.form.uploadVoucher': 'Upload Voucher',

View File

@ -423,7 +423,9 @@ export default {
'warehouse.inbound.tip.currentWarehouse': '预报归属当前仓库;可在顶部切换仓库',
'warehouse.inbound.tip.markReference': '选填;一般填物流快递单号或主唛头,便于到仓核对',
'warehouse.inbound.tip.inboundType': '普通入库或退货入库,影响后续统计分类',
'warehouse.inbound.tip.boxSkuDetail': '按箱维护 SKU 与计划数量;可加箱或一次加 5 箱',
'warehouse.inbound.tip.boxSkuDetail': '按箱维护 SKU 与计划数量;「复制加箱」可按当前箱批量复制重量、尺寸、SKU 与计划',
'warehouse.inbound.tip.copyAddBox': '按当前选中箱复制重量、尺寸、SKU 与计划数量',
'warehouse.inbound.form.batchAddCount': '添加箱数',
'warehouse.inbound.tip.markBoxNo': '单箱唛头/箱号,选填',
'warehouse.inbound.tip.skuLines': '每箱至少选一个 SKU;选完后自动新增空行',
'warehouse.inbound.form.markReference': '唛头',
@ -449,6 +451,9 @@ export default {
'warehouse.inbound.form.autoAddSkuRow': '选择 SKU 后会自动新增下一行',
'warehouse.inbound.actions.addBox': '加箱',
'warehouse.inbound.actions.addBox5': '一次加 5 箱',
'warehouse.inbound.actions.addBoxN': '添加 {n} 箱',
'warehouse.inbound.actions.copyAddBox': '复制加箱',
'warehouse.inbound.actions.copyAddBoxTitle': '复制当前箱',
'warehouse.inbound.actions.removeBox': '移除箱子',
'warehouse.inbound.actions.removeCurrentBox': '删除当前箱',
'warehouse.inbound.actions.addSku': '选择库内 SKU',
@ -458,7 +463,7 @@ export default {
'warehouse.inbound.drawer.detail': '入库详情',
'warehouse.inbound.box': '箱',
'warehouse.inbound.line.skuCode': 'SKU',
'warehouse.inbound.line.plannedQty': '计划',
'warehouse.inbound.line.plannedQty': '计划数量',
'warehouse.inbound.line.receivedQty': '实收',
'warehouse.inbound.line.binLocation': '上架库位',
'warehouse.inbound.form.selectBin': '请选择库位',
@ -4690,7 +4695,7 @@ export default {
// 用户 - 充值
'user.recharge.deposit.panel.subtitle': '资金充值',
'user.recharge.deposit.panel.title': '3 步完成充值',
'user.recharge.deposit.panel.title': '账户充值',
'user.recharge.deposit.panel.desc': '选择金额 · 上传凭证 · 等待审核',
'user.recharge.deposit.steps.selectAmount.title': '选择充值金额',
'user.recharge.deposit.steps.selectAmount.desc': '选择金额并确认支付方式',
@ -4707,10 +4712,13 @@ export default {
'user.recharge.deposit.tips.item2': '上传清晰完整的支付截图',
'user.recharge.deposit.tips.item3': '资金到账后会短信/站内信提醒',
'user.recharge.deposit.form.currentAmount': '当前充值金额',
'user.recharge.deposit.form.stepChip': '步骤 {step} · {tip}',
'user.recharge.deposit.form.stepChip': '步骤 {step}/3',
'user.recharge.deposit.form.selectAmount': '选择金额',
'user.recharge.deposit.form.custom': '自定义',
'user.recharge.deposit.form.inputAmount': '输入金额',
'user.recharge.deposit.form.customPlaceholder': '请输入充值金额',
'user.recharge.deposit.form.customConfirm': '确定',
'user.recharge.deposit.form.customCancel': '取消',
'user.recharge.deposit.form.paymentMethod': '支付方式',
'user.recharge.deposit.form.qrTip': '点击二维码可放大查看详情',
'user.recharge.deposit.form.uploadVoucher': '上传凭证',

View File

@ -204,13 +204,16 @@ onMounted(() => {
}
.h5-login-card {
background: var(--app-acrylic-bg);
border: 1px solid var(--app-acrylic-border);
/* 对齐官网/PC 登录 pressed:白底 + inset 凹陷 */
background: #ffffff;
border: none;
border-radius: 20px;
padding: 28px 20px 22px;
box-shadow: none;
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
box-shadow:
inset 0 2px 8px rgba(15, 23, 42, 0.07),
inset 0 1px 1px rgba(15, 23, 42, 0.04);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.h5-login-card__brand {
@ -262,11 +265,27 @@ onMounted(() => {
.h5-login-form :deep(.arco-input-wrapper),
.h5-login-form :deep(.arco-input-password) {
border-radius: 12px;
background: var(--app-acrylic-bg);
border: 1px solid var(--app-acrylic-border);
background: #f0f3f4;
border: none;
min-height: 48px;
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
backdrop-filter: none;
-webkit-backdrop-filter: none;
box-shadow: inset 0 2px 8px rgba(15, 23, 42, 0.08);
transition: box-shadow 0.2s ease, background 0.2s ease;
&:hover {
background: #f0f3f4;
box-shadow: inset 0 3px 10px rgba(15, 23, 42, 0.1);
}
&.arco-input-wrapper-focus,
&.arco-input-focus {
background: #f0f3f4;
border: none;
box-shadow:
inset 0 3px 10px rgba(15, 23, 42, 0.1),
0 0 0 3px color-mix(in srgb, var(--app-primary-color) 12%, transparent);
}
}
.h5-login-error {

View File

@ -468,8 +468,9 @@ onMounted(() => {
height: 32px;
padding: 0 12px;
border-radius: 8px;
border: 1px solid var(--h5-border, #e5e6eb);
background: var(--h5-surface, #fff);
border: none;
background: var(--h5-well-bg, #f0f3f4);
box-shadow: var(--h5-well-shadow, inset 0 2px 5px rgba(15, 23, 42, 0.08));
color: var(--h5-text-2, #4e5969);
font-size: 13px;
cursor: pointer;
@ -518,8 +519,8 @@ onMounted(() => {
padding: 14px;
border-radius: var(--h5-radius, 16px);
background: var(--h5-surface, #fff);
border: 1px solid var(--h5-border-soft, #f2f3f5);
box-shadow: var(--h5-shadow, 0 4px 16px rgba(15, 23, 42, 0.05));
border: none;
box-shadow: var(--h5-shadow);
}
.h5-mon__section-head {
@ -582,8 +583,9 @@ onMounted(() => {
min-height: 76px;
padding: 12px 14px;
border-radius: 12px;
border: 1px solid var(--h5-border, #e5e6eb);
background: transparent;
border: none;
background: var(--h5-well-bg, #f0f3f4);
box-shadow: var(--h5-well-shadow, inset 0 2px 5px rgba(15, 23, 42, 0.08));
text-align: left;
cursor: default;
-webkit-tap-highlight-color: transparent;
@ -770,8 +772,9 @@ onMounted(() => {
gap: 6px;
padding: 12px 8px;
border-radius: 12px;
background: transparent;
border: 1px solid var(--h5-border, #e5e6eb);
background: var(--h5-well-bg, #f0f3f4);
border: none;
box-shadow: var(--h5-well-shadow, inset 0 2px 5px rgba(15, 23, 42, 0.08));
text-align: center;
span {

View File

@ -12,8 +12,16 @@
--h5-border-soft: #f2f3f5;
--h5-radius: 16px;
--h5-radius-sm: 12px;
--h5-shadow: 0 4px 16px rgba(15, 23, 42, 0.05);
--h5-shadow-lift: 0 8px 24px rgba(15, 23, 42, 0.08);
/* pressed:外层白卡 / 内层浅井(阴影偏淡) */
--h5-shadow:
inset 0 2px 8px rgba(15, 23, 42, 0.07),
inset 0 1px 1px rgba(15, 23, 42, 0.04);
--h5-shadow-lift:
inset 0 3px 10px rgba(15, 23, 42, 0.09),
inset 0 1px 2px rgba(15, 23, 42, 0.05);
--h5-well-bg: #f7f8fa;
--h5-well-shadow: inset 0 2px 4px rgba(15, 23, 42, 0.06);
--h5-well-shadow-deep: inset 0 3px 6px rgba(15, 23, 42, 0.08);
--h5-warning: #ff7d00;
--h5-warning-bg: #fff7e8;
--h5-danger: #f53f3f;

View File

@ -479,8 +479,9 @@ onMounted(() => {
height: 32px;
padding: 0 12px;
border-radius: 8px;
border: 1px solid var(--h5-border, #e5e6eb);
background: var(--h5-surface, #fff);
border: none;
background: var(--h5-well-bg, #f0f3f4);
box-shadow: var(--h5-well-shadow, inset 0 2px 5px rgba(15, 23, 42, 0.08));
color: var(--h5-text-2, #4e5969);
font-size: 13px;
cursor: pointer;
@ -491,7 +492,7 @@ onMounted(() => {
}
&:active:not(:disabled) {
background: #f7f8fa;
box-shadow: var(--h5-well-shadow-deep, inset 0 3px 8px rgba(15, 23, 42, 0.1));
}
}
@ -511,8 +512,8 @@ onMounted(() => {
padding: 14px;
border-radius: var(--h5-radius, 16px);
background: var(--h5-surface, #fff);
border: 1px solid var(--h5-border-soft, #f2f3f5);
box-shadow: var(--h5-shadow, 0 4px 16px rgba(15, 23, 42, 0.05));
border: none;
box-shadow: var(--h5-shadow);
}
.h5-wp__section-head {
@ -570,9 +571,10 @@ onMounted(() => {
align-items: center;
min-height: 76px;
padding: 12px 14px;
border: 1px solid var(--h5-border, #e5e6eb);
border: none;
border-radius: 12px;
background: transparent;
background: var(--h5-well-bg, #f0f3f4);
box-shadow: var(--h5-well-shadow, inset 0 2px 5px rgba(15, 23, 42, 0.08));
text-align: left;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
@ -660,8 +662,9 @@ onMounted(() => {
min-height: 64px;
padding: 12px 14px;
border-radius: 12px;
border: 1px solid var(--h5-border, #e5e6eb);
background: transparent;
border: none;
background: var(--h5-well-bg, #f0f3f4);
box-shadow: var(--h5-well-shadow, inset 0 2px 5px rgba(15, 23, 42, 0.08));
}
.h5-wp__stat-main {

View File

@ -75,15 +75,11 @@ function clearPriceContext() {
gap: 10px;
padding: 12px 12px 12px 14px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.78);
background: linear-gradient(
155deg,
rgba(255, 255, 255, 0.92) 0%,
rgba(255, 255, 255, 0.7) 100%
);
border: none;
background: #ffffff;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.9),
0 6px 16px color-mix(in srgb, var(--app-primary-color) 10%, transparent);
inset 0 2px 8px rgba(15, 23, 42, 0.07),
inset 0 1px 1px rgba(15, 23, 42, 0.04);
flex-shrink: 0;
}
@ -150,13 +146,13 @@ function clearPriceContext() {
position: relative;
flex: 1;
min-height: 0;
border-radius: 18px;
border-radius: 16px;
overflow: hidden;
background: rgba(255, 255, 255, 0.55);
border: 1px solid rgba(255, 255, 255, 0.78);
background: #ffffff;
border: none;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.85),
0 8px 24px rgba(15, 23, 42, 0.05);
inset 0 2px 8px rgba(15, 23, 42, 0.07),
inset 0 1px 1px rgba(15, 23, 42, 0.04);
display: flex;
flex-direction: column;

View File

@ -439,13 +439,16 @@ onMounted(async () => {
}
.h5-login-card {
background: var(--app-acrylic-bg);
border: 1px solid var(--app-acrylic-border);
/* 对齐官网/PC 登录 pressed:白底 + inset 凹陷 */
background: #ffffff;
border: none;
border-radius: 20px;
padding: 28px 20px 22px;
box-shadow: none;
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
box-shadow:
inset 0 2px 8px rgba(15, 23, 42, 0.07),
inset 0 1px 1px rgba(15, 23, 42, 0.04);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
/* 品牌放进卡片:一个完整登录单元(Clerk / Vercel 鉴权卡常见) */
@ -504,22 +507,26 @@ onMounted(async () => {
.h5-login-form :deep(.arco-input-wrapper),
.h5-login-form :deep(.arco-input-password) {
border-radius: 12px;
background: var(--app-acrylic-bg);
border: 1px solid var(--app-acrylic-border);
background: #f0f3f4;
border: none;
min-height: 48px;
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
transition:
border-color 0.2s var(--login-ease),
box-shadow 0.2s var(--login-ease),
background 0.2s var(--login-ease);
backdrop-filter: none;
-webkit-backdrop-filter: none;
box-shadow: inset 0 2px 8px rgba(15, 23, 42, 0.08);
transition: box-shadow 0.2s var(--login-ease), background 0.2s var(--login-ease);
&:hover {
background: #f0f3f4;
box-shadow: inset 0 3px 10px rgba(15, 23, 42, 0.1);
}
&:hover,
&.arco-input-wrapper-focus,
&.arco-input-focus {
background: var(--app-acrylic-hover-bg);
border-color: var(--app-acrylic-border-strong);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--app-primary-color) 14%, transparent);
background: #f0f3f4;
border: none;
box-shadow:
inset 0 3px 10px rgba(15, 23, 42, 0.1),
0 0 0 3px color-mix(in srgb, var(--app-primary-color) 12%, transparent);
}
}

View File

@ -1406,10 +1406,14 @@ onMounted(() => {
.quote-form {
background: #fff;
border-radius: 18px;
/* 对齐工作台 pressed:白底 + inset 凹陷 */
background: #ffffff;
border: none;
border-radius: 16px;
padding: 18px 16px 14px;
box-shadow: 0 8px 28px rgba(15, 23, 42, 0.06);
box-shadow:
inset 0 2px 8px rgba(15, 23, 42, 0.07),
inset 0 1px 1px rgba(15, 23, 42, 0.04);
text-align: left;
&.is-collapsed {
@ -2130,7 +2134,7 @@ onMounted(() => {
align-items: center;
justify-content: center;
gap: 8px;
box-shadow: 0 8px 20px color-mix(in srgb, var(--app-primary-color) 28%, transparent);
box-shadow: none;
&:active {
transform: scale(0.985);

View File

@ -1,29 +1,29 @@
<template>
<div ref="logisticsMonitorRoot" class="logistics-monitor-container">
<div class="logistics-monitor-card">
<div class="logistics-monitor-header">
<h2 class="logistics-monitor-title">{{ $t('monitor.logistics.pageTitle') }}</h2>
<div class="monitor-topbar">
<h2 class="logistics-monitor-title">{{ $t('monitor.logistics.pageTitle') }}</h2>
<SlidingTabs
v-model="monitorTab"
class="monitor-top-tabs"
:items="monitorTabItems"
:aria-label="$t('monitor.logistics.pageTitle')"
/>
<div class="monitor-topbar__spacer" />
<div v-show="monitorTab === 'order'" class="monitor-ops-toolbar">
<span class="monitor-ops-toolbar__label">{{ $t('monitor.logistics.filter.opsPeriod') }}</span>
<a-radio-group v-model="opsDimension" type="button" size="small">
<a-radio value="day">{{ $t('monitor.logistics.filter.periodDay') }}</a-radio>
<a-radio value="week">{{ $t('monitor.logistics.filter.periodWeek') }}</a-radio>
<a-radio value="month">{{ $t('monitor.logistics.filter.periodMonth') }}</a-radio>
</a-radio-group>
</div>
</div>
<div class="logistics-monitor-body">
<a-spin :loading="overviewLoading" style="width: 100%">
<a-tabs
v-model:active-key="monitorTab"
type="rounded"
:animation="false"
:justify="true"
class="monitor-top-tabs"
>
<a-tab-pane key="order" :title="$t('monitor.logistics.tab.order')">
<div class="monitor-ops-toolbar">
<span class="monitor-ops-toolbar__label">{{ $t('monitor.logistics.filter.opsPeriod') }}</span>
<a-radio-group v-model="opsDimension" type="button" size="small">
<a-radio value="day">{{ $t('monitor.logistics.filter.periodDay') }}</a-radio>
<a-radio value="week">{{ $t('monitor.logistics.filter.periodWeek') }}</a-radio>
<a-radio value="month">{{ $t('monitor.logistics.filter.periodMonth') }}</a-radio>
</a-radio-group>
</div>
<div class="logistics-monitor-body">
<a-spin :loading="overviewLoading" style="width: 100%; height: 100%">
<div class="monitor-shell">
<div class="monitor-tab-panels">
<div v-show="monitorTab === 'order'" class="monitor-tab-panel">
<a-row :gutter="[12, 12]" align="stretch" class="logistics-monitor-stats">
<a-col
v-for="card in opsStatCards"
@ -34,16 +34,23 @@
:lg="8"
:xl="8"
>
<a-card hoverable class="stat-card" :body-style="{ padding: '12px', height: '100%' }">
<a-card hoverable class="stat-card" :body-style="{ padding: '12px 14px 10px', height: '100%' }">
<div class="stat-item">
<div class="stat-item__head">
<div class="stat-label">{{ $t(card.labelKey) }}</div>
<div class="stat-item__icon stat-item__icon--primary">
<img v-if="card.iconSrc" :src="card.iconSrc" alt="" class="stat-item__icon-img" />
<icon-bar-chart v-else />
<div class="stat-item__main">
<div class="stat-item__meta">
<img
v-if="card.iconSrc"
:src="card.iconSrc"
alt=""
class="stat-item__avatar"
/>
<icon-bar-chart v-else class="stat-item__avatar-icon" />
<span class="stat-label">{{ $t(card.labelKey) }}</span>
</div>
<div class="stat-item__body">
<span class="stat-value">{{ card.value }}</span>
</div>
</div>
<div class="stat-value">{{ card.value }}</div>
<div v-if="card.growth.show" class="stat-footer">
<div class="stat-trend" :class="card.growth.up ? 'up' : 'down'">
<icon-arrow-up v-if="card.growth.up" />
@ -67,20 +74,24 @@
:lg="6"
:xl="6"
>
<a-card hoverable class="stat-card" :body-style="{ padding: '12px', height: '100%' }">
<a-card hoverable class="stat-card" :body-style="{ padding: '12px 14px 10px', height: '100%' }">
<div class="stat-item">
<div class="stat-item__head">
<div class="stat-label">{{ $t(card.labelKey) }}</div>
<div
class="stat-item__icon"
:class="card.warning ? 'stat-item__icon--warning' : 'stat-item__icon--primary'"
>
<img v-if="card.iconSrc" :src="card.iconSrc" alt="" class="stat-item__icon-img" />
<icon-bar-chart v-else />
<div class="stat-item__main">
<div class="stat-item__meta">
<img
v-if="card.iconSrc"
:src="card.iconSrc"
alt=""
class="stat-item__avatar"
/>
<icon-bar-chart v-else class="stat-item__avatar-icon" />
<span class="stat-label">{{ $t(card.labelKey) }}</span>
</div>
<div class="stat-item__body">
<span class="stat-value" :class="{ 'stat-value--warning': card.warning }">
{{ card.value }}
</span>
</div>
</div>
<div class="stat-value" :class="{ 'stat-value--warning': card.warning }">
{{ card.value }}
</div>
</div>
</a-card>
@ -100,9 +111,9 @@
</a-card>
</a-col>
</a-row>
</a-tab-pane>
</div>
<a-tab-pane key="customer" :title="$t('monitor.logistics.tab.customer')">
<div v-show="monitorTab === 'customer'" class="monitor-tab-panel">
<a-row :gutter="[12, 12]" align="stretch" class="logistics-monitor-stats">
<a-col
v-for="card in customerStatCards"
@ -113,19 +124,29 @@
:lg="12"
:xl="6"
>
<a-card hoverable class="stat-card" :body-style="{ padding: '12px', height: '100%' }">
<a-card hoverable class="stat-card" :body-style="{ padding: '12px 14px 10px', height: '100%' }">
<div class="stat-item">
<div class="stat-item__head">
<div class="stat-label">{{ $t(card.labelKey) }}</div>
<div
class="stat-item__icon"
:class="card.warning ? 'stat-item__icon--warning' : card.icon === 'online' ? 'stat-item__icon--online' : 'stat-item__icon--primary'"
>
<icon-user-group />
<div class="stat-item__main">
<div class="stat-item__meta">
<icon-user-group
class="stat-item__avatar-icon"
:class="{
'is-warning': card.warning,
'is-online': card.icon === 'online',
}"
/>
<span class="stat-label">{{ $t(card.labelKey) }}</span>
</div>
<div class="stat-item__body">
<span class="stat-value" :class="{ 'stat-value--warning': card.warning }">
{{ card.value }}
</span>
</div>
</div>
<div class="stat-value" :class="{ 'stat-value--warning': card.warning }">{{ card.value }}</div>
<div class="stat-footer">
<div
v-if="card.growth.show || card.hintKey || card.extraHintKey"
class="stat-footer"
>
<div v-if="card.growth.show" class="stat-trend" :class="card.growth.up ? 'up' : 'down'">
<icon-arrow-up v-if="card.growth.up" />
<icon-arrow-down v-else />
@ -186,9 +207,9 @@
</a-card>
</a-col>
</a-row>
</a-tab-pane>
</div>
<a-tab-pane key="track" :title="$t('monitor.logistics.tab.track')">
<div v-show="monitorTab === 'track'" class="monitor-tab-panel">
<a-row :gutter="[16, 16]" class="logistics-monitor-sign-rate-row">
<a-col :span="24">
<a-card class="chart-card">
@ -258,10 +279,10 @@
</a-card>
</a-col>
</a-row>
</a-tab-pane>
</a-tabs>
</a-spin>
</div>
</div>
</div>
</div>
</a-spin>
</div>
</div>
</template>
@ -279,6 +300,7 @@
type MonitorDimension,
type MonitorOverview,
} from '@/api/monitor';
import SlidingTabs from '@/components/SlidingTabs/index.vue';
import iconOrderNew from '@/assets/icons/workplace/order-new.svg?url';
import iconPayment from '@/assets/icons/workplace/order-payment.svg?url';
import iconGrowth from '@/assets/icons/workplace/order-growth.svg?url';
@ -290,18 +312,26 @@
const { t } = useI18n();
const monitorTabItems = computed(() => [
{ key: 'order', label: t('monitor.logistics.tab.order') },
{ key: 'customer', label: t('monitor.logistics.tab.customer') },
{ key: 'track', label: t('monitor.logistics.tab.track') },
]);
const overview = ref<MonitorOverview | null>(null);
const overviewLoading = ref(true);
const opsDimension = ref<MonitorDimension>('day');
async function loadOverview() {
overviewLoading.value = true;
/** silent:切换日/周/月时不整页 Spin,避免闪白 */
async function loadOverview(opts?: { silent?: boolean }) {
const silent = opts?.silent ?? false;
if (!silent) overviewLoading.value = true;
try {
overview.value = await getMonitorOverview(opsDimension.value);
} catch {
overview.value = null;
if (!silent) overview.value = null;
} finally {
overviewLoading.value = false;
if (!silent) overviewLoading.value = false;
await nextTick();
refreshChartsForTab(monitorTab.value);
}
@ -919,17 +949,17 @@
series: [
{
name: t('monitor.logistics.series.revenue'),
type: 'line',
smooth: true,
type: 'bar',
barMaxWidth: 28,
data: revenueValues,
itemStyle: { color: revenueColor },
itemStyle: { color: revenueColor, borderRadius: [4, 4, 0, 0] },
},
{
name: t('monitor.logistics.series.grossProfit'),
type: 'line',
smooth: true,
type: 'bar',
barMaxWidth: 28,
data: grossValues,
itemStyle: { color: grossColor },
itemStyle: { color: grossColor, borderRadius: [4, 4, 0, 0] },
},
],
},
@ -987,10 +1017,8 @@
watch(
monitorTab,
(key, prev) => {
if (prev === 'order') disposeOrderCharts();
if (prev === 'track') disposeTrackCharts();
if (prev === 'customer') disposeCustomerCharts();
(key) => {
// 面板用 v-show 常驻,切 Tab 只 resize / 补画,不 dispose,避免闪一下
nextTick(() => {
if (key === 'order') ensureOrderLineCharts();
if (key === 'track') ensureTrackCharts();
@ -1002,7 +1030,7 @@
);
watch(opsDimension, () => {
void loadOverview();
void loadOverview({ silent: true });
});
watch([filterLine, filterSignDays], () => {
@ -1027,27 +1055,22 @@
layoutResizeObserver?.disconnect();
layoutResizeObserver = null;
window.removeEventListener('resize', onResize);
orderChartInstance?.dispose();
profitChartInstance?.dispose();
signRateChartInstance?.dispose();
customerOrderTrendInstance?.dispose();
customerReceivableTrendInstance?.dispose();
pieTrackCompleteInstance?.dispose();
pieTrackTimelyInstance?.dispose();
pieOtdInstance?.dispose();
orderChartInstance = null;
profitChartInstance = null;
signRateChartInstance = null;
customerOrderTrendInstance = null;
customerReceivableTrendInstance = null;
pieTrackCompleteInstance = null;
pieTrackTimelyInstance = null;
pieOtdInstance = null;
disposeOrderCharts();
disposeTrackCharts();
disposeCustomerCharts();
});
</script>
<style scoped lang="less">
.logistics-monitor-container {
--lm-well-bg: #ffffff;
--lm-well-shadow:
inset 0 0 0 1px rgba(15, 23, 42, 0.06),
inset 0 1px 4px rgba(15, 23, 42, 0.05);
--lm-well-shadow-deep:
inset 0 0 0 1px rgba(15, 23, 42, 0.08),
inset 0 2px 6px rgba(15, 23, 42, 0.07);
box-sizing: border-box;
flex: 1;
min-height: 0;
@ -1059,26 +1082,46 @@
overflow: hidden;
}
.logistics-monitor-card {
.monitor-topbar {
display: flex;
flex-direction: column;
width: 100%;
flex: 1;
min-height: 0;
height: 100%;
max-height: 100%;
background: var(--app-card-bg);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
padding: 16px;
box-sizing: border-box;
overflow: hidden;
align-items: center;
flex-wrap: wrap;
gap: 12px 16px;
flex-shrink: 0;
margin-bottom: 12px;
min-height: 36px;
}
.logistics-monitor-header {
margin-bottom: 16px;
.monitor-topbar__spacer {
flex: 1 1 auto;
min-width: 8px;
}
.logistics-monitor-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text-1);
line-height: 32px;
white-space: nowrap;
}
.monitor-top-tabs {
flex-shrink: 0;
text-align: center;
max-width: 100%;
}
.monitor-ops-toolbar {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.monitor-ops-toolbar__label {
font-size: 13px;
color: var(--color-text-2);
white-space: nowrap;
}
.logistics-monitor-body {
@ -1088,72 +1131,41 @@
flex: 1;
min-height: 0;
overflow: hidden;
}
.monitor-top-tabs {
width: 100%;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
:deep(.arco-spin) {
display: flex;
flex-direction: column;
height: 100%;
max-height: 100%;
}
:deep(.arco-tabs) {
:deep(.arco-spin-children) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
:deep(.arco-tabs-nav) {
justify-content: flex-start;
flex-shrink: 0;
}
:deep(.arco-tabs-nav-tab-list) {
justify-content: flex-start;
flex-wrap: wrap;
}
:deep(.arco-tabs-content) {
flex: 1;
min-height: 0;
min-width: 0;
padding: 12px 0 0;
box-sizing: border-box;
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
:deep(.arco-tabs-content-list) {
height: 100%;
}
:deep(.arco-tabs-pane) {
overflow: visible;
}
}
.logistics-monitor-title {
margin: 0;
font-size: 22px;
font-weight: 600;
color: var(--color-text-1);
}
.monitor-ops-toolbar {
.monitor-shell {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
flex-direction: column;
flex: 1;
min-height: 0;
width: 100%;
}
.monitor-ops-toolbar__label {
font-size: 13px;
color: var(--color-text-2);
white-space: nowrap;
.monitor-tab-panels {
flex: 1;
min-height: 0;
min-width: 0;
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.monitor-tab-panel {
min-width: 0;
}
.monitor-queue-title {
@ -1223,12 +1235,19 @@
.stat-card {
flex: 1;
width: 100%;
min-height: 0;
min-height: 84px;
height: 100%;
display: flex;
flex-direction: column;
border: none !important;
border-radius: 14px !important;
background: var(--lm-well-bg, #ffffff) !important;
box-shadow: var(--lm-well-shadow) !important;
transition: box-shadow 0.2s ease;
:deep(.arco-card) {
height: 100%;
display: flex;
flex-direction: column;
&:hover {
background: var(--lm-well-bg, #ffffff) !important;
box-shadow: var(--lm-well-shadow-deep) !important;
}
:deep(.arco-card-body) {
@ -1236,77 +1255,89 @@
display: flex;
flex-direction: column;
box-sizing: border-box;
background: transparent;
}
}
/* 对齐工作台:左 icon+标题竖排,右数值;icon 无底框 */
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
min-height: 0;
}
.stat-item__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
margin-bottom: 4px;
min-height: 40px;
}
.stat-item__icon {
flex-shrink: 0;
margin-top: -2px;
width: 36px;
height: 36px;
border-radius: 8px;
.stat-item__main {
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
justify-content: space-between;
gap: 10px;
min-width: 0;
flex: 1;
}
.stat-item__icon-img {
width: 28px;
height: 28px;
.stat-item__meta {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6px;
min-width: 0;
}
.stat-item__avatar {
display: block;
width: 40px;
height: 40px;
object-fit: contain;
background: transparent;
border: none;
border-radius: 0;
}
.stat-item__icon--primary {
background: var(--app-primary-color-light);
.stat-item__avatar-icon {
display: inline-flex;
width: 40px;
height: 40px;
align-items: center;
justify-content: center;
font-size: 22px;
color: var(--app-primary-color);
background: transparent;
&.is-warning {
color: rgb(var(--orange-6));
}
&.is-online {
color: rgb(var(--cyan-6));
}
}
.stat-item__icon--warning {
background: rgb(var(--orange-1));
color: rgb(var(--orange-6));
}
.stat-item__icon--danger {
background: rgb(var(--red-1));
color: rgb(var(--red-6));
}
.stat-item__icon--online {
background: rgb(var(--cyan-1));
color: rgb(var(--cyan-6));
.stat-item__body {
display: flex;
align-items: baseline;
justify-content: flex-end;
flex-shrink: 0;
min-width: 0;
}
.stat-label {
flex: 1;
min-width: 0;
font-size: 13px;
line-height: 1.35;
color: var(--color-text-2);
padding-top: 2px;
padding-right: 4px;
max-width: 100%;
font-size: 12px;
line-height: 1.3;
color: var(--color-text-3);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.stat-value {
font-size: clamp(18px, 2.4vw, 22px);
font-weight: 600;
line-height: 1.25;
font-size: 28px;
font-weight: 700;
font-variant-numeric: tabular-nums;
line-height: 1;
letter-spacing: -0.02em;
color: var(--color-text-1);
word-break: break-all;
}
@ -1324,9 +1355,10 @@
min-height: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: flex-end;
gap: 4px;
padding-top: 8px;
gap: 2px;
text-align: right;
}
.stat-trend {
@ -1358,6 +1390,16 @@
.chart-card {
height: auto;
width: 100%;
border: none !important;
border-radius: 12px !important;
background: var(--lm-well-bg, #ffffff) !important;
box-shadow: var(--lm-well-shadow, inset 0 2px 5px rgba(15, 23, 42, 0.08)) !important;
:deep(.arco-card-header),
:deep(.arco-card-body) {
background: transparent;
border-color: transparent;
}
}
.chart-host {

View File

@ -240,18 +240,24 @@ onMounted(() => {
height: 28px;
padding: 0 10px;
border-radius: 8px;
border: 1px solid var(--app-acrylic-border);
background: var(--app-acrylic-bg);
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
border: none;
background: #ffffff;
box-shadow:
inset 0 0 0 1px rgba(15, 23, 42, 0.06),
inset 0 1px 4px rgba(15, 23, 42, 0.05);
backdrop-filter: none;
-webkit-backdrop-filter: none;
color: var(--aw-ink);
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: box-shadow 0.2s ease;
&:hover:not(:disabled) {
border-color: var(--app-acrylic-border-strong);
background: var(--app-acrylic-hover-bg);
background: #ffffff;
box-shadow:
inset 0 0 0 1px rgba(15, 23, 42, 0.08),
inset 0 2px 6px rgba(15, 23, 42, 0.07);
}
&:disabled {
@ -285,9 +291,27 @@ onMounted(() => {
.stat-card {
min-height: 84px;
padding: 12px 14px 10px;
border: none;
background: #ffffff;
box-shadow:
inset 0 0 0 1px rgba(15, 23, 42, 0.06),
inset 0 1px 4px rgba(15, 23, 42, 0.05);
backdrop-filter: none;
-webkit-backdrop-filter: none;
&.is-primary {
border-color: color-mix(in srgb, var(--app-primary-color) 22%, var(--app-acrylic-border));
border: none;
box-shadow:
inset 0 0 0 1px color-mix(in srgb, var(--app-primary-color) 22%, transparent),
inset 0 1px 4px rgba(15, 23, 42, 0.05);
}
&--clickable:hover {
border-color: transparent;
background: #ffffff;
box-shadow:
inset 0 0 0 1px rgba(15, 23, 42, 0.08),
inset 0 2px 6px rgba(15, 23, 42, 0.07);
}
}

View File

@ -99,6 +99,21 @@ const items = computed(() =>
margin: 0;
padding: 10px 12px 8px;
border-radius: 14px;
border: none;
background: #ffffff;
box-shadow:
inset 0 0 0 1px rgba(15, 23, 42, 0.06),
inset 0 1px 4px rgba(15, 23, 42, 0.05);
backdrop-filter: none;
-webkit-backdrop-filter: none;
&.stat-card--clickable:hover {
border-color: transparent;
background: #ffffff;
box-shadow:
inset 0 0 0 1px rgba(15, 23, 42, 0.08),
inset 0 2px 6px rgba(15, 23, 42, 0.07);
}
}
.stat-grid--inner .stat-card__value {

View File

@ -156,13 +156,28 @@ function go(path: string, query?: Record<string, string>) {
<style scoped lang="less">
.admin-workplace {
/* 对齐官网首页 pressed 卡片:无描边 + inset 凹陷 */
--aw-ink: #1a2e38;
--aw-muted: rgba(26, 46, 56, 0.68);
--aw-soft: rgba(26, 46, 56, 0.48);
--aw-edge: var(--app-acrylic-border);
--aw-shadow: none;
--aw-edge: transparent;
--aw-radius: 16px;
--aw-panel-bg: var(--app-acrylic-bg);
/* 外层面板白底压入;内卡同白 + 更轻 inset(上方灰感减淡) */
--aw-panel-bg: #ffffff;
--aw-panel-shadow:
inset 0 2px 8px rgba(15, 23, 42, 0.07),
inset 0 1px 1px rgba(15, 23, 42, 0.04);
--aw-panel-shadow-deep:
inset 0 2px 10px rgba(15, 23, 42, 0.09),
inset 0 1px 1px rgba(15, 23, 42, 0.05);
--aw-well-bg: #ffffff;
/* 轻压 + 一圈细边,避免只压顶导致底边“断线” */
--aw-well-shadow:
inset 0 0 0 1px rgba(15, 23, 42, 0.06),
inset 0 1px 4px rgba(15, 23, 42, 0.05);
--aw-well-shadow-deep:
inset 0 0 0 1px rgba(15, 23, 42, 0.08),
inset 0 2px 6px rgba(15, 23, 42, 0.07);
width: 100%;
height: 100%;
@ -217,13 +232,14 @@ function go(path: string, query?: Record<string, string>) {
}
.aw-panel {
border: 1px solid var(--aw-edge);
border: none;
border-radius: var(--aw-radius);
background: var(--aw-panel-bg);
box-shadow: var(--aw-shadow);
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
box-shadow: var(--aw-panel-shadow);
backdrop-filter: none;
-webkit-backdrop-filter: none;
min-width: 0;
transition: box-shadow 0.2s ease;
}
.aw-panel--hero {
@ -319,7 +335,6 @@ function go(path: string, query?: Record<string, string>) {
.aw-link {
--tone: var(--app-primary-color);
--tone-edge: var(--app-acrylic-border);
--tone-strong: var(--app-primary-color);
display: flex;
@ -328,19 +343,18 @@ function go(path: string, query?: Record<string, string>) {
min-height: 56px;
padding: 8px 12px;
border-radius: 12px;
border: 1px solid var(--tone-edge);
background: var(--app-acrylic-bg);
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
box-shadow: none;
border: none;
background: var(--aw-well-bg);
backdrop-filter: none;
-webkit-backdrop-filter: none;
box-shadow: var(--aw-well-shadow);
text-align: left;
cursor: pointer;
transition: border-color 0.2s ease, background 0.2s ease;
transition: box-shadow 0.2s ease, background 0.2s ease;
&:hover {
border-color: var(--app-acrylic-border-strong);
background: var(--app-acrylic-hover-bg);
box-shadow: none;
background: var(--aw-well-bg);
box-shadow: var(--aw-well-shadow-deep);
}
&:focus-visible {
@ -351,31 +365,26 @@ function go(path: string, query?: Record<string, string>) {
.aw-tone--ember {
--tone: #c45a3a;
--tone-edge: rgba(196, 90, 58, 0.35);
--tone-strong: #b84a2e;
}
.aw-tone--apricot {
--tone: #d9924a;
--tone-edge: rgba(217, 146, 74, 0.35);
--tone-strong: #c47d32;
}
.aw-tone--honey {
--tone: #d4a05c;
--tone-edge: rgba(212, 160, 92, 0.35);
--tone-strong: #c08a3e;
}
.aw-tone--clay {
--tone: #c17a52;
--tone-edge: rgba(193, 122, 82, 0.35);
--tone-strong: #a8663f;
}
.aw-tone--sand {
--tone: #c9a07a;
--tone-edge: rgba(201, 160, 122, 0.35);
--tone-strong: #b08a62;
}
@ -420,6 +429,68 @@ function go(path: string, query?: Record<string, string>) {
border-radius: 0;
}
/* 内层统计卡 / 世界时钟 / 刷新钮:浅灰井 + inset,嵌进白面板 */
.admin-workplace :deep(.stat-card) {
border: none;
background: var(--aw-well-bg);
box-shadow: var(--aw-well-shadow);
backdrop-filter: none;
-webkit-backdrop-filter: none;
transition: box-shadow 0.2s ease, background 0.2s ease;
}
.admin-workplace :deep(.stat-card--clickable:hover) {
border-color: transparent;
background: var(--aw-well-bg);
box-shadow: var(--aw-well-shadow-deep);
}
.admin-workplace :deep(.stat-card.is-primary) {
border: none;
box-shadow:
inset 0 0 0 1px color-mix(in srgb, var(--app-primary-color) 22%, transparent),
inset 0 1px 4px rgba(15, 23, 42, 0.05);
}
.admin-workplace :deep(.aw-ops__refresh),
.admin-workplace :deep(.care-clockout) {
border: none;
background: var(--aw-well-bg);
box-shadow: var(--aw-well-shadow);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.admin-workplace :deep(.aw-ops__refresh:hover:not(:disabled)),
.admin-workplace :deep(.care-clockout:hover) {
border-color: transparent;
background: var(--aw-well-bg);
box-shadow: var(--aw-well-shadow-deep);
}
.admin-workplace :deep(.wt-card) {
border: none;
background: var(--aw-well-bg);
box-shadow: var(--aw-well-shadow);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.admin-workplace :deep(.announcement-item) {
border: none;
background: var(--aw-well-bg);
box-shadow: var(--aw-well-shadow);
backdrop-filter: none;
-webkit-backdrop-filter: none;
transition: box-shadow 0.2s ease, background 0.2s ease;
}
.admin-workplace :deep(.announcement-item:hover) {
border-color: transparent;
background: var(--aw-well-bg);
box-shadow: var(--aw-well-shadow-deep);
}
@media (max-width: 1100px) {
.admin-workplace__container {
flex-direction: column;

View File

@ -42,11 +42,21 @@ defineOptions({ name: 'CustomerWorkplace' })
<style lang="less" scoped>
.workplace-container {
/* 对齐管理台:外层轻压,内卡细边 + 轻 inset(上方不发灰) */
--wp-card-radius: 16px;
--wp-inner-radius: 12px;
--wp-card-border: 1px solid var(--app-acrylic-border);
--wp-card-bg: var(--app-acrylic-bg);
--wp-card-shadow: none;
--wp-card-border: none;
--wp-card-bg: #ffffff;
--wp-card-shadow:
inset 0 2px 8px rgba(15, 23, 42, 0.07),
inset 0 1px 1px rgba(15, 23, 42, 0.04);
--wp-well-bg: #ffffff;
--wp-well-shadow:
inset 0 0 0 1px rgba(15, 23, 42, 0.06),
inset 0 1px 4px rgba(15, 23, 42, 0.05);
--wp-well-shadow-deep:
inset 0 0 0 1px rgba(15, 23, 42, 0.08),
inset 0 2px 6px rgba(15, 23, 42, 0.07);
width: 100%;
height: 100%;
min-height: 100%;
@ -185,12 +195,14 @@ defineOptions({ name: 'CustomerWorkplace' })
border-radius: var(--wp-card-radius);
background: var(--wp-card-bg);
box-shadow: var(--wp-card-shadow);
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
backdrop-filter: none;
-webkit-backdrop-filter: none;
flex-shrink: 0;
:deep(.arco-card) {
background: transparent;
border: none;
box-shadow: none;
}
:deep(.arco-card-header),
@ -266,12 +278,66 @@ defineOptions({ name: 'CustomerWorkplace' })
}
:deep(.marketing-shortcuts-panel) {
border: var(--wp-card-border);
border: none;
border-radius: var(--wp-card-radius);
background: var(--wp-card-bg);
box-shadow: var(--wp-card-shadow);
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
/* 内层统计卡 / 世界时钟 / 快捷入口 / 站内信 */
.workplace-container :deep(.stat-card) {
border: none;
background: var(--wp-well-bg);
box-shadow: var(--wp-well-shadow);
backdrop-filter: none;
-webkit-backdrop-filter: none;
transition: box-shadow 0.2s ease;
}
.workplace-container :deep(.stat-card--clickable:hover) {
border-color: transparent;
background: var(--wp-well-bg);
box-shadow: var(--wp-well-shadow-deep);
}
.workplace-container :deep(.wt-card) {
border: none;
background: var(--wp-well-bg);
box-shadow: var(--wp-well-shadow);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.workplace-container :deep(.marketing-chip) {
border: none;
background: var(--wp-well-bg);
box-shadow: var(--wp-well-shadow);
backdrop-filter: none;
-webkit-backdrop-filter: none;
transition: box-shadow 0.2s ease;
}
.workplace-container :deep(.marketing-chip:hover) {
border-color: transparent;
background: var(--wp-well-bg);
box-shadow: var(--wp-well-shadow-deep);
}
.workplace-container :deep(.announcement-item) {
border: none;
background: var(--wp-well-bg);
box-shadow: var(--wp-well-shadow);
backdrop-filter: none;
-webkit-backdrop-filter: none;
transition: box-shadow 0.2s ease;
}
.workplace-container :deep(.announcement-item:hover) {
border-color: transparent;
background: var(--wp-well-bg);
box-shadow: var(--wp-well-shadow-deep);
}
:deep(.chart-controls) {

View File

@ -325,10 +325,11 @@
margin-left: 4px;
padding: 8px 12px;
border-radius: 12px;
border: 1px solid var(--app-acrylic-border-strong);
background: var(--app-acrylic-hover-bg);
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
border: none;
background: #ffffff;
box-shadow: var(--wp-well-shadow, var(--aw-well-shadow));
backdrop-filter: none;
-webkit-backdrop-filter: none;
white-space: nowrap;
}
@ -361,18 +362,19 @@
flex-shrink: 0;
height: 44px;
padding: 0 12px 0 14px;
border: 1px solid var(--app-acrylic-border);
border: none;
border-radius: 12px;
background: var(--app-acrylic-bg);
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
background: #ffffff;
box-shadow: var(--wp-well-shadow, var(--aw-well-shadow));
backdrop-filter: none;
-webkit-backdrop-filter: none;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
transition: box-shadow 0.2s ease;
&:hover {
border-color: var(--app-acrylic-border-strong);
background: var(--app-acrylic-hover-bg);
box-shadow: none;
border-color: transparent;
background: #ffffff;
box-shadow: var(--wp-well-shadow-deep, var(--aw-well-shadow-deep));
.balance-chip__cta {
opacity: 1;
@ -380,8 +382,11 @@
}
&--low {
border-color: rgba(255, 125, 0, 0.45);
border: none;
background: rgba(255, 125, 0, 0.06);
box-shadow:
inset 0 2px 5px rgba(255, 125, 0, 0.12),
inset 0 0 0 1px rgba(255, 125, 0, 0.35);
.balance-chip__value {
color: #ff7d00;

View File

@ -280,10 +280,21 @@ onMounted(() => {
margin-top: 8px;
}
/* 首页统计卡与营销入口统一磨砂质感(继承 stat-card.less) */
/* 统计卡:白底轻 inset(外层面板由 CustomerWorkplace 压入) */
.stat-card {
min-height: 84px;
padding: 12px 14px 10px;
border: none;
background: #ffffff;
box-shadow: var(--wp-well-shadow);
backdrop-filter: none;
-webkit-backdrop-filter: none;
&--clickable:hover {
border-color: transparent;
background: #ffffff;
box-shadow: var(--wp-well-shadow-deep);
}
}
.stat-card--compact {

View File

@ -150,22 +150,22 @@ onMounted(async () => {
min-height: 64px;
margin: 0;
padding: 10px 12px;
border: 1px solid var(--app-acrylic-border);
border: none;
border-radius: var(--wp-inner-radius, 12px);
background: var(--app-acrylic-bg);
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
box-shadow: none;
background: #ffffff;
backdrop-filter: none;
-webkit-backdrop-filter: none;
box-shadow: var(--wp-well-shadow);
text-align: left;
font: inherit;
color: inherit;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
transition: box-shadow 0.2s ease;
&:hover {
border-color: var(--app-acrylic-border-strong);
background: var(--app-acrylic-hover-bg);
box-shadow: none;
border-color: transparent;
background: #ffffff;
box-shadow: var(--wp-well-shadow-deep);
transform: none;
}

View File

@ -278,26 +278,30 @@
gap: 6px;
min-width: 0;
padding: 8px 10px;
border: 1px solid var(--app-acrylic-border);
border: none;
border-radius: var(--wp-inner-radius, 12px);
background: var(--app-acrylic-bg);
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
background: #ffffff;
box-shadow: var(--wp-well-shadow);
backdrop-filter: none;
-webkit-backdrop-filter: none;
color: var(--color-text-2);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: border-color 0.2s, background 0.2s, color 0.2s;
transition: box-shadow 0.2s ease, color 0.2s ease;
&:hover {
border-color: var(--app-acrylic-border-strong);
background: var(--app-acrylic-hover-bg);
border-color: transparent;
background: #ffffff;
box-shadow: var(--wp-well-shadow-deep);
}
&.is-active {
border-color: color-mix(in srgb, var(--app-primary-color) 22%, var(--app-acrylic-border));
background: var(--app-acrylic-hover-bg);
box-shadow: none;
border: none;
background: #ffffff;
box-shadow:
inset 0 0 0 1px color-mix(in srgb, var(--app-primary-color) 28%, transparent),
inset 0 1px 4px rgba(15, 23, 42, 0.05);
color: var(--app-primary-color, rgb(var(--primary-6)));
.biz-switch__icon {
@ -327,18 +331,19 @@
}
.flow-step {
border: 1px solid var(--app-acrylic-border);
border: none;
border-radius: var(--wp-inner-radius, 12px);
background: var(--app-acrylic-bg);
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
background: #ffffff;
box-shadow: var(--wp-well-shadow);
backdrop-filter: none;
-webkit-backdrop-filter: none;
overflow: hidden;
transition: border-color 0.2s, background 0.2s;
transition: box-shadow 0.2s ease;
&:hover {
border-color: var(--app-acrylic-border-strong);
background: var(--app-acrylic-hover-bg);
box-shadow: none;
border-color: transparent;
background: #ffffff;
box-shadow: var(--wp-well-shadow-deep);
}
&__main {
@ -412,18 +417,22 @@
align-items: center;
gap: 6px;
padding: 4px 10px;
border: 1px solid var(--app-acrylic-border);
border: none;
border-radius: 14px;
background: transparent;
background: #ffffff;
box-shadow: var(--wp-well-shadow);
color: var(--color-text-2);
font-size: 12px;
cursor: pointer;
transition: border-color 0.2s, color 0.2s, background 0.2s;
transition: box-shadow 0.2s ease, color 0.2s ease;
&:hover {
border-color: var(--app-acrylic-border-strong);
border-color: transparent;
color: var(--app-primary-color, rgb(var(--primary-6)));
background: var(--app-acrylic-hover-bg);
background: #ffffff;
box-shadow:
inset 0 0 0 1px color-mix(in srgb, var(--app-primary-color) 22%, transparent),
inset 0 1px 4px rgba(15, 23, 42, 0.05);
}
}
@ -444,18 +453,22 @@
align-items: center;
gap: 6px;
padding: 5px 10px;
border: 1px solid var(--app-acrylic-border);
border: none;
border-radius: 16px;
background: transparent;
background: #ffffff;
box-shadow: var(--wp-well-shadow);
color: var(--color-text-2);
font-size: 12px;
cursor: pointer;
transition: border-color 0.2s, color 0.2s, background 0.2s;
transition: box-shadow 0.2s ease, color 0.2s ease;
&:hover {
border-color: var(--app-acrylic-border-strong);
border-color: transparent;
color: var(--app-primary-color, rgb(var(--primary-6)));
background: var(--app-acrylic-hover-bg);
background: #ffffff;
box-shadow:
inset 0 0 0 1px color-mix(in srgb, var(--app-primary-color) 22%, transparent),
inset 0 1px 4px rgba(15, 23, 42, 0.05);
}
&__icon {

View File

@ -7,9 +7,6 @@
}"
aria-live="polite"
>
<span class="login-care-banner__blob login-care-banner__blob--a" aria-hidden="true" />
<span class="login-care-banner__blob login-care-banner__blob--b" aria-hidden="true" />
<span class="login-care-banner__blob login-care-banner__blob--c" aria-hidden="true" />
<div class="login-care-banner__body">
<p class="login-care-banner__greeting">{{ greeting }}</p>
<p class="login-care-banner__note">{{ note }}</p>
@ -90,10 +87,11 @@ onBeforeUnmount(() => {
<style lang="less" scoped>
.login-care-banner {
--care-ink: #5c3d2e;
--care-muted: rgba(92, 61, 46, 0.72);
--care-edge: rgba(255, 214, 170, 0.55);
--care-glow: rgba(255, 176, 120, 0.35);
/* 与登录卡片同系:贴合页底 + 单层 inset,无彩斑 */
--care-ink: #1e293b;
--care-muted: rgba(71, 85, 105, 0.88);
--care-bg: var(--app-page-bg, #f0f3f4);
--care-shadow: inset 0 3px 12px rgba(15, 23, 42, 0.1);
position: absolute;
top: calc(16px + env(safe-area-inset-top, 0px));
@ -101,99 +99,34 @@ onBeforeUnmount(() => {
z-index: 40;
width: min(360px, calc(100vw - 40px));
padding: 16px 18px;
border-radius: 22px;
border-radius: 20px;
text-align: left;
pointer-events: none;
box-sizing: border-box;
overflow: hidden;
isolation: isolate;
border: 1px solid var(--care-edge);
background:
linear-gradient(145deg, rgba(255, 248, 240, 0.82), rgba(255, 236, 220, 0.55)),
rgba(255, 250, 245, 0.65);
backdrop-filter: blur(16px) saturate(1.25);
-webkit-backdrop-filter: blur(16px) saturate(1.25);
box-shadow:
0 10px 28px rgba(180, 110, 70, 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.75);
border: none;
background: var(--care-bg);
backdrop-filter: none;
-webkit-backdrop-filter: none;
box-shadow: var(--care-shadow);
opacity: 0;
transform: translate3d(18px, -12px, 0) scale(0.92);
transform: translate3d(12px, -8px, 0);
transform-origin: top right;
transition:
opacity 0.42s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.5s cubic-bezier(0.34, 1.35, 0.64, 1),
box-shadow 0.42s ease,
border-radius 0.8s ease;
opacity 0.35s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.4s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.3s ease;
&.is-visible {
opacity: 1;
transform: translate3d(0, 0, 0) scale(1);
border-radius: 24px 18px 26px 20px;
box-shadow:
0 14px 36px rgba(180, 110, 70, 0.16),
0 0 0 1px rgba(255, 255, 255, 0.35) inset;
animation: care-liquid-morph 8s ease-in-out infinite;
transform: translate3d(0, 0, 0);
box-shadow: inset 0 4px 14px rgba(15, 23, 42, 0.12);
}
&.is-after-hours {
--care-ink: #4a3a55;
--care-muted: rgba(74, 58, 85, 0.72);
--care-edge: rgba(220, 190, 230, 0.5);
--care-glow: rgba(190, 150, 220, 0.28);
background:
linear-gradient(145deg, rgba(252, 246, 255, 0.88), rgba(240, 228, 250, 0.6)),
rgba(250, 246, 255, 0.7);
}
}
.login-care-banner__blob {
position: absolute;
border-radius: 50%;
filter: blur(18px);
opacity: 0.85;
pointer-events: none;
z-index: 0;
mix-blend-mode: multiply;
}
.login-care-banner__blob--a {
width: 140px;
height: 140px;
top: -48px;
right: -36px;
background: radial-gradient(circle, #ffb48a 0%, rgba(255, 180, 138, 0) 70%);
animation: care-blob-a 7s ease-in-out infinite;
}
.login-care-banner__blob--b {
width: 120px;
height: 120px;
bottom: -50px;
left: -28px;
background: radial-gradient(circle, #ffd6a5 0%, rgba(255, 214, 165, 0) 72%);
animation: care-blob-b 9s ease-in-out infinite;
}
.login-care-banner__blob--c {
width: 90px;
height: 90px;
top: 40%;
left: 42%;
background: radial-gradient(circle, #ffe8c8 0%, rgba(255, 232, 200, 0) 70%);
animation: care-blob-c 6.5s ease-in-out infinite;
}
.login-care-banner.is-after-hours {
.login-care-banner__blob--a {
background: radial-gradient(circle, #d7b8ef 0%, rgba(215, 184, 239, 0) 70%);
}
.login-care-banner__blob--b {
background: radial-gradient(circle, #f2c9de 0%, rgba(242, 201, 222, 0) 72%);
}
.login-care-banner__blob--c {
background: radial-gradient(circle, #e8d9f8 0%, rgba(232, 217, 248, 0) 70%);
--care-ink: #1e293b;
--care-muted: rgba(71, 85, 105, 0.88);
}
}
@ -204,9 +137,9 @@ onBeforeUnmount(() => {
.login-care-banner__greeting {
margin: 0 0 6px;
font-size: 20px;
font-size: 18px;
font-weight: 700;
letter-spacing: 0.02em;
letter-spacing: 0.01em;
line-height: 1.35;
color: var(--care-ink);
}
@ -218,49 +151,6 @@ onBeforeUnmount(() => {
color: var(--care-muted);
}
@keyframes care-liquid-morph {
0%,
100% {
border-radius: 24px 18px 26px 20px;
}
33% {
border-radius: 20px 26px 18px 24px;
}
66% {
border-radius: 26px 20px 22px 28px;
}
}
@keyframes care-blob-a {
0%,
100% {
transform: translate(0, 0) scale(1);
}
50% {
transform: translate(-18px, 14px) scale(1.12);
}
}
@keyframes care-blob-b {
0%,
100% {
transform: translate(0, 0) scale(1);
}
50% {
transform: translate(16px, -12px) scale(1.18);
}
}
@keyframes care-blob-c {
0%,
100% {
transform: translate(0, 0) scale(1);
}
50% {
transform: translate(-10px, -8px) scale(1.25);
}
}
@media screen and (max-width: 768px) {
.login-care-banner {
top: calc(12px + env(safe-area-inset-top, 0px));
@ -270,21 +160,15 @@ onBeforeUnmount(() => {
transform-origin: top center;
&:not(.is-visible) {
transform: translate3d(0, -10px, 0) scale(0.94);
transform: translate3d(0, -8px, 0);
}
}
}
@media (prefers-reduced-motion: reduce) {
.login-care-banner,
.login-care-banner__blob {
animation: none !important;
}
.login-care-banner {
transition: opacity 0.2s ease;
transform: none;
border-radius: 16px;
&.is-visible {
transform: none;

View File

@ -13,57 +13,36 @@
<form class="nova-login__form" autocomplete="off" @submit.prevent="handleSubmit">
<div class="nova-login__input-group">
<div class="nova-login__field-label">{{ accountFieldLabel }}</div>
<div class="nova-login__input-wrapper">
<span class="nova-login__prefix-icon" aria-hidden="true">
<icon-user :size="16" />
</span>
<div class="nova-login__input-shell">
<input
v-model="loginForm.account"
type="text"
class="nova-login__input"
:placeholder="accountPlaceholder"
autocomplete="username"
@focus="onAccountFocus"
@blur="onAccountBlur"
@keydown.enter.prevent="focusPasswordInput"
/>
</div>
</div>
<a-input
v-model="loginForm.account"
size="large"
allow-clear
class="nova-login__arco-input"
:placeholder="accountPlaceholder"
autocomplete="username"
@focus="onAccountFocus"
@blur="onAccountBlur"
@press-enter="focusPasswordInput"
/>
<div v-if="accountError" class="nova-login__field-error">{{ accountError }}</div>
</div>
<div class="nova-login__input-group">
<div class="nova-login__field-label">{{ t('login.field.password') }}</div>
<div class="nova-login__input-wrapper">
<span class="nova-login__prefix-icon" aria-hidden="true">
<icon-lock :size="16" />
</span>
<div class="nova-login__input-shell">
<input
ref="passwordInputRef"
v-model="loginForm.password"
:type="showPassword ? 'text' : 'password'"
class="nova-login__input"
:placeholder="t('login.form.password.placeholder')"
autocomplete="current-password"
@focus="onPasswordFocus"
@blur="onPasswordBlur"
@input="syncPasswordCharacterState"
@keydown.enter.prevent="handleSubmit"
/>
</div>
<button
type="button"
class="nova-login__eye-toggle"
tabindex="-1"
:aria-label="showPassword ? t('login.hidePassword') : t('login.showPassword')"
@click="togglePasswordVisible"
>
<icon-eye-invisible v-if="!showPassword" :size="16" />
<icon-eye v-else :size="16" />
</button>
</div>
<a-input-password
ref="passwordInputRef"
v-model="loginForm.password"
size="large"
allow-clear
class="nova-login__arco-input"
:placeholder="t('login.form.password.placeholder')"
autocomplete="current-password"
@focus="onPasswordFocus"
@blur="onPasswordBlur"
@input="syncPasswordCharacterState"
@press-enter="handleSubmit"
@visibility-change="onPasswordVisibilityChange"
/>
<div v-if="passwordError" class="nova-login__field-error">{{ passwordError }}</div>
</div>
@ -117,13 +96,7 @@ import { ref, reactive, computed, nextTick, inject, type ShallowRef } from 'vue'
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { Message } from '@arco-design/web-vue';
import {
IconUser,
IconLock,
IconWechat,
IconEye,
IconEyeInvisible,
} from '@arco-design/web-vue/es/icon';
import { IconWechat } from '@arco-design/web-vue/es/icon';
import { OmsLoginType } from '@/api/rbac';
import { useRbacUserStore } from '@/store/modules/rbac-user';
import { setToken } from '@/utils/auth';
@ -153,7 +126,7 @@ const emit = defineEmits<{
const router = useRouter();
const rbacUserStore = useRbacUserStore();
const passwordInputRef = ref<HTMLInputElement | null>(null);
const passwordInputRef = ref<{ focus?: () => void } | null>(null);
const loading = ref(false);
const showPassword = ref(false);
const accountError = ref('');
@ -290,8 +263,8 @@ function syncPasswordCharacterState() {
loginCharacters.value?.onPasswordInput(loginForm.password.length, showPassword.value);
}
function togglePasswordVisible() {
showPassword.value = !showPassword.value;
function onPasswordVisibilityChange(visible: boolean) {
showPassword.value = visible;
syncPasswordCharacterState();
}
@ -456,102 +429,35 @@ async function handleSubmit() {
text-transform: uppercase;
}
.nova-login__input-wrapper {
position: relative;
display: flex;
align-items: center;
width: 100%;
min-width: 0;
height: 50px;
overflow: hidden;
background: var(--app-acrylic-bg);
border: 1px solid var(--app-acrylic-border);
border-radius: 14px;
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
/* 与 H5 `.h5-login-form` 输入井一致 */
.nova-login__form :deep(.arco-input-wrapper),
.nova-login__form :deep(.arco-input-password) {
border-radius: 12px;
background: #f0f3f4;
border: none;
min-height: 48px;
backdrop-filter: none;
-webkit-backdrop-filter: none;
box-shadow: inset 0 2px 8px rgba(15, 23, 42, 0.08);
transition: box-shadow 0.2s ease, background 0.2s ease;
&:hover {
border-color: var(--app-acrylic-border-strong);
background: var(--app-acrylic-hover-bg);
background: #f0f3f4;
box-shadow: inset 0 3px 10px rgba(15, 23, 42, 0.1);
}
&:focus-within {
border-color: var(--app-primary-color);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--app-primary-color) 15%, transparent);
background: var(--app-acrylic-hover-bg);
&.arco-input-wrapper-focus,
&.arco-input-focus {
background: #f0f3f4;
border: none;
box-shadow:
inset 0 3px 10px rgba(15, 23, 42, 0.1),
0 0 0 3px color-mix(in srgb, var(--app-primary-color) 12%, transparent);
}
}
.nova-login__prefix-icon {
display: flex;
flex-shrink: 0;
align-items: center;
padding-left: 14px;
color: #94a3b8;
}
.nova-login__input-shell {
flex: 1;
min-width: 0;
height: 100%;
overflow: hidden;
display: flex;
align-items: center;
}
.nova-login__input-wrapper:focus-within .nova-login__prefix-icon {
color: var(--app-primary-color);
}
.nova-login__input {
.nova-login__arco-input {
width: 100%;
min-width: 0;
height: 100%;
border: none;
outline: none;
background: transparent;
font-size: 14px;
color: #111827;
padding: 0 12px 0 4px;
font-family: inherit;
box-sizing: border-box;
&::placeholder {
color: #9aa4b2;
}
&:-webkit-autofill,
&:-webkit-autofill:hover,
&:-webkit-autofill:focus,
&:-webkit-autofill:active {
-webkit-text-fill-color: #111827;
caret-color: #111827;
box-shadow: 0 0 0 1000px rgba(255, 255, 255, 0.42) inset;
-webkit-box-shadow: 0 0 0 1000px rgba(255, 255, 255, 0.42) inset;
transition: background-color 99999s ease-out 0s;
}
}
.nova-login__input-wrapper:focus-within .nova-login__input:-webkit-autofill,
.nova-login__input-wrapper:hover .nova-login__input:-webkit-autofill {
box-shadow: 0 0 0 1000px rgba(255, 255, 255, 0.52) inset;
-webkit-box-shadow: 0 0 0 1000px rgba(255, 255, 255, 0.52) inset;
}
.nova-login__eye-toggle {
display: flex;
flex-shrink: 0;
align-items: center;
padding-right: 14px;
color: #64748b;
cursor: pointer;
background: none;
border: none;
&:hover {
color: var(--app-primary-color);
}
}
.nova-login__field-error {

View File

@ -47,13 +47,6 @@
/>
</aside>
<div
v-if="shellReady && !isCustomerLogin"
class="login-characters-panel login-characters-panel--admin"
>
<LoginCharacters v-if="showCharacters" @register="registerLoginCharacters" />
</div>
<div v-if="shellReady" ref="rightPaneRef" class="login-pane login-pane--right">
<div ref="loginFormRef" :class="['login-form', { 'login-form--ready': shellReady }]">
<div class="login-form-toolbar">
@ -346,7 +339,6 @@ onBeforeUnmount(() => {
/* Safety net: admin overlays are outside pane clip-paths */
.login-hero-panel--admin,
.login-characters-panel--admin,
:deep(.login-care-banner) {
opacity: 0 !important;
visibility: hidden !important;
@ -481,22 +473,6 @@ onBeforeUnmount(() => {
padding: 0 0 16px 8px;
}
.login-characters-panel--admin {
left: auto;
right: 0;
bottom: 0;
z-index: 6;
padding: 0;
transform: scale(0.82);
transform-origin: bottom right;
opacity: 1;
}
.login-characters-panel--admin :deep(.login-characters__stage) {
width: 450px;
transform-origin: bottom right;
}
.login-form-toolbar {
position: absolute;
top: 14px;
@ -535,29 +511,35 @@ onBeforeUnmount(() => {
}
.login-form {
/* 对齐 H5 登录卡:白底 + 轻压 inset */
--login-card-bg: #ffffff;
--login-card-shadow:
inset 0 2px 8px rgba(15, 23, 42, 0.07),
inset 0 1px 1px rgba(15, 23, 42, 0.04);
position: relative;
z-index: 10;
width: 100%;
max-width: 430px;
min-height: auto;
padding: 51px 32px 45px;
background: var(--app-acrylic-bg);
border: 1px solid var(--app-acrylic-border);
border-radius: 24px;
box-shadow: none;
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
background: var(--login-card-bg);
border: none;
outline: none;
border-radius: 20px;
box-shadow: var(--login-card-shadow);
backdrop-filter: none;
-webkit-backdrop-filter: none;
display: flex;
flex-direction: column;
opacity: 1;
transform: none;
transition: border-color 0.2s ease, background 0.2s ease;
transition: box-shadow 0.2s ease, background 0.2s ease;
}
.login-form:hover {
border-color: var(--app-acrylic-border-strong);
background: var(--app-acrylic-hover-bg);
box-shadow: none;
background: var(--login-card-bg);
box-shadow: var(--login-card-shadow);
}
.login-form--ready {

View File

@ -29,9 +29,9 @@
parallaxStrength?: number;
grain?: boolean;
grainIntensity?: number;
/** Cap canvas DPR (retina 2–3 is expensive for raymarch). */
/** Cap canvas DPR (retina is expensive for raymarch). */
maxDpr?: number;
/** Draw buffer scale 0–1; CSS stretches to full size (softer, cheaper). */
/** Draw buffer scale 0–1; CSS stretches to full size. */
renderScale?: number;
/** Target animation FPS; lower = less GPU. */
fps?: number;
@ -380,7 +380,7 @@ void main() {
const currentMouse: [number, number] = [0.5, 0.5];
const targetMouse: [number, number] = [0.5, 0.5];
// Window-level so parallax works behind UI
// Window-level so parallax works behind UI overlays
const onPointerMove = (e: PointerEvent) => {
if (!props.mouseInteraction) return;
const rect = container.getBoundingClientRect();
@ -404,10 +404,10 @@ void main() {
const paint = (t: number) => {
mu.iTime.value = (t - t0) * 0.001;
if (props.mouseInteraction) {
currentMouse[0] += 0.05 * (targetMouse[0] - currentMouse[0]);
currentMouse[1] += 0.05 * (targetMouse[1] - currentMouse[1]);
}
const tx = props.mouseInteraction ? targetMouse[0] : 0.5;
const ty = props.mouseInteraction ? targetMouse[1] : 0.5;
currentMouse[0] += 0.05 * (tx - currentMouse[0]);
currentMouse[1] += 0.05 * (ty - currentMouse[1]);
const m = mu.uMouse.value;
m[0] = currentMouse[0];
m[1] = currentMouse[1];

View File

@ -3,26 +3,26 @@
<div class="ai-shell-bg" aria-hidden="true">
<GradientWaves
class="ai-shell-bg__waves"
horizon-color="#0369a1"
wave-color="#0ea5c9"
crest-color="#e0f7fc"
:speed="waveMotion ? 0.2 : 0"
:amplitude="waveMotion ? 2.7 : 0"
:wave-scale="0.64"
:wave-ratio="0.85"
:swell="30"
:turbulence="waveMotion ? 10 : 0"
:tilt="1.06"
horizon-color="#023e8a"
wave-color="#0077b6"
crest-color="#caf0f8"
:speed="0.34"
:amplitude="3.3"
:wave-scale="0.72"
:wave-ratio="0.82"
:swell="42"
:turbulence="26"
:tilt="1.08"
:zoom="0.92"
:height="4.8"
:fog-depth="22"
detail="low"
:brightness="1.02"
:opacity="0.82"
:mouse-interaction="false"
:parallax-strength="0"
:brightness="1.08"
:opacity="1.0"
:mouse-interaction="true"
:parallax-strength="0.4"
:grain="false"
:fps="waveMotion ? 28 : 12"
:fps="30"
:render-scale="0.72"
:max-dpr="1.25"
/>
@ -339,7 +339,6 @@
const growthDoors = HOME_DOORS.filter((d) => !d.primary);
const shortTitle = ref(false);
const titleAnimate = ref(true);
const waveMotion = ref(true);
const draft = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
@ -381,7 +380,6 @@
function syncMediaPrefs() {
shortTitle.value = !!mqNarrow?.matches;
waveMotion.value = !mqReduce?.matches;
titleAnimate.value = !mqReduce?.matches;
}

View File

@ -56,7 +56,8 @@
}>();
const partners = SITE_PARTNERS;
const loop = !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
/** Fixed strip — no auto-scroll / drag loop */
const loop = false;
const PX_PER_SEC = 26;
const viewRef = ref<HTMLElement | null>(null);

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,7 @@
}
body.command-home {
background: var(--kc-page-bg);
background: #f3f5f6;
color: var(--kc-text);
font-family: var(--font-default);
font-size: 1.05rem;

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
import { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { Modal } from '@arco-design/web-vue'
import { IconDelete, IconPlus, IconQuestionCircle } from '@arco-design/web-vue/es/icon'
import { IconDelete, IconDown, IconPlus, IconQuestionCircle } from '@arco-design/web-vue/es/icon'
import HandsontablePro from '@/components/HandsontablePro/index.vue'
import PaginationPro from '@/components/PaginationPro/index.vue'
import ModalPro from '@/components/ModalPro/index.vue'
@ -51,13 +51,24 @@ interface InboundBoxForm {
key: string
boxSeqNo: string
markBoxNo: string
weightG?: number
/** 表单展示/录入用 kg;提交时换算为后端 WeightG */
weightKg?: number
lengthCm?: number
widthCm?: number
heightCm?: number
lines: InboundLineForm[]
}
function gramsToKg(weightG?: number | null) {
if (weightG == null || Number.isNaN(Number(weightG))) return undefined
return Number((Number(weightG) / 1000).toFixed(3))
}
function kgToGrams(weightKg?: number | null) {
if (weightKg == null || Number.isNaN(Number(weightKg))) return undefined
return Number((Number(weightKg) * 1000).toFixed(2))
}
interface ReceiveLineForm {
lineId: number
skuCode: string
@ -138,6 +149,10 @@ const formModel = reactive({
remark: '',
})
const boxRows = ref<InboundBoxForm[]>([])
/** 复制加箱:按当前箱复制重量/尺寸/SKU/计划 */
const batchAddCount = ref(5)
const batchAddPresets = [3, 5, 10] as const
const batchAddPopoverVisible = ref(false)
const boxTabItems = computed(() =>
boxRows.value.map((box) => {
@ -175,8 +190,8 @@ function displayOrDash(value: unknown) {
}
function formatBoxWeight(box: InboundBoxForm) {
if (box.weightG == null || Number.isNaN(Number(box.weightG))) return '-'
return `${box.weightG} g`
if (box.weightKg == null || Number.isNaN(Number(box.weightKg))) return '-'
return `${box.weightKg} kg`
}
function formatBoxSize(box: InboundBoxForm) {
@ -258,6 +273,28 @@ function createBox(seq: number): InboundBoxForm {
}
}
function cloneBoxFrom(source: InboundBoxForm, seq: number): InboundBoxForm {
const filledLines = source.lines
.filter((l) => l.skuId)
.map((l) => ({
skuId: l.skuId,
skuCode: l.skuCode,
plannedQty: Number(l.plannedQty) > 0 ? Number(l.plannedQty) : 1,
}))
return {
key: `box-${Date.now()}-${seq}-${Math.random().toString(36).slice(2, 7)}`,
boxSeqNo: String(seq),
markBoxNo: `BOX-${String(seq).padStart(3, '0')}`,
weightKg: source.weightKg,
lengthCm: source.lengthCm,
widthCm: source.widthCm,
heightCm: source.heightCm,
lines: filledLines.length
? [...filledLines, { skuId: undefined, skuCode: '', plannedQty: 1 }]
: [{ skuId: undefined, skuCode: '', plannedQty: 1 }],
}
}
function resetBoxes() {
const first = createBox(1)
boxRows.value = [first]
@ -272,23 +309,49 @@ function addBox() {
activeBoxKey.value = box.key
}
function addBoxes(count: number) {
function addBoxes(count?: number) {
const n = Math.max(1, Math.min(99, Math.floor(Number(count ?? batchAddCount.value) || 1)))
batchAddCount.value = n
const source =
boxRows.value.find((b) => b.key === activeBoxKey.value) || boxRows.value[boxRows.value.length - 1]
const start = boxRows.value.length
const added = Array.from({ length: count }, (_, i) => createBox(start + i + 1))
const added = Array.from({ length: n }, (_, i) =>
source ? cloneBoxFrom(source, start + i + 1) : createBox(start + i + 1),
)
boxRows.value = [...boxRows.value, ...added]
if (added.length) activeBoxKey.value = added[added.length - 1].key
}
function confirmBatchAdd() {
addBoxes()
batchAddPopoverVisible.value = false
}
function resequenceBoxes(boxes: InboundBoxForm[]) {
boxes.forEach((box, i) => {
const seq = i + 1
const prevAutoMark = `BOX-${String(Number(box.boxSeqNo) || 0).padStart(3, '0')}`
box.boxSeqNo = String(seq)
// 箱号仍是系统默认 BOX-xxx 时同步重排,自定义唛头保留
if (!box.markBoxNo?.trim() || box.markBoxNo === prevAutoMark) {
box.markBoxNo = `BOX-${String(seq).padStart(3, '0')}`
}
})
}
function removeActiveBox() {
if (boxRows.value.length <= 1) {
$baseMessage(t('warehouse.inbound.message.keepOneBox'), 'warning')
return
}
const key = activeBoxKey.value
const idx = boxRows.value.findIndex((b) => b.key === key)
const remain = boxRows.value.filter((b) => b.key !== key)
resequenceBoxes(remain)
boxRows.value = remain
selectedBoxKeys.value = selectedBoxKeys.value.filter((k) => k !== key)
activeBoxKey.value = remain[remain.length - 1]?.key || ''
const nextIdx = Math.min(Math.max(idx, 0), remain.length - 1)
activeBoxKey.value = remain[nextIdx]?.key || remain[remain.length - 1]?.key || ''
}
function removeLine(box: InboundBoxForm, index: number) {
@ -599,7 +662,7 @@ async function openInboundForm(row: any) {
key: `box-open-${pickField(b, 'id', 'Id') || index}-${Date.now()}`,
boxSeqNo: String(pickField(b, 'boxSeqNo', 'BoxSeqNo') ?? index + 1),
markBoxNo: String(pickField(b, 'markBoxNo', 'MarkBoxNo') ?? ''),
weightG: pickField<number>(b, 'weightG', 'WeightG') ?? undefined,
weightKg: gramsToKg(pickField<number>(b, 'weightG', 'WeightG')),
lengthCm: pickField<number>(b, 'lengthCm', 'LengthCm') ?? undefined,
widthCm: pickField<number>(b, 'widthCm', 'WidthCm') ?? undefined,
heightCm: pickField<number>(b, 'heightCm', 'HeightCm') ?? undefined,
@ -638,7 +701,7 @@ async function handleSave() {
.map((box) => ({
boxSeqNo: box.boxSeqNo,
markBoxNo: box.markBoxNo,
weightG: box.weightG,
weightG: kgToGrams(box.weightKg),
lengthCm: box.lengthCm,
widthCm: box.widthCm,
heightCm: box.heightCm,
@ -1020,9 +1083,60 @@ onMounted(async () => {
<template #icon><IconPlus :size="12" /></template>
{{ t('warehouse.inbound.actions.addBox') }}
</a-button>
<a-button size="mini" @click="addBoxes(5)">
{{ t('warehouse.inbound.actions.addBox5') }}
</a-button>
<a-popover
v-model:popup-visible="batchAddPopoverVisible"
trigger="click"
position="bl"
content-class="inbound-batch-add-popover"
>
<a-button size="mini">
{{ t('warehouse.inbound.actions.copyAddBox') }}
<IconDown :size="12" class="inbound-batch-add-trigger-icon" />
</a-button>
<template #content>
<div class="inbound-batch-add">
<div class="inbound-batch-add__title">
{{ t('warehouse.inbound.actions.copyAddBoxTitle') }}
</div>
<div class="inbound-batch-add__hint">
{{ t('warehouse.inbound.tip.copyAddBox') }}
</div>
<div class="inbound-batch-add__presets">
<a-button
v-for="n in batchAddPresets"
:key="n"
size="mini"
:class="{ 'app-btn-primary-soft': batchAddCount === n }"
@click="batchAddCount = n"
>
{{ n }}
</a-button>
</div>
<div class="inbound-batch-add__row">
<span class="inbound-batch-add__label">
{{ t('warehouse.inbound.form.batchAddCount') }}
</span>
<a-input-number
v-model="batchAddCount"
:min="1"
:max="99"
size="mini"
hide-button
class="inbound-batch-add__n"
/>
</div>
<a-button
type="primary"
size="mini"
long
class="inbound-batch-add__confirm"
@click="confirmBatchAdd"
>
{{ t('warehouse.inbound.actions.addBoxN', { n: batchAddCount || 1 }) }}
</a-button>
</div>
</template>
</a-popover>
<a-button
size="mini"
status="danger"
@ -1075,13 +1189,14 @@ onMounted(async () => {
<div class="inbound-box-meta__field inbound-box-meta__field--weight">
<span class="inbound-box-meta__label">{{ t('warehouse.inbound.form.boxWeight') }}</span>
<a-input-number
v-model="box.weightG"
v-model="box.weightKg"
:min="0"
:precision="3"
hide-button
style="width: 100%"
:placeholder="t('warehouse.inbound.form.optional')"
>
<template #suffix>g</template>
<template #suffix>kg</template>
</a-input-number>
</div>
<div class="inbound-box-meta__field inbound-box-meta__field--size">
@ -1230,6 +1345,14 @@ onMounted(async () => {
.wh-form-box-tabs {
margin-bottom: 12px;
width: 100%;
min-width: 0;
overflow: hidden;
}
.inbound-batch-add-trigger-icon {
margin-left: 2px;
vertical-align: -1px;
}
.inbound-view-basic {
@ -1387,3 +1510,53 @@ onMounted(async () => {
min-height: 32px;
}
</style>
<!-- popover 内容 teleport 到 body,需非 scoped -->
<style lang="less">
.inbound-batch-add-popover {
.inbound-batch-add {
width: 220px;
&__title {
margin-bottom: 4px;
font-size: 13px;
font-weight: 600;
color: var(--color-text-1);
}
&__hint {
margin-bottom: 10px;
font-size: 12px;
line-height: 1.5;
color: var(--color-text-3);
}
&__presets {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
&__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 12px;
}
&__label {
font-size: 13px;
color: var(--color-text-2);
}
&__n {
width: 72px;
}
&__confirm {
height: 32px;
}
}
}
</style>