Files
medical-mall/docs/admin/PAGE_STRUCTURE_SPECIFICATION.md
2026-02-02 20:07:37 +08:00

34 KiB
Raw Blame History

Uni-App-X 页面结构规范 - CRMEB 风格

📚 概述

本文档定义了 mall 项目中所有 admin 页面的标准结构和编写方法,参考 CRMEB 的专业设计模式。


1. 页面基本结构

1.1 完整模板

所有 admin 页面都应遵循此结构:

<template>
  <AdminLayout :currentPage="pageName">
    <!-- 1. 页面标题和操作按钮 -->
    <view class="page-header">
      <view class="header-left">
        <text class="page-title">{{ pageTitle }}</text>
        <text class="page-subtitle">{{ pageSubtitle }}</text>
      </view>
      <view class="header-right">
        <button class="btn btn-primary" @click="handleCreate">
          <text class="icon">+</text>
          <text>新增</text>
        </button>
      </view>
    </view>

    <!-- 2. 搜索和过滤区域 -->
    <view class="search-section">
      <view class="search-card">
        <form class="search-form">
          <view class="form-row">
            <view class="form-item">
              <text class="form-label">搜索:</text>
              <input
                v-model="searchForm.keyword"
                class="input"
                placeholder="请输入关键词"
              />
            </view>
            <view class="form-actions">
              <button class="btn btn-primary" @click="handleSearch">搜索</button>
              <button class="btn btn-default" @click="handleReset">重置</button>
            </view>
          </view>
        </form>
      </view>
    </view>

    <!-- 3. 内容区域 -->
    <view class="content-section">
      <!-- 列表或详情内容 -->
      <view class="content-card">
        <!-- 内容 -->
      </view>
    </view>

    <!-- 4. 底部操作区 -->
    <view class="page-footer">
      <!-- 分页或其他底部操作 -->
    </view>
  </AdminLayout>
</template>

<script setup lang="uts">
import { ref, onShow, onMounted } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'

// ============ 页面元数据 ============
const pageName = ref('page-name')
const pageTitle = ref('页面标题')
const pageSubtitle = ref('页面描述')

// ============ 状态数据 ============
const searchForm = ref({
  keyword: '',
  status: '',
})

// ============ 生命周期 ============
onShow(() => {
  // 页面显示时执行
  loadData()
})

onMounted(() => {
  // 组件挂载后执行
  initializeData()
})

// ============ 数据加载 ============
const loadData = () => {
  // 从 API 加载数据
}

const initializeData = () => {
  // 初始化数据
}

// ============ 事件处理 ============
const handleCreate = () => {
  // 新增操作
}

const handleSearch = () => {
  // 搜索操作
}

const handleReset = () => {
  // 重置表单
  searchForm.value = {
    keyword: '',
    status: '',
  }
}
</script>

<style scoped lang="scss">
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: $space-lg;
  padding-bottom: $space-md;
  border-bottom: 1px solid $border-light;
}

.header-left {
  display: flex;
  flex-direction: column;
}

.page-title {
  font-size: $font-size-lg;
  font-weight: $font-weight-bold;
  color: $text-primary;
  margin-bottom: $space-xs;
}

.page-subtitle {
  font-size: $font-size-sm;
  color: $text-secondary;
}

.header-right {
  display: flex;
  gap: $space-sm;
}

.search-section {
  margin-bottom: $space-lg;
}

.search-card {
  background: $background-primary;
  border-radius: $radius;
  padding: $space-md;
  box-shadow: $shadow;
}

.search-form {
  display: flex;
  flex-direction: column;
  gap: $space-md;
}

.form-row {
  display: flex;
  gap: $space-md;
  flex-wrap: wrap;
  align-items: flex-end;
}

.form-item {
  display: flex;
  align-items: center;
  gap: $space-md;
  flex: 1;
  min-width: 200px;
}

.form-label {
  color: $text-secondary;
  font-size: $font-size-sm;
  white-space: nowrap;
}

