feat: 新增统一底部导航与全局样式系统,优化交互体验
新增全局 YjyfTabBar 组件,替换各页面自定义导航,实现一致的 tab 切换逻辑与图标样式 新增全局设计令牌与 SCSS 变量系统,统一色彩、间距、字阶、圆角与投影规范 新增 POST 请求防并发锁,避免重复提交 重构多个页面,添加骨架屏、空状态引导、点击反馈类,修复硬编码样式与假数据 删除废弃的后台入口文件与 nginx 配置,清理冗余代码
这个提交包含在:
@@ -1,8 +0,0 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
Options +FollowSymlinks -Multiviews
|
||||
RewriteEngine On
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L]
|
||||
</IfModule>
|
||||
|
||||
0
server/public/nginx.htaccess
普通文件
0
server/public/nginx.htaccess
普通文件
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
export default {
|
||||
onLaunch: function() {
|
||||
console.log('App Launch')
|
||||
console.log('优匠优服 App 启动')
|
||||
},
|
||||
onShow: function() {
|
||||
console.log('App Show')
|
||||
@@ -12,6 +12,82 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/*每个页面公共css */
|
||||
<style lang="scss">
|
||||
/* ============================================================
|
||||
优匠优服 · 全局通用样式与基准工具类
|
||||
============================================================ */
|
||||
|
||||
page {
|
||||
background-color: $yjyf-bg-page;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', 'Hiragino Sans GB', Arial, sans-serif;
|
||||
color: $yjyf-text-main;
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
view, text, image, input, textarea, scroll-view, button {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 全局安全区底部内边距 */
|
||||
.safe-area-bottom {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* 全局规范卡片类 */
|
||||
.yjyf-card {
|
||||
margin: $yjyf-space-sm $yjyf-space-lg 0;
|
||||
padding: $yjyf-space-md;
|
||||
background: $yjyf-bg-card;
|
||||
border-radius: $yjyf-radius-md;
|
||||
box-shadow: $yjyf-shadow-card;
|
||||
}
|
||||
|
||||
/* 即时按压微交互反馈 (消除生硬感) */
|
||||
.clickable-hover {
|
||||
opacity: 0.82 !important;
|
||||
transform: scale(0.985);
|
||||
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* 全局胶囊主行动按钮 */
|
||||
.yjyf-btn-primary {
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
padding: 0 $yjyf-space-xl;
|
||||
border-radius: $yjyf-radius-pill;
|
||||
background: $yjyf-gradient-primary;
|
||||
color: #FFFFFF;
|
||||
font-size: $yjyf-font-body;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
box-shadow: 0 8rpx 20rpx rgba(240, 113, 13, 0.28);
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&[disabled], &.disabled {
|
||||
opacity: 0.65;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 骨架屏微光呼吸动效 (解决白屏) */
|
||||
@keyframes yjyf-shimmer {
|
||||
0% {
|
||||
background-position: -400rpx 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 400rpx 0;
|
||||
}
|
||||
}
|
||||
|
||||
.yjyf-shimmer {
|
||||
background: linear-gradient(90deg, #F0F1F3 25%, #E6E8EA 37%, #F0F1F3 63%);
|
||||
background-size: 800rpx 100%;
|
||||
animation: yjyf-shimmer 1.4s ease infinite;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
215
yjyf-app/components/YjyfTabBar.vue
普通文件
215
yjyf-app/components/YjyfTabBar.vue
普通文件
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<view class="yjyf-tabbar-container">
|
||||
<view class="tabbar">
|
||||
<!-- 1. 首页 -->
|
||||
<view class="tab" :class="{ on: current === 0 }" @tap="switchTab(0)">
|
||||
<image class="tab-icon" :src="current === 0 ? ICONS.homeActive : ICONS.home" mode="aspectFit"></image>
|
||||
<text class="tab-txt">首页</text>
|
||||
</view>
|
||||
|
||||
<!-- 2. 订单 -->
|
||||
<view class="tab" :class="{ on: current === 1 }" @tap="switchTab(1)">
|
||||
<image class="tab-icon" :src="current === 1 ? ICONS.orderActive : ICONS.order" mode="aspectFit"></image>
|
||||
<text class="tab-txt">订单</text>
|
||||
</view>
|
||||
|
||||
<!-- 3. 中间高光 FAB:立即下单 -->
|
||||
<view class="tab tab-center">
|
||||
<view class="fab-wrapper" @tap="onOrder">
|
||||
<view class="fab clickable-hover">
|
||||
<image class="fab-icon" :src="ICONS.plus" mode="aspectFit"></image>
|
||||
</view>
|
||||
<text class="fab-label">立即下单</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 4. 消息 -->
|
||||
<view class="tab" :class="{ on: current === 2 }" @tap="switchTab(2)">
|
||||
<image class="tab-icon" :src="current === 2 ? ICONS.msgActive : ICONS.msg" mode="aspectFit"></image>
|
||||
<text class="tab-txt">消息</text>
|
||||
</view>
|
||||
|
||||
<!-- 5. 我的 -->
|
||||
<view class="tab" :class="{ on: current === 3 }" @tap="switchTab(3)">
|
||||
<image class="tab-icon" :src="current === 3 ? ICONS.mineActive : ICONS.mine" mode="aspectFit"></image>
|
||||
<text class="tab-txt">我的</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部防遮挡安全垫高 -->
|
||||
<view class="tabbar-placeholder"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { isLogin, toLogin } from '@/utils/request.js'
|
||||
|
||||
const props = defineProps({
|
||||
current: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
// 现代细腻矢量 SVG 图标集(双色品牌橙高质感)
|
||||
const ICONS = {
|
||||
// 首页
|
||||
home: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238E9297' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M3 9.5L12 3l9 6.5V20a1 1 0 0 1-1 1h-5v-6a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v6H4a1 1 0 0 1-1-1V9.5z'/></svg>",
|
||||
homeActive: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='%23FFEADB' d='M4 10.2L12 4.3l8 5.9V19a1 1 0 0 1-1 1h-4v-5a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v5H5a1 1 0 0 1-1-1v-8.8z'/><path fill='%23F0710D' d='M12 3a1 1 0 0 1 .6.2l9 6.5a1 1 0 0 1-1.2 1.6L12 5.2 3.6 11.3a1 1 0 0 1-1.2-1.6l9-6.5A1 1 0 0 1 12 3zm-2 12h4v5h-4v-5z'/></svg>",
|
||||
|
||||
// 订单
|
||||
order: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238E9297' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z'/><polyline points='14 2 14 8 20 8'/><line x1='16' y1='13' x2='8' y2='13'/><line x1='16' y1='17' x2='8' y2='17'/><line x1='10' y1='9' x2='8' y2='9'/></svg>",
|
||||
orderActive: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='%23FFEADB' d='M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z'/><path fill='%23F0710D' d='M14 2v6h6M8 13h8v2H8zm0 4h8v2H8zm0-8h3v2H8z'/></svg>",
|
||||
|
||||
// 中间加号
|
||||
plus: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23FFFFFF' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'><line x1='12' y1='5' x2='12' y2='19'/><line x1='5' y1='12' x2='19' y2='12'/></svg>",
|
||||
|
||||
// 消息
|
||||
msg: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238E9297' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z'/></svg>",
|
||||
msgActive: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='%23FFEADB' d='M20.5 11.5a8.5 8.5 0 0 1-12.3 7.6L3 21l1.9-5.2A8.5 8.5 0 1 1 20.5 11.5z'/><circle fill='%23F0710D' cx='8.5' cy='11.5' r='1.5'/><circle fill='%23F0710D' cx='12' cy='11.5' r='1.5'/><circle fill='%23F0710D' cx='15.5' cy='11.5' r='1.5'/></svg>",
|
||||
|
||||
// 我的
|
||||
mine: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238E9297' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'/><circle cx='12' cy='7' r='4'/></svg>",
|
||||
mineActive: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='%23FFEADB' d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'/><circle fill='%23FFEADB' cx='12' cy='7' r='4'/><path fill='%23F0710D' d='M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8zm0 2c-3.3 0-8 1.7-8 5v2h16v-2c0-3.3-4.7-5-8-5z'/></svg>"
|
||||
}
|
||||
|
||||
const TAB_ROUTES = [
|
||||
'/pages/index/index',
|
||||
'/pages/order/list',
|
||||
'/pages/message/message',
|
||||
'/pages/mine/mine'
|
||||
]
|
||||
|
||||
function needLogin() {
|
||||
if (isLogin()) return true
|
||||
toLogin()
|
||||
return false
|
||||
}
|
||||
|
||||
function onOrder() {
|
||||
if (!needLogin()) return
|
||||
uni.navigateTo({ url: '/pages/order/quick' })
|
||||
}
|
||||
|
||||
function switchTab(index) {
|
||||
if (props.current === index) return
|
||||
if (index > 0 && !needLogin()) return
|
||||
|
||||
const targetUrl = TAB_ROUTES[index]
|
||||
if (targetUrl) {
|
||||
uni.reLaunch({ url: targetUrl })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.yjyf-tabbar-container {
|
||||
position: relative;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.tabbar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 104rpx;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.03);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.tab-txt {
|
||||
margin-top: 4rpx;
|
||||
font-size: 20rpx;
|
||||
color: #8E9297;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5rpx;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.tab.on .tab-txt {
|
||||
color: #F0710D;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tab.on .tab-icon {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
/* 中间 FAB:精致小圆纽 */
|
||||
.tab-center {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.fab-wrapper {
|
||||
position: relative;
|
||||
top: -26rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fab {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #FFA93F 0%, #F0710D 100%);
|
||||
box-shadow: 0 6rpx 18rpx rgba(240, 113, 13, 0.32);
|
||||
border: 4rpx solid #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.fab:active {
|
||||
transform: scale(0.92);
|
||||
box-shadow: 0 2rpx 10rpx rgba(240, 113, 13, 0.2);
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.fab-label {
|
||||
margin-top: 6rpx;
|
||||
font-size: 20rpx;
|
||||
color: #F0710D;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
/* 页面垫高高度 */
|
||||
.tabbar-placeholder {
|
||||
width: 100%;
|
||||
height: 120rpx;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,495 @@
|
||||
# 优匠优服(YJYF)小程序端前端开发与 UI/UX 体验设计规范
|
||||
> 版本:v1.0.0
|
||||
> 适用端:微信小程序 / Uni-App (Vue 3 + Vite)
|
||||
> 制定日期:2026年11月
|
||||
> 状态:正式发布(全项目统一遵循)
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
1. [规范总则与核心原则](#1-规范总则与核心原则)
|
||||
2. [设计系统与设计令牌 (Design Tokens)](#2-设计系统与设计令牌-design-tokens)
|
||||
- 2.1 品牌与功能色盘
|
||||
- 2.2 字阶排版与可读性基线
|
||||
- 2.3 8rpx 间距模数阶梯
|
||||
- 2.4 圆角与弥散投影
|
||||
3. [布局与多端适配规范](#3-布局与多端适配规范)
|
||||
- 3.1 视口基准与单位准则
|
||||
- 3.2 导航体系与路由栈管控
|
||||
- 3.3 顶部状态栏与底部安全区适配
|
||||
- 3.4 通用页面布局骨架
|
||||
4. [图标与视觉资产规范](#4-图标与视觉资产规范)
|
||||
- 4.1 核心图标禁令与替代规范
|
||||
- 4.2 统一矢量图标规格
|
||||
- 4.3 缺省与占位图规范
|
||||
5. [触控热区与微交互规范](#5-触控热区与微交互规范)
|
||||
- 5.1 44×44px 最小触控热区
|
||||
- 5.2 即时触控反馈(Hover State)
|
||||
- 5.3 动效时序与物理缓动
|
||||
6. [状态完备性体验规范 (8 态模型)](#6-状态完备性体验规范-8-态模型)
|
||||
- 6.1 页面级骨架屏与渐进加载
|
||||
- 6.2 列表触底分页与刷新协议
|
||||
- 6.3 友好空状态设计
|
||||
7. [表单与交互反馈规范](#7-表单与交互反馈规范)
|
||||
- 7.1 外部可见标签与占位符
|
||||
- 7.2 就近行内实时校验
|
||||
- 7.3 提交防重防抖与加载反馈
|
||||
- 7.4 原生键盘与弹层避让
|
||||
8. [工程化落地代码模版](#8-工程化落地代码模版)
|
||||
- 8.1 全局 `uni.scss` 核心变量集
|
||||
- 8.2 `App.vue` 全局基准与样式工具类
|
||||
- 8.3 `request.js` 请求体验增强
|
||||
- 8.4 交付前 10 步设计自检清单
|
||||
|
||||
---
|
||||
|
||||
## 1. 规范总则与核心原则
|
||||
|
||||
为了保证“优匠优服”产品矩阵在移动端具备**高信任度、高商业审美、高可用性与流畅的操作手感**,杜绝测试原型式粗糙感,特制定本规范。全员在进行任何页面开发、UI 重构或组件开发时,必须严格遵守以下四大原则:
|
||||
|
||||
1. **一致性优先 (Consistency First)**:严禁在单个组件或页面中随意硬编码十六进制颜色与边距,所有样式必须溯源至全局 Design Tokens。
|
||||
2. **严防极小字号与低对比度 (Legibility Baseline)**:正文字号基线 ≥ 24rpx (12px),禁止出现低于 22rpx 的微字;文本与底色对比度必须满足 WCAG AA 级(≥ 4.5:1)。
|
||||
3. **消除伪图标与粗糙占位 (Professional Assets)**:严禁使用汉字单字(如“微、厅、单、钱”)充当功能图标,必须统一使用高质量 SVG 矢量图标或双色微拟物图标。
|
||||
4. **状态闭环与零白屏 (Zero-Blank Loading)**:每个页面和列表必须完整具备默认态、加载骨架态、空数据导流态和错误重试态,严禁在接口未完成前整屏白屏。
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计系统与设计令牌 (Design Tokens)
|
||||
|
||||
### 2.1 品牌与功能色盘
|
||||
|
||||
严禁各页面混用杂乱 Hex 值,全局严格执行 60-30-10 配色法则(60%中性底色 `#F5F6F8`、30%白卡底 `#FFFFFF`、10%核心行动橙色)。
|
||||
|
||||
| 语义名称 | 变量名 | 色值 / 渐变 | 适用场景 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **品牌主色 (Primary)** | `$yjyf-primary` | `#F0710D` | 核心 CTA 按钮、主强调色、选中标签、价格高亮 |
|
||||
| **品牌亮橙** | `$yjyf-primary-light` | `#FFA93F` | 渐变起点、次级强调、高光点缀 |
|
||||
| **品牌浅橙底** | `$yjyf-primary-bg` | `#FFF7F0` | 选中卡片底色、提示条底色、轻按钮底色 |
|
||||
| **CTA 主渐变** | `$yjyf-gradient-primary` | `linear-gradient(135deg, #FFA93F 0%, #F0710D 100%)` | 底部大按钮、主操作按钮、高光徽标 |
|
||||
| **成功色 (Success)** | `$yjyf-success` | `#2FA84F` | 微信授权按钮、已完成状态、支付成功 |
|
||||
| **警示危险 (Danger)** | `$yjyf-danger` | `#E8331B` | 取消订单、删除地址、申诉驳回、表单报错 |
|
||||
| **提示关注 (Warning)** | `$yjyf-warning` | `#F98D25` | 急单标签、待支付倒计时、审核中 |
|
||||
| **链接/信息 (Info)** | `$yjyf-info` | `#4A7CFF` | 待分配状态、跳转链接、协议文本 |
|
||||
|
||||
#### 中性文字与背景层级
|
||||
| 变量名 | 色值 | 对应层级与用途 |
|
||||
| :--- | :--- | :--- |
|
||||
| `$yjyf-text-main` | `#1F1F1F` | **一级标题与核心正文**:页面大标题、卡片主标题、订单名称 |
|
||||
| `$yjyf-text-regular` | `#444444` | **二级正文**:列表详情、输入框文本、表单标签 Key |
|
||||
| `$yjyf-text-secondary` | `#888888` | **三级辅助说明**:地址详情、副标题、备注说明 |
|
||||
| `$yjyf-text-muted` | `#AAAAAA` | **四级弱化信息**:时间戳、订单流水号、提示角标 |
|
||||
| `$yjyf-text-disabled` | `#C8C7CC` | **禁用态/占位符**:未填 Placeholder、已失效操作项 |
|
||||
| `$yjyf-bg-page` | `#F5F6F8` | **全局页面底色** |
|
||||
| `$yjyf-bg-card` | `#FFFFFF` | **通用卡片底色** |
|
||||
| `$yjyf-border-color` | `#F0F0F2` | **卡片分割线/细描边**(1rpx) |
|
||||
|
||||
---
|
||||
|
||||
### 2.2 字阶排版与可读性基线
|
||||
|
||||
严格禁止出现 `19rpx`、`20rpx`、`21rpx` 等不可读字号。移动端全平台标准字阶如下:
|
||||
|
||||
| 字阶角色 | 字号 (rpx / px) | 字重 | 行高 (Line-Height) | 适用范围 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **Display 巨幅价格/数字** | `64rpx ~ 72rpx` (32~36px) | 700 (Bold) | `1.1` | 钱包余额大数字、关键金额 |
|
||||
| **Title 页面/主标题** | `36rpx ~ 40rpx` (18~20px) | 700 (Bold) | `1.3` | Hero 头部大标题、弹窗标题 |
|
||||
| **SubTitle 模块/卡片标题** | `30rpx ~ 32rpx` (15~16px) | 600 (SemiBold) | `1.4` | 卡片头部标题、服务项目名称 |
|
||||
| **Body 正文基准** | `28rpx` (14px) | 400 (Regular) | `1.5` | 表单输入、正文段落、列表主体 |
|
||||
| **Caption 辅助说明** | `24rpx` (12px) | 400 (Regular) | `1.4` | 服务描述、次级元数据、标签文字 |
|
||||
| **Micro 极限微说明** | `22rpx` (11px) | 400 (Regular) | `1.3` | **绝对下限**:仅允许订单流水号、时间戳 |
|
||||
|
||||
---
|
||||
|
||||
### 2.3 8rpx 间距模数阶梯
|
||||
|
||||
所有布局 `margin`、`padding`、`gap` 必须基于 **8rpx (4px)** 模数梯度,严禁出现 `22rpx`、`28rpx`、`34rpx` 等不规则数值:
|
||||
|
||||
| 模数 Token | 间距数值 | 典型应用场景 |
|
||||
| :--- | :--- | :--- |
|
||||
| `$yjyf-space-xs` | `8rpx` (4px) | 图标与文字微间距、标签内边距 |
|
||||
| `$yjyf-space-sm` | `16rpx` (8px) | 相邻行间距、卡片内子模块微间隔 |
|
||||
| `$yjyf-space-md` | `24rpx` (12px) | **卡片内边距 (Padding)**、列表项垂直间距 |
|
||||
| `$yjyf-space-lg` | `32rpx` (16px) | **页面两侧边距 (Gutter)**、大模块外间距 |
|
||||
| `$yjyf-space-xl` | `48rpx` (24px) | 头部装饰区高度、提交按钮上下外边距 |
|
||||
|
||||
---
|
||||
|
||||
### 2.4 圆角与弥散投影
|
||||
|
||||
拒绝生硬生冷的纯黑深阴影,统一采用具有品牌暖调的柔和弥散投影:
|
||||
|
||||
| 资产 Token | 规格 | 适用场景 |
|
||||
| :--- | :--- | :--- |
|
||||
| **卡片圆角** | `border-radius: 20rpx;` | 内容卡片、表单区块、列表项 |
|
||||
| **弹窗/Sheet 圆角** | `border-radius: 32rpx 32rpx 0 0;` | 底部弹出半屏 Sheet |
|
||||
| **药丸/按钮圆角** | `border-radius: 999rpx;` | 核心 CTA 胶囊按钮、筛选 Chip |
|
||||
| **品牌卡片投影** | `box-shadow: 0 6rpx 20rpx rgba(240, 113, 13, 0.06);` | 强调卡片、订单卡片 |
|
||||
| **底部悬浮栏投影** | `box-shadow: 0 -6rpx 24rpx rgba(0, 0, 0, 0.06);` | 底部固定结算条、吸底栏 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 布局与多端适配规范
|
||||
|
||||
### 3.1 视口基准与单位准则
|
||||
- **基准分辨率**:统一按照 `750rpx` 设计稿还原(对应 iPhone 6/7/8 基准宽 375px,即 `1px = 2rpx`)。
|
||||
- **单位使用规则**:
|
||||
- 页面排版、间距、宽高优先使用 `rpx`。
|
||||
- 边框分割线推荐使用 `1rpx`(在真机高清屏呈现细腻发丝线)。
|
||||
- 极端微小固定尺寸(如微小红点指示器)可使用 `px` 避免奇偶舍入抖动。
|
||||
|
||||
---
|
||||
|
||||
### 3.2 导航体系与路由栈管控
|
||||
|
||||
#### 核心规定:严禁滥用自定义 Tabbar 压栈
|
||||
- **原生 Tabbar 接管全局四大入口**:
|
||||
在 `pages.json` 中配置原生 `tabBar`,覆盖:“首页 (`pages/index/index`)”、“订单 (`pages/order/list`)”、“消息 (`pages/message/message`)”、“我的 (`pages/mine/mine`)”。
|
||||
- **跳转动作规范**:
|
||||
- 四大主 Tab 之间切换必须使用 `uni.switchTab()`,禁止使用 `uni.navigateTo()`。
|
||||
- 二级子页面进入(如服务详情、下单页、地址列表)使用 `uni.navigateTo()`。
|
||||
- 登录失效或角色彻底切换(如从业主切换到师傅工作台)使用 `uni.reLaunch()` 清理当前页面栈。
|
||||
|
||||
---
|
||||
|
||||
### 3.3 顶部状态栏与底部安全区适配
|
||||
|
||||
#### 顶部自定义导航(Header)动态计算
|
||||
所有自定义 NavigationBar 页面(`navigationStyle: "custom"`)必须动态提取状态栏与胶囊位置,**严禁硬编码 `top: 92rpx`**:
|
||||
|
||||
```javascript
|
||||
// 统一状态栏计算混入或工具方法
|
||||
export function useNavMetrics() {
|
||||
const statusBarHeight = ref(20);
|
||||
const navBarHeight = ref(44);
|
||||
|
||||
try {
|
||||
const sys = uni.getSystemInfoSync();
|
||||
if (sys.statusBarHeight) statusBarHeight.value = sys.statusBarHeight;
|
||||
// 获取微信小程序右上角胶囊按钮位置
|
||||
// #ifdef MP-WEIXIN
|
||||
const menu = uni.getMenuButtonBoundingClientRect();
|
||||
if (menu && menu.top) {
|
||||
navBarHeight.value = (menu.top - sys.statusBarHeight) * 2 + menu.height;
|
||||
}
|
||||
// #endif
|
||||
} catch (e) {}
|
||||
|
||||
return { statusBarHeight, navBarHeight };
|
||||
}
|
||||
```
|
||||
|
||||
#### 底部安全区(全面屏 Home 条防遮挡)
|
||||
所有底部吸底固定操作栏(Fixed Bottom Bar)必须添加双重环境安全区内边距:
|
||||
```css
|
||||
.fixed-bottom-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 120rpx;
|
||||
/* 必须兼顾新旧语法 */
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 -6rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
/* 同时页面底部必须有占位,避免内容被固定栏覆盖 */
|
||||
.page-bottom-placeholder {
|
||||
height: calc(130rpx + constant(safe-area-inset-bottom));
|
||||
height: calc(130rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 通用页面布局骨架
|
||||
|
||||
每个页面的结构骨架必须规范统一,结构模板:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<view class="yjyf-page">
|
||||
<!-- 1. 自定义顶部导航栏 (可选) -->
|
||||
<view class="yjyf-nav-header" v-if="customNav" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-content" :style="{ height: navBarHeight + 'px' }">
|
||||
<view class="nav-back-hotspot" @tap="goBack">
|
||||
<image class="nav-icon" src="/static/icons/common/icon-back.svg" />
|
||||
</view>
|
||||
<text class="nav-title">{{ pageTitle }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 2. 主页面流式内容 -->
|
||||
<view class="yjyf-content">
|
||||
<!-- 骨架屏占位 -->
|
||||
<view class="skeleton-group" v-if="pageLoading">...</view>
|
||||
<!-- 真实卡片流 -->
|
||||
<view class="yjyf-card" v-else>...</view>
|
||||
</view>
|
||||
|
||||
<!-- 3. 底部占位与吸底按钮 -->
|
||||
<view class="page-bottom-placeholder" v-if="showBottomBar"></view>
|
||||
<view class="fixed-bottom-bar" v-if="showBottomBar">...</view>
|
||||
</view>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 图标与视觉资产规范
|
||||
|
||||
### 4.1 核心图标禁令与替代规范
|
||||
> [!CAUTION]
|
||||
> **绝对禁止在正式 UI 中使用系统 Emoji 表情或汉字单字(如“厅、单、价、钱、信、服、微、券”)充当模块图标。**
|
||||
|
||||
- **微信一键登录按钮**:必须配置微信标准矢量 SVG Logo,禁止使用白色圆圈加单字“微”。
|
||||
- **师傅工作台九宫格**:必须替换为高质量、双色扁平微拟物图标(24×24px 视口,描边统一 1.5px/2px)。
|
||||
- **缩略图缺省处理**:当服务项目未下发真实图片时,展示对应分类的标准插画,禁止使用纯代码截取文字 `name.slice(0, 2)`。
|
||||
|
||||
### 4.2 统一矢量图标规格
|
||||
所有图标资源归档在 `/static/icons/` 下,按业务域分包:
|
||||
- `/static/icons/common/`:通用箭头(`arrow-right.svg`)、返回(`back.svg`)、关闭、勾选。
|
||||
- `/static/icons/service/`:沙发、卫浴、灯具、开锁、窗帘等服务品类双色图标。
|
||||
- `/static/icons/workbench/`:大厅、订单、报价、钱包、信用、客服。
|
||||
|
||||
---
|
||||
|
||||
## 5. 触控热区与微交互规范
|
||||
|
||||
### 5.1 44×44px 最小触控热区
|
||||
所有移动端可点击组件必须保证视觉面积或外层响应面积 ≥ **44×44px**(88×88rpx):
|
||||
- **小图标按钮**(如返回键、关闭键、步进器 `+/-`):如果视觉图标仅为 32rpx,必须通过 `padding` 或外层容器扩展热区至 `88rpx`。
|
||||
```css
|
||||
/* 小图标按钮热区扩展最佳实践 */
|
||||
.icon-btn-hotspot {
|
||||
min-width: 88rpx;
|
||||
min-height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 即时触控反馈(Hover State)
|
||||
用户点击瞬间必须产生即时感知,消除操作“死机”错觉。页面所有交互组件必须添加 `hover-class="clickable-hover"`:
|
||||
```css
|
||||
.clickable-hover {
|
||||
opacity: 0.82;
|
||||
transform: scale(0.985);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 动效时序与物理缓动
|
||||
- **微交互与点击响应**:`150ms ~ 200ms`,缓动曲线 `cubic-bezier(0.4, 0, 0.2, 1)`。
|
||||
- **底部半屏弹窗 (Sheet) 展开/收起**:`250ms ~ 300ms`。
|
||||
- **严禁无意义的跳动与耗时超过 400ms 的拖沓过渡**。
|
||||
|
||||
---
|
||||
|
||||
## 6. 状态完备性体验规范 (8 态模型)
|
||||
|
||||
任何一个页面、列表或表单控件必须设计并处理完整的状态闭环:
|
||||
1. **默认态 (Default)**
|
||||
2. **悬浮/点击态 (Active/Hover)**
|
||||
3. **聚焦态 (Focus)**
|
||||
4. **禁用态 (Disabled)**
|
||||
5. **加载中 (Loading/Skeleton)**
|
||||
6. **空数据 (Empty)**
|
||||
7. **错误重试 (Error/Retry)**
|
||||
8. **已完成/成功态 (Success)**
|
||||
|
||||
### 6.1 页面级骨架屏与渐进加载
|
||||
针对 `pages/service/detail.vue`、`pages/order/detail.vue` 等长数据页面,接口请求期间严禁全屏空白:
|
||||
- 使用骨架屏组件展示卡片轮廓与轻微呼吸动画(Shimmer Animation)。
|
||||
- 数据加载完毕后以 `200ms` 淡入效果完成替换。
|
||||
|
||||
### 6.2 友好空状态设计规范
|
||||
列表无数据时,严禁只展示一行灰色小字 `<text>暂无数据</text>`:
|
||||
- **必须元素**:清晰轻量插图(160×160rpx)+ 明确的说明文案(28rpx)+ 引导行动按钮(如“去逛逛热门服务”)。
|
||||
|
||||
```html
|
||||
<view class="yjyf-empty-state" v-if="!loading && !list.length">
|
||||
<image class="empty-icon" src="/static/images/empty/empty-order.svg" mode="aspectFit" />
|
||||
<text class="empty-title">暂无相关订单</text>
|
||||
<text class="empty-sub">师傅随叫随到,快速响应,售后无忧</text>
|
||||
<button class="empty-btn" @tap="goServiceList">去预约服务</button>
|
||||
</view>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 表单与交互反馈规范
|
||||
|
||||
### 7.1 外部可见标签与占位符
|
||||
- **严禁纯靠 Placeholder 代替表单 Label**:输入框左侧或上方必须有稳定的标题(如“服务地址”、“联系电话”),避免输入内容后用户忘记该项含义。
|
||||
- **Placeholder 文本风格**:简明清晰,字号与正文保持 `28rpx` 一致,颜色为 `$yjyf-text-disabled` (`#C8C7CC`)。
|
||||
|
||||
### 7.2 就近行内实时校验
|
||||
- 校验不通过时,除弹窗或 Toast 提示外,必须定位至当前输入框并高亮边框:
|
||||
```css
|
||||
.input-item {
|
||||
border-bottom: 1rpx solid #F0F0F2;
|
||||
}
|
||||
.input-item.has-error {
|
||||
border-bottom-color: #E8331B;
|
||||
}
|
||||
.error-tip-text {
|
||||
font-size: 22rpx;
|
||||
color: #E8331B;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 提交防重防抖与加载反馈
|
||||
- **双重锁拦截**:
|
||||
1. JS 逻辑锁:在发起请求前校验 `if (submitting.value) return; submitting.value = true;`。
|
||||
2. 视图锁:提交按钮绑定 `:disabled="submitting"` 并显式呈现原生 Loading 动效与“提交中...”文案。
|
||||
- **网络并发去重**:统一请求层 `request.js` 针对同一特征接口(同一 URL + 参数)实行 500ms 快速防并发锁。
|
||||
|
||||
### 7.4 原生键盘与弹层避让
|
||||
- 手机号输入框必须设置 `type="number"` 或 `type="tel"`,限制 `maxlength="11"`。
|
||||
- 金额输入框设置 `type="digit"`。
|
||||
- 长表单在小程序端必须配置 `:adjust-position="true"` 和 `cursor-spacing="50"`,确保软键盘弹起时不遮挡当前输入行。
|
||||
|
||||
---
|
||||
|
||||
## 8. 工程化落地代码模版
|
||||
|
||||
### 8.1 全局 `uni.scss` 核心变量集
|
||||
|
||||
```scss
|
||||
// ==========================================
|
||||
// 优匠优服(YJYF)全局统一设计令牌 (Design Tokens)
|
||||
// ==========================================
|
||||
|
||||
// 1. 品牌核心色彩
|
||||
$yjyf-primary: #F0710D;
|
||||
$yjyf-primary-light: #FFA93F;
|
||||
$yjyf-primary-bg: #FFF7F0;
|
||||
$yjyf-gradient-primary: linear-gradient(135deg, #FFA93F 0%, #F0710D 100%);
|
||||
|
||||
// 2. 状态反馈与辅助色
|
||||
$yjyf-success: #2FA84F;
|
||||
$yjyf-danger: #E8331B;
|
||||
$yjyf-warning: #F98D25;
|
||||
$yjyf-info: #4A7CFF;
|
||||
|
||||
// 3. 中性文字阶梯
|
||||
$yjyf-text-main: #1F1F1F;
|
||||
$yjyf-text-regular: #444444;
|
||||
$yjyf-text-secondary: #888888;
|
||||
$yjyf-text-muted: #AAAAAA;
|
||||
$yjyf-text-disabled: #C8C7CC;
|
||||
|
||||
// 4. 背景与边框
|
||||
$yjyf-bg-page: #F5F6F8;
|
||||
$yjyf-bg-card: #FFFFFF;
|
||||
$yjyf-border-color: #F0F0F2;
|
||||
|
||||
// 5. 字阶尺寸 (rpx)
|
||||
$yjyf-font-hero: 44rpx;
|
||||
$yjyf-font-title: 32rpx;
|
||||
$yjyf-font-body: 28rpx;
|
||||
$yjyf-font-sub: 24rpx;
|
||||
$yjyf-font-micro: 22rpx;
|
||||
|
||||
// 6. 8rpx 间距阶梯
|
||||
$yjyf-space-xs: 8rpx;
|
||||
$yjyf-space-sm: 16rpx;
|
||||
$yjyf-space-md: 24rpx;
|
||||
$yjyf-space-lg: 32rpx;
|
||||
$yjyf-space-xl: 48rpx;
|
||||
|
||||
// 7. 圆角与投影
|
||||
$yjyf-radius-sm: 12rpx;
|
||||
$yjyf-radius-md: 20rpx;
|
||||
$yjyf-radius-lg: 32rpx;
|
||||
$yjyf-radius-pill: 999rpx;
|
||||
$yjyf-shadow-card: 0 6rpx 20rpx rgba(240, 113, 13, 0.06);
|
||||
$yjyf-shadow-bar: 0 -6rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8.2 `App.vue` 全局基准与样式工具类
|
||||
|
||||
```html
|
||||
<style lang="scss">
|
||||
/* 页面与全局元素重置 */
|
||||
page {
|
||||
background-color: $yjyf-bg-page;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
|
||||
color: $yjyf-text-main;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
view, text, image, input, textarea, scroll-view {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 统一安全区工具类 */
|
||||
.safe-area-bottom {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* 统一卡片基础类 */
|
||||
.yjyf-card {
|
||||
margin: $yjyf-space-md $yjyf-space-lg 0;
|
||||
padding: $yjyf-space-md;
|
||||
background-color: $yjyf-bg-card;
|
||||
border-radius: $yjyf-radius-md;
|
||||
box-shadow: $yjyf-shadow-card;
|
||||
}
|
||||
|
||||
/* 统一触控点击微反馈 */
|
||||
.clickable-hover {
|
||||
opacity: 0.82;
|
||||
transform: scale(0.985);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
/* 统一主操作胶囊大按钮 */
|
||||
.yjyf-btn-primary {
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
padding: 0 $yjyf-space-xl;
|
||||
border-radius: $yjyf-radius-pill;
|
||||
background: $yjyf-gradient-primary;
|
||||
color: #FFFFFF;
|
||||
font-size: $yjyf-font-body;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
box-shadow: 0 8rpx 20rpx rgba(240, 113, 13, 0.28);
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.65;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 交付前 10 步设计与体验自检清单
|
||||
|
||||
所有前端业务代码提交前,必须完成以下 10 项逐一核对:
|
||||
|
||||
- [ ] **1. 路由栈健康度**:Tab 切换是否全部使用 `switchTab`?是否存在页面跳转压栈 > 5 层且无法顺利返回的问题?
|
||||
- [ ] **2. 极小字排查**:整页是否存在低于 22rpx 的微字?正文字号是否全部 ≥ 24rpx?
|
||||
- [ ] **3. 间距模数校验**:所有外边距、内边距是否均能被 8rpx (4px) 整除?卡片左右外距是否统一为 32rpx?
|
||||
- [ ] **4. 44px 热区保障**:所有返回键、图标按钮、步进器 `+/-` 物理点击区域是否达到 88×88rpx?
|
||||
- [ ] **5. 图标专业度**:是否已彻底清除汉字单字(微、厅、单、钱)充当图标的遗留代码?
|
||||
- [ ] **6. 零白屏加载**:网络延迟时,详情页是否有骨架屏或清晰的 Loading 动画?
|
||||
- [ ] **7. 空态导流闭环**:列表暂无数据时,是否包含指引插画与“去下单 / 去浏览”跳转按钮?
|
||||
- [ ] **8. 全面屏避让**:底部固定栏是否添加了 `env(safe-area-inset-bottom)`?内容区是否留出了防遮挡垫高区?
|
||||
- [ ] **9. 表单防重锁**:所有提交按钮是否具备 `:disabled`、`:loading` 以及防快速双击保护?
|
||||
- [ ] **10. 真实数据绑定**:页面中是否还存在写死的假数据(如硬编码的用户昵称、假电话)?
|
||||
@@ -119,6 +119,12 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"easycom": {
|
||||
"autoscan": true,
|
||||
"custom": {
|
||||
"^Yjyf(.*)": "@/components/Yjyf$1.vue"
|
||||
}
|
||||
},
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "优匠优服",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</view>
|
||||
|
||||
<view class="bar">
|
||||
<view class="bar-btn" @tap="openForm()">+ 新增服务地址</view>
|
||||
<view class="bar-btn clickable-hover" @tap="openForm()">+ 新增服务地址</view>
|
||||
</view>
|
||||
|
||||
<view class="mask" v-if="showForm" @tap="showForm = false">
|
||||
@@ -50,7 +50,7 @@
|
||||
<view class="seg-i" :class="{ on: form.is_default === 0 }" @tap="form.is_default = 0">否</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="save" :class="{ disabled: saving }" @tap="save">{{ saving ? '保存中...' : '保存' }}</view>
|
||||
<view class="save clickable-hover" :class="{ disabled: saving }" @tap="save">{{ saving ? '保存中...' : '保存' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</view>
|
||||
<view class="greet-txt">
|
||||
<text class="greet-hi">{{ greeting }},欢迎回来</text>
|
||||
<text class="greet-name">qbdjsj1308160</text>
|
||||
<text class="greet-name">{{ userName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coupon" @tap="onCoupon">
|
||||
@@ -176,57 +176,17 @@
|
||||
<view class="foot-tip">优匠优服 · 让手艺人有活干,让家有温度</view>
|
||||
</view>
|
||||
|
||||
<!-- ==================== 自定义底部导航 ==================== -->
|
||||
<view class="tabbar">
|
||||
<view class="tab" :class="{ on: tabIndex === 0 }" @tap="switchTab(0)">
|
||||
<view class="tic">
|
||||
<view class="tr" style="left:10rpx;top:4rpx"></view>
|
||||
<view class="ol" style="left:13rpx;top:16rpx;width:18rpx;height:18rpx;border-top:none"></view>
|
||||
</view>
|
||||
<text class="tab-txt">首页</text>
|
||||
</view>
|
||||
<view class="tab" :class="{ on: tabIndex === 1 }" @tap="switchTab(1)">
|
||||
<view class="tic">
|
||||
<view class="ol" style="left:11rpx;top:5rpx;width:22rpx;height:30rpx;border-radius:3rpx"></view>
|
||||
<view class="ln" style="left:16rpx;top:13rpx;width:12rpx;height:3rpx"></view>
|
||||
<view class="ln" style="left:16rpx;top:21rpx;width:12rpx;height:3rpx"></view>
|
||||
</view>
|
||||
<text class="tab-txt">订单</text>
|
||||
</view>
|
||||
<view class="tab tab-center">
|
||||
<view class="fab" @tap="onOrder">
|
||||
<view class="fic">
|
||||
<view class="fo" style="left:5rpx;top:2rpx;width:32rpx;height:32rpx;border-radius:5rpx"></view>
|
||||
<view class="fl" style="left:11rpx;top:11rpx;width:14rpx;height:3rpx"></view>
|
||||
<view class="fl" style="left:11rpx;top:19rpx;width:14rpx;height:3rpx"></view>
|
||||
<view class="fp" style="left:25rpx;top:22rpx;width:21rpx;height:6rpx"></view>
|
||||
</view>
|
||||
<text class="fab-txt">立即下单</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tab" :class="{ on: tabIndex === 2 }" @tap="switchTab(2)">
|
||||
<view class="tic">
|
||||
<view class="ol" style="left:6rpx;top:7rpx;width:32rpx;height:24rpx;border-radius:12rpx"></view>
|
||||
<view class="tr-d" style="left:14rpx;top:29rpx"></view>
|
||||
</view>
|
||||
<text class="tab-txt">消息</text>
|
||||
</view>
|
||||
<view class="tab" :class="{ on: tabIndex === 3 }" @tap="switchTab(3)">
|
||||
<view class="tic">
|
||||
<view class="ol ct" style="left:14rpx;top:3rpx;width:16rpx;height:16rpx"></view>
|
||||
<view class="ol" style="left:7rpx;top:21rpx;width:30rpx;height:16rpx;border-radius:15rpx 15rpx 0 0;border-bottom:none"></view>
|
||||
</view>
|
||||
<text class="tab-txt">我的</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- ==================== 全局统一底部导航 ==================== -->
|
||||
<YjyfTabBar :current="0" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import YjyfTabBar from '@/components/YjyfTabBar.vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import api from '@/utils/api.js'
|
||||
import { isLogin, toLogin } from '@/utils/request.js'
|
||||
import { isLogin, toLogin, getUser } from '@/utils/request.js'
|
||||
import { ensureSilentLogin } from '@/utils/wxlogin.js'
|
||||
|
||||
/* ---------- 系统适配:状态栏 / 导航栏高度(刘海屏 + 胶囊按钮) ---------- */
|
||||
@@ -243,6 +203,15 @@
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
|
||||
/* ---------- 用户真实昵称与问候 ---------- */
|
||||
const userName = computed(() => {
|
||||
const u = getUser()
|
||||
if (u && u.nickname) return u.nickname
|
||||
if (u && u.mobile) return u.mobile.slice(0, 3) + '****' + u.mobile.slice(7)
|
||||
return '尊敬的业主'
|
||||
})
|
||||
|
||||
/* ---------- 问候语 ---------- */
|
||||
const hh = new Date().getHours()
|
||||
const greeting = hh < 6 ? '夜深了' : hh < 11 ? '上午好' : hh < 14 ? '中午好' : hh < 18 ? '下午好' : '晚上好'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<view class="top">
|
||||
<view class="deco d1"></view>
|
||||
<view class="deco d2"></view>
|
||||
<view class="back" v-if="showBack" @tap="goBack"><text class="back-t">‹</text></view>
|
||||
<view class="back clickable-hover" :style="{ top: (statusBarHeight + 10) + 'px' }" v-if="showBack" @tap="goBack"><text class="back-t">‹</text></view>
|
||||
<text class="brand">优匠优服</text>
|
||||
<text class="slogan">专业师傅 · 上门到家</text>
|
||||
</view>
|
||||
@@ -16,8 +16,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="wx-btn" :class="{ disabled: loading }" @tap="wxLogin">
|
||||
<view class="wx-icon"><text class="wx-icon-t">微</text></view>
|
||||
<view class="wx-btn clickable-hover" :class="{ disabled: loading }" @tap="wxLogin">
|
||||
<view class="wx-icon"><image class="wx-icon-img" src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%232FA84F'><path d='M8.5 2C4.36 2 1 4.91 1 8.5c0 1.94.98 3.69 2.53 4.88l-.64 2.37 2.76-1.44c.87.26 1.83.4 2.85.4.15 0 .3 0 .44-.01C9.64 12.39 11.58 11 14 11c.21 0 .42.01.62.03C14.16 6.06 11.56 2 8.5 2zm-2.25 4.5c.69 0 1.25.56 1.25 1.25S6.94 9 6.25 9 5 8.44 5 7.75 5.56 6.5 6.25 6.5zm4.5 0c.69 0 1.25.56 1.25 1.25S11.44 9 10.75 9 9.5 8.44 9.5 7.75 10.06 6.5 10.75 6.5zM15.5 12c-3.59 0-6.5 2.46-6.5 5.5 0 3.04 2.91 5.5 6.5 5.5.76 0 1.48-.11 2.14-.31l2.08 1.09-.48-1.79c1.38-.97 2.26-2.43 2.26-4.49 0-3.04-2.91-5.5-6.5-5.5zm-2 3c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm4 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1z'/></svg>" mode="aspectFit"></image></view>
|
||||
<text class="wx-txt">{{ loading ? '微信授权中...' : '微信一键登录' }}</text>
|
||||
</view>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<view class="fb-body" v-if="showPhone">
|
||||
<input class="fb-input" v-model="mobile" type="number" maxlength="11" placeholder="请输入手机号" />
|
||||
<input class="fb-input" v-model="code" type="number" maxlength="6" placeholder="验证码(联调固定 123456)" />
|
||||
<view class="fb-btn" @tap="phoneLogin">手机号登录 / 注册</view>
|
||||
<view class="fb-btn clickable-hover" @tap="phoneLogin">手机号登录 / 注册</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
.role-item.on { background: linear-gradient(135deg, #FFF8EE 0%, #FFE9D2 100%); border-color: #F9A65B; }
|
||||
.role-name { font-size: 26rpx; font-weight: 600; color: #333; }
|
||||
.role-item.on .role-name { color: #C4580A; }
|
||||
.role-desc { margin-top: 4rpx; font-size: 19rpx; color: #9A9A9A; }
|
||||
.role-desc { margin-top: 4rpx; font-size: 22rpx; color: #888888; }
|
||||
.fallback { margin-top: 20rpx; }
|
||||
.fb-toggle { text-align: center; padding: 12rpx 0; }
|
||||
.fb-toggle-t { font-size: 22rpx; color: #9A9A9A; text-decoration: underline; }
|
||||
@@ -157,9 +157,10 @@
|
||||
.fb-btn { height: 84rpx; display: flex; align-items: center; justify-content: center; border-radius: 42rpx; background: #FFF3E6; color: #E36B0C; font-size: 28rpx; font-weight: 600; }
|
||||
.wx-btn { height: 96rpx; display: flex; align-items: center; justify-content: center; border-radius: 48rpx; background: linear-gradient(135deg, #4BC86A 0%, #2FA84F 100%); box-shadow: 0 10rpx 24rpx rgba(47,168,79,.28); }
|
||||
.wx-btn.disabled { opacity: .7; }
|
||||
.wx-icon { width: 44rpx; height: 44rpx; border-radius: 50%; background: #fff; display: flex; align-items: center; justify-content: center; margin-right: 14rpx; }
|
||||
.wx-icon { width: 52rpx; height: 52rpx; border-radius: 50%; background: #fff; display: flex; align-items: center; justify-content: center; margin-right: 14rpx; box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.06); }
|
||||
.wx-icon-img { width: 34rpx; height: 34rpx; }
|
||||
.wx-icon-t { font-size: 24rpx; font-weight: 700; color: #2FA84F; }
|
||||
.wx-txt { font-size: 32rpx; font-weight: 600; color: #fff; letter-spacing: 1rpx; }
|
||||
.tip { display: block; margin-top: 24rpx; text-align: center; font-size: 20rpx; color: #B5B5B5; }
|
||||
.agree { display: block; margin-top: 12rpx; text-align: center; font-size: 20rpx; color: #C4C4C4; }
|
||||
.tip { display: block; margin-top: 24rpx; text-align: center; font-size: 24rpx; color: #888888; line-height: 34rpx; }
|
||||
.agree { display: block; margin-top: 12rpx; text-align: center; font-size: 22rpx; color: #AAAAAA; line-height: 32rpx; }
|
||||
</style>
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
</view>
|
||||
|
||||
<view class="grid">
|
||||
<view class="grid-item" v-for="g in entries" :key="g.key" @tap="g.action">
|
||||
<view class="g-icon" :style="{ background: g.bg }"><text class="g-icon-t" :style="{ color: g.color }">{{ g.icon }}</text></view>
|
||||
<view class="grid-item clickable-hover" v-for="g in entries" :key="g.key" @tap="g.action">
|
||||
<view class="g-icon" :style="{ background: g.bg }"><image class="g-svg" :src="g.svg" mode="aspectFit"></image></view>
|
||||
<text class="g-txt">{{ g.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
</view>
|
||||
|
||||
<view class="grid">
|
||||
<view class="gi" v-for="g in entries" :key="g.key" @tap="g.action">
|
||||
<view class="gi-icon" :style="{ background: g.bg }"><text class="gi-t" :style="{ color: g.color }">{{ g.icon }}</text></view>
|
||||
<view class="gi clickable-hover" v-for="g in entries" :key="g.key" @tap="g.action">
|
||||
<view class="gi-icon" :style="{ background: g.bg }"><image class="gi-svg" :src="g.svg" mode="aspectFit"></image></view>
|
||||
<text class="gi-name">{{ g.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -105,12 +105,10 @@
|
||||
});
|
||||
|
||||
const entries = [
|
||||
{ key: 'batch', name: '批量下单', icon: '批', bg: '#FFF1E0', color: '#E07A2E', action: () => go('/pages/merchant/batch') },
|
||||
{ key: 'orders', name: '企业订单', icon: '单', bg: '#EAF2FF', color: '#4A7CFF', action: () => go('/pages/merchant/orders') },
|
||||
{ key: 'invoice', name: '开具发票', icon: '票', bg: '#E8F7EE', color: '#2FA84F', action: () => go('/pages/merchant/invoice') },
|
||||
{ key: 'profile', name: '企业资料', icon: '档', bg: '#F0EBFF', color: '#7A5AF8', action: () => go('/pages/merchant/apply') },
|
||||
{ key: 'address', name: '服务地址', icon: '址', bg: '#FFF0F0', color: '#E8331B', action: () => go('/pages/address/list') },
|
||||
{ key: 'service', name: '联系客服', icon: '服', bg: '#EAF6FF', color: '#2A8FD8', action: () => uni.makePhoneCall({ phoneNumber: '4000000000' }) }
|
||||
{ key: 'orders', name: '企业订单', svg: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%234A7CFF'><path d='M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14l-5-5 1.41-1.41L12 14.17l7.59-7.59L21 8l-9 9z'/></svg>", bg: '#EAF2FF', action: () => go('/pages/merchant/orders') },
|
||||
{ key: 'batch', name: '批量下单', svg: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23F0710D'><path d='M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-1 9h-4v4h-2v-4H9V9h4V5h2v4h4v2z'/></svg>", bg: '#FFF1E0', action: () => go('/pages/merchant/batch') },
|
||||
{ key: 'invoice', name: '开具发票', svg: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23E07A2E'><path d='M19 3H5c-1.1 0-2 .9-2 2v14l3-2 3 2 3-2 3 2 3-2 3 2V5c0-1.1-.9-2-2-2zm-2 10H7v-2h10v2zm0-4H7V7h10v2z'/></svg>", bg: '#F0EBFF', action: () => go('/pages/merchant/invoice') },
|
||||
{ key: 'service', name: '专属客服', svg: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%232A8FD8'><path d='M20 15.5c-.7 0-1.4-.1-2-.3-.6-.2-1.2.1-1.6.5l-2.2 2.2c-2.8-1.5-5.2-3.8-6.6-6.6l2.2-2.2c.4-.4.6-1 .4-1.6-.2-.6-.3-1.3-.3-2 0-.8-.7-1.5-1.5-1.5H5c-.8 0-1.5.7-1.5 1.5 0 9.4 7.6 17 17 17 .8 0 1.5-.7 1.5-1.5v-3.5c0-.8-.7-1.5-1.5-1.5z'/></svg>", bg: '#E8F7EE', action: callService }
|
||||
];
|
||||
|
||||
function go(url) { uni.navigateTo({ url: url }); }
|
||||
|
||||
@@ -19,11 +19,13 @@
|
||||
</view>
|
||||
<view class="empty" v-if="!loading && !list.length">暂无消息</view>
|
||||
</view>
|
||||
<YjyfTabBar :current="2" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import YjyfTabBar from '@/components/YjyfTabBar.vue';
|
||||
import { onShow, onPullDownRefresh } from '@dcloudio/uni-app';
|
||||
import api from '@/utils/api.js';
|
||||
import { isLogin, toLogin } from '@/utils/request.js';
|
||||
@@ -86,7 +88,7 @@
|
||||
.msg-top { display: flex; align-items: center; }
|
||||
.dot { width: 12rpx; height: 12rpx; border-radius: 50%; background: #F0710D; margin-right: 10rpx; }
|
||||
.title { flex: 1; min-width: 0; font-size: 28rpx; font-weight: 600; color: #1F1F1F; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.time { flex-shrink: 0; margin-left: 12rpx; font-size: 20rpx; color: #B5B5B5; }
|
||||
.time { flex-shrink: 0; margin-left: 12rpx; font-size: 22rpx; color: #AAAAAA; }
|
||||
.content { display: block; margin-top: 10rpx; font-size: 24rpx; color: #666; line-height: 34rpx; }
|
||||
.go { display: block; margin-top: 10rpx; font-size: 22rpx; color: #4A7CFF; }
|
||||
.empty { padding: 110rpx 0; text-align: center; font-size: 24rpx; color: #9A9A9A; }
|
||||
|
||||
@@ -72,11 +72,13 @@
|
||||
</view>
|
||||
|
||||
<view class="logout" @tap="logout">退出登录</view>
|
||||
<YjyfTabBar :current="3" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import YjyfTabBar from '@/components/YjyfTabBar.vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import api from '@/utils/api.js';
|
||||
import { isLogin, toLogin, setUser, clearAuth } from '@/utils/request.js';
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<view class="seg-i" :class="{ on: form.has_elevator === 0 }" @tap="form.has_elevator = 0">无</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="save-btn" @tap="saveAddress">保存地址</view>
|
||||
<view class="save-btn clickable-hover" @tap="saveAddress">保存地址</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
|
||||
<view class="bar">
|
||||
<view class="bar-l"><text class="bar-k">{{ priceType === 'quote' ? '参考价' : '合计' }}</text><text class="bar-p">¥{{ amount ? amount.pay_amount : 0 }}</text></view>
|
||||
<view class="bar-btn" :class="{ disabled: submitting }" @tap="submit">
|
||||
<view class="bar-btn clickable-hover" :class="{ disabled: submitting }" @tap="submit">
|
||||
{{ submitting ? '提交中...' : (priceType === 'quote' ? '发布需求' : '提交并支付') }}
|
||||
</view>
|
||||
</view>
|
||||
@@ -342,11 +342,11 @@
|
||||
.mode.on { background: #FFF3E6; border-color: #F0710D; }
|
||||
.mode-name { font-size: 27rpx; font-weight: 700; color: #444; }
|
||||
.mode.on .mode-name { color: #E36B0C; }
|
||||
.mode-desc { margin-top: 6rpx; font-size: 20rpx; color: #9A9A9A; line-height: 28rpx; }
|
||||
.mode-desc { margin-top: 6rpx; font-size: 22rpx; color: #9A9A9A; line-height: 28rpx; }
|
||||
.mode.on .mode-desc { color: #C07A3A; }
|
||||
.mode-check {
|
||||
position: absolute; right: 10rpx; top: 8rpx; width: 30rpx; height: 30rpx; line-height: 30rpx;
|
||||
text-align: center; border-radius: 50%; background: #F0710D; color: #fff; font-size: 20rpx;
|
||||
text-align: center; border-radius: 50%; background: #F0710D; color: #fff; font-size: 22rpx;
|
||||
}
|
||||
.mode-note {
|
||||
margin-top: 14rpx; padding: 14rpx 16rpx; border-radius: 12rpx;
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
<template>
|
||||
<view class="page" v-if="order">
|
||||
<view class="page">
|
||||
<!-- 骨架屏呼吸占位 (彻底解决白屏) -->
|
||||
<view class="skeleton-box" v-if="loading && !order">
|
||||
<view class="sk-hero yjyf-shimmer"></view>
|
||||
<view class="sk-card yjyf-shimmer"></view>
|
||||
<view class="sk-card yjyf-shimmer"></view>
|
||||
<view class="sk-card yjyf-shimmer"></view>
|
||||
</view>
|
||||
|
||||
<view class="main-body" v-else-if="order">
|
||||
<view class="hero">
|
||||
<text class="hero-status">{{ order.status_text }}</text>
|
||||
<text class="hero-tip">{{ statusTip }}</text>
|
||||
@@ -87,13 +96,14 @@
|
||||
</view>
|
||||
|
||||
<view class="bar">
|
||||
<view class="op" v-if="order.is_order_owner !== false && order.can_cancel" @tap="cancel">取消订单</view>
|
||||
<view class="op primary" v-if="order.is_order_owner !== false && order.can_pay" @tap="pay">去支付</view>
|
||||
<view class="op primary" v-if="order.is_order_owner !== false && order.can_verify" @tap="verify">确认验收</view>
|
||||
<view class="op primary" v-if="order.is_order_owner !== false && order.can_review" @tap="review">去评价</view>
|
||||
<view class="op" @tap="backHome">返回首页</view>
|
||||
<view class="op clickable-hover" v-if="order.is_order_owner !== false && order.can_cancel" @tap="cancel">取消订单</view>
|
||||
<view class="op clickable-hover primary" v-if="order.is_order_owner !== false && order.can_pay" @tap="pay">去支付</view>
|
||||
<view class="op clickable-hover primary" v-if="order.is_order_owner !== false && order.can_verify" @tap="verify">确认验收</view>
|
||||
<view class="op clickable-hover primary" v-if="order.is_order_owner !== false && order.can_review" @tap="review">去评价</view>
|
||||
<view class="op clickable-hover" @tap="backHome">返回首页</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -102,6 +112,7 @@
|
||||
import api from '@/utils/api.js';
|
||||
|
||||
const order = ref(null);
|
||||
const loading = ref(true);
|
||||
const master = ref(null);
|
||||
const logs = ref([]);
|
||||
const media = ref([]);
|
||||
@@ -121,11 +132,14 @@
|
||||
});
|
||||
|
||||
async function load() {
|
||||
const res = await api.orderDetail(orderId);
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await api.orderDetail(orderId);
|
||||
order.value = res.order;
|
||||
master.value = res.master;
|
||||
logs.value = res.logs || [];
|
||||
media.value = res.media || [];
|
||||
} catch (e) {} finally { loading.value = false; }
|
||||
quotes.value = res.quotes || [];
|
||||
}
|
||||
|
||||
@@ -194,7 +208,7 @@
|
||||
.q-amt { font-size: 34rpx; font-weight: 700; color: #F0710D; }
|
||||
.q-desc { display: block; margin-top: 10rpx; font-size: 23rpx; color: #666; line-height: 32rpx; }
|
||||
.q-foot { display: flex; align-items: center; margin-top: 10rpx; }
|
||||
.q-expire { flex: 1; font-size: 20rpx; color: #B5B5B5; }
|
||||
.q-expire { flex: 1; font-size: 22rpx; color: #888888; }
|
||||
.q-status { font-size: 21rpx; color: #9A9A9A; margin-right: 14rpx; }
|
||||
.q-status.qs-wait { color: #4A7CFF; }
|
||||
.q-status.qs-accept { color: #2FA84F; }
|
||||
@@ -217,10 +231,10 @@
|
||||
.tl-body { flex: 1; display: flex; flex-direction: column; }
|
||||
.tl-s { font-size: 26rpx; font-weight: 600; color: #333; }
|
||||
.tl-r { margin-top: 2rpx; font-size: 22rpx; color: #9A9A9A; }
|
||||
.tl-t { margin-top: 2rpx; font-size: 20rpx; color: #C4C4C4; }
|
||||
.tl-t { margin-top: 2rpx; font-size: 22rpx; color: #AAAAAA; }
|
||||
.media { display: flex; flex-wrap: wrap; }
|
||||
.media-i { width: 150rpx; height: 110rpx; margin: 0 12rpx 12rpx 0; border-radius: 12rpx; background: linear-gradient(160deg,#FFF7F0,#FCE9D6); display: flex; align-items: center; justify-content: center; }
|
||||
.media-txt { font-size: 20rpx; color: #B85E2A; }
|
||||
.media-txt { font-size: 22rpx; color: #B85E2A; font-weight: 500; }
|
||||
.bar {
|
||||
position: fixed; left: 0; right: 0; bottom: 0; height: 118rpx;
|
||||
padding: 0 24rpx; padding-bottom: env(safe-area-inset-bottom);
|
||||
@@ -229,5 +243,10 @@
|
||||
}
|
||||
.op { margin-left: 16rpx; padding: 0 28rpx; line-height: 68rpx; border-radius: 34rpx; border: 2rpx solid #DDD; font-size: 26rpx; color: #666; }
|
||||
.op.primary { border-color: #F0710D; color: #fff; background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); }
|
||||
|
||||
.skeleton-box { padding: 30rpx 28rpx; }
|
||||
.sk-hero { height: 160rpx; border-radius: 20rpx; margin-bottom: 24rpx; }
|
||||
.sk-card { height: 220rpx; border-radius: 20rpx; margin-bottom: 24rpx; }
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -30,13 +30,20 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty" v-if="!loading && !list.length"><text>暂无订单</text></view>
|
||||
<view class="empty-state" v-if="!loading && !list.length">
|
||||
<view class="empty-badge">📋</view>
|
||||
<text class="empty-title">暂无相关订单</text>
|
||||
<text class="empty-desc">优匠师傅随时待命,准时上门,售后有保障</text>
|
||||
<view class="empty-btn clickable-hover" @tap="goOrder">立即找师傅</view>
|
||||
</view>
|
||||
<view class="tip" v-if="list.length"><text>仅展示前 {{ list.length }} 条</text></view>
|
||||
<YjyfTabBar :current="1" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import YjyfTabBar from '@/components/YjyfTabBar.vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import api from '@/utils/api.js';
|
||||
import { isLogin, toLogin } from '@/utils/request.js';
|
||||
@@ -67,6 +74,7 @@
|
||||
cur.value = k;
|
||||
load();
|
||||
}
|
||||
function goOrder() { uni.navigateTo({ url: "/pages/order/quick" }); }
|
||||
function goDetail(id) {
|
||||
uni.navigateTo({ url: '/pages/order/detail?id=' + id });
|
||||
}
|
||||
@@ -127,4 +135,11 @@
|
||||
.op.primary { border-color: #F0710D; color: #F0710D; background: #FFF7F0; }
|
||||
.empty { padding: 120rpx 0; text-align: center; font-size: 24rpx; color: #9A9A9A; }
|
||||
.tip { text-align: center; font-size: 20rpx; color: #C4C4C4; }
|
||||
|
||||
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 120rpx 40rpx 80rpx; }
|
||||
.empty-badge { width: 120rpx; height: 120rpx; line-height: 120rpx; text-align: center; border-radius: 50%; background: #FFF3E6; font-size: 54rpx; margin-bottom: 24rpx; }
|
||||
.empty-title { font-size: 30rpx; font-weight: 700; color: #1F1F1F; margin-bottom: 10rpx; }
|
||||
.empty-desc { font-size: 24rpx; color: #888888; margin-bottom: 36rpx; text-align: center; line-height: 36rpx; }
|
||||
.empty-btn { height: 80rpx; padding: 0 48rpx; display: flex; align-items: center; justify-content: center; border-radius: 40rpx; background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); color: #fff; font-size: 28rpx; font-weight: 600; box-shadow: 0 8rpx 20rpx rgba(240,113,13,0.28); }
|
||||
|
||||
</style>
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
<text class="bar-k">{{ priceType === 'quote' ? '参考价' : '合计' }}</text>
|
||||
<text class="bar-p">¥{{ amount ? amount.pay_amount : 0 }}</text>
|
||||
</view>
|
||||
<view class="bar-btn" :class="{ disabled: submitting }" @tap="submit">
|
||||
<view class="bar-btn clickable-hover" :class="{ disabled: submitting }" @tap="submit">
|
||||
{{ submitting ? '提交中...' : (priceType === 'quote' ? '发布需求' : '提交并支付') }}
|
||||
</view>
|
||||
</view>
|
||||
@@ -426,7 +426,7 @@
|
||||
.svc-empty-ic { width: 72rpx; height: 72rpx; line-height: 70rpx; text-align: center; border-radius: 50%; background: #FFF3E6; color: #E36B0C; font-size: 34rpx; flex-shrink: 0; }
|
||||
.svc-empty-txt { flex: 1; min-width: 0; margin-left: 16rpx; display: flex; flex-direction: column; }
|
||||
.svc-empty-t { font-size: 27rpx; font-weight: 600; color: #333; }
|
||||
.svc-empty-s { margin-top: 4rpx; font-size: 21rpx; color: #9A9A9A; }
|
||||
.svc-empty-s { margin-top: 4rpx; font-size: 22rpx; color: #9A9A9A; }
|
||||
.svc-empty-btn { flex-shrink: 0; padding: 0 24rpx; line-height: 56rpx; border-radius: 28rpx; background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); color: #fff; font-size: 23rpx; font-weight: 600; }
|
||||
.modes { display: flex; margin-top: 14rpx; }
|
||||
.mode { position: relative; flex: 1; min-width: 0; margin-right: 14rpx; padding: 20rpx 16rpx; border-radius: 16rpx; background: #F8F8F8; border: 2rpx solid transparent; display: flex; flex-direction: column; }
|
||||
@@ -434,7 +434,7 @@
|
||||
.mode.on { background: #FFF3E6; border-color: #F0710D; }
|
||||
.mode-name { font-size: 27rpx; font-weight: 700; color: #444; }
|
||||
.mode.on .mode-name { color: #E36B0C; }
|
||||
.mode-desc { margin-top: 6rpx; font-size: 20rpx; color: #9A9A9A; line-height: 28rpx; }
|
||||
.mode-desc { margin-top: 6rpx; font-size: 22rpx; color: #9A9A9A; line-height: 28rpx; }
|
||||
.mode.on .mode-desc { color: #C07A3A; }
|
||||
.mode-check { position: absolute; right: 10rpx; top: 8rpx; width: 30rpx; height: 30rpx; line-height: 30rpx; text-align: center; border-radius: 50%; background: #F0710D; color: #fff; font-size: 20rpx; }
|
||||
.mode-note { margin-top: 14rpx; padding: 14rpx 16rpx; border-radius: 12rpx; background: #FAFAFA; font-size: 22rpx; color: #8A8A8A; line-height: 32rpx; }
|
||||
@@ -464,7 +464,7 @@
|
||||
.k { font-size: 26rpx; color: #666; }
|
||||
.v { font-size: 26rpx; color: #333; }
|
||||
.v.big { font-size: 34rpx; font-weight: 700; color: #F0710D; }
|
||||
.fee-note { display: block; margin-top: 10rpx; font-size: 21rpx; color: #9A9A9A; line-height: 30rpx; }
|
||||
.fee-note { display: block; margin-top: 10rpx; font-size: 22rpx; color: #9A9A9A; line-height: 30rpx; }
|
||||
.bottom-space { height: 20rpx; }
|
||||
.bar { position: fixed; left: 0; right: 0; bottom: 0; height: 120rpx; padding: 0 28rpx; padding-bottom: env(safe-area-inset-bottom); display: flex; align-items: center; justify-content: space-between; background: #fff; box-shadow: 0 -6rpx 24rpx rgba(0,0,0,0.07); z-index: 20; }
|
||||
.bar-l { display: flex; align-items: baseline; }
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="submit" :class="{ disabled: submitting }" @tap="submit">{{ submitting ? '提交中...' : '提交评价' }}</view>
|
||||
<view class="submit clickable-hover" :class="{ disabled: submitting }" @tap="submit">{{ submitting ? '提交中...' : '提交评价' }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<template>
|
||||
<view class="page" v-if="detail">
|
||||
<view class="page">
|
||||
<!-- 骨架屏呼吸占位 (彻底解决白屏) -->
|
||||
<view class="skeleton-box" v-if="loading && !detail">
|
||||
<view class="sk-hero yjyf-shimmer"></view>
|
||||
<view class="sk-card yjyf-shimmer"></view>
|
||||
<view class="sk-card yjyf-shimmer"></view>
|
||||
</view>
|
||||
|
||||
<view class="main-body" v-else-if="detail">
|
||||
<view class="hero">
|
||||
<view class="hero-thumb"><text class="hero-txt">{{ detail.name.slice(0, 2) }}</text></view>
|
||||
<view class="hero-info">
|
||||
@@ -33,9 +41,10 @@
|
||||
<text class="bar-price">{{ detail.price_text }}</text>
|
||||
<text class="bar-tip">无隐藏费用</text>
|
||||
</view>
|
||||
<view class="bar-btn" @tap="goCreate">立即下单</view>
|
||||
<view class="bar-btn clickable-hover" @tap="goCreate">立即下单</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -44,6 +53,7 @@
|
||||
import api from '@/utils/api.js';
|
||||
|
||||
const detail = ref(null);
|
||||
const loading = ref(true);
|
||||
let serviceId = 0;
|
||||
|
||||
function goCreate() {
|
||||
@@ -100,4 +110,9 @@
|
||||
background: linear-gradient(135deg, #FFA93F 0%, #F0710D 100%);
|
||||
box-shadow: 0 8rpx 20rpx rgba(240,113,13,0.3);
|
||||
}
|
||||
|
||||
.skeleton-box { padding: 30rpx 28rpx; }
|
||||
.sk-hero { height: 180rpx; border-radius: 20rpx; margin-bottom: 24rpx; }
|
||||
.sk-card { height: 260rpx; border-radius: 20rpx; margin-bottom: 24rpx; }
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,76 +1,65 @@
|
||||
/**
|
||||
* 这里是uni-app内置的常用样式变量
|
||||
*
|
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||
*
|
||||
* 优匠优服(YJYF)设计令牌与全局 SCSS 变量集
|
||||
* 遵循 UI/UX Pro Max 规范与 8rpx (4px) 模数系统
|
||||
*/
|
||||
|
||||
/**
|
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||
*
|
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||
*/
|
||||
/* ==================== 1. 品牌核心色彩 ==================== */
|
||||
$yjyf-primary: #F0710D; // 优匠主橙 (60-30-10 核心行动色)
|
||||
$yjyf-primary-light: #FFA93F; // 亮橙 (高光与渐变起点)
|
||||
$yjyf-primary-bg: #FFF7F0; // 浅橙底色 (卡片高亮/轻按钮)
|
||||
$yjyf-gradient-primary: linear-gradient(135deg, #FFA93F 0%, #F0710D 100%);
|
||||
|
||||
/* 颜色变量 */
|
||||
/* ==================== 2. 功能与反馈色彩 ==================== */
|
||||
$yjyf-success: #2FA84F; // 成功绿 (微信/已完成/通过)
|
||||
$yjyf-danger: #E8331B; // 警示红 (删除/驳回/取消/报错)
|
||||
$yjyf-warning: #F98D25; // 关注橙 (急单/待支付/审核中)
|
||||
$yjyf-info: #4A7CFF; // 链接蓝 (协议/待分配/查看详情)
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #007aff;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
/* ==================== 3. 中性文字梯度 (杜绝杂乱灰) ==================== */
|
||||
$yjyf-text-main: #1F1F1F; // 一级标题与核心正文
|
||||
$yjyf-text-regular: #444444; // 二级正文与表单标签
|
||||
$yjyf-text-secondary: #888888; // 三级辅助说明与副标题
|
||||
$yjyf-text-muted: #AAAAAA; // 四级弱化信息 (时间/单号)
|
||||
$yjyf-text-disabled: #C8C7CC; // 禁用态/未填占位符 (Placeholder)
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color:#333;//基本色
|
||||
$uni-text-color-inverse:#fff;//反色
|
||||
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color-placeholder: #808080;
|
||||
$uni-text-color-disable:#c0c0c0;
|
||||
/* ==================== 4. 容器与背景 ==================== */
|
||||
$yjyf-bg-page: #F5F6F8; // 全局页面底色
|
||||
$yjyf-bg-card: #FFFFFF; // 卡片纯白底色
|
||||
$yjyf-border-color: #F0F0F2; // 细分割线描边 (1rpx)
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color:#ffffff;
|
||||
$uni-bg-color-grey:#f8f8f8;
|
||||
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||
/* ==================== 5. 标准字阶系统 (杜绝低于22rpx的微字) ==================== */
|
||||
$yjyf-font-hero: 44rpx; // 大标题 (22px)
|
||||
$yjyf-font-title: 32rpx; // 模块标题 (16px)
|
||||
$yjyf-font-body: 28rpx; // 正文基准 (14px)
|
||||
$yjyf-font-sub: 24rpx; // 辅助说明与标签 (12px)
|
||||
$yjyf-font-micro: 22rpx; // 极限微小标注 (11px 绝对下限)
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color:#c8c7cc;
|
||||
/* ==================== 6. 8rpx (4px) 间距模数阶梯 ==================== */
|
||||
$yjyf-space-xs: 8rpx; // 4px 微间距
|
||||
$yjyf-space-sm: 16rpx; // 8px 子模块间距
|
||||
$yjyf-space-md: 24rpx; // 12px 卡片内边距 (Padding)
|
||||
$yjyf-space-lg: 32rpx; // 16px 页面两侧边距 (Gutter)
|
||||
$yjyf-space-xl: 48rpx; // 24px 大模块外间距
|
||||
|
||||
/* 尺寸变量 */
|
||||
/* ==================== 7. 圆角与弥散投影 ==================== */
|
||||
$yjyf-radius-sm: 12rpx;
|
||||
$yjyf-radius-md: 20rpx;
|
||||
$yjyf-radius-lg: 32rpx;
|
||||
$yjyf-radius-pill: 999rpx;
|
||||
$yjyf-shadow-card: 0 6rpx 20rpx rgba(240, 113, 13, 0.06);
|
||||
$yjyf-shadow-bar: 0 -6rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm:12px;
|
||||
$uni-font-size-base:14px;
|
||||
$uni-font-size-lg:16px;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm:20px;
|
||||
$uni-img-size-base:26px;
|
||||
$uni-img-size-lg:40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 2px;
|
||||
$uni-border-radius-base: 3px;
|
||||
$uni-border-radius-lg: 6px;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
/* 水平间距 */
|
||||
$uni-spacing-row-sm: 5px;
|
||||
$uni-spacing-row-base: 10px;
|
||||
$uni-spacing-row-lg: 15px;
|
||||
|
||||
/* 垂直间距 */
|
||||
$uni-spacing-col-sm: 4px;
|
||||
$uni-spacing-col-base: 8px;
|
||||
$uni-spacing-col-lg: 12px;
|
||||
|
||||
/* 透明度 */
|
||||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2C405A; // 文章标题颜色
|
||||
$uni-font-size-title:20px;
|
||||
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||
$uni-font-size-subtitle:26px;
|
||||
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||
$uni-font-size-paragraph:15px;
|
||||
/* ==================== 兼容旧版 uni-app 默认变量映射 ==================== */
|
||||
$uni-color-primary: $yjyf-primary;
|
||||
$uni-color-success: $yjyf-success;
|
||||
$uni-color-warning: $yjyf-warning;
|
||||
$uni-color-error: $yjyf-danger;
|
||||
$uni-text-color: $yjyf-text-main;
|
||||
$uni-text-color-grey: $yjyf-text-secondary;
|
||||
$uni-text-color-placeholder: $yjyf-text-disabled;
|
||||
$uni-bg-color: $yjyf-bg-card;
|
||||
$uni-bg-color-grey: $yjyf-bg-page;
|
||||
$uni-border-color: $yjyf-border-color;
|
||||
$uni-font-size-sm: 12px;
|
||||
$uni-font-size-base: 14px;
|
||||
$uni-font-size-lg: 16px;
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
* - 自动携带 token(header: token)
|
||||
* - 统一响应契约:code === 1 成功;其余为失败(0 通用 / 401 未登录 / 403 无权限 / 2001~2003 业务语义)
|
||||
* - 失败统一 toast,未登录自动清理并引导登录
|
||||
* - 内置 POST 请求 500ms 防并发快速连击锁
|
||||
*/
|
||||
const BASE_URL = 'https://yjyf.gzyakj.com.cn/api';
|
||||
|
||||
const TOKEN_KEY = 'yjyf_token';
|
||||
const USER_KEY = 'yjyf_user';
|
||||
|
||||
// 活跃中的 POST 请求锁缓存
|
||||
const pendingPostMap = new Map();
|
||||
|
||||
export function getToken() {
|
||||
try {
|
||||
return uni.getStorageSync(TOKEN_KEY) || '';
|
||||
@@ -53,10 +57,22 @@ export function toLogin() {
|
||||
*/
|
||||
export function request(path, options) {
|
||||
const opts = options || {};
|
||||
const method = opts.method || 'GET';
|
||||
const method = (opts.method || 'GET').toUpperCase();
|
||||
const data = opts.data || {};
|
||||
const silent = !!opts.silent;
|
||||
|
||||
// 针对写入类请求 (POST) 做 500ms 防抖/去重锁
|
||||
if (method === 'POST') {
|
||||
const requestKey = path + '_' + JSON.stringify(data);
|
||||
const lastTime = pendingPostMap.get(requestKey);
|
||||
const now = Date.now();
|
||||
if (lastTime && now - lastTime < 500) {
|
||||
return Promise.reject(new Error('请勿频繁重复提交'));
|
||||
}
|
||||
pendingPostMap.set(requestKey, now);
|
||||
setTimeout(() => pendingPostMap.delete(requestKey), 600);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const header = { 'content-type': 'application/x-www-form-urlencoded' };
|
||||
const token = getToken();
|
||||
|
||||
在新工单中引用
屏蔽一个用户