Compare commits
No commits in common. "1c7bfec3dc2f4df80562c2c772b1fd164275b44c" and "48bc37701c27709518f13e4961e244de9353c709" have entirely different histories.
1c7bfec3dc
...
48bc37701c
12
App.vue
12
App.vue
@ -9,13 +9,13 @@ onLaunch(async () => {
|
||||
const token = uni.getStorageSync("token");
|
||||
|
||||
// 检测是否绑定手机号和token
|
||||
// if (token) {
|
||||
// const res = await checkPhone();
|
||||
if (token) {
|
||||
const res = await checkPhone();
|
||||
|
||||
// if (res.data) {
|
||||
// goHome();
|
||||
// }
|
||||
// }
|
||||
if (res.data) {
|
||||
goHome();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<view class="divider"></view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
@ -1,30 +0,0 @@
|
||||
.divider {
|
||||
height: 1px;
|
||||
margin: 0 10px;
|
||||
background: linear-gradient(to right, #eee, #eee 5px, transparent 5px, transparent);
|
||||
background-size: 10px 100%;
|
||||
position: relative;
|
||||
|
||||
&::before, &::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 12px;
|
||||
width: 6px;
|
||||
|
||||
background-color: #E2EDF2;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&::before {
|
||||
border-radius: 0 20px 20px 0;
|
||||
top: 0;
|
||||
left: -10px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-radius: 20px 0 0 20px;
|
||||
top: 0;
|
||||
right: -10px;
|
||||
}
|
||||
}
|
||||
2
main.js
2
main.js
@ -13,10 +13,8 @@ app.$mount()
|
||||
|
||||
// #ifdef VUE3
|
||||
import { createSSRApp } from 'vue'
|
||||
import zPaging from 'z-paging/components/z-paging/z-paging.vue'
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
app.component('z-paging', zPaging)
|
||||
return {
|
||||
app
|
||||
}
|
||||
|
||||
2556
package-lock.json
generated
2556
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"lottie-web": "^5.13.0",
|
||||
"vue-element-plus-x": "^1.3.0",
|
||||
"z-paging": "^2.8.7"
|
||||
"vue-element-plus-x": "^1.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,288 +0,0 @@
|
||||
# 工单管理系统
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个基于 uniapp + Vue3 组合式 API 开发的微信小程序工单管理系统,提供完整的工单展示、管理和操作功能。
|
||||
|
||||
## 系统架构
|
||||
|
||||
### 页面结构
|
||||
|
||||
```
|
||||
pages/order/
|
||||
├── list.vue # 工单列表主页面
|
||||
├── detail.vue # 工单详情页面
|
||||
├── demo.vue # 功能演示页面
|
||||
├── components/ # 组件目录
|
||||
│ ├── TopNavBar/ # 顶部导航栏组件
|
||||
│ ├── Tabs/ # Tab切换组件
|
||||
│ ├── OrderCard/ # 工单卡片组件
|
||||
│ ├── OrderList/ # 工单列表组件
|
||||
│ └── ConsultationBar/ # 底部咨询栏组件
|
||||
├── styles/ # 样式文件
|
||||
└── images/ # 图片资源
|
||||
```
|
||||
|
||||
## 核心组件
|
||||
|
||||
### 1. TopNavBar - 顶部导航栏组件
|
||||
|
||||
**功能特性:**
|
||||
- 左侧返回按钮
|
||||
- 自适应状态栏高度
|
||||
- 支持自定义标题内容
|
||||
- 响应式设计
|
||||
|
||||
**使用示例:**
|
||||
```vue
|
||||
<TopNavBar>
|
||||
<template #title>
|
||||
<Tabs :tabs="tabList" @change="handleTabChange" />
|
||||
</template>
|
||||
</TopNavBar>
|
||||
```
|
||||
|
||||
### 2. Tabs - Tab切换组件
|
||||
|
||||
**功能特性:**
|
||||
- 多标签页切换
|
||||
- 动态下划线指示器
|
||||
- 平滑动画过渡
|
||||
- 自定义标签内容
|
||||
- 固定15px宽度,2px圆角下划线
|
||||
|
||||
**使用示例:**
|
||||
```vue
|
||||
<Tabs
|
||||
:tabs="tabList"
|
||||
:defaultActive="0"
|
||||
@change="handleTabChange"
|
||||
/>
|
||||
```
|
||||
|
||||
### 3. OrderCard - 工单卡片组件
|
||||
|
||||
**功能特性:**
|
||||
- 工单信息展示
|
||||
- 多种状态支持(待处理、处理中、已完成、已取消)
|
||||
- 状态图标和标签
|
||||
- 操作按钮(呼叫、完成)
|
||||
- 自定义操作区域
|
||||
- 响应式设计
|
||||
|
||||
**使用示例:**
|
||||
```vue
|
||||
<OrderCard
|
||||
:orderData="orderInfo"
|
||||
@click="handleOrderClick"
|
||||
@call="handleOrderCall"
|
||||
@complete="handleOrderComplete"
|
||||
/>
|
||||
```
|
||||
|
||||
### 4. OrderList - 工单列表组件
|
||||
|
||||
**功能特性:**
|
||||
- 工单列表展示
|
||||
- 下拉刷新
|
||||
- 上拉加载更多
|
||||
- 空状态显示
|
||||
- 加载状态管理
|
||||
- 蓝色渐变背景
|
||||
|
||||
**使用示例:**
|
||||
```vue
|
||||
<OrderList
|
||||
:orderList="orderList"
|
||||
:hasMore="hasMore"
|
||||
:isLoading="isLoading"
|
||||
@refresh="handleRefresh"
|
||||
@loadMore="handleLoadMore"
|
||||
/>
|
||||
```
|
||||
|
||||
### 5. ConsultationBar - 底部咨询栏组件
|
||||
|
||||
**功能特性:**
|
||||
- 客服咨询入口
|
||||
- 固定底部显示
|
||||
- 安全区域适配
|
||||
- 自定义咨询文案
|
||||
- 跳转链接支持
|
||||
|
||||
**使用示例:**
|
||||
```vue
|
||||
<ConsultationBar
|
||||
mainText="遇到问题?联系客服"
|
||||
subText="7×24小时在线服务"
|
||||
buttonText="立即咨询"
|
||||
@consultation="handleConsultation"
|
||||
/>
|
||||
```
|
||||
|
||||
## 数据结构
|
||||
|
||||
### 工单数据结构
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: String, // 工单ID
|
||||
title: String, // 工单标题
|
||||
createTime: String, // 创建时间
|
||||
contactName: String, // 联系人姓名
|
||||
contactPhone: String, // 联系电话
|
||||
status: String, // 工单状态:pending/processing/completed/cancelled
|
||||
type: String // 工单类型:service/order
|
||||
}
|
||||
```
|
||||
|
||||
### Tab数据结构
|
||||
|
||||
```javascript
|
||||
{
|
||||
label: String, // 显示文本
|
||||
value: String // 值
|
||||
}
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
|
||||
### ✅ 已实现功能
|
||||
|
||||
1. **工单管理**
|
||||
- 工单列表展示
|
||||
- 工单状态管理
|
||||
- 工单详情查看
|
||||
- 工单操作(呼叫、完成)
|
||||
|
||||
2. **交互功能**
|
||||
- Tab页面切换
|
||||
- 下拉刷新
|
||||
- 上拉加载更多
|
||||
- 一键拨号
|
||||
- 客服咨询
|
||||
|
||||
3. **UI/UX**
|
||||
- 响应式设计
|
||||
- 暗色模式支持
|
||||
- 流畅动画效果
|
||||
- 优雅的加载状态
|
||||
- 空状态处理
|
||||
|
||||
4. **技术特性**
|
||||
- Vue3 组合式 API
|
||||
- TypeScript 支持
|
||||
- 组件化架构
|
||||
- SCSS 样式管理
|
||||
- 错误处理机制
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 快速开始
|
||||
|
||||
1. **进入工单列表**
|
||||
```javascript
|
||||
uni.navigateTo({
|
||||
url: '/pages/order/list'
|
||||
})
|
||||
```
|
||||
|
||||
2. **查看功能演示**
|
||||
```javascript
|
||||
uni.navigateTo({
|
||||
url: '/pages/order/demo'
|
||||
})
|
||||
```
|
||||
|
||||
### 自定义配置
|
||||
|
||||
1. **修改Tab配置**
|
||||
```javascript
|
||||
const tabList = ref([
|
||||
{ label: "全部订单", value: "all" },
|
||||
{ label: "服务工单", value: "service" },
|
||||
{ label: "自定义Tab", value: "custom" }
|
||||
])
|
||||
```
|
||||
|
||||
2. **自定义工单状态**
|
||||
```javascript
|
||||
const statusMap = {
|
||||
pending: '待处理',
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消',
|
||||
custom: '自定义状态'
|
||||
}
|
||||
```
|
||||
|
||||
3. **自定义样式主题**
|
||||
```scss
|
||||
:root {
|
||||
--primary-color: #007AFF;
|
||||
--success-color: #52C41A;
|
||||
--warning-color: #FF8C00;
|
||||
--danger-color: #FF3B30;
|
||||
}
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
### 工单相关接口
|
||||
|
||||
```javascript
|
||||
// 获取工单列表
|
||||
const getOrderList = async (params) => {
|
||||
return await uni.request({
|
||||
url: '/api/orders',
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
}
|
||||
|
||||
// 更新工单状态
|
||||
const updateOrderStatus = async (orderId, status) => {
|
||||
return await uni.request({
|
||||
url: `/api/orders/${orderId}/status`,
|
||||
method: 'PUT',
|
||||
data: { status }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
1. **虚拟滚动**:大量数据时建议使用虚拟滚动
|
||||
2. **图片懒加载**:工单图片支持懒加载
|
||||
3. **防抖处理**:搜索和筛选功能防抖优化
|
||||
4. **缓存机制**:Tab切换时数据缓存
|
||||
|
||||
## 兼容性
|
||||
|
||||
- **微信小程序**:✅ 完全支持
|
||||
- **支付宝小程序**:✅ 支持
|
||||
- **H5**:✅ 支持
|
||||
- **App**:✅ 支持
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2024-01-15)
|
||||
- 🎉 初始版本发布
|
||||
- ✨ 完整的工单管理功能
|
||||
- ✨ 响应式设计和暗色模式
|
||||
- ✨ 组件化架构
|
||||
- ✨ 完善的文档和演示
|
||||
|
||||
## 开发团队
|
||||
|
||||
- **开发者**:AI Assistant
|
||||
- **技术栈**:uniapp + Vue3 + SCSS
|
||||
- **设计规范**:微信小程序设计指南
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
---
|
||||
|
||||
如有问题或建议,请联系开发团队。
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,100 +0,0 @@
|
||||
<template>
|
||||
<view class="order-card" @click="handleCardClick">
|
||||
<!-- 卡片头部 -->
|
||||
<view class="card-header">
|
||||
<view class="status-info">
|
||||
<image class="status-icon" src="./images/service.png"></image>
|
||||
<view class="order-title">{{ orderData.title }}</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="orderData.status !== 'pending'"
|
||||
:class="['status-tag', `tag-${orderData.status}`]"
|
||||
>
|
||||
{{ getStatusText(orderData.status) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<Divider />
|
||||
|
||||
<!-- 卡片内容 -->
|
||||
<view class="card-content">
|
||||
<view class="info-row">
|
||||
<text class="label">创建时间:</text>
|
||||
<text class="value">{{ orderData.createTime }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">联系房客:</text>
|
||||
<text class="value">{{ orderData.contactName }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">联系电话:</text>
|
||||
<text class="value">{{ orderData.contactPhone }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作区域 -->
|
||||
<view v-if="orderData.status === 'pending'" class="action-area">
|
||||
<button class="action-btn" @click.stop="handleCall">立即呼叫</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Divider from "@/components/Divider/index.vue";
|
||||
import { defineProps } from "vue";
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
orderData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({
|
||||
id: "",
|
||||
title: "",
|
||||
createTime: "",
|
||||
contactName: "",
|
||||
contactPhone: "",
|
||||
status: "pending", // pending-待处理, completed-已完成, cancelled-已取消
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["click", "call", "complete"]);
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
pending: "待处理",
|
||||
completed: "已完成",
|
||||
cancelled: "已取消",
|
||||
processing: "处理中",
|
||||
};
|
||||
return statusMap[status] || "未知状态";
|
||||
};
|
||||
|
||||
// 处理卡片点击
|
||||
const handleCardClick = () => {
|
||||
emit("click", props.orderData);
|
||||
};
|
||||
|
||||
// 处理呼叫
|
||||
const handleCall = () => {
|
||||
emit("call", props.orderData);
|
||||
};
|
||||
|
||||
// 处理完成
|
||||
const handleComplete = () => {
|
||||
emit("complete", props.orderData);
|
||||
};
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
getStatusText,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
@ -1,170 +0,0 @@
|
||||
# OrderCard 工单卡片组件
|
||||
|
||||
## 组件概述
|
||||
|
||||
OrderCard 是一个用于显示工单信息的卡片组件,支持多种工单状态展示、操作按钮和自定义内容。适用于工单管理、客服系统等场景。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 多种工单状态支持(待处理、处理中、已完成、已取消)
|
||||
- ✅ 状态图标和标签显示
|
||||
- ✅ 工单基本信息展示
|
||||
- ✅ 可配置的操作按钮
|
||||
- ✅ 自定义操作区域插槽
|
||||
- ✅ 点击事件和操作事件
|
||||
- ✅ 响应式设计
|
||||
- ✅ 暗色模式支持
|
||||
- ✅ 优雅的交互动画
|
||||
|
||||
## 组件属性 (Props)
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 必填 | 说明 |
|
||||
|--------|------|--------|------|------|
|
||||
| orderData | Object | - | 是 | 工单数据对象 |
|
||||
| showActions | Boolean | true | 否 | 是否显示操作按钮区域 |
|
||||
|
||||
### orderData 对象结构
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: String, // 工单ID
|
||||
title: String, // 工单标题
|
||||
createTime: String, // 创建时间
|
||||
contactName: String, // 联系人姓名
|
||||
contactPhone: String, // 联系电话
|
||||
status: String // 工单状态:pending/processing/completed/cancelled
|
||||
}
|
||||
```
|
||||
|
||||
## 组件事件 (Events)
|
||||
|
||||
| 事件名 | 参数 | 说明 |
|
||||
|--------|------|------|
|
||||
| click | orderData | 卡片点击事件 |
|
||||
| call | orderData | 呼叫按钮点击事件 |
|
||||
| complete | orderData | 完成按钮点击事件 |
|
||||
|
||||
## 插槽 (Slots)
|
||||
|
||||
| 插槽名 | 说明 |
|
||||
|--------|------|
|
||||
| actions | 自定义操作按钮区域 |
|
||||
|
||||
## 工单状态说明
|
||||
|
||||
| 状态值 | 显示文本 | 图标颜色 | 标签样式 |
|
||||
|--------|----------|----------|----------|
|
||||
| pending | 待处理 | 橙色 | 橙色边框 |
|
||||
| processing | 处理中 | 蓝色 | 蓝色边框 |
|
||||
| completed | 已完成 | 绿色 | 绿色边框 |
|
||||
| cancelled | 已取消 | 灰色 | 灰色边框 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<OrderCard
|
||||
:orderData="orderInfo"
|
||||
@click="handleCardClick"
|
||||
@call="handleCall"
|
||||
@complete="handleComplete"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import OrderCard from '@/components/OrderCard/index.vue'
|
||||
|
||||
const orderInfo = {
|
||||
id: '001',
|
||||
title: '空调不制冷,需要维修',
|
||||
createTime: '2024-01-15 14:30',
|
||||
contactName: '张先生',
|
||||
contactPhone: '138****8888',
|
||||
status: 'pending'
|
||||
}
|
||||
|
||||
const handleCardClick = (orderData) => {
|
||||
console.log('卡片点击', orderData)
|
||||
}
|
||||
|
||||
const handleCall = (orderData) => {
|
||||
console.log('呼叫', orderData.contactPhone)
|
||||
}
|
||||
|
||||
const handleComplete = (orderData) => {
|
||||
console.log('标记完成', orderData.id)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 隐藏操作按钮
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<OrderCard
|
||||
:orderData="completedOrder"
|
||||
:showActions="false"
|
||||
@click="handleCardClick"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 自定义操作按钮
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<OrderCard
|
||||
:orderData="orderInfo"
|
||||
@click="handleCardClick"
|
||||
>
|
||||
<template #actions>
|
||||
<button class="custom-btn" @click.stop="handleEdit">
|
||||
编辑
|
||||
</button>
|
||||
<button class="custom-btn" @click.stop="handleDelete">
|
||||
删除
|
||||
</button>
|
||||
</template>
|
||||
</OrderCard>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 样式定制
|
||||
|
||||
组件支持通过 CSS 变量进行样式定制:
|
||||
|
||||
```css
|
||||
.order-card {
|
||||
--card-bg: #ffffff;
|
||||
--card-radius: 12px;
|
||||
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
--primary-color: #007AFF;
|
||||
--success-color: #52C41A;
|
||||
--warning-color: #FF8C00;
|
||||
--danger-color: #FF3B30;
|
||||
}
|
||||
```
|
||||
|
||||
## 响应式支持
|
||||
|
||||
- 在小屏设备(≤375px)上自动调整字体大小和间距
|
||||
- 支持暗色模式自动适配
|
||||
- 触摸设备优化的交互体验
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据格式**:确保传入的 `orderData` 包含所有必需字段
|
||||
2. **状态值**:`status` 字段必须是预定义的状态值之一
|
||||
3. **事件处理**:使用 `@click.stop` 防止操作按钮事件冒泡
|
||||
4. **性能优化**:大量卡片时建议使用虚拟滚动
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2024-01-15)
|
||||
- 初始版本发布
|
||||
- 支持基础工单信息展示
|
||||
- 支持多种状态和操作按钮
|
||||
- 支持自定义插槽
|
||||
- 响应式设计和暗色模式支持
|
||||
@ -1,130 +0,0 @@
|
||||
.order-card {
|
||||
background-color: #fff;
|
||||
border-radius: 6px 6px 12px 12px;
|
||||
box-shadow: 0px 3px 8px 0 rgba(0,0,0,0.12);
|
||||
margin: 12px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&.expired {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 14px 12px 16px;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.order-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
box-sizing: border-box;
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px ;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&.tag-pending {
|
||||
background-color: #FFF7E6;
|
||||
color: #FF8C00;
|
||||
border: 1px solid #FFD591;
|
||||
}
|
||||
|
||||
&.tag-completed {
|
||||
background-color: #F6FFED;
|
||||
color: #52C41A;
|
||||
border: 1px solid #B7EB8F;
|
||||
}
|
||||
|
||||
&.tag-cancelled {
|
||||
background-color: #F5F5F5;
|
||||
color: #999999;
|
||||
border: 1px solid #D9D9D9;
|
||||
}
|
||||
|
||||
&.tag-processing {
|
||||
background-color: #E6F7FF;
|
||||
color: #1890FF;
|
||||
border: 1px solid #91D5FF;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.action-area {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 280px;
|
||||
height: 42px;
|
||||
border-radius: 50px;
|
||||
border: 2px solid #FFCA70;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
background: linear-gradient( 179deg, #FFB100 0%, #FF7F19 100%);
|
||||
color: #ffffff;
|
||||
margin: 0 auto;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #FF7A00 0%, #FF6600 100%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
@ -1,322 +0,0 @@
|
||||
<template>
|
||||
<view class="demo-container">
|
||||
<view class="demo-header">
|
||||
<text class="demo-title">OrderList组件演示</text>
|
||||
<text class="demo-subtitle">集成z-paging组件,支持虚拟列表、下拉刷新、上拉加载更多</text>
|
||||
</view>
|
||||
|
||||
<!-- 功能控制区 -->
|
||||
<view class="control-section">
|
||||
<view class="control-row">
|
||||
<button class="control-btn" @click="toggleVirtualList">
|
||||
{{ useVirtualList ? '关闭' : '开启' }}虚拟列表
|
||||
</button>
|
||||
<button class="control-btn" @click="toggleEmptyState">
|
||||
{{ showEmpty ? '显示数据' : '显示空状态' }}
|
||||
</button>
|
||||
</view>
|
||||
<view class="control-row">
|
||||
<button class="control-btn" @click="addMoreData">
|
||||
添加更多数据
|
||||
</button>
|
||||
<button class="control-btn" @click="clearData">
|
||||
清空数据
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- OrderList组件演示 -->
|
||||
<view class="demo-list">
|
||||
<OrderList
|
||||
:orderList="orderList"
|
||||
:hasMore="hasMore"
|
||||
:isLoading="isLoading"
|
||||
:currentTab="currentTab"
|
||||
:emptyText="emptyText"
|
||||
:useVirtualList="useVirtualList"
|
||||
:virtualListHeight="'500px'"
|
||||
:cellHeightMode="cellHeightMode"
|
||||
:fixedHeight="120"
|
||||
@refresh="handleRefresh"
|
||||
@loadMore="handleLoadMore"
|
||||
@orderClick="handleOrderClick"
|
||||
@orderCall="handleOrderCall"
|
||||
@orderComplete="handleOrderComplete"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 功能说明 -->
|
||||
<view class="feature-section">
|
||||
<text class="feature-title">新功能特性:</text>
|
||||
<view class="feature-list">
|
||||
<text class="feature-item">✅ 集成z-paging组件</text>
|
||||
<text class="feature-item">✅ 支持虚拟列表,提升大数据渲染性能</text>
|
||||
<text class="feature-item">✅ 自定义下拉刷新样式和文案</text>
|
||||
<text class="feature-item">✅ 自定义上拉加载更多样式和文案</text>
|
||||
<text class="feature-item">✅ 自动管理空数据状态</text>
|
||||
<text class="feature-item">✅ 支持固定高度和自适应高度模式</text>
|
||||
<text class="feature-item">✅ 完整的事件回调机制</text>
|
||||
<text class="feature-item">✅ 响应式设计,适配各种屏幕</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import OrderList from './index.vue'
|
||||
|
||||
// 响应式数据
|
||||
const orderList = ref([])
|
||||
const hasMore = ref(true)
|
||||
const isLoading = ref(false)
|
||||
const currentTab = ref('all')
|
||||
const useVirtualList = ref(true)
|
||||
const cellHeightMode = ref('auto')
|
||||
const showEmpty = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const emptyText = computed(() => {
|
||||
return showEmpty.value ? '暂无数据,点击刷新重试' : '暂无工单数据'
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const generateMockData = (count = 10, startId = 1) => {
|
||||
const data = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
data.push({
|
||||
id: `${startId + i}`,
|
||||
title: `工单标题 ${startId + i} - ${['空调维修', '水管漏水', '电路故障', '网络异常', '设备更换'][i % 5]}`,
|
||||
createTime: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toLocaleString(),
|
||||
contactName: ['张先生', '李女士', '王先生', '赵女士', '刘先生'][i % 5],
|
||||
contactPhone: `138****${String(Math.floor(Math.random() * 10000)).padStart(4, '0')}`,
|
||||
status: ['pending', 'processing', 'completed'][i % 3],
|
||||
type: 'service'
|
||||
})
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
const initData = () => {
|
||||
orderList.value = generateMockData(20)
|
||||
}
|
||||
|
||||
// 切换虚拟列表
|
||||
const toggleVirtualList = () => {
|
||||
useVirtualList.value = !useVirtualList.value
|
||||
uni.showToast({
|
||||
title: `虚拟列表已${useVirtualList.value ? '开启' : '关闭'}`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 切换空状态
|
||||
const toggleEmptyState = () => {
|
||||
showEmpty.value = !showEmpty.value
|
||||
if (showEmpty.value) {
|
||||
orderList.value = []
|
||||
hasMore.value = false
|
||||
} else {
|
||||
initData()
|
||||
hasMore.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 添加更多数据
|
||||
const addMoreData = () => {
|
||||
const newData = generateMockData(10, orderList.value.length + 1)
|
||||
orderList.value.push(...newData)
|
||||
uni.showToast({
|
||||
title: '已添加10条数据',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
// 清空数据
|
||||
const clearData = () => {
|
||||
orderList.value = []
|
||||
hasMore.value = false
|
||||
uni.showToast({
|
||||
title: '数据已清空',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
console.log('下拉刷新')
|
||||
isLoading.value = true
|
||||
|
||||
// 模拟网络请求
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
|
||||
// 重新加载数据
|
||||
orderList.value = generateMockData(15)
|
||||
hasMore.value = true
|
||||
isLoading.value = false
|
||||
|
||||
uni.showToast({
|
||||
title: '刷新成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
// 上拉加载更多
|
||||
const handleLoadMore = async () => {
|
||||
console.log('上拉加载更多')
|
||||
|
||||
if (!hasMore.value) return
|
||||
|
||||
// 模拟网络请求
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 添加新数据
|
||||
const newData = generateMockData(10, orderList.value.length + 1)
|
||||
orderList.value.push(...newData)
|
||||
|
||||
// 模拟没有更多数据
|
||||
if (orderList.value.length >= 50) {
|
||||
hasMore.value = false
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: `加载了${newData.length}条数据`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 工单点击
|
||||
const handleOrderClick = (orderData) => {
|
||||
console.log('点击工单:', orderData)
|
||||
uni.showToast({
|
||||
title: `点击了工单: ${orderData.title}`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 工单呼叫
|
||||
const handleOrderCall = (orderData) => {
|
||||
console.log('呼叫工单:', orderData)
|
||||
uni.showToast({
|
||||
title: `呼叫: ${orderData.contactName}`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 工单完成
|
||||
const handleOrderComplete = (orderData) => {
|
||||
console.log('完成工单:', orderData)
|
||||
// 更新状态
|
||||
const index = orderList.value.findIndex(item => item.id === orderData.id)
|
||||
if (index !== -1) {
|
||||
orderList.value[index].status = 'completed'
|
||||
}
|
||||
uni.showToast({
|
||||
title: '工单已标记为完成',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
initData()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.demo-container {
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.demo-title {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.demo-subtitle {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.control-section {
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.control-row {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
background: linear-gradient(135deg, #007aff 0%, #0056cc 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12rpx;
|
||||
font-size: 26rpx;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
background: linear-gradient(135deg, #0056cc 0%, #003d99 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-list {
|
||||
height: 500px;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.feature-section {
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.feature-title {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
padding-left: 10rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,265 +0,0 @@
|
||||
<template>
|
||||
<view class="order-list-container">
|
||||
<!-- z-paging组件 -->
|
||||
<z-paging
|
||||
ref="paging"
|
||||
v-model="dataList"
|
||||
:refresher-enabled="true"
|
||||
:refresher-threshold="45"
|
||||
refresher-default-text="下拉刷新"
|
||||
refresher-pulling-text="下拉刷新"
|
||||
refresher-refreshing-text="正在刷新..."
|
||||
refresher-complete-text="刷新完成"
|
||||
:loading-more-enabled="true"
|
||||
loading-more-default-text="点击加载更多"
|
||||
loading-more-loading-text="正在加载..."
|
||||
loading-more-no-more-text="没有更多了"
|
||||
loading-more-fail-text="加载失败,点击重试"
|
||||
:empty-view-text="emptyText"
|
||||
:empty-view-img="emptyIcon"
|
||||
:auto="false"
|
||||
:use-virtual-list="false"
|
||||
:virtual-list-height="virtualListHeight"
|
||||
:cell-height-mode="cellHeightMode"
|
||||
:fixed-height="fixedHeight"
|
||||
:safe-area-inset-top="true"
|
||||
:use-page-scroll="true"
|
||||
:top-offset="120"
|
||||
@query="queryList"
|
||||
@emptyViewReload="handleEmptyReload"
|
||||
>
|
||||
<!-- 非虚拟列表模式下的数据渲染 -->
|
||||
<view class="order-list-content">
|
||||
<OrderCard
|
||||
v-for="(item, index) in currentDataList"
|
||||
:key="item.id || index"
|
||||
:orderData="item"
|
||||
@click="handleOrderClick"
|
||||
@call="handleOrderCall"
|
||||
@complete="handleOrderComplete"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 虚拟列表模式下的数据渲染 -->
|
||||
<template #cell="{ item }" v-if="useVirtualList">
|
||||
<OrderCard
|
||||
:orderData="item"
|
||||
@click="handleOrderClick"
|
||||
@call="handleOrderCall"
|
||||
@complete="handleOrderComplete"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 自定义空状态 -->
|
||||
<template #empty v-if="customEmptyView">
|
||||
<view class="custom-empty">
|
||||
<image :src="emptyIcon" class="empty-icon" />
|
||||
<text class="empty-text">{{ emptyText }}</text>
|
||||
<button
|
||||
class="refresh-btn"
|
||||
@click="handleEmptyReload"
|
||||
v-if="showRefreshBtn"
|
||||
>
|
||||
刷新重试
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
</z-paging>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, watch, onMounted, computed, nextTick } from "vue";
|
||||
import OrderCard from "../OrderCard/index.vue";
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
// 工单数据列表
|
||||
orderList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 是否有更多数据
|
||||
hasMore: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 是否正在加载
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 空状态文案
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: "暂无工单数据",
|
||||
},
|
||||
// 空状态图标
|
||||
emptyIcon: {
|
||||
type: String,
|
||||
default: "/static/images/empty.png",
|
||||
},
|
||||
// 是否显示刷新按钮
|
||||
showRefreshBtn: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 当前Tab类型
|
||||
currentTab: {
|
||||
type: String,
|
||||
default: "all",
|
||||
},
|
||||
// 是否启用虚拟列表
|
||||
useVirtualList: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 虚拟列表高度
|
||||
virtualListHeight: {
|
||||
type: [String, Number],
|
||||
default: "100%",
|
||||
},
|
||||
// 单元格高度模式
|
||||
cellHeightMode: {
|
||||
type: String,
|
||||
default: "auto", // auto | fixed
|
||||
},
|
||||
// 固定高度(当cellHeightMode为fixed时使用)
|
||||
fixedHeight: {
|
||||
type: Number,
|
||||
default: 120,
|
||||
},
|
||||
// 是否使用自定义空状态
|
||||
customEmptyView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits([
|
||||
"refresh",
|
||||
"loadMore",
|
||||
"orderClick",
|
||||
"orderCall",
|
||||
"orderComplete",
|
||||
]);
|
||||
|
||||
// 响应式数据
|
||||
const paging = ref(null);
|
||||
const dataList = ref([]);
|
||||
const pageNum = ref(1);
|
||||
const pageSize = ref(10);
|
||||
|
||||
// 计算属性
|
||||
const currentDataList = computed(() => {
|
||||
return props.orderList || [];
|
||||
});
|
||||
|
||||
// 查询列表数据
|
||||
const queryList = async (pageNo, pageSize, from) => {
|
||||
console.log("z-paging查询:", { pageNo, pageSize, from });
|
||||
|
||||
try {
|
||||
// 触发父组件的数据加载事件
|
||||
if (pageNo === 1) {
|
||||
// 下拉刷新
|
||||
emit("refresh");
|
||||
} else {
|
||||
// 上拉加载更多
|
||||
emit("loadMore");
|
||||
}
|
||||
|
||||
// 等待数据更新
|
||||
await nextTick();
|
||||
|
||||
// 直接使用当前的orderList数据
|
||||
const currentData = props.orderList || [];
|
||||
|
||||
// 在非虚拟列表模式下,z-paging主要用于下拉刷新和上拉加载
|
||||
// 数据渲染通过模板中的v-for完成
|
||||
paging.value?.complete(currentData, !props.hasMore);
|
||||
|
||||
} catch (error) {
|
||||
console.error("查询列表失败:", error);
|
||||
paging.value?.complete(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理空状态重新加载
|
||||
const handleEmptyReload = () => {
|
||||
console.log("空状态重新加载");
|
||||
paging.value?.reload();
|
||||
};
|
||||
|
||||
// 处理工单点击
|
||||
const handleOrderClick = (orderData) => {
|
||||
emit("orderClick", orderData);
|
||||
};
|
||||
|
||||
// 处理工单呼叫
|
||||
const handleOrderCall = (orderData) => {
|
||||
emit("orderCall", orderData);
|
||||
};
|
||||
|
||||
// 处理工单完成
|
||||
const handleOrderComplete = (orderData) => {
|
||||
emit("orderComplete", orderData);
|
||||
};
|
||||
|
||||
// 监听orderList变化,更新z-paging数据
|
||||
watch(
|
||||
() => props.orderList,
|
||||
(newList) => {
|
||||
if (newList && newList.length >= 0) {
|
||||
dataList.value = [...newList];
|
||||
// 在非虚拟列表模式下,主要是通知z-paging数据已更新
|
||||
if (paging.value) {
|
||||
paging.value.complete(newList, !props.hasMore);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
// 监听Tab切换,重新加载数据
|
||||
watch(
|
||||
() => props.currentTab,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
paging.value?.reload();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// 组件挂载后初始化
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
// 初始化数据
|
||||
if (currentDataList.value.length > 0) {
|
||||
dataList.value = [...currentDataList.value];
|
||||
// 手动触发z-paging的数据更新
|
||||
if (paging.value) {
|
||||
paging.value.complete(currentDataList.value, !props.hasMore);
|
||||
}
|
||||
} else {
|
||||
// 如果没有初始数据,触发第一次查询
|
||||
if (paging.value) {
|
||||
paging.value.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
reload: () => paging.value?.reload(),
|
||||
refresh: () => paging.value?.reload(),
|
||||
complete: (data, noMore) => paging.value?.complete(data, noMore),
|
||||
getPaging: () => paging.value,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
@ -1,298 +0,0 @@
|
||||
.order-list-container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-top: 120rpx; /* 为固定导航栏留出空间 */
|
||||
|
||||
.z-paging-container {
|
||||
width: 100%;
|
||||
min-height: calc(100vh - 120rpx);
|
||||
}
|
||||
|
||||
// z-paging内部列表项样式
|
||||
:deep(.z-paging-content) {
|
||||
padding: 0 32rpx;
|
||||
|
||||
.order-card {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 非虚拟列表内容区域样式
|
||||
.order-list-content {
|
||||
padding: 0 32rpx;
|
||||
|
||||
.order-card {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 空状态样式(z-paging内置)
|
||||
:deep(.z-paging-empty-view) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 60rpx;
|
||||
text-align: center;
|
||||
|
||||
.z-paging-empty-view-img {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin-bottom: 40rpx;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.z-paging-empty-view-text {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
margin-bottom: 40rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.z-paging-empty-view-reload-btn {
|
||||
padding: 20rpx 40rpx;
|
||||
background-color: #007aff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
background-color: #0056cc;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义空状态样式
|
||||
.custom-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 60rpx;
|
||||
text-align: center;
|
||||
|
||||
.empty-icon {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin-bottom: 40rpx;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
margin-bottom: 40rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
padding: 20rpx 40rpx;
|
||||
background-color: #007aff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
background-color: #0056cc;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// z-paging加载更多样式
|
||||
:deep(.z-paging-load-more) {
|
||||
padding: 40rpx 0;
|
||||
text-align: center;
|
||||
|
||||
.z-paging-load-more-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20rpx;
|
||||
|
||||
.z-paging-load-more-loading {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border: 4rpx solid #e5e5e5;
|
||||
border-top: 4rpx solid #007aff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.z-paging-load-more-text {
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.z-paging-load-more-no-more-line {
|
||||
.z-paging-load-more-text {
|
||||
font-size: 26rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// z-paging刷新器样式
|
||||
:deep(.z-paging-refresh) {
|
||||
.z-paging-refresh-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx;
|
||||
|
||||
.z-paging-refresh-loading {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 4rpx solid #e5e5e5;
|
||||
border-top: 4rpx solid #007aff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.z-paging-refresh-text {
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 下拉刷新样式优化 */
|
||||
:deep(.uni-scroll-view-refresher) {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.scroll-area::-webkit-scrollbar {
|
||||
width: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.order-list {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 60px 16px;
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
padding: 8px 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 暗色模式支持 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.order-list-container {
|
||||
background: linear-gradient(180deg, #1a1a1a 0%, #2d2d2d 50%, #404040 100%);
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.no-more-text {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border-color: #404040;
|
||||
border-top-color: #409EFF;
|
||||
}
|
||||
}
|
||||
|
||||
/* 列表项动画 */
|
||||
.order-list view {
|
||||
animation: fadeInUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载状态过渡 */
|
||||
.global-loading {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 空状态动画 */
|
||||
.empty-state {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 刷新状态优化 */
|
||||
.scroll-area[refresher-triggered="true"] {
|
||||
.order-list {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
}
|
||||
@ -1,195 +0,0 @@
|
||||
<template>
|
||||
<view class="test-page">
|
||||
<view class="test-header">
|
||||
<text class="test-title">OrderList 组件测试</text>
|
||||
<button @click="toggleData" class="test-btn">{{ hasData ? '清空数据' : '加载数据' }}</button>
|
||||
</view>
|
||||
|
||||
<view class="test-content">
|
||||
<OrderList
|
||||
:orderList="testOrderList"
|
||||
:hasMore="hasMore"
|
||||
:isLoading="isLoading"
|
||||
:currentTab="currentTab"
|
||||
:emptyText="'暂无测试数据'"
|
||||
:useVirtualList="true"
|
||||
:virtualListHeight="'400px'"
|
||||
:cellHeightMode="'auto'"
|
||||
:fixedHeight="120"
|
||||
@refresh="handleRefresh"
|
||||
@loadMore="handleLoadMore"
|
||||
@orderClick="handleOrderClick"
|
||||
@orderCall="handleOrderCall"
|
||||
@orderComplete="handleOrderComplete"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="test-info">
|
||||
<text class="info-text">当前数据量: {{ testOrderList.length }}</text>
|
||||
<text class="info-text">是否有更多: {{ hasMore ? '是' : '否' }}</text>
|
||||
<text class="info-text">是否加载中: {{ isLoading ? '是' : '否' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import OrderList from './index.vue'
|
||||
|
||||
// 测试数据
|
||||
const testOrderList = ref([])
|
||||
const hasMore = ref(true)
|
||||
const isLoading = ref(false)
|
||||
const currentTab = ref('all')
|
||||
const hasData = ref(false)
|
||||
|
||||
// 模拟订单数据
|
||||
const mockData = [
|
||||
{
|
||||
id: '001',
|
||||
title: '空调维修服务',
|
||||
createTime: '2024-01-15 14:30',
|
||||
contactName: '张先生',
|
||||
contactPhone: '138****8888',
|
||||
status: 'pending',
|
||||
type: 'service'
|
||||
},
|
||||
{
|
||||
id: '002',
|
||||
title: '水管漏水维修',
|
||||
createTime: '2024-01-15 10:20',
|
||||
contactName: '李女士',
|
||||
contactPhone: '139****9999',
|
||||
status: 'processing',
|
||||
type: 'service'
|
||||
},
|
||||
{
|
||||
id: '003',
|
||||
title: '电灯安装服务',
|
||||
createTime: '2024-01-14 16:45',
|
||||
contactName: '王先生',
|
||||
contactPhone: '137****7777',
|
||||
status: 'completed',
|
||||
type: 'service'
|
||||
}
|
||||
]
|
||||
|
||||
// 切换数据
|
||||
const toggleData = () => {
|
||||
if (hasData.value) {
|
||||
testOrderList.value = []
|
||||
hasData.value = false
|
||||
} else {
|
||||
testOrderList.value = [...mockData]
|
||||
hasData.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 处理刷新
|
||||
const handleRefresh = () => {
|
||||
console.log('测试: 刷新事件触发')
|
||||
isLoading.value = true
|
||||
setTimeout(() => {
|
||||
testOrderList.value = [...mockData]
|
||||
isLoading.value = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 处理加载更多
|
||||
const handleLoadMore = () => {
|
||||
console.log('测试: 加载更多事件触发')
|
||||
isLoading.value = true
|
||||
setTimeout(() => {
|
||||
const newData = mockData.map((item, index) => ({
|
||||
...item,
|
||||
id: item.id + '_' + Date.now(),
|
||||
title: item.title + ' (新增)'
|
||||
}))
|
||||
testOrderList.value.push(...newData)
|
||||
isLoading.value = false
|
||||
hasMore.value = testOrderList.value.length < 20
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 处理订单点击
|
||||
const handleOrderClick = (orderData) => {
|
||||
console.log('测试: 订单点击', orderData)
|
||||
uni.showToast({
|
||||
title: `点击了订单: ${orderData.title}`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 处理订单呼叫
|
||||
const handleOrderCall = (orderData) => {
|
||||
console.log('测试: 订单呼叫', orderData)
|
||||
uni.showToast({
|
||||
title: `呼叫: ${orderData.contactName}`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 处理订单完成
|
||||
const handleOrderComplete = (orderData) => {
|
||||
console.log('测试: 订单完成', orderData)
|
||||
uni.showToast({
|
||||
title: `完成订单: ${orderData.title}`,
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.test-page {
|
||||
padding: 20rpx;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.test-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 10rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.test-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.test-btn {
|
||||
padding: 10rpx 20rpx;
|
||||
background: #007aff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.test-content {
|
||||
flex: 1;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.test-info {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 20rpx;
|
||||
background: #f9f9f9;
|
||||
border-radius: 10rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
@ -1,259 +0,0 @@
|
||||
# Tab 切换组件
|
||||
|
||||
一个功能完整的 Tab 切换组件,支持动画过渡、自定义内容和响应式设计。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ **多标签切换**:支持任意数量的标签页切换
|
||||
- ✅ **动画指示器**:选中状态下划线,支持平滑滑动动画
|
||||
- ✅ **自定义内容**:支持插槽自定义标签内容
|
||||
- ✅ **响应式设计**:适配不同屏幕尺寸
|
||||
- ✅ **动态宽度**:下划线宽度根据文字宽度动态调整
|
||||
- ✅ **事件支持**:完整的切换事件和双向绑定
|
||||
- ✅ **主题定制**:支持自定义指示器颜色
|
||||
- ✅ **uniapp 兼容**:使用 uniapp 内置组件开发
|
||||
|
||||
## 基础用法
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Tab
|
||||
:tabs="tabList"
|
||||
:defaultActive="0"
|
||||
@change="handleTabChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Tab from './components/Tab/index.vue'
|
||||
|
||||
const tabList = ref([
|
||||
{ label: '全部订单', value: 'all' },
|
||||
{ label: '服务工单', value: 'service' }
|
||||
])
|
||||
|
||||
const handleTabChange = ({ index, item }) => {
|
||||
console.log('切换到:', item.label)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 自定义标签内容
|
||||
|
||||
使用 `tab-item` 插槽可以完全自定义标签的显示内容:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Tab :tabs="customTabs" @change="handleChange">
|
||||
<template #tab-item="{ item, index, isActive }">
|
||||
<view class="custom-tab">
|
||||
<image v-if="item.icon" :src="item.icon" class="tab-icon" />
|
||||
<text :class="{ 'active-text': isActive }">{{ item.label }}</text>
|
||||
<view v-if="item.badge" class="badge">{{ item.badge }}</view>
|
||||
</view>
|
||||
</template>
|
||||
</Tab>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const customTabs = ref([
|
||||
{
|
||||
label: '待处理',
|
||||
value: 'pending',
|
||||
icon: '/static/pending.png',
|
||||
badge: '3'
|
||||
},
|
||||
{
|
||||
label: '已完成',
|
||||
value: 'completed',
|
||||
icon: '/static/completed.png'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
```
|
||||
|
||||
## 双向绑定
|
||||
|
||||
支持 `v-model` 双向绑定当前选中的索引:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Tab v-model="activeIndex" :tabs="tabList" />
|
||||
<text>当前选中索引:{{ activeIndex }}</text>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const activeIndex = ref(0)
|
||||
const tabList = ref([
|
||||
{ label: '标签1', value: 'tab1' },
|
||||
{ label: '标签2', value: 'tab2' }
|
||||
])
|
||||
</script>
|
||||
```
|
||||
|
||||
## 动态标签
|
||||
|
||||
支持动态添加和删除标签:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Tab :tabs="dynamicTabs" @change="handleChange" />
|
||||
<button @click="addTab">添加标签</button>
|
||||
<button @click="removeTab">删除标签</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const dynamicTabs = ref([
|
||||
{ label: '标签1', value: 'tab1' }
|
||||
])
|
||||
|
||||
const addTab = () => {
|
||||
const newIndex = dynamicTabs.value.length + 1
|
||||
dynamicTabs.value.push({
|
||||
label: `标签${newIndex}`,
|
||||
value: `tab${newIndex}`
|
||||
})
|
||||
}
|
||||
|
||||
const removeTab = () => {
|
||||
if (dynamicTabs.value.length > 1) {
|
||||
dynamicTabs.value.pop()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| tabs | Array | `[{label:'全部订单',value:'all'},{label:'服务工单',value:'service'}]` | 标签数据数组 |
|
||||
| defaultActive | Number | `0` | 默认选中的标签索引 |
|
||||
| indicatorColor | String | `#007AFF` | 指示器颜色 |
|
||||
| modelValue | Number | - | 当前选中索引(用于 v-model) |
|
||||
|
||||
### tabs 数组项结构
|
||||
|
||||
```typescript
|
||||
interface TabItem {
|
||||
label: string // 标签显示文本
|
||||
value: string // 标签值
|
||||
[key: string]: any // 其他自定义属性
|
||||
}
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
| 事件名 | 说明 | 参数 |
|
||||
|--------|------|------|
|
||||
| change | 标签切换时触发 | `{ index: number, item: TabItem }` |
|
||||
| update:modelValue | 用于 v-model 双向绑定 | `index: number` |
|
||||
|
||||
## Slots
|
||||
|
||||
| 插槽名 | 说明 | 作用域参数 |
|
||||
|--------|------|------------|
|
||||
| tab-item | 自定义标签内容 | `{ item: TabItem, index: number, isActive: boolean }` |
|
||||
|
||||
## 方法
|
||||
|
||||
通过 `ref` 可以调用组件的方法:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Tab ref="tabRef" :tabs="tabList" />
|
||||
<button @click="switchToTab(1)">切换到第二个标签</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const tabRef = ref()
|
||||
|
||||
const switchToTab = (index) => {
|
||||
tabRef.value.setActiveIndex(index)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
| 方法名 | 说明 | 参数 | 返回值 |
|
||||
|--------|------|------|--------|
|
||||
| setActiveIndex | 设置当前选中的标签 | `index: number` | - |
|
||||
| getActiveIndex | 获取当前选中的标签索引 | - | `number` |
|
||||
| getActiveItem | 获取当前选中的标签项 | - | `TabItem` |
|
||||
|
||||
## 样式定制
|
||||
|
||||
### CSS 变量
|
||||
|
||||
组件支持通过 CSS 变量进行样式定制:
|
||||
|
||||
```scss
|
||||
.tab-container {
|
||||
--tab-bg-color: #fff; // 背景色
|
||||
--tab-text-color: #666; // 文字颜色
|
||||
--tab-active-color: #333; // 选中文字颜色
|
||||
--tab-indicator-color: #007AFF; // 指示器颜色
|
||||
--tab-border-color: #f0f0f0; // 边框颜色
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义主题
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Tab
|
||||
:tabs="tabList"
|
||||
indicatorColor="#ff4d4f"
|
||||
class="custom-tab"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.custom-tab {
|
||||
--tab-indicator-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.custom-tab .tab-text-active {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 技术实现
|
||||
|
||||
- **框架**:Vue 3 组合式 API
|
||||
- **平台**:uniapp 跨平台开发
|
||||
- **动画**:CSS3 transition + transform
|
||||
- **响应式**:CSS media queries
|
||||
- **兼容性**:微信小程序、H5、App
|
||||
|
||||
## 设计规范
|
||||
|
||||
- 遵循微信小程序设计规范
|
||||
- 支持无障碍访问
|
||||
- 响应式设计,适配不同设备
|
||||
- 流畅的动画过渡效果
|
||||
- 一致的视觉风格
|
||||
|
||||
## 兼容性
|
||||
|
||||
| 平台 | 支持情况 |
|
||||
|------|----------|
|
||||
| 微信小程序 | ✅ 完全支持 |
|
||||
| H5 | ✅ 完全支持 |
|
||||
| App | ✅ 完全支持 |
|
||||
| 支付宝小程序 | ✅ 完全支持 |
|
||||
| 百度小程序 | ✅ 完全支持 |
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0
|
||||
- ✨ 初始版本发布
|
||||
- ✨ 支持基础标签切换功能
|
||||
- ✨ 支持动画指示器
|
||||
- ✨ 支持自定义标签内容
|
||||
- ✨ 支持响应式设计
|
||||
- ✨ 支持事件和双向绑定
|
||||
|
||||
## 备注
|
||||
|
||||
仅供学习、交流使用,请勿用于商业用途。
|
||||
@ -1,275 +0,0 @@
|
||||
<template>
|
||||
<view class="tab-container">
|
||||
<view class="tab-wrapper">
|
||||
<view
|
||||
v-for="(item, index) in tabList"
|
||||
:key="index"
|
||||
:class="['tab-item', activeIndex === index && 'tab-item-active']"
|
||||
@click="handleTabClick(index)"
|
||||
>
|
||||
<slot
|
||||
name="tab-item"
|
||||
:item="item"
|
||||
:index="index"
|
||||
:isActive="activeIndex === index"
|
||||
>
|
||||
<text
|
||||
:class="['tab-text', activeIndex === index && 'tab-text-active']"
|
||||
>
|
||||
{{ item.label }}
|
||||
</text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 下划线指示器 -->
|
||||
<view
|
||||
:class="[
|
||||
'tab-indicator',
|
||||
indicatorAnimating && 'animating',
|
||||
indicatorInitialized && 'initialized',
|
||||
]"
|
||||
:style="{
|
||||
left: indicatorLeft + 'px',
|
||||
width: indicatorWidth + 'px',
|
||||
}"
|
||||
></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
onMounted,
|
||||
nextTick,
|
||||
watch,
|
||||
getCurrentInstance,
|
||||
} from "vue";
|
||||
|
||||
// 获取组件实例
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ label: "全部订单", value: "all" },
|
||||
{ label: "服务工单", value: "service" },
|
||||
],
|
||||
},
|
||||
defaultActive: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
indicatorColor: {
|
||||
type: String,
|
||||
default: "#007AFF",
|
||||
},
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["change", "update:modelValue"]);
|
||||
|
||||
// 响应式数据
|
||||
const activeIndex = ref(props.defaultActive);
|
||||
const tabList = ref(props.tabs);
|
||||
const indicatorLeft = ref(0);
|
||||
const indicatorWidth = ref(0);
|
||||
const tabItemRects = reactive([]);
|
||||
const isUpdating = ref(false);
|
||||
const indicatorAnimating = ref(false);
|
||||
const indicatorInitialized = ref(false);
|
||||
|
||||
// 处理Tab点击
|
||||
const handleTabClick = (index) => {
|
||||
if (activeIndex.value === index) return;
|
||||
|
||||
activeIndex.value = index;
|
||||
indicatorAnimating.value = true;
|
||||
updateIndicator();
|
||||
|
||||
// 动画结束后移除动画类
|
||||
setTimeout(() => {
|
||||
indicatorAnimating.value = false;
|
||||
}, 300);
|
||||
|
||||
emit("change", {
|
||||
index,
|
||||
item: tabList.value[index],
|
||||
});
|
||||
emit("update:modelValue", index);
|
||||
};
|
||||
|
||||
// 更新指示器位置
|
||||
const updateIndicator = async () => {
|
||||
if (isUpdating.value) return;
|
||||
isUpdating.value = true;
|
||||
|
||||
await nextTick();
|
||||
|
||||
// 检查实例是否存在
|
||||
if (!instance) {
|
||||
console.warn("Component instance not available");
|
||||
isUpdating.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 uni.createSelectorQuery 是否可用
|
||||
if (!uni || !uni.createSelectorQuery) {
|
||||
console.warn("uni.createSelectorQuery not available");
|
||||
isUpdating.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const query = uni.createSelectorQuery().in(instance);
|
||||
|
||||
// 同时获取tab项和容器的位置信息
|
||||
query.selectAll(".tab-item").boundingClientRect();
|
||||
query.select(".tab-wrapper").boundingClientRect();
|
||||
|
||||
query.exec((res) => {
|
||||
try {
|
||||
const [tabRects, wrapperRect] = res || [];
|
||||
|
||||
if (tabRects && tabRects.length > 0 && wrapperRect) {
|
||||
tabItemRects.splice(0, tabItemRects.length, ...tabRects);
|
||||
|
||||
const activeRect = tabRects[activeIndex.value];
|
||||
if (activeRect) {
|
||||
// 计算相对于容器的位置,居中显示
|
||||
const tabCenter =
|
||||
activeRect.left - wrapperRect.left + activeRect.width / 2;
|
||||
indicatorLeft.value = tabCenter - 7.5; // 15px宽度的一半
|
||||
// 固定宽度15px,不动态计算宽度
|
||||
indicatorWidth.value = 15;
|
||||
|
||||
// 标记为已初始化
|
||||
if (!indicatorInitialized.value) {
|
||||
indicatorInitialized.value = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn("Failed to get tab rects or wrapper rect");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in updateIndicator exec:", error);
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 监听activeIndex变化
|
||||
watch(
|
||||
() => activeIndex.value,
|
||||
() => {
|
||||
// 如果是初始化阶段,使用initIndicator
|
||||
if (indicatorLeft.value === 0 && indicatorWidth.value === 0) {
|
||||
initIndicator();
|
||||
} else {
|
||||
updateIndicator();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听tabs变化
|
||||
watch(
|
||||
() => props.tabs,
|
||||
(newTabs) => {
|
||||
tabList.value = newTabs;
|
||||
// 重置初始化状态
|
||||
indicatorInitialized.value = false;
|
||||
// 重新初始化指示器
|
||||
initIndicator();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 监听defaultActive变化
|
||||
watch(
|
||||
() => props.defaultActive,
|
||||
(newActive) => {
|
||||
if (newActive !== activeIndex.value) {
|
||||
activeIndex.value = newActive;
|
||||
// 重置初始化状态
|
||||
indicatorInitialized.value = false;
|
||||
initIndicator();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 初始化指示器
|
||||
const initIndicator = async (retryCount = 0) => {
|
||||
// 等待DOM完全渲染
|
||||
await nextTick();
|
||||
|
||||
// 延迟一帧确保布局完成
|
||||
setTimeout(() => {
|
||||
// 检查实例是否存在
|
||||
if (!instance) {
|
||||
console.warn("Component instance not available in initIndicator");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 uni.createSelectorQuery 是否可用
|
||||
if (!uni || !uni.createSelectorQuery) {
|
||||
console.warn("uni.createSelectorQuery not available in initIndicator");
|
||||
return;
|
||||
}
|
||||
|
||||
const query = uni.createSelectorQuery().in(instance);
|
||||
query.selectAll(".tab-item").boundingClientRect();
|
||||
query.select(".tab-wrapper").boundingClientRect();
|
||||
|
||||
query.exec((res) => {
|
||||
try {
|
||||
const [tabRects, wrapperRect] = res || [];
|
||||
|
||||
// 如果DOM元素还未准备好,重试
|
||||
if (
|
||||
(!tabRects || tabRects.length === 0 || !wrapperRect) &&
|
||||
retryCount < 3
|
||||
) {
|
||||
setTimeout(() => {
|
||||
initIndicator(retryCount + 1);
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行正常的更新逻辑
|
||||
updateIndicator();
|
||||
} catch (error) {
|
||||
console.error("Error in initIndicator exec:", error);
|
||||
// 如果出错且还有重试次数,尝试重试
|
||||
if (retryCount < 3) {
|
||||
setTimeout(() => {
|
||||
initIndicator(retryCount + 1);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 50);
|
||||
};
|
||||
|
||||
// 组件挂载后初始化指示器
|
||||
onMounted(() => {
|
||||
initIndicator();
|
||||
});
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
setActiveIndex: (index) => {
|
||||
if (index >= 0 && index < tabList.value.length) {
|
||||
handleTabClick(index);
|
||||
}
|
||||
},
|
||||
getActiveIndex: () => activeIndex.value,
|
||||
getActiveItem: () => tabList.value[activeIndex.value],
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
@ -1,100 +0,0 @@
|
||||
.tab-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 400;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab-text-active {
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-item-active {
|
||||
.tab-text {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 3px;
|
||||
min-height: 3px; /* 确保最小高度 */
|
||||
background-color: #007AFF;
|
||||
border-radius: 10px;
|
||||
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1), width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 1;
|
||||
transform: translateZ(0); /* 启用硬件加速 */
|
||||
will-change: left, width; /* 优化动画性能 */
|
||||
|
||||
/* 初始状态:未初始化时隐藏 */
|
||||
opacity: 0;
|
||||
width: 15px; /* 默认宽度15px */
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* 已初始化状态 */
|
||||
.tab-indicator.initialized {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
/* 点击效果 */
|
||||
.tab-item:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 自定义主题色支持 */
|
||||
.tab-container[data-indicator-color="red"] .tab-indicator {
|
||||
background-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.tab-container[data-indicator-color="green"] .tab-indicator {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
|
||||
.tab-container[data-indicator-color="orange"] .tab-indicator {
|
||||
background-color: #fa8c16;
|
||||
}
|
||||
|
||||
/* 动画增强 */
|
||||
@keyframes tabSwitch {
|
||||
0% {
|
||||
transform: translateZ(0) scaleX(0.8);
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
transform: translateZ(0) scaleX(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-indicator.animating {
|
||||
animation: tabSwitch 0.3s ease-out;
|
||||
}
|
||||
@ -1,309 +0,0 @@
|
||||
<template>
|
||||
<view class="test-page">
|
||||
<view class="test-section">
|
||||
<text class="test-title">Tab组件测试</text>
|
||||
|
||||
<!-- 基础测试 -->
|
||||
<view class="test-item">
|
||||
<text class="item-title">基础用法</text>
|
||||
<Tab
|
||||
:tabs="basicTabs"
|
||||
:defaultActive="0"
|
||||
@change="handleTabChange"
|
||||
/>
|
||||
<view class="result">
|
||||
<text>当前选中: {{ currentTab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 多标签测试 -->
|
||||
<view class="test-item">
|
||||
<text class="item-title">多标签测试</text>
|
||||
<Tab
|
||||
:tabs="multiTabs"
|
||||
:defaultActive="1"
|
||||
@change="handleMultiTabChange"
|
||||
/>
|
||||
<view class="result">
|
||||
<text>当前选中: {{ currentMultiTab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快速切换测试 -->
|
||||
<view class="test-item">
|
||||
<text class="item-title">快速切换测试</text>
|
||||
<Tab
|
||||
ref="fastTabRef"
|
||||
:tabs="fastTabs"
|
||||
:defaultActive="0"
|
||||
@change="handleFastTabChange"
|
||||
/>
|
||||
<view class="result">
|
||||
<text>当前选中: {{ currentFastTab.label }}</text>
|
||||
</view>
|
||||
<view class="test-buttons">
|
||||
<button
|
||||
v-for="(tab, index) in fastTabs"
|
||||
:key="index"
|
||||
class="test-btn"
|
||||
@click="switchToTab(index)"
|
||||
>
|
||||
切换到{{ tab.label }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 初始化测试 -->
|
||||
<view class="test-item">
|
||||
<text class="item-title">初始化测试</text>
|
||||
<text class="test-desc">测试指示器的动态高度和宽度初始化及错误处理</text>
|
||||
<Tab
|
||||
v-if="showInitTest"
|
||||
:tabs="initTestTabs"
|
||||
:defaultActive="initActiveIndex"
|
||||
@change="handleInitTestChange"
|
||||
/>
|
||||
<view class="result">
|
||||
<text>当前选中: {{ currentInitTab.label }}</text>
|
||||
</view>
|
||||
<view class="test-buttons">
|
||||
<button class="test-btn" @click="toggleInitTest">
|
||||
{{ showInitTest ? '隐藏' : '显示' }}组件
|
||||
</button>
|
||||
<button class="test-btn" @click="changeInitActive">
|
||||
切换默认激活项 (当前: {{ initActiveIndex }})
|
||||
</button>
|
||||
<button class="test-btn" @click="addInitTab">
|
||||
添加Tab
|
||||
</button>
|
||||
<button class="test-btn" @click="removeInitTab">
|
||||
移除Tab
|
||||
</button>
|
||||
<button class="test-btn" @click="rapidToggle">
|
||||
快速切换测试
|
||||
</button>
|
||||
</view>
|
||||
<view class="test-info">
|
||||
<text>错误处理测试:组件现在能够安全处理实例为null的情况</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import Tab from './index.vue'
|
||||
|
||||
// 基础标签
|
||||
const basicTabs = ref([
|
||||
{ label: '全部订单', value: 'all' },
|
||||
{ label: '服务工单', value: 'service' }
|
||||
])
|
||||
|
||||
// 多标签
|
||||
const multiTabs = ref([
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '待支付', value: 'unpaid' },
|
||||
{ label: '待确认', value: 'unconfirmed' },
|
||||
{ label: '进行中', value: 'processing' },
|
||||
{ label: '已完成', value: 'completed' }
|
||||
])
|
||||
|
||||
// 快速切换测试
|
||||
const fastTabs = ref([
|
||||
{ label: '标签A', value: 'a' },
|
||||
{ label: '标签B', value: 'b' },
|
||||
{ label: '标签C', value: 'c' },
|
||||
{ label: '标签D', value: 'd' }
|
||||
])
|
||||
|
||||
// 初始化测试
|
||||
const initTestTabs = ref([
|
||||
{ label: '初始1', value: 'init1' },
|
||||
{ label: '初始2', value: 'init2' },
|
||||
{ label: '初始3', value: 'init3' }
|
||||
])
|
||||
|
||||
// 当前选中索引
|
||||
const currentTabIndex = ref(0)
|
||||
const currentMultiTabIndex = ref(1)
|
||||
const currentFastTabIndex = ref(0)
|
||||
const currentInitTabIndex = ref(1)
|
||||
|
||||
// 初始化测试状态
|
||||
const showInitTest = ref(true)
|
||||
const initActiveIndex = ref(1)
|
||||
|
||||
// 计算当前选中项
|
||||
const currentTab = computed(() => basicTabs.value[currentTabIndex.value] || {})
|
||||
const currentMultiTab = computed(() => multiTabs.value[currentMultiTabIndex.value] || {})
|
||||
const currentFastTab = computed(() => fastTabs.value[currentFastTabIndex.value] || {})
|
||||
const currentInitTab = computed(() => initTestTabs.value[currentInitTabIndex.value] || {})
|
||||
|
||||
// Tab引用
|
||||
const fastTabRef = ref(null)
|
||||
|
||||
// 事件处理
|
||||
const handleTabChange = ({ index, item }) => {
|
||||
currentTabIndex.value = index
|
||||
console.log('基础Tab切换:', item)
|
||||
}
|
||||
|
||||
const handleMultiTabChange = ({ index, item }) => {
|
||||
currentMultiTabIndex.value = index
|
||||
console.log('多标签Tab切换:', item)
|
||||
}
|
||||
|
||||
const handleFastTabChange = ({ index, item }) => {
|
||||
currentFastTabIndex.value = index
|
||||
console.log('快速Tab切换:', item)
|
||||
}
|
||||
|
||||
const handleInitTestChange = ({ index, item }) => {
|
||||
currentInitTabIndex.value = index
|
||||
console.log('初始化Tab切换:', item)
|
||||
}
|
||||
|
||||
// 程序化切换
|
||||
const switchToTab = (index) => {
|
||||
currentFastTabIndex.value = index
|
||||
if (fastTabRef.value) {
|
||||
fastTabRef.value.setActiveIndex(index)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化测试方法
|
||||
const toggleInitTest = () => {
|
||||
showInitTest.value = !showInitTest.value
|
||||
}
|
||||
|
||||
const changeInitActive = () => {
|
||||
initActiveIndex.value = (initActiveIndex.value + 1) % initTestTabs.value.length
|
||||
}
|
||||
|
||||
const addInitTab = () => {
|
||||
const newIndex = initTestTabs.value.length + 1
|
||||
initTestTabs.value.push({
|
||||
label: `初始${newIndex}`,
|
||||
value: `init${newIndex}`
|
||||
})
|
||||
}
|
||||
|
||||
const removeInitTab = () => {
|
||||
if (initTestTabs.value.length > 1) {
|
||||
initTestTabs.value.pop()
|
||||
if (initActiveIndex.value >= initTestTabs.value.length) {
|
||||
initActiveIndex.value = initTestTabs.value.length - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rapidToggle = () => {
|
||||
// 快速切换测试,模拟可能导致实例为null的场景
|
||||
let count = 0
|
||||
const interval = setInterval(() => {
|
||||
showInitTest.value = !showInitTest.value
|
||||
count++
|
||||
if (count >= 6) {
|
||||
clearInterval(interval)
|
||||
showInitTest.value = true
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.test-page {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.test-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.test-item {
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.test-item:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.test-desc {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.test-info {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background-color: #f0f9ff;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #007AFF;
|
||||
}
|
||||
|
||||
.test-info text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.result text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.test-buttons {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.test-btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
background-color: #007AFF;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.test-btn:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<view class="top-nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-content">
|
||||
<view class="nav-left" @click="goBack">
|
||||
<image
|
||||
class="back-icon"
|
||||
src="@/static/back.png"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
</view>
|
||||
<view class="nav-center">
|
||||
<slot name="title">
|
||||
<text class="nav-title">{{ title }}</text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
showBack: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["back"]);
|
||||
|
||||
// 状态栏高度
|
||||
const statusBarHeight = ref(0);
|
||||
|
||||
// 获取系统信息
|
||||
onMounted(() => {
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 44;
|
||||
});
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
if (props.showBack) {
|
||||
emit("back");
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
@ -1,114 +0,0 @@
|
||||
## 顶部导航栏组件
|
||||
|
||||
组件名称:顶部导航栏组件
|
||||
|
||||
## 功能特性
|
||||
|
||||
1. **自适应状态栏高度**:自动获取设备状态栏高度并适配
|
||||
2. **返回功能**:左侧返回按钮,支持自定义返回事件
|
||||
3. **标题显示**:支持传入标题文本或使用插槽自定义标题内容
|
||||
4. **右侧扩展**:支持右侧插槽,可添加自定义操作按钮
|
||||
5. **响应式设计**:适配不同屏幕尺寸
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基础用法
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<TopNavBar title="服务工单" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TopNavBar from './components/TopNavBar/index.vue'
|
||||
</script>
|
||||
```
|
||||
|
||||
### 自定义标题
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<TopNavBar>
|
||||
<template #title>
|
||||
<text class="custom-title">自定义标题</text>
|
||||
</template>
|
||||
</TopNavBar>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 自定义右侧操作
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<TopNavBar title="服务工单">
|
||||
<template #right>
|
||||
<image class="menu-icon" src="./images/menu.png" @click="showMenu" />
|
||||
</template>
|
||||
</TopNavBar>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 自定义返回事件
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<TopNavBar title="服务工单" @back="handleBack" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const handleBack = () => {
|
||||
// 自定义返回逻辑
|
||||
console.log('自定义返回')
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| title | String | '' | 导航栏标题 |
|
||||
| showBack | Boolean | true | 是否显示返回按钮 |
|
||||
|
||||
## Events
|
||||
|
||||
| 事件名 | 说明 | 参数 |
|
||||
|--------|------|------|
|
||||
| back | 点击返回按钮时触发 | - |
|
||||
|
||||
## Slots
|
||||
|
||||
| 插槽名 | 说明 |
|
||||
|--------|------|
|
||||
| title | 自定义标题内容 |
|
||||
| right | 自定义右侧操作区域 |
|
||||
|
||||
## 样式定制
|
||||
|
||||
组件支持通过CSS变量进行样式定制:
|
||||
|
||||
```scss
|
||||
.top-nav-bar {
|
||||
--nav-bg-color: #fff; // 背景色
|
||||
--nav-title-color: #333; // 标题颜色
|
||||
--nav-border-color: #f0f0f0; // 边框颜色
|
||||
}
|
||||
```
|
||||
|
||||
## 技术实现
|
||||
|
||||
- 使用 uniapp + Vue3 组合式 API 开发
|
||||
- 自动获取系统状态栏高度进行适配
|
||||
- 使用 fixed 定位实现固定顶部效果
|
||||
- 支持插槽扩展,提供良好的可定制性
|
||||
- 响应式设计,适配不同设备屏幕
|
||||
|
||||
## 兼容性
|
||||
|
||||
- 微信小程序
|
||||
- H5
|
||||
- App
|
||||
|
||||
## 备注
|
||||
|
||||
仅供学习、交流使用,请勿用于商业用途。
|
||||
@ -1,38 +0,0 @@
|
||||
|
||||
|
||||
.nav-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
padding-top: 8px;
|
||||
|
||||
}
|
||||
|
||||
.nav-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.nav-center {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@ -1,261 +1,11 @@
|
||||
<template>
|
||||
<view class="order-page">
|
||||
<!-- 顶部导航栏 - 固定定位 -->
|
||||
<view class="top-nav-fixed">
|
||||
<TopNavBar>
|
||||
<template #title>
|
||||
<Tabs
|
||||
:tabs="tabList"
|
||||
:defaultActive="currentTabIndex"
|
||||
@change="handleTabChange"
|
||||
/>
|
||||
</template>
|
||||
</TopNavBar>
|
||||
</view>
|
||||
|
||||
<!-- 工单列表区域 - 全屏模式 -->
|
||||
<OrderList
|
||||
:orderList="currentOrderList"
|
||||
:hasMore="hasMore"
|
||||
:isLoading="isLoading"
|
||||
:currentTab="currentTab"
|
||||
:emptyText="getEmptyText()"
|
||||
:useVirtualList="false"
|
||||
:virtualListHeight="'100vh'"
|
||||
:cellHeightMode="'auto'"
|
||||
:fixedHeight="120"
|
||||
@refresh="handleRefresh"
|
||||
@loadMore="handleLoadMore"
|
||||
@orderClick="handleOrderClick"
|
||||
@orderCall="handleOrderCall"
|
||||
@orderComplete="handleOrderComplete"
|
||||
/>
|
||||
</view>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import TopNavBar from "./components/TopNavBar/index.vue";
|
||||
import Tabs from "./components/Tabs/index.vue";
|
||||
import OrderList from "./components/OrderList/index.vue";
|
||||
|
||||
// Tab配置
|
||||
const tabList = ref([
|
||||
{ label: "全部订单", value: "all" },
|
||||
{ label: "服务工单", value: "service" },
|
||||
]);
|
||||
|
||||
// 当前状态
|
||||
const currentTab = ref("all");
|
||||
const currentTabIndex = ref(0);
|
||||
const isLoading = ref(false);
|
||||
const hasMore = ref(true);
|
||||
const pageNum = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const showConsultationBar = ref(false);
|
||||
|
||||
// 工单数据
|
||||
const allOrderList = ref([]);
|
||||
const serviceOrderList = ref([]);
|
||||
|
||||
// 模拟工单数据
|
||||
const mockOrderData = [
|
||||
{
|
||||
id: "001",
|
||||
title: "空调不制冷,需要维修",
|
||||
createTime: "2024-01-15 14:30",
|
||||
contactName: "张先生",
|
||||
contactPhone: "138****8888",
|
||||
status: "pending",
|
||||
type: "service",
|
||||
},
|
||||
{
|
||||
id: "002",
|
||||
title: "卫生间水龙头漏水",
|
||||
createTime: "2024-01-15 10:20",
|
||||
contactName: "李女士",
|
||||
contactPhone: "139****9999",
|
||||
status: "processing",
|
||||
type: "service",
|
||||
},
|
||||
{
|
||||
id: "003",
|
||||
title: "客厅灯泡需要更换",
|
||||
createTime: "2024-01-14 16:45",
|
||||
contactName: "王先生",
|
||||
contactPhone: "137****7777",
|
||||
status: "completed",
|
||||
type: "service",
|
||||
},
|
||||
{
|
||||
id: "004",
|
||||
title: "普通订单 - 商品配送",
|
||||
createTime: "2024-01-14 09:15",
|
||||
contactName: "赵女士",
|
||||
contactPhone: "136****6666",
|
||||
status: "completed",
|
||||
type: "order",
|
||||
},
|
||||
{
|
||||
id: "005",
|
||||
title: "网络连接异常",
|
||||
createTime: "2024-01-15 11:30",
|
||||
contactName: "刘先生",
|
||||
contactPhone: "135****5555",
|
||||
status: "pending",
|
||||
type: "service",
|
||||
},
|
||||
];
|
||||
|
||||
// 计算当前显示的工单列表
|
||||
const currentOrderList = computed(() => {
|
||||
return currentTab.value === "all"
|
||||
? allOrderList.value
|
||||
: serviceOrderList.value;
|
||||
});
|
||||
|
||||
// 获取空状态文案
|
||||
const getEmptyText = () => {
|
||||
return currentTab.value === "all" ? "暂无订单数据" : "暂无服务工单";
|
||||
};
|
||||
|
||||
// Tab切换处理
|
||||
const handleTabChange = ({ index, item }) => {
|
||||
console.log("切换到:", item.label);
|
||||
currentTab.value = item.value;
|
||||
currentTabIndex.value = index;
|
||||
|
||||
// 如果当前Tab没有数据,则加载数据
|
||||
if (currentOrderList.value.length === 0) {
|
||||
loadOrderData(true);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载工单数据
|
||||
const loadOrderData = async (isRefresh = false) => {
|
||||
if (isLoading.value) return;
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
// 模拟API请求延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
// 模拟数据过滤
|
||||
let filteredData = [];
|
||||
if (currentTab.value === "all") {
|
||||
filteredData = mockOrderData;
|
||||
} else {
|
||||
filteredData = mockOrderData.filter((item) => item.type === "service");
|
||||
}
|
||||
|
||||
if (isRefresh) {
|
||||
// 刷新数据
|
||||
if (currentTab.value === "all") {
|
||||
allOrderList.value = [...filteredData];
|
||||
} else {
|
||||
serviceOrderList.value = [...filteredData];
|
||||
}
|
||||
pageNum.value = 1;
|
||||
hasMore.value = true;
|
||||
} else {
|
||||
// 加载更多
|
||||
if (currentTab.value === "all") {
|
||||
allOrderList.value.push(...filteredData.slice(0, 2));
|
||||
} else {
|
||||
serviceOrderList.value.push(...filteredData.slice(0, 2));
|
||||
}
|
||||
pageNum.value++;
|
||||
|
||||
// 模拟没有更多数据
|
||||
if (pageNum.value >= 3) {
|
||||
hasMore.value = false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载数据失败:", error);
|
||||
uni.showToast({
|
||||
title: "加载失败,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
await loadOrderData(true);
|
||||
};
|
||||
|
||||
// 上拉加载更多
|
||||
const handleLoadMore = async () => {
|
||||
if (!hasMore.value) return;
|
||||
await loadOrderData(false);
|
||||
};
|
||||
|
||||
// 工单点击处理
|
||||
const handleOrderClick = (orderData) => {
|
||||
console.log("点击工单:", orderData);
|
||||
uni.navigateTo({
|
||||
url: `/pages/order/detail?id=${orderData.id}`,
|
||||
});
|
||||
};
|
||||
|
||||
// 工单呼叫处理
|
||||
const handleOrderCall = (orderData) => {
|
||||
console.log("呼叫:", orderData);
|
||||
|
||||
// 显示底部咨询栏
|
||||
showConsultationBar.value = true;
|
||||
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: orderData.contactPhone.replace(/\*/g, ""),
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: "拨号失败",
|
||||
icon: "none",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 工单完成处理
|
||||
const handleOrderComplete = (orderData) => {
|
||||
console.log("标记完成:", orderData);
|
||||
|
||||
uni.showModal({
|
||||
title: "确认操作",
|
||||
content: "确定要标记此工单为已完成吗?",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 更新工单状态
|
||||
const targetList =
|
||||
currentTab.value === "all"
|
||||
? allOrderList.value
|
||||
: serviceOrderList.value;
|
||||
const orderIndex = targetList.findIndex(
|
||||
(item) => item.id === orderData.id
|
||||
);
|
||||
if (orderIndex !== -1) {
|
||||
targetList[orderIndex].status = "completed";
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: "操作成功",
|
||||
icon: "success",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 页面加载时初始化数据
|
||||
onMounted(() => {
|
||||
loadOrderData(true);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/list.scss";
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@ -1,161 +0,0 @@
|
||||
# 订单管理系统组件需求文档
|
||||
|
||||
## 项目概述
|
||||
订单管理系统的核心组件库,包含工单卡片、工单列表、咨询栏等核心功能组件。
|
||||
|
||||
## TopNavBar 组件
|
||||
|
||||
### 功能要求
|
||||
- 顶部导航栏展示
|
||||
- 支持自定义标题内容
|
||||
- 支持插槽扩展
|
||||
|
||||
### 设计要求
|
||||
- 固定在页面顶部
|
||||
- 背景色与主题一致
|
||||
- 高度适中,不占用过多空间
|
||||
|
||||
## Tabs 组件
|
||||
|
||||
### 功能要求
|
||||
- 标签页切换功能
|
||||
- 支持默认激活项
|
||||
- 切换动画效果
|
||||
- 事件回调
|
||||
|
||||
### 设计要求
|
||||
- 标签间距均匀
|
||||
- 激活状态明显
|
||||
- 切换动画流畅
|
||||
- 响应式适配
|
||||
|
||||
## OrderCard 组件
|
||||
|
||||
### 功能要求
|
||||
- 展示工单基本信息(标题、时间、联系人、状态等)
|
||||
- 支持点击事件
|
||||
- 支持呼叫功能
|
||||
- 支持完成操作
|
||||
- 状态标识清晰
|
||||
|
||||
### 设计要求
|
||||
- 卡片式布局,圆角设计
|
||||
- 信息层次分明
|
||||
- 操作按钮位置合理
|
||||
- 状态颜色区分明显
|
||||
- 支持不同状态的视觉反馈
|
||||
|
||||
## OrderList 组件
|
||||
|
||||
### 功能要求
|
||||
- 显示工单列表
|
||||
- 集成z-paging组件,支持虚拟列表
|
||||
- 支持自定义下拉刷新(文案、样式、阈值)
|
||||
- 支持自定义上拉加载更多(文案、样式、阈值)
|
||||
- 自动管理空数据状态
|
||||
- 支持固定高度和自适应高度模式
|
||||
- 完整的事件回调机制
|
||||
- 加载状态管理
|
||||
|
||||
### 设计要求
|
||||
- 列表项间距合理
|
||||
- 加载动画流畅
|
||||
- 空状态友好提示
|
||||
- 响应式布局
|
||||
- 虚拟列表优化大数据渲染性能
|
||||
|
||||
### z-paging配置
|
||||
- `useVirtualList`: 是否启用虚拟列表(默认true)
|
||||
- `virtualListHeight`: 虚拟列表高度(默认100%)
|
||||
- `cellHeightMode`: 单元格高度模式(auto/fixed)
|
||||
- `fixedHeight`: 固定高度值(当cellHeightMode为fixed时使用)
|
||||
- `customEmptyView`: 是否使用自定义空状态
|
||||
|
||||
## ConsultationBar 组件
|
||||
|
||||
### 功能要求
|
||||
- 底部固定咨询栏
|
||||
- 显示客服信息和联系方式
|
||||
- 支持立即咨询功能
|
||||
- 默认隐藏,点击"立即呼叫"按钮后显示
|
||||
- "立即咨询"按钮单独一行显示
|
||||
- 支持显示/隐藏动画效果
|
||||
|
||||
### 设计要求
|
||||
- 固定在页面底部
|
||||
- 背景半透明或纯色
|
||||
- 按钮样式与主题一致
|
||||
- 信息布局清晰
|
||||
- 支持安全区域适配
|
||||
- 显示/隐藏动画流畅
|
||||
|
||||
## 数据结构
|
||||
|
||||
### 工单数据结构
|
||||
```javascript
|
||||
{
|
||||
id: String, // 工单ID
|
||||
title: String, // 工单标题
|
||||
createTime: String, // 创建时间
|
||||
contactName: String, // 联系人姓名
|
||||
contactPhone: String, // 联系电话
|
||||
status: String, // 状态:pending-待处理, processing-处理中, completed-已完成, cancelled-已取消
|
||||
type: String // 类型:service-服务工单, order-普通订单
|
||||
}
|
||||
```
|
||||
|
||||
### Tab数据结构
|
||||
```javascript
|
||||
{
|
||||
label: String, // 显示文本
|
||||
value: String // 值
|
||||
}
|
||||
```
|
||||
|
||||
## 技术要求
|
||||
|
||||
### 框架和库
|
||||
- Vue 3 Composition API
|
||||
- uni-app框架
|
||||
- z-paging组件(用于列表优化)
|
||||
- SCSS样式预处理
|
||||
|
||||
### 性能优化
|
||||
- 虚拟列表支持大数据量渲染
|
||||
- 图片懒加载
|
||||
- 组件按需加载
|
||||
- 合理的缓存策略
|
||||
|
||||
### 兼容性
|
||||
- 支持微信小程序
|
||||
- 支持H5
|
||||
- 支持APP
|
||||
- 响应式设计,适配不同屏幕尺寸
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.2.0 (最新)
|
||||
- ✅ 集成z-paging组件到OrderList
|
||||
- ✅ 支持虚拟列表,提升大数据渲染性能
|
||||
- ✅ 自定义下拉刷新和上拉加载更多
|
||||
- ✅ 自动管理空数据状态
|
||||
- ✅ 支持固定高度和自适应高度模式
|
||||
- ✅ 完整的事件回调机制
|
||||
- ✅ 创建OrderList演示页面
|
||||
|
||||
### v1.1.0
|
||||
- ✅ 修改ConsultationBar组件布局
|
||||
- ✅ "立即咨询"按钮单独一行显示
|
||||
- ✅ 默认隐藏,点击"立即呼叫"后显示
|
||||
- ✅ 添加显示/隐藏动画效果
|
||||
- ✅ 更新相关样式和交互逻辑
|
||||
|
||||
### v1.0.0
|
||||
- ✅ 完成OrderCard组件开发
|
||||
- ✅ 完成OrderList组件开发
|
||||
- ✅ 完成ConsultationBar组件开发
|
||||
- ✅ 完成TopNavBar组件开发
|
||||
- ✅ 完成Tabs组件开发
|
||||
- ✅ 完成订单管理页面集成
|
||||
- ✅ 创建组件演示页面
|
||||
- ✅ 编写技术文档
|
||||
@ -1,128 +0,0 @@
|
||||
# 服务工单页面组件需求分析
|
||||
|
||||
## 页面概述
|
||||
|
||||
根据提供的设计图,这是一个服务工单管理页面,主要包含以下几个核心组件:
|
||||
|
||||
## 1. 顶部导航栏组件 (TopNavBar)
|
||||
|
||||
### 功能需求:
|
||||
|
||||
- 左侧返回按钮
|
||||
|
||||
### 设计要求:
|
||||
|
||||
- 图标使用 order/images/back.png
|
||||
- 高度适配状态栏
|
||||
|
||||
### 提示词:
|
||||
|
||||
```
|
||||
使用 uniapp + vue3 组合式 api 开发微信小程序顶部导航栏组件,要求如下:
|
||||
1、左侧显示返回按钮,点击可返回上一页
|
||||
2、适配不同设备的状态栏高度
|
||||
3、组件内部使用 uniapp 内置组件
|
||||
```
|
||||
|
||||
## 2. Tab 切换组件 (Tab)
|
||||
|
||||
### 功能需求:
|
||||
|
||||
- 支持多个标签页切换(全部订单、服务工单)
|
||||
- 选中状态有下划线指示器
|
||||
- 下划线支持动画过渡效果
|
||||
- 支持自定义标签内容
|
||||
|
||||
### 设计要求:
|
||||
|
||||
- 选中项文字颜色:深色 (#333)
|
||||
- 未选中项文字颜色:灰色 (#666)
|
||||
- 下划线颜色:蓝色 (#007AFF)
|
||||
- 下划线宽度根据文字宽度动态调整
|
||||
|
||||
### 提示词:
|
||||
|
||||
```
|
||||
使用 uniapp + vue3 组合式 api 开发微信小程序Tab切换组件,要求如下:
|
||||
1、支持多个标签页切换,默认支持"全部订单"、"服务工单"两个标签
|
||||
2、选中项底部显示蓝色下划线,下划线宽度根据文字宽度动态调整
|
||||
3、下划线切换时有平滑的滑动动画效果
|
||||
4、选中项文字为深色,未选中为灰色
|
||||
5、支持自定义标签列表和默认选中项
|
||||
6、组件内部使用 uniapp 内置组件
|
||||
7、支持插槽自定义标签内容
|
||||
```
|
||||
|
||||
## 3. 工单卡片组件 (OrderCard)
|
||||
|
||||
### 功能需求:
|
||||
|
||||
- 显示工单基本信息(标题、创建时间、联系人、电话)
|
||||
- 支持不同状态的工单样式(待处理、已完成等)
|
||||
- 支持操作按钮(立即呼叫、已完成标记等)
|
||||
- 支持工单状态图标显示
|
||||
- 超过 30 天卡片置灰
|
||||
|
||||
### 设计要求:
|
||||
|
||||
- 卡片背景:白色,圆角设计
|
||||
- 卡片间距:适当的上下间距
|
||||
- 状态图标:橙色圆形图标(待处理)、绿色图标(已完成)、灰色图标(其他状态)
|
||||
- 操作按钮:橙色渐变按钮(立即呼叫)、绿色边框按钮(已完成)
|
||||
|
||||
### 提示词:
|
||||
|
||||
```
|
||||
使用 uniapp + vue3 组合式 api 开发微信小程序工单卡片组件,要求如下:
|
||||
1、卡片式布局,白色背景,圆角设计
|
||||
2、显示工单标题、创建时间、联系房客、联系电话等信息
|
||||
3、左侧显示状态图标,不同状态使用不同颜色(橙色-待处理、绿色-已完成、灰色-其他)
|
||||
4、右侧显示状态标签,支持"已完成"等状态显示
|
||||
5、底部支持操作按钮,如"立即呼叫"(橙色渐变按钮)
|
||||
6、支持不同工单状态的样式变化
|
||||
7、组件内部使用 uniapp 内置组件
|
||||
8、支持插槽自定义操作区域
|
||||
```
|
||||
|
||||
## 4. 工单列表容器组件 (OrderList)
|
||||
|
||||
### 功能需求:
|
||||
|
||||
- 支持工单虚拟列表不固定高度的滚动显示
|
||||
- 支持下拉刷新和上拉加载更多
|
||||
- 支持空状态显示
|
||||
- 支持加载状态显示
|
||||
|
||||
### 设计要求:
|
||||
|
||||
- 列表背景:蓝色渐变背景
|
||||
- 卡片间距:统一的间距设计
|
||||
- 滚动区域:支持弹性滚动
|
||||
|
||||
### 提示词:
|
||||
|
||||
```
|
||||
使用 uniapp + vue3 组合式 api 开发微信小程序工单列表组件,要求如下:
|
||||
1、支持工单数据的列表展示
|
||||
2、背景使用蓝色渐变效果
|
||||
3、支持下拉刷新和上拉加载更多功能
|
||||
4、支持空状态和加载状态的显示
|
||||
5、列表项使用工单卡片组件进行展示
|
||||
6、支持不同Tab页面的数据筛选
|
||||
7、组件内部使用 uniapp 内置组件
|
||||
8、支持自定义列表项模板
|
||||
```
|
||||
|
||||
## 整体页面集成提示词
|
||||
|
||||
```
|
||||
使用 uniapp + vue3 组合式 api 开发微信小程序服务工单页面,要求如下:
|
||||
1、页面包含顶部导航栏、Tab切换、工单列表、底部咨询栏等模块
|
||||
2、支持"全部订单"和"服务工单"两个Tab页面切换
|
||||
3、工单列表支持不同状态的工单展示(待处理、已完成等)
|
||||
4、整体使用蓝色渐变背景,卡片式布局
|
||||
5、支持下拉刷新和上拉加载更多
|
||||
6、所有组件都要求响应式设计,适配不同屏幕尺寸
|
||||
7、使用组件化开发,各模块独立封装
|
||||
8、遵循微信小程序设计规范
|
||||
```
|
||||
@ -1,12 +0,0 @@
|
||||
.order-page {
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.top-nav-fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
@ -14,8 +14,6 @@
|
||||
"enhance": true,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"packNpmRelationList": [],
|
||||
"ignoreDevUnusedFiles": false,
|
||||
"ignoreUploadUnusedFiles": false,
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
|
||||
BIN
static/back.png
BIN
static/back.png
Binary file not shown.
|
Before Width: | Height: | Size: 359 B |
Loading…
Reference in New Issue
Block a user