.input {
  height: $input-height;
  padding: 0 $space-sm;
  border: 1px solid $border-color;
  border-radius: $radius-xs;
  flex: 1;
}

.form-actions {
  display: flex;
  gap: $space-sm;
}

.content-section {
  margin-bottom: $space-lg;
}

.content-card {
  background: $background-primary;
  border-radius: $radius;
  overflow: hidden;
  box-shadow: $shadow;
}

.page-footer {
  display: flex;
  justify-content: center;
  padding: $space-lg 0;
}
</style>

2. 列表页面ListPage

2.1 结构说明

列表页面的典型结构:

┌─────────────────────────────────────┐
│ 页面标题          [+ 新增] [导出]    │  <- page-header
├─────────────────────────────────────┤
│ ┌───────────────────────────────────┐│
│ │ 搜索 [输入框]     [搜索] [重置]    ││  <- search-section
│ └───────────────────────────────────┘│
├─────────────────────────────────────┤
│ ┌───────────────────────────────────┐│
│ │ 表格头 | 表格头 | 表格头 | 操作    ││
│ ├───────────────────────────────────┤│
│ │ 行数据 | 行数据 | 行数据 | 编辑删除 ││  <- list-card
│ │ 行数据 | 行数据 | 行数据 | 编辑删除 ││
│ │ 行数据 | 行数据 | 行数据 | 编辑删除 ││
│ └───────────────────────────────────┘│
├─────────────────────────────────────┤
│    [上一页] 第 1 页,共 10 页 [下一页] │  <- pagination
└─────────────────────────────────────┘

2.2 完整示例代码

<template>
  <AdminLayout :currentPage="pageName">
    <!-- 标题栏 -->
    <view class="page-header">
      <view class="header-left">
        <text class="page-title">系统管理</text>
        <text class="page-subtitle">管理系统配置和参数</text>
      </view>
      <view class="header-right">
        <button class="btn btn-primary" @click="handleCreate">
          <text>+ 新增</text>
        </button>
        <button class="btn btn-default" @click="handleExport">
          <text>导出</text>
        </button>
      </view>
    </view>

    <!-- 搜索区 -->
    <view class="search-section">
      <view class="search-card">
        <view class="form-row">
          <view class="form-item">
            <text class="form-label">名称:</text>
            <input
              v-model="searchForm.name"
              class="input"
              placeholder="请输入名称"
            />
          </view>
          <view class="form-item">
            <text class="form-label">状态:</text>
            <select v-model="searchForm.status" class="input">
              <option value="">全部</option>
              <option value="1">启用</option>
              <option value="0">禁用</option>
            </select>
          </view>
          <view class="form-actions">
            <button class="btn btn-primary" @click="handleSearch">搜索</button>
            <button class="btn btn-default" @click="handleReset">重置</button>
          </view>
        </view>
      </view>
    </view>

    <!-- 列表内容 -->
    <view class="content-section">
      <view class="list-card">
        <!-- 列表头 -->
        <view class="list-header">
          <view class="list-col col-checkbox">
            <input type="checkbox" v-model="selectAll" />
          </view>
          <view class="list-col col-id">ID</view>
          <view class="list-col col-name">名称</view>
          <view class="list-col col-status">状态</view>
          <view class="list-col col-time">创建时间</view>
          <view class="list-col col-action">操作</view>
        </view>

        <!-- 列表体 -->
        <view v-if="items.length > 0" class="list-body">
          <view class="list-item" v-for="item in items" :key="item.id">
            <view class="list-col col-checkbox">
              <input type="checkbox" :value="item.id" v-model="selectedIds" />
            </view>
            <view class="list-col col-id">{{ item.id }}</view>
            <view class="list-col col-name">
              <text class="item-name">{{ item.name }}</text>
            </view>
            <view class="list-col col-status">
              <view
                :class="['status-badge', `status-${item.status}`]"
              >
                {{ statusMap[item.status] }}
              </view>
            </view>
            <view class="list-col col-time">{{ formatTime(item.createTime) }}</view>
            <view class="list-col col-action">
              <button class="btn-link" @click="handleEdit(item)">编辑</button>
              <button class="btn-link btn-danger" @click="handleDelete(item)">删除</button>
            </view>
          </view>
        </view>

        <!-- 空状态 -->
        <view v-else class="list-empty">
          <text>暂无数据</text>
        </view>
      </view>
    </view>

    <!-- 分页 -->
    <view class="pagination">
      <button
        class="btn btn-sm"
        :disabled="pagination.page <= 1"
        @click="handlePrevPage"
      >
        上一页
      </button>
      <text class="page-info">
        第 {{ pagination.page }} 页,共 {{ pagination.total }} 页
      </text>
      <button
        class="btn btn-sm"
        :disabled="pagination.page >= pagination.total"
        @click="handleNextPage"
      >
        下一页
      </button>
    </view>
  </AdminLayout>
