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

1508 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Uni-App-X 页面结构规范 - CRMEB 风格
## 📚 概述
本文档定义了 mall 项目中所有 admin 页面的标准结构和编写方法,参考 CRMEB 的专业设计模式。
---
## 1. 页面基本结构
### 1.1 完整模板
所有 admin 页面都应遵循此结构:
```uvue
<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 完整示例代码
```uvue
<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 完整示例代码
```uvue
<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 完整示例代码
```uvue
<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 布局规则
```scss
// 水平排列
.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 栅格
```scss
// 栅格容器
.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: 如何处理长文本溢出?
```scss
// 单行溢出
.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