1508 lines
34 KiB
Markdown
1508 lines
34 KiB
Markdown
# 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
|