feat(admin): standard pagination + synced profile columns on users list

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-22 18:10:53 +08:00
parent 2f6b6afc92
commit eebf3b3334
12 changed files with 495 additions and 59 deletions
+30 -14
View File
@@ -264,8 +264,11 @@ describe('嵌入代码 v2 入口(UI-04:displayMode=vertical-stream → /embe
})
describe('buildPreviewPlacement 本地预览停靠几何(草稿即时反映,纯本地)', () => {
it('desktop:按配置展开(collapsed=collapsedDefault),几何随草稿字段变化', () => {
const placement = buildPreviewPlacement({ ...DEFAULT_EMBED_SETTINGS }, 'desktop')
it('desktop:按配置展开(collapsed=collapsedDefault),几何随草稿字段变化(carousel)', () => {
const placement = buildPreviewPlacement(
{ ...DEFAULT_EMBED_SETTINGS, displayMode: 'carousel' },
'desktop'
)
expect(placement).toEqual({
side: 'right',
size: 'md',
@@ -278,7 +281,14 @@ describe('buildPreviewPlacement 本地预览停靠几何(草稿即时反映,
})
// 表单字段变化 → 预览几何即时跟随(未保存修改即时反映的纯逻辑基础)
const changed = buildPreviewPlacement(
{ ...DEFAULT_EMBED_SETTINGS, position: 'left', size: 'sm', offsetX: 40, zIndex: 2000 },
{
...DEFAULT_EMBED_SETTINGS,
displayMode: 'carousel',
position: 'left',
size: 'sm',
offsetX: 40,
zIndex: 2000
},
'desktop'
)
expect(changed.side).toBe('left')
@@ -294,24 +304,30 @@ describe('buildPreviewPlacement 本地预览停靠几何(草稿即时反映,
})
// collapsedDefault 仅作用于桌面预览初始形态
expect(
buildPreviewPlacement({ ...DEFAULT_EMBED_SETTINGS, collapsedDefault: true }, 'desktop')
.collapsed
buildPreviewPlacement(
{ ...DEFAULT_EMBED_SETTINGS, displayMode: 'carousel', collapsedDefault: true },
'desktop'
).collapsed
).toBe(true)
})
it('mobile:窄屏强制 sm + 默认收起(§5.4 <480px);宽度按剩余宽收缩防溢出', () => {
const placement = buildPreviewPlacement({ ...DEFAULT_EMBED_SETTINGS }, 'mobile')
it('v2(vertical-stream):恒 195 宽(无 sm 降档),mobile 仍默认收起(§2.1)', () => {
expect(buildPreviewPlacement({ ...DEFAULT_EMBED_SETTINGS }, 'desktop').width).toBe(195)
expect(buildPreviewPlacement({ ...DEFAULT_EMBED_SETTINGS }, 'mobile').width).toBe(195)
expect(buildPreviewPlacement({ ...DEFAULT_EMBED_SETTINGS }, 'mobile').size).toBe('md')
expect(buildPreviewPlacement({ ...DEFAULT_EMBED_SETTINGS }, 'mobile').collapsed).toBe(true)
expect(buildPreviewPlacement({ ...DEFAULT_EMBED_SETTINGS }, 'desktop').collapsed).toBe(false)
})
it('mobile:窄屏强制 sm + 默认收起(§5.4 <480px);宽度按剩余宽收缩防溢出(carousel)', () => {
const v1 = { ...DEFAULT_EMBED_SETTINGS, displayMode: 'carousel' }
const placement = buildPreviewPlacement(v1, 'mobile')
expect(placement.size).toBe('sm')
expect(placement.width).toBe(280)
expect(placement.collapsed).toBe(true) // 窄屏强制收起(与 collapsedDefault 无关)
expect(
buildPreviewPlacement({ ...DEFAULT_EMBED_SETTINGS, collapsedDefault: false }, 'mobile')
.collapsed
).toBe(true)
expect(buildPreviewPlacement({ ...v1, collapsedDefault: false }, 'mobile').collapsed).toBe(true)
// 偏移挤占剩余宽 → 宽度取小(375 - 120 - 8 = 247)
expect(buildPreviewPlacement({ ...DEFAULT_EMBED_SETTINGS, offsetX: 120 }, 'mobile').width).toBe(
247
)
expect(buildPreviewPlacement({ ...v1, offsetX: 120 }, 'mobile').width).toBe(247)
})
})
+10 -4
View File
@@ -39,6 +39,10 @@ export const EMBED_Z_INDEX_MAX = 2147483000
export const EMBED_SCROLL_SPEED_MIN = 8
export const EMBED_SCROLL_SPEED_MAX = 32
/** v2 固定窗尺寸(CSS px,§2.1 硬性:PC/手机同值;对齐 loader V2_WIDTH/HEIGHT)。 */
export const EMBED_V2_WIDTH = 195
export const EMBED_V2_HEIGHT = 280
/** size → 悬浮组件宽度 px(sm 窄屏降级档;与 loader SIZE_WIDTH 对齐) */
export const EMBED_SIZE_WIDTH: Record<Api.EmbedSlot.Size, number> = { sm: 280, md: 320 }
@@ -253,22 +257,24 @@ export const EMBED_PREVIEW_FRAME_WIDTH: Record<'desktop' | 'mobile', number> = {
mobile: 375
}
/** 表单草稿 → 预览停靠几何(desktop=按配置;mobile=窄屏强制 sm+收起)。
/** 表单草稿 → 预览停靠几何(desktop=按配置;mobile=窄屏强制收起)。
* 数值全部经契约夹取——表单输入的中间态(空/越界)预览不抖动;宽度按
* loader applyWrapperBox 同规则取配置宽与视口剩余宽的较小值。 */
* loader applyWrapperBox 同规则取配置宽与视口剩余宽的较小值。
* UI-04:vertical-stream 恒 195 宽(§2.1 硬性,无 sm 降档——mobile 仅默认收起)。 */
export function buildPreviewPlacement(
draft: EmbedSettingsDraft,
mode: 'desktop' | 'mobile',
frameWidth: number = EMBED_PREVIEW_FRAME_WIDTH[mode]
): EmbedPreviewPlacement {
const narrow = mode === 'mobile'
const size: Api.EmbedSlot.Size = narrow ? 'sm' : draft.size
const isV2 = draft.displayMode === 'vertical-stream'
const size: Api.EmbedSlot.Size = narrow && !isV2 ? 'sm' : draft.size
const offsetX = clampOffsetX(draft.offsetX)
const available = Math.max(frameWidth - offsetX - 8, EMBED_PREVIEW_MIN_WIDTH)
return {
side: draft.position,
size,
width: Math.min(EMBED_SIZE_WIDTH[size], available),
width: Math.min(isV2 ? EMBED_V2_WIDTH : EMBED_SIZE_WIDTH[size], available),
offsetX,
offsetBottom: clampOffsetBottom(draft.offsetBottom),
zIndex: clampZIndex(draft.zIndex),