</template>

<script setup lang="uts">
import { ref, onShow } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'

const pageName = ref('system-info')
const selectAll = ref(false)
const selectedIds = ref<string[]>([])

const searchForm = ref({
  name: '',
  status: '',
})

const pagination = ref({
  page: 1,
  limit: 10,
  total: 0,
})

const items = ref<any[]>([])

const statusMap = {
  '1': '启用',
  '0': '禁用',
}

onShow(() => {
  loadData()
})

const loadData = () => {
  // 调用 API 加载数据
  // this.$api.list(searchForm.value, pagination.value).then(res => {
  //   items.value = res.data.items
  //   pagination.value.total = res.data.total
  // })
}

const handleCreate = () => {
  // 跳转到新增页面
  uni.navigateTo({
    url: `/pages/mall/admin/system/system-info-edit?mode=create`,
  })
}

const handleEdit = (item: any) => {
  uni.navigateTo({
    url: `/pages/mall/admin/system/system-info-edit?id=${item.id}&mode=edit`,
  })
}

const handleDelete = (item: any) => {
  // 确认删除
  uni.showModal({
    title: '删除确认',
    content: `确定删除 "${item.name}" 吗?`,
    success(res) {
      if (res.confirm) {
        // this.$api.delete(item.id).then(() => {
        //   uni.showToast({ title: '删除成功' })
        //   loadData()
        // })
      }
    },
  })
}

const handleSearch = () => {
  pagination.value.page = 1
  loadData()
}

const handleReset = () => {
  searchForm.value = { name: '', status: '' }
  handleSearch()
}

const handleExport = () => {
  // 导出数据
}

const handlePrevPage = () => {
  if (pagination.value.page > 1) {
    pagination.value.page--
    loadData()
  }
}

const handleNextPage = () => {
  if (pagination.value.page < pagination.value.total) {
    pagination.value.page++
    loadData()
  }
}

const formatTime = (time: string) => {
  return time.split(' ')[0]
}
</script>

<style scoped lang="scss">
// 页面头部
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: $space-lg;
  padding-bottom: $space-md;
  border-bottom: 1px solid $border-light;
}

.header-left {
  display: flex;
  flex-direction: column;
  gap: $space-xs;
}

.page-title {
  font-size: $font-size-lg;
  font-weight: $font-weight-bold;
  color: $text-primary;
}

.page-subtitle {
  font-size: $font-size-sm;
  color: $text-secondary;
}

.header-right {
  display: flex;
  gap: $space-sm;
}

// 搜索区域
.search-section {
  margin-bottom: $space-lg;
}

.search-card {
  background: $background-primary;
  border-radius: $radius;
  padding: $space-md;
  box-shadow: $shadow;
}

.form-row {
  display: flex;
  gap: $space-md;
  align-items: flex-end;
  flex-wrap: wrap;
}

.form-item {
  display: flex;
  align-items: center;
  gap: $space-md;
  flex: 1;
  min-width: 250px;
}

