Files
sub2api-cn-relay-manager/frontend/src/components/common
IanShaw027 20c71acb3b fix(mobile): 优化移动端表格、操作栏和弹窗显示
**问题描述**:
- 表格在移动端显示列过多,需要横向滚动,内容被截断
- 顶部操作栏按钮拥挤,占用过多空间
- 弹窗表单在小屏幕上布局不合理
- "更多"操作菜单定位错误,位置过高或超出屏幕
- 滚动页面时菜单不会自动关闭,与卡片分离

**解决方案**:

1. **DataTable 组件 - 移动端卡片视图**
   - 在 < 768px 时自动切换到卡片布局
   - 每个表格行渲染为独立卡片,所有字段清晰可见
   - 操作按钮在卡片底部,触摸目标足够大
   - 支持深色模式,包含加载和空状态
   - 自动应用于所有使用 DataTable 的管理页面

2. **UsersView 顶部操作栏优化**
   - 移动端:搜索框全宽 + 次要按钮显示为图标 + 创建按钮突出
   - 桌面端:保持原有布局(图标 + 文字)
   - 使用响应式 Tailwind classes

3. **UserCreateModal 弹窗优化**
   - 余额/并发数字段:移动端单列,桌面端双列
   - 弹窗边距:移动端 8px,桌面端 16px

4. **操作菜单定位修复**
   - UsersView: 移动端菜单居中对齐按钮,智能定位
   - AccountsView: 移动端菜单优先显示在按钮下方
   - 所有情况下确保菜单不超出屏幕边界
   - 添加滚动监听,滚动时自动关闭菜单

**影响范围**:
- 所有使用 DataTable 的管理页面(8 个页面)自动获得移动端卡片视图
- 用户管理和账号管理页面的操作菜单定位优化
- 创建用户弹窗的响应式布局优化

**技术要点**:
- 使用 Tailwind 响应式断点(md:, sm:)
- 触摸目标 ≥ 44px
- 完整支持深色模式
- 向后兼容,桌面端保持原有布局
2026-01-15 10:08:14 +08:00
..
2025-12-18 13:50:39 +08:00
2025-12-18 14:26:55 +08:00

Common Components

This directory contains reusable Vue 3 components built with Composition API, TypeScript, and TailwindCSS.

Components

DataTable.vue

A generic data table component with sorting, loading states, and custom cell rendering.

Props:

  • columns: Column[] - Array of column definitions with key, label, sortable, and formatter
  • data: any[] - Array of data objects to display
  • loading?: boolean - Show loading skeleton
  • rowKey?: string | (row: any) => string | number - Row key field or resolver (defaults to row.id, falls back to index)

Slots:

  • empty - Custom empty state content
  • cell-{key} - Custom cell renderer for specific column (receives row and value)

Usage:

<DataTable
  :columns="[
    { key: 'name', label: 'Name', sortable: true },
    { key: 'email', label: 'Email' },
    { key: 'status', label: 'Status', formatter: (val) => val.toUpperCase() }
  ]"
  :data="users"
  :loading="isLoading"
>
  <template #cell-actions="{ row }">
    <button @click="editUser(row)">Edit</button>
  </template>
</DataTable>

Pagination.vue

Pagination component with page numbers, navigation, and page size selector.

Props:

  • total: number - Total number of items
  • page: number - Current page (1-indexed)
  • pageSize: number - Items per page
  • pageSizeOptions?: number[] - Available page size options (default: [10, 20, 50, 100])

Events:

  • update:page - Emitted when page changes
  • update:pageSize - Emitted when page size changes

Usage:

<Pagination
  :total="totalUsers"
  :page="currentPage"
  :pageSize="pageSize"
  @update:page="currentPage = $event"
  @update:pageSize="pageSize = $event"
/>

Modal.vue

Modal dialog with customizable size and close behavior.

Props:

  • show: boolean - Control modal visibility
  • title: string - Modal title
  • size?: 'sm' | 'md' | 'lg' | 'xl' | 'full' - Modal size (default: 'md')
  • closeOnEscape?: boolean - Close on Escape key (default: true)
  • closeOnClickOutside?: boolean - Close on backdrop click (default: true)

Events:

  • close - Emitted when modal should close

Slots:

  • default - Modal body content
  • footer - Modal footer content

Usage:

<Modal :show="showModal" title="Edit User" size="lg" @close="showModal = false">
  <form @submit.prevent="saveUser">
    <!-- Form content -->
  </form>

  <template #footer>
    <button @click="showModal = false">Cancel</button>
    <button @click="saveUser">Save</button>
  </template>
</Modal>

ConfirmDialog.vue

Confirmation dialog built on top of Modal component.

Props:

  • show: boolean - Control dialog visibility
  • title: string - Dialog title
  • message: string - Confirmation message
  • confirmText?: string - Confirm button text (default: 'Confirm')
  • cancelText?: string - Cancel button text (default: 'Cancel')
  • danger?: boolean - Use danger/red styling (default: false)

Events:

  • confirm - Emitted when user confirms
  • cancel - Emitted when user cancels

Usage:

<ConfirmDialog
  :show="showDeleteConfirm"
  title="Delete User"
  message="Are you sure you want to delete this user? This action cannot be undone."
  confirm-text="Delete"
  cancel-text="Cancel"
  danger
  @confirm="deleteUser"
  @cancel="showDeleteConfirm = false"
/>

StatCard.vue

Statistics card component for displaying metrics with optional change indicators.

Props:

  • title: string - Card title
  • value: number | string - Main value to display
  • icon?: Component - Icon component
  • change?: number - Percentage change value
  • changeType?: 'up' | 'down' | 'neutral' - Change direction (default: 'neutral')
  • formatValue?: (value) => string - Custom value formatter

Usage:

<StatCard title="Total Users" :value="1234" :icon="UserIcon" :change="12.5" change-type="up" />

Toast.vue

Toast notification component that automatically displays toasts from the app store.

Usage:

<!-- Add once in App.vue or layout -->
<Toast />
// Trigger toasts from anywhere using the app store
import { useAppStore } from '@/stores/app'

const appStore = useAppStore()

appStore.addToast({
  type: 'success',
  title: 'Success!',
  message: 'User created successfully',
  duration: 3000
})

appStore.addToast({
  type: 'error',
  message: 'Failed to delete user'
})

LoadingSpinner.vue

Simple animated loading spinner.

Props:

  • size?: 'sm' | 'md' | 'lg' | 'xl' - Spinner size (default: 'md')
  • color?: 'primary' | 'secondary' | 'white' | 'gray' - Spinner color (default: 'primary')

Usage:

<LoadingSpinner size="lg" color="primary" />

EmptyState.vue

Empty state placeholder with icon, message, and optional action button.

Props:

  • icon?: Component - Icon component
  • title: string - Empty state title
  • description: string - Empty state description
  • actionText?: string - Action button text
  • actionTo?: string | object - Router link destination
  • actionIcon?: boolean - Show plus icon in button (default: true)

Slots:

  • icon - Custom icon content
  • action - Custom action button/link

Usage:

<EmptyState
  title="No users found"
  description="Get started by creating your first user account."
  action-text="Add User"
  :action-to="{ name: 'users-create' }"
/>

Import

You can import components individually:

import { DataTable, Pagination, Modal } from '@/components/common'

Or import specific components:

import DataTable from '@/components/common/DataTable.vue'

Features

All components include:

  • TypeScript support with proper type definitions
  • Accessibility with ARIA attributes and keyboard navigation
  • Responsive design with mobile-friendly layouts
  • TailwindCSS styling for consistent design
  • Vue 3 Composition API with <script setup>
  • Slot support for customization