Files
xinghuoapi/frontend/src/components/layout/EXAMPLES.md
ianshaw 5deef27e1d style(frontend): 优化 Components 代码风格和结构
- 统一移除语句末尾分号,规范代码格式
- 优化组件类型定义和 props 声明
- 改进组件文档和示例代码
- 提升代码可读性和一致性
2025-12-26 00:10:01 -08:00

13 KiB

Layout Component Examples

Example 1: Dashboard Page

<template>
  <AppLayout>
    <div class="space-y-6">
      <h1 class="text-3xl font-bold text-gray-900">Dashboard</h1>

      <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
        <!-- Stats Cards -->
        <div class="rounded-lg bg-white p-6 shadow">
          <div class="text-sm text-gray-600">API Keys</div>
          <div class="text-2xl font-bold text-gray-900">5</div>
        </div>

        <div class="rounded-lg bg-white p-6 shadow">
          <div class="text-sm text-gray-600">Total Usage</div>
          <div class="text-2xl font-bold text-gray-900">1,234</div>
        </div>

        <div class="rounded-lg bg-white p-6 shadow">
          <div class="text-sm text-gray-600">Balance</div>
          <div class="text-2xl font-bold text-indigo-600">${{ balance }}</div>
        </div>

        <div class="rounded-lg bg-white p-6 shadow">
          <div class="text-sm text-gray-600">Status</div>
          <div class="text-2xl font-bold text-green-600">Active</div>
        </div>
      </div>

      <div class="rounded-lg bg-white p-6 shadow">
        <h2 class="mb-4 text-xl font-semibold">Recent Activity</h2>
        <p class="text-gray-600">No recent activity</p>
      </div>
    </div>
  </AppLayout>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { AppLayout } from '@/components/layout'
import { useAuthStore } from '@/stores'

const authStore = useAuthStore()
const balance = computed(() => authStore.user?.balance.toFixed(2) || '0.00')
</script>

Example 2: Login Page

<template>
  <AuthLayout>
    <h2 class="mb-6 text-2xl font-bold text-gray-900">Welcome Back</h2>

    <form @submit.prevent="handleSubmit" class="space-y-4">
      <div>
        <label for="username" class="mb-1 block text-sm font-medium text-gray-700">
          Username
        </label>
        <input
          id="username"
          v-model="form.username"
          type="text"
          required
          class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-indigo-500"
          placeholder="Enter your username"
        />
      </div>

      <div>
        <label for="password" class="mb-1 block text-sm font-medium text-gray-700">
          Password
        </label>
        <input
          id="password"
          v-model="form.password"
          type="password"
          required
          class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-indigo-500"
          placeholder="Enter your password"
        />
      </div>

      <button
        type="submit"
        :disabled="loading"
        class="w-full rounded-lg bg-indigo-600 px-4 py-2 text-white transition-colors hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50"
      >
        {{ loading ? 'Logging in...' : 'Login' }}
      </button>
    </form>

    <template #footer>
      <p class="text-gray-600">
        Don't have an account?
        <router-link to="/register" class="font-medium text-indigo-600 hover:underline">
          Sign up
        </router-link>
      </p>
    </template>
  </AuthLayout>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { AuthLayout } from '@/components/layout'
import { useAuthStore, useAppStore } from '@/stores'

const router = useRouter()
const authStore = useAuthStore()
const appStore = useAppStore()

const form = ref({
  username: '',
  password: ''
})

const loading = ref(false)

async function handleSubmit() {
  loading.value = true
  try {
    await authStore.login(form.value)
    appStore.showSuccess('Login successful!')
    await router.push('/dashboard')
  } catch (error) {
    appStore.showError('Invalid username or password')
  } finally {
    loading.value = false
  }
}
</script>

Example 3: API Keys Page with Custom Header Title

<template>
  <AppLayout>
    <div class="space-y-6">
      <!-- Custom page header -->
      <div class="flex items-center justify-between">
        <h1 class="text-3xl font-bold text-gray-900">API Keys</h1>
        <button
          @click="showCreateModal = true"
          class="rounded-lg bg-indigo-600 px-4 py-2 text-white transition-colors hover:bg-indigo-700"
        >
          Create New Key
        </button>
      </div>

      <!-- API Keys List -->
      <div class="overflow-hidden rounded-lg bg-white shadow">
        <table class="min-w-full divide-y divide-gray-200">
          <thead class="bg-gray-50">
            <tr>
              <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-500">Name</th>
              <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-500">Key</th>
              <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-500">
                Status
              </th>
              <th class="px-6 py-3 text-left text-xs font-medium uppercase text-gray-500">
                Created
              </th>
              <th class="px-6 py-3 text-right text-xs font-medium uppercase text-gray-500">
                Actions
              </th>
            </tr>
          </thead>
          <tbody class="divide-y divide-gray-200 bg-white">
            <tr v-for="key in apiKeys" :key="key.id">
              <td class="whitespace-nowrap px-6 py-4">{{ key.name }}</td>
              <td class="px-6 py-4 font-mono text-sm">{{ key.key }}</td>
              <td class="px-6 py-4">
                <span
                  class="rounded-full px-2 py-1 text-xs"
                  :class="
                    key.status === 'active'
                      ? 'bg-green-100 text-green-800'
                      : 'bg-red-100 text-red-800'
                  "
                >
                  {{ key.status }}
                </span>
              </td>
              <td class="px-6 py-4 text-sm text-gray-500">
                {{ new Date(key.created_at).toLocaleDateString() }}
              </td>
              <td class="px-6 py-4 text-right">
                <button class="text-sm text-red-600 hover:text-red-800">Delete</button>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </AppLayout>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { AppLayout } from '@/components/layout'