.form-label {
  color: $text-secondary;
  font-size: $font-size-sm;
  white-space: nowrap;
  width: 60px;
}

.input {
  height: $input-height;
  padding: 0 $space-sm;
  border: 1px solid $border-color;
  border-radius: $radius-xs;
  font-size: $font-size;
  flex: 1;
}

.form-actions {
  display: flex;
  gap: $space-sm;
}

// 列表卡片
.content-section {
  margin-bottom: $space-lg;
}

.list-card {
  background: $background-primary;
  border-radius: $radius;
  overflow: hidden;
  box-shadow: $shadow;
}

.list-header {
  display: flex;
  background: $background-secondary;
  border-bottom: 1px solid $border-light;
  padding: $space-md;
  font-weight: $font-weight-semibold;
  color: $text-primary;
  font-size: $font-size-sm;
}

.list-col {
  display: flex;
  align-items: center;
  padding: 0 $space-sm;

  &.col-checkbox {
    width: 40px;
    flex: none;
  }

  &.col-id {
    width: 60px;
    flex: none;
  }

  &.col-name {
    flex: 2;
  }

  &.col-status {
    flex: 1;
  }

  &.col-time {
    flex: 1;
  }

  &.col-action {
    width: 120px;
    flex: none;
    justify-content: flex-end;
  }
}

.list-body {
  display: flex;
  flex-direction: column;
}

.list-item {
  display: flex;
  align-items: center;
  padding: $space-md;
  border-bottom: 1px solid $border-light;
  transition: background-color $transition-duration $transition-timing;

  &:hover {
    background-color: $background-secondary;
  }

  &:last-child {
    border-bottom: none;
  }
}

.item-name {
  color: $primary-color;
  font-weight: $font-weight-medium;
}

.status-badge {
  display: inline-block;
  padding: $space-xs $space-sm;
  border-radius: $radius-xs;
  font-size: $font-size-xs;
  text-align: center;
  white-space: nowrap;

  &.status-1 {
    background: rgba($success-color, 0.1);
    color: $success-color;
  }

  &.status-0 {
    background: rgba($error-color, 0.1);
    color: $error-color;
  }
}

.btn-link {
  background: none;
  border: none;
  color: $primary-color;
  padding: 0;
  font-size: $font-size-sm;
  cursor: pointer;
  margin-right: $space-sm;

  &:last-child {
    margin-right: 0;
  }

  &.btn-danger {
    color: $error-color;
  }
}

.list-empty {
  padding: $space-xl;
  text-align: center;
  color: $text-tertiary;
  font-size: $font-size;
}

// 分页
.pagination {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: $space-md;
  padding: $space-lg 0;
}

.page-info {
  color: $text-secondary;
  font-size: $font-size-sm;
}
</style>

3. 表单页面FormPage

3.1 结构说明

表单页面的典型结构:

┌─────────────────────────────────────┐
│ 页面标题(新增/编辑)                 │  <- page-header
├─────────────────────────────────────┤
│ ┌───────────────────────────────────┐│
│ │ 基本信息                           ││
│ │ ├─ 名称 [输入框]                   ││
│ │ ├─ 描述 [文本框]                   ││
│ │ ├─ 分类 [下拉选择]                 ││
│ │ └─ 状态 [单选按钮]                 ││  <- form-card
│ └───────────────────────────────────┘│
│ ┌───────────────────────────────────┐│
│ │ 详细配置                           ││
│ │ ├─ 参数1 [输入框]                  ││
│ │ └─ 参数2 [输入框]                  ││
│ └───────────────────────────────────┘│
├─────────────────────────────────────┤
│              [保存] [取消]            │  <- form-footer
└─────────────────────────────────────┘

3.2 完整示例代码

<template>
  <AdminLayout :currentPage="pageName">
    <!-- 页面标题 -->
    <view class="page-header">
      <view class="header-left">
        <text class="page-title">{{ isEdit ? '编辑' : '新增' }}系统信息</text>
      </view>
    </view>

    <!-- 表单区域 -->
    <view class="form-section">
      <!-- 基本信息卡片 -->
      <view class="form-card">
        <view class="form-card-header">
          <text class="form-section-title">基本信息</text>
        </view>
        <view class="form-card-body">
          <!-- 名称 -->
          <view class="form-group">
            <label class="form-label required">名称</label>
            <input
              v-model="form.name"
              class="input"
              placeholder="请输入名称"
              @blur="validateField('name')"
            />
            <text v-if="errors.name" class="form-error">{{ errors.name }}</text>
          </view>

          <!-- 描述 -->
          <view class="form-group">
            <label class="form-label">描述</label>
            <textarea
              v-model="form.description"
              class="textarea"
              placeholder="请输入描述(可选)"
            />
          </view>

          <!-- 分类 -->
          <view class="form-group">
            <label class="form-label required">分类</label>
            <select v-model="form.category" class="input">
              <option value="">请选择分类</option>
              <option value="system">系统</option>
              <option value="user">用户</option>
              <option value="product">商品</option>
            </select>
            <text v-if="errors.category" class="form-error">{{ errors.category }}</text>
          </view>

          <!-- 状态 -->
          <view class="form-group">
            <label class="form-label">状态</label>
            <view class="radio-group">
              <label class="radio-item">
                <input type="radio" value="1" v-model="form.status" />
                <text class="radio-label">启用</text>
              </label>
              <label class="radio-item">
                <input type="radio" value="0" v-model="form.status" />
                <text class="radio-label">禁用</text>
              </label>
            </view>
          </view>
        </view>
      </view>

      <!-- 高级设置卡片 -->
      <view class="form-card">
        <view class="form-card-header">
          <text class="form-section-title">高级设置</text>
        </view>
        <view class="form-card-body">
          <!-- 参数1 -->
          <view class="form-group">
            <label class="form-label">参数1</label>
            <input
              v-model="form.param1"
              class="input"
              placeholder="请输入参数1"
            />
          </view>

          <!-- 参数2 -->
          <view class="form-group">
            <label class="form-label">参数2</label>
            <input
              v-model="form.param2"
              class="input"
              placeholder="请输入参数2"
            />
          </view>
        </view>
      </view>
    </view>

    <!-- 表单底部 -->
    <view class="form-footer">
      <button class="btn btn-primary" @click="handleSubmit" :disabled="loading">
        <text>{{ loading ? '保存中...' : '保存' }}</text>
      </button>
      <button class="btn btn-default" @click="handleCancel">
        <text>取消</text>
      </button>
    </view>
  </AdminLayout>
</template>

<script setup lang="uts">
import { ref, computed, onMounted } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'

const pageName = ref('system-info-edit')
const loading = ref(false)
const isEdit = ref(false)

const form = ref({
  name: '',
  description: '',
  category: '',
  status: '1',
  param1: '',
  param2: '',
})

const errors = ref({
  name: '',
  category: '',
})

onMounted(() => {
  // 获取路由参数
  const route = getCurrentPages()[0].$route
  const id = route?.query?.id
  const mode = route?.query?.mode

  isEdit.value = mode === 'edit'

  if (isEdit.value && id) {
    loadData(id)
  }
})

const loadData = (id: string) => {
  // 调用 API 加载数据
  // this.$api.get(id).then(res => {
  //   form.value = res.data
  // })
}

const validateField = (field: string) => {
  switch (field) {
    case 'name':
      errors.value.name = form.value.name ? '' : '名称不能为空'
      break
    case 'category':
      errors.value.category = form.value.category ? '' : '分类不能为空'
      break
  }
}

const validateForm = () => {
  validateField('name')
  validateField('category')
  return !errors.value.name && !errors.value.category
}

