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 <div
ref="rootRef" ref="rootRef"
class="t-tabs" class="t-tabs"
:class="[`t-tabs--${size}`]" :class="[`t-tabs--${size}`, { 't-tabs--dragging': isDragging }]"
role="tablist" role="tablist"
:aria-label="ariaLabel" :aria-label="ariaLabel"
@pointerdown="onPointerDown"
@pointermove="onPointerMove"
@pointerup="onPointerUp"
@pointercancel="onPointerUp"
@click.capture="onClickCapture"
> >
<span ref="pillRef" class="t-tabs-pill" aria-hidden="true" /> <span ref="pillRef" class="t-tabs-pill" aria-hidden="true" />
<button <button
@ -59,6 +64,14 @@ const emit = defineEmits<{
const rootRef = ref<HTMLElement | null>(null) const rootRef = ref<HTMLElement | null>(null)
const pillRef = ref<HTMLElement | null>(null) const pillRef = ref<HTMLElement | null>(null)
const tabRefs = new Map<string, HTMLElement>() 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) { function setTabRef(key: string, el: unknown) {
const node = el as HTMLElement | null const node = el as HTMLElement | null
@ -66,6 +79,12 @@ function setTabRef(key: string, el: unknown) {
else tabRefs.delete(key) else tabRefs.delete(key)
} }
function canScroll() {
const el = rootRef.value
if (!el) return false
return el.scrollWidth > el.clientWidth + 1
}
function movePill(animate: boolean) { function movePill(animate: boolean) {
const pill = pillRef.value const pill = pillRef.value
const tab = tabRefs.get(props.modelValue) const tab = tabRefs.get(props.modelValue)
@ -88,7 +107,21 @@ function movePill(animate: boolean) {
pill.style.width = `${width}px` 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) { function onSelect(key: string) {
if (suppressClick) return
if (key === props.modelValue) return if (key === props.modelValue) return
const item = props.items.find((i) => i.key === key) const item = props.items.find((i) => i.key === key)
if (item?.disabled) return if (item?.disabled) return
@ -96,6 +129,64 @@ function onSelect(key: string) {
emit('change', key) 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() { function onWindowResize() {
movePill(false) movePill(false)
} }
@ -103,7 +194,10 @@ function onWindowResize() {
let resizeObserver: ResizeObserver | null = null let resizeObserver: ResizeObserver | null = null
onMounted(() => { onMounted(() => {
nextTick(() => movePill(false)) nextTick(() => {
movePill(false)
scrollActiveIntoView()
})
if (typeof ResizeObserver !== 'undefined' && rootRef.value) { if (typeof ResizeObserver !== 'undefined' && rootRef.value) {
resizeObserver = new ResizeObserver(() => movePill(false)) resizeObserver = new ResizeObserver(() => movePill(false))
resizeObserver.observe(rootRef.value) resizeObserver.observe(rootRef.value)
@ -122,6 +216,7 @@ watch(
async () => { async () => {
await nextTick() await nextTick()
movePill(true) movePill(true)
scrollActiveIntoView()
} }
) )
@ -130,6 +225,7 @@ watch(
async () => { async () => {
await nextTick() await nextTick()
movePill(false) movePill(false)
scrollActiveIntoView()
} }
) )
@ -162,10 +258,21 @@ defineExpose({ syncPill: () => movePill(false) })
overflow-y: hidden; overflow-y: hidden;
scrollbar-width: none; scrollbar-width: none;
box-sizing: border-box; box-sizing: border-box;
touch-action: pan-x;
user-select: none;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
} }
&--dragging {
cursor: grabbing;
.t-tab {
pointer-events: none;
cursor: grabbing;
}
}
} }
.t-tab { .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.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.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.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.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.tip.skuLines': 'Select at least one SKU per box; a blank row is added after selection',
'warehouse.inbound.form.markReference': 'Mark Ref', '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.form.autoAddSkuRow': 'A new row is added after you select a SKU',
'warehouse.inbound.actions.addBox': 'Add box', 'warehouse.inbound.actions.addBox': 'Add box',
'warehouse.inbound.actions.addBox5': 'Add 5 boxes', '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.removeBox': 'Remove boxes',
'warehouse.inbound.actions.removeCurrentBox': 'Remove current box', 'warehouse.inbound.actions.removeCurrentBox': 'Remove current box',
'warehouse.inbound.actions.addSku': 'Add SKU line', 'warehouse.inbound.actions.addSku': 'Add SKU line',
@ -458,7 +463,7 @@ export default {
'warehouse.inbound.drawer.detail': 'Inbound Detail', 'warehouse.inbound.drawer.detail': 'Inbound Detail',
'warehouse.inbound.box': 'Box', 'warehouse.inbound.box': 'Box',
'warehouse.inbound.line.skuCode': 'SKU', 'warehouse.inbound.line.skuCode': 'SKU',
'warehouse.inbound.line.plannedQty': 'Planned', 'warehouse.inbound.line.plannedQty': 'Planned Qty',
'warehouse.inbound.line.receivedQty': 'Received', 'warehouse.inbound.line.receivedQty': 'Received',
'warehouse.inbound.line.binLocation': 'Putaway Bin', 'warehouse.inbound.line.binLocation': 'Putaway Bin',
'warehouse.inbound.form.selectBin': 'Select bin', 'warehouse.inbound.form.selectBin': 'Select bin',
@ -4666,7 +4671,7 @@ export default {
// User - Recharge // User - Recharge
'user.recharge.deposit.panel.subtitle': 'Fund 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.panel.desc': 'Select Amount · Upload Voucher · Wait for Review',
'user.recharge.deposit.steps.selectAmount.title': 'Select Amount', 'user.recharge.deposit.steps.selectAmount.title': 'Select Amount',
'user.recharge.deposit.steps.selectAmount.desc': 'Select amount and confirm payment method', '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.item2': 'Upload clear and complete payment screenshot',
'user.recharge.deposit.tips.item3': 'SMS/In-site message reminder after funds arrive', '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.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.selectAmount': 'Select Amount',
'user.recharge.deposit.form.custom': 'Custom', 'user.recharge.deposit.form.custom': 'Custom',
'user.recharge.deposit.form.inputAmount': 'Enter Amount', '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.paymentMethod': 'Payment Method',
'user.recharge.deposit.form.qrTip': 'Click QR code to zoom in and view details', 'user.recharge.deposit.form.qrTip': 'Click QR code to zoom in and view details',
'user.recharge.deposit.form.uploadVoucher': 'Upload Voucher', 'user.recharge.deposit.form.uploadVoucher': 'Upload Voucher',

View File

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

View File

@ -204,13 +204,16 @@ onMounted(() => {
} }
.h5-login-card { .h5-login-card {
background: var(--app-acrylic-bg); /* 对齐官网/PC 登录 pressed:白底 + inset 凹陷 */
border: 1px solid var(--app-acrylic-border); background: #ffffff;
border: none;
border-radius: 20px; border-radius: 20px;
padding: 28px 20px 22px; padding: 28px 20px 22px;
box-shadow: none; box-shadow:
backdrop-filter: var(--app-acrylic-blur); inset 0 2px 8px rgba(15, 23, 42, 0.07),
-webkit-backdrop-filter: var(--app-acrylic-blur); inset 0 1px 1px rgba(15, 23, 42, 0.04);
backdrop-filter: none;
-webkit-backdrop-filter: none;
} }
.h5-login-card__brand { .h5-login-card__brand {
@ -262,11 +265,27 @@ onMounted(() => {
.h5-login-form :deep(.arco-input-wrapper), .h5-login-form :deep(.arco-input-wrapper),
.h5-login-form :deep(.arco-input-password) { .h5-login-form :deep(.arco-input-password) {
border-radius: 12px; border-radius: 12px;
background: var(--app-acrylic-bg); background: #f0f3f4;
border: 1px solid var(--app-acrylic-border); border: none;
min-height: 48px; min-height: 48px;
backdrop-filter: var(--app-acrylic-blur); backdrop-filter: none;
-webkit-backdrop-filter: var(--app-acrylic-blur); -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 { .h5-login-error {

View File

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

View File

@ -12,8 +12,16 @@
--h5-border-soft: #f2f3f5; --h5-border-soft: #f2f3f5;
--h5-radius: 16px; --h5-radius: 16px;
--h5-radius-sm: 12px; --h5-radius-sm: 12px;
--h5-shadow: 0 4px 16px rgba(15, 23, 42, 0.05); /* pressed:外层白卡 / 内层浅井(阴影偏淡) */
--h5-shadow-lift: 0 8px 24px rgba(15, 23, 42, 0.08); --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: #ff7d00;
--h5-warning-bg: #fff7e8; --h5-warning-bg: #fff7e8;
--h5-danger: #f53f3f; --h5-danger: #f53f3f;

View File

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

View File

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

View File

@ -439,13 +439,16 @@ onMounted(async () => {
} }
.h5-login-card { .h5-login-card {
background: var(--app-acrylic-bg); /* 对齐官网/PC 登录 pressed:白底 + inset 凹陷 */
border: 1px solid var(--app-acrylic-border); background: #ffffff;
border: none;
border-radius: 20px; border-radius: 20px;
padding: 28px 20px 22px; padding: 28px 20px 22px;
box-shadow: none; box-shadow:
backdrop-filter: var(--app-acrylic-blur); inset 0 2px 8px rgba(15, 23, 42, 0.07),
-webkit-backdrop-filter: var(--app-acrylic-blur); inset 0 1px 1px rgba(15, 23, 42, 0.04);
backdrop-filter: none;
-webkit-backdrop-filter: none;
} }
/* 品牌放进卡片:一个完整登录单元(Clerk / Vercel 鉴权卡常见) */ /* 品牌放进卡片:一个完整登录单元(Clerk / Vercel 鉴权卡常见) */
@ -504,22 +507,26 @@ onMounted(async () => {
.h5-login-form :deep(.arco-input-wrapper), .h5-login-form :deep(.arco-input-wrapper),
.h5-login-form :deep(.arco-input-password) { .h5-login-form :deep(.arco-input-password) {
border-radius: 12px; border-radius: 12px;
background: var(--app-acrylic-bg); background: #f0f3f4;
border: 1px solid var(--app-acrylic-border); border: none;
min-height: 48px; min-height: 48px;
backdrop-filter: var(--app-acrylic-blur); backdrop-filter: none;
-webkit-backdrop-filter: var(--app-acrylic-blur); -webkit-backdrop-filter: none;
transition: box-shadow: inset 0 2px 8px rgba(15, 23, 42, 0.08);
border-color 0.2s var(--login-ease), transition: box-shadow 0.2s var(--login-ease), background 0.2s var(--login-ease);
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-wrapper-focus,
&.arco-input-focus { &.arco-input-focus {
background: var(--app-acrylic-hover-bg); background: #f0f3f4;
border-color: var(--app-acrylic-border-strong); border: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--app-primary-color) 14%, transparent); 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 { .quote-form {
background: #fff; /* 对齐工作台 pressed:白底 + inset 凹陷 */
border-radius: 18px; background: #ffffff;
border: none;
border-radius: 16px;
padding: 18px 16px 14px; 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; text-align: left;
&.is-collapsed { &.is-collapsed {
@ -2130,7 +2134,7 @@ onMounted(() => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 8px; gap: 8px;
box-shadow: 0 8px 20px color-mix(in srgb, var(--app-primary-color) 28%, transparent); box-shadow: none;
&:active { &:active {
transform: scale(0.985); transform: scale(0.985);

View File

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

View File

@ -240,18 +240,24 @@ onMounted(() => {
height: 28px; height: 28px;
padding: 0 10px; padding: 0 10px;
border-radius: 8px; border-radius: 8px;
border: 1px solid var(--app-acrylic-border); border: none;
background: var(--app-acrylic-bg); background: #ffffff;
backdrop-filter: var(--app-acrylic-blur); box-shadow:
-webkit-backdrop-filter: var(--app-acrylic-blur); 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); color: var(--aw-ink);
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: box-shadow 0.2s ease;
&:hover:not(:disabled) { &:hover:not(:disabled) {
border-color: var(--app-acrylic-border-strong); background: #ffffff;
background: var(--app-acrylic-hover-bg); box-shadow:
inset 0 0 0 1px rgba(15, 23, 42, 0.08),
inset 0 2px 6px rgba(15, 23, 42, 0.07);
} }
&:disabled { &:disabled {
@ -285,9 +291,27 @@ onMounted(() => {
.stat-card { .stat-card {
min-height: 84px; min-height: 84px;
padding: 12px 14px 10px; 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 { &.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; margin: 0;
padding: 10px 12px 8px; padding: 10px 12px 8px;
border-radius: 14px; 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 { .stat-grid--inner .stat-card__value {

View File

@ -156,13 +156,28 @@ function go(path: string, query?: Record<string, string>) {
<style scoped lang="less"> <style scoped lang="less">
.admin-workplace { .admin-workplace {
/* 对齐官网首页 pressed 卡片:无描边 + inset 凹陷 */
--aw-ink: #1a2e38; --aw-ink: #1a2e38;
--aw-muted: rgba(26, 46, 56, 0.68); --aw-muted: rgba(26, 46, 56, 0.68);
--aw-soft: rgba(26, 46, 56, 0.48); --aw-soft: rgba(26, 46, 56, 0.48);
--aw-edge: var(--app-acrylic-border); --aw-edge: transparent;
--aw-shadow: none;
--aw-radius: 16px; --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%; width: 100%;
height: 100%; height: 100%;
@ -217,13 +232,14 @@ function go(path: string, query?: Record<string, string>) {
} }
.aw-panel { .aw-panel {
border: 1px solid var(--aw-edge); border: none;
border-radius: var(--aw-radius); border-radius: var(--aw-radius);
background: var(--aw-panel-bg); background: var(--aw-panel-bg);
box-shadow: var(--aw-shadow); box-shadow: var(--aw-panel-shadow);
backdrop-filter: var(--app-acrylic-blur); backdrop-filter: none;
-webkit-backdrop-filter: var(--app-acrylic-blur); -webkit-backdrop-filter: none;
min-width: 0; min-width: 0;
transition: box-shadow 0.2s ease;
} }
.aw-panel--hero { .aw-panel--hero {
@ -319,7 +335,6 @@ function go(path: string, query?: Record<string, string>) {
.aw-link { .aw-link {
--tone: var(--app-primary-color); --tone: var(--app-primary-color);
--tone-edge: var(--app-acrylic-border);
--tone-strong: var(--app-primary-color); --tone-strong: var(--app-primary-color);
display: flex; display: flex;
@ -328,19 +343,18 @@ function go(path: string, query?: Record<string, string>) {
min-height: 56px; min-height: 56px;
padding: 8px 12px; padding: 8px 12px;
border-radius: 12px; border-radius: 12px;
border: 1px solid var(--tone-edge); border: none;
background: var(--app-acrylic-bg); background: var(--aw-well-bg);
backdrop-filter: var(--app-acrylic-blur); backdrop-filter: none;
-webkit-backdrop-filter: var(--app-acrylic-blur); -webkit-backdrop-filter: none;
box-shadow: none; box-shadow: var(--aw-well-shadow);
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
transition: border-color 0.2s ease, background 0.2s ease; transition: box-shadow 0.2s ease, background 0.2s ease;
&:hover { &:hover {
border-color: var(--app-acrylic-border-strong); background: var(--aw-well-bg);
background: var(--app-acrylic-hover-bg); box-shadow: var(--aw-well-shadow-deep);
box-shadow: none;
} }
&:focus-visible { &:focus-visible {
@ -351,31 +365,26 @@ function go(path: string, query?: Record<string, string>) {
.aw-tone--ember { .aw-tone--ember {
--tone: #c45a3a; --tone: #c45a3a;
--tone-edge: rgba(196, 90, 58, 0.35);
--tone-strong: #b84a2e; --tone-strong: #b84a2e;
} }
.aw-tone--apricot { .aw-tone--apricot {
--tone: #d9924a; --tone: #d9924a;
--tone-edge: rgba(217, 146, 74, 0.35);
--tone-strong: #c47d32; --tone-strong: #c47d32;
} }
.aw-tone--honey { .aw-tone--honey {
--tone: #d4a05c; --tone: #d4a05c;
--tone-edge: rgba(212, 160, 92, 0.35);
--tone-strong: #c08a3e; --tone-strong: #c08a3e;
} }
.aw-tone--clay { .aw-tone--clay {
--tone: #c17a52; --tone: #c17a52;
--tone-edge: rgba(193, 122, 82, 0.35);
--tone-strong: #a8663f; --tone-strong: #a8663f;
} }
.aw-tone--sand { .aw-tone--sand {
--tone: #c9a07a; --tone: #c9a07a;
--tone-edge: rgba(201, 160, 122, 0.35);
--tone-strong: #b08a62; --tone-strong: #b08a62;
} }
@ -420,6 +429,68 @@ function go(path: string, query?: Record<string, string>) {
border-radius: 0; 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) { @media (max-width: 1100px) {
.admin-workplace__container { .admin-workplace__container {
flex-direction: column; flex-direction: column;

View File

@ -42,11 +42,21 @@ defineOptions({ name: 'CustomerWorkplace' })
<style lang="less" scoped> <style lang="less" scoped>
.workplace-container { .workplace-container {
/* 对齐管理台:外层轻压,内卡细边 + 轻 inset(上方不发灰) */
--wp-card-radius: 16px; --wp-card-radius: 16px;
--wp-inner-radius: 12px; --wp-inner-radius: 12px;
--wp-card-border: 1px solid var(--app-acrylic-border); --wp-card-border: none;
--wp-card-bg: var(--app-acrylic-bg); --wp-card-bg: #ffffff;
--wp-card-shadow: none; --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%; width: 100%;
height: 100%; height: 100%;
min-height: 100%; min-height: 100%;
@ -185,12 +195,14 @@ defineOptions({ name: 'CustomerWorkplace' })
border-radius: var(--wp-card-radius); border-radius: var(--wp-card-radius);
background: var(--wp-card-bg); background: var(--wp-card-bg);
box-shadow: var(--wp-card-shadow); box-shadow: var(--wp-card-shadow);
backdrop-filter: var(--app-acrylic-blur); backdrop-filter: none;
-webkit-backdrop-filter: var(--app-acrylic-blur); -webkit-backdrop-filter: none;
flex-shrink: 0; flex-shrink: 0;
:deep(.arco-card) { :deep(.arco-card) {
background: transparent; background: transparent;
border: none;
box-shadow: none;
} }
:deep(.arco-card-header), :deep(.arco-card-header),
@ -266,12 +278,66 @@ defineOptions({ name: 'CustomerWorkplace' })
} }
:deep(.marketing-shortcuts-panel) { :deep(.marketing-shortcuts-panel) {
border: var(--wp-card-border); border: none;
border-radius: var(--wp-card-radius); border-radius: var(--wp-card-radius);
background: var(--wp-card-bg); background: var(--wp-card-bg);
box-shadow: var(--wp-card-shadow); box-shadow: var(--wp-card-shadow);
backdrop-filter: var(--app-acrylic-blur); backdrop-filter: none;
-webkit-backdrop-filter: var(--app-acrylic-blur); -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) { :deep(.chart-controls) {

View File

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

View File

@ -280,10 +280,21 @@ onMounted(() => {
margin-top: 8px; margin-top: 8px;
} }
/* 首页统计卡与营销入口统一磨砂质感(继承 stat-card.less) */ /* 统计卡:白底轻 inset(外层面板由 CustomerWorkplace 压入) */
.stat-card { .stat-card {
min-height: 84px; min-height: 84px;
padding: 12px 14px 10px; 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 { .stat-card--compact {

View File

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

View File

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

View File

@ -7,9 +7,6 @@
}" }"
aria-live="polite" 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"> <div class="login-care-banner__body">
<p class="login-care-banner__greeting">{{ greeting }}</p> <p class="login-care-banner__greeting">{{ greeting }}</p>
<p class="login-care-banner__note">{{ note }}</p> <p class="login-care-banner__note">{{ note }}</p>
@ -90,10 +87,11 @@ onBeforeUnmount(() => {
<style lang="less" scoped> <style lang="less" scoped>
.login-care-banner { .login-care-banner {
--care-ink: #5c3d2e; /* 与登录卡片同系:贴合页底 + 单层 inset,无彩斑 */
--care-muted: rgba(92, 61, 46, 0.72); --care-ink: #1e293b;
--care-edge: rgba(255, 214, 170, 0.55); --care-muted: rgba(71, 85, 105, 0.88);
--care-glow: rgba(255, 176, 120, 0.35); --care-bg: var(--app-page-bg, #f0f3f4);
--care-shadow: inset 0 3px 12px rgba(15, 23, 42, 0.1);
position: absolute; position: absolute;
top: calc(16px + env(safe-area-inset-top, 0px)); top: calc(16px + env(safe-area-inset-top, 0px));
@ -101,99 +99,34 @@ onBeforeUnmount(() => {
z-index: 40; z-index: 40;
width: min(360px, calc(100vw - 40px)); width: min(360px, calc(100vw - 40px));
padding: 16px 18px; padding: 16px 18px;
border-radius: 22px; border-radius: 20px;
text-align: left; text-align: left;
pointer-events: none; pointer-events: none;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
isolation: isolate; isolation: isolate;
border: 1px solid var(--care-edge); border: none;
background: background: var(--care-bg);
linear-gradient(145deg, rgba(255, 248, 240, 0.82), rgba(255, 236, 220, 0.55)), backdrop-filter: none;
rgba(255, 250, 245, 0.65); -webkit-backdrop-filter: none;
backdrop-filter: blur(16px) saturate(1.25); box-shadow: var(--care-shadow);
-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);
opacity: 0; opacity: 0;
transform: translate3d(18px, -12px, 0) scale(0.92); transform: translate3d(12px, -8px, 0);
transform-origin: top right; transform-origin: top right;
transition: transition:
opacity 0.42s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.35s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.5s cubic-bezier(0.34, 1.35, 0.64, 1), transform 0.4s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.42s ease, box-shadow 0.3s ease;
border-radius 0.8s ease;
&.is-visible { &.is-visible {
opacity: 1; opacity: 1;
transform: translate3d(0, 0, 0) scale(1); transform: translate3d(0, 0, 0);
border-radius: 24px 18px 26px 20px; box-shadow: inset 0 4px 14px rgba(15, 23, 42, 0.12);
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;
} }
&.is-after-hours { &.is-after-hours {
--care-ink: #4a3a55; --care-ink: #1e293b;
--care-muted: rgba(74, 58, 85, 0.72); --care-muted: rgba(71, 85, 105, 0.88);
--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%);
} }
} }
@ -204,9 +137,9 @@ onBeforeUnmount(() => {
.login-care-banner__greeting { .login-care-banner__greeting {
margin: 0 0 6px; margin: 0 0 6px;
font-size: 20px; font-size: 18px;
font-weight: 700; font-weight: 700;
letter-spacing: 0.02em; letter-spacing: 0.01em;
line-height: 1.35; line-height: 1.35;
color: var(--care-ink); color: var(--care-ink);
} }
@ -218,49 +151,6 @@ onBeforeUnmount(() => {
color: var(--care-muted); 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) { @media screen and (max-width: 768px) {
.login-care-banner { .login-care-banner {
top: calc(12px + env(safe-area-inset-top, 0px)); top: calc(12px + env(safe-area-inset-top, 0px));
@ -270,21 +160,15 @@ onBeforeUnmount(() => {
transform-origin: top center; transform-origin: top center;
&:not(.is-visible) { &:not(.is-visible) {
transform: translate3d(0, -10px, 0) scale(0.94); transform: translate3d(0, -8px, 0);
} }
} }
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.login-care-banner,
.login-care-banner__blob {
animation: none !important;
}
.login-care-banner { .login-care-banner {
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
transform: none; transform: none;
border-radius: 16px;
&.is-visible { &.is-visible {
transform: none; transform: none;

View File

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

View File

@ -47,13 +47,6 @@
/> />
</aside> </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 v-if="shellReady" ref="rightPaneRef" class="login-pane login-pane--right">
<div ref="loginFormRef" :class="['login-form', { 'login-form--ready': shellReady }]"> <div ref="loginFormRef" :class="['login-form', { 'login-form--ready': shellReady }]">
<div class="login-form-toolbar"> <div class="login-form-toolbar">
@ -346,7 +339,6 @@ onBeforeUnmount(() => {
/* Safety net: admin overlays are outside pane clip-paths */ /* Safety net: admin overlays are outside pane clip-paths */
.login-hero-panel--admin, .login-hero-panel--admin,
.login-characters-panel--admin,
:deep(.login-care-banner) { :deep(.login-care-banner) {
opacity: 0 !important; opacity: 0 !important;
visibility: hidden !important; visibility: hidden !important;
@ -481,22 +473,6 @@ onBeforeUnmount(() => {
padding: 0 0 16px 8px; 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 { .login-form-toolbar {
position: absolute; position: absolute;
top: 14px; top: 14px;
@ -535,29 +511,35 @@ onBeforeUnmount(() => {
} }
.login-form { .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; position: relative;
z-index: 10; z-index: 10;
width: 100%; width: 100%;
max-width: 430px; max-width: 430px;
min-height: auto; min-height: auto;
padding: 51px 32px 45px; padding: 51px 32px 45px;
background: var(--app-acrylic-bg); background: var(--login-card-bg);
border: 1px solid var(--app-acrylic-border); border: none;
border-radius: 24px; outline: none;
box-shadow: none; border-radius: 20px;
backdrop-filter: var(--app-acrylic-blur); box-shadow: var(--login-card-shadow);
-webkit-backdrop-filter: var(--app-acrylic-blur); backdrop-filter: none;
-webkit-backdrop-filter: none;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
opacity: 1; opacity: 1;
transform: none; 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 { .login-form:hover {
border-color: var(--app-acrylic-border-strong); background: var(--login-card-bg);
background: var(--app-acrylic-hover-bg); box-shadow: var(--login-card-shadow);
box-shadow: none;
} }
.login-form--ready { .login-form--ready {

View File

@ -29,9 +29,9 @@
parallaxStrength?: number; parallaxStrength?: number;
grain?: boolean; grain?: boolean;
grainIntensity?: number; grainIntensity?: number;
/** Cap canvas DPR (retina 2–3 is expensive for raymarch). */ /** Cap canvas DPR (retina is expensive for raymarch). */
maxDpr?: number; 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; renderScale?: number;
/** Target animation FPS; lower = less GPU. */ /** Target animation FPS; lower = less GPU. */
fps?: number; fps?: number;
@ -380,7 +380,7 @@ void main() {
const currentMouse: [number, number] = [0.5, 0.5]; const currentMouse: [number, number] = [0.5, 0.5];
const targetMouse: [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) => { const onPointerMove = (e: PointerEvent) => {
if (!props.mouseInteraction) return; if (!props.mouseInteraction) return;
const rect = container.getBoundingClientRect(); const rect = container.getBoundingClientRect();
@ -404,10 +404,10 @@ void main() {
const paint = (t: number) => { const paint = (t: number) => {
mu.iTime.value = (t - t0) * 0.001; mu.iTime.value = (t - t0) * 0.001;
if (props.mouseInteraction) { const tx = props.mouseInteraction ? targetMouse[0] : 0.5;
currentMouse[0] += 0.05 * (targetMouse[0] - currentMouse[0]); const ty = props.mouseInteraction ? targetMouse[1] : 0.5;
currentMouse[1] += 0.05 * (targetMouse[1] - currentMouse[1]); currentMouse[0] += 0.05 * (tx - currentMouse[0]);
} currentMouse[1] += 0.05 * (ty - currentMouse[1]);
const m = mu.uMouse.value; const m = mu.uMouse.value;
m[0] = currentMouse[0]; m[0] = currentMouse[0];
m[1] = currentMouse[1]; m[1] = currentMouse[1];

View File

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

View File

@ -56,7 +56,8 @@
}>(); }>();
const partners = SITE_PARTNERS; 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 PX_PER_SEC = 26;
const viewRef = ref<HTMLElement | null>(null); 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 { body.command-home {
background: var(--kc-page-bg); background: #f3f5f6;
color: var(--kc-text); color: var(--kc-text);
font-family: var(--font-default); font-family: var(--font-default);
font-size: 1.05rem; 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 { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Modal } from '@arco-design/web-vue' 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 HandsontablePro from '@/components/HandsontablePro/index.vue'
import PaginationPro from '@/components/PaginationPro/index.vue' import PaginationPro from '@/components/PaginationPro/index.vue'
import ModalPro from '@/components/ModalPro/index.vue' import ModalPro from '@/components/ModalPro/index.vue'
@ -51,13 +51,24 @@ interface InboundBoxForm {
key: string key: string
boxSeqNo: string boxSeqNo: string
markBoxNo: string markBoxNo: string
weightG?: number /** 表单展示/录入用 kg;提交时换算为后端 WeightG */
weightKg?: number
lengthCm?: number lengthCm?: number
widthCm?: number widthCm?: number
heightCm?: number heightCm?: number
lines: InboundLineForm[] 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 { interface ReceiveLineForm {
lineId: number lineId: number
skuCode: string skuCode: string
@ -138,6 +149,10 @@ const formModel = reactive({
remark: '', remark: '',
}) })
const boxRows = ref<InboundBoxForm[]>([]) const boxRows = ref<InboundBoxForm[]>([])
/** 复制加箱:按当前箱复制重量/尺寸/SKU/计划 */
const batchAddCount = ref(5)
const batchAddPresets = [3, 5, 10] as const
const batchAddPopoverVisible = ref(false)
const boxTabItems = computed(() => const boxTabItems = computed(() =>
boxRows.value.map((box) => { boxRows.value.map((box) => {
@ -175,8 +190,8 @@ function displayOrDash(value: unknown) {
} }
function formatBoxWeight(box: InboundBoxForm) { function formatBoxWeight(box: InboundBoxForm) {
if (box.weightG == null || Number.isNaN(Number(box.weightG))) return '-' if (box.weightKg == null || Number.isNaN(Number(box.weightKg))) return '-'
return `${box.weightG} g` return `${box.weightKg} kg`
} }
function formatBoxSize(box: InboundBoxForm) { 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() { function resetBoxes() {
const first = createBox(1) const first = createBox(1)
boxRows.value = [first] boxRows.value = [first]
@ -272,23 +309,49 @@ function addBox() {
activeBoxKey.value = box.key 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 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] boxRows.value = [...boxRows.value, ...added]
if (added.length) activeBoxKey.value = added[added.length - 1].key 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() { function removeActiveBox() {
if (boxRows.value.length <= 1) { if (boxRows.value.length <= 1) {
$baseMessage(t('warehouse.inbound.message.keepOneBox'), 'warning') $baseMessage(t('warehouse.inbound.message.keepOneBox'), 'warning')
return return
} }
const key = activeBoxKey.value const key = activeBoxKey.value
const idx = boxRows.value.findIndex((b) => b.key === key)
const remain = boxRows.value.filter((b) => b.key !== key) const remain = boxRows.value.filter((b) => b.key !== key)
resequenceBoxes(remain)
boxRows.value = remain boxRows.value = remain
selectedBoxKeys.value = selectedBoxKeys.value.filter((k) => k !== key) 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) { 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()}`, key: `box-open-${pickField(b, 'id', 'Id') || index}-${Date.now()}`,
boxSeqNo: String(pickField(b, 'boxSeqNo', 'BoxSeqNo') ?? index + 1), boxSeqNo: String(pickField(b, 'boxSeqNo', 'BoxSeqNo') ?? index + 1),
markBoxNo: String(pickField(b, 'markBoxNo', 'MarkBoxNo') ?? ''), 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, lengthCm: pickField<number>(b, 'lengthCm', 'LengthCm') ?? undefined,
widthCm: pickField<number>(b, 'widthCm', 'WidthCm') ?? undefined, widthCm: pickField<number>(b, 'widthCm', 'WidthCm') ?? undefined,
heightCm: pickField<number>(b, 'heightCm', 'HeightCm') ?? undefined, heightCm: pickField<number>(b, 'heightCm', 'HeightCm') ?? undefined,
@ -638,7 +701,7 @@ async function handleSave() {
.map((box) => ({ .map((box) => ({
boxSeqNo: box.boxSeqNo, boxSeqNo: box.boxSeqNo,
markBoxNo: box.markBoxNo, markBoxNo: box.markBoxNo,
weightG: box.weightG, weightG: kgToGrams(box.weightKg),
lengthCm: box.lengthCm, lengthCm: box.lengthCm,
widthCm: box.widthCm, widthCm: box.widthCm,
heightCm: box.heightCm, heightCm: box.heightCm,
@ -1020,9 +1083,60 @@ onMounted(async () => {
<template #icon><IconPlus :size="12" /></template> <template #icon><IconPlus :size="12" /></template>
{{ t('warehouse.inbound.actions.addBox') }} {{ t('warehouse.inbound.actions.addBox') }}
</a-button> </a-button>
<a-button size="mini" @click="addBoxes(5)"> <a-popover
{{ t('warehouse.inbound.actions.addBox5') }} v-model:popup-visible="batchAddPopoverVisible"
</a-button> 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 <a-button
size="mini" size="mini"
status="danger" status="danger"
@ -1075,13 +1189,14 @@ onMounted(async () => {
<div class="inbound-box-meta__field inbound-box-meta__field--weight"> <div class="inbound-box-meta__field inbound-box-meta__field--weight">
<span class="inbound-box-meta__label">{{ t('warehouse.inbound.form.boxWeight') }}</span> <span class="inbound-box-meta__label">{{ t('warehouse.inbound.form.boxWeight') }}</span>
<a-input-number <a-input-number
v-model="box.weightG" v-model="box.weightKg"
:min="0" :min="0"
:precision="3"
hide-button hide-button
style="width: 100%" style="width: 100%"
:placeholder="t('warehouse.inbound.form.optional')" :placeholder="t('warehouse.inbound.form.optional')"
> >
<template #suffix>g</template> <template #suffix>kg</template>
</a-input-number> </a-input-number>
</div> </div>
<div class="inbound-box-meta__field inbound-box-meta__field--size"> <div class="inbound-box-meta__field inbound-box-meta__field--size">
@ -1230,6 +1345,14 @@ onMounted(async () => {
.wh-form-box-tabs { .wh-form-box-tabs {
margin-bottom: 12px; margin-bottom: 12px;
width: 100%;
min-width: 0;
overflow: hidden;
}
.inbound-batch-add-trigger-icon {
margin-left: 2px;
vertical-align: -1px;
} }
.inbound-view-basic { .inbound-view-basic {
@ -1387,3 +1510,53 @@ onMounted(async () => {
min-height: 32px; min-height: 32px;
} }
</style> </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>