import type { ApiKey } from '@/types'

const showCreateModal = ref(false)
const apiKeys = ref<ApiKey[]>([])

// Fetch API keys on mount
// fetchApiKeys();
</script>

Example 4: Admin Users Page

<template>
  <AppLayout>
    <div class="space-y-6">
      <div class="flex items-center justify-between">
        <h1 class="text-3xl font-bold text-gray-900">User Management</h1>
        <button
          @click="showCreateUser = true"
          class="rounded-lg bg-indigo-600 px-4 py-2 text-white transition-colors hover:bg-indigo-700"
        >
          Create User
        </button>
      </div>

      <!-- Users Table -->
      <div class="rounded-lg bg-white shadow">
        <div class="p-6">
          <div class="space-y-4">
            <div
              v-for="user in users"
              :key="user.id"
              class="flex items-center justify-between border-b pb-4"
            >
              <div>
                <div class="font-medium text-gray-900">{{ user.username }}</div>
                <div class="text-sm text-gray-500">{{ user.email }}</div>
              </div>
              <div class="flex items-center space-x-4">
                <span
                  class="rounded-full px-2 py-1 text-xs"
                  :class="
                    user.role === 'admin'
                      ? 'bg-purple-100 text-purple-800'
                      : 'bg-blue-100 text-blue-800'
                  "
                >
                  {{ user.role }}
                </span>
                <span class="text-sm font-medium text-gray-700">
                  ${{ user.balance.toFixed(2) }}
                </span>
                <button class="text-sm text-indigo-600 hover:text-indigo-800">Edit</button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </AppLayout>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { AppLayout } from '@/components/layout'
import type { User } from '@/types'

const showCreateUser = ref(false)
const users = ref<User[]>([])

// Fetch users on mount
// fetchUsers();
</script>

Example 5: Profile Page

<template>
  <AppLayout>
    <div class="max-w-2xl space-y-6">
      <h1 class="text-3xl font-bold text-gray-900">Profile Settings</h1>

      <!-- User Info Card -->
      <div class="space-y-4 rounded-lg bg-white p-6 shadow">
        <h2 class="text-xl font-semibold text-gray-900">Account Information</h2>

        <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
          <div>
            <label class="mb-1 block text-sm font-medium text-gray-700"> Username </label>
            <div class="rounded-lg bg-gray-50 px-3 py-2 text-gray-900">
              {{ user?.username }}
            </div>
          </div>

          <div>
            <label class="mb-1 block text-sm font-medium text-gray-700"> Email </label>
            <div class="rounded-lg bg-gray-50 px-3 py-2 text-gray-900">
              {{ user?.email }}
            </div>
          </div>

          <div>
            <label class="mb-1 block text-sm font-medium text-gray-700"> Role </label>
            <div class="rounded-lg bg-gray-50 px-3 py-2">
              <span
                class="rounded-full px-2 py-1 text-xs"
                :class="
                  user?.role === 'admin'
                    ? 'bg-purple-100 text-purple-800'
                    : 'bg-blue-100 text-blue-800'
                "
              >
                {{ user?.role }}
              </span>
            </div>
          </div>

          <div>
            <label class="mb-1 block text-sm font-medium text-gray-700"> Balance </label>
            <div class="rounded-lg bg-gray-50 px-3 py-2 font-semibold text-indigo-600">
              ${{ user?.balance.toFixed(2) }}
            </div>
          </div>
        </div>
      </div>

      <!-- Change Password Card -->
      <div class="space-y-4 rounded-lg bg-white p-6 shadow">
        <h2 class="text-xl font-semibold text-gray-900">Change Password</h2>

        <form @submit.prevent="handleChangePassword" class="space-y-4">
          <div>
            <label for="old-password" class="mb-1 block text-sm font-medium text-gray-700">
              Current Password
            </label>
            <input
              id="old-password"
              v-model="passwordForm.old_password"
              type="password"
              required
              class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:ring-2 focus:ring-indigo-500"
            />
          </div>

          <div>
            <label for="new-password" class="mb-1 block text-sm font-medium text-gray-700">
              New Password
            </label>
            <input
              id="new-password"
              v-model="passwordForm.new_password"
              type="password"
              required
              class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:ring-2 focus:ring-indigo-500"
            />
          </div>

          <button
            type="submit"
            class="rounded-lg bg-indigo-600 px-4 py-2 text-white transition-colors hover:bg-indigo-700"
          >
            Update Password
          </button>
        </form>
      </div>
    </div>
  </AppLayout>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import { AppLayout } from '@/components/layout'
import { useAuthStore, useAppStore } from '@/stores'

const authStore = useAuthStore()
const appStore = useAppStore()

const user = computed(() => authStore.user)

const passwordForm = ref({
  old_password: '',
  new_password: ''
})

async function handleChangePassword() {
  try {
    // await changePasswordAPI(passwordForm.value);
    appStore.showSuccess('Password updated successfully!')
    passwordForm.value = { old_password: '', new_password: '' }
  } catch (error) {
    appStore.showError('Failed to update password')
  }
}
</script>

Tips for Using Layouts

  1. Page Titles: Set route meta to automatically display page titles in the header
  2. Loading States: Use appStore.setLoading(true/false) for global loading indicators
  3. Toast Notifications: Use appStore.showSuccess(), appStore.showError(), etc.
  4. Authentication: All authenticated pages should use AppLayout
  5. Auth Pages: Login and Register pages should use AuthLayout
  6. Sidebar State: The sidebar state persists across navigation
  7. Mobile First: All examples are responsive by default using Tailwind's mobile-first approach