const handleSubmit = async () => {
  if (!validateForm()) {
    return
  }

  loading.value = true
  try {
    // const res = await this.$api[isEdit.value ? 'update' : 'create'](form.value)
    // uni.showToast({ title: '保存成功' })
    // setTimeout(() => {
    //   uni.navigateBack()
    // }, 1000)
  } catch (error) {
    uni.showToast({ title: '保存失败', icon: 'error' })
  } finally {
    loading.value = false
  }
}

const handleCancel = () => {
  uni.showModal({
    title: '提示',
    content: '放弃编辑并返回?',
    success(res) {
      if (res.confirm) {
        uni.navigateBack()
      }
    },
  })
}
</script>

<style scoped lang="scss">
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: $space-lg;
  padding-bottom: $space-md;
  border-bottom: 1px solid $border-light;
}

.header-left {
  display: flex;
  flex-direction: column;
}

.page-title {
  font-size: $font-size-lg;
  font-weight: $font-weight-bold;
  color: $text-primary;
}

// 表单区域
.form-section {
  margin-bottom: $space-lg;
  display: flex;
  flex-direction: column;
  gap: $space-lg;
}

.form-card {
  background: $background-primary;
  border-radius: $radius;
  overflow: hidden;
  box-shadow: $shadow;
}

.form-card-header {
  padding: $space-md $space-md;
  border-bottom: 1px solid $border-light;
  background: $background-secondary;
}

.form-section-title {
  font-size: $font-size-md;
  font-weight: $font-weight-semibold;
  color: $text-primary;
}

.form-card-body {
  padding: $space-md;
  display: flex;
  flex-direction: column;
  gap: $space-lg;
}

.form-group {
  display: flex;
  flex-direction: column;
  gap: $space-sm;
}

.form-label {
  font-size: $font-size-sm;
  color: $text-primary;
  font-weight: $font-weight-medium;

  &.required::before {
    content: '*';
    color: $error-color;
    margin-right: $space-xs;
  }
}

.input {
  height: $input-height;
  padding: 0 $space-sm;
  border: 1px solid $border-color;
  border-radius: $radius-xs;
  font-size: $font-size;
  color: $text-primary;

  &:focus {
    outline: none;
    border-color: $primary-color;
    box-shadow: 0 0 0 3px rgba($primary-color, 0.1);
  }

  &::placeholder {
    color: $text-tertiary;
  }
}

.textarea {
  padding: $space-sm;
  border: 1px solid $border-color;
  border-radius: $radius-xs;
  font-size: $font-size;
  color: $text-primary;
  min-height: 100px;
  resize: vertical;
  font-family: inherit;

  &:focus {
    outline: none;
    border-color: $primary-color;
    box-shadow: 0 0 0 3px rgba($primary-color, 0.1);
  }

  &::placeholder {
    color: $text-tertiary;
  }
}

.form-error {
  font-size: $font-size-xs;
  color: $error-color;
}

.radio-group {
  display: flex;
  gap: $space-md;
  flex-wrap: wrap;
}

.radio-item {
  display: flex;
  align-items: center;
  gap: $space-xs;
  cursor: pointer;
  user-select: none;
}

.radio-label {
  font-size: $font-size-sm;
  color: $text-primary;
}

// 表单底部
.form-footer {
  display: flex;
  justify-content: center;
  gap: $space-md;
  padding: $space-lg 0;
  border-top: 1px solid $border-light;
}
</style>

4. 详情页面DetailPage

4.1 结构说明

详情页面的典型结构:

┌─────────────────────────────────────┐
│ [< 返回] 页面标题      [编辑] [删除]  │  <- page-header
├─────────────────────────────────────┤
│ ┌───────────────────────────────────┐│
│ │ 基本信息                           ││
│ │ ├─ 名称: 某某                      ││
│ │ ├─ 描述: 描述内容                  ││
│ │ └─ 创建时间: 2024-01-01           ││  <- info-card
│ └───────────────────────────────────┘│
│ ┌───────────────────────────────────┐│
│ │ 操作日志                           ││
│ │ ├─ 2024-01-01 10:00 - 创建        ││  <- log-card
│ │ └─ 2024-01-02 11:00 - 更新        ││
│ └───────────────────────────────────┘│
└─────────────────────────────────────┘

4.2 完整示例代码

<template>
  <AdminLayout :currentPage="pageName">
    <!-- 页面标题 -->
    <view class="page-header">
      <view class="header-left">
        <button class="btn-icon" @click="handleGoBack">
          <text>← 返回</text>
        </button>
        <text class="page-title">{{ item.name }}</text>
      </view>
      <view class="header-right">
        <button class="btn btn-default" @click="handleEdit">编辑</button>
        <button class="btn btn-danger" @click="handleDelete">删除</button>
      </view>
    </view>

    <!-- 基本信息 -->
    <view class="detail-card">
      <view class="card-header">
        <text class="card-title">基本信息</text>
      </view>
      <view class="card-body">
        <view class="info-row">
          <text class="info-label">ID:</text>
          <text class="info-value">{{ item.id }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">名称:</text>
          <text class="info-value">{{ item.name }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">描述:</text>
          <text class="info-value">{{ item.description }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">状态:</text>
          <view class="status-badge" :class="[`status-${item.status}`]">
            {{ statusMap[item.status] }}
          </view>
        </view>
        <view class="info-row">
          <text class="info-label">创建时间:</text>
          <text class="info-value">{{ formatTime(item.createTime) }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">更新时间:</text>
          <text class="info-value">{{ formatTime(item.updateTime) }}</text>
        </view>
      </view>
    </view>

    <!-- 操作日志 -->
    <view class="detail-card">
      <view class="card-header">
        <text class="card-title">操作日志</text>
      </view>
      <view class="card-body">
        <view class="timeline">
          <view class="timeline-item" v-for="log in logs" :key="log.id">
            <view class="timeline-marker"></view>
            <view class="timeline-content">
              <view class="log-time">{{ formatTime(log.createTime) }}</view>
              <view class="log-action">{{ log.action }}</view>
              <view class="log-operator">操作人: {{ log.operator }}</view>
            </view>
          </view>
        </view>
      </view>
    </view>
  </AdminLayout>
</template>

<script setup lang="uts">
import { ref, onMounted } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'

const pageName = ref('system-info-detail')

const item = ref({
  id: '',
  name: '',
  description: '',
  status: '1',
  createTime: '',
  updateTime: '',
})

const logs = ref<any[]>([])

const statusMap = {
  '1': '启用',
  '0': '禁用',
}

onMounted(() => {
  const route = getCurrentPages()[0].$route
  const id = route?.query?.id
  if (id) {
    loadData(id)
  }
})

const loadData = (id: string) => {
  // 调用 API 加载详情
  // this.$api.getDetail(id).then(res => {
  //   item.value = res.data
  //   logs.value = res.data.logs
  // })
}

const handleGoBack = () => {
  uni.navigateBack()
}

const handleEdit = () => {
  uni.navigateTo({
    url: `/pages/mall/admin/system/system-info-edit?id=${item.value.id}&mode=edit`,
  })
}

const handleDelete = () => {
  uni.showModal({
    title: '删除确认',
    content: `确定删除吗?`,
    success(res) {
      if (res.confirm) {
        // this.$api.delete(item.value.id).then(() => {
        //   uni.showToast({ title: '删除成功' })
        //   setTimeout(() => {
        //     uni.navigateBack()
        //   }, 1000)
        // })
      }
    },
  })
}

const formatTime = (time: string) => {
  return time
}
</script>

<style scoped lang="scss">
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: $space-lg;
  padding-bottom: $space-md;
  border-bottom: 1px solid $border-light;
}

.header-left {
  display: flex;
  align-items: center;
  gap: $space-md;
}

.btn-icon {
  background: none;
  border: none;
  color: $primary-color;
  padding: 0;
  cursor: pointer;
}

.page-title {
  font-size: $font-size-lg;
  font-weight: $font-weight-bold;
  color: $text-primary;
}

.header-right {
  display: flex;
  gap: $space-sm;
}

// 详情卡片
.detail-card {
  background: $background-primary;
  border-radius: $radius;
  overflow: hidden;
  box-shadow: $shadow;
  margin-bottom: $space-lg;
}

.card-header {
  padding: $space-md;
  border-bottom: 1px solid $border-light;
  background: $background-secondary;
}

.card-title {
  font-size: $font-size-md;
  font-weight: $font-weight-semibold;
  color: $text-primary;
}

.card-body {
  padding: $space-md;
}

.info-row {
  display: flex;
  gap: $space-md;
  padding: $space-sm 0;
  border-bottom: 1px solid $border-light;

  &:last-child {
    border-bottom: none;
  }
}

.info-label {
  width: 120px;
  color: $text-secondary;
  font-size: $font-size-sm;
  font-weight: $font-weight-medium;
  white-space: nowrap;
}

.info-value {
  color: $text-primary;
  font-size: $font-size;
  flex: 1;
}

.status-badge {
  display: inline-block;
  padding: $space-xs $space-sm;
  border-radius: $radius-xs;
  font-size: $font-size-xs;

  &.status-1 {
    background: rgba($success-color, 0.1);
    color: $success-color;
  }

  &.status-0 {
    background: rgba($error-color, 0.1);
    color: $error-color;
  }
}

// 时间线
.timeline {
  display: flex;
  flex-direction: column;
  gap: $space-lg;
}

.timeline-item {
  display: flex;
  gap: $space-md;
}

.timeline-marker {
  width: 12px;
  height: 12px;
  border-radius: $radius-full;
  background: $primary-color;
  margin-top: $space-xs;
  flex: none;
}

.timeline-content {
  flex: 1;
  padding-bottom: $space-lg;
  border-left: 2px solid $border-light;
  padding-left: $space-md;
}

.log-time {
  font-size: $font-size-xs;
  color: $text-tertiary;
  margin-bottom: $space-xs;
}

.log-action {
  font-size: $font-size;
  color: $text-primary;
  margin-bottom: $space-xs;
}

.log-operator {
  font-size: $font-size-sm;
  color: $text-secondary;
}
</style>

5. 布局规范

5.1 FlexBox 布局规则

// 水平排列
.flex-row {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: $space-md;
}

// 垂直排列
.flex-col {
  display: flex;
  flex-direction: column;
  gap: $space-md;
}

// 间隔排列
.space-between {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

// 居中排列
.center {
  display: flex;
  justify-content: center;
  align-items: center;
}

5.2 Grid 栅格

// 栅格容器
.grid {
  display: grid;
  gap: $space-lg;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}

// 响应式栅格
@media (min-width: $breakpoint-lg) {
  .grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

6. 常见问题

Q1: 如何处理长文本溢出?

// 单行溢出
.text-truncate {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

// 多行溢出
.text-clamp {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

Q2: 如何实现响应式页面?

始终采用"移动优先"策略:

  1. 先为移动设备设计样式
  2. 再使用 @media 为更大屏幕添加样式
  3. 使用断点变量,不要硬编码断点值

Q3: 如何管理颜色主题?

所有颜色值必须使用 uni.scss 中定义的变量。如需更换主题,只需修改变量值。


总结

页面开发核心原则:

  1. 统一结构 - 所有页面遵循相同的结构模板
  2. 设计系统 - 所有样式使用 uni.scss 变量
  3. 组件复用 - 使用 AdminLayout 等通用组件
  4. 交互一致 - 遵循相同的交互和验证模式
  5. 响应式设计 - 移动优先,逐步增强

禁止做法:

  • 不要创建不遵循模板的页面
  • 不要使用硬编码的样式值
  • 不要重复代码,尽量复用组件
  • 不要创建孤立的页面样式

文档版本: 1.0
最后更新: 2026-01-31
维护者: AI